@ionivetech/mugiwara 0.6.6 → 0.8.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 (138) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -7
  3. package/.codex-plugin/plugin.json +2 -7
  4. package/.cursor-plugin/plugin.json +2 -7
  5. package/.kimi-plugin/plugin.json +2 -7
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/.opencode/plugins/mugiwara.mjs +3 -18
  8. package/AGENTS.md +5 -4
  9. package/GEMINI.md +3 -3
  10. package/README.md +203 -331
  11. package/content/agents/brook-healing.md +8 -4
  12. package/content/agents/chopper-checkpoint.md +7 -3
  13. package/content/agents/eval-runner.md +4 -4
  14. package/content/agents/franky-gates.md +4 -4
  15. package/content/agents/jinbe-security.md +6 -7
  16. package/content/agents/luffy-orchestrator.md +11 -13
  17. package/content/agents/memory-keeper.md +4 -4
  18. package/content/agents/nami-planner.md +4 -4
  19. package/content/agents/resume-coordinator.md +7 -7
  20. package/content/agents/robin-reviewer.md +7 -3
  21. package/content/agents/sanji-quality.md +2 -2
  22. package/content/agents/skeptic-verifier.md +2 -2
  23. package/content/agents/usopp-brainstorm.md +3 -3
  24. package/content/agents/zoro-execution.md +6 -6
  25. package/content/skills/mugiwara-backend/SKILL.md +54 -43
  26. package/content/skills/mugiwara-backend/references/database.md +61 -0
  27. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  28. package/content/skills/mugiwara-checkpoint/SKILL.md +23 -11
  29. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
  30. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  31. package/content/skills/mugiwara-execution/SKILL.md +37 -37
  32. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  33. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  34. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  35. package/content/skills/mugiwara-frontend/SKILL.md +44 -43
  36. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  37. package/content/skills/mugiwara-gates/SKILL.md +23 -14
  38. package/content/skills/mugiwara-healing/SKILL.md +26 -25
  39. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  40. package/content/skills/mugiwara-orchestration/SKILL.md +11 -11
  41. package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
  42. package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
  43. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  44. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
  45. package/content/skills/mugiwara-planning/SKILL.md +26 -29
  46. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  47. package/content/skills/mugiwara-planning/references/plan-template.md +24 -2
  48. package/content/skills/mugiwara-quality/SKILL.md +21 -12
  49. package/content/skills/mugiwara-resume/SKILL.md +8 -3
  50. package/content/skills/mugiwara-review/SKILL.md +19 -13
  51. package/content/skills/mugiwara-security/SKILL.md +47 -36
  52. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  53. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  54. package/content/skills/mugiwara-workflow/SKILL.md +11 -17
  55. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
  56. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
  57. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
  58. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  59. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
  60. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
  61. package/content/skills/mugiwara-workflow/references/workspace-layout.md +42 -38
  62. package/dist/mugiwara.js +1642 -528
  63. package/gemini-extension.json +1 -1
  64. package/hooks/auto-savepoint.js +18 -10
  65. package/hooks/auto-savepoint.ts +23 -23
  66. package/hooks/engagement-marker.js +1 -1
  67. package/hooks/engagement-marker.ts +1 -1
  68. package/hooks/pipeline-guard.js +17 -13
  69. package/hooks/pipeline-guard.ts +24 -20
  70. package/hooks/session-start.js +13 -10
  71. package/hooks/session-start.ts +24 -19
  72. package/package.json +2 -2
  73. package/plugin.json +1 -1
  74. package/references/complexity.md +27 -4
  75. package/references/definition-of-done.md +1 -1
  76. package/references/multi-actor.md +20 -2
  77. package/references/posture-routing.md +31 -0
  78. package/references/prose-style.md +54 -0
  79. package/scripts/benchmark-governor.ts +516 -0
  80. package/scripts/benchmark-thresholds.json +47 -0
  81. package/scripts/check-doc-links.ts +46 -0
  82. package/scripts/conformance.ts +6 -16
  83. package/scripts/coverage-gate.ts +20 -6
  84. package/scripts/gate-selftest.ts +138 -7
  85. package/scripts/lane.sh +54 -8
  86. package/scripts/lib/lane-base.sh +4 -4
  87. package/scripts/lib/patterns.sh +5 -0
  88. package/scripts/policy-force.ts +22 -0
  89. package/scripts/retrieval-eval.ts +10 -4
  90. package/scripts/savepoint.sh +155 -58
  91. package/scripts/validate-content.ts +43 -11
  92. package/scripts/verify-install.ts +9 -7
  93. package/src/adaptive-budget.ts +178 -0
  94. package/src/args.ts +4 -3
  95. package/src/budget.ts +47 -0
  96. package/src/check-artifacts.ts +45 -0
  97. package/src/cli.ts +257 -108
  98. package/src/cognition.ts +234 -0
  99. package/src/config.ts +107 -0
  100. package/src/context.ts +72 -0
  101. package/src/continue.ts +29 -10
  102. package/src/cost.ts +186 -0
  103. package/src/evidence.ts +160 -0
  104. package/src/installer.ts +25 -34
  105. package/src/integrity.ts +158 -0
  106. package/src/investigation.ts +72 -0
  107. package/src/mission.ts +341 -94
  108. package/src/policy.ts +156 -0
  109. package/src/posture.ts +86 -0
  110. package/src/provenance.ts +116 -0
  111. package/src/reporting.ts +225 -0
  112. package/src/rollback.ts +95 -0
  113. package/src/routing.ts +69 -0
  114. package/src/run.ts +2 -2
  115. package/src/scope.ts +321 -0
  116. package/src/sign.ts +235 -0
  117. package/src/slop.ts +306 -0
  118. package/src/targets/claude.ts +2 -2
  119. package/src/work.ts +273 -0
  120. package/.opencode/commands/mugiwara-execute.md +0 -13
  121. package/.opencode/commands/mugiwara-heal.md +0 -13
  122. package/.opencode/commands/mugiwara-onboard.md +0 -14
  123. package/.opencode/commands/mugiwara-plan.md +0 -17
  124. package/.opencode/commands/mugiwara-ship.md +0 -13
  125. package/.opencode/commands/using-mugiwara.md +0 -20
  126. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  127. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  128. package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
  129. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  130. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  131. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  132. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  133. package/content/skills/using-mugiwara/SKILL.md +0 -51
  134. package/references/token-budget.md +0 -56
  135. package/scripts/evidence.sh +0 -81
  136. package/scripts/initiative.ts +0 -296
  137. package/scripts/mission-report.sh +0 -293
  138. package/src/onboard.ts +0 -207
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli.ts
3
- import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
4
- import { spawnSync } from 'node:child_process';
3
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { execFileSync } from 'node:child_process';
5
5
  import { homedir } from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
