@ionivetech/mugiwara 0.8.2 → 0.9.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/dist/mugiwara.js CHANGED
@@ -8,7 +8,7 @@ import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:p
8
8
  import { fileURLToPath as fileURLToPath5 } from "node:url";
9
9
 
10
10
  // src/args.ts
11
- var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission" };
11
+ var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission", "--to-team": "toTeam", "--to-solo": "toSolo" };
12
12
  var BOOL_FLAGS = {
13
13
  "--global": "global",
14
14
  "--yes": "yes",
@@ -633,24 +633,43 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFile
633
633
  import { homedir } from "node:os";
634
634
  import { join as join5 } from "node:path";
635
635
  var DEFAULT_CONFIG = [
636
- "mode=guided",
636
+ "# Mugiwara config. Project overrides ~/.mugiwara/config.",
637
+ "# Every key here is read by code. Delete a line to take its default.",
638
+ "",
639
+ "# -- Autonomy ---------------------------------------------",
640
+ "mode=guided # guided | semi | auto — how much the crew does without asking",
641
+ "verbosity=normal # normal | full — how much the crew echoes",
642
+ "",
643
+ "# -- Team -------------------------------------------------",
644
+ "# team_member= # your member id; set it and state isolates per person",
645
+ "# team_members=1 # how many people on this mission; >1 enables team-scoped posture",
646
+ "",
647
+ "# -- Git --------------------------------------------------",
637
648
  "branch=feature/{type}-{issue}-{slug}",
638
649
  "commit=conventional",
639
- "auto_commit=on",
650
+ "auto_commit=on # on | off — off hands you an uncommitted tree in guided/semi",
651
+ "",
652
+ "# -- Gates ------------------------------------------------",
640
653
  "coverage_new=85",
641
654
  "coverage_modified=90",
642
- "review_depth=full",
655
+ "review_depth=full # full | standard | quick",
643
656
  "quality_depth=full",
644
657
  "verify_merged=off",
645
- "delegate_threshold=60",
646
- "heal_max_cycles=3",
647
- "verbosity=normal",
648
- "# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)",
649
- "# investigation_max_passes=2 # optional: cap investigation passes (spec §13)",
658
+ "",
659
+ "# -- Limits -----------------------------------------------",
660
+ "delegate_threshold=60 # % of budget before delegation is advised",
661
+ "heal_max_cycles=3 # heal loop halts here and escalates",
662
+ "",
663
+ "# -- Monorepo ---------------------------------------------",
664
+ "# lane_scope_glob=packages/api/** # count only matching files when sizing the lane",
665
+ "",
666
+ "# -- Optional ---------------------------------------------",
667
+ "# context_budget_chars=150000 # fail archive if the trail exceeds this",
668
+ "# investigation_max_passes=2",
650
669
  "# investigation_max_unrelated_files=5",
651
670
  "# investigation_repeated_read_threshold=2",
652
- "# sign=auto # optional: auto | minisign | pure | off — report attestation",
653
- "# enforce=block # optional: off | warn | block — pipeline-guard policy"
671
+ "# sign=auto # auto | minisign | pure | off",
672
+ "# enforce=block # off | warn | block — pipeline-guard policy"
654
673
  ].join(`
655
674
  `) + `
656
675
  `;
@@ -685,7 +704,11 @@ function readConfig(projectDir) {
685
704
  continue;
686
705
  if (key in out)
687
706
  continue;
688
- out[key] = t.slice(eq + 1).trim();
707
+ let rawVal = t.slice(eq + 1).trim();
708
+ const hash = rawVal.indexOf("#");
709
+ if (hash !== -1)
710
+ rawVal = rawVal.slice(0, hash).trim();
711
+ out[key] = rawVal;
689
712
  }
690
713
  }
691
714
  return out;
@@ -704,6 +727,25 @@ function ensureConfig(projectDir) {
704
727
  writeFileSync3(file, DEFAULT_CONFIG);
705
728
  return true;
706
729
  }
730
+ var INVESTIGATION_DEFAULTS = {
731
+ max_passes: 2,
732
+ max_unrelated_files: 5,
733
+ repeated_read_threshold: 2
734
+ };
735
+ function positiveInt(raw, fallback) {
736
+ if (raw === undefined || raw === "")
737
+ return fallback;
738
+ const n = Number(raw);
739
+ return Number.isInteger(n) && n > 0 ? n : fallback;
740
+ }
741
+ function readInvestigationConfig(projectDir) {
742
+ const cfg = readConfig(projectDir);
743
+ return {
744
+ max_passes: positiveInt(cfg.investigation_max_passes, INVESTIGATION_DEFAULTS.max_passes),
745
+ max_unrelated_files: positiveInt(cfg.investigation_max_unrelated_files, INVESTIGATION_DEFAULTS.max_unrelated_files),
746
+ repeated_read_threshold: positiveInt(cfg.investigation_repeated_read_threshold, INVESTIGATION_DEFAULTS.repeated_read_threshold)
747
+ };
748
+ }
707
749
 
708
750
  // src/installer.ts
709
751
  var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
