@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
@@ -1,61 +1,72 @@
1
1
  #!/usr/bin/env bun
2
- // scripts/verify-install.ts — G1: verify all references/*.md pointers resolve after install.
3
- // Installs to temp dir for one target per tier, then checks every pointer.
2
+ // scripts/verify-install.ts — G1: prove the shipped prose does not lie.
3
+ //
4
+ // Three checks, all of which failed silently before:
5
+ // A. pointer resolution — every `references/*.md` pointer resolves after install
6
+ // B. prose path validity — every `.mugiwara/…`, `scripts/…`, `docs/…` path a
7
+ // skill or agent NAMES either exists in the repo or matches a declared
8
+ // runtime shape. Four BLOCKER/MAJOR findings shared one root cause:
9
+ // nothing verified that a path named in prose actually exists.
10
+ // C. reference reachability — every reference file has an inbound pointer.
11
+ // Unreachable files still install into every project.
4
12
 
5
13
  import { existsSync, mkdtempSync, rmSync, readFileSync, readdirSync } from 'node:fs';
6
- import { join, dirname } from 'node:path';
14
+ import { join, dirname, basename, relative } from 'node:path';
7
15
  import { tmpdir } from 'node:os';
8
16
  import { installTo } from '../src/installer.ts';
9
- import { targets } from '../src/targets/index.ts';
17
+ import { targets, TARGET_IDS } from '../src/targets/index.ts';
10
18
 
11
- const tierTargets = [
12
- { id: 'claude', tier: 1 },
13
- { id: 'codex', tier: 2 },
14
- { id: 'kilo', tier: 3 },
15
- ];
19
+ const repoRoot = join(import.meta.dirname, '..');
20
+ const fail: string[] = [];
21
+
22
+ function findMd(root: string, out: string[] = []): string[] {
23
+ if (!existsSync(root)) return out;
24
+ for (const ent of readdirSync(root, { withFileTypes: true })) {
25
+ const p = join(root, ent.name);
26
+ if (ent.isDirectory()) findMd(p, out);
27
+ else if (ent.name.endsWith('.md')) out.push(p);
28
+ }
29
+ return out;
30
+ }
16
31
 
17
- let total = 0;
18
- let broken = 0;
32
+ // ---------------------------------------------------------------------------
33
+ // A. pointer resolution, across EVERY install target (was 3 of 9)
34
+ // ---------------------------------------------------------------------------
35
+ let pointers = 0;
36
+ let brokenPointers = 0;
19
37
 
