@ionivetech/mugiwara 0.6.6 → 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 (110) 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/plugins/mugiwara.mjs +3 -18
  7. package/AGENTS.md +5 -4
  8. package/GEMINI.md +3 -3
  9. package/README.md +69 -63
  10. package/content/agents/brook-healing.md +8 -4
  11. package/content/agents/chopper-checkpoint.md +7 -3
  12. package/content/agents/eval-runner.md +4 -4
  13. package/content/agents/franky-gates.md +3 -3
  14. package/content/agents/jinbe-security.md +6 -7
  15. package/content/agents/luffy-orchestrator.md +11 -13
  16. package/content/agents/memory-keeper.md +4 -4
  17. package/content/agents/nami-planner.md +4 -4
  18. package/content/agents/resume-coordinator.md +7 -7
  19. package/content/agents/robin-reviewer.md +7 -3
  20. package/content/agents/sanji-quality.md +2 -2
  21. package/content/agents/skeptic-verifier.md +2 -2
  22. package/content/agents/usopp-brainstorm.md +3 -3
  23. package/content/agents/zoro-execution.md +6 -6
  24. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  25. package/content/skills/mugiwara-backend/references/database.md +61 -0
  26. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  27. package/content/skills/mugiwara-checkpoint/SKILL.md +6 -5
  28. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
  29. package/content/skills/mugiwara-execution/SKILL.md +8 -8
  30. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  31. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  32. package/content/skills/mugiwara-frontend/SKILL.md +3 -2
  33. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  34. package/content/skills/mugiwara-gates/SKILL.md +8 -5
  35. package/content/skills/mugiwara-healing/SKILL.md +2 -2
  36. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  37. package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
  38. package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
  39. package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
  40. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
  41. package/content/skills/mugiwara-planning/SKILL.md +3 -18
  42. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  43. package/content/skills/mugiwara-quality/SKILL.md +5 -2
  44. package/content/skills/mugiwara-resume/SKILL.md +2 -2
  45. package/content/skills/mugiwara-review/SKILL.md +3 -2
  46. package/content/skills/mugiwara-security/SKILL.md +1 -1
  47. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  48. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  49. package/content/skills/mugiwara-workflow/SKILL.md +9 -12
  50. package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
  51. package/dist/mugiwara.js +904 -462
  52. package/gemini-extension.json +1 -1
  53. package/hooks/auto-savepoint.js +18 -10
  54. package/hooks/auto-savepoint.ts +23 -23
  55. package/hooks/engagement-marker.js +1 -1
  56. package/hooks/engagement-marker.ts +1 -1
  57. package/hooks/pipeline-guard.js +16 -12
  58. package/hooks/pipeline-guard.ts +22 -19
  59. package/hooks/session-start.js +13 -10
  60. package/hooks/session-start.ts +24 -19
  61. package/package.json +2 -2
  62. package/plugin.json +1 -1
  63. package/references/complexity.md +27 -4
  64. package/references/definition-of-done.md +1 -1
  65. package/references/multi-actor.md +0 -3
  66. package/references/prose-style.md +54 -0
  67. package/scripts/check-doc-links.ts +40 -0
  68. package/scripts/conformance.ts +6 -16
  69. package/scripts/coverage-gate.ts +20 -6
  70. package/scripts/gate-selftest.ts +118 -7
  71. package/scripts/lane.sh +54 -8
  72. package/scripts/lib/patterns.sh +5 -0
  73. package/scripts/policy-force.ts +22 -0
  74. package/scripts/retrieval-eval.ts +1 -1
  75. package/scripts/savepoint.sh +136 -58
  76. package/scripts/validate-content.ts +22 -9
  77. package/scripts/verify-install.ts +9 -7
  78. package/src/args.ts +2 -2
  79. package/src/budget.ts +56 -0
  80. package/src/cli.ts +161 -110
  81. package/src/continue.ts +29 -10
  82. package/src/installer.ts +25 -20
  83. package/src/integrity.ts +158 -0
  84. package/src/mission.ts +228 -95
  85. package/src/policy.ts +156 -0
  86. package/src/provenance.ts +116 -0
  87. package/src/rollback.ts +95 -0
  88. package/src/routing.ts +69 -0
  89. package/src/run.ts +2 -2
  90. package/src/sign.ts +61 -0
  91. package/src/targets/claude.ts +2 -2
  92. package/.opencode/commands/mugiwara-execute.md +0 -13
  93. package/.opencode/commands/mugiwara-heal.md +0 -13
  94. package/.opencode/commands/mugiwara-onboard.md +0 -14
  95. package/.opencode/commands/mugiwara-plan.md +0 -17
  96. package/.opencode/commands/mugiwara-ship.md +0 -13
  97. package/.opencode/commands/using-mugiwara.md +0 -20
  98. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  99. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  100. package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
  101. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  102. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  103. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  104. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  105. package/content/skills/using-mugiwara/SKILL.md +0 -51
  106. package/references/token-budget.md +0 -56
  107. package/scripts/evidence.sh +0 -81
  108. package/scripts/initiative.ts +0 -296
  109. package/scripts/mission-report.sh +0 -293
  110. 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,10 @@ 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 } from './sign.ts';
