@ionivetech/mugiwara 0.6.0 → 0.6.1
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/mugiwara-helpers.mjs +24 -0
- package/.opencode/plugins/mugiwara.mjs +10 -2
- package/AGENTS.md +1 -1
- package/README.md +115 -62
- package/content/skills/mugiwara-brainstorm/SKILL.md +7 -0
- package/content/skills/mugiwara-execution/SKILL.md +41 -41
- package/content/skills/mugiwara-execution/references/dispatch.md +41 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +28 -24
- package/content/skills/mugiwara-orchestration/references/closure.md +34 -0
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +12 -11
- package/content/skills/mugiwara-planning/SKILL.md +6 -0
- package/content/skills/mugiwara-pr/SKILL.md +7 -0
- package/content/skills/mugiwara-quality/SKILL.md +7 -0
- package/content/skills/mugiwara-ship/SKILL.md +10 -8
- package/content/skills/mugiwara-testcases/SKILL.md +7 -0
- package/content/skills/mugiwara-workflow/SKILL.md +2 -2
- package/content/skills/using-mugiwara/SKILL.md +7 -1
- package/dist/mugiwara.js +190 -10
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.ts +0 -0
- package/hooks/session-start.ts +0 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/evidence.sh +16 -1
- package/scripts/gate-selftest.ts +52 -1
- package/scripts/initiative.ts +34 -20
- package/scripts/lane.sh +4 -2
- package/scripts/mission-report.sh +152 -29
- package/scripts/savepoint.sh +57 -13
- package/scripts/validate-content.ts +20 -0
- package/src/cli.ts +20 -3
- package/src/installer.ts +37 -1
- package/src/mission.ts +111 -1
- package/src/targets/claude.ts +27 -8
package/scripts/gate-selftest.ts
CHANGED
|
@@ -54,7 +54,7 @@ if (!existsSync(join(root, 'scripts', 'validate-content.ts'))) {
|
|
|
54
54
|
const makeBody = (sectionLines: number) => {
|
|
55
55
|
let body = '---\nname: test-skill\ndescription: Test skill for section-length gate, twenty chars\n---\n\n# Test\n\n## Skip when\n\n- Test\n\n## Long Section\n\n';
|
|
56
56
|
for (let i = 0; i < sectionLines; i++) body += `Line ${i + 1} of content for section length test.\n`;
|
|
57
|
-
body += '\n';
|
|
57
|
+
body += '\n## Red flags\n\n- Test flag\n';
|
|
58
58
|
return body;
|
|
59
59
|
};
|
|
60
60
|
|
|
@@ -262,5 +262,56 @@ if (!existsSync(brainstormSkill)) {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// --- F2: red-flags gate — strip the heading from one skill, prove red ---
|
|
266
|
+
console.log('\nF2 — red-flags gate');
|
|
267
|
+
const redSkill = join(root, 'content', 'skills', 'mugiwara-workflow', 'SKILL.md');
|
|
268
|
+
if (!existsSync(redSkill)) {
|
|
269
|
+
console.log(' ⚠ mugiwara-workflow/SKILL.md not found, skipping');
|
|
270
|
+
} else {
|
|
271
|
+
const original = readFileSync(redSkill, 'utf8');
|
|
272
|
+
try {
|
|
273
|
+
const broken = original.replace('## Red flags', '## Disabled');
|
|
274
|
+
writeFileSync(redSkill, broken);
|
|
275
|
+
assert('missing Red flags → exit 1', false, () => run('F2-redflags', 'bun scripts/validate-content.ts'));
|
|
276
|
+
} finally {
|
|
277
|
+
writeFileSync(redSkill, original);
|
|
278
|
+
assert('restored → exit 0', true, () => run('F2-redflags', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- W1: write-boundary gate — strip the section, prove red ---
|
|
283
|
+
console.log('\nW1 — write-boundary gate');
|
|
284
|
+
const wbSkill = join(root, 'content', 'skills', 'mugiwara-orchestration', 'SKILL.md');
|
|
285
|
+
if (!existsSync(wbSkill)) {
|
|
286
|
+
console.log(' ⚠ mugiwara-orchestration/SKILL.md not found, skipping');
|
|
287
|
+
} else {
|
|
288
|
+
const original = readFileSync(wbSkill, 'utf8');
|
|
289
|
+
try {
|
|
290
|
+
const broken = original.replace('## Write boundary', '## Boundary');
|
|
291
|
+
writeFileSync(wbSkill, broken);
|
|
292
|
+
assert('missing write boundary → exit 1', false, () => run('W1-wb', 'bun scripts/validate-content.ts'));
|
|
293
|
+
} finally {
|
|
294
|
+
writeFileSync(wbSkill, original);
|
|
295
|
+
assert('restored → exit 0', true, () => run('W1-wb', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- F14: agent-count gate — flip an internal agent to user-facing, prove red ---
|
|
300
|
+
console.log('\nF14 — agent-count gate');
|
|
301
|
+
const countAgent = join(root, 'content', 'agents', 'eval-runner.md');
|
|
302
|
+
if (!existsSync(countAgent)) {
|
|
303
|
+
console.log(' ⚠ eval-runner.md not found, skipping');
|
|
304
|
+
} else {
|
|
305
|
+
const original = readFileSync(countAgent, 'utf8');
|
|
306
|
+
try {
|
|
307
|
+
const broken = original.replace('internal: true', 'internal: false');
|
|
308
|
+
writeFileSync(countAgent, broken);
|
|
309
|
+
assert('count drift → exit 1', false, () => run('F14-count', 'bun scripts/validate-content.ts'));
|
|
310
|
+
} finally {
|
|
311
|
+
writeFileSync(countAgent, original);
|
|
312
|
+
assert('restored → exit 0', true, () => run('F14-count', 'bun scripts/validate-content.ts --check-manifest --check-docs'));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
265
316
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
266
317
|
process.exit(failed > 0 ? 1 : 0);
|
package/scripts/initiative.ts
CHANGED
|
@@ -46,6 +46,15 @@ function resolvePlan(planFile: string): string {
|
|
|
46
46
|
throw new Error(err);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// A plan is a markdown document. Garbage (no markdown structure) is not a
|
|
50
|
+
// "solo mission" — it is a malformed plan. Heading check keeps the
|
|
51
|
+
// no-sub-missions-but-valid-markdown case working as solo mission.
|
|
52
|
+
function assertPlanContent(content: string, planFile: string): void {
|
|
53
|
+
if (!/^#{1,6}\s/m.test(content)) {
|
|
54
|
+
throw new Error(`Not a valid plan file: "${planFile}" — expected markdown (no heading found)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
function parseSubMissions(content: string): SubMission[] {
|
|
50
59
|
const lines = content.split('\n');
|
|
51
60
|
let inTable = false;
|
|
@@ -93,6 +102,7 @@ function parseSubMissions(content: string): SubMission[] {
|
|
|
93
102
|
|
|
94
103
|
function cmdStatus(planFile: string): void {
|
|
95
104
|
const content = readFileSync(planFile, 'utf8');
|
|
105
|
+
assertPlanContent(content, planFile);
|
|
96
106
|
const subs = parseSubMissions(content);
|
|
97
107
|
|
|
98
108
|
if (subs.length === 0) {
|
|
@@ -122,6 +132,7 @@ function cmdStatus(planFile: string): void {
|
|
|
122
132
|
|
|
123
133
|
function cmdConflictCheck(planFile: string): void {
|
|
124
134
|
const content = readFileSync(planFile, 'utf8');
|
|
135
|
+
assertPlanContent(content, planFile);
|
|
125
136
|
const subs = parseSubMissions(content);
|
|
126
137
|
|
|
127
138
|
if (subs.length === 0) {
|
|
@@ -161,15 +172,13 @@ function cmdConflictCheck(planFile: string): void {
|
|
|
161
172
|
}
|
|
162
173
|
|
|
163
174
|
function cmdSetStatus(planFile: string, id: string, status: string): void {
|
|
164
|
-
if (!STATUS_MARKERS[id]) {
|
|
165
|
-
// `status` here is the status string from args — check it
|
|
166
|
-
}
|
|
167
175
|
if (!STATUS_MARKERS[status]) {
|
|
168
176
|
console.error(`Invalid status: "${status}". Use: pending, in-progress, done, blocked`);
|
|
169
177
|
process.exit(1);
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
const content = readFileSync(planFile, 'utf8');
|
|
181
|
+
assertPlanContent(content, planFile);
|
|
173
182
|
const lines = content.split('\n');
|
|
174
183
|
const newMarker = STATUS_MARKERS[status];
|
|
175
184
|
let inTable = false;
|
|
@@ -225,24 +234,29 @@ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
|
225
234
|
|
|
226
235
|
const cmd = args[0];
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
try {
|
|
238
|
+
if (cmd === 'status') {
|
|
239
|
+
if (args.length < 2) { console.error('Missing plan-file argument.'); process.exit(1); }
|
|
240
|
+
cmdStatus(resolvePlan(args[1]));
|
|
241
|
+
} else if (cmd === 'conflict-check') {
|
|
242
|
+
if (args.length < 2) { console.error('Missing plan-file argument.'); process.exit(1); }
|
|
243
|
+
cmdConflictCheck(resolvePlan(args[1]));
|
|
244
|
+
} else if (cmd === 'set-status') {
|
|
245
|
+
const idIdx = args.indexOf('--id');
|
|
246
|
+
const statusIdx = args.indexOf('--status');
|
|
247
|
+
if (idIdx === -1 || statusIdx === -1 || args.length < 6) {
|
|
248
|
+
console.error('set-status requires --id <id> --status <status>');
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
const id = args[idIdx + 1];
|
|
252
|
+
const statusVal = args[statusIdx + 1];
|
|
253
|
+
cmdSetStatus(resolvePlan(args[1]), id, statusVal);
|
|
254
|
+
} else {
|
|
255
|
+
console.error(`Unknown command: "${cmd}"`);
|
|
256
|
+
console.log(USAGE);
|
|
239
257
|
process.exit(1);
|
|
240
258
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
cmdSetStatus(resolvePlan(args[1]), id, statusVal);
|
|
244
|
-
} else {
|
|
245
|
-
console.error(`Unknown command: "${cmd}"`);
|
|
246
|
-
console.log(USAGE);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
247
261
|
process.exit(1);
|
|
248
262
|
}
|
package/scripts/lane.sh
CHANGED
|
@@ -51,7 +51,9 @@ else
|
|
|
51
51
|
REASON="$FILE_COUNT files"
|
|
52
52
|
fi
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# Sensitive-path escalation is unconditional — it wins over any count-based
|
|
55
|
+
# lane AND over the docs-only downgrade below (bug C9).
|
|
56
|
+
if [ "$HAS_SENSITIVE" -eq 1 ]; then
|
|
55
57
|
PREV="$LANE"
|
|
56
58
|
LANE="full"
|
|
57
59
|
REASON="sensitive paths ($SENSITIVE) — escalated from $PREV"
|
|
@@ -63,7 +65,7 @@ fi
|
|
|
63
65
|
# Product surface for mugiwara: content/, src/, scripts/, test/, hooks/,
|
|
64
66
|
# .opencode/, .claude/, evals/ — everything else is docs/config/asset.
|
|
65
67
|
PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
|
|
66
|
-
if [ "$LANE" = "full" ] && [ -n "$CHANGED" ]; then
|
|
68
|
+
if [ "$LANE" = "full" ] && [ "$HAS_SENSITIVE" -eq 0 ] && [ -n "$CHANGED" ]; then
|
|
67
69
|
CODE_COUNT=$(echo "$CHANGED" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
68
70
|
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
69
71
|
PREV="$LANE"
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
# scripts/mission-report.sh — generate aggregate mission report from state.json
|
|
3
3
|
# + per-mission wave artifacts. Usage: mission-report.sh <mission>
|
|
4
4
|
# Output: .mugiwara/reports/YYYY-MM-DD-<mission>.md — one-file summary of all waves.
|
|
5
|
+
# Enriched: mission header, token budget (WARN/STOP flag), tasks (state or plan
|
|
6
|
+
# doc fallback), gate/quality excerpt from 06-closure.md, evidence file list.
|
|
7
|
+
# Every section degrades to n/a when state.json or results/ artifacts are absent.
|
|
5
8
|
set -u
|
|
6
9
|
|
|
7
10
|
MISSION="${1:-}"
|
|
@@ -17,8 +20,6 @@ RESULTS_DIR="$MUGIWARA_DIR/results/$MISSION"
|
|
|
17
20
|
REVIEW_DIR="$MUGIWARA_DIR/review"
|
|
18
21
|
ISSUES_DIR="$MUGIWARA_DIR/issues"
|
|
19
22
|
|
|
20
|
-
[ -f "$STATE_FILE" ] || { echo "mission-report: $STATE_FILE not found" >&2; exit 1; }
|
|
21
|
-
|
|
22
23
|
export STATE_FILE REPORT_DIR RESULTS_DIR REVIEW_DIR ISSUES_DIR MISSION
|
|
23
24
|
node << 'NODE'
|
|
24
25
|
const fs = require('fs');
|
|
@@ -29,33 +30,49 @@ const reportDir = process.env.REPORT_DIR || ".mugiwara/reports";
|
|
|
29
30
|
const resultsDir = process.env.RESULTS_DIR || ".mugiwara/results/unknown";
|
|
30
31
|
const reviewDir = process.env.REVIEW_DIR || ".mugiwara/review";
|
|
31
32
|
const issuesDir = process.env.ISSUES_DIR || ".mugiwara/issues";
|
|
33
|
+
const mugiDir = path.dirname(stateFile);
|
|
32
34
|
const mission = process.env.MISSION || "unknown";
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
// state.json is optional — degrade gracefully, never hard-exit
|
|
39
|
+
let s = null;
|
|
40
|
+
if (fs.existsSync(stateFile)) {
|
|
41
|
+
try {
|
|
42
|
+
s = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error("mission-report: warning: " + stateFile + " unreadable — degraded report");
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
console.error("mission-report: warning: " + stateFile + " not found — degraded report");
|
|
37
48
|
}
|
|
38
49
|
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
const val = (obj, key, dflt) => {
|
|
51
|
+
if (!s || !obj) return dflt;
|
|
52
|
+
const v = obj[key];
|
|
53
|
+
return (v === undefined || v === null || v === "") ? dflt : v;
|
|
54
|
+
};
|
|
41
55
|
|
|
42
56
|
const now = new Date().toISOString().slice(0, 10);
|
|
43
57
|
const reportFile = path.join(reportDir, now + "-" + mission + ".md");
|
|
44
|
-
const lane = s
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
58
|
+
const lane = val(s, 'lane', 'n/a');
|
|
59
|
+
const laneReason = val(s, 'lane_reason', 'n/a');
|
|
60
|
+
const mode = val(s, 'mode', 'n/a');
|
|
61
|
+
const actor = val(s, 'actor', 'n/a');
|
|
62
|
+
const branch = val(s, 'branch', 'n/a');
|
|
63
|
+
const wave = val(s, 'wave', 'n/a');
|
|
64
|
+
const filesTouched = val(s, 'files_touched', 0);
|
|
65
|
+
const locDelta = val(s, 'loc_delta', 0);
|
|
66
|
+
const sensitive = s ? (s.sensitive_paths || []) : [];
|
|
67
|
+
const tasks = s ? (s.tasks || {}) : {};
|
|
68
|
+
const blockers = val(s, 'blockers_open', 0);
|
|
69
|
+
const healCycle = val(s, 'heal_cycle', 1);
|
|
70
|
+
const tokens = Number(val(s, 'tokens_est', 0)) || 0;
|
|
71
|
+
const tokensSource = val(s, 'tokens_source', 'n/a');
|
|
72
|
+
const budget = Number(val(s, 'budget', 0)) || 0;
|
|
73
|
+
const budgetStatus = val(s, 'budget_status', 'n/a');
|
|
74
|
+
const evidence = s ? (s.evidence || []) : [];
|
|
75
|
+
const updated = val(s, 'updated_at', now);
|
|
59
76
|
|
|
60
77
|
// --- wave artifact scan: results/<mission>/NN-*.md with verdict sniffing ---
|
|
61
78
|
const WAVE_LABEL = {
|
|
@@ -117,26 +134,133 @@ if (fs.existsSync(issuesDir)) {
|
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
// --- tasks: state.json counts, plan-doc fallback (savepoint-style counting) ---
|
|
138
|
+
function findPlan(mDir, m) {
|
|
139
|
+
const plansDir = path.join(mDir, 'plans');
|
|
140
|
+
if (!fs.existsSync(plansDir)) return null;
|
|
141
|
+
const direct = path.join(plansDir, m + '.md');
|
|
142
|
+
if (fs.existsSync(direct)) return direct;
|
|
143
|
+
const matches = fs.readdirSync(plansDir).filter(f => f.endsWith('-' + m + '.md'));
|
|
144
|
+
return matches.length ? path.join(plansDir, matches.sort()[0]) : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let tasksDone = tasks.done || 0;
|
|
148
|
+
let tasksTotal = tasks.total || 0;
|
|
149
|
+
let tasksSource = "state.json";
|
|
150
|
+
if (tasksTotal === 0) {
|
|
151
|
+
const plan = findPlan(mugiDir, mission);
|
|
152
|
+
if (plan) {
|
|
153
|
+
const lines = fs.readFileSync(plan, 'utf8').split(/\r?\n/);
|
|
154
|
+
const done = lines.filter(l => l.includes('[x]')).length;
|
|
155
|
+
const total = done + lines.filter(l => l.includes('[ ]')).length;
|
|
156
|
+
if (total > 0) {
|
|
157
|
+
tasksDone = done;
|
|
158
|
+
tasksTotal = total;
|
|
159
|
+
tasksSource = "plan doc";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (tasksDone === 0 && tasksTotal === 0) { tasksDone = 'n/a'; tasksTotal = 'n/a'; }
|
|
164
|
+
|
|
165
|
+
// --- token budget flag: WARN/STOP line when status is not ok ---
|
|
166
|
+
let budgetFlag = "";
|
|
167
|
+
if (budgetStatus === "warn" || budgetStatus === "stop") {
|
|
168
|
+
const warnAt = Math.floor(budget * 3 / 2);
|
|
169
|
+
const stopAt = budget * 3;
|
|
170
|
+
if (budgetStatus === "stop") {
|
|
171
|
+
budgetFlag = "🛑 STOP: tokens " + tokens.toLocaleString() + " ≥ 3× budget " + budget.toLocaleString() + " (stop at " + stopAt.toLocaleString() + ") — halt; escalate to Luffy";
|
|
172
|
+
} else {
|
|
173
|
+
budgetFlag = "⚠ WARN: tokens " + tokens.toLocaleString() + " ≥ 1.5× budget " + budget.toLocaleString() + " (warn at " + warnAt.toLocaleString() + ") — checkpoint before continuing";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- gate/quality excerpt: headings + body from 06-closure.md, as-is ---
|
|
178
|
+
function sectionExcerpt(file, heading) {
|
|
179
|
+
if (!fs.existsSync(file)) return null;
|
|
180
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
181
|
+
let collecting = false;
|
|
182
|
+
let content = 0;
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const l of lines) {
|
|
185
|
+
if (/^##\s/.test(l)) {
|
|
186
|
+
if (collecting) break;
|
|
187
|
+
if (l.trim() === heading) { collecting = true; out.push(l); continue; }
|
|
188
|
+
} else if (collecting) {
|
|
189
|
+
if (l.trim()) {
|
|
190
|
+
out.push(l);
|
|
191
|
+
content++;
|
|
192
|
+
if (content >= 20) break; // cap excerpt
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return collecting ? out : null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const closureFile = path.join(resultsDir, "06-closure.md");
|
|
200
|
+
const gateSec = sectionExcerpt(closureFile, "## Gate verdicts");
|
|
201
|
+
const testsSec = sectionExcerpt(closureFile, "## Tests");
|
|
202
|
+
|
|
203
|
+
// --- evidence file list: all .md artifacts in results/<mission>/ ---
|
|
204
|
+
let evidenceFiles = [];
|
|
205
|
+
if (fs.existsSync(resultsDir)) {
|
|
206
|
+
evidenceFiles = fs.readdirSync(resultsDir).filter(f => f.endsWith('.md')).sort();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- assemble report ---
|
|
120
210
|
let report = "# Mission: " + mission + " . " + now + "\n\n";
|
|
121
|
-
|
|
211
|
+
|
|
212
|
+
report += "## Mission header\n\n| Field | Value |\n|-------|-------|\n";
|
|
213
|
+
report += "| Mission | " + mission + " |\n";
|
|
214
|
+
report += "| Branch | " + branch + " |\n";
|
|
215
|
+
report += "| Lane | " + lane + " |\n";
|
|
216
|
+
report += "| Lane reason | " + laneReason + " |\n";
|
|
217
|
+
report += "| Mode | " + mode + " |\n";
|
|
218
|
+
report += "| Wave | " + wave + " |\n";
|
|
219
|
+
report += "| Actor | " + actor + " |\n\n";
|
|
220
|
+
|
|
221
|
+
report += "## Token budget\n\n| Field | Value |\n|-------|-------|\n";
|
|
222
|
+
report += "| Tokens (est) | " + tokens.toLocaleString() + " |\n";
|
|
223
|
+
report += "| Tokens source | " + tokensSource + " |\n";
|
|
224
|
+
report += "| Budget | " + budget.toLocaleString() + " |\n";
|
|
225
|
+
report += "| Budget status | " + budgetStatus + " |\n";
|
|
226
|
+
if (budgetFlag) report += "\n" + budgetFlag;
|
|
227
|
+
report += "\n\n";
|
|
228
|
+
|
|
229
|
+
report += "## Tasks\n\n| Field | Value |\n|-------|-------|\n";
|
|
230
|
+
report += "| Done | " + tasksDone + " |\n";
|
|
231
|
+
report += "| Total | " + tasksTotal + " |\n";
|
|
232
|
+
report += "| Source | " + tasksSource + " |\n\n";
|
|
122
233
|
|
|
123
234
|
report += "## What changed\n\n";
|
|
124
235
|
report += filesTouched + " files, +" + locDelta + " LOC";
|
|
125
236
|
if (sensitive.length) report += "\nSensitive paths: " + sensitive.join(", ");
|
|
237
|
+
report += "\n\n";
|
|
126
238
|
|
|
127
|
-
report += "
|
|
239
|
+
report += "## Waves\n\n| Wave | Artifact | Verdict |\n|------|----------|---------|";
|
|
128
240
|
if (waveRows.length) {
|
|
129
241
|
for (const r of waveRows) report += "\n| " + r.label + " | `" + r.file + "` | " + r.verdict + " |";
|
|
130
242
|
} else {
|
|
131
243
|
report += "\n| (no wave artifacts found in `" + resultsDir + "`) | | |";
|
|
132
244
|
}
|
|
245
|
+
report += "\n\n";
|
|
246
|
+
|
|
247
|
+
report += "## Gate & quality\n\n";
|
|
248
|
+
if (gateSec) report += gateSec.join("\n") + "\n";
|
|
249
|
+
if (testsSec) report += (gateSec ? "\n" : "") + testsSec.join("\n") + "\n";
|
|
250
|
+
if (!gateSec && !testsSec) report += "n/a\n";
|
|
251
|
+
report += "\n";
|
|
133
252
|
|
|
134
|
-
report += "
|
|
253
|
+
report += "## Evidence files\n\n";
|
|
254
|
+
report += evidenceFiles.length ? evidenceFiles.map(f => "- " + f).join("\n") : "n/a";
|
|
255
|
+
report += "\n\n";
|
|
256
|
+
|
|
257
|
+
report += "## Review & blockers\n\n";
|
|
135
258
|
report += "Review + security files: " + (reviewFiles.length ? reviewFiles.join(", ") : "none") + "\n";
|
|
136
259
|
report += "Findings: " + reviewFindings + "\n";
|
|
137
260
|
report += "Blocker ledger rows: " + issueRows.length + (issueRows.length ? "\n" + issueRows.map(r => "- " + r).join("\n") : "");
|
|
261
|
+
report += "\n\n";
|
|
138
262
|
|
|
139
|
-
report += "
|
|
263
|
+
report += "## State\n\n| Field | Value |\n|-------|-------|\n";
|
|
140
264
|
report += "| Wave | " + wave + " |\n";
|
|
141
265
|
report += "| Tasks | " + (tasks.done || 0) + "/" + (tasks.total || 0) + " done |\n";
|
|
142
266
|
report += "| Blockers open | " + blockers + " |\n";
|
|
@@ -144,9 +268,8 @@ report += "| Heal cycles | " + healCycle + " |\n";
|
|
|
144
268
|
report += "| Tokens used | " + tokens.toLocaleString() + " / " + budget.toLocaleString() + " |\n\n";
|
|
145
269
|
|
|
146
270
|
report += "## Evidence\n\n";
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
report += "\n## Updated\n\n" + updated + "\n";
|
|
271
|
+
report += evidence.length ? evidence.map(e => "- " + e).join("\n") : "n/a";
|
|
272
|
+
report += "\n\n## Updated\n\n" + updated + "\n";
|
|
150
273
|
|
|
151
274
|
fs.writeFileSync(reportFile, report);
|
|
152
275
|
console.log("\u2713 mission report: " + reportFile);
|
package/scripts/savepoint.sh
CHANGED
|
@@ -31,6 +31,11 @@ fi
|
|
|
31
31
|
case "$MISSION" in
|
|
32
32
|
""|*[!a-zA-Z0-9._-]*) die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-])" ;;
|
|
33
33
|
esac
|
|
34
|
+
# dot-only names (".", "..", "...") pass the char allowlist but resolve upward
|
|
35
|
+
# through join(...,"..") — reject them before any path is built from MISSION.
|
|
36
|
+
if [[ "$MISSION" =~ ^\.+$ ]]; then
|
|
37
|
+
die "invalid mission name \"$MISSION\" (allowlist: [a-zA-Z0-9._-], not a dot-path)"
|
|
38
|
+
fi
|
|
34
39
|
|
|
35
40
|
# per-branch state file when --branch used
|
|
36
41
|
BRANCH_SLUG=$(echo "$BRANCH" | tr '/' '-')
|
|
@@ -59,20 +64,35 @@ if [ "$BASE_SHA" != "unknown" ]; then
|
|
|
59
64
|
fi
|
|
60
65
|
[ -z "$LOC_DELTA" ] && LOC_DELTA=0
|
|
61
66
|
|
|
62
|
-
SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env|
|
|
67
|
+
SENSITIVE_PATTERNS="auth/|payment/|billing/|crypto/|secrets/|\.env$|config/.*key|migration/|\.sql$|schema\.|\.prisma$|\.terraform|\.tf$"
|
|
63
68
|
SENSITIVE_PATHS=$(echo "$CHANGED_FILES" | grep -E "$SENSITIVE_PATTERNS" 2>/dev/null | tr '\n' ',' | sed 's/,$//' || true)
|
|
64
69
|
|
|
65
70
|
LANE="direct"
|
|
66
71
|
LANE_REASON=""
|
|
67
|
-
|
|
72
|
+
# Sensitive-path escalation is unconditional — it wins over any count-based
|
|
73
|
+
# lane AND over the docs-only downgrade below (bug C9).
|
|
74
|
+
if [ -n "$SENSITIVE_PATHS" ]; then
|
|
75
|
+
LANE="full"
|
|
76
|
+
LANE_REASON="sensitive paths: $SENSITIVE_PATHS"
|
|
77
|
+
elif [ "$FILES_TOUCHED" -ge 9 ] 2>/dev/null; then
|
|
68
78
|
LANE="full"
|
|
69
|
-
LANE_REASON="$
|
|
79
|
+
LANE_REASON="$FILES_TOUCHED files"
|
|
70
80
|
elif [ "$FILES_TOUCHED" -ge 3 ] 2>/dev/null; then
|
|
71
81
|
LANE="standard"
|
|
72
82
|
LANE_REASON="$FILES_TOUCHED files"
|
|
73
83
|
elif [ "$FILES_TOUCHED" -ge 2 ] 2>/dev/null; then
|
|
74
84
|
LANE="lean"
|
|
75
85
|
LANE_REASON="$FILES_TOUCHED files"
|
|
86
|
+
elif [ "$FILES_TOUCHED" -eq 1 ] 2>/dev/null; then
|
|
87
|
+
# 1-file rule mirrors lane.sh: >=20 added LOC -> lean, else direct
|
|
88
|
+
ADDED=$(git diff --numstat "$BASE_SHA"..HEAD 2>/dev/null | awk '{s+=$1} END {print s+0}')
|
|
89
|
+
if [ "${ADDED:-0}" -ge 20 ] 2>/dev/null; then
|
|
90
|
+
LANE="lean"
|
|
91
|
+
LANE_REASON="1 file, $ADDED LOC"
|
|
92
|
+
else
|
|
93
|
+
LANE="direct"
|
|
94
|
+
LANE_REASON="1 file, <20 LOC"
|
|
95
|
+
fi
|
|
76
96
|
else
|
|
77
97
|
LANE="direct"
|
|
78
98
|
LANE_REASON="$FILES_TOUCHED file(s) under 20 LOC"
|
|
@@ -82,7 +102,7 @@ fi
|
|
|
82
102
|
# surface never escalate to full from file count alone; sensitive-path
|
|
83
103
|
# escalation above still wins.
|
|
84
104
|
PRODUCT_PAT="^content/|^src/|^scripts/|^test/|^hooks/|^\.opencode/|^\.claude/|^evals/"
|
|
85
|
-
if [ "$LANE" = "full" ] && [ -n "$CHANGED_FILES" ]; then
|
|
105
|
+
if [ "$LANE" = "full" ] && [ -z "$SENSITIVE_PATHS" ] && [ -n "$CHANGED_FILES" ]; then
|
|
86
106
|
CODE_COUNT=$(echo "$CHANGED_FILES" | grep -E "$PRODUCT_PAT" 2>/dev/null | grep -c . || true)
|
|
87
107
|
if [ -z "$CODE_COUNT" ] || [ "$CODE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
88
108
|
PREV="$LANE"
|
|
@@ -91,20 +111,26 @@ if [ "$LANE" = "full" ] && [ -n "$CHANGED_FILES" ]; then
|
|
|
91
111
|
fi
|
|
92
112
|
fi
|
|
93
113
|
|
|
94
|
-
# task counts from plan doc
|
|
95
|
-
|
|
114
|
+
# task counts from plan doc — plan is written date-prefixed (plans/YYYY-MM-DD-<mission>.md)
|
|
115
|
+
# or bare (plans/<mission>.md); glob both, first match wins.
|
|
116
|
+
PLAN_FILE=$(ls "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md 2>/dev/null | head -1 || true)
|
|
96
117
|
TASKS_DONE=0
|
|
97
118
|
TASKS_TOTAL=0
|
|
98
119
|
if [ -n "$PLAN_FILE" ] && [ -f "$PLAN_FILE" ]; then
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
# total counts ALL task lines (checked + unchecked); done counts checked only.
|
|
121
|
+
# A fully-completed plan must read total=N done=N, never total=0 (the old
|
|
122
|
+
# unchecked-only grep degenerated a done plan to tasks.total=0).
|
|
123
|
+
TASKS_TOTAL=$(grep -cE '^\s*-\s*\[[ xX]\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
124
|
+
TASKS_DONE=$(grep -c '\[x\]' "$PLAN_FILE" 2>/dev/null || true)
|
|
101
125
|
fi
|
|
102
126
|
|
|
103
127
|
# blocker count
|
|
104
128
|
BLOCKERS_FILE=$(ls "$MUGIWARA_DIR/issues/${MISSION}-blockers.md" 2>/dev/null || true)
|
|
105
129
|
BLOCKERS_OPEN=0
|
|
106
130
|
if [ -n "$BLOCKERS_FILE" ] && [ -f "$BLOCKERS_FILE" ]; then
|
|
107
|
-
|
|
131
|
+
# data rows start with a wave number; header "| wave |" and separator
|
|
132
|
+
# "|---|" are excluded by the ^\| ?[0-9]+ \| pattern
|
|
133
|
+
BLOCKERS_OPEN=$(grep -cE '^\| ?[0-9]+ ?\|' "$BLOCKERS_FILE" 2>/dev/null || true)
|
|
108
134
|
fi
|
|
109
135
|
|
|
110
136
|
# heal cycle — count WAVE-8 banner occurrences in the trace, not the word
|
|
@@ -113,7 +139,7 @@ fi
|
|
|
113
139
|
HEAL_CYCLE=1
|
|
114
140
|
TRACE_FILE=$(ls "$MUGIWARA_DIR/results/${MISSION}/"*trace*.md 2>/dev/null | head -1 || true)
|
|
115
141
|
if [ -n "$TRACE_FILE" ] && [ -f "$TRACE_FILE" ]; then
|
|
116
|
-
HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null ||
|
|
142
|
+
HEAL_COUNT=$(grep -ci '^.*Wave 8.*\|wave 8' "$TRACE_FILE" 2>/dev/null || true)
|
|
117
143
|
HEAL_CYCLE=$((HEAL_COUNT + 1))
|
|
118
144
|
fi
|
|
119
145
|
|
|
@@ -132,8 +158,25 @@ if [ -f package.json ]; then
|
|
|
132
158
|
[ -f "$PKG_JSON" ] && SKILL_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version.split('.')[0])}catch(e){console.log('1')}" "$PKG_JSON" 2>/dev/null || echo "1")
|
|
133
159
|
fi
|
|
134
160
|
|
|
135
|
-
# tokens
|
|
136
|
-
|
|
161
|
+
# tokens proxy (F7): deterministic estimate when the harness does not report
|
|
162
|
+
# real usage. Monotonic beats precise — LANE_BASE stands in for the skills
|
|
163
|
+
# loaded this lane; loc_delta and written-artifact words scale with growth.
|
|
164
|
+
# MUGIWARA_TOKENS overrides as the reported value.
|
|
165
|
+
LANE_BASE=0
|
|
166
|
+
case "$LANE" in
|
|
167
|
+
lean) LANE_BASE=1500 ;;
|
|
168
|
+
standard) LANE_BASE=4000 ;;
|
|
169
|
+
full) LANE_BASE=9000 ;;
|
|
170
|
+
spike) LANE_BASE=1000 ;;
|
|
171
|
+
esac
|
|
172
|
+
DOC_WORDS=$(cat "$MUGIWARA_DIR"/results/${MISSION}/*.md "$MUGIWARA_DIR"/plans/${MISSION}.md "$MUGIWARA_DIR"/plans/*-${MISSION}.md "$MUGIWARA_DIR"/spec/${MISSION}.md "$MUGIWARA_DIR"/spec/*-${MISSION}.md "$MUGIWARA_DIR"/logs/${MISSION}.md "$MUGIWARA_DIR"/logs/*-${MISSION}.md 2>/dev/null | wc -w | tr -d ' ')
|
|
173
|
+
LOC_TOKENS=$(( LOC_DELTA > 0 ? LOC_DELTA * 12 : 0 ))
|
|
174
|
+
TOKENS_SOURCE="computed"
|
|
175
|
+
TOKENS_EST=$(( LANE_BASE + DOC_WORDS * 135 / 100 + LOC_TOKENS ))
|
|
176
|
+
if [ -n "${MUGIWARA_TOKENS:-}" ]; then
|
|
177
|
+
TOKENS_EST="${MUGIWARA_TOKENS}"
|
|
178
|
+
TOKENS_SOURCE="reported"
|
|
179
|
+
fi
|
|
137
180
|
|
|
138
181
|
# budget per lane
|
|
139
182
|
BUDGET=0
|
|
@@ -191,6 +234,7 @@ const data = {
|
|
|
191
234
|
blockers_open: parseInt(process.argv[15], 10),
|
|
192
235
|
heal_cycle: parseInt(process.argv[16], 10),
|
|
193
236
|
tokens_est: parseInt(process.argv[17], 10) || 0,
|
|
237
|
+
tokens_source: process.argv[26] || 'computed',
|
|
194
238
|
budget: parseInt(process.argv[18], 10) || 0,
|
|
195
239
|
budget_status: process.argv[19],
|
|
196
240
|
skill_version: process.argv[20],
|
|
@@ -205,7 +249,7 @@ require('fs').writeFileSync(process.argv[23], JSON.stringify(data, null, 2) + '\
|
|
|
205
249
|
"$BLOCKERS_OPEN" "$HEAL_CYCLE" "$TOKENS_EST" "$BUDGET" \
|
|
206
250
|
"$STATUS" "$SKILL_VERSION" "$EVIDENCE" \
|
|
207
251
|
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
208
|
-
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE"
|
|
252
|
+
"$STATE_FILE" "$LANE_PREV" "$LANE_ROSE" "$TOKENS_SOURCE"
|
|
209
253
|
|
|
210
254
|
if [ "$LANE_ROSE" = true ]; then
|
|
211
255
|
echo "⚠ LANE ROSE: $LANE_PREV → $LANE ($LANE_REASON) — escalate per check-in protocol"
|
|
@@ -27,6 +27,7 @@ function checkFile(file: string, wantName: string, kind: 'skill' | 'agent'): Rec
|
|
|
27
27
|
if (bullets.length === 0) errors.push(`skill ${file}: "## Skip when" needs ≥1 bullet`);
|
|
28
28
|
if (bullets.length > 4) errors.push(`skill ${file}: "## Skip when" block exceeds 4 bullets`);
|
|
29
29
|
}
|
|
30
|
+
if (kind === 'skill' && !body.includes('## Red flags')) errors.push(`skill ${file}: missing required "## Red flags" block`);
|
|
30
31
|
if (kind === 'skill') {
|
|
31
32
|
const lines = body.split(/\r?\n/);
|
|
32
33
|
const headingRe = /^## /;
|
|
@@ -145,6 +146,25 @@ for (const f of agentFiles) {
|
|
|
145
146
|
if (scope === 'source' && !SOURCE_SCOPED.has(name)) errors.push(`agent ${f}: only zoro-execution and brook-healing may declare write-scope: source`);
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
// --- write-boundary gate (W1): the hub skill must carry the refusal rule ---
|
|
150
|
+
const hubFile = join(root, 'skills', 'mugiwara-orchestration', 'SKILL.md');
|
|
151
|
+
if (existsSync(hubFile) && !readFileSync(hubFile, 'utf8').includes('## Write boundary')) {
|
|
152
|
+
errors.push('skill mugiwara-orchestration: missing "## Write boundary" section (non-executors refuse source writes)');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- agent-count gate (F14): user-facing/internal split derivable from content/agents/ ---
|
|
156
|
+
const internalAgents = agentFiles.filter(f => {
|
|
157
|
+
const parsed = parseFrontmatter(readFileSync(join(agentDir, f), 'utf8'));
|
|
158
|
+
return parsed.data.internal === 'true';
|
|
159
|
+
});
|
|
160
|
+
const canonicalCount = `${agentFiles.length - internalAgents.length} agents (+${internalAgents.length} internal)`;
|
|
161
|
+
for (const doc of ['README.md', 'docs/index.md', 'docs/concepts/agents.md']) {
|
|
162
|
+
const p = join(import.meta.dirname, '..', doc);
|
|
163
|
+
if (existsSync(p) && !readFileSync(p, 'utf8').includes(canonicalCount)) {
|
|
164
|
+
errors.push(`${doc}: missing canonical agent count "${canonicalCount}"`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
// --- hub-rule gate (F3): every non-Luffy agent carries both hub sections ---
|
|
149
169
|
for (const f of agentFiles) {
|
|
150
170
|
const name = f.replace(/\.md$/, '');
|