@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.
Files changed (135) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -9
  3. package/.codex-plugin/plugin.json +2 -8
  4. package/.cursor-plugin/plugin.json +2 -8
  5. package/.kimi-plugin/plugin.json +2 -8
  6. package/.opencode/commands/mugiwara-continue.md +44 -31
  7. package/.opencode/commands/mugiwara-review.md +1 -1
  8. package/.opencode/commands/mugiwara-security.md +1 -1
  9. package/.opencode/commands/mugiwara.md +2 -2
  10. package/.opencode/plugins/mugiwara.mjs +3 -19
  11. package/AGENTS.md +6 -6
  12. package/GEMINI.md +3 -3
  13. package/README.md +82 -74
  14. package/content/agents/brook-healing.md +12 -11
  15. package/content/agents/chopper-checkpoint.md +14 -13
  16. package/content/agents/eval-runner.md +6 -9
  17. package/content/agents/franky-gates.md +7 -10
  18. package/content/agents/jinbe-security.md +10 -14
  19. package/content/agents/luffy-orchestrator.md +20 -22
  20. package/content/agents/memory-keeper.md +9 -12
  21. package/content/agents/nami-planner.md +7 -10
  22. package/content/agents/resume-coordinator.md +12 -15
  23. package/content/agents/robin-reviewer.md +13 -12
  24. package/content/agents/sanji-quality.md +9 -12
  25. package/content/agents/skeptic-verifier.md +7 -10
  26. package/content/agents/usopp-brainstorm.md +6 -9
  27. package/content/agents/zoro-execution.md +9 -12
  28. package/content/skills/mugiwara-backend/SKILL.md +4 -0
  29. package/content/skills/mugiwara-backend/references/database.md +61 -0
  30. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  31. package/content/skills/mugiwara-checkpoint/SKILL.md +17 -14
  32. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +5 -5
  33. package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
  34. package/content/skills/mugiwara-execution/SKILL.md +24 -24
  35. package/content/skills/mugiwara-execution/references/dispatch.md +3 -3
  36. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  37. package/content/skills/mugiwara-frontend/SKILL.md +5 -6
  38. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  39. package/content/skills/mugiwara-gates/SKILL.md +14 -10
  40. package/content/skills/mugiwara-git/SKILL.md +7 -2
  41. package/content/skills/mugiwara-healing/SKILL.md +7 -5
  42. package/content/skills/mugiwara-healing/references/workers.md +2 -2
  43. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  44. package/content/skills/mugiwara-orchestration/SKILL.md +28 -26
  45. package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
  46. package/content/skills/mugiwara-orchestration/references/closure.md +18 -13
  47. package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
  48. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +32 -22
  49. package/content/skills/mugiwara-planning/SKILL.md +5 -18
  50. package/content/skills/mugiwara-planning/references/plan-template.md +3 -3
  51. package/content/skills/mugiwara-quality/SKILL.md +10 -7
  52. package/content/skills/mugiwara-resume/SKILL.md +20 -22
  53. package/content/skills/mugiwara-review/SKILL.md +7 -2
  54. package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
  55. package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
  56. package/content/skills/mugiwara-security/SKILL.md +6 -2
  57. package/content/skills/mugiwara-ship/SKILL.md +2 -2
  58. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  59. package/content/skills/mugiwara-testcases/SKILL.md +5 -3
  60. package/content/skills/mugiwara-workflow/SKILL.md +32 -35
  61. package/content/skills/mugiwara-workflow/references/workspace-layout.md +41 -40
  62. package/dist/mugiwara.js +1370 -153
  63. package/gemini-extension.json +1 -1
  64. package/hooks/auto-savepoint.js +114 -0
  65. package/hooks/auto-savepoint.ts +102 -0
  66. package/hooks/engagement-marker.js +59 -0
  67. package/hooks/engagement-marker.ts +97 -0
  68. package/hooks/hooks.json +41 -2
  69. package/hooks/mugiwara-mode-tracker.js +79 -0
  70. package/hooks/pipeline-guard.js +186 -0
  71. package/hooks/pipeline-guard.ts +270 -0
  72. package/hooks/session-start.js +109 -0
  73. package/hooks/session-start.ts +62 -45
  74. package/package.json +7 -3
  75. package/plugin.json +1 -1
  76. package/references/agent-protocol.md +15 -0
  77. package/references/complexity.md +77 -0
  78. package/references/definition-of-done.md +1 -1
  79. package/references/multi-actor.md +4 -7
  80. package/references/prose-style.md +54 -0
  81. package/references/skill-versioning.md +1 -1
  82. package/references/wave-banners.md +16 -16
  83. package/scripts/build-hooks.ts +56 -0
  84. package/scripts/check-doc-links.ts +40 -0
  85. package/scripts/conformance.ts +23 -16
  86. package/scripts/coverage-gate.ts +193 -0
  87. package/scripts/gate-selftest.ts +118 -7
  88. package/scripts/install.ps1 +5 -4
  89. package/scripts/install.sh +4 -4
  90. package/scripts/lane.sh +56 -6
  91. package/scripts/lib/patterns.sh +89 -0
  92. package/scripts/policy-force.ts +22 -0
  93. package/scripts/retrieval-eval.ts +1 -1
  94. package/scripts/savepoint.sh +249 -67
  95. package/scripts/validate-content.ts +22 -9
  96. package/scripts/verify-install.ts +156 -44
  97. package/src/args.ts +2 -2
  98. package/src/budget.ts +56 -0
  99. package/src/cli.ts +272 -4
  100. package/src/continue.ts +262 -0
  101. package/src/installer.ts +59 -15
  102. package/src/integrity.ts +158 -0
  103. package/src/mission.ts +228 -95
  104. package/src/policy.ts +156 -0
  105. package/src/provenance.ts +116 -0
  106. package/src/rollback.ts +95 -0
  107. package/src/routing.ts +69 -0
  108. package/src/run.ts +82 -0
  109. package/src/sign.ts +61 -0
  110. package/src/targets/claude.ts +140 -2
  111. package/src/targets/copilot.ts +20 -1
  112. package/src/targets/generic.ts +9 -4
  113. package/src/targets/opencode.ts +0 -1
  114. package/.opencode/commands/mugiwara-execute.md +0 -13
  115. package/.opencode/commands/mugiwara-heal.md +0 -13
  116. package/.opencode/commands/mugiwara-onboard.md +0 -15
  117. package/.opencode/commands/mugiwara-plan.md +0 -17
  118. package/.opencode/commands/mugiwara-ship.md +0 -13
  119. package/.opencode/commands/using-mugiwara.md +0 -20
  120. package/content/agents/onboarding-guide.md +0 -124
  121. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  122. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  123. package/content/skills/mugiwara-context-budget/SKILL.md +0 -64
  124. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  125. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  126. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  127. package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
  128. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  129. package/content/skills/using-mugiwara/SKILL.md +0 -51
  130. package/references/token-budget.md +0 -34
  131. package/scripts/evidence.sh +0 -81
  132. package/scripts/initiative.ts +0 -296
  133. package/scripts/mission-report.sh +0 -292
  134. package/scripts/onboard.ts +0 -266
  135. 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 existsSync7, readdirSync as readdirSync5, rmSync as rmSync3 } from "node:fs";
