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