@@ -11,9 +11,15 @@ import { targets, TARGET_IDS } from './targets/index.ts';
11
11
  import { installTo, removeInstalled, VERSION, ensureProjectGitignore, removeProjectGitignore } from './installer.ts';
12
12
  import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
13
13
  import { resetMission, archiveMission } from './mission.ts';
14
- import { runOnboard } from './onboard.ts';
15
14
  import { runScript, RUNNABLE } from './run.ts';
16
15
  import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor } from './continue.ts';
16
+ import { blamePath } from './provenance.ts';
17
+ import { signReport, verifyReport, ensurePureKey, hasMinisign } from './sign.ts';
18
+ import { ensureConfig } from './config.ts';
19
+ import { costEnvelope } from './cost.ts';
20
+ import { computeLiveSlop } from './slop.ts';
21
+ import { loadRegistry } from './evidence.ts';
22
+ import { buildCostLedger, toCostJSON } from './reporting.ts';
17
23
 
18
24
  const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
19
25
  const flag = (v: FlagValue): boolean => v === true;
@@ -22,6 +28,23 @@ export async function run(argv: string[]): Promise<void> {
22
28
  const { command, flags, _ } = parseArgs(argv);
23
29
  if (flag(flags.help) || command === 'help') return help();
24
30
  if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
31
+ // `continue` and `status` are read-only position commands: dispatch before
32
+ // config bootstrap so a fresh project never gets a .mugiwara/config created
33
+ // and no setup chatter is printed before missions/members are listed.
34
+ if (command === 'continue' || command === 'status') {
35
+ return command === 'continue' ? continueCmd(flags, _) : statusCmd(flags);
36
+ }
37
+ // A command on a fresh project must be immediately usable — bootstrap the
38
+ // default .mugiwara/config when it is missing (not only at install time).
39
+ // Skipped for install/update --dry-run: a dry run must not mutate the
40
+ // project (the installer writes the config itself on a real install).
41
+ const isDryRunInstall = (command === 'install' || command === 'update') && flag(flags.dryRun);
42
+ if (!isDryRunInstall) {
43
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
44
+ if (ensureConfig(projectDir)) {
45
+ console.log(`default .mugiwara/config written at ${join(projectDir, '.mugiwara', 'config')} (edit it to customise)`);
46
+ }
47
+ }
25
48
  switch (command) {
26
49
  case 'install': return install(flags);
27
50
  case 'update': return install({ ...flags, force: true });
@@ -29,13 +52,15 @@ export async function run(argv: string[]): Promise<void> {
29
52
  case 'list': return list(flags);
30
53
  case 'reset': return resetCmd(flags);
31
54
  case 'archive': return archive(flags, _);
32
- case 'onboard': return onboardCmd(flags);
55
+ case 'clean': return cleanCmd(flags);
33
56
  case 'continue': return continueCmd(flags, _);
34
57
  case 'status': return statusCmd(flags);
58
+ case 'cost': return costCmd(flags, _);
35
59
  case 'run': return runCmd(flags, _);
36
60
  case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
37
- case 'initiative': return initiativeCmd(flags, _);
38
- case 'start': return startCmd(flags, _);
61
+ case 'blame': return blameCmd(flags, _);
62
+ case 'handoff': return handoffCmd(flags, _);
63
+ case 'sign': return signCmd(flags, _);
39
64
  default: throw new Error(`Unknown command: ${command}`);
40
65
  }
41
66
  }
@@ -59,17 +84,74 @@ function archive(flags: Args['flags'], positionals: string[]): void {
59
84
  if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
60
85
  const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
61
86
  if (result.report) console.log(`archive target: ${result.report}`);
87
+ else console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
62
88
  if (result.removed.length) console.log(`${flag(flags.dryRun) ? 'would remove' : 'removed'}: ${result.removed.join(', ')}`);
63
89
  if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
64
90
  if (result.index) console.log(`index updated: ${result.index}`);
65
91
  }
66
92
 
67
- async function onboardCmd(flags: Args['flags']): Promise<void> {
93
+ /**
94
+ * `mugiwara clean` — batch-archive every closed mission. A mission is closed
95
+ * when its dir holds a report.md and no live state.json/<member>.json. With
96
+ * --all, missions with live state are included too (--force overrides the
97
+ * safety stop). --before <date> restricts to missions whose state was last
98
+ * touched before that date.
99
+ */
100
+ function cleanCmd(flags: Args['flags']): void {
68
101
  const projectDir = resolve(str(flags.project) ?? process.cwd());
69
- if (!existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
70
- await runOnboard(projectDir);
102
+ const dryRun = flag(flags.dryRun);
103
+ const root = join(projectDir, '.mugiwara', 'missions');
104
+ if (!existsSync(root)) { console.log('nothing to clean (.mugiwara/missions/ does not exist).'); return; }
105
+ const before = str(flags.before);
106
+ const beforeMs = before ? Date.parse(before) : NaN;
107
+ if (before && !Number.isFinite(beforeMs)) { console.error(`invalid --before date: ${before}`); process.exit(1); }
108
+
109
+ let candidates = readdirSync(root, { withFileTypes: true })
110
+ .filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name))
111
+ .map((e) => e.name);
112
+ // default: CLOSED missions only — a report.md present and no live session
113
+ // state. --all widens to every mission dir, including in-flight ones.
114
+ // --before additionally treats an in-flight mission as closable when its
115
+ // newest state was last touched before the date: untouched work is safe to
116
+ // fold even without a report.md yet.
117
+ const stateFiles = (m: string): string[] =>
118
+ readdirSync(join(root, m)).filter((f) => {
119
+ const stem = f.replace(/\.json$/, '');
120
+ return f.endsWith('.json') && stem !== 'continue' && !stem.startsWith('continue-');
121
+ });
122
+ const hasLiveState = (m: string): boolean => stateFiles(m).length > 0;
123
+ const staleBefore = (m: string): boolean => {
124
+ if (!Number.isFinite(beforeMs)) return false;
125
+ for (const f of stateFiles(m)) {
126
+ try {
127
+ const ts = Date.parse(JSON.parse(readFileSync(join(root, m, f), 'utf8')).updated_at ?? '') || 0;
128
+ if (ts === 0 || ts >= beforeMs) return false; // unknown freshness → never assume stale
129
+ } catch { return false; }
130
+ }
131
+ return true;
132
+ };
133
+ if (!flag(flags.all)) {
134
+ candidates = candidates.filter((m) =>
135
+ (existsSync(join(root, m, 'report.md')) && !hasLiveState(m))
136
+ || staleBefore(m),
137
+ );
138
+ } else if (!flag(flags.force)) {
139
+ const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
140
+ if (live.length) {
141
+ console.error(`✗ in-flight mission(s): ${live.join(', ')}. Use --force to archive them anyway.`);
142
+ process.exit(1);
143
+ }
144
+ }
145
+ if (!candidates.length) { console.log('nothing to clean.'); return; }
146
+ for (const m of candidates) {
147
+ const r = archiveMission(projectDir, m, { dryRun });
148
+ console.log(`${dryRun ? 'would clean' : 'cleaned'} ${m}${r.report ? ` → ${r.report}` : ''}`);
149
+ if (r.index) console.log(`index updated: ${r.index}`);
150
+ }
71
151
  }
72
152
 
153
+
154
+
73
155
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
74
156
  const interactive = !flag(flags.yes);
75
157
  if (interactive && !process.stdin.isTTY) {
@@ -140,22 +222,8 @@ async function install(flags: Args['flags']): Promise<void> {
140
222
  });
141
223
  console.log(`\nOK mugiwara ${VERSION} installed (manifest: ${file})`);
142
224
  if (allNotes.length) console.log(`${allNotes.length} note(s) above may need attention.`);
143
- // TASK 8: name exactly one next step, and state the active mode + enforce
144
- // value since both change behaviour.
145
- if (scope === 'project') {
146
- const cfg = join(projectDir, '.mugiwara', 'config');
147
- let mode = 'guided', enforce = 'block';
148
- if (existsSync(cfg)) {
149
- for (const line of readFileSync(cfg, 'utf8').split(/\r?\n/)) {
150
- const [k, v] = line.split('=').map((s) => s.trim());
151
- if (k === 'mode') mode = v;
152
- if (k === 'enforce') enforce = v;
153
- }
154
- }
155
- console.log(`\nNext: run \`mugiwara onboard\` to customise (mode=${mode}, enforce=${enforce} now).`);
156
- } else {
157
- console.log('\nNext: run `mugiwara onboard` in a project to write its .mugiwara/config.');
158
- }
225
+ // A fresh install writes a default .mugiwara/config point at it directly.
226
+ console.log('\nNext: edit .mugiwara/config to customise (mode, branch, coverage, depths).');
159
227
  }