17
18
 
18
19
  const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
19
20
  const flag = (v: FlagValue): boolean => v === true;
@@ -29,13 +30,14 @@ export async function run(argv: string[]): Promise<void> {
29
30
  case 'list': return list(flags);
30
31
  case 'reset': return resetCmd(flags);
31
32
  case 'archive': return archive(flags, _);
32
- case 'onboard': return onboardCmd(flags);
33
+ case 'clean': return cleanCmd(flags);
33
34
  case 'continue': return continueCmd(flags, _);
34
35
  case 'status': return statusCmd(flags);
35
36
  case 'run': return runCmd(flags, _);
36
37
  case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
37
- case 'initiative': return initiativeCmd(flags, _);
38
- case 'start': return startCmd(flags, _);
38
+ case 'blame': return blameCmd(flags, _);
39
+ case 'handoff': return handoffCmd(flags, _);
40
+ case 'sign': return signCmd(flags, _);
39
41
  default: throw new Error(`Unknown command: ${command}`);
40
42
  }
41
43
  }
@@ -59,17 +61,74 @@ function archive(flags: Args['flags'], positionals: string[]): void {
59
61
  if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
60
62
  const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
61
63
  if (result.report) console.log(`archive target: ${result.report}`);
64
+ else console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
62
65
  if (result.removed.length) console.log(`${flag(flags.dryRun) ? 'would remove' : 'removed'}: ${result.removed.join(', ')}`);
63
66
  if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
64
67
  if (result.index) console.log(`index updated: ${result.index}`);
65
68
  }
66
69
 
