@ionivetech/mugiwara 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/README.md +196 -330
  8. package/content/agents/brook-healing.md +1 -1
  9. package/content/agents/franky-gates.md +1 -1
  10. package/content/agents/luffy-orchestrator.md +2 -2
  11. package/content/agents/memory-keeper.md +5 -0
  12. package/content/agents/usopp-brainstorm.md +3 -2
  13. package/content/agents/zoro-execution.md +4 -3
  14. package/content/skills/mugiwara-backend/SKILL.md +52 -43
  15. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  16. package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
  17. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  18. package/content/skills/mugiwara-execution/SKILL.md +34 -33
  19. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  20. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  21. package/content/skills/mugiwara-frontend/SKILL.md +44 -44
  22. package/content/skills/mugiwara-gates/SKILL.md +28 -16
  23. package/content/skills/mugiwara-healing/SKILL.md +30 -25
  24. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  25. package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
  26. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  27. package/content/skills/mugiwara-planning/SKILL.md +28 -14
  28. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  29. package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
  30. package/content/skills/mugiwara-quality/SKILL.md +8 -13
  31. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  32. package/content/skills/mugiwara-resume/SKILL.md +3 -9
  33. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  34. package/content/skills/mugiwara-review/SKILL.md +17 -24
  35. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  36. package/content/skills/mugiwara-security/SKILL.md +47 -35
  37. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  38. package/content/skills/mugiwara-workflow/SKILL.md +13 -13
  39. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  40. package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
  41. package/dist/mugiwara.js +1802 -316
  42. package/gemini-extension.json +1 -1
  43. package/hooks/mugiwara-mode-tracker.js +24 -4
  44. package/hooks/mugiwara-mode-tracker.ts +36 -7
  45. package/hooks/pipeline-guard.js +1 -1
  46. package/hooks/pipeline-guard.ts +2 -1
  47. package/hooks/session-start.js +6 -1
  48. package/hooks/session-start.ts +8 -1
  49. package/package.json +2 -2
  50. package/plugin.json +1 -1
  51. package/references/cost-governor.md +104 -0
  52. package/references/multi-actor.md +21 -0
  53. package/references/posture-routing.md +31 -0
  54. package/references/wave-banners.md +1 -2
  55. package/scripts/benchmark-governor.ts +516 -0
  56. package/scripts/benchmark-thresholds.json +47 -0
  57. package/scripts/check-doc-links.ts +8 -2
  58. package/scripts/gate-selftest.ts +104 -21
  59. package/scripts/lib/lane-base.sh +4 -4
  60. package/scripts/retrieval-eval.ts +9 -3
  61. package/scripts/savepoint.sh +41 -2
  62. package/scripts/validate-content.ts +82 -3
  63. package/scripts/verify-install.ts +20 -0
  64. package/scripts/write-metrics.ts +73 -0
  65. package/src/adaptive-budget.ts +178 -0
  66. package/src/args.ts +3 -2
  67. package/src/budget.ts +18 -16
  68. package/src/check-artifacts.ts +45 -0
  69. package/src/cli.ts +221 -8
  70. package/src/cognition.ts +234 -0
  71. package/src/config.ts +113 -0
  72. package/src/context.ts +72 -0
  73. package/src/continue.ts +29 -0
  74. package/src/cost.ts +189 -0
  75. package/src/evidence.ts +160 -0
  76. package/src/installer.ts +2 -16
  77. package/src/integrity.ts +65 -16
  78. package/src/investigation.ts +72 -0
  79. package/src/mission.ts +246 -16
  80. package/src/policy.ts +355 -2
  81. package/src/posture.ts +86 -0
  82. package/src/provenance.ts +29 -9
  83. package/src/reporting.ts +225 -0
  84. package/src/scope.ts +321 -0
  85. package/src/sign.ts +234 -18
  86. package/src/slop.ts +306 -0
  87. package/src/work.ts +273 -0