160
228
 
161
229
  async function uninstall(flags: Args['flags']): Promise<void> {
@@ -261,7 +329,13 @@ function continueCmd(flags: Args['flags'], positionals: string[]): void {
261
329
  }
262
330
 
263
331
  const r = resolveContinue(entries, mission, member);
264
- if (r.kind === 'resume') { console.log(formatResume(r.entry)); return; }
332
+ if (r.kind === 'resume') {
333
+ console.log(formatResume(r.entry));
334
+ const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
335
+ const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
336
+ if (stale) console.log(stale);
337
+ return;
338
+ }
265
339
 
266
340
  if (r.kind === 'none') {
267
341
  console.log('No mission in flight. Start one with Flow 0 triage (mugiwara-orchestration).');
@@ -299,6 +373,61 @@ function statusCmd(flags: Args['flags']): void {
299
373
  }
300
374
  }
301
375
 
376
+ /** `mugiwara cost [--mission <id>] [--json] [--ledger]` — show cost ledger, avoided work, efficiency, trail. */
377
+ function costCmd(flags: Args['flags'], positionals: string[]): void {
378
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
379
+ const mission = str(flags.mission) ?? positionals[1] ?? (() => {
380
+ const states = readState(projectDir);
381
+ if (states.length === 1) return states[0].mission;
382
+ if (states.length > 1) {
383
+ console.error('multiple missions in flight — specify --mission <id>');
384
+ process.exit(1);
385
+ }
386
+ return null;
387
+ })();
388
+ if (!mission) {
389
+ console.error('usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]');
390
+ process.exit(1);
391
+ }
392
+ const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
393
+ if (!existsSync(missionDir)) {
394
+ console.error(`No cost ledger found for mission "${mission}"`);
395
+ process.exit(1);
396
+ }
397
+ const states = readState(projectDir).filter((s) => s.mission === mission);
398
+ const envelope = states.length
399
+ ? costEnvelope({ lane: (states[0] as unknown as { lane?: string }).lane, budget: (states[0] as unknown as { budget?: number }).budget, tokens_est: (states[0] as unknown as { tokens_est?: number }).tokens_est })
400
+ : costEnvelope({ lane: 'full', tokens_est: 0 });
401
+ // live slop (§3.3): run existing detectors over state already available
402
+ // (heal cycle, context registry repeated reads) so slop_interventions is real.
403
+ const state0 = states[0] as unknown as { heal_cycle?: number };
404
+ let repeatedReads = 0;
405
+ try {
406
+ const reg = loadRegistry(missionDir);
407
+ repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
408
+ } catch {
409
+ repeatedReads = 0;
410
+ }
411
+ const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
412
+ const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
413
+ if (flag(flags.json)) {
414
+ console.log(toCostJSON(ledger));
415
+ return;
416
+ }
417
+ console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
418
+ console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
419
+ console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
420
+ if (ledger.avoided.slop_interventions > 0) {
421
+ const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(', ');
422
+ console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
423
+ }
424
+ console.log(`Trail: ${ledger.trail.length} decisions`);
425
+ if (flag(flags.ledger) && ledger.trail.length) {
426
+ for (const t of ledger.trail.slice(0, 20)) console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ''}`);
427
+ if (ledger.trail.length > 20) console.log(`… ${ledger.trail.length - 20} more`);
428
+ }
429
+ }
430
+
302
431
  /** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
303
432
  function runCmd(flags: Args['flags'], positionals: string[]): void {
304
433
  const projectDir = resolve(str(flags.project) ?? process.cwd());
@@ -311,88 +440,100 @@ function runCmd(flags: Args['flags'], positionals: string[]): void {
311
440
  if (code !== 0) process.exit(code);
312
441
  }
313
442
 
314
- /** `mugiwara initiative <status|conflict-check|set-status> <plan> [args]` team sub-mission coordination. */
315
- function initiativeCmd(flags: Args['flags'], positionals: string[]): void {
443
+ /** `mugiwara blame <path>` provenance note on the last commit touching path. */
444
+ function blameCmd(flags: Args['flags'], positionals: string[]): void {
316
445
  const projectDir = resolve(str(flags.project) ?? process.cwd());
317
- const sub = positionals[1];
318
- if (!sub) {
319
- console.error('usage: mugiwara initiative <status|conflict-check|set-status> <plan-file> [--id <id> --status <s>]');
320
- process.exit(1);
446
+ const path = positionals[1];
447
+ if (!path) { console.error('usage: mugiwara blame <file-path>'); process.exit(1); }
448
+ console.log(blamePath(projectDir, path));
449
+ }
450
+
451
+ /**
452
+ * Staleness: has main moved since the mission's recorded base?
453
+ * N commits behind = the ground this mission started from has shifted.
454
+ */
455
+ export function stalenessLine(projectDir: string, baseSha: string): string | null {
456
+ if (!baseSha || baseSha === 'unknown') return null;
457
+ const git = (args: string[]): string => {
458
+ try {
459
+ return execFileSync('git', args, { cwd: projectDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
460
+ } catch { return ''; }
461
+ };
462
+ let main = '';
463
+ for (const ref of ['main', 'master']) {
464
+ main = git(['rev-parse', '--verify', ref]);
465
+ if (main) break;
321
466
  }
322
- const script = join(import.meta.dirname, '..', 'scripts', 'initiative.ts');
323
- // initiative.ts is TypeScript — spawn `bun` (which strips types), not node,
324
- // which only strips types on >=22.18. engines floor is >=20.11.
325
- const bun = process.platform === 'win32' ? 'bun.exe' : 'bun';
326
- const r = spawnSync(bun, [script, ...positionals.slice(1)], {
327
- cwd: projectDir,
328
- stdio: 'inherit',
329
- env: process.env,
330
- });
331
- if (r.error) throw r.error;
332
- if (r.status) process.exit(r.status);
467
+ if (!main) return null;
468
+ try {
469
+ const behind = Number(git(['rev-list', '--count', `${baseSha}..${main}`])) || 0;
470
+ return behind > 0
471
+ ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing`
472
+ : null;
473
+ } catch { return null; }
333
474
  }