67
- async function onboardCmd(flags: Args['flags']): Promise<void> {
70
+ /**
71
+ * `mugiwara clean` — batch-archive every closed mission. A mission is closed
72
+ * when its dir holds a report.md and no live state.json/<member>.json. With
73
+ * --all, missions with live state are included too (--force overrides the
74
+ * safety stop). --before <date> restricts to missions whose state was last
75
+ * touched before that date.
76
+ */
77
+ function cleanCmd(flags: Args['flags']): void {
68
78
  const projectDir = resolve(str(flags.project) ?? process.cwd());
69
- if (!existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
70
- await runOnboard(projectDir);
79
+ const dryRun = flag(flags.dryRun);
80
+ const root = join(projectDir, '.mugiwara', 'missions');
81
+ if (!existsSync(root)) { console.log('nothing to clean (.mugiwara/missions/ does not exist).'); return; }
82
+ const before = str(flags.before);
83
+ const beforeMs = before ? Date.parse(before) : NaN;
84
+ if (before && !Number.isFinite(beforeMs)) { console.error(`invalid --before date: ${before}`); process.exit(1); }
85
+
86
+ let candidates = readdirSync(root, { withFileTypes: true })
87
+ .filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name))
88
+ .map((e) => e.name);
89
+ // default: CLOSED missions only — a report.md present and no live session
90
+ // state. --all widens to every mission dir, including in-flight ones.
91
+ // --before additionally treats an in-flight mission as closable when its
92
+ // newest state was last touched before the date: untouched work is safe to
93
+ // fold even without a report.md yet.
94
+ const stateFiles = (m: string): string[] =>
95
+ readdirSync(join(root, m)).filter((f) => {
96
+ const stem = f.replace(/\.json$/, '');
97
+ return f.endsWith('.json') && stem !== 'continue' && !stem.startsWith('continue-');
98
+ });
99
+ const hasLiveState = (m: string): boolean => stateFiles(m).length > 0;
100
+ const staleBefore = (m: string): boolean => {
101
+ if (!Number.isFinite(beforeMs)) return false;
102
+ for (const f of stateFiles(m)) {
103
+ try {
104
+ const ts = Date.parse(JSON.parse(readFileSync(join(root, m, f), 'utf8')).updated_at ?? '') || 0;
105
+ if (ts === 0 || ts >= beforeMs) return false; // unknown freshness → never assume stale
106
+ } catch { return false; }
107
+ }
108
+ return true;
109
+ };
110
+ if (!flag(flags.all)) {
111
+ candidates = candidates.filter((m) =>
112
+ (existsSync(join(root, m, 'report.md')) && !hasLiveState(m))
113
+ || staleBefore(m),
114
+ );
115
+ } else if (!flag(flags.force)) {
116
+ const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
117
+ if (live.length) {
118
+ console.error(`✗ in-flight mission(s): ${live.join(', ')}. Use --force to archive them anyway.`);
119
+ process.exit(1);
120
+ }
121
+ }
122
+ if (!candidates.length) { console.log('nothing to clean.'); return; }
123
+ for (const m of candidates) {
124
+ const r = archiveMission(projectDir, m, { dryRun });
125
+ console.log(`${dryRun ? 'would clean' : 'cleaned'} ${m}${r.report ? ` → ${r.report}` : ''}`);
126
+ if (r.index) console.log(`index updated: ${r.index}`);
127
+ }
71
128
  }
72
129
 
130
+
131
+
73
132
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
74
133
  const interactive = !flag(flags.yes);
75
134
  if (interactive && !process.stdin.isTTY) {
@@ -140,22 +199,8 @@ async function install(flags: Args['flags']): Promise<void> {
140
199
  });
141
200
  console.log(`\nOK mugiwara ${VERSION} installed (manifest: ${file})`);
142
201
  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
- }
202
+ // A fresh install writes a default .mugiwara/config point at it directly.
203
+ console.log('\nNext: edit .mugiwara/config to customise (mode, branch, coverage, depths).');
159
204
  }
160
205
 