@@ -0,0 +1,178 @@
1
+ // src/adaptive-budget.ts
2
+ // Phase 7 Adaptive Budget & Circuit Breaker — reservation, projection,
3
+ // adaptive/expansion, progressive thresholds, circuit breaker, anomaly
4
+ // (Native Cost Governor, plan §51 Phase 7, §24–§29).
5
+ //
6
+ // Boundary: pure verdict functions over explicit inputs (unit-testable), plus a
7
+ // record helper that persists via the sanitized recordOptDecision (§41). No new
8
+ // config keys; savepoint.sh/lane-base.sh untouched. The crew acts — this module
9
+ // records.
10
+
11
+ import { recordOptDecision } from './cost.ts';
12
+
13
+ // ── Budget reservation (§25) ──
14
+
15
+ export type BudgetReservation = { remaining: number; expected_max: number; available: number; reserved: number };
16
+
17
+ export function reserveBudget(input: { remaining: number; expected_max: number }): BudgetReservation {
18
+ const reserved = input.expected_max;
19
+ const available = Math.max(0, input.remaining - reserved);
20
+ return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
21
+ }
22
+
23
+ // ── Budget projection (§26) ──
24
+
25
+ export type BudgetProjection = {
26
+ current: number;
27
+ remaining_required: number;
28
+ expected_conditional: number;
29
+ possible_healing: number;
30
+ projected_min: number;
31
+ projected_max: number;
32
+ };
33
+
34
+ export function projectBudget(input: {
35
+ current: number;
36
+ remaining_required: number;
37
+ expected_conditional: number;
38
+ possible_healing: number;
39
+ }): BudgetProjection {
40
+ const projected_min = input.current + input.remaining_required + input.expected_conditional;
41
+ const projected_max = projected_min + input.possible_healing;
42
+ return {
43
+ current: input.current,
44
+ remaining_required: input.remaining_required,
45
+ expected_conditional: input.expected_conditional,
46
+ possible_healing: input.possible_healing,
47
+ projected_min,
48
+ projected_max,
49
+ };
50
+ }
51
+
52
+ // ── Evidence-backed expansion (§27) ──
53
+
54
+ export type ExpansionInput = {
55
+ reason: string;
56
+ has_evidence: boolean;
57
+ scope_expanded?: boolean;
58
+ security_path?: boolean;
59
+ test_surface_expanded?: boolean;
60
+ architecture_dependency?: boolean;
61
+ legitimate_healing?: boolean;
62
+ };
63
+
64
+ export type ExpansionVerdict = { allowed: boolean; reason: string };
65
+
66
+ const VALID_REASONS = new Set([
67
+ 'scope legitimately expanded',
68
+ 'security-sensitive path',
69
+ 'test surface larger',
70
+ 'architecture dependency',
71
+ 'legitimate healing',
72
+ ]);
73
+
74
+ function isValidReason(reason: string): boolean {
75
+ return VALID_REASONS.has(reason);
76
+ }
77
+
78
+ function hasValidFlag(input: ExpansionInput): boolean {
79
+ return !!(
80
+ input.scope_expanded ||
81
+ input.security_path ||
82
+ input.test_surface_expanded ||
83
+ input.architecture_dependency ||
84
+ input.legitimate_healing
85
+ );
86
+ }
87
+
88
+ export function evaluateExpansion(input: ExpansionInput): ExpansionVerdict {
89
+ if (!input.has_evidence) {
90
+ return { allowed: false, reason: 'deny — no evidence' };
91
+ }
92
+ if (!input.reason || !isValidReason(input.reason)) {
93
+ return { allowed: false, reason: `deny — invalid reason: ${input.reason || '(empty)'}` };
94
+ }
95
+ if (!hasValidFlag(input)) {
96
+ return { allowed: false, reason: `deny — valid reason ${input.reason} but no matching flag set` };
97
+ }
98
+ return { allowed: true, reason: `allow — ${input.reason} with evidence` };
99
+ }
100
+
101
+ // ── Progressive thresholds (§28) ──
102
+
103
+ export type AdaptiveStatus = 'ok' | 'optimize' | 'aggressive' | 'protect' | 'pause' | 'warning' | 'stop';
104
+
105
+ export function checkProgressiveThreshold(input: { budget: number; used: number }): { status: AdaptiveStatus; pct: number } {
106
+ const pct = input.budget > 0 ? Math.round((input.used / input.budget) * 100) : 0;
107
+ let status: AdaptiveStatus = 'ok';
108
+ if (pct >= 300) status = 'stop';
109
+ else if (pct >= 150) status = 'warning';
110
+ else if (pct >= 100) status = 'pause';
111
+ else if (pct >= 90) status = 'protect';
112
+ else if (pct >= 75) status = 'aggressive';
113
+ else if (pct >= 60) status = 'optimize';
114
+ return { status, pct };
115
+ }
116
+
117
+ // ── Cost circuit breaker (§29) ──
118
+
119
+ export type CircuitBreakerInput = {
120
+ expected: number;
121
+ actual: number;
122
+ progress_delta: number;
123
+ scope_expanded: boolean;
124
+ evidence_delta: number;
125
+ };
126
+
127
+ export type CircuitBreakerVerdict = { tripped: boolean; reason: string };
128
+
129
+ export function checkCircuitBreaker(input: CircuitBreakerInput): CircuitBreakerVerdict {
130
+ // note: double-threshold (actual >= expected*2), tune if §29 needs finer signal
131
+ const doubled = input.expected * 2;
132
+ const noProgress = input.progress_delta === 0;
133
+ const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
134
+ const overDoubled = input.actual >= doubled;
135
+ if (overDoubled && noProgress && noScopeOrEvidence) {
136
+ return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
137
+ }
138
+ if (!overDoubled) return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
139
+ if (!noProgress) return { tripped: false, reason: 'no breaker — progress made' };
140
+ return { tripped: false, reason: 'no breaker — scope expanded or evidence gained' };
141
+ }
142
+
143
+ // ── Budget anomaly (§24, re-consumes slop 5k-zero-progress signal) ──
144
+
145
+ export type BudgetAnomalyInput = {
146
+ progress_before: number;
147
+ progress_after: number;
148
+ tokens_before: number;
149
+ tokens_after: number;
150
+ };
151
+
152
+ export type BudgetAnomaly = { anomaly: boolean; reason: string };
153
+
154
+ export function detectBudgetAnomaly(input: BudgetAnomalyInput): BudgetAnomaly {
155
+ const tokens_delta = input.tokens_after - input.tokens_before;
156
+ const progress_delta = input.progress_after - input.progress_before;
157
+ if (tokens_delta >= 5000 && progress_delta === 0) {
158
+ return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
159
+ }
160
+ if (tokens_delta < 5000 && progress_delta === 0) {
161
+ return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
162
+ }
163
+ return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
164
+ }
165
+
166
+ // ── Decision trail (§41) ──
167
+
168
+ export function recordBudgetDecision(
169
+ missionDir: string,
170
+ d: { decision: string; reason: string; evidence?: string },
171
+ ): void {
172
+ recordOptDecision(missionDir, {
173
+ actor: 'budget-governor',
174
+ decision: d.decision,
175
+ reason: d.reason,
176
+ evidence: d.evidence,
177
+ });
178
+ }
package/src/args.ts CHANGED
@@ -6,11 +6,12 @@ export type Args = {
6
6
  flags: Record<string, FlagValue>;
7
7
  };