334
475
 
335
- /** `mugiwara start <mission> [member]` begin a mission from its plan. */
336
- function startCmd(flags: Args['flags'], positionals: string[]): void {
476
+ /** `mugiwara handoff <mission>` — a report the next engineer can act on. */
477
+ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
337
478
  const projectDir = resolve(str(flags.project) ?? process.cwd());
338
479
  const mission = positionals[1];
339
- if (!mission) {
340
- console.error('usage: mugiwara start <mission> [member]');
341
- process.exit(1);
342
- }
343
- // find the plan doc: plans/<mission>.md or plans/YYYY-MM-DD-<mission>.md
344
- const plansDir = join(projectDir, '.mugiwara', 'plans');
345
- let planFile = '';
346
- if (existsSync(plansDir)) {
347
- const bare = join(plansDir, `${mission}.md`);
348
- if (existsSync(bare)) planFile = bare;
349
- else {
350
- const dated = readdirSync(plansDir).filter((f) => f.endsWith(`-${mission}.md`)).sort().pop();
351
- if (dated) planFile = join(plansDir, dated);
352
- }
353
- }
354
- if (!planFile) {
355
- console.error(`mugiwara start: no plan found for "${mission}" in ${plansDir}`);
356
- process.exit(1);
480
+ if (!mission) { console.error('usage: mugiwara handoff <mission> [--project <dir>]'); process.exit(1); }
481
+ const states = readState(projectDir).filter((s) => s.mission === mission);
482
+ if (!states.length) { console.error(`no in-flight mission "${mission}"`); process.exit(1); }
483
+ const lines = [
484
+ `# Handoff: ${mission}`,
485
+ '',
486
+ `Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
487
+ '',
488
+ '| | |',
489
+ '|---|---|',
490
+ ];
491
+ for (const s of states) {
492
+ const scope = s.member ? ` [${s.member}]` : '';
493
+ lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? ' (rose)' : ''}, mode ${s.mode} |`);
494
+ lines.push(`| Branch | \`${s.branch}\` |`);
495
+ lines.push(`| Actor | ${s.actor || '(unknown)'} |`);
496
+ if (s.next_action) lines.push(`| Next action | ${s.next_action} |`);
497
+ if (s.blockers_open) lines.push(`| Open blockers | ${s.blockers_open} |`);
498
+ if (s.heal_cycle) lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? ' — HALTED' : ''} |`);
499
+ if (s.evidence.length) lines.push(`| Evidence | ${s.evidence.join(', ')} |`);
500
+ const stale = stalenessLine(projectDir, s.base_sha);
501
+ if (stale) lines.push(`| Staleness | ${stale.replace('⚠ stale base: ', '')} |`);
357
502
  }
358
- const plan = readFileSync(planFile, 'utf8');
503
+ lines.push('', '## Resuming', '', `\`mugiwara continue ${mission}\` prints the exact resume point.`);
504
+ lines.push('Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.');
505
+ const out = join('.mugiwara', 'missions', mission, 'handoff.md');
506
+ writeFileSync(resolve(projectDir, out), lines.join('\n') + '\n');
507
+ console.log(lines.join('\n'));
508
+ console.log(`\nwritten: ${out}`);
509
+ }
359
510
 