161
206
  async function uninstall(flags: Args['flags']): Promise<void> {
@@ -261,7 +306,13 @@ function continueCmd(flags: Args['flags'], positionals: string[]): void {
261
306
  }
262
307
 
263
308
  const r = resolveContinue(entries, mission, member);
264
- if (r.kind === 'resume') { console.log(formatResume(r.entry)); return; }
309
+ if (r.kind === 'resume') {
310
+ console.log(formatResume(r.entry));
311
+ const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
312
+ const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
313
+ if (stale) console.log(stale);
314
+ return;
315
+ }
265
316
 
266
317
  if (r.kind === 'none') {
267
318
  console.log('No mission in flight. Start one with Flow 0 triage (mugiwara-orchestration).');
@@ -311,88 +362,84 @@ function runCmd(flags: Args['flags'], positionals: string[]): void {
311
362
  if (code !== 0) process.exit(code);
312
363
  }
313
364
 
314
- /** `mugiwara initiative <status|conflict-check|set-status> <plan> [args]` team sub-mission coordination. */
315
- function initiativeCmd(flags: Args['flags'], positionals: string[]): void {
365
+ /** `mugiwara blame <path>` provenance note on the last commit touching path. */
366
+ function blameCmd(flags: Args['flags'], positionals: string[]): void {
316
367
  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);
368
+ const path = positionals[1];
369
+ if (!path) { console.error('usage: mugiwara blame <file-path>'); process.exit(1); }
370
+ console.log(blamePath(projectDir, path));
371
+ }
372
+
373
+ /**
374
+ * Staleness: has main moved since the mission's recorded base?
375
+ * N commits behind = the ground this mission started from has shifted.
376
+ */
377
+ export function stalenessLine(projectDir: string, baseSha: string): string | null {
378
+ if (!baseSha || baseSha === 'unknown') return null;
379
+ const git = (args: string[]): string => {
380
+ try {
381
+ return execFileSync('git', args, { cwd: projectDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
382
+ } catch { return ''; }
383
+ };
384
+ let main = '';
385
+ for (const ref of ['main', 'master']) {
386
+ main = git(['rev-parse', '--verify', ref]);
387
+ if (main) break;
321
388
  }
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);
389
+ if (!main) return null;
390
+ try {
391
+ const behind = Number(git(['rev-list', '--count', `${baseSha}..${main}`])) || 0;
392
+ return behind > 0
393
+ ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing`
394
+ : null;
395
+ } catch { return null; }
333
396
  }
334
397
 
335
- /** `mugiwara start <mission> [member]` begin a mission from its plan. */
336
- function startCmd(flags: Args['flags'], positionals: string[]): void {
398
+ /** `mugiwara handoff <mission>` — a report the next engineer can act on. */
399
+ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
337
400
  const projectDir = resolve(str(flags.project) ?? process.cwd());
338
401
  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);
357
- }
358
- const plan = readFileSync(planFile, 'utf8');
359
-
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);
377
- }
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);
402
+ if (!mission) { console.error('usage: mugiwara handoff <mission> [--project <dir>]'); process.exit(1); }
403
+ const states = readState(projectDir).filter((s) => s.mission === mission);
404
+ if (!states.length) { console.error(`no in-flight mission "${mission}"`); process.exit(1); }
405
+ const lines = [
406
+ `# Handoff: ${mission}`,
407
+ '',
408
+ `Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
409
+ '',
410
+ '| | |',
411
+ '|---|---|',
412
+ ];
413
+ for (const s of states) {
414
+ const scope = s.member ? ` [${s.member}]` : '';
415
+ lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? ' (rose)' : ''}, mode ${s.mode} |`);
416
+ lines.push(`| Branch | \`${s.branch}\` |`);
417
+ lines.push(`| Actor | ${s.actor || '(unknown)'} |`);
418
+ if (s.next_action) lines.push(`| Next action | ${s.next_action} |`);
419
+ if (s.blockers_open) lines.push(`| Open blockers | ${s.blockers_open} |`);
420
+ if (s.heal_cycle) lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? ' — HALTED' : ''} |`);
421
+ if (s.evidence.length) lines.push(`| Evidence | ${s.evidence.join(', ')} |`);
422
+ const stale = stalenessLine(projectDir, s.base_sha);
423
+ if (stale) lines.push(`| Staleness | ${stale.replace('⚠ stale base: ', '')} |`);
393
424
  }
425
+ lines.push('', '## Resuming', '', `\`mugiwara continue ${mission}\` prints the exact resume point.`);
426
+ lines.push('Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.');
427
+ const out = join('.mugiwara', 'missions', mission, 'handoff.md');
428
+ writeFileSync(resolve(projectDir, out), lines.join('\n') + '\n');
429
+ console.log(lines.join('\n'));
430
+ console.log(`\nwritten: ${out}`);
431
+ }
394
432
 
395
- console.log(`mugiwara start: ${mission}${member ? ` [${member}]` : ''} begun at Flow 0. Resume anytime with: mugiwara continue ${mission}${member ? ` ${member}` : ''}`);
433
+ /** `mugiwara sign <mission>` / `--verify` optional minisign attestation. */
434
+ function signCmd(flags: Args['flags'], _: string[]): void {
435
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
436
+ const mission = _[1];
437
+ if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--project <dir>]'); process.exit(1); }
438
+ const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
439
+ if (!existsSync(missionDir)) { console.error(`no mission dir: ${missionDir}`); process.exit(1); }
440
+ const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
441
+ console.log(`${r.ok ? '✓' : '✗'} ${r.message}`);
442
+ if (!r.ok) process.exit(1);
396
443
  }
