@ionivetech/mugiwara 0.6.5 → 0.7.0
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 +3 -9
- package/.codex-plugin/plugin.json +2 -8
- package/.cursor-plugin/plugin.json +2 -8
- package/.kimi-plugin/plugin.json +2 -8
- package/.opencode/commands/mugiwara-continue.md +44 -31
- package/.opencode/commands/mugiwara-review.md +1 -1
- package/.opencode/commands/mugiwara-security.md +1 -1
- package/.opencode/commands/mugiwara.md +2 -2
- package/.opencode/plugins/mugiwara.mjs +3 -19
- package/AGENTS.md +6 -6
- package/GEMINI.md +3 -3
- package/README.md +82 -74
- package/content/agents/brook-healing.md +12 -11
- package/content/agents/chopper-checkpoint.md +14 -13
- package/content/agents/eval-runner.md +6 -9
- package/content/agents/franky-gates.md +7 -10
- package/content/agents/jinbe-security.md +10 -14
- package/content/agents/luffy-orchestrator.md +20 -22
- package/content/agents/memory-keeper.md +9 -12
- package/content/agents/nami-planner.md +7 -10
- package/content/agents/resume-coordinator.md +12 -15
- package/content/agents/robin-reviewer.md +13 -12
- package/content/agents/sanji-quality.md +9 -12
- package/content/agents/skeptic-verifier.md +7 -10
- package/content/agents/usopp-brainstorm.md +6 -9
- package/content/agents/zoro-execution.md +9 -12
- package/content/skills/mugiwara-backend/SKILL.md +4 -0
- package/content/skills/mugiwara-backend/references/database.md +61 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +17 -14
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +5 -5
- package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +24 -24
- package/content/skills/mugiwara-execution/references/dispatch.md +3 -3
- package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +5 -6
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +14 -10
- package/content/skills/mugiwara-git/SKILL.md +7 -2
- package/content/skills/mugiwara-healing/SKILL.md +7 -5
- package/content/skills/mugiwara-healing/references/workers.md +2 -2
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +28 -26
- package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
- package/content/skills/mugiwara-orchestration/references/closure.md +18 -13
- package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +32 -22
- package/content/skills/mugiwara-planning/SKILL.md +5 -18
- package/content/skills/mugiwara-planning/references/plan-template.md +3 -3
- package/content/skills/mugiwara-quality/SKILL.md +10 -7
- package/content/skills/mugiwara-resume/SKILL.md +20 -22
- package/content/skills/mugiwara-review/SKILL.md +7 -2
- package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +6 -2
- package/content/skills/mugiwara-ship/SKILL.md +2 -2
- package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
- package/content/skills/mugiwara-testcases/SKILL.md +5 -3
- package/content/skills/mugiwara-workflow/SKILL.md +32 -35
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +41 -40
- package/dist/mugiwara.js +1370 -153
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +114 -0
- package/hooks/auto-savepoint.ts +102 -0
- package/hooks/engagement-marker.js +59 -0
- package/hooks/engagement-marker.ts +97 -0
- package/hooks/hooks.json +41 -2
- package/hooks/mugiwara-mode-tracker.js +79 -0
- package/hooks/pipeline-guard.js +186 -0
- package/hooks/pipeline-guard.ts +270 -0
- package/hooks/session-start.js +109 -0
- package/hooks/session-start.ts +62 -45
- package/package.json +7 -3
- package/plugin.json +1 -1
- package/references/agent-protocol.md +15 -0
- package/references/complexity.md +77 -0
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +4 -7
- package/references/prose-style.md +54 -0
- package/references/skill-versioning.md +1 -1
- package/references/wave-banners.md +16 -16
- package/scripts/build-hooks.ts +56 -0
- package/scripts/check-doc-links.ts +40 -0
- package/scripts/conformance.ts +23 -16
- package/scripts/coverage-gate.ts +193 -0
- package/scripts/gate-selftest.ts +118 -7
- package/scripts/install.ps1 +5 -4
- package/scripts/install.sh +4 -4
- package/scripts/lane.sh +56 -6
- package/scripts/lib/patterns.sh +89 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +1 -1
- package/scripts/savepoint.sh +249 -67
- package/scripts/validate-content.ts +22 -9
- package/scripts/verify-install.ts +156 -44
- package/src/args.ts +2 -2
- package/src/budget.ts +56 -0
- package/src/cli.ts +272 -4
- package/src/continue.ts +262 -0
- package/src/installer.ts +59 -15
- package/src/integrity.ts +158 -0
- package/src/mission.ts +228 -95
- package/src/policy.ts +156 -0
- package/src/provenance.ts +116 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +82 -0
- package/src/sign.ts +61 -0
- package/src/targets/claude.ts +140 -2
- package/src/targets/copilot.ts +20 -1
- package/src/targets/generic.ts +9 -4
- package/src/targets/opencode.ts +0 -1
- package/.opencode/commands/mugiwara-execute.md +0 -13
- package/.opencode/commands/mugiwara-heal.md +0 -13
- package/.opencode/commands/mugiwara-onboard.md +0 -15
- package/.opencode/commands/mugiwara-plan.md +0 -17
- package/.opencode/commands/mugiwara-ship.md +0 -13
- package/.opencode/commands/using-mugiwara.md +0 -20
- package/content/agents/onboarding-guide.md +0 -124
- package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
- package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
- package/content/skills/mugiwara-context-budget/SKILL.md +0 -64
- package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
- package/content/skills/mugiwara-pr/SKILL.md +0 -69
- package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
- package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
- package/content/skills/mugiwara-sunset/SKILL.md +0 -82
- package/content/skills/using-mugiwara/SKILL.md +0 -51
- package/references/token-budget.md +0 -34
- package/scripts/evidence.sh +0 -81
- package/scripts/initiative.ts +0 -296
- package/scripts/mission-report.sh +0 -292
- package/scripts/onboard.ts +0 -266
- package/scripts/probe.ts +0 -40
package/dist/mugiwara.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
6
|
+
import { homedir as homedir4 } from "node:os";
|
|
7
|
+
import { join as join15, resolve as resolve2 } from "node:path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
8
9
|
|
|
9
10
|
// src/args.ts
|
|
10
|
-
var VALUE_FLAGS = { "--project": "project", "--target": "target" };
|
|
11
|
+
var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before" };
|
|
11
12
|
var BOOL_FLAGS = {
|
|
12
13
|
"--global": "global",
|
|
13
14
|
"--yes": "yes",
|
|
@@ -16,6 +17,8 @@ var BOOL_FLAGS = {
|
|
|
16
17
|
"--dry-run": "dryRun",
|
|
17
18
|
"--keep-logs": "keepLogs",
|
|
18
19
|
"--check": "check",
|
|
20
|
+
"--all": "all",
|
|
21
|
+
"--verify": "verify",
|
|
19
22
|
"--help": "help",
|
|
20
23
|
"-h": "help",
|
|
21
24
|
"--version": "version",
|
|
@@ -81,7 +84,7 @@ async function confirm(rl, question) {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
// src/targets/claude.ts
|
|
84
|
-
import { existsSync, mkdirSync, copyFileSync, chmodSync, readdirSync } from "node:fs";
|
|
87
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, chmodSync, readdirSync } from "node:fs";
|
|
85
88
|
import { dirname, join } from "node:path";
|
|
86
89
|
import { fileURLToPath } from "node:url";
|
|
87
90
|
|
|
@@ -119,6 +122,107 @@ function toolsFromScope(scope) {
|
|
|
119
122
|
return "Read, Grep, Glob, Write, Bash, WebFetch, WebSearch";
|
|
120
123
|
return;
|
|
121
124
|
}
|
|
125
|
+
var SCRIPTS_SRC = join(here, "..", "..", "scripts");
|
|
126
|
+
function copyScripts(dstDir) {
|
|
127
|
+
if (!existsSync(SCRIPTS_SRC))
|
|
128
|
+
return [];
|
|
129
|
+
const written = [];
|
|
130
|
+
const copyDir = (src, dst) => {
|
|
131
|
+
if (!existsSync(src))
|
|
132
|
+
return;
|
|
133
|
+
for (const f of readdirSync(src)) {
|
|
134
|
+
if (!f.endsWith(".sh") || f === "install.sh")
|
|
135
|
+
continue;
|
|
136
|
+
const to = join(dst, f);
|
|
137
|
+
mkdirSync(dst, { recursive: true });
|
|
138
|
+
copyFileSync(join(src, f), to);
|
|
139
|
+
chmodSync(to, 493);
|
|
140
|
+
written.push(to);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
copyDir(SCRIPTS_SRC, dstDir);
|
|
144
|
+
copyDir(join(SCRIPTS_SRC, "lib"), join(dstDir, "lib"));
|
|
145
|
+
return written;
|
|
146
|
+
}
|
|
147
|
+
function wireSettings(root, hooksDir) {
|
|
148
|
+
const file = join(root, "settings.json");
|
|
149
|
+
const events = {
|
|
150
|
+
SessionStart: { file: "session-start.js", timeout: 10 },
|
|
151
|
+
UserPromptSubmit: { file: "mugiwara-mode-tracker.js", timeout: 5 },
|
|
152
|
+
Stop: { file: "auto-savepoint.js", timeout: 20 },
|
|
153
|
+
SubagentStop: { file: "pipeline-guard.js", timeout: 15 },
|
|
154
|
+
PostToolUse: { file: "engagement-marker.js", timeout: 5, matcher: "Task|Skill" }
|
|
155
|
+
};
|
|
156
|
+
let settings = {};
|
|
157
|
+
if (existsSync(file)) {
|
|
158
|
+
try {
|
|
159
|
+
settings = JSON.parse(readFileSync(file, "utf8"));
|
|
160
|
+
} catch {
|
|
161
|
+
return { written: [], notes: [`${file} is not valid JSON — add the mugiwara hooks manually`] };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const hooks = settings.hooks ?? {};
|
|
165
|
+
let changed = false;
|
|
166
|
+
for (const [event, spec] of Object.entries(events)) {
|
|
167
|
+
const command = join(hooksDir, spec.file);
|
|
168
|
+
if (!existsSync(command))
|
|
169
|
+
continue;
|
|
170
|
+
const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
171
|
+
const already = groups.some((g) => g.hooks?.some((h) => h.command?.includes(spec.file)));
|
|
172
|
+
if (already)
|
|
173
|
+
continue;
|
|
174
|
+
const group = { hooks: [{ type: "command", command: JSON.stringify(command), timeout: spec.timeout }] };
|
|
175
|
+
if (spec.matcher)
|
|
176
|
+
group.matcher = spec.matcher;
|
|
177
|
+
groups.push(group);
|
|
178
|
+
hooks[event] = groups;
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
if (!changed)
|
|
182
|
+
return { written: [], notes: [] };
|
|
183
|
+
settings.hooks = hooks;
|
|
184
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + `
|
|
185
|
+
`);
|
|
186
|
+
return { written: [], notes: [`hooks registered in ${file}`] };
|
|
187
|
+
}
|
|
188
|
+
function unwireSettings(root) {
|
|
189
|
+
const file = join(root, "settings.json");
|
|
190
|
+
if (!existsSync(file))
|
|
191
|
+
return { changed: [], notes: [] };
|
|
192
|
+
let settings;
|
|
193
|
+
try {
|
|
194
|
+
settings = JSON.parse(readFileSync(file, "utf8"));
|
|
195
|
+
} catch {
|
|
196
|
+
return { changed: [], notes: [`${file} is not valid JSON — left untouched`] };
|
|
197
|
+
}
|
|
198
|
+
const hooks = settings.hooks;
|
|
199
|
+
if (!hooks)
|
|
200
|
+
return { changed: [], notes: [] };
|
|
201
|
+
const ours = new Set(["session-start.js", "mugiwara-mode-tracker.js", "auto-savepoint.js", "pipeline-guard.js", "engagement-marker.js"]);
|
|
202
|
+
const isOurs = (h) => [...ours].some((f) => h.command?.includes(join("hooks", f)) || h.command?.includes(`hooks/${f}`));
|
|
203
|
+
let changed = false;
|
|
204
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
205
|
+
if (!Array.isArray(groups))
|
|
206
|
+
continue;
|
|
207
|
+
const kept = groups.map((g) => ({ ...g, hooks: (g.hooks ?? []).filter((h) => !isOurs(h)) })).filter((g) => g.hooks.length > 0);
|
|
208
|
+
if (kept.length !== groups.length || kept.some((g, i) => g.hooks.length !== groups[i]?.hooks?.length)) {
|
|
209
|
+
changed = true;
|
|
210
|
+
}
|
|
211
|
+
if (kept.length)
|
|
212
|
+
hooks[event] = kept;
|
|
213
|
+
else {
|
|
214
|
+
delete hooks[event];
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!changed)
|
|
219
|
+
return { changed: [], notes: [] };
|
|
220
|
+
if (!Object.keys(hooks).length)
|
|
221
|
+
delete settings.hooks;
|
|
222
|
+
writeFileSync(file, JSON.stringify(settings, null, 2) + `
|
|
223
|
+
`);
|
|
224
|
+
return { changed: [file], notes: [] };
|
|
225
|
+
}
|
|
122
226
|
var target = {
|
|
123
227
|
id: "claude",
|
|
124
228
|
label: "Claude Code",
|
|
@@ -157,7 +261,7 @@ var target = {
|
|
|
157
261
|
return { written: [], notes: [] };
|
|
158
262
|
if (existsSync(HOOKS_SRC)) {
|
|
159
263
|
for (const f of readdirSync(HOOKS_SRC)) {
|
|
160
|
-
if (
|
|
264
|
+
if (!/\.(ts|js|json)$/.test(f))
|
|
161
265
|
continue;
|
|
162
266
|
const dst = join(root, "hooks", f);
|
|
163
267
|
if (!existsSync(dst)) {
|
|
@@ -167,6 +271,10 @@ var target = {
|
|
|
167
271
|
written.push(dst);
|
|
168
272
|
}
|
|
169
273
|
}
|
|
274
|
+
written.push(...copyScripts(join(root, "scripts")));
|
|
275
|
+
const wired = wireSettings(root, join(root, "hooks"));
|
|
276
|
+
written.push(...wired.written);
|
|
277
|
+
notes.push(...wired.notes);
|
|
170
278
|
}
|
|
171
279
|
if (existsSync(COMMANDS_SRC)) {
|
|
172
280
|
const dstDir = join(root, "commands");
|
|
@@ -184,6 +292,12 @@ var target = {
|
|
|
184
292
|
}
|
|
185
293
|
}
|
|
186
294
|
return { written, notes };
|
|
295
|
+
},
|
|
296
|
+
postUninstall({ scope, projectDir, home, dryRun }) {
|
|
297
|
+
if (dryRun)
|
|
298
|
+
return { changed: [], notes: [] };
|
|
299
|
+
const root = scope === "global" ? join(home, ".claude") : join(projectDir, ".claude");
|
|
300
|
+
return unwireSettings(root);
|
|
187
301
|
}
|
|
188
302
|
};
|
|
189
303
|
|
|
@@ -208,8 +322,7 @@ var CREW = {
|
|
|
208
322
|
"skeptic-verifier": { color: "#64748b", temperature: 0.1, steps: 12 },
|
|
209
323
|
"eval-runner": { color: "#14b8a6", temperature: 0.2, steps: 15 },
|
|
210
324
|
"resume-coordinator": { color: "#d97706", temperature: 0.2, steps: 10 },
|
|
211
|
-
"memory-keeper": { color: "#d946ef", temperature: 0.2, steps: 8 }
|
|
212
|
-
"onboarding-guide": { color: "#0ea5e9", temperature: 0.3, steps: 15 }
|
|
325
|
+
"memory-keeper": { color: "#d946ef", temperature: 0.2, steps: 8 }
|
|
213
326
|
};
|
|
214
327
|
function readBannerColors() {
|
|
215
328
|
try {
|
|
@@ -319,7 +432,17 @@ var target3 = {
|
|
|
319
432
|
transformSkill(data, body) {
|
|
320
433
|
return {
|
|
321
434
|
relPath: `${data.name}.instructions.md`,
|
|
322
|
-
text: stringifyFrontmatter({ description: data.description, applyTo: "**/*" },
|
|
435
|
+
text: stringifyFrontmatter({ description: data.description, applyTo: "**/*" }, `# ${data.name}
|
|
436
|
+
|
|
437
|
+
> ${data.description}
|
|
438
|
+
|
|
439
|
+
Full skill: read \`.mugiwara/refs/${data.name}/${data.name}.md\` when the crew invokes this role.`)
|
|
440
|
+
};
|
|
441
|
+
},
|
|
442
|
+
transformSkillFull(data, body) {
|
|
443
|
+
return {
|
|
444
|
+
relPath: `${data.name}.md`,
|
|
445
|
+
text: stringifyFrontmatter({ description: data.description }, body)
|
|
323
446
|
};
|
|
324
447
|
},
|
|
325
448
|
transformAgent(data, body) {
|
|
@@ -334,7 +457,7 @@ var target3 = {
|
|
|
334
457
|
};
|
|
335
458
|
|
|
336
459
|
// src/targets/generic.ts
|
|
337
|
-
import { existsSync as existsSync3, writeFileSync } from "node:fs";
|
|
460
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
338
461
|
import { join as join4 } from "node:path";
|
|
339
462
|
function makeGeneric(opts) {
|
|
340
463
|
const { id, label, rulesDir, bootstrapFile, bootstrapPointer, tier = bootstrapPointer ? 2 : 3 } = opts;
|
|
@@ -360,7 +483,7 @@ function makeGeneric(opts) {
|
|
|
360
483
|
|
|
361
484
|
## Skip when
|
|
362
485
|
|
|
363
|
-
Full skill: \`${data.name}\` — read \`.mugiwara/refs/${data.name}.md\` when the crew invokes this role.`
|
|
486
|
+
Full skill: \`${data.name}\` — read \`.mugiwara/refs/${data.name}/${data.name}.md\` when the crew invokes this role.`
|
|
364
487
|
};
|
|
365
488
|
}
|
|
366
489
|
return { relPath: `${data.name}.md`, text: `# ${data.name}
|
|
@@ -386,7 +509,7 @@ ${body}` };
|
|
|
386
509
|
|
|
387
510
|
> ${data.description}
|
|
388
511
|
|
|
389
|
-
Skills: ${data.skills ?? ""}. Read \`.mugiwara/refs/${data.name}.md\` when embodying this role.
|
|
512
|
+
Skills: ${data.skills ?? ""}. Read \`.mugiwara/refs/${data.name}/${data.name}.md\` when embodying this role.
|
|
390
513
|
|
|
391
514
|
Only zoro-execution and brook-healing may modify source code.
|
|
392
515
|
Return your output to luffy-orchestrator; do not choose the next step.`
|
|
@@ -411,8 +534,8 @@ Skills used: ${data.skills ?? ""}
|
|
|
411
534
|
|
|
412
535
|
${body}` };
|
|
413
536
|
},
|
|
414
|
-
refsDir({ projectDir }) {
|
|
415
|
-
return join4(projectDir, ".mugiwara", "refs");
|
|
537
|
+
refsDir({ projectDir }, skillName) {
|
|
538
|
+
return join4(projectDir, ".mugiwara", "refs", skillName);
|
|
416
539
|
},
|
|
417
540
|
postInstall({ projectDir, dryRun }) {
|
|
418
541
|
if (!bootstrapFile)
|
|
@@ -422,7 +545,7 @@ ${body}` };
|
|
|
422
545
|
const file = join4(projectDir, bootstrapFile);
|
|
423
546
|
if (!existsSync3(file)) {
|
|
424
547
|
if (!dryRun)
|
|
425
|
-
|
|
548
|
+
writeFileSync2(file, `${bootstrapPointer}
|
|
426
549
|
`);
|
|
427
550
|
written.push(file);
|
|
428
551
|
} else {
|
|
@@ -497,7 +620,7 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
|
|
|
497
620
|
var TARGET_IDS = Object.keys(targets);
|
|
498
621
|
|
|
499
622
|
// src/installer.ts
|
|
500
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync as
|
|
623
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync3, copyFileSync as copyFileSync3, rmSync, lstatSync } from "node:fs";
|
|
501
624
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
502
625
|
import { homedir } from "node:os";
|
|
503
626
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
@@ -561,7 +684,7 @@ function installTo(target10, opts) {
|
|
|
561
684
|
}
|
|
562
685
|
if (!dryRun) {
|
|
563
686
|
mkdirSync3(dirname3(absPath), { recursive: true });
|
|
564
|
-
|
|
687
|
+
writeFileSync3(absPath, text);
|
|
565
688
|
}
|
|
566
689
|
result.written.push(absPath);
|
|
567
690
|
};
|
|
@@ -610,6 +733,40 @@ function installTo(target10, opts) {
|
|
|
610
733
|
result.written.push(...post.written);
|
|
611
734
|
result.notes.push(...post.notes);
|
|
612
735
|
}
|
|
736
|
+
if (scope === "project") {
|
|
737
|
+
const configPath = join5(projectDir, ".mugiwara", "config");
|
|
738
|
+
let configExists = false;
|
|
739
|
+
try {
|
|
740
|
+
configExists = lstatSync(configPath).isFile() || lstatSync(configPath).isSymbolicLink();
|
|
741
|
+
} catch {
|
|
742
|
+
configExists = false;
|
|
743
|
+
}
|
|
744
|
+
if (!configExists) {
|
|
745
|
+
const body = [
|
|
746
|
+
"mode=guided",
|
|
747
|
+
"branch=feature/{type}-{issue}-{slug}",
|
|
748
|
+
"commit=conventional",
|
|
749
|
+
"auto_commit=on",
|
|
750
|
+
"coverage_new=90",
|
|
751
|
+
"coverage_modified=80",
|
|
752
|
+
"review_depth=full",
|
|
753
|
+
"quality_depth=full",
|
|
754
|
+
"verify_merged=off",
|
|
755
|
+
"delegate_threshold=60",
|
|
756
|
+
"heal_max_cycles=3",
|
|
757
|
+
"verbosity=normal",
|
|
758
|
+
"# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)"
|
|
759
|
+
].join(`
|
|
760
|
+
`) + `
|
|
761
|
+
`;
|
|
762
|
+
if (!dryRun) {
|
|
763
|
+
mkdirSync3(dirname3(configPath), { recursive: true });
|
|
764
|
+
writeFileSync3(configPath, body);
|
|
765
|
+
}
|
|
766
|
+
result.written.push(configPath);
|
|
767
|
+
result.notes.push(`default config written: ${configPath} (edit it to customise)`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
613
770
|
return result;
|
|
614
771
|
}
|
|
615
772
|
function removeInstalled(manifest, { dryRun = false } = {}) {
|
|
@@ -648,11 +805,11 @@ function assertNotSymlink(file) {
|
|
|
648
805
|
}
|
|
649
806
|
var GITIGNORE_BLOCK_START = "# >>> mugiwara >>>";
|
|
650
807
|
var GITIGNORE_BLOCK_END = "# <<< mugiwara <<<";
|
|
651
|
-
var
|
|
652
|
-
var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit
|
|
653
|
-
# Ignore session state
|
|
654
|
-
.mugiwara/
|
|
655
|
-
.mugiwara/
|
|
808
|
+
var GITIGNORE_LEGACY = [".mugiwara/state/", "# mugiwara"];
|
|
809
|
+
var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit plan.md, flows/, review.md,
|
|
810
|
+
# decisions.md, blockers.md, report.md under .mugiwara/missions/. Ignore session state.
|
|
811
|
+
.mugiwara/missions/**/*.json
|
|
812
|
+
.mugiwara/index.md
|
|
656
813
|
.mugiwara/config
|
|
657
814
|
.mugiwara/refs/
|
|
658
815
|
# <<< mugiwara <<<
|
|
@@ -663,10 +820,10 @@ function ensureProjectGitignore(projectDir, opts = {}) {
|
|
|
663
820
|
assertNotSymlink(path);
|
|
664
821
|
let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
|
|
665
822
|
if (existing) {
|
|
666
|
-
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/
|
|
823
|
+
if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
|
|
667
824
|
return { appended: false, notes: [] };
|
|
668
825
|
}
|
|
669
|
-
const hadOld = existing.includes(
|
|
826
|
+
const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
|
|
670
827
|
if (hadOld) {
|
|
671
828
|
const clean = removeProjectGitignore(projectDir, { dryRun });
|
|
672
829
|
existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
|
|
@@ -677,7 +834,7 @@ function ensureProjectGitignore(projectDir, opts = {}) {
|
|
|
677
834
|
` : "";
|
|
678
835
|
if (!dryRun) {
|
|
679
836
|
mkdirSync3(dirname3(path), { recursive: true });
|
|
680
|
-
|
|
837
|
+
writeFileSync3(path, existing + separator + GITIGNORE_BLOCK);
|
|
681
838
|
}
|
|
682
839
|
return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
|
|
683
840
|
}
|
|
@@ -695,18 +852,20 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
695
852
|
if (end < start)
|
|
696
853
|
return { removed: false, notes: ["delimiter mismatch — .gitignore left untouched"] };
|
|
697
854
|
cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
|
|
698
|
-
} else if (current.includes(
|
|
855
|
+
} else if (current.includes("# mugiwara")) {
|
|
699
856
|
const LEGACY_LINES = new Set([
|
|
700
857
|
".mugiwara/state.json",
|
|
701
858
|
".mugiwara/state-*.json",
|
|
702
859
|
".mugiwara/config",
|
|
860
|
+
".mugiwara/state/",
|
|
861
|
+
".mugiwara/continue/",
|
|
703
862
|
".mugiwara/continue.md",
|
|
704
863
|
".mugiwara/refs/"
|
|
705
864
|
]);
|
|
706
865
|
const lines = current.split(`
|
|
707
866
|
`).filter((l) => {
|
|
708
867
|
const t = l.trim();
|
|
709
|
-
if (t.startsWith(
|
|
868
|
+
if (t.startsWith("# mugiwara"))
|
|
710
869
|
return false;
|
|
711
870
|
return !LEGACY_LINES.has(t);
|
|
712
871
|
});
|
|
@@ -724,12 +883,12 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
|
|
|
724
883
|
if (cleaned === current)
|
|
725
884
|
return { removed: false, notes: [] };
|
|
726
885
|
if (!dryRun)
|
|
727
|
-
|
|
886
|
+
writeFileSync3(path, cleaned);
|
|
728
887
|
return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
|
|
729
888
|
}
|
|
730
889
|
|
|
731
890
|
// src/manifest.ts
|
|
732
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as
|
|
891
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
733
892
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
734
893
|
function manifestPath({ scope, projectDir, home }) {
|
|
735
894
|
return scope === "global" ? join6(home, ".mugiwara", "manifest.json") : join6(projectDir, ".mugiwara", "manifest.json");
|
|
@@ -739,24 +898,441 @@ function readManifest(file) {
|
|
|
739
898
|
}
|
|
740
899
|
function writeManifest(file, data) {
|
|
741
900
|
mkdirSync4(dirname4(file), { recursive: true });
|
|
742
|
-
|
|
901
|
+
writeFileSync4(file, JSON.stringify(data, null, 2) + `
|
|
902
|
+
`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/mission.ts
|
|
906
|
+
import { existsSync as existsSync8, rmSync as rmSync2, readFileSync as readFileSync7, readdirSync as readdirSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7, renameSync, openSync, writeSync, closeSync } from "node:fs";
|
|
907
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
908
|
+
import { join as join11 } from "node:path";
|
|
909
|
+
|
|
910
|
+
// src/integrity.ts
|
|
911
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
|
|
912
|
+
import { isAbsolute, join as join7, relative } from "node:path";
|
|
913
|
+
var SECRET_PATTERNS = [
|
|
914
|
+
[/AKIA[0-9A-Z]{16}/, "AWS access key id"],
|
|
915
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
|
|
916
|
+
[/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
|
|
917
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
|
|
918
|
+
[/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
|
|
919
|
+
[/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
|
|
920
|
+
[/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
|
|
921
|
+
];
|
|
922
|
+
var ALLOW_SECRET = "mugiwara:allow-secret";
|
|
923
|
+
function findSecrets(body) {
|
|
924
|
+
const out = [];
|
|
925
|
+
for (const line of body.split(/\r?\n/)) {
|
|
926
|
+
if (line.includes(ALLOW_SECRET))
|
|
927
|
+
continue;
|
|
928
|
+
for (const [re, label] of SECRET_PATTERNS) {
|
|
929
|
+
const hit = line.match(re);
|
|
930
|
+
if (hit)
|
|
931
|
+
out.push({ label, hit: hit[0] });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
|
|
937
|
+
function trailFiles(dir) {
|
|
938
|
+
const out = [];
|
|
939
|
+
const walk = (d) => {
|
|
940
|
+
for (const e of readdirSync4(d, { withFileTypes: true })) {
|
|
941
|
+
const p = join7(d, e.name);
|
|
942
|
+
if (e.isDirectory())
|
|
943
|
+
walk(p);
|
|
944
|
+
else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
|
|
945
|
+
out.push(p);
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
walk(dir);
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
function linkedPaths(md) {
|
|
952
|
+
const out = [];
|
|
953
|
+
for (const m of md.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
954
|
+
const t = m[1];
|
|
955
|
+
if (/^(https?:|mailto:|#|\/\/)/.test(t))
|
|
956
|
+
continue;
|
|
957
|
+
out.push(t.split("#")[0]);
|
|
958
|
+
}
|
|
959
|
+
return out.filter(Boolean);
|
|
960
|
+
}
|
|
961
|
+
function hasCommandOutputShape(body) {
|
|
962
|
+
return /`[^`]+`/.test(body) || /\b(exit\s+[01]|✓|✗|\bPASS\b|\bFAIL\b|passed|failed|\d+\s+(passed|failed))\b/i.test(body);
|
|
963
|
+
}
|
|
964
|
+
function collectPassCitedPaths(missionDir) {
|
|
965
|
+
const out = [];
|
|
966
|
+
for (const f of trailFiles(missionDir)) {
|
|
967
|
+
let body;
|
|
968
|
+
try {
|
|
969
|
+
body = readFileSync5(f, "utf8");
|
|
970
|
+
} catch {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
for (const line of body.split(/\r?\n/)) {
|
|
974
|
+
if (!/\bPASS\b/.test(line))
|
|
975
|
+
continue;
|
|
976
|
+
for (const p of linkedPaths(line))
|
|
977
|
+
out.push(p);
|
|
978
|
+
for (const m of line.matchAll(/(?:^|[\s"'(])([a-zA-Z0-9._\/-]+\.(?:md|txt|log|json))\b/g)) {
|
|
979
|
+
const cand = m[1];
|
|
980
|
+
if (cand.includes("/"))
|
|
981
|
+
out.push(cand);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return [...new Set(out)];
|
|
986
|
+
}
|
|
987
|
+
function checkTrail(missionDir, projectRoot) {
|
|
988
|
+
const issues = [];
|
|
989
|
+
const files = trailFiles(missionDir);
|
|
990
|
+
for (const f of files) {
|
|
991
|
+
let body;
|
|
992
|
+
try {
|
|
993
|
+
body = readFileSync5(f, "utf8");
|
|
994
|
+
} catch {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
for (const target10 of linkedPaths(body)) {
|
|
998
|
+
if (isAbsolute(target10))
|
|
999
|
+
continue;
|
|
1000
|
+
const fromMission = join7(missionDir, target10);
|
|
1001
|
+
const fromRoot = join7(projectRoot, target10);
|
|
1002
|
+
if (!existsSync6(fromMission) && !existsSync6(fromRoot)) {
|
|
1003
|
+
issues.push({
|
|
1004
|
+
kind: "dangling-path",
|
|
1005
|
+
detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for (const { label, hit } of findSecrets(body)) {
|
|
1010
|
+
issues.push({
|
|
1011
|
+
kind: "secret",
|
|
1012
|
+
detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const evidencePaths = [];
|
|
1017
|
+
const evidenceFile = join7(missionDir, "state.json");
|
|
1018
|
+
if (existsSync6(evidenceFile)) {
|
|
1019
|
+
try {
|
|
1020
|
+
const s = JSON.parse(readFileSync5(evidenceFile, "utf8"));
|
|
1021
|
+
if (Array.isArray(s.evidence)) {
|
|
1022
|
+
for (const e of s.evidence) {
|
|
1023
|
+
if (typeof e !== "string" || !e.trim())
|
|
1024
|
+
continue;
|
|
1025
|
+
evidencePaths.push(e);
|
|
1026
|
+
const cand = join7(projectRoot, e);
|
|
1027
|
+
if (!isAbsolute(e) && !existsSync6(cand) && !existsSync6(join7(missionDir, e))) {
|
|
1028
|
+
issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
} catch {}
|
|
1033
|
+
}
|
|
1034
|
+
const passCited = collectPassCitedPaths(missionDir);
|
|
1035
|
+
for (const e of passCited) {
|
|
1036
|
+
if (!e.trim() || isAbsolute(e))
|
|
1037
|
+
continue;
|
|
1038
|
+
const candMission = join7(missionDir, e);
|
|
1039
|
+
const candRoot = join7(projectRoot, e);
|
|
1040
|
+
const resolved = existsSync6(candMission) ? candMission : existsSync6(candRoot) ? candRoot : null;
|
|
1041
|
+
if (!resolved)
|
|
1042
|
+
continue;
|
|
1043
|
+
let body;
|
|
1044
|
+
try {
|
|
1045
|
+
body = readFileSync5(resolved, "utf8");
|
|
1046
|
+
} catch {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (!hasCommandOutputShape(body)) {
|
|
1050
|
+
issues.push({ kind: "evidence-thin", detail: `evidence "${e}" exists but lacks command output (no backticked command or exit-status token)` });
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return issues;
|
|
1054
|
+
}
|
|
1055
|
+
function formatIssues(issues) {
|
|
1056
|
+
return issues.map((i) => ` ✗ [${i.kind}] ${i.detail}`).join(`
|
|
743
1057
|
`);
|
|
744
1058
|
}
|
|
745
1059
|
|
|
1060
|
+
// src/rollback.ts
|
|
1061
|
+
import { execFileSync } from "node:child_process";
|
|
1062
|
+
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
1063
|
+
import { join as join8 } from "node:path";
|
|
1064
|
+
function git(cwd, args) {
|
|
1065
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1066
|
+
}
|
|
1067
|
+
function buildRollback(input, commitsNewestFirst, filesTouched) {
|
|
1068
|
+
const lines = [
|
|
1069
|
+
"#!/usr/bin/env bash",
|
|
1070
|
+
`# Rollback map for mission "${input.mission}" — generated at closure.`,
|
|
1071
|
+
"# Human-executed. Review before running; mugiwara never runs this.",
|
|
1072
|
+
`# Branch: ${input.branch}`,
|
|
1073
|
+
`# Base: ${input.baseSha}`,
|
|
1074
|
+
"",
|
|
1075
|
+
"set -euo pipefail",
|
|
1076
|
+
""
|
|
1077
|
+
];
|
|
1078
|
+
if (!commitsNewestFirst.length && !filesTouched.length) {
|
|
1079
|
+
lines.push("# No commits between base and HEAD on this branch — nothing to revert.", "");
|
|
1080
|
+
return lines.join(`
|
|
1081
|
+
`);
|
|
1082
|
+
}
|
|
1083
|
+
if (!commitsNewestFirst.length) {
|
|
1084
|
+
lines.push("# UNRESOLVED: squash-merged state detected.", `# git rev-list ${input.baseSha}..${input.branch} is empty, but the diff`, `# ${input.baseSha}..${input.branch} touches ${filesTouched.length} file(s):`, "# the mission's changes were collapsed into commit(s) already on the base", "# ref. No per-commit revert list can be derived automatically.", "# Locate the squash commit, review it, then revert it:", `# git log --oneline ${input.baseSha}..HEAD --grep="${input.mission}"`, "# git revert <squash-commit>", "# Files carrying the squashed changes (verify each after reverting):", ...filesTouched.map((f) => `# ${f}`), "", 'echo "ROLLBACK INCOMPLETE: squash-merged state — locate and revert the squash commit manually" >&2', "exit 1", "");
|
|
1085
|
+
return lines.join(`
|
|
1086
|
+
`);
|
|
1087
|
+
}
|
|
1088
|
+
lines.push("# Revert newest-first so earlier reverts never conflict with later ones.", "git revert --no-edit \\", commitsNewestFirst.map((c) => ` ${c}`).join(" \\\n"), "");
|
|
1089
|
+
if (filesTouched.length) {
|
|
1090
|
+
lines.push("# Files this mission touched (verify the working tree is clean afterwards):", ...filesTouched.map((f) => `# ${f}`), "");
|
|
1091
|
+
}
|
|
1092
|
+
return lines.join(`
|
|
1093
|
+
`);
|
|
1094
|
+
}
|
|
1095
|
+
function generateRollback(projectDir, missionDir, input) {
|
|
1096
|
+
try {
|
|
1097
|
+
const range = `${input.baseSha}..${input.branch}`;
|
|
1098
|
+
const revList = git(projectDir, ["rev-list", "--reverse", range]).split(/\r?\n/).filter(Boolean);
|
|
1099
|
+
const commitsNewestFirst = [...revList].reverse();
|
|
1100
|
+
const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
|
|
1101
|
+
const body = buildRollback(input, commitsNewestFirst, filesTouched);
|
|
1102
|
+
const file = join8(missionDir, "rollback.sh");
|
|
1103
|
+
writeFileSync5(file, body, { mode: 493 });
|
|
1104
|
+
return { file: "rollback.sh", commits: commitsNewestFirst.length };
|
|
1105
|
+
} catch {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/provenance.ts
|
|
1111
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1112
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
1113
|
+
import { join as join9 } from "node:path";
|
|
1114
|
+
function git2(cwd, args) {
|
|
1115
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1116
|
+
}
|
|
1117
|
+
function modelLabel() {
|
|
1118
|
+
return process.env.MUGIWARA_MODEL?.trim() || process.env.ANTHROPIC_MODEL?.trim() || "model-unrecorded (set MUGIWARA_MODEL to attribute)";
|
|
1119
|
+
}
|
|
1120
|
+
function buildNote(s) {
|
|
1121
|
+
const gates = s.evidence.length ? s.evidence.join(" · ") : "no evidence recorded";
|
|
1122
|
+
const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
|
|
1123
|
+
const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(", ")}` : s.model ?? modelLabel();
|
|
1124
|
+
return [
|
|
1125
|
+
`mission: ${s.mission}`,
|
|
1126
|
+
`agent: ${s.actor || "unknown"} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
|
|
1127
|
+
`tasks: ${s.tasks_done}/${s.tasks_total}`,
|
|
1128
|
+
`gates/evidence: ${gates}`,
|
|
1129
|
+
`branch: ${s.branch}`,
|
|
1130
|
+
"human review: pending (PR review is the terminal gate)"
|
|
1131
|
+
].join(`
|
|
1132
|
+
`);
|
|
1133
|
+
}
|
|
1134
|
+
function renderProvenanceMd(note, sha) {
|
|
1135
|
+
const lines = [
|
|
1136
|
+
"# Provenance",
|
|
1137
|
+
"",
|
|
1138
|
+
"<!-- paste below into the PR description or a PR comment -->",
|
|
1139
|
+
"",
|
|
1140
|
+
"```",
|
|
1141
|
+
note,
|
|
1142
|
+
"```",
|
|
1143
|
+
"",
|
|
1144
|
+
sha ? `Commit: ${sha}` : "Commit: not recorded (no git head resolved at closure)",
|
|
1145
|
+
"",
|
|
1146
|
+
"Query locally after pushing notes:",
|
|
1147
|
+
"`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`."
|
|
1148
|
+
];
|
|
1149
|
+
return lines.join(`
|
|
1150
|
+
`) + `
|
|
1151
|
+
`;
|
|
1152
|
+
}
|
|
1153
|
+
function attachGitNote(projectDir, branch, note) {
|
|
1154
|
+
try {
|
|
1155
|
+
let sha;
|
|
1156
|
+
try {
|
|
1157
|
+
sha = git2(projectDir, ["rev-parse", "--verify", branch]);
|
|
1158
|
+
} catch {
|
|
1159
|
+
sha = git2(projectDir, ["rev-parse", "HEAD"]);
|
|
1160
|
+
}
|
|
1161
|
+
git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
|
|
1162
|
+
return { sha };
|
|
1163
|
+
} catch {
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function blamePath(projectDir, path) {
|
|
1168
|
+
let sha;
|
|
1169
|
+
try {
|
|
1170
|
+
sha = git2(projectDir, ["log", "-1", "--format=%H", "--", path]);
|
|
1171
|
+
} catch {
|
|
1172
|
+
return `blame: not a git repository (${projectDir})`;
|
|
1173
|
+
}
|
|
1174
|
+
if (!sha)
|
|
1175
|
+
return `blame: no commit touches "${path}"`;
|
|
1176
|
+
try {
|
|
1177
|
+
const note = git2(projectDir, ["notes", "--ref=mugiwara", "show", sha]);
|
|
1178
|
+
return `${path} @ ${sha.slice(0, 7)}
|
|
1179
|
+
${note}`;
|
|
1180
|
+
} catch {
|
|
1181
|
+
return `${path} @ ${sha.slice(0, 7)}
|
|
1182
|
+
(no mugiwara provenance note on this commit)`;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function writeProvenance(projectDir, missionDir, state) {
|
|
1186
|
+
const note = buildNote(state);
|
|
1187
|
+
const attached = attachGitNote(projectDir, state.branch, note);
|
|
1188
|
+
writeFileSync6(join9(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/routing.ts
|
|
1192
|
+
var DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
|
|
1193
|
+
var TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
|
|
1194
|
+
function scorePath(path, sensitivePaths, evidenceJoined) {
|
|
1195
|
+
const reasons = [];
|
|
1196
|
+
let score = 0;
|
|
1197
|
+
const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, "/")) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")).test(path)));
|
|
1198
|
+
if (sensitiveHit) {
|
|
1199
|
+
score += 100;
|
|
1200
|
+
reasons.push("sensitive path");
|
|
1201
|
+
}
|
|
1202
|
+
if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
|
|
1203
|
+
score += 50;
|
|
1204
|
+
reasons.push("production code");
|
|
1205
|
+
} else if (TEST_PAT.test(path)) {
|
|
1206
|
+
score += 10;
|
|
1207
|
+
reasons.push("test scaffolding — skim unless behavior changed");
|
|
1208
|
+
} else {
|
|
1209
|
+
score += 5;
|
|
1210
|
+
reasons.push("docs/config");
|
|
1211
|
+
}
|
|
1212
|
+
if (!evidenceJoined.includes(path)) {
|
|
1213
|
+
score += 20;
|
|
1214
|
+
reasons.push("not covered by recorded evidence");
|
|
1215
|
+
}
|
|
1216
|
+
return { score, reasons };
|
|
1217
|
+
}
|
|
1218
|
+
function rankFiles(paths, state) {
|
|
1219
|
+
const sensitive = state.sensitive_paths ?? [];
|
|
1220
|
+
const ev = state.evidence.join(" ");
|
|
1221
|
+
return paths.map((path) => {
|
|
1222
|
+
const { score, reasons } = scorePath(path, sensitive, ev);
|
|
1223
|
+
return { path, score, reasons };
|
|
1224
|
+
}).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
1225
|
+
}
|
|
1226
|
+
function renderRouting(ranked, mission) {
|
|
1227
|
+
if (!ranked.length)
|
|
1228
|
+
return "";
|
|
1229
|
+
const lines = [
|
|
1230
|
+
"",
|
|
1231
|
+
"## Review routing",
|
|
1232
|
+
"",
|
|
1233
|
+
`Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
|
|
1234
|
+
""
|
|
1235
|
+
];
|
|
1236
|
+
ranked.forEach((r, i) => {
|
|
1237
|
+
lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join("; ")}`);
|
|
1238
|
+
});
|
|
1239
|
+
lines.push("");
|
|
1240
|
+
return lines.join(`
|
|
1241
|
+
`);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/budget.ts
|
|
1245
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync } from "node:fs";
|
|
1246
|
+
import { homedir as homedir2 } from "node:os";
|
|
1247
|
+
import { join as join10 } from "node:path";
|
|
1248
|
+
function readBudgetConfig(projectDir) {
|
|
1249
|
+
for (const base of [projectDir, homedir2()]) {
|
|
1250
|
+
const file = join10(base, ".mugiwara", "config");
|
|
1251
|
+
if (!existsSync7(file))
|
|
1252
|
+
continue;
|
|
1253
|
+
for (const line of readFileSync6(file, "utf8").split(/\r?\n/)) {
|
|
1254
|
+
const t = line.trim();
|
|
1255
|
+
if (!t || t.startsWith("#"))
|
|
1256
|
+
continue;
|
|
1257
|
+
const eq = t.indexOf("=");
|
|
1258
|
+
if (eq === -1)
|
|
1259
|
+
continue;
|
|
1260
|
+
if (t.slice(0, eq).trim() !== "context_budget_chars")
|
|
1261
|
+
continue;
|
|
1262
|
+
const n = Number(t.slice(eq + 1).trim());
|
|
1263
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return 0;
|
|
1267
|
+
}
|
|
1268
|
+
function measureContextChars(missionDir) {
|
|
1269
|
+
let total = 0;
|
|
1270
|
+
const add = (p) => {
|
|
1271
|
+
try {
|
|
1272
|
+
total += statSync(p).size;
|
|
1273
|
+
} catch {}
|
|
1274
|
+
};
|
|
1275
|
+
for (const f of readdirSync5(missionDir)) {
|
|
1276
|
+
if (/\.md$/.test(f))
|
|
1277
|
+
add(join10(missionDir, f));
|
|
1278
|
+
}
|
|
1279
|
+
for (const sub of ["flows", "waves"]) {
|
|
1280
|
+
const dir = join10(missionDir, sub);
|
|
1281
|
+
if (existsSync7(dir)) {
|
|
1282
|
+
for (const f of readdirSync5(dir))
|
|
1283
|
+
add(join10(dir, f));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return total;
|
|
1287
|
+
}
|
|
1288
|
+
function formatFootprint(chars, budget) {
|
|
1289
|
+
const base = `Context footprint: ${chars} chars`;
|
|
1290
|
+
if (!budget)
|
|
1291
|
+
return `${base} (no budget configured)`;
|
|
1292
|
+
return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
746
1295
|
// src/mission.ts
|
|
747
|
-
|
|
748
|
-
|
|
1296
|
+
function isStateFile(f) {
|
|
1297
|
+
const stem = f.replace(/\.json$/, "");
|
|
1298
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1299
|
+
}
|
|
1300
|
+
function primaryState(dir, files) {
|
|
1301
|
+
const name = files.includes("state.json") ? "state.json" : files.find((f) => f.endsWith(".json") && f !== "continue.json" && !f.startsWith("continue-"));
|
|
1302
|
+
if (!name)
|
|
1303
|
+
return null;
|
|
1304
|
+
try {
|
|
1305
|
+
return JSON.parse(readFileSync7(join11(dir, name), "utf8"));
|
|
1306
|
+
} catch {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
function changedFiles(projectDir, state) {
|
|
1311
|
+
const base = typeof state?.base_sha === "string" ? state.base_sha : "";
|
|
1312
|
+
const branch = typeof state?.branch === "string" ? state.branch : "";
|
|
1313
|
+
if (!base || base === "unknown" || !branch)
|
|
1314
|
+
return [];
|
|
1315
|
+
try {
|
|
1316
|
+
return execFileSync3("git", ["diff", "--name-only", base, branch], {
|
|
1317
|
+
cwd: projectDir,
|
|
1318
|
+
encoding: "utf8",
|
|
1319
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1320
|
+
}).split(/\r?\n/).filter(Boolean);
|
|
1321
|
+
} catch {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
749
1325
|
function activeActor(projectDir) {
|
|
750
|
-
const
|
|
751
|
-
if (!
|
|
1326
|
+
const missionsDir = join11(projectDir, ".mugiwara", "missions");
|
|
1327
|
+
if (!existsSync8(missionsDir))
|
|
752
1328
|
return null;
|
|
753
|
-
const missions =
|
|
1329
|
+
const missions = readdirSync6(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
754
1330
|
let latest = null;
|
|
755
1331
|
for (const mission of missions) {
|
|
756
|
-
const d =
|
|
757
|
-
for (const f of
|
|
1332
|
+
const d = join11(missionsDir, mission);
|
|
1333
|
+
for (const f of readdirSync6(d).filter(isStateFile)) {
|
|
758
1334
|
try {
|
|
759
|
-
const s = JSON.parse(
|
|
1335
|
+
const s = JSON.parse(readFileSync7(join11(d, f), "utf8"));
|
|
760
1336
|
const t = Date.parse(s.updated_at || "") || 0;
|
|
761
1337
|
if (!latest || t > latest.updated)
|
|
762
1338
|
latest = { actor: s.actor || null, updated: t };
|
|
@@ -766,8 +1342,8 @@ function activeActor(projectDir) {
|
|
|
766
1342
|
return latest ? latest.actor : null;
|
|
767
1343
|
}
|
|
768
1344
|
function resetMission(projectDir, keepLogs, force) {
|
|
769
|
-
const root =
|
|
770
|
-
if (!
|
|
1345
|
+
const root = join11(projectDir, ".mugiwara");
|
|
1346
|
+
if (!existsSync8(root))
|
|
771
1347
|
return { removed: [], kept: [] };
|
|
772
1348
|
if (!force) {
|
|
773
1349
|
const actor = activeActor(projectDir);
|
|
@@ -777,140 +1353,506 @@ function resetMission(projectDir, keepLogs, force) {
|
|
|
777
1353
|
}
|
|
778
1354
|
const removed = [];
|
|
779
1355
|
const kept = [];
|
|
780
|
-
for (const dir of ["spec", "plans", "results", "review", "issues", "reports"]) {
|
|
781
|
-
const p =
|
|
782
|
-
if (
|
|
783
|
-
rmSync2(p, { recursive: true, force: true });
|
|
784
|
-
removed.push(dir);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
for (const dir of ["state", "continue"]) {
|
|
788
|
-
const p = join7(root, dir);
|
|
789
|
-
if (existsSync6(p)) {
|
|
1356
|
+
for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
|
|
1357
|
+
const p = join11(root, dir);
|
|
1358
|
+
if (existsSync8(p)) {
|
|
790
1359
|
rmSync2(p, { recursive: true, force: true });
|
|
791
1360
|
removed.push(dir);
|
|
792
1361
|
}
|
|
793
1362
|
}
|
|
794
|
-
for (const f of
|
|
1363
|
+
for (const f of readdirSync6(root)) {
|
|
795
1364
|
if (/^state(-.+)?\.json$/.test(f)) {
|
|
796
|
-
const p =
|
|
797
|
-
if (
|
|
1365
|
+
const p = join11(root, f);
|
|
1366
|
+
if (existsSync8(p)) {
|
|
798
1367
|
rmSync2(p);
|
|
799
1368
|
removed.push(f);
|
|
800
1369
|
}
|
|
801
1370
|
}
|
|
802
1371
|
}
|
|
803
1372
|
if (!keepLogs) {
|
|
804
|
-
const p
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1373
|
+
for (const p of [join11(root, "lessons.md"), join11(root, "logs")]) {
|
|
1374
|
+
if (existsSync8(p)) {
|
|
1375
|
+
rmSync2(p, { recursive: true, force: true });
|
|
1376
|
+
removed.push(p.startsWith(join11(root, "logs")) ? "logs" : "lessons.md");
|
|
1377
|
+
}
|
|
808
1378
|
}
|
|
809
|
-
} else
|
|
810
|
-
|
|
1379
|
+
} else {
|
|
1380
|
+
if (existsSync8(join11(root, "lessons.md")))
|
|
1381
|
+
kept.push("lessons.md");
|
|
1382
|
+
else if (existsSync8(join11(root, join11("logs", "lessons.md"))))
|
|
1383
|
+
kept.push(join11("logs", "lessons.md"));
|
|
811
1384
|
}
|
|
812
1385
|
for (const f of ["config", "manifest.json", "backup"]) {
|
|
813
|
-
if (
|
|
1386
|
+
if (existsSync8(join11(root, f)))
|
|
814
1387
|
kept.push(f);
|
|
815
1388
|
}
|
|
816
1389
|
return { removed, kept };
|
|
817
1390
|
}
|
|
818
1391
|
function archiveMission(projectDir, mission, opts = {}) {
|
|
819
1392
|
const { dryRun = false } = opts;
|
|
820
|
-
const root =
|
|
1393
|
+
const root = join11(projectDir, ".mugiwara");
|
|
821
1394
|
if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
|
|
822
1395
|
throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
|
|
823
1396
|
const removed = [];
|
|
824
1397
|
const kept = [];
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
return
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const m = n.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
|
|
834
|
-
return !!m && m[2] === mission;
|
|
835
|
-
});
|
|
836
|
-
if (f)
|
|
837
|
-
report = join7("reports", f);
|
|
838
|
-
}
|
|
839
|
-
const resultsDir = join7(root, "results", mission);
|
|
840
|
-
if (existsSync6(resultsDir)) {
|
|
841
|
-
for (const f of readdirSync4(resultsDir)) {
|
|
842
|
-
kept.push(join7("results", mission, f));
|
|
1398
|
+
const dir = join11(root, "missions", mission);
|
|
1399
|
+
if (!existsSync8(dir))
|
|
1400
|
+
return { report: null, removed, kept };
|
|
1401
|
+
if (!dryRun) {
|
|
1402
|
+
const issues = checkTrail(dir, projectDir);
|
|
1403
|
+
if (issues.length) {
|
|
1404
|
+
throw new Error(`closure integrity gate failed — fix these before archiving:
|
|
1405
|
+
${formatIssues(issues)}`);
|
|
843
1406
|
}
|
|
844
1407
|
}
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
removed.push(join7("spec", f));
|
|
1408
|
+
const files = readdirSync6(dir);
|
|
1409
|
+
const state = primaryState(dir, files);
|
|
1410
|
+
const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
|
|
1411
|
+
try {
|
|
1412
|
+
const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
|
|
1413
|
+
return typeof s.model === "string" ? s.model : "";
|
|
1414
|
+
} catch {
|
|
1415
|
+
return "";
|
|
854
1416
|
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const p = join7(d, f);
|
|
864
|
-
if (!dryRun)
|
|
865
|
-
rmSync2(p, { force: true });
|
|
866
|
-
removed.push(join7(dir, f));
|
|
1417
|
+
}).filter(Boolean))];
|
|
1418
|
+
let costSection = "";
|
|
1419
|
+
if (!dryRun && state) {
|
|
1420
|
+
const chars = measureContextChars(dir);
|
|
1421
|
+
const budget = readBudgetConfig(projectDir);
|
|
1422
|
+
const footprintLine = formatFootprint(chars, budget);
|
|
1423
|
+
if (budget && chars > budget) {
|
|
1424
|
+
throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
|
|
867
1425
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1426
|
+
const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
|
|
1427
|
+
const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
|
|
1428
|
+
const lane = typeof state.lane === "string" ? state.lane : "unknown";
|
|
1429
|
+
const laneBudget = lane === "lean" ? 12000 : lane === "standard" ? 25000 : lane === "full" ? 50000 : lane === "spike" ? 3000 : 0;
|
|
1430
|
+
const effBudget = budget || laneBudget;
|
|
1431
|
+
const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
|
|
1432
|
+
const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
|
|
1433
|
+
const srcLabel = src === "reported" ? "provider-reported" : "estimator";
|
|
1434
|
+
let reportedTotal = 0;
|
|
1435
|
+
let hasReported = false;
|
|
1436
|
+
for (const f of files.filter(isStateFile)) {
|
|
1437
|
+
try {
|
|
1438
|
+
const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
|
|
1439
|
+
if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
|
|
1440
|
+
reportedTotal += s.tokens_est;
|
|
1441
|
+
hasReported = true;
|
|
1442
|
+
}
|
|
1443
|
+
} catch {}
|
|
1444
|
+
}
|
|
1445
|
+
if (!hasReported && src === "reported" && est > 0) {
|
|
1446
|
+
reportedTotal = est;
|
|
1447
|
+
hasReported = true;
|
|
878
1448
|
}
|
|
1449
|
+
costSection = [
|
|
1450
|
+
"## Cost",
|
|
1451
|
+
"",
|
|
1452
|
+
"| Metric | Value |",
|
|
1453
|
+
"|--------|-------|",
|
|
1454
|
+
`| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
|
|
1455
|
+
`| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? Math.round(effBudget * 1.5).toLocaleString() : "—"} · stop ${effBudget ? (effBudget * 3).toLocaleString() : "—"}) |`,
|
|
1456
|
+
`| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${est >= effBudget * 3 ? "STOP" : est >= effBudget * 1.5 ? "WARN" : "OK"}` : "no lane budget"} |`,
|
|
1457
|
+
`| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`
|
|
1458
|
+
].join(`
|
|
1459
|
+
`);
|
|
1460
|
+
if (hasReported) {
|
|
1461
|
+
costSection += `
|
|
1462
|
+
| **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
|
|
1463
|
+
}
|
|
1464
|
+
costSection += `
|
|
1465
|
+
`;
|
|
879
1466
|
}
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1467
|
+
const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
|
|
1468
|
+
const fold = [];
|
|
1469
|
+
for (const f of FOLD_TOP) {
|
|
1470
|
+
if (files.includes(f))
|
|
1471
|
+
fold.push(f);
|
|
885
1472
|
}
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1473
|
+
const flowsDir = join11(dir, "flows");
|
|
1474
|
+
const legacyWavesDir = join11(dir, "waves");
|
|
1475
|
+
const artDir = existsSync8(flowsDir) ? flowsDir : existsSync8(legacyWavesDir) ? legacyWavesDir : flowsDir;
|
|
1476
|
+
const artRel = artDir === legacyWavesDir ? "waves" : "flows";
|
|
1477
|
+
if (existsSync8(artDir)) {
|
|
1478
|
+
for (const f of readdirSync6(artDir).sort())
|
|
1479
|
+
fold.push(join11(artRel, f));
|
|
891
1480
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1481
|
+
let report = "";
|
|
1482
|
+
const reportPath = join11(dir, "report.md");
|
|
1483
|
+
if (files.includes("report.md"))
|
|
1484
|
+
report = readFileSync7(reportPath, "utf8");
|
|
1485
|
+
else if (existsSync8(join11(artDir, "06-closure.md")))
|
|
1486
|
+
report = readFileSync7(join11(artDir, "06-closure.md"), "utf8");
|
|
1487
|
+
if (!dryRun) {
|
|
1488
|
+
mkdirSync5(dir, { recursive: true });
|
|
1489
|
+
if (fold.length) {
|
|
1490
|
+
const sections = fold.map((f) => {
|
|
1491
|
+
const body = readFileSync7(join11(dir, f), "utf8").trim();
|
|
1492
|
+
const name = f.includes("/") ? f.split("/").pop() ?? f : f;
|
|
1493
|
+
return `
|
|
1494
|
+
|
|
1495
|
+
## Archived: ${name}
|
|
1496
|
+
|
|
1497
|
+
${body}`;
|
|
1498
|
+
}).join("");
|
|
1499
|
+
const tmp = `${reportPath}.tmp`;
|
|
1500
|
+
const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
|
|
1501
|
+
mission,
|
|
1502
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1503
|
+
sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
|
|
1504
|
+
}), mission) : "";
|
|
1505
|
+
writeFileSync7(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
|
|
1506
|
+
${costSection}
|
|
1507
|
+
` : "") + `
|
|
1508
|
+
`);
|
|
1509
|
+
renameSync(tmp, reportPath);
|
|
1510
|
+
}
|
|
1511
|
+
for (const f of fold) {
|
|
1512
|
+
rmSync2(join11(dir, f), { force: true, recursive: true });
|
|
1513
|
+
removed.push(join11("missions", mission, f));
|
|
1514
|
+
}
|
|
1515
|
+
for (const f of files.filter((f2) => f2.endsWith(".json")))
|
|
1516
|
+
rmSync2(join11(dir, f), { force: true });
|
|
1517
|
+
if (existsSync8(artDir) && readdirSync6(artDir).length === 0)
|
|
1518
|
+
rmSync2(artDir, { recursive: true, force: true });
|
|
1519
|
+
if (!existsSync8(reportPath)) {
|
|
1520
|
+
writeFileSync7(reportPath, `# Mission: ${mission}
|
|
1521
|
+
|
|
1522
|
+
Archived before closure — no wave artifacts were present.
|
|
1523
|
+
`);
|
|
1524
|
+
}
|
|
1525
|
+
if (state && typeof state.branch === "string") {
|
|
1526
|
+
const rb = generateRollback(projectDir, dir, {
|
|
1527
|
+
mission,
|
|
1528
|
+
branch: state.branch,
|
|
1529
|
+
baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
|
|
1530
|
+
});
|
|
1531
|
+
if (rb)
|
|
1532
|
+
kept.push(join11("missions", mission, rb.file));
|
|
1533
|
+
try {
|
|
1534
|
+
writeProvenance(projectDir, dir, {
|
|
1535
|
+
mission,
|
|
1536
|
+
actor: typeof state.actor === "string" ? state.actor : "",
|
|
1537
|
+
lane: typeof state.lane === "string" ? state.lane : "",
|
|
1538
|
+
mode: typeof state.mode === "string" ? state.mode : "",
|
|
1539
|
+
branch: state.branch,
|
|
1540
|
+
tasks_done: Number(state.tasks_done) || 0,
|
|
1541
|
+
tasks_total: Number(state.tasks_total) || 0,
|
|
1542
|
+
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1543
|
+
models: stageModels
|
|
1544
|
+
});
|
|
1545
|
+
kept.push(join11("missions", mission, "provenance.md"));
|
|
1546
|
+
} catch {}
|
|
1547
|
+
}
|
|
1548
|
+
} else {
|
|
1549
|
+
for (const f of fold)
|
|
1550
|
+
removed.push(join11("missions", mission, f));
|
|
897
1551
|
}
|
|
1552
|
+
removed.push(join11("missions", mission, "<session state>"));
|
|
1553
|
+
if (files.includes("plan.md"))
|
|
1554
|
+
kept.push(join11("missions", mission, "plan.md"));
|
|
1555
|
+
if (files.includes("handoff.md"))
|
|
1556
|
+
kept.push(join11("missions", mission, "handoff.md"));
|
|
1557
|
+
kept.push(join11("missions", mission, "report.md"));
|
|
898
1558
|
let index;
|
|
899
|
-
const indexFile =
|
|
900
|
-
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
1559
|
+
const indexFile = join11(root, "index.md");
|
|
1560
|
+
const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
|
|
901
1561
|
`;
|
|
902
1562
|
if (!dryRun) {
|
|
903
|
-
|
|
904
|
-
const existing = existsSync6(indexFile) ? readFileSync5(indexFile, "utf8") : "";
|
|
1563
|
+
const existing = existsSync8(indexFile) ? readFileSync7(indexFile, "utf8") : "";
|
|
905
1564
|
if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
|
|
906
1565
|
const header = existing ? "" : `# Mission index
|
|
907
1566
|
|
|
908
1567
|
`;
|
|
909
|
-
|
|
1568
|
+
const fd = openSync(indexFile, "a");
|
|
1569
|
+
try {
|
|
1570
|
+
writeSync(fd, header + line);
|
|
1571
|
+
} finally {
|
|
1572
|
+
closeSync(fd);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
index = "index.md";
|
|
1576
|
+
}
|
|
1577
|
+
return { report: join11("missions", mission, "report.md"), removed, kept, index };
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/run.ts
|
|
1581
|
+
import { existsSync as existsSync9, readdirSync as readdirSync7 } from "node:fs";
|
|
1582
|
+
import { spawnSync } from "node:child_process";
|
|
1583
|
+
import { dirname as dirname5, join as join12, resolve } from "node:path";
|
|
1584
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1585
|
+
var here3 = dirname5(fileURLToPath4(import.meta.url));
|
|
1586
|
+
var SCRIPTS_DIR = join12(here3, "..", "scripts");
|
|
1587
|
+
var RUNNABLE = ["savepoint.sh", "lane.sh"];
|
|
1588
|
+
function findBash() {
|
|
1589
|
+
const explicit = process.env.MUGIWARA_BASH?.trim();
|
|
1590
|
+
if (explicit)
|
|
1591
|
+
return existsSync9(explicit) ? explicit : null;
|
|
1592
|
+
const candidates = process.platform === "win32" ? [
|
|
1593
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1594
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1595
|
+
join12(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
|
|
1596
|
+
] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
|
|
1597
|
+
for (const p of candidates) {
|
|
1598
|
+
if (p && existsSync9(p))
|
|
1599
|
+
return p;
|
|
1600
|
+
}
|
|
1601
|
+
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
|
|
1602
|
+
encoding: "utf8"
|
|
1603
|
+
});
|
|
1604
|
+
const first = probe.stdout?.split(/\r?\n/).find((l) => l.trim());
|
|
1605
|
+
return first?.trim() || null;
|
|
1606
|
+
}
|
|
1607
|
+
function noBashMessage() {
|
|
1608
|
+
const remedy = process.platform === "win32" ? "Install Git for Windows (which ships bash)" : "Install bash with your package manager (e.g. `apk add bash`, `apt install bash`)";
|
|
1609
|
+
return `no bash found. ${remedy}, or set MUGIWARA_BASH to a bash executable.`;
|
|
1610
|
+
}
|
|
1611
|
+
function runScript(name, args, projectDir) {
|
|
1612
|
+
if (!/^[a-z0-9-]+\.sh$/.test(name)) {
|
|
1613
|
+
throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
|
|
1614
|
+
}
|
|
1615
|
+
const script = join12(SCRIPTS_DIR, name);
|
|
1616
|
+
if (!existsSync9(script)) {
|
|
1617
|
+
const have = existsSync9(SCRIPTS_DIR) ? readdirSync7(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
|
|
1618
|
+
throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
|
|
1619
|
+
}
|
|
1620
|
+
const bash = findBash();
|
|
1621
|
+
if (!bash)
|
|
1622
|
+
throw new Error(noBashMessage());
|
|
1623
|
+
const r = spawnSync(bash, [script, ...args], {
|
|
1624
|
+
cwd: resolve(projectDir),
|
|
1625
|
+
stdio: "inherit",
|
|
1626
|
+
env: process.env
|
|
1627
|
+
});
|
|
1628
|
+
if (r.error)
|
|
1629
|
+
throw r.error;
|
|
1630
|
+
return r.status ?? 1;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/continue.ts
|
|
1634
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync8 } from "node:fs";
|
|
1635
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
1636
|
+
import { join as join13 } from "node:path";
|
|
1637
|
+
var SAFE = /^[A-Za-z0-9._-]+$/;
|
|
1638
|
+
var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
|
|
1639
|
+
var num = (v) => {
|
|
1640
|
+
const n = Number(v);
|
|
1641
|
+
return Number.isFinite(n) ? n : 0;
|
|
1642
|
+
};
|
|
1643
|
+
var text = (v) => typeof v === "string" ? v : "";
|
|
1644
|
+
function gitActor(cwd) {
|
|
1645
|
+
const env = (process.env.STATE_ACTOR ?? "").trim();
|
|
1646
|
+
if (env)
|
|
1647
|
+
return env;
|
|
1648
|
+
const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
|
|
1649
|
+
if (author)
|
|
1650
|
+
return author;
|
|
1651
|
+
const git3 = (args) => {
|
|
1652
|
+
try {
|
|
1653
|
+
return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1654
|
+
} catch {
|
|
1655
|
+
return "";
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
const name = git3(["config", "user.name"]);
|
|
1659
|
+
const email = git3(["config", "user.email"]);
|
|
1660
|
+
if (name && email)
|
|
1661
|
+
return `${name} <${email}>`;
|
|
1662
|
+
return name || process.env.USER || process.env.USERNAME || "";
|
|
1663
|
+
}
|
|
1664
|
+
function scan(projectDir, kind, map) {
|
|
1665
|
+
const base = join13(projectDir, ".mugiwara", "missions");
|
|
1666
|
+
if (!existsSync10(base))
|
|
1667
|
+
return [];
|
|
1668
|
+
const out = [];
|
|
1669
|
+
const missions = readdirSync8(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
|
|
1670
|
+
for (const mission of missions) {
|
|
1671
|
+
const dir = join13(base, mission);
|
|
1672
|
+
for (const f of readdirSync8(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
1673
|
+
const stem = f.slice(0, -".json".length);
|
|
1674
|
+
let member;
|
|
1675
|
+
if (kind === "state") {
|
|
1676
|
+
if (stem === "continue" || stem.startsWith("continue-"))
|
|
1677
|
+
continue;
|
|
1678
|
+
member = stem === "state" ? null : stem;
|
|
1679
|
+
} else {
|
|
1680
|
+
if (stem === "continue")
|
|
1681
|
+
member = null;
|
|
1682
|
+
else if (stem.startsWith("continue-"))
|
|
1683
|
+
member = stem.slice("continue-".length);
|
|
1684
|
+
else
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (member !== null && !isSafeKey(member))
|
|
1688
|
+
continue;
|
|
1689
|
+
try {
|
|
1690
|
+
const raw = JSON.parse(readFileSync8(join13(dir, f), "utf8"));
|
|
1691
|
+
if (text(raw.mission) !== mission)
|
|
1692
|
+
continue;
|
|
1693
|
+
out.push(map(raw, member));
|
|
1694
|
+
} catch {}
|
|
910
1695
|
}
|
|
911
|
-
index = join7("reports", "index.md");
|
|
912
1696
|
}
|
|
913
|
-
return
|
|
1697
|
+
return out;
|
|
1698
|
+
}
|
|
1699
|
+
function readContinue(projectDir) {
|
|
1700
|
+
return scan(projectDir, "continue", (r, member) => ({
|
|
1701
|
+
mission: text(r.mission),
|
|
1702
|
+
member,
|
|
1703
|
+
actor: text(r.actor),
|
|
1704
|
+
branch: text(r.branch),
|
|
1705
|
+
flow: num(r.flow ?? r.wave),
|
|
1706
|
+
mode: text(r.mode) || "guided",
|
|
1707
|
+
tasks_done: num(r.tasks_done),
|
|
1708
|
+
tasks_total: num(r.tasks_total),
|
|
1709
|
+
lane: text(r.lane) || "direct",
|
|
1710
|
+
next_action: text(r.next_action),
|
|
1711
|
+
next_session_prompt: text(r.next_session_prompt),
|
|
1712
|
+
updated_at: text(r.updated_at)
|
|
1713
|
+
}));
|
|
1714
|
+
}
|
|
1715
|
+
function readState(projectDir) {
|
|
1716
|
+
return scan(projectDir, "state", (r, member) => {
|
|
1717
|
+
const tasks = r.tasks ?? {};
|
|
1718
|
+
return {
|
|
1719
|
+
mission: text(r.mission),
|
|
1720
|
+
member,
|
|
1721
|
+
actor: text(r.actor),
|
|
1722
|
+
branch: text(r.branch),
|
|
1723
|
+
flow: num(r.flow ?? r.wave),
|
|
1724
|
+
mode: text(r.mode) || "guided",
|
|
1725
|
+
tasks_done: num(tasks.done),
|
|
1726
|
+
tasks_total: num(tasks.total),
|
|
1727
|
+
lane: text(r.lane) || "direct",
|
|
1728
|
+
lane_reason: text(r.lane_reason),
|
|
1729
|
+
lane_rose: r.lane_rose === true,
|
|
1730
|
+
lane_prev: text(r.lane_prev),
|
|
1731
|
+
lane_peak: text(r.lane_peak),
|
|
1732
|
+
base_sha: text(r.base_sha),
|
|
1733
|
+
sensitive_paths: Array.isArray(r.sensitive_paths) ? r.sensitive_paths.map(text).filter(Boolean) : [],
|
|
1734
|
+
next_action: text(r.next_action),
|
|
1735
|
+
next_session_prompt: text(r.next_session_prompt),
|
|
1736
|
+
updated_at: text(r.updated_at),
|
|
1737
|
+
blockers_open: num(r.blockers_open),
|
|
1738
|
+
heal_cycle: num(r.heal_cycle),
|
|
1739
|
+
heal_max_cycles: num(r.heal_max_cycles) || 3,
|
|
1740
|
+
heal_halt: r.heal_halt === true,
|
|
1741
|
+
delegate_threshold: num(r.delegate_threshold) || 60,
|
|
1742
|
+
delegate_due: r.delegate_due === true,
|
|
1743
|
+
tokens_est: num(r.tokens_est),
|
|
1744
|
+
budget: num(r.budget),
|
|
1745
|
+
budget_status: text(r.budget_status) || "ok",
|
|
1746
|
+
files_touched: num(r.files_touched),
|
|
1747
|
+
evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : []
|
|
1748
|
+
};
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
function resolveContinue(entries, mission, member) {
|
|
1752
|
+
if (!entries.length)
|
|
1753
|
+
return { kind: "none" };
|
|
1754
|
+
if (!mission) {
|
|
1755
|
+
const missions = [...new Set(entries.map((e) => e.mission))];
|
|
1756
|
+
if (missions.length === 1)
|
|
1757
|
+
return resolveContinue(entries, missions[0], member);
|
|
1758
|
+
return { kind: "missions", entries };
|
|
1759
|
+
}
|
|
1760
|
+
const inMission = entries.filter((e) => e.mission === mission);
|
|
1761
|
+
if (!inMission.length) {
|
|
1762
|
+
return { kind: "unknown-mission", mission, known: [...new Set(entries.map((e) => e.mission))] };
|
|
1763
|
+
}
|
|
1764
|
+
if (member) {
|
|
1765
|
+
const hit = inMission.find((e) => e.member === member);
|
|
1766
|
+
if (!hit) {
|
|
1767
|
+
return {
|
|
1768
|
+
kind: "unknown-member",
|
|
1769
|
+
mission,
|
|
1770
|
+
member,
|
|
1771
|
+
known: inMission.map((e) => e.member ?? "(solo)")
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
return { kind: "resume", entry: hit };
|
|
1775
|
+
}
|
|
1776
|
+
const solo = inMission.find((e) => e.member === null);
|
|
1777
|
+
if (solo && inMission.length === 1)
|
|
1778
|
+
return { kind: "resume", entry: solo };
|
|
1779
|
+
return { kind: "members", mission, entries: inMission };
|
|
1780
|
+
}
|
|
1781
|
+
function pad(s, n) {
|
|
1782
|
+
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
1783
|
+
}
|
|
1784
|
+
function formatTable(entries) {
|
|
1785
|
+
const rows = entries.map((e) => [
|
|
1786
|
+
e.mission,
|
|
1787
|
+
e.member ?? "—",
|
|
1788
|
+
String(e.flow),
|
|
1789
|
+
`${e.tasks_done}/${e.tasks_total}`,
|
|
1790
|
+
e.lane,
|
|
1791
|
+
e.mode
|
|
1792
|
+
]);
|
|
1793
|
+
const head = ["MISSION", "MEMBER", "FLOW", "TASKS", "LANE", "MODE"];
|
|
1794
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1795
|
+
const line = (cells) => " " + cells.map((c, i) => pad(c, widths[i])).join(" ").trimEnd();
|
|
1796
|
+
return [line(head), ...rows.map(line)].join(`
|
|
1797
|
+
`);
|
|
1798
|
+
}
|
|
1799
|
+
function formatResume(e) {
|
|
1800
|
+
const scope = e.member ? ` [${e.member}]` : "";
|
|
1801
|
+
const next = e.next_session_prompt || "(no next_session_prompt recorded)";
|
|
1802
|
+
return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// src/sign.ts
|
|
1806
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1807
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
1808
|
+
import { homedir as homedir3 } from "node:os";
|
|
1809
|
+
import { join as join14 } from "node:path";
|
|
1810
|
+
function signArgs(reportPath, secretKey) {
|
|
1811
|
+
return ["-Sm", reportPath, "-s", secretKey];
|
|
1812
|
+
}
|
|
1813
|
+
function verifyArgs(reportPath, pubKey) {
|
|
1814
|
+
return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
|
|
1815
|
+
}
|
|
1816
|
+
function hasMinisign() {
|
|
1817
|
+
try {
|
|
1818
|
+
execFileSync5("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
1819
|
+
return true;
|
|
1820
|
+
} catch {
|
|
1821
|
+
return false;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function defaultKey(flag) {
|
|
1825
|
+
return join14(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
|
|
1826
|
+
}
|
|
1827
|
+
function signReport(projectDir, missionDir) {
|
|
1828
|
+
const report = join14(missionDir, "report.md");
|
|
1829
|
+
if (!existsSync11(report))
|
|
1830
|
+
return { ok: false, message: "no report.md to sign — archive first" };
|
|
1831
|
+
if (!hasMinisign()) {
|
|
1832
|
+
return { ok: false, message: "minisign not installed — signing skipped (install minisign to enable attestation)" };
|
|
1833
|
+
}
|
|
1834
|
+
const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
|
|
1835
|
+
try {
|
|
1836
|
+
execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
|
|
1837
|
+
return { ok: true, message: `signed ${report}.minisig (key: ${secretKey})` };
|
|
1838
|
+
} catch (e) {
|
|
1839
|
+
return { ok: false, message: `signing failed: ${e.message}` };
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
function verifyReport(projectDir, missionDir) {
|
|
1843
|
+
const report = join14(missionDir, "report.md");
|
|
1844
|
+
const sig = `${report}.minisig`;
|
|
1845
|
+
if (!existsSync11(sig))
|
|
1846
|
+
return { ok: false, message: "not signed (no .minisig beside report.md)" };
|
|
1847
|
+
if (!hasMinisign())
|
|
1848
|
+
return { ok: false, message: "minisign not installed — cannot verify" };
|
|
1849
|
+
const pubKey = existsSync11(defaultKey("public")) ? defaultKey("public") : null;
|
|
1850
|
+
try {
|
|
1851
|
+
execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
|
|
1852
|
+
return { ok: true, message: "signature verifies against report.md" };
|
|
1853
|
+
} catch {
|
|
1854
|
+
return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing" };
|
|
1855
|
+
}
|
|
914
1856
|
}
|
|
915
1857
|
|
|
916
1858
|
// src/cli.ts
|
|
@@ -937,12 +1879,28 @@ async function run(argv) {
|
|
|
937
1879
|
return resetCmd(flags);
|
|
938
1880
|
case "archive":
|
|
939
1881
|
return archive(flags, _);
|
|
1882
|
+
case "clean":
|
|
1883
|
+
return cleanCmd(flags);
|
|
1884
|
+
case "continue":
|
|
1885
|
+
return continueCmd(flags, _);
|
|
1886
|
+
case "status":
|
|
1887
|
+
return statusCmd(flags);
|
|
1888
|
+
case "run":
|
|
1889
|
+
return runCmd(flags, _);
|
|
1890
|
+
case "savepoint":
|
|
1891
|
+
return runCmd(flags, ["run", "savepoint.sh", ..._.slice(1)]);
|
|
1892
|
+
case "blame":
|
|
1893
|
+
return blameCmd(flags, _);
|
|
1894
|
+
case "handoff":
|
|
1895
|
+
return handoffCmd(flags, _);
|
|
1896
|
+
case "sign":
|
|
1897
|
+
return signCmd(flags, _);
|
|
940
1898
|
default:
|
|
941
1899
|
throw new Error(`Unknown command: ${command}`);
|
|
942
1900
|
}
|
|
943
1901
|
}
|
|
944
1902
|
function resetCmd(flags) {
|
|
945
|
-
const projectDir =
|
|
1903
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
946
1904
|
const force = flag(flags.force);
|
|
947
1905
|
const result = resetMission(projectDir, flag(flags.keepLogs), force);
|
|
948
1906
|
if (result.blocked) {
|
|
@@ -957,7 +1915,7 @@ function resetCmd(flags) {
|
|
|
957
1915
|
console.log(`kept: ${result.kept.join(", ")}`);
|
|
958
1916
|
}
|
|
959
1917
|
function archive(flags, positionals) {
|
|
960
|
-
const projectDir =
|
|
1918
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
961
1919
|
const mission = positionals[1];
|
|
962
1920
|
if (!mission) {
|
|
963
1921
|
console.error("usage: mugiwara archive <mission> [--project <dir>] [--dry-run]");
|
|
@@ -966,6 +1924,8 @@ function archive(flags, positionals) {
|
|
|
966
1924
|
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
967
1925
|
if (result.report)
|
|
968
1926
|
console.log(`archive target: ${result.report}`);
|
|
1927
|
+
else
|
|
1928
|
+
console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
|
|
969
1929
|
if (result.removed.length)
|
|
970
1930
|
console.log(`${flag(flags.dryRun) ? "would remove" : "removed"}: ${result.removed.join(", ")}`);
|
|
971
1931
|
if (result.kept.length)
|
|
@@ -973,6 +1933,60 @@ function archive(flags, positionals) {
|
|
|
973
1933
|
if (result.index)
|
|
974
1934
|
console.log(`index updated: ${result.index}`);
|
|
975
1935
|
}
|
|
1936
|
+
function cleanCmd(flags) {
|
|
1937
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1938
|
+
const dryRun = flag(flags.dryRun);
|
|
1939
|
+
const root = join15(projectDir, ".mugiwara", "missions");
|
|
1940
|
+
if (!existsSync12(root)) {
|
|
1941
|
+
console.log("nothing to clean (.mugiwara/missions/ does not exist).");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const before = str(flags.before);
|
|
1945
|
+
const beforeMs = before ? Date.parse(before) : NaN;
|
|
1946
|
+
if (before && !Number.isFinite(beforeMs)) {
|
|
1947
|
+
console.error(`invalid --before date: ${before}`);
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
let candidates = readdirSync9(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
|
|
1951
|
+
const stateFiles = (m) => readdirSync9(join15(root, m)).filter((f) => {
|
|
1952
|
+
const stem = f.replace(/\.json$/, "");
|
|
1953
|
+
return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
|
|
1954
|
+
});
|
|
1955
|
+
const hasLiveState = (m) => stateFiles(m).length > 0;
|
|
1956
|
+
const staleBefore = (m) => {
|
|
1957
|
+
if (!Number.isFinite(beforeMs))
|
|
1958
|
+
return false;
|
|
1959
|
+
for (const f of stateFiles(m)) {
|
|
1960
|
+
try {
|
|
1961
|
+
const ts = Date.parse(JSON.parse(readFileSync9(join15(root, m, f), "utf8")).updated_at ?? "") || 0;
|
|
1962
|
+
if (ts === 0 || ts >= beforeMs)
|
|
1963
|
+
return false;
|
|
1964
|
+
} catch {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
return true;
|
|
1969
|
+
};
|
|
1970
|
+
if (!flag(flags.all)) {
|
|
1971
|
+
candidates = candidates.filter((m) => existsSync12(join15(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
|
|
1972
|
+
} else if (!flag(flags.force)) {
|
|
1973
|
+
const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
|
|
1974
|
+
if (live.length) {
|
|
1975
|
+
console.error(`✗ in-flight mission(s): ${live.join(", ")}. Use --force to archive them anyway.`);
|
|
1976
|
+
process.exit(1);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (!candidates.length) {
|
|
1980
|
+
console.log("nothing to clean.");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
for (const m of candidates) {
|
|
1984
|
+
const r = archiveMission(projectDir, m, { dryRun });
|
|
1985
|
+
console.log(`${dryRun ? "would clean" : "cleaned"} ${m}${r.report ? ` → ${r.report}` : ""}`);
|
|
1986
|
+
if (r.index)
|
|
1987
|
+
console.log(`index updated: ${r.index}`);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
976
1990
|
async function resolveOptions(flags) {
|
|
977
1991
|
const interactive = !flag(flags.yes);
|
|
978
1992
|
if (interactive && !process.stdin.isTTY) {
|
|
@@ -987,8 +2001,8 @@ async function resolveOptions(flags) {
|
|
|
987
2001
|
} else
|
|
988
2002
|
scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
|
|
989
2003
|
}
|
|
990
|
-
const projectDir =
|
|
991
|
-
if (scope === "project" && !
|
|
2004
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2005
|
+
if (scope === "project" && !existsSync12(projectDir))
|
|
992
2006
|
throw new Error(`Project dir not found: ${projectDir}`);
|
|
993
2007
|
let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
|
|
994
2008
|
if (targetIds && targetIds.includes("all"))
|
|
@@ -1013,7 +2027,7 @@ async function resolveOptions(flags) {
|
|
|
1013
2027
|
}
|
|
1014
2028
|
async function install(flags) {
|
|
1015
2029
|
const { scope, projectDir, targetIds } = await resolveOptions(flags);
|
|
1016
|
-
const home =
|
|
2030
|
+
const home = homedir4();
|
|
1017
2031
|
const allFiles = [];
|
|
1018
2032
|
const allNotes = [];
|
|
1019
2033
|
const installed = [];
|
|
@@ -1055,17 +2069,19 @@ Dry run — nothing written.`);
|
|
|
1055
2069
|
OK mugiwara ${VERSION} installed (manifest: ${file})`);
|
|
1056
2070
|
if (allNotes.length)
|
|
1057
2071
|
console.log(`${allNotes.length} note(s) above may need attention.`);
|
|
2072
|
+
console.log(`
|
|
2073
|
+
Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
|
|
1058
2074
|
}
|
|
1059
2075
|
async function uninstall(flags) {
|
|
1060
2076
|
const scope = flag(flags.global) ? "global" : "project";
|
|
1061
|
-
const projectDir =
|
|
1062
|
-
const home =
|
|
2077
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2078
|
+
const home = homedir4();
|
|
1063
2079
|
const file = manifestPath({ scope, projectDir, home });
|
|
1064
2080
|
const manifest = readManifest(file);
|
|
1065
2081
|
if (!manifest) {
|
|
1066
2082
|
if (scope === "project") {
|
|
1067
2083
|
const globalFile = manifestPath({ scope: "global", projectDir, home });
|
|
1068
|
-
if (
|
|
2084
|
+
if (existsSync12(globalFile)) {
|
|
1069
2085
|
console.log(`No project manifest found, but a global install exists. Try:
|
|
1070
2086
|
mugiwara uninstall --global`);
|
|
1071
2087
|
return;
|
|
@@ -1085,6 +2101,16 @@ async function uninstall(flags) {
|
|
|
1085
2101
|
}
|
|
1086
2102
|
}
|
|
1087
2103
|
const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
|
|
2104
|
+
for (const id of manifest.targets) {
|
|
2105
|
+
const t = targets[id];
|
|
2106
|
+
if (!t?.postUninstall)
|
|
2107
|
+
continue;
|
|
2108
|
+
const post = t.postUninstall({ scope, projectDir, home, dryRun: flag(flags.dryRun) });
|
|
2109
|
+
for (const f of post.changed)
|
|
2110
|
+
console.log(` unwired mugiwara hooks from ${f}`);
|
|
2111
|
+
for (const n of post.notes)
|
|
2112
|
+
console.log(` note: ${n}`);
|
|
2113
|
+
}
|
|
1088
2114
|
if (!flag(flags.dryRun)) {
|
|
1089
2115
|
if (scope === "project") {
|
|
1090
2116
|
const gi = removeProjectGitignore(projectDir);
|
|
@@ -1092,13 +2118,13 @@ async function uninstall(flags) {
|
|
|
1092
2118
|
console.log(` note: ${n}`);
|
|
1093
2119
|
}
|
|
1094
2120
|
rmSync3(file);
|
|
1095
|
-
const mugiDir =
|
|
1096
|
-
if (
|
|
2121
|
+
const mugiDir = join15(scope === "global" ? home : projectDir, ".mugiwara");
|
|
2122
|
+
if (existsSync12(mugiDir) && readdirSync9(mugiDir).length === 0) {
|
|
1097
2123
|
rmSync3(mugiDir, { recursive: true, force: true });
|
|
1098
2124
|
}
|
|
1099
2125
|
if (manifest.targets.includes("opencode")) {
|
|
1100
|
-
const opencodeCache =
|
|
1101
|
-
if (
|
|
2126
|
+
const opencodeCache = join15(home, ".cache", "opencode", "packages", "@ionivetech");
|
|
2127
|
+
if (existsSync12(opencodeCache)) {
|
|
1102
2128
|
rmSync3(opencodeCache, { recursive: true, force: true });
|
|
1103
2129
|
console.log(" cleared opencode npm cache (stale plugin versions)");
|
|
1104
2130
|
}
|
|
@@ -1107,8 +2133,8 @@ async function uninstall(flags) {
|
|
|
1107
2133
|
console.log(`OK removed ${removed.length} files`);
|
|
1108
2134
|
}
|
|
1109
2135
|
function list(flags) {
|
|
1110
|
-
const home =
|
|
1111
|
-
const projectDir =
|
|
2136
|
+
const home = homedir4();
|
|
2137
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
1112
2138
|
let found = false;
|
|
1113
2139
|
for (const [label, file] of [
|
|
1114
2140
|
["project", manifestPath({ scope: "project", projectDir, home })],
|
|
@@ -1119,7 +2145,7 @@ function list(flags) {
|
|
|
1119
2145
|
continue;
|
|
1120
2146
|
found = true;
|
|
1121
2147
|
if (flag(flags.check)) {
|
|
1122
|
-
const missing = m.files.filter((f) => !
|
|
2148
|
+
const missing = m.files.filter((f) => !existsSync12(f));
|
|
1123
2149
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
|
|
1124
2150
|
} else {
|
|
1125
2151
|
console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
|
|
@@ -1128,6 +2154,178 @@ function list(flags) {
|
|
|
1128
2154
|
if (!found)
|
|
1129
2155
|
console.log("No mugiwara installation found.");
|
|
1130
2156
|
}
|
|
2157
|
+
function continueCmd(flags, positionals) {
|
|
2158
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2159
|
+
const [mission, member] = positionals.slice(1);
|
|
2160
|
+
let entries = readContinue(projectDir);
|
|
2161
|
+
if (!flag(flags.all)) {
|
|
2162
|
+
const actor = gitActor(projectDir);
|
|
2163
|
+
const mine = entries.filter((e) => e.actor === actor);
|
|
2164
|
+
if (mine.length)
|
|
2165
|
+
entries = mine;
|
|
2166
|
+
}
|
|
2167
|
+
const r = resolveContinue(entries, mission, member);
|
|
2168
|
+
if (r.kind === "resume") {
|
|
2169
|
+
console.log(formatResume(r.entry));
|
|
2170
|
+
const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
|
|
2171
|
+
const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
|
|
2172
|
+
if (stale)
|
|
2173
|
+
console.log(stale);
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
if (r.kind === "none") {
|
|
2177
|
+
console.log("No mission in flight. Start one with Flow 0 triage (mugiwara-orchestration).");
|
|
2178
|
+
} else if (r.kind === "missions") {
|
|
2179
|
+
console.log(`${new Set(r.entries.map((e) => e.mission)).size} missions in flight:
|
|
2180
|
+
`);
|
|
2181
|
+
console.log(formatTable(r.entries));
|
|
2182
|
+
console.log(`
|
|
2183
|
+
Pick one: mugiwara continue <mission> [member]`);
|
|
2184
|
+
} else if (r.kind === "members") {
|
|
2185
|
+
console.log(`Mission "${r.mission}" has ${r.entries.length} members in flight:
|
|
2186
|
+
`);
|
|
2187
|
+
console.log(formatTable(r.entries));
|
|
2188
|
+
console.log(`
|
|
2189
|
+
Pick one: mugiwara continue ${r.mission} <member>`);
|
|
2190
|
+
} else if (r.kind === "unknown-mission") {
|
|
2191
|
+
console.error(`No in-flight mission "${r.mission}". Known: ${r.known.join(", ") || "(none)"}`);
|
|
2192
|
+
} else {
|
|
2193
|
+
console.error(`Mission "${r.mission}" has no member "${r.member}". Known: ${r.known.join(", ")}`);
|
|
2194
|
+
}
|
|
2195
|
+
process.exit(2);
|
|
2196
|
+
}
|
|
2197
|
+
function statusCmd(flags) {
|
|
2198
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2199
|
+
const states = readState(projectDir);
|
|
2200
|
+
if (!states.length) {
|
|
2201
|
+
console.log("No mission state on disk.");
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const actor = flag(flags.all) ? null : gitActor(projectDir);
|
|
2205
|
+
const rows = actor ? states.filter((s) => s.actor === actor).length ? states.filter((s) => s.actor === actor) : states : states;
|
|
2206
|
+
for (const s of rows) {
|
|
2207
|
+
const scope = s.member ? ` [${s.member}]` : "";
|
|
2208
|
+
console.log(`${s.mission}${scope}`);
|
|
2209
|
+
console.log(` flow ${s.flow} · ${s.tasks_done}/${s.tasks_total} tasks · lane ${s.lane}${s.lane_rose ? " ⬆ ROSE" : ""}${s.lane_reason ? ` (${s.lane_reason})` : ""} · mode ${s.mode}`);
|
|
2210
|
+
console.log(` blockers ${s.blockers_open} · heal cycle ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? " — HALT" : ""} · files touched ${s.files_touched}`);
|
|
2211
|
+
if (s.budget)
|
|
2212
|
+
console.log(` tokens ${s.tokens_est}/${s.budget} (${s.budget_status})${s.delegate_due ? " · delegate due" : ""}`);
|
|
2213
|
+
console.log(` branch ${s.branch} · updated ${s.updated_at}`);
|
|
2214
|
+
if (s.evidence.length)
|
|
2215
|
+
console.log(` evidence: ${s.evidence.join(", ")}`);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
function runCmd(flags, positionals) {
|
|
2219
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2220
|
+
const name = positionals[1];
|
|
2221
|
+
if (!name) {
|
|
2222
|
+
console.error(`usage: mugiwara run <script> [args...]
|
|
2223
|
+
scripts: ${RUNNABLE.join(", ")}`);
|
|
2224
|
+
process.exit(1);
|
|
2225
|
+
}
|
|
2226
|
+
const code = runScript(name, positionals.slice(2), projectDir);
|
|
2227
|
+
if (code !== 0)
|
|
2228
|
+
process.exit(code);
|
|
2229
|
+
}
|
|
2230
|
+
function blameCmd(flags, positionals) {
|
|
2231
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2232
|
+
const path = positionals[1];
|
|
2233
|
+
if (!path) {
|
|
2234
|
+
console.error("usage: mugiwara blame <file-path>");
|
|
2235
|
+
process.exit(1);
|
|
2236
|
+
}
|
|
2237
|
+
console.log(blamePath(projectDir, path));
|
|
2238
|
+
}
|
|
2239
|
+
function stalenessLine(projectDir, baseSha) {
|
|
2240
|
+
if (!baseSha || baseSha === "unknown")
|
|
2241
|
+
return null;
|
|
2242
|
+
const git3 = (args) => {
|
|
2243
|
+
try {
|
|
2244
|
+
return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2245
|
+
} catch {
|
|
2246
|
+
return "";
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
let main = "";
|
|
2250
|
+
for (const ref of ["main", "master"]) {
|
|
2251
|
+
main = git3(["rev-parse", "--verify", ref]);
|
|
2252
|
+
if (main)
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
if (!main)
|
|
2256
|
+
return null;
|
|
2257
|
+
try {
|
|
2258
|
+
const behind = Number(git3(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
|
|
2259
|
+
return behind > 0 ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing` : null;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return null;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function handoffCmd(flags, positionals) {
|
|
2265
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2266
|
+
const mission = positionals[1];
|
|
2267
|
+
if (!mission) {
|
|
2268
|
+
console.error("usage: mugiwara handoff <mission> [--project <dir>]");
|
|
2269
|
+
process.exit(1);
|
|
2270
|
+
}
|
|
2271
|
+
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
2272
|
+
if (!states.length) {
|
|
2273
|
+
console.error(`no in-flight mission "${mission}"`);
|
|
2274
|
+
process.exit(1);
|
|
2275
|
+
}
|
|
2276
|
+
const lines = [
|
|
2277
|
+
`# Handoff: ${mission}`,
|
|
2278
|
+
"",
|
|
2279
|
+
`Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
|
|
2280
|
+
"",
|
|
2281
|
+
"| | |",
|
|
2282
|
+
"|---|---|"
|
|
2283
|
+
];
|
|
2284
|
+
for (const s of states) {
|
|
2285
|
+
const scope = s.member ? ` [${s.member}]` : "";
|
|
2286
|
+
lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? " (rose)" : ""}, mode ${s.mode} |`);
|
|
2287
|
+
lines.push(`| Branch | \`${s.branch}\` |`);
|
|
2288
|
+
lines.push(`| Actor | ${s.actor || "(unknown)"} |`);
|
|
2289
|
+
if (s.next_action)
|
|
2290
|
+
lines.push(`| Next action | ${s.next_action} |`);
|
|
2291
|
+
if (s.blockers_open)
|
|
2292
|
+
lines.push(`| Open blockers | ${s.blockers_open} |`);
|
|
2293
|
+
if (s.heal_cycle)
|
|
2294
|
+
lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? " — HALTED" : ""} |`);
|
|
2295
|
+
if (s.evidence.length)
|
|
2296
|
+
lines.push(`| Evidence | ${s.evidence.join(", ")} |`);
|
|
2297
|
+
const stale = stalenessLine(projectDir, s.base_sha);
|
|
2298
|
+
if (stale)
|
|
2299
|
+
lines.push(`| Staleness | ${stale.replace("⚠ stale base: ", "")} |`);
|
|
2300
|
+
}
|
|
2301
|
+
lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
|
|
2302
|
+
lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
|
|
2303
|
+
const out = join15(".mugiwara", "missions", mission, "handoff.md");
|
|
2304
|
+
writeFileSync8(resolve2(projectDir, out), lines.join(`
|
|
2305
|
+
`) + `
|
|
2306
|
+
`);
|
|
2307
|
+
console.log(lines.join(`
|
|
2308
|
+
`));
|
|
2309
|
+
console.log(`
|
|
2310
|
+
written: ${out}`);
|
|
2311
|
+
}
|
|
2312
|
+
function signCmd(flags, _) {
|
|
2313
|
+
const projectDir = resolve2(str(flags.project) ?? process.cwd());
|
|
2314
|
+
const mission = _[1];
|
|
2315
|
+
if (!mission) {
|
|
2316
|
+
console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
|
|
2317
|
+
process.exit(1);
|
|
2318
|
+
}
|
|
2319
|
+
const missionDir = join15(projectDir, ".mugiwara", "missions", mission);
|
|
2320
|
+
if (!existsSync12(missionDir)) {
|
|
2321
|
+
console.error(`no mission dir: ${missionDir}`);
|
|
2322
|
+
process.exit(1);
|
|
2323
|
+
}
|
|
2324
|
+
const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
|
|
2325
|
+
console.log(`${r.ok ? "✓" : "✗"} ${r.message}`);
|
|
2326
|
+
if (!r.ok)
|
|
2327
|
+
process.exit(1);
|
|
2328
|
+
}
|
|
1131
2329
|
function help() {
|
|
1132
2330
|
console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
|
|
1133
2331
|
|
|
@@ -1137,8 +2335,23 @@ Usage:
|
|
|
1137
2335
|
mugiwara uninstall remove installed files via manifest
|
|
1138
2336
|
mugiwara list show installations
|
|
1139
2337
|
mugiwara list --check health check: show installations + missing files
|
|
1140
|
-
mugiwara reset wipe mission state (
|
|
1141
|
-
mugiwara archive <m>
|
|
2338
|
+
mugiwara reset wipe mission state (missions/ + legacy dirs)
|
|
2339
|
+
mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
|
|
2340
|
+
mugiwara clean [--all] [--before <date>]
|
|
2341
|
+
batch-archive every closed mission (report.md present, no live state)
|
|
2342
|
+
mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
|
|
2343
|
+
mugiwara continue <m> [member]
|
|
2344
|
+
print the exact resume point for that mission/member
|
|
2345
|
+
mugiwara status computed mission state: wave, tasks, lane, blockers, budget
|
|
2346
|
+
mugiwara blame <path> provenance note on the last commit touching <path>
|
|
2347
|
+
(fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
|
|
2348
|
+
mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
|
|
2349
|
+
engineer can act on (computed state + staleness check)
|
|
2350
|
+
mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
|
|
2351
|
+
mugiwara run <script> [args...]
|
|
2352
|
+
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
2353
|
+
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
2354
|
+
shorthand for: mugiwara run savepoint.sh ...
|
|
1142
2355
|
mugiwara --help this help
|
|
1143
2356
|
mugiwara --version print version
|
|
1144
2357
|
|
|
@@ -1150,9 +2363,12 @@ Flags:
|
|
|
1150
2363
|
--force overwrite differing files (with backup)
|
|
1151
2364
|
--dry-run print actions without writing
|
|
1152
2365
|
--check with list: report missing files (health check)
|
|
1153
|
-
|
|
2366
|
+
--all with continue/status: every actor; with clean: include in-flight missions
|
|
2367
|
+
--force with clean --all: archive in-flight missions anyway
|
|
2368
|
+
--before <date> with clean: also archive missions untouched since this date
|
|
2369
|
+
--keep-logs with reset: keep lessons.md (lessons ledger survives)`);
|
|
1154
2370
|
}
|
|
1155
|
-
var isMain = process.argv[1] !== undefined &&
|
|
2371
|
+
var isMain = process.argv[1] !== undefined && fileURLToPath5(import.meta.url) === resolve2(process.argv[1]);
|
|
1156
2372
|
if (isMain) {
|
|
1157
2373
|
run(process.argv.slice(2)).catch((err) => {
|
|
1158
2374
|
console.error(`mugiwara: ${err.message}`);
|
|
@@ -1160,5 +2376,6 @@ if (isMain) {
|
|
|
1160
2376
|
});
|
|
1161
2377
|
}
|
|
1162
2378
|
export {
|
|
1163
|
-
run
|
|
2379
|
+
run,
|
|
2380
|
+
stalenessLine
|
|
1164
2381
|
};
|