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