20
- for (const tt of tierTargets) {
21
- const target = targets[tt.id];
22
- if (!target) { console.log(`⚠ target ${tt.id} not found, skipping`); continue; }
23
- const dir = mkdtempSync(join(tmpdir(), `mugi-verify-${tt.id}-`));
38
+ for (const id of TARGET_IDS) {
39
+ const target = targets[id];
40
+ const dir = mkdtempSync(join(tmpdir(), `mugi-verify-${id}-`));
24
41
  try {
25
42
  installTo(target, { scope: 'project', projectDir: dir, dryRun: false, force: true });
26
43
 
27
44
  const skillsDir = target.paths({ scope: 'project', projectDir: dir, home: '' }).skillsDir;
28
45
  const mugiwaraRefsDir = join(dir, '.mugiwara', 'refs');
29
46
 
30
- function findSkillFiles(root: string): string[] {
31
- const out: string[] = [];
32
- if (!existsSync(root)) return out;
33
- for (const ent of readdirSync(root, { withFileTypes: true })) {
34
- const p = join(root, ent.name);
35
- if (ent.isDirectory()) out.push(...findSkillFiles(p));
36
- else if (ent.name.endsWith('.md')) out.push(p);
37
- }
38
- return out;
39
- }
40
-
41
- const skillFiles = findSkillFiles(skillsDir)
42
- .filter(f => !f.replace(skillsDir, '').includes('/references/'));
47
+ const skillFiles = findMd(skillsDir).filter((f) => !f.replace(skillsDir, '').includes('/references/'));
43
48
 
44
49
  for (const file of skillFiles) {
45
- const body = readFileSync(file, 'utf8');
46
- const refs = [...body.matchAll(/`([^`]*references\/[^`]+\.md)`/g)];
50
+ // Targets emit skills either as `<skillsDir>/<name>/SKILL.md` (claude) or
51
+ // as a flat `<skillsDir>/<name>.instructions.md` (copilot). Both forms
52
+ // must yield the skill name, because non-native targets store references
53
+ // per-skill under `.mugiwara/refs/<name>/`.
54
+ const relSkill = relative(skillsDir, file);
55
+ const skillName = relSkill.includes('/') ? relSkill.split('/')[0] : basename(relSkill).split('.')[0];
47
56
 
48
- for (const m of refs) {
49
- total++;
57
+ for (const m of readFileSync(file, 'utf8').matchAll(/`([^`]*references\/[^`]+\.md)`/g)) {
58
+ pointers++;
50
59
  const pointer = m[1];
51
- const localResolve = join(dirname(file), pointer);
52
- const mugiwaraResolve = join(mugiwaraRefsDir, pointer.replace(/^(?:_shared\/)?references\//, ''));
53
- const resolved = [localResolve, mugiwaraResolve].find(p => existsSync(p));
54
-
55
- if (!resolved) {
56
- broken++;
57
- const relFile = file.replace(dir + '/', '');
58
- console.log(`✗ ${relFile}: \`${pointer}\` → not found`);
60
+ const candidates = [
61
+ join(dirname(file), pointer),
62
+ join(mugiwaraRefsDir, pointer.replace(/^(?:_shared\/)?references\//, '')),
63
+ ...(target.refsDir
64
+ ? [join(target.refsDir({ scope: 'project', projectDir: dir, home: '' }, skillName), basename(pointer))]
65
+ : []),
66
+ ];
67
+ if (!candidates.some((p) => existsSync(p))) {
68
+ brokenPointers++;
69
+ fail.push(`✗ [${id}] ${file.replace(dir + '/', '')}: \`${pointer}\` → not found`);
59
70
  }
60
71
  }
61
72
  }
@@ -64,9 +75,110 @@ for (const tt of tierTargets) {
64
75
  }
65
76
  }
66
77
 
67
- if (broken > 0) {
68
- console.log(`\n✗ ${broken}/${total} pointers cannot resolve after install`);
69
- process.exit(1);
78
+ // ---------------------------------------------------------------------------
79
+ // B. prose path validity
80
+ // ---------------------------------------------------------------------------
81
+ // `.mugiwara/**` paths are written at RUNTIME — they never exist in a clone
82
+ // (`.mugiwara/` is gitignored), so existence proves nothing. What can be
83
+ // verified is the SHAPE: prose must name the same filename template the
84
+ // scripts actually write. This is the check that would have caught
85
+ // savepoint.sh globbing `issues/<mission>-blockers.md` while 15 prose sites
86
+ // mandated the dated `issues/YYYY-MM-DD-<mission>-blockers.md`.
87
+ const RUNTIME_SHAPES: RegExp[] = [
88
+ /^\.mugiwara\/?(\*\*)?$/, // bare mention of the dir
89
+ /^~?\/?\.mugiwara\/config$/, // project + global config
90
+ /^\.mugiwara\/lessons\.md$/,
91
+ /^\.mugiwara\/index\.md$/,
92
+ /^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/(plan|spec|decisions|blockers|review|security|report)\.md$/,
93
+ /^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/flows\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/,
94
+ /^\.mugiwara\/missions\/<mission>\/flows\/?$/,
95
+ /^\.mugiwara\/missions\/(<mission>|[a-z0-9._-]+)\/waves\/([0-9]{2}-[a-z-]+|eval|resume|todos)\.md$/, // legacy layout, still read
96
+ /^\.mugiwara\/missions\/<mission>\/waves\/?$/, // legacy layout, still read
97
+ /^\.mugiwara\/missions\/<mission>\/(state\.json|<member>\.json)$/,
98
+ /^\.mugiwara\/missions\/<mission>\/(continue\.json|continue-<member>\.json)$/,
99
+ ];
100
+
101
+ // Shape violations that exist today and are NOT fixed in this mission because
102
+ // another worker holds `content/**` prose this wave. Ratchet: a NEW violation
103
+ // fails the gate; these two are reported, then must be deleted from this list.
104
+ // Prose paths whose SHAPE is known-wrong but not yet fixed. Reported every run
105
+ // so they cannot rot quietly; emptied as each is repaired. Keep this empty —
106
+ // an entry here is a defect with a due date, never a permanent exemption.
107
+ const KNOWN_SHAPE_DRIFT = new Map<string, string>([]);
108
+
109
+ /** `[label](path)`, `bun scripts/x.ts --flag`, trailing punctuation → the bare path. */
110
+ function normalizePath(raw: string): string {
111
+ let s = raw.trim();
112
+ const link = s.match(/^\[[^\]]*\]\(([^)]+)\)$/);
113
+ if (link) s = link[1];
114
+ s = s.replace(/^(?:bun|bash|sh|npx|node)\s+/, '');
115
+ s = s.split(/\s/)[0];
116
+ return s.replace(/[.,;:]$/, '');
117
+ }
118
+
119
+ const PROSE_PATH = /`([^`\n]*?(?:\.mugiwara\/|scripts\/|docs\/)[^`\n]*?)`/g;
120
+ const proseFiles = [...findMd(join(repoRoot, 'content')), ...findMd(join(repoRoot, 'references'))];
121
+
122
+ let prosePaths = 0;
123
+ const drift: string[] = [];
124
+ for (const file of proseFiles) {
125
+ const rel = relative(repoRoot, file);
126
+ readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
127
+ for (const m of line.matchAll(PROSE_PATH)) {
128
+ const p = normalizePath(m[1]);
129
+ if (!p.includes('/')) continue;
130
+ prosePaths++;
131
+ const at = `${rel}:${i + 1}`;
132
+
133
+ if (/(^|\/)\.mugiwara\//.test(p) || p === '.mugiwara/' || p === '.mugiwara/**') {
134
+ if (RUNTIME_SHAPES.some((re) => re.test(p))) continue;
135
+ if (KNOWN_SHAPE_DRIFT.has(p)) { drift.push(` ${at}: \`${p}\` — ${KNOWN_SHAPE_DRIFT.get(p)}`); continue; }
136
+ fail.push(`✗ ${at}: \`${p}\` → not a declared runtime path shape`);
137
+ continue;
138
+ }
139
+ // scripts/ and docs/ are SHIPPED files — they must exist in the repo
140
+ if (!existsSync(join(repoRoot, p))) fail.push(`✗ ${at}: \`${p}\` → file does not exist`);
141
+ }
142
+ });
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // C. reference reachability (file → pointer, the inverse of check A)
147
+ // ---------------------------------------------------------------------------
148
+ // Hard zero. The 18 orphans this ratchet was holding were all substantive —
149
+ // worksheets, rubrics and worked examples their own skill visibly needed — so
150
+ // every one was wired to its parent section rather than deleted. With the
151
+ // backlog at zero the ratchet becomes what it should always have been: a new
152
+ // reference file must be pointed at by the prose that needs it, or it is not
153
+ // a reference, it is dead weight shipped to every project.
154
+ const ORPHAN_BASELINE = 0;
155
+
156
+ const corpus = proseFiles.map((f) => ({ file: f, body: readFileSync(f, 'utf8') }));
157
+ const refFiles = proseFiles.filter((f) => f.includes('/references/') || relative(repoRoot, f).startsWith('references/'));
158
+ const orphans: string[] = [];
159
+ for (const rf of refFiles) {
160
+ const needle = `references/${basename(rf)}`;
161
+ const reachable = corpus.some((c) => c.file !== rf && c.body.includes(needle));
162
+ if (!reachable) orphans.push(relative(repoRoot, rf));
163
+ }
164
+ if (orphans.length > ORPHAN_BASELINE) {
165
+ fail.push(
166
+ `✗ ${orphans.length} unreachable reference files (baseline ${ORPHAN_BASELINE}) — a new orphan was added:\n` +
167
+ orphans.map((o) => ` ${o}`).join('\n'),
168
+ );
70
169
  }
71
170
 
72
- console.log(`✓ ${total} pointers checked across ${tierTargets.length} targets — all resolve`);
171
+ // ---------------------------------------------------------------------------
172
+ console.log(` ${pointers} pointers checked across ${TARGET_IDS.length} targets`);
173
+ console.log(` ${prosePaths} prose paths checked in ${proseFiles.length} files`);
174
+ console.log(` ${orphans.length}/${refFiles.length} reference files unreachable (baseline ${ORPHAN_BASELINE})`);
175
+ if (orphans.length) orphans.forEach((o) => console.log(` orphan: ${o}`));
176
+ if (drift.length) { console.log(' known prose-path drift (owned elsewhere, must be fixed):'); drift.forEach((d) => console.log(d)); }
177
+
178
+ if (fail.length) {
179
+ console.log('');
180
+ fail.forEach((f) => console.log(f));
181
+ console.log(`\n✗ verify-install: ${fail.length} problem(s) (${brokenPointers} broken pointers)`);
182
+ process.exit(1);
183
+ }
184
+ console.log('✓ verify-install: pointers resolve, prose paths valid, no new orphans');
package/src/args.ts CHANGED
@@ -6,10 +6,10 @@ export type Args = {
6
6
  flags: Record<string, FlagValue>;
7
7
  };
8
8
 
9
- const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target' };
9
+ const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
- '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check',
12
+ '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',
13
13
  '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
14
  };
15
15
 
package/src/budget.ts ADDED
@@ -0,0 +1,56 @@
1
+ // src/budget.ts
2
+ // Context budget as a gate: a mission's trail is itself
3
+ // context the next reader must load. At archive time, measure the total
4
+ // footprint of the artifacts that survive and compare against the configured
5
+ // ceiling — a bloated trail is caught like a failed test, with a visible
6
+ // number in the report.
7
+ //
8
+ // Token telemetry stays honest elsewhere: the estimator remains the default;
9
+ // `tokens_source: reported` activates only where the harness exposes real
10
+ // usage (see docs/concepts/cost.md). This module does not estimate tokens.
11
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+
15
+ export function readBudgetConfig(projectDir: string): number {
16
+ for (const base of [projectDir, homedir()]) {
17
+ const file = join(base, '.mugiwara', 'config');
18
+ if (!existsSync(file)) continue;
19
+ for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
20
+ const t = line.trim();
21
+ if (!t || t.startsWith('#')) continue;
22
+ const eq = t.indexOf('=');
23
+ if (eq === -1) continue;
24
+ if (t.slice(0, eq).trim() !== 'context_budget_chars') continue;
25
+ const n = Number(t.slice(eq + 1).trim());
26
+ return Number.isFinite(n) && n > 0 ? n : 0;
27
+ }
28
+ }
29
+ return 0; // unset — measurement still recorded, never enforced
30
+ }
31
+
32
+ /** Sum of bytes across the trail: top-level *.md + flows/* (legacy waves/* counts too). */
33
+ export function measureContextChars(missionDir: string): number {
34
+ let total = 0;
35
+ const add = (p: string): void => {
36
+ try { total += statSync(p).size; } catch { /* vanished mid-measure */ }
37
+ };
38
+ for (const f of readdirSync(missionDir)) {
39
+ if (/\.md$/.test(f)) add(join(missionDir, f));
40
+ }
41
+ for (const sub of ['flows', 'waves']) {
42
+ const dir = join(missionDir, sub);
43
+ if (existsSync(dir)) {
44
+ for (const f of readdirSync(dir)) add(join(dir, f));
45
+ }
46
+ }
47
+ return total;
48
+ }
49
+
50
+ export function formatFootprint(chars: number, budget: number): string {
51
+ const base = `Context footprint: ${chars} chars`;
52
+ if (!budget) return `${base} (no budget configured)`;
53
+ return chars > budget
54
+ ? `${base} — OVER budget ${budget}`
55
+ : `${base} (budget ${budget})`;
56
+ }
package/src/cli.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // src/cli.ts
3
- import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
3
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { execFileSync } from 'node:child_process';
4
5
  import { homedir } from 'node:os';
5
6
  import { dirname, join, resolve } from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
@@ -10,6 +11,10 @@ import { targets, TARGET_IDS } from './targets/index.ts';
10
11
  import { installTo, removeInstalled, VERSION, ensureProjectGitignore, removeProjectGitignore } from './installer.ts';
11
12
  import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
12
13
  import { resetMission, archiveMission } from './mission.ts';
14
+ import { runScript, RUNNABLE } from './run.ts';
15
+ import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor } from './continue.ts';
16
+ import { blamePath } from './provenance.ts';
17
+ import { signReport, verifyReport } from './sign.ts';
13
18
 
14
19
  const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
15
20
  const flag = (v: FlagValue): boolean => v === true;
@@ -25,6 +30,14 @@ export async function run(argv: string[]): Promise<void> {
25
30
  case 'list': return list(flags);
26
31
  case 'reset': return resetCmd(flags);
27
32
  case 'archive': return archive(flags, _);
33
+ case 'clean': return cleanCmd(flags);
34
+ case 'continue': return continueCmd(flags, _);
35
+ case 'status': return statusCmd(flags);
36
+ case 'run': return runCmd(flags, _);
37
+ case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
38
+ case 'blame': return blameCmd(flags, _);
39
+ case 'handoff': return handoffCmd(flags, _);
40
+ case 'sign': return signCmd(flags, _);
28
41
  default: throw new Error(`Unknown command: ${command}`);
29
42
  }
30
43
  }
@@ -48,11 +61,74 @@ function archive(flags: Args['flags'], positionals: string[]): void {
48
61
  if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
49
62
  const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
50
63
  if (result.report) console.log(`archive target: ${result.report}`);
64
+ else console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
51
65
  if (result.removed.length) console.log(`${flag(flags.dryRun) ? 'would remove' : 'removed'}: ${result.removed.join(', ')}`);
52
66
  if (result.kept.length) console.log(`kept: ${result.kept.join(', ')}`);
53
67
  if (result.index) console.log(`index updated: ${result.index}`);
54
68
  }
55
69
 
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 {
78
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
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
+ }
128
+ }
129
+
130
+
131
+
56
132
  async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; projectDir: string; targetIds: string[] }> {
57
133
  const interactive = !flag(flags.yes);
58
134
  if (interactive && !process.stdin.isTTY) {
@@ -123,6 +199,8 @@ async function install(flags: Args['flags']): Promise<void> {
123
199
  });
124
200
  console.log(`\nOK mugiwara ${VERSION} installed (manifest: ${file})`);
125
201
  if (allNotes.length) console.log(`${allNotes.length} note(s) above may need attention.`);
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).');
126
204
  }