8
8
 
9
- const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before' };
9
+ const VALUE_FLAGS: Record<string, string> = { '--project': 'project', '--target': 'target', '--before': 'before', '--backend': 'backend', '--mission': 'mission' };
10
10
  const BOOL_FLAGS: Record<string, string> = {
11
11
  '--global': 'global', '--yes': 'yes', '-y': 'yes', '--force': 'force',
12
12
  '--dry-run': 'dryRun', '--keep-logs': 'keepLogs', '--check': 'check', '--all': 'all', '--verify': 'verify',
13
- '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
13
+ '--gen-key': 'genKey', '--help': 'help', '-h': 'help', '--version': 'version', '-v': 'version',
14
+ '--json': 'json', '--ledger': 'ledger',
14
15
  };
15
16
 
16
17
  export function parseArgs(argv: string[]): Args {
package/src/budget.ts CHANGED
@@ -8,25 +8,16 @@
8
8
  // Token telemetry stays honest elsewhere: the estimator remains the default;
9
9
  // `tokens_source: reported` activates only where the harness exposes real
10
10
  // usage (see docs/concepts/cost.md). This module does not estimate tokens.
11
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
12
- import { homedir } from 'node:os';
11
+ import { existsSync, readdirSync, statSync } from 'node:fs';
13
12
  import { join } from 'node:path';
13
+ import { readConfig } from './config.ts';
14
14
 
15
15
  export function readBudgetConfig(projectDir: string): number {
16
- for (const base of [projectDir, homedir()]) {
17
- const file = join(base, '.mugiwara', 'config');
18
- if (!existsSync(file)) continue;
19
- for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
20
- const t = line.trim();
21
- if (!t || t.startsWith('#')) continue;
22
- const eq = t.indexOf('=');
23
- if (eq === -1) continue;
24
- if (t.slice(0, eq).trim() !== 'context_budget_chars') continue;
25
- const n = Number(t.slice(eq + 1).trim());
26
- return Number.isFinite(n) && n > 0 ? n : 0;
27
- }
28
- }
29
- return 0; // unset — measurement still recorded, never enforced
16
+ const cfg = readConfig(projectDir);
17
+ const raw = cfg.context_budget_chars;
18
+ if (raw === undefined || raw === '') return 0;
19
+ const n = Number(raw);
20
+ return Number.isFinite(n) && n > 0 ? n : 0;
30
21
  }