397
444
 
398
445
  function help(): void {
@@ -404,21 +451,23 @@ Usage:
404
451
  mugiwara uninstall remove installed files via manifest
405
452
  mugiwara list show installations
406
453
  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)
454
+ mugiwara reset wipe mission state (missions/ + legacy dirs)
455
+ mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
456
+ mugiwara clean [--all] [--before <date>]
457
+ batch-archive every closed mission (report.md present, no live state)
410
458
  mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
411
459
  mugiwara continue <m> [member]
412
460
  print the exact resume point for that mission/member
413
461
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
462
+ mugiwara blame <path> provenance note on the last commit touching <path>
463
+ (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
464
+ mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
465
+ engineer can act on (computed state + staleness check)
466
+ mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
414
467
  mugiwara run <script> [args...]
415
468
  run a bundled harness script here (${RUNNABLE.join(', ')})
416
- mugiwara savepoint <mission> [member] [wave] [mode]
469
+ mugiwara savepoint <mission> [member] [flow] [mode]
417
470
  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
471
  mugiwara --help this help
423
472
  mugiwara --version print version
424
473
 
@@ -430,8 +479,10 @@ Flags:
430
479
  --force overwrite differing files (with backup)
431
480
  --dry-run print actions without writing
432
481
  --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)`);
482
+ --all with continue/status: every actor; with clean: include in-flight missions
483
+ --force with clean --all: archive in-flight missions anyway
484
+ --before <date> with clean: also archive missions untouched since this date
485
+ --keep-logs with reset: keep lessons.md (lessons ledger survives)`);
435
486
  }
436
487
 
437
488
  const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
package/src/continue.ts CHANGED
@@ -6,9 +6,10 @@
6
6
  // in one process spawn.
7
7
  //
8
8
  // Reads what scripts/savepoint.sh writes:
9
- // .mugiwara/continue/<mission>/state.json (solo)
10
- // .mugiwara/continue/<mission>/<member>.json (team)
11
- // .mugiwara/state/<mission>/... (same shape, more fields)
9
+ // .mugiwara/missions/<mission>/state.json (solo state)
10
+ // .mugiwara/missions/<mission>/<member>.json (team state)
11
+ // .mugiwara/missions/<mission>/continue.json (solo resume point)
12
+ // .mugiwara/missions/<mission>/continue-<member>.json (team resume point)
12
13
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
13
14
  import { execFileSync } from 'node:child_process';
14
15
  import { join } from 'node:path';