127
205
 
128
206
  async function uninstall(flags: Args['flags']): Promise<void> {
@@ -150,6 +228,16 @@ async function uninstall(flags: Args['flags']): Promise<void> {
150
228
  if (!ok) { console.log('Aborted.'); return; }
151
229
  }
152
230
  const removed = removeInstalled(manifest, { dryRun: flag(flags.dryRun) });
231
+ // Un-merge anything we injected into files the user owns. These are
232
+ // deliberately absent from the manifest — deleting them would destroy the
233
+ // user's own configuration alongside ours.
234
+ for (const id of manifest.targets) {
235
+ const t = targets[id];
236
+ if (!t?.postUninstall) continue;
237
+ const post = t.postUninstall({ scope, projectDir, home, dryRun: flag(flags.dryRun) });
238
+ for (const f of post.changed) console.log(` unwired mugiwara hooks from ${f}`);
239
+ for (const n of post.notes) console.log(` note: ${n}`);
240
+ }
153
241
  if (!flag(flags.dryRun)) {
154
242
  if (scope === 'project') {
155
243
  const gi = removeProjectGitignore(projectDir);
@@ -192,6 +280,168 @@ function list(flags: Args['flags']): void {
192
280
  if (!found) console.log('No mugiwara installation found.');
193
281
  }
194
282
 
283
+ /**
284
+ * `mugiwara continue [mission] [member]` — the deterministic half of resume.
285
+ *
286
+ * Selecting which mission/member to resume is a directory scan, not a judgement
287
+ * call, so it runs here instead of costing the host model a reasoning turn.
288
+ * Only the last step (verifying next_action against the plan) needs a model,
289
+ * and that happens after this prints.
290
+ *
291
+ * Exit codes: 0 = a single resume point was printed; 2 = ambiguous or absent,
292
+ * the caller must stop and let the user pick.
293
+ */
294
+ function continueCmd(flags: Args['flags'], positionals: string[]): void {
295
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
296
+ const [mission, member] = positionals.slice(1);
297
+ let entries = readContinue(projectDir);
298
+
299
+ // default to this actor's work; --all crosses actors on a shared checkout
300
+ if (!flag(flags.all)) {
301
+ const actor = gitActor(projectDir);
302
+ const mine = entries.filter((e) => e.actor === actor);
303
+ // an actor-less savepoint (older file, or git identity unset) is still the
304
+ // only thing on disk — showing nothing would look like "no missions"
305
+ if (mine.length) entries = mine;
306
+ }
307
+
308
+ const r = resolveContinue(entries, mission, member);
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
+ }
316
+
317
+ if (r.kind === 'none') {
318
+ console.log('No mission in flight. Start one with Flow 0 triage (mugiwara-orchestration).');
319
+ } else if (r.kind === 'missions') {
320
+ console.log(`${new Set(r.entries.map((e) => e.mission)).size} missions in flight:\n`);
321
+ console.log(formatTable(r.entries));
322
+ console.log('\nPick one: mugiwara continue <mission> [member]');
323
+ } else if (r.kind === 'members') {
324
+ console.log(`Mission "${r.mission}" has ${r.entries.length} members in flight:\n`);
325
+ console.log(formatTable(r.entries));
326
+ console.log(`\nPick one: mugiwara continue ${r.mission} <member>`);
327
+ } else if (r.kind === 'unknown-mission') {
328
+ console.error(`No in-flight mission "${r.mission}". Known: ${r.known.join(', ') || '(none)'}`);
329
+ } else {
330
+ console.error(`Mission "${r.mission}" has no member "${r.member}". Known: ${r.known.join(', ')}`);
331
+ }
332
+ process.exit(2);
333
+ }
334
+
335
+ /** `mugiwara status` — one screen of computed mission state, no model needed. */
336
+ function statusCmd(flags: Args['flags']): void {
337
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
338
+ const states = readState(projectDir);
339
+ if (!states.length) { console.log('No mission state on disk.'); return; }
340
+ const actor = flag(flags.all) ? null : gitActor(projectDir);
341
+ const rows = actor ? (states.filter((s) => s.actor === actor).length ? states.filter((s) => s.actor === actor) : states) : states;
342
+ for (const s of rows) {
343
+ const scope = s.member ? ` [${s.member}]` : '';
344
+ console.log(`${s.mission}${scope}`);
345
+ 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}`);
346
+ console.log(` blockers ${s.blockers_open} · heal cycle ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? ' — HALT' : ''} · files touched ${s.files_touched}`);
347
+ if (s.budget) console.log(` tokens ${s.tokens_est}/${s.budget} (${s.budget_status})${s.delegate_due ? ' · delegate due' : ''}`);
348
+ console.log(` branch ${s.branch} · updated ${s.updated_at}`);
349
+ if (s.evidence.length) console.log(` evidence: ${s.evidence.join(', ')}`);
350
+ }
351
+ }
352
+
353
+ /** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
354
+ function runCmd(flags: Args['flags'], positionals: string[]): void {
355
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
356
+ const name = positionals[1];
357
+ if (!name) {
358
+ console.error(`usage: mugiwara run <script> [args...]\n scripts: ${RUNNABLE.join(', ')}`);
359
+ process.exit(1);
360
+ }
361
+ const code = runScript(name, positionals.slice(2), projectDir);
362
+ if (code !== 0) process.exit(code);
363
+ }
364
+
365
+ /** `mugiwara blame <path>` — provenance note on the last commit touching path. */
366
+ function blameCmd(flags: Args['flags'], positionals: string[]): void {
367
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
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;
388
+ }
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; }
396
+ }
397
+
398
+ /** `mugiwara handoff <mission>` — a report the next engineer can act on. */
399
+ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
400
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
401
+ const mission = positionals[1];
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: ', '')} |`);
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
+ }
432
+
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);
443
+ }
444
+
195
445
  function help(): void {
196
446
  console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
197
447
 
@@ -201,8 +451,23 @@ Usage:
201
451
  mugiwara uninstall remove installed files via manifest
202
452
  mugiwara list show installations
203
453
  mugiwara list --check health check: show installations + missing files
204
- mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
205
- mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
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)
458
+ mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
459
+ mugiwara continue <m> [member]
460
+ print the exact resume point for that mission/member
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)
467
+ mugiwara run <script> [args...]
468
+ run a bundled harness script here (${RUNNABLE.join(', ')})
469
+ mugiwara savepoint <mission> [member] [flow] [mode]
470
+ shorthand for: mugiwara run savepoint.sh ...
206
471
  mugiwara --help this help
207
472
  mugiwara --version print version
208
473
 
@@ -214,7 +479,10 @@ Flags:
214
479
  --force overwrite differing files (with backup)
215
480
  --dry-run print actions without writing
216
481
  --check with list: report missing files (health check)
217
- --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)`);
218
486
  }
219
487
 
220
488
  const isMain = process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]);