31
22
 
32
23
  /** Sum of bytes across the trail: top-level *.md + flows/* (legacy waves/* counts too). */
@@ -54,3 +45,14 @@ export function formatFootprint(chars: number, budget: number): string {
54
45
  ? `${base} — OVER budget ${budget}`
55
46
  : `${base} (budget ${budget})`;
56
47
  }
48
+
49
+ // ── Auto-compress threshold (T4) — 80% of budget ───────────────────────────
50
+ export const COMPRESS_THRESHOLD_PCT = 0.8;
51
+
52
+ export function shouldCompress(budget: number, chars: number): boolean {
53
+ return budget > 0 && chars > Math.floor(budget * COMPRESS_THRESHOLD_PCT);
54
+ }
55
+
56
+ export function compressThreshold(budget: number): number {
57
+ return Math.floor(budget * COMPRESS_THRESHOLD_PCT);
58
+ }
@@ -0,0 +1,45 @@
1
+ // src/check-artifacts.ts
2
+ // Mission artifact gate (roadmap v0.8 item 4): every Lane 2+ mission must
3
+ // carry its evidence trail — plan.md (the contract) and flows/* execution
4
+ // evidence — or the archive refuses to fold, same as the secret gate.
5
+ // Lane 0/1 missions are audit-lite: no plan/flows required.
6
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ export interface ArtifactCheck {
10
+ ok: boolean;
11
+ missing: string[];
12
+ lane: string | null;
13
+ }
14
+
15
+ const LANE_MIN = new Set(['standard', 'full', 'spike']);
16
+
17
+ /** Lane 2+ (standard/full/spike) missions require the evidence trail. */
18
+ export function checkMissionArtifacts(missionDir: string): ArtifactCheck {
19
+ const statePath = join(missionDir, 'state.json');
20
+ if (!existsSync(statePath)) {
21
+ // archived mission (report.md survives, state folded away) — nothing to check
22
+ return { ok: true, missing: [], lane: null };
23
+ }
24
+ let lane = 'unknown';
25
+ try {
26
+ const s = JSON.parse(readFileSyncSafe(statePath)) as Record<string, unknown>;
27
+ if (typeof s.lane === 'string') lane = s.lane;
28
+ } catch { /* unreadable state → treat as unknown lane */ }
29
+
30
+ if (!LANE_MIN.has(lane)) {
31
+ return { ok: true, missing: [], lane }; // audit-lite lanes pass without trail
32
+ }
33
+
34
+ const missing: string[] = [];
35
+ if (!existsSync(join(missionDir, 'plan.md'))) missing.push('plan.md');
36
+ const flowsDir = join(missionDir, 'flows');
37
+ const hasFlows = existsSync(flowsDir) && readdirSync(flowsDir).length > 0;
38
+ if (!hasFlows) missing.push('flows/ (no execution evidence)');
39
+
40
+ return { ok: missing.length === 0, missing, lane };
41
+ }
42
+
43
+ function readFileSyncSafe(p: string): string {
44
+ try { return readFileSync(p, 'utf8'); } catch { return ''; }
45
+ }
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,9 +12,15 @@ 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 } from './continue.ts';
16
16
  import { blamePath } from './provenance.ts';
