@ionivetech/mugiwara 0.6.2 → 0.6.4

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 (73) 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/.opencode/commands/mugiwara-continue.md +42 -10
  7. package/.opencode/commands/mugiwara-execute.md +1 -1
  8. package/.opencode/commands/mugiwara-heal.md +1 -1
  9. package/.opencode/commands/mugiwara-plan.md +1 -1
  10. package/.opencode/commands/mugiwara-review.md +1 -1
  11. package/.opencode/commands/mugiwara-security.md +1 -1
  12. package/.opencode/commands/mugiwara-ship.md +1 -1
  13. package/.opencode/mugiwara-helpers.mjs +1 -1
  14. package/.opencode/plugins/mugiwara.mjs +62 -20
  15. package/AGENTS.md +16 -5
  16. package/README.md +67 -15
  17. package/content/agents/brook-healing.md +1 -1
  18. package/content/agents/chopper-checkpoint.md +1 -1
  19. package/content/agents/eval-runner.md +1 -1
  20. package/content/agents/franky-gates.md +1 -1
  21. package/content/agents/jinbe-security.md +1 -1
  22. package/content/agents/memory-keeper.md +1 -1
  23. package/content/agents/nami-planner.md +2 -2
  24. package/content/agents/resume-coordinator.md +11 -11
  25. package/content/agents/robin-reviewer.md +1 -1
  26. package/content/agents/sanji-quality.md +1 -1
  27. package/content/agents/skeptic-verifier.md +1 -1
  28. package/content/agents/usopp-brainstorm.md +2 -2
  29. package/content/agents/zoro-execution.md +3 -3
  30. package/content/skills/mugiwara-agent-security/SKILL.md +1 -18
  31. package/content/skills/mugiwara-agent-security/references/checklist.md +20 -0
  32. package/content/skills/mugiwara-brainstorm/SKILL.md +7 -1
  33. package/content/skills/mugiwara-execution/SKILL.md +14 -13
  34. package/content/skills/mugiwara-execution/references/resume-batching.md +4 -4
  35. package/content/skills/mugiwara-healing/SKILL.md +2 -37
  36. package/content/skills/mugiwara-healing/references/workers.md +38 -0
  37. package/content/skills/mugiwara-orchestration/SKILL.md +11 -8
  38. package/content/skills/mugiwara-orchestration/references/check-ins.md +26 -7
  39. package/content/skills/mugiwara-orchestration/references/closure.md +9 -0
  40. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +9 -12
  41. package/content/skills/mugiwara-planning/SKILL.md +7 -11
  42. package/content/skills/mugiwara-planning/references/plan-template.md +4 -4
  43. package/content/skills/mugiwara-pr/SKILL.md +11 -19
  44. package/content/skills/mugiwara-pr/references/verdict-format.md +31 -0
  45. package/content/skills/mugiwara-resume/SKILL.md +37 -22
  46. package/content/skills/mugiwara-ship/SKILL.md +1 -23
  47. package/content/skills/mugiwara-ship/references/cleanup.md +25 -0
  48. package/content/skills/mugiwara-workflow/SKILL.md +7 -3
  49. package/content/skills/mugiwara-workflow/references/workspace-layout.md +11 -3
  50. package/content/skills/using-mugiwara/SKILL.md +1 -1
  51. package/dist/mugiwara.js +138 -42
  52. package/gemini-extension.json +1 -1
  53. package/hooks/session-start.ts +113 -2
  54. package/package.json +3 -3
  55. package/plugin.json +1 -1
  56. package/references/multi-actor.md +39 -14
  57. package/references/skill-versioning.md +3 -3
  58. package/references/token-budget.md +1 -1
  59. package/references/wave-banners.md +75 -0
  60. package/scripts/evidence.sh +9 -0
  61. package/scripts/gate-selftest.ts +154 -0
  62. package/scripts/lane-base.ts +114 -0
  63. package/scripts/lane.sh +5 -3
  64. package/scripts/lib/lane-base.sh +19 -0
  65. package/scripts/lib/patterns.sh +21 -0
  66. package/scripts/mission-report.sh +21 -5
  67. package/scripts/savepoint.sh +170 -56
  68. package/scripts/setup-fixtures.ts +108 -0
  69. package/scripts/validate-content.ts +61 -0
  70. package/src/cli.ts +5 -1
  71. package/src/installer.ts +70 -8
  72. package/src/mission.ts +37 -19
  73. package/src/targets/opencode.ts +34 -6
