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