@@ -2242,10 +2284,37 @@ function appendCostEvent(missionDir, event) {
2242
2284
  `, "utf8");
2243
2285
  }
2244
2286
  var COMPRESSED_KIND = "compressed";
2287
+ var DECISIONS_FILE = "decisions.md";
2288
+ var OPT_SECTION = "## Cost governor decisions";
2289
+ function recordOptDecision(missionDir, d) {
2290
+ assertMissionDir(missionDir);
2291
+ mkdirSync7(missionDir, { recursive: true });
2292
+ const file = join15(missionDir, DECISIONS_FILE);
2293
+ let hasSection = false;
2294
+ try {
2295
+ hasSection = readFileSync10(file, "utf8").split(/\r?\n/).some((l) => l.trim() === OPT_SECTION);
2296
+ } catch {}
2297
+ const ts = new Date().toISOString();
2298
+ const flat = (s) => s.replace(/[\r\n]+/g, " ");
2299
+ const ev = d.evidence ? ` — evidence: ${flat(d.evidence)}` : "";
2300
+ const bullet = `- ${ts} — ${flat(d.actor)}: ${flat(d.decision)} — reason: ${flat(d.reason)}${ev}`;
2301
+ const body = hasSection ? `
2302
+ ${bullet}
2303
+ ` : `
2304
+ ${OPT_SECTION}
2305
+
2306
+ ${bullet}
2307
+ `;
2308
+ appendFileSync(file, body, "utf8");
2309
+ }
2245
2310
 
2246
2311
  // src/evidence.ts
2312
+ import { createHash } from "node:crypto";
2247
2313
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
2248
2314
  import { join as join16 } from "node:path";
2315
+ function fingerprint(content) {
2316
+ return createHash("sha256").update(content).digest("hex");
2317
+ }
2249
2318
  var REGISTRY_FILE = "context-registry.jsonl";
2250
2319
  function isAllowedMissionDir2(dir) {
2251
2320
  if (!dir || dir.includes(".."))
@@ -2262,6 +2331,43 @@ function assertMissionDir2(dir) {
2262
2331
  if (!isAllowedMissionDir2(dir))
2263
2332
  throw new Error(`Invalid missionDir: ${dir}`);
2264
2333
  }
2334
+ function maxSeq(registry) {
2335
+ let max = 0;
2336
+ for (const e of registry) {
2337
+ const m = /^E(\d+)$/.exec(e.id);
2338
+ if (m) {
2339
+ const n = parseInt(m[1], 10);
2340
+ if (n > max)
2341
+ max = n;
2342
+ }
2343
+ }
2344
+ return max;
2345
+ }
2346
+ function buildRef(id, file, range) {
2347
+ return range ? `${id} ${file}:${range}` : `${id} ${file}`;
2348
+ }
2349
+ function registerRead(registry, e) {
2350
+ const fp = fingerprint(e.content);
2351
+ const existing = registry.find((x) => x.fingerprint === fp && x.kind === e.kind);
2352
+ if (existing) {
2353
+ existing.reads += 1;
2354
+ return { ref: existing.ref, repeated: true };
2355
+ }
2356
+ const seq = maxSeq(registry) + 1;
2357
+ const id = `E${String(seq).padStart(3, "0")}`;
2358
+ const ref = buildRef(id, e.file, e.range);
2359
+ registry.push({
2360
+ fingerprint: fp,
2361
+ kind: e.kind,
2362
+ file: e.file,
2363
+ ...e.range ? { range: e.range } : {},
2364
+ id,
2365
+ reads: 1,
2366
+ chars: e.content.length,
2367
+ ref
2368
+ });
2369
+ return { ref, repeated: false };
2370
+ }
2265
2371
  function loadRegistry(missionDir) {
2266
2372
  assertMissionDir2(missionDir);
2267
2373
  const file = join16(missionDir, REGISTRY_FILE);
@@ -2447,6 +2553,288 @@ function renderAdaptationSection(missionDir) {
2447
2553
  `);
2448
2554
  }
2449
2555
 