@@ -37,6 +38,8 @@ export type StateEntry = ContinueEntry & {
37
38
  lane_rose: boolean;
38
39
  lane_prev: string;
39
40
  lane_peak: string;
41
+ base_sha: string;
42
+ sensitive_paths: string[];
40
43
  blockers_open: number;
41
44
  heal_cycle: number;
42
45
  heal_max_cycles: number;
@@ -81,9 +84,14 @@ export function gitActor(cwd: string): string {
81
84
  return name || process.env.USER || process.env.USERNAME || '';
82
85
  }
83
86
 
84
- /** Read every JSON under `.mugiwara/<root>/<mission>/*.json`. Corrupt files are skipped. */
85
- function scan<T>(projectDir: string, root: string, map: (raw: Record<string, unknown>, member: string | null) => T): T[] {
86
- const base = join(projectDir, '.mugiwara', root);
87
+ /**
88
+ * Read every mission dir under `.mugiwara/missions/<mission>/`, picking the
89
+ * files this reader owns: state readers take `state.json` / `<member>.json`,
90
+ * continue readers take `continue.json` / `continue-<member>.json`. Corrupt
91
+ * files are skipped.
92
+ */
93
+ function scan<T>(projectDir: string, kind: 'state' | 'continue', map: (raw: Record<string, unknown>, member: string | null) => T): T[] {
94
+ const base = join(projectDir, '.mugiwara', 'missions');
87
95
  if (!existsSync(base)) return [];
88
96
  const out: T[] = [];
89
97
  const missions = readdirSync(base, { withFileTypes: true })
@@ -93,9 +101,18 @@ function scan<T>(projectDir: string, root: string, map: (raw: Record<string, unk
93
101
  for (const mission of missions) {
94
102
  const dir = join(base, mission);
95
103
  for (const f of readdirSync(dir).filter((n) => n.endsWith('.json')).sort()) {
96
- // state.json is the solo (member-less) file; <member>.json is a team member
97
104
  const stem = f.slice(0, -'.json'.length);
98
- const member = stem === 'state' ? null : stem;
105
+ // state files: `state` (solo) or `<member>`; continue files: `continue`
106
+ // (solo) or `continue-<member>`. Each scan ignores the other kind.
107
+ let member: string | null;
108
+ if (kind === 'state') {
109
+ if (stem === 'continue' || stem.startsWith('continue-')) continue;
110
+ member = stem === 'state' ? null : stem;
111
+ } else {
112
+ if (stem === 'continue') member = null;
113
+ else if (stem.startsWith('continue-')) member = stem.slice('continue-'.length);
114
+ else continue;
115
+ }
99
116
  if (member !== null && !isSafeKey(member)) continue;
100
117
  try {
101
118
  const raw = JSON.parse(readFileSync(join(dir, f), 'utf8')) as Record<string, unknown>;
@@ -145,8 +162,10 @@ export function readState(projectDir: string): StateEntry[] {
145
162
  lane_rose: r.lane_rose === true,
146
163
  lane_prev: text(r.lane_prev),
147
164
  lane_peak: text(r.lane_peak),
148
- next_action: '',
149
- next_session_prompt: '',
165
+ base_sha: text(r.base_sha),
166
+ sensitive_paths: Array.isArray(r.sensitive_paths) ? r.sensitive_paths.map(text).filter(Boolean) : [],
167
+ next_action: text(r.next_action),
168
+ next_session_prompt: text(r.next_session_prompt),
150
169
  updated_at: text(r.updated_at),
151
170
  blockers_open: num(r.blockers_open),
152
171
  heal_cycle: num(r.heal_cycle),
package/src/installer.ts CHANGED
@@ -161,10 +161,10 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
161
161
  result.notes.push(...post.notes);
162
162
  }
163
163
 
164
- // TASK 8: a fresh install must be immediately usable — write a default
165
- // .mugiwara/config (the same defaults `mugiwara onboard` would write) so no
166
- // key silently falls back. Only for project scope; global installs don't own
167
- // a project config. Never overwrite an existing config.
164
+ // A fresh install must be immediately usable — write a default
165
+ // .mugiwara/config so no key silently falls back. Only for project scope;
166
+ // global installs don't own a project config. Never overwrite an existing
167
+ // config.
168
168
  if (scope === 'project') {
169
169
  const configPath = join(projectDir, '.mugiwara', 'config');
170
170
  // lstat, not existsSync: a pre-created symlinked config must not be
@@ -181,13 +181,15 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
181
181
  'coverage_modified=80',
182
182
  'review_depth=full',
183
183
  'quality_depth=full',
184
+ 'verify_merged=off',
184
185
  'delegate_threshold=60',
185
186
  'heal_max_cycles=3',
186
187
  'verbosity=normal',
188
+ '# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)',
187
189
  ].join('\n') + '\n';
188
190
  if (!dryRun) { mkdirSync(dirname(configPath), { recursive: true }); writeFileSync(configPath, body); }
189
191
  result.written.push(configPath);
190
- result.notes.push(`default config written: ${configPath} (run \`mugiwara onboard\` to customise)`);
192
+ result.notes.push(`default config written: ${configPath} (edit it to customise)`);
191
193
  }
192
194
  }
193
195
  return result;
@@ -223,13 +225,14 @@ function assertNotSymlink(file: string): void {
223
225
 
224
226
  const GITIGNORE_BLOCK_START = '# >>> mugiwara >>>';
225
227
  const GITIGNORE_BLOCK_END = '# <<< mugiwara <<<';
226
- // legacy marker from v0.6.2 block was undelimited, single '# mugiwara'
227
- // header. Kept for detection so an old install is not double-appended.
228
- const GITIGNORE_MARKER = '# mugiwara';
229
- const GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
230
- # Ignore session state and regenerated files.
231
- .mugiwara/state/
232
- .mugiwara/continue/
228
+ // legacy markers pre-0.7 blocks ignored per-type dirs (.mugiwara/state/,
229
+ // .mugiwara/continue/) instead of the mission-first layout. Kept for
230
+ // detection so an old install is upgraded, not double-appended.
231
+ const GITIGNORE_LEGACY = ['.mugiwara/state/', '# mugiwara'];
232
+ const GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit plan.md, flows/, review.md,
233
+ # decisions.md, blockers.md, report.md under .mugiwara/missions/. Ignore session state.
234
+ .mugiwara/missions/**/*.json
235
+ .mugiwara/index.md
233
236
  .mugiwara/config
234
237
  .mugiwara/refs/
235
238
  # <<< mugiwara <<<
@@ -242,14 +245,14 @@ export function ensureProjectGitignore(projectDir: string, opts: { dryRun?: bool
242
245
  let existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
243
246
  if (existing) {
244
247
  // Delimited block present with the current entries → nothing to do.
245
- if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes('.mugiwara/state/')) {
248
+ if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes('.mugiwara/missions/**/*.json')) {
246
249
  return { appended: false, notes: [] };
247
250
  }
248
- // Legacy (v0.6.2) or outdated delimited block → upgrade in place:
249
- // strip the old mugiwara entries (both forms), then append the new block.
251
+ // Legacy (pre-0.7) or outdated delimited block → upgrade in place:
252
+ // strip the old mugiwara entries, then append the new block.
250
253
  // Without this, upgraded projects keep committing per-wave state/continue
251
254
  // JSON to git — session state must stay ignored (Robin MAJOR, Jinbe Low).
252
- const hadOld = existing.includes(GITIGNORE_MARKER) || existing.includes(GITIGNORE_BLOCK_START);
255
+ const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
253
256
  if (hadOld) {
254
257
  const clean = removeProjectGitignore(projectDir, { dryRun });
255
258
  existing = clean.removed ? (existsSync(path) ? readFileSync(path, 'utf8') : '') : existing;
@@ -281,20 +284,22 @@ export function removeProjectGitignore(projectDir: string, { dryRun = false }: {
281
284
  const end = current.indexOf(GITIGNORE_BLOCK_END);
282
285
  if (end < start) return { removed: false, notes: ['delimiter mismatch — .gitignore left untouched'] };
283
286
  cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
284
- } else if (current.includes(GITIGNORE_MARKER)) {
285
- // legacy (v0.6.2): strip the exact undelimited block — the header, the
286
- // explanatory comment, and the five known lines. Prefix-matching would
287
+ } else if (current.includes('# mugiwara')) {
288
+ // legacy (pre-0.7): strip the exact undelimited block — the header, the
289
+ // explanatory comment, and the known lines. Prefix-matching would
287
290
  // delete user-owned lines that merely start with a mugiwara path.
288
291
  const LEGACY_LINES = new Set([
289
292
  '.mugiwara/state.json',
290
293
  '.mugiwara/state-*.json',
291
294
  '.mugiwara/config',
295
+ '.mugiwara/state/',
296
+ '.mugiwara/continue/',
292
297
  '.mugiwara/continue.md',
293
298
  '.mugiwara/refs/',
294
299
  ]);
295
300
  const lines = current.split('\n').filter(l => {
296
301
  const t = l.trim();
297
- if (t.startsWith(GITIGNORE_MARKER)) return false; // header + comment
302
+ if (t.startsWith('# mugiwara')) return false; // header + comment
298
303
  return !LEGACY_LINES.has(t);
299
304
  });
300
305
  cleaned = lines.join('\n');