5
- import { homedir as homedir2 } from "node:os";
6
- import { join as join8, resolve } from "node:path";
7
- import { fileURLToPath as fileURLToPath4 } from "node:url";
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 (!f.endsWith(".ts"))
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: "**/*" }, body)
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
- writeFileSync(file, `${bootstrapPointer}
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 writeFileSync2, copyFileSync as copyFileSync3, rmSync, lstatSync } from "node:fs";
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
- writeFileSync2(absPath, text);
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 GITIGNORE_MARKER = "# mugiwara";
652
- var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
653
- # Ignore session state and regenerated files.
654
- .mugiwara/state/
655
- .mugiwara/continue/
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/state/")) {
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(GITIGNORE_MARKER) || existing.includes(GITIGNORE_BLOCK_START);
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
- writeFileSync2(path, existing + separator + GITIGNORE_BLOCK);
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(GITIGNORE_MARKER)) {
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(GITIGNORE_MARKER))
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
- writeFileSync2(path, cleaned);
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 writeFileSync3 } from "node:fs";
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
- writeFileSync3(file, JSON.stringify(data, null, 2) + `
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
- import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync5, readdirSync as readdirSync4, mkdirSync as mkdirSync5, appendFileSync } from "node:fs";
748
- import { join as join7 } from "node:path";
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 stateDir = join7(projectDir, ".mugiwara", "state");
751
- if (!existsSync6(stateDir))
1326
+ const missionsDir = join11(projectDir, ".mugiwara", "missions");
1327
+ if (!existsSync8(missionsDir))
752
1328
  return null;
