@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/.opencode/commands/mugiwara-continue.md +42 -10
- package/.opencode/commands/mugiwara-execute.md +1 -1
- package/.opencode/commands/mugiwara-heal.md +1 -1
- package/.opencode/commands/mugiwara-plan.md +1 -1
- package/.opencode/commands/mugiwara-review.md +1 -1
- package/.opencode/commands/mugiwara-security.md +1 -1
- package/.opencode/commands/mugiwara-ship.md +1 -1
- package/.opencode/mugiwara-helpers.mjs +1 -1
- package/.opencode/plugins/mugiwara.mjs +62 -20
- package/AGENTS.md +16 -5
- package/README.md +67 -15
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/chopper-checkpoint.md +1 -1
- package/content/agents/eval-runner.md +1 -1
- package/content/agents/franky-gates.md +1 -1
- package/content/agents/jinbe-security.md +1 -1
- package/content/agents/memory-keeper.md +1 -1
- package/content/agents/nami-planner.md +2 -2
- package/content/agents/resume-coordinator.md +11 -11
- package/content/agents/robin-reviewer.md +1 -1
- package/content/agents/sanji-quality.md +1 -1
- package/content/agents/skeptic-verifier.md +1 -1
- package/content/agents/usopp-brainstorm.md +2 -2
- package/content/agents/zoro-execution.md +3 -3
- package/content/skills/mugiwara-agent-security/SKILL.md +1 -18
- package/content/skills/mugiwara-agent-security/references/checklist.md +20 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +7 -1
- package/content/skills/mugiwara-execution/SKILL.md +14 -13
- package/content/skills/mugiwara-execution/references/resume-batching.md +4 -4
- package/content/skills/mugiwara-healing/SKILL.md +2 -37
- package/content/skills/mugiwara-healing/references/workers.md +38 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +11 -8
- package/content/skills/mugiwara-orchestration/references/check-ins.md +26 -7
- package/content/skills/mugiwara-orchestration/references/closure.md +9 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +9 -12
- package/content/skills/mugiwara-planning/SKILL.md +7 -11
- package/content/skills/mugiwara-planning/references/plan-template.md +4 -4
- package/content/skills/mugiwara-pr/SKILL.md +11 -19
- package/content/skills/mugiwara-pr/references/verdict-format.md +31 -0
- package/content/skills/mugiwara-resume/SKILL.md +37 -22
- package/content/skills/mugiwara-ship/SKILL.md +1 -23
- package/content/skills/mugiwara-ship/references/cleanup.md +25 -0
- package/content/skills/mugiwara-workflow/SKILL.md +7 -3
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +11 -3
- package/content/skills/using-mugiwara/SKILL.md +1 -1
- package/dist/mugiwara.js +138 -42
- package/gemini-extension.json +1 -1
- package/hooks/session-start.ts +113 -2
- package/package.json +3 -3
- package/plugin.json +1 -1
- package/references/multi-actor.md +39 -14
- package/references/skill-versioning.md +3 -3
- package/references/token-budget.md +1 -1
- package/references/wave-banners.md +75 -0
- package/scripts/evidence.sh +9 -0
- package/scripts/gate-selftest.ts +154 -0
- package/scripts/lane-base.ts +114 -0
- package/scripts/lane.sh +5 -3
- package/scripts/lib/lane-base.sh +19 -0
- package/scripts/lib/patterns.sh +21 -0
- package/scripts/mission-report.sh +21 -5
- package/scripts/savepoint.sh +170 -56
- package/scripts/setup-fixtures.ts +108 -0
- package/scripts/validate-content.ts +61 -0
- package/src/cli.ts +5 -1
- package/src/installer.ts +70 -8
- package/src/mission.ts +37 -19
- 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.
|
package/scripts/evidence.sh
CHANGED
|
@@ -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
|
package/scripts/gate-selftest.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|