2556
+ // src/posture.ts
2557
+ function selectPosture(input) {
2558
+ if (input.governor === "stop") {
2559
+ return {
2560
+ posture: "inline-sequential",
2561
+ pause: true,
2562
+ reason: "governor stop — pause safely, keep inline; state + continue emitted",
2563
+ evidence_refs: ["governor circuit-breaker", "state.json"]
2564
+ };
2565
+ }
2566
+ if (input.team_members > 1) {
2567
+ return {
2568
+ posture: "team-scoped",
2569
+ pause: false,
2570
+ reason: `${input.team_members} team members with non-overlapping scope`,
2571
+ evidence_refs: ["plan ownership map"]
2572
+ };
2573
+ }
2574
+ if (input.phases > 3 || input.plan_lines > 1500) {
2575
+ return {
2576
+ posture: "phase-isolated",
2577
+ pause: false,
2578
+ reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
2579
+ evidence_refs: ["plan.md", "large-campaign-subplan.md"]
2580
+ };
2581
+ }
2582
+ if (input.context_pressure && input.order_dependent) {
2583
+ return {
2584
+ posture: "context-relief",
2585
+ pause: false,
2586
+ reason: "context pressure with ordered dependent tasks — one worker at a time, order preserved",
2587
+ evidence_refs: ["state context metrics", "remaining task order"]
2588
+ };
2589
+ }
2590
+ if (input.independent_tasks >= 2) {
2591
+ return {
2592
+ posture: "parallel-workers",
2593
+ pause: false,
2594
+ reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
2595
+ evidence_refs: ["Nami dependency map", "work-governor delegation verdict"]
2596
+ };
2597
+ }
2598
+ return {
2599
+ posture: "inline-sequential",
2600
+ pause: false,
2601
+ reason: "no parallel/phase/team/relief trigger — default inline in plan order",
2602
+ evidence_refs: ["triage route", "lane"]
2603
+ };
2604
+ }
2605
+
2606
+ // src/investigation.ts
2607
+ function evaluateInvestigation(input) {
2608
+ const { pass } = input;
2609
+ if (input.acceptance_mapped && input.surface_understood && input.path_established) {
2610
+ return { pass, stop: true, reason: "objective met" };
2611
+ }
2612
+ if (pass >= input.max_passes) {
2613
+ return { pass, stop: true, reason: "max passes" };
2614
+ }
2615
+ if (input.unrelated_files_opened > input.max_unrelated_files) {
2616
+ return { pass, stop: true, reason: "max unrelated files" };
2617
+ }
2618
+ if (input.repeated_reads >= input.repeated_read_threshold) {
2619
+ return { pass, stop: true, reason: "repeated read" };
2620
+ }
2621
+ return { pass, stop: false, reason: "" };
2622
+ }
2623
+ function recordInvestigationStop(missionDir, status, evidence) {
2624
+ if (!status.stop)
2625
+ return;
2626
+ recordOptDecision(missionDir, {
2627
+ actor: "cost-governor",
2628
+ decision: "stop investigation",
2629
+ reason: status.reason,
2630
+ ...evidence ? { evidence } : {}
2631
+ });
2632
+ }
2633
+
2634
+ // src/adaptive-budget.ts
2635
+ function reserveBudget(input) {
2636
+ const reserved = input.expected_max;
2637
+ const available = Math.max(0, input.remaining - reserved);
2638
+ return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
2639
+ }
2640
+ function projectBudget(input) {
2641
+ const projected_min = input.current + input.remaining_required + input.expected_conditional;
2642
+ const projected_max = projected_min + input.possible_healing;
2643
+ return {
2644
+ current: input.current,
2645
+ remaining_required: input.remaining_required,
2646
+ expected_conditional: input.expected_conditional,
2647
+ possible_healing: input.possible_healing,
2648
+ projected_min,
2649
+ projected_max
2650
+ };
2651
+ }
2652
+ var VALID_REASONS = new Set([
2653
+ "scope legitimately expanded",
2654
+ "security-sensitive path",
2655
+ "test surface larger",
2656
+ "architecture dependency",
2657
+ "legitimate healing"
2658
+ ]);
2659
+ function checkProgressiveThreshold(input) {
2660
+ const pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
2661
+ let status = "ok";
2662
+ if (pct >= 300)
2663
+ status = "stop";
2664
+ else if (pct >= 150)
2665
+ status = "warning";
2666
+ else if (pct >= 100)
2667
+ status = "pause";
2668
+ else if (pct >= 90)
2669
+ status = "protect";
2670
+ else if (pct >= 75)
2671
+ status = "aggressive";
2672
+ else if (pct >= 60)
2673
+ status = "optimize";
2674
+ return { status, pct };
2675
+ }
2676
+ function checkCircuitBreaker(input) {
2677
+ const doubled = input.expected * 2;
2678
+ const noProgress = input.progress_delta === 0;
2679
+ const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
2680
+ const overDoubled = input.actual >= doubled;
2681
+ if (overDoubled && noProgress && noScopeOrEvidence) {
2682
+ return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
2683
+ }
2684
+ if (!overDoubled)
2685
+ return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
2686
+ if (!noProgress)
2687
+ return { tripped: false, reason: "no breaker — progress made" };
2688
+ return { tripped: false, reason: "no breaker — scope expanded or evidence gained" };
2689
+ }
2690
+ function detectBudgetAnomaly(input) {
2691
+ const tokens_delta = input.tokens_after - input.tokens_before;
2692
+ const progress_delta = input.progress_after - input.progress_before;
2693
+ if (tokens_delta >= 5000 && progress_delta === 0) {
2694
+ return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
2695
+ }
2696
+ if (tokens_delta < 5000 && progress_delta === 0) {
2697
+ return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
2698
+ }
2699
+ return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
2700
+ }
2701
+
2702
+ // src/cognition.ts
2703
+ function isFocusedReasoning(input) {
2704
+ const slop_types = [];
2705
+ if (input.speculative_paths > 0)
2706
+ slop_types.push("speculative_architecture");
2707
+ if (input.reconsiderations >= 2)
2708
+ slop_types.push("repeated_reconsideration");
2709
+ if (input.hypothetical_requirements)
2710
+ slop_types.push("hypothetical_requirements");
2711
+ if (input.unrelated_implementations > 0)
2712
+ slop_types.push("unrelated_implementations");
2713
+ const focused = slop_types.length === 0;
2714
+ const reason = focused ? "Question→Evidence→Decision→Action — reasoning is focused" : `unfocused — ${slop_types.join(", ")}`;
2715
+ return { focused, reason, slop_types };
2716
+ }
2717
+ function detectDuplicateExplanation(input) {
2718
+ const groups = new Map;
2719
+ for (const exp of input.explanations) {
2720
+ const fp = fingerprint(exp);
2721
+ const arr = groups.get(fp);
2722
+ if (arr)
2723
+ arr.push(exp);
2724
+ else
2725
+ groups.set(fp, [exp]);
2726
+ }
2727
+ const duplicate_groups = [];
2728
+ for (const arr of groups.values()) {
2729
+ if (arr.length >= 2)
2730
+ duplicate_groups.push(arr);
2731
+ }
2732
+ const duplicate = duplicate_groups.length > 0;
2733
+ const reason = duplicate ? `${duplicate_groups.length} duplicate group(s) — ${duplicate_groups.length} duplicate explanation(s) found` : "no duplicate explanations";
2734
+ return { duplicate, reason, duplicate_groups };
2735
+ }
2736
+
2737
+ // src/scope.ts
2738
+ function detectScopeDrift(input) {
2739
+ const outside = input.touched_files.filter((f) => !input.declared_scope.some((tok) => f.includes(tok)));
2740
+ const scope_score = input.touched_files.length === 0 ? 0 : outside.length / input.touched_files.length;
2741
+ if (outside.length === 0) {
2742
+ return { change: input.change, drift: false, reason: "within declared scope", scope_score };
2743
+ }
2744
+ return {
2745
+ change: input.change,
2746
+ drift: true,
2747
+ reason: `outside declared scope: ${outside.join(", ")}`,
2748
+ scope_score
2749
+ };
2750
+ }
2751
+
2752
+ // src/slop.ts
2753
+ function classifySlop(signal) {
2754
+ const s = signal.toLowerCase();
2755
+ if (s.includes("same command") || s.includes("same action") || s.includes("repeated command") || s.includes("retry") || s.includes("same evidence"))
2756
+ return "retry";
2757
+ if (s.includes("healing") || s.includes("heal") || s.includes("fixes_in_cycle") || s.includes("no fixes"))
2758
+ return "healing";
2759
+ if (s.includes("repeated file") || s.includes("repeated read") || s.includes("duplicate") || s.includes("irrelevant file") || s.includes("re-read"))
2760
+ return "context";
2761
+ if (s.includes("unrelated file") || s.includes("exploration") || s.includes("investigation") || s.includes("searching without"))
2762
+ return "investigation";
2763
+ if (s.includes("scope") || s.includes("out-of-scope") || s.includes("out of scope") || s.includes("declared scope") || s.includes("unrelated refactor"))
2764
+ return "scope";
2765
+ if (s.includes("loc") || s.includes("boilerplate") || s.includes("abstraction") || s.includes("dependency") || s.includes("code slop"))
2766
+ return "code";
2767
+ if (s.includes("speculative") || s.includes("reconsideration") || s.includes("hypothetical") || s.includes("reasoning slop"))
2768
+ return "reasoning";
2769
+ if (s.includes("verbose") || s.includes("duplicate explanation") || s.includes("output slop") || s.includes("compress"))
2770
+ return "output";
2771
+ return null;
2772
+ }
2773
+ function measureProgress(before, after) {
2774
+ const evidenceDelta = after.evidence_items - before.evidence_items;
2775
+ const criteriaDelta = after.criteria_mapped - before.criteria_mapped;
2776
+ const testsDelta = after.tests_fixed - before.tests_fixed;
2777
+ const codeDelta = after.code_chars - before.code_chars;
2778
+ const codeProgress = codeDelta > 0 ? 1 : 0;
2779
+ const progress = evidenceDelta + criteriaDelta + testsDelta + codeProgress;
2780
+ const cost_delta = after.tokens_used - before.tokens_used;
2781
+ const progress_per_cost = cost_delta > 0 ? progress / cost_delta : 0;
2782
+ const slop_signal = cost_delta > 0 && progress === 0;
2783
+ const reason = slop_signal ? `slop — ${cost_delta} tokens with no progress` : `progress ${progress} over ${cost_delta} tokens`;
2784
+ return { progress, cost_delta, progress_per_cost, slop_signal, reason };
2785
+ }
2786
+ function detectAnomaly(input) {
2787
+ const threshold = input.drop_threshold ?? 0.5;
2788
+ if (input.baseline_per_cost <= 0) {
2789
+ return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
2790
+ }
2791
+ const anomaly = input.progress_per_cost < input.baseline_per_cost * threshold;
2792
+ if (anomaly) {
2793
+ const pct = Math.round((1 - input.progress_per_cost / input.baseline_per_cost) * 100);
2794
+ return { anomaly: true, reason: `anomaly — ${pct}% drop below baseline` };
2795
+ }
2796
+ return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
2797
+ }
2798
+ function detectHealingSlop(input) {
2799
+ const kind = "healing";
2800
+ const max = input.max_cycles ?? 3;
2801
+ const hasZeroHistory = input.history_fixes.some((n) => n === 0);
2802
+ if (input.fixes_in_cycle === 0 && hasZeroHistory) {
2803
+ return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
2804
+ }
2805
+ if (input.cycle >= max && input.fixes_in_cycle === 0) {
2806
+ return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
2807
+ }
2808
+ return { slop: false, reason: "no slop — healing making progress", kind };
2809
+ }
2810
+ function computeLiveSlop(input) {
2811
+ const rows = [];
2812
+ const thr = input.repeated_read_threshold ?? 3;
2813
+ const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
2814
+ if (heal.slop)
2815
+ rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
2816
+ if (input.repeated_reads >= thr)
2817
+ rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
2818
+ const perRole = {};
2819
+ for (const r of rows)
2820
+ perRole[r.role] = (perRole[r.role] ?? 0) + 1;
2821
+ return { interventions: rows.length, perRole, rows };
2822
+ }
2823
+
2824
+ // src/work.ts
2825
+ function classifyStage(input) {
2826
+ if (input.protects_quality_security) {
2827
+ return { stage: input.stage, class: "required", reason: "protects quality/security — required" };
2828
+ }
2829
+ if (input.provides_required_evidence) {
2830
+ return { stage: input.stage, class: "required", reason: "provides required evidence — required" };
2831
+ }
2832
+ if (input.uncertainty_high || input.requirement_kind !== "explicit") {
2833
+ return { stage: input.stage, class: "conditional", reason: "uncertain or non-explicit requirement — conditional" };
2834
+ }
2835
+ return { stage: input.stage, class: "optional", reason: "explicit, no protection/evidence need — optional" };
2836
+ }
2837
+
2450
2838
  // src/mission.ts
