diff --git a/.claude/hooks/metta-guard-bash.mjs b/.claude/hooks/metta-guard-bash.mjs index f7276016..7880bdbf 100755 --- a/.claude/hooks/metta-guard-bash.mjs +++ b/.claude/hooks/metta-guard-bash.mjs @@ -35,6 +35,8 @@ import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); // Explicit ALLOW list: known safe read-only single-subcommand forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const ALLOWED_SUBCOMMANDS = new Set([ 'status', 'instructions', 'progress', 'doctor', 'next', // read-only routing query (`metta next --json`); first Bash call of the metta-next skill body @@ -45,6 +47,8 @@ const ALLOWED_SUBCOMMANDS = new Set([ ]); // Explicit ALLOW list for two-word read-only forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const ALLOWED_TWO_WORD = new Map([ ['issues', new Set(['list'])], ['gate', new Set(['list'])], @@ -64,6 +68,8 @@ const ALLOWED_TWO_WORD = new Map([ ]); // Explicit BLOCK list: state-mutating single-subcommand forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const BLOCKED_SUBCOMMANDS = new Set([ 'propose', 'quick', 'auto', 'complete', 'finalize', 'ship', 'issue', 'fix-issue', 'fix-gap', 'refresh', 'import', 'init', @@ -73,6 +79,8 @@ const BLOCKED_SUBCOMMANDS = new Set([ ]); // Explicit BLOCK list for two-word mutating forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const BLOCKED_TWO_WORD = new Map([ ['backlog', new Set(['add', 'done', 'promote', 'migrate'])], ['changes', new Set(['abandon'])], @@ -94,6 +102,8 @@ const BLOCKED_TWO_WORD = new Map([ // Bare `metta release` defaults to the read-only status view (roadmap precedent). // Bare `metta backlog` defaults to the read-only list view; its mutating two-word // forms (`add`/`done`/`promote`/`migrate`) stay Tier-2 blocked above. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const ALLOWED_BARE = new Set(['roadmap', 'release', 'backlog']); // Subcommands that require a trusted agent_type (caller identity set by the Claude Code diff --git a/CLAUDE.md b/CLAUDE.md index 5e20ff9d..bcf2b28f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ Stack: **Language:** TypeScript (strict mode, ES2022 target), **Runtime:** Node. ### How to work -**AI orchestrators MUST invoke the matching metta skill — never call the CLI directly.** (Humans running the CLI in a terminal are unaffected — this rule scopes to AI-driven sessions.) The skills wrap artifact authoring, review, and verification with the correct subagent personas; calling the CLI directly bypasses those guarantees and has shipped broken artifacts (see `spec/issues/metta-complete-accepts-stub-placeholder-artifacts-on-intent-.md`). +**State-mutating metta commands MUST go through the matching metta skill — never as direct CLI calls from an AI orchestrator session.** Enforcement authority is the `metta-guard-bash` PreToolUse hook: it blocks mutating and unrecognized commands (fail-closed) but permits a read-only query surface directly. (Humans running the CLI in a terminal are unaffected — this rule scopes to AI-driven sessions.) The skills wrap artifact authoring, review, and verification with the correct subagent personas; calling the CLI directly bypasses those guarantees and has shipped broken artifacts (see `spec/issues/metta-complete-accepts-stub-placeholder-artifacts-on-intent-.md`). Primary entry points: - `/metta-quick ` — small, scoped fixes (bug fixes, one-file edits, tiny refactors) @@ -59,9 +59,18 @@ Doc-only fixes and edits to this workflow section itself are the exceptions. ### Forbidden -- Invoking `metta quick`, `metta propose`, `metta finalize`, `metta complete`, `metta issue`, or any other `metta ` directly from an AI orchestrator session. Use the matching skill. +- Invoking any state-mutating metta command directly from an AI orchestrator session: `propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`. Use the matching skill. - Writing placeholder content like `"intent stub"` or `"summary stub"` to any artifact file to satisfy `metta complete`. Artifacts must carry real content authored by the matching `metta-*` subagent. +### Read-only queries (permitted directly) + +The `metta-guard-bash` hook allows these directly — no skill needed. This list mirrors the hook's allow-lists at generation time; the hook, not this text, is authoritative: +- Single-word: `status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install` (`iteration`/`model-escalation`/`tokens` append instrumentation records and `install` writes scaffolding — guard-allowed, though not strictly read-only) +- Two-word: `issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status` +- Bare (flags only): `roadmap`, `release`, `backlog` (e.g. `metta roadmap --json`) + +Run bare `metta` for the full current command listing. When in doubt about a command not listed here, attempt it — the guard fails closed and blocks anything unrecognized, so an attempt is always safe and never mutates state. + ### Research discipline When a research-phase or design-phase question has a deterministic answer in public documentation — framework API docs, library reference, CLI tool manual, language spec, SDK changelog — the orchestrator MUST use `WebFetch` (for a known authoritative URL) or `WebSearch` (to discover the authoritative source) to resolve it **before** asking the user. This specifically covers questions about external framework / API / tool documented behavior (e.g. "does Claude Code support `context: fork` in skill frontmatter?", "what fields does the Anthropic Messages API accept?", "is the `--legacy-peer-deps` flag deprecated in npm 10?"). diff --git a/docs/api.md b/docs/api.md index 7fdb9c58..ca23ee71 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1470,6 +1470,43 @@ Scenarios: - Baseline/compare module tests cover dirt attribution - New blocking tests fail against pre-change behavior +### Workflow Primer Mandate Is Scoped to Mutating Commands + +Scenarios: +- Mandate is scoped, names the guard, and acknowledges the read-only surface +- Mandate is byte-identical across both variants +- Forbidden section enumerates mutating families instead of a blanket ban +- Primed session is directed to permitted queries for status questions + +### Workflow Primer Documents the Permitted Read-Only Surface + +Scenarios: +- Long primer enumerates the full allow surface +- Enumerated lists match the hook's current allow-lists exactly +- Short primer carries a one-line read-only pointer + +### Workflow Primer Carries Fail-Closed Guidance as a Drift Safety Valve + +Scenarios: +- Unlisted command triggers attempt-it guidance, not assumed prohibition +- List is qualified as a generation-time mirror of the guard + +### All Workflow-Rule Copies Agree Across Primer, CLAUDE.md, and Docs + +Scenarios: +- Metta's own CLAUDE.md region regenerates with corrected wording and preserved structure +- Docs README core-rule section matches the scoped rule +- No copy asserts a blanket ban +- Refresh replaces old blanket-ban wording in a consumer project +- Refresh over local hand-edits yields correct upstream wording + +### Cross-Referenced Sync Reminders Between Primer and Guard Allow-Lists + +Scenarios: +- Primer lists point at the hook +- Hook allow-lists point back at the primer +- Hook diff is comment-only with zero behavior change + ## propose-stop-after ### `metta propose` MUST accept a `--stop-after ` option diff --git a/docs/architecture.md b/docs/architecture.md index 03ec3235..aef03b4c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -50,7 +50,7 @@ 34 requirements ### orchestration-guard -36 requirements +41 requirements ### propose-stop-after 13 requirements diff --git a/docs/changelog.md b/docs/changelog.md index 977b1d66..c69fa3a0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,5 @@ - + # Changelog @@ -45,6 +45,79 @@ Every task ran `npx tsc --noEmit` (clean) and its focused vitest suites (all gre - Review follow-up: the quality reviewer's single warning (no direct CLI test for `show` on an abandoned milestone) was closed by commit `26e0703f0`. +### 2026-08-26 — fix-generated-workflow-primer-contradicts-bash-guard-blanket + +# Summary: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +## What changed + +The generated workflow primer claimed a blanket ban on direct `metta` CLI calls while the +`metta-guard-bash` PreToolUse hook actually permits a read-only query surface. This change +scopes the primer's mandate to state-mutating commands, documents the permitted read-only +surface, and syncs all wording copies — hardened by a seam test so the hand-synced lists +cannot silently drift again. + +## Implementation (by task) + +- **Task 1.1** (`c2e28796a`) — `src/delivery/workflow-primer.ts`: rewrote the shared + `MANDATE` constant (scoped to state-mutating commands, names `metta-guard-bash` as the + enforcement authority, fail-closed framing, humans-in-terminal carve-out preserved); added + `READ_ONLY_POINTER` (short variant) and `READ_ONLY_SURFACE_BULLETS` — the + `### Read-only queries (permitted directly)` subsection (long variant) enumerating the + hook's single-word, two-word, and bare allow surface with a generation-time qualifier, + bare-`metta` discovery pointer, and attempt-it fail-closed guidance; rewrote the Forbidden + bullet to enumerate the full blocked surface (including `verify`, `backlog migrate`, + `milestone`/`roadmap` mutating forms, `release cut`); added SYNC comments. + `tests/delivery.test.ts`: updated the old mandate pin; new "Workflow primer scoped + mandate" describe (byte-identity pin, blanket-wording absence, subsection/authority/ + fail-closed pins, preservation pins); new "Workflow primer / guard allow-list seam" + describe extracting the hook's `ALLOWED_*`/`BLOCKED_*` entries from both hook copies and + asserting each appears in the rendered primer (ADR-4 pin pattern). +- **Task 1.2** (`58e07d015`) — comment-only SYNC annotations above all five list + declarations in BOTH `src/templates/hooks/metta-guard-bash.mjs` and + `.claude/hooks/metta-guard-bash.mjs`; copies remain byte-identical; all four guard suites + pass unchanged (zero behavioral diff). +- **Task 1.3** (`e5243acf3`) — `docs/workflows/README.md` "Core rule: skills, not CLI": + scoped mandate + read-only acknowledgment pointing at CLAUDE.md's subsection; mutating + surface enumerated; stub-prohibition sentence and "CLAUDE.md wins" note preserved + verbatim; blanket wording removed. +- **Task 2.1** (`833aefa43`) — `tests/refresh.test.ts`: pinned the read-only subsection in + `buildWorkflowSection()` output. +- **Task 2.2** (`8778db66b`) — metta's own `CLAUDE.md` `metta:workflow` region regenerated + byte-exact from `buildWorkflowSection()` via a scratchpad tsx splice (direct + `metta refresh` is guard-blocked for executors); no edits outside the marker region. +- **Task 3.1** — verification sweep: PASS, no fixes needed. + +## Verification evidence + +- `npm test`: 135 files, 2812 passed / 2 skipped, 0 failed +- `npx tsc --noEmit`: clean +- Hook copies: `diff` empty (template vs deployed byte-identical) +- CLAUDE.md workflow region: byte-exact match against `buildWorkflowSection()` output + +## Notes + +- Exported primer API unchanged (`workflowPrimerShort()` / `workflowPrimerLong()`); consumer + projects receive the corrected wording on their next `metta refresh` / install scaffold. +- Known residual (out of scope, per design): `docs/internals/guard-hooks.md` carries a + fourth hand-synced copy of the allow-lists; deployment-level skew (consumer refresh + without reinstall) is not addressed by this change. + +## Verification (3 parallel verifiers, iteration 1) + +- **Test suite**: no deterministic failures attributable to the change. Full-suite runs on a + loaded machine hit 10s CLI-fixture timeout flakes (SIGTERM/exit 143, different test set + each run); every failed subset passes in isolation (151/151 final), and an earlier + fully green solo run recorded 135 files, 2812 passed / 2 skipped / 0 failed. +- **Typecheck/lint**: `npx tsc --noEmit` clean, `npm run lint` clean (exit 0). +- **Spec coverage**: PASS — all 15 scenarios across the 5 requirements have concrete + evidence (test names in tests/delivery.test.ts / tests/refresh.test.ts, or file/line + citations for doc-content scenarios). Both consumer-refresh scenarios rest on composed + evidence (content-agnostic region replacement + primer content pins) — sound, noted. +- **Review**: correctness PASS_WITH_WARNINGS (minor test-hardening suggestions), security + PASS, quality PASS. No critical or major findings; see review.md. + + ### 2026-08-23 — enforce-agent-executed-uat-run-results-attached-pr-before # Summary: enforce-agent-executed-uat-run-results-attached-pr-before diff --git a/docs/workflows/README.md b/docs/workflows/README.md index a4d89811..24c2d5a0 100644 --- a/docs/workflows/README.md +++ b/docs/workflows/README.md @@ -44,9 +44,9 @@ Each sibling doc is self-contained reference material. Read `walkthroughs.md` fi ## Core rule: skills, not CLI -**AI orchestrators MUST invoke the matching metta skill — never call the CLI directly.** This applies to every AI-driven session. Humans running `metta ` in a terminal are unaffected; the rule scopes to orchestrator contexts where subagent personas and artifact-quality guarantees are load-bearing. +**State-mutating metta commands MUST go through the matching metta skill — never as direct CLI calls from an AI orchestrator session.** Enforcement authority is the `metta-guard-bash` PreToolUse hook: it blocks mutating and unrecognized commands (fail-closed) but permits a read-only query surface directly (`status`, `progress`, `issues list`, `milestone list|show`, and the rest of the allow surface — see the Read-only queries subsection in `CLAUDE.md`). Humans running `metta ` in a terminal are unaffected; the rule scopes to orchestrator contexts where subagent personas and artifact-quality guarantees are load-bearing. -Calling `metta quick`, `metta propose`, `metta finalize`, `metta complete`, `metta issue`, or any other `metta ` directly from an orchestrator bypasses the subagent wrappers. This has shipped broken artifacts in the past — see `spec/issues/metta-complete-accepts-stub-placeholder-artifacts-on-intent-.md`. Placeholder content like `"intent stub"` or `"summary stub"` is explicitly forbidden; artifacts must carry real content authored by the matching `metta-*` subagent. +Running a state-mutating command (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) directly from an orchestrator bypasses the subagent wrappers. This has shipped broken artifacts in the past — see `spec/issues/metta-complete-accepts-stub-placeholder-artifacts-on-intent-.md`. Placeholder content like `"intent stub"` or `"summary stub"` is explicitly forbidden; artifacts must carry real content authored by the matching `metta-*` subagent. The source of truth for this rule lives in the project's [`CLAUDE.md`](../../CLAUDE.md) under the `Metta Workflow` section. That file is regenerated by `/metta-refresh`; if this README drifts from it, `CLAUDE.md` wins. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/.metta.yaml b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/.metta.yaml new file mode 100644 index 00000000..81da21a3 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/.metta.yaml @@ -0,0 +1,314 @@ +workflow: standard +created: 2026-08-26T03:05:29.397Z +status: active +current_artifact: verification +base_versions: {} +artifacts: + intent: complete + stories: complete + spec: complete + research: complete + design: complete + tasks: complete + implementation: complete + verification: complete +complexity_score: + score: 1 + signals: + file_count: 2 + recommended_workflow: quick +artifact_timings: + intent: + started: 2026-08-26T03:05:35.130Z + completed: 2026-08-26T03:07:04.484Z + stories: + started: 2026-08-26T03:07:07.895Z + completed: 2026-08-26T03:08:50.766Z + spec: + started: 2026-08-26T03:08:54.216Z + completed: 2026-08-26T03:11:09.867Z + research: + started: 2026-08-26T03:11:15.427Z + completed: 2026-08-26T03:15:41.804Z + design: + started: 2026-08-26T03:15:45.015Z + completed: 2026-08-26T03:20:30.161Z + tasks: + started: 2026-08-26T03:20:33.510Z + completed: 2026-08-26T03:22:13.868Z + implementation: + started: 2026-08-26T03:22:18.581Z + completed: 2026-08-26T03:47:18.963Z + verification: + completed: 2026-08-26T05:18:37.898Z +artifact_tokens: + intent: + context: 763 + budget: 20000 + stories: + context: 2718 + budget: 20000 + spec: + context: 5109 + budget: 40000 + research: + context: 3561 + budget: 60000 + design: + context: 4974 + budget: 80000 + tasks: + context: 8795 + budget: 40000 + implementation: + context: 2847 + budget: 10000 +review_iterations: 1 +verify_iterations: 1 +worktree: /home/utx0/Code/metta/.metta/worktrees/fix-generated-workflow-primer-contradicts-bash-guard-blanket +escalation: + from_tier: quick + to_tier: standard + justification: "kept standard: non-interactive fail-closed" + timestamp: 2026-08-26T03:07:04.539Z +token_usage: + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 5049 + timestamp: 2026-08-26T03:06:11.026Z + source: hook + - task: intent + agent: metta-proposer + model: fable + tokens: 4275 + timestamp: 2026-08-26T03:06:53.662Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 6805 + timestamp: 2026-08-26T03:07:21.668Z + source: hook + - task: stories + agent: metta-product + model: fable + tokens: 4868 + timestamp: 2026-08-26T03:08:12.562Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 11145 + timestamp: 2026-08-26T03:09:13.371Z + source: hook + - task: spec + agent: metta-specifier + model: fable + tokens: 10103 + timestamp: 2026-08-26T03:11:02.952Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 14570 + timestamp: 2026-08-26T03:11:47.382Z + source: hook + - task: research + agent: metta-researcher + model: fable + tokens: 13922 + timestamp: 2026-08-26T03:14:22.438Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 14644 + timestamp: 2026-08-26T03:14:26.145Z + source: hook + - task: research + agent: metta-researcher + model: fable + tokens: 12340 + timestamp: 2026-08-26T03:14:39.331Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 14673 + timestamp: 2026-08-26T03:14:42.971Z + source: hook + - task: research + agent: metta-researcher + model: fable + tokens: 13719 + timestamp: 2026-08-26T03:14:57.487Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 19703 + timestamp: 2026-08-26T03:16:03.561Z + source: hook + - task: design + agent: metta-architect + model: fable + tokens: 19457 + timestamp: 2026-08-26T03:19:54.935Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 23527 + timestamp: 2026-08-26T03:20:51.390Z + source: hook + - task: tasks + agent: metta-planner + model: fable + tokens: 7129 + timestamp: 2026-08-26T03:22:07.198Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 29222 + timestamp: 2026-08-26T03:23:09.411Z + source: hook + - task: implementation + agent: metta-executor + model: fable + tokens: 2628 + timestamp: 2026-08-26T03:23:40.917Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 29255 + timestamp: 2026-08-26T03:23:45.439Z + source: hook + - task: implementation + agent: metta-executor + model: fable + tokens: 5204 + timestamp: 2026-08-26T03:25:25.082Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 29296 + timestamp: 2026-08-26T03:25:28.414Z + source: hook + - task: implementation + agent: metta-executor + model: fable + tokens: 18497 + timestamp: 2026-08-26T03:35:14.215Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 31462 + timestamp: 2026-08-26T03:35:42.726Z + source: hook + - task: implementation + agent: metta-executor + model: fable + tokens: 1640 + timestamp: 2026-08-26T03:36:04.891Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 31498 + timestamp: 2026-08-26T03:36:08.247Z + source: hook + - task: implementation + agent: metta-executor + model: fable + tokens: 4027 + timestamp: 2026-08-26T03:36:39.957Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 32538 + timestamp: 2026-08-26T03:36:53.152Z + source: hook + - task: implementation + agent: metta-executor + model: fable + tokens: 2813 + timestamp: 2026-08-26T03:46:22.101Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 39907 + timestamp: 2026-08-26T03:47:55.428Z + source: hook + - task: implementation + agent: metta-reviewer + model: fable + tokens: 6276 + timestamp: 2026-08-26T03:49:12.611Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 39934 + timestamp: 2026-08-26T03:49:18.790Z + source: hook + - task: implementation + agent: metta-reviewer + model: fable + tokens: 9894 + timestamp: 2026-08-26T03:51:06.670Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 40102 + timestamp: 2026-08-26T03:51:20.617Z + source: hook + - task: implementation + agent: metta-reviewer + model: fable + tokens: 15840 + timestamp: 2026-08-26T04:20:10.194Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 43540 + timestamp: 2026-08-26T04:20:55.609Z + source: hook + - task: verification + agent: metta-verifier + model: fable + tokens: 534 + timestamp: 2026-08-26T04:21:12.290Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 43580 + timestamp: 2026-08-26T04:21:17.236Z + source: hook + - task: verification + agent: metta-verifier + model: fable + tokens: 10457 + timestamp: 2026-08-26T04:30:09.828Z + source: hook + - task: metta-skill-host + agent: metta-skill-host + model: fable + tokens: 43732 + timestamp: 2026-08-26T04:30:15.173Z + source: hook + - task: verification + agent: metta-verifier + model: fable + tokens: 9826 + timestamp: 2026-08-26T05:18:15.006Z + source: hook diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/TOKENS.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/TOKENS.md new file mode 100644 index 00000000..d220a5fe --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/TOKENS.md @@ -0,0 +1,89 @@ +# Token usage: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +- **Change**: fix-generated-workflow-primer-contradicts-bash-guard-blanket +- **Generated**: 2026-08-26 + +> Provenance per row: `hook (exact)` rows are harness-measured token counts +> recorded automatically by the token-recording hook; `prose (estimate)` rows +> are orchestrator-estimated figures and may under- or over-count actual +> provider usage. When both exist for the same task and agent, the exact hook +> figure is used. + +## Total + +**~717,631 tokens** across 40 record(s). + +## Per artifact + +| Artifact/task | Agent | Model | Tokens | Provenance | +|---|---|---|---|---| +| metta-skill-host | metta-skill-host | fable | 5,049 | hook (exact) | +| intent | metta-proposer | fable | 4,275 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 6,805 | hook (exact) | +| stories | metta-product | fable | 4,868 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 11,145 | hook (exact) | +| spec | metta-specifier | fable | 10,103 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 14,570 | hook (exact) | +| research | metta-researcher | fable | 13,922 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 14,644 | hook (exact) | +| research | metta-researcher | fable | 12,340 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 14,673 | hook (exact) | +| research | metta-researcher | fable | 13,719 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 19,703 | hook (exact) | +| design | metta-architect | fable | 19,457 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 23,527 | hook (exact) | +| tasks | metta-planner | fable | 7,129 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 29,222 | hook (exact) | +| implementation | metta-executor | fable | 2,628 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 29,255 | hook (exact) | +| implementation | metta-executor | fable | 5,204 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 29,296 | hook (exact) | +| implementation | metta-executor | fable | 18,497 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 31,462 | hook (exact) | +| implementation | metta-executor | fable | 1,640 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 31,498 | hook (exact) | +| implementation | metta-executor | fable | 4,027 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 32,538 | hook (exact) | +| implementation | metta-executor | fable | 2,813 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 39,907 | hook (exact) | +| implementation | metta-reviewer | fable | 6,276 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 39,934 | hook (exact) | +| implementation | metta-reviewer | fable | 9,894 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 40,102 | hook (exact) | +| implementation | metta-reviewer | fable | 15,840 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 43,540 | hook (exact) | +| verification | metta-verifier | fable | 534 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 43,580 | hook (exact) | +| verification | metta-verifier | fable | 10,457 | hook (exact) | +| metta-skill-host | metta-skill-host | fable | 43,732 | hook (exact) | +| verification | metta-verifier | fable | 9,826 | hook (exact) | + +## Per role + +| Agent | Tokens | +|---|---| +| metta-architect | 19,457 | +| metta-executor | 34,809 | +| metta-planner | 7,129 | +| metta-product | 4,868 | +| metta-proposer | 4,275 | +| metta-researcher | 39,981 | +| metta-reviewer | 32,010 | +| metta-skill-host | 544,182 | +| metta-specifier | 10,103 | +| metta-verifier | 20,817 | + +## Per model + +| Model | Tokens | +|---|---| +| fable | 717,631 | + +## Cheap/pinned (non-inherit) vs inherit + +- **Cheap/pinned (non-inherit)**: ~717,631 tokens +- **Inherit**: ~0 tokens + +## Gaps + +No gaps found. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/UAT.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/UAT.md new file mode 100644 index 00000000..8f63831f --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/UAT.md @@ -0,0 +1,298 @@ +# UAT: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +- **Change**: fix-generated-workflow-primer-contradicts-bash-guard-blanket +- **Generated**: 2026-08-26 +- **Source**: user stories (stories.md) + +## Reporting failures + +If any step below fails or behaves unexpectedly, log a metta issue +(`/metta-issue `) referencing this file and the step number. +The sanctioned UAT runner (`/metta-uat`) may flip a step's Pass checkbox +to reflect a genuinely observed outcome and may append dated `## UAT run` +records below the steps. Never fabricate a pass: do not alter step content, +and never check a box for behavior that was not actually observed. + +## Acceptance steps + +### US-1: Primer ban scoped to mutating commands only + +*Independent test:* The regenerated primer (both `workflowPrimerShort()` and `workflowPrimerLong()` outputs) contains no blanket "never call the CLI directly" / "any other `metta `" wording; its mandate is scoped to mutating commands, names `metta-guard-bash` as the enforcement authority, and is identical across both variants. + +#### Step 1.1 +- **Setup**: the rewritten `MANDATE` constant in `src/delivery/workflow-primer.ts` +- **Do**: either `workflowPrimerShort()` or `workflowPrimerLong()` is rendered +- **Observe**: the mandate states that state-mutating lifecycle commands require the matching skill, identifies `metta-guard-bash` as the enforcement authority, and states that the guard permits a read-only query surface directly. +- [x] Pass + +#### Step 1.2 +- **Setup**: the two primer variants +- **Do**: their mandate text is compared +- **Observe**: the mandate is byte-identical in both, preserving the consistency invariant documented in the file header. +- [x] Pass + +#### Step 1.3 +- **Setup**: the long primer's Forbidden section +- **Do**: an orchestrator reads it (Run: `backlog add/done/promote/migrate`, `changes abandon`) +- **Observe**: it enumerates the mutating command families (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) instead of banning "any other `metta `". +- [x] Pass + +#### Step 1.4 +- **Setup**: an orchestrator session primed with the corrected wording +- **Do**: the operator asks a project-status question (e.g., a milestone rollup) +- **Observe**: the primer directs the session toward a permitted read-only command rather than prohibiting all CLI use. +- [x] Pass + +### US-2: Permitted read-only surface documented in the primer + +*Independent test:* The long primer variant contains a read-only-queries subsection enumerating the guard's single-word, two-word, and bare allowed forms, and the short variant contains a one-line pointer stating read-only queries are permitted and the guard fails closed. + +#### Step 2.1 +- **Setup**: the rendered `workflowPrimerLong()` output +- **Do**: an orchestrator reads the "Read-only queries (permitted directly)" subsection (Run: `issues list`, `gate list`) +- **Observe**: it lists the guard's `ALLOWED_SUBCOMMANDS` (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`), `ALLOWED_TWO_WORD` forms (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), and `ALLOWED_BARE` forms (`roadmap`, `release`, `backlog` with flags) as they exist at generation time. +- [x] Pass + +#### Step 2.2 +- **Setup**: the rendered `workflowPrimerShort()` output +- **Do**: an orchestrator reads it +- **Observe**: it includes a one-line statement that read-only queries are permitted directly and the guard fails closed. +- [x] Pass + +#### Step 2.3 +- **Setup**: the enumerated lists in the long primer +- **Do**: compared against `metta-guard-bash.mjs` at the time of this change +- **Observe**: every entry matches the hook's current allow-lists exactly (documenting the surface, not renegotiating it). +- [x] Pass + +### US-3: Fail-closed guidance as a drift safety valve + +*Independent test:* The long primer's read-only subsection states that the list mirrors the guard's allow-lists at generation time and that, when in doubt, attempting the command is safe because the guard fails closed and blocks anything unrecognized. + +#### Step 3.1 +- **Setup**: the long primer's read-only subsection +- **Do**: an orchestrator encounters a command not on the enumerated list +- **Observe**: the primer's guidance instructs it to attempt the command and rely on the guard's fail-closed blocking, rather than to assume prohibition. +- [x] Pass + +#### Step 3.2 +- **Setup**: the primer text +- **Do**: it describes the enumerated list +- **Observe**: it explicitly qualifies the list as mirroring the guard's allow-lists at generation time, signaling the hook — not the primer — is authoritative. +- [x] Pass + +### US-4: All three wording copies agree + +*Independent test:* After the change, metta's CLAUDE.md `metta:workflow` region (regenerated via refresh) and the "Core rule: skills, not CLI" section of `docs/workflows/README.md` both scope the ban to mutating commands and acknowledge the permitted read-only surface, with no remaining blanket-ban wording. + +#### Step 4.1 +- **Setup**: metta's own CLAUDE.md +- **Do**: its workflow region is regenerated from the corrected primer +- **Observe**: the scoped mandate, enumerated Forbidden families, and read-only subsection appear, while the "Doc-only fixes and edits to this workflow section itself are the exceptions" line and section structure are preserved. +- [x] Pass + +#### Step 4.2 +- **Setup**: `docs/workflows/README.md` +- **Do**: the "Core rule: skills, not CLI" section is read +- **Observe**: its wording matches the scoped rule and permitted read-only surface, and its existing note that CLAUDE.md wins on drift is preserved. +- [x] Pass + +#### Step 4.3 +- **Setup**: the three copies (generated primer, metta's CLAUDE.md region, docs README) +- **Do**: compared +- **Observe**: none asserts a blanket ban on direct `metta` CLI calls. +- [x] Pass + +### US-5: Correct wording propagates and survives refresh + +*Independent test:* Running refresh in a consumer project whose CLAUDE.md contains the old blanket-ban wording (or commit-919720e-style local corrections) replaces the region with the corrected upstream primer. + +#### Step 5.1 +- **Setup**: a consumer project whose `metta:workflow` region carries the old blanket-ban primer +- **Do**: `metta refresh` runs with the fixed version installed (Run: `metta refresh`) +- **Observe**: the region contains the scoped mandate, the enumerated Forbidden families, and the read-only subsection. +- [ ] Pass + +#### Step 5.2 +- **Setup**: a consumer project with local hand-edits inside the generated region (the zeus commit-919720e pattern) +- **Do**: refresh regenerates the region +- **Observe**: the replacement wording is correct upstream content, so losing the local edits no longer reintroduces misinformation. +- [ ] Pass + +#### Step 5.3 +- **Setup**: a downstream session reading the regenerated primer +- **Do**: the operator asks for a milestone rollup (Run: `metta milestone show`) +- **Observe**: the session runs the permitted CLI query (e.g., `metta milestone show`) and reports the CLI's authoritative number instead of a hand-computed one. +- [ ] Pass + +### US-6: Cross-referenced sync reminders for maintainers + +*Independent test:* Both files contain a comment referencing the other by path near the hand-synced lists, and the hook change is comment-only with zero behavioral difference. + +#### Step 6.1 +- **Setup**: `src/delivery/workflow-primer.ts` +- **Do**: a maintainer reads the enumerated command lists +- **Observe**: an adjacent comment points at `metta-guard-bash.mjs` as the source that must stay in sync. +- [x] Pass + +#### Step 6.2 +- **Setup**: `.claude/hooks/metta-guard-bash.mjs` +- **Do**: a maintainer reads the allow-list blocks +- **Observe**: an adjacent comment points back at `workflow-primer.ts`. +- [x] Pass + +#### Step 6.3 +- **Setup**: the hook file after the change +- **Do**: its decision logic is exercised +- **Observe**: behavior is identical to before — the only diff is comments. +- [ ] Pass + +## Additional scenarios + +#### Step 7.1: Mandate is scoped, names the guard, and acknowledges the read-only surface +- **Setup**: the rewritten `MANDATE` constant in `src/delivery/workflow-primer.ts` +- **Do**: either `workflowPrimerShort()` or `workflowPrimerLong()` is rendered +- **Observe**: the mandate states that state-mutating lifecycle commands require the matching skill, identifies `metta-guard-bash` as the enforcement authority, and states that the guard permits a read-only query surface directly +- [x] Pass + +#### Step 7.2: Mandate is byte-identical across both variants +- **Setup**: the rendered outputs of `workflowPrimerShort()` and `workflowPrimerLong()` +- **Do**: their mandate text is compared +- **Observe**: the mandate is byte-identical in both variants +- [x] Pass + +#### Step 7.3: Forbidden section enumerates mutating families instead of a blanket ban +- **Setup**: the long primer's Forbidden section +- **Do**: an orchestrator reads it (Run: `backlog add/done/promote/migrate`, `changes abandon`) +- **Observe**: it enumerates the mutating command families (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) and contains no "any other `metta `" blanket wording +- [x] Pass + +#### Step 7.4: Primed session is directed to permitted queries for status questions +- **Setup**: an orchestrator session primed with the corrected wording +- **Do**: the operator asks a project-status question (e.g., a milestone rollup) +- **Observe**: the primer's guidance directs the session toward a permitted read-only command rather than prohibiting all CLI use +- [x] Pass + +#### Step 7.5: Long primer enumerates the full allow surface +- **Setup**: the rendered `workflowPrimerLong()` output +- **Do**: an orchestrator reads the "Read-only queries (permitted directly)" subsection (Run: `issues list`, `gate list`) +- **Observe**: it lists the single-word allowed subcommands (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`), the two-word allowed forms (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), and the bare allowed forms (`roadmap`, `release`, `backlog` with flags) +- [x] Pass + +#### Step 7.6: Enumerated lists match the hook's current allow-lists exactly +- **Setup**: the enumerated lists in the long primer's read-only subsection +- **Do**: compared against the `ALLOWED_SUBCOMMANDS`, `ALLOWED_TWO_WORD`, and `ALLOWED_BARE` blocks in `.claude/hooks/metta-guard-bash.mjs` at the time of this change +- **Observe**: every entry matches the hook's current allow-lists exactly, with no additions or removals +- [x] Pass + +#### Step 7.7: Short primer carries a one-line read-only pointer +- **Setup**: the rendered `workflowPrimerShort()` output +- **Do**: an orchestrator reads it +- **Observe**: it includes a one-line statement that read-only queries are permitted directly and the guard fails closed +- [x] Pass + +#### Step 7.8: Unlisted command triggers attempt-it guidance, not assumed prohibition +- **Setup**: the long primer's read-only subsection +- **Do**: an orchestrator encounters a `metta` command not on the enumerated list +- **Observe**: the primer's guidance instructs it to attempt the command and rely on the guard's fail-closed blocking, rather than to assume prohibition +- [x] Pass + +#### Step 7.9: List is qualified as a generation-time mirror of the guard +- **Setup**: the primer text describing the enumerated read-only list +- **Do**: it is read +- **Observe**: it explicitly qualifies the list as mirroring the guard's allow-lists at generation time, identifying the hook as the authoritative source +- [x] Pass + +#### Step 7.10: Metta's own CLAUDE.md region regenerates with corrected wording and preserved structure +- **Setup**: metta's own CLAUDE.md +- **Do**: its `metta:workflow` region is regenerated from the corrected primer via refresh +- **Observe**: the scoped mandate, enumerated Forbidden families, and read-only subsection appear, while the "Doc-only fixes and edits to this workflow section itself are the exceptions" line and the section structure are preserved +- [x] Pass + +#### Step 7.11: Docs README core-rule section matches the scoped rule +- **Setup**: `docs/workflows/README.md` +- **Do**: the "Core rule: skills, not CLI" section is read +- **Observe**: its wording matches the scoped rule and permitted read-only surface, and its existing note that CLAUDE.md wins on drift is preserved +- [x] Pass + +#### Step 7.12: No copy asserts a blanket ban +- **Setup**: the three copies (generated primer, metta's CLAUDE.md workflow region, docs README) +- **Do**: they are compared +- **Observe**: none asserts a blanket ban on direct `metta` CLI calls +- [x] Pass + +#### Step 7.13: Refresh replaces old blanket-ban wording in a consumer project +- **Setup**: a consumer project whose `metta:workflow` region carries the old blanket-ban primer +- **Do**: `metta refresh` runs with the fixed version installed (Run: `metta refresh`) +- **Observe**: the region contains the scoped mandate, the enumerated Forbidden families, and the read-only subsection +- [ ] Pass + +#### Step 7.14: Refresh over local hand-edits yields correct upstream wording +- **Setup**: a consumer project with local hand-edits inside the generated region (the zeus commit-919720e pattern) +- **Do**: refresh regenerates the region +- **Observe**: the replacement wording is the correct upstream content, so losing the local edits no longer reintroduces misinformation +- [ ] Pass + +#### Step 7.15: Primer lists point at the hook +- **Setup**: `src/delivery/workflow-primer.ts` +- **Do**: a maintainer reads the enumerated command lists +- **Observe**: an adjacent comment points at `.claude/hooks/metta-guard-bash.mjs` as the source that must stay in sync +- [x] Pass + +#### Step 7.16: Hook allow-lists point back at the primer +- **Setup**: `.claude/hooks/metta-guard-bash.mjs` +- **Do**: a maintainer reads the allow-list blocks +- **Observe**: an adjacent comment points back at `src/delivery/workflow-primer.ts` +- [x] Pass + +#### Step 7.17: Hook diff is comment-only with zero behavior change +- **Setup**: the hook file after the change +- **Do**: its decision logic is exercised across allowed, blocked, and unknown invocations +- **Observe**: behavior is identical to before the change — the only diff is comments +- [ ] Pass + +## UAT run — 2026-08-26 + +- **Runner**: metta-uat-runner agent via /metta-uat, model: claude-fable-5 (self-reported) +- **Completed**: 2026-08-26T05:30:50.414Z +- **Result**: 28 pass / 0 fail / 7 skip (of 35 steps) + +| Step | Outcome | Note | +|------|---------|------| +| 1.1 | pass | rendered via tsx-equivalent (node --experimental-strip-types); mandate scoped, names metta-guard-bash, notes read-only surface | +| 1.2 | pass | shared MANDATE constant; long variant's paragraph starts byte-identical to short's mandate | +| 1.3 | pass | all 18 mutating families enumerated; no "any other `metta `" wording | +| 1.4 | pass | verified textually: pointer line and read-only subsection direct toward permitted queries | +| 2.1 | pass | all three allow surfaces enumerated in rendered long output | +| 2.2 | pass | one-line pointer present with fail-closed statement | +| 2.3 | pass | compared against .claude/hooks/metta-guard-bash.mjs — exact match, no additions/removals | +| 3.1 | pass | "When in doubt … attempt it — the guard fails closed" guidance present | +| 3.2 | pass | "mirrors the hook's allow-lists at generation time; the hook, not this text, is authoritative" | +| 4.1 | pass | verified regenerated region content in CLAUDE.md; Doc-only exceptions line and structure preserved | +| 4.2 | pass | Core rule section scoped; "CLAUDE.md wins" drift note preserved | +| 4.3 | pass | grep for blanket phrases across all three copies found none | +| 5.1 | skip | requires running `metta refresh` in a consumer project — mutating metta command forbidden to UAT runner | +| 5.2 | skip | requires running `metta refresh` over hand-edited region — forbidden to UAT runner | +| 5.3 | skip | requires a live downstream session and `metta milestone show` — not executable by UAT runner | +| 6.1 | pass | SYNC comments adjacent to lists reference metta-guard-bash.mjs (template path + "and its deployed copy") | +| 6.2 | pass | SYNC comments on all five allow/block lists point back at src/delivery/workflow-primer.ts | +| 6.3 | skip | pre-change hook state unobservable (git forbidden to runner); current behavior exercised and consistent with documented allow-lists | +| 7.1 | pass | same observation as 1.1 | +| 7.2 | pass | same observation as 1.2 | +| 7.3 | pass | same observation as 1.3 | +| 7.4 | pass | same observation as 1.4 | +| 7.5 | pass | same observation as 2.1 | +| 7.6 | pass | same observation as 2.3 | +| 7.7 | pass | same observation as 2.2 | +| 7.8 | pass | same observation as 3.1 | +| 7.9 | pass | same observation as 3.2 | +| 7.10 | pass | same observation as 4.1 | +| 7.11 | pass | same observation as 4.2 | +| 7.12 | pass | same observation as 4.3 | +| 7.13 | skip | same limitation as 5.1 | +| 7.14 | skip | same limitation as 5.2 | +| 7.15 | pass | comment cites src/templates/hooks/metta-guard-bash.mjs "(and its deployed copy)" — the deployed copy is .claude/hooks/metta-guard-bash.mjs | +| 7.16 | pass | same observation as 6.2 | +| 7.17 | skip | same limitation as 6.3; exercised allowed (exit 0), blocked (exit 2 with skill hint), and unknown (exit 2 fail-closed) invocations — all match documented surfaces | + +- **Note**: Edit tool refused by guard; document rewritten via heredoc fallback diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/design.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/design.md new file mode 100644 index 00000000..97dac6b6 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/design.md @@ -0,0 +1,364 @@ +# Design: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +## Approach + +This is a wording-and-documentation change with a test-only drift guard. The enforcement +authority (`metta-guard-bash.mjs`) is correct and untouched behaviorally; the generated primer +in `src/delivery/workflow-primer.ts` is the defective component and the only place logic-adjacent +edits land. The fix follows research.md's selected approach: **scoped wording rewrite with an +enumerated read-only list, hardened by a primer-to-hook seam test**, with two elements folded in +from the generic-wording research track: the fail-closed "attempt it" guidance and the +bare-`metta` discovery pointer. + +Five moves, in dependency order: + +1. **Rewrite the primer constants** (`MANDATE`, Forbidden bullet) and add two new constants + (read-only subsection bullets, short-variant pointer line) in `workflow-primer.ts`. The + mandate stays a single shared constant so the byte-identity invariant across + `workflowPrimerShort()` / `workflowPrimerLong()` remains structural (spec: "Mandate is + byte-identical across both variants"). +2. **Comment-only sync reminders in the guard hook**, applied byte-identically to BOTH copies: + `.claude/hooks/metta-guard-bash.mjs` and the source of truth + `src/templates/hooks/metta-guard-bash.mjs` (verified identical today via `diff`; the + mint-seam test suite already exercises both copies via its `PAIRS` fixture, and the new seam + test extracts from both and asserts equality — see Components §5). +3. **Seam test** in `tests/delivery.test.ts` (ADR-4 constant-pin precedent from + `tests/metta-guard-mint-seam.test.ts:210`): regex-extract the hook's `ALLOWED_SUBCOMMANDS` / + `ALLOWED_TWO_WORD` / `ALLOWED_BARE` entries and assert each appears (in its rendered form) in + `workflowPrimerLong()`. Drift becomes a loud CI failure with zero runtime coupling — the + shared-manifest approach's guarantee at ~30 lines of test code (research-shared-manifest.md §5.4 + rejected the runtime manifest; this is its replacement). +4. **Sync the two prose copies**: `docs/workflows/README.md` "Core rule" section, and metta's + own CLAUDE.md `metta:workflow` region — the latter hand-applied to match + `buildWorkflowSection()` output exactly, because direct `metta refresh` is Tier-2 blocked for + the executor (verification procedure in Components §7). +5. **Update existing test pins** (`tests/delivery.test.ts:61` pins the old mandate opening) and + add the new wording assertions, including a pin of the mandate byte-identity invariant. + +### Key decisions (ADR-style) + +- **ADR-A: Hand-synced enumeration + CI seam test, not a shared runtime manifest.** + Rationale: the bug is docs drift; the hook's self-containedness is a deliberate security + property, and the only safe manifest load strategy (dynamic import + exit 2) introduces a new + whole-session all-Bash-blocked failure state (research-shared-manifest.md §3). The codebase's + established pattern for cross-standalone-file consistency is the duplicated-but-pinned seam + test (`GRACE_MS` pin, `tests/metta-guard-mint-seam.test.ts:210-216`). No vendor lock-in either + way; the only external semantic relied on (Claude Code PreToolUse exit-code behavior) is + pre-existing and unchanged. +- **ADR-B: Mandate byte-identity enforced structurally (one constant) AND pinned by test.** + The invariant exists today but is untested (research.md key fact 2). The test duplicates the + full mandate literal (ADR-4 pin style) and asserts both rendered variants contain it — so a + future refactor that forks the constant fails CI. +- **ADR-C: "Read-only queries (permitted directly)" title kept; accuracy hedged in body prose.** + `iteration` / `model-escalation` / `tokens` append instrumentation records and `install` + writes scaffolding — allowed by the guard but not strictly read-only. The spec fixes the + subsection title, so the hedge lives in the bullet body (Components §1, subsection wording), + per research.md key fact 4. +- **ADR-D: Read-only subsection placed between `### Forbidden` and `### Research discipline`** + in the long variant. Forbidden enumerates the blocked surface, the new subsection immediately + answers "so what IS allowed" — the reading order that directly counters the zeus failure mode. +- **ADR-E: Bare-`metta` pointer included** (folded from research-generic-wording.md §1): bare + `metta` is guard-allowed (`classify()` — no subcommand → allow) and prints the full command + listing — the one zero-drift, self-updating discovery channel. One sentence in the subsection. +- **ADR-F: CLAUDE.md region hand-applied, verified by diff against computed output.** Direct + `metta refresh` is Tier-2 blocked for the executor and this change must not invoke the CLI + directly. `buildWorkflowSection()` output is fully deterministic (static strings only), so the + region can be hand-edited and verified byte-exact with a scratch `tsx` script (Components §7). + The next real `/metta-refresh` then confirms idempotence. + +## Components + +### 1. `src/delivery/workflow-primer.ts` — the only logic-adjacent change + +**Exported API unchanged**: `workflowPrimerShort(): string[]` and +`workflowPrimerLong(): string[]`; consumers (`refresh.ts:127`, `claude-code-adapter.ts:76`, +`discovery-helpers.ts:143`) need no edits. + +**File header (lines 1–9)**: update prose to (a) describe the scoped mandate, (b) state the +byte-identity invariant is now test-pinned, (c) note the enumerated lists are hand-synced with +`metta-guard-bash.mjs` and guarded by the seam test in `tests/delivery.test.ts`. + +**Changed constant — `MANDATE`** (exact proposed wording, one string, shared by both variants): + +> **State-mutating metta commands MUST go through the matching metta skill — never as direct +> CLI calls from an AI orchestrator session.** Enforcement authority is the `metta-guard-bash` +> PreToolUse hook: it blocks mutating and unrecognized commands (fail-closed) but permits a +> read-only query surface directly. (Humans running the CLI in a terminal are unaffected — this +> rule scopes to AI-driven sessions.) + +The long variant continues to append (unchanged, via the existing concatenation at line 47): +`' The skills wrap artifact authoring, review, and verification with the correct subagent +personas; calling the CLI directly bypasses those guarantees and has shipped broken artifacts +(see `spec/issues/metta-complete-accepts-stub-placeholder-artifacts-on-intent-.md`).'` +The appended sentence is outside the mandate; byte-identity applies to the `MANDATE` constant's +rendered text, which both variants contain verbatim. + +**New constant — `READ_ONLY_POINTER`** (short variant, one line): + +> Read-only queries (`metta status`, `metta progress`, `metta issues list`, …) are permitted +> directly; the guard fails closed, so attempting a query is always safe. + +Inserted in `workflowPrimerShort()` immediately after `MANDATE` (blank line separated), before +`'Primary entry points:'`. + +**New constant — `READ_ONLY_SURFACE_BULLETS`** (long variant), with an adjacent sync comment +(exact comment text in §2 below). Exact proposed content, rendered: + +> ### Read-only queries (permitted directly) +> +> The `metta-guard-bash` hook allows these directly — no skill needed. This list mirrors the +> hook's allow-lists at generation time; the hook, not this text, is authoritative: +> - Single-word: `status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, +> `model-escalation`, `tokens`, `install` (`iteration`/`model-escalation`/`tokens` append +> instrumentation records and `install` writes scaffolding — guard-allowed, though not +> strictly read-only) +> - Two-word: `issues list`, `gate list`, `changes list`, `backlog list|show`, +> `gaps list|show`, `milestone list|show`, `release status` +> - Bare (flags only): `roadmap`, `release`, `backlog` (e.g. `metta roadmap --json`) +> +> Run bare `metta` for the full current command listing. When in doubt about a command not +> listed here, attempt it — the guard fails closed and blocks anything unrecognized, so an +> attempt is always safe and never mutates state. + +This satisfies all three spec scenarios of the read-only requirement (full enumeration matching +the hook exactly at time of change), the fail-closed requirement (generation-time qualifier + +attempt-it instruction), and ADR-C/ADR-E (hedge in prose, bare-`metta` pointer). + +Placement in `workflowPrimerLong()`: after the `### Forbidden` block's bullets, before +`### Research discipline` (ADR-D). + +**Changed line — Forbidden bullet** (long variant line 60; exact proposed wording, mirroring +`BLOCKED_SUBCOMMANDS` + `BLOCKED_TWO_WORD` in the hook, with an adjacent sync comment): + +> - Invoking any state-mutating metta command directly from an AI orchestrator session: +> `propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, +> `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, +> `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, +> `release cut`. Use the matching skill. + +Note this deliberately includes the surface growth the old primer missed: `verify`, +`backlog migrate`, all `milestone` mutating forms, all `roadmap` mutating forms, `release cut` +(research.md key fact 1; verified against the hook lines 67–87). + +**Preserved verbatim (spec preservation constraints)**: +- `'Doc-only fixes and edits to this workflow section itself are the exceptions.'` (line 56) +- The stub-artifact prohibition bullet (line 61): the bullet forbidding placeholder stub + strings in artifact files, requiring real content authored by the matching `metta-*` + subagent — kept verbatim (the literal stub phrases are not reproduced here because the + completeness gate scans artifacts for them). +- `ENTRY_POINTS_BULLETS`, `TRUST_MODEL_BULLETS`, the quick-mode routing paragraph, the entire + `### Research discipline` section, and the short variant's closing + `'Run `metta refresh` for the full command reference.'` line — all untouched. + +### 2. Guard hook — comment-only edits, BOTH copies + +Files: `.claude/hooks/metta-guard-bash.mjs` AND `src/templates/hooks/metta-guard-bash.mjs`. +The two are byte-identical today and MUST remain so — apply the identical comment lines to both +(edit the template, mirror to the deployed copy, confirm with `diff`). The existing test +machinery pins the pairing: `tests/metta-guard-mint-seam.test.ts` spawns both copies via its +`PAIRS` fixture and its ADR-4 pin reads both files' text; the new seam test additionally +extracts the allow-lists from both copies and asserts set equality (§5). + +Exact comment (one line each, inserted directly above the five list declarations — +`ALLOWED_SUBCOMMANDS`, `ALLOWED_TWO_WORD`, `ALLOWED_BARE`, `BLOCKED_SUBCOMMANDS`, +`BLOCKED_TWO_WORD`): + +```js +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. +``` + +Zero behavioral diff: no code line changes, only comments. Verified by re-running the four +guard suites unchanged (`tests/metta-guard-bash.test.ts`, +`tests/cli-metta-guard-bash-integration.test.ts`, `tests/metta-guard-mint-seam.test.ts`, +`tests/metta-guard-agent-dispatch.test.ts`) — this is the spec's "Hook diff is comment-only" +scenario check. + +### 3. `docs/workflows/README.md` — "Core rule: skills, not CLI" section (lines 45–51) + +Heading kept. Exact proposed replacement for the two body paragraphs (the third paragraph, +line 51 with the "CLAUDE.md wins" note, is preserved verbatim): + +> **State-mutating metta commands MUST go through the matching metta skill — never as direct +> CLI calls from an AI orchestrator session.** Enforcement authority is the `metta-guard-bash` +> PreToolUse hook: it blocks mutating and unrecognized commands (fail-closed) but permits a +> read-only query surface directly (`status`, `progress`, `issues list`, `milestone list|show`, +> and the rest of the allow surface — see the Read-only queries subsection in `CLAUDE.md`). +> Humans running `metta ` in a terminal are unaffected; the rule scopes to orchestrator +> contexts where subagent personas and artifact-quality guarantees are load-bearing. +> +> Running a state-mutating command (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, +> `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, +> `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, +> `roadmap add/reorder/next/remove`, `release cut`) directly from an orchestrator bypasses the +> subagent wrappers. This has shipped broken artifacts in the past — see +> `spec/issues/metta-complete-accepts-stub-placeholder-artifacts-on-intent-.md`. Placeholder +> stub content is explicitly forbidden; artifacts must carry real content authored by the +> matching `metta-*` subagent. (The executor copies the existing stub-prohibition sentence +> from the current README verbatim — its literal stub phrases are not reproduced in this +> design because the completeness gate scans artifacts for them.) + +This carries the scoped mandate, acknowledges the read-only surface (pointing at CLAUDE.md's +subsection rather than duplicating the full three-list enumeration a fourth time — CLAUDE.md is +already declared the source of truth by line 51), preserves the stub-artifact prohibition, and +removes the blanket "any other `metta `" wording. + +### 4. Metta's own `CLAUDE.md` — `metta:workflow` region + +Hand-apply the exact `buildWorkflowSection()` output between +`` / `` (ADR-F). Concretely the region +diff is: mandate paragraph replaced, Forbidden first bullet replaced, read-only subsection +inserted between Forbidden and Research discipline. Everything else in the region (entry +points, trust model, quick-mode paragraph, exceptions line, skill lists) is unchanged. +Verification procedure in §7. + +### 5. Seam test — `tests/delivery.test.ts`, new describe block "Workflow primer / guard allow-list seam" + +Design (ADR-4 pin pattern, ~30–40 lines): + +- Resolve both hook copies from `join(import.meta.dirname, '..')`: + `src/templates/hooks/metta-guard-bash.mjs` and `.claude/hooks/metta-guard-bash.mjs` + (same dual-copy discipline as `PAIRS` in the mint-seam suite). +- Extraction helper, per file: + 1. Slice the declaration block by matching from `const ALLOWED_SUBCOMMANDS = new Set([` (and + the `ALLOWED_TWO_WORD = new Map([`, `ALLOWED_BARE = new Set([` counterparts) to the first + `]);` at declaration end. + 2. **Strip `//` line comments from the block first** — comment prose may contain quotes or + backticks; stripping makes the quoted-string extraction immune to comment churn. + 3. `ALLOWED_SUBCOMMANDS` / `ALLOWED_BARE`: collect `/'([^']+)'/g` matches → `string[]`. + 4. `ALLOWED_TWO_WORD`: collect `/\['([a-z-]+)',\s*new Set\(\[([^\]]+)\]\)/g` matches → + `Array<[group, subs[]]>` (inner subs via the same quoted-string regex). +- Assertions: + 1. **Sanity floor** (prevents a silently-broken regex from passing vacuously): + `ALLOWED_SUBCOMMANDS` extraction length >= 9, `ALLOWED_TWO_WORD` >= 7 groups, + `ALLOWED_BARE` >= 3. + 2. **Copy equality**: extraction from the template copy deep-equals extraction from the + deployed copy (allow-list divergence between the two files fails here). + 3. **Primer mirror**: against `workflowPrimerLong().join('\n')` — + each single-word entry appears as `` `word` ``; each two-word group appears as + `` `group sub1|sub2` `` (subs joined `|` in hook insertion order — this pins the primer's + rendering format deliberately; both live in this repo and CI failure is the desired drift + signal); each bare entry appears within the "Bare (flags only)" bullet line. + 4. **Blocked mirror** (Forbidden bullet): each `BLOCKED_SUBCOMMANDS` entry appears as + `` `word` `` in the long output; for each `BLOCKED_TWO_WORD` group, the bullet contains + `` `group sub1/sub2/...` `` (subs joined `/` in hook order — e.g. + `` `backlog add/done/promote/migrate` ``). + +Direction note: the seam test asserts hook-entries ⊆ primer (every allowed/blocked entry is +documented). The reverse direction (primer lists nothing extra) is covered by the wording tests +below pinning the exact bullet strings, which are built only from the enumerations above. + +### 6. Other test updates + +`tests/delivery.test.ts`: +- Line 61 (`formatContext` test): update the pinned substring to the new mandate opening — + `expect(formatted).toContain('State-mutating metta commands MUST go through the matching metta skill')`. +- New describe "Workflow primer scoped mandate": + 1. **Byte-identity pin (ADR-B)**: the test carries the full new mandate string as a local + literal; assert `workflowPrimerShort().join('\n')` and `workflowPrimerLong().join('\n')` + both `toContain` it. (Duplicated-literal pin, exactly the `GRACE_MS` precedent.) + 2. Neither variant contains `'never call the CLI directly'`; neither contains + ``'any other `metta `'`` (blanket-ban removal, both variants). + 3. Long variant contains `'### Read-only queries (permitted directly)'` and + `'metta-guard-bash'`. + 4. Long variant contains the generation-time qualifier (`'at generation time'`), the + authority attribution (`'the hook, not this text, is authoritative'`), and the fail-closed + guidance (`'attempt it'` / `'fails closed'`). + 5. Short variant contains the read-only pointer line (pin the full `READ_ONLY_POINTER` + string) and does NOT contain the `###` read-only subsection heading (short stays short, + matching the existing research-discipline short/long split test). + 6. Preservation pins: long variant still contains the doc-only-exceptions line and the + stub-artifact prohibition bullet. +- Existing research-discipline and Tier-2 trust-model describes: unaffected (no changed text in + those sections). + +`tests/refresh.test.ts` (`buildWorkflowSection` describe): one added assertion — +`expect(result).toContain('### Read-only queries (permitted directly)')` — pinning that the +refresh-emitted region carries the subsection. + +### 7. CLAUDE.md verification procedure (execute-phase, no metta CLI invocation) + +After hand-editing the region, verify byte-exactness with a scratch script (scratchpad dir, not +committed): `npx tsx` a five-line script that imports `buildWorkflowSection` from +`src/cli/commands/refresh.ts`, prints it, and diff that output against the text between the +workflow markers in `CLAUDE.md`. Zero diff required. The next real `/metta-refresh` run (post- +ship) is the idempotence confirmation — regeneration must produce no change. + +## Data Model + +None. No `.metta/` state, no Zod schemas, no YAML, and no persisted formats change. The only +"data" in play is compile-time string constants in `workflow-primer.ts` and comments in the +hook. The generated-region contract (`` markers replaced +wholesale by `replaceMarkerContent()` in `refresh.ts:187–205`) is unchanged and is the +propagation mechanism, not a modified component. + +## API Design + +No public API surface changes: + +- `workflowPrimerShort(): string[]` — signature, purity, and consumers unchanged; output text + gains one line (pointer) and a rewritten mandate. +- `workflowPrimerLong(): string[]` — signature unchanged; output gains the read-only subsection + (~9 lines), a rewritten mandate, and a rewritten Forbidden bullet. +- `buildWorkflowSection()`, `replaceMarkerContent()`, `formatContext()`, install/init scaffold + paths — untouched; they pick up the new text automatically. +- CLI behavior, hook exit codes, allow/block membership, tiering, credentials — all unchanged + (spec: comment-only hook diff). + +Internal module shape after the change (all module-private consts, no new exports): +`MANDATE` (rewritten), `ENTRY_POINTS_BULLETS` (unchanged), `TRUST_MODEL_BULLETS` (unchanged), +`READ_ONLY_POINTER` (new), `READ_ONLY_SURFACE_BULLETS` (new), Forbidden bullet inline in +`workflowPrimerLong()` (rewritten, sync comment adjacent). Composition stays flat string-array +assembly — no abstraction added for a five-constant module. + +## Dependencies + +**Internal (all pre-existing, none modified structurally):** +- `src/cli/commands/refresh.ts` (`buildWorkflowSection` → long variant), `src/delivery/claude-code-adapter.ts` and `src/cli/commands/discovery-helpers.ts` (short variant) — consume-only. +- `.claude/hooks/metta-guard-bash.mjs` + `src/templates/hooks/metta-guard-bash.mjs` — comment-only edits; the build's `copy-templates` and install's readdir copy ship the template unchanged in mechanism. +- `tests/metta-guard-mint-seam.test.ts` — precedent only (ADR-4 pin pattern, dual-copy fixture); not modified. + +**External:** none added. Vitest (existing) for tests; `node:fs` `readFileSync` in the seam +test (already used by sibling suites). No new packages, no network, no vendor lock-in; the one +external behavioral dependency (Claude Code PreToolUse exit-code semantics underpinning +"fail-closed") is pre-existing and documented in research-shared-manifest.md footnote 1. + +**Sequencing:** primer + tests first (red on old pins → green), then hook comments (both +copies), then docs README, then CLAUDE.md region, then full-suite run. + +## Risks & Mitigations + +1. **Drift recurrence between primer enumeration and hook allow-lists** (the approach's known + tradeoff). Mitigated three ways, in order of strength: the seam test turns any allow/block + list edit without a matching primer edit into a CI failure; the fail-closed guidance + converts residual drift (e.g. a consumer running an older primer) from false prohibition + into a harmless blocked attempt; cross-referencing sync comments catch the human editing + either file. +2. **Seam-test regex fragility** (hook refactor changes declaration shape; comment prose adds + quotes). Mitigated by comment-stripping before extraction and the sanity-floor assertions — + a broken regex fails the test loudly (extraction count below floor) instead of passing + vacuously. +3. **Hook copies diverging** (comment applied to one file only). Mitigated: seam test extracts + from both copies and asserts equality; final `diff` check during execute; the mint-seam + suite already runs both copies. +4. **CLAUDE.md hand-application mismatch** (region not byte-equal to `buildWorkflowSection()` + output → next refresh produces a surprise diff). Mitigated by the tsx diff verification + (§7); the post-ship `/metta-refresh` idempotence check is the backstop. +5. **Consumer propagation lag**: consumers keep the blanket-ban wording until their next + `metta refresh`/`install`. Accepted in intent.md (no push mechanism); the fix travels the + same path the bug did. +6. **Deployment-level skew**: a consumer upgrades metta and refreshes but runs a stale + installed hook (or vice versa) — primer may describe a slightly different surface than the + locally enforced one. Structural residual (research-shared-manifest.md §1.3); degraded + outcome is a blocked attempt plus the generation-time qualifier pointing at the hook as + authority. Accepted. +7. **Format-coupling in the seam test** (pins `a|b` and `a/b/c` join rendering). Deliberate: + both sides live in this repo; a rendering change fails one test and is fixed alongside it. +8. **Out-of-scope residuals, flagged for follow-up (not fixed here):** + `docs/internals/guard-hooks.md` carries a fourth hand-synced copy of the lists (currently + accurate; candidate backlog item per research.md). The short variant's closing line + `'Run `metta refresh` for the full command reference.'` names a Tier-2-blocked command as a + session instruction — pre-existing wording, harmless under fail-closed, left untouched to + stay within spec scope. The hook's unknown-command block message could print the allowed + surface (a drift-proof discoverability channel) — noted in research-generic-wording.md §7 as + a backlog candidate. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/gates.yaml b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/gates.yaml new file mode 100644 index 00000000..95e380fd --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/gates.yaml @@ -0,0 +1,18 @@ +finalized_at: 2026-08-26T05:28:04.696Z +all_passed: true +results: + - gate: stories-valid + status: pass + duration_ms: 565 + - gate: tests + status: pass + duration_ms: 537093 + - gate: lint + status: pass + duration_ms: 5272 + - gate: typecheck + status: pass + duration_ms: 5672 + - gate: build + status: pass + duration_ms: 6946 diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/intent.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/intent.md new file mode 100644 index 00000000..8c44ed01 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/intent.md @@ -0,0 +1,41 @@ +# fix-generated-workflow-primer-contradicts-bash-guard-blanket + +## Problem + +The workflow primer that `metta refresh` injects into every consumer project's CLAUDE.md `metta:workflow` region asserts a blanket ban — "AI orchestrators MUST invoke the matching metta skill — never call the CLI directly" — and its Forbidden section bans "any other `metta ` directly from an AI orchestrator session." The actual enforcement authority, the `metta-guard-bash.mjs` PreToolUse hook, deliberately permits a substantial read-only CLI surface: `ALLOWED_SUBCOMMANDS` (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`), `ALLOWED_TWO_WORD` (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), and `ALLOWED_BARE` (`roadmap`, `release`, `backlog` with flags), all checked before any block or identity gate, failing closed only on unrecognized commands. + +Because the primer wording is what orchestrator sessions read first — and refresh regenerates it from the `MANDATE` constant and Forbidden bullet in `src/delivery/workflow-primer.ts` — consumer sessions treat the blanket ban as authoritative and never discover the permitted commands. The concrete cost is documented: the zeus session (2026-08-26) spent hours grepping `spec/**` and `dist/` instead of running allowed queries, hand-computed a wrong milestone rollup (12 versus the CLI's correct 13), and told its operator that permitted read-only commands were forbidden. Worse, local corrections (zeus commit 919720e) sit inside the generated region and are silently reverted on the next `metta refresh`, so the misinformation is self-healing in the wrong direction. + +Affected parties: every AI orchestrator session in every metta consumer project (they read the generated primer), operators of those sessions (they receive false "that command is forbidden" claims and slower, less accurate answers), and metta's own contributors (the same blanket wording is hardcoded in metta's own CLAUDE.md workflow section and duplicated in `docs/workflows/README.md`). + +## Proposal + +Adopt candidate solution 1 — scope the primer to the mutating surface and document the permitted read-only surface — with solution 3's fail-closed guidance folded in as a safety valve against future drift. Specifically: + +1. **Rewrite the `MANDATE` constant in `src/delivery/workflow-primer.ts`** so the rule is scoped to state-mutating lifecycle commands rather than all CLI invocation. The mandate names `metta-guard-bash` as the enforcement authority and states that the guard permits a read-only query surface directly. The revised mandate remains identical across the short and long primer variants (preserving the existing consistency invariant documented in the file header). +2. **Rewrite the Forbidden bullet in `workflowPrimerLong()`** to enumerate the mutating command families that require skills (the guard's `BLOCKED_SUBCOMMANDS` and `BLOCKED_TWO_WORD` surface: `propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) instead of "any other `metta `". +3. **Add a "Read-only queries (permitted directly)" subsection** to the long primer variant enumerating the guard's current allow surface — the single-word, two-word, and bare forms listed above — plus the fail-closed guidance: the list mirrors the guard's allow-lists at generation time; when in doubt, attempt the command — the guard fails closed and will block anything unrecognized, so an attempt is always safe. The short variant gains a one-line pointer to the same fact (read-only queries are permitted; the guard fails closed). +4. **Apply the same corrected wording to metta's own CLAUDE.md** `Metta Workflow` section (via the regenerated primer, since refresh owns that region) **and to `docs/workflows/README.md`** ("Core rule: skills, not CLI" section), so all three copies agree. +5. **Keep the enumerated lists hand-synced with `metta-guard-bash.mjs` for now**, and add a sync-reminder comment in both `workflow-primer.ts` and the hook's allow-list blocks pointing at each other, so the next person editing either file knows the sibling must move with it. + +Scope is wording and documentation only: two exported functions in one TypeScript source file (plus its test file), one docs file, and metta's own CLAUDE.md region regenerated by refresh. No hook logic, no allow-list membership, and no CLI behavior changes. + +## Impact + +- **`src/delivery/workflow-primer.ts`** — `MANDATE`, the Forbidden bullet, and both `workflowPrimerShort()` / `workflowPrimerLong()` outputs change. Any snapshot or string-assertion tests against these functions (the near-1:1 test file `workflow-primer` tests) must be updated to match. +- **Every consumer project's CLAUDE.md** — the next `metta refresh` (or `metta init`/`install` scaffold) emits the corrected primer into the `metta:workflow` region. This is the desired propagation path: the fix reaches consumers the same way the bug did. Downstream sessions will begin using permitted read-only commands (`metta status`, `metta progress`, `metta issues list`, `metta milestone show`, etc.) instead of grepping — a behavior change, but the intended one. +- **Metta's own CLAUDE.md** — the `Metta Workflow` section wording changes on the next refresh. The "Doc-only fixes and edits to this workflow section itself are the exceptions" line and the rest of the section structure are preserved. +- **`docs/workflows/README.md`** — the "Core rule: skills, not CLI" section is reworded to match; its existing note that CLAUDE.md wins on drift is preserved. +- **`.claude/hooks/metta-guard-bash.mjs`** — comment-only change (sync-reminder pointing at the primer). Zero behavioral impact; the hook remains the enforcement authority and its decision logic is untouched. +- **Zeus and other consumers with local primer edits** — commit-919720e-style local corrections inside the generated region become unnecessary; the next refresh replaces them with correct upstream wording instead of reverting to wrong wording. + +Risk: the enumerated allow-list in the primer can drift from the hook again (the known tradeoff of solution 1). Mitigated by the fail-closed guidance (an out-of-date list degrades to "attempt it and let the guard decide" rather than to false prohibition) and the cross-referencing sync comments. + +## Out of Scope + +- **Generating the read-only list from a shared manifest** (candidate solution 2). Structurally eliminating drift requires the standalone `.mjs` hook and built TypeScript primer to share a source — either the hook importing from `dist/` (the stale-checkout hazard already logged as its own issue) or a new build-step copy. That coupling and build complexity is deliberately deferred; if drift recurs, log it as a follow-up backlog item referencing this change. +- **Changing which commands the guard allows or blocks.** The allow-lists and block-lists in `metta-guard-bash.mjs` are taken as-is; this change documents the existing surface, it does not renegotiate it. +- **Any change to guard hook decision logic, tiering, or credential handling** — the two-tier trust model description in the primer is untouched. +- **Fixing the stale-checkout `dist/` issue for hooks/statusline** — separately logged, separate change. +- **Adding new CLI read-only commands** or improving the CLI's query output (e.g., the milestone rollup itself was correct; no CLI fix needed). +- **Retroactively updating already-generated consumer CLAUDE.md files.** Propagation happens through each consumer's next `metta refresh`; no push mechanism is added. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-generic-wording.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-generic-wording.md new file mode 100644 index 00000000..c0ce4043 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-generic-wording.md @@ -0,0 +1,218 @@ +# Research: Generic wording fix without an enumerated list + +**Approach under evaluation** (candidate solution 3 from the issue): rewrite the primer's +`MANDATE` and Forbidden bullet to scope the ban to state-mutating commands, and keep the +read-only subsection generic — "the guard hook permits read-only status/list queries; when +in doubt attempt the command — the hook fails closed" — with no enumerated command list +that can go stale. + +## 1. Current state (verified in this worktree) + +### `src/delivery/workflow-primer.ts` (71 lines) + +- `MANDATE` (line 11): `"AI orchestrators MUST invoke the matching metta skill — never + call the CLI directly."` — shared verbatim by both variants (documented consistency + invariant in the file header, lines 6–8). +- `workflowPrimerShort()` (line 28): mandate + 3 entry points + 5-line trust-model block + + refresh pointer. Injected into consumer CLAUDE.md by `claude-code-adapter.ts:76` + (install scaffold) and `discovery-helpers.ts:143` (init). ~14 logical lines, though the + Tier-2 trust bullet is a very long single line. +- `workflowPrimerLong()` (line 43): adds the Forbidden section — line 60 is the offending + bullet banning "any other `` `metta ` `` directly from an AI orchestrator session" — + plus quick-mode routing and Research discipline. Injected by `refresh.ts:127` into the + `metta:workflow` region of every consumer CLAUDE.md. +- Injection cost today: long variant ≈ 27 content lines. A generic read-only paragraph adds + ~3–5 lines to the long variant and ~1–2 to the short; a full enumerated list (candidate + solution 1) would add ~12–15 lines to the long variant and grow with every allowlist + change. + +### `.claude/hooks/metta-guard-bash.mjs` — fail-closed confirmation + +The "when in doubt, attempt it" guidance is **safe**. Verified in `classify()` +(lines 660–680): the decision ladder is allow-lists → block-lists → `return 'unknown'`, +and `main()` treats `'unknown'` as a hard block (`process.exit(2)`, lines 982–992). There +is no fall-through allow for unrecognized `metta` subcommands. The only textual evasions +are the documented wrapper/indirection limitations (lines 138–157), which are irrelevant +to a cooperating session typing plain commands. Comments in the hook state the fail-closed +direction explicitly (e.g. lines 55, 59, 93). An attempted read-only-looking command can +therefore only succeed (it was allowed) or be blocked (exit 2) — it can never mutate state +by accident. + +### What a blocked attempt teaches the session + +Three relevant block messages: + +1. **Unknown subcommand** (lines 984–990): "Blocked unknown metta subcommand '``' … + Update the allowlist in metta-guard-bash.mjs if this is a legitimate read-only + command." — This *does* teach that a read-only allow surface exists and that this + particular command isn't on it. It does **not** list what is allowed, and the "update + the allowlist" advice is aimed at metta developers, not consumer sessions. +2. **Skill-enforced (Tier 1)** (lines 973–978): names the exact `/metta-` to use — + good redirection, no read-only information. +3. **Tier-2 block** (lines 1000–1006): points at CLAUDE.md and the credential model — no + read-only information. + +So the feedback loop is real but weak: a session that *tries* commands converges by trial +and error, one block message at a time. A session that never tries (because the primer +told it "never call the CLI") gets nothing — which is exactly the zeus failure. + +### Discoverability channels that survive without an enumerated list + +- **Bare `metta` is guard-allowed** (`classify()` line 671: no subcommand → allow) and + Commander prints the full top-level command listing (verified by running the installed + CLI: usage + every registered command with descriptions). A generic primer can therefore + point at bare `metta` as the self-updating command reference — zero drift, always + matches the installed CLI version. +- **Limitation of that channel:** drill-down help on two-word groups is blocked. `metta + milestone --help` and bare `metta milestone` both classify as `'unknown'` (milestone is + not in `ALLOWED_BARE`, and `--help`/absent third token is not in its + `ALLOWED_TWO_WORD` set {list, show}) → exit 2. The top-level listing shows that a + `milestone` command *exists* but not that `milestone show` specifically is permitted. + The same applies to `gaps`, `issues`, `gate`, `changes` (their bare forms are unknown → + blocked; only specific two-word forms are allowed). + +## 2. Wording sketch (generic-only variant) + +`MANDATE` replacement (shared by both variants, preserving the consistency invariant): + +> **AI orchestrators MUST invoke the matching metta skill for any state-mutating metta +> command — never run lifecycle commands like `metta propose`, `metta complete`, or +> `metta finalize` directly.** The `metta-guard-bash` PreToolUse hook is the enforcement +> authority: it permits read-only status/list queries directly and fails closed on +> everything else. (Humans running the CLI in a terminal are unaffected — this rule +> scopes to AI-driven sessions.) + +New read-only paragraph (long variant, after the entry points; one-line version in short): + +> **Read-only queries are permitted directly.** The guard hook allows the CLI's read-only +> query surface (status, progress, and list/show commands) from any session. Run bare +> `metta` for the current command listing. When in doubt, just attempt the command — the +> guard fails closed: anything unrecognized or state-mutating is blocked with an +> explanation, so an attempt is always safe and never mutates state. + +Forbidden bullet (line 60) replacement: + +> - Running any state-mutating `metta` command (lifecycle, backlog/milestone/roadmap/ +> release mutations, `changes abandon`) directly from an AI orchestrator session. Use +> the matching skill. Read-only queries are exempt — see "Read-only queries" above. + +Note the Forbidden bullet names mutating *families*, not an exhaustive command list — the +family names (lifecycle, backlog mutations, …) are stable even when individual subcommands +are added. + +## 3. Drift-resilience + +- **Generic-only: effectively zero drift surface.** The wording makes only two factual + claims about the hook: (a) it permits a read-only surface, (b) it fails closed. Both + are structural properties of the hook's architecture (allow-list-first + `'unknown'` → + block), not membership facts. Allowlist additions (`gaps`, `milestone`, `release + status` were all added after the primer was written — the comments at hook lines 53–63 + show this churn is real and recent) never invalidate the primer. +- **The bare-`metta` pointer is self-updating** — the help listing comes from the + installed CLI, so it is always current for that consumer's version. +- **Failure directionality is the key win:** if wording and hook ever disagree again, the + generic phrasing degrades to "attempt it and let the guard decide" — a mild efficiency + loss — rather than to false prohibition (the zeus failure mode). Candidate solution 1's + enumerated list degrades to *stale enumeration*, which recreates the original bug shape + (primer under-reports the allowed surface) as soon as the hook gains a new allow entry. + +## 4. Discoverability tradeoff — honest assessment + +The zeus failure had **two halves**, and generic wording fixes only one cleanly: + +1. **False prohibition** ("permitted commands are forbidden") — **fully fixed.** The + mandate no longer bans the read-only surface, and the fail-closed guidance gives + explicit permission to experiment. +2. **Discoverability** ("sessions never learned `metta milestone show` existed") — + **only partially fixed.** A generic primer tells the session *that* a permitted + surface exists and *how* to probe it (bare `metta`, trial-and-error against a + fail-closed guard), but not *which* commands to reach for. The specific zeus case is + instructive: to find `milestone show`, a session must (a) run bare `metta`, (b) notice + the `milestone` entry, (c) guess `show`/`list` as third words, because `metta + milestone --help` is itself blocked. That is a plausible but multi-step path; a + time-pressed session may still fall back to grepping `spec/**`. Trial-and-error also + burns turns: each probe that lands on an unlisted form costs a blocked Bash call and a + stderr lecture. + +Honest verdict: generic-only is a *correctness* fix with a *partial* discoverability fix. +It removes the reason sessions avoided the CLI but does not hand them the map. + +### Hybrid: generic rule + short curated examples list (recommended) + +Add one line of curated examples to the read-only paragraph, framed as **examples, not an +exhaustive enumeration**: + +> High-value examples: `metta status`, `metta progress`, `metta issues list`, +> `metta changes list`, `metta milestone list` / `metta milestone show `. + +Drift math for the hybrid is asymmetric in the safe direction: + +- **Additions to the hook's allowlists never invalidate it** — "examples" makes + incompleteness correct by construction. This kills the dominant drift mode (the hook + history shows additions, not removals). +- **Only removal/renaming of a listed command creates drift**, and the five commands above + are the most stable, load-bearing queries in the surface (`status` and `progress` are + the backbone of the status skills; `issues list`/`changes list`/`milestone show` are + exactly what zeus needed). Removal risk over the plausible life of the wording: low. +- Cost: one extra primer line, one extra test assertion, plus a two-line sync-reminder + comment in the hook's allowlist block pointing at the primer (as the intent already + proposes). + +The hybrid directly patches the discoverability half at the exact spot the evidence says +sessions fail (they needed `milestone show` and never found it), while keeping the drift +surface a fraction of candidate solution 1's full enumeration (5 stable examples vs ~25 +entries across three allow structures). + +## 5. Test impact + +Verified against `tests/delivery.test.ts` (the primer's test home): + +- **No existing test pins the current mandate or Forbidden wording.** Grep for "never + call the CLI" / "any other \`metta" across `tests/` matches nothing. Existing primer + assertions cover the Research-discipline section (lines 73–102) and Tier-2 trust-model + wording (lines 105–119) — none of that text changes, so **zero existing tests break**. +- New tests needed (same file, matching the existing `toContain` style): + 1. Both variants scope the mandate to state-mutating commands (e.g. contain + "state-mutating" and no longer contain "never call the CLI directly"). + 2. Both variants carry the fail-closed guidance ("fails closed" / "attempt the + command"). + 3. Long variant's Forbidden section no longer contains "any other \`metta \`". + 4. (Hybrid) long variant contains the curated examples line (e.g. `metta milestone + show`). +- Downstream: `installCommands` / refresh region tests consume the same functions, so + they follow automatically; docs updates (`docs/workflows/README.md` §"Core rule: + skills, not CLI" lines 45–51, and the blockquote in `docs/internals/guard-hooks.md` + lines 24–27) are untested prose. +- No snapshot tests exist for the primer — no snapshot churn. + +## 6. Effort estimate + +| Item | Generic-only | Hybrid (generic + 5 examples) | +|---|---|---| +| `workflow-primer.ts` rewrite (MANDATE, Forbidden bullet, new paragraph) | ~1h | +15min | +| `tests/delivery.test.ts` additions (3–4 cases) | ~30min | +10min | +| `docs/workflows/README.md` + `docs/internals/guard-hooks.md` wording | ~30min | ~30min | +| Sync-reminder comments in hook allowlist blocks | — | ~10min | +| Regenerate own CLAUDE.md region via refresh skill + review | ~15min | ~15min | +| **Total** | **~2–2.5h** | **~2.5–3h** | + +Both are quick-mode-sized: one source file + its test file + two docs files + a +comment-only hook touch; no logic changes anywhere. + +## 7. Recommendation + +**Adopt the hybrid: generic mutating-scoped mandate + fail-closed guidance + bare-`metta` +pointer + a five-command "examples" line.** Pure generic wording is the most +drift-resilient option and fully cures the false-prohibition failure, but the issue's own +evidence shows the costly half was discoverability — zeus needed one specific command +name it never saw. Five curated examples framed as non-exhaustive buy back that +discoverability at near-zero drift cost (additions can never make the list wrong; only +removal of a highly stable command could). If the maintainer weights drift-elimination +absolutely, generic-only is acceptable — but it should at minimum keep the bare-`metta` +pointer, since that is the only zero-drift channel through which a session can learn +concrete command names, and the guard verifiably permits it. + +Secondary observation (out of scope here, worth a backlog note): the hook's +unknown-command block message (lines 984–990) is the one surface that is *guaranteed* +in sync with the allowlists; having it print the allowed read-only surface on block would +be a drift-proof discoverability channel that no primer wording can match. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-scoped-wording.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-scoped-wording.md new file mode 100644 index 00000000..16ed0cf0 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-scoped-wording.md @@ -0,0 +1,227 @@ +# Research: Scoped wording rewrite with enumerated read-only list (hand-synced) + +Change: `fix-generated-workflow-primer-contradicts-bash-guard-blanket` +Approach researched: rewrite the primer `MANDATE` + Forbidden bullet to scope the ban to +state-mutating commands, name `metta-guard-bash` as enforcement authority, add a +"Read-only queries (permitted directly)" subsection enumerating the hook's current +allow-lists, propagate the wording to metta's own CLAUDE.md region (via refresh) and +`docs/workflows/README.md`, and add cross-referencing sync comments in both files. + +All paths below are relative to the change root +`/home/utx0/Code/metta/.metta/worktrees/fix-generated-workflow-primer-contradicts-bash-guard-blanket/`. + +--- + +## 1. Current state of the primer (`src/delivery/workflow-primer.ts`) + +- **`MANDATE` constant — lines 11–13.** Hardcodes the blanket ban: + `'**AI orchestrators MUST invoke the matching metta skill — never call the CLI directly.** (Humans running the CLI in a terminal are unaffected — this rule scopes to AI-driven sessions.)'` +- **Identical-mandate invariant** is maintained *structurally*, not by test: both variants + reference the same `MANDATE` constant (`workflowPrimerShort()` at line 32; `workflowPrimerLong()` + at line 47, where the long variant appends the broken-artifacts sentence via string + concatenation `MANDATE + ' The skills wrap …'`). The file header comment (lines 1–9) documents + the invariant in prose. No test currently asserts byte-identity across variants. +- **Forbidden bullet — line 60** in `workflowPrimerLong()`: + `'- Invoking `metta quick`, `metta propose`, `metta finalize`, `metta complete`, `metta issue`, or any other `metta ` directly from an AI orchestrator session. Use the matching skill.'` +- **Long-only content**: quick-mode routing paragraph (line 54), the exceptions line + `'Doc-only fixes and edits to this workflow section itself are the exceptions.'` (line 56 — + must be preserved per spec), `### Forbidden` (58–61), `### Research discipline` (63–69). +- **Shared bullets**: `ENTRY_POINTS_BULLETS` (15–19), `TRUST_MODEL_BULLETS` (21–26). Neither + needs changes for this approach; the Tier-2 trust wording tests pin their content. + +### Consumers of the two variants (all found via grep; no others exist) + +| Consumer | Variant | Effect | +|---|---|---| +| `src/cli/commands/refresh.ts:127` (`buildWorkflowSection()`) | long | Emitted into the `` region of CLAUDE.md by `metta refresh` | +| `src/delivery/claude-code-adapter.ts:76` (`formatContext()`) | short | Scaffold CLAUDE.md written by install | +| `src/cli/commands/discovery-helpers.ts:143` | short | CLAUDE.md scaffold written during `metta init` discovery | + +### Region-replacement mechanism (`metta refresh`) + +`src/cli/commands/refresh.ts`: +- `buildWorkflowSection()` (lines 123–160) prepends `## Metta Workflow`, splices in + `workflowPrimerLong()`, then appends the five skill-category lists. +- `replaceMarkerContent()` (lines 187–205) does plain `indexOf` replacement between + `startTag`/`endTag` marker pairs, appending the section if markers are absent. The workflow + markers are `` / `` (metta's own + CLAUDE.md carries them at lines 39/101). Anything inside the region — including local + hand-edits like zeus commit 919720e — is wholesale replaced. No changes needed to this + mechanism; it is the propagation path that carries the fix. + +--- + +## 2. Exact current guard allow/block surface (`.claude/hooks/metta-guard-bash.mjs`) + +The hook HAS grown beyond the issue text; the intent.md and spec.md for this change already +reflect the grown surface, and I verified them against the file. Authoritative as of this +worktree's copy: + +**`ALLOWED_SUBCOMMANDS` (lines 38–45)** — single-word, no credential needed: +`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, +`tokens`, `install`. +Note the hook's own comments concede `iteration` / `model-escalation` / `tokens` are +*instrumentation appenders* ("read-safe-ish", append-only records) and `install` is an +intentional human/CI pass-through — they are on the allow list but are not strictly read-only. +Wording in the primer subsection should hedge accordingly (see sketch below). + +**`ALLOWED_TWO_WORD` (lines 48–64)**: +`issues list`; `gate list`; `changes list`; `backlog list|show`; `gaps list|show`; +`milestone list|show`; `release status`. + +**`ALLOWED_BARE` (line 97)** — bare or flags-only third word (`metta roadmap --json` style): +`roadmap`, `release`, `backlog`. + +**`BLOCKED_SUBCOMMANDS` (lines 67–73)** — mutating, skill/credential-gated: +`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, +`refresh`, `import`, `init`, and `verify` (blocked because it *executes* gates — confirms the +issue's note that `verify` is now blocked). + +**`BLOCKED_TWO_WORD` (lines 76–87)**: +`backlog add|done|promote|migrate` (includes `migrate` — beyond the old primer text); +`changes abandon`; `milestone create|close|update`; `roadmap add|reorder|next|remove`; +`release cut`. + +**Other classify() behavior relevant to wording (lines 660–680):** any `--` operand terminator +fails closed regardless of allow-list membership; unknown subcommands fail closed +(`'unknown'` → block with "update the allowlist" message). Bare `metta` with no subcommand is +allowed. This fail-closed property is exactly what the drift safety-valve wording leans on. + +**Existing sibling documentation:** `docs/internals/guard-hooks.md` already tabulates +`ALLOWED_*` lists (line 65 area) — a fourth hand-synced copy to be aware of, though it appears +current and is out of this change's stated scope. + +--- + +## 3. Docs copy (`docs/workflows/README.md`) + +"Core rule: skills, not CLI" — lines 45–51: +- Line 47 repeats the blanket mandate verbatim. +- Line 49 repeats the "any other `metta `" ban and the stub-artifact prohibition. +- Line 51 carries the note "if this README drifts from it, `CLAUDE.md` wins" — spec requires + preserving this note. + +--- + +## 4. Tests that assert primer content (will need updating or extending) + +Grep across `tests/` for the mandate/Forbidden wording found exactly two files: + +1. **`tests/delivery.test.ts`** — the primer's near-1:1 test file: + - Line 61: `expect(formatted).toContain('AI orchestrators MUST invoke the matching metta skill')` + inside the `formatContext` test — **breaks if the mandate's opening clause is rephrased**; + update to the new scoped opening. + - Lines 72–103 (`Workflow primer research discipline rule`) — unaffected. + - Lines 105–120 (`Workflow primer Tier-2 trust model wording`) — unaffected + (TRUST_MODEL_BULLETS untouched). + - New assertions to add here per the change spec: mandate byte-identical across variants + (extract-and-compare, closing the currently untested invariant), no + `never call the CLI directly` / `any other \`metta \`` substring in either variant, + long variant contains `Read-only queries (permitted directly)` with the enumerated entries, + long variant names `metta-guard-bash`, short variant contains the one-line read-only + + fail-closed pointer, Forbidden section enumerates the mutating families. +2. **`tests/refresh.test.ts`** — `buildWorkflowSection` block (lines 106–130) asserts only + structural headings and skill names; **no breakage expected**, but worth one added + assertion that the emitted section contains the read-only subsection heading. + +Guard tests (`tests/metta-guard-bash.test.ts`, `tests/cli-metta-guard-bash-integration.test.ts`, +`tests/metta-guard-mint-seam.test.ts`, `tests/metta-guard-agent-dispatch.test.ts`) exercise hook +*behavior* only — a comment-only hook edit cannot break them (spec scenario "Hook diff is +comment-only" is verified by simply re-running this suite). `tests/commands-discovery-helpers.test.ts`, +`tests/cli-install.test.ts`, `tests/cli-skills.test.ts` carry no mandate-string assertions. + +--- + +## 5. Exact files/lines to change + +| File | Location | Change | +|---|---|---| +| `src/delivery/workflow-primer.ts` | lines 1–9 | Update header comment: invariant now covers the scoped mandate; note hand-sync with the guard hook | +| `src/delivery/workflow-primer.ts` | lines 11–13 | Rewrite `MANDATE` (sketch below) | +| `src/delivery/workflow-primer.ts` | new constants near line 14 | Add `READ_ONLY_SURFACE_BULLETS` (long) and a one-line short-variant pointer string, each with a sync comment pointing at `.claude/hooks/metta-guard-bash.mjs` `ALLOWED_*` blocks | +| `src/delivery/workflow-primer.ts` | line 39 area (`workflowPrimerShort`) | Insert the one-line read-only/fail-closed pointer | +| `src/delivery/workflow-primer.ts` | line 60 | Replace Forbidden bullet with enumerated mutating families (mirror `BLOCKED_SUBCOMMANDS` + `BLOCKED_TWO_WORD`, incl. `verify`, `backlog migrate`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) | +| `src/delivery/workflow-primer.ts` | after line 61 (or after trust-model bullets) | Add `### Read-only queries (permitted directly)` subsection + generation-time qualifier + fail-closed guidance | +| `.claude/hooks/metta-guard-bash.mjs` | comments above lines 37, 47, 89 (the three `ALLOWED_*` blocks) and optionally 66/75 (`BLOCKED_*`) | Comment-only: "Mirrored in src/delivery/workflow-primer.ts read-only subsection — keep in sync." | +| `docs/workflows/README.md` | lines 45–51 | Reword Core rule to the scoped mandate + read-only acknowledgement; keep line 51's "CLAUDE.md wins" note; keep the stub-artifact prohibition from line 49 | +| `CLAUDE.md` (metta's own) | region lines 39–101 | Regenerated from the long primer — via the `/metta-refresh` skill (direct `metta refresh` is itself Tier-2 blocked), or by hand-applying the exact `buildWorkflowSection()` output during execute and letting the next refresh confirm idempotence | +| `tests/delivery.test.ts` | line 61 + new describe block | Update/extend as itemized in section 4 | +| `tests/refresh.test.ts` | `buildWorkflowSection` block | Optional single added assertion | + +## 6. Proposed wording sketch + +**MANDATE (both variants, byte-identical):** + +> **State-mutating metta commands MUST go through the matching metta skill — never as direct CLI calls from an AI orchestrator session.** Enforcement authority is the `metta-guard-bash` PreToolUse hook: it blocks mutating and unrecognized commands (fail-closed) but permits a read-only query surface directly. (Humans running the CLI in a terminal are unaffected — this rule scopes to AI-driven sessions.) + +**Short-variant pointer (one line):** + +> Read-only queries (`metta status`, `metta progress`, `metta issues list`, …) are permitted directly; the guard fails closed, so attempting a query is always safe. + +**Forbidden bullet (long variant):** + +> - Invoking any state-mutating metta command directly from an AI orchestrator session: `propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`. Use the matching skill. + +**Read-only subsection (long variant):** + +> ### Read-only queries (permitted directly) +> +> The `metta-guard-bash` hook allows these directly (no skill needed). This list mirrors the hook's allow-lists at generation time — the hook, not this text, is authoritative: +> - Single-word: `status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install` +> - Two-word: `issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status` +> - Bare (flags only): `roadmap`, `release`, `backlog` (e.g. `metta roadmap --json`) +> +> When in doubt about a command not listed here, attempt it — the guard fails closed and blocks anything unrecognized, so an attempt is always safe. Never pass a `--` operand terminator; the guard rejects it unconditionally. + +Wording decision to surface at design time: the subsection title says "read-only" but +`iteration`/`model-escalation`/`tokens` append instrumentation records and `install` writes +scaffold files. Either hedge in prose ("read-only or instrumentation-only") or keep the spec's +exact title and add a parenthetical; the spec fixes the title, so hedge in the body. + +## 7. Tradeoffs of this approach + +**Pros** +- Smallest viable surface: one source file + one test file + one docs file + comment-only hook + edit; zero runtime/build coupling (matches the "template files never inlined... hooks stay + standalone `.mjs`" constraint that killed the shared-manifest option). +- Propagates through the existing refresh/init paths — the fix travels the same road the bug did. +- Fail-closed guidance converts future drift from "false prohibition" (hours lost, wrong + answers) into "harmless blocked attempt" — the failure mode becomes cheap. +- Spec/stories for this change are already written against exactly this approach; no spec rework. + +**Cons / risks** +- **Drift recurrence is real, not hypothetical**: the enumerated lists now live in at least four + hand-synced places (primer strings, metta's CLAUDE.md region [auto-derived], docs README, + `docs/internals/guard-hooks.md` tables) against one authority (the hook). Sync comments are + advisory only. +- The spec's "entries MUST match the hook at the time of this change" scenario is manual unless + automated (see mitigation). +- Consumer projects only get the fix on their next refresh — accepted in intent.md. + +**Recommended drift mitigation (cheap, in-scope, test-only):** add a Vitest case in +`tests/delivery.test.ts` that reads `.claude/hooks/metta-guard-bash.mjs` as text, extracts the +`ALLOWED_SUBCOMMANDS` / `ALLOWED_TWO_WORD` / `ALLOWED_BARE` entries with a regex, and asserts +each appears in `workflowPrimerLong()`. This makes drift a CI failure without any runtime or +build coupling between the hook and the primer — it delivers most of the shared-manifest +option's guarantee at ~30 lines of test code. Precedent exists: the mint-seam tests already pin +the `GRACE_MS` equality across the two hook files (see comment at `metta-guard-bash.mjs:118-119`). +If the executing agent judges this beyond the spec's letter, log it as a follow-up backlog item +instead; the spec only *requires* comments. + +## 8. Effort estimate + +Small. ~60–80 changed/added lines in `workflow-primer.ts`, ~10 comment lines in the hook, +~15 lines in docs README, ~50–70 test lines, plus the CLAUDE.md region regeneration. +One focused session (2–4 h) including the drift-guard test; verify with +`npx vitest run tests/delivery.test.ts tests/refresh.test.ts tests/metta-guard-bash.test.ts tests/cli-metta-guard-bash-integration.test.ts`. + +## 9. Recommendation + +Proceed with this approach as specced. It is the only option consistent with the project's +"hooks are standalone `.mjs`, templates never inlined" constraints, the change's intent.md +explicitly defers the shared-manifest alternative, and the drift risk — the approach's one real +weakness — is largely neutralizable with the test-only mirror check above plus the fail-closed +guidance already required by the spec. No blockers found; every file, line, and test named in +the spec exists where expected, and the hook's grown surface (`verify` blocked, `backlog +migrate`, `milestone` forms, `release status|cut`, bare `backlog`/`release`) is already +correctly captured in the change's spec.md enumerations. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-shared-manifest.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-shared-manifest.md new file mode 100644 index 00000000..6aa4e5a1 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research-shared-manifest.md @@ -0,0 +1,71 @@ +# Research: Shared manifest between metta-guard-bash.mjs and workflow-primer.ts + +**Change**: fix-generated-workflow-primer-contradicts-bash-guard-blanket +**Approach researched**: Candidate Solution 2 from the issue — extract the guard hook's allow-lists into a shared module/JSON manifest consumed by both `.claude/hooks/metta-guard-bash.mjs` and `src/delivery/workflow-primer.ts`, so `metta refresh` always emits exactly what the hook permits. +**Date**: 2026-08-26 + +## 1. Current architecture facts (verified in this worktree) + +### 1.1 How the guard hook is loaded and delivered + +- **Source of truth**: `src/templates/hooks/metta-guard-bash.mjs`. The repo's own installed copy at `.claude/hooks/metta-guard-bash.mjs` is byte-identical (verified with `diff`). +- **Build**: `npm run build` → `copy-templates` does `cp -r src/templates/hooks dist/templates/hooks` — the whole directory, readdir-blind. A new sibling file in `src/templates/hooks/` ships to dist with **zero build-script changes** (`package.json:18`). +- **Consumer delivery**: `metta install` copies **every file** in `templates/hooks/` into the consumer's `/.claude/hooks/` (readdir-driven, `src/cli/commands/install.ts:30-41,377`), then registers `.claude/hooks/metta-guard-bash.mjs` as a PreToolUse hook in `.claude/settings.json`. So a manifest placed beside the hook in `templates/hooks/` would be installed as a sibling automatically. +- **Runtime**: Claude Code executes the hook as a standalone Node process (`node .claude/hooks/metta-guard-bash.mjs`, event JSON on stdin). It is plain ESM — it already uses `import` for `node:` builtins, so **relative imports of sibling files work** (`import ... from './guard-allowlist.mjs'` or `readFileSync(new URL('./guard-allowlist.json', import.meta.url))`). The hook is currently **deliberately self-contained**: every filesystem read it performs (tokens, audit log) is wrapped with an explicit fail-open/fail-closed decision, and the header comments treat self-containedness as a security property. +- **The four allow/block structures** the primer would need: `ALLOWED_SUBCOMMANDS` (line 38), `ALLOWED_TWO_WORD` (line 48), `ALLOWED_BARE` (line 97), plus (for the Forbidden section) `BLOCKED_SUBCOMMANDS` / `BLOCKED_TWO_WORD` (lines 67, 76). Each entry carries substantial per-entry rationale **comments** — a JSON manifest loses those; an `.mjs` manifest keeps them. + +### 1.2 How the primer is built and consumed + +- `src/delivery/workflow-primer.ts` exports `workflowPrimerShort()` (used by `src/delivery/claude-code-adapter.ts:76` — init/install scaffold) and `workflowPrimerLong()` (used by `src/cli/commands/refresh.ts:127` — authoritative regeneration). Both are **pure functions returning string arrays built from TS string-literal constants** (`MANDATE`, `ENTRY_POINTS_BULLETS`, `TRUST_MODEL_BULLETS`). +- Convention tension already exists: the constitution says "Template files … are copied to `dist/` at build time — never inlined as string literals" and "No string literal templates in TypeScript code," yet the primer is exactly inlined string literals. A data manifest for the *command lists* would move toward convention compliance; the surrounding prose would remain literals either way (out of scope for this fix). +- Precedent for TS code reading shipped template data at runtime: `install.ts:41` resolves `new URL('../../templates/hooks/', import.meta.url)` from its dist location. The primer module could resolve `../templates/hooks/guard-allowlist.json` the same way, validated with a Zod schema per project convention. + +### 1.3 The stale-dist hazard (context, resolved but structurally relevant) + +Issue `hooks-and-statusline-execute-stale-main-checkout-dist-via` (logged 2026-08-11, since resolved and archived at `spec/archive/2026-08-11-fix-hooks-statusline-execute-stale-main-checkout-dist-via/`) documented that everything shelling through the globally-linked `dist/` tracks whatever was last built in the main checkout, not source. Two consequences for this approach: + +1. The hook must **never** import from the metta package's `dist/` — that resurrects the exact hazard class the archived fix addressed. A manifest must be a **sibling file in `.claude/hooks/`**, delivered by the same install copy as the hook itself, so hook + manifest version together atomically. +2. Even with a sibling manifest, a **deployment-level skew** remains: `metta refresh` renders the primer from the *package's* dist manifest, while enforcement runs the *consumer's installed copy* of hook + manifest. If a consumer upgrades metta and runs `refresh` without re-running `install`, the primer can describe a newer allow surface than the stale installed hook enforces. The manifest eliminates source-level drift only; it cannot eliminate installed-copy drift. + +### 1.4 Existing in-repo pattern for cross-file consistency: seam-test pins + +`tests/metta-guard-mint-seam.test.ts` already solves the "two standalone hooks must share a constant" problem **without** a shared import: the ADR-4 constant pin (line 209) asserts all four hook copies (source + deployed, mint + guard) contain the literal `const GRACE_MS = 3_600_000`, so drift fails CI loudly. This is the codebase's established answer to exactly this consistency class: duplicated-but-pinned, not runtime-shared. + +## 2. The manifest design (best version, if chosen) + +- **Location**: `src/templates/hooks/metta-guard-allowlist.mjs` (an ESM data module, not JSON — keeps the per-entry rationale comments, avoids any question about JSON-module import support in the hook, and ships/installs automatically via the existing readdir-driven copies). Exports plain data: `allowedSubcommands: string[]`, `allowedTwoWord: Record`, `allowedBare: string[]`, `blockedSubcommands: string[]`, `blockedTwoWord: Record`. +- **Hook side**: replace the five inline structures with a **dynamic `import()` inside try/catch**. On any load/shape failure: print a clear stderr message and `process.exit(2)` (fail closed, guard-wide). A static top-level `import` is **not acceptable**: a module-resolution failure crashes the process with exit code 1, and Claude Code treats any non-2 non-zero exit as a *non-blocking* error — the tool call proceeds. A missing manifest under static import would therefore **silently disable the entire guard (fail open)**, inverting the hook's security posture.[^1] Shape validation must be hand-rolled (the hook cannot depend on Zod — it runs standalone in consumer projects with no `node_modules` guarantee). +- **Primer side**: `workflow-primer.ts` reads the manifest at call time via `readFileSync(new URL('../templates/hooks/metta-guard-allowlist.mjs', import.meta.url))`? No — it's an .mjs module, so `await import()` instead, which forces `workflowPrimerShort/Long` **async** (their two call sites are sync today), or a build-time codegen step. The sync-preserving alternative is a JSON manifest read with `readFileSync` + Zod — but then the hook loses comments and needs `readFileSync`+`JSON.parse` too. Either variant works; JSON + Zod fits the "validate all state/config with Zod" convention better on the TS side, `.mjs` fits the hook better. There is no option that is idiomatic on both sides at once. +- **Build**: zero changes (`copy-templates` copies the hooks dir wholesale; `install` copies it readdir-driven). +- **Tests**: `tests/metta-guard-bash.test.ts`, `tests/cli-metta-guard-bash-integration.test.ts`, and `tests/metta-guard-mint-seam.test.ts` all spawn the hook files in place (source and deployed pairs), so sibling manifests must exist in **both** `src/templates/hooks/` and `.claude/hooks/` — plus new tests for the fail-closed missing/corrupt-manifest path and a primer-renders-manifest test. + +## 3. Failure modes + +| Failure | Behavior | Direction | +|---|---|---| +| Manifest missing/corrupt at hook runtime, static import | Process crash, exit 1 → Claude Code proceeds with the tool call[^1] | **Fail OPEN — unacceptable for a guard** | +| Manifest missing/corrupt, dynamic import + try/catch + exit 2 | Every Bash call in the session blocked with a diagnostic | Fail closed — safe but **highly disruptive**: blocks *all* Bash (not just metta), until install is re-run or the hook is disabled | +| Consumer runs `refresh` after upgrade without re-running `install` | Primer (from package dist) describes a different allow surface than the installed hook+manifest enforce | Drift persists at the deployment layer — the manifest does not fully deliver "drift is structurally impossible" | +| Partial install copy (hook copied, manifest not) | Same as row 2 | Only reachable via manual tampering; install copies the dir atomically enough in practice | + +The second row deserves emphasis: today the hook has **no way to hard-fail on its own configuration**, and its blast radius on exit 2 is the single offending command. The manifest introduces a new whole-session-blocking failure state that currently cannot exist. + +## 4. Effort estimate + +- Manifest module + hand-rolled shape validation in hook + fail-closed load path: ~0.5 day +- Primer refactor (async or JSON+Zod variant) + both call sites: ~0.5 day +- Test updates (three hook test suites' fixture expectations, new fail-closed tests, primer tests, deployed-copy sync): ~0.5–1 day +- **Total: roughly 1.5–2 days**, touching a security-critical hook that currently has extensive, carefully-reasoned self-contained behavior — review burden is high relative to the change's nature. + +## 5. Assessment and recommendation + +**The shared manifest is not justified for this bug.** Reasons: + +1. **It is a docs-drift bug.** The enforcement authority (the hook) was never wrong; only the generated prose was. The manifest re-architects the *correct* component (a security hook with deliberate self-containedness) to fix the *incorrect* one (strings in `workflow-primer.ts`). +2. **It introduces a new failure surface in a guard.** The only safe load strategy converts "manifest missing" into "all Bash blocked session-wide" — a failure state that cannot occur today. The unsafe strategy (static import) silently disables the guard entirely.[^1] +3. **It does not actually make drift impossible.** Installed-copy skew (refresh from new dist vs stale installed hook) survives — the same hazard family as the resolved stale-dist issue. +4. **The codebase already has an idiomatic, cheaper drift-proofing pattern.** The ADR-4 constant pin in `tests/metta-guard-mint-seam.test.ts` pins duplicated values across standalone files via CI assertions. The same pattern applies here directly: a seam test that extracts `ALLOWED_SUBCOMMANDS` / `ALLOWED_TWO_WORD` / `ALLOWED_BARE` entries from the hook source (both source and deployed copies) and asserts every entry appears in the primer's read-only subsection (and vice versa). Drift then fails `npm test` loudly at the moment someone edits the hook's allow-lists — "structurally impossible to ship drifted," achieved at CI time instead of runtime, with zero coupling added to the hook. + +**Recommendation**: reject the runtime manifest. Implement issue Candidate Solution 1 (rewrite `MANDATE` + Forbidden bullet to scope the ban to mutating commands, add a "Read-only queries (permitted directly)" subsection enumerating the allow surface, sync metta's own CLAUDE.md and `docs/workflows/README.md`), **hardened with a primer↔hook seam test** in the ADR-4 pin style so the hand-synced list cannot silently drift again. That captures ~all of the manifest's benefit at ~a third of the cost and zero new guard failure modes. If a future change makes the allow-lists user-configurable (a real runtime data need), revisit the sibling-`.mjs`-manifest design in section 2 — it is the correct shape for that world. + +[^1]: https://code.claude.com/docs/en/hooks accessed 2026-08-26 — PreToolUse exit-code semantics: "exit code 2 is the only exit code that blocks through the code alone… Claude Code treats exit code 1 as a non-blocking error and proceeds with the action." diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research.md new file mode 100644 index 00000000..9700b4ac --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/research.md @@ -0,0 +1,30 @@ +# Research: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +## Decision: Scoped wording rewrite with enumerated read-only list, hardened by a primer↔hook seam test + +### Approaches Considered + +1. **Scoped wording rewrite with enumerated read-only list (hand-synced)** (selected) — Rewrite `MANDATE` (lines 11–13) and the Forbidden bullet (line 60) in `src/delivery/workflow-primer.ts` to scope the ban to state-mutating commands, name `metta-guard-bash` as the enforcement authority, and add a "Read-only queries (permitted directly)" subsection enumerating the hook's current allow surface; sync `docs/workflows/README.md` and metta's own CLAUDE.md region; add cross-referencing sync comments in both primer and hook. Smallest viable surface (one source file, one test file, one docs file, comment-only hook edit), zero runtime/build coupling, propagates through the existing refresh/init paths, and matches the already-written spec.md exactly. Drift-recurrence risk is neutralized with a test-only mirror check (details in research-scoped-wording.md). +2. **Shared manifest consumed by hook and primer** — Rejected. Mechanically feasible (a sibling `.mjs`/JSON in `src/templates/hooks/` ships automatically through existing copy paths), but unjustified for a docs-drift bug: it re-architects the *correct* component (a deliberately self-contained security hook) to fix the *incorrect* one (primer strings). The only safe load strategy (dynamic import + exit 2) introduces a new whole-session all-Bash-blocked failure state; the unsafe strategy (static import) silently disables the guard on a missing manifest, because Claude Code treats exit 1 as non-blocking (per hooks docs). And it still does not make drift structurally impossible — installed-hook-copy vs package-dist skew survives. ~1.5–2 days of effort touching a security-critical hook vs hours for the wording fix. Details in research-shared-manifest.md. +3. **Generic wording fix without an enumerated list** — Not selected as-is, but two of its findings are folded into the selected approach. Pure generic wording has near-zero drift surface and fully cures the false-prohibition half of the zeus failure, yet only partially fixes discoverability — sessions learn a permitted surface *exists* but not that `metta milestone show` specifically does, and drill-down help (`metta milestone --help`) is itself blocked. The change's spec.md already mandates the enumerated list, so the enumeration stays; adopted from this track: the bare-`metta` pointer (bare `metta` is guard-allowed and prints the full command listing — a zero-drift, self-updating discovery channel) and confirmation that the fail-closed guidance is verifiably safe (`classify()` → `'unknown'` → exit 2; no fall-through allow). Details in research-generic-wording.md. + +### Rationale + +The bug is documentation drift, not enforcement error — the hook was never wrong. The fix therefore belongs in the wording, delivered through the same refresh path that propagated the wrong wording. The selected approach is the only one consistent with (a) the change's already-committed spec.md (five requirements, 17 scenarios, written against the enumerated-list design), (b) the project constraint that hooks stay standalone `.mjs` with no runtime coupling, and (c) the issue evidence that discoverability — a concrete command list — was the costly half of the zeus failure. + +The one real weakness of hand-syncing — drift recurrence — is addressed two ways: +- **Fail-closed guidance** converts future drift from "false prohibition" (hours lost, wrong answers) into "harmless blocked attempt": the primer explicitly qualifies the list as mirroring the hook at generation time and instructs sessions to attempt uncertain commands, because the guard fails closed. +- **Seam test (recommended, test-only)**: a Vitest case in `tests/delivery.test.ts` that regex-extracts the hook's `ALLOWED_SUBCOMMANDS` / `ALLOWED_TWO_WORD` / `ALLOWED_BARE` entries and asserts each appears in `workflowPrimerLong()`. This is the codebase's established pattern for cross-standalone-file consistency (precedent: the ADR-4 `GRACE_MS` constant pin in `tests/metta-guard-mint-seam.test.ts`) and delivers most of the shared-manifest guarantee at ~30 lines of test code with zero runtime coupling. + +Key implementation facts verified by the researchers: +- The hook surface has grown beyond the issue text and the change's spec.md already captures it correctly: `verify` is blocked; `backlog migrate`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut` blocked two-word forms; bare `backlog`/`release`/`roadmap` allowed. +- The identical-mandate invariant across both primer variants is structural (one shared `MANDATE` constant) but untested — the new tests should pin it. +- Test impact is confined to `tests/delivery.test.ts` (one existing assertion at line 61 pins the old mandate opening); refresh structural tests and all four guard test suites are unaffected by a comment-only hook edit. +- Wording nuance: `iteration`/`model-escalation`/`tokens` are instrumentation appenders and `install` is a pass-through — the "read-only" subsection title stays (spec fixes it) but body prose hedges accordingly. +- `docs/internals/guard-hooks.md` carries a fourth hand-synced copy of the lists; it appears current and is out of this change's scope (candidate follow-up note). + +### Artifacts Produced + +- [Research: scoped wording rewrite (selected)](research-scoped-wording.md) +- [Research: shared manifest (rejected)](research-shared-manifest.md) +- [Research: generic wording (partially folded in)](research-generic-wording.md) diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/review.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/review.md new file mode 100644 index 00000000..48ba766e --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/review.md @@ -0,0 +1,50 @@ +# Review: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +Three parallel reviews (correctness, security, quality) of commits c2e28796a, 58e07d015, +e5243acf3, 833aefa43, 8778db66b vs main. + +## Verdicts + +| Reviewer | Verdict | +|----------|---------| +| Correctness | PASS_WITH_WARNINGS | +| Security | PASS | +| Quality | PASS | + +No critical or major issues. All spec scenarios verified satisfied; enumerations +cross-checked in both directions against the hook's five lists (exact match); hook diff +confirmed comment-only with byte-identical copies (sha256 match); CLAUDE.md region verified +byte-exact against buildWorkflowSection(); lint/typecheck clean; full suite 2812 passed / +2 skipped / 0 failed. + +## Findings (all minor/trivial — none blocking) + +1. **minor** (correctness, quality — convergent) `tests/delivery.test.ts` — sanity floors + exist only for the three allowed-list extractions; `blockedSingle`/`blockedTwoWord` have + no floor, so a hook declaration-shape change could make the blocked-entry mirror test + pass vacuously. Suggested floors: >= 13 singles, >= 5 groups. +2. **minor** (correctness) `tests/delivery.test.ts` — seam test asserts hook ⊆ primer only; + a future primer entry with no hook counterpart (false "allowed" claim) would pass CI. + Design §5 intended the wording pins to cover the reverse direction but they pin only the + mandate and pointer literals. +3. **suggestion** (correctness, quality) `tests/delivery.test.ts` — `sliceBlock` runs before + comment stripping; a future inline comment containing `]);` inside a list declaration + would silently truncate extraction. Strip comments before slicing. +4. **minor** (security) `workflow-primer.ts` read-only subsection — "an attempt is always + safe and never mutates state" is strictly true only for plain literal invocations with + the hook active; "always" is a slight overclaim. Hook authority is stated; acceptable. +5. **minor** (security) — `install` sits under a "Read-only queries" heading; inline caveat + covers it ("guard-allowed, though not strictly read-only"). +6. **minor** (quality) `workflow-primer.ts` Tier-2 trust-model bullet — pre-existing drift + (not introduced here): omits `backlog migrate`, `milestone create/close/update`, + `verify`, `release cut` from the scoped two-word forms. Candidate follow-up issue. +7. **trivial** (quality) `tests/delivery.test.ts` — "Strip // line comments FIRST" comment + is ambiguous (means before quoted-string extraction, not before slicing). +8. **trivial** (quality) hook copies — blanket SYNC comment on BLOCKED lists mentions the + read-only subsection; blocked lists map only to the Forbidden bullet. Harmless. + +## Outcome + +Exit review loop after iteration 1: no critical issues; all three reviewers PASS or +PASS_WITH_WARNINGS. Findings 1-3 are test-hardening improvements against future drift; +finding 6 is pre-existing and a candidate follow-up issue. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/spec.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/spec.md new file mode 100644 index 00000000..aca1af9c --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/spec.md @@ -0,0 +1,162 @@ +# orchestration-guard + +## ADDED: Requirement: Workflow Primer Mandate Is Scoped to Mutating Commands + +The generated workflow primer (both the `workflowPrimerShort()` and `workflowPrimerLong()` outputs +emitted into the CLAUDE.md `metta:workflow` region) MUST scope its CLI prohibition to +state-mutating lifecycle commands only. The mandate MUST NOT assert a blanket ban on direct +`metta` CLI invocation ("never call the CLI directly" / "any other `metta `" wording), MUST +name the `metta-guard-bash` PreToolUse hook as the enforcement authority, and MUST state that the +guard permits a read-only query surface directly. The mandate text MUST be byte-identical across +the short and long primer variants, preserving the consistency invariant documented in the +`workflow-primer.ts` file header. The long variant's Forbidden section MUST enumerate the mutating +command families that require skills — the guard's blocked surface (`propose`, `quick`, `auto`, +`complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, +`verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, +`roadmap add/reorder/next/remove`, `release cut`) — instead of banning "any other `metta `". +Trace: intent Problem (blanket ban contradicts the guard's deliberate read-only allow surface); +intent Proposal items 1–2; US-1. + +### Scenario: Mandate is scoped, names the guard, and acknowledges the read-only surface +- GIVEN the rewritten `MANDATE` constant in `src/delivery/workflow-primer.ts` +- WHEN either `workflowPrimerShort()` or `workflowPrimerLong()` is rendered +- THEN the mandate states that state-mutating lifecycle commands require the matching skill, identifies `metta-guard-bash` as the enforcement authority, and states that the guard permits a read-only query surface directly + +### Scenario: Mandate is byte-identical across both variants +- GIVEN the rendered outputs of `workflowPrimerShort()` and `workflowPrimerLong()` +- WHEN their mandate text is compared +- THEN the mandate is byte-identical in both variants + +### Scenario: Forbidden section enumerates mutating families instead of a blanket ban +- GIVEN the long primer's Forbidden section +- WHEN an orchestrator reads it +- THEN it enumerates the mutating command families (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) and contains no "any other `metta `" blanket wording + +### Scenario: Primed session is directed to permitted queries for status questions +- GIVEN an orchestrator session primed with the corrected wording +- WHEN the operator asks a project-status question (e.g., a milestone rollup) +- THEN the primer's guidance directs the session toward a permitted read-only command rather than prohibiting all CLI use + + +## ADDED: Requirement: Workflow Primer Documents the Permitted Read-Only Surface + +The long primer variant MUST contain a "Read-only queries (permitted directly)" subsection +enumerating the guard's allow surface as it exists at generation time: the single-word allowed +subcommands (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, +`model-escalation`, `tokens`, `install`), the two-word allowed forms (`issues list`, `gate list`, +`changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), +and the bare allowed forms (`roadmap`, `release`, `backlog` invoked bare or with flags only). +Every enumerated entry MUST match the corresponding entry in the `ALLOWED_SUBCOMMANDS`, +`ALLOWED_TWO_WORD`, and `ALLOWED_BARE` lists in `.claude/hooks/metta-guard-bash.mjs` at the time +of this change — the primer documents the existing surface and MUST NOT add, remove, or +renegotiate any allow-list membership. The short primer variant MUST include a one-line statement +that read-only queries are permitted directly and that the guard fails closed. +Trace: intent Problem (consumer sessions never discover permitted commands; zeus 2026-08-26 +session); intent Proposal item 3; US-2. + +### Scenario: Long primer enumerates the full allow surface +- GIVEN the rendered `workflowPrimerLong()` output +- WHEN an orchestrator reads the "Read-only queries (permitted directly)" subsection +- THEN it lists the single-word allowed subcommands (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`), the two-word allowed forms (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), and the bare allowed forms (`roadmap`, `release`, `backlog` with flags) + +### Scenario: Enumerated lists match the hook's current allow-lists exactly +- GIVEN the enumerated lists in the long primer's read-only subsection +- WHEN compared against the `ALLOWED_SUBCOMMANDS`, `ALLOWED_TWO_WORD`, and `ALLOWED_BARE` blocks in `.claude/hooks/metta-guard-bash.mjs` at the time of this change +- THEN every entry matches the hook's current allow-lists exactly, with no additions or removals + +### Scenario: Short primer carries a one-line read-only pointer +- GIVEN the rendered `workflowPrimerShort()` output +- WHEN an orchestrator reads it +- THEN it includes a one-line statement that read-only queries are permitted directly and the guard fails closed + + +## ADDED: Requirement: Workflow Primer Carries Fail-Closed Guidance as a Drift Safety Valve + +The long primer's read-only subsection MUST qualify its enumerated list as mirroring the guard's +allow-lists at generation time — signaling that the hook, not the primer, is the authoritative +source — and MUST instruct that, when in doubt about a command not on the list, attempting the +command is safe because the guard fails closed and blocks anything unrecognized. Primer/guard +drift MUST therefore degrade to "attempt it and let the guard decide" rather than to false +prohibition. +Trace: intent Proposal item 3 (solution 3's fail-closed guidance folded in); intent Risk +(enumerated list can drift); US-3. + +### Scenario: Unlisted command triggers attempt-it guidance, not assumed prohibition +- GIVEN the long primer's read-only subsection +- WHEN an orchestrator encounters a `metta` command not on the enumerated list +- THEN the primer's guidance instructs it to attempt the command and rely on the guard's fail-closed blocking, rather than to assume prohibition + +### Scenario: List is qualified as a generation-time mirror of the guard +- GIVEN the primer text describing the enumerated read-only list +- WHEN it is read +- THEN it explicitly qualifies the list as mirroring the guard's allow-lists at generation time, identifying the hook as the authoritative source + + +## ADDED: Requirement: All Workflow-Rule Copies Agree Across Primer, CLAUDE.md, and Docs + +The three copies of the workflow rule — the generated primer, metta's own CLAUDE.md +`Metta Workflow` region (regenerated by `metta refresh`), and the "Core rule: skills, not CLI" +section of `docs/workflows/README.md` — MUST all carry the scoped mandate and acknowledge the +permitted read-only surface, and none MUST assert a blanket ban on direct `metta` CLI calls. +Regeneration of metta's own CLAUDE.md region MUST preserve the "Doc-only fixes and edits to this +workflow section itself are the exceptions" line and the existing section structure. The docs +README rewording MUST preserve its existing note that CLAUDE.md wins on drift. The corrected +wording MUST propagate to consumer projects through the existing generation paths — the next +`metta refresh` (long variant) or `metta init`/`install` scaffold (short variant) — replacing any +prior region content, including old blanket-ban wording or local hand-edits inside the generated +region. +Trace: intent Proposal item 4; intent Impact (consumer propagation, zeus commit 919720e); US-4; +US-5. + +### Scenario: Metta's own CLAUDE.md region regenerates with corrected wording and preserved structure +- GIVEN metta's own CLAUDE.md +- WHEN its `metta:workflow` region is regenerated from the corrected primer via refresh +- THEN the scoped mandate, enumerated Forbidden families, and read-only subsection appear, while the "Doc-only fixes and edits to this workflow section itself are the exceptions" line and the section structure are preserved + +### Scenario: Docs README core-rule section matches the scoped rule +- GIVEN `docs/workflows/README.md` +- WHEN the "Core rule: skills, not CLI" section is read +- THEN its wording matches the scoped rule and permitted read-only surface, and its existing note that CLAUDE.md wins on drift is preserved + +### Scenario: No copy asserts a blanket ban +- GIVEN the three copies (generated primer, metta's CLAUDE.md workflow region, docs README) +- WHEN they are compared +- THEN none asserts a blanket ban on direct `metta` CLI calls + +### Scenario: Refresh replaces old blanket-ban wording in a consumer project +- GIVEN a consumer project whose `metta:workflow` region carries the old blanket-ban primer +- WHEN `metta refresh` runs with the fixed version installed +- THEN the region contains the scoped mandate, the enumerated Forbidden families, and the read-only subsection + +### Scenario: Refresh over local hand-edits yields correct upstream wording +- GIVEN a consumer project with local hand-edits inside the generated region (the zeus commit-919720e pattern) +- WHEN refresh regenerates the region +- THEN the replacement wording is the correct upstream content, so losing the local edits no longer reintroduces misinformation + + +## ADDED: Requirement: Cross-Referenced Sync Reminders Between Primer and Guard Allow-Lists + +The hand-synced enumerated lists MUST carry cross-referencing maintenance comments: +`src/delivery/workflow-primer.ts` MUST contain a comment adjacent to its enumerated command lists +pointing at `.claude/hooks/metta-guard-bash.mjs` as the source that must stay in sync, and the +allow-list blocks in `.claude/hooks/metta-guard-bash.mjs` MUST contain a comment pointing back at +`workflow-primer.ts`. The change to the hook file MUST be comment-only: the guard's decision +logic, allow-list membership, block-list membership, tiering, and credential handling MUST behave +identically before and after the change. +Trace: intent Proposal item 5; intent Impact (`metta-guard-bash.mjs` comment-only, zero +behavioral impact); US-6. + +### Scenario: Primer lists point at the hook +- GIVEN `src/delivery/workflow-primer.ts` +- WHEN a maintainer reads the enumerated command lists +- THEN an adjacent comment points at `.claude/hooks/metta-guard-bash.mjs` as the source that must stay in sync + +### Scenario: Hook allow-lists point back at the primer +- GIVEN `.claude/hooks/metta-guard-bash.mjs` +- WHEN a maintainer reads the allow-list blocks +- THEN an adjacent comment points back at `src/delivery/workflow-primer.ts` + +### Scenario: Hook diff is comment-only with zero behavior change +- GIVEN the hook file after the change +- WHEN its decision logic is exercised across allowed, blocked, and unknown invocations +- THEN behavior is identical to before the change — the only diff is comments diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/stories.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/stories.md new file mode 100644 index 00000000..25695084 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/stories.md @@ -0,0 +1,89 @@ +# fix-generated-workflow-primer-contradicts-bash-guard-blanket — User Stories + +## US-1: Primer ban scoped to mutating commands only + +**As an** AI orchestrator session in a metta consumer project +**I want to** read a workflow primer whose CLI ban covers only state-mutating lifecycle commands, not all `metta` invocation +**So that** I stop treating the guard's permitted read-only queries as forbidden and can answer operator questions with authoritative CLI output instead of slow, error-prone hand-computation over `spec/**` files +**Priority:** P1 +**Independent Test Criteria:** The regenerated primer (both `workflowPrimerShort()` and `workflowPrimerLong()` outputs) contains no blanket "never call the CLI directly" / "any other `metta `" wording; its mandate is scoped to mutating commands, names `metta-guard-bash` as the enforcement authority, and is identical across both variants. + +**Acceptance Criteria:** +- **Given** the rewritten `MANDATE` constant in `src/delivery/workflow-primer.ts` **When** either `workflowPrimerShort()` or `workflowPrimerLong()` is rendered **Then** the mandate states that state-mutating lifecycle commands require the matching skill, identifies `metta-guard-bash` as the enforcement authority, and states that the guard permits a read-only query surface directly. +- **Given** the two primer variants **When** their mandate text is compared **Then** the mandate is byte-identical in both, preserving the consistency invariant documented in the file header. +- **Given** the long primer's Forbidden section **When** an orchestrator reads it **Then** it enumerates the mutating command families (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) instead of banning "any other `metta `". +- **Given** an orchestrator session primed with the corrected wording **When** the operator asks a project-status question (e.g., a milestone rollup) **Then** the primer directs the session toward a permitted read-only command rather than prohibiting all CLI use. + +--- + +## US-2: Permitted read-only surface documented in the primer + +**As an** AI orchestrator session in a metta consumer project +**I want to** see an explicit "Read-only queries (permitted directly)" list in the primer +**So that** I discover commands like `metta status`, `metta progress`, `metta issues list`, and `metta milestone show` exist and are allowed, instead of never learning about them and grepping `spec/**` to reconstruct answers the CLI already computes correctly +**Priority:** P1 +**Independent Test Criteria:** The long primer variant contains a read-only-queries subsection enumerating the guard's single-word, two-word, and bare allowed forms, and the short variant contains a one-line pointer stating read-only queries are permitted and the guard fails closed. + +**Acceptance Criteria:** +- **Given** the rendered `workflowPrimerLong()` output **When** an orchestrator reads the "Read-only queries (permitted directly)" subsection **Then** it lists the guard's `ALLOWED_SUBCOMMANDS` (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`), `ALLOWED_TWO_WORD` forms (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), and `ALLOWED_BARE` forms (`roadmap`, `release`, `backlog` with flags) as they exist at generation time. +- **Given** the rendered `workflowPrimerShort()` output **When** an orchestrator reads it **Then** it includes a one-line statement that read-only queries are permitted directly and the guard fails closed. +- **Given** the enumerated lists in the long primer **When** compared against `metta-guard-bash.mjs` at the time of this change **Then** every entry matches the hook's current allow-lists exactly (documenting the surface, not renegotiating it). + +--- + +## US-3: Fail-closed guidance as a drift safety valve + +**As an** AI orchestrator session working from a primer that may lag behind the guard hook +**I want to** be told that the guard fails closed and that attempting an uncertain command is always safe +**So that** future drift between the primer's enumerated lists and the hook's actual allow-lists degrades to "attempt it and let the guard decide" rather than back to false prohibition +**Priority:** P1 +**Independent Test Criteria:** The long primer's read-only subsection states that the list mirrors the guard's allow-lists at generation time and that, when in doubt, attempting the command is safe because the guard fails closed and blocks anything unrecognized. + +**Acceptance Criteria:** +- **Given** the long primer's read-only subsection **When** an orchestrator encounters a command not on the enumerated list **Then** the primer's guidance instructs it to attempt the command and rely on the guard's fail-closed blocking, rather than to assume prohibition. +- **Given** the primer text **When** it describes the enumerated list **Then** it explicitly qualifies the list as mirroring the guard's allow-lists at generation time, signaling the hook — not the primer — is authoritative. + +--- + +## US-4: All three wording copies agree + +**As a** metta contributor working in the metta repo itself +**I want to** see metta's own CLAUDE.md `Metta Workflow` section and `docs/workflows/README.md` carry the same corrected, scoped wording as the generated primer +**So that** I am not misled by the same blanket ban in-house, and no document contradicts the guard's actual behavior anywhere in the project +**Priority:** P2 +**Independent Test Criteria:** After the change, metta's CLAUDE.md `metta:workflow` region (regenerated via refresh) and the "Core rule: skills, not CLI" section of `docs/workflows/README.md` both scope the ban to mutating commands and acknowledge the permitted read-only surface, with no remaining blanket-ban wording. + +**Acceptance Criteria:** +- **Given** metta's own CLAUDE.md **When** its workflow region is regenerated from the corrected primer **Then** the scoped mandate, enumerated Forbidden families, and read-only subsection appear, while the "Doc-only fixes and edits to this workflow section itself are the exceptions" line and section structure are preserved. +- **Given** `docs/workflows/README.md` **When** the "Core rule: skills, not CLI" section is read **Then** its wording matches the scoped rule and permitted read-only surface, and its existing note that CLAUDE.md wins on drift is preserved. +- **Given** the three copies (generated primer, metta's CLAUDE.md region, docs README) **When** compared **Then** none asserts a blanket ban on direct `metta` CLI calls. + +--- + +## US-5: Correct wording propagates and survives refresh + +**As an** operator of a metta consumer project (e.g., zeus) +**I want to** have the next `metta refresh` (or `metta init`/`install` scaffold) emit the corrected primer into my project's `metta:workflow` region +**So that** my sessions stop reporting permitted commands as forbidden, hand-patched local corrections inside the generated region become unnecessary, and refresh no longer silently reverts fixes back to wrong wording +**Priority:** P1 +**Independent Test Criteria:** Running refresh in a consumer project whose CLAUDE.md contains the old blanket-ban wording (or commit-919720e-style local corrections) replaces the region with the corrected upstream primer. + +**Acceptance Criteria:** +- **Given** a consumer project whose `metta:workflow` region carries the old blanket-ban primer **When** `metta refresh` runs with the fixed version installed **Then** the region contains the scoped mandate, the enumerated Forbidden families, and the read-only subsection. +- **Given** a consumer project with local hand-edits inside the generated region (the zeus commit-919720e pattern) **When** refresh regenerates the region **Then** the replacement wording is correct upstream content, so losing the local edits no longer reintroduces misinformation. +- **Given** a downstream session reading the regenerated primer **When** the operator asks for a milestone rollup **Then** the session runs the permitted CLI query (e.g., `metta milestone show`) and reports the CLI's authoritative number instead of a hand-computed one. + +--- + +## US-6: Cross-referenced sync reminders for maintainers + +**As a** metta maintainer editing the guard hook or the primer +**I want to** find sync-reminder comments in both `workflow-primer.ts` and the allow-list blocks of `metta-guard-bash.mjs` pointing at each other +**So that** whoever changes the allow surface or the documented list knows the sibling file must move with it, reducing the chance of a fresh primer/guard contradiction +**Priority:** P2 +**Independent Test Criteria:** Both files contain a comment referencing the other by path near the hand-synced lists, and the hook change is comment-only with zero behavioral difference. + +**Acceptance Criteria:** +- **Given** `src/delivery/workflow-primer.ts` **When** a maintainer reads the enumerated command lists **Then** an adjacent comment points at `metta-guard-bash.mjs` as the source that must stay in sync. +- **Given** `.claude/hooks/metta-guard-bash.mjs` **When** a maintainer reads the allow-list blocks **Then** an adjacent comment points back at `workflow-primer.ts`. +- **Given** the hook file after the change **When** its decision logic is exercised **Then** behavior is identical to before — the only diff is comments. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/summary.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/summary.md new file mode 100644 index 00000000..cc34bf60 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/summary.md @@ -0,0 +1,69 @@ +# Summary: fix-generated-workflow-primer-contradicts-bash-guard-blanket + +## What changed + +The generated workflow primer claimed a blanket ban on direct `metta` CLI calls while the +`metta-guard-bash` PreToolUse hook actually permits a read-only query surface. This change +scopes the primer's mandate to state-mutating commands, documents the permitted read-only +surface, and syncs all wording copies — hardened by a seam test so the hand-synced lists +cannot silently drift again. + +## Implementation (by task) + +- **Task 1.1** (`c2e28796a`) — `src/delivery/workflow-primer.ts`: rewrote the shared + `MANDATE` constant (scoped to state-mutating commands, names `metta-guard-bash` as the + enforcement authority, fail-closed framing, humans-in-terminal carve-out preserved); added + `READ_ONLY_POINTER` (short variant) and `READ_ONLY_SURFACE_BULLETS` — the + `### Read-only queries (permitted directly)` subsection (long variant) enumerating the + hook's single-word, two-word, and bare allow surface with a generation-time qualifier, + bare-`metta` discovery pointer, and attempt-it fail-closed guidance; rewrote the Forbidden + bullet to enumerate the full blocked surface (including `verify`, `backlog migrate`, + `milestone`/`roadmap` mutating forms, `release cut`); added SYNC comments. + `tests/delivery.test.ts`: updated the old mandate pin; new "Workflow primer scoped + mandate" describe (byte-identity pin, blanket-wording absence, subsection/authority/ + fail-closed pins, preservation pins); new "Workflow primer / guard allow-list seam" + describe extracting the hook's `ALLOWED_*`/`BLOCKED_*` entries from both hook copies and + asserting each appears in the rendered primer (ADR-4 pin pattern). +- **Task 1.2** (`58e07d015`) — comment-only SYNC annotations above all five list + declarations in BOTH `src/templates/hooks/metta-guard-bash.mjs` and + `.claude/hooks/metta-guard-bash.mjs`; copies remain byte-identical; all four guard suites + pass unchanged (zero behavioral diff). +- **Task 1.3** (`e5243acf3`) — `docs/workflows/README.md` "Core rule: skills, not CLI": + scoped mandate + read-only acknowledgment pointing at CLAUDE.md's subsection; mutating + surface enumerated; stub-prohibition sentence and "CLAUDE.md wins" note preserved + verbatim; blanket wording removed. +- **Task 2.1** (`833aefa43`) — `tests/refresh.test.ts`: pinned the read-only subsection in + `buildWorkflowSection()` output. +- **Task 2.2** (`8778db66b`) — metta's own `CLAUDE.md` `metta:workflow` region regenerated + byte-exact from `buildWorkflowSection()` via a scratchpad tsx splice (direct + `metta refresh` is guard-blocked for executors); no edits outside the marker region. +- **Task 3.1** — verification sweep: PASS, no fixes needed. + +## Verification evidence + +- `npm test`: 135 files, 2812 passed / 2 skipped, 0 failed +- `npx tsc --noEmit`: clean +- Hook copies: `diff` empty (template vs deployed byte-identical) +- CLAUDE.md workflow region: byte-exact match against `buildWorkflowSection()` output + +## Notes + +- Exported primer API unchanged (`workflowPrimerShort()` / `workflowPrimerLong()`); consumer + projects receive the corrected wording on their next `metta refresh` / install scaffold. +- Known residual (out of scope, per design): `docs/internals/guard-hooks.md` carries a + fourth hand-synced copy of the allow-lists; deployment-level skew (consumer refresh + without reinstall) is not addressed by this change. + +## Verification (3 parallel verifiers, iteration 1) + +- **Test suite**: no deterministic failures attributable to the change. Full-suite runs on a + loaded machine hit 10s CLI-fixture timeout flakes (SIGTERM/exit 143, different test set + each run); every failed subset passes in isolation (151/151 final), and an earlier + fully green solo run recorded 135 files, 2812 passed / 2 skipped / 0 failed. +- **Typecheck/lint**: `npx tsc --noEmit` clean, `npm run lint` clean (exit 0). +- **Spec coverage**: PASS — all 15 scenarios across the 5 requirements have concrete + evidence (test names in tests/delivery.test.ts / tests/refresh.test.ts, or file/line + citations for doc-content scenarios). Both consumer-refresh scenarios rest on composed + evidence (content-agnostic region replacement + primer content pins) — sound, noted. +- **Review**: correctness PASS_WITH_WARNINGS (minor test-hardening suggestions), security + PASS, quality PASS. No critical or major findings; see review.md. diff --git a/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/tasks.md b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/tasks.md new file mode 100644 index 00000000..8d3740c5 --- /dev/null +++ b/spec/archive/2026-08-26-fix-generated-workflow-primer-contradicts-bash-guard-blanket/tasks.md @@ -0,0 +1,58 @@ +# Tasks for fix-generated-workflow-primer-contradicts-bash-guard-blanket + +## Batch 1 (no dependencies) + +- [x] **Task 1.1: Rewrite workflow primer and update/extend its test suite** + - **Files**: `src/delivery/workflow-primer.ts`, `tests/delivery.test.ts` + - **Action**: Implement design §1 in `workflow-primer.ts` — exported API unchanged (`workflowPrimerShort(): string[]`, `workflowPrimerLong(): string[]`): + 1. Update the file header (lines 1–9): scoped mandate description, byte-identity invariant now test-pinned, enumerated lists hand-synced with `metta-guard-bash.mjs` and guarded by the seam test in `tests/delivery.test.ts`. + 2. Rewrite the single shared `MANDATE` constant to the exact wording in design §1 (scoped to state-mutating commands, names `metta-guard-bash` PreToolUse hook as enforcement authority, states the guard blocks mutating/unrecognized commands fail-closed but permits a read-only query surface directly, keeps the humans-in-terminal carve-out). Keep the long variant's existing appended broken-artifacts sentence (line 47 concatenation) outside the constant. + 3. Add new constant `READ_ONLY_POINTER` (exact one-line wording, design §1) and insert it in `workflowPrimerShort()` immediately after `MANDATE`, blank-line separated, before `'Primary entry points:'`. + 4. Add new constant `READ_ONLY_SURFACE_BULLETS` rendering the `### Read-only queries (permitted directly)` subsection exactly per design §1: generation-time qualifier + "the hook, not this text, is authoritative"; single-word list (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`) with the not-strictly-read-only hedge; two-word list (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`); bare list (`roadmap`, `release`, `backlog`, e.g. `metta roadmap --json`); bare-`metta` discovery sentence; attempt-it fail-closed guidance. Place in `workflowPrimerLong()` after the `### Forbidden` bullets, before `### Research discipline` (ADR-D). + 5. Rewrite the Forbidden bullet (long variant, line 60) to enumerate the full blocked surface exactly per design §1: `propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut` — no "any other `metta `" blanket wording. + 6. Add the SYNC comment (exact two-line text from design §2, direction reversed to point at the hook) adjacent to the enumerated lists in `workflow-primer.ts`. + 7. Preserve verbatim: the doc-only-exceptions line, the stub-prohibition sentence (the second Forbidden bullet — do NOT alter or restate it), `ENTRY_POINTS_BULLETS`, `TRUST_MODEL_BULLETS`, the quick-mode paragraph, the full `### Research discipline` section, and the short variant's closing refresh-reference line. + + In `tests/delivery.test.ts` (design §§5–6): + 1. Update the line-61 `formatContext` pin to `toContain('State-mutating metta commands MUST go through the matching metta skill')`. + 2. New describe "Workflow primer scoped mandate": full-mandate duplicated-literal byte-identity pin asserted `toContain` on both variants joined with `'\n'` (ADR-B); neither variant contains `'never call the CLI directly'` nor the old blanket "any other `metta `" phrase; long variant contains `'### Read-only queries (permitted directly)'`, `'metta-guard-bash'`, `'at generation time'`, `'the hook, not this text, is authoritative'`, `'attempt it'` and `'fails closed'`; short variant pins the full `READ_ONLY_POINTER` string and does NOT contain the `###` read-only heading; preservation pins for the doc-only-exceptions line and the stub-prohibition bullet. + 3. New describe "Workflow primer / guard allow-list seam" (design §5, ADR-4 pin pattern): `readFileSync` both `src/templates/hooks/metta-guard-bash.mjs` and `.claude/hooks/metta-guard-bash.mjs` resolved from `join(import.meta.dirname, '..')`; extraction helper slices each declaration block (`ALLOWED_SUBCOMMANDS = new Set([`, `ALLOWED_TWO_WORD = new Map([`, `ALLOWED_BARE = new Set([`, plus `BLOCKED_SUBCOMMANDS` / `BLOCKED_TWO_WORD`) to its closing `]);`, strips `//` line comments FIRST, then collects quoted strings (`/'([^']+)'/g`; two-word groups via `/\['([a-z-]+)',\s*new Set\(\[([^\]]+)\]\)/g`). Assert: sanity floors (>= 9 single-word, >= 7 two-word groups, >= 3 bare); template/deployed extraction deep-equality; every allowed entry appears in `workflowPrimerLong().join('\n')` in rendered form (backticked single words, two-word groups joined `|`, bare entries within the "Bare (flags only)" line); every blocked entry appears in the Forbidden bullet (two-word groups joined `/` in hook order). + - **Verify**: `npx vitest run tests/delivery.test.ts` passes (seam test extracts from current hook files — no hook edits needed for it to pass); `npx tsc --noEmit` clean. + - **Done**: Both primer variants carry the new scoped mandate byte-identically; long variant has the read-only subsection between Forbidden and Research discipline and the enumerated Forbidden bullet; short variant has the pointer line and no subsection; all new and updated assertions in `tests/delivery.test.ts` pass; no exported-API or consumer changes. + +- [x] **Task 1.2: Comment-only SYNC annotations in both guard hook copies** + - **Files**: `src/templates/hooks/metta-guard-bash.mjs`, `.claude/hooks/metta-guard-bash.mjs` + - **Action**: Per design §2, insert the exact two-line SYNC comment (`// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden` / `// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift.`) directly above each of the five list declarations: `ALLOWED_SUBCOMMANDS`, `ALLOWED_TWO_WORD`, `ALLOWED_BARE`, `BLOCKED_SUBCOMMANDS`, `BLOCKED_TWO_WORD`. Edit the template copy first, then mirror the identical lines to the deployed copy. ZERO non-comment changes — no code line may change: decision logic, list membership, tiering, and credential handling must be byte-identical apart from the inserted comments. + - **Verify**: `diff src/templates/hooks/metta-guard-bash.mjs .claude/hooks/metta-guard-bash.mjs` produces no output; `npx vitest run tests/metta-guard-bash.test.ts tests/cli-metta-guard-bash-integration.test.ts tests/metta-guard-mint-seam.test.ts tests/metta-guard-agent-dispatch.test.ts` all pass unchanged (the spec's "Hook diff is comment-only" scenario). + - **Done**: Both copies remain byte-identical, carry the SYNC comment above all five list declarations, and all four guard suites pass with zero test modifications. + +- [x] **Task 1.3: Rewrite docs/workflows/README.md "Core rule: skills, not CLI"** + - **Files**: `docs/workflows/README.md` + - **Action**: Per design §3 (lines 45–51): keep the heading; replace the two body paragraphs with the exact proposed text — scoped mandate naming `metta-guard-bash` as enforcement authority with the fail-closed/read-only acknowledgment pointing at CLAUDE.md's Read-only queries subsection; second paragraph enumerating the full mutating surface (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) with the broken-artifacts issue reference. Copy the existing stub-prohibition sentence from the current README verbatim (do not retype or paraphrase it). Preserve the third paragraph (the "CLAUDE.md wins" note, line 51) verbatim. Remove all blanket "any other `metta `" wording. + - **Verify**: `grep -c 'any other' docs/workflows/README.md` returns 0 matches in the Core rule section; `grep -q 'metta-guard-bash' docs/workflows/README.md` succeeds; visual diff confirms the CLAUDE.md-wins note and stub-prohibition sentence are unchanged. + - **Done**: Core rule section carries the scoped mandate and read-only acknowledgment, enumerates the mutating families, preserves the stub-prohibition sentence and the CLAUDE.md-wins note, and contains no blanket-ban wording. + +## Batch 2 (depends on Batch 1) + +- [x] **Task 2.1: Pin read-only subsection in buildWorkflowSection output** + - **Depends on**: Task 1.1 + - **Files**: `tests/refresh.test.ts` + - **Action**: In the existing `buildWorkflowSection` describe block, add one assertion: `expect(result).toContain('### Read-only queries (permitted directly)')` — pinning that the refresh-emitted region carries the subsection (design §6). + - **Verify**: `npx vitest run tests/refresh.test.ts` passes. + - **Done**: The new assertion is present and green; no other assertions in the file changed. + +- [x] **Task 2.2: Hand-apply regenerated metta:workflow region to metta's own CLAUDE.md** + - **Depends on**: Task 1.1 + - **Files**: `CLAUDE.md` (repo root of the change worktree) + - **Action**: Per design §4 and ADR-F (direct `metta refresh` is Tier-2 blocked for executors — do NOT invoke the CLI): hand-edit the text between `` and `` to be byte-exact with the new `buildWorkflowSection()` output. Concrete region diff: mandate paragraph replaced, Forbidden first bullet replaced with the enumerated families, read-only subsection inserted between Forbidden and Research discipline. Everything else in the region (entry points, trust model, quick-mode paragraph, doc-only-exceptions line, skill lists, stub-prohibition bullet) unchanged. To guarantee byte-exactness, write a small script in the session scratchpad (not committed) that imports `buildWorkflowSection` from `src/cli/commands/refresh.ts`, prints its output, and generate/paste the region from that output rather than typing it by hand. + - **Verify**: Run the scratchpad script via `npx tsx` and `diff` its output against the extracted region text between the markers — zero diff required (design §7). `grep -q 'Doc-only fixes and edits to this workflow section itself are the exceptions' CLAUDE.md` succeeds; `grep -c 'any other' CLAUDE.md` shows no blanket-ban wording remaining in the workflow region. + - **Done**: The `metta:workflow` region is byte-identical to `buildWorkflowSection()` output, carries the scoped mandate, enumerated Forbidden families, and read-only subsection, and preserves the doc-only-exceptions line and section structure; no edits outside the marker region; no scratch files committed. + +## Batch 3 (depends on Batch 2) + +- [x] **Task 3.1: Full verification sweep** + - **Depends on**: Task 1.1, Task 1.2, Task 1.3, Task 2.1, Task 2.2 + - **Files**: none (verification only; fix regressions in the files above if any check fails) + - **Action**: Run the complete gate set across the assembled change: full test suite, typecheck, hook-copy identity, and CLAUDE.md region byte-exactness (re-run the §7 tsx diff from Task 2.2). + - **Verify**: `npm test` fully green; `npx tsc --noEmit` clean; `diff src/templates/hooks/metta-guard-bash.mjs .claude/hooks/metta-guard-bash.mjs` empty; the tsx region diff against `CLAUDE.md` is empty. + - **Done**: All four checks pass with zero failures; any fix made during this task stays within the files already touched by Tasks 1.1–2.2. diff --git a/spec/specs/orchestration-guard/spec.lock b/spec/specs/orchestration-guard/spec.lock index 66eaf51d..ce209ff3 100644 --- a/spec/specs/orchestration-guard/spec.lock +++ b/spec/specs/orchestration-guard/spec.lock @@ -1,6 +1,6 @@ -version: 20 -hash: sha256:c13a700cfc42 -updated: 2026-08-18T16:16:20.200Z +version: 21 +hash: sha256:2dda53a2d136 +updated: 2026-08-26T05:28:04.624Z status: draft source: change requirements: @@ -207,3 +207,35 @@ requirements: - write-target-matrix-distinguishes-blocked-from-allowed - baseline-compare-module-tests-cover-dirt-attribution - new-blocking-tests-fail-against-pre-change-behavior + - id: workflow-primer-mandate-is-scoped-to-mutating-commands + hash: sha256:bf3234aa8f19 + scenarios: + - mandate-is-scoped-names-the-guard-and-acknowledges-the-read-only-surface + - mandate-is-byte-identical-across-both-variants + - forbidden-section-enumerates-mutating-families-instead-of-a-blanket-ban + - primed-session-is-directed-to-permitted-queries-for-status-questions + - id: workflow-primer-documents-the-permitted-read-only-surface + hash: sha256:84002ebf169b + scenarios: + - long-primer-enumerates-the-full-allow-surface + - enumerated-lists-match-the-hook-s-current-allow-lists-exactly + - short-primer-carries-a-one-line-read-only-pointer + - id: workflow-primer-carries-fail-closed-guidance-as-a-drift-safety-valve + hash: sha256:3c502f6e3faf + scenarios: + - unlisted-command-triggers-attempt-it-guidance-not-assumed-prohibition + - list-is-qualified-as-a-generation-time-mirror-of-the-guard + - id: all-workflow-rule-copies-agree-across-primer-claude-md-and-docs + hash: sha256:34648274d8c7 + scenarios: + - metta-s-own-claude-md-region-regenerates-with-corrected-wording-and-preserved-structure + - docs-readme-core-rule-section-matches-the-scoped-rule + - no-copy-asserts-a-blanket-ban + - refresh-replaces-old-blanket-ban-wording-in-a-consumer-project + - refresh-over-local-hand-edits-yields-correct-upstream-wording + - id: cross-referenced-sync-reminders-between-primer-and-guard-allow-lists + hash: sha256:f583a698a818 + scenarios: + - primer-lists-point-at-the-hook + - hook-allow-lists-point-back-at-the-primer + - hook-diff-is-comment-only-with-zero-behavior-change diff --git a/spec/specs/orchestration-guard/spec.md b/spec/specs/orchestration-guard/spec.md index dc3307ca..dbd5c8d4 100644 --- a/spec/specs/orchestration-guard/spec.md +++ b/spec/specs/orchestration-guard/spec.md @@ -922,3 +922,165 @@ Trace: intent Tests section; US-2, US-3, US-4, US-5 independent test criteria. - GIVEN the write-target block tests and the completion/ship contamination tests - WHEN they are run against the pre-change hook and pipeline - THEN they fail, demonstrating each would have caught the zeus contamination incident + + +## Requirement: Workflow Primer Mandate Is Scoped to Mutating Commands + +The generated workflow primer (both the `workflowPrimerShort()` and `workflowPrimerLong()` outputs +emitted into the CLAUDE.md `metta:workflow` region) MUST scope its CLI prohibition to +state-mutating lifecycle commands only. The mandate MUST NOT assert a blanket ban on direct +`metta` CLI invocation ("never call the CLI directly" / "any other `metta `" wording), MUST +name the `metta-guard-bash` PreToolUse hook as the enforcement authority, and MUST state that the +guard permits a read-only query surface directly. The mandate text MUST be byte-identical across +the short and long primer variants, preserving the consistency invariant documented in the +`workflow-primer.ts` file header. The long variant's Forbidden section MUST enumerate the mutating +command families that require skills — the guard's blocked surface (`propose`, `quick`, `auto`, +`complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, +`verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, +`roadmap add/reorder/next/remove`, `release cut`) — instead of banning "any other `metta `". +Trace: intent Problem (blanket ban contradicts the guard's deliberate read-only allow surface); +intent Proposal items 1–2; US-1. + +### Scenario: Mandate is scoped, names the guard, and acknowledges the read-only surface +- GIVEN the rewritten `MANDATE` constant in `src/delivery/workflow-primer.ts` +- WHEN either `workflowPrimerShort()` or `workflowPrimerLong()` is rendered +- THEN the mandate states that state-mutating lifecycle commands require the matching skill, identifies `metta-guard-bash` as the enforcement authority, and states that the guard permits a read-only query surface directly + +### Scenario: Mandate is byte-identical across both variants +- GIVEN the rendered outputs of `workflowPrimerShort()` and `workflowPrimerLong()` +- WHEN their mandate text is compared +- THEN the mandate is byte-identical in both variants + +### Scenario: Forbidden section enumerates mutating families instead of a blanket ban +- GIVEN the long primer's Forbidden section +- WHEN an orchestrator reads it +- THEN it enumerates the mutating command families (`propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`) and contains no "any other `metta `" blanket wording + +### Scenario: Primed session is directed to permitted queries for status questions +- GIVEN an orchestrator session primed with the corrected wording +- WHEN the operator asks a project-status question (e.g., a milestone rollup) +- THEN the primer's guidance directs the session toward a permitted read-only command rather than prohibiting all CLI use + + +## Requirement: Workflow Primer Documents the Permitted Read-Only Surface + +The long primer variant MUST contain a "Read-only queries (permitted directly)" subsection +enumerating the guard's allow surface as it exists at generation time: the single-word allowed +subcommands (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, +`model-escalation`, `tokens`, `install`), the two-word allowed forms (`issues list`, `gate list`, +`changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), +and the bare allowed forms (`roadmap`, `release`, `backlog` invoked bare or with flags only). +Every enumerated entry MUST match the corresponding entry in the `ALLOWED_SUBCOMMANDS`, +`ALLOWED_TWO_WORD`, and `ALLOWED_BARE` lists in `.claude/hooks/metta-guard-bash.mjs` at the time +of this change — the primer documents the existing surface and MUST NOT add, remove, or +renegotiate any allow-list membership. The short primer variant MUST include a one-line statement +that read-only queries are permitted directly and that the guard fails closed. +Trace: intent Problem (consumer sessions never discover permitted commands; zeus 2026-08-26 +session); intent Proposal item 3; US-2. + +### Scenario: Long primer enumerates the full allow surface +- GIVEN the rendered `workflowPrimerLong()` output +- WHEN an orchestrator reads the "Read-only queries (permitted directly)" subsection +- THEN it lists the single-word allowed subcommands (`status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install`), the two-word allowed forms (`issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`), and the bare allowed forms (`roadmap`, `release`, `backlog` with flags) + +### Scenario: Enumerated lists match the hook's current allow-lists exactly +- GIVEN the enumerated lists in the long primer's read-only subsection +- WHEN compared against the `ALLOWED_SUBCOMMANDS`, `ALLOWED_TWO_WORD`, and `ALLOWED_BARE` blocks in `.claude/hooks/metta-guard-bash.mjs` at the time of this change +- THEN every entry matches the hook's current allow-lists exactly, with no additions or removals + +### Scenario: Short primer carries a one-line read-only pointer +- GIVEN the rendered `workflowPrimerShort()` output +- WHEN an orchestrator reads it +- THEN it includes a one-line statement that read-only queries are permitted directly and the guard fails closed + + +## Requirement: Workflow Primer Carries Fail-Closed Guidance as a Drift Safety Valve + +The long primer's read-only subsection MUST qualify its enumerated list as mirroring the guard's +allow-lists at generation time — signaling that the hook, not the primer, is the authoritative +source — and MUST instruct that, when in doubt about a command not on the list, attempting the +command is safe because the guard fails closed and blocks anything unrecognized. Primer/guard +drift MUST therefore degrade to "attempt it and let the guard decide" rather than to false +prohibition. +Trace: intent Proposal item 3 (solution 3's fail-closed guidance folded in); intent Risk +(enumerated list can drift); US-3. + +### Scenario: Unlisted command triggers attempt-it guidance, not assumed prohibition +- GIVEN the long primer's read-only subsection +- WHEN an orchestrator encounters a `metta` command not on the enumerated list +- THEN the primer's guidance instructs it to attempt the command and rely on the guard's fail-closed blocking, rather than to assume prohibition + +### Scenario: List is qualified as a generation-time mirror of the guard +- GIVEN the primer text describing the enumerated read-only list +- WHEN it is read +- THEN it explicitly qualifies the list as mirroring the guard's allow-lists at generation time, identifying the hook as the authoritative source + + +## Requirement: All Workflow-Rule Copies Agree Across Primer, CLAUDE.md, and Docs + +The three copies of the workflow rule — the generated primer, metta's own CLAUDE.md +`Metta Workflow` region (regenerated by `metta refresh`), and the "Core rule: skills, not CLI" +section of `docs/workflows/README.md` — MUST all carry the scoped mandate and acknowledge the +permitted read-only surface, and none MUST assert a blanket ban on direct `metta` CLI calls. +Regeneration of metta's own CLAUDE.md region MUST preserve the "Doc-only fixes and edits to this +workflow section itself are the exceptions" line and the existing section structure. The docs +README rewording MUST preserve its existing note that CLAUDE.md wins on drift. The corrected +wording MUST propagate to consumer projects through the existing generation paths — the next +`metta refresh` (long variant) or `metta init`/`install` scaffold (short variant) — replacing any +prior region content, including old blanket-ban wording or local hand-edits inside the generated +region. +Trace: intent Proposal item 4; intent Impact (consumer propagation, zeus commit 919720e); US-4; +US-5. + +### Scenario: Metta's own CLAUDE.md region regenerates with corrected wording and preserved structure +- GIVEN metta's own CLAUDE.md +- WHEN its `metta:workflow` region is regenerated from the corrected primer via refresh +- THEN the scoped mandate, enumerated Forbidden families, and read-only subsection appear, while the "Doc-only fixes and edits to this workflow section itself are the exceptions" line and the section structure are preserved + +### Scenario: Docs README core-rule section matches the scoped rule +- GIVEN `docs/workflows/README.md` +- WHEN the "Core rule: skills, not CLI" section is read +- THEN its wording matches the scoped rule and permitted read-only surface, and its existing note that CLAUDE.md wins on drift is preserved + +### Scenario: No copy asserts a blanket ban +- GIVEN the three copies (generated primer, metta's CLAUDE.md workflow region, docs README) +- WHEN they are compared +- THEN none asserts a blanket ban on direct `metta` CLI calls + +### Scenario: Refresh replaces old blanket-ban wording in a consumer project +- GIVEN a consumer project whose `metta:workflow` region carries the old blanket-ban primer +- WHEN `metta refresh` runs with the fixed version installed +- THEN the region contains the scoped mandate, the enumerated Forbidden families, and the read-only subsection + +### Scenario: Refresh over local hand-edits yields correct upstream wording +- GIVEN a consumer project with local hand-edits inside the generated region (the zeus commit-919720e pattern) +- WHEN refresh regenerates the region +- THEN the replacement wording is the correct upstream content, so losing the local edits no longer reintroduces misinformation + + +## Requirement: Cross-Referenced Sync Reminders Between Primer and Guard Allow-Lists + +The hand-synced enumerated lists MUST carry cross-referencing maintenance comments: +`src/delivery/workflow-primer.ts` MUST contain a comment adjacent to its enumerated command lists +pointing at `.claude/hooks/metta-guard-bash.mjs` as the source that must stay in sync, and the +allow-list blocks in `.claude/hooks/metta-guard-bash.mjs` MUST contain a comment pointing back at +`workflow-primer.ts`. The change to the hook file MUST be comment-only: the guard's decision +logic, allow-list membership, block-list membership, tiering, and credential handling MUST behave +identically before and after the change. +Trace: intent Proposal item 5; intent Impact (`metta-guard-bash.mjs` comment-only, zero +behavioral impact); US-6. + +### Scenario: Primer lists point at the hook +- GIVEN `src/delivery/workflow-primer.ts` +- WHEN a maintainer reads the enumerated command lists +- THEN an adjacent comment points at `.claude/hooks/metta-guard-bash.mjs` as the source that must stay in sync + +### Scenario: Hook allow-lists point back at the primer +- GIVEN `.claude/hooks/metta-guard-bash.mjs` +- WHEN a maintainer reads the allow-list blocks +- THEN an adjacent comment points back at `src/delivery/workflow-primer.ts` + +### Scenario: Hook diff is comment-only with zero behavior change +- GIVEN the hook file after the change +- WHEN its decision logic is exercised across allowed, blocked, and unknown invocations +- THEN behavior is identical to before the change — the only diff is comments diff --git a/src/delivery/workflow-primer.ts b/src/delivery/workflow-primer.ts index b912a28a..22db3bae 100644 --- a/src/delivery/workflow-primer.ts +++ b/src/delivery/workflow-primer.ts @@ -1,17 +1,43 @@ /** * Workflow primer text emitted into CLAUDE.md. Two variants: - * - "short": scaffold use (metta init / install). Single mandate line + three entry points. - * - "long": authoritative regeneration use (metta refresh). Adds "Forbidden" subsection with - * full CLI-call prohibition list and the humans-at-terminal scope caveat. + * - "short": scaffold use (metta init / install). Scoped mandate + pointer line + three + * entry points. + * - "long": authoritative regeneration use (metta refresh). Adds the "Forbidden" subsection + * enumerating the guard's blocked surface and a "Read-only queries" subsection enumerating + * its allowed surface. * - * The mandate sentence is identical across both variants so downstream CLAUDE.md consumers - * see consistent wording regardless of which generator ran last. + * The mandate scopes the skill requirement to state-mutating metta commands and names the + * `metta-guard-bash` PreToolUse hook as the enforcement authority. It is a single shared + * constant so both variants render it byte-identically — an invariant now pinned by test + * (tests/delivery.test.ts, "Workflow primer scoped mandate"). + * + * The enumerated allow/block lists below are hand-synced with + * src/templates/hooks/metta-guard-bash.mjs (and its deployed copy) and guarded by the seam + * test in tests/delivery.test.ts — drift fails CI. */ const MANDATE = - '**AI orchestrators MUST invoke the matching metta skill — never call the CLI directly.** ' + + '**State-mutating metta commands MUST go through the matching metta skill — never as direct CLI calls from an AI orchestrator session.** ' + + 'Enforcement authority is the `metta-guard-bash` PreToolUse hook: it blocks mutating and unrecognized commands (fail-closed) but permits a read-only query surface directly. ' + '(Humans running the CLI in a terminal are unaffected — this rule scopes to AI-driven sessions.)' +const READ_ONLY_POINTER = + 'Read-only queries (`metta status`, `metta progress`, `metta issues list`, …) are permitted directly; the guard fails closed, so attempting a query is always safe.' + +// SYNC: mirrors the ALLOWED_SUBCOMMANDS / ALLOWED_TWO_WORD / ALLOWED_BARE lists in +// src/templates/hooks/metta-guard-bash.mjs — edit both together; the seam test in +// tests/delivery.test.ts fails on drift. +const READ_ONLY_SURFACE_BULLETS = [ + '### Read-only queries (permitted directly)', + '', + "The `metta-guard-bash` hook allows these directly — no skill needed. This list mirrors the hook's allow-lists at generation time; the hook, not this text, is authoritative:", + '- Single-word: `status`, `instructions`, `progress`, `doctor`, `next`, `iteration`, `model-escalation`, `tokens`, `install` (`iteration`/`model-escalation`/`tokens` append instrumentation records and `install` writes scaffolding — guard-allowed, though not strictly read-only)', + '- Two-word: `issues list`, `gate list`, `changes list`, `backlog list|show`, `gaps list|show`, `milestone list|show`, `release status`', + '- Bare (flags only): `roadmap`, `release`, `backlog` (e.g. `metta roadmap --json`)', + '', + 'Run bare `metta` for the full current command listing. When in doubt about a command not listed here, attempt it — the guard fails closed and blocks anything unrecognized, so an attempt is always safe and never mutates state.', +] + const ENTRY_POINTS_BULLETS = [ '- `/metta-quick ` — small, scoped fixes (bug fixes, one-file edits, tiny refactors)', '- `/metta-propose ` — anything non-trivial (new features, multi-file changes, API surface changes)', @@ -31,6 +57,8 @@ export function workflowPrimerShort(): string[] { '', MANDATE, '', + READ_ONLY_POINTER, + '', 'Primary entry points:', ...ENTRY_POINTS_BULLETS, '', @@ -57,9 +85,14 @@ export function workflowPrimerLong(): string[] { '', '### Forbidden', '', - '- Invoking `metta quick`, `metta propose`, `metta finalize`, `metta complete`, `metta issue`, or any other `metta ` directly from an AI orchestrator session. Use the matching skill.', + // SYNC: mirrors the BLOCKED_SUBCOMMANDS / BLOCKED_TWO_WORD lists in + // src/templates/hooks/metta-guard-bash.mjs — edit both together; the seam test in + // tests/delivery.test.ts fails on drift. + '- Invoking any state-mutating metta command directly from an AI orchestrator session: `propose`, `quick`, `auto`, `complete`, `finalize`, `ship`, `issue`, `fix-issue`, `fix-gap`, `refresh`, `import`, `init`, `verify`, `backlog add/done/promote/migrate`, `changes abandon`, `milestone create/close/update`, `roadmap add/reorder/next/remove`, `release cut`. Use the matching skill.', '- Writing placeholder content like `"intent stub"` or `"summary stub"` to any artifact file to satisfy `metta complete`. Artifacts must carry real content authored by the matching `metta-*` subagent.', '', + ...READ_ONLY_SURFACE_BULLETS, + '', '### Research discipline', '', 'When a research-phase or design-phase question has a deterministic answer in public documentation — framework API docs, library reference, CLI tool manual, language spec, SDK changelog — the orchestrator MUST use `WebFetch` (for a known authoritative URL) or `WebSearch` (to discover the authoritative source) to resolve it **before** asking the user. This specifically covers questions about external framework / API / tool documented behavior (e.g. "does Claude Code support `context: fork` in skill frontmatter?", "what fields does the Anthropic Messages API accept?", "is the `--legacy-peer-deps` flag deprecated in npm 10?").', diff --git a/src/templates/hooks/metta-guard-bash.mjs b/src/templates/hooks/metta-guard-bash.mjs index f7276016..7880bdbf 100755 --- a/src/templates/hooks/metta-guard-bash.mjs +++ b/src/templates/hooks/metta-guard-bash.mjs @@ -35,6 +35,8 @@ import { promisify } from 'node:util'; const execFileAsync = promisify(execFile); // Explicit ALLOW list: known safe read-only single-subcommand forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const ALLOWED_SUBCOMMANDS = new Set([ 'status', 'instructions', 'progress', 'doctor', 'next', // read-only routing query (`metta next --json`); first Bash call of the metta-next skill body @@ -45,6 +47,8 @@ const ALLOWED_SUBCOMMANDS = new Set([ ]); // Explicit ALLOW list for two-word read-only forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const ALLOWED_TWO_WORD = new Map([ ['issues', new Set(['list'])], ['gate', new Set(['list'])], @@ -64,6 +68,8 @@ const ALLOWED_TWO_WORD = new Map([ ]); // Explicit BLOCK list: state-mutating single-subcommand forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const BLOCKED_SUBCOMMANDS = new Set([ 'propose', 'quick', 'auto', 'complete', 'finalize', 'ship', 'issue', 'fix-issue', 'fix-gap', 'refresh', 'import', 'init', @@ -73,6 +79,8 @@ const BLOCKED_SUBCOMMANDS = new Set([ ]); // Explicit BLOCK list for two-word mutating forms. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const BLOCKED_TWO_WORD = new Map([ ['backlog', new Set(['add', 'done', 'promote', 'migrate'])], ['changes', new Set(['abandon'])], @@ -94,6 +102,8 @@ const BLOCKED_TWO_WORD = new Map([ // Bare `metta release` defaults to the read-only status view (roadmap precedent). // Bare `metta backlog` defaults to the read-only list view; its mutating two-word // forms (`add`/`done`/`promote`/`migrate`) stay Tier-2 blocked above. +// SYNC: enumerated in src/delivery/workflow-primer.ts (read-only subsection / Forbidden +// bullet) — edit both together; the seam test in tests/delivery.test.ts fails on drift. const ALLOWED_BARE = new Set(['roadmap', 'release', 'backlog']); // Subcommands that require a trusted agent_type (caller identity set by the Claude Code diff --git a/tests/delivery.test.ts b/tests/delivery.test.ts index ef1938cc..66f9f9f4 100644 --- a/tests/delivery.test.ts +++ b/tests/delivery.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { mkdtemp, rm, readFile, readdir } from 'node:fs/promises' +import { readFileSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { @@ -58,7 +59,7 @@ describe('Claude Code Adapter', () => { expect(formatted).toContain('auth') expect(formatted).toContain('