diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef23a58..2aa6d9c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@ All notable changes to this project. Format: [Keep a Changelog](https://keepacha
## Unreleased
+- **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.
- **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..c462da3 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,102 @@ 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, which=None) -> 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 a warned toolchain
+ binary is also absent from the host PATH and the project looks containerized
+ (a compose file or root Dockerfile is present), append a note that the
+ host-side format/check hooks will no-op because the toolchain lives in the
+ container. `which` resolves a binary on the host PATH (defaults to
+ shutil.which; injectable for tests).
+
+ 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)."""
+ fv = form_values or {}
+ if which is None:
+ import shutil
+ which = shutil.which
+
+ # Reuse extract_first_binaries (shlex-guarded) for the three commands the
+ # Stop hook runs; install/build/dev aren't run by stop-run-checks.sh, so
+ # naming them here would misattribute the runtime skip to that hook.
+ bins = set(extract_first_binaries(
+ typecheck=fv.get("cmd_typecheck"),
+ lint=fv.get("cmd_lint"),
+ test=fv.get("cmd_test"),
+ ).values())
+
+ 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 = []
+ warned_bins = set()
+ for manifest in sorted(wanted):
+ if (target_dir / manifest).exists():
+ continue # configured stack matches the repo root — fine
+ warned_bins |= wanted[manifest]
+ 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))
+
+ # Container note only when a warned toolchain binary isn't on the host PATH —
+ # then a compose/Dockerfile means it likely lives in the container and the
+ # host-side hooks no-op. If the binary IS on the host (e.g. a monorepo whose
+ # compose file is only for backing services), the hooks run fine.
+ if warnings and any(which(b) is None for b in warned_bins):
+ 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 +2719,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/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..e1a57f4
--- /dev/null
+++ b/test/cc-manifest/test-stack-reality-preflight.sh
@@ -0,0 +1,90 @@
+#!/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 ONLY when a warned binary is absent from the host
+# PATH (toolchain lives in the container) and a compose/Dockerfile is present
+# 4b. SUPPRESS the container note when the binary IS on the host (monorepo whose
+# compose file is only for backing services — the hooks run fine)
+# 5. WARN "can't run here" when the manifest is absent everywhere
+# 6. ignore cmd_install/build/dev (stop-run-checks runs only typecheck/lint/test)
+# 7. survive a malformed quote in a check command (shlex guard)
+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. $3 picks the
+# host-PATH stub: none = nothing installed, all = everything installed, else real.
+run() { # $1 = dir ; $2 = form_values JSON ; $3 = which mode (none|all|real)
+ DIR="$1" FV="$2" WHICH="${3:-real}" python3 -c "
+import os, json, sys; sys.path.insert(0, '$proj_root')
+from configure import check_stack_reality
+from pathlib import Path
+wm = os.environ['WHICH']
+if wm == 'none':
+ which = lambda b: None # nothing on the host PATH
+elif wm == 'all':
+ which = lambda b: '/usr/bin/' + b # everything on the host PATH
+else:
+ which = None # real shutil.which
+print(json.dumps(check_stack_reality(Path(os.environ['DIR']), json.loads(os.environ['FV']), which=which)))
+"
+}
+
+# 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"}' all)
+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 + toolchain ABSENT from host (which=none) -> warn + container 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"}' none)
+echo "$out" | grep -q "docker compose exec" \
+ || { echo "FAIL: containerized + toolchain-off-host should get the container note; got: $out"; exit 1; }
+
+# 4b. A1: same layout but toolchain IS on host (which=all) -> subdir warn, NO container note
+out=$(run "$d" '{"cmd_typecheck":"pnpm typecheck","cmd_lint":"pnpm lint","cmd_test":"pnpm test"}' all)
+echo "$out" | grep -q "no package.json at the project root" \
+ || { echo "FAIL: subdir warning should still fire when toolchain on host; got: $out"; exit 1; }
+echo "$out" | grep -q "docker compose exec" \
+ && { echo "FAIL: container note must be SUPPRESSED when the binary is on the host PATH; got: $out"; exit 1; } || true
+
+# 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 (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 (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\"}" all)
+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 — root-mismatch warns, names subdirs, container note gated on host-PATH absence, stop-hook-key scope, survives malformed quotes, silent when matched"