@ionivetech/mugiwara 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -9
- package/.codex-plugin/plugin.json +2 -8
- package/.cursor-plugin/plugin.json +2 -8
- package/.kimi-plugin/plugin.json +2 -8
- package/.opencode/commands/mugiwara-continue.md +44 -31
- package/.opencode/commands/mugiwara-review.md +1 -1
- package/.opencode/commands/mugiwara-security.md +1 -1
- package/.opencode/commands/mugiwara.md +2 -2
- package/.opencode/plugins/mugiwara.mjs +3 -19
- package/AGENTS.md +6 -6
- package/GEMINI.md +3 -3
- package/README.md +82 -74
- package/content/agents/brook-healing.md +12 -11
- package/content/agents/chopper-checkpoint.md +14 -13
- package/content/agents/eval-runner.md +6 -9
- package/content/agents/franky-gates.md +7 -10
- package/content/agents/jinbe-security.md +10 -14
- package/content/agents/luffy-orchestrator.md +20 -22
- package/content/agents/memory-keeper.md +9 -12
- package/content/agents/nami-planner.md +7 -10
- package/content/agents/resume-coordinator.md +12 -15
- package/content/agents/robin-reviewer.md +13 -12
- package/content/agents/sanji-quality.md +9 -12
- package/content/agents/skeptic-verifier.md +7 -10
- package/content/agents/usopp-brainstorm.md +6 -9
- package/content/agents/zoro-execution.md +9 -12
- package/content/skills/mugiwara-backend/SKILL.md +4 -0
- package/content/skills/mugiwara-backend/references/database.md +61 -0
- package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
- package/content/skills/mugiwara-checkpoint/SKILL.md +17 -14
- package/content/skills/mugiwara-checkpoint/references/ledger-format.md +5 -5
- package/content/skills/mugiwara-contract-first/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +24 -24
- package/content/skills/mugiwara-execution/references/dispatch.md +3 -3
- package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
- package/content/skills/mugiwara-frontend/SKILL.md +5 -6
- package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
- package/content/skills/mugiwara-gates/SKILL.md +14 -10
- package/content/skills/mugiwara-git/SKILL.md +7 -2
- package/content/skills/mugiwara-healing/SKILL.md +7 -5
- package/content/skills/mugiwara-healing/references/workers.md +2 -2
- package/content/skills/mugiwara-lessons/SKILL.md +2 -2
- package/content/skills/mugiwara-orchestration/SKILL.md +28 -26
- package/content/skills/mugiwara-orchestration/references/check-ins.md +17 -16
- package/content/skills/mugiwara-orchestration/references/closure.md +18 -13
- package/content/skills/mugiwara-orchestration/references/output-contract.md +9 -9
- package/content/skills/mugiwara-orchestration/references/triage-escalation.md +32 -22
- package/content/skills/mugiwara-planning/SKILL.md +5 -18
- package/content/skills/mugiwara-planning/references/plan-template.md +3 -3
- package/content/skills/mugiwara-quality/SKILL.md +10 -7
- package/content/skills/mugiwara-resume/SKILL.md +20 -22
- package/content/skills/mugiwara-review/SKILL.md +7 -2
- package/content/skills/mugiwara-review/references/five-axis-worksheet.md +1 -1
- package/content/skills/mugiwara-root-cause/SKILL.md +2 -0
- package/content/skills/mugiwara-security/SKILL.md +6 -2
- package/content/skills/mugiwara-ship/SKILL.md +2 -2
- package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
- package/content/skills/mugiwara-testcases/SKILL.md +5 -3
- package/content/skills/mugiwara-workflow/SKILL.md +32 -35
- package/content/skills/mugiwara-workflow/references/workspace-layout.md +41 -40
- package/dist/mugiwara.js +1370 -153
- package/gemini-extension.json +1 -1
- package/hooks/auto-savepoint.js +114 -0
- package/hooks/auto-savepoint.ts +102 -0
- package/hooks/engagement-marker.js +59 -0
- package/hooks/engagement-marker.ts +97 -0
- package/hooks/hooks.json +41 -2
- package/hooks/mugiwara-mode-tracker.js +79 -0
- package/hooks/pipeline-guard.js +186 -0
- package/hooks/pipeline-guard.ts +270 -0
- package/hooks/session-start.js +109 -0
- package/hooks/session-start.ts +62 -45
- package/package.json +7 -3
- package/plugin.json +1 -1
- package/references/agent-protocol.md +15 -0
- package/references/complexity.md +77 -0
- package/references/definition-of-done.md +1 -1
- package/references/multi-actor.md +4 -7
- package/references/prose-style.md +54 -0
- package/references/skill-versioning.md +1 -1
- package/references/wave-banners.md +16 -16
- package/scripts/build-hooks.ts +56 -0
- package/scripts/check-doc-links.ts +40 -0
- package/scripts/conformance.ts +23 -16
- package/scripts/coverage-gate.ts +193 -0
- package/scripts/gate-selftest.ts +118 -7
- package/scripts/install.ps1 +5 -4
- package/scripts/install.sh +4 -4
- package/scripts/lane.sh +56 -6
- package/scripts/lib/patterns.sh +89 -0
- package/scripts/policy-force.ts +22 -0
- package/scripts/retrieval-eval.ts +1 -1
- package/scripts/savepoint.sh +249 -67
- package/scripts/validate-content.ts +22 -9
- package/scripts/verify-install.ts +156 -44
- package/src/args.ts +2 -2
- package/src/budget.ts +56 -0
- package/src/cli.ts +272 -4
- package/src/continue.ts +262 -0
- package/src/installer.ts +59 -15
- package/src/integrity.ts +158 -0
- package/src/mission.ts +228 -95
- package/src/policy.ts +156 -0
- package/src/provenance.ts +116 -0
- package/src/rollback.ts +95 -0
- package/src/routing.ts +69 -0
- package/src/run.ts +82 -0
- package/src/sign.ts +61 -0
- package/src/targets/claude.ts +140 -2
- package/src/targets/copilot.ts +20 -1
- package/src/targets/generic.ts +9 -4
- package/src/targets/opencode.ts +0 -1
- package/.opencode/commands/mugiwara-execute.md +0 -13
- package/.opencode/commands/mugiwara-heal.md +0 -13
- package/.opencode/commands/mugiwara-onboard.md +0 -15
- package/.opencode/commands/mugiwara-plan.md +0 -17
- package/.opencode/commands/mugiwara-ship.md +0 -13
- package/.opencode/commands/using-mugiwara.md +0 -20
- package/content/agents/onboarding-guide.md +0 -124
- package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
- package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
- package/content/skills/mugiwara-context-budget/SKILL.md +0 -64
- package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
- package/content/skills/mugiwara-pr/SKILL.md +0 -69
- package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
- package/content/skills/mugiwara-security/references/owasp-mapping.md +0 -30
- package/content/skills/mugiwara-sunset/SKILL.md +0 -82
- package/content/skills/using-mugiwara/SKILL.md +0 -51
- package/references/token-budget.md +0 -34
- package/scripts/evidence.sh +0 -81
- package/scripts/initiative.ts +0 -296
- package/scripts/mission-report.sh +0 -292
- package/scripts/onboard.ts +0 -266
- package/scripts/probe.ts +0 -40
|
@@ -1,61 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// scripts/verify-install.ts — G1:
|
|
3
|
-
//
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
21
|
-
const target = targets[
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
|
49
|
-
|
|
57
|
+
for (const m of readFileSync(file, 'utf8').matchAll(/`([^`]*references\/[^`]+\.md)`/g)) {
|
|
58
|
+
pointers++;
|
|
50
59
|
const pointer = m[1];
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
205
|
-
mugiwara archive <m>
|
|
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
|
-
|
|
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]);
|