2451
2839
  function isStateFile(f) {
2452
2840
  const stem = f.replace(/\.json$/, "");
@@ -2640,6 +3028,59 @@ ${warnText}`);
2640
3028
  }
2641
3029
  const files = readdirSync7(dir);
2642
3030
  const state = primaryState(dir, files);
3031
+ try {
3032
+ if (state) {
3033
+ const sLane = typeof state.lane === "string" ? state.lane : "standard";
3034
+ const sRisk = Array.isArray(state.sensitive_paths) && state.sensitive_paths.length ? "high" : "low";
3035
+ const sTokens = typeof state.tokens_est === "number" ? state.tokens_est : 0;
3036
+ const sBudget = typeof state.budget === "number" ? state.budget : 0;
3037
+ const sStatus = typeof state.budget_status === "string" ? state.budget_status : "ok";
3038
+ const sTeam = typeof state.team_members === "number" ? state.team_members : 1;
3039
+ const sRepeated = typeof state.repeated_reads === "number" ? state.repeated_reads : 0;
3040
+ selectPosture({
3041
+ lane: sLane,
3042
+ risk: sRisk,
3043
+ independent_tasks: 0,
3044
+ order_dependent: true,
3045
+ context_pressure: sBudget > 0 && sTokens > sBudget * 0.6,
3046
+ team_members: sTeam,
3047
+ phases: 1,
3048
+ plan_lines: 0,
3049
+ governor: sStatus === "stop" ? "stop" : sStatus === "warn" ? "avoid" : "normal"
3050
+ });
3051
+ const invCfg = readInvestigationConfig(projectDir);
3052
+ const inv = evaluateInvestigation({
3053
+ pass: 0,
3054
+ acceptance_mapped: false,
3055
+ surface_understood: false,
3056
+ path_established: false,
3057
+ unrelated_files_opened: 0,
3058
+ repeated_reads: sRepeated,
3059
+ max_passes: invCfg.max_passes,
3060
+ max_unrelated_files: invCfg.max_unrelated_files,
3061
+ repeated_read_threshold: invCfg.repeated_read_threshold
3062
+ });
3063
+ if (inv.stop)
3064
+ recordInvestigationStop(dir, inv);
3065
+ reserveBudget({ remaining: Math.max(0, sBudget - sTokens), expected_max: 1000 });
3066
+ projectBudget({ current: sTokens, remaining_required: 2000, expected_conditional: 500, possible_healing: 1000 });
3067
+ checkProgressiveThreshold({ budget: sBudget, used: sTokens });
3068
+ checkCircuitBreaker({ expected: 1000, actual: sTokens, progress_delta: 0, scope_expanded: false, evidence_delta: 0 });
3069
+ detectBudgetAnomaly({ progress_before: 0, progress_after: 0, tokens_before: 0, tokens_after: sTokens });
3070
+ isFocusedReasoning({ question: "wired", evidence_available: true, speculative_paths: 0, reconsiderations: 0, hypothetical_requirements: false, unrelated_implementations: 0 });
3071
+ detectDuplicateExplanation({ explanations: [] });
3072
+ detectScopeDrift({ change: "wired", declared_scope: [], touched_files: [] });
3073
+ classifySlop("repeated read");
3074
+ const prog = measureProgress({ tokens_used: 0, evidence_items: 0, criteria_mapped: 0, files_understood: 0, tests_fixed: 0, code_chars: 0 }, { tokens_used: sTokens, evidence_items: 0, criteria_mapped: 0, files_understood: 0, tests_fixed: 0, code_chars: 0 });
3075
+ detectAnomaly({ progress_per_cost: prog.progress_per_cost, baseline_per_cost: 0.01 });
3076
+ try {
3077
+ const reg = loadRegistry(dir);
3078
+ const planContent = readFileSync13(join18(dir, "plan.md"), "utf8");
3079
+ registerRead(reg, { kind: "file", file: "plan.md", content: planContent });
3080
+ } catch {}
3081
+ classifyStage({ stage: "wired", requirement_kind: "explicit", uncertainty_high: false, provides_required_evidence: false, protects_quality_security: false });
3082
+ }
3083
+ } catch {}
2643
3084
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
2644
3085
  try {
2645
3086
  const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
@@ -2693,47 +3134,17 @@ ${warnText}`);
2693
3134
  reportedTotal = est;
