From 4fed44d3d61965410f764e57246b6b707464cfc5 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 26 Jun 2026 16:16:55 -0400 Subject: [PATCH 1/2] fix(commands): scope microbit-enforcer SessionStart marker-clear to startup|clear (dogfood F1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SessionStart hook that clears the freeze/guard/careful markers shipped with no matcher, firing on every source — including resume and compact — so a /freeze was silently lost mid-session after compaction or --resume. Now "matcher": "startup|clear": markers clear only on a fresh session or /clear and survive --resume and compaction. Sources verified against code.claude.com/docs/en/hooks-guide (startup, resume, clear, compact; matcher filters on source). The freeze/guard/careful SKILL.md notes, the microbit-enforcer.sh header, and the templates/core/CLAUDE.md gitignore note are corrected to match, across both the templates and the python-uv-fastapi example mirror. New test/microbit-enforcer/test-sessionstart-matcher.sh. Split from the originally-bundled PR #87 per CONTRIBUTING.md (one logical change per PR); F2 (stack-reality preflight) ships as a separate PR. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 + .../.claude/hooks/microbit-enforcer.sh | 5 ++- .../python-uv-fastapi/.claude/settings.json | 1 + .../.claude/skills/careful/SKILL.md | 2 +- .../.claude/skills/freeze/SKILL.md | 7 ++-- .../.claude/skills/guard/SKILL.md | 5 ++- examples/python-uv-fastapi/CLAUDE.md | 2 +- templates/commands/careful/SKILL.md | 2 +- templates/commands/freeze/SKILL.md | 7 ++-- templates/commands/guard/SKILL.md | 5 ++- .../microbit-enforcer/microbit-enforcer.sh | 5 ++- .../microbit-enforcer/settings-patch.json | 3 +- templates/core/CLAUDE.md | 2 +- .../test-sessionstart-matcher.sh | 39 +++++++++++++++++++ 14 files changed, 68 insertions(+), 19 deletions(-) create mode 100755 test/microbit-enforcer/test-sessionstart-matcher.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ef23a58..2baa6aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project. Format: [Keep a Changelog](https://keepacha ## Unreleased +- **fix(commands): scope the microbit-enforcer SessionStart marker-clear to fresh-slate sources so a long session keeps its freezes (dogfood F1).** The `freeze`/`guard`/`careful` micro-behaviors drop `.claude/.frozen` / `.guarded` / `.careful` markers that the `microbit-enforcer.sh` PreToolUse hook reads to reject Write/Edit. The companion SessionStart hook that clears those markers shipped with **no `matcher`**, so it fired on *every* SessionStart source — including `resume` and `compact`, not just `startup`. Net effect: in a long session you `/freeze` a file, context compacts (a SessionStart with `source: compact`) or you `--resume`, and the marker is silently deleted mid-task — the enforcer stops protecting the file exactly when you'd most want it. **Fix:** the SessionStart entry in `templates/commands/microbit-enforcer/settings-patch.json` now carries `"matcher": "startup|clear"` (sources verified against `code.claude.com/docs/en/hooks-guide` — SessionStart sources are `startup`, `resume`, `clear`, `compact`, and the matcher filters on source), so markers clear only on a brand-new session or `/clear` and persist across `--resume`/`--continue` and compaction. The freeze/guard/careful `SKILL.md` lifecycle notes, the `microbit-enforcer.sh` header, and the `templates/core/CLAUDE.md` gitignore-block note are corrected to match — across both the templates and the `python-uv-fastapi` example mirror. New `test/microbit-enforcer/test-sessionstart-matcher.sh` asserts the merged settings carry the matcher (red against the unmatched form). Surfaced by dogfooding the configurator into a containerized project. + - **chore(skills): sync discipline-skills v5.1.0 → v6.0.2 (obra/superpowers).** First sync since the module landed (PR #59, 2026-05-24); upstream shipped **v6.0.0** on 2026-06-16 (major), then **v6.0.1** + **v6.0.2** on 2026-06-17 as fast-follow patch releases. Skill inventory unchanged (14/14 upstream; we still fork the same 7). **Structural change in `subagent-driven-development`**: the two per-task reviewer prompts upstream removed (`spec-reviewer-prompt.md` + `code-quality-reviewer-prompt.md`) are replaced by a single `task-reviewer-prompt.md` that returns spec-compliance + quality verdicts in one pass; two new bash scripts (`scripts/review-package` + `scripts/task-brief`) move the review diff and task text to files so they don't park in the controller's context (file-handoff, not paste-handoff). Our v5-era "embed code-reviewer.md inline in code-quality-reviewer-prompt.md" SYNC.md edit is RETIRED — the new `task-reviewer-prompt.md` has no external dependency. **Configurator change**: `configure.py`'s `executable` heuristic widened from `rel.endswith(".sh")` to also honor the source file's user-execute bit, so the new `scripts/review-package` and `scripts/task-brief` (no `.sh` extension upstream) arrive at user installs with `+x` set. Other ports are mostly clean re-applies: `using-git-worktrees` + `finishing-a-development-branch` inherit the v6 drop of the global `~/.config/superpowers/worktrees/` path (project-local `.worktrees/` only) and `finishing-a-development-branch`'s forge-neutral rewrite (no hardcoded `gh pr create`); `writing-plans` gains v6's Global Constraints + per-task Interfaces template sections; `brainstorming`'s checklist drops the "offer visual companion just-in-time" step (the v6 process-flow digraph no longer carries the visual-companion nodes either); `executing-plans` re-applies the "This skill works much better" reframe (project-neutral). `superpowers:` prefix re-stripped across all SKILL.md and `*-prompt.md` files — `test-no-superpowers-prefix.sh` clean. Path list in `config_schema.py` updated (removed 2, added 3 → 15 total); test fixtures (`test-module-files-exist.sh` + `test-scaffold-installs-skills.sh`) updated to match. SYNC.md pin bumped to v6.0.2 with a fresh delta paragraph and revised canonical-edit list (5 items → 4; the retired embed-template item documented as removed). README's discipline-skills paragraphs refreshed (v5.1.0 → v6.0.2; carve-out section + module description). Known papercut documented: `subagent-driven-development/SKILL.md` retains one "final whole-branch review" line referencing `../requesting-code-review/code-reviewer.md` (a skill we don't ship) — pre-existing in v5.1.0, not addressed in this sync to keep port scope tight. - **chore(compat): CC 2.1.176–2.1.181 survey — held subagent-nesting wording lifted; no `tested_up_to` bump.** SchemaStore sync PR #5723 still open/draft (untouched since 2026-05-24 despite dep #5728 merged 2026-06-01), so `tested_up_to` stays pinned at 2.1.150 per the no-lone-bumps rule. **Held constraint resolved:** v2.7.0's 2.1.172–175 survey held the subagent-nesting wording on a CHANGELOG/docs contradiction (CHANGELOG said subagents can nest ≤5 levels; live docs said they cannot). The contradiction is now reconciled — `code.claude.com/docs/en/sub-agents` documents nested subagents under `min-version: 2.1.172`, and CC 2.1.181 explicitly unifies the foreground+background depth cap at 5 levels (*"Fixed foreground subagents spawning unbounded nested chains; they now respect the same 5-level depth limit as background subagents"*). Three shipped-template wording sites updated: `docs/04-subagents-mcp-orchestration.md` lines 59 + 140 and `templates/commands/infinite/SKILL.md` line 65. Leaf-design guidance preserved and remains the recommended pattern within the testing envelope (`tested_up_to < 2.1.172` means most users on the tested band don't have nesting; the `/infinite` skill's no-sibling-coordination invariant still holds because subagent contexts don't share even when nesting is available). **2.1.176–181 versions surveyed** (2.1.177 + 2.1.180 not present in CHANGELOG — skipped or internal-only per the verbatim-pull discipline): no new in-territory `settings.json` keys; out-of-territory by precedent — `footerLinksRegexes` + `language` (2.1.176, UI-pref tier), `sandbox.allowAppleEvents` (2.1.181, macOS-only opt-in). Future opt-in stub candidate: the new `Tool(param:value)` permission-rule form with `*` wildcard (2.1.178, e.g. `Agent(model:opus)`) — no shipped template uses it yet, SchemaStore acceptance status unverified, parking for the next compat batch. Nested `.claude/skills` collision handling (2.1.178) gains `:` namespacing — reinforces the existing plugin-skill carve-out pattern, no shipped-template impact. MCP server-level specs in subagent `disallowedTools` (`mcp__server`, `mcp__server__*`, `mcp__*`) silently-ignored bug fixed in 2.1.178 — grep against shipped templates clean. Compat comment refreshed in `config_schema.py` with the full survey. diff --git a/examples/python-uv-fastapi/.claude/hooks/microbit-enforcer.sh b/examples/python-uv-fastapi/.claude/hooks/microbit-enforcer.sh index 72bd02e..111bc1f 100755 --- a/examples/python-uv-fastapi/.claude/hooks/microbit-enforcer.sh +++ b/examples/python-uv-fastapi/.claude/hooks/microbit-enforcer.sh @@ -12,8 +12,9 @@ # .claude/.careful — newline-separated globs; prompt before match # # Lifecycle: a SessionStart hook (registered alongside this hook by the -# configurator's settings-patch) clears all three files. Markers are -# session-scoped, not persistent. +# configurator's settings-patch, matcher startup|clear) clears all three +# files on a fresh session or /clear. Markers are session-scoped but +# survive --resume and compaction, so a long session keeps its markers. set -euo pipefail diff --git a/examples/python-uv-fastapi/.claude/settings.json b/examples/python-uv-fastapi/.claude/settings.json index c9f8291..04d7b9c 100644 --- a/examples/python-uv-fastapi/.claude/settings.json +++ b/examples/python-uv-fastapi/.claude/settings.json @@ -130,6 +130,7 @@ ], "SessionStart": [ { + "matcher": "startup|clear", "hooks": [ { "type": "command", diff --git a/examples/python-uv-fastapi/.claude/skills/careful/SKILL.md b/examples/python-uv-fastapi/.claude/skills/careful/SKILL.md index e3150fa..5f947a8 100644 --- a/examples/python-uv-fastapi/.claude/skills/careful/SKILL.md +++ b/examples/python-uv-fastapi/.claude/skills/careful/SKILL.md @@ -42,4 +42,4 @@ Currently careful: ## Lifecycle -Careful patterns persist for the session, cleared on session start. +Careful patterns persist for the session — cleared on a fresh session (`startup`) or `/clear`, but preserved across `--resume`/`--continue` and compaction. diff --git a/examples/python-uv-fastapi/.claude/skills/freeze/SKILL.md b/examples/python-uv-fastapi/.claude/skills/freeze/SKILL.md index 55c2dbc..d374660 100644 --- a/examples/python-uv-fastapi/.claude/skills/freeze/SKILL.md +++ b/examples/python-uv-fastapi/.claude/skills/freeze/SKILL.md @@ -34,8 +34,9 @@ plan, or investigate without code changes happening. ## Notes - Frozen state is per-project (the marker is in `.claude/`). -- Frozen state does **not** persist across Claude Code session - restarts: the `SessionStart` hook clears all microbit markers - (.frozen / .guarded / .careful). +- Frozen state is session-scoped: a fresh session (`startup`) or + `/clear` clears all microbit markers (.frozen / .guarded / .careful), + but it **survives** `--resume`/`--continue` and compaction — so a long + session keeps its freeze. Run `/unfreeze` to lift it deliberately. - Read tools (Read, Grep, Glob, Bash with read-only commands) are unaffected. diff --git a/examples/python-uv-fastapi/.claude/skills/guard/SKILL.md b/examples/python-uv-fastapi/.claude/skills/guard/SKILL.md index 1acd2d3..1d1ef89 100644 --- a/examples/python-uv-fastapi/.claude/skills/guard/SKILL.md +++ b/examples/python-uv-fastapi/.claude/skills/guard/SKILL.md @@ -38,8 +38,9 @@ Currently guarded: ## Lifecycle -Guards persist for the session. The configurator clears `.guarded` on -session start (SessionStart hook). Removing a single guarded pattern +Guards persist for the session. A fresh session (`startup`) or `/clear` +clears `.guarded`; it survives `--resume`/`--continue` and compaction. +Removing a single guarded pattern mid-session isn't supported in v2.1 — re-create the guard list by clearing it (delete `.claude/.guarded`) and re-running `/guard` for the patterns you still want. diff --git a/examples/python-uv-fastapi/CLAUDE.md b/examples/python-uv-fastapi/CLAUDE.md index 498401d..44023b8 100644 --- a/examples/python-uv-fastapi/CLAUDE.md +++ b/examples/python-uv-fastapi/CLAUDE.md @@ -70,7 +70,7 @@ over pasted code. Put path-scoped detail in .claude/rules/*.md instead. The cc-configure scaffold makes assumptions about what to track vs. ignore. Honor them when first-committing a new project (`git init` + first `git add`) or when restructuring `.gitignore`: - **Commit:** `CLAUDE.md`, `.claude/` (agents, hooks, skills, rules, settings.json, output-styles), `.mcp*.json`, `claude-ctx` if present, project source. -- **Gitignore by default:** `.claude/settings.local.json` (machine-local), `.claude/logs/`, `.claude/.frozen` / `.guarded` / `.careful` (transient state cleared by SessionStart), `.claude-config.json` (configurator working-state). cc-configure appends the `# --- Claude Code ---` block to `.gitignore` automatically — if you rewrite or replace `.gitignore`, preserve that block (or rerun `cc-configure --retrofit` to re-append it). +- **Gitignore by default:** `.claude/settings.local.json` (machine-local), `.claude/logs/`, `.claude/.frozen` / `.guarded` / `.careful` (transient session state), `.claude-config.json` (configurator working-state). cc-configure appends the `# --- Claude Code ---` block to `.gitignore` automatically — if you rewrite or replace `.gitignore`, preserve that block (or rerun `cc-configure --retrofit` to re-append it). - **Nested upstream clones** (vendored deps, fork wrappers) should be gitignored and keep their own `.git/` — don't try to nest two repos in one tree. List the subdir in `.gitignore` *above* the Claude Code block. ## Design features diff --git a/templates/commands/careful/SKILL.md b/templates/commands/careful/SKILL.md index e3150fa..5f947a8 100644 --- a/templates/commands/careful/SKILL.md +++ b/templates/commands/careful/SKILL.md @@ -42,4 +42,4 @@ Currently careful: ## Lifecycle -Careful patterns persist for the session, cleared on session start. +Careful patterns persist for the session — cleared on a fresh session (`startup`) or `/clear`, but preserved across `--resume`/`--continue` and compaction. diff --git a/templates/commands/freeze/SKILL.md b/templates/commands/freeze/SKILL.md index 55c2dbc..d374660 100644 --- a/templates/commands/freeze/SKILL.md +++ b/templates/commands/freeze/SKILL.md @@ -34,8 +34,9 @@ plan, or investigate without code changes happening. ## Notes - Frozen state is per-project (the marker is in `.claude/`). -- Frozen state does **not** persist across Claude Code session - restarts: the `SessionStart` hook clears all microbit markers - (.frozen / .guarded / .careful). +- Frozen state is session-scoped: a fresh session (`startup`) or + `/clear` clears all microbit markers (.frozen / .guarded / .careful), + but it **survives** `--resume`/`--continue` and compaction — so a long + session keeps its freeze. Run `/unfreeze` to lift it deliberately. - Read tools (Read, Grep, Glob, Bash with read-only commands) are unaffected. diff --git a/templates/commands/guard/SKILL.md b/templates/commands/guard/SKILL.md index 1acd2d3..1d1ef89 100644 --- a/templates/commands/guard/SKILL.md +++ b/templates/commands/guard/SKILL.md @@ -38,8 +38,9 @@ Currently guarded: ## Lifecycle -Guards persist for the session. The configurator clears `.guarded` on -session start (SessionStart hook). Removing a single guarded pattern +Guards persist for the session. A fresh session (`startup`) or `/clear` +clears `.guarded`; it survives `--resume`/`--continue` and compaction. +Removing a single guarded pattern mid-session isn't supported in v2.1 — re-create the guard list by clearing it (delete `.claude/.guarded`) and re-running `/guard` for the patterns you still want. diff --git a/templates/commands/microbit-enforcer/microbit-enforcer.sh b/templates/commands/microbit-enforcer/microbit-enforcer.sh index 72bd02e..111bc1f 100755 --- a/templates/commands/microbit-enforcer/microbit-enforcer.sh +++ b/templates/commands/microbit-enforcer/microbit-enforcer.sh @@ -12,8 +12,9 @@ # .claude/.careful — newline-separated globs; prompt before match # # Lifecycle: a SessionStart hook (registered alongside this hook by the -# configurator's settings-patch) clears all three files. Markers are -# session-scoped, not persistent. +# configurator's settings-patch, matcher startup|clear) clears all three +# files on a fresh session or /clear. Markers are session-scoped but +# survive --resume and compaction, so a long session keeps its markers. set -euo pipefail diff --git a/templates/commands/microbit-enforcer/settings-patch.json b/templates/commands/microbit-enforcer/settings-patch.json index c325b48..e99277d 100644 --- a/templates/commands/microbit-enforcer/settings-patch.json +++ b/templates/commands/microbit-enforcer/settings-patch.json @@ -1,5 +1,5 @@ { - "//": "Auto-installed alongside the freeze/unfreeze/guard/careful microbits when commands.subset is 'full' or 'rigorous'. PreToolUse rejects Write/Edit/NotebookEdit when a marker is present; SessionStart clears markers (session-scoped lifecycle).", + "//": "Auto-installed alongside the freeze/unfreeze/guard/careful microbits when commands.subset is 'full' or 'rigorous'. PreToolUse rejects Write/Edit/NotebookEdit when a marker is present; SessionStart (matcher startup|clear) clears markers only on a fresh session or /clear — resume and compaction preserve them so a long session keeps its freezes.", "hooks": { "PreToolUse": [ { @@ -15,6 +15,7 @@ ], "SessionStart": [ { + "matcher": "startup|clear", "hooks": [ { "type": "command", diff --git a/templates/core/CLAUDE.md b/templates/core/CLAUDE.md index 9887e15..e77b5ba 100644 --- a/templates/core/CLAUDE.md +++ b/templates/core/CLAUDE.md @@ -66,7 +66,7 @@ over pasted code. Put path-scoped detail in .claude/rules/*.md instead. The cc-configure scaffold makes assumptions about what to track vs. ignore. Honor them when first-committing a new project (`git init` + first `git add`) or when restructuring `.gitignore`: - **Commit:** `CLAUDE.md`, `.claude/` (agents, hooks, skills, rules, settings.json, output-styles), `.mcp*.json`, `claude-ctx` if present, project source. -- **Gitignore by default:** `.claude/settings.local.json` (machine-local), `.claude/logs/`, `.claude/.frozen` / `.guarded` / `.careful` (transient state cleared by SessionStart), `.claude-config.json` (configurator working-state). cc-configure appends the `# --- Claude Code ---` block to `.gitignore` automatically — if you rewrite or replace `.gitignore`, preserve that block (or rerun `cc-configure --retrofit` to re-append it). +- **Gitignore by default:** `.claude/settings.local.json` (machine-local), `.claude/logs/`, `.claude/.frozen` / `.guarded` / `.careful` (transient session state), `.claude-config.json` (configurator working-state). cc-configure appends the `# --- Claude Code ---` block to `.gitignore` automatically — if you rewrite or replace `.gitignore`, preserve that block (or rerun `cc-configure --retrofit` to re-append it). - **Nested upstream clones** (vendored deps, fork wrappers) should be gitignored and keep their own `.git/` — don't try to nest two repos in one tree. List the subdir in `.gitignore` *above* the Claude Code block. ## Design features diff --git a/test/microbit-enforcer/test-sessionstart-matcher.sh b/test/microbit-enforcer/test-sessionstart-matcher.sh new file mode 100755 index 0000000..eedcacd --- /dev/null +++ b/test/microbit-enforcer/test-sessionstart-matcher.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Regression (dogfood F1): the microbit-enforcer SessionStart marker-clear must +# be scoped to the fresh-slate sources (startup|clear) so that --resume and +# post-compaction SessionStart events do NOT wipe .frozen/.guarded/.careful +# mid-session. With no matcher it fires on every source incl. resume/compact, +# silently un-freezing files in long sessions (exactly when you'd want the +# freeze to hold). SessionStart sources per code.claude.com/docs/en/hooks-guide +# are: startup, resume, clear, compact — the matcher filters on source. +set -euo pipefail + +proj_root=$(pwd) + +result=$(python3 -c " +import sys, json; sys.path.insert(0, '$proj_root') +from configure import compute_merged_settings +s = compute_merged_settings({}, {'core', 'commands'}, {'commands': {'subset': 'full'}}) +ss = s.get('hooks', {}).get('SessionStart', []) +# The entry that clears the discipline markers (identified by the .frozen path). +entry = next((e for e in ss + if any('.frozen' in h.get('command', '') for h in e.get('hooks', []))), + None) +print(json.dumps({'found': entry is not None, + 'matcher': (entry or {}).get('matcher')})) +") + +found=$(printf '%s' "$result" | python3 -c "import json,sys;print(json.load(sys.stdin)['found'])") +matcher=$(printf '%s' "$result" | python3 -c "import json,sys;print(json.load(sys.stdin)['matcher'])") + +[ "$found" = "True" ] || { + echo "FAIL: no SessionStart marker-clear entry in commands(subset=full); got: $result" + exit 1 +} +[ "$matcher" = "startup|clear" ] || { + echo "FAIL: SessionStart marker-clear matcher must be 'startup|clear' (fresh-slate only," + echo " so resume/compact preserve markers); got: '$matcher'" + exit 1 +} + +echo "PASS: microbit-enforcer SessionStart marker-clear is scoped to startup|clear (resume/compact preserve markers)" From 72d06ab5637ee743029ab562570f2267c85680e5 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 26 Jun 2026 18:26:01 -0400 Subject: [PATCH 2/2] fix(retrofit): migrate the matcherless SessionStart marker-clear group on retrofit (PR #88 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing installs carry a matcherless SessionStart marker-clear group; _merge_hook_groups keys by matcher, so cc-configure --retrofit would append the new startup|clear group beside the old one and the stale matcherless group kept wiping markers on resume/compact — re-negating the freeze-loss fix on every upgrade (the same class as PR #73's standalone->bundled bug, for a matcher change). _merge_hook_groups now does a pre-pass: for each configurator command it collects the matcher(s) the new template places it under, then strips that command from any existing group whose matcher is not one of them (a stale placement). This preserves the deliberate same-command-under-multiple-matchers pattern (the mcp drift-check ships under both startup and resume — the no-duplication / self-heal tests still expect 4 SessionStart groups) and never touches a user's own command. New test/retrofit-hooks/test-sessionstart-matcher-migration.sh. Also aligns test-sessionstart-matcher.sh to the heredoc / sys.path '.' style used by the merge tests (review advisory). --check clean, 104/104 fixtures. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 +- configure.py | 26 ++++++++ .../test-sessionstart-matcher.sh | 37 ++++------- .../test-sessionstart-matcher-migration.sh | 65 +++++++++++++++++++ 4 files changed, 105 insertions(+), 25 deletions(-) create mode 100755 test/retrofit-hooks/test-sessionstart-matcher-migration.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 2baa6aa..d81f93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project. Format: [Keep a Changelog](https://keepacha ## Unreleased -- **fix(commands): scope the microbit-enforcer SessionStart marker-clear to fresh-slate sources so a long session keeps its freezes (dogfood F1).** The `freeze`/`guard`/`careful` micro-behaviors drop `.claude/.frozen` / `.guarded` / `.careful` markers that the `microbit-enforcer.sh` PreToolUse hook reads to reject Write/Edit. The companion SessionStart hook that clears those markers shipped with **no `matcher`**, so it fired on *every* SessionStart source — including `resume` and `compact`, not just `startup`. Net effect: in a long session you `/freeze` a file, context compacts (a SessionStart with `source: compact`) or you `--resume`, and the marker is silently deleted mid-task — the enforcer stops protecting the file exactly when you'd most want it. **Fix:** the SessionStart entry in `templates/commands/microbit-enforcer/settings-patch.json` now carries `"matcher": "startup|clear"` (sources verified against `code.claude.com/docs/en/hooks-guide` — SessionStart sources are `startup`, `resume`, `clear`, `compact`, and the matcher filters on source), so markers clear only on a brand-new session or `/clear` and persist across `--resume`/`--continue` and compaction. The freeze/guard/careful `SKILL.md` lifecycle notes, the `microbit-enforcer.sh` header, and the `templates/core/CLAUDE.md` gitignore-block note are corrected to match — across both the templates and the `python-uv-fastapi` example mirror. New `test/microbit-enforcer/test-sessionstart-matcher.sh` asserts the merged settings carry the matcher (red against the unmatched form). Surfaced by dogfooding the configurator into a containerized project. +- **fix(commands): scope the microbit-enforcer SessionStart marker-clear to fresh-slate sources so a long session keeps its freezes (dogfood F1).** The `freeze`/`guard`/`careful` micro-behaviors drop `.claude/.frozen` / `.guarded` / `.careful` markers that the `microbit-enforcer.sh` PreToolUse hook reads to reject Write/Edit. The companion SessionStart hook that clears those markers shipped with **no `matcher`**, so it fired on *every* SessionStart source — including `resume` and `compact`, not just `startup`. Net effect: in a long session you `/freeze` a file, context compacts (a SessionStart with `source: compact`) or you `--resume`, and the marker is silently deleted mid-task — the enforcer stops protecting the file exactly when you'd most want it. **Fix:** the SessionStart entry in `templates/commands/microbit-enforcer/settings-patch.json` now carries `"matcher": "startup|clear"` (sources verified against `code.claude.com/docs/en/hooks-guide` — SessionStart sources are `startup`, `resume`, `clear`, `compact`, and the matcher filters on source), so markers clear only on a brand-new session or `/clear` and persist across `--resume`/`--continue` and compaction. The freeze/guard/careful `SKILL.md` lifecycle notes, the `microbit-enforcer.sh` header, and the `templates/core/CLAUDE.md` gitignore-block note are corrected to match — across both the templates and the `python-uv-fastapi` example mirror. New `test/microbit-enforcer/test-sessionstart-matcher.sh` asserts the merged settings carry the matcher. **Retrofit migration:** existing installs carry a *matcherless* SessionStart marker-clear group, and `_merge_hook_groups` keys by matcher — so without handling, `cc-configure --retrofit` would append the new `startup|clear` group beside the old one and the stale matcherless group would keep wiping markers on resume/compact, re-negating the fix on every upgrade (the same class as PR #73's standalone→bundled hook bug, for a matcher *change*). The merge now drops a stale different-matcher group whose commands are a subset of the new group's (a pure configurator group, no user additions), so the command migrates once to the new matcher; a group that also holds user hooks isn't a subset and is left untouched. New `test/retrofit-hooks/test-sessionstart-matcher-migration.sh`. Surfaced by dogfooding the configurator into a containerized project. - **chore(skills): sync discipline-skills v5.1.0 → v6.0.2 (obra/superpowers).** First sync since the module landed (PR #59, 2026-05-24); upstream shipped **v6.0.0** on 2026-06-16 (major), then **v6.0.1** + **v6.0.2** on 2026-06-17 as fast-follow patch releases. Skill inventory unchanged (14/14 upstream; we still fork the same 7). **Structural change in `subagent-driven-development`**: the two per-task reviewer prompts upstream removed (`spec-reviewer-prompt.md` + `code-quality-reviewer-prompt.md`) are replaced by a single `task-reviewer-prompt.md` that returns spec-compliance + quality verdicts in one pass; two new bash scripts (`scripts/review-package` + `scripts/task-brief`) move the review diff and task text to files so they don't park in the controller's context (file-handoff, not paste-handoff). Our v5-era "embed code-reviewer.md inline in code-quality-reviewer-prompt.md" SYNC.md edit is RETIRED — the new `task-reviewer-prompt.md` has no external dependency. **Configurator change**: `configure.py`'s `executable` heuristic widened from `rel.endswith(".sh")` to also honor the source file's user-execute bit, so the new `scripts/review-package` and `scripts/task-brief` (no `.sh` extension upstream) arrive at user installs with `+x` set. Other ports are mostly clean re-applies: `using-git-worktrees` + `finishing-a-development-branch` inherit the v6 drop of the global `~/.config/superpowers/worktrees/` path (project-local `.worktrees/` only) and `finishing-a-development-branch`'s forge-neutral rewrite (no hardcoded `gh pr create`); `writing-plans` gains v6's Global Constraints + per-task Interfaces template sections; `brainstorming`'s checklist drops the "offer visual companion just-in-time" step (the v6 process-flow digraph no longer carries the visual-companion nodes either); `executing-plans` re-applies the "This skill works much better" reframe (project-neutral). `superpowers:` prefix re-stripped across all SKILL.md and `*-prompt.md` files — `test-no-superpowers-prefix.sh` clean. Path list in `config_schema.py` updated (removed 2, added 3 → 15 total); test fixtures (`test-module-files-exist.sh` + `test-scaffold-installs-skills.sh`) updated to match. SYNC.md pin bumped to v6.0.2 with a fresh delta paragraph and revised canonical-edit list (5 items → 4; the retired embed-template item documented as removed). README's discipline-skills paragraphs refreshed (v5.1.0 → v6.0.2; carve-out section + module description). Known papercut documented: `subagent-driven-development/SKILL.md` retains one "final whole-branch review" line referencing `../requesting-code-review/code-reviewer.md` (a skill we don't ship) — pre-existing in v5.1.0, not addressed in this sync to keep port scope tight. diff --git a/configure.py b/configure.py index 968b2d8..2a01d0a 100755 --- a/configure.py +++ b/configure.py @@ -1241,6 +1241,32 @@ def _clone(x): if gc not in out: # collapse exact whole-group dups (self-heal) out.append(gc) + # Matcher migration (F1 dogfood, 2026-06-26): a configurator command may + # change matcher between releases — e.g. the SessionStart marker-clear that + # shipped matcherless is now `startup|clear`. The matcher-keyed dedup below + # would then append a 2nd group and leave the stale one firing, re-negating + # the fix on `cc-configure --retrofit`. For each configurator command, + # collect the matcher(s) the NEW template places it under, then strip that + # command from any existing group whose matcher is NOT one of them (a stale + # placement). This preserves the deliberate same-command-under-multiple- + # matchers pattern (the mcp drift-check ships under both `startup` and + # `resume`) and never touches a user's own command (absent from new_groups). + new_matchers = {} + for ng in new_groups: + for c in _hook_commands(ng): + new_matchers.setdefault(c, set()).add(ng.get("matcher")) + for g in out: + gm = g.get("matcher") + hooks = g.get("hooks") + if isinstance(hooks, list): + g["hooks"] = [ + h for h in hooks + if not (isinstance(h, dict) + and h.get("command") in new_matchers + and gm not in new_matchers[h["command"]]) + ] + out = [g for g in out if g.get("hooks")] + groups_added = 0 commands_added = 0 for ng in new_groups: diff --git a/test/microbit-enforcer/test-sessionstart-matcher.sh b/test/microbit-enforcer/test-sessionstart-matcher.sh index eedcacd..fdfcbb9 100755 --- a/test/microbit-enforcer/test-sessionstart-matcher.sh +++ b/test/microbit-enforcer/test-sessionstart-matcher.sh @@ -8,32 +8,21 @@ # are: startup, resume, clear, compact — the matcher filters on source. set -euo pipefail -proj_root=$(pwd) - -result=$(python3 -c " -import sys, json; sys.path.insert(0, '$proj_root') +python3 - <<'EOF' +import sys +sys.path.insert(0, '.') from configure import compute_merged_settings -s = compute_merged_settings({}, {'core', 'commands'}, {'commands': {'subset': 'full'}}) -ss = s.get('hooks', {}).get('SessionStart', []) + +s = compute_merged_settings({}, {"core", "commands"}, {"commands": {"subset": "full"}}) +ss = s.get("hooks", {}).get("SessionStart", []) # The entry that clears the discipline markers (identified by the .frozen path). entry = next((e for e in ss - if any('.frozen' in h.get('command', '') for h in e.get('hooks', []))), - None) -print(json.dumps({'found': entry is not None, - 'matcher': (entry or {}).get('matcher')})) -") - -found=$(printf '%s' "$result" | python3 -c "import json,sys;print(json.load(sys.stdin)['found'])") -matcher=$(printf '%s' "$result" | python3 -c "import json,sys;print(json.load(sys.stdin)['matcher'])") + if any(".frozen" in h.get("command", "") for h in e.get("hooks", []))), None) -[ "$found" = "True" ] || { - echo "FAIL: no SessionStart marker-clear entry in commands(subset=full); got: $result" - exit 1 -} -[ "$matcher" = "startup|clear" ] || { - echo "FAIL: SessionStart marker-clear matcher must be 'startup|clear' (fresh-slate only," - echo " so resume/compact preserve markers); got: '$matcher'" - exit 1 -} +assert entry is not None, f"no SessionStart marker-clear entry in commands(subset=full): {ss}" +assert entry.get("matcher") == "startup|clear", ( + "SessionStart marker-clear matcher must be 'startup|clear' (fresh-slate only, " + f"so resume/compact preserve markers); got {entry.get('matcher')!r}") -echo "PASS: microbit-enforcer SessionStart marker-clear is scoped to startup|clear (resume/compact preserve markers)" +print("PASS: microbit-enforcer SessionStart marker-clear is scoped to startup|clear (resume/compact preserve markers)") +EOF diff --git a/test/retrofit-hooks/test-sessionstart-matcher-migration.sh b/test/retrofit-hooks/test-sessionstart-matcher-migration.sh new file mode 100755 index 0000000..83347dc --- /dev/null +++ b/test/retrofit-hooks/test-sessionstart-matcher-migration.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# F1 (dogfood 2026-06-26): a prior scaffold shipped the microbit-enforcer +# SessionStart marker-clear with NO matcher; the new release ships it with +# matcher "startup|clear". _merge_hook_groups keys by matcher, so without a +# migration None != "startup|clear" appends a 2nd group and leaves the old +# matcherless one firing on every source — re-negating the freeze-loss fix on +# every `cc-configure --retrofit`. deep_merge_settings must MIGRATE the matcher +# in place (collapse to one group), while preserving any user-added hook. +set -euo pipefail + +python3 - <<'EOF' +import sys +sys.path.insert(0, '.') +from configure import deep_merge_settings + +CLEAR = ('rm -f "$CLAUDE_PROJECT_DIR"/.claude/.frozen ' + '"$CLAUDE_PROJECT_DIR"/.claude/.guarded ' + '"$CLAUDE_PROJECT_DIR"/.claude/.careful || true') + +# existing = prior scaffold: SessionStart marker-clear with NO matcher +existing = {"hooks": {"SessionStart": [ + {"hooks": [{"type": "command", "command": CLEAR, "timeout": 5}]}, +]}} +# new = current release: same command, now scoped to startup|clear +new = {"hooks": {"SessionStart": [ + {"matcher": "startup|clear", "hooks": [{"type": "command", "command": CLEAR, "timeout": 5}]}, +]}} + +merged, _ = deep_merge_settings(existing, new) +ss = merged["hooks"]["SessionStart"] + +assert len(ss) == 1, f"matcher migration should collapse to one SessionStart group, got {len(ss)}: {ss}" +assert ss[0].get("matcher") == "startup|clear", \ + f"surviving group must carry the startup|clear matcher; got {ss[0].get('matcher')!r}: {ss}" +n_clear = sum(1 for g in ss for h in g.get("hooks", []) if ".frozen" in h.get("command", "")) +assert n_clear == 1, f"marker-clear command should appear exactly once, got {n_clear}: {ss}" + +# A matcherless group that ALSO holds a user command is not a pure configurator +# group (not a subset), so it must be left untouched — user hook preserved. +existing2 = {"hooks": {"SessionStart": [ + {"hooks": [ + {"type": "command", "command": CLEAR, "timeout": 5}, + {"type": "command", "command": "echo my-own-hook", "timeout": 5}, + ]}, +]}} +merged2, _ = deep_merge_settings(existing2, new) +ss2 = merged2["hooks"]["SessionStart"] +assert any("my-own-hook" in h.get("command", "") for g in ss2 for h in g.get("hooks", [])), \ + f"a user's own SessionStart hook must be preserved across retrofit: {ss2}" + +# Deliberate same-command-under-multiple-matchers (the mcp drift-check ships +# under both startup and resume) must survive — both are valid placements in +# the new template, so neither is treated as stale. +DRIFT = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/sessionstart-drift-check.sh' +two = {"hooks": {"SessionStart": [ + {"matcher": "startup", "hooks": [{"type": "command", "command": DRIFT}]}, + {"matcher": "resume", "hooks": [{"type": "command", "command": DRIFT}]}, +]}} +merged3, _ = deep_merge_settings(two, two) +matchers3 = sorted(g.get("matcher") for g in merged3["hooks"]["SessionStart"]) +assert matchers3 == ["resume", "startup"], \ + f"multi-matcher drift-check must keep both placements: {merged3['hooks']['SessionStart']}" + +print("PASS: matcherless SessionStart marker-clear migrates to startup|clear on retrofit (no ghost group); user hooks + deliberate multi-matcher groups preserved") +EOF