diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef23a58..13104bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ 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 (which fires 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 to. **Fix:** the SessionStart entry in `templates/commands/microbit-enforcer/settings-patch.json` now carries `"matcher": "startup|clear"` (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 an explicit `/clear` and persist across `--resume`/`--continue` and compaction. New `test/microbit-enforcer/test-sessionstart-matcher.sh` asserts the merged settings carry the matcher (red against the unmatched form). `examples/python-uv-fastapi/.claude/settings.json` updated to match; the `templates/core/CLAUDE.md` gitignore-block note softened from "transient state cleared by SessionStart" to "transient session state," and the freeze/guard/careful `SKILL.md` lifecycle notes + the `microbit-enforcer.sh` header corrected to match — across both the templates and the `python-uv-fastapi` example mirror (markers clear on a fresh session or `/clear`, persist across `--resume`/compaction). 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 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 the manifest is one level down (`frontend/package.json` in a split frontend/backend layout) and adding a note when a `docker-compose.yml`/`Dockerfile` is present that the host-side format/check hooks call host tools and will no-op if the toolchain lives in the container. Warning-only; never blocks scaffolding; stays silent when the stack matches the root or when no command maps to a known manifest (bare `tsc`/`pytest`/`ruff`). Scoped to the three commands `stop-run-checks.sh` actually runs (typecheck/lint/test), so the warning never misattributes an install/build/dev skip to the Stop hook; command parsing is guarded so a malformed quote degrades to a plain split instead of crashing (same guard applied to the pre-existing `extract_first_binaries`). 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, containerized-note, absent-everywhere, install-only-silent, malformed-quote-safe) + a malformed-quote case in `test-check-commands-extracted.sh`. 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.
- **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/configure.py b/configure.py
index 968b2d8..a9d4117 100755
--- a/configure.py
+++ b/configure.py
@@ -944,6 +944,23 @@ def check_settings_validates(settings: dict) -> list:
)
+# Maps a check-command's first binary to the stack manifest that signals
+# "this stack is scaffolded at this directory." Mirrors manifest_for() in
+# templates/git-workflow/hooks/stop-run-checks.sh — keep the two in sync.
+# Values are a subset of KNOWN_STACK_MANIFESTS.
+BINARY_TO_MANIFEST = {
+ "pnpm": "package.json", "npm": "package.json",
+ "yarn": "package.json", "bun": "package.json",
+ "uv": "pyproject.toml", "poetry": "pyproject.toml",
+ "pip": "pyproject.toml", "pip3": "pyproject.toml",
+ "cargo": "Cargo.toml", "rustc": "Cargo.toml",
+ "go": "go.mod",
+ "bundle": "Gemfile", "gem": "Gemfile",
+ "mvn": "pom.xml",
+ "gradle": "build.gradle", "./gradlew": "build.gradle",
+}
+
+
def detect_stack_manifests(target_dir: Path) -> list:
"""Return sorted list of known stack-manifest filenames present at target_dir.
@@ -967,12 +984,95 @@ def extract_first_binaries(typecheck: str = None, lint: str = None, test: str =
for kind, cmd in (("typecheck", typecheck), ("lint", lint), ("test", test)):
if not cmd or not cmd.strip():
continue
- parts = shlex.split(cmd)
+ try:
+ parts = shlex.split(cmd)
+ except ValueError:
+ parts = cmd.split() # unbalanced quote etc. — degrade, don't crash
if parts:
out[kind] = parts[0]
return out
+def check_stack_reality(target_dir, form_values: dict) -> list:
+ """Warn when a configured check command's stack manifest is missing from the
+ project root — the exact condition that makes the Stop hook
+ (stop-run-checks.sh, skip rule 3) silently skip that check at runtime.
+
+ For each missing manifest, note whether it exists one directory level down
+ (monorepo / split frontend-backend layout) so the user knows the toolchain
+ is present but the root-anchored hook won't find it. When the project also
+ looks containerized (a compose file or root Dockerfile is present), append a
+ note that the host-side format/check hooks call host tools and will no-op if
+ the real toolchain lives in the container.
+
+ Warning-only; never blocks scaffolding. Returns a list of strings for the
+ caller to render under a [ STACK WARNINGS ] block. Silent when every
+ configured command's manifest is present at the root, or when no configured
+ command maps to a known manifest (e.g. bare tsc / pytest / ruff)."""
+ import shlex
+ fv = form_values or {}
+ # Only the three commands the Stop hook (stop-run-checks.sh) actually runs —
+ # naming install/build/dev here would misattribute the runtime skip to it.
+ bins = set()
+ for key in ("cmd_typecheck", "cmd_lint", "cmd_test"):
+ val = fv.get(key)
+ if isinstance(val, str) and val.strip():
+ try:
+ parts = shlex.split(val)
+ except ValueError:
+ parts = val.split() # unbalanced quote etc. — degrade, don't crash
+ if parts:
+ bins.add(parts[0])
+
+ wanted = {} # manifest -> set of configured binaries that imply it
+ for b in bins:
+ manifest = BINARY_TO_MANIFEST.get(b)
+ if manifest:
+ wanted.setdefault(manifest, set()).add(b)
+ if not wanted:
+ return []
+
+ try:
+ subdirs = [p for p in target_dir.iterdir()
+ if p.is_dir() and not p.name.startswith(".")]
+ except OSError:
+ subdirs = []
+
+ warnings = []
+ for manifest in sorted(wanted):
+ if (target_dir / manifest).exists():
+ continue # configured stack matches the repo root — fine
+ bins_str = "/".join(sorted(wanted[manifest]))
+ found_in = sorted(p.name for p in subdirs if (p / manifest).exists())
+ if found_in:
+ where = ", ".join(d + "/" for d in found_in[:3])
+ warnings.append(
+ "configured `{b}` checks but no {m} at the project root "
+ "(found in {w}). stop-run-checks.sh runs from the root and "
+ "will silently skip these — point your cmd_* values at the "
+ "subdir (e.g. `cd {d} && ...`) or scaffold it separately.".format(
+ b=bins_str, m=manifest, w=where, d=found_in[0]))
+ else:
+ warnings.append(
+ "configured `{b}` checks but no {m} anywhere in the tree — "
+ "these checks can't run here. Fix the stack in "
+ ".claude-config.json or re-run cc-configure.".format(
+ b=bins_str, m=manifest))
+
+ if warnings:
+ container = next(
+ (n for n in ("docker-compose.yml", "docker-compose.yaml",
+ "compose.yml", "compose.yaml", "Dockerfile")
+ if (target_dir / n).exists()), None)
+ if container:
+ warnings.append(
+ "detected {c} — if your toolchain runs in containers, the "
+ "format-on-write and stop-run-checks hooks call host tools and "
+ "will no-op. Point cmd_* at `docker compose exec ...`.".format(
+ c=container))
+ return warnings
+
+
def _configurator_sha():
"""Short git SHA of the configurator's OWN checkout (REPO_ROOT), or None
when unavailable (not a git repo, git missing, timeout, any error).
@@ -2612,6 +2712,17 @@ def main():
for w in env_warnings:
print(f" {yellow('!')} {w}")
+ # Surface a configured check stack that doesn't exist at the repo root —
+ # the runtime condition that makes stop-run-checks.sh silently self-disable.
+ stack_warnings = check_stack_reality(target_dir, config["formValues"])
+ if stack_warnings:
+ print()
+ print(bold(yellow("[ STACK WARNINGS ]")))
+ for w in stack_warnings:
+ print(f" {yellow('!')} {w}")
+ print(dim(" stop-run-checks.sh skips any check whose stack manifest is absent at the"))
+ print(dim(" project root, so a mismatched stack disables the typecheck/lint/test loop."))
+
# Surface pre-existing design docs (typical output of a prior superpowers
# brainstorming session). Informational, not a warning — the install is
# fine; we just want to nudge the user to fold their design into CLAUDE.md
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/cc-manifest/test-check-commands-extracted.sh b/test/cc-manifest/test-check-commands-extracted.sh
index b378114..09ef368 100755
--- a/test/cc-manifest/test-check-commands-extracted.sh
+++ b/test/cc-manifest/test-check-commands-extracted.sh
@@ -37,4 +37,14 @@ print(json.dumps(extract_first_binaries(typecheck=' ', lint='ruff check', test
expected='{"lint": "ruff"}'
[ "$result" = "$expected" ] || { echo "FAIL: whitespace not skipped: $result != $expected"; exit 1; }
-echo "PASS: extract_first_binaries pulls first token, skips empty/None/whitespace"
+# Malformed quote must not crash (B1): degrades to a plain split, first token kept
+result=$(python3 -c "
+import sys; sys.path.insert(0, '$proj_root')
+from configure import extract_first_binaries
+import json
+print(json.dumps(extract_first_binaries(test=\"pnpm test --grep 'foo\")))
+")
+expected='{"test": "pnpm"}'
+[ "$result" = "$expected" ] || { echo "FAIL: malformed quote should degrade, not crash: $result != $expected"; exit 1; }
+
+echo "PASS: extract_first_binaries pulls first token, skips empty/None/whitespace, survives malformed quotes"
diff --git a/test/cc-manifest/test-stack-reality-preflight.sh b/test/cc-manifest/test-stack-reality-preflight.sh
new file mode 100755
index 0000000..fdb98ab
--- /dev/null
+++ b/test/cc-manifest/test-stack-reality-preflight.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# check_stack_reality() (dogfood F2) warns when a configured check command's
+# stack manifest is absent from the project root — the same condition that makes
+# stop-run-checks.sh silently skip at runtime (skip rule 3). It must:
+# 1. stay SILENT when the manifest is present at root (matched stack)
+# 2. stay SILENT when no configured command maps to a known manifest
+# 3. WARN + name the subdir when the manifest lives one level down (monorepo)
+# 4. add a container note when a compose/Dockerfile is present
+# 5. WARN "can't run here" when the manifest is absent everywhere
+set -euo pipefail
+
+proj_root=$(pwd)
+tmp=$(mktemp -d)
+trap "rm -rf $tmp" EXIT
+
+# Pass DIR + form-values JSON via env to dodge nested-quote hell.
+run() { # $1 = dir ; $2 = form_values JSON
+ DIR="$1" FV="$2" python3 -c "
+import os, json, sys; sys.path.insert(0, '$proj_root')
+from configure import check_stack_reality
+from pathlib import Path
+print(json.dumps(check_stack_reality(Path(os.environ['DIR']), json.loads(os.environ['FV']))))
+"
+}
+
+# 1. Matched stack (pnpm + root package.json) -> silent
+d="$tmp/matched"; mkdir -p "$d"; touch "$d/package.json"
+out=$(run "$d" '{"cmd_test":"pnpm test","cmd_lint":"pnpm lint"}')
+[ "$out" = "[]" ] || { echo "FAIL: matched stack should be silent; got: $out"; exit 1; }
+
+# 2. No mappable binary (bare tsc / pytest) -> silent
+d="$tmp/nomap"; mkdir -p "$d"
+out=$(run "$d" '{"cmd_typecheck":"tsc --noEmit","cmd_test":"pytest"}')
+[ "$out" = "[]" ] || { echo "FAIL: unmappable binaries should be silent; got: $out"; exit 1; }
+
+# 3. Monorepo: pnpm configured, package.json only in frontend/ -> warn + name subdir
+d="$tmp/monorepo"; mkdir -p "$d/frontend"; touch "$d/frontend/package.json"
+out=$(run "$d" '{"cmd_test":"pnpm test"}')
+echo "$out" | grep -q "no package.json at the project root" \
+ || { echo "FAIL: monorepo should warn about missing root package.json; got: $out"; exit 1; }
+echo "$out" | grep -q "frontend/" \
+ || { echo "FAIL: monorepo warning should name the subdir frontend/; got: $out"; exit 1; }
+
+# 4. Containerized monorepo (the dogfood shape: pnpm config, no root package.json,
+# frontend/package.json, docker-compose.yml) -> warn + container no-op note
+d="$tmp/container"; mkdir -p "$d/frontend"
+touch "$d/frontend/package.json" "$d/docker-compose.yml"
+out=$(run "$d" '{"cmd_typecheck":"pnpm typecheck","cmd_lint":"pnpm lint","cmd_test":"pnpm test","cmd_install":"pnpm install"}')
+echo "$out" | grep -q "docker-compose.yml" \
+ || { echo "FAIL: containerized project should get the container no-op note; got: $out"; exit 1; }
+
+# 5. Manifest absent everywhere -> warn "can't run here"
+d="$tmp/absent"; mkdir -p "$d"
+out=$(run "$d" '{"cmd_test":"cargo test"}')
+echo "$out" | grep -q "anywhere in the tree" \
+ || { echo "FAIL: fully-absent manifest should warn can't-run; got: $out"; exit 1; }
+
+# 6. cmd_install/build/dev are NOT scanned (A1: stop-run-checks.sh runs only
+# typecheck/lint/test) — a missing manifest for install alone stays silent.
+d="$tmp/installonly"; mkdir -p "$d"
+out=$(run "$d" '{"cmd_install":"pnpm install"}')
+[ "$out" = "[]" ] || { echo "FAIL: cmd_install alone must not warn (stop-run-checks never runs it); got: $out"; exit 1; }
+
+# 7. Malformed quote in a check command must NOT crash (B1: shlex guard) —
+# it degrades to a plain split and still extracts the first binary.
+d="$tmp/badquote"; mkdir -p "$d/frontend"; touch "$d/frontend/package.json"
+out=$(run "$d" "{\"cmd_test\":\"pnpm test --grep 'foo\"}")
+echo "$out" | grep -q "no package.json at the project root" \
+ || { echo "FAIL: malformed-quote cmd should degrade gracefully and still warn; got: $out"; exit 1; }
+
+echo "PASS: check_stack_reality warns on root-manifest mismatch, names subdirs, flags containers, scopes to stop-hook keys, survives malformed quotes, silent when matched"
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)"