@ionivetech/mugiwara 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/README.md +2 -2
  7. package/content/agents/brook-healing.md +1 -1
  8. package/content/agents/memory-keeper.md +5 -0
  9. package/content/agents/usopp-brainstorm.md +3 -2
  10. package/content/agents/zoro-execution.md +4 -3
  11. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  12. package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
  13. package/content/skills/mugiwara-execution/SKILL.md +4 -3
  14. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  15. package/content/skills/mugiwara-gates/SKILL.md +6 -0
  16. package/content/skills/mugiwara-healing/SKILL.md +5 -1
  17. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  18. package/content/skills/mugiwara-orchestration/SKILL.md +7 -6
  19. package/content/skills/mugiwara-planning/SKILL.md +2 -0
  20. package/content/skills/mugiwara-quality/SKILL.md +3 -14
  21. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  22. package/content/skills/mugiwara-resume/SKILL.md +3 -14
  23. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  24. package/content/skills/mugiwara-review/SKILL.md +3 -15
  25. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  26. package/content/skills/mugiwara-security/SKILL.md +1 -0
  27. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  28. package/content/skills/mugiwara-workflow/SKILL.md +28 -25
  29. package/dist/mugiwara.js +1323 -402
  30. package/gemini-extension.json +1 -1
  31. package/hooks/mugiwara-mode-tracker.js +24 -4
  32. package/hooks/mugiwara-mode-tracker.ts +36 -7
  33. package/hooks/session-start.js +6 -1
  34. package/hooks/session-start.ts +8 -1
  35. package/package.json +2 -2
  36. package/plugin.json +1 -1
  37. package/references/cost-governor.md +104 -0
  38. package/references/wave-banners.md +1 -2
  39. package/scripts/gate-selftest.ts +239 -21
  40. package/scripts/lane-base.ts +16 -0
  41. package/scripts/lane.sh +5 -1
  42. package/scripts/lib/lane-base.sh +1 -1
  43. package/scripts/savepoint.sh +48 -5
  44. package/scripts/validate-content.ts +60 -0
  45. package/scripts/verify-install.ts +20 -0
  46. package/scripts/write-metrics.ts +73 -0
  47. package/src/budget.ts +11 -0
  48. package/src/cli.ts +185 -28
  49. package/src/config.ts +6 -0
  50. package/src/continue.ts +36 -1
  51. package/src/cost.ts +4 -1
  52. package/src/installer.ts +27 -4
  53. package/src/integrity.ts +105 -25
  54. package/src/mission.ts +123 -7
  55. package/src/policy.ts +372 -4
  56. package/src/provenance.ts +29 -9
  57. package/src/sign.ts +45 -3
  58. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
  59. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
  60. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
  61. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
  62. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +0 -14
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mugiwara",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -54,12 +54,22 @@ function applyModeChange(mode) {
54
54
  writeFileSync(tmp, body);
55
55
  renameSync(tmp, file);
56
56
  }
57
+ function isCodexInput(parsed) {
58
+ if (process.env.CODEX_HOME || process.env.CODEX_THREAD_ID)
59
+ return true;
60
+ if (typeof parsed.turn_id === "string")
61
+ return true;
62
+ if (typeof parsed.cwd === "string" && typeof parsed.model === "string")
63
+ return true;
64
+ return false;
65
+ }
57
66
  async function main() {
58
67
  let input = "";
59
68
  for await (const chunk of process.stdin)
60
69
  input += chunk;
61
70
  if (!input.trim()) {
62
- process.stdout.write(JSON.stringify({ prompt: "" }));
71
+ const codexEmpty = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
72
+ process.stdout.write(JSON.stringify(codexEmpty ? {} : { prompt: "" }));
63
73
  return;
64
74
  }
65
75
  let parsed;
@@ -68,12 +78,22 @@ async function main() {
68
78
  } catch {
69
79
  parsed = { prompt: input };
70
80
  }
71
- const prompt = parsed.prompt ?? "";
81
+ const prompt = typeof parsed.prompt === "string" ? parsed.prompt : "";
72
82
  const change = parseModeChange(prompt);
73
83
  if (change)
74
84
  applyModeChange(change);
75
- process.stdout.write(JSON.stringify({ prompt }));
85
+ const codex = isCodexInput(parsed);
86
+ if (codex) {
87
+ if (change) {
88
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: `Mugiwara mode changed to ${change}` } }));
89
+ } else {
90
+ process.stdout.write(JSON.stringify({}));
91
+ }
92
+ } else {
93
+ process.stdout.write(JSON.stringify({ prompt }));
94
+ }
76
95
  }
77
96
  main().catch(() => {
78
- process.stdout.write(JSON.stringify({ prompt: "" }));
97
+ const codexFallback = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
98
+ process.stdout.write(JSON.stringify(codexFallback ? {} : { prompt: "" }));
79
99
  });
@@ -57,24 +57,53 @@ function applyModeChange(mode: string) {
57
57
  renameSync(tmp, file);
58
58
  }
59
59
 