360
- // team plan = has a ## Sub-missions table; solo = none.
361
- const subMatch = plan.match(/##\s*Sub-missions/i);
362
- let member = positionals[2] ?? '';
363
- if (subMatch && !member) {
364
- // team: find the row whose Assignee matches this git actor
365
- const actor = gitActor(projectDir);
366
- const name = actor.split('<')[0].trim();
367
- const rowRe = /^\|\s*([\w.-]+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm;
368
- let m: RegExpExecArray | null;
369
- let found = '';
370
- while ((m = rowRe.exec(plan))) {
371
- const assignee = m[3].trim();
372
- if (assignee === name || assignee === actor) { found = m[1].trim(); break; }
373
- }
374
- if (!found) {
375
- console.error(`mugiwara start: no sub-mission assigned to "${name}" in ${planFile}. Pass a member explicitly: mugiwara start ${mission} <member>`);
376
- process.exit(1);
511
+ /** `mugiwara sign <mission>` / `--verify` / `--gen-key` optional attestation. */
512
+ function signCmd(flags: Args['flags'], _: string[]): void {
513
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
514
+ if (flag(flags.genKey)) {
515
+ const backend = str(flags.backend) ?? 'auto';
516
+ const home = homedir();
517
+ if (backend === 'minisign') {
518
+ if (!hasMinisign()) { console.error('✗ minisign not installed — cannot generate keys with this backend'); process.exit(1); }
519
+ try {
520
+ execFileSync('minisign', ['-G'], { stdio: 'inherit' });
521
+ console.log('✓ minisign key pair generated in ~/.mugiwara/');
522
+ return;
523
+ } catch { console.error('✗ key generation failed'); process.exit(1); }
377
524
  }
378
- member = found;
379
- }
380
-
381
- // write the savepoint: mission [member] flow=0 mode=auto lane=full (start)
382
- const code = runScript('savepoint.sh', [mission, member, '0', 'auto', 'full'], projectDir);
383
- if (code !== 0) process.exit(code);
384
-
385
- // team plan: mark the sub-mission in-progress in the plan (source of truth)
386
- if (subMatch && member) {
387
- const init = join(import.meta.dirname, '..', 'scripts', 'initiative.ts');
388
- const bun = process.platform === 'win32' ? 'bun.exe' : 'bun';
389
- const r = spawnSync(bun, [init, 'set-status', planFile, '--id', member, '--status', 'in-progress'], {
390
- cwd: projectDir, stdio: 'inherit', env: process.env,
391
- });
392
- if (r.status) process.exit(r.status);
525
+ // pure (default)
526
+ const dir = ensurePureKey(home);
527
+ console.log(`✓ pure ed25519 key pair ready: ${join(dir, 'mugiwara.key')} / ${join(dir, 'mugiwara.pub')}`);
528
+ return;
393
529
  }
394
-
395
- console.log(`mugiwara start: ${mission}${member ? ` [${member}]` : ''} begun at Flow 0. Resume anytime with: mugiwara continue ${mission}${member ? ` ${member}` : ''}`);
530
+ const mission = _[1];
531
+ if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]'); process.exit(1); }
532
+ const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
533
+ if (!existsSync(missionDir)) { console.error(`no mission dir: ${missionDir}`); process.exit(1); }
534
+ const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
535
+ console.log(`${r.ok ? '✓' : '✗'} ${r.message}`);
536
+ if (!r.ok) process.exit(1);
396
537
  }
397
538
 
398
539
  function help(): void {
@@ -404,21 +545,27 @@ Usage:
404
545
  mugiwara uninstall remove installed files via manifest
405
546
  mugiwara list show installations
406
547
  mugiwara list --check health check: show installations + missing files
407
- mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
408
- mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
409
- mugiwara onboard run the zero-LLM onboarding wizard (writes .mugiwara/config)
548
+ mugiwara reset wipe mission state (missions/ + legacy dirs)
549
+ mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
550
+ mugiwara clean [--all] [--before <date>]
551
+ batch-archive every closed mission (report.md present, no live state)
410
552
  mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
411
553
  mugiwara continue <m> [member]
412
554
  print the exact resume point for that mission/member
413
555
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
556
+ mugiwara cost [--mission <id>] [--json] [--ledger]
557
+ show cost ledger, avoided work, efficiency, trail (human + JSON)
558
+ mugiwara blame <path> provenance note on the last commit touching <path>
559
+ (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
560
+ mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
561
+ engineer can act on (computed state + staleness check)
562
+ mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
563
+ mugiwara sign --gen-key [--backend pure|minisign]
564
+ create signing keys (pure ed25519 default)
414
565
  mugiwara run <script> [args...]
415
566
  run a bundled harness script here (${RUNNABLE.join(', ')})
416
- mugiwara savepoint <mission> [member] [wave] [mode]
567
+ mugiwara savepoint <mission> [member] [flow] [mode]
417
568
  shorthand for: mugiwara run savepoint.sh ...
418
- mugiwara initiative <status|conflict-check|set-status> <plan> [args]
419
- team sub-mission coordination (status / conflict-check / set-status)
420
- mugiwara start <mission> [member]
421
- begin a mission from its plan (solo, or team sub-mission by git actor)
422
569
  mugiwara --help this help
423
570
  mugiwara --version print version
424
571
 
@@ -430,8 +577,10 @@ Flags:
430
577
  --force overwrite differing files (with backup)
431
578
  --dry-run print actions without writing
432
579
  --check with list: report missing files (health check)
433
- --all with continue/status: every actor, not just yours
434
- --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
580
+ --all with continue/status: every actor; with clean: include in-flight missions
581
+ --force with clean --all: archive in-flight missions anyway
582
+ --before <date> with clean: also archive missions untouched since this date
583
+ --keep-logs with reset: keep lessons.md (lessons ledger survives)`);
435
584
  }
436
585
 
437
586
  const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]);