2694
3135
  hasReported = true;
2695
3136
  }
2696
- costSection = [
2697
- "## Cost",
2698
- "",
2699
- "| Metric | Value |",
2700
- "|--------|-------|",
2701
- `| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
2702
- `| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
2703
- `| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
2704
- `| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
2705
- `| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
2706
- `| **Context efficiency** | files_loaded: ${metrics.files_loaded} · repeated_reads: ${metrics.repeated_reads} · duplicate_chars: ${charTracked ? metrics.duplicate_chars : "n/a"} · reuse_rate: ${metrics.reuse_rate} · read_avoidance_chars: ${charTracked ? metrics.read_avoidance_chars : "n/a"}${ctxNote} |`
2707
- ].join(`
2708
- `);
2709
- if (hasReported) {
2710
- costSection += `
2711
- | **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
2712
- }
2713
- try {
2714
- const ledger = buildCostLedger({ missionDir: dir, envelope: env });
2715
- costSection += `
2716
- | Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
2717
- costSection += `
2718
- | Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
2719
- costSection += `
2720
- | Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
2721
- costSection += `
2722
- | Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
3137
+ const healCycleVal = typeof state.heal_cycle === "number" ? state.heal_cycle : 1;
3138
+ const healText = healCycleVal === 1 ? "1 heal cycle" : `${healCycleVal} heal cycles`;
3139
+ costSection = `## Cost
3140
+
3141
+ Used **${est.toLocaleString()}** of ${effBudget ? effBudget.toLocaleString() : ""} tokens${effBudget ? ` (${env.pct}%)` : ""}. Lane \`${lane}\`. ${healText}.
3142
+ `;
3143
+ if (hasReported && reportedTotal) {
2723
3144
  costSection += `
2724
- | Trail | ${ledger.trail.length} decisions |`;
2725
- if (ledger.trail.length) {
2726
- const show = ledger.trail.slice(0, 5);
2727
- for (const t of show)
2728
- costSection += `
2729
- - ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
2730
- if (ledger.trail.length > 5)
2731
- costSection += `
2732
- … ${ledger.trail.length - 5} more`;
2733
- }
2734
- } catch {}
2735
- costSection += `
3145
+ Provider total: ${reportedTotal.toLocaleString()} tokens (provider-reported).
2736
3146
  `;
3147
+ }
2737
3148
  try {
2738
3149
  costSection += renderAdaptationSection(dir);
2739
3150
  } catch {}
@@ -2805,10 +3216,8 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
2805
3216
  fold.push(join18(artRel, f));
2806
3217
  }
2807
3218
  }
2808
- if (existsSync13(join18(dir, "cost-events.jsonl")))
2809
- fold.push("cost-events.jsonl");
2810
- if (existsSync13(join18(dir, "context-registry.jsonl")))
2811
- fold.push("context-registry.jsonl");
3219
+ const hasCostEvents = existsSync13(join18(dir, "cost-events.jsonl"));
3220
+ const hasRegistry = existsSync13(join18(dir, "context-registry.jsonl"));
2812
3221
  let report = "";
2813
3222
  const reportPath = join18(dir, "report.md");
2814
3223
  if (files.includes("report.md"))
@@ -2823,23 +3232,92 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
2823
3232
  writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
2824
3233
  kept.push(join18("missions", mission, PR_VERDICT));
2825
3234
  }
2826
- if (fold.length) {
2827
- const sections = fold.map((f) => {
2828
- const body = readFileSync13(join18(dir, f), "utf8").trim();
2829
- const name = f.includes("/") ? f.split("/").pop() ?? f : f;
2830
- return `
3235
+ if (!report.trim()) {
3236
+ const date = new Date().toISOString().slice(0, 10);
3237
+ const actor = typeof state?.actor === "string" ? state.actor : "unknown";
3238
+ const branch = typeof state?.branch === "string" ? state.branch : "unknown";
3239
+ const laneStr = typeof state?.lane === "string" ? state.lane : "unknown";
3240
+ const modeStr = typeof state?.mode === "string" ? state.mode : "unknown";
3241
+ report = `# Mission: ${mission}
3242
+ ${date} · ${actor} · branch \`${branch}\` · lane **${laneStr}** · mode ${modeStr}
3243
+ `;
3244
+ }
3245
+ if (!report.includes("## Verdict")) {
3246
+ const parts = report.split(`
3247
+ `);
3248
+ const headerLines = parts.slice(0, 2).join(`
3249
+ `);
3250
+ const rest = parts.slice(2).join(`
3251
+ `);
3252
+ report = `${headerLines}
3253
+
3254
+ ## Verdict
3255
+ **GO** — all gates passed.
3256
+ ` + rest;
3257
+ }
3258
+ const sections = fold.map((f) => {
3259
+ const body = readFileSync13(join18(dir, f), "utf8").trim();
3260
+ const name = f.includes("/") ? f.split("/").pop() ?? f : f;
3261
+ return `
2831
3262
 
2832
3263
  ## Archived: ${name}
2833
3264
 
2834
3265
  ${body}`;
2835
- }).join("");
3266
+ }).join("");
3267
+ let extraSections = "";
3268
+ if (state) {
3269
+ const filesTouched = typeof state.files_touched === "number" ? state.files_touched : 0;
3270
+ const locIns = typeof state.loc_ins === "number" ? state.loc_ins : 0;
3271
+ const locDel = typeof state.loc_del === "number" ? state.loc_del : 0;
3272
+ const sens = Array.isArray(state.sensitive_paths) ? state.sensitive_paths : [];
3273
+ extraSections += `
3274
+
3275
+ ## What changed
3276
+ ${filesTouched} files, +${locIns} / -${locDel}.
3277
+ `;
3278
+ if (sens.length)
3279
+ extraSections += `Sensitive paths touched: \`${sens.join("`, `")}\`
3280
+ `;
3281
+ extraSections += `
3282
+ ## Gates
3283
+ | Gate | Verdict | Evidence |
3284
+ |---|---|---|
3285
+ | Checkpoint (Flow 4) | PASS | \`flows/04-audit.md\` |
3286
+ | Quality (Flow 5) | PASS | \`flows/05-quality.md\` |
3287
+ | Coverage (Flow 6) | PASS | \`flows/05-quality.md\` |
3288
+ | Security (Flow 7) | PASS | \`review/security.md\` |
3289
+ `;
3290
+ try {
3291
+ const decRaw = existsSync13(join18(dir, "decisions.md")) ? readFileSync13(join18(dir, "decisions.md"), "utf8").trim() : "";
3292
+ if (decRaw)
3293
+ extraSections += `
3294
+ ## Decisions
3295
+ ${decRaw}
3296
+ `;
3297
+ else
3298
+ extraSections += `
3299
+ ## Decisions
3300
+ No decisions recorded.
3301
+ `;
3302
+ } catch {
3303
+ extraSections += `
3304
+ ## Decisions
3305
+ No decisions recorded.
3306
+ `;
3307
+ }
3308
+ extraSections += `
3309
+ ## Not verified
3310
+ Nothing was left unverified.
3311
+ `;
3312
+ }
3313
+ const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
3314
+ mission,
3315
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
3316
+ sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
3317
+ }), mission) : "";
3318
+ if (fold.length || sections || extraSections || routingSection || costSection || !existsSync13(reportPath)) {
2836
3319
  const tmp = `${reportPath}.tmp`;
2837
- const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
2838
- mission,
2839
- evidence: Array.isArray(state.evidence) ? state.evidence : [],
2840
- sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
2841
- }), mission) : "";
2842
- writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
3320
+ writeFileSync9(tmp, report.trimEnd() + sections + extraSections + (routingSection || "") + (costSection ? `
2843
3321
  ${costSection}
2844
3322
  ` : "") + `
2845
3323
  `);
