17 Commits
Author SHA1 Message Date
Bastien Chanot 1cb77c3f57 Merge release/1.3.0 into main 2026-07-20 19:40:23 +02:00
Bastien Chanot 711eacd900 chore(release): 1.3.0 — version.txt + CHANGELOG 2026-07-20 19:39:35 +02:00
Bastien Chanot ce5b7fb3f4 Merge chore/readme-seo-env into develop 2026-07-20 19:25:07 +02:00
Bastien Chanot 10589d484b docs: README polish — real clone URL + make targets, magic example simplified, SEO env vars section
- Fresh-install block: clone URL → github.com/bchanot/claude, bash
  install.sh/doctor.sh → make install / make doctor (user pass)
- magic MCP example: placeholder key line instead of WRONG/RIGHT contrast
- new subsection: SEO data layer needs GOOGLE_OAUTH_CLIENT_ID/SECRET +
  CRUX_API_KEY in ~/.claude/.env (GCP steps, make seo-connect, graceful
  degradation) — mirrors .env.example
2026-07-20 19:25:07 +02:00
Bastien Chanot dc90aae9bd Merge chore/purge-transient-docs into develop
# Conflicts:
#	.claude/tasks/TODO.md
2026-07-20 17:07:11 +02:00
Bastien Chanot 37c79f0524 Merge feature/profile-managed-externals into develop 2026-07-20 17:04:53 +02:00
Bastien Chanot e75ea79ae6 chore(tasks): reconcile 2026-07-20 — 4 stale claims corrected + pending-gates section
- seo/geo STATUS: H1+C1 were done (url-guard, sitemap verb) and the branch
  merged (92301fe) + shipped v1.2.0 — 'NEXT'/'nothing merged' lines stale
- ctx7 + opus-pin sections: 'NO merge' notes stale (8ee7d19, 17fbe51 both
  shipped v1.2.0)
- f1c9c474 transcript decision moot: auto-rotated (cleanupPeriodDays=7)
- new open items: 2 unmerged branches + Makefile help-text fix
2026-07-20 16:46:42 +02:00
Bastien Chanot 655e364e80 chore(docs): purge transient plan/spec artifacts missed by post-merge cleanup
- docs/plans + docs/specs (deploy-skill 2026-06-27): predate the BDR-065
  lifecycle codification, never swept
- docs/superpowers/{plans,specs} (model-routing 2026-07-15): 6-wave
  chantier — final W6 merge closed it without the purge step
Git history at the feature commits is the archive (BDR-065).
2026-07-20 16:40:58 +02:00
Bastien Chanot ecbe8abde7 docs: profile list ×3 + test glob + BDR-ID strip + layout tree → ARCHITECTURE.md — /doc clean pass 2026-07-20 16:29:31 +02:00
Bastien Chanot 8008d8233c feat(profile): set symmetric on managed externals + MCPs (BDR-079)
- MANAGED_EXTERNALS (emil-design-eng, frontend-design,
  design-motion-principles, impeccable) + MANAGED_MCPS (magic):
  cmd_set now trims both when the profile does not list them —
  design leftovers no longer survive a 'set backend'
- cmd_set refactored to 4 symmetric trim helpers; nothing outside
  the MANAGED_* allowlists is ever auto-toggled (darwin-skill manual)
- enable_skill external: from-source fallback (ln -sf
  skills-external/<name>), mirrors toggle-external.sh
- stale usage() NOTE + SKILL.md updated to the both-ways reality
- hermetic test: 16 checks, fixture repo + fake claude shim (gstack
  on-demand, from-source, park/restore, magic add/remove, non-managed
  untouched); shellcheck + full make test green
