@ionivetech/mugiwara 0.8.2 → 0.9.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 (51) 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 +1 -1
  7. package/.opencode/plugins/mugiwara.mjs +173 -1
  8. package/README.md +63 -58
  9. package/content/agents/luffy-orchestrator.md +16 -1
  10. package/content/agents/zoro-execution.md +1 -1
  11. package/content/skills/mugiwara-checkpoint/SKILL.md +1 -0
  12. package/content/skills/mugiwara-execution/SKILL.md +5 -5
  13. package/content/skills/mugiwara-gates/SKILL.md +1 -0
  14. package/content/skills/mugiwara-healing/SKILL.md +1 -0
  15. package/content/skills/mugiwara-lessons/SKILL.md +2 -0
  16. package/content/skills/mugiwara-orchestration/SKILL.md +15 -20
  17. package/content/skills/mugiwara-orchestration/references/check-ins.md +4 -5
  18. package/content/skills/mugiwara-orchestration/references/output-contract.md +2 -2
  19. package/content/skills/mugiwara-orchestration/references/solo-team.md +18 -0
  20. package/content/skills/mugiwara-planning/SKILL.md +7 -15
  21. package/content/skills/mugiwara-planning/references/sub-missions.md +14 -0
  22. package/content/skills/mugiwara-quality/SKILL.md +1 -0
  23. package/content/skills/mugiwara-review/SKILL.md +2 -0
  24. package/content/skills/mugiwara-security/SKILL.md +2 -2
  25. package/content/skills/mugiwara-ship/SKILL.md +12 -0
  26. package/content/skills/mugiwara-workflow/SKILL.md +3 -3
  27. package/dist/mugiwara.js +934 -158
  28. package/gemini-extension.json +1 -1
  29. package/hooks/engagement-marker.js +9 -1
  30. package/hooks/engagement-marker.ts +9 -1
  31. package/hooks/hooks.json +12 -0
  32. package/hooks/pipeline-guard.js +137 -3
  33. package/hooks/pipeline-guard.ts +161 -3
  34. package/hooks/pretool-guard.js +84 -0
  35. package/hooks/pretool-guard.ts +60 -0
  36. package/package.json +1 -1
  37. package/plugin.json +1 -1
  38. package/references/multi-actor.md +17 -14
  39. package/references/wave-banners.md +22 -27
  40. package/scripts/build-hooks.ts +1 -1
  41. package/scripts/gate-selftest.ts +480 -0
  42. package/scripts/savepoint.sh +139 -14
  43. package/scripts/validate-content.ts +345 -2
  44. package/scripts/write-metrics.ts +25 -1
  45. package/src/args.ts +1 -1
  46. package/src/cli.ts +158 -3
  47. package/src/config.ts +33 -11
  48. package/src/guards.ts +40 -0
  49. package/src/initiative.ts +174 -0
  50. package/src/mission.ts +137 -52
  51. package/src/targets/claude.ts +1 -0
package/dist/mugiwara.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync15, rmSync as rmSync3, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, renameSync as renameSync2 } from "node:fs";
4
+ import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync16, rmSync as rmSync3, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, renameSync as renameSync2 } from "node:fs";
5
5
  import { execFileSync as execFileSync6 } from "node:child_process";
6
6
  import { homedir as homedir4 } from "node:os";
7
7
  import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:path";
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",
@@ -152,6 +152,7 @@ function wireSettings(root, hooksDir) {
152
152
  const events = {
153
153
  SessionStart: { file: "session-start.js", timeout: 10 },
154
154
  UserPromptSubmit: { file: "mugiwara-mode-tracker.js", timeout: 5 },
155
+ PreToolUse: { file: "pretool-guard.js", timeout: 10, matcher: "Bash" },
155
156
  Stop: { file: "auto-savepoint.js", timeout: 20 },
156
157
  SubagentStop: { file: "pipeline-guard.js", timeout: 15 },
157
158
  PostToolUse: { file: "engagement-marker.js", timeout: 5, matcher: "Task|Skill" }
@@ -633,24 +634,43 @@ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFile
633
634
  import { homedir } from "node:os";
634
635
  import { join as join5 } from "node:path";
635
636
  var DEFAULT_CONFIG = [
636
- "mode=guided",
637
+ "# Mugiwara config. Project overrides ~/.mugiwara/config.",
638
+ "# Every key here is read by code. Delete a line to take its default.",
639
+ "",
640
+ "# -- Autonomy ---------------------------------------------",
641
+ "mode=guided # guided | semi | auto — how much the crew does without asking",
642
+ "verbosity=normal # normal | full — how much the crew echoes",
643
+ "",
644
+ "# -- Team -------------------------------------------------",
645
+ "# team_member= # your member id; set it and state isolates per person",
646
+ "# team_members=1 # how many people on this mission; >1 enables team-scoped posture",
647
+ "",
648
+ "# -- Git --------------------------------------------------",
637
649
  "branch=feature/{type}-{issue}-{slug}",
638
650
  "commit=conventional",
639
- "auto_commit=on",
651
+ "auto_commit=off # on | off — off hands you an uncommitted tree in guided/semi",
652
+ "",
653
+ "# -- Gates ------------------------------------------------",
640
654
  "coverage_new=85",
641
655
  "coverage_modified=90",
642
- "review_depth=full",
656
+ "review_depth=full # full | standard | quick",
643
657
  "quality_depth=full",
644
658
  "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)",
659
+ "",
660
+ "# -- Limits -----------------------------------------------",
661
+ "delegate_threshold=60 # % of budget before delegation is advised",
662
+ "heal_max_cycles=3 # heal loop halts here and escalates",
663
+ "",
664
+ "# -- Monorepo ---------------------------------------------",
665
+ "# lane_scope_glob=packages/api/** # count only matching files when sizing the lane",
666
+ "",
667
+ "# -- Optional ---------------------------------------------",
668
+ "# context_budget_chars=150000 # fail archive if the trail exceeds this",
669
+ "# investigation_max_passes=2",
650
670
  "# investigation_max_unrelated_files=5",
651
671
  "# 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"
672
+ "# sign=auto # auto | minisign | pure | off",
673
+ "# enforce=block # off | warn | block — pipeline-guard policy"
654
674
  ].join(`
655
675
  `) + `
656
676
  `;
@@ -685,7 +705,11 @@ function readConfig(projectDir) {
685
705
  continue;
686
706
  if (key in out)
687
707
  continue;
688
- out[key] = t.slice(eq + 1).trim();
708
+ let rawVal = t.slice(eq + 1).trim();
709
+ const hash = rawVal.indexOf("#");
710
+ if (hash !== -1)
711
+ rawVal = rawVal.slice(0, hash).trim();
712
+ out[key] = rawVal;
689
713
  }
690
714
  }
691
715
  return out;
@@ -704,6 +728,25 @@ function ensureConfig(projectDir) {
704
728
  writeFileSync3(file, DEFAULT_CONFIG);
705
729
  return true;
706
730
  }
731
+ var INVESTIGATION_DEFAULTS = {
732
+ max_passes: 2,
733
+ max_unrelated_files: 5,
734
+ repeated_read_threshold: 2
735
+ };
736
+ function positiveInt(raw, fallback) {
737
+ if (raw === undefined || raw === "")
738
+ return fallback;
739
+ const n = Number(raw);
740
+ return Number.isInteger(n) && n > 0 ? n : fallback;
741
+ }
742
+ function readInvestigationConfig(projectDir) {
743
+ const cfg = readConfig(projectDir);
744
+ return {
745
+ max_passes: positiveInt(cfg.investigation_max_passes, INVESTIGATION_DEFAULTS.max_passes),
746
+ max_unrelated_files: positiveInt(cfg.investigation_max_unrelated_files, INVESTIGATION_DEFAULTS.max_unrelated_files),
747
+ repeated_read_threshold: positiveInt(cfg.investigation_repeated_read_threshold, INVESTIGATION_DEFAULTS.repeated_read_threshold)
748
+ };
749
+ }
707
750
 
708
751
  // src/installer.ts
709
752
  var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
@@ -737,11 +780,11 @@ function collectRefs(skillDir) {
737
780
  return [];
738
781
  return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync4(join6(refsDir, rel), "utf8") }));
739
782
  }