@@ -0,0 +1,75 @@
1
+ # Wave Banners — formats + crew colors
2
+
3
+ The single source for wave-banner rendering and crew colors. The opencode
4
+ plugin and the opencode-target generator derive agent UI colors from the table
5
+ below — change a color HERE, never in code. This file is machine-parsed:
6
+ keep the table format exact (one row per agent, pipes, no extra columns).
7
+
8
+ ## Terminal format (ANSI truecolor)
9
+
10
+ Every wave opens with the banner line in the owning agent's color, and closes
11
+ with a handoff line. Wrap the whole line in the ANSI truecolor of the agent:
12
+
13
+ ```
14
+ \x1b[38;2;R;G;Bm==================== WAVE 3 — ZORO (EXECUTION) ====================\x1b[0m
15
+ → Wave 4 — Chopper (Checkpoint)
16
+ ```
17
+
18
+ - RGB values come from the `hex` column below; R/G/B are the hex channels in
19
+ decimal (truecolor `38;2;R;G;B`). If the terminal lacks truecolor, use the
20
+ `ansi-256` index: `\x1b[38;5;Nm`.
21
+ - Wave 9's handoff: `→ closure`.
22
+ - The literal `WAVE N —` text must stay exact: `scripts/savepoint.sh` counts
23
+ heal cycles by grepping `wave 8` (case-insensitive) in the trace. A banner
24
+ that drops the literal silently resets the heal loop.
25
+ - Never convey the wave by color alone — the crew name and emoji always
26
+ accompany the color.
27
+
28
+ ## UI format (markdown UIs)
29
+
30
+ Rich UIs (Claude Code UI, VSCode, Codex) strip or garble ANSI escapes. Emit
31
+ the emoji-heading form instead — no ANSI, no equals line:
32
+
33
+ ```
34
+ ## ⚔️ WAVE 3 — ZORO (EXECUTION)
35
+ → Wave 4 — Chopper (Checkpoint)
36
+ ```
37
+
38
+ Same literal `WAVE N —` rule applies.
39
+
40
+ ## Crew colors
41
+
42
+ | agent-id | role | hex | ansi-256 | emoji |
43
+ |----------|------|-----|----------|-------|
44
+ | luffy-orchestrator | Luffy | #ef4444 | 196 | 🏴‍☠️ |
45
+ | usopp-brainstorm | Usopp | #b45309 | 130 | 🎯 |
46
+ | nami-planner | Nami | #f97316 | 208 | 🧭 |
47
+ | zoro-execution | Zoro | #22c55e | 34 | ⚔️ |
48
+ | chopper-checkpoint | Chopper | #60a5fa | 75 | 🩺 |
49
+ | sanji-quality | Sanji | #facc15 | 220 | 🍳 |
50
+ | franky-gates | Franky | #06b6d4 | 45 | 🔧 |
51
+ | robin-reviewer | Robin | #8b5cf6 | 99 | 📚 |
52
+ | jinbe-security | Jinbe | #6366f1 | 63 | 🌊 |
53
+ | brook-healing | Brook | #2dd4bf | 43 | 🎻 |
54
+ | skeptic-verifier | Skeptic | #64748b | 245 | 🔍 |
55
+ | eval-runner | EvalRunner | #14b8a6 | 37 | 🧪 |
56
+ | resume-coordinator | Resume | #d97706 | 172 | 🔄 |
57
+ | memory-keeper | MemoryKeeper | #d946ef | 200 | 🧠 |
58
+ | onboarding-guide | Guide | #0ea5e9 | 75 | 🚀 |
59
+
60
+ Skeptic, EvalRunner, Resume, MemoryKeeper and Guide are internal/dispatch or
61
+ wizard agents — their banners appear only when a wave or worker names them.
62
+
63
+ ## Rules
64
+
65
+ 1. Banner before EVERY wave; handoff after it. No wave starts without its
66
+ banner (orchestration red flag).
67
+ 2. The color comes from this table only — never invent a hex mid-mission.
68
+ 3. Terminal variant when the session looks like a terminal; UI variant for
69
+ markdown-rendering UIs. When unsure, the UI variant is safe everywhere.
70
+ 4. Only the crew table's colors and the two SGR forms above (truecolor,
71
+ 256-index) may appear in a banner — never other escape families (OSC,
72
+ title, cursor, other SGR codes). The banner is a fixed template, not a
73
+ formatting playground.
74
+ 5. Adding a crew member? Add the row here first; the plugin and target
75
+ generator pick it up automatically.
@@ -55,5 +55,14 @@ EVIDENCE_FILE="$RESULTS_DIR/${LABEL}-${HASH}.log"
55
55
  } > "$EVIDENCE_FILE"