2026-07-20 14:47:53 +02:00
Bastien Chanot 3c243ece97 Merge release/1.2.1 into main 2026-07-20 14:34:09 +02:00
Bastien Chanot 3166c1161e Merge release/1.2.1 into develop 2026-07-20 14:34:09 +02:00
Bastien Chanot e5a62cc049 chore(release): 1.2.1 — version.txt + CHANGELOG 2026-07-20 14:32:50 +02:00
Bastien Chanot b3a03fd974 chore(memory): journal — v1.2.0 cut + doc pass 2026-07-20 14:24:39 +02:00
Bastien Chanot aeb7bc05d8 Merge chore/doc-sync-v1.2.0 into develop 2026-07-20 14:24:11 +02:00
Bastien Chanot 45e679ac23 docs: README model-routing v2 table (BDR-076/077) + ctx7 surfaces (BDR-078) + STEP 2b — post-release 1.2.0 doc pass 2026-07-20 14:21:24 +02:00
Bastien Chanot 2d38ffd843 Merge release/1.2.0 into develop 2026-07-20 14:07:28 +02:00
15 changed files with 317 additions and 2723 deletions
+3
View File
@@ -1065,3 +1065,6 @@ Supersedes BDR-076 scope + amends BDR-066. Doctrine: session model (Fable) = mai
### BDR-078 — ctx7 coverage: central fast-libs list + once-per-session reminder hook; every code path covered [accepted] (2026-07-20) ### BDR-078 — ctx7 coverage: central fast-libs list + once-per-session reminder hook; every code path covered [accepted] (2026-07-20)
Refines BDR-053 (single surface). Audit 2026-07-20: coverage PARTIAL — find-docs fired on user doc-questions only; ship-feature 0c / init-project 5c pre-fetched; /feat //bugfix executors + ad-hoc coding NEVER consulted ctx7; fast-libs list hardcoded 3× (drift risk). 4 closures shipped: (a) find-docs description += BEFORE-writing-code trigger (fast-moving lib, even without doc question, unless fresh cache) + cache-first rule in body (tee fetched docs to .ctx7-cache/); (b) feater+bugfixer briefs += fast-lib docs rule — read fresh `.ctx7-cache/<lib>*.md`, else `npx ctx7@latest` fetch max 2 topics, else `ctx7 cache miss: <lib>` in NOTES + proceed (executors lack Skill tool → Bash path); (c) hooks/ctx7-reminder.sh UserPromptSubmit — ONE fire/session (sentinel on session_id), only when project manifest carries fast-libs; reports cache state; skips <task-notification> turns; always exit 0; (d) lib/fast-libs.sh = SINGLE SOURCE (detect / cache-status verbs, JS package.json anchored full-key match + Python requirements/pyproject, 7-day freshness, LC_ALL=C sort locale-independent) consumed by hook + 3 pipeline skills + 2 briefs. 2nd session surface DELIBERATE, not a BDR-053 reversal: 053 killed a 490-tok ALWAYS-ON rule duplicate; hook costs ~0 quiet, 1 line once when fast-libs present. Alternatives rejected: PreToolUse Edit/Write gate (fires per-edit = noise); description-only fix (probabilistic, executors unreachable). Tests: lib/tests/fast-libs.test.sh 11 checks (anchored/near-miss/py/none, cache fresh/stale/missing, hook fire/sentinel/quiet×2); shellcheck + full make test green. Branch feature/ctx7-coverage, unmerged (human gate). Refines BDR-053 (single surface). Audit 2026-07-20: coverage PARTIAL — find-docs fired on user doc-questions only; ship-feature 0c / init-project 5c pre-fetched; /feat //bugfix executors + ad-hoc coding NEVER consulted ctx7; fast-libs list hardcoded 3× (drift risk). 4 closures shipped: (a) find-docs description += BEFORE-writing-code trigger (fast-moving lib, even without doc question, unless fresh cache) + cache-first rule in body (tee fetched docs to .ctx7-cache/); (b) feater+bugfixer briefs += fast-lib docs rule — read fresh `.ctx7-cache/<lib>*.md`, else `npx ctx7@latest` fetch max 2 topics, else `ctx7 cache miss: <lib>` in NOTES + proceed (executors lack Skill tool → Bash path); (c) hooks/ctx7-reminder.sh UserPromptSubmit — ONE fire/session (sentinel on session_id), only when project manifest carries fast-libs; reports cache state; skips <task-notification> turns; always exit 0; (d) lib/fast-libs.sh = SINGLE SOURCE (detect / cache-status verbs, JS package.json anchored full-key match + Python requirements/pyproject, 7-day freshness, LC_ALL=C sort locale-independent) consumed by hook + 3 pipeline skills + 2 briefs. 2nd session surface DELIBERATE, not a BDR-053 reversal: 053 killed a 490-tok ALWAYS-ON rule duplicate; hook costs ~0 quiet, 1 line once when fast-libs present. Alternatives rejected: PreToolUse Edit/Write gate (fires per-edit = noise); description-only fix (probabilistic, executors unreachable). Tests: lib/tests/fast-libs.test.sh 11 checks (anchored/near-miss/py/none, cache fresh/stale/missing, hook fire/sentinel/quiet×2); shellcheck + full make test green. Branch feature/ctx7-coverage, unmerged (human gate).
Amendment (same session): skills/find-docs = machine-owned dist (gitignored, ctx7 regenerates on fresh clone) → durable copy of closure (a) lives in install-plugins.sh STEP ctx7 (idempotent grep-guarded python patch, fixture-verified); live SKILL.md carries the same edit uncommitted by design. Amendment (same session): skills/find-docs = machine-owned dist (gitignored, ctx7 regenerates on fresh clone) → durable copy of closure (a) lives in install-plugins.sh STEP ctx7 (idempotent grep-guarded python patch, fixture-verified); live SKILL.md carries the same edit uncommitted by design.
### BDR-079 — profile `set` symmetric on managed externals + MCPs [accepted] (2026-07-20)
Audit (user ask "profile toggles externals both ways?"): ASYMMETRIC. Enable side OK — gstack on-demand from submodule when pack off (shared `skills-disabled/gstack__*` convention with toggle-external.sh, interoperable), externals restored from parked, magic delegated to toggle-external. Disable side MISSING: `cmd_set` trimmed only gstack + MANAGED_PLUGINS → `set backend` left emil/frontend-design/design-motion/impeccable active + magic registered; SKILL.md claimed both-ways toggle (true only at enable). Shipped: (1) `MANAGED_EXTERNALS` (emil-design-eng, frontend-design, design-motion-principles, impeccable = exact union of profile `external` usage; darwin-skill excluded — not task-type-driven) + `MANAGED_MCPS` (magic) allowlists, same doctrine as MANAGED_PLUGINS; (2) cmd_set refactored to 4 trim helpers (`disable_{gstack,plugins,externals,mcps}_not_in`) — symmetric, nothing outside allowlists ever auto-touched; (3) enable_skill external += from-source fallback (`ln -sf skills-external/<name>`, mirrors toggle-external) — closes the "missing symlink" warn; (4) stale usage() NOTE ("NOT toggled automatically") + SKILL.md fixed. Hermetic test profile-set-managed.test.sh 16 checks: fixture repo (both *_REPO_OVERRIDE), fake `claude` shim on PATH logging calls + flat-file MCP registry — gstack on-demand, external from-source, park/restore round-trip, magic add/remove calls, non-managed untouched. shellcheck + make test green. Branch feature/profile-managed-externals, unmerged (human gate).
+2
View File
@@ -416,3 +416,5 @@ rules:
## 2026-07-20 ## 2026-07-20
- ctx7 coverage audit (user ask "ctx7 appelé à chaque techno ?") → verdict PARTIAL. 4 gaps: find-docs question-only, /feat //bugfix executors blind, ad-hoc coding uncovered, fast-libs hardcoded 3×. All 4 closed → BDR-078 (fast-libs.sh single source + ctx7-reminder hook + description trigger + executor-brief rule). fast-libs test 11/0, make test + review-guards green. feature/ctx7-coverage, UNMERGED. - ctx7 coverage audit (user ask "ctx7 appelé à chaque techno ?") → verdict PARTIAL. 4 gaps: find-docs question-only, /feat //bugfix executors blind, ad-hoc coding uncovered, fast-libs hardcoded 3×. All 4 closed → BDR-078 (fast-libs.sh single source + ctx7-reminder hook + description trigger + executor-brief rule). fast-libs test 11/0, make test + review-guards green. feature/ctx7-coverage, UNMERGED.
- v1.2.0 cut + pushed (release-candidate flow: prep/finish via release-executor, tag on main 51b6572). CHANGELOG backfilled at prep: 10 entries added to Unreleased (plan-challenge, seo-data verbs, model-tiering v2, integrity pass, safe_fetch/url-guard) — was ctx7-only. /doc full post-release: README model-routing table v1→v2 reframe + ctx7 two-surface wording, chore/doc-sync-v1.2.0 merged. All pushed on explicit go.
- profile↔toggle-external audit (user) → enable side already symmetric (gstack on-demand LIVE), disable side missing → BDR-079: MANAGED_EXTERNALS+MANAGED_MCPS trim at set, external from-source fallback, 16-check hermetic test (claude shim). feature/profile-managed-externals, UNMERGED.
+33 -5
View File
@@ -1,5 +1,31 @@
# TODO # TODO
## 2026-07-20 — pending merge gates (reconcile)
- [x] merge feature/profile-managed-externals → develop (BDR-079 profile
symmetry + /doc clean pass: README/USAGE/ARCHITECTURE.md) — 37c79f0
- [x] merge chore/purge-transient-docs → develop (docs/ transient purge
655e364 + reconcile e75ea79) — reaches main at next release
- [ ] Makefile help text: profiles 5/10 listed (:57) + test glob missing
run-*.sh (:31) — 2-line hotfix (flagged by /doc audit)
## 2026-07-20 — profile ↔ toggle-external symmetry (feature/profile-managed-externals, BDR-079)
Audit verdict: gstack on-demand + design enable already work; DISABLE side
missing — `set backend` leaves emil/frontend-design/design-motion/impeccable
active + magic registered. Doc claims auto-toggle both ways (only enable true).
- [x] profile.sh: `MANAGED_EXTERNALS` (emil-design-eng, frontend-design,
design-motion-principles, impeccable — union of profile usage) +
`MANAGED_MCPS` (magic) allowlists; cmd_set refactored to 4 trim
helpers (disable_{gstack,plugins,externals,mcps}_not_in).
- [x] profile.sh enable_skill external: from-source fallback
(`ln -sf skills-external/<name>`) mirroring toggle-external.
- [x] Texts: cmd_set info line, usage() NOTE (stale "NOT toggled
automatically"), header; skills/profile/SKILL.md Mechanism+tradeoffs.
- [x] Hermetic test lib/tests/profile-set-managed.test.sh — 16/0: gstack
on-demand, external from-source, park/restore round-trip, magic
add/remove via claude shim, non-managed untouched.
- [x] Gate: shellcheck OK + make test exit 0 (review-guards 5/0). BDR-079 +
journal + CHANGELOG done. Merged 37c79f0 (2026-07-20).
## 2026-07-20 — ctx7 coverage extension (feature/ctx7-coverage, BDR-078) ## 2026-07-20 — ctx7 coverage extension (feature/ctx7-coverage, BDR-078)
Close the 4 gaps from the ctx7 coverage audit: /feat //bugfix + ad-hoc coding Close the 4 gaps from the ctx7 coverage audit: /feat //bugfix + ad-hoc coding
never consult ctx7; fast-libs list hardcoded 3×; zero deterministic backstop. never consult ctx7; fast-libs list hardcoded 3×; zero deterministic backstop.
@@ -18,7 +44,7 @@ never consult ctx7; fast-libs list hardcoded 3×; zero deterministic backstop.
- [x] `lib/tests/fast-libs.test.sh` (lib verbs + hook fire/sentinel/quiet) - [x] `lib/tests/fast-libs.test.sh` (lib verbs + hook fire/sentinel/quiet)
— 11/0, auto-discovered by the make test glob. — 11/0, auto-discovered by the make test glob.
- [x] Gate: shellcheck + make test green (review-guards 5/0). BDR-078 + - [x] Gate: shellcheck + make test green (review-guards 5/0). BDR-078 +
journal + CHANGELOG done. Committed on branch, NO merge (human gate). journal + CHANGELOG done. Merged 8ee7d19, shipped v1.2.0.
## 2026-07-19 — Opus-pin dispatched judgment agents (branch feature/opus-pin-audit-agents) ## 2026-07-19 — Opus-pin dispatched judgment agents (branch feature/opus-pin-audit-agents)
@@ -45,8 +71,8 @@ on audits). User approved: opus for judgment agents, drop local opus pin.
- [x] `.claude/settings.local.json` — drop `"model": "opus-4-8[1m]"` - [x] `.claude/settings.local.json` — drop `"model": "opus-4-8[1m]"`
(local, gitignored; Fable default from settings.json applies). (local, gitignored; Fable default from settings.json applies).
- [x] Tests: model-routing + loops-light + shellcheck + make test. - [x] Tests: model-routing + loops-light + shellcheck + make test.
- [x] Memory: BDR-076 append + journal line. Commit (feat + chore), - [x] Memory: BDR-076 append + journal line. Commit (feat + chore);
NO merge (human gate). merged 17fbe51, shipped v1.2.0 (reconcile 2026-07-20).
## 2026-07-17 — STATUS seo/geo parity (branch bugfix/seo-geo-integrity — MERGED to develop, 92301fe; "UNMERGED" note was stale, corrected 2026-07-19 W0) ## 2026-07-17 — STATUS seo/geo parity (branch bugfix/seo-geo-integrity — MERGED to develop, 92301fe; "UNMERGED" note was stale, corrected 2026-07-19 W0)
PHASE 1 — integrity: **DONE 7/7**. I3 8b0c98c · I1 57c67f2 · I2 4ea2fb8 · PHASE 1 — integrity: **DONE 7/7**. I3 8b0c98c · I1 57c67f2 · I2 4ea2fb8 ·
@@ -54,8 +80,8 @@ I5 64f175f · I4 e70e1d6 · I6 9da1dec · I8 acd452b. Plus 9cd7b51 (A1+A2, two
process anomalies surfaced by dogfooding /harden at zenquality.fr from the process anomalies surfaced by dogfooding /harden at zenquality.fr from the
wrong CWD). wrong CWD).
PHASE 2 — free wins: W3 fe93b79 · W1 a6d423b · **W2 DEFERRED** (see below). PHASE 2 — free wins: W3 fe93b79 · W1 a6d423b · **W2 DEFERRED** (see below).
NEXT: H1 (SSRF/injection guard) → C1 (sitemap crawl). Human merge gate: all H1 DONE (url-guard 7d6aa09) · C1 DONE (sitemap verb, C1a/b/c). Branch MERGED
10 commits await review; nothing merged to develop. to develop (92301fe), shipped in v1.2.0 (reconcile 2026-07-20).
### Plan corrections made while executing (the plan was wrong 4×) ### Plan corrections made while executing (the plan was wrong 4×)
- **B3 KILLED** — GSC Links API does not exist. Verified against the API - **B3 KILLED** — GSC Links API does not exist. Verified against the API
@@ -478,6 +504,8 @@ manipuler une valeur de secret — edits sur les mécanismes seulement.
Transcript `f1c9c474-...jsonl` (generic-api-key, 8) — PAS choisi Transcript `f1c9c474-...jsonl` (generic-api-key, 8) — PAS choisi
par l'utilisateur parmi les options (auto-inspect / TODO / rm) → par l'utilisateur parmi les options (auto-inspect / TODO / rm) →
**laissé intact, à trancher** ; ni lu ni caractérisé (règle job7). **laissé intact, à trancher** ; ni lu ni caractérisé (règle job7).
[sans objet : transcript auto-roté (cleanupPeriodDays=7), absent
du disque — reconcile 2026-07-20]
- [x] **NOUVEAU (bruit, pas un item D)** : transcript de CETTE session - [x] **NOUVEAU (bruit, pas un item D)** : transcript de CETTE session
(`4b5c02a9-...jsonl`, aws-access-token, 2) = mes propres fixtures (`4b5c02a9-...jsonl`, aws-access-token, 2) = mes propres fixtures
synthétiques de test (AKIA random) loggées dans mon propre synthétiques de test (AKIA random) loggées dans mon propre
+33
View File
@@ -0,0 +1,33 @@
# Architecture — claude-config
Repo layout and structural principles. Command workflows live in
[`USAGE.md`](./USAGE.md); version history in [`CHANGELOG.md`](./CHANGELOG.md).
## Project layout
```
claude-config/
├── CLAUDE.global.md # Global coding preferences — deployed as ~/.claude/CLAUDE.md
├── CLAUDE.md # Project-scope instructions (this repo only)
├── settings.json # Global permissions (deny / ask / allow rules)
├── install.sh # Bootstrap: Claude Code CLI + auth + submodules + link + plugins
├── install-plugins.sh # One-shot installer: prerequisites + all plugins
├── link.sh # Symlinks this repo into ~/.claude/
├── doctor.sh # Setup diagnostic
├── update-all.sh # One-command update for all components
├── Makefile # Unified entry point: make install / doctor / update
├── plugins.lock.json # Version pinning for non-marketplace dependencies
├── hooks/ # Session start, statusline, RTK rewrite + ctx7 + design-toolchain reminders
├── agents/ # Execution units called by skills (never invoked directly)
├── skills/ # Entry points invoked via /skill-name
├── skills-external/ # Vendored skill packs (gstack submodule + installer-fetched design packs)
├── templates/ # Per-project templates (CLAUDE.md, settings, memory registries, deploy runbook, gitignore)
└── lib/ # Shared shell libs (gitflow, profiles, commit helpers, archetypes, tests)
```
## Architecture principles
- `skills/` = entry points you invoke via `/skill-name`
- `agents/` = execution units called by skills (never invoked directly by user)
- `templates/` = symlinked to `~/.claude/templates/` — copy into projects via `/onboard` or manually
- **Graphify** builds a knowledge graph of any codebase (`/graphify query`), producing a navigable wiki in `graphify-out/wiki/`. This map helps Claude understand project structure, find relevant code faster, and reason across files. Essential for large-scope tasks (multi-file features, complex bugs, architectural changes). Small tasks should skip it and read files directly.
+16
View File
@@ -6,6 +6,22 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased] ## [Unreleased]
## [1.3.0] — 2026-07-20
### Added
- **Profile switches now toggle external packs and MCPs both ways (BDR-079)** — `profile.sh set` was asymmetric: it enabled what a profile listed (including gstack skills on demand when the whole pack is off, and the `magic` MCP) but never disabled the managed leftovers, so `set backend` after design work kept emil-design-eng / frontend-design / design-motion-principles / impeccable active and magic registered. `set` now trims managed externals (`MANAGED_EXTERNALS`) and managed MCPs (`MANAGED_MCPS`, delegated to `toggle-external.sh`) not listed in the profile — same allowlist doctrine as `MANAGED_PLUGINS`, nothing outside the allowlists is ever auto-touched (darwin-skill stays manual). Also: an `external` entry whose symlink never existed is now created from `skills-external/` (mirroring toggle-external's from-source path), and the stale "NOT toggled automatically" note in `profile.sh` usage was corrected. Covered by a hermetic 16-check test (`lib/tests/profile-set-managed.test.sh`) with a fake `claude` shim.
### Changed
- **README restructured for public readers** — the project-layout tree and architecture principles moved verbatim to a new `ARCHITECTURE.md` (README links it); bare decision-registry citations (`BDR-XXX`) stripped from README prose, meaning preserved; `/profile` documentation corrected in three places to the real 10-profile set (web / seo / web-full / full / backend / design / dev / qa / audit / minimal); fresh-install block now uses the real clone URL + `make install` / `make doctor`; new "SEO data layer" subsection documents the `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` / `CRUX_API_KEY` vars in `~/.claude/.env` (mirrors `.env.example`, `make seo-connect` one-time consent).
### Fixed
- **Transient planning artifacts purged from the repo** — `docs/plans`, `docs/specs`, `docs/superpowers/{plans,specs}` (deploy-skill 2026-06-27, model-routing 2026-07-15) were run-time pipeline artifacts that should have been deleted in their chantiers' post-merge cleanup and slipped through (one pair predates the lifecycle rule, one missed the purge step of a 6-wave chantier). Git history at the feature commits remains their archive; `docs/` no longer exists.
## [1.2.1] — 2026-07-20
### Fixed
- **README caught up with the code it describes** — the "Agent model routing" section still presented the BDR-066 v1 scheme (7 rows factually wrong after model-tiering v2): reframed to the BDR-076/077 4-tier table verified against agent frontmatters (opus-pinned judgment agents, per-mode splits for doc-syncer / handover-doc-writer / seo-geo pipelines, plugin-probe added, unpinned inline agents listed as such). Also: Context7 paragraph rewritten to the two-surface model (find-docs = sole doc-fetch surface, `ctx7-reminder` hook = scoped session nudge, BDR-078), `hooks/` tree line now mentions the ctx7 reminder, and the `/ship-feature` workflow block gained its STEP 2b (adversarial plan-challenge) line. Docs-only release — no code change.
## [1.2.0] — 2026-07-20 ## [1.2.0] — 2026-07-20
### Added ### Added
+57 -52
View File
@@ -11,56 +11,39 @@ Global Claude Code configuration — agents, skills, plugins, and project templa
This repo is your personal Claude Code setup, versioned and reproducible across machines. This repo is your personal Claude Code setup, versioned and reproducible across machines.
``` See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the full project layout and
claude-config/ structural principles (skills = entry points, agents = execution units,
├── CLAUDE.global.md # Global coding preferences — deployed as ~/.claude/CLAUDE.md templates = per-project scaffolding, graphify = codebase knowledge graph).
├── CLAUDE.md # Project-scope instructions (this repo only)
├── settings.json # Global permissions (deny / ask / allow rules)
├── install.sh # Bootstrap: Claude Code CLI + auth + submodules + link + plugins
├── install-plugins.sh # One-shot installer: prerequisites + all plugins
├── link.sh # Symlinks this repo into ~/.claude/
├── doctor.sh # Setup diagnostic
├── update-all.sh # One-command update for all components
├── Makefile # Unified entry point: make install / doctor / update
├── plugins.lock.json # Version pinning for non-marketplace dependencies
├── hooks/ # Session start, statusline, RTK rewrite + design-toolchain guards
├── agents/ # Execution units called by skills (never invoked directly)
├── skills/ # Entry points invoked via /skill-name
├── skills-external/ # Vendored skill packs (gstack submodule + installer-fetched design packs)
├── templates/ # Per-project templates (CLAUDE.md, settings, memory registries, deploy runbook, gitignore)
└── lib/ # Shared shell libs (gitflow, profiles, commit helpers, archetypes, tests)
```
**Architecture principle:** ### Agent model routing (model-tiering v2)
- `skills/` = entry points you invoke via `/skill-name`
- `agents/` = execution units called by skills (never invoked directly by user)
- `templates/` = symlinked to `~/.claude/templates/` — copy into projects via `/onboard` or manually
- **Graphify** builds a knowledge graph of any codebase (`/graphify query`), producing a navigable wiki in `graphify-out/wiki/`. This map helps Claude understand project structure, find relevant code faster, and reason across files. Essential for large-scope tasks (multi-file features, complex bugs, architectural changes). Small tasks should skip it and read files directly.
### Agent model routing (BDR-066) Doctrine: the session model (Fable) does main-loop reflection ONLY —
brainstorm, plan, contract, audit judgment, gates, loop decisions — enforced
Reflection (brainstorm, plan, contract, audit judgment, loop decisions) runs by a blocking gate (`lib/model-gate.md` + `lib/model-check.sh`) at the entry
INLINE on the session model — assumed Fable/Opus, enforced by a blocking of the 13 reflection orchestrators. Nothing dispatched inherits silently:
gate (`lib/model-gate.md` + `lib/model-check.sh`) at the entry of the 13 typed agents carry a frontmatter pin, built-ins get an explicit `model=` at
reflection orchestrators. Execution runs on pinned subagents: every call site.
| Agent | Model | Tier | | Agent | Model | Tier |
|---|---|---| |---|---|---|
| feater, hotfixer, bugfixer | sonnet (pinned) | executors — code from a closed plan (feat), fix from a closed diagnosis (bugfix), fix-bundle appliers | | feater, hotfixer, bugfixer | sonnet (pinned) | executors — code from a closed plan (feat), fix from a closed diagnosis (bugfix), fix-bundle appliers |
| verifier, security-auditor | sonnet (pinned) | fresh gates (≤3×/loop) | | verifier, security-auditor | sonnet (pinned) | fresh gates (≤3×/loop) |
| commit-changer, release-executor, code-cleaner | sonnet (pinned) | dispatched execution — grouping+commit / release spans / approved cleanup (the audit + approval gate stay in the dispatcher) | | commit-changer, release-executor, code-cleaner | sonnet (pinned) | dispatched execution — grouping+commit / release spans / approved cleanup (audit + approval gates stay in the dispatcher) |
| doc-syncer, onboarder, scaffolder, refactorer, interviewer, plugin-advisor | sonnet (pinned) | workers | | onboarder, scaffolder, refactorer, validator-analyzer, plugin-probe | sonnet (pinned) | workers — config generation, scaffold, refactor, deterministic W3C/WCAG runner, mechanical plugin probe |
| status-reporter | haiku (pinned) | mechanical collector | | status-reporter | haiku (pinned) | mechanical collector |
| handover-doc-writer | sonnet (pinned) | deliverable writer — synthesizes + renders the client doc from a resolved PACKAGE (dispatched by client-handover) | | analyzer, plan-challenger, plugin-advisor | opus (pinned) | dispatched judgment — pre-plan analysis, 3-lens adversarial plan challenge (`/ship-feature` STEP 2b), plugin-fit reasoning |
| analyzer, seo-analyzer, geo-analyzer, validator-analyzer, client-handover-writer | inherit session (Fable/Opus) | reflection / audit / inline playbooks / ship-and-handover pipeline | | seo-analyzer, geo-analyzer | opus pin (judge mode); collect/template spans dispatched `model="sonnet"` | 3-mode audit pipelines — judgment fail-closed on opus, mechanical collect + templating on sonnet |
| plan-challenger | inherit session (Fable/Opus) | fresh adversarial plan challenger — 3 parallel lenses (correctness/robustness/simplicity), dispatched by `/ship-feature` STEP 2b before the validation gate | | doc-syncer | sonnet pin; audit mode dispatched `model="opus"` | two-mode: audit (drift judgment, opus) / patch (mechanical apply, sonnet) |
| handover-doc-writer | sonnet pin; synthesize mode dispatched `model="opus"` | two-mode: synthesize (opus) / render (sonnet) — client deliverable |
| interviewer, client-handover-writer | unpinned (inline-load = session model) | they ARE the main loop — a frontmatter pin would be inert |
| Explore (built-in) | inherit session (Fable/Opus) | search feeds reflection — kept on the big model, not pinned down | | Explore (built-in) | inherit session (Fable/Opus) | search feeds reflection — kept on the big model, not pinned down |
The pure-execution skills `/doc`, `/status`, `/commit-change`, The pure-execution skills `/doc`, `/status`, `/commit-change`,
`/release-candidate` **dispatch** their agent (instead of inline-loading it) `/release-candidate` **dispatch** their agent (instead of inline-loading it)
so the pin takes effect and the work leaves the big session model; `/hotfix` so the pin takes effect and the work leaves the big session model; `/hotfix`
was split like `/feat` (reflection inline + gate, `hotfixer` executor) and so was split like `/feat` (reflection inline + gate, `hotfixer` executor) and so
joins the gated group (13th). joins the gated group (13th); `/client-handover`'s nested skill-runner
children are dispatched `model:"fable"` (they carry reflection).
--- ---
@@ -68,14 +51,14 @@ joins the gated group (13th).
```bash ```bash
# 1. Clone with submodules # 1. Clone with submodules
git clone --recurse-submodules git@github.com:youruser/claude-config.git git clone --recurse-submodules https://github.com/bchanot/claude
cd claude-config cd claude
# 2. Bootstrap (CLI + auth + symlinks + plugins) # 2. Bootstrap (CLI + auth + symlinks + plugins)
bash install.sh make install
# 3. Verify setup # 3. Verify setup
bash doctor.sh make doctor
# 4. Restart Claude Code — plugins load automatically # 4. Restart Claude Code — plugins load automatically
``` ```
@@ -84,9 +67,12 @@ All scripts use their own location to find the repo — run them from anywhere.
The plugins step logs to `install-YYYYMMDD-HHMMSS.log`. The plugins step logs to `install-YYYYMMDD-HHMMSS.log`.
**Optional — Context7** (fast doc lookup for React / Next.js / Prisma…): the plugins **Optional — Context7** (fast doc lookup for React / Next.js / Prisma…): the plugins
step installs the `ctx7` CLI and wires it into Claude Code itself — single surface = step installs the `ctx7` CLI and wires it into Claude Code. The doc-fetch surface is
the `find-docs` skill; the generated `rules/context7.md` is purged by design the `find-docs` skill alone (the generated `rules/context7.md` is purged by
(BDR-053). If you run `ctx7 setup` manually, delete that rule or re-run `make plugin`. design; if you run `ctx7 setup` manually, delete that rule or re-run `make plugin`).
A once-per-session `ctx7-reminder` hook nudges toward it when the current project
carries fast-moving libs (`lib/fast-libs.sh`) — a scoped second surface, a
refinement of the single-surface rule, not a reversal.
```bash ```bash
ctx7 login # optional: OAuth / API key for higher rate limits ctx7 login # optional: OAuth / API key for higher rate limits
@@ -152,7 +138,7 @@ a different package, ships its own conflicting `graphify` bin) — see
| `/web-validate` | W3C HTML/CSS validity + WCAG 2.1 accessibility audit | | `/web-validate` | W3C HTML/CSS validity + WCAG 2.1 accessibility audit |
| `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) | | `/geo` | GEO-only audit — AI-search visibility (ChatGPT, Perplexity, Claude, Gemini…) |
| `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) | | `/client-handover` | Final project delivery — audits + branded deliverable (Markdown / HTML / PDF) |
| `/profile` | Activate a skill profile (design / dev / qa / audit / minimal) | | `/profile` | Activate a skill profile (web / seo / web-full / full / backend / design / dev / qa / audit / minimal) |
| `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean | | `/tour` | Grouped all-axes sweep — cleanup + security + reconcile + doc, fix and loop until clean |
> This table lists personal skills. Gstack skills (investigate, review, retro, > This table lists personal skills. Gstack skills (investigate, review, retro,
@@ -186,6 +172,7 @@ cd my-existing-project/
/ship-feature "feature description" /ship-feature "feature description"
# → STEP 0: plugin check # → STEP 0: plugin check
# → STEP 1-2: brainstorm + plan (superpowers) # → STEP 1-2: brainstorm + plan (superpowers)
# → STEP 2b: adversarial plan-challenge (3 lenses, report-only)
# → STEP 3: validation gate — user approval required # → STEP 3: validation gate — user approval required
# → STEP 4-7: implement (TDD) → review → capitalize (memory) # → STEP 4-7: implement (TDD) → review → capitalize (memory)
# → STEP 8: sync README (doc-sync) # → STEP 8: sync README (doc-sync)
@@ -227,17 +214,15 @@ See [`templates/settings/SETTINGS.md`](templates/settings/SETTINGS.md) for the f
`~/.claude.json` (or the project's `.mcp.json`) — if you pass the real secret `~/.claude.json` (or the project's `.mcp.json`) — if you pass the real secret
on that command line, it materializes as a second plaintext copy outside on that command line, it materializes as a second plaintext copy outside
`~/.claude/.env`, invisible to the repo's `.gitignore`/allowlist reach (this `~/.claude/.env`, invisible to the repo's `.gitignore`/allowlist reach (this
bit us once: job7/BDR-026). bit us once).
Claude Code expands `${VAR}` and `${VAR:-default}` in `mcpServers` config — Claude Code expands `${VAR}` and `${VAR:-default}` in `mcpServers` config —
in `env`, `command`, `args`, `url`, and `headers` — for both project (`.mcp.json`) in `env`, `command`, `args`, `url`, and `headers` — for both project (`.mcp.json`)
and user (`~/.claude.json`) scope. Use that instead of a literal value: and user (`~/.claude.json`) scope. Use that instead of a literal value:
```bash ```bash
# WRONG — plaintext key lands in ~/.claude.json: MAGIC_API_KEY=<Enter your magic api key here from https://21st.dev/settings/api-keys >
claude mcp add magic --scope user --env API_KEY="$MAGIC_API_KEY" -- npx -y @21st-dev/magic@latest # single-quoted so bash doesn't expand it; Claude Code expands it at
# RIGHT — single-quoted so bash doesn't expand it; Claude Code expands it at
# launch, reading the var from its own process environment: # launch, reading the var from its own process environment:
claude mcp add magic --scope user --env 'API_KEY=${MAGIC_API_KEY}' -- npx -y @21st-dev/magic@latest claude mcp add magic --scope user --env 'API_KEY=${MAGIC_API_KEY}' -- npx -y @21st-dev/magic@latest
``` ```
@@ -255,6 +240,26 @@ There is no `claude mcp add` flag that writes the reference form for you —
the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as the `${VAR}` syntax has to be typed by hand (or via a wrapper script), same as
above. above.
### SEO data layer (`/seo` FULL) — Google OAuth + CrUX keys
The same `~/.claude/.env` also feeds `lib/seo-data`, which pulls real Google
Search Console and Chrome UX Report data into `/seo` FULL audits. Add these
three vars (template with the GCP console steps in `.env.example`):
```bash
# OAuth Desktop client — GCP console → APIs & Services → Credentials →
# OAuth client (Desktop). Consent scope: webmasters.readonly only.
GOOGLE_OAUTH_CLIENT_ID=<your-client-id.apps.googleusercontent.com>
GOOGLE_OAUTH_CLIENT_SECRET=<your-client-secret>
# CrUX + PageSpeed API key — GCP console → Credentials → API key,
# restricted to those two APIs. https://developer.chrome.com/docs/crux/api
CRUX_API_KEY=<your-crux-api-key>
```
Then run the one-time consent flow: `make seo-connect` (per-label token
store, multi-site safe). Missing credentials never break an audit — `/seo`
degrades gracefully to anonymous PageSpeed lab data.
### magic MCP (`@21st-dev/magic`) — known callback-injection risk ### magic MCP (`@21st-dev/magic`) — known callback-injection risk
`21st_magic_component_builder` opens an **unauthenticated** local callback `21st_magic_component_builder` opens an **unauthenticated** local callback
@@ -264,7 +269,7 @@ can `POST` to it and that body is injected **verbatim** into the tool result
the model consumes (job8 audit, `dist/utils/callback-server.js:36`). This is the model consumes (job8 audit, `dist/utils/callback-server.js:36`). This is
in the third-party package's code, not this repo's config — **we don't patch in the third-party package's code, not this repo's config — **we don't patch
it**. The mitigation lives entirely on our side: `settings.json` it**. The mitigation lives entirely on our side: `settings.json`
`permissions.ask` explicitly lists all 4 `mcp__magic__*` tools ([[BDR-059]]), `permissions.ask` explicitly lists all 4 `mcp__magic__*` tools,
so every call — builder included — requires a live confirmation and can so every call — builder included — requires a live confirmation and can
never auto-execute. Don't allowlist never auto-execute. Don't allowlist
`21st_magic_component_builder` or `21st_magic_component_refiner` (arbitrary `21st_magic_component_builder` or `21st_magic_component_refiner` (arbitrary
@@ -290,10 +295,10 @@ make plugin # install plugins only
make link # create/update symlinks into ~/.claude/ make link # create/update symlinks into ~/.claude/
make doctor # diagnostic make doctor # diagnostic
make update # update Claude Code, config, submodules, plugins, and verify make update # update Claude Code, config, submodules, plugins, and verify
make test # run deterministic tests (lib/tests/*.test.sh + lib/seo-data/*.test.sh + lib/gitflow-test.sh) make test # run deterministic tests (lib/tests/*.test.sh + lib/seo-data/*.test.sh + lib/gitflow-test.sh + lib/tests/run-*.sh)
make onboard # onboard an existing project (run from its dir) make onboard # onboard an existing project (run from its dir)
make seo-connect # connect a Google account for /seo FULL (OAuth consent) make seo-connect # connect a Google account for /seo FULL (OAuth consent)
make profile cmd="set X" # activate a skill profile (design/dev/qa/audit/minimal/full) make profile cmd="set X" # activate a skill profile (web/seo/web-full/full/backend/design/dev/qa/audit/minimal)
make profile-list # list skill profiles make profile-list # list skill profiles
make profile-current # show the active profile make profile-current # show the active profile
make profile-reset # re-enable all gstack skills make profile-reset # re-enable all gstack skills
+1 -1
View File
@@ -163,7 +163,7 @@ Tu veux...
| `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) | | `/pdf-translate` | Traduire un PDF vers une autre langue | Sortie HTML fidèle (images, layout, style préservés) |
| `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) | | `/impeccable` | Audit/polish design + détecteur anti-slop déterministe | 23 verbes ; `npx impeccable detect` (exit 0/2) |
| `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre | | `/tour` | Sweep groupé sur un ou plusieurs projets | Sécu + nettoyage + reconcile + doc, boucle jusqu'à un pass propre |
| `/profile` | Changer le profil de skills | design / dev / qa / audit / minimal | | `/profile` | Changer le profil de skills | web / seo / web-full / full / backend / design / dev / qa / audit / minimal |
> Cette table couvre les skills personnels principaux. Les plugins (gstack, > Cette table couvre les skills personnels principaux. Les plugins (gstack,
> pr-review-toolkit…) et marketplaces externes en ajoutent beaucoup d'autres — > pr-review-toolkit…) et marketplaces externes en ajoutent beaucoup d'autres —
-385
View File
@@ -1,385 +0,0 @@
# Deploy Skill — Implementation Plan
> **Superseded by BDR-054** (`52f6678`): the shipped skill has NO `NEXT.sh` file and NO
> AskUserQuestion hand-back — see `skills/deploy/SKILL.md` for current behavior. This
> plan is kept as historical record; do not implement its NEXT.sh/hand-back sections.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a `deploy` skill — a per-project shell runbook that re-instantiates from the delta since the last deploy, hands control to the user for out-of-band execution, resumes cold (even in a new session), and learns from deploy errors in place.
**Architecture:** A surgical-commit helper (`lib/deploy-commit.sh`, allowlist-scoped to `.claude/deploy/`) is the foundation. Five per-project artifacts under `.claude/deploy/` carry runbook, incident ledger, deploy oracle, in-flight bridge, and the instantiated checklist. The skill is a two-moment SKILL.md (before → user deploys out-of-band → after, on the user's report), resumable cold from the JSON bridge per the `audit-delta` state-file convention. Bootstrap scaffolds the runbook for a project that has none.
**Tech Stack:** Bash (helper + git), Markdown (SKILL.md + runbook + ledger), JSON (oracle + bridge). No new runtime deps — Claude reads JSON natively in skill steps; the helper never parses JSON.
## Global Constraints
- Surgical commits only: `deploy-commit.sh` commits via explicit argv pathspec, never `git add -A`. (mirror BDR-034/036)
- Allowlist scope = `.claude/deploy/` ONLY; any other path is a loud rc-4 refusal. Inverse of `doc-commit.sh`'s `.claude/**` exclusion (BDR-022). Verified: real `doc-commit.sh` returns rc 4 on `.claude/deploy/PROCEDURE.md`.
- Delta = `git diff --name-only <base_sha> HEAD` — **explicit two endpoints, no dots** (two-dot ≡ this; three-dot undercounts — verified). Never `git rev-list` ancestry (phantom deltas on rebase — verified).
- First-deploy detection = `[ -f .claude/deploy/STATE.json ]` (deterministic). NEVER `git describe` (hard-errors rc 128 on no tag — verified).
- Resume convention = `audit-delta`: "the state file is the only memory between runs; never infer prior scope from context." Bridge read at STEP 0.
- Helper inherits from `lib/memory-commit.sh`/`lib/doc-commit.sh`: rc 3 on unsafe git state (detached/merge/rebase/cherry-pick), short-hash on stdout only on a real commit, per-file changed-paths filter, diagnostics to stderr.
- User executes the deploy out-of-band (prod ssh) — the skill NEVER runs deploy commands itself.
- Registries/spec language English; the spec of record is `docs/specs/2026-06-27-deploy-skill-design.md`.
---
## Decisions resolved at plan time
**§10 (cross-session state) — TRANCHÉ: separate bridge artifact.**
- Bridge = `.claude/deploy/PENDING.json` (JSON), **distinct from the ephemeral `NEXT.sh`**, **uncommitted** (transient local working state; gitignored). Schema:
```json
{ "base_sha": "<deployed STATE sha>", "target_sha": "<HEAD at instantiation>",
"delta": ["supabase/migrations/0033_x.sql", "docker-compose.yml"],
"step_reached": "awaiting-user", "started_at": "<ISO-8601>", "runbook_rev": "<PROCEDURE.md commit sha>" }
```
- Follows `audit-delta` ("state file is the only memory between runs"). Resolves the n°1↔n°3 coupling: NEXT.sh stays ephemeral per §3; the bridge persists and carries base+target+delta so moment 3 lays the correct marker and capitalizes the correct incident — **without re-parsing shell**, readable cold.
- Form-novelty (mid-flow pause-resume) is new → `writing-skills` formalizes the convention in Task 3.
- **LIMIT (acknowledged, not to be discovered):** `PENDING.json` is gitignored ⇒ cold-resume is **same-machine only** — it does not survive a clone or a move to another machine. Acceptable because a project's deploys run from one local; recorded as a constraint, not assumed away.
**§8 item 1 — tag push:** annotated tag `git tag -a deploy/<YYYY-MM-DD> <target_sha> -m "<summary>"` laid in MARK (success). **Project knob `# @config push_deploy_tags=true|false`** in the `PROCEDURE.md` header (default `false`): when true, MARK runs `git push origin deploy/<date>` — always **best-effort/non-fatal** (the push never blocks the deploy; tag is a bookmark, STATE.json is the oracle). Same-day re-deploy → suffix `-N`.
**§8 item 2 — INCIDENTS ID/name:** `.claude/deploy/INCIDENTS.md`, append-only, entries `DEP-NNN` (next = `grep '^## DEP-' | max+1`), fields mirror `blockers.md`: date, step, error (verbatim), root cause, fix. Resolution derivable from git: the commit that adds the entry IS the fix (atomic patch+incident); recover via `git log -S 'DEP-NNN' -- .claude/deploy/INCIDENTS.md`. Name confirmed `INCIDENTS.md` (not `ERRORS-LEARNED.md`).
**§8 item 3 — `@delta:` grammar:** directives on a runbook step's preceding comment line, patterns matched against the delta file list. `glob=` carries TWO required semantics (a single "checklist-only" reading was REJECTED — it breaks the game example, where step 3 runs `psql -f 0033` THEN `psql -f 0034` = one command PER file):
- `# @delta:<name> glob=<pat>:each` — **repeat**: emit the step's command once per delta file matching `<pat>` (e.g. `psql -f <each>`).
- `# @delta:<name> glob=<pat>:list` — **checklist**: emit the command once, with matching files as `# VERIFY:` items (e.g. `supabase migration up`).
- `# @delta:<name> when=<pat>[,<pat>...]` — **conditional**: include the step only if the delta intersects any pattern (e.g. rebuild when compose/Dockerfile changed).
- Patterns are git-pathspec/shell-glob; comma-separates alternatives. **Un-annotated step = fixed**, always emitted verbatim. The exact `:each`/`:list` keyword spelling is DEFERRED to `writing-skills` (Task 3); both semantics are mandatory.
**§8 item 4 — frontmatter / gates:**
```yaml
name: deploy
description: |
Use when deploying a project via its per-project runbook — instantiates the
delta since last deploy, hands off for out-of-band execution, resumes cold,
learns from errors.
Triggers: "deploy", "déploie", "run the deploy", "ship to prod", "deploy runbook".
allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion]
```
Gate vocabulary reused from `capitalize`/`client-handover`: `all / pick <IDs> / edit <ID> / skip-all`. Gates marked **[GATE]** in Task 3.
---
## File Structure
- Create `lib/deploy-commit.sh` — surgical commit helper, allowlist `.claude/deploy/`. (Task 1)
- Create `lib/tests/deploy-commit.test.sh` — real-git behavioral tests. (Task 1)
- Create `skills/deploy/SKILL.md` — the two-moment skill. (Task 3)
- Create `templates/deploy/PROCEDURE.md` — annotated starter runbook (scaffold source). (Task 2/4)
- Create `templates/deploy/INCIDENTS.md` — empty ledger header. (Task 2)
- Modify `.gitignore` — ignore `.claude/deploy/NEXT.sh` and `.claude/deploy/PENDING.json`. (Task 2)
- Per-project, created at runtime (NOT in this repo): `.claude/deploy/{PROCEDURE.md, INCIDENTS.md, STATE.json, PENDING.json, NEXT.sh}`.
**Artifact lifecycle:**
| Artifact | Committed? | Lifecycle |
|---|---|---|
| `PROCEDURE.md` | yes (deploy-commit) | in-place edits (learning) |
| `INCIDENTS.md` | yes (deploy-commit) | append-only `DEP-NNN` |
| `STATE.json` | yes (deploy-commit) | overwritten on success = oracle |
| `PENDING.json` | **no** (gitignored) | written at hand-back, deleted on success = cold-resume bridge |
| `NEXT.sh` | **no** (gitignored) | regenerated per deploy, ephemeral checklist |
---
### Task 1: `lib/deploy-commit.sh` — surgical commit helper (FOUNDATION, TDD)
**Files:**
- Create: `lib/deploy-commit.sh`
- Test: `lib/tests/deploy-commit.test.sh`
**Interfaces:**
- Produces: `deploy-commit.sh pending <file>...` → exit 0 if any passed file in-scope has changes, else 1. `deploy-commit.sh commit "<msg>" <file>...` → commits ONLY passed in-scope files, prints short hash on stdout; rc 0 success, rc 1 clean/no-op, rc 3 unsafe git state, rc 4 out-of-scope path.
- Consumes: nothing (foundation).
- [ ] **Step 1: Write the failing test harness**
```bash
# lib/tests/deploy-commit.test.sh
#!/usr/bin/env bash
set -u
H="$(cd "$(dirname "$0")/.." && pwd)/deploy-commit.sh"
pass=0; fail=0
mkrepo() { local d; d=$(mktemp -d); git -C "$d" init -q; git -C "$d" config user.email t@t;
git -C "$d" config user.name t; mkdir -p "$d/.claude/deploy"; printf 'x\n' >"$d/seed";
git -C "$d" add seed; git -C "$d" commit -q -m seed; printf '%s' "$d"; }
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
d=$(mkrepo); printf 'run\n' >"$d/.claude/deploy/PROCEDURE.md"
out=$( cd "$d" && bash "$H" commit "docs(deploy): t" .claude/deploy/PROCEDURE.md ); rc=$?
check T1-rc "$rc" 0
check T1-committed-only "$(git -C "$d" show --name-only --format= HEAD)" ".claude/deploy/PROCEDURE.md"
check T1-hash-nonempty "$([ -n "$out" ] && echo y || echo n)" y
d=$(mkrepo); printf 'b\n' >"$d/src.txt"
( cd "$d" && bash "$H" commit "x" src.txt ) >/dev/null 2>&1; check T2-out-of-scope-rc "$?" 4
d=$(mkrepo)
( cd "$d" && bash "$H" commit "x" ".claude/deploy/../memory/secret" ) >/dev/null 2>&1
check T3-traversal-rc "$?" 4
d=$(mkrepo); printf 'p\n' >"$d/.claude/deploy/PROCEDURE.md"; printf 's\n' >"$d/src.txt"
( cd "$d" && bash "$H" commit "x" .claude/deploy/PROCEDURE.md src.txt ) >/dev/null 2>&1
check T4-mixed-refuses-all "$?" 4
check T4-nothing-committed "$(git -C "$d" rev-list --count HEAD)" 1
d=$(mkrepo); git -C "$d" checkout -q --detach
printf 'p\n' >"$d/.claude/deploy/PROCEDURE.md"
( cd "$d" && bash "$H" commit "x" .claude/deploy/PROCEDURE.md ) >/dev/null 2>&1
check T5-unsafe-rc "$?" 3
d=$(mkrepo)
( cd "$d" && bash "$H" pending .claude/deploy/PROCEDURE.md ); check T6-pending-clean-rc "$?" 1
d=$(mkrepo); printf 'p\n' >"$d/.claude/deploy/PROCEDURE.md"
printf 'i\n' >"$d/.claude/deploy/INCIDENTS.md"; printf '{}\n' >"$d/.claude/deploy/STATE.json"
( cd "$d" && bash "$H" commit "docs(deploy): learn" .claude/deploy/PROCEDURE.md \
.claude/deploy/INCIDENTS.md .claude/deploy/STATE.json ) >/dev/null 2>&1
check T7-atomic-rc "$?" 0
check T7-three-files "$(git -C "$d" show --name-only --format= HEAD | grep -c deploy)" 3
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
```
- [ ] **Step 2: Run the test, verify it FAILS**
Run: `bash lib/tests/deploy-commit.test.sh`
Expected: FAIL (helper absent) — every check fails or the harness errors on missing `lib/deploy-commit.sh`.
- [ ] **Step 3: Implement `lib/deploy-commit.sh`**
```bash
#!/usr/bin/env bash
# deploy-commit.sh — surgical commit for the .claude/deploy/ runbook family.
# Allowlist scope = .claude/deploy/ ONLY (inverse of doc-commit's .claude exclusion).
set -u
_in_git_repo() { git rev-parse --is-inside-work-tree >/dev/null 2>&1; }
_unsafe_state() { # 0 = unsafe
local g; g=$(git rev-parse --git-dir 2>/dev/null) || return 0
git symbolic-ref -q HEAD >/dev/null 2>&1 || return 0 # detached HEAD
[ -e "$g/MERGE_HEAD" ] || [ -d "$g/rebase-merge" ] || \
[ -d "$g/rebase-apply" ] || [ -e "$g/CHERRY_PICK_HEAD" ] && return 0
return 1
}
_out_of_scope() { # 0 = forbidden, 1 = in scope
case "$1" in
*..*) return 0 ;; # traversal — forbidden FIRST
.claude/deploy/*) return 1 ;; # allowed
*) return 0 ;; # everything else forbidden
esac
}
_scope_violations() { local p; for p in "$@"; do _out_of_scope "$p" && printf '%s\n' "$p"; done; }
_changed_only() { # echo passed files that actually have changes
local p; for p in "$@"; do
[ -n "$(git status --porcelain -- "$p" 2>/dev/null)" ] && printf '%s\n' "$p"; done
}
cmd="${1:-}"; shift || true
_in_git_repo || { echo "deploy-commit: not a git repo" >&2; exit 2; }
case "$cmd" in
pending)
[ "$#" -gt 0 ] || { echo "deploy-commit: pending needs file args" >&2; exit 2; }
[ -n "$(_changed_only "$@")" ] && exit 0 || exit 1 ;;
commit)
msg="${1:-}"; shift || true
[ -n "$msg" ] && [ "$#" -gt 0 ] || { echo "deploy-commit: commit needs <msg> <file>..." >&2; exit 2; }
viol=$(_scope_violations "$@")
if [ -n "$viol" ]; then
{ echo "deploy-commit: REFUSED — path(s) outside .claude/deploy/ allowlist:";
printf ' - %s\n' $viol;
echo "deploy-commit: NOTHING committed. Caller must pass only .claude/deploy/ files."; } >&2
exit 4
fi
_unsafe_state && { echo "deploy-commit: unsafe git state (detached/merge/rebase) — not committing" >&2; exit 3; }
mapfile -t changed < <(_changed_only "$@")
[ "${#changed[@]}" -gt 0 ] || exit 1
git commit -q -m "$msg" -- "${changed[@]}" || { echo "deploy-commit: git commit failed" >&2; exit 1; }
git rev-parse --short HEAD ;;
*) echo "usage: deploy-commit.sh pending <file>... | commit \"<msg>\" <file>..." >&2; exit 2 ;;
esac
```
- [ ] **Step 4: Run the test, verify it PASSES**
Run: `bash lib/tests/deploy-commit.test.sh`
Expected: `PASS=12 FAIL=0` (exit 0).
- [ ] **Step 5: shellcheck**
Run: `shellcheck lib/deploy-commit.sh lib/tests/deploy-commit.test.sh`
Expected: clean (matches repo Health Stack norm).
- [ ] **Step 6: Commit**
```bash
git add lib/deploy-commit.sh lib/tests/deploy-commit.test.sh
git commit -m "feat(deploy): deploy-commit.sh — allowlist surgical commit for .claude/deploy/"
```
---
### Task 2: Artifacts + bridge formats (§10 materialized)
**Files:**
- Create: `templates/deploy/PROCEDURE.md`, `templates/deploy/INCIDENTS.md`
- Modify: `.gitignore`
**Interfaces:**
- Produces: the on-disk shapes the skill reads/writes — `PROCEDURE.md` annotation grammar, `INCIDENTS.md` `DEP-NNN` template, `STATE.json` and `PENDING.json` schemas.
- Consumes: nothing.
- [ ] **Step 1: Write `templates/deploy/PROCEDURE.md`** (annotated starter — fixed steps verbatim, dynamic steps annotated)
```bash
#!/usr/bin/env bash
# === deploy runbook (reference) — NOT run directly. Instantiated to NEXT.sh per delta. ===
# Fixed steps run every deploy; `# @delta:` steps re-instantiate from the delta.
# @config push_deploy_tags=false
# NOTE grammar: glob=<pat>:each repeats the command per matching file (e.g. psql -f <each>);
# glob=<pat>:list runs once + lists matching files as VERIFY items; when=<pat,...> is conditional.
# 1) backup BEFORE any forward-only migration
ssh "$DEPLOY_HOST" 'pg_dump "$DB" > ~/backups/pre-deploy-$(date +%F-%H%M).sql' # VERIFY: dump size > 0
# @delta:migrations glob=supabase/migrations/*.sql:list
# 2) apply NEW migrations (one command; skill lists the delta migrations to VERIFY)
ssh "$DEPLOY_HOST" 'supabase migration up' # VERIFY: "Applied" for each
# @delta:rebuild when=docker-compose*.yml,Dockerfile,Dockerfile.*
# 3) rebuild + restart services (only if build inputs changed)
ssh "$DEPLOY_HOST" 'docker compose up -d --build' # VERIFY: docker compose ps healthy
# @delta:deps when=package.json,*lock*,requirements.txt,pyproject.toml
# 4) install deps (only if manifests changed)
ssh "$DEPLOY_HOST" 'cd app && npm ci' # VERIFY: exit 0
# 5) reload cache + smoke test (fixed)
ssh "$DEPLOY_HOST" 'systemctl reload app'
curl -fsS https://$DEPLOY_HOST/health # VERIFY: HTTP 200
```
- [ ] **Step 2: Write `templates/deploy/INCIDENTS.md`** (ledger header)
```markdown
# Deploy incidents (append-only) — DEP-NNN
<!-- One entry per incident. Next ID = grep '^## DEP-' | max+1. Mirrors blockers.md. -->
<!-- Resolution = the commit that adds this entry (atomic patch+incident). Recover: git log -S 'DEP-NNN' -- .claude/deploy/INCIDENTS.md -->
<!-- ## DEP-NNN — <step> failed
- date: YYYY-MM-DD
- step: <runbook step + label>
- error: `<verbatim error>`
- cause: <root cause>
- fix: <what changed in PROCEDURE.md> -->
```
- [ ] **Step 3: Record the JSON schemas** (no parsing in shell — Claude reads them in skill steps)
`STATE.json` (committed oracle, overwritten on success):
```json
{ "deployed_sha": "<sha>", "deployed_at": "<ISO-8601>", "outcome": "ok",
"tag": "deploy/<YYYY-MM-DD>" }
```
`PENDING.json` (gitignored bridge, deleted on success): schema as in "Decisions resolved at plan time / §10".
- [ ] **Step 4: Update `.gitignore`**
```gitignore
# deploy: transient per-deploy state (the runbook/ledger/oracle ARE committed)
.claude/deploy/NEXT.sh
.claude/deploy/PENDING.json
```
- [ ] **Step 5: Verify templates are well-formed**
Run: `bash -n templates/deploy/PROCEDURE.md && grep -c '^# @delta:' templates/deploy/PROCEDURE.md`
Expected: no syntax error; `3` annotations.
- [ ] **Step 6: Commit**
```bash
git add templates/deploy/PROCEDURE.md templates/deploy/INCIDENTS.md .gitignore
git commit -m "feat(deploy): runbook/ledger templates + bridge schemas + gitignore transient state"
```
---
### Task 3: `skills/deploy/SKILL.md` — the two-moment skill (REQUIRES writing-skills)
> **At this task, invoke `superpowers:writing-skills`** to shape SKILL.md to house conventions AND to formalize the **cross-session cold-resume** form (deploy's defining novelty; `audit-delta` is the state-file precedent, `client-handover` only an in-context pause). The step behaviors below are the contract; writing-skills governs structure/frontmatter/spine.
**Files:**
- Create: `skills/deploy/SKILL.md`
**Interfaces:**
- Consumes: `lib/deploy-commit.sh` (Task 1); artifact shapes (Task 2).
- Produces: the runtime behavior. STEP spine below.
**STEP spine (each = a SKILL.md section; [GATE] = mandatory stop):**
- [ ] **STEP 0 — PRE-FLIGHT + RESUME BRANCH.** Read `.claude/deploy/PENDING.json` FIRST (state file = only memory between runs).
- `PENDING.json` present → **RESUME**: jump to STEP 3 with its `{base, target, delta, step_reached}` (do not recompute).
- else `PROCEDURE.md` absent → **BOOTSTRAP** (Task 4).
- else → FRESH: continue STEP 1.
- [ ] **STEP 1 — DELTA.** `base = STATE.json.deployed_sha` (or, if `STATE.json` absent, first-deploy = full runbook). `git diff --name-only <base> HEAD` → delta file list. `target = git rev-parse HEAD`.
- [ ] **STEP 2 — INSTANTIATE + [GATE].** Expand `PROCEDURE.md`: emit fixed steps verbatim; expand `@delta:glob=…:each` steps by repeating the command per matching delta file, and `@delta:glob=…:list` steps once with matching files as `# VERIFY:` items; include `@delta:when=` steps only if the delta intersects. Read `INCIDENTS.md` and prepend matching `# PRE-WARN: DEP-NNN …` notes. Write `NEXT.sh`. **[GATE]** present `NEXT.sh` → `all / edit / skip-all`. On approve: write `PENDING.json` (`step_reached: awaiting-user`), then **hand back** (AskUserQuestion: "Run NEXT.sh step by step. Report back: Deployed OK / Failed at step X / Not yet").
- [ ] **STEP 3 — RESUME / REACT** (entry point on the user's report; may be a fresh session).
- "Deployed OK" → STEP 5.
- "Failed at step X: <err>" → STEP 4.
- "Not yet" → re-state pending, stop.
- [ ] **STEP 4 — LEARN + [GATE] + ATOMIC COMMIT.** Diagnose. Draft: (a) in-place `PROCEDURE.md` patch to step X; (b) `INCIDENTS.md` append `DEP-NNN` (error verbatim). **[GATE]** `all / pick / edit / skip-all` (significant edit). On approve: write both, then **one atomic** `bash lib/deploy-commit.sh commit "docs(deploy): patch <step> — recovered from <err>" .claude/deploy/PROCEDURE.md .claude/deploy/INCIDENTS.md`. The commit that adds `DEP-NNN` IS its resolution (derive via git later). Then bump `PENDING.json.runbook_rev` to the new `PROCEDURE.md` commit sha (keep `step_reached` at X). **Resume = REGENERATE `NEXT.sh` from `step_reached` against the PATCHED runbook** (steps X…end — X+1…end never ran), NOT replay a single step. The bumped `runbook_rev` is exactly the trigger: runbook changed ⇒ prior `NEXT.sh` is stale ⇒ regenerate. Re-present via STEP 2's hand-back.
- [ ] **STEP 5 — MARK (success).** Write `STATE.json` (`deployed_sha = PENDING.target_sha`, outcome ok, tag). `git tag -a deploy/<date> <target> -m "<summary>"`; **if `@config push_deploy_tags=true`** then `git push origin deploy/<date>` (best-effort, non-fatal). `bash lib/deploy-commit.sh commit "chore(deploy): mark <date> @ <short>" .claude/deploy/STATE.json`. **Delete `PENDING.json`** (+ `NEXT.sh`). Report.
- [ ] **Verification scenarios** (dry-run walkthroughs, no prod):
- First deploy (no `STATE.json`): full runbook fires; STATE laid; PENDING deleted.
- Delta deploy: only changed-bucket steps instantiate; `git diff` form is `<base> HEAD`.
- **Cold resume**: write a `PENDING.json` by hand, start `deploy` in a *fresh* context → STEP 0 detects it, resumes at STEP 3 from disk alone (no conversation memory).
- Failure→learn: report "failed at step X" → patch + DEP append committed atomically (one sha, both files).
- [ ] **Commit:** `git add skills/deploy/SKILL.md && git commit -m "feat(deploy): two-moment cross-session skill (resumes cold from PENDING.json)"`
---
### Task 4: Bootstrap (project without a runbook)
**Files:**
- Modify: `skills/deploy/SKILL.md` (STEP 0 BOOTSTRAP branch)
**Interfaces:**
- Consumes: `templates/deploy/*` (Task 2); STEP spine (Task 3).
- [ ] **Step 1 — BOOTSTRAP branch + [GATE].** When `PROCEDURE.md` absent, offer two paths (AskUserQuestion):
- **Paste** — user provides an existing runbook → adopt verbatim, then propose `@delta:` annotations for migration/build/deps steps.
- **Scaffold** — detect artifacts (`supabase/migrations/`, `docker-compose*.yml`/`Dockerfile`, `package.json`/lockfiles, `.env*`) + short interview (ssh host, backup cmd, health URL, rollback note) → fill `templates/deploy/PROCEDURE.md`.
- **[GATE]** present drafted `PROCEDURE.md` → `all / edit / skip-all`. On approve: write `PROCEDURE.md` + empty `INCIDENTS.md`; `bash lib/deploy-commit.sh commit "feat(deploy): bootstrap runbook" .claude/deploy/PROCEDURE.md .claude/deploy/INCIDENTS.md`. First deploy then proceeds (no STATE.json ⇒ full runbook).
- [ ] **Step 2 — Verify:** dry-run on a repo with `supabase/migrations/` + `docker-compose.yml` present → scaffold proposes migration + rebuild steps annotated; on a bare repo → interview-only path.
- [ ] **Commit:** `git add skills/deploy/SKILL.md && git commit -m "feat(deploy): bootstrap — paste-or-scaffold initial runbook"`
---
## Gates identified
- **[GATE] STEP 2** — approve instantiated `NEXT.sh` before hand-back.
- **[GATE] STEP 4** — approve runbook patch + `DEP-NNN` incident before the atomic learning commit.
- **[GATE] STEP 0/Task 4** — approve scaffolded `PROCEDURE.md` before first write.
- **Hand-back (STEP 2→3)** — AskUserQuestion is the resume point; the user executes out-of-band.
- **Task gates** — each Task ends test-green + shellcheck-clean + committed before the next (deps: 1 → 2 → 3 → 4).
## Self-review
- **Spec coverage:** 4 artifacts + bridge (§3/§10) → Task 2; STATE-oracle + `<base> HEAD` delta (§4) → Task 1 constraints + STEP 1; runbook+INCIDENTS learning, atomic couple (§5) → STEP 4; `deploy-commit.sh` inverse allowlist (§6) → Task 1; bootstrap (§7) → Task 4; two-moment cold resume (§10) → STEP 0/2/3 + PENDING.json. All §8 items resolved above. ✓
- **Placeholder scan:** none — helper code, test code, schemas, annotation grammar all concrete.
- **Type consistency:** `STATE.json.deployed_sha` (STEP 1 base, STEP 5 write), `PENDING.json.{base_sha,target_sha,delta,step_reached}` (STEP 0 read, STEP 2 write, STEP 4 update), `deploy-commit.sh commit "<msg>" <file>...` (Tasks 1/3/4) — names align.
- **Open at execution (not assumed):** the `writing-skills` consultation in Task 3 may rename/restructure SKILL.md sections to match the formalized cold-resume convention, and finalizes the `@delta:` `:each`/`:list` keyword spelling (both semantics mandatory); STEP behaviors and the §6 helper contract above are fixed regardless.
## Execution Handoff
Build order is strict by dependency: **Task 1 (helper, foundation) → Task 2 (formats) → Task 3 (skill, writing-skills) → Task 4 (bootstrap)**.
@@ -1,165 +0,0 @@
# Deploy skill — design spec
> **Superseded by BDR-054** (`52f6678`): the shipped skill has NO `NEXT.sh` file and NO
> AskUserQuestion hand-back — see `skills/deploy/SKILL.md` for current behavior. This
> spec is kept as historical record; do not implement its NEXT.sh/hand-back sections.
- **Date:** 2026-06-27
- **Status:** Design approved (5 knobs settled). **No skill code written yet.** Next step = implementation plan.
- **Scope:** A new `deploy` skill = a per-project shell RUNBOOK that lives in `.claude/deploy/`, gets re-instantiated from the delta since the last deploy, and LEARNS from deploy errors in place.
## 1. Vision — deployment memory that learns
Three moments:
1. **BEFORE** — produce the *instantiated* runbook: reference runbook + delta since last deploy, parameterized steps rewritten with the real artifacts (e.g. the migration step lists the migrations actually added since last deploy, not the runbook's examples).
2. **DURING** — the **user executes out-of-band** (prod ssh — Claude must not run it) and reports `deployed and tested` OR `failed at step X, here is the error` → fix together until success.
3. **AFTER** — on confirmed success: (a) if errors were hit + fixed, update the reference runbook so the next deploy does not repeat them; (b) lay the marker "deployed up to here" for the next diff.
Structural ancestor in the corpus: `client-handover` (BEFORE baseline → DURING user-deploy gate via `AskUserQuestion` → AFTER validate + react). No existing skill owns a learning per-project runbook — clean gap, no `.claude/deploy/` precedent.
## 2. Locked decisions
| # | Knob | Decision |
|---|------|----------|
| 1 | Marker / oracle | **STATE file is the oracle** (deployed SHA), **annotated tag** added as a human bookmark only |
| 2 | Learning storage | **In-place runbook edits + append-only `INCIDENTS.md`** (distinct jobs, atomic coupling) |
| 3 | Parameterization | **`# @delta:` annotations** bind dynamic steps to path-patterns; un-annotated steps are fixed |
| 4 | Bootstrap | **Offer both** — user pastes an existing runbook OR skill scaffolds via artifact detection + interview |
| 5 | Execution model | **`NEXT.sh` is a step-by-step CHECKLIST** — runnable shell, but driven by hand with manual `# VERIFY:` gates; never `bash NEXT.sh` unattended |
**Why #5 is design-time, not impl:** the execution model is load-bearing for moments 2 and 3. Moment 2 is defined as "user reports *failed at step X*", and moment 3's LEARN loop must know *which* step failed to patch it. A single `bash NEXT.sh` blob collapses both into "exited non-zero somewhere" and can strand a prod deploy (migrations, restarts) in partial state with no step control. Checklist is *entailed* by the three-moment structure, not merely safer.
Treated as settled corollaries: user executes out-of-band; a **new** `lib/deploy-commit.sh` helper (existing helpers cannot commit the runbook — see §6, verified).
## 3. Architecture
```
.claude/deploy/
PROCEDURE.md reference runbook — fixed shell + `# @delta:` annotated steps (edited IN-PLACE)
INCIDENTS.md DEP-NNN incident ledger: date, step, error verbatim, root cause,
fix (APPEND-ONLY; resolution = introducing commit, derive via git)
STATE.json deployed SHA + timestamp + outcome — the diff oracle (overwritten each deploy)
NEXT.sh instantiated runbook — EPHEMERAL, not committed ; run STEP-BY-STEP
(checklist, manual # VERIFY: gates) — never `bash NEXT.sh` unattended
lib/deploy-commit.sh surgical commit, allowlist = .claude/deploy/ , rc3 unsafe-git guard, short-hash stdout
Skill STEP spine (PRE-FLIGHT -> PROPOSE+GATE -> WRITE+COMMIT, house style):
0 PRE-FLIGHT runbook present? absent -> bootstrap (paste | scaffold+interview)
1 DELTA STATE absent -> first deploy = full runbook ; else diff <STATE_SHA> HEAD
2 INSTANTIATE expand @delta steps + read INCIDENTS pre-warns -> NEXT.sh -> GATE
3 (user executes out-of-band; reports "done" | "failed at step X: <err>")
4 LEARN on failure: patch PROCEDURE step + append DEP-NNN -> GATE -> deploy-commit (ATOMIC)
5 MARK on success: write STATE@sha ; annotate + push tag ; optional doc
```
## 4. Delta mechanism — verified (git 2.53.0)
All three facts re-run live before writing this spec; observed output recorded, not assumed.
**First-deploy detection = STATE-absent, deterministic. `describe` is off the detection path.**
```
[ -f .claude/deploy/STATE.json ] => exit 1 (absent = first deploy) <- THE detector
git describe --tags --match 'deploy/*' => fatal: No names found ; exit 128 <- only the reason NOT to use describe
[ -f .claude/deploy/STATE.json ] => exit 0 (present = delta path)
```
**Delta = `git diff --name-only <STATE_SHA> HEAD`** (two explicit endpoints; no dots, so it cannot be misread as three-dot).
```
LINEAR git diff --name-only <sha> HEAD => 0033_new.sql, svc.yml (== two-dot == three-dot; merge-base == STATE)
DIVERGED two-dot sideA sideB => fileA.txt, fileB.txt (both endpoints = true tree delta)
DIVERGED three-dot sideA...sideB => fileB.txt (merge-base — UNDERCOUNTS)
```
Two-dot/explicit-endpoints is the literal tree difference between the deployed tree and HEAD = what deploy needs. It is also rebase-robust: an orphaned marker still yields the correct tree diff, whereas `git rev-list A..B` (ancestry) reports phantom deltas after history rewrite (LRN-054's trap; verified in an earlier run). **Never use `rev-list` ancestry for the artifact list.**
**delta -> steps:** `# @delta:<kind>` annotations bind a dynamic step to the path-pattern that feeds it; the diff buckets straight into steps:
```
# @delta:migrations glob=supabase/migrations/*.sql
# @delta:rebuild when=docker-compose*.yml,Dockerfile
# @delta:deps when=package.json,*lock*
```
## 5. Learning model — runbook + INCIDENTS, non-redundant
| Artifact | Job | Lifecycle |
|---|---|---|
| `PROCEDURE.md` | The corrected procedure you run. A fix is baked into the step so the next run cannot repeat it. | in-place |
| `INCIDENTS.md` | The incident ledger; **read at BEFORE-time to pre-warn** ("0033 hit a lock timeout last deploy; runbook already carries `--timeout`, watch for it"). | append-only |
The pre-warn read is the function `git log` serves badly — that is why the ledger is not duplication. This mirrors the memory system's own split (append-only `journal.md`/`blockers.md` alongside in-place TODO/code).
**Coupling invariant:** one incident → **one in-place `PROCEDURE.md` patch + one `INCIDENTS.md` append, committed atomically in a single `deploy-commit.sh` call.** Never one without the other (mirrors BDR-034/036 "couple the commit to the integration step"). Significant patch (changes a prod path) → surface + approve before writing.
## 6. `lib/deploy-commit.sh` — new helper, inverse `.claude/` rule (verified)
Neither existing helper can commit the runbook — confirmed live:
```
REAL doc-commit.sh .claude/deploy/PROCEDURE.md => rc 4 "REFUSED — out-of-scope ... BDR-022 ... NOTHING committed"
REAL memory-commit.sh pending (deploy changed) => rc 1 (ignores it; allowlist = .claude/memory|tasks only)
```
`doc-commit.sh` is built to keep `.claude/**` *out* of public-doc commits; `.claude/deploy/` is under `.claude/`, so reuse is not just blocked, it is semantically wrong. `deploy-commit.sh` needs the **inverse** rule: a TARGET allowlist for `.claude/deploy/*`, modeled on `memory-commit.sh` (rc 3 unsafe-git guard, short-hash on stdout, `chore(deploy):`/`docs(deploy):` messages).
Allowlist guard — traversal reject ordered FIRST. Prototype matrix verified live:
```sh
_in_deploy_scope() {
case "$1" in
*..*) return 1 ;; # reject path traversal FIRST
.claude/deploy/*) return 0 ;; # ALLOW the deploy family only
*) return 1 ;; # reject everything else
esac
}
```
```
ALLOW .claude/deploy/{PROCEDURE.md,INCIDENTS.md,STATE}
REJECT .claude/memory/* .claude/tasks/* .claude/secret CLAUDE.md src/*
REJECT .claude/deploy (bare dir, no slash)
REJECT .claude/deploy-other/x (trailing-slash requirement closes prefix confusion)
REJECT .claude/deploy/../memory/secret (traversal closed by *..* matched first)
```
## 7. Bootstrap
`STEP 0 PRE-FLIGHT`: `PROCEDURE.md` present? Absent → bootstrap, two offered paths:
1. **Paste** — user supplies an existing runbook (the game example); skill adopts + annotates it.
2. **Scaffold** — skill detects deploy artifacts (migrations dir, compose/Dockerfile, package scripts, `.env`) + a short interview (ssh target, backup cmd, rollback note) → writes an annotated `PROCEDURE.md`.
First deploy has no marker → STATE-absent ⇒ full runbook fires; then lay STATE at the deployed SHA. The first deploy *is* the creation of the runbook + the first marker.
## 8. Open items (for the implementation plan)
> `NEXT.sh` execution model resolved → decision #5 (checklist), promoted to design-time.
- Tag push: tags don't push by default → AFTER step should `git push --tag deploy/<date>` or remind.
- `INCIDENTS.md` ID/format detail (mirror `blockers.md` `DEP-NNN`); confirm name vs `ERRORS-LEARNED.md`.
- `@delta:` annotation grammar (glob= vs when=) — finalize the small DSL.
- Frontmatter `allowed-tools` set; STEP gate wording reuse from `capitalize`/`client-handover`.
## 9. Build sequencing & a structural flag
**Two distinct disciplines, in order — do not conflate:**
1. `writing-plans` — global task ordering (helper → skill → bootstrap), dependencies, gates. The build plan.
2. → execution →
3. At the *skill* task ONLY: `writing-skills` — the discipline for the SKILL.md itself (structure, frontmatter, spine, config conventions). Used WHEN we reach the skill task, **not before** (it does not fire at plan time).
**Structural flag for `writing-skills` to resolve — do NOT assume the linear-spine convention suffices:**
deploy's spine is unusual — **two parts split by out-of-band execution**: STEP 0–2 before → *user deploys by hand* → STEP 4–5 after, on the `done`/`failed` report. A skill that **hands back control mid-run and resumes**.
Preliminary recon (confirm at the skill task — NOT verified now):
- The 6 completion flux (close, ship-feature, feat, bugfix, hotfix, commit-change) appear linear one-shot — synchronous gates at most, no out-of-band hand-back.
- The relevant precedent is OUTSIDE those 6: `client-handover` already hands back — a synchronous "Deploy done?" `AskUserQuestion` pause (STEP 5) — but it holds state in *conversation context*, not on disk.
- deploy's genuinely-new bit *may* be **disk-bridged resume** (`NEXT.sh` + `STATE` on disk as the bridge) — but **whether `NEXT.sh` alone suffices to resume cross-session is an OPEN design question, not a settled answer** (see §10). An earlier draft of this spec framed it as resolved; it is not. `writing-skills` must establish the convention (how to mark "I wait for your return here", detect + resume a pending deploy, hold state across the gap) — confirm there, do not assume the linear mould suffices.
## 10. Open design question (DESIGN-TIME, unresolved) — state across the two moments
deploy is a **two-moment skill**: moments 0–2 (BEFORE) → user deploys out-of-band → moment 3 (AFTER) on the `done`/`failed` report. **The report may arrive in a different session.** So the design must answer how state crosses the gap and what moment 3 must know to resume correctly.
> **`skill deux-temps, état entre temps = [à concevoir : NEXT.sh seul suffit-il pour reprendre cross-session ?]`**
Sub-questions (to settle when we resume — NOT now, NOT assumed):
- **What must the bridge record?** Moment 3 must (a) lay the correct marker = `STATE ← target sha`, and (b) capitalize the correct incident (which step, which delta). HEAD may have moved since NEXT.sh was generated → "current HEAD" is unsafe. The bridge must persist at least **{base STATE sha, target sha, delta manifest}** — inside NEXT.sh (header block) or a sidecar (`.claude/deploy/PENDING`)? Undecided.
- **Resume detection (re-entrancy):** STEP 0 PRE-FLIGHT must detect "a deploy is pending, awaiting your report" — likely *pending-bridge present + STATE not advanced to target* — and branch RESUME (ask done/failed) vs FRESH. Is moment 3 a new `deploy` call that re-detects from disk, or a `deploy --report`? Undecided.
- **Ephemeral vs persistent tension — LINKED to sub-question 1 (not independent).** §3 calls NEXT.sh "EPHEMERAL, not committed", yet a cross-session bridge MUST survive on disk. So: **if the bridge must persist, NEXT.sh-as-bridge is impossible while NEXT.sh stays ephemeral.** Likely *binary* resolution at plan time — either (a) NEXT.sh becomes persistent (contradicts §3), or (b) the bridge is a **separate** "deploy-in-progress" artifact `{base/target/delta}` distinct from NEXT.sh. Settle with `writing-skills`. (Uncommitted local state is fine; note the single-machine assumption — an uncommitted bridge won't follow a clone.)
- **Form-novelty — deploy's DEFINING characteristic: cross-session COLD resume.** `client-handover` is a *near* precedent, not exact: it hands back **in-context** (same conversation, state held in memory). deploy must resume with the **context lost** — so the **disk alone must carry everything to resume cold**. No existing skill resumes without context; that is what sets deploy apart, and it makes sub-question 1 **load-bearing** (disk must suffice for a cold restart). deploy likely introduces a NEW skill form → `writing-skills` establishes the convention. Confirm there.
**Next step:** `writing-plans` to turn this spec into an implementation plan (helper first, then skill); at the skill task, `writing-skills` to shape it to convention and **resolve the §10 two-moment state question** — which is design-time, deferred only because we are stopped here, not because it is impl detail.
File diff suppressed because it is too large Load Diff
@@ -1,143 +0,0 @@
# Model routing — reflection inline (big model) / execution pinned (Sonnet) — design
**Date**: 2026-07-15 · **Status**: approved (user, 2026-07-15) · **Branch**: `feature/model-routing`
**Lifecycle**: transient planning artifact (BDR-065) — committed during the run, deleted post-merge.
## Principle
The session model is assumed to be a big reasoning model (Fable 5, or Opus when
Fable is unavailable). Everything that **thinks** — brainstorming, planning,
technical decisions, audits, loop decisions — runs INLINE in the main
conversation, or in subagents that inherit the session model. Everything that
**executes** a ready-made plan — writing code, applying fix bundles, commits,
deliverable rendering — runs on Sonnet-pinned subagents. A blocking gate
enforces the "session = big model" assumption at the entry of every reflection
orchestrator.
User verdicts baked in (2026-07-14/15):
- Scope = hybrid: ship-feature/init-project execution → sonnet; `/feat`
re-architected (plan inline → dispatch executor); bugfix/hotfix stay fully
inline (BDR-050 conserved for them).
- Gate = BLOCKING, not advisory.
- Audit agents inherit the session model (no opus pin); the gate extends to
audit orchestrators.
- verifier + security-auditor KEEP `model: sonnet` (job9 decision confirmed).
- client-handover-writer → sonnet (requires converting its inline-load to a
true dispatch; human gates relocate to the main loop).
## 1. Blocking model gate
New `lib/model-check.sh`: resolves the current session model from
`settings.json` (physical path resolution — LRN-023 class), normalizes
(`claude-fable-5[1m]` → fable, `claude-opus-*` → opus, sonnet, haiku), prints
`big|small|unknown`. Exit 0 = big, 2 = small, 3 = unknown.
New `lib/model-gate.md` snippet (same include pattern as `lib/design-gate.md`):
run the check; `small` → STOP the skill: "session model is <X> — reflection
requires Fable/Opus. Switch with /model, then relaunch." `unknown` →
fail-visible: show the raw value, ask the user to confirm or abort (BDR-025
doctrine — unknown never silently passes).
Wired as a STEP 0 line in the reflection orchestrators:
`ship-feature, init-project, feat, bugfix, onboard, seo, geo, web-validate,
harden, audit-delta, tour, code-clean`.
NOT wired in: `hotfix` (trivial by definition), `commit-change`, `doc`,
`status`, `release-candidate`.
Caveats to prove at implementation time:
- `/model` mid-session rewrites settings.json (LRN-098 observed it once —
re-prove with a live flip-test before trusting the source).
- The helper itself must be flip-tested (LRN-096: an unproven guard is a
vacuous guard).
## 2. Frontmatter pins (`agents/*.md`)
| Agent | Before | After | Rationale |
|---|---|---|---|
| feater | (inherit) | **sonnet** | executor as subagent: seo/geo L1 applier + new /feat dispatch |
| hotfixer | (inherit) | **sonnet** | L1 applier (seo/geo/web-validate); /hotfix inline unaffected (pin inert on inline load) |
| client-handover-writer | opus | **sonnet** | deliverable executor; pin becomes EFFECTIVE only with §5 dispatch conversion (today's opus pin is inert — the agent is inline-loaded) |
| analyzer | haiku | **(none — inherit)** | analysis feeds the plan = reflection; runs big via the session model |
| verifier | sonnet | keep | F1 confirmed (job9) |
| security-auditor | sonnet | keep | F1 confirmed (job9) |
| seo-analyzer, geo-analyzer, validator-analyzer | (inherit) | keep (inherit) | audit = reflection = session model; covered by the gate |
| code-cleaner | (inherit) | keep (inherit) | audit phase = reflection; fixes hand off to refactorer (sonnet) via CODE-CLEAN-SCOPE.md (job9 H1) |
| doc-syncer, onboarder, scaffolder, refactorer, interviewer, plugin-advisor | sonnet | keep | workers/executors |
| status-reporter | haiku | keep | mechanical collector |
| bugfixer, commit-changer | (inherit) | keep | inline-only playbooks — a pin would be inert |
## 3. `/feat` re-architecture (partial supersede of BDR-050 — feat only)
`skills/feat/SKILL.md` absorbs the reflection: analyze-before-plan, design
gate, MINI-PLAN, contract (`lib/contract-interview.md`) — all inline. Then
dispatches `Agent(subagent_type="feater")` (sonnet via pin) with: the
contract, the plan, the branch name, repo conventions.
`agents/feater.md` is rewritten as a pure executor: implement the plan to the
letter, run project checks, commit (no attribution trailers), return a
structured summary. No user interaction inside feater (subagents cannot ask) —
every decision must be closed pre-dispatch.
The verify-secure loop moves out of feater.md into the /feat main loop
(LRN-083 invariant: loop decisions live in the main loop): fresh verifier →
ECARTS → re-dispatch feater with the verdict deltas, bounded 3×; then the
security gate. Escalation paths unchanged.
## 4. SDD execution pinned (ship-feature STEP 4, init-project STEP 8)
One instruction line in each SKILL.md: every implementation subagent
dispatched under `superpowers:subagent-driven-development` MUST carry
`model: "sonnet"` in the Agent call. No fork of the superpowers skill — the
main loop emits the Agent calls and controls the params.
## 5. client-handover conversion (inline-load → true dispatch)
`skills/client-handover/SKILL.md`: collect params inline (URL, logo, options),
then `Agent(subagent_type="client-handover-writer")` — the sonnet pin becomes
effective. Human gates (per-axis threshold escalation, overrides) RELOCATE to
the main loop: the writer returns a structured `GATE NEEDED` status instead of
asking; the dispatcher asks the user and re-dispatches (or continues via
SendMessage) with the decision. `AskUserQuestion` is removed from the writer's
tools.
OPEN VERIFY POINT: the writer's own nested dispatches (seo/harden re-runs as
general-purpose subagents) — verify at implementation what nested children
inherit (session model vs parent model). If they inherit the sonnet parent,
the re-run audits violate the principle → force the model explicitly in those
nested dispatches or lift them to the main loop.
## 6. web-validate fixes → L1 applier
STEP 3 stops applying fixes via inline Edit; dispatches `hotfixer` (sonnet)
with the fix bundle — same pattern as seo/geo (BDR-061 alignment).
## 7. Memory / doc / tests
- New BDR: model-routing principle (reflection inline big / executors sonnet /
blocking gate); partial supersede of BDR-050 (feat only); records F1
(verifier/security stay sonnet) and the analyzer haiku→inherit change.
- README: agent-model table refresh. CHANGELOG Unreleased entry.
- Tests: flip-tests for `model-check.sh` (fable[1m] / opus / sonnet / garbage
fixtures); gate STOP proven on a small-model fixture (LRN-096); /feat smoke
on a throwaway repo (LRN-079): plan inline → dispatch carries sonnet →
verify loop decided in main loop; grep census: no executor dispatch without
an effective pin.
## Out of scope / accepted deviations
- `/doc` and `/commit-change` stay inline on the session model (judgment and
execution interleaved; converting them buys little). Revisit under quota
pressure.
- bugfix/hotfix fully inline (BDR-050 conserved).
- No per-agent "fable-else-opus" fallback exists in the harness — the session
model IS the fallback mechanism; the gate is its backstop.
## Risks
- Model strings in settings.json may change shape with CC updates →
model-check must return `unknown` (fail-visible), never guess.
- feater as a subagent loses main-conversation context → the plan becomes the
contract; weak plans cost verify-loop iterations. Mitigation:
contract-interview stays mandatory in /feat.
- Nested model inheritance under client-handover-writer unknown → §5 verify
point.
+80 -15
View File
@@ -14,6 +14,9 @@
# - MCPs: delegated to lib/toggle-external.sh for known servers (magic), # - MCPs: delegated to lib/toggle-external.sh for known servers (magic),
# advisory otherwise # advisory otherwise
# - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally) # - CLIs: advisory only (rtk, gsd, ctx7, graphify — installed externally)
# - `set` is SYMMETRIC on managed items (BDR-079): plugins, external packs
# and MCPs in the MANAGED_* allowlists are disabled when the profile
# does not list them — nothing outside those lists is ever auto-toggled.
# #
# Always-on plugins (never toggled by `set`): security-guidance, # Always-on plugins (never toggled by `set`): security-guidance,
# superpowers + rtk hook + .claude internal. The script refuses to disable # superpowers + rtk hook + .claude internal. The script refuses to disable
@@ -61,6 +64,23 @@ MANAGED_PLUGINS=(
"pr-review-toolkit@claude-code-plugins" "pr-review-toolkit@claude-code-plugins"
) )
# External skill packs that are toggle-managed by `set` — same allowlist
# doctrine as MANAGED_PLUGINS: listed here only when the enabled state is
# task-type-driven. `set` disables these when the profile does not list
# them; anything else external (e.g. darwin-skill) is never auto-touched.
MANAGED_EXTERNALS=(
emil-design-eng
frontend-design
design-motion-principles
impeccable
)
# MCP servers that are toggle-managed by `set`, both ways (enable AND
# disable), delegated to lib/toggle-external.sh. Same allowlist doctrine.
MANAGED_MCPS=(
magic
)
# Plugins that MUST stay enabled — `set` will refuse to disable these even if # Plugins that MUST stay enabled — `set` will refuse to disable these even if
# they're not in the profile. (Defensive: belt-and-suspenders alongside # they're not in the profile. (Defensive: belt-and-suspenders alongside
# MANAGED_PLUGINS allowlist.) # MANAGED_PLUGINS allowlist.)
@@ -271,6 +291,11 @@ enable_skill() {
ok "enabled: $skill ($type)" ok "enabled: $skill ($type)"
elif [ -e "$SKILLS_DIR/$skill" ]; then elif [ -e "$SKILLS_DIR/$skill" ]; then
: :
elif [ "$type" = external ] && [ -d "$REPO/skills-external/$skill" ]; then
# Symlink never created (or hand-removed): recreate it from the
# vendored pack — mirrors toggle-external.sh's from-source path.
ln -sf "$REPO/skills-external/$skill" "$SKILLS_DIR/$skill"
ok "enabled: $skill (external, symlink created)"
else else
warn "missing: $skill ($type)" warn "missing: $skill ($type)"
fi fi
@@ -422,6 +447,48 @@ parked_gstack_count() {
find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' ' find "$DISABLED_DIR" -maxdepth 1 -name 'gstack__*' 2>/dev/null | wc -l | tr -d ' '
} }
# ── `set` trim helpers — one per managed category ─────────────
# Each disables the managed items NOT listed in the given profile. Allowlist
# doctrine: only MANAGED_* entries are ever auto-disabled.
disable_plugins_not_in() {
local prof="$1" keep_file p plugin_name marketplace
keep_file="$(mktemp)"
read_profile "$prof" \
| awk -F'\t' '$2 ~ /^plugin@/ { sub(/^plugin@/, "", $2); print $1"@"$2 }' \
| sort -u > "$keep_file"
for p in "${MANAGED_PLUGINS[@]}"; do
if ! grep -qx "$p" "$keep_file"; then
plugin_name="${p%@*}"
marketplace="${p#*@}"
disable_skill "$plugin_name" "plugin@${marketplace}"
fi
done
rm -f "$keep_file"
}
disable_externals_not_in() {
local prof="$1" keep_file x
keep_file="$(mktemp)"
read_profile "$prof" | awk -F'\t' '$2 == "external" { print $1 }' \
| sort -u > "$keep_file"
for x in "${MANAGED_EXTERNALS[@]}"; do
grep -qx "$x" "$keep_file" || disable_skill "$x" external
done
rm -f "$keep_file"
}
disable_mcps_not_in() {
local prof="$1" keep_file s
keep_file="$(mktemp)"
read_profile "$prof" | awk -F'\t' '$2 == "mcp" { print $1 }' \
| sort -u > "$keep_file"
for s in "${MANAGED_MCPS[@]}"; do
grep -qx "$s" "$keep_file" || disable_skill "$s" mcp
done
rm -f "$keep_file"
}
# ── Commands ────────────────────────────────────────────── # ── Commands ──────────────────────────────────────────────
cmd_list() { cmd_list() {
@@ -506,24 +573,20 @@ cmd_apply() {
cmd_set() { cmd_set() {
local prof="$1" local prof="$1"
info "Setting profile: $prof (exclusive — disables non-listed gstack skills + managed plugins)" info "Setting profile: $prof (exclusive — disables non-listed gstack skills + managed plugins/externals/MCPs)"
# Disable gstack-origin skills not in profile. # Disable gstack-origin skills not in profile.
disable_gstack_not_in "$prof" disable_gstack_not_in "$prof"
# Disable managed plugins not in profile (PROTECTED_PLUGINS are excluded # Disable managed plugins not in profile (PROTECTED_PLUGINS are excluded
# by disable_skill itself — belt and suspenders). # by disable_skill itself — belt and suspenders).
local plugin_keep_file p plugin_name marketplace disable_plugins_not_in "$prof"
plugin_keep_file="$(mktemp)"
read_profile "$prof" | awk -F'\t' '$2 ~ /^plugin@/ { sub(/^plugin@/, "", $2); print $1"@"$2 }' | sort -u > "$plugin_keep_file" # Symmetry (BDR-079): a profile switch also parks the managed external
for p in "${MANAGED_PLUGINS[@]}"; do # packs and unregisters the managed MCPs the new profile does not need —
if ! grep -qx "$p" "$plugin_keep_file"; then # design leftovers (emil, magic…) no longer survive a `set backend`.
plugin_name="${p%@*}" disable_externals_not_in "$prof"
marketplace="${p#*@}" disable_mcps_not_in "$prof"
disable_skill "$plugin_name" "plugin@${marketplace}"
fi
done
rm -f "$plugin_keep_file"
# Enable everything listed in the profile. # Enable everything listed in the profile.
cmd_apply "$prof" cmd_apply "$prof"
@@ -679,9 +742,11 @@ EXAMPLES:
bash lib/profile.sh reset # restore everything bash lib/profile.sh reset # restore everything
NOTE: NOTE:
Plugin and MCP entries print advisory commands — they are NOT toggled "set" toggles the MANAGED items automatically, both ways: plugins
automatically. Run "claude plugin enable|disable" or "claude mcp add|remove" (ui-ux-pro-max, plugin-dev, pr-review-toolkit), external packs
yourself for those. (emil-design-eng, frontend-design, design-motion-principles, impeccable)
and the magic MCP. Anything outside those allowlists stays advisory —
run "claude plugin enable|disable" or "claude mcp add|remove" yourself.
EOF EOF
} }
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# lib/tests/profile-set-managed.test.sh — `set` symmetry on managed
# externals + MCPs, gstack on-demand, external from-source (BDR-079).
# Hermetic: fixture repo via *_REPO_OVERRIDE + fake `claude` on PATH.
set -u
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
pass=0; fail=0
check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1));
printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; }
FX="$(mktemp -d)"; trap 'rm -rf "$FX"' EXIT
mkdir -p "$FX/skills" "$FX/skills-disabled" "$FX/lib/profiles" "$FX/bin" \
"$FX/skills-external/emil-design-eng" "$FX/skills-external/other-ext"
for g in gs-a gs-b gs-c; do
mkdir -p "$FX/skills-external/gstack/$g"
touch "$FX/skills-external/gstack/$g/SKILL.md"
done
cp "$ROOT/lib/profile.sh" "$ROOT/lib/toggle-external.sh" "$FX/lib/"
printf 'MAGIC_API_KEY=test-secret-000\n' > "$FX/.env"
# Non-managed external, enabled from the start — must never be touched.
ln -s "$FX/skills-external/other-ext" "$FX/skills/other-ext"
cat > "$FX/lib/profiles/designish.profile" <<'EOF'
gs-a
gs-b
emil-design-eng external
magic mcp
EOF
cat > "$FX/lib/profiles/backendish.profile" <<'EOF'
gs-c
EOF
# Fake claude: logs every call; keeps MCP registry state in a flat file.
cat > "$FX/bin/claude" <<EOF
#!/usr/bin/env bash
FX="$FX"
echo "\$*" >> "\$FX/claude-calls.log"
case "\$1 \${2:-}" in
"mcp list") cat "\$FX/mcp-state" 2>/dev/null ;;
"mcp add") echo "magic: stub" > "\$FX/mcp-state" ;;
"mcp remove") : > "\$FX/mcp-state" ;;
esac
exit 0
EOF
chmod +x "$FX/bin/claude"
run() { PATH="$FX/bin:$PATH" PROFILE_REPO_OVERRIDE="$FX" \
TOGGLE_EXTERNAL_REPO_OVERRIDE="$FX" bash "$FX/lib/profile.sh" "$@"; }
# --- set designish: gstack on-demand + external from-source + magic on ---
run set designish >/dev/null 2>&1
check T1-gsa-on "$([ -e "$FX/skills/gs-a" ] && echo on || echo off)" on
check T2-gsb-on "$([ -e "$FX/skills/gs-b" ] && echo on || echo off)" on
check T3-gsc-off "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" off
check T4-emil-src "$([ -L "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
check T5-magic-on "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1
check T6-add-call "$(grep -c '^mcp add magic' "$FX/claude-calls.log")" 1
# --- set backendish: managed leftovers parked/unregistered ---
run set backendish >/dev/null 2>&1
check T7-gsc-on "$([ -e "$FX/skills/gs-c" ] && echo on || echo off)" on
check T8-gsa-park "$([ -e "$FX/skills-disabled/gstack__gs-a" ] && echo p || echo n)" p
check T9-emil-off "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" off
check T10-emil-park "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" p
check T11-magic-off "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null || true)" 0
check T12-rm-call "$(grep -c '^mcp remove magic' "$FX/claude-calls.log")" 1
check T13-other-untouched "$([ -e "$FX/skills/other-ext" ] && echo on || echo off)" on
# --- back to designish: parked external restored (not re-sourced) ---
run set designish >/dev/null 2>&1
check T14-emil-back "$([ -e "$FX/skills/emil-design-eng" ] && echo on || echo off)" on
check T15-park-gone "$([ -e "$FX/skills-disabled/emil-design-eng" ] && echo p || echo n)" n
check T16-magic-back "$(grep -c '^magic:' "$FX/mcp-state" 2>/dev/null)" 1
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
+15 -3
View File
@@ -61,6 +61,15 @@ protected — `set` will refuse to disable them even if the profile omits them.
**Managed plugins** that `set` may disable when not in profile: **Managed plugins** that `set` may disable when not in profile:
`ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`, `ui-ux-pro-max@ui-ux-pro-max-skill`, `plugin-dev@claude-code-plugins`,
`pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled. `pr-review-toolkit@claude-code-plugins`. Other plugins are never auto-toggled.
**Managed externals** (`emil-design-eng`, `frontend-design`,
`design-motion-principles`, `impeccable`) and **managed MCPs** (`magic`)
follow the same symmetry (BDR-079): `set` enables them when the profile
lists them (from parked state, or from `skills-external/` if the symlink
never existed) and parks/unregisters them when it does not — e.g. `set
backend` after design work turns emil and magic off. `darwin-skill` and any
other unlisted external are never auto-touched. gstack works the same
all the way down: a profile listing gstack skills while the whole pack is
off (via `toggle-external.sh`) re-enables JUST those skills on demand.
## Commands ## Commands
@@ -117,8 +126,11 @@ bash "$HOME/.claude/lib/profile.sh" $ARGUMENTS
update-check, learnings — script doesn't touch that infra. Disabled skills update-check, learnings — script doesn't touch that infra. Disabled skills
are just hidden from Claude Code's scanner; the gstack repo stays installed. are just hidden from Claude Code's scanner; the gstack repo stays installed.
- Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max, - Profile changes DO toggle the managed Claude Code plugins (ui-ux-pro-max,
plugin-dev, pr-review-toolkit) and the `magic` MCP — see the Mechanism table plugin-dev, pr-review-toolkit), the managed external packs (emil-design-eng,
above (BDR-008). Anything outside that managed set stays manual: frontend-design, design-motion-principles, impeccable) and the `magic` MCP —
`claude plugin enable|disable`, `claude mcp add|remove`. in BOTH directions: `set` enables what the profile lists and disables the
managed leftovers it doesn't (BDR-008, BDR-079). Anything outside those
allowlists stays manual: `claude plugin enable|disable`, `claude mcp
add|remove`.
- `set` is destructive in the sense that it disables non-listed gstack skills. - `set` is destructive in the sense that it disables non-listed gstack skills.
Use `apply` if the user wants additive behavior. Use `apply` if the user wants additive behavior.
+1 -1
View File
@@ -1 +1 @@
1.2.0 1.3.0