753
- const missions = readdirSync4(stateDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
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 = join7(stateDir, mission);
757
- for (const f of readdirSync4(d).filter((f2) => f2.endsWith(".json"))) {
1332
+ const d = join11(missionsDir, mission);
1333
+ for (const f of readdirSync6(d).filter(isStateFile)) {
758
1334
  try {
759
- const s = JSON.parse(readFileSync5(join7(d, f), "utf8"));
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 = join7(projectDir, ".mugiwara");
770
- if (!existsSync6(root))
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 = join7(root, dir);
782
- if (existsSync6(p)) {
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 readdirSync4(root)) {
1363
+ for (const f of readdirSync6(root)) {
795
1364
  if (/^state(-.+)?\.json$/.test(f)) {
796
- const p = join7(root, f);
797
- if (existsSync6(p)) {
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 = join7(root, "logs");
805
- if (existsSync6(p)) {
806
- rmSync2(p, { recursive: true, force: true });
807
- removed.push("logs");
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 if (existsSync6(join7(root, "logs"))) {
810
- kept.push("logs");
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 (existsSync6(join7(root, f)))
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 = join7(projectDir, ".mugiwara");
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 belongs = (f) => {
826
- const base = f.replace(/^\d{4}-\d{2}-\d{2}-/, "");
827
- return base === `${mission}.md` || base.startsWith(`${mission}-`);
828
- };
829
- let report = null;
830
- const reportsDir = join7(root, "reports");
831
- if (existsSync6(reportsDir)) {
832
- const f = readdirSync4(reportsDir).find((n) => {
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 specDir = join7(root, "spec");
846
- if (existsSync6(specDir)) {
847
- for (const f of readdirSync4(specDir)) {
848
- if (!belongs(f))
849
- continue;
850
- const p = join7(specDir, f);
851
- if (!dryRun)
852
- rmSync2(p);
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
- for (const dir of ["review", "issues"]) {
857
- const d = join7(root, dir);
858
- if (!existsSync6(d))
859
- continue;
860
- for (const f of readdirSync4(d)) {
861
- if (!belongs(f))
862
- continue;
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
- const logsDir = join7(root, "logs");
870
- if (existsSync6(logsDir)) {
871
- for (const f of readdirSync4(logsDir)) {
872
- if (!belongs(f))
873
- continue;
874
- const p = join7(logsDir, f);
875
- if (!dryRun)
876
- rmSync2(p);
877
- removed.push(join7("logs", f));
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 contDir = join7(root, "continue", mission);
881
- if (existsSync6(contDir)) {
882
- if (!dryRun)
883
- rmSync2(contDir, { recursive: true, force: true });
884
- removed.push(join7("continue", mission));
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 stateDir = join7(root, "state", mission);
887
- if (existsSync6(stateDir)) {
888
- if (!dryRun)
889
- rmSync2(stateDir, { recursive: true, force: true });
890
- removed.push(join7("state", mission));
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
- if (report)
893
- kept.push(report);
894
- for (const k of ["plans", "config", join7("logs", "lessons.md")]) {
895
- if (existsSync6(join7(root, k)))
896
- kept.push(k);
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 = join7(root, "reports", "index.md");
900
- const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}${report ? ` → ${report}` : ""}
1559
+ const indexFile = join11(root, "index.md");
1560
+ const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
901
1561
  `;
902
1562
  if (!dryRun) {
903
- mkdirSync5(join7(root, "reports"), { recursive: true });
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
- appendFileSync(indexFile, header + line);
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 { report, removed, kept, index };
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 = resolve(str(flags.project) ?? process.cwd());
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 = resolve(str(flags.project) ?? process.cwd());
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 = resolve(str(flags.project) ?? process.cwd());
991
- if (scope === "project" && !existsSync7(projectDir))
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 = homedir2();
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 = resolve(str(flags.project) ?? process.cwd());
1062
- const home = homedir2();
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 (existsSync7(globalFile)) {
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 = join8(scope === "global" ? home : projectDir, ".mugiwara");
1096
- if (existsSync7(mugiDir) && readdirSync5(mugiDir).length === 0) {
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 = join8(home, ".cache", "opencode", "packages", "@ionivetech");
1101
- if (existsSync7(opencodeCache)) {
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 = homedir2();
1111
- const projectDir = resolve(str(flags.project) ?? process.cwd());
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) => !existsSync7(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 (spec/plans/results/review/issues[/logs])
1141
- mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
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
- --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
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 && fileURLToPath4(import.meta.url) === resolve(process.argv[1]);
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
  };