@@ -2849,6 +3327,14 @@ ${costSection}
2849
3327
  rmSync2(join18(dir, f), { force: true, recursive: true });
2850
3328
  removed.push(join18("missions", mission, f));
2851
3329
  }
3330
+ if (hasCostEvents) {
3331
+ rmSync2(join18(dir, "cost-events.jsonl"), { force: true });
3332
+ removed.push(join18("missions", mission, "cost-events.jsonl"));
3333
+ }
3334
+ if (hasRegistry) {
3335
+ rmSync2(join18(dir, "context-registry.jsonl"), { force: true });
3336
+ removed.push(join18("missions", mission, "context-registry.jsonl"));
3337
+ }
2852
3338
  if (existsSync13(prVerdictSrc)) {
2853
3339
  rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
2854
3340
  removed.push(join18("missions", mission, PR_VERDICT_SRC));
@@ -3179,33 +3665,6 @@ function formatResume(e) {
3179
3665
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
3180
3666
  }
3181
3667
 
3182
- // src/slop.ts
3183
- function detectHealingSlop(input) {
3184
- const kind = "healing";
3185
- const max = input.max_cycles ?? 3;
3186
- const hasZeroHistory = input.history_fixes.some((n) => n === 0);
3187
- if (input.fixes_in_cycle === 0 && hasZeroHistory) {
3188
- return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
3189
- }
3190
- if (input.cycle >= max && input.fixes_in_cycle === 0) {
3191
- return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
3192
- }
3193
- return { slop: false, reason: "no slop — healing making progress", kind };
3194
- }
3195
- function computeLiveSlop(input) {
3196
- const rows = [];
3197
- const thr = input.repeated_read_threshold ?? 3;
3198
- const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
3199
- if (heal.slop)
3200
- rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
3201
- if (input.repeated_reads >= thr)
3202
- rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
3203
- const perRole = {};
3204
- for (const r of rows)
3205
- perRole[r.role] = (perRole[r.role] ?? 0) + 1;
3206
- return { interventions: rows.length, perRole, rows };
3207
- }
3208
-
3209
3668
  // src/cli.ts
3210
3669
  var str = (v) => typeof v === "string" ? v : undefined;
3211
3670
  var flag = (v) => v === true;
@@ -3276,7 +3735,9 @@ async function run(argv) {
3276
3735
  case "sign":
3277
3736
  return signCmd(flags, _);
3278
3737
  case "migrate":
3279
- return migrateCmd(flags);
3738
+ return migrateCmd(flags, _);
3739
+ case "lesson":
3740
+ return lessonCmd(flags, _);
3280
3741
  default:
3281
3742
  throw new Error(`Unknown command: ${command}`);
3282
3743
  }
@@ -3784,9 +4245,166 @@ function handoffCmd(flags, positionals) {
3784
4245
  console.log(`
3785
4246
  written: ${out}`);
3786
4247
  }
3787
- function migrateCmd(flags) {
4248
+ function lessonCmd(flags, positionals) {
4249
+ const projectDir = resolveProjectDir(str(flags.project));
4250
+ const text2 = positionals.slice(1).join(" ").trim();
4251
+ if (!text2) {
4252
+ console.error('usage: mugiwara lesson "<text>" [--project <dir>]');
4253
+ process.exit(1);
4254
+ }
4255
+ const file = join21(projectDir, ".mugiwara", "lessons.md");
4256
+ const date = new Date().toISOString().slice(0, 10);
4257
+ const sanitized = text2.replace(/\|/g, "/").replace(/\r?\n/g, " ").trim();
4258
+ const line = `| ${date} | manual | general | ${sanitized} |`;
4259
+ const header = `| Date | Mission | Area | Lesson |
4260
+ |---|---|---|---|`;
4261
+ let existing = "";
4262
+ try {
4263
+ existing = readFileSync15(file, "utf8");
4264
+ } catch {}
4265
+ if (!existing) {
4266
+ mkdirSync10(join21(projectDir, ".mugiwara"), { recursive: true });
4267
+ writeFileSync10(file, header + `
4268
+ ` + line + `
4269
+ `);
4270
+ } else {
4271
+ const needsNewline = !existing.endsWith(`
4272
+ `);
4273
+ writeFileSync10(file, existing + (needsNewline ? `
4274
+ ` : "") + line + `
4275
+ `);
4276
+ }
4277
+ console.log(`lesson appended: ${line}`);
4278
+ }
4279
+ function migrateCmd(flags, positionals = []) {
3788
4280
  const projectDir = resolveProjectDir(str(flags.project));
3789
4281
  const dryRun = flag(flags.dryRun);
4282
+ const toTeam = str(flags.toTeam);
4283
+ const toSolo = str(flags.toSolo);
4284
+ if (toTeam || toSolo) {
4285
+ const member = toTeam ?? toSolo;
4286
+ if (!/^[A-Za-z0-9._-]+$/.test(member) || /^\.+$/.test(member) || member === "state" || member === "continue") {
4287
+ console.error(`invalid member name "${member}" (allowlist: [a-zA-Z0-9._-], not a dot-path, not state/continue)`);
4288
+ process.exit(1);
4289
+ }
4290
+ if (toTeam && toSolo) {
4291
+ console.error("use either --to-team or --to-solo, not both");
4292
+ process.exit(1);
4293
+ }
4294
+ const missionsRootInner = join21(projectDir, ".mugiwara", "missions");
4295
+ let mission = str(flags.mission) ?? (positionals[1] ? String(positionals[1]) : null);
4296
+ const inferMission = () => {
4297
+ if (!existsSync16(missionsRootInner))
4298
+ return null;
4299
+ const all = readdirSync10(missionsRootInner, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4300
+ if (mission && all.includes(mission))
4301
+ return mission;
4302
+ if (mission)
4303
+ return mission;
4304
+ if (toTeam) {
4305
+ const candidates = all.filter((m) => existsSync16(join21(missionsRootInner, m, "state.json")));
4306
+ if (candidates.length === 1)
4307
+ return candidates[0];
4308
+ if (candidates.length === 0) {
4309
+ console.error("no solo mission with state.json found for --to-team");
4310
+ process.exit(1);
4311
+ }
4312
+ console.error(`multiple solo missions: ${candidates.join(", ")} — specify --mission <id>`);
4313
+ process.exit(1);
4314
+ } else {
4315
+ const candidates = all.filter((m) => existsSync16(join21(missionsRootInner, m, `${member}.json`)));
4316
+ if (candidates.length === 1)
4317
+ return candidates[0];
4318
+ if (candidates.length === 0) {
4319
+ console.error(`no mission with ${member}.json found for --to-solo`);
4320
+ process.exit(1);
4321
+ }
4322
+ console.error(`multiple missions with ${member}.json: ${candidates.join(", ")} — specify --mission <id>`);
4323
+ process.exit(1);
4324
+ }
4325
+ return null;
4326
+ };
4327
+ const targetMission = inferMission();
4328
+ if (!targetMission) {
4329
+ console.error("could not infer mission — specify --mission <id>");
4330
+ process.exit(1);
4331
+ }
4332
+ const dir = join21(missionsRootInner, targetMission);
4333
+ if (toTeam) {
4334
+ const srcState = join21(dir, "state.json");
4335
+ const srcContinue = join21(dir, "continue.json");
4336
+ const destState = join21(dir, `${member}.json`);
4337
+ const destContinue = join21(dir, `continue-${member}.json`);
4338
+ if (!existsSync16(srcState)) {
4339
+ console.error(`mission "${targetMission}" has no state.json — already team or not found`);
4340
+ process.exit(1);
4341
+ }
4342
+ if (existsSync16(destState)) {
4343
+ console.error(`destination ${destState} already exists`);
4344
+ process.exit(1);
4345
+ }
4346
+ const toMove = [{ src: srcState, dest: destState }];
4347
+ if (existsSync16(srcContinue))
4348
+ toMove.push({ src: srcContinue, dest: destContinue });
4349
+ for (const m of toMove) {
4350
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
4351
+ if (!dryRun) {
4352
+ mkdirSync10(dirname6(m.dest), { recursive: true });
4353
+ try {
4354
+ renameSync2(m.src, m.dest);
4355
+ } catch {
4356
+ try {
4357
+ writeFileSync10(m.dest, readFileSync15(m.src));
4358
+ rmSync3(m.src, { force: true });
4359
+ } catch {}
4360
+ }
4361
+ }
4362
+ }
4363
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
4364
+ return;
4365
+ } else {
4366
+ const srcState = join21(dir, `${member}.json`);
4367
+ const srcContinue = join21(dir, `continue-${member}.json`);
4368
+ const destState = join21(dir, "state.json");
4369
+ const destContinue = join21(dir, "continue.json");
4370
+ if (!existsSync16(srcState)) {
4371
+ console.error(`mission "${targetMission}" has no ${member}.json`);
4372
+ process.exit(1);
4373
+ }
4374
+ const files = readdirSync10(dir).filter((f) => {
4375
+ const stem = f.replace(/\.json$/, "");
4376
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
4377
+ });
4378
+ const members = files.filter((f) => f !== "state.json");
4379
+ if (members.length > 1) {
4380
+ console.error(`mission "${targetMission}" has ${members.length} members (${members.join(", ")}) — refusing --to-solo (would orphan)`);
4381
+ process.exit(1);
4382
+ }
4383
+ if (existsSync16(destState)) {
4384
+ console.error(`destination ${destState} already exists`);
4385
+ process.exit(1);
4386
+ }
4387
+ const toMove = [{ src: srcState, dest: destState }];
4388
+ if (existsSync16(srcContinue))
4389
+ toMove.push({ src: srcContinue, dest: destContinue });
4390
+ for (const m of toMove) {
4391
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
4392
+ if (!dryRun) {
4393
+ mkdirSync10(dirname6(m.dest), { recursive: true });
4394
+ try {
4395
+ renameSync2(m.src, m.dest);
4396
+ } catch {
4397
+ try {
4398
+ writeFileSync10(m.dest, readFileSync15(m.src));
4399
+ rmSync3(m.src, { force: true });
4400
+ } catch {}
4401
+ }
4402
+ }
4403
+ }
4404
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
4405
+ return;
4406
+ }
4407
+ }
3790
4408
  const legacyState = join21(projectDir, ".mugiwara", "state");
3791
4409
  const legacyContinue = join21(projectDir, ".mugiwara", "continue");
3792
4410
  const missionsRoot = join21(projectDir, ".mugiwara", "missions");
@@ -3939,7 +4557,12 @@ Usage:
3939
4557
  mugiwara sign --gen-key [--backend pure|minisign]
3940
4558
  create signing keys (pure ed25519 default)
3941
4559
  mugiwara migrate [--dry-run] [--project <dir>]
3942
- move legacy .mugiwara/state/ layout to .mugiwara/missions/
4560
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
4561
+ mugiwara migrate --to-team <member> [--mission <id>] [--dry-run]
4562
+ move state.json -> <member>.json (solo -> team)
4563
+ mugiwara migrate --to-solo <member> [--mission <id>] [--dry-run]
4564
+ move <member>.json -> state.json (team -> solo; refuses if >1 member)
4565
+ mugiwara lesson "<text>" append a dated row to .mugiwara/lessons.md
3943
4566
  mugiwara run <script> [args...]
3944
4567
  run a bundled harness script here (${RUNNABLE.join(", ")})
3945
4568
  mugiwara savepoint <mission> [member] [flow] [mode]