@ionivetech/mugiwara 0.8.0 → 0.8.2
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 +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +4 -3
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-gates/SKILL.md +6 -0
- package/content/skills/mugiwara-healing/SKILL.md +5 -1
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +7 -6
- package/content/skills/mugiwara-planning/SKILL.md +2 -0
- package/content/skills/mugiwara-quality/SKILL.md +3 -14
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -14
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +3 -15
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +1 -0
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +28 -25
- package/dist/mugiwara.js +1323 -402
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/wave-banners.md +1 -2
- package/scripts/gate-selftest.ts +239 -21
- package/scripts/lane-base.ts +16 -0
- package/scripts/lane.sh +5 -1
- package/scripts/lib/lane-base.sh +1 -1
- package/scripts/savepoint.sh +48 -5
- package/scripts/validate-content.ts +60 -0
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/budget.ts +11 -0
- package/src/cli.ts +185 -28
- package/src/config.ts +6 -0
- package/src/continue.ts +36 -1
- package/src/cost.ts +4 -1
- package/src/installer.ts +27 -4
- package/src/integrity.ts +105 -25
- package/src/mission.ts +123 -7
- package/src/policy.ts +372 -4
- package/src/provenance.ts +29 -9
- package/src/sign.ts +45 -3
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
- package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +0 -14
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/cli.ts
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -12,7 +12,7 @@ import { installTo, removeInstalled, VERSION, ensureProjectGitignore, removeProj
|
|
|
12
12
|
import { manifestPath, readManifest, writeManifest, type Scope } from './manifest.ts';
|
|
13
13
|
import { resetMission, archiveMission } from './mission.ts';
|
|
14
14
|
import { runScript, RUNNABLE } from './run.ts';
|
|
15
|
-
import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor } from './continue.ts';
|
|
15
|
+
import { readContinue, readState, resolveContinue, formatTable, formatResume, gitActor, hasLegacyLayout, CURRENT_SCHEMA_VERSION, unreadableStateFiles } from './continue.ts';
|
|
16
16
|
import { blamePath } from './provenance.ts';
|
|
17
17
|
import { signReport, verifyReport, ensurePureKey, hasMinisign } from './sign.ts';
|
|
18
18
|
import { ensureConfig } from './config.ts';
|
|
@@ -20,31 +20,50 @@ import { costEnvelope } from './cost.ts';
|
|
|
20
20
|
import { computeLiveSlop } from './slop.ts';
|
|
21
21
|
import { loadRegistry } from './evidence.ts';
|
|
22
22
|
import { buildCostLedger, toCostJSON } from './reporting.ts';
|
|
23
|
+
import { enforceHarnessPolicy } from './policy.ts';
|
|
23
24
|
|
|
24
25
|
const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
25
26
|
const flag = (v: FlagValue): boolean => v === true;
|
|
26
27
|
|
|
28
|
+
// Anchor to the repo root so running from a package subdirectory does not
|
|
29
|
+
// create a shadow .mugiwara/ there. (B4)
|
|
30
|
+
function resolveProjectDir(explicit?: string): string {
|
|
31
|
+
if (explicit) return resolve(explicit);
|
|
32
|
+
try {
|
|
33
|
+
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
|
|
34
|
+
if (root) return root;
|
|
35
|
+
} catch { /* not a git repo — fall through */ }
|
|
36
|
+
return process.cwd();
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
export async function run(argv: string[]): Promise<void> {
|
|
28
40
|
const { command, flags, _ } = parseArgs(argv);
|
|
29
41
|
if (flag(flags.help) || command === 'help') return help();
|
|
30
42
|
if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
// harness.require_enforcement — enterprise gate: refuse rules-based harnesses
|
|
44
|
+
// (only opencode is runtime-enforced). Covers run/savepoint/archive/status
|
|
45
|
+
// + other workflow commands; install/update/uninstall/list are setup and bypass.
|
|
46
|
+
{
|
|
47
|
+
const bypass = new Set(['install', 'update', 'uninstall', 'list']);
|
|
48
|
+
if (!bypass.has(command)) {
|
|
49
|
+
const projectDirForHarness = resolveProjectDir(str(flags.project));
|
|
50
|
+
enforceHarnessPolicy(projectDirForHarness);
|
|
51
|
+
}
|
|
36
52
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
53
|
+
// Bootstrap default .mugiwara/config on any command when missing — including
|
|
54
|
+
// `continue`/`status` — so tier-3 agents that only mkdir .mugiwara still get
|
|
55
|
+
// a config. Also covered by readConfig() auto-create for non-CLI entry.
|
|
56
|
+
// Skipped for install/update --dry-run: a dry run must not mutate the project.
|
|
41
57
|
const isDryRunInstall = (command === 'install' || command === 'update') && flag(flags.dryRun);
|
|
42
58
|
if (!isDryRunInstall) {
|
|
43
|
-
const projectDir =
|
|
59
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
44
60
|
if (ensureConfig(projectDir)) {
|
|
45
61
|
console.log(`default .mugiwara/config written at ${join(projectDir, '.mugiwara', 'config')} (edit it to customise)`);
|
|
46
62
|
}
|
|
47
63
|
}
|
|
64
|
+
if (command === 'continue' || command === 'status') {
|
|
65
|
+
return command === 'continue' ? continueCmd(flags, _) : statusCmd(flags);
|
|
66
|
+
}
|
|
48
67
|
switch (command) {
|
|
49
68
|
case 'install': return install(flags);
|
|
50
69
|
case 'update': return install({ ...flags, force: true });
|
|
@@ -61,12 +80,13 @@ export async function run(argv: string[]): Promise<void> {
|
|
|
61
80
|
case 'blame': return blameCmd(flags, _);
|
|
62
81
|
case 'handoff': return handoffCmd(flags, _);
|
|
63
82
|
case 'sign': return signCmd(flags, _);
|
|
83
|
+
case 'migrate': return migrateCmd(flags);
|
|
64
84
|
default: throw new Error(`Unknown command: ${command}`);
|
|
65
85
|
}
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
function resetCmd(flags: Args['flags']): void {
|
|
69
|
-
const projectDir =
|
|
89
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
70
90
|
const force = flag(flags.force);
|
|
71
91
|
const result = resetMission(projectDir, flag(flags.keepLogs), force);
|
|
72
92
|
if (result.blocked) {
|
|
@@ -79,7 +99,7 @@ function resetCmd(flags: Args['flags']): void {
|
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
function archive(flags: Args['flags'], positionals: string[]): void {
|
|
82
|
-
const projectDir =
|
|
102
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
83
103
|
const mission = positionals[1];
|
|
84
104
|
if (!mission) { console.error('usage: mugiwara archive <mission> [--project <dir>] [--dry-run]'); process.exit(1); }
|
|
85
105
|
const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
|
|
@@ -98,7 +118,7 @@ function archive(flags: Args['flags'], positionals: string[]): void {
|
|
|
98
118
|
* touched before that date.
|
|
99
119
|
*/
|
|
100
120
|
function cleanCmd(flags: Args['flags']): void {
|
|
101
|
-
const projectDir =
|
|
121
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
102
122
|
const dryRun = flag(flags.dryRun);
|
|
103
123
|
const root = join(projectDir, '.mugiwara', 'missions');
|
|
104
124
|
if (!existsSync(root)) { console.log('nothing to clean (.mugiwara/missions/ does not exist).'); return; }
|
|
@@ -164,7 +184,7 @@ async function resolveOptions(flags: Args['flags']): Promise<{ scope: Scope; pro
|
|
|
164
184
|
if (!interactive) { scope = 'project'; }
|
|
165
185
|
else scope = (await choose(rl!, 'Install scope?', ['global (user-wide)', 'project (this repo)'])) === 0 ? 'global' : 'project';
|
|
166
186
|
}
|
|
167
|
-
const projectDir =
|
|
187
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
168
188
|
if (scope === 'project' && !existsSync(projectDir)) throw new Error(`Project dir not found: ${projectDir}`);
|
|
169
189
|
|
|
170
190
|
let targetIds = str(flags.target)?.split(',').map(s => s.trim()) ?? null;
|
|
@@ -222,13 +242,15 @@ async function install(flags: Args['flags']): Promise<void> {
|
|
|
222
242
|
});
|
|
223
243
|
console.log(`\nOK mugiwara ${VERSION} installed (manifest: ${file})`);
|
|
224
244
|
if (allNotes.length) console.log(`${allNotes.length} note(s) above may need attention.`);
|
|
245
|
+
console.log('CLI: run `npm i -g @ionivetech/mugiwara` so the crew can call `mugiwara savepoint/archive/continue`.');
|
|
246
|
+
console.log(' Without it the crew degrades to inline-only — no state, no resume, no closure gate.');
|
|
225
247
|
// A fresh install writes a default .mugiwara/config — point at it directly.
|
|
226
248
|
console.log('\nNext: edit .mugiwara/config to customise (mode, branch, coverage, depths).');
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
async function uninstall(flags: Args['flags']): Promise<void> {
|
|
230
252
|
const scope: Scope = flag(flags.global) ? 'global' : 'project';
|
|
231
|
-
const projectDir =
|
|
253
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
232
254
|
const home = homedir();
|
|
233
255
|
const file = manifestPath({ scope, projectDir, home });
|
|
234
256
|
const manifest = readManifest(file);
|
|
@@ -282,9 +304,27 @@ async function uninstall(flags: Args['flags']): Promise<void> {
|
|
|
282
304
|
console.log(`OK removed ${removed.length} files`);
|
|
283
305
|
}
|
|
284
306
|
|
|
307
|
+
function legacyWarning(projectDir: string): void {
|
|
308
|
+
if (hasLegacyLayout(projectDir)) {
|
|
309
|
+
console.error('⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function schemaWarnings(projectDir: string): void {
|
|
314
|
+
const states = readState(projectDir);
|
|
315
|
+
for (const s of states) {
|
|
316
|
+
const v = s.schema_version;
|
|
317
|
+
if (v !== CURRENT_SCHEMA_VERSION) {
|
|
318
|
+
const wrote = v === null || v === undefined || v === '' ? 'unknown' : String(v);
|
|
319
|
+
console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ''}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
285
324
|
function list(flags: Args['flags']): void {
|
|
286
325
|
const home = homedir();
|
|
287
|
-
const projectDir =
|
|
326
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
327
|
+
legacyWarning(projectDir);
|
|
288
328
|
let found = false;
|
|
289
329
|
for (const [label, file] of [
|
|
290
330
|
['project', manifestPath({ scope: 'project', projectDir, home })],
|
|
@@ -315,10 +355,28 @@ function list(flags: Args['flags']): void {
|
|
|
315
355
|
* the caller must stop and let the user pick.
|
|
316
356
|
*/
|
|
317
357
|
function continueCmd(flags: Args['flags'], positionals: string[]): void {
|
|
318
|
-
const projectDir =
|
|
358
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
359
|
+
legacyWarning(projectDir);
|
|
360
|
+
schemaWarnings(projectDir);
|
|
319
361
|
const [mission, member] = positionals.slice(1);
|
|
320
362
|
let entries = readContinue(projectDir);
|
|
321
363
|
|
|
364
|
+
// If the requested member's state file is unreadable, refuse rather than
|
|
365
|
+
// resuming from continue-<member>.json alone. A resume point without its
|
|
366
|
+
// state is a guess. (B6)
|
|
367
|
+
if (mission) {
|
|
368
|
+
// readState populates unreadableStateFiles for state files; entries already captured
|
|
369
|
+
readState(projectDir);
|
|
370
|
+
const badState = unreadableStateFiles();
|
|
371
|
+
const target = member ? `${mission}/${member}.json` : `${mission}/state.json`;
|
|
372
|
+
if (badState.includes(target)) {
|
|
373
|
+
console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ''} has unreadable state: ${target}`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
// re-read continue entries after the state scan cleared unreadable (preserve original entries)
|
|
377
|
+
// entries already holds the correct continue data, no need to re-read
|
|
378
|
+
}
|
|
379
|
+
|
|
322
380
|
// default to this actor's work; --all crosses actors on a shared checkout
|
|
323
381
|
if (!flag(flags.all)) {
|
|
324
382
|
const actor = gitActor(projectDir);
|
|
@@ -357,9 +415,19 @@ function continueCmd(flags: Args['flags'], positionals: string[]): void {
|
|
|
357
415
|
|
|
358
416
|
/** `mugiwara status` — one screen of computed mission state, no model needed. */
|
|
359
417
|
function statusCmd(flags: Args['flags']): void {
|
|
360
|
-
const projectDir =
|
|
418
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
419
|
+
legacyWarning(projectDir);
|
|
420
|
+
schemaWarnings(projectDir);
|
|
361
421
|
const states = readState(projectDir);
|
|
362
|
-
|
|
422
|
+
const bad = unreadableStateFiles();
|
|
423
|
+
if (bad.length) {
|
|
424
|
+
console.error(`⚠ ${bad.length} unreadable state file(s): ${bad.join(', ')}`);
|
|
425
|
+
console.error(' These are not "no mission" — they are corrupt. Inspect or delete them.');
|
|
426
|
+
}
|
|
427
|
+
if (!states.length) {
|
|
428
|
+
console.log(bad.length ? 'No readable mission state on disk.' : 'No mission state on disk.');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
363
431
|
const actor = flag(flags.all) ? null : gitActor(projectDir);
|
|
364
432
|
const rows = actor ? (states.filter((s) => s.actor === actor).length ? states.filter((s) => s.actor === actor) : states) : states;
|
|
365
433
|
for (const s of rows) {
|
|
@@ -375,7 +443,7 @@ function statusCmd(flags: Args['flags']): void {
|
|
|
375
443
|
|
|
376
444
|
/** `mugiwara cost [--mission <id>] [--json] [--ledger]` — show cost ledger, avoided work, efficiency, trail. */
|
|
377
445
|
function costCmd(flags: Args['flags'], positionals: string[]): void {
|
|
378
|
-
const projectDir =
|
|
446
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
379
447
|
const mission = str(flags.mission) ?? positionals[1] ?? (() => {
|
|
380
448
|
const states = readState(projectDir);
|
|
381
449
|
if (states.length === 1) return states[0].mission;
|
|
@@ -430,7 +498,7 @@ function costCmd(flags: Args['flags'], positionals: string[]): void {
|
|
|
430
498
|
|
|
431
499
|
/** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
|
|
432
500
|
function runCmd(flags: Args['flags'], positionals: string[]): void {
|
|
433
|
-
const projectDir =
|
|
501
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
434
502
|
const name = positionals[1];
|
|
435
503
|
if (!name) {
|
|
436
504
|
console.error(`usage: mugiwara run <script> [args...]\n scripts: ${RUNNABLE.join(', ')}`);
|
|
@@ -442,7 +510,7 @@ function runCmd(flags: Args['flags'], positionals: string[]): void {
|
|
|
442
510
|
|
|
443
511
|
/** `mugiwara blame <path>` — provenance note on the last commit touching path. */
|
|
444
512
|
function blameCmd(flags: Args['flags'], positionals: string[]): void {
|
|
445
|
-
const projectDir =
|
|
513
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
446
514
|
const path = positionals[1];
|
|
447
515
|
if (!path) { console.error('usage: mugiwara blame <file-path>'); process.exit(1); }
|
|
448
516
|
console.log(blamePath(projectDir, path));
|
|
@@ -475,10 +543,15 @@ export function stalenessLine(projectDir: string, baseSha: string): string | nul
|
|
|
475
543
|
|
|
476
544
|
/** `mugiwara handoff <mission>` — a report the next engineer can act on. */
|
|
477
545
|
function handoffCmd(flags: Args['flags'], positionals: string[]): void {
|
|
478
|
-
const projectDir =
|
|
546
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
479
547
|
const mission = positionals[1];
|
|
480
548
|
if (!mission) { console.error('usage: mugiwara handoff <mission> [--project <dir>]'); process.exit(1); }
|
|
481
549
|
const states = readState(projectDir).filter((s) => s.mission === mission);
|
|
550
|
+
const bad = unreadableStateFiles().filter((p) => p.startsWith(`${mission}/`));
|
|
551
|
+
if (bad.length) {
|
|
552
|
+
console.error(`✗ mission "${mission}" has unreadable state: ${bad.join(', ')}`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
482
555
|
if (!states.length) { console.error(`no in-flight mission "${mission}"`); process.exit(1); }
|
|
483
556
|
const lines = [
|
|
484
557
|
`# Handoff: ${mission}`,
|
|
@@ -508,9 +581,91 @@ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
|
|
|
508
581
|
console.log(`\nwritten: ${out}`);
|
|
509
582
|
}
|
|
510
583
|
|
|
584
|
+
export function migrateCmd(flags: Args['flags']): void {
|
|
585
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
586
|
+
const dryRun = flag(flags.dryRun);
|
|
587
|
+
const legacyState = join(projectDir, '.mugiwara', 'state');
|
|
588
|
+
const legacyContinue = join(projectDir, '.mugiwara', 'continue');
|
|
589
|
+
const missionsRoot = join(projectDir, '.mugiwara', 'missions');
|
|
590
|
+
const moves: Array<{ src: string; dest: string }> = [];
|
|
591
|
+
|
|
592
|
+
const collect = (srcRoot: string, isContinue: boolean) => {
|
|
593
|
+
if (!existsSync(srcRoot)) return;
|
|
594
|
+
const walk = (dir: string) => {
|
|
595
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
596
|
+
const full = join(dir, e.name);
|
|
597
|
+
if (e.isDirectory()) walk(full);
|
|
598
|
+
else if (e.isFile() && e.name.endsWith('.json')) {
|
|
599
|
+
const rel = full.slice(srcRoot.length + 1);
|
|
600
|
+
let destRel: string;
|
|
601
|
+
if (isContinue) {
|
|
602
|
+
const parts = rel.split('/');
|
|
603
|
+
const file = parts.pop()!;
|
|
604
|
+
const mission = parts.join('/');
|
|
605
|
+
const stem = file.slice(0, -'.json'.length);
|
|
606
|
+
let destFile: string;
|
|
607
|
+
if (stem === 'state') destFile = 'continue.json';
|
|
608
|
+
else destFile = `continue-${stem}.json`;
|
|
609
|
+
destRel = mission ? join(mission, destFile) : destFile;
|
|
610
|
+
} else {
|
|
611
|
+
destRel = rel;
|
|
612
|
+
}
|
|
613
|
+
moves.push({ src: full, dest: join(missionsRoot, destRel) });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
walk(srcRoot);
|
|
618
|
+
};
|
|
619
|
+
collect(legacyState, false);
|
|
620
|
+
collect(legacyContinue, true);
|
|
621
|
+
|
|
622
|
+
// legacy flat: .mugiwara/state.json style? treat any top-level .mugiwara/state*.json as not legacy missions but still warn
|
|
623
|
+
// Already covered by state/ dir; nothing more to collect.
|
|
624
|
+
|
|
625
|
+
if (!moves.length) {
|
|
626
|
+
if (!existsSync(legacyState) && !existsSync(legacyContinue)) {
|
|
627
|
+
console.log('no legacy layout found (.mugiwara/state/ does not exist)');
|
|
628
|
+
} else {
|
|
629
|
+
console.log('no legacy state files to migrate');
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (const m of moves) {
|
|
635
|
+
console.log(`${dryRun ? 'would migrate' : 'migrated'} ${m.src} → ${m.dest}`);
|
|
636
|
+
if (!dryRun) {
|
|
637
|
+
mkdirSync(dirname(m.dest), { recursive: true });
|
|
638
|
+
try {
|
|
639
|
+
const raw = JSON.parse(readFileSync(m.src, 'utf8')) as Record<string, unknown>;
|
|
640
|
+
raw.schema_version = CURRENT_SCHEMA_VERSION;
|
|
641
|
+
writeFileSync(m.dest, JSON.stringify(raw, null, 2) + '\n');
|
|
642
|
+
rmSync(m.src, { force: true });
|
|
643
|
+
} catch {
|
|
644
|
+
try { renameSync(m.src, m.dest); } catch { /* ignore */ }
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!dryRun) {
|
|
649
|
+
const prune = (root: string) => {
|
|
650
|
+
if (!existsSync(root)) return;
|
|
651
|
+
const walkPrune = (dir: string) => {
|
|
652
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
653
|
+
if (e.isDirectory()) walkPrune(join(dir, e.name));
|
|
654
|
+
}
|
|
655
|
+
try { if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
656
|
+
};
|
|
657
|
+
walkPrune(root);
|
|
658
|
+
try { if (existsSync(root) && readdirSync(root).length === 0) rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
659
|
+
};
|
|
660
|
+
prune(legacyState);
|
|
661
|
+
prune(legacyContinue);
|
|
662
|
+
}
|
|
663
|
+
console.log(`${dryRun ? 'would migrate' : 'migrated'} ${moves.length} file(s)${dryRun ? ' (dry run)' : ''}`);
|
|
664
|
+
}
|
|
665
|
+
|
|
511
666
|
/** `mugiwara sign <mission>` / `--verify` / `--gen-key` — optional attestation. */
|
|
512
667
|
function signCmd(flags: Args['flags'], _: string[]): void {
|
|
513
|
-
const projectDir =
|
|
668
|
+
const projectDir = resolveProjectDir(str(flags.project));
|
|
514
669
|
if (flag(flags.genKey)) {
|
|
515
670
|
const backend = str(flags.backend) ?? 'auto';
|
|
516
671
|
const home = homedir();
|
|
@@ -562,10 +717,12 @@ Usage:
|
|
|
562
717
|
mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
|
|
563
718
|
mugiwara sign --gen-key [--backend pure|minisign]
|
|
564
719
|
create signing keys (pure ed25519 default)
|
|
720
|
+
mugiwara migrate [--dry-run] [--project <dir>]
|
|
721
|
+
move legacy .mugiwara/state/ layout to .mugiwara/missions/
|
|
565
722
|
mugiwara run <script> [args...]
|
|
566
|
-
|
|
723
|
+
run a bundled harness script here (${RUNNABLE.join(', ')})
|
|
567
724
|
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
568
|
-
|
|
725
|
+
shorthand for: mugiwara run savepoint.sh ...
|
|
569
726
|
mugiwara --help this help
|
|
570
727
|
mugiwara --version print version
|
|
571
728
|
|
package/src/config.ts
CHANGED
|
@@ -40,6 +40,12 @@ function configPaths(projectDir: string): string[] {
|
|
|
40
40
|
* blank lines are skipped; values are trimmed.
|
|
41
41
|
*/
|
|
42
42
|
export function readConfig(projectDir: string): Record<string, string> {
|
|
43
|
+
try {
|
|
44
|
+
const file = join(projectDir, '.mugiwara', 'config');
|
|
45
|
+
let exists = false;
|
|
46
|
+
try { exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink(); } catch { exists = false; }
|
|
47
|
+
if (!exists) ensureConfig(projectDir);
|
|
48
|
+
} catch { /* best-effort: auto-create must not break reads */ }
|
|
43
49
|
const out: Record<string, string> = {};
|
|
44
50
|
for (const file of configPaths(projectDir)) {
|
|
45
51
|
if (!existsSync(file)) continue;
|
package/src/continue.ts
CHANGED
|
@@ -14,6 +14,32 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
|
|
17
|
+
export const CURRENT_SCHEMA_VERSION = 2;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* v0.6 legacy layout: `.mugiwara/state/<mission>/<member>.json`
|
|
21
|
+
* (and `.mugiwara/continue/<mission>/...`) is invisible to v0.8
|
|
22
|
+
* `missions/` readers. Detect it so status/continue can warn.
|
|
23
|
+
*/
|
|
24
|
+
export function hasLegacyLayout(projectDir: string): boolean {
|
|
25
|
+
for (const legacy of [join(projectDir, '.mugiwara', 'state'), join(projectDir, '.mugiwara', 'continue')]) {
|
|
26
|
+
if (!existsSync(legacy)) continue;
|
|
27
|
+
try {
|
|
28
|
+
const walk = (dir: string): boolean => {
|
|
29
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
if (e.isFile() && e.name.endsWith('.json')) return true;
|
|
31
|
+
if (e.isDirectory() && walk(join(dir, e.name))) return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
|
35
|
+
if (walk(legacy)) return true;
|
|
36
|
+
} catch { /* ignore */ }
|
|
37
|
+
}
|
|
38
|
+
// also legacy flat files at .mugiwara/state.json style already handled by missions scan,
|
|
39
|
+
// but treat top-level .mugiwara/state/*.json absence as no legacy
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
17
43
|
/** Mission/member allowlist — identical to savepoint.sh and mission.ts. */
|
|
18
44
|
const SAFE = /^[A-Za-z0-9._-]+$/;
|
|
19
45
|
const isSafeKey = (s: string): boolean => SAFE.test(s) && !/^\.+$/.test(s);
|
|
@@ -51,6 +77,7 @@ export type StateEntry = ContinueEntry & {
|
|
|
51
77
|
budget_status: string;
|
|
52
78
|
files_touched: number;
|
|
53
79
|
evidence: string[];
|
|
80
|
+
schema_version: number | string | null;
|
|
54
81
|
};
|
|
55
82
|
|
|
56
83
|
const num = (v: unknown): number => {
|
|
@@ -84,6 +111,11 @@ export function gitActor(cwd: string): string {
|
|
|
84
111
|
return name || process.env.USER || process.env.USERNAME || '';
|
|
85
112
|
}
|
|
86
113
|
|
|
114
|
+
const unreadable: string[] = [];
|
|
115
|
+
|
|
116
|
+
/** State files that exist but could not be parsed. Cleared by each scan. (B6) */
|
|
117
|
+
export function unreadableStateFiles(): string[] { return [...unreadable]; }
|
|
118
|
+
|
|
87
119
|
/**
|
|
88
120
|
* Read every mission dir under `.mugiwara/missions/<mission>/`, picking the
|
|
89
121
|
* files this reader owns: state readers take `state.json` / `<member>.json`,
|
|
@@ -91,6 +123,7 @@ export function gitActor(cwd: string): string {
|
|
|
91
123
|
* files are skipped.
|
|
92
124
|
*/
|
|
93
125
|
function scan<T>(projectDir: string, kind: 'state' | 'continue', map: (raw: Record<string, unknown>, member: string | null) => T): T[] {
|
|
126
|
+
unreadable.length = 0;
|
|
94
127
|
const base = join(projectDir, '.mugiwara', 'missions');
|
|
95
128
|
if (!existsSync(base)) return [];
|
|
96
129
|
const out: T[] = [];
|
|
@@ -121,7 +154,7 @@ function scan<T>(projectDir: string, kind: 'state' | 'continue', map: (raw: Reco
|
|
|
121
154
|
if (text(raw.mission) !== mission) continue;
|
|
122
155
|
out.push(map(raw, member));
|
|
123
156
|
} catch {
|
|
124
|
-
|
|
157
|
+
unreadable.push(join(mission, f));
|
|
125
158
|
}
|
|
126
159
|
}
|
|
127
160
|
}
|
|
@@ -148,6 +181,7 @@ export function readContinue(projectDir: string): ContinueEntry[] {
|
|
|
148
181
|
export function readState(projectDir: string): StateEntry[] {
|
|
149
182
|
return scan(projectDir, 'state', (r, member) => {
|
|
150
183
|
const tasks = (r.tasks ?? {}) as Record<string, unknown>;
|
|
184
|
+
const sv = r.schema_version;
|
|
151
185
|
return {
|
|
152
186
|
mission: text(r.mission),
|
|
153
187
|
member,
|
|
@@ -178,6 +212,7 @@ export function readState(projectDir: string): StateEntry[] {
|
|
|
178
212
|
budget_status: text(r.budget_status) || 'ok',
|
|
179
213
|
files_touched: num(r.files_touched),
|
|
180
214
|
evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : [],
|
|
215
|
+
schema_version: typeof sv === 'number' || typeof sv === 'string' ? sv : null,
|
|
181
216
|
};
|
|
182
217
|
});
|
|
183
218
|
}
|
package/src/cost.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const LANE_BUDGET: Record<string, number> = {
|
|
|
33
33
|
lean: 12000,
|
|
34
34
|
standard: 25000,
|
|
35
35
|
full: 50000,
|
|
36
|
-
spike:
|
|
36
|
+
spike: 9000,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/** Token estimate for skills/agents loaded in this lane (0 for unknown/direct). */
|
|
@@ -149,6 +149,9 @@ export function appendCostEvent(missionDir: string, event: Omit<CostEvent, 'ts'>
|
|
|
149
149
|
appendFileSync(join(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + '\n', 'utf8');
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// ── Auto-compress (T4) — compressed event kind ───────────────────────────────
|
|
153
|
+
export const COMPRESSED_KIND = 'compressed';
|
|
154
|
+
|
|
152
155
|
// ── Optimization decision records — structured rows in decisions.md ──
|
|
153
156
|
|
|
154
157
|
export type OptDecision = {
|
package/src/installer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/installer.ts
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, rmSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, copyFileSync, rmSync, lstatSync, chmodSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -57,6 +57,7 @@ export interface Target {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export const CONTENT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'content');
|
|
60
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
60
61
|
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')) as { version: string };
|
|
61
62
|
export const VERSION = pkg.version;
|
|
62
63
|
|
|
@@ -99,9 +100,13 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
99
100
|
const backupRoot = join(scope === 'global' ? home : projectDir, '.mugiwara');
|
|
100
101
|
const result: InstallResult = { written: [], skipped: [], backedUp: [], notes: [] };
|
|
101
102
|
|
|
102
|
-
const writeOne = (absPath: string, text: string) => {
|
|
103
|
+
const writeOne = (absPath: string, text: string, mode?: number) => {
|
|
103
104
|
if (existsSync(absPath)) {
|
|
104
|
-
if (readFileSync(absPath, 'utf8') === text) {
|
|
105
|
+
if (readFileSync(absPath, 'utf8') === text) {
|
|
106
|
+
// ensure mode even when content unchanged
|
|
107
|
+
if (!dryRun && mode !== undefined) { try { chmodSync(absPath, mode); } catch { /* ignore */ } }
|
|
108
|
+
result.skipped.push(absPath); return;
|
|
109
|
+
}
|
|
105
110
|
if (!force) {
|
|
106
111
|
result.skipped.push(absPath);
|
|
107
112
|
result.notes.push(`conflict (not overwritten; run update to replace with backup): ${absPath}`);
|
|
@@ -114,7 +119,11 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
114
119
|
if (!dryRun) { mkdirSync(backupDir, { recursive: true }); copyFileSync(absPath, backupFile); }
|
|
115
120
|
result.backedUp.push(absPath);
|
|
116
121
|
}
|
|
117
|
-
if (!dryRun) {
|
|
122
|
+
if (!dryRun) {
|
|
123
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
124
|
+
writeFileSync(absPath, text);
|
|
125
|
+
if (mode !== undefined) { try { chmodSync(absPath, mode); } catch { /* ignore */ } }
|
|
126
|
+
}
|
|
118
127
|
result.written.push(absPath);
|
|
119
128
|
};
|
|
120
129
|
|
|
@@ -156,6 +165,20 @@ export function installTo(target: Target, opts: InstallOptions): InstallResult {
|
|
|
156
165
|
for (const r of sharedRefs) writeOne(join(sharedRoot, r.relPath), r.text);
|
|
157
166
|
}
|
|
158
167
|
|
|
168
|
+
// Shell fallbacks: pure sh, no Node. The critical path (lane sizing + state)
|
|
169
|
+
// must survive on a harness where the CLI cannot run. See plan.md B1.
|
|
170
|
+
{
|
|
171
|
+
const SHELL_FALLBACKS = ['lane.sh', 'savepoint.sh', 'lib/patterns.sh', 'lib/lane-base.sh'];
|
|
172
|
+
const mugiwaraDir = join(scope === 'global' ? home : projectDir, '.mugiwara');
|
|
173
|
+
for (const rel of SHELL_FALLBACKS) {
|
|
174
|
+
const src = join(REPO_ROOT, 'scripts', rel);
|
|
175
|
+
if (!existsSync(src)) continue;
|
|
176
|
+
const text = readFileSync(src, 'utf8');
|
|
177
|
+
const dest = join(mugiwaraDir, 'bin', rel);
|
|
178
|
+
writeOne(dest, text, 0o755);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
if (target.postInstall) {
|
|
160
183
|
const post = target.postInstall({ scope, projectDir, home, dryRun, files: result.written });
|
|
161
184
|
result.written.push(...post.written);
|