Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. **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.

- **feat(preflight): `[ STACK WARNINGS ]` — flag a configured check stack that doesn't exist at the repo root (dogfood F2).** A user who keeps the default Node/pnpm intake answers (or otherwise configures `pnpm`/`uv`/`cargo`/… check commands) on a project whose manifest lives in a subdir or behind a container got **zero feedback** that the scaffolded Stop hook would never run: `stop-run-checks.sh` skips any check whose stack manifest is absent at the root (skip rule 3), so the whole typecheck/lint/test loop self-disables silently. New `check_stack_reality(target_dir, form_values)` preflight maps each configured check command's (typecheck/lint/test) first-binary — via the shlex-guarded `extract_first_binaries` — to its stack manifest (new `BINARY_TO_MANIFEST`, mirroring `manifest_for()` in `stop-run-checks.sh`) and warns when that manifest is missing from the project root, naming the subdir when it's one level down (`frontend/package.json`). When a warned binary is **also absent from the host PATH** and a `docker-compose.yml`/`Dockerfile` is present, it appends a note that the host-side format/check hooks will no-op because the toolchain lives in the container — host-PATH-gated, so a monorepo whose compose file only backs services (host toolchain present) doesn't get a false "no-op" warning. Warning-only; never blocks scaffolding; stays silent when the stack matches the root or no command maps to a known manifest (bare `tsc`/`pytest`/`ruff`). Rendered as a `[ STACK WARNINGS ]` block alongside the existing preflights. New `test/cc-manifest/test-stack-reality-preflight.sh` (matched-silent, unmappable-silent, monorepo-names-subdir, container-note-when-toolchain-off-host, container-note-suppressed-when-on-host, absent-everywhere, install-only-silent, malformed-quote-safe) + a malformed-quote case in `test-check-commands-extracted.sh`. Split out of the originally-bundled #87 per `CONTRIBUTING.md` (one logical change per PR); F1 (the SessionStart matcher fix) ships separately. Surfaced by dogfooding the configurator into a Python + FastAPI + Vite project that runs entirely via Docker.

- **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.
Expand Down
26 changes: 26 additions & 0 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions examples/python-uv-fastapi/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
],
"SessionStart": [
{
"matcher": "startup|clear",
"hooks": [
{
"type": "command",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ Currently careful:

## Lifecycle

Careful patterns persist for the session, cleared on session start.
Careful patterns persist for the sessioncleared on a fresh session (`startup`) or `/clear`, but preserved across `--resume`/`--continue` and compaction.
7 changes: 4 additions & 3 deletions examples/python-uv-fastapi/.claude/skills/freeze/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions examples/python-uv-fastapi/.claude/skills/guard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion examples/python-uv-fastapi/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion templates/commands/careful/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ Currently careful:

## Lifecycle

Careful patterns persist for the session, cleared on session start.
Careful patterns persist for the sessioncleared on a fresh session (`startup`) or `/clear`, but preserved across `--resume`/`--continue` and compaction.
7 changes: 4 additions & 3 deletions templates/commands/freeze/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions templates/commands/guard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions templates/commands/microbit-enforcer/microbit-enforcer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion templates/commands/microbit-enforcer/settings-patch.json
Original file line number Diff line number Diff line change
@@ -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": [
{
Expand All @@ -15,6 +15,7 @@
],
"SessionStart": [
{
"matcher": "startup|clear",
"hooks": [
{
"type": "command",
Expand Down
2 changes: 1 addition & 1 deletion templates/core/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions test/microbit-enforcer/test-sessionstart-matcher.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/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

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", [])
# 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)

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}")

print("PASS: microbit-enforcer SessionStart marker-clear is scoped to startup|clear (resume/compact preserve markers)")
EOF
Loading