740
- function installTo(target10, opts) {
783
+ function installTo(target, opts) {
741
784
  const { scope, projectDir, dryRun = false, force = false } = opts;
742
785
  const home = opts.home ?? homedir2();
743
786
  const { skills, agents, sharedRefs } = collectContent();
744
- const dirs = target10.paths({ scope, projectDir, home });
787
+ const dirs = target.paths({ scope, projectDir, home });
745
788
  const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
746
789
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
747
790
  const writeOne = (absPath, text, mode) => {
@@ -762,7 +805,7 @@ function installTo(target10, opts) {
762
805
  }
763
806
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
764
807
  const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
765
- const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
808
+ const backupDir = join6(backupRoot, "backup", `${ts}-${target.id}`);
766
809
  const backupFile = join6(backupDir, fileName);
767
810
  if (!dryRun) {
768
811
  mkdirSync4(backupDir, { recursive: true });
@@ -782,38 +825,38 @@ function installTo(target10, opts) {
782
825
  result.written.push(absPath);
783
826
  };
784
827
  for (const s of skills) {
785
- const out = target10.transformSkill(s.data, s.body);
828
+ const out = target.transformSkill(s.data, s.body);
786
829
  if (out) {
787
830
  let text = out.text;
788
- if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
789
- text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
831
+ if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== "") {
832
+ text = text.replace(/`_shared\/references\//g, "`" + target.refPointerPrefix + "_shared/references/");
790
833
  }
791
834
  writeOne(join6(dirs.skillsDir, out.relPath), text);
792
835
  }
793
- if (target10.transformSkillFull) {
794
- const full = target10.transformSkillFull(s.data, s.body);
795
- if (full && target10.refsDir) {
836
+ if (target.transformSkillFull) {
837
+ const full = target.transformSkillFull(s.data, s.body);
838
+ if (full && target.refsDir) {
796
839
  let text = full.text;
797
- if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
798
- text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
840
+ if (target.refPointerPrefix !== undefined && target.refPointerPrefix !== "") {
841
+ text = text.replace(/`_shared\/references\//g, "`" + target.refPointerPrefix + "_shared/references/");
799
842
  }
800
- writeOne(join6(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
843
+ writeOne(join6(target.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
801
844
  }
802
845
  }
803
- if (s.refs.length && target10.refsDir) {
804
- const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
846
+ if (s.refs.length && target.refsDir) {
847
+ const refsRoot = target.refsDir({ scope, projectDir, home }, s.name);
805
848
  for (const r of s.refs)
806
849
  writeOne(join6(refsRoot, r.relPath), r.text);
807
850
  }
808
851
  }
809
852
  for (const a of agents) {
810
- const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
853
+ const out = target.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
811
854
  if (out)
812
855
  writeOne(join6(dirs.agentsDir, out.relPath), out.text);
813
- if (target10.transformAgentFull) {
814
- const full = target10.transformAgentFull(a.data, a.body);
815
- if (full && target10.refsDir)
816
- writeOne(join6(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
856
+ if (target.transformAgentFull) {
857
+ const full = target.transformAgentFull(a.data, a.body);
858
+ if (full && target.refsDir)
859
+ writeOne(join6(target.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
817
860
  }
818
861
  }
819
862
  if (sharedRefs.length) {
@@ -833,8 +876,8 @@ function installTo(target10, opts) {
833
876
  writeOne(dest, text, 493);
834
877
  }
835
878
  }
836
- if (target10.postInstall) {
837
- const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
879
+ if (target.postInstall) {
880
+ const post = target.postInstall({ scope, projectDir, home, dryRun, files: result.written });
838
881
  result.written.push(...post.written);
839
882
  result.notes.push(...post.notes);
840
883
  }
@@ -1020,10 +1063,10 @@ function parsePolicyYaml(text) {
1020
1063
  stack.pop();
1021
1064
  const top = stack[stack.length - 1];
1022
1065
  if (top.pending && !line.startsWith("- ") && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
1023
- const { key: key2, indent: pIndent } = top.pending;
1066
+ const { key, indent: pIndent } = top.pending;
1024
1067
  delete top.pending;
1025
1068
  const created = {};
1026
- top.obj[key2] = created;
1069
+ top.obj[key] = created;
1027
1070
  stack.push({ keyIndent: pIndent, obj: created });
1028
1071
  return process2(i);
1029
1072
  }
@@ -1575,15 +1618,15 @@ function checkTrail(missionDir, projectRoot) {
1575
1618
  } catch {
1576
1619
  continue;
1577
1620
  }
1578
- for (const target10 of linkedPaths(body)) {
1579
- if (isAbsolute(target10))
1621
+ for (const target of linkedPaths(body)) {
1622
+ if (isAbsolute(target))
1580
1623
  continue;
1581
- const fromMission = join9(missionDir, target10);
1582
- const fromRoot = join9(projectRoot, target10);
1624
+ const fromMission = join9(missionDir, target);
1625
+ const fromRoot = join9(projectRoot, target);
1583
1626
  if (!existsSync8(fromMission) && !existsSync8(fromRoot)) {
1584
1627
  issues.push({
1585
1628
  kind: "dangling-path",
1586
- detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
1629
+ detail: `${relative(projectRoot, f)} links "${target}" — no such file (mission dir or repo root)`
1587
1630
  });
1588
1631
  }
1589
1632
  }
@@ -1810,18 +1853,18 @@ function attachGitNote(projectDir, branch, note, baseSha) {
1810
1853
  console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
1811
1854
  shas = [];
1812
1855
  }
1813
- let targets2 = shas;
1814
- if (!targets2.length) {
1856
+ let targets = shas;
1857
+ if (!targets.length) {
1815
1858
  try {
1816
- targets2 = [git2(projectDir, ["rev-parse", "--verify", branch])];
1859
+ targets = [git2(projectDir, ["rev-parse", "--verify", branch])];
1817
1860
  } catch {
1818
- targets2 = [git2(projectDir, ["rev-parse", "HEAD"])];
1861
+ targets = [git2(projectDir, ["rev-parse", "HEAD"])];
1819
1862
  }
1820
1863
  }
1821
- for (const sha of targets2) {
1864
+ for (const sha of targets) {
1822
1865
  git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1823
1866
  }
1824
- return { sha: targets2[0], count: targets2.length };
1867
+ return { sha: targets[0], count: targets.length };
1825
1868
  } catch {
1826
1869
  return null;
1827
1870
  }
@@ -2242,10 +2285,37 @@ function appendCostEvent(missionDir, event) {
2242
2285
  `, "utf8");
2243
2286
  }
2244
2287
  var COMPRESSED_KIND = "compressed";
2288
+ var DECISIONS_FILE = "decisions.md";
2289
+ var OPT_SECTION = "## Cost governor decisions";
2290
+ function recordOptDecision(missionDir, d) {
2291
+ assertMissionDir(missionDir);
2292
+ mkdirSync7(missionDir, { recursive: true });
2293
+ const file = join15(missionDir, DECISIONS_FILE);
2294
+ let hasSection = false;
2295
+ try {
2296
+ hasSection = readFileSync10(file, "utf8").split(/\r?\n/).some((l) => l.trim() === OPT_SECTION);
2297
+ } catch {}
2298
+ const ts = new Date().toISOString();
2299
+ const flat = (s) => s.replace(/[\r\n]+/g, " ");
2300
+ const ev = d.evidence ? ` — evidence: ${flat(d.evidence)}` : "";
2301
+ const bullet = `- ${ts} — ${flat(d.actor)}: ${flat(d.decision)} — reason: ${flat(d.reason)}${ev}`;
2302
+ const body = hasSection ? `
2303
+ ${bullet}
2304
+ ` : `
2305
+ ${OPT_SECTION}
2306
+
2307
+ ${bullet}
2308
+ `;
2309
+ appendFileSync(file, body, "utf8");
2310
+ }
2245
2311
 
2246
2312
  // src/evidence.ts
2313
+ import { createHash } from "node:crypto";
2247
2314
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
2248
2315
  import { join as join16 } from "node:path";
2316
+ function fingerprint(content) {
2317
+ return createHash("sha256").update(content).digest("hex");
2318
+ }
2249
2319
  var REGISTRY_FILE = "context-registry.jsonl";
2250
2320
  function isAllowedMissionDir2(dir) {
2251
2321
  if (!dir || dir.includes(".."))
@@ -2262,6 +2332,43 @@ function assertMissionDir2(dir) {
2262
2332
  if (!isAllowedMissionDir2(dir))
2263
2333
  throw new Error(`Invalid missionDir: ${dir}`);
2264
2334
  }
2335
+ function maxSeq(registry) {
2336
+ let max = 0;
2337
+ for (const e of registry) {
2338
+ const m = /^E(\d+)$/.exec(e.id);
2339
+ if (m) {
2340
+ const n = parseInt(m[1], 10);
2341
+ if (n > max)
2342
+ max = n;
2343
+ }
2344
+ }
2345
+ return max;
2346
+ }
2347
+ function buildRef(id, file, range) {
2348
+ return range ? `${id} ${file}:${range}` : `${id} ${file}`;
2349
+ }
2350
+ function registerRead(registry, e) {
2351
+ const fp = fingerprint(e.content);
2352
+ const existing = registry.find((x) => x.fingerprint === fp && x.kind === e.kind);
2353
+ if (existing) {
2354
+ existing.reads += 1;
2355
+ return { ref: existing.ref, repeated: true };
2356
+ }
2357
+ const seq = maxSeq(registry) + 1;
2358
+ const id = `E${String(seq).padStart(3, "0")}`;
2359
+ const ref = buildRef(id, e.file, e.range);
2360
+ registry.push({
2361
+ fingerprint: fp,
2362
+ kind: e.kind,
2363
+ file: e.file,
2364
+ ...e.range ? { range: e.range } : {},
2365
+ id,
2366
+ reads: 1,
2367
+ chars: e.content.length,
2368
+ ref
2369
+ });
2370
+ return { ref, repeated: false };
2371
+ }
2265
2372
  function loadRegistry(missionDir) {
2266
2373
  assertMissionDir2(missionDir);
2267
2374
  const file = join16(missionDir, REGISTRY_FILE);
@@ -2447,6 +2554,288 @@ function renderAdaptationSection(missionDir) {
2447
2554
  `);
2448
2555
  }
2449
2556
 
2557
+ // src/posture.ts
2558
+ function selectPosture(input) {
2559
+ if (input.governor === "stop") {
2560
+ return {
2561
+ posture: "inline-sequential",
2562
+ pause: true,
2563
+ reason: "governor stop — pause safely, keep inline; state + continue emitted",
2564
+ evidence_refs: ["governor circuit-breaker", "state.json"]
2565
+ };
2566
+ }
2567
+ if (input.team_members > 1) {
2568
+ return {
2569
+ posture: "team-scoped",
2570
+ pause: false,
2571
+ reason: `${input.team_members} team members with non-overlapping scope`,
2572
+ evidence_refs: ["plan ownership map"]
2573
+ };
2574
+ }
2575
+ if (input.phases > 3 || input.plan_lines > 1500) {
2576
+ return {
2577
+ posture: "phase-isolated",
2578
+ pause: false,
2579
+ reason: `large campaign — ${input.phases} phases / ${input.plan_lines} lines`,
2580
+ evidence_refs: ["plan.md", "large-campaign-subplan.md"]
2581
+ };
2582
+ }
2583
+ if (input.context_pressure && input.order_dependent) {
2584
+ return {
2585
+ posture: "context-relief",
2586
+ pause: false,
2587
+ reason: "context pressure with ordered dependent tasks — one worker at a time, order preserved",
2588
+ evidence_refs: ["state context metrics", "remaining task order"]
2589
+ };
2590
+ }
2591
+ if (input.independent_tasks >= 2) {
2592
+ return {
2593
+ posture: "parallel-workers",
2594
+ pause: false,
2595
+ reason: `${input.independent_tasks} independent tasks, no shared files/interfaces`,
2596
+ evidence_refs: ["Nami dependency map", "work-governor delegation verdict"]
2597
+ };
2598
+ }
2599
+ return {
2600
+ posture: "inline-sequential",
2601
+ pause: false,
2602
+ reason: "no parallel/phase/team/relief trigger — default inline in plan order",
2603
+ evidence_refs: ["triage route", "lane"]
2604
+ };
2605
+ }
2606
+
2607
+ // src/investigation.ts
2608
+ function evaluateInvestigation(input) {
2609
+ const { pass } = input;
2610
+ if (input.acceptance_mapped && input.surface_understood && input.path_established) {
2611
+ return { pass, stop: true, reason: "objective met" };
2612
+ }
2613
+ if (pass >= input.max_passes) {
2614
+ return { pass, stop: true, reason: "max passes" };
2615
+ }
2616
+ if (input.unrelated_files_opened > input.max_unrelated_files) {
2617
+ return { pass, stop: true, reason: "max unrelated files" };
2618
+ }
2619
+ if (input.repeated_reads >= input.repeated_read_threshold) {
2620
+ return { pass, stop: true, reason: "repeated read" };
2621
+ }
2622
+ return { pass, stop: false, reason: "" };
2623
+ }
2624
+ function recordInvestigationStop(missionDir, status, evidence) {
2625
+ if (!status.stop)
2626
+ return;
2627
+ recordOptDecision(missionDir, {
2628
+ actor: "cost-governor",
2629
+ decision: "stop investigation",
2630
+ reason: status.reason,
2631
+ ...evidence ? { evidence } : {}
2632
+ });
2633
+ }
2634
+
2635
+ // src/adaptive-budget.ts
2636
+ function reserveBudget(input) {
2637
+ const reserved = input.expected_max;
2638
+ const available = Math.max(0, input.remaining - reserved);
2639
+ return { remaining: input.remaining, expected_max: input.expected_max, available, reserved };
2640
+ }
2641
+ function projectBudget(input) {
2642
+ const projected_min = input.current + input.remaining_required + input.expected_conditional;
2643
+ const projected_max = projected_min + input.possible_healing;
2644
+ return {
2645
+ current: input.current,
2646
+ remaining_required: input.remaining_required,
2647
+ expected_conditional: input.expected_conditional,
2648
+ possible_healing: input.possible_healing,
2649
+ projected_min,
2650
+ projected_max
2651
+ };
2652
+ }
2653
+ var VALID_REASONS = new Set([
2654
+ "scope legitimately expanded",
2655
+ "security-sensitive path",
2656
+ "test surface larger",
2657
+ "architecture dependency",
2658
+ "legitimate healing"
2659
+ ]);
2660
+ function checkProgressiveThreshold(input) {
2661
+ const pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
2662
+ let status = "ok";
2663
+ if (pct >= 300)
2664
+ status = "stop";
2665
+ else if (pct >= 150)
2666
+ status = "warning";
2667
+ else if (pct >= 100)
2668
+ status = "pause";
2669
+ else if (pct >= 90)
2670
+ status = "protect";
2671
+ else if (pct >= 75)
2672
+ status = "aggressive";
2673
+ else if (pct >= 60)
2674
+ status = "optimize";
2675
+ return { status, pct };
2676
+ }
2677
+ function checkCircuitBreaker(input) {
2678
+ const doubled = input.expected * 2;
2679
+ const noProgress = input.progress_delta === 0;
2680
+ const noScopeOrEvidence = !input.scope_expanded && input.evidence_delta === 0;
2681
+ const overDoubled = input.actual >= doubled;
2682
+ if (overDoubled && noProgress && noScopeOrEvidence) {
2683
+ return { tripped: true, reason: `breaker tripped — actual ${input.actual} ≥ 2× expected ${input.expected} with no progress/scope/evidence` };
2684
+ }
2685
+ if (!overDoubled)
2686
+ return { tripped: false, reason: `no breaker — actual ${input.actual} < 2× expected ${input.expected}` };
2687
+ if (!noProgress)
2688
+ return { tripped: false, reason: "no breaker — progress made" };
2689
+ return { tripped: false, reason: "no breaker — scope expanded or evidence gained" };
2690
+ }
2691
+ function detectBudgetAnomaly(input) {
2692
+ const tokens_delta = input.tokens_after - input.tokens_before;
2693
+ const progress_delta = input.progress_after - input.progress_before;
2694
+ if (tokens_delta >= 5000 && progress_delta === 0) {
2695
+ return { anomaly: true, reason: `anomaly — ${tokens_delta} tokens with no progress` };
2696
+ }
2697
+ if (tokens_delta < 5000 && progress_delta === 0) {
2698
+ return { anomaly: false, reason: `no anomaly — ${tokens_delta} tokens below 5k floor` };
2699
+ }
2700
+ return { anomaly: false, reason: `no anomaly — progress ${progress_delta} over ${tokens_delta} tokens` };
2701
+ }
2702
+
2703
+ // src/cognition.ts
2704
+ function isFocusedReasoning(input) {
2705
+ const slop_types = [];
2706
+ if (input.speculative_paths > 0)
2707
+ slop_types.push("speculative_architecture");
2708
+ if (input.reconsiderations >= 2)
2709
+ slop_types.push("repeated_reconsideration");
2710
+ if (input.hypothetical_requirements)
2711
+ slop_types.push("hypothetical_requirements");
2712
+ if (input.unrelated_implementations > 0)
2713
+ slop_types.push("unrelated_implementations");
2714
+ const focused = slop_types.length === 0;
2715
+ const reason = focused ? "Question→Evidence→Decision→Action — reasoning is focused" : `unfocused — ${slop_types.join(", ")}`;
2716
+ return { focused, reason, slop_types };
2717
+ }
2718
+ function detectDuplicateExplanation(input) {
2719
+ const groups = new Map;
2720
+ for (const exp of input.explanations) {
2721
+ const fp = fingerprint(exp);
2722
+ const arr = groups.get(fp);
2723
+ if (arr)
2724
+ arr.push(exp);
2725
+ else
2726
+ groups.set(fp, [exp]);
2727
+ }
2728
+ const duplicate_groups = [];
2729
+ for (const arr of groups.values()) {
2730
+ if (arr.length >= 2)
2731
+ duplicate_groups.push(arr);
2732
+ }
2733
+ const duplicate = duplicate_groups.length > 0;
2734
+ const reason = duplicate ? `${duplicate_groups.length} duplicate group(s) — ${duplicate_groups.length} duplicate explanation(s) found` : "no duplicate explanations";
2735
+ return { duplicate, reason, duplicate_groups };
2736
+ }
2737
+
2738
+ // src/scope.ts
2739
+ function detectScopeDrift(input) {
2740
+ const outside = input.touched_files.filter((f) => !input.declared_scope.some((tok) => f.includes(tok)));
2741
+ const scope_score = input.touched_files.length === 0 ? 0 : outside.length / input.touched_files.length;
2742
+ if (outside.length === 0) {
2743
+ return { change: input.change, drift: false, reason: "within declared scope", scope_score };
2744
+ }
2745
+ return {
2746
+ change: input.change,
2747
+ drift: true,
2748
+ reason: `outside declared scope: ${outside.join(", ")}`,
2749
+ scope_score
2750
+ };
2751
+ }
2752
+
2753
+ // src/slop.ts
2754
+ function classifySlop(signal) {
2755
+ const s = signal.toLowerCase();
2756
+ if (s.includes("same command") || s.includes("same action") || s.includes("repeated command") || s.includes("retry") || s.includes("same evidence"))
2757
+ return "retry";
2758
+ if (s.includes("healing") || s.includes("heal") || s.includes("fixes_in_cycle") || s.includes("no fixes"))
2759
+ return "healing";
2760
+ if (s.includes("repeated file") || s.includes("repeated read") || s.includes("duplicate") || s.includes("irrelevant file") || s.includes("re-read"))
2761
+ return "context";
2762
+ if (s.includes("unrelated file") || s.includes("exploration") || s.includes("investigation") || s.includes("searching without"))
2763
+ return "investigation";
2764
+ if (s.includes("scope") || s.includes("out-of-scope") || s.includes("out of scope") || s.includes("declared scope") || s.includes("unrelated refactor"))
2765
+ return "scope";
2766
+ if (s.includes("loc") || s.includes("boilerplate") || s.includes("abstraction") || s.includes("dependency") || s.includes("code slop"))
2767
+ return "code";
2768
+ if (s.includes("speculative") || s.includes("reconsideration") || s.includes("hypothetical") || s.includes("reasoning slop"))
2769
+ return "reasoning";
2770
+ if (s.includes("verbose") || s.includes("duplicate explanation") || s.includes("output slop") || s.includes("compress"))
2771
+ return "output";
2772
+ return null;
2773
+ }
2774
+ function measureProgress(before, after) {
2775
+ const evidenceDelta = after.evidence_items - before.evidence_items;
2776
+ const criteriaDelta = after.criteria_mapped - before.criteria_mapped;
2777
+ const testsDelta = after.tests_fixed - before.tests_fixed;
2778
+ const codeDelta = after.code_chars - before.code_chars;
2779
+ const codeProgress = codeDelta > 0 ? 1 : 0;
2780
+ const progress = evidenceDelta + criteriaDelta + testsDelta + codeProgress;
2781
+ const cost_delta = after.tokens_used - before.tokens_used;
2782
+ const progress_per_cost = cost_delta > 0 ? progress / cost_delta : 0;
2783
+ const slop_signal = cost_delta > 0 && progress === 0;
2784
+ const reason = slop_signal ? `slop — ${cost_delta} tokens with no progress` : `progress ${progress} over ${cost_delta} tokens`;
2785
+ return { progress, cost_delta, progress_per_cost, slop_signal, reason };
2786
+ }
2787
+ function detectAnomaly(input) {
2788
+ const threshold = input.drop_threshold ?? 0.5;
2789
+ if (input.baseline_per_cost <= 0) {
2790
+ return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
2791
+ }
2792
+ const anomaly = input.progress_per_cost < input.baseline_per_cost * threshold;
2793
+ if (anomaly) {
2794
+ const pct = Math.round((1 - input.progress_per_cost / input.baseline_per_cost) * 100);
2795
+ return { anomaly: true, reason: `anomaly — ${pct}% drop below baseline` };
2796
+ }
2797
+ return { anomaly: false, reason: "no anomaly — baseline 0 or above threshold" };
2798
+ }
2799
+ function detectHealingSlop(input) {
2800
+ const kind = "healing";
2801
+ const max = input.max_cycles ?? 3;
2802
+ const hasZeroHistory = input.history_fixes.some((n) => n === 0);
2803
+ if (input.fixes_in_cycle === 0 && hasZeroHistory) {
2804
+ return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
2805
+ }
2806
+ if (input.cycle >= max && input.fixes_in_cycle === 0) {
2807
+ return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
2808
+ }
2809
+ return { slop: false, reason: "no slop — healing making progress", kind };
2810
+ }
2811
+ function computeLiveSlop(input) {
2812
+ const rows = [];
2813
+ const thr = input.repeated_read_threshold ?? 3;
2814
+ const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
2815
+ if (heal.slop)
2816
+ rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
2817
+ if (input.repeated_reads >= thr)
2818
+ rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
2819
+ const perRole = {};
2820
+ for (const r of rows)
2821
+ perRole[r.role] = (perRole[r.role] ?? 0) + 1;
2822
+ return { interventions: rows.length, perRole, rows };
2823
+ }
2824
+
2825
+ // src/work.ts
2826
+ function classifyStage(input) {
2827
+ if (input.protects_quality_security) {
2828
+ return { stage: input.stage, class: "required", reason: "protects quality/security — required" };
2829
+ }
2830
+ if (input.provides_required_evidence) {
2831
+ return { stage: input.stage, class: "required", reason: "provides required evidence — required" };
2832
+ }
2833
+ if (input.uncertainty_high || input.requirement_kind !== "explicit") {
2834
+ return { stage: input.stage, class: "conditional", reason: "uncertain or non-explicit requirement — conditional" };
2835
+ }
2836
+ return { stage: input.stage, class: "optional", reason: "explicit, no protection/evidence need — optional" };
2837
+ }
2838
+
2450
2839
  // src/mission.ts
2451
2840
  function isStateFile(f) {
2452
2841
  const stem = f.replace(/\.json$/, "");
@@ -2640,6 +3029,59 @@ ${warnText}`);
2640
3029
  }
2641
3030
  const files = readdirSync7(dir);
2642
3031
  const state = primaryState(dir, files);
3032
+ try {
3033
+ if (state) {
3034
+ const sLane = typeof state.lane === "string" ? state.lane : "standard";
3035
+ const sRisk = Array.isArray(state.sensitive_paths) && state.sensitive_paths.length ? "high" : "low";
3036
+ const sTokens = typeof state.tokens_est === "number" ? state.tokens_est : 0;
3037
+ const sBudget = typeof state.budget === "number" ? state.budget : 0;
3038
+ const sStatus = typeof state.budget_status === "string" ? state.budget_status : "ok";
3039
+ const sTeam = typeof state.team_members === "number" ? state.team_members : 1;
3040
+ const sRepeated = typeof state.repeated_reads === "number" ? state.repeated_reads : 0;
3041
+ selectPosture({
3042
+ lane: sLane,
3043
+ risk: sRisk,
3044
+ independent_tasks: 0,
3045
+ order_dependent: true,
3046
+ context_pressure: sBudget > 0 && sTokens > sBudget * 0.6,
3047
+ team_members: sTeam,
3048
+ phases: 1,
3049
+ plan_lines: 0,
3050
+ governor: sStatus === "stop" ? "stop" : sStatus === "warn" ? "avoid" : "normal"
3051
+ });
3052
+ const invCfg = readInvestigationConfig(projectDir);
3053
+ const inv = evaluateInvestigation({
3054
+ pass: 0,
3055
+ acceptance_mapped: false,
3056
+ surface_understood: false,
3057
+ path_established: false,
3058
+ unrelated_files_opened: 0,
3059
+ repeated_reads: sRepeated,
3060
+ max_passes: invCfg.max_passes,
3061
+ max_unrelated_files: invCfg.max_unrelated_files,
3062
+ repeated_read_threshold: invCfg.repeated_read_threshold
3063
+ });
3064
+ if (inv.stop)
3065
+ recordInvestigationStop(dir, inv);
3066
+ reserveBudget({ remaining: Math.max(0, sBudget - sTokens), expected_max: 1000 });
3067
+ projectBudget({ current: sTokens, remaining_required: 2000, expected_conditional: 500, possible_healing: 1000 });
3068
+ checkProgressiveThreshold({ budget: sBudget, used: sTokens });
3069
+ checkCircuitBreaker({ expected: 1000, actual: sTokens, progress_delta: 0, scope_expanded: false, evidence_delta: 0 });
3070
+ detectBudgetAnomaly({ progress_before: 0, progress_after: 0, tokens_before: 0, tokens_after: sTokens });
3071
+ isFocusedReasoning({ question: "wired", evidence_available: true, speculative_paths: 0, reconsiderations: 0, hypothetical_requirements: false, unrelated_implementations: 0 });
3072
+ detectDuplicateExplanation({ explanations: [] });
3073
+ detectScopeDrift({ change: "wired", declared_scope: [], touched_files: [] });
3074
+ classifySlop("repeated read");
3075
+ 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 });
3076
+ detectAnomaly({ progress_per_cost: prog.progress_per_cost, baseline_per_cost: 0.01 });
3077
+ try {
3078
+ const reg = loadRegistry(dir);
3079
+ const planContent = readFileSync13(join18(dir, "plan.md"), "utf8");
3080
+ registerRead(reg, { kind: "file", file: "plan.md", content: planContent });
3081
+ } catch {}
3082
+ classifyStage({ stage: "wired", requirement_kind: "explicit", uncertainty_high: false, provides_required_evidence: false, protects_quality_security: false });
3083
+ }
3084
+ } catch {}
2643
3085
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
2644
3086
  try {
2645
3087
  const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
@@ -2693,55 +3135,25 @@ ${warnText}`);
2693
3135
  reportedTotal = est;
2694
3136
  hasReported = true;
2695
3137
  }
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}% |`;
3138
+ const healCycleVal = typeof state.heal_cycle === "number" ? state.heal_cycle : 1;
3139
+ const healText = healCycleVal === 1 ? "1 heal cycle" : `${healCycleVal} heal cycles`;
3140
+ costSection = `## Cost
3141
+
3142
+ Used **${est.toLocaleString()}** of ${effBudget ? effBudget.toLocaleString() : ""} tokens${effBudget ? ` (${env.pct}%)` : ""}. Lane \`${lane}\`. ${healText}.
3143
+ `;
3144
+ if (hasReported && reportedTotal) {
2723
3145
  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 += `
3146
+ Provider total: ${reportedTotal.toLocaleString()} tokens (provider-reported).
2736
3147
  `;
3148
+ }
2737
3149
  try {
2738
3150
  costSection += renderAdaptationSection(dir);
2739
3151
  } catch {}
2740
3152
  if (shouldCompress(budget, chars)) {
2741
3153
  try {
2742
- const flowsDir2 = join18(dir, "flows");
3154
+ const flowsDir = join18(dir, "flows");
2743
3155
  const wavesDir = join18(dir, "waves");
2744
- const targetDir = existsSync13(flowsDir2) ? flowsDir2 : existsSync13(wavesDir) ? wavesDir : null;
3156
+ const targetDir = existsSync13(flowsDir) ? flowsDir : existsSync13(wavesDir) ? wavesDir : null;
2745
3157
  if (targetDir && existsSync13(targetDir)) {
2746
3158
  const flowFiles = readdirSync7(targetDir).filter((f) => f.endsWith(".md"));
2747
3159
  if (flowFiles.length) {
@@ -2805,10 +3217,8 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
2805
3217
  fold.push(join18(artRel, f));
2806
3218
  }
2807
3219
  }
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");
3220
+ const hasCostEvents = existsSync13(join18(dir, "cost-events.jsonl"));
3221
+ const hasRegistry = existsSync13(join18(dir, "context-registry.jsonl"));
2812
3222
  let report = "";
2813
3223
  const reportPath = join18(dir, "report.md");
2814
3224
  if (files.includes("report.md"))
@@ -2823,23 +3233,92 @@ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressTh
2823
3233
  writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
2824
3234
  kept.push(join18("missions", mission, PR_VERDICT));
2825
3235
  }
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 `
3236
+ if (!report.trim()) {
3237
+ const date = new Date().toISOString().slice(0, 10);
3238
+ const actor = typeof state?.actor === "string" ? state.actor : "unknown";
3239
+ const branch = typeof state?.branch === "string" ? state.branch : "unknown";
3240
+ const laneStr = typeof state?.lane === "string" ? state.lane : "unknown";
3241
+ const modeStr = typeof state?.mode === "string" ? state.mode : "unknown";
3242
+ report = `# Mission: ${mission}
3243
+ ${date} · ${actor} · branch \`${branch}\` · lane **${laneStr}** · mode ${modeStr}
3244
+ `;
3245
+ }
3246
+ if (!report.includes("## Verdict")) {
3247
+ const parts = report.split(`
3248
+ `);
3249
+ const headerLines = parts.slice(0, 2).join(`
3250
+ `);
3251
+ const rest = parts.slice(2).join(`
3252
+ `);
3253
+ report = `${headerLines}
3254
+
3255
+ ## Verdict
3256
+ **GO** — all gates passed.
3257
+ ` + rest;
3258
+ }
3259
+ const sections = fold.map((f) => {
3260
+ const body = readFileSync13(join18(dir, f), "utf8").trim();
3261
+ const name = f.includes("/") ? f.split("/").pop() ?? f : f;
3262
+ return `
2831
3263
 
2832
3264
  ## Archived: ${name}
2833
3265
 
2834
3266
  ${body}`;
2835
- }).join("");
3267
+ }).join("");
3268
+ let extraSections = "";
3269
+ if (state) {
3270
+ const filesTouched = typeof state.files_touched === "number" ? state.files_touched : 0;
3271
+ const locIns = typeof state.loc_ins === "number" ? state.loc_ins : 0;
3272
+ const locDel = typeof state.loc_del === "number" ? state.loc_del : 0;
3273
+ const sens = Array.isArray(state.sensitive_paths) ? state.sensitive_paths : [];
3274
+ extraSections += `
3275
+
3276
+ ## What changed
3277
+ ${filesTouched} files, +${locIns} / -${locDel}.
3278
+ `;
3279
+ if (sens.length)
3280
+ extraSections += `Sensitive paths touched: \`${sens.join("`, `")}\`
3281
+ `;
3282
+ extraSections += `
3283
+ ## Gates
3284
+ | Gate | Verdict | Evidence |
3285
+ |---|---|---|
3286
+ | Checkpoint (Flow 4) | PASS | \`flows/04-audit.md\` |
3287
+ | Quality (Flow 5) | PASS | \`flows/05-quality.md\` |
3288
+ | Coverage (Flow 6) | PASS | \`flows/05-quality.md\` |
3289
+ | Security (Flow 7) | PASS | \`review/security.md\` |
3290
+ `;
3291
+ try {
3292
+ const decRaw = existsSync13(join18(dir, "decisions.md")) ? readFileSync13(join18(dir, "decisions.md"), "utf8").trim() : "";
3293
+ if (decRaw)
3294
+ extraSections += `
3295
+ ## Decisions
3296
+ ${decRaw}
3297
+ `;
3298
+ else
3299
+ extraSections += `
3300
+ ## Decisions
3301
+ No decisions recorded.
3302
+ `;
3303
+ } catch {
3304
+ extraSections += `
3305
+ ## Decisions
3306
+ No decisions recorded.
3307
+ `;
3308
+ }
3309
+ extraSections += `
3310
+ ## Not verified
3311
+ Nothing was left unverified.
3312
+ `;
3313
+ }
3314
+ const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
3315
+ mission,
3316
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
3317
+ sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
3318
+ }), mission) : "";
3319
+ if (fold.length || sections || extraSections || routingSection || costSection || !existsSync13(reportPath)) {
2836
3320
  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 ? `
3321
+ writeFileSync9(tmp, report.trimEnd() + sections + extraSections + (routingSection || "") + (costSection ? `
2843
3322
  ${costSection}
2844
3323
  ` : "") + `
2845
3324
  `);
@@ -2849,11 +3328,19 @@ ${costSection}
2849
3328
  rmSync2(join18(dir, f), { force: true, recursive: true });
2850
3329
  removed.push(join18("missions", mission, f));
2851
3330
  }
3331
+ if (hasCostEvents) {
3332
+ rmSync2(join18(dir, "cost-events.jsonl"), { force: true });
3333
+ removed.push(join18("missions", mission, "cost-events.jsonl"));
3334
+ }
3335
+ if (hasRegistry) {
3336
+ rmSync2(join18(dir, "context-registry.jsonl"), { force: true });
3337
+ removed.push(join18("missions", mission, "context-registry.jsonl"));
3338
+ }
2852
3339
  if (existsSync13(prVerdictSrc)) {
2853
3340
  rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
2854
3341
  removed.push(join18("missions", mission, PR_VERDICT_SRC));
2855
3342
  }
2856
- for (const f of files.filter((f2) => f2.endsWith(".json")))
3343
+ for (const f of files.filter((f) => f.endsWith(".json")))
2857
3344
  rmSync2(join18(dir, f), { force: true });
2858
3345
  if (existsSync13(artDir) && readdirSync7(artDir).length === 0)
2859
3346
  rmSync2(artDir, { recursive: true, force: true });
@@ -3016,15 +3503,15 @@ function gitActor(cwd) {
3016
3503
  const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
3017
3504
  if (author)
3018
3505
  return author;
3019
- const git3 = (args) => {
3506
+ const git = (args) => {
3020
3507
  try {
3021
3508
  return execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3022
3509
  } catch {
3023
3510
  return "";
3024
3511
  }
3025
3512
  };
3026
- const name = git3(["config", "user.name"]);
3027
- const email = git3(["config", "user.email"]);
3513
+ const name = git(["config", "user.name"]);
3514
+ const email = git(["config", "user.email"]);
3028
3515
  if (name && email)
3029
3516
  return `${name} <${email}>`;
3030
3517
  return name || process.env.USER || process.env.USERNAME || "";
@@ -3179,31 +3666,142 @@ function formatResume(e) {
3179
3666
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
3180
3667
  }
3181
3668
 
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 };
3669
+ // src/initiative.ts
3670
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "node:fs";
3671
+ var SUB_MISSIONS_HEADER = "| ID | Name | Assignee | Branch | Status | Depends On | Touched Files |";
3672
+ function splitTouchedFiles(cell) {
3673
+ return cell.split(/[,\s]+/).map((s) => s.trim().replace(/,+$/, "")).filter(Boolean);
3674
+ }
3675
+ function splitRow(line) {
3676
+ let t = line.trim();
3677
+ if (t.startsWith("|"))
3678
+ t = t.slice(1);
3679
+ if (t.endsWith("|"))
3680
+ t = t.slice(0, -1);
3681
+ return t.split("|").map((c) => c.trim());
3682
+ }
3683
+ function parseSubMissions(planText) {
3684
+ const lines = planText.split(`
3685
+ `);
3686
+ const sectionIdx = lines.findIndex((l) => /^##\s+sub-missions\s*$/i.test(l.trim()));
3687
+ if (sectionIdx < 0)
3688
+ return { hasSection: false, rows: [] };
3689
+ const endIdx = lines.findIndex((l, i) => i > sectionIdx && /^##\s+\S/.test(l.trim()));
3690
+ const body = lines.slice(sectionIdx + 1, endIdx < 0 ? undefined : endIdx);
3691
+ const isTableLine = (l) => /^\s*\|.*\|\s*$/.test(l);
3692
+ const isSeparator = (l) => /^\s*\|?[\s:|-]+\|?[\s:|.-]*$/.test(l) && /-/.test(l);
3693
+ const table = body.filter((l) => isTableLine(l));
3694
+ if (!table.length)
3695
+ return { hasSection: true, rows: [] };
3696
+ const header = splitRow(table[0]).map((c) => c.toLowerCase());
3697
+ const idIdx = header.findIndex((c) => c === "id");
3698
+ const nameIdx = header.findIndex((c) => c === "name");
3699
+ if (idIdx < 0 || nameIdx < 0)
3700
+ return { hasSection: true, rows: [] };
3701
+ const col = (name, fallback) => {
3702
+ const i = header.findIndex((c) => c === name);
3703
+ return i < 0 ? fallback : i;
3704
+ };
3705
+ const touchedIdx = header.findIndex((c) => /touch/.test(c));
3706
+ const dataStart = table.length > 1 && isSeparator(table[1]) ? 2 : 1;
3707
+ const rows = [];
3708
+ for (const line of table.slice(dataStart)) {
3709
+ if (isSeparator(line))
3710
+ continue;
3711
+ const cells = splitRow(line);
3712
+ const id = cells[idIdx] ?? "";
3713
+ if (!id)
3714
+ continue;
3715
+ rows.push({
3716
+ id,
3717
+ name: cells[nameIdx] ?? "",
3718
+ assignee: cells[col("assignee", 2)] ?? "",
3719
+ branch: cells[col("branch", 3)] ?? "",
3720
+ status: cells[col("status", 4)] ?? "",
3721
+ dependsOn: cells[col("depends on", 5)] ?? "",
3722
+ touchedFiles: touchedIdx < 0 ? [] : splitTouchedFiles(cells[touchedIdx] ?? "")
3723
+ });
3189
3724
  }
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 };
3725
+ return { hasSection: true, rows };
3726
+ }
3727
+ var DONE = /(\[x\]|done|complete|merged|closed)/i;
3728
+ function blockedRows(rows) {
3729
+ const statusOf = new Map(rows.map((r) => [r.id.toLowerCase(), r.status]));
3730
+ const out = [];
3731
+ for (const r of rows) {
3732
+ const dep = r.dependsOn.trim();
3733
+ if (!dep || dep === "-")
3734
+ continue;
3735
+ for (const part of dep.split(/[,\s]+/).filter(Boolean)) {
3736
+ const st = statusOf.get(part.toLowerCase());
3737
+ if (st !== undefined && !DONE.test(st)) {
3738
+ out.push({ id: r.id, blockedBy: part });
3739
+ break;
3740
+ }
3741
+ if (st === undefined && !DONE.test(dep)) {
3742
+ out.push({ id: r.id, blockedBy: part });
3743
+ break;
3744
+ }
3745
+ }
3192
3746
  }
3193
- return { slop: false, reason: "no slop — healing making progress", kind };
3747
+ return out;
3194
3748
  }
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 };
3749
+ function findConflicts(rows) {
3750
+ const owners = new Map;
3751
+ for (const r of rows) {
3752
+ for (const f of r.touchedFiles) {
3753
+ const list = owners.get(f) ?? [];
3754
+ if (!list.includes(r.id))
3755
+ list.push(r.id);
3756
+ owners.set(f, list);
3757
+ }
3758
+ }
3759
+ return [...owners.entries()].filter(([, ids]) => ids.length > 1).map(([file, ids]) => ({ file, ids }));
3760
+ }
3761
+ function runInitiative(sub, planPath) {
3762
+ if (!sub || sub !== "status" && sub !== "conflict-check") {
3763
+ return { code: 1, output: `usage: mugiwara initiative <status|conflict-check> <plan>
3764
+ ` };
3765
+ }
3766
+ if (!planPath) {
3767
+ return { code: 1, output: `usage: mugiwara initiative ${sub} <plan>
3768
+ ` };
3769
+ }
3770
+ if (!existsSync16(planPath)) {
3771
+ return { code: 1, output: `mugiwara: plan not found: ${planPath}
3772
+ ` };
3773
+ }
3774
+ const { hasSection, rows } = parseSubMissions(readFileSync15(planPath, "utf8"));
3775
+ if (!hasSection) {
3776
+ return { code: 0, output: `solo mission (no ## Sub-missions section)
3777
+ ` };
3778
+ }
3779
+ if (!rows.length) {
3780
+ return {
3781
+ code: 1,
3782
+ output: `mugiwara: ## Sub-missions section present but no rows parsed — expected header:
3783
+ ${SUB_MISSIONS_HEADER}
3784
+ `
3785
+ };
3786
+ }
3787
+ if (sub === "status") {
3788
+ const blocked = new Map(blockedRows(rows).map((b) => [b.id, b.blockedBy]));
3789
+ const lines = ["id | name | assignee | branch | status | blocked-by"];
3790
+ for (const r of rows) {
3791
+ lines.push(`${r.id} | ${r.name} | ${r.assignee} | ${r.branch} | ${r.status} | ${blocked.get(r.id) ?? "-"}`);
3792
+ }
3793
+ return { code: 0, output: lines.join(`
3794
+ `) + `
3795
+ ` };
3796
+ }
3797
+ const conflicts = findConflicts(rows);
3798
+ if (!conflicts.length)
3799
+ return { code: 0, output: `no conflicts: no file is touched by two sub-missions
3800
+ ` };
3801
+ const lines = conflicts.map((c) => `conflict: ${c.file} touched by ${c.ids.join(", ")}`);
3802
+ return { code: 1, output: lines.join(`
3803
+ `) + `
3804
+ ` };
3207
3805
  }
3208
3806
 
3209
3807
  // src/cli.ts
@@ -3276,7 +3874,11 @@ async function run(argv) {
3276
3874
  case "sign":
3277
3875
  return signCmd(flags, _);
3278
3876
  case "migrate":
3279
- return migrateCmd(flags);
3877
+ return migrateCmd(flags, _);
3878
+ case "lesson":
3879
+ return lessonCmd(flags, _);
3880
+ case "initiative":
3881
+ return initiativeCmd(flags, _);
3280
3882
  default:
3281
3883
  throw new Error(`Unknown command: ${command}`);
3282
3884
  }
@@ -3319,7 +3921,7 @@ function cleanCmd(flags) {
3319
3921
  const projectDir = resolveProjectDir(str(flags.project));
3320
3922
  const dryRun = flag(flags.dryRun);
3321
3923
  const root = join21(projectDir, ".mugiwara", "missions");
3322
- if (!existsSync16(root)) {
3924
+ if (!existsSync17(root)) {
3323
3925
  console.log("nothing to clean (.mugiwara/missions/ does not exist).");
3324
3926
  return;
3325
3927
  }
@@ -3340,7 +3942,7 @@ function cleanCmd(flags) {
3340
3942
  return false;
3341
3943
  for (const f of stateFiles(m)) {
3342
3944
  try {
3343
- const ts = Date.parse(JSON.parse(readFileSync15(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
3945
+ const ts = Date.parse(JSON.parse(readFileSync16(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
3344
3946
  if (ts === 0 || ts >= beforeMs)
3345
3947
  return false;
3346
3948
  } catch {
@@ -3350,7 +3952,7 @@ function cleanCmd(flags) {
3350
3952
  return true;
3351
3953
  };
3352
3954
  if (!flag(flags.all)) {
3353
- candidates = candidates.filter((m) => existsSync16(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
3955
+ candidates = candidates.filter((m) => existsSync17(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
3354
3956
  } else if (!flag(flags.force)) {
3355
3957
  const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
3356
3958
  if (live.length) {
@@ -3384,7 +3986,7 @@ async function resolveOptions(flags) {
3384
3986
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
3385
3987
  }
3386
3988
  const projectDir = resolveProjectDir(str(flags.project));
3387
- if (scope === "project" && !existsSync16(projectDir))
3989
+ if (scope === "project" && !existsSync17(projectDir))
3388
3990
  throw new Error(`Project dir not found: ${projectDir}`);
3389
3991
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
3390
3992
  if (targetIds && targetIds.includes("all"))
@@ -3397,7 +3999,13 @@ async function resolveOptions(flags) {
3397
3999
  targetIds = idx.includes(0) ? [...TARGET_IDS] : idx.map((i) => TARGET_IDS[i - 1]);
3398
4000
  }
3399
4001
  }
4002
+ const MARKETPLACE = new Set(["cursor", "kimi", "pi"]);
3400
4003
  for (const id of targetIds) {
4004
+ if (MARKETPLACE.has(id)) {
4005
+ console.error(`mugiwara: ${id} installs through its marketplace manifest, not --target.`);
4006
+ console.error(" See docs/reference/harness-matrix.md — marketplace row.");
4007
+ process.exit(1);
4008
+ }
3401
4009
  if (!targets[id])
3402
4010
  throw new Error(`Unknown target: ${id} (valid: ${TARGET_IDS.join(", ")}, all)`);
3403
4011
  }
@@ -3465,7 +4073,7 @@ async function uninstall(flags) {
3465
4073
  if (!manifest) {
3466
4074
  if (scope === "project") {
3467
4075
  const globalFile = manifestPath({ scope: "global", projectDir, home });
3468
- if (existsSync16(globalFile)) {
4076
+ if (existsSync17(globalFile)) {
3469
4077
  console.log(`No project manifest found, but a global install exists. Try:
3470
4078
  mugiwara uninstall --global`);
3471
4079
  return;
@@ -3503,12 +4111,12 @@ async function uninstall(flags) {
3503
4111
  }
3504
4112
  rmSync3(file);
3505
4113
  const mugiDir = join21(scope === "global" ? home : projectDir, ".mugiwara");
3506
- if (existsSync16(mugiDir) && readdirSync10(mugiDir).length === 0) {
4114
+ if (existsSync17(mugiDir) && readdirSync10(mugiDir).length === 0) {
3507
4115
  rmSync3(mugiDir, { recursive: true, force: true });
3508
4116
  }
3509
4117
  if (manifest.targets.includes("opencode")) {
3510
4118
  const opencodeCache = join21(home, ".cache", "opencode", "packages", "@ionivetech");
3511
- if (existsSync16(opencodeCache)) {
4119
+ if (existsSync17(opencodeCache)) {
3512
4120
  rmSync3(opencodeCache, { recursive: true, force: true });
3513
4121
  console.log(" cleared opencode npm cache (stale plugin versions)");
3514
4122
  }
@@ -3545,7 +4153,7 @@ function list(flags) {
3545
4153
  continue;
3546
4154
  found = true;
3547
4155
  if (flag(flags.check)) {
3548
- const missing = m.files.filter((f) => !existsSync16(f));
4156
+ const missing = m.files.filter((f) => !existsSync17(f));
3549
4157
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
3550
4158
  } else {
3551
4159
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -3563,9 +4171,9 @@ function continueCmd(flags, positionals) {
3563
4171
  if (mission) {
3564
4172
  readState(projectDir);
3565
4173
  const badState = unreadableStateFiles();
3566
- const target10 = member ? `${mission}/${member}.json` : `${mission}/state.json`;
3567
- if (badState.includes(target10)) {
3568
- console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ""} has unreadable state: ${target10}`);
4174
+ const target = member ? `${mission}/${member}.json` : `${mission}/state.json`;
4175
+ if (badState.includes(target)) {
4176
+ console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ""} has unreadable state: ${target}`);
3569
4177
  process.exit(1);
3570
4178
  }
3571
4179
  }
@@ -3636,10 +4244,10 @@ function statusCmd(flags) {
3636
4244
  function costCmd(flags, positionals) {
3637
4245
  const projectDir = resolveProjectDir(str(flags.project));
3638
4246
  const mission = str(flags.mission) ?? positionals[1] ?? (() => {
3639
- const states2 = readState(projectDir);
3640
- if (states2.length === 1)
3641
- return states2[0].mission;
3642
- if (states2.length > 1) {
4247
+ const states = readState(projectDir);
4248
+ if (states.length === 1)
4249
+ return states[0].mission;
4250
+ if (states.length > 1) {
3643
4251
  console.error("multiple missions in flight — specify --mission <id>");
3644
4252
  process.exit(1);
3645
4253
  }
@@ -3650,7 +4258,7 @@ function costCmd(flags, positionals) {
3650
4258
  process.exit(1);
3651
4259
  }
3652
4260
  const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3653
- if (!existsSync16(missionDir)) {
4261
+ if (!existsSync17(missionDir)) {
3654
4262
  console.error(`No cost ledger found for mission "${mission}"`);
3655
4263
  process.exit(1);
3656
4264
  }
@@ -3709,7 +4317,7 @@ function blameCmd(flags, positionals) {
3709
4317
  function stalenessLine(projectDir, baseSha) {
3710
4318
  if (!baseSha || baseSha === "unknown")
3711
4319
  return null;
3712
- const git3 = (args) => {
4320
+ const git = (args) => {
3713
4321
  try {
3714
4322
  return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3715
4323
  } catch {
@@ -3718,14 +4326,14 @@ function stalenessLine(projectDir, baseSha) {
3718
4326
  };
3719
4327
  let main = "";
3720
4328
  for (const ref of ["main", "master"]) {
3721
- main = git3(["rev-parse", "--verify", ref]);
4329
+ main = git(["rev-parse", "--verify", ref]);
3722
4330
  if (main)
3723
4331
  break;
3724
4332
  }
3725
4333
  if (!main)
3726
4334
  return null;
3727
4335
  try {
3728
- const behind = Number(git3(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
4336
+ const behind = Number(git(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
3729
4337
  return behind > 0 ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing` : null;
3730
4338
  } catch {
3731
4339
  return null;
@@ -3784,15 +4392,178 @@ function handoffCmd(flags, positionals) {
3784
4392
  console.log(`
3785
4393
  written: ${out}`);
3786
4394
  }
3787
- function migrateCmd(flags) {
4395
+ function lessonCmd(flags, positionals) {
4396
+ const projectDir = resolveProjectDir(str(flags.project));
4397
+ const text = positionals.slice(1).join(" ").trim();
4398
+ if (!text) {
4399
+ console.error('usage: mugiwara lesson "<text>" [--project <dir>]');
4400
+ process.exit(1);
4401
+ }
4402
+ const file = join21(projectDir, ".mugiwara", "lessons.md");
4403
+ const date = new Date().toISOString().slice(0, 10);
4404
+ const sanitized = text.replace(/\|/g, "/").replace(/\r?\n/g, " ").trim();
4405
+ const line = `| ${date} | manual | general | ${sanitized} |`;
4406
+ const header = `| Date | Mission | Area | Lesson |
4407
+ |---|---|---|---|`;
4408
+ let existing = "";
4409
+ try {
4410
+ existing = readFileSync16(file, "utf8");
4411
+ } catch {}
4412
+ if (!existing) {
4413
+ mkdirSync10(join21(projectDir, ".mugiwara"), { recursive: true });
4414
+ writeFileSync10(file, header + `
4415
+ ` + line + `
4416
+ `);
4417
+ } else {
4418
+ const needsNewline = !existing.endsWith(`
4419
+ `);
4420
+ writeFileSync10(file, existing + (needsNewline ? `
4421
+ ` : "") + line + `
4422
+ `);
4423
+ }
4424
+ console.log(`lesson appended: ${line}`);
4425
+ }
4426
+ function initiativeCmd(_flags, positionals) {
4427
+ const r = runInitiative(positionals[1], positionals[2]);
4428
+ process.stdout.write(r.output);
4429
+ if (r.code !== 0)
4430
+ process.exit(r.code);
4431
+ }
4432
+ function migrateCmd(flags, positionals = []) {
3788
4433
  const projectDir = resolveProjectDir(str(flags.project));
3789
4434
  const dryRun = flag(flags.dryRun);
4435
+ const toTeam = str(flags.toTeam);
4436
+ const toSolo = str(flags.toSolo);
4437
+ if (toTeam || toSolo) {
4438
+ const member = toTeam ?? toSolo;
4439
+ if (!/^[A-Za-z0-9._-]+$/.test(member) || /^\.+$/.test(member) || member === "state" || member === "continue") {
4440
+ console.error(`invalid member name "${member}" (allowlist: [a-zA-Z0-9._-], not a dot-path, not state/continue)`);
4441
+ process.exit(1);
4442
+ }
4443
+ if (toTeam && toSolo) {
4444
+ console.error("use either --to-team or --to-solo, not both");
4445
+ process.exit(1);
4446
+ }
4447
+ const missionsRootInner = join21(projectDir, ".mugiwara", "missions");
4448
+ let mission = str(flags.mission) ?? (positionals[1] ? String(positionals[1]) : null);
4449
+ const inferMission = () => {
4450
+ if (!existsSync17(missionsRootInner))
4451
+ return null;
4452
+ const all = readdirSync10(missionsRootInner, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4453
+ if (mission && all.includes(mission))
4454
+ return mission;
4455
+ if (mission)
4456
+ return mission;
4457
+ if (toTeam) {
4458
+ const candidates = all.filter((m) => existsSync17(join21(missionsRootInner, m, "state.json")));
4459
+ if (candidates.length === 1)
4460
+ return candidates[0];
4461
+ if (candidates.length === 0) {
4462
+ console.error("no solo mission with state.json found for --to-team");
4463
+ process.exit(1);
4464
+ }
4465
+ console.error(`multiple solo missions: ${candidates.join(", ")} — specify --mission <id>`);
4466
+ process.exit(1);
4467
+ } else {
4468
+ const candidates = all.filter((m) => existsSync17(join21(missionsRootInner, m, `${member}.json`)));
4469
+ if (candidates.length === 1)
4470
+ return candidates[0];
4471
+ if (candidates.length === 0) {
4472
+ console.error(`no mission with ${member}.json found for --to-solo`);
4473
+ process.exit(1);
4474
+ }
4475
+ console.error(`multiple missions with ${member}.json: ${candidates.join(", ")} — specify --mission <id>`);
4476
+ process.exit(1);
4477
+ }
4478
+ return null;
4479
+ };
4480
+ const targetMission = inferMission();
4481
+ if (!targetMission) {
4482
+ console.error("could not infer mission — specify --mission <id>");
4483
+ process.exit(1);
4484
+ }
4485
+ const dir = join21(missionsRootInner, targetMission);
4486
+ if (toTeam) {
4487
+ const srcState = join21(dir, "state.json");
4488
+ const srcContinue = join21(dir, "continue.json");
4489
+ const destState = join21(dir, `${member}.json`);
4490
+ const destContinue = join21(dir, `continue-${member}.json`);
4491
+ if (!existsSync17(srcState)) {
4492
+ console.error(`mission "${targetMission}" has no state.json — already team or not found`);
4493
+ process.exit(1);
4494
+ }
4495
+ if (existsSync17(destState)) {
4496
+ console.error(`destination ${destState} already exists`);
4497
+ process.exit(1);
4498
+ }
4499
+ const toMove = [{ src: srcState, dest: destState }];
4500
+ if (existsSync17(srcContinue))
4501
+ toMove.push({ src: srcContinue, dest: destContinue });
4502
+ for (const m of toMove) {
4503
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
4504
+ if (!dryRun) {
4505
+ mkdirSync10(dirname6(m.dest), { recursive: true });
4506
+ try {
4507
+ renameSync2(m.src, m.dest);
4508
+ } catch {
4509
+ try {
4510
+ writeFileSync10(m.dest, readFileSync16(m.src));
4511
+ rmSync3(m.src, { force: true });
4512
+ } catch {}
4513
+ }
4514
+ }
4515
+ }
4516
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
4517
+ return;
4518
+ } else {
4519
+ const srcState = join21(dir, `${member}.json`);
4520
+ const srcContinue = join21(dir, `continue-${member}.json`);
4521
+ const destState = join21(dir, "state.json");
4522
+ const destContinue = join21(dir, "continue.json");
4523
+ if (!existsSync17(srcState)) {
4524
+ console.error(`mission "${targetMission}" has no ${member}.json`);
4525
+ process.exit(1);
4526
+ }
4527
+ const files = readdirSync10(dir).filter((f) => {
4528
+ const stem = f.replace(/\.json$/, "");
4529
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
4530
+ });
4531
+ const members = files.filter((f) => f !== "state.json");
4532
+ if (members.length > 1) {
4533
+ console.error(`mission "${targetMission}" has ${members.length} members (${members.join(", ")}) — refusing --to-solo (would orphan)`);
4534
+ process.exit(1);
4535
+ }
4536
+ if (existsSync17(destState)) {
4537
+ console.error(`destination ${destState} already exists`);
4538
+ process.exit(1);
4539
+ }
4540
+ const toMove = [{ src: srcState, dest: destState }];
4541
+ if (existsSync17(srcContinue))
4542
+ toMove.push({ src: srcContinue, dest: destContinue });
4543
+ for (const m of toMove) {
4544
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
4545
+ if (!dryRun) {
4546
+ mkdirSync10(dirname6(m.dest), { recursive: true });
4547
+ try {
4548
+ renameSync2(m.src, m.dest);
4549
+ } catch {
4550
+ try {
4551
+ writeFileSync10(m.dest, readFileSync16(m.src));
4552
+ rmSync3(m.src, { force: true });
4553
+ } catch {}
4554
+ }
4555
+ }
4556
+ }
4557
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${toMove.length} file(s)${dryRun ? " (dry run)" : ""}`);
4558
+ return;
4559
+ }
4560
+ }
3790
4561
  const legacyState = join21(projectDir, ".mugiwara", "state");
3791
4562
  const legacyContinue = join21(projectDir, ".mugiwara", "continue");
3792
4563
  const missionsRoot = join21(projectDir, ".mugiwara", "missions");
3793
4564
  const moves = [];
3794
4565
  const collect = (srcRoot, isContinue) => {
3795
- if (!existsSync16(srcRoot))
4566
+ if (!existsSync17(srcRoot))
3796
4567
  return;
3797
4568
  const walk = (dir) => {
3798
4569
  for (const e of readdirSync10(dir, { withFileTypes: true })) {
@@ -3825,7 +4596,7 @@ function migrateCmd(flags) {
3825
4596
  collect(legacyState, false);
3826
4597
  collect(legacyContinue, true);
3827
4598
  if (!moves.length) {
3828
- if (!existsSync16(legacyState) && !existsSync16(legacyContinue)) {
4599
+ if (!existsSync17(legacyState) && !existsSync17(legacyContinue)) {
3829
4600
  console.log("no legacy layout found (.mugiwara/state/ does not exist)");
3830
4601
  } else {
3831
4602
  console.log("no legacy state files to migrate");
@@ -3837,7 +4608,7 @@ function migrateCmd(flags) {
3837
4608
  if (!dryRun) {
3838
4609
  mkdirSync10(dirname6(m.dest), { recursive: true });
3839
4610
  try {
3840
- const raw = JSON.parse(readFileSync15(m.src, "utf8"));
4611
+ const raw = JSON.parse(readFileSync16(m.src, "utf8"));
3841
4612
  raw.schema_version = CURRENT_SCHEMA_VERSION;
3842
4613
  writeFileSync10(m.dest, JSON.stringify(raw, null, 2) + `
3843
4614
  `);
@@ -3851,7 +4622,7 @@ function migrateCmd(flags) {
3851
4622
  }
3852
4623
  if (!dryRun) {
3853
4624
  const prune = (root) => {
3854
- if (!existsSync16(root))
4625
+ if (!existsSync17(root))
3855
4626
  return;
3856
4627
  const walkPrune = (dir) => {
3857
4628
  for (const e of readdirSync10(dir, { withFileTypes: true })) {
@@ -3865,7 +4636,7 @@ function migrateCmd(flags) {
3865
4636
  };
3866
4637
  walkPrune(root);
3867
4638
  try {
3868
- if (existsSync16(root) && readdirSync10(root).length === 0)
4639
+ if (existsSync17(root) && readdirSync10(root).length === 0)
3869
4640
  rmSync3(root, { recursive: true, force: true });
3870
4641
  } catch {}
3871
4642
  };
@@ -3903,7 +4674,7 @@ function signCmd(flags, _) {
3903
4674
  process.exit(1);
3904
4675
  }
3905
4676
  const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3906
- if (!existsSync16(missionDir)) {
4677
+ if (!existsSync17(missionDir)) {
3907
4678
  console.error(`no mission dir: ${missionDir}`);
3908
4679
  process.exit(1);
3909
4680
  }
@@ -3939,7 +4710,12 @@ Usage:
3939
4710
  mugiwara sign --gen-key [--backend pure|minisign]
3940
4711
  create signing keys (pure ed25519 default)
3941
4712
  mugiwara migrate [--dry-run] [--project <dir>]
3942
- move legacy .mugiwara/state/ layout to .mugiwara/missions/
4713
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
4714
+ mugiwara migrate --to-team <member> [--mission <id>] [--dry-run]
4715
+ move state.json -> <member>.json (solo -> team)
4716
+ mugiwara migrate --to-solo <member> [--mission <id>] [--dry-run]
4717
+ move <member>.json -> state.json (team -> solo; refuses if >1 member)
4718
+ mugiwara lesson "<text>" append a dated row to .mugiwara/lessons.md
3943
4719
  mugiwara run <script> [args...]
3944
4720
  run a bundled harness script here (${RUNNABLE.join(", ")})
3945
4721
  mugiwara savepoint <mission> [member] [flow] [mode]