56
56
  EXIT_CODE=$?
57
57
 
58
+ # trailer: exit code + verdict (D6). The verdict is PASS/FAIL derived from the
59
+ # exit code — the check's own outcome, not the harness's opinion.
60
+ if [ "$EXIT_CODE" -eq 0 ]; then
61
+ VERDICT="PASS"
62
+ else
63
+ VERDICT="FAIL"
64
+ fi
65
+ printf '# Exit: %s\n# Verdict: %s\n' "$EXIT_CODE" "$VERDICT" >> "$EVIDENCE_FILE"
66
+
58
67
  echo "$EVIDENCE_FILE"
59
68
  exit $EXIT_CODE
@@ -89,6 +89,160 @@ if (!existsSync(join(root, 'test', 'savepoint.test.ts'))) {
89
89
  }
90
90
  }
91
91
 
92
+ // --- D1 mutation: break LANE_PREV resolve (require-style) → lane-integrity red ---
93
+ console.log('\nD1 — LANE_PREV resolve mutation');
94
+ const savepointFile = join(root, 'scripts', 'savepoint.sh');
95
+ if (!existsSync(savepointFile)) {
96
+ console.log(' ⚠ savepoint.sh not found, skipping');
97
+ } else {
98
+ const original = readFileSync(savepointFile, 'utf8');
99
+ try {
100
+ // reintroduce the D1 defect: read lane_prev with require() of a relative path
101
+ const broken = original.replace(
102
+ /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\)/,
103
+ "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)"
104
+ );
105
+ if (broken === original) {
106
+ console.log(' ⚠ D1 mutation pattern not found — skipping');
107
+ } else {
108
+ writeFileSync(savepointFile, broken);
109
+ assert('broken LANE_PREV resolve → lane-integrity fails', false, () => run('D1', 'bun run test -- lane-integrity -t "lane_prev"'));
110
+ }
111
+ } finally {
112
+ writeFileSync(savepointFile, original);
113
+ assert('restored → lane-integrity passes', true, () => run('D1', 'bun run test -- lane-integrity -t "lane_prev"'));
114
+ }
115
+ }
116
+
117
+ // --- D2 mutation: break monotonic clamp (drop lane_rank) → red ---
118
+ console.log('\nD2 — monotonic clamp mutation');
119
+ if (!existsSync(savepointFile)) {
120
+ console.log(' ⚠ savepoint.sh not found, skipping');
121
+ } else {
122
+ const original = readFileSync(savepointFile, 'utf8');
123
+ try {
124
+ // neuter the clamp: make lane_rank always return 0 so a drop never holds
125
+ const broken = original.replace(
126
+ /lane_rank\(\) \{\n case "\$1" in\n direct\) echo 0 ;;[\s\S]*?\n esac\n\}/,
127
+ 'lane_rank() {\n echo 0\n}'
128
+ );
129
+ if (broken === original) {
130
+ console.log(' ⚠ D2 mutation pattern not found — skipping');
131
+ } else {
132
+ writeFileSync(savepointFile, broken);
133
+ assert('broken clamp → lane-integrity fails', false, () => run('D2', 'bun run test -- lane-integrity -t "clamp"'));
134
+ }
135
+ } finally {
136
+ writeFileSync(savepointFile, original);
137
+ assert('restored → lane-integrity passes', true, () => run('D2', 'bun run test -- lane-integrity -t "clamp"'));
138
+ }
139
+ }
140
+
141
+ // --- D3 mutation: empty SENSITIVE_PATS → lane-integrity red ---
142
+ console.log('\nD3 — sensitive patterns mutation');
143
+ const patternsFile = join(root, 'scripts', 'lib', 'patterns.sh');
144
+ if (!existsSync(patternsFile)) {
145
+ console.log(' ⚠ patterns.sh not found, skipping');
146
+ } else {
147
+ const original = readFileSync(patternsFile, 'utf8');
148
+ try {
149
+ // reintroduce the D3 defect: singular-only list (no payments/, migrations/)
150
+ const broken = original.replace(
151
+ /SENSITIVE_PATS=.*/,
152
+ 'SENSITIVE_PATS="auth/|payment/|billing/|crypto/|secrets/|\\.env$|config/.*key|migration/|\\.sql$|schema\\.|\\.prisma$|\\.terraform|\\.tf$"'
153
+ );
154
+ if (broken === original) {
155
+ console.log(' ⚠ D3 mutation pattern not found — skipping');
156
+ } else {
157
+ writeFileSync(patternsFile, broken);
158
+ assert('singular sensitive patterns → lane-integrity fails', false, () => run('D3', 'bun run test -- lane-integrity -t "payments"'));
159
+ }
160
+ } finally {
161
+ writeFileSync(patternsFile, original);
162
+ assert('restored → lane-integrity passes', true, () => run('D3', 'bun run test -- lane-integrity -t "payments"'));
163
+ }
164
+ }
165
+
166
+ // --- D3b mutation: strip the NEW pattern categories → lane-integrity red ---
167
+ console.log('\nD3b — new category strip mutation');
168
+ if (!existsSync(patternsFile)) {
169
+ console.log(' ⚠ patterns.sh not found, skipping');
170
+ } else {
171
+ const original = readFileSync(patternsFile, 'utf8');
172
+ try {
173
+ // derive the broken list from the LIVE source, stripping the exact tokens
174
+ // of the v0.6.4 D3 families — a hard-coded baseline rots the moment a
175
+ // category is added (G3). New family tokens must be added here too.
176
+ const D3B_FAMILY_TOKENS = new Set([
177
+ // raw regex tokens as they appear in live SENSITIVE_PATS (backslashes included)
178
+ 'oauth2?/', 'credential', 'sessions?/', 'tokens?/', 'rbac', 'permissions?/', 'acls?/', 'iam/',
179
+ '\\.p12$', '\\.key$', '\\.pem$', 'migrate/', 'Dockerfile', 'docker-compose', '\\.github/workflows/',
180
+ 'webhooks?/', 'secret/', 'secrets?\\.ya?ml$', '\\.tfvars$', '\\.env$', '\\.env\\.',
181
+ ]);
182
+ const live = original.match(/SENSITIVE_PATS="([^"]+)"/)?.[1] ?? '';
183
+ const broken = live.split('|').filter(t => !D3B_FAMILY_TOKENS.has(t)).join('|');
184
+ const brokenLine = `SENSITIVE_PATS="${broken}"`;
185
+ if (broken === live || !live) {
186
+ console.log(' ⚠ D3b: no D3 family tokens found in live SENSITIVE_PATS — skipping');
187
+ } else {
188
+ writeFileSync(patternsFile, original.replace(/SENSITIVE_PATS="[^"]*"/, brokenLine));
189
+ assert('missing new categories → lane-integrity fails', false, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
190
+ }
191
+ } finally {
192
+ writeFileSync(patternsFile, original);
193
+ assert('restored → lane-integrity passes', true, () => run('D3b', 'bun run test -- lane-integrity -t "sensitive-paths"'));
194
+ }
195
+ }
196
+
197
+ // --- D4 mutation: zero LOC_TOKENS → lane-integrity red ---
198
+ console.log('\nD4 — churn token mutation');
199
+ if (!existsSync(savepointFile)) {
200
+ console.log(' ⚠ savepoint.sh not found, skipping');
201
+ } else {
202
+ const original = readFileSync(savepointFile, 'utf8');
203
+ try {
204
+ // revert to delta-based (0 on deletions/refactors)
205
+ const broken = original.replace(
206
+ /LOC_TOKENS=\$\(\( LOC_CHURN \* 12 \)\)/,
207
+ 'LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))'
208
+ );
209
+ if (broken === original) {
210
+ console.log(' ⚠ D4 mutation pattern not found — skipping');
211
+ } else {
212
+ writeFileSync(savepointFile, broken);
213
+ assert('zero churn tokens → lane-integrity fails', false, () => run('D4', 'bun run test -- lane-integrity -t "churn"'));
214
+ }
215
+ } finally {
216
+ writeFileSync(savepointFile, original);
217
+ assert('restored → lane-integrity passes', true, () => run('D4', 'bun run test -- lane-integrity -t "churn"'));
218
+ }
219
+ }
220
+
221
+ // --- D10 mutation: break continue writer → savepoint test red ---
222
+ console.log('\nD10 — continue writer mutation');
223
+ if (!existsSync(savepointFile)) {
224
+ console.log(' ⚠ savepoint.sh not found, skipping');
225
+ } else {
226
+ const original = readFileSync(savepointFile, 'utf8');
227
+ try {
228
+ // silently drop the continue writer block (make it a no-op). Anchor on the
229
+ // D10 header comment so the regex hits the writer, not the STATE_FILE if.
230
+ const broken = original.replace(
231
+ /# --- continue\/<mission>\/<member>\.json \(D10\): machine-written resume point ---[\s\S]*?\nfi\n\n/,
232
+ '# --- continue writer disabled (D10) ---\n\n'
233
+ );
234
+ if (broken === original) {
235
+ console.log(' ⚠ D10 mutation pattern not found — skipping');
236
+ } else {
237
+ writeFileSync(savepointFile, broken);
238
+ assert('broken continue writer → savepoint fails', false, () => run('D10', 'bun run test -- savepoint -t "D10"'));
239
+ }
240
+ } finally {
241
+ writeFileSync(savepointFile, original);
242
+ assert('restored → savepoint passes', true, () => run('D10', 'bun run test -- savepoint -t "D10"'));
243
+ }
244
+ }
245
+
92
246
  // --- Retrieval eval ratchet ---
93
247
  console.log('\nRetrieval eval ratchet');
94
248
  if (!existsSync(join(root, 'scripts', 'retrieval-eval.ts'))) {
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bun
2
+ // scripts/lane-base.ts — G5: validate LANE_BASE constants against measured
3
+ // content instruction load. Each lane loads a fixed set of skills + agents
4
+ // (wave owners from docs/concepts/workflow.md). The honest base is the sum of
5
+ // their body word-sums × 1.35 tokens/word. A constant that drifts more than
6
+ // the tolerance from that load fails — content growth must be reflected in
7
+ // lane-base.sh, or the estimate is a lie (D5).
8
+ //
9
+ // Run: bun scripts/lane-base.ts — validate (CI gate)
10
+ // bun scripts/lane-base.ts --show — print computed load per lane
11
+
12
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+
15
+ const root = join(import.meta.dirname, '..');
16
+ const skills = join(root, 'content', 'skills');
17
+ const agents = join(root, 'content', 'agents');
18
+
19
+ // wave → owning skill + agent (workflow.md rows)
20
+ const WAVE_OWNER: Record<string, [string, string]> = {
21
+ '0': ['mugiwara-orchestration', 'luffy-orchestrator'],
22
+ '1': ['mugiwara-brainstorm', 'usopp-brainstorm'],
23
+ '2': ['mugiwara-planning', 'nami-planner'],
24
+ '3': ['mugiwara-execution', 'zoro-execution'],
25
+ '4': ['mugiwara-checkpoint', 'chopper-checkpoint'],
26
+ '4.5': ['mugiwara-claim-audit', 'skeptic-verifier'],
27
+ '5': ['mugiwara-quality', 'sanji-quality'],
28
+ '6': ['mugiwara-gates', 'franky-gates'],
29
+ '7': ['mugiwara-review', 'robin-reviewer'],
30
+ '8': ['mugiwara-healing', 'brook-healing'],
31
+ };
32
+
33
+ // skills always loaded regardless of lane: none — resume/lessons only load
34
+ // mid-mission on interruption, too cheap to count as a lane base.
35
+ const ALWAYS: Array<[string, string]> = [];
36
+
37
+ const LANE_WAVES: Record<string, string[]> = {
38
+ direct: [],
39
+ lean: ['0', '3', '5'],
40
+ standard: ['0', '2', '3', '4', '7'],
41
+ full: ['0', '1', '2', '3', '4', '4.5', '5', '6', '7', '8'],
42
+ spike: ['0', '1'],
43
+ };
44
+
45
+ const TOKENS_PER_WORD = 1.35;
46
+ const TOLERANCE = 0.2; // ±20% — a real drift, not rounding
47
+
48
+ function wordCount(file: string): number {
49
+ if (!existsSync(file)) return 0;
50
+ return readFileSync(file, 'utf8').split(/\s+/).filter(Boolean).length;
51
+ }
52
+
53
+ function laneLoad(lane: string): number {
54
+ const waves = LANE_WAVES[lane] ?? [];
55
+ const owners: Array<[string, string]> = [...ALWAYS];
56
+ for (const w of waves) {
57
+ if (WAVE_OWNER[w]) owners.push(WAVE_OWNER[w]);
58
+ }
59
+ let words = 0;
60
+ for (const [skill, agent] of owners) {
61
+ words += wordCount(join(skills, skill, 'SKILL.md'));
62
+ words += wordCount(join(agents, agent + '.md'));
63
+ }
64
+ return Math.round(words * TOKENS_PER_WORD);
65
+ }
66
+
67
+ // constants live in scripts/lib/lane-base.sh — parse them, not hardcode
68
+ function parseConstants(): Record<string, { base: number; budget: number }> {
69
+ const text = readFileSync(join(root, 'scripts', 'lib', 'lane-base.sh'), 'utf8');
70
+ const out: Record<string, { base: number; budget: number }> = {};
71
+ for (const lane of ['lean', 'standard', 'full', 'spike']) {
72
+ const base = text.match(new RegExp(`LANE_BASE_${lane}=(\\d+)`));
73
+ const budget = text.match(new RegExp(`BUDGET_${lane}=(\\d+)`));
74
+ out[lane] = {
75
+ base: base ? parseInt(base[1], 10) : 0,
76
+ budget: budget ? parseInt(budget[1], 10) : 0,
77
+ };
78
+ }
79
+ return out;
80
+ }
81
+
82
+ const SHOW = process.argv.includes('--show');
83
+ const lanes = ['direct', 'lean', 'standard', 'full', 'spike'];
84
+ let failures = 0;
85
+
86
+ const constants = parseConstants();
87
+ for (const lane of lanes) {
88
+ const computed = laneLoad(lane);
89
+ const declared = constants[lane]?.base ?? 0;
90
+ if (SHOW) {
91
+ console.log(`${lane.padEnd(9)} computed=${computed.toString().padEnd(6)} declared=${declared} budget=${constants[lane]?.budget ?? 0}`);
92
+ }
93
+ // direct runs no pipeline — base 0 is correct by design, skip
94
+ if (lane === 'direct') continue;
95
+ // spike is a deliberate floor (resize lane, brainstorm only) — not
96
+ // content-derived, never validated
97
+ if (lane === 'spike') continue;
98
+ if (declared === 0) {
99
+ console.log(` ✗ ${lane}: LANE_BASE is 0 — missing constant in lane-base.sh`);
100
+ failures++;
101
+ continue;
102
+ }
103
+ const drift = Math.abs(computed - declared) / declared;
104
+ if (drift > TOLERANCE) {
105
+ console.log(` ✗ ${lane}: LANE_BASE ${declared} drifts ${(drift * 100).toFixed(0)}% from computed load ${computed} — update lane-base.sh`);
106
+ failures++;
107
+ }
108
+ }
109
+
110
+ if (failures > 0) {
111
+ console.log(`\nlane-base: ${failures} constant(s) drifted from content load`);
112
+ process.exit(1);
113
+ }
114
+ console.log('\nlane-base: constants match content load');
package/scripts/lane.sh CHANGED
@@ -3,6 +3,10 @@
3
3
  # Usage: lane.sh [base-ref] [--json]
4
4
  set -u
5
5
 
6
+ # shared path patterns — single source of truth (D3)
7
+ # shellcheck source=scripts/lib/patterns.sh
8
+ source "$(dirname "$0")/lib/patterns.sh"
9
+
6
10
  BASE="${1:-main}"
7
11
  JSON_OUT=0
8
12
  [ "${2:-}" = "--json" ] && JSON_OUT=1
@@ -19,8 +23,7 @@ CHANGED=$(git diff --name-only "$BASE"..HEAD 2>/dev/null || git diff --name-only
19
23
  FILE_COUNT=0
20
24
  [ -n "$CHANGED" ] && FILE_COUNT=$(echo "$CHANGED" | wc -l | tr -d ' ')
21
25
 
22
- SENSITIVE_PATS="auth/|payment/|billing/|crypto/|secrets/|\.env$|config/.*key|migration/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$"
23
- SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | head -5 | tr '\n' ',' | sed 's/,$//' || true)
26
+ SENSITIVE=$(echo "$CHANGED" | grep -E "$SENSITIVE_PATS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
24
27
  HAS_SENSITIVE=0
25
28
  [ -n "$SENSITIVE" ] && HAS_SENSITIVE=1
26
29
 
@@ -64,7 +67,6 @@ fi
64
67
  # code surface actually touched. Sensitive-path escalation above still wins.
65
68
  # Product surface for mugiwara: content/, src/, scripts/, test/, hooks/,
66
69
  # .opencode/, .claude/, evals/ — everything else is docs/config/asset.
67
- PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
68
70
  if [ "$LANE" = "full" ] && [ "$HAS_SENSITIVE" -eq 0 ] && [ -n "$CHANGED" ]; then
69
71
  CODE_COUNT=$(echo "$CHANGED" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
70
72
  if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/lane-base.sh — LANE_BASE + BUDGET constants, single source of
3
+ # truth. Validated by scripts/lane-base.ts against measured content word-sums.
4
+ # Change a constant here → run `bun scripts/lane-base.ts`; the gate fails if
5
+ # the constant drifts from the honest instruction load (D5).
6
+
7
+ # LANE_BASE: token estimate for skills/agents loaded in this lane. Derived
8
+ # from content word-sums × 1.35 (see scripts/lane-base.ts). spike is a
9
+ # resize, not a rise — small base, tiny budget.
10
+ LANE_BASE_lean=7000
11
+ LANE_BASE_standard=13000
12
+ LANE_BASE_full=23000
13
+ LANE_BASE_spike=1000
14
+
15
+ # BUDGET: warn at 1.5×, stop at 3×.
16
+ BUDGET_lean=12000
17
+ BUDGET_standard=25000
18
+ BUDGET_full=50000
19
+ BUDGET_spike=3000
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/patterns.sh — single source of truth for lane path patterns.
3
+ # Sourced by lane.sh and savepoint.sh. Extend here, never in the consumers.
4
+
5
+ # Sensitive-path escalation patterns. A changed file matching any of these
6
+ # always escalates to Lane 3 (Full), regardless of file count. Plural forms
7
+ # included (payments/, migrations/) and, since v0.6.4, the D3 categories:
8
+ # oauth, credential(s), session(s)/, token(s)/, rbac, permission(s), acl(s)/,
9
+ # iam/, cert keys (.pem/.key/.p12), migrate/, Dockerfile, docker-compose,
10
+ # .github/workflows/, webhooks?/, secret yaml, .tfvars, and .env variants
11
+ # (.env.local, .env.production — .env$ alone missed them, D3 follow-up).
12
+ # Dir-anchored (not bare): oauth2?/, permissions?/, tokens?/, sessions?/,
13
+ # acls?/ — bare forms over-matched docs (oauth-guide.md, permissionless.ts).
14
+ # Deliberately NOT matched: package.json (dependency churn is policy-as-code,
15
+ # not a sensitive lane trigger), authors/ (contains "auth" but never auth/).
16
+ SENSITIVE_PATS="auth/|oauth2?/|payment/|payments/|billing/|crypto/|secrets/|credential|sessions?/|tokens?/|rbac|permissions?/|acls?/|iam/|\.env$|\.env\.|config/.*key|\.p12$|\.key$|\.pem$|migration/|migrations/|migrate/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$|Dockerfile|docker-compose|\.github/workflows/|webhooks?/|secret/|secrets?\.ya?ml$|\.tfvars$"
17
+
18
+ # Product surface: paths that count toward file-based lane sizing. Changes
19
+ # outside this surface are docs/config/asset and never escalate to full on
20
+ # count alone (path-weighted sizing).
21
+ PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
@@ -12,25 +12,38 @@ MISSION="${1:-}"
12
12
  case "$MISSION" in
13
13
  *[!a-zA-Z0-9._-]*) echo "mission-report: invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-])" >&2; exit 1 ;;
14
14
  esac
15
+ if [[ "$MISSION" =~ ^\.+$ ]]; then
16
+ echo "mission-report: invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-], not a dot-path)" >&2; exit 1
17
+ fi
15
18
 
16
19
  MUGIWARA_DIR="${MUGIWARA_DIR:-.mugiwara}"
17
- STATE_FILE="$MUGIWARA_DIR/state.json"
20
+ # state lives per (mission, member): solo = state/<mission>/state.json;
21
+ # MEMBER env selects a team member's state, else the solo state.
22
+ MEMBER="${MEMBER:-}"
23
+ case "$MEMBER" in
24
+ *[!a-zA-Z0-9._-]*) echo "mission-report: invalid member \"$MEMBER\" (allowlist: [a-zA-Z0-9._-])" >&2; exit 1 ;;
25
+ esac
26
+ if [ -n "$MEMBER" ]; then
27
+ STATE_FILE="$MUGIWARA_DIR/state/$MISSION/$MEMBER.json"
28
+ else
29
+ STATE_FILE="$MUGIWARA_DIR/state/$MISSION/state.json"
30
+ fi
18
31
  REPORT_DIR="$MUGIWARA_DIR/reports"
19
32
  RESULTS_DIR="$MUGIWARA_DIR/results/$MISSION"
20
33
  REVIEW_DIR="$MUGIWARA_DIR/review"
21
34
  ISSUES_DIR="$MUGIWARA_DIR/issues"
22
35
 
23
- export STATE_FILE REPORT_DIR RESULTS_DIR REVIEW_DIR ISSUES_DIR MISSION
36
+ export STATE_FILE REPORT_DIR RESULTS_DIR REVIEW_DIR ISSUES_DIR MISSION MUGIWARA_DIR
24
37
  node << 'NODE'
25
38
  const fs = require('fs');
26
39
  const path = require('path');
27
40
 
28
- const stateFile = process.env.STATE_FILE || ".mugiwara/state.json";
41
+ const stateFile = process.env.STATE_FILE || ".mugiwara/state/unknown/state.json";
29
42
  const reportDir = process.env.REPORT_DIR || ".mugiwara/reports";
30
43
  const resultsDir = process.env.RESULTS_DIR || ".mugiwara/results/unknown";
31
44
  const reviewDir = process.env.REVIEW_DIR || ".mugiwara/review";
32
45
  const issuesDir = process.env.ISSUES_DIR || ".mugiwara/issues";
33
- const mugiDir = path.dirname(stateFile);
46
+ const mugiDir = process.env.MUGIWARA_DIR || ".mugiwara";
34
47
  const mission = process.env.MISSION || "unknown";
35
48
 
36
49
  fs.mkdirSync(reportDir, { recursive: true });
@@ -56,6 +69,7 @@ const val = (obj, key, dflt) => {
56
69
  const now = new Date().toISOString().slice(0, 10);
57
70
  const reportFile = path.join(reportDir, now + "-" + mission + ".md");
58
71
  const lane = val(s, 'lane', 'n/a');
72
+ const lanePeak = val(s, 'lane_peak', 'n/a');
59
73
  const laneReason = val(s, 'lane_reason', 'n/a');
60
74
  const mode = val(s, 'mode', 'n/a');
61
75
  const actor = val(s, 'actor', 'n/a');
@@ -63,6 +77,7 @@ const branch = val(s, 'branch', 'n/a');
63
77
  const wave = val(s, 'wave', 'n/a');
64
78
  const filesTouched = val(s, 'files_touched', 0);
65
79
  const locDelta = val(s, 'loc_delta', 0);
80
+ const locChurn = val(s, 'loc_churn', 0);
66
81
  const sensitive = s ? (s.sensitive_paths || []) : [];
67
82
  const tasks = s ? (s.tasks || {}) : {};
68
83
  const blockers = val(s, 'blockers_open', 0);
@@ -213,6 +228,7 @@ report += "## Mission header\n\n| Field | Value |\n|-------|-------|\n";
213
228
  report += "| Mission | " + mission + " |\n";
214
229
  report += "| Branch | " + branch + " |\n";
215
230
  report += "| Lane | " + lane + " |\n";
231
+ report += "| Lane peak | " + lanePeak + " |\n";
216
232
  report += "| Lane reason | " + laneReason + " |\n";
217
233
  report += "| Mode | " + mode + " |\n";
218
234
  report += "| Wave | " + wave + " |\n";
@@ -232,7 +248,7 @@ report += "| Total | " + tasksTotal + " |\n";
232
248
  report += "| Source | " + tasksSource + " |\n\n";
233
249
 
234
250
  report += "## What changed\n\n";
235
- report += filesTouched + " files, +" + locDelta + " LOC";
251
+ report += filesTouched + " files, +" + locDelta + " LOC (" + locChurn + " churn)";
236
252
  if (sensitive.length) report += "\nSensitive paths: " + sensitive.join(", ");
237
253
  report += "\n\n";
238
254