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

- **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 `<dir>:<name>` 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.
Expand Down
120 changes: 119 additions & 1 deletion configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 <svc> ...`.".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).
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion test/cc-manifest/test-check-commands-extracted.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
90 changes: 90 additions & 0 deletions test/cc-manifest/test-stack-reality-preflight.sh
Original file line number Diff line number Diff line change
@@ -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"