Review date: 2026-06-09. Scope: everything under plugin/, plus the
root-level install/marketplace files. Items ordered by severity.
Status: ALL SECTIONS RESOLVED (2026-06-09, three passes: A, then B, then C, re-reviewing the remaining sections after each pass). Final verification: test-hooks 30/30, test-wiki 19/19, test-scripts 24/24 (all three wired into tests/verify.sh), doctor.sh exit 0, hooks.json / marketplace.json valid JSON, every shell script passes bash -n. The only remaining decision is non-code and owner-only: choosing a license (C7 note). Per-item details below; resolution notes inline. A post-fix 7-angle code review of the full branch diff produced 9 findings (section D below) — all fixed in the same pass.
-
A1.
doctor.shfails on a fresh clone.plugin/scripts/doctor.sh:77still checkscommands/kickoff.md, removed ina0b92ff("remove /kickoff, make /kickoff-team the default"). Result:failed=1, exit 1 → README's "exits non-zero if the plugin install is broken" tells every new user their install is broken. The same loop is also missingbootstrap.md, which does exist. Fix the command list toinit kickoff-team kickoff-workflow nightly-run close-loop bootstrap. -
A2. Stale references to the removed
/kickoffcommand.plugin/commands/init.md:26— tells the user to run/smurf:kickoff "<goal>".plugin/commands/kickoff-workflow.md:2— "additive 3rd mode alongside /kickoff and /kickoff-team" (there are only two commands now).
-
A3. Stop hook clobbers the orchestrator's summary.
on-stop-summary.shwrites$RUN_DIR/summary.mdunconditionally (cat > …). In autonomous runsCLAUDE_RUN_TSpins both writers to the same directory, so the hook's generic template overwrites the richer summary the orchestrator is contractually required to write (orchestrator.md OUTPUT CONTRACT), which/smurf:nightly-runthen reads. Write to a different file (e.g.stop-summary.md) or skip whensummary.mdalready exists. -
A4.
autonomous-run.shbudget is resolved but never enforced.BUDGETis computed from policy (lines 59–67) and written tometa.txt, but theclaude -pinvocation passes no--max-budget-usd— the header comment ("--max-budget-usd is best-effort") implies it is passed. Either pass the flag or delete the resolution and fix the comments. Related: theMODE=team/elsebranches at lines 180–184 build the identical prompt — dead conditional left over from the/kickoffremoval. -
A5. Headless allowlist doesn't cover what the agents are told to run.
autonomous-run.shsets--allowedTools "…,Bash(./verify.sh),Bash(git *), Bash(gh *),Bash(curl …),Bash(python3 *),Bash(jq *),Bash(yq *),…", but:- all seven specialist agents instruct pre-flight via
Bash(cat "…smurf.md")and a compoundcat … 2>/dev/null || cat …(developer.md:19-21, qa-engineer.md:14-16, product-owner.md:19-21, architect.md:19-21, devops.md:14-16, marketing.md:13-15, sales-feedback.md:13-15) —catis not allowlisted → silently denied in headless runs; - the kickoff-workflow gate uses
printenv …andclaude --version— also not allowlisted. Fix by switching agent pre-flights to theReadtool (orchestrator.md already recommends exactly this) and/or extending the allowlist.
- all seven specialist agents instruct pre-flight via
-
A6. Contradictory story on compound Bash commands (3-way).
orchestrator.md:13-16andkickoff-workflow.md:15-16claim "this plugin's PreToolUse hook rejects compound commands (no&&,||, …)".pre-tool-bash-guard.sh:5-8andpolicy.yaml:4-9explicitly state the opposite: denylist only, compound commands pass through.bootstrap.mdbans compounds ("three separate Bash calls — no compound commands", lines 155, 313, 347) yet line 57 instructsgit add <paths> && git commit -m '…'. Pick one model and make all five files agree.
-
A7. Wave numbering drift.
marketing.md:3says "Invoke as wave 5", but inorchestrator.mdwave 5 is Deploy (devops) and Promote (marketing + sales-feedback) is wave 6. -
A8. Story
## Statussection missing from the PO contract.gherkin-stories/SKILL.md,build-wiki-index.py(parse_story) andwiki_lint.py(orphan check) all depend on a trailing## Statusblock, butproduct-owner.mdCONTRACT's trailing-block template omits it entirely — every PO-authored story indexes as statusunknownand the orphan-story lint can never fire. Alsoproduct-owner.md:116says to mark superseded stories "at the top", while the skill puts Status at the bottom. -
A9. ADR ports section name drift defeats the port-conflict lint.
architect.mdtemplate uses## Ports / Adapters (or modules);wiki_lint.py:96only matchesPorts / Adapters,Ports, orPorts / Adapters / Modules(the adr-template skill's spelling). ADRs written from the architect.md template are invisible to the port-conflict check. Align architect.md with the skill. -
A10. GNU-only commands break the hooks on macOS. The repo works hard at macOS compat elsewhere (bash-3.2 workarounds in policy-guard.sh,
gtimeoutfallback in autonomous-run.sh), but:session-start-context.sh:33usesfind -printf(GNU-only);on-subagent-complete.sh:27usestac(GNU-only). Use portable equivalents (ls -t/stat -f,tail -rfallback or awk).
-
A11.
doctor.shhard-requires PyYAML; the scripts deliberately don't. The "policy.yaml is valid YAML" check (doctor.sh:54) importsyaml, while build-wiki-index.py / append-wiki-log.py / wiki_lint.py all carry an ImportError fallback so PyYAML is optional. On a machine without PyYAML, doctor reports the plugin "broken" even though everything works. -
A12. Slack notification can never fire with real content.
autonomous-run.sh:214parses.messages[-1].contentout ofrun.ndjson, but--output-format stream-jsonemits NDJSON events ({"type":"assistant",…}, terminal{"type":"result","result":…}) — there is no.messagesarray, soLASTis always empty and the webhook is silently skipped. Read theresultevent instead. -
A13.
plugin.jsonversion1.0.0.19is not semver (4 segments). Marketplace tooling expectsMAJOR.MINOR.PATCH. -
A14. OpenRouter cost field is wrong.
openrouter-curl/SKILL.md:56andmarketing.md:49-50readusage.total_cost; OpenRouter's chat completions returnusage.cost, and only when the request includes"usage": {"include": true}— as written the cost is always0.
Fixed 2026-06-09 (second pass). B1+B7 landed together in
pre-commit-verify.sh (header corrected, compound git commit
detection hardened, verify_command wired from policy — 4 new hook
tests, suite 29/29). B6 wired --max-turns from
max_turns_orchestrator (policy value raised 60→200 to preserve the
previously enforced behaviour) and documented close-loop's
fixed-scope constants. B8 removed permissionMode: ask — per the
docs the field is ignored for plugin subagents and ask was never a
valid value. Phase 8 in the README turned out to be the wiki layer
(per docs/specs/00-overview.md), so B4 added it rather than
renumbering.
-
B1.
pre-commit-verify.shheader lies about its registration. Lines 10-11 claim a^git commitmatcher "registered in settings.json"; it's actually registered inhooks/hooks.jsonwith matcherBashand self-filters. Also note: a compoundcd x && git commitbypasses the^git commitregex, so verify can be skipped — document or harden. -
B2.
smurf.md:10-12points todocs/research.md— the file does not exist anywhere in the repo (policy.yaml and kickoff-team.md also cite "research §1.7"). Either add the doc or drop the references. -
B3. README "Force Agent-Teams" section is stale. Since #12,
/smurf:kickoff-teamattempts Agent Teams and degrades — nothing forces it. Same stale wording in doctor.sh:121 ("required for /smurf:kickoff-team" — it's optional now). -
B4. README Status section drift. "13/13 hook smoke tests pass" — the suite now reports 25 passing (was 24 at review time; the A3 fix added a no-clobber test); phases jump 7 → 9 with no Phase 8.
-
B5.
developer.md:13-15names/kickoff-teamfor both modes ("In Agent Teams mode (/kickoff-team) … in subagent mode (/kickoff-team)") — leftover from when subagent mode was/kickoff. -
B6. Hard-coded caps contradict the house rule. smurf.md says "Edit policy.yaml, never hard-code numbers in agent prompts or scripts", yet
autonomous-run.shhard-codes--max-turns 200(policy saysmax_turns_orchestrator: 60) andclose-loop.py:138-139hard-codes--max-turns 20/--max-budget-usd 1.50. Note: the autonomous-run budget is no longer hard-coded — A4 wired--max-budget-usdto policy; the turn caps and close-loop values remain. -
B7.
verify_commandpolicy key is read by no hook or script.pre-commit-verify.shhard-codes./verify.sh. Either wire the key up or document it as agent-prompt-only. -
B8.
devops.mdfrontmatterpermissionMode: ask— not a documented value (documented:default,acceptEdits,plan,bypassPermissions). Verify it does anything; the agent body also asserts "every Bash invocation prompts", which depends on it. -
B9.
/smurf:close-loopuses!-inline execution for aclaude -pcall that can run for minutes, whilenightly-run.mdexplicitly warns that long-running scripts must use background Bash, not!expansion. Inconsistent guidance for the same problem.
Resolved 2026-06-09 (third pass). C5 was confirmed as a real bug by the official docs (subagents cannot spawn subagents) — both kickoff commands now instruct the MAIN session to adopt the orchestrator role, and the duplicated wave-3 gate/probe prose (C3) was collapsed into pointers at orchestrator.md's canonical sections in the same rewrite. C9 added tests/test-scripts.sh (24 tests: doctor self-check, init-project scaffolding/idempotency/merge paths, stubbed-claude autonomous runs incl. watchdog and the C10 fallback commit) and wired all three suites into tests/verify.sh. Open decision left for the owner (not a code task): pick a license (no LICENSE file exists to mirror into plugin.json — see C7 note).
-
C1. Make the session-start hook quiet outside smurf projects. The plugin's SessionStart hook fires in every project once installed, injecting "unknown — docs/rigor-level.md missing" noise. Exit silently (no output) when no smurf scaffolding is detected.
-
C2. Restrict
close-loop.pyMCP surface to read-only.--allowedTools …,mcp__github,…grants the whole GitHub MCP server, including write tools, while the prompt merely asks the model not to use them. Allowlist the specific read tools instead. (Done as part of the B6 close-loop.py edit: github restricted tolist_issues/get_issue/search_issues; sentry/linear stay server-level because their tool names depend on user-supplied configs and both are read-oriented.) -
C3. Deduplicate the wave-3 gate prose. The Dynamic-Workflows gate and the Agent-Teams capability probe are spelled out nearly verbatim in both
orchestrator.mdand the two kickoff commands — they have already drifted (A6). Keep the canonical text in one place and reference it. -
C4. Merge the two
matcher: "Bash"PreToolUse entries inhooks/hooks.jsoninto one entry with two hooks — same behaviour, less duplication. -
C5. CONFIRMED BUG (was: verify): the orchestrator cannot spawn subagents when invoked via
@orchestrator. Docs verified (code.claude.com/docs/en/sub-agents): "Subagents cannot spawn other subagents" — the Agent/Task tool is unavailable inside a subagent. Fix: kickoff commands must instruct the MAIN session to adopt the orchestrator role (bootstrap.md already uses this pattern). The kickoff commands invoke@orchestrator: $ARGUMENTS, i.e. the orchestrator runs as a subagent, and subagents normally cannot useTaskto spawn further subagents. If that restriction applies on the targeted CLI versions, the entire wave model silently degrades to inline execution; the commands may need to instruct the main session to assume the orchestrator role instead. -
C6. Fragile permission rule in
init-project.sh:68.RULE="Bash(bash \"$(dirname "$CLAUDE_PLUGIN_ROOT")/:*)"only matches when the agent quotes the path exactly the same way (bash "<path>/…"). Add the unquoted variant too, or document the dependency. -
C7. Add metadata:
licenseinplugin.json; description/owner metadata inmarketplace.json. (The non-semver version part of the original finding was fixed in A13.) -
C8. Decide where QA reports live. Agents write
qa/<id>.mdat the project root and bootstrap commits them; the directory is never scaffolded, gitignored, or documented in README's file inventory.docs/qa/(committed) or.claude/runs/<ts>/qa/(ephemeral) would be more deliberate. -
C9. Test coverage for the shell entry points.
tests/covers hooks and the wiki scripts well, butinit-project.sh(JSON merge paths),autonomous-run.sh(budget/watchdog/fallback log row), anddoctor.shitself (A1 would have been caught by a self-test) are untested. Also discovered while fixing section A:tests/verify.shonly runstest-hooks.sh—tests/test-wiki.sh(19 tests) is never invoked by the verify entrypoint; wire it in.- The A4/A12 verification used an ad-hoc stubbed-
clauderun ofautonomous-run.sh; promoting that stub pattern intotests/would cover the budget flag, result parsing, and fallback log row. (The A3 fix already added a stop-summary no-clobber test totest-hooks.sh.)
-
C10. Wiki log row is left uncommitted on the fallback path. When
autonomous-run.shappends the fallback row todocs/wiki/log.md(orchestrator crashed/timed out), nothing commits it — the next run starts with a dirty tree. Commit it in the script or document why not.
A 7-angle review (line-by-line, removed-behavior, cross-file,
reuse/simplification/efficiency/altitude) of a0b92ff...HEAD found no
correctness bugs in the plugin changes, but caught doc drift the fixes
introduced in docs/specs/ plus four cleanup items. All fixed:
- D1. spec 09 said
verify_commandis "informational; hook always invokes ./verify.sh" — now documents that pre-commit-verify.sh executes it. - D2. spec 09 embedded the pre-C4 hooks registration (two
Bashmatcher groups,.claude/settings.jsonlocation) — snippet and registration location updated; the pre-commit hook's self-filtering (incl. compound commands) documented. - D3. spec 14 cap table said
max_turns_orchestratordefault 60 — now 200 with the enforcement note. - D4. spec 12 outputs table credited
summary.mdsolely to the Stop hook — now documents orchestrator ownership + thestop-summary.mddivert; the--max-turnsrationale section notes both flags come from policy. - D5. nightly-run.md now mentions
stop-summary.mdin the post-run summary instructions. - D6. Policy parsing consolidated to one parser per language:
plugin/lib/policy.sh(policy_file/policy_scalar/policy_list, yq with awk fallback — sourced by policy-guard.sh, pre-commit-verify.sh, autonomous-run.sh) andplugin/scripts/_policy.py(imported by the three wiki scripts). Net: five divergent parsers → two, ~80 lines removed, and the yq-missing-keynullartifact in the old budget lookup fixed. - D7. kickoff-team.md no longer enumerates the Team*/Task* tool list (drift-prone duplication) — points at orchestrator.md wave 3.
- D8. autonomous-run.sh's fallback wiki-log commit honours
wiki.log_pathoverrides viapolicy_scalarinstead of hardcodingdocs/wiki/log.md(verified end-to-end with a custom path). - D9. Test helpers unified in
tests/common.sh(assert_exit/assert_ok/assert_cmd+test_summary), sourced by all three suites.
Verified after the fixes: test-hooks 30/30, test-wiki 19/19, test-scripts 24/24, doctor.sh 60 checks / 0 failed (now covers lib/policy.sh and _policy.py), policy.sh unit-tested with and without yq, _policy.py exercised with PyYAML blocked.