17
- import { signReport, verifyReport } from './sign.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';
23
+ import { enforceHarnessPolicy } from './policy.ts';
18
24
 
19
25
  const str = (v: FlagValue): string | undefined => (typeof v === 'string' ? v : undefined);
20
26
  const flag = (v: FlagValue): boolean => v === true;
@@ -23,6 +29,30 @@ export async function run(argv: string[]): Promise<void> {
23
29
  const { command, flags, _ } = parseArgs(argv);
24
30
  if (flag(flags.help) || command === 'help') return help();
25
31
  if (flag(flags.version)) { console.log(`mugiwara ${VERSION}`); return; }
32
+ // harness.require_enforcement — enterprise gate: refuse rules-based harnesses
33
+ // (only opencode is runtime-enforced). Covers run/savepoint/archive/status
34
+ // + other workflow commands; install/update/uninstall/list are setup and bypass.
35
+ {
36
+ const bypass = new Set(['install', 'update', 'uninstall', 'list']);
37
+ if (!bypass.has(command)) {
38
+ const projectDirForHarness = resolve(str(flags.project) ?? process.cwd());
39
+ enforceHarnessPolicy(projectDirForHarness);
40
+ }
41
+ }
42
+ // Bootstrap default .mugiwara/config on any command when missing — including
43
+ // `continue`/`status` — so tier-3 agents that only mkdir .mugiwara still get
44
+ // a config. Also covered by readConfig() auto-create for non-CLI entry.
45
+ // Skipped for install/update --dry-run: a dry run must not mutate the project.
46
+ const isDryRunInstall = (command === 'install' || command === 'update') && flag(flags.dryRun);
47
+ if (!isDryRunInstall) {
48
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
49
+ if (ensureConfig(projectDir)) {
50
+ console.log(`default .mugiwara/config written at ${join(projectDir, '.mugiwara', 'config')} (edit it to customise)`);
51
+ }
52
+ }
53
+ if (command === 'continue' || command === 'status') {
54
+ return command === 'continue' ? continueCmd(flags, _) : statusCmd(flags);
55
+ }
26
56
  switch (command) {
27
57
  case 'install': return install(flags);
28
58
  case 'update': return install({ ...flags, force: true });
@@ -33,11 +63,13 @@ export async function run(argv: string[]): Promise<void> {
33
63
  case 'clean': return cleanCmd(flags);
34
64
  case 'continue': return continueCmd(flags, _);
35
65
  case 'status': return statusCmd(flags);
66
+ case 'cost': return costCmd(flags, _);
36
67
  case 'run': return runCmd(flags, _);
37
68
  case 'savepoint': return runCmd(flags, ['run', 'savepoint.sh', ..._.slice(1)]);
38
69
  case 'blame': return blameCmd(flags, _);
39
70
  case 'handoff': return handoffCmd(flags, _);
40
71
  case 'sign': return signCmd(flags, _);
72
+ case 'migrate': return migrateCmd(flags);
41
73
  default: throw new Error(`Unknown command: ${command}`);
42
74
  }
43
75
  }
@@ -259,9 +291,27 @@ async function uninstall(flags: Args['flags']): Promise<void> {
259
291
  console.log(`OK removed ${removed.length} files`);
260
292
  }
261
293
 
294
+ function legacyWarning(projectDir: string): void {
295
+ if (hasLegacyLayout(projectDir)) {
296
+ console.error('⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)');
297
+ }
298
+ }
299
+
300
+ function schemaWarnings(projectDir: string): void {
301
+ const states = readState(projectDir);
302
+ for (const s of states) {
303
+ const v = s.schema_version;
304
+ if (v !== CURRENT_SCHEMA_VERSION) {
305
+ const wrote = v === null || v === undefined || v === '' ? 'unknown' : String(v);
306
+ console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ''}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
307
+ }
308
+ }
309
+ }
310
+
262
311
  function list(flags: Args['flags']): void {
263
312
  const home = homedir();
264
313
  const projectDir = resolve(str(flags.project) ?? process.cwd());
314
+ legacyWarning(projectDir);
265
315
  let found = false;
266
316
  for (const [label, file] of [
267
317
  ['project', manifestPath({ scope: 'project', projectDir, home })],
@@ -293,6 +343,8 @@ function list(flags: Args['flags']): void {
293
343
  */
294
344
  function continueCmd(flags: Args['flags'], positionals: string[]): void {
295
345
  const projectDir = resolve(str(flags.project) ?? process.cwd());
346
+ legacyWarning(projectDir);
347
+ schemaWarnings(projectDir);
296
348
  const [mission, member] = positionals.slice(1);
297
349
  let entries = readContinue(projectDir);
298
350
 
@@ -335,6 +387,8 @@ function continueCmd(flags: Args['flags'], positionals: string[]): void {
335
387
  /** `mugiwara status` — one screen of computed mission state, no model needed. */
336
388
  function statusCmd(flags: Args['flags']): void {
337
389
  const projectDir = resolve(str(flags.project) ?? process.cwd());
390
+ legacyWarning(projectDir);
391
+ schemaWarnings(projectDir);
338
392
  const states = readState(projectDir);
339
393
  if (!states.length) { console.log('No mission state on disk.'); return; }
340
394
  const actor = flag(flags.all) ? null : gitActor(projectDir);
@@ -350,6 +404,61 @@ function statusCmd(flags: Args['flags']): void {
350
404
  }
351
405
  }
352
406
 
407
+ /** `mugiwara cost [--mission <id>] [--json] [--ledger]` — show cost ledger, avoided work, efficiency, trail. */
408
+ function costCmd(flags: Args['flags'], positionals: string[]): void {
409
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
410
+ const mission = str(flags.mission) ?? positionals[1] ?? (() => {
411
+ const states = readState(projectDir);
412
+ if (states.length === 1) return states[0].mission;
413
+ if (states.length > 1) {
414
+ console.error('multiple missions in flight — specify --mission <id>');
415
+ process.exit(1);
416
+ }
417
+ return null;
418
+ })();
419
+ if (!mission) {
420
+ console.error('usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]');
421
+ process.exit(1);
422
+ }
423
+ const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
424
+ if (!existsSync(missionDir)) {
425
+ console.error(`No cost ledger found for mission "${mission}"`);
426
+ process.exit(1);
427
+ }
428
+ const states = readState(projectDir).filter((s) => s.mission === mission);
429
+ const envelope = states.length
430
+ ? 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 })
431
+ : costEnvelope({ lane: 'full', tokens_est: 0 });
432
+ // live slop (§3.3): run existing detectors over state already available
433
+ // (heal cycle, context registry repeated reads) so slop_interventions is real.
434
+ const state0 = states[0] as unknown as { heal_cycle?: number };
435
+ let repeatedReads = 0;
436
+ try {
437
+ const reg = loadRegistry(missionDir);
438
+ repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
439
+ } catch {
440
+ repeatedReads = 0;
441
+ }
442
+ const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
443
+ const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
444
+ if (flag(flags.json)) {
445
+ console.log(toCostJSON(ledger));
446
+ return;
447
+ }
448
+ console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
449
+ console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
450
+ console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
451
+ if (ledger.avoided.slop_interventions > 0) {
452
+ const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(', ');
453
+ console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
454
+ }
455
+ console.log(`Trail: ${ledger.trail.length} decisions`);
456
+ if (flag(flags.ledger) && ledger.trail.length) {
457
+ 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}` : ''}`);
458
+ if (ledger.trail.length > 20) console.log(`… ${ledger.trail.length - 20} more`);
459
+ }
460
+ }
461
+
353
462
  /** `mugiwara run <script.sh> [args]` — run a bundled harness script here. */
354
463
  function runCmd(flags: Args['flags'], positionals: string[]): void {
355
464
  const projectDir = resolve(str(flags.project) ?? process.cwd());
@@ -430,11 +539,109 @@ function handoffCmd(flags: Args['flags'], positionals: string[]): void {
430
539
  console.log(`\nwritten: ${out}`);
431
540
  }
432
541
 
433
- /** `mugiwara sign <mission>` / `--verify` — optional minisign attestation. */
542
+ export function migrateCmd(flags: Args['flags']): void {
543
+ const projectDir = resolve(str(flags.project) ?? process.cwd());
544
+ const dryRun = flag(flags.dryRun);
545
+ const legacyState = join(projectDir, '.mugiwara', 'state');
546
+ const legacyContinue = join(projectDir, '.mugiwara', 'continue');
547
+ const missionsRoot = join(projectDir, '.mugiwara', 'missions');
548
+ const moves: Array<{ src: string; dest: string }> = [];
549
+
550
+ const collect = (srcRoot: string, isContinue: boolean) => {
551
+ if (!existsSync(srcRoot)) return;
552
+ const walk = (dir: string) => {
553
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
554
+ const full = join(dir, e.name);
555
+ if (e.isDirectory()) walk(full);
556
+ else if (e.isFile() && e.name.endsWith('.json')) {
557
+ const rel = full.slice(srcRoot.length + 1);
558
+ let destRel: string;
559
+ if (isContinue) {
560
+ const parts = rel.split('/');
561
+ const file = parts.pop()!;
562
+ const mission = parts.join('/');
563
+ const stem = file.slice(0, -'.json'.length);
564
+ let destFile: string;
565
+ if (stem === 'state') destFile = 'continue.json';
566
+ else destFile = `continue-${stem}.json`;
567
+ destRel = mission ? join(mission, destFile) : destFile;
568
+ } else {
569
+ destRel = rel;
570
+ }
571
+ moves.push({ src: full, dest: join(missionsRoot, destRel) });
572
+ }
573
+ }
574
+ };
575
+ walk(srcRoot);
576
+ };
577
+ collect(legacyState, false);
578
+ collect(legacyContinue, true);
579
+
580
+ // legacy flat: .mugiwara/state.json style? treat any top-level .mugiwara/state*.json as not legacy missions but still warn
581
+ // Already covered by state/ dir; nothing more to collect.
582
+
583
+ if (!moves.length) {
584
+ if (!existsSync(legacyState) && !existsSync(legacyContinue)) {
585
+ console.log('no legacy layout found (.mugiwara/state/ does not exist)');
586
+ } else {
587
+ console.log('no legacy state files to migrate');
588
+ }
589
+ return;
590
+ }
591
+
592
+ for (const m of moves) {
593
+ console.log(`${dryRun ? 'would migrate' : 'migrated'} ${m.src} → ${m.dest}`);
594
+ if (!dryRun) {
595
+ mkdirSync(dirname(m.dest), { recursive: true });
596
+ try {
597
+ const raw = JSON.parse(readFileSync(m.src, 'utf8')) as Record<string, unknown>;
598
+ raw.schema_version = CURRENT_SCHEMA_VERSION;
599
+ writeFileSync(m.dest, JSON.stringify(raw, null, 2) + '\n');
600
+ rmSync(m.src, { force: true });
601
+ } catch {
602
+ try { renameSync(m.src, m.dest); } catch { /* ignore */ }
603
+ }
604
+ }
605
+ }
606
+ if (!dryRun) {
607
+ const prune = (root: string) => {
608
+ if (!existsSync(root)) return;
609
+ const walkPrune = (dir: string) => {
610
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
611
+ if (e.isDirectory()) walkPrune(join(dir, e.name));
612
+ }
613
+ try { if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
614
+ };
615
+ walkPrune(root);
616
+ try { if (existsSync(root) && readdirSync(root).length === 0) rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
617
+ };
618
+ prune(legacyState);
619
+ prune(legacyContinue);
620
+ }
621
+ console.log(`${dryRun ? 'would migrate' : 'migrated'} ${moves.length} file(s)${dryRun ? ' (dry run)' : ''}`);
622
+ }
623
+
624
+ /** `mugiwara sign <mission>` / `--verify` / `--gen-key` — optional attestation. */
434
625
  function signCmd(flags: Args['flags'], _: string[]): void {
435
626
  const projectDir = resolve(str(flags.project) ?? process.cwd());
627
+ if (flag(flags.genKey)) {
628
+ const backend = str(flags.backend) ?? 'auto';
629
+ const home = homedir();
630
+ if (backend === 'minisign') {
631
+ if (!hasMinisign()) { console.error('✗ minisign not installed — cannot generate keys with this backend'); process.exit(1); }
632
+ try {
633
+ execFileSync('minisign', ['-G'], { stdio: 'inherit' });
634
+ console.log('✓ minisign key pair generated in ~/.mugiwara/');
635
+ return;
636
+ } catch { console.error('✗ key generation failed'); process.exit(1); }
637
+ }
638
+ // pure (default)
639
+ const dir = ensurePureKey(home);
640
+ console.log(`✓ pure ed25519 key pair ready: ${join(dir, 'mugiwara.key')} / ${join(dir, 'mugiwara.pub')}`);
641
+ return;
642
+ }
436
643
  const mission = _[1];
437
- if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--project <dir>]'); process.exit(1); }
644
+ if (!mission) { console.error('usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]'); process.exit(1); }
438
645
  const missionDir = join(projectDir, '.mugiwara', 'missions', mission);
439
646
  if (!existsSync(missionDir)) { console.error(`no mission dir: ${missionDir}`); process.exit(1); }
440
647
  const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
@@ -459,15 +666,21 @@ Usage:
459
666
  mugiwara continue <m> [member]
460
667
  print the exact resume point for that mission/member
461
668
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
669
+ mugiwara cost [--mission <id>] [--json] [--ledger]
670
+ show cost ledger, avoided work, efficiency, trail (human + JSON)
462
671
  mugiwara blame <path> provenance note on the last commit touching <path>
463
672
  (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
464
673
  mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
465
674
  engineer can act on (computed state + staleness check)
466
- mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
675
+ mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
676
+ mugiwara sign --gen-key [--backend pure|minisign]
677
+ create signing keys (pure ed25519 default)
678
+ mugiwara migrate [--dry-run] [--project <dir>]
679
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
467
680
  mugiwara run <script> [args...]
468
- run a bundled harness script here (${RUNNABLE.join(', ')})
681
+ run a bundled harness script here (${RUNNABLE.join(', ')})
469
682
  mugiwara savepoint <mission> [member] [flow] [mode]
470
- shorthand for: mugiwara run savepoint.sh ...
683
+ shorthand for: mugiwara run savepoint.sh ...
471
684
  mugiwara --help this help
472
685
  mugiwara --version print version
473
686