60
+ // Codex vs Claude: Codex UserPromptSubmit expects {} or {hookSpecificOutput:{hookEventName,additionalContext}}
61
+ // Claude expects {prompt:""} — Codex rejects "prompt" (additionalProperties:false).
62
+ // Detect Codex via env or input shape (turn_id/cwd/model are Codex-only).
63
+ function isCodexInput(parsed: Record<string, unknown>): boolean {
64
+ if (process.env.CODEX_HOME || process.env.CODEX_THREAD_ID) return true;
65
+ if (typeof parsed.turn_id === 'string') return true;
66
+ if (typeof parsed.cwd === 'string' && typeof parsed.model === 'string') return true;
67
+ return false;
68
+ }
69
+
60
70
  // main
61
71
  async function main() {
62
72
  let input = '';
63
73
  for await (const chunk of process.stdin) input += chunk;
64
- if (!input.trim()) { process.stdout.write(JSON.stringify({ prompt: '' })); return; }
74
+ if (!input.trim()) {
75
+ // empty stdin — Codex expects {}, Claude expects {prompt:""}
76
+ // Emit Codex-safe empty ( {} ) — Claude also accepts {} as no-op (prompt passthrough)
77
+ // but to keep Claude behavior, sniff env: if Codex-like env, emit {}, else prompt
78
+ const codexEmpty = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
79
+ process.stdout.write(JSON.stringify(codexEmpty ? {} : { prompt: '' }));
80
+ return;
81
+ }
65
82
 
66
- let parsed: { prompt?: string };
67
- try { parsed = JSON.parse(input); } catch { parsed = { prompt: input }; }
68
- const prompt = parsed.prompt ?? '';
83
+ let parsed: Record<string, unknown>;
84
+ try { parsed = JSON.parse(input) as Record<string, unknown>; } catch { parsed = { prompt: input }; }
85
+ const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : '';
69
86
 
70
87
  const change = parseModeChange(prompt);
71
88
  if (change) applyModeChange(change);
72
89
 
73
- // pass-through always return the prompt unchanged
74
- process.stdout.write(JSON.stringify({ prompt }));
90
+ const codex = isCodexInput(parsed);
91
+ if (codex) {
92
+ // Codex schema: additionalProperties:false — "prompt" is invalid. Use hookSpecificOutput or {}.
93
+ if (change) {
94
+ process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: `Mugiwara mode changed to ${change}` } }));
95
+ } else {
96
+ process.stdout.write(JSON.stringify({}));
97
+ }
98
+ } else {
99
+ // Claude: pass-through prompt
100
+ process.stdout.write(JSON.stringify({ prompt }));
101
+ }
75
102
  }
76
103
 
77
104
  main().catch(() => {
78
105
  // silent — hook must never block the conversation
79
- process.stdout.write(JSON.stringify({ prompt: '' }));
106
+ // Codex-safe fallback: {} (valid for both, but Claude prefers prompt however {} is also accepted as no-op)
107
+ const codexFallback = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID);
108
+ process.stdout.write(JSON.stringify(codexFallback ? {} : { prompt: '' }));
80
109
  });
@@ -104,6 +104,11 @@ if (active.length === 1 && mode === "auto") {
104
104
  ${lines}
105
105
  ` + `Run /mugiwara continue <mission> [member] to resume one explicitly.`;
106
106
  }
107
+ var isCodexSession = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID || process.env.CODEX_HOME_DIR);
107
108
  if (resumeContext) {
108
- console.log(JSON.stringify({ additionalContext: resumeContext }));
109
+ if (isCodexSession) {
110
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: resumeContext } }));
111
+ } else {
112
+ console.log(JSON.stringify({ additionalContext: resumeContext }));
113
+ }
109
114
  }
@@ -131,6 +131,13 @@ if (active.length === 1 && mode === 'auto') {
131
131
 
132
132
  // Silent unless there is in-flight work: a session that never used mugiwara
133
133
  // gets zero injected context.
134
+ // Codex vs Claude: Codex SessionStart expects {hookSpecificOutput:{hookEventName,additionalContext}}
135
+ // Claude expects {additionalContext} — Codex rejects top-level additionalContext (additionalProperties:false).
136
+ const isCodexSession = !!(process.env.CODEX_HOME || process.env.CODEX_THREAD_ID || process.env.CODEX_HOME_DIR);
134
137
  if (resumeContext) {
135
- console.log(JSON.stringify({ additionalContext: resumeContext }));
138
+ if (isCodexSession) {
139
+ console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: resumeContext } }));
140
+ } else {
141
+ console.log(JSON.stringify({ additionalContext: resumeContext }));
142
+ }
136
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionivetech/mugiwara",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, self-healing. Installs into Claude Code, opencode, Copilot, Gemini, Codex, Cursor, Kimi, pi, Windsurf, Cline, Kilo, Antigravity.",
5
5
  "homepage": "https://github.com/ionivetech/mugiwara#readme",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "sync-version": "bun scripts/sync-version.ts",
64
64
  "build-hooks": "bun scripts/build-hooks.ts",
65
65
  "build-hooks:check": "bun scripts/build-hooks.ts --check",
66
- "gate": "bun run build-hooks:check && bun run typecheck && bun run test:coverage && bun run build && bun scripts/validate-content.ts --check-manifest --check-docs --check-doc-integrity && bun scripts/lane-base.ts && bun scripts/check-doc-links.ts && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts && bun scripts/benchmark-governor.ts && bun scripts/verify-install.ts && bun scripts/conformance.ts && bun run coverage-gate",
66
+ "gate": "bun run build-hooks:check && bun run typecheck && bun run test:coverage && bun run build && bun scripts/write-metrics.ts && bun scripts/validate-content.ts --check-manifest --check-docs --check-doc-integrity --check-readme-metrics && bun scripts/lane-base.ts && bun scripts/check-doc-links.ts && bun run verify-pack && bun scripts/run-evals.ts && bun scripts/retrieval-eval.ts && bun scripts/benchmark-governor.ts && bun scripts/verify-install.ts && bun scripts/conformance.ts && bun run coverage-gate",
67
67
  "verify-pack": "npm pack --dry-run 2>&1 | node -e \"let s='';process.stdin.on('data',d=>s+=d);process.stdin.on('end',()=>{if(!s.includes('ionivetech-mugiwara')){console.error('npm pack failed');process.exit(1)};console.log('npm package clean')})\"",
68
68
  "prepack": "bun run build && bun run sync-version"
69
69
  },
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mugiwara",
3
3
  "description": "The Straw Hat crew of AI agents and skills: brainstorm, plan, execute, checkpoint, quality, gates, review, security, healing.",
4
- "version": "0.8.0",
4
+ "version": "0.8.2",
5
5
  "author": {
6
6
  "name": "ionivetech"
7
7
  },
@@ -0,0 +1,104 @@
1
+ # Cost Governor — Terse & Low-Cost Execution
2
+
3
+ Single source for Work, Scope/Code, Cognitive/Output, Stop-Slop, Adaptive Budget, Benchmark. Verdicts recommended, not enforced; crew acts. Trail rows → `.mugiwara/missions/<mission>/decisions.md` → `## Cost governor decisions`. `savepoint`/`lane-base`/`config` untouched.
4
+
5
+ ## Ladder — before adding code, run top to bottom, stop at first that holds
6
+
7
+ 1. Does this need to exist at all? Speculative need → skip, one line why (YAGNI).
8
+ 2. Already in codebase? Reuse helper/util/pattern nearby → reuse it.
9
+ 3. Stdlib does it? Use it.
10
+ 4. Native platform covers it? `<input type="date">` over picker lib, CSS over JS, DB constraint over app code → native.
11
+ 5. Already-installed dependency solves it? Use it. Never add new dep for a few lines.
12
+ 6. Can it be one line? One line.
13
+ 7. Only then: minimum code that works. No unrequested abstraction (one impl → no interface/factory/config), no boilerplate for later, deletion over addition, fewest files, shortest diff.
14
+
15
+ Each step: grep callers first; fix root cause in shared function, not symptom in caller. One guard in shared path beats guards in every caller.
16
+
17
+ ## Output — terse, deduped
18
+
19
+ Reasoning: Question → Evidence → Decision → Action. No speculative architecture, hypothetical requirements, repeated reconsideration, unrelated implementations. Investigation ends when `acceptance_mapped + surface_understood + path_established` or limits hit with concrete reason (§13). Alternatives ≤3, evidence-backed only.
20
+
21
+ Output: Decision / Action / Result / Evidence / Blocker only, mission-focused. Duplicate explanations fingerprinted and dropped. Every verdict → `cognitive-governor` trail row.
22
+
23
+ ## Scope & code
24
+
25
+ Prefer smallest correct scope — reuse + local modification over new architecture (§14). Abstraction justified only when used in ≥2 places or required by contract, never speculative (§15). Dependency added only with explicit justification (§16). Minimum sufficient implementation, never minimum LOC at expense of verification/quality (§15/§38). Measure change surface. Code waste (unnecessary helper/abstraction/wrapper/interface/config/dependency/generated code/refactor) named. Trail row `scope-governor`.
26
+
27
+ ## Slop — taxonomy, signals, measurement, intervention
28
+
29
+ Taxonomy 8 kinds (§21): investigation, context, reasoning, output, code, retry, healing, scope.
30
+
31
+ Signals (§22): repeated reads/commands, token-without-evidence, LOC-without-acceptance, abstraction-without-justification.
32
+
33
+ Measure (§23): evidence/criteria/tests/code vs cost delta — cost grows without progress → slop. Flag anomaly (§24): 5k tokens zero progress, work-to-cost drop.
34
+
35
+ Intervene (§20): tolerate / stop / compress / escalate by severity.
36
+
37
+ Detectors — six categories:
38
+ - retry §21.6/§31 same-action same-evidence same-failure → STOP
39
+ - healing §21.7/§32 no progress → stop; `heal_cycle ≥ 3` → halt
40
+ - scope §21.8 out-of-scope without acceptance → reject
41
+ - context §21.2 duplicate/irrelevant → discard/compress
42
+ - investigation §21.1 unbounded exploration → stop
43
+ - code §21.5 unnecessary abstraction/dependency/boilerplate → remove/simplify
44
+
45
+ Trail row `slop-governor`.
46
+
47
+ ## Budget — reserve, projection, thresholds, breaker
48
+
49
+ Reserve expected max before expensive stages (Review/Security/Healing). Continuously project `current + remaining required + expected conditional + possible healing` (§26).
50
+
51
+ Expand only with evidence (§27 valid: scope legitimately expanded, security-sensitive path, test surface larger, architecture dependency, legitimate healing; invalid: verbosity/reread/repeat/unnecessary code).
52
+
53
+ Thresholds (§28): 60% → optimize, 75% → aggressive, 90% → protect, 100% → pause, 150% → warning, 300% → stop.
54
+
55
+ Breaker (§29): actual ≥ 2× expected without progress/scope/evidence → trip.
56
+
57
+ Anomaly (§24): flag 5k-zero-progress, re-consumes slop signal.
58
+
59
+ Record every non-ok verdict via `recordBudgetDecision` (§41). Trail row `budget-governor`. Ledger aggregates envelope+events+registry+trail; `mugiwara cost` surfaces ledger (--json); report Cost section renders ledger+avoided+efficiency+trail (§43).
60
+
61
+ ## Benchmark & hardening
62
+
63
+ Tracks `scripts/benchmark-governor.ts` harness (deterministic, no network).
64
+
65
+ Cost suite (§48) — 4 workloads:
66
+ - lean-trivial: projected 8000 + overhead 1000, context ≤20000, evidence ≥1, surface 2 files 50 LOC
67
+ - standard-feature: projected 15000 + overhead 1500, context ≤40000, evidence ≥3
68
+ - large-repo: projected 22000 + overhead 2200, context ≤80000, evidence ≥5, surface 50 files
69
+ - long-mission: projected 23000 + overhead 2300, context ≤90000, 9 stages projection ≤ budget
70
+ Check: `measured.tokens ≤ projected + overhead` else fail; `measured.context ≤ max` else fail
71
+
72
+ Stop-Slop suite (§45) — 12 scenarios detect→classify→intervene:
73
+ - endless-exploration → investigation slop → stop
74
+ - repeated-reads (3× no evidence) → context slop → stop; with concrete reason → tolerate
75
+ - repeated-commands (same cmd+evidence fail) → retry slop → stop
76
+ - repeated-failed-test → retry slop → stop
77
+ - repeated-reasoning → reasoning slop → stop
78
+ - unnecessary-abstraction → code slop → stop
79
+ - unnecessary-dependency → code slop → stop
80
+ - unrelated-refactor → scope slop → stop
81
+ - verbose-output → output slop → stop
82
+ - no-progress-healing (cycle ≥3, 0 fixes) → healing slop → stop
83
+ - premature-completion → scope slop → escalate
84
+ - excessive-context (repeated reads + duplicate chars) → context slop → stop
85
+
86
+ Stress (bench-only, no runtime): large repository 50 files within scope → pass; long mission 9 stages projection ≤ full budget → pass; runaway 2× expected no progress → breaker tripped.
87
+
88
+ Thresholds live in `scripts/benchmark-thresholds.json` (or THRESHOLDS const) — `tokens > projected+overhead` fail, `context > max` fail, only move on explicit fixture update, ratchet like retrieval-eval. Regression (§49): cost down but correctness/evidence/security/quality/scope down → fail. Determinism: harness pure over explicit fixture inputs, no Date.now/Math.random/network. CI: `package.json:gate` includes harness; `gate-selftest` tampers thresholds → harness must exit 1. Harness measures, not enforces.
89
+
90
+ ## Reporting & trail
91
+
92
+ Ledger aggregates envelope+events+registry+trail; `mugiwara cost` surfaces ledger; report Cost section renders ledger+avoided+efficiency+trail (§43). Every verdict lands as trail row in decisions.md.
93
+
94
+ ## Checklist
95
+
96
+ - [ ] ladder run before each code addition, no skipped rung
97
+ - [ ] output Decision/Action/Result/Evidence/Blocker, deduped
98
+ - [ ] scope/code: reuse checked, abstraction & dep justified
99
+ - [ ] slop: taxonomy classified, signals measured, intervention applied
100
+ - [ ] budget: reserved, projected, thresholds respected, breaker armed
101
+ - [ ] benchmark: 4 cost + 12 slop + 3 stress green, thresholds ratcheted
102
+ - [ ] trail rows written for every non-trivial verdict
103
+
104
+ Unchecked boxes are not done.
@@ -53,8 +53,7 @@ their banners appear only when a wave or worker names them.
53
53
 
54
54
  ## Rules
55
55
 
56
- 1. Banner before EVERY flow stage; handoff after it. No flow stage starts without its
57
- banner (orchestration red flag).
56
+ 1. Banner before EVERY flow stage; handoff after it. Main thread emits `===== FLOW N — CREW =====` FIRST line and `→ Flow N+1 — Crew` LAST line even when subagent does work — covers Flow 0 Luffy, 1 Usopp, 2 Nami, 3 Zoro, 4 Chopper, 5 Sanji, 6 Franky, 7 Robin/Jinbe, 8 Brook, 9 Luffy. No flow stage starts without its banner (orchestration red flag).
58
57
  2. The color comes from this table only — never invent a hex mid-mission.
59
58
  3. One form everywhere: equals line `===== <emoji> FLOW N — <CREW> (ROLE) =====` — five `=` per side, the crew emoji from the table leading the line, ANSI-wrapped in terminals, plain in markdown-rendering UIs. When unsure, the plain form is safe everywhere.
60
59
  4. Only the crew table's colors and the two SGR forms above (truecolor,
@@ -6,6 +6,8 @@ import { execSync } from 'node:child_process';
6
6
  import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync, unlinkSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
7
7
  import { join, dirname } from 'node:path';
8
8
  import { tmpdir } from 'node:os';
9
+ import { gatesForLane } from '../src/policy.ts';
10
+ import { budgetForLane } from '../src/cost.ts';
9
11
 
10
12
  const root = join(import.meta.dirname, '..');
11
13
  let passed = 0;
@@ -91,9 +93,11 @@ console.log('\nCost gate — measured vs stated index chars');
91
93
  const costFile = join(root, 'docs', 'concepts', 'cost.md');
92
94
  const original = readFileSync(costFile, 'utf8');
93
95
  try {
94
- const drifted = original.replace(/\*\*Current:\*\* \d[\d,]* chars/, '**Current:** 1 chars');
95
- if (drifted === original) {
96
- console.log(' ⚠ "**Current:** N chars" pattern not found in cost.md — skipping');
96
+ const costPattern = /\*\*Current:\*\* \d[\d,]* chars/;
97
+ const drifted = original.replace(costPattern, '**Current:** 1 chars');
98
+ if (!costPattern.test(original) || drifted === original) {
99
+ console.error('✗ COST: mutation target not found — the gate it guards may be dead.');
100
+ failed++;
97
101
  } else {
98
102
  writeFileSync(costFile, drifted);
99
103
  assert('drifted stated index chars → exit 1', false, () => run('COST', 'bun scripts/validate-content.ts'));
@@ -134,12 +138,14 @@ if (!existsSync(savepointFile)) {
134
138
  const original = readFileSync(savepointFile, 'utf8');
135
139
  try {
136
140
  // reintroduce the D1 defect: read lane_prev with require() of a relative path
141
+ const d1Pattern = /PREV_JSON=\$\(node -e "try\{const fs=require\('fs'\);const s=JSON\.parse\(fs\.readFileSync\(process\.argv\[1\],'utf8'\)\);process\.stdout\.write\(JSON\.stringify\(\{mission:s\.mission\|\|'',lane:s\.lane\|\|'',peak:s\.lane_peak\|\|''\}\)\)\}catch\(e\)\{process\.stdout\.write\('\{\}'\)\}" "\$STATE_FILE" 2>\/dev\/null \|\| true\)/;
137
142
  const broken = original.replace(
138
- /PREV_JSON=\$\(node -e "try\{const fs=require\('fs'\);const s=JSON\.parse\(fs\.readFileSync\(process\.argv\[1\],'utf8'\)\);process\.stdout\.write\(JSON\.stringify\(\{mission:s\.mission\|\|'',lane:s\.lane\|\|'',peak:s\.lane_peak\|\|''\}\)\)\}catch\(e\)\{process\.stdout\.write\('\{\}'\)\}" "\$STATE_FILE" 2>\/dev\/null \|\| true\)/,
143
+ d1Pattern,
139
144
  "PREV_JSON=$(node -e \"try{const s=require(process.argv[1]);process.stdout.write(JSON.stringify({mission:s.mission||'',lane:s.lane||'',peak:s.lane_peak||''}))}catch(e){process.stdout.write('{}')}\" \"$STATE_FILE\" 2>/dev/null || true)"
140
145
  );
141
- if (broken === original) {
142
- console.log('D1 mutation pattern not found — skipping');
146
+ if (!d1Pattern.test(original) || broken === original) {
147
+ console.error('D1: mutation target not found — the gate it guards may be dead.');
148
+ failed++;
143
149
  } else {
144
150
  writeFileSync(savepointFile, broken);
145
151
  assert('broken LANE_PREV resolve → lane-integrity fails', false, () => run('D1', 'bun run test -- lane-integrity -t "lane_prev"'));
@@ -158,12 +164,14 @@ if (!existsSync(savepointFile)) {
158
164
  const original = readFileSync(savepointFile, 'utf8');
159
165
  try {
160
166
  // neuter the clamp: make lane_rank always return 0 so a drop never holds
167
+ const d2Pattern = /lane_rank\(\) \{\n case "\$1" in\n direct\) echo 0 ;;[\s\S]*?\n esac\n\}/;
161
168
  const broken = original.replace(
162
- /lane_rank\(\) \{\n case "\$1" in\n direct\) echo 0 ;;[\s\S]*?\n esac\n\}/,
169
+ d2Pattern,
163
170
  'lane_rank() {\n echo 0\n}'
164
171
  );
165
- if (broken === original) {
166
- console.log('D2 mutation pattern not found — skipping');
172
+ if (!d2Pattern.test(original) || broken === original) {
173
+ console.error('D2: mutation target not found — the gate it guards may be dead.');
174
+ failed++;
167
175
  } else {
168
176
  writeFileSync(savepointFile, broken);
169
177
  assert('broken clamp → lane-integrity fails', false, () => run('D2', 'bun run test -- lane-integrity -t "clamp"'));
@@ -183,12 +191,14 @@ if (!existsSync(patternsFile)) {
183
191
  const original = readFileSync(patternsFile, 'utf8');
184
192
  try {
185
193
  // reintroduce the D3 defect: singular-only list (no payments/, migrations/)
194
+ const d3Pattern = /SENSITIVE_PATS=.*/;
186
195
  const broken = original.replace(
187
- /SENSITIVE_PATS=.*/,
196
+ d3Pattern,
188
197
  'SENSITIVE_PATS="auth/|payment/|billing/|crypto/|secrets/|\\.env$|config/.*key|migration/|\\.sql$|schema\\.|\\.prisma$|\\.terraform|\\.tf$"'
189
198
  );
190
- if (broken === original) {
191
- console.log('D3 mutation pattern not found — skipping');
199
+ if (!d3Pattern.test(original) || broken === original) {
200
+ console.error('D3: mutation target not found — the gate it guards may be dead.');
201
+ failed++;
192
202
  } else {
193
203
  writeFileSync(patternsFile, broken);
194
204
  assert('singular sensitive patterns → lane-integrity fails', false, () => run('D3', 'bun run test -- lane-integrity -t "payments"'));
@@ -218,10 +228,12 @@ if (!existsSync(patternsFile)) {
218
228
  const live = original.match(/SENSITIVE_PATS="([^"]+)"/)?.[1] ?? '';
219
229
  const broken = live.split('|').filter(t => !D3B_FAMILY_TOKENS.has(t)).join('|');
220
230
  const brokenLine = `SENSITIVE_PATS="${broken}"`;
221
- if (broken === live || !live) {
222
- console.log(' ⚠ D3b: no D3 family tokens found in live SENSITIVE_PATS — skipping');
231
+ const d3bPattern = /SENSITIVE_PATS="[^"]*"/;
232
+ if (!d3bPattern.test(original) || broken === live || !live) {
233
+ console.error('✗ D3b: mutation target not found — the gate it guards may be dead.');
234
+ failed++;
223
235
  } else {
224
- writeFileSync(patternsFile, original.replace(/SENSITIVE_PATS="[^"]*"/, brokenLine));
236
+ writeFileSync(patternsFile, original.replace(d3bPattern, brokenLine));
225
237
  assert('missing new categories → lane-integrity fails', false, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
226
238
  }
227
239
  } finally {
@@ -238,12 +250,14 @@ if (!existsSync(savepointFile)) {
238
250
  const original = readFileSync(savepointFile, 'utf8');
239
251
  try {
240
252
  // revert to delta-based (0 on deletions/refactors)
253
+ const d4Pattern = /LOC_TOKENS=\$\(\( LOC_CHURN \* 12 \)\)/;
241
254
  const broken = original.replace(
242
- /LOC_TOKENS=\$\(\( LOC_CHURN \* 12 \)\)/,
255
+ d4Pattern,
243
256
  'LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))'
244
257
  );
245
- if (broken === original) {
246
- console.log('D4 mutation pattern not found — skipping');
258
+ if (!d4Pattern.test(original) || broken === original) {
259
+ console.error('D4: mutation target not found — the gate it guards may be dead.');
260
+ failed++;
247
261
  } else {
248
262
  writeFileSync(savepointFile, broken);
249
263
  assert('zero churn tokens → lane-integrity fails', false, () => run('D4', 'bun run test -- lane-integrity -t "churn"'));
@@ -263,12 +277,14 @@ if (!existsSync(savepointFile)) {
263
277
  try {
264
278
  // silently drop the continue writer block (make it a no-op). Anchor on the
265
279
  // D10 header comment so the regex hits the writer, not the STATE_FILE if.
280
+ const d10Pattern = /# --- continue.*\(D10\): machine-written resume point ---[\s\S]*?\nfi\n\n/;
266
281
  const broken = original.replace(
267
- /# --- continue\/<mission>\/<member>\.json \(D10\): machine-written resume point ---[\s\S]*?\nfi\n\n/,
282
+ d10Pattern,
268
283
  '# --- continue writer disabled (D10) ---\n\n'
269
284
  );
270
- if (broken === original) {
271
- console.log('D10 mutation pattern not found — skipping');
285
+ if (!d10Pattern.test(original) || broken === original) {
286
+ console.error('D10: mutation target not found — the gate it guards may be dead.');
287
+ failed++;
272
288
  } else {
273
289
  writeFileSync(savepointFile, broken);
274
290
  assert('broken continue writer → savepoint fails', false, () => run('D10', 'bun run test -- savepoint -t "D10"'));
@@ -612,5 +628,207 @@ console.log('\nDOCLINKS — doc link resolution');
612
628
  }
613
629
  }
614
630
 
631
+ // --- T3: lane-aware gates — direct 3 steps, full 12 steps ---
632
+ console.log('\nT3 — lane-aware gates');
633
+ {
634
+ const policyFile = join(root, 'src', 'policy.ts');
635
+ const originalPolicy = readFileSync(policyFile, 'utf8');
636
+ try {
637
+ assert('direct lane → 3 steps', true, () => gatesForLane('direct').length === 3);
638
+ assert('direct lane includes typecheck+build', true, () => {
639
+ const s = gatesForLane('direct');
640
+ return s.includes('typecheck') && s.includes('build');
641
+ });
642
+ assert('lean lane → 6 steps with validate-content', true, () => {
643
+ const s = gatesForLane('lean');
644
+ return s.length === 6 && s.includes('validate-content');
645
+ });
646
+ assert('standard lane → 9 steps', true, () => gatesForLane('standard').length === 9);
647
+ assert('full lane → 12 steps with evals/retrieval/conformance', true, () => {
648
+ const s = gatesForLane('full');
649
+ return s.length === 12 && s.includes('run-evals') && s.includes('retrieval-eval') && s.includes('conformance');
650
+ });
651
+ assert('budget direct → 0, full → 50000', true, () => budgetForLane('direct') === 0 && budgetForLane('full') === 50000);
652
+ assert('budget spike → 9000 (direct fixture 9k)', true, () => budgetForLane('spike') === 9000);
653
+ // mutation: break direct step count → should fail (file content shows not 3)
654
+ const broken = originalPolicy.replace(
655
+ "direct: ['build-hooks:check', 'typecheck', 'build']",
656
+ "direct: ['typecheck']"
657
+ );
658
+ if (broken !== originalPolicy) {
659
+ writeFileSync(policyFile, broken);
660
+ assert('broken direct gate → not 3 steps', false, () => readFileSync(policyFile, 'utf8').includes("direct: ['build-hooks:check', 'typecheck', 'build']"));
661
+ } else {
662
+ console.error('✗ T3: mutation target not found — the gate it guards may be dead.');
663
+ failed++;
664
+ }
665
+ } finally {
666
+ writeFileSync(policyFile, originalPolicy);
667
+ assert('restored → direct 3 steps', true, () => {
668
+ const txt = readFileSync(policyFile, 'utf8');
669
+ return txt.includes("direct: ['build-hooks:check', 'typecheck', 'build']");
670
+ });
671
+ }
672
+ assert('full still includes conformance (conformance 71→74)', true, () => {
673
+ const txt = readFileSync(policyFile, 'utf8');
674
+ return txt.includes("'conformance'") && txt.includes("full:");
675
+ });
676
+ }
677
+
678
+ // --- B1: CLI availability — remove section → content validation fails ---
679
+ console.log('\nB1 — CLI availability');
680
+ {
681
+ const wf = join(root, 'content', 'skills', 'mugiwara-workflow', 'SKILL.md');
682
+ const original = readFileSync(wf, 'utf8');
683
+ try {
684
+ const b1Pattern = /## CLI availability[\s\S]*?## Artifact trust/;
685
+ const b1Broken = original.replace(b1Pattern, '## Artifact trust');
686
+ if (!b1Pattern.test(original) || b1Broken === original) {
687
+ console.error('✗ B1: mutation target not found — the gate it guards may be dead.');
688
+ failed++;
689
+ } else {
690
+ writeFileSync(wf, b1Broken);
691
+ assert('missing CLI availability → grep fails', false, () => run('B1-grep', 'grep -q "CLI availability" content/skills/mugiwara-workflow/SKILL.md'));
692
+ }
693
+ } finally {
694
+ writeFileSync(wf, original);
695
+ assert('restored → content validation passes', true, () => run('B1-restore', 'grep -q "CLI availability" content/skills/mugiwara-workflow/SKILL.md'));
696
+ }
697
+ }
698
+
699
+ // --- B2: team evidence gate — revert to single state.json → integrity gate fails on team ---
700
+ console.log('\nB2 — team evidence gate');
701
+ {
702
+ const integ = join(root, 'src', 'integrity.ts');
703
+ const original = readFileSync(integ, 'utf8');
704
+ try {
705
+ const fixedPattern = /const stateFiles = existsSync\(missionDir\)/;
706
+ const broken = original.replace(fixedPattern, "const evidenceFile = join(missionDir, 'state.json'); // B2 revert");
707
+ const fullPattern = / \/\/ Solo layout writes state\.json; team layout writes <member>\.json per member\./;
708
+ let b2Broken = original;
709
+ if (fullPattern.test(original)) {
710
+ // remove the team-aware block header to make grep for stateFiles fail for the specific definition
711
+ b2Broken = original.replace(fixedPattern, "const evidenceFile = join(missionDir, 'state.json'); // B2 revert");
712
+ // also need to remove remaining stateFiles references to make grep fail - replace all stateFiles with evidenceFile
713
+ b2Broken = b2Broken.replace(/stateFiles/g, 'evidenceFile');
714
+ }
715
+ if (!fixedPattern.test(original) || broken === original) {
716
+ console.error('✗ B2: mutation target not found — the gate it guards may be dead.');
717
+ failed++;
718
+ } else {
719
+ writeFileSync(integ, b2Broken);
720
+ assert('single state.json → team evidence gate dead', false, () => run('B2', 'grep -q "const stateFiles = existsSync" src/integrity.ts'));
721
+ }
722
+ } finally {
723
+ writeFileSync(integ, original);
724
+ assert('restored → team gate present', true, () => run('B2-restore', 'grep -q "const stateFiles = existsSync" src/integrity.ts'));
725
+ }
726
+ }
727
+
728
+ // --- B3: task counter — restore unanchored grep → savepoint task test fails ---
729
+ console.log('\nB3 — task counter');
730
+ {
731
+ const sp = join(root, 'scripts', 'savepoint.sh');
732
+ const original = readFileSync(sp, 'utf8');
733
+ try {
734
+ const broken = original.replace('TASKS_TOTAL=$(count_boxes "$PLAN_FILE" \'[ xX]\')', 'TASKS_TOTAL=$(grep -cE \'^\\s*-\\s*\\[[ xX]\\]\' "$PLAN_FILE" 2>/dev/null || true)')
735
+ .replace('TASKS_DONE=$(count_boxes "$PLAN_FILE" \'[xX]\')', 'TASKS_DONE=$(grep -c \'\\[x\\]\' "$PLAN_FILE" 2>/dev/null || true)');
736
+ if (broken === original) {
737
+ console.error('✗ B3: mutation target not found — the gate it guards may be dead.');
738
+ failed++;
739
+ } else {
740
+ writeFileSync(sp, broken);
741
+ assert('unanchored grep → savepoint task test fails', false, () => run('B3', 'bun run test -- savepoint -t "B3: task counting"'));
742
+ }
743
+ } finally {
744
+ writeFileSync(sp, original);
745
+ assert('restored → savepoint task test passes', true, () => run('B3-restore', 'bun run test -- savepoint -t "B3: task counting"'));
746
+ }
747
+ }
748
+
749
+ // --- B4: repo root — restore [ -d .git ] → lane subdirectory test fails ---
750
+ console.log('\nB4 — repo root');
751
+ {
752
+ const lane = join(root, 'scripts', 'lane.sh');
753
+ const original = readFileSync(lane, 'utf8');
754
+ try {
755
+ const broken = original.replace(
756
+ /# Resolve the repo root: handles subdirectories and git worktrees[\s\S]*?cd "\$REPO_ROOT" \|\| \{ echo "lane: cannot enter repo root" >&2; exit 1; \}/,
757
+ '[ -d .git ] || { echo "lane: not a git repository" >&2; exit 1; }'
758
+ );
759
+ if (broken === original) {
760
+ console.error('✗ B4: mutation target not found — the gate it guards may be dead.');
761
+ failed++;
762
+ } else {
763
+ writeFileSync(lane, broken);
764
+ assert('[ -d .git ] → lane subdirectory gate dead', false, () => run('B4', 'grep -q "git rev-parse --show-toplevel" scripts/lane.sh'));
765
+ }
766
+ } finally {
767
+ writeFileSync(lane, original);
768
+ assert('restored → lane uses git rev-parse', true, () => run('B4-restore', 'grep -q "git rev-parse --show-toplevel" scripts/lane.sh'));
769
+ }
770
+ }
771
+
772
+ // --- B5: spike budget — set below base → lane-base fails ---
773
+ console.log('\nB5 — spike budget');
774
+ {
775
+ const baseFile = join(root, 'scripts', 'lib', 'lane-base.sh');
776
+ const original = readFileSync(baseFile, 'utf8');
777
+ try {
778
+ const broken = original.replace('BUDGET_spike=9000', 'BUDGET_spike=3000');
779
+ if (broken === original) {
780
+ console.error('✗ B5: mutation target not found — the gate it guards may be dead.');
781
+ failed++;
782
+ } else {
783
+ writeFileSync(baseFile, broken);
784
+ assert('BUDGET_spike 3000 < LANE_BASE 5411 → lane-base fails', false, () => run('B5', 'bun scripts/lane-base.ts'));
785
+ }
786
+ } finally {
787
+ writeFileSync(baseFile, original);
788
+ assert('restored → lane-base passes', true, () => run('B5-restore', 'bun scripts/lane-base.ts'));
789
+ }
790
+ }
791
+
792
+ // --- B6: corrupt state — swallow parse errors again → status test fails ---
793
+ console.log('\nB6 — corrupt state');
794
+ {
795
+ const cont = join(root, 'src', 'continue.ts');
796
+ const original = readFileSync(cont, 'utf8');
797
+ try {
798
+ const broken = original.replace('unreadable.push(join(mission, f));', '// corrupt savepoint — skip, never crash the listing');
799
+ if (broken === original) {
800
+ console.error('✗ B6: mutation target not found — the gate it guards may be dead.');
801
+ failed++;
802
+ } else {
803
+ writeFileSync(cont, broken);
804
+ assert('swallow parse errors → unreadable gate dead', false, () => run('B6', 'grep -q "unreadableStateFiles" src/continue.ts && grep -q "unreadable.push" src/continue.ts'));
805
+ }
806
+ } finally {
807
+ writeFileSync(cont, original);
808
+ assert('restored → corrupt state surfaced', true, () => run('B6-restore', 'grep -q "unreadable.push" src/continue.ts'));
809
+ }
810
+ }
811
+
812
+ // --- B7: zero evidence — remove check → integrity team test fails ---
813
+ console.log('\nB7 — zero evidence');
814
+ {
815
+ const integ = join(root, 'src', 'integrity.ts');
816
+ const original = readFileSync(integ, 'utf8');
817
+ try {
818
+ const b7Pattern = /mission declares no evidence/;
819
+ const broken = original.replace(b7Pattern, 'ZERO_EVIDENCE_REMOVED');
820
+ if (!b7Pattern.test(original) || broken === original) {
821
+ console.error('✗ B7: mutation target not found — the gate it guards may be dead.');
822
+ failed++;
823
+ } else {
824
+ writeFileSync(integ, broken);
825
+ assert('no zero-evidence check → integrity gate dead', false, () => run('B7', 'grep -q "mission declares no evidence" src/integrity.ts'));
826
+ }
827
+ } finally {
828
+ writeFileSync(integ, original);
829
+ assert('restored → zero-evidence warned', true, () => run('B7-restore', 'grep -q "mission declares no evidence" src/integrity.ts'));
830
+ }
831
+ }
832
+
615
833
  console.log(`\n${passed} passed, ${failed} failed`);
616
834
  process.exit(failed > 0 ? 1 : 0);