@ionivetech/mugiwara 0.6.6 → 0.8.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.
Files changed (138) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -7
  3. package/.codex-plugin/plugin.json +2 -7
  4. package/.cursor-plugin/plugin.json +2 -7
  5. package/.kimi-plugin/plugin.json +2 -7
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/.opencode/plugins/mugiwara.mjs +3 -18
  8. package/AGENTS.md +5 -4
  9. package/GEMINI.md +3 -3
  10. package/README.md +203 -331
  11. package/content/agents/brook-healing.md +8 -4
  12. package/content/agents/chopper-checkpoint.md +7 -3
  13. package/content/agents/eval-runner.md +4 -4
  14. package/content/agents/franky-gates.md +4 -4
  15. package/content/agents/jinbe-security.md +6 -7
  16. package/content/agents/luffy-orchestrator.md +11 -13
  17. package/content/agents/memory-keeper.md +4 -4
  18. package/content/agents/nami-planner.md +4 -4
  19. package/content/agents/resume-coordinator.md +7 -7
  20. package/content/agents/robin-reviewer.md +7 -3
  21. package/content/agents/sanji-quality.md +2 -2
  22. package/content/agents/skeptic-verifier.md +2 -2
  23. package/content/agents/usopp-brainstorm.md +3 -3
  24. package/content/agents/zoro-execution.md +6 -6
  25. package/content/skills/mugiwara-backend/SKILL.md +54 -43
  26. package/content/skills/mugiwara-backend/references/database.md +61 -0
  27. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  28. package/content/skills/mugiwara-checkpoint/SKILL.md +23 -11
  29. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
  30. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  31. package/content/skills/mugiwara-execution/SKILL.md +37 -37
  32. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  33. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  34. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  35. package/content/skills/mugiwara-frontend/SKILL.md +44 -43
  36. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  37. package/content/skills/mugiwara-gates/SKILL.md +23 -14
  38. package/content/skills/mugiwara-healing/SKILL.md +26 -25
  39. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  40. package/content/skills/mugiwara-orchestration/SKILL.md +11 -11
  41. package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
  42. package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
  43. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  44. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
  45. package/content/skills/mugiwara-planning/SKILL.md +26 -29
  46. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  47. package/content/skills/mugiwara-planning/references/plan-template.md +24 -2
  48. package/content/skills/mugiwara-quality/SKILL.md +21 -12
  49. package/content/skills/mugiwara-resume/SKILL.md +8 -3
  50. package/content/skills/mugiwara-review/SKILL.md +19 -13
  51. package/content/skills/mugiwara-security/SKILL.md +47 -36
  52. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  53. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  54. package/content/skills/mugiwara-workflow/SKILL.md +11 -17
  55. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
  56. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
  57. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
  58. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  59. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
  60. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
  61. package/content/skills/mugiwara-workflow/references/workspace-layout.md +42 -38
  62. package/dist/mugiwara.js +1642 -528
  63. package/gemini-extension.json +1 -1
  64. package/hooks/auto-savepoint.js +18 -10
  65. package/hooks/auto-savepoint.ts +23 -23
  66. package/hooks/engagement-marker.js +1 -1
  67. package/hooks/engagement-marker.ts +1 -1
  68. package/hooks/pipeline-guard.js +17 -13
  69. package/hooks/pipeline-guard.ts +24 -20
  70. package/hooks/session-start.js +13 -10
  71. package/hooks/session-start.ts +24 -19
  72. package/package.json +2 -2
  73. package/plugin.json +1 -1
  74. package/references/complexity.md +27 -4
  75. package/references/definition-of-done.md +1 -1
  76. package/references/multi-actor.md +20 -2
  77. package/references/posture-routing.md +31 -0
  78. package/references/prose-style.md +54 -0
  79. package/scripts/benchmark-governor.ts +516 -0
  80. package/scripts/benchmark-thresholds.json +47 -0
  81. package/scripts/check-doc-links.ts +46 -0
  82. package/scripts/conformance.ts +6 -16
  83. package/scripts/coverage-gate.ts +20 -6
  84. package/scripts/gate-selftest.ts +138 -7
  85. package/scripts/lane.sh +54 -8
  86. package/scripts/lib/lane-base.sh +4 -4
  87. package/scripts/lib/patterns.sh +5 -0
  88. package/scripts/policy-force.ts +22 -0
  89. package/scripts/retrieval-eval.ts +10 -4
  90. package/scripts/savepoint.sh +155 -58
  91. package/scripts/validate-content.ts +43 -11
  92. package/scripts/verify-install.ts +9 -7
  93. package/src/adaptive-budget.ts +178 -0
  94. package/src/args.ts +4 -3
  95. package/src/budget.ts +47 -0
  96. package/src/check-artifacts.ts +45 -0
  97. package/src/cli.ts +257 -108
  98. package/src/cognition.ts +234 -0
  99. package/src/config.ts +107 -0
  100. package/src/context.ts +72 -0
  101. package/src/continue.ts +29 -10
  102. package/src/cost.ts +186 -0
  103. package/src/evidence.ts +160 -0
  104. package/src/installer.ts +25 -34
  105. package/src/integrity.ts +158 -0
  106. package/src/investigation.ts +72 -0
  107. package/src/mission.ts +341 -94
  108. package/src/policy.ts +156 -0
  109. package/src/posture.ts +86 -0
  110. package/src/provenance.ts +116 -0
  111. package/src/reporting.ts +225 -0
  112. package/src/rollback.ts +95 -0
  113. package/src/routing.ts +69 -0
  114. package/src/run.ts +2 -2
  115. package/src/scope.ts +321 -0
  116. package/src/sign.ts +235 -0
  117. package/src/slop.ts +306 -0
  118. package/src/targets/claude.ts +2 -2
  119. package/src/work.ts +273 -0
  120. package/.opencode/commands/mugiwara-execute.md +0 -13
  121. package/.opencode/commands/mugiwara-heal.md +0 -13
  122. package/.opencode/commands/mugiwara-onboard.md +0 -14
  123. package/.opencode/commands/mugiwara-plan.md +0 -17
  124. package/.opencode/commands/mugiwara-ship.md +0 -13
  125. package/.opencode/commands/using-mugiwara.md +0 -20
  126. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  127. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  128. package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
  129. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  130. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  131. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  132. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  133. package/content/skills/using-mugiwara/SKILL.md +0 -51
  134. package/references/token-budget.md +0 -56
  135. package/scripts/evidence.sh +0 -81
  136. package/scripts/initiative.ts +0 -296
  137. package/scripts/mission-report.sh +0 -293
  138. package/src/onboard.ts +0 -207
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 existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync7, rmSync as rmSync3 } from "node:fs";
5
- import { spawnSync as spawnSync2 } from "node:child_process";
6
- import { homedir as homedir2 } from "node:os";
7
- import { join as join11, resolve as resolve2 } from "node:path";
4
+ import { existsSync as existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
5
+ import { execFileSync as execFileSync6 } from "node:child_process";
6
+ import { homedir as homedir4 } from "node:os";
7
+ import { join as join20, 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" };
11
+ var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before", "--backend": "backend", "--mission": "mission" };
12
12
  var BOOL_FLAGS = {
13
13
  "--global": "global",
14
14
  "--yes": "yes",
@@ -18,10 +18,14 @@ var BOOL_FLAGS = {
18
18
  "--keep-logs": "keepLogs",
19
19
  "--check": "check",
20
20
  "--all": "all",
21
+ "--verify": "verify",
22
+ "--gen-key": "genKey",
21
23
  "--help": "help",
22
24
  "-h": "help",
23
25
  "--version": "version",
24
- "-v": "version"
26
+ "-v": "version",
27
+ "--json": "json",
28
+ "--ledger": "ledger"
25
29
  };
26
30
  function parseArgs(argv) {
27
31
  const out = { _: [], flags: {}, command: "install" };
@@ -130,7 +134,7 @@ function copyScripts(dstDir) {
130
134
  if (!existsSync(src))
131
135
  return;
132
136
  for (const f of readdirSync(src)) {
133
- if (!f.endsWith(".sh") && f !== "initiative.ts" || f === "install.sh")
137
+ if (!f.endsWith(".sh") || f === "install.sh")
134
138
  continue;
135
139
  const to = join(dst, f);
136
140
  mkdirSync(dst, { recursive: true });
@@ -619,50 +623,118 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
619
623
  var TARGET_IDS = Object.keys(targets);
620
624
 
621
625
  // src/installer.ts
622
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync3, copyFileSync as copyFileSync3, rmSync, lstatSync } from "node:fs";
623
- import { dirname as dirname3, join as join5 } from "node:path";
624
- import { homedir } from "node:os";
626
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync4, copyFileSync as copyFileSync3, rmSync, lstatSync as lstatSync2 } from "node:fs";
627
+ import { dirname as dirname3, join as join6 } from "node:path";
628
+ import { homedir as homedir2 } from "node:os";
625
629
  import { fileURLToPath as fileURLToPath3 } from "node:url";
626
- var CONTENT_DIR = join5(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
627
- var pkg = JSON.parse(readFileSync3(join5(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
630
+
631
+ // src/config.ts
632
+ import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
633
+ import { homedir } from "node:os";
634
+ import { join as join5 } from "node:path";
635
+ var DEFAULT_CONFIG = [
636
+ "mode=guided",
637
+ "branch=feature/{type}-{issue}-{slug}",
638
+ "commit=conventional",
639
+ "auto_commit=on",
640
+ "coverage_new=85",
641
+ "coverage_modified=90",
642
+ "review_depth=full",
643
+ "quality_depth=full",
644
+ "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)",
650
+ "# investigation_max_unrelated_files=5",
651
+ "# 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"
654
+ ].join(`
655
+ `) + `
656
+ `;
657
+ function configPaths(projectDir) {
658
+ return [join5(projectDir, ".mugiwara", "config"), join5(homedir(), ".mugiwara", "config")];
659
+ }
660
+ function readConfig(projectDir) {
661
+ const out = {};
662
+ for (const file of configPaths(projectDir)) {
663
+ if (!existsSync4(file))
664
+ continue;
665
+ for (const line of readFileSync3(file, "utf8").split(/\r?\n/)) {
666
+ const t = line.trim();
667
+ if (!t || t.startsWith("#"))
668
+ continue;
669
+ const eq = t.indexOf("=");
670
+ if (eq === -1)
671
+ continue;
672
+ const key = t.slice(0, eq).trim();
673
+ if (!key)
674
+ continue;
675
+ if (key in out)
676
+ continue;
677
+ out[key] = t.slice(eq + 1).trim();
678
+ }
679
+ }
680
+ return out;
681
+ }
682
+ function ensureConfig(projectDir) {
683
+ const file = join5(projectDir, ".mugiwara", "config");
684
+ let exists = false;
685
+ try {
686
+ exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
687
+ } catch {
688
+ exists = false;
689
+ }
690
+ if (exists)
691
+ return false;
692
+ mkdirSync3(join5(projectDir, ".mugiwara"), { recursive: true });
693
+ writeFileSync3(file, DEFAULT_CONFIG);
694
+ return true;
695
+ }
696
+
697
+ // src/installer.ts
698
+ var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
699
+ var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
628
700
  var VERSION = pkg.version;
629
701
  function collectContent() {
630
- const skillNames = readdirSync3(join5(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
702
+ const skillNames = readdirSync3(join6(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
631
703
  const skills = skillNames.map((name) => {
632
- const { data, body } = parseFrontmatter(readFileSync3(join5(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
633
- return { name, data, body, refs: collectRefs(join5(CONTENT_DIR, "skills", name)) };
704
+ const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
705
+ return { name, data, body, refs: collectRefs(join6(CONTENT_DIR, "skills", name)) };
634
706
  });
635
- const agents = readdirSync3(join5(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
636
- const { data, body } = parseFrontmatter(readFileSync3(join5(CONTENT_DIR, "agents", f), "utf8"));
707
+ const agents = readdirSync3(join6(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
708
+ const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "agents", f), "utf8"));
637
709
  return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
638
710
  });
639
- const sharedRefsDir = join5(dirname3(CONTENT_DIR), "references");
711
+ const sharedRefsDir = join6(dirname3(CONTENT_DIR), "references");
640
712
  const sharedRefs = [];
641
- if (existsSync4(sharedRefsDir)) {
713
+ if (existsSync5(sharedRefsDir)) {
642
714
  for (const f of readdirSync3(sharedRefsDir)) {
643
715
  if (!f.endsWith(".md"))
644
716
  continue;
645
- sharedRefs.push({ relPath: f, text: readFileSync3(join5(sharedRefsDir, f), "utf8") });
717
+ sharedRefs.push({ relPath: f, text: readFileSync4(join6(sharedRefsDir, f), "utf8") });
646
718
  }
647
719
  }
648
720
  return { skills, agents, sharedRefs };
649
721
  }
650
722
  function collectRefs(skillDir) {
651
- const refsDir = join5(skillDir, "references");
652
- if (!existsSync4(refsDir))
723
+ const refsDir = join6(skillDir, "references");
724
+ if (!existsSync5(refsDir))
653
725
  return [];
654
- return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync3(join5(refsDir, rel), "utf8") }));
726
+ return readdirSync3(refsDir, { recursive: true }).map((f) => String(f)).filter((f) => f.endsWith(".md")).map((rel) => ({ relPath: rel, text: readFileSync4(join6(refsDir, rel), "utf8") }));
655
727
  }
656
728
  function installTo(target10, opts) {
657
729
  const { scope, projectDir, dryRun = false, force = false } = opts;
658
- const home = opts.home ?? homedir();
730
+ const home = opts.home ?? homedir2();
659
731
  const { skills, agents, sharedRefs } = collectContent();
660
732
  const dirs = target10.paths({ scope, projectDir, home });
661
- const backupRoot = join5(scope === "global" ? home : projectDir, ".mugiwara");
733
+ const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
662
734
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
663
735
  const writeOne = (absPath, text) => {
664
- if (existsSync4(absPath)) {
665
- if (readFileSync3(absPath, "utf8") === text) {
736
+ if (existsSync5(absPath)) {
737
+ if (readFileSync4(absPath, "utf8") === text) {
666
738
  result.skipped.push(absPath);
667
739
  return;
668
740
  }
@@ -673,17 +745,17 @@ function installTo(target10, opts) {
673
745
  }
674
746
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
675
747
  const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
676
- const backupDir = join5(backupRoot, "backup", `${ts}-${target10.id}`);
677
- const backupFile = join5(backupDir, fileName);
748
+ const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
749
+ const backupFile = join6(backupDir, fileName);
678
750
  if (!dryRun) {
679
- mkdirSync3(backupDir, { recursive: true });
751
+ mkdirSync4(backupDir, { recursive: true });
680
752
  copyFileSync3(absPath, backupFile);
681
753
  }
682
754
  result.backedUp.push(absPath);
683
755
  }
684
756
  if (!dryRun) {
685
- mkdirSync3(dirname3(absPath), { recursive: true });
686
- writeFileSync3(absPath, text);
757
+ mkdirSync4(dirname3(absPath), { recursive: true });
758
+ writeFileSync4(absPath, text);
687
759
  }
688
760
  result.written.push(absPath);
689
761
  };
@@ -694,7 +766,7 @@ function installTo(target10, opts) {
694
766
  if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
695
767
  text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
696
768
  }
697
- writeOne(join5(dirs.skillsDir, out.relPath), text);
769
+ writeOne(join6(dirs.skillsDir, out.relPath), text);
698
770
  }
699
771
  if (target10.transformSkillFull) {
700
772
  const full = target10.transformSkillFull(s.data, s.body);
@@ -703,29 +775,29 @@ function installTo(target10, opts) {
703
775
  if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
704
776
  text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
705
777
  }
706
- writeOne(join5(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
778
+ writeOne(join6(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
707
779
  }
708
780
  }
709
781
  if (s.refs.length && target10.refsDir) {
710
782
  const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
711
783
  for (const r of s.refs)
712
- writeOne(join5(refsRoot, r.relPath), r.text);
784
+ writeOne(join6(refsRoot, r.relPath), r.text);
713
785
  }
714
786
  }
715
787
  for (const a of agents) {
716
788
  const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
717
789
  if (out)
718
- writeOne(join5(dirs.agentsDir, out.relPath), out.text);
790
+ writeOne(join6(dirs.agentsDir, out.relPath), out.text);
719
791
  if (target10.transformAgentFull) {
720
792
  const full = target10.transformAgentFull(a.data, a.body);
721
793
  if (full && target10.refsDir)
722
- writeOne(join5(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
794
+ writeOne(join6(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
723
795
  }
724
796
  }
725
797
  if (sharedRefs.length) {
726
- const sharedRoot = join5(dirs.skillsDir, "_shared", "references");
798
+ const sharedRoot = join6(dirs.skillsDir, "_shared", "references");
727
799
  for (const r of sharedRefs)
728
- writeOne(join5(sharedRoot, r.relPath), r.text);
800
+ writeOne(join6(sharedRoot, r.relPath), r.text);
729
801
  }
730
802
  if (target10.postInstall) {
731
803
  const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
@@ -733,35 +805,20 @@ function installTo(target10, opts) {
733
805
  result.notes.push(...post.notes);
734
806
  }
735
807
  if (scope === "project") {
736
- const configPath = join5(projectDir, ".mugiwara", "config");
808
+ const configPath = join6(projectDir, ".mugiwara", "config");
737
809
  let configExists = false;
738
810
  try {
739
- configExists = lstatSync(configPath).isFile() || lstatSync(configPath).isSymbolicLink();
811
+ configExists = lstatSync2(configPath).isFile() || lstatSync2(configPath).isSymbolicLink();
740
812
  } catch {
741
813
  configExists = false;
742
814
  }
743
815
  if (!configExists) {
744
- const body = [
745
- "mode=guided",
746
- "branch=feature/{type}-{issue}-{slug}",
747
- "commit=conventional",
748
- "auto_commit=on",
749
- "coverage_new=90",
750
- "coverage_modified=80",
751
- "review_depth=full",
752
- "quality_depth=full",
753
- "delegate_threshold=60",
754
- "heal_max_cycles=3",
755
- "verbosity=normal"
756
- ].join(`
757
- `) + `
758
- `;
759
816
  if (!dryRun) {
760
- mkdirSync3(dirname3(configPath), { recursive: true });
761
- writeFileSync3(configPath, body);
817
+ mkdirSync4(dirname3(configPath), { recursive: true });
818
+ writeFileSync4(configPath, DEFAULT_CONFIG);
762
819
  }
763
820
  result.written.push(configPath);
764
- result.notes.push(`default config written: ${configPath} (run \`mugiwara onboard\` to customise)`);
821
+ result.notes.push(`default config written: ${configPath} (edit it to customise)`);
765
822
  }
766
823
  }
767
824
  return result;
@@ -769,7 +826,7 @@ function installTo(target10, opts) {
769
826
  function removeInstalled(manifest, { dryRun = false } = {}) {
770
827
  const removed = [];
771
828
  for (const f of manifest.files) {
772
- if (existsSync4(f)) {
829
+ if (existsSync5(f)) {
773
830
  if (!dryRun)
774
831
  rmSync(f);
775
832
  removed.push(f);
@@ -778,7 +835,7 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
778
835
  if (!dryRun) {
779
836
  for (const f of manifest.files) {
780
837
  let d = dirname3(f);
781
- while (existsSync4(d) && readdirSync3(d).length === 0) {
838
+ while (existsSync5(d) && readdirSync3(d).length === 0) {
782
839
  if (d.endsWith("/.mugiwara") || d === dirname3(d))
783
840
  break;
784
841
  rmSync(d, { recursive: true, force: true });
@@ -789,10 +846,10 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
789
846
  return removed;
790
847
  }
791
848
  function assertNotSymlink(file) {
792
- if (!existsSync4(file))
849
+ if (!existsSync5(file))
793
850
  return;
794
851
  try {
795
- if (lstatSync(file).isSymbolicLink())
852
+ if (lstatSync2(file).isSymbolicLink())
796
853
  throw new Error(`refusing to follow symlink: ${file}`);
797
854
  } catch (e) {
798
855
  if (e.code === "ENOENT")
@@ -802,45 +859,45 @@ function assertNotSymlink(file) {
802
859
  }
803
860
  var GITIGNORE_BLOCK_START = "# >>> mugiwara >>>";
804
861
  var GITIGNORE_BLOCK_END = "# <<< mugiwara <<<";
805
- var GITIGNORE_MARKER = "# mugiwara";
806
- var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
807
- # Ignore session state and regenerated files.
808
- .mugiwara/state/
809
- .mugiwara/continue/
862
+ var GITIGNORE_LEGACY = [".mugiwara/state/", "# mugiwara"];
863
+ var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit plan.md, flows/, review.md,
864
+ # decisions.md, blockers.md, report.md under .mugiwara/missions/. Ignore session state.
865
+ .mugiwara/missions/**/*.json
866
+ .mugiwara/index.md
810
867
  .mugiwara/config
811
868
  .mugiwara/refs/
812
869
  # <<< mugiwara <<<
813
870
  `;
814
871
  function ensureProjectGitignore(projectDir, opts = {}) {
815
872
  const { dryRun = false } = opts;
816
- const path = join5(projectDir, ".gitignore");
873
+ const path = join6(projectDir, ".gitignore");
817
874
  assertNotSymlink(path);
818
- let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
875
+ let existing = existsSync5(path) ? readFileSync4(path, "utf8") : "";
819
876
  if (existing) {
820
- if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/state/")) {
877
+ if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
821
878
  return { appended: false, notes: [] };
822
879
  }
823
- const hadOld = existing.includes(GITIGNORE_MARKER) || existing.includes(GITIGNORE_BLOCK_START);
880
+ const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
824
881
  if (hadOld) {
825
882
  const clean = removeProjectGitignore(projectDir, { dryRun });
826
- existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
883
+ existing = clean.removed ? existsSync5(path) ? readFileSync4(path, "utf8") : "" : existing;
827
884
  }
828
885
  }
829
886
  const separator = existing.length && !existing.endsWith(`
830
887
  `) ? `
831
888
  ` : "";
832
889
  if (!dryRun) {
833
- mkdirSync3(dirname3(path), { recursive: true });
834
- writeFileSync3(path, existing + separator + GITIGNORE_BLOCK);
890
+ mkdirSync4(dirname3(path), { recursive: true });
891
+ writeFileSync4(path, existing + separator + GITIGNORE_BLOCK);
835
892
  }
836
893
  return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
837
894
  }
838
895
  function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
839
- const path = join5(projectDir, ".gitignore");
896
+ const path = join6(projectDir, ".gitignore");
840
897
  assertNotSymlink(path);
841
- if (!existsSync4(path))
898
+ if (!existsSync5(path))
842
899
  return { removed: false, notes: [] };
843
- const current = readFileSync3(path, "utf8");
900
+ const current = readFileSync4(path, "utf8");
844
901
  const delimited = current.includes(GITIGNORE_BLOCK_START);
845
902
  let cleaned;
846
903
  if (delimited) {
@@ -849,18 +906,20 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
849
906
  if (end < start)
850
907
  return { removed: false, notes: ["delimiter mismatch — .gitignore left untouched"] };
851
908
  cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
852
- } else if (current.includes(GITIGNORE_MARKER)) {
909
+ } else if (current.includes("# mugiwara")) {
853
910
  const LEGACY_LINES = new Set([
854
911
  ".mugiwara/state.json",
855
912
  ".mugiwara/state-*.json",
856
913
  ".mugiwara/config",
914
+ ".mugiwara/state/",
915
+ ".mugiwara/continue/",
857
916
  ".mugiwara/continue.md",
858
917
  ".mugiwara/refs/"
859
918
  ]);
860
919
  const lines = current.split(`
861
920
  `).filter((l) => {
862
921
  const t = l.trim();
863
- if (t.startsWith(GITIGNORE_MARKER))
922
+ if (t.startsWith("# mugiwara"))
864
923
  return false;
865
924
  return !LEGACY_LINES.has(t);
866
925
  });
@@ -878,39 +937,746 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
878
937
  if (cleaned === current)
879
938
  return { removed: false, notes: [] };
880
939
  if (!dryRun)
881
- writeFileSync3(path, cleaned);
940
+ writeFileSync4(path, cleaned);
882
941
  return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
883
942
  }
884
943
 
885
944
  // src/manifest.ts
886
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
887
- import { dirname as dirname4, join as join6 } from "node:path";
945
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
946
+ import { dirname as dirname4, join as join7 } from "node:path";
888
947
  function manifestPath({ scope, projectDir, home }) {
889
- return scope === "global" ? join6(home, ".mugiwara", "manifest.json") : join6(projectDir, ".mugiwara", "manifest.json");
948
+ return scope === "global" ? join7(home, ".mugiwara", "manifest.json") : join7(projectDir, ".mugiwara", "manifest.json");
890
949
  }
891
950
  function readManifest(file) {
892
- return existsSync5(file) ? JSON.parse(readFileSync4(file, "utf8")) : null;
951
+ return existsSync6(file) ? JSON.parse(readFileSync5(file, "utf8")) : null;
893
952
  }
894
953
  function writeManifest(file, data) {
895
- mkdirSync4(dirname4(file), { recursive: true });
896
- writeFileSync4(file, JSON.stringify(data, null, 2) + `
954
+ mkdirSync5(dirname4(file), { recursive: true });
955
+ writeFileSync5(file, JSON.stringify(data, null, 2) + `
897
956
  `);
898
957
  }
899
958
 
900
959
  // src/mission.ts
901
- import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync5, readdirSync as readdirSync4, mkdirSync as mkdirSync5, appendFileSync } from "node:fs";
902
- import { join as join7 } from "node:path";
960
+ import { existsSync as existsSync11, rmSync as rmSync2, readFileSync as readFileSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, renameSync, openSync, writeSync, closeSync } from "node:fs";
961
+ import { execFileSync as execFileSync3 } from "node:child_process";
962
+ import { join as join16 } from "node:path";
963
+
964
+ // src/integrity.ts
965
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
966
+ import { isAbsolute, join as join8, relative } from "node:path";
967
+ var SECRET_PATTERNS = [
968
+ [/AKIA[0-9A-Z]{16}/, "AWS access key id"],
969
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
970
+ [/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
971
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
972
+ [/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
973
+ [/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
974
+ [/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
975
+ ];
976
+ var ALLOW_SECRET = "mugiwara:allow-secret";
977
+ function findSecrets(body) {
978
+ const out = [];
979
+ for (const line of body.split(/\r?\n/)) {
980
+ if (line.includes(ALLOW_SECRET))
981
+ continue;
982
+ for (const [re, label] of SECRET_PATTERNS) {
983
+ const hit = line.match(re);
984
+ if (hit)
985
+ out.push({ label, hit: hit[0] });
986
+ }
987
+ }
988
+ return out;
989
+ }
990
+ var TRAIL_EXTS = new Set([".md", ".json", ".sh", ".jsonl"]);
991
+ function trailFiles(dir) {
992
+ const out = [];
993
+ const walk = (d) => {
994
+ for (const e of readdirSync4(d, { withFileTypes: true })) {
995
+ const p = join8(d, e.name);
996
+ if (e.isDirectory())
997
+ walk(p);
998
+ else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
999
+ out.push(p);
1000
+ }
1001
+ };
1002
+ walk(dir);
1003
+ return out;
1004
+ }
1005
+ function linkedPaths(md) {
1006
+ const out = [];
1007
+ for (const m of md.matchAll(/\]\(([^)\s]+)\)/g)) {
1008
+ const t = m[1];
1009
+ if (/^(https?:|mailto:|#|\/\/)/.test(t))
1010
+ continue;
1011
+ out.push(t.split("#")[0]);
1012
+ }
1013
+ return out.filter(Boolean);
1014
+ }
1015
+ function hasCommandOutputShape(body) {
1016
+ return /`[^`]+`/.test(body) || /\b(exit\s+[01]|✓|✗|\bPASS\b|\bFAIL\b|passed|failed|\d+\s+(passed|failed))\b/i.test(body);
1017
+ }
1018
+ function collectPassCitedPaths(missionDir) {
1019
+ const out = [];
1020
+ for (const f of trailFiles(missionDir)) {
1021
+ let body;
1022
+ try {
1023
+ body = readFileSync6(f, "utf8");
1024
+ } catch {
1025
+ continue;
1026
+ }
1027
+ for (const line of body.split(/\r?\n/)) {
1028
+ if (!/\bPASS\b/.test(line))
1029
+ continue;
1030
+ for (const p of linkedPaths(line))
1031
+ out.push(p);
1032
+ for (const m of line.matchAll(/(?:^|[\s"'(])([a-zA-Z0-9._\/-]+\.(?:md|txt|log|json))\b/g)) {
1033
+ const cand = m[1];
1034
+ if (cand.includes("/"))
1035
+ out.push(cand);
1036
+ }
1037
+ }
1038
+ }
1039
+ return [...new Set(out)];
1040
+ }
1041
+ function checkTrail(missionDir, projectRoot) {
1042
+ const issues = [];
1043
+ const files = trailFiles(missionDir);
1044
+ for (const f of files) {
1045
+ let body;
1046
+ try {
1047
+ body = readFileSync6(f, "utf8");
1048
+ } catch {
1049
+ continue;
1050
+ }
1051
+ for (const target10 of linkedPaths(body)) {
1052
+ if (isAbsolute(target10))
1053
+ continue;
1054
+ const fromMission = join8(missionDir, target10);
1055
+ const fromRoot = join8(projectRoot, target10);
1056
+ if (!existsSync7(fromMission) && !existsSync7(fromRoot)) {
1057
+ issues.push({
1058
+ kind: "dangling-path",
1059
+ detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
1060
+ });
1061
+ }
1062
+ }
1063
+ for (const { label, hit } of findSecrets(body)) {
1064
+ issues.push({
1065
+ kind: "secret",
1066
+ detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
1067
+ });
1068
+ }
1069
+ }
1070
+ const evidencePaths = [];
1071
+ const evidenceFile = join8(missionDir, "state.json");
1072
+ if (existsSync7(evidenceFile)) {
1073
+ try {
1074
+ const s = JSON.parse(readFileSync6(evidenceFile, "utf8"));
1075
+ if (Array.isArray(s.evidence)) {
1076
+ for (const e of s.evidence) {
1077
+ if (typeof e !== "string" || !e.trim())
1078
+ continue;
1079
+ evidencePaths.push(e);
1080
+ const cand = join8(projectRoot, e);
1081
+ if (!isAbsolute(e) && !existsSync7(cand) && !existsSync7(join8(missionDir, e))) {
1082
+ issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1083
+ }
1084
+ }
1085
+ }
1086
+ } catch {}
1087
+ }
1088
+ const passCited = collectPassCitedPaths(missionDir);
1089
+ for (const e of passCited) {
1090
+ if (!e.trim() || isAbsolute(e))
1091
+ continue;
1092
+ const candMission = join8(missionDir, e);
1093
+ const candRoot = join8(projectRoot, e);
1094
+ const resolved = existsSync7(candMission) ? candMission : existsSync7(candRoot) ? candRoot : null;
1095
+ if (!resolved)
1096
+ continue;
1097
+ let body;
1098
+ try {
1099
+ body = readFileSync6(resolved, "utf8");
1100
+ } catch {
1101
+ continue;
1102
+ }
1103
+ if (!hasCommandOutputShape(body)) {
1104
+ issues.push({ kind: "evidence-thin", detail: `evidence "${e}" exists but lacks command output (no backticked command or exit-status token)` });
1105
+ }
1106
+ }
1107
+ return issues;
1108
+ }
1109
+ function formatIssues(issues) {
1110
+ return issues.map((i) => ` ✗ [${i.kind}] ${i.detail}`).join(`
1111
+ `);
1112
+ }
1113
+
1114
+ // src/check-artifacts.ts
1115
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
1116
+ import { join as join9 } from "node:path";
1117
+ var LANE_MIN = new Set(["standard", "full", "spike"]);
1118
+ function checkMissionArtifacts(missionDir) {
1119
+ const statePath = join9(missionDir, "state.json");
1120
+ if (!existsSync8(statePath)) {
1121
+ return { ok: true, missing: [], lane: null };
1122
+ }
1123
+ let lane = "unknown";
1124
+ try {
1125
+ const s = JSON.parse(readFileSyncSafe(statePath));
1126
+ if (typeof s.lane === "string")
1127
+ lane = s.lane;
1128
+ } catch {}
1129
+ if (!LANE_MIN.has(lane)) {
1130
+ return { ok: true, missing: [], lane };
1131
+ }
1132
+ const missing = [];
1133
+ if (!existsSync8(join9(missionDir, "plan.md")))
1134
+ missing.push("plan.md");
1135
+ const flowsDir = join9(missionDir, "flows");
1136
+ const hasFlows = existsSync8(flowsDir) && readdirSync5(flowsDir).length > 0;
1137
+ if (!hasFlows)
1138
+ missing.push("flows/ (no execution evidence)");
1139
+ return { ok: missing.length === 0, missing, lane };
1140
+ }
1141
+ function readFileSyncSafe(p) {
1142
+ try {
1143
+ return readFileSync7(p, "utf8");
1144
+ } catch {
1145
+ return "";
1146
+ }
1147
+ }
1148
+
1149
+ // src/rollback.ts
1150
+ import { execFileSync } from "node:child_process";
1151
+ import { writeFileSync as writeFileSync6 } from "node:fs";
1152
+ import { join as join10 } from "node:path";
1153
+ function git(cwd, args) {
1154
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1155
+ }
1156
+ function buildRollback(input, commitsNewestFirst, filesTouched) {
1157
+ const lines = [
1158
+ "#!/usr/bin/env bash",
1159
+ `# Rollback map for mission "${input.mission}" — generated at closure.`,
1160
+ "# Human-executed. Review before running; mugiwara never runs this.",
1161
+ `# Branch: ${input.branch}`,
1162
+ `# Base: ${input.baseSha}`,
1163
+ "",
1164
+ "set -euo pipefail",
1165
+ ""
1166
+ ];
1167
+ if (!commitsNewestFirst.length && !filesTouched.length) {
1168
+ lines.push("# No commits between base and HEAD on this branch — nothing to revert.", "");
1169
+ return lines.join(`
1170
+ `);
1171
+ }
1172
+ if (!commitsNewestFirst.length) {
1173
+ lines.push("# UNRESOLVED: squash-merged state detected.", `# git rev-list ${input.baseSha}..${input.branch} is empty, but the diff`, `# ${input.baseSha}..${input.branch} touches ${filesTouched.length} file(s):`, "# the mission's changes were collapsed into commit(s) already on the base", "# ref. No per-commit revert list can be derived automatically.", "# Locate the squash commit, review it, then revert it:", `# git log --oneline ${input.baseSha}..HEAD --grep="${input.mission}"`, "# git revert <squash-commit>", "# Files carrying the squashed changes (verify each after reverting):", ...filesTouched.map((f) => `# ${f}`), "", 'echo "ROLLBACK INCOMPLETE: squash-merged state — locate and revert the squash commit manually" >&2', "exit 1", "");
1174
+ return lines.join(`
1175
+ `);
1176
+ }
1177
+ lines.push("# Revert newest-first so earlier reverts never conflict with later ones.", "git revert --no-edit \\", commitsNewestFirst.map((c) => ` ${c}`).join(" \\\n"), "");
1178
+ if (filesTouched.length) {
1179
+ lines.push("# Files this mission touched (verify the working tree is clean afterwards):", ...filesTouched.map((f) => `# ${f}`), "");
1180
+ }
1181
+ return lines.join(`
1182
+ `);
1183
+ }
1184
+ function generateRollback(projectDir, missionDir, input) {
1185
+ try {
1186
+ const range = `${input.baseSha}..${input.branch}`;
1187
+ const revList = git(projectDir, ["rev-list", "--reverse", range]).split(/\r?\n/).filter(Boolean);
1188
+ const commitsNewestFirst = [...revList].reverse();
1189
+ const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
1190
+ const body = buildRollback(input, commitsNewestFirst, filesTouched);
1191
+ const file = join10(missionDir, "rollback.sh");
1192
+ writeFileSync6(file, body, { mode: 493 });
1193
+ return { file: "rollback.sh", commits: commitsNewestFirst.length };
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+
1199
+ // src/provenance.ts
1200
+ import { execFileSync as execFileSync2 } from "node:child_process";
1201
+ import { writeFileSync as writeFileSync7 } from "node:fs";
1202
+ import { join as join11 } from "node:path";
1203
+ function git2(cwd, args) {
1204
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1205
+ }
1206
+ function modelLabel() {
1207
+ return process.env.MUGIWARA_MODEL?.trim() || process.env.ANTHROPIC_MODEL?.trim() || "model-unrecorded (set MUGIWARA_MODEL to attribute)";
1208
+ }
1209
+ function buildNote(s) {
1210
+ const gates = s.evidence.length ? s.evidence.join(" · ") : "no evidence recorded";
1211
+ const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
1212
+ const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(", ")}` : s.model ?? modelLabel();
1213
+ return [
1214
+ `mission: ${s.mission}`,
1215
+ `agent: ${s.actor || "unknown"} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
1216
+ `tasks: ${s.tasks_done}/${s.tasks_total}`,
1217
+ `gates/evidence: ${gates}`,
1218
+ `branch: ${s.branch}`,
1219
+ "human review: pending (PR review is the terminal gate)"
1220
+ ].join(`
1221
+ `);
1222
+ }
1223
+ function renderProvenanceMd(note, sha) {
1224
+ const lines = [
1225
+ "# Provenance",
1226
+ "",
1227
+ "<!-- paste below into the PR description or a PR comment -->",
1228
+ "",
1229
+ "```",
1230
+ note,
1231
+ "```",
1232
+ "",
1233
+ sha ? `Commit: ${sha}` : "Commit: not recorded (no git head resolved at closure)",
1234
+ "",
1235
+ "Query locally after pushing notes:",
1236
+ "`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`."
1237
+ ];
1238
+ return lines.join(`
1239
+ `) + `
1240
+ `;
1241
+ }
1242
+ function attachGitNote(projectDir, branch, note) {
1243
+ try {
1244
+ let sha;
1245
+ try {
1246
+ sha = git2(projectDir, ["rev-parse", "--verify", branch]);
1247
+ } catch {
1248
+ sha = git2(projectDir, ["rev-parse", "HEAD"]);
1249
+ }
1250
+ git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1251
+ return { sha };
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ }
1256
+ function blamePath(projectDir, path) {
1257
+ let sha;
1258
+ try {
1259
+ sha = git2(projectDir, ["log", "-1", "--format=%H", "--", path]);
1260
+ } catch {
1261
+ return `blame: not a git repository (${projectDir})`;
1262
+ }
1263
+ if (!sha)
1264
+ return `blame: no commit touches "${path}"`;
1265
+ try {
1266
+ const note = git2(projectDir, ["notes", "--ref=mugiwara", "show", sha]);
1267
+ return `${path} @ ${sha.slice(0, 7)}
1268
+ ${note}`;
1269
+ } catch {
1270
+ return `${path} @ ${sha.slice(0, 7)}
1271
+ (no mugiwara provenance note on this commit)`;
1272
+ }
1273
+ }
1274
+ function writeProvenance(projectDir, missionDir, state) {
1275
+ const note = buildNote(state);
1276
+ const attached = attachGitNote(projectDir, state.branch, note);
1277
+ writeFileSync7(join11(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1278
+ }
1279
+
1280
+ // src/routing.ts
1281
+ var DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
1282
+ var TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
1283
+ function scorePath(path, sensitivePaths, evidenceJoined) {
1284
+ const reasons = [];
1285
+ let score = 0;
1286
+ const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, "/")) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")).test(path)));
1287
+ if (sensitiveHit) {
1288
+ score += 100;
1289
+ reasons.push("sensitive path");
1290
+ }
1291
+ if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
1292
+ score += 50;
1293
+ reasons.push("production code");
1294
+ } else if (TEST_PAT.test(path)) {
1295
+ score += 10;
1296
+ reasons.push("test scaffolding — skim unless behavior changed");
1297
+ } else {
1298
+ score += 5;
1299
+ reasons.push("docs/config");
1300
+ }
1301
+ if (!evidenceJoined.includes(path)) {
1302
+ score += 20;
1303
+ reasons.push("not covered by recorded evidence");
1304
+ }
1305
+ return { score, reasons };
1306
+ }
1307
+ function rankFiles(paths, state) {
1308
+ const sensitive = state.sensitive_paths ?? [];
1309
+ const ev = state.evidence.join(" ");
1310
+ return paths.map((path) => {
1311
+ const { score, reasons } = scorePath(path, sensitive, ev);
1312
+ return { path, score, reasons };
1313
+ }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
1314
+ }
1315
+ function renderRouting(ranked, mission) {
1316
+ if (!ranked.length)
1317
+ return "";
1318
+ const lines = [
1319
+ "",
1320
+ "## Review routing",
1321
+ "",
1322
+ `Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
1323
+ ""
1324
+ ];
1325
+ ranked.forEach((r, i) => {
1326
+ lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join("; ")}`);
1327
+ });
1328
+ lines.push("");
1329
+ return lines.join(`
1330
+ `);
1331
+ }
1332
+
1333
+ // src/budget.ts
1334
+ import { existsSync as existsSync9, readdirSync as readdirSync6, statSync } from "node:fs";
1335
+ import { join as join12 } from "node:path";
1336
+ function readBudgetConfig(projectDir) {
1337
+ const cfg = readConfig(projectDir);
1338
+ const raw = cfg.context_budget_chars;
1339
+ if (raw === undefined || raw === "")
1340
+ return 0;
1341
+ const n = Number(raw);
1342
+ return Number.isFinite(n) && n > 0 ? n : 0;
1343
+ }
1344
+ function measureContextChars(missionDir) {
1345
+ let total = 0;
1346
+ const add = (p) => {
1347
+ try {
1348
+ total += statSync(p).size;
1349
+ } catch {}
1350
+ };
1351
+ for (const f of readdirSync6(missionDir)) {
1352
+ if (/\.md$/.test(f))
1353
+ add(join12(missionDir, f));
1354
+ }
1355
+ for (const sub of ["flows", "waves"]) {
1356
+ const dir = join12(missionDir, sub);
1357
+ if (existsSync9(dir)) {
1358
+ for (const f of readdirSync6(dir))
1359
+ add(join12(dir, f));
1360
+ }
1361
+ }
1362
+ return total;
1363
+ }
1364
+ function formatFootprint(chars, budget) {
1365
+ const base = `Context footprint: ${chars} chars`;
1366
+ if (!budget)
1367
+ return `${base} (no budget configured)`;
1368
+ return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
1369
+ }
1370
+
1371
+ // src/cost.ts
1372
+ import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
1373
+ import { join as join13 } from "node:path";
1374
+ var LANE_BUDGET = {
1375
+ lean: 12000,
1376
+ standard: 25000,
1377
+ full: 50000,
1378
+ spike: 3000
1379
+ };
1380
+ function budgetForLane(lane) {
1381
+ return LANE_BUDGET[lane] ?? 0;
1382
+ }
1383
+ function warnAt(budget) {
1384
+ return Math.floor(budget * 3 / 2);
1385
+ }
1386
+ function stopAt(budget) {
1387
+ return budget * 3;
1388
+ }
1389
+ function budgetStatus(budget, tokens) {
1390
+ if (budget > 0 && tokens >= stopAt(budget))
1391
+ return "stop";
1392
+ if (budget > 0 && tokens >= warnAt(budget))
1393
+ return "warn";
1394
+ return "ok";
1395
+ }
1396
+ function costEnvelope(state) {
1397
+ const planned = typeof state.budget === "number" && state.budget > 0 ? state.budget : budgetForLane(state.lane ?? "");
1398
+ const used = typeof state.tokens_est === "number" ? state.tokens_est : 0;
1399
+ const remaining = Math.max(planned - used, 0);
1400
+ const pct = planned > 0 ? Math.round(used / planned * 100) : 0;
1401
+ return {
1402
+ planned,
1403
+ used,
1404
+ remaining,
1405
+ pct,
1406
+ warn_at: warnAt(planned),
1407
+ stop_at: stopAt(planned),
1408
+ status: budgetStatus(planned, used)
1409
+ };
1410
+ }
1411
+ var COST_EVENTS_FILE = "cost-events.jsonl";
1412
+ function isAllowedMissionDir(dir) {
1413
+ if (!dir || dir.includes(".."))
1414
+ return false;
1415
+ if (dir.includes(".mugiwara/missions"))
1416
+ return true;
1417
+ if (dir.includes("mugiwara-"))
1418
+ return true;
1419
+ if (dir.startsWith("/tmp/") && dir.includes("-"))
1420
+ return true;
1421
+ return false;
1422
+ }
1423
+ function assertMissionDir(dir) {
1424
+ if (!isAllowedMissionDir(dir))
1425
+ throw new Error(`Invalid missionDir: ${dir}`);
1426
+ }
1427
+ function appendCostEvent(missionDir, event) {
1428
+ assertMissionDir(missionDir);
1429
+ mkdirSync6(missionDir, { recursive: true });
1430
+ const line = Object.assign({ ts: new Date().toISOString() }, event);
1431
+ appendFileSync(join13(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
1432
+ `, "utf8");
1433
+ }
1434
+
1435
+ // src/evidence.ts
1436
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9 } from "node:fs";
1437
+ import { join as join14 } from "node:path";
1438
+ var REGISTRY_FILE = "context-registry.jsonl";
1439
+ function isAllowedMissionDir2(dir) {
1440
+ if (!dir || dir.includes(".."))
1441
+ return false;
1442
+ if (dir.includes(".mugiwara/missions"))
1443
+ return true;
1444
+ if (dir.includes("mugiwara-"))
1445
+ return true;
1446
+ if (dir.startsWith("/tmp/") && dir.includes("-"))
1447
+ return true;
1448
+ return false;
1449
+ }
1450
+ function assertMissionDir2(dir) {
1451
+ if (!isAllowedMissionDir2(dir))
1452
+ throw new Error(`Invalid missionDir: ${dir}`);
1453
+ }
1454
+ function loadRegistry(missionDir) {
1455
+ assertMissionDir2(missionDir);
1456
+ const file = join14(missionDir, REGISTRY_FILE);
1457
+ try {
1458
+ const out = [];
1459
+ for (const line of readFileSync9(file, "utf8").split(/\r?\n/)) {
1460
+ if (!line.trim())
1461
+ continue;
1462
+ let e;
1463
+ try {
1464
+ e = JSON.parse(line);
1465
+ } catch {
1466
+ continue;
1467
+ }
1468
+ if (e === null || typeof e !== "object")
1469
+ continue;
1470
+ const entry = e;
1471
+ const ok = typeof entry.fingerprint === "string" && typeof entry.kind === "string" && typeof entry.file === "string" && typeof entry.id === "string" && typeof entry.ref === "string" && typeof entry.reads === "number" && Number.isFinite(entry.reads) && entry.reads >= 0;
1472
+ if (!ok)
1473
+ continue;
1474
+ entry.reads = Math.floor(entry.reads);
1475
+ out.push(entry);
1476
+ }
1477
+ return out;
1478
+ } catch {
1479
+ return [];
1480
+ }
1481
+ }
1482
+
1483
+ // src/context.ts
1484
+ function contextStatus(budgetChars, chars) {
1485
+ return budgetChars > 0 && chars > budgetChars ? "over" : "ok";
1486
+ }
1487
+ function computeContextMetrics(stats) {
1488
+ const reuse_rate = stats.reads_total > 0 ? stats.reads_reused / stats.reads_total : 0;
1489
+ const duplicate_chars = stats.total_chars - stats.unique_chars;
1490
+ return {
1491
+ files_loaded: stats.files_loaded,
1492
+ repeated_reads: stats.repeated_reads,
1493
+ duplicate_chars,
1494
+ reuse_rate,
1495
+ read_avoidance_chars: duplicate_chars
1496
+ };
1497
+ }
1498
+
1499
+ // src/reporting.ts
1500
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
1501
+ import { join as join15 } from "node:path";
1502
+ function isAllowedMissionDir3(dir) {
1503
+ if (!dir || dir.includes(".."))
1504
+ return false;
1505
+ if (dir.includes(".mugiwara/missions"))
1506
+ return true;
1507
+ if (dir.includes("mugiwara-"))
1508
+ return true;
1509
+ if (dir.startsWith("/tmp/") && dir.includes("-"))
1510
+ return true;
1511
+ return false;
1512
+ }
1513
+ function assertMissionDir3(dir) {
1514
+ if (!isAllowedMissionDir3(dir))
1515
+ throw new Error(`Invalid missionDir: ${dir}`);
1516
+ }
1517
+ function parseDecisionTrail(missionDir) {
1518
+ assertMissionDir3(missionDir);
1519
+ const file = join15(missionDir, "decisions.md");
1520
+ if (!existsSync10(file))
1521
+ return [];
1522
+ const raw = readFileSync10(file, "utf8");
1523
+ const lines = raw.split(/\r?\n/);
1524
+ const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
1525
+ const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
1526
+ const out = [];
1527
+ const bulletRe = /^-\s+(.+?)\s+—\s+(.+?):\s+(.+?)\s+—\s+reason:\s+(.+?)(?:\s+—\s+evidence:\s+(.+))?\s*$/;
1528
+ for (const line of scan) {
1529
+ const m = bulletRe.exec(line.trim());
1530
+ if (!m)
1531
+ continue;
1532
+ out.push({ ts: m[1], actor: m[2], decision: m[3], reason: m[4], ...m[5] ? { evidence: m[5] } : {} });
1533
+ }
1534
+ return out;
1535
+ }
1536
+ function loadCostEvents(missionDir) {
1537
+ assertMissionDir3(missionDir);
1538
+ const file = join15(missionDir, "cost-events.jsonl");
1539
+ if (!existsSync10(file))
1540
+ return [];
1541
+ const out = [];
1542
+ for (const line of readFileSync10(file, "utf8").split(/\r?\n/)) {
1543
+ if (!line.trim())
1544
+ continue;
1545
+ try {
1546
+ const e = JSON.parse(line);
1547
+ if (e && typeof e === "object" && typeof e.mission === "string")
1548
+ out.push(e);
1549
+ } catch {
1550
+ continue;
1551
+ }
1552
+ }
1553
+ return out;
1554
+ }
1555
+ function computeAvoidedMetrics(input) {
1556
+ const dup = input.registryMetrics?.duplicateCount ?? 0;
1557
+ const rep = input.registryMetrics?.repeatedReads ?? 0;
1558
+ const contexts_avoided = dup + rep;
1559
+ const stages_avoided = input.workMetrics?.stagesAvoided ?? 0;
1560
+ const slop_interventions = input.slopMetrics?.interventions ?? 0;
1561
+ const tokens_avoided_est = contexts_avoided * 150;
1562
+ return { stages_avoided, contexts_avoided, slop_interventions, tokens_avoided_est };
1563
+ }
1564
+ function computeEfficiencyMetrics(input) {
1565
+ const reuse_rate = input.totalReads > 0 ? Math.round(input.reuseHits / input.totalReads * 100) / 100 : 0;
1566
+ const duplicate_avoidance_chars = input.duplicateChars;
1567
+ const budget_efficiency_pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
1568
+ return { reuse_rate, duplicate_avoidance_chars, budget_efficiency_pct };
1569
+ }
1570
+ function buildCostLedger(input) {
1571
+ assertMissionDir3(input.missionDir);
1572
+ const events = loadCostEvents(input.missionDir);
1573
+ let registrySize = 0;
1574
+ let totalReads = 0;
1575
+ let reuseHits = 0;
1576
+ let duplicateChars = 0;
1577
+ try {
1578
+ const reg = loadRegistry(input.missionDir);
1579
+ registrySize = reg.length;
1580
+ totalReads = reg.reduce((s, e) => s + e.reads, 0);
1581
+ reuseHits = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
1582
+ const unique = reg.reduce((s, e) => s + (e.chars ?? 0), 0);
1583
+ const total = reg.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
1584
+ duplicateChars = total - unique;
1585
+ if (duplicateChars < 0)
1586
+ duplicateChars = 0;
1587
+ } catch {
1588
+ registrySize = 0;
1589
+ }
1590
+ const decisions = parseDecisionTrail(input.missionDir);
1591
+ const repeatedReads = reuseHits;
1592
+ const duplicateCount = duplicateChars > 0 ? 1 : 0;
1593
+ const avoided = computeAvoidedMetrics({
1594
+ registryMetrics: { duplicateCount: 0, repeatedReads: reuseHits },
1595
+ workMetrics: { stagesAvoided: input.workSummary?.stagesAvoided ?? 0 },
1596
+ slopMetrics: { interventions: input.slopSummary?.interventions ?? 0 }
1597
+ });
1598
+ if (duplicateChars > 0 && avoided.contexts_avoided === 0) {
1599
+ avoided.contexts_avoided = 1;
1600
+ avoided.tokens_avoided_est = 150;
1601
+ }
1602
+ const efficiency = computeEfficiencyMetrics({
1603
+ totalReads,
1604
+ reuseHits,
1605
+ duplicateChars,
1606
+ budget: input.envelope.planned,
1607
+ used: input.envelope.used
1608
+ });
1609
+ return {
1610
+ envelope: input.envelope,
1611
+ ledger: { events, registrySize, decisions },
1612
+ avoided,
1613
+ efficiency,
1614
+ trail: decisions
1615
+ };
1616
+ }
1617
+ function toCostJSON(ledger) {
1618
+ return JSON.stringify({ envelope: ledger.envelope, ledger: ledger.ledger, avoided: ledger.avoided, efficiency: ledger.efficiency, trail: ledger.trail }, null, 2);
1619
+ }
1620
+ function summarizeAdaptation(missionDir) {
1621
+ const trail = parseDecisionTrail(missionDir);
1622
+ const postureRe = /posture|switch|adapt|pause|parallel-workers|context-relief|phase-isolated|team-scoped/i;
1623
+ const rows = trail.filter((t) => postureRe.test(`${t.decision} ${t.reason}`)).map((t) => ({ ts: t.ts, decision: t.decision, reason: t.reason }));
1624
+ return { count: rows.length, rows };
1625
+ }
1626
+ function renderAdaptationSection(missionDir) {
1627
+ const { count, rows } = summarizeAdaptation(missionDir);
1628
+ if (count === 0)
1629
+ return "";
1630
+ const lines = ["", "## Adaptation", "", "| Time | Decision | Reason |", "|---|---|---|"];
1631
+ for (const r of rows)
1632
+ lines.push(`| ${r.ts} | ${r.decision} | ${r.reason} |`);
1633
+ if (rows.length === 0)
1634
+ lines.push("| — | (posture rows recorded in decisions.md) | |");
1635
+ return lines.join(`
1636
+ `);
1637
+ }
1638
+
1639
+ // src/mission.ts
1640
+ function isStateFile(f) {
1641
+ const stem = f.replace(/\.json$/, "");
1642
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
1643
+ }
1644
+ function primaryState(dir, files) {
1645
+ const name = files.includes("state.json") ? "state.json" : files.find((f) => f.endsWith(".json") && f !== "continue.json" && !f.startsWith("continue-"));
1646
+ if (!name)
1647
+ return null;
1648
+ try {
1649
+ return JSON.parse(readFileSync11(join16(dir, name), "utf8"));
1650
+ } catch {
1651
+ return null;
1652
+ }
1653
+ }
1654
+ function changedFiles(projectDir, state) {
1655
+ const base = typeof state?.base_sha === "string" ? state.base_sha : "";
1656
+ const branch = typeof state?.branch === "string" ? state.branch : "";
1657
+ if (!base || base === "unknown" || !branch)
1658
+ return [];
1659
+ try {
1660
+ return execFileSync3("git", ["diff", "--name-only", base, branch], {
1661
+ cwd: projectDir,
1662
+ encoding: "utf8",
1663
+ stdio: ["ignore", "pipe", "ignore"]
1664
+ }).split(/\r?\n/).filter(Boolean);
1665
+ } catch {
1666
+ return [];
1667
+ }
1668
+ }
903
1669
  function activeActor(projectDir) {
904
- const stateDir = join7(projectDir, ".mugiwara", "state");
905
- if (!existsSync6(stateDir))
1670
+ const missionsDir = join16(projectDir, ".mugiwara", "missions");
1671
+ if (!existsSync11(missionsDir))
906
1672
  return null;
907
- const missions = readdirSync4(stateDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1673
+ const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
908
1674
  let latest = null;
909
1675
  for (const mission of missions) {
910
- const d = join7(stateDir, mission);
911
- for (const f of readdirSync4(d).filter((f2) => f2.endsWith(".json"))) {
1676
+ const d = join16(missionsDir, mission);
1677
+ for (const f of readdirSync7(d).filter(isStateFile)) {
912
1678
  try {
913
- const s = JSON.parse(readFileSync5(join7(d, f), "utf8"));
1679
+ const s = JSON.parse(readFileSync11(join16(d, f), "utf8"));
914
1680
  const t = Date.parse(s.updated_at || "") || 0;
915
1681
  if (!latest || t > latest.updated)
916
1682
  latest = { actor: s.actor || null, updated: t };
@@ -920,8 +1686,8 @@ function activeActor(projectDir) {
920
1686
  return latest ? latest.actor : null;
921
1687
  }
922
1688
  function resetMission(projectDir, keepLogs, force) {
923
- const root = join7(projectDir, ".mugiwara");
924
- if (!existsSync6(root))
1689
+ const root = join16(projectDir, ".mugiwara");
1690
+ if (!existsSync11(root))
925
1691
  return { removed: [], kept: [] };
926
1692
  if (!force) {
927
1693
  const actor = activeActor(projectDir);
@@ -931,361 +1697,326 @@ function resetMission(projectDir, keepLogs, force) {
931
1697
  }
932
1698
  const removed = [];
933
1699
  const kept = [];
934
- for (const dir of ["spec", "plans", "results", "review", "issues", "reports"]) {
935
- const p = join7(root, dir);
936
- if (existsSync6(p)) {
937
- rmSync2(p, { recursive: true, force: true });
938
- removed.push(dir);
939
- }
940
- }
941
- for (const dir of ["state", "continue"]) {
942
- const p = join7(root, dir);
943
- if (existsSync6(p)) {
1700
+ for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
1701
+ const p = join16(root, dir);
1702
+ if (existsSync11(p)) {
944
1703
  rmSync2(p, { recursive: true, force: true });
945
1704
  removed.push(dir);
946
1705
  }
947
1706
  }
948
- for (const f of readdirSync4(root)) {
1707
+ for (const f of readdirSync7(root)) {
949
1708
  if (/^state(-.+)?\.json$/.test(f)) {
950
- const p = join7(root, f);
951
- if (existsSync6(p)) {
1709
+ const p = join16(root, f);
1710
+ if (existsSync11(p)) {
952
1711
  rmSync2(p);
953
1712
  removed.push(f);
954
1713
  }
955
1714
  }
956
1715
  }
957
1716
  if (!keepLogs) {
958
- const p = join7(root, "logs");
959
- if (existsSync6(p)) {
960
- rmSync2(p, { recursive: true, force: true });
961
- removed.push("logs");
1717
+ for (const p of [join16(root, "lessons.md"), join16(root, "logs")]) {
1718
+ if (existsSync11(p)) {
1719
+ rmSync2(p, { recursive: true, force: true });
1720
+ removed.push(p.startsWith(join16(root, "logs")) ? "logs" : "lessons.md");
1721
+ }
962
1722
  }
963
- } else if (existsSync6(join7(root, "logs"))) {
964
- kept.push("logs");
1723
+ } else {
1724
+ if (existsSync11(join16(root, "lessons.md")))
1725
+ kept.push("lessons.md");
1726
+ else if (existsSync11(join16(root, join16("logs", "lessons.md"))))
1727
+ kept.push(join16("logs", "lessons.md"));
965
1728
  }
966
1729
  for (const f of ["config", "manifest.json", "backup"]) {
967
- if (existsSync6(join7(root, f)))
1730
+ if (existsSync11(join16(root, f)))
968
1731
  kept.push(f);
969
1732
  }
970
1733
  return { removed, kept };
971
1734
  }
972
1735
  function archiveMission(projectDir, mission, opts = {}) {
973
1736
  const { dryRun = false } = opts;
974
- const root = join7(projectDir, ".mugiwara");
1737
+ const root = join16(projectDir, ".mugiwara");
975
1738
  if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
976
1739
  throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
977
1740
  const removed = [];
978
1741
  const kept = [];
979
- const belongs = (f) => {
980
- const base = f.replace(/^\d{4}-\d{2}-\d{2}-/, "");
981
- return base === `${mission}.md` || base.startsWith(`${mission}-`);
982
- };
983
- let report = null;
984
- const reportsDir = join7(root, "reports");
985
- if (existsSync6(reportsDir)) {
986
- const f = readdirSync4(reportsDir).find((n) => {
987
- const m = n.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
988
- return !!m && m[2] === mission;
989
- });
990
- if (f)
991
- report = join7("reports", f);
992
- }
993
- const resultsDir = join7(root, "results", mission);
994
- if (existsSync6(resultsDir)) {
995
- for (const f of readdirSync4(resultsDir)) {
996
- kept.push(join7("results", mission, f));
1742
+ const dir = join16(root, "missions", mission);
1743
+ if (!existsSync11(dir))
1744
+ return { report: null, removed, kept };
1745
+ if (!dryRun) {
1746
+ const issues = checkTrail(dir, projectDir);
1747
+ if (issues.length) {
1748
+ throw new Error(`closure integrity gate failed — fix these before archiving:
1749
+ ${formatIssues(issues)}`);
997
1750
  }
998
- }
999
- const specDir = join7(root, "spec");
1000
- if (existsSync6(specDir)) {
1001
- for (const f of readdirSync4(specDir)) {
1002
- if (!belongs(f))
1003
- continue;
1004
- const p = join7(specDir, f);
1005
- if (!dryRun)
1006
- rmSync2(p);
1007
- removed.push(join7("spec", f));
1751
+ const artifacts = checkMissionArtifacts(dir);
1752
+ if (!artifacts.ok) {
1753
+ throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
1008
1754
  }
1009
1755
  }
1010
- for (const dir of ["review", "issues"]) {
1011
- const d = join7(root, dir);
1012
- if (!existsSync6(d))
1013
- continue;
1014
- for (const f of readdirSync4(d)) {
1015
- if (!belongs(f))
1016
- continue;
1017
- const p = join7(d, f);
1018
- if (!dryRun)
1019
- rmSync2(p, { force: true });
1020
- removed.push(join7(dir, f));
1756
+ const files = readdirSync7(dir);
1757
+ const state = primaryState(dir, files);
1758
+ const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
1759
+ try {
1760
+ const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
1761
+ return typeof s.model === "string" ? s.model : "";
1762
+ } catch {
1763
+ return "";
1764
+ }
1765
+ }).filter(Boolean))];
1766
+ let costSection = "";
1767
+ if (!dryRun && state) {
1768
+ const chars = measureContextChars(dir);
1769
+ const budget = readBudgetConfig(projectDir);
1770
+ const footprintLine = formatFootprint(chars, budget);
1771
+ const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
1772
+ const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
1773
+ const lane = typeof state.lane === "string" ? state.lane : "unknown";
1774
+ const laneBudget = budgetForLane(lane);
1775
+ const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
1776
+ const effBudget = budget || laneBudget;
1777
+ const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
1778
+ const srcLabel = src === "reported" ? "provider-reported" : "estimator";
1779
+ const statusLabel = env.status.toUpperCase();
1780
+ const ctxStatus = contextStatus(budget, chars);
1781
+ const registry = loadRegistry(dir);
1782
+ const reads_total = registry.reduce((s, e) => s + e.reads, 0);
1783
+ const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
1784
+ const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
1785
+ const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
1786
+ const charTracked = registry.length > 0 && total_chars > 0;
1787
+ const metrics = registry.length ? computeContextMetrics({
1788
+ files_loaded: registry.length,
1789
+ reads_total,
1790
+ reads_reused: repeated_reads,
1791
+ unique_chars,
1792
+ total_chars,
1793
+ repeated_reads
1794
+ }) : { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
1795
+ const ctxNote = registry.length ? charTracked ? "" : " (char data not tracked)" : " (no registry — reads not tracked)";
1796
+ let reportedTotal = 0;
1797
+ let hasReported = false;
1798
+ for (const f of files.filter(isStateFile)) {
1799
+ try {
1800
+ const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
1801
+ if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
1802
+ reportedTotal += s.tokens_est;
1803
+ hasReported = true;
1804
+ }
1805
+ } catch {}
1806
+ }
1807
+ if (!hasReported && src === "reported" && est > 0) {
1808
+ reportedTotal = est;
1809
+ hasReported = true;
1810
+ }
1811
+ costSection = [
1812
+ "## Cost",
1813
+ "",
1814
+ "| Metric | Value |",
1815
+ "|--------|-------|",
1816
+ `| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
1817
+ `| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
1818
+ `| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
1819
+ `| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
1820
+ `| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
1821
+ `| **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} |`
1822
+ ].join(`
1823
+ `);
1824
+ if (hasReported) {
1825
+ costSection += `
1826
+ | **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
1827
+ }
1828
+ try {
1829
+ const ledger = buildCostLedger({ missionDir: dir, envelope: env });
1830
+ costSection += `
1831
+ | Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
1832
+ costSection += `
1833
+ | Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
1834
+ costSection += `
1835
+ | Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
1836
+ costSection += `
1837
+ | Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
1838
+ costSection += `
1839
+ | Trail | ${ledger.trail.length} decisions |`;
1840
+ if (ledger.trail.length) {
1841
+ const show = ledger.trail.slice(0, 5);
1842
+ for (const t of show)
1843
+ costSection += `
1844
+ - ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
1845
+ if (ledger.trail.length > 5)
1846
+ costSection += `
1847
+ … ${ledger.trail.length - 5} more`;
1848
+ }
1849
+ } catch {}
1850
+ costSection += `
1851
+ `;
1852
+ try {
1853
+ costSection += renderAdaptationSection(dir);
1854
+ } catch {}
1855
+ appendCostEvent(dir, {
1856
+ kind: "closure",
1857
+ mission,
1858
+ tokens_est: est,
1859
+ budget: laneBudget,
1860
+ status: env.status,
1861
+ context_chars: chars,
1862
+ context_status: ctxStatus,
1863
+ context_metrics: metrics
1864
+ });
1865
+ if (budget && chars > budget) {
1866
+ throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
1021
1867
  }
1022
1868
  }
1023
- const logsDir = join7(root, "logs");
1024
- if (existsSync6(logsDir)) {
1025
- for (const f of readdirSync4(logsDir)) {
1026
- if (!belongs(f))
1869
+ const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
1870
+ const fold = [];
1871
+ for (const f of FOLD_TOP) {
1872
+ if (files.includes(f))
1873
+ fold.push(f);
1874
+ }
1875
+ const PR_VERDICT = "pr-verdict.md";
1876
+ const PR_VERDICT_SRC = join16("flows", "07-pr-verdict.md");
1877
+ const flowsDir = join16(dir, "flows");
1878
+ const legacyWavesDir = join16(dir, "waves");
1879
+ const artDir = existsSync11(flowsDir) ? flowsDir : existsSync11(legacyWavesDir) ? legacyWavesDir : flowsDir;
1880
+ const artRel = artDir === legacyWavesDir ? "waves" : "flows";
1881
+ if (existsSync11(artDir)) {
1882
+ for (const f of readdirSync7(artDir).sort()) {
1883
+ if (artRel === "flows" && f === "07-pr-verdict.md")
1027
1884
  continue;
1028
- const p = join7(logsDir, f);
1029
- if (!dryRun)
1030
- rmSync2(p);
1031
- removed.push(join7("logs", f));
1885
+ fold.push(join16(artRel, f));
1032
1886
  }
1033
1887
  }
1034
- const contDir = join7(root, "continue", mission);
1035
- if (existsSync6(contDir)) {
1036
- if (!dryRun)
1037
- rmSync2(contDir, { recursive: true, force: true });
1038
- removed.push(join7("continue", mission));
1039
- }
1040
- const stateDir = join7(root, "state", mission);
1041
- if (existsSync6(stateDir)) {
1042
- if (!dryRun)
1043
- rmSync2(stateDir, { recursive: true, force: true });
1044
- removed.push(join7("state", mission));
1045
- }
1046
- if (report)
1047
- kept.push(report);
1048
- for (const k of ["plans", "config", join7("logs", "lessons.md")]) {
1049
- if (existsSync6(join7(root, k)))
1050
- kept.push(k);
1051
- }
1888
+ if (existsSync11(join16(dir, "cost-events.jsonl")))
1889
+ fold.push("cost-events.jsonl");
1890
+ if (existsSync11(join16(dir, "context-registry.jsonl")))
1891
+ fold.push("context-registry.jsonl");
1892
+ let report = "";
1893
+ const reportPath = join16(dir, "report.md");
1894
+ if (files.includes("report.md"))
1895
+ report = readFileSync11(reportPath, "utf8");
1896
+ else if (existsSync11(join16(artDir, "06-closure.md")))
1897
+ report = readFileSync11(join16(artDir, "06-closure.md"), "utf8");
1898
+ if (!dryRun) {
1899
+ mkdirSync8(dir, { recursive: true });
1900
+ const prVerdictSrc = join16(dir, PR_VERDICT_SRC);
1901
+ if (existsSync11(prVerdictSrc)) {
1902
+ const prVerdictPath = join16(dir, PR_VERDICT);
1903
+ writeFileSync8(prVerdictPath, readFileSync11(prVerdictSrc, "utf8"));
1904
+ kept.push(join16("missions", mission, PR_VERDICT));
1905
+ }
1906
+ if (fold.length) {
1907
+ const sections = fold.map((f) => {
1908
+ const body = readFileSync11(join16(dir, f), "utf8").trim();
1909
+ const name = f.includes("/") ? f.split("/").pop() ?? f : f;
1910
+ return `
1911
+
1912
+ ## Archived: ${name}
1913
+
1914
+ ${body}`;
1915
+ }).join("");
1916
+ const tmp = `${reportPath}.tmp`;
1917
+ const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
1918
+ mission,
1919
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
1920
+ sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
1921
+ }), mission) : "";
1922
+ writeFileSync8(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1923
+ ${costSection}
1924
+ ` : "") + `
1925
+ `);
1926
+ renameSync(tmp, reportPath);
1927
+ }
1928
+ for (const f of fold) {
1929
+ rmSync2(join16(dir, f), { force: true, recursive: true });
1930
+ removed.push(join16("missions", mission, f));
1931
+ }
1932
+ if (existsSync11(prVerdictSrc)) {
1933
+ rmSync2(join16(dir, PR_VERDICT_SRC), { force: true });
1934
+ removed.push(join16("missions", mission, PR_VERDICT_SRC));
1935
+ }
1936
+ for (const f of files.filter((f2) => f2.endsWith(".json")))
1937
+ rmSync2(join16(dir, f), { force: true });
1938
+ if (existsSync11(artDir) && readdirSync7(artDir).length === 0)
1939
+ rmSync2(artDir, { recursive: true, force: true });
1940
+ if (!existsSync11(reportPath)) {
1941
+ writeFileSync8(reportPath, `# Mission: ${mission}
1942
+
1943
+ Archived before closure — no wave artifacts were present.
1944
+ `);
1945
+ }
1946
+ if (state && typeof state.branch === "string") {
1947
+ const rb = generateRollback(projectDir, dir, {
1948
+ mission,
1949
+ branch: state.branch,
1950
+ baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
1951
+ });
1952
+ if (rb)
1953
+ kept.push(join16("missions", mission, rb.file));
1954
+ try {
1955
+ writeProvenance(projectDir, dir, {
1956
+ mission,
1957
+ actor: typeof state.actor === "string" ? state.actor : "",
1958
+ lane: typeof state.lane === "string" ? state.lane : "",
1959
+ mode: typeof state.mode === "string" ? state.mode : "",
1960
+ branch: state.branch,
1961
+ tasks_done: Number(state.tasks_done) || 0,
1962
+ tasks_total: Number(state.tasks_total) || 0,
1963
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
1964
+ models: stageModels
1965
+ });
1966
+ kept.push(join16("missions", mission, "provenance.md"));
1967
+ } catch {}
1968
+ }
1969
+ } else {
1970
+ for (const f of fold)
1971
+ removed.push(join16("missions", mission, f));
1972
+ }
1973
+ removed.push(join16("missions", mission, "<session state>"));
1974
+ if (files.includes("plan.md"))
1975
+ kept.push(join16("missions", mission, "plan.md"));
1976
+ if (files.includes("handoff.md"))
1977
+ kept.push(join16("missions", mission, "handoff.md"));
1978
+ kept.push(join16("missions", mission, "report.md"));
1052
1979
  let index;
1053
- const indexFile = join7(root, "reports", "index.md");
1054
- const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}${report ? ` → ${report}` : ""}
1980
+ const indexFile = join16(root, "index.md");
1981
+ const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
1055
1982
  `;
1056
1983
  if (!dryRun) {
1057
- mkdirSync5(join7(root, "reports"), { recursive: true });
1058
- const existing = existsSync6(indexFile) ? readFileSync5(indexFile, "utf8") : "";
1984
+ const existing = existsSync11(indexFile) ? readFileSync11(indexFile, "utf8") : "";
1059
1985
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
1060
1986
  const header = existing ? "" : `# Mission index
1061
1987
 
1062
1988
  `;
1063
- appendFileSync(indexFile, header + line);
1064
- }
1065
- index = join7("reports", "index.md");
1066
- }
1067
- return { report, removed, kept, index };
1068
- }
1069
-
1070
- // src/onboard.ts
1071
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
1072
- import { join as join8 } from "node:path";
1073
- var DEFAULT_ORDER = [
1074
- "mode",
1075
- "branch",
1076
- "commit",
1077
- "auto_commit",
1078
- "coverage_new",
1079
- "coverage_modified",
1080
- "review_depth",
1081
- "quality_depth",
1082
- "delegate_threshold",
1083
- "heal_max_cycles",
1084
- "verbosity"
1085
- ];
1086
- var DEFAULTS = {
1087
- auto_commit: "on",
1088
- delegate_threshold: "60",
1089
- heal_max_cycles: "3",
1090
- verbosity: "normal"
1091
- };
1092
- var BRANCH_PRESETS = {
1093
- 1: "feature/{type}-{issue}-{slug}",
1094
- 2: "feature/{slug}",
1095
- 3: "feat/{slug}"
1096
- };
1097
- var COMMIT_PRESETS = {
1098
- 1: "conventional",
1099
- 2: "gitmoji",
1100
- 3: "plain"
1101
- };
1102
- function parseConfig(lines) {
1103
- const map = new Map;
1104
- for (const line of lines) {
1105
- const t = line.trim();
1106
- if (!t || t.startsWith("#"))
1107
- continue;
1108
- const eq = t.indexOf("=");
1109
- if (eq === -1)
1110
- continue;
1111
- const k = t.slice(0, eq).trim();
1112
- const v = t.slice(eq + 1).trim();
1113
- if (!map.has(k))
1114
- map.set(k, v);
1115
- }
1116
- return map;
1117
- }
1118
- function buildConfig(existing, updates) {
1119
- const merged = new Map(existing);
1120
- for (const [k, v] of Object.entries(updates))
1121
- merged.set(k, v);
1122
- const lines = [];
1123
- for (const k of DEFAULT_ORDER) {
1124
- const v = merged.get(k) ?? DEFAULTS[k];
1125
- if (v === undefined)
1126
- continue;
1127
- lines.push(`${k}=${v}`);
1128
- merged.delete(k);
1129
- }
1130
- for (const [k, v] of merged)
1131
- lines.push(`${k}=${v}`);
1132
- return lines.join(`
1133
- `) + `
1134
- `;
1135
- }
1136
- async function askFreeText(rl, prompt) {
1137
- for (;; ) {
1138
- const v = (await rl.question(prompt)).trim();
1139
- if (v)
1140
- return v;
1141
- console.log(" Cannot be empty.");
1142
- }
1143
- }
1144
- function assertNotSymlink2(file) {
1145
- if (!existsSync7(file))
1146
- return;
1147
- try {
1148
- if (lstatSync2(file).isSymbolicLink())
1149
- throw new Error(`refusing to follow symlink: ${file}`);
1150
- } catch (e) {
1151
- if (e.code === "ENOENT")
1152
- return;
1153
- throw e;
1154
- }
1155
- }
1156
- async function runOnboard(projectDir) {
1157
- if (!process.stdin.isTTY) {
1158
- throw new Error("Not a terminal. Run `mugiwara onboard` in a terminal, or edit .mugiwara/config directly.");
1159
- }
1160
- const mugiwaraDir = join8(projectDir, ".mugiwara");
1161
- const configPath = join8(mugiwaraDir, "config");
1162
- assertNotSymlink2(configPath);
1163
- const existing = parseConfig(existsSync7(configPath) ? readFileSync6(configPath, "utf8").split(/\r?\n/) : []);
1164
- const rl = createRl();
1165
- try {
1166
- console.log("");
1167
- console.log("╔══════════════════════════════════════╗");
1168
- console.log("║ Mugiwara Onboarding Wizard ║");
1169
- console.log("╚══════════════════════════════════════╝");
1170
- console.log("");
1171
- if (existing.size > 0) {
1172
- const redo = await confirm(rl, "Existing .mugiwara/config found. Re-onboard?");
1173
- if (!redo) {
1174
- console.log("Onboarding cancelled. Config preserved.");
1175
- return;
1989
+ const fd = openSync(indexFile, "a");
1990
+ try {
1991
+ writeSync(fd, header + line);
1992
+ } finally {
1993
+ closeSync(fd);
1176
1994
  }
1177
- console.log("");
1178
- }
1179
- const branchChoice = await choose(rl, "Q1 — Branch naming pattern:", [
1180
- "Trunk-based — feature/{type}-{issue}-{slug}",
1181
- "GitFlow — feature/{slug}",
1182
- "GitHub Flow — feat/{slug}",
1183
- "Custom — enter your own pattern"
1184
- ]);
1185
- let branch;
1186
- if (branchChoice === 3) {
1187
- branch = await askFreeText(rl, " Branch pattern (placeholders: {type} {issue} {slug}, e.g. {issue}-{slug}): ");
1188
- } else {
1189
- branch = BRANCH_PRESETS[branchChoice + 1];
1190
1995
  }
1191
- const modeIdx = await choose(rl, "Q2 — Autonomy mode:", [
1192
- "guided — ask before every wave transition",
1193
- "semi — auto-advance through waves, pause on failures",
1194
- "auto — full auto-pilot"
1195
- ]);
1196
- const mode = ["guided", "semi", "auto"][modeIdx];
1197
- const reviewIdx = await choose(rl, "Q3 — Code review depth:", [
1198
- "full — breaking-change map, five-axis review, <=3 cycles",
1199
- "standard — five-axis review, 1 cycle",
1200
- "quick — diff-only, no caller-map"
1201
- ]);
1202
- const reviewDepth = ["full", "standard", "quick"][reviewIdx];
1203
- const qualityIdx = await choose(rl, "Q4 — Quality check depth:", [
1204
- "full — format, lint, typecheck, test, build",
1205
- "standard — lint, typecheck, test",
1206
- "quick — test only"
1207
- ]);
1208
- const qualityDepth = ["full", "standard", "quick"][qualityIdx];
1209
- const covIdx = await choose(rl, "Q5 — Test coverage threshold:", [
1210
- "90/80 — new code 90%, modified 80%",
1211
- "80/70 — new code 80%, modified 70%",
1212
- "custom — enter your own values",
1213
- "none — 0/0, no coverage enforcement"
1214
- ]);
1215
- let coverageNew = 90;
1216
- let coverageModified = 80;
1217
- if (covIdx === 1) {
1218
- coverageNew = 80;
1219
- coverageModified = 70;
1220
- } else if (covIdx === 2) {
1221
- coverageNew = Number.parseInt(await askFreeText(rl, " Coverage for new code (%): "), 10) || 0;
1222
- coverageModified = Number.parseInt(await askFreeText(rl, " Coverage for modified code (%): "), 10) || 0;
1223
- } else if (covIdx === 3) {
1224
- coverageNew = 0;
1225
- coverageModified = 0;
1226
- }
1227
- const commitChoice = await choose(rl, "Q6 — Commit style:", [
1228
- "Conventional Commits — feat:, fix:, chore:, docs:",
1229
- "Gitmoji — emoji-prefixed conventional",
1230
- "Plain — no prefix, imperative sentence",
1231
- "Custom template — enter your own (placeholders: {type} {issue} {title})"
1232
- ]);
1233
- let commit;
1234
- if (commitChoice === 3) {
1235
- commit = await askFreeText(rl, " Commit template (e.g. {issue}: {title}): ");
1236
- } else {
1237
- commit = COMMIT_PRESETS[commitChoice + 1];
1238
- }
1239
- if (!existsSync7(mugiwaraDir))
1240
- mkdirSync6(mugiwaraDir, { recursive: true });
1241
- const body = buildConfig(existing, {
1242
- mode,
1243
- branch,
1244
- commit,
1245
- coverage_new: String(coverageNew),
1246
- coverage_modified: String(coverageModified),
1247
- review_depth: reviewDepth,
1248
- quality_depth: qualityDepth
1249
- });
1250
- writeFileSync5(configPath, `# .mugiwara/config — written by mugiwara onboard on ${new Date().toISOString().slice(0, 10)}
1251
- ${body}`);
1252
- console.log("╔══════════════════════════════════════╗");
1253
- console.log("║ Onboarding Complete ║");
1254
- console.log("╚══════════════════════════════════════╝");
1255
- console.log("");
1256
- console.log(` Branch pattern: ${branch}`);
1257
- console.log(` Autonomy mode: ${mode}`);
1258
- console.log(` Review depth: ${reviewDepth}`);
1259
- console.log(` Quality depth: ${qualityDepth}`);
1260
- console.log(` Coverage: ${coverageNew}/${coverageModified}`);
1261
- console.log(` Commit style: ${commit}`);
1262
- console.log("");
1263
- console.log(` Config written: ${configPath}`);
1264
- console.log("");
1265
- } finally {
1266
- rl.close();
1996
+ index = "index.md";
1267
1997
  }
1998
+ return { report: join16("missions", mission, "report.md"), removed, kept, index };
1268
1999
  }
1269
2000
 
1270
2001
  // src/run.ts
1271
- import { existsSync as existsSync8, readdirSync as readdirSync5 } from "node:fs";
2002
+ import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
1272
2003
  import { spawnSync } from "node:child_process";
1273
- import { dirname as dirname5, join as join9, resolve } from "node:path";
2004
+ import { dirname as dirname5, join as join17, resolve } from "node:path";
1274
2005
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1275
2006
  var here3 = dirname5(fileURLToPath4(import.meta.url));
1276
- var SCRIPTS_DIR = join9(here3, "..", "scripts");
1277
- var RUNNABLE = ["savepoint.sh", "lane.sh", "evidence.sh", "mission-report.sh"];
2007
+ var SCRIPTS_DIR = join17(here3, "..", "scripts");
2008
+ var RUNNABLE = ["savepoint.sh", "lane.sh"];
1278
2009
  function findBash() {
1279
2010
  const explicit = process.env.MUGIWARA_BASH?.trim();
1280
2011
  if (explicit)
1281
- return existsSync8(explicit) ? explicit : null;
2012
+ return existsSync12(explicit) ? explicit : null;
1282
2013
  const candidates = process.platform === "win32" ? [
1283
2014
  "C:\\Program Files\\Git\\bin\\bash.exe",
1284
2015
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
1285
- join9(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2016
+ join17(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
1286
2017
  ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
1287
2018
  for (const p of candidates) {
1288
- if (p && existsSync8(p))
2019
+ if (p && existsSync12(p))
1289
2020
  return p;
1290
2021
  }
1291
2022
  const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
@@ -1302,9 +2033,9 @@ function runScript(name, args, projectDir) {
1302
2033
  if (!/^[a-z0-9-]+\.sh$/.test(name)) {
1303
2034
  throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
1304
2035
  }
1305
- const script = join9(SCRIPTS_DIR, name);
1306
- if (!existsSync8(script)) {
1307
- const have = existsSync8(SCRIPTS_DIR) ? readdirSync5(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
2036
+ const script = join17(SCRIPTS_DIR, name);
2037
+ if (!existsSync12(script)) {
2038
+ const have = existsSync12(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
1308
2039
  throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
1309
2040
  }
1310
2041
  const bash = findBash();
@@ -1321,9 +2052,9 @@ function runScript(name, args, projectDir) {
1321
2052
  }
1322
2053
 
1323
2054
  // src/continue.ts
1324
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "node:fs";
1325
- import { execFileSync } from "node:child_process";
1326
- import { join as join10 } from "node:path";
2055
+ import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync9 } from "node:fs";
2056
+ import { execFileSync as execFileSync4 } from "node:child_process";
2057
+ import { join as join18 } from "node:path";
1327
2058
  var SAFE = /^[A-Za-z0-9._-]+$/;
1328
2059
  var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
1329
2060
  var num = (v) => {
@@ -1338,34 +2069,46 @@ function gitActor(cwd) {
1338
2069
  const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
1339
2070
  if (author)
1340
2071
  return author;
1341
- const git = (args) => {
2072
+ const git3 = (args) => {
1342
2073
  try {
1343
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2074
+ return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1344
2075
  } catch {
1345
2076
  return "";
1346
2077
  }
1347
2078
  };
1348
- const name = git(["config", "user.name"]);
1349
- const email = git(["config", "user.email"]);
2079
+ const name = git3(["config", "user.name"]);
2080
+ const email = git3(["config", "user.email"]);
1350
2081
  if (name && email)
1351
2082
  return `${name} <${email}>`;
1352
2083
  return name || process.env.USER || process.env.USERNAME || "";
1353
2084
  }
1354
- function scan(projectDir, root, map) {
1355
- const base = join10(projectDir, ".mugiwara", root);
1356
- if (!existsSync9(base))
2085
+ function scan(projectDir, kind, map) {
2086
+ const base = join18(projectDir, ".mugiwara", "missions");
2087
+ if (!existsSync13(base))
1357
2088
  return [];
1358
2089
  const out = [];
1359
- const missions = readdirSync6(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
2090
+ const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
1360
2091
  for (const mission of missions) {
1361
- const dir = join10(base, mission);
1362
- for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
2092
+ const dir = join18(base, mission);
2093
+ for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
1363
2094
  const stem = f.slice(0, -".json".length);
1364
- const member = stem === "state" ? null : stem;
2095
+ let member;
2096
+ if (kind === "state") {
2097
+ if (stem === "continue" || stem.startsWith("continue-"))
2098
+ continue;
2099
+ member = stem === "state" ? null : stem;
2100
+ } else {
2101
+ if (stem === "continue")
2102
+ member = null;
2103
+ else if (stem.startsWith("continue-"))
2104
+ member = stem.slice("continue-".length);
2105
+ else
2106
+ continue;
2107
+ }
1365
2108
  if (member !== null && !isSafeKey(member))
1366
2109
  continue;
1367
2110
  try {
1368
- const raw = JSON.parse(readFileSync7(join10(dir, f), "utf8"));
2111
+ const raw = JSON.parse(readFileSync12(join18(dir, f), "utf8"));
1369
2112
  if (text(raw.mission) !== mission)
1370
2113
  continue;
1371
2114
  out.push(map(raw, member));
@@ -1407,8 +2150,10 @@ function readState(projectDir) {
1407
2150
  lane_rose: r.lane_rose === true,
1408
2151
  lane_prev: text(r.lane_prev),
1409
2152
  lane_peak: text(r.lane_peak),
1410
- next_action: "",
1411
- next_session_prompt: "",
2153
+ base_sha: text(r.base_sha),
2154
+ sensitive_paths: Array.isArray(r.sensitive_paths) ? r.sensitive_paths.map(text).filter(Boolean) : [],
2155
+ next_action: text(r.next_action),
2156
+ next_session_prompt: text(r.next_session_prompt),
1412
2157
  updated_at: text(r.updated_at),
1413
2158
  blockers_open: num(r.blockers_open),
1414
2159
  heal_cycle: num(r.heal_cycle),
@@ -1478,6 +2223,221 @@ function formatResume(e) {
1478
2223
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
1479
2224
  }
1480
2225
 
2226
+ // src/sign.ts
2227
+ import { execFileSync as execFileSync5 } from "node:child_process";
2228
+ import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
2229
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
2230
+ import { homedir as homedir3 } from "node:os";
2231
+ import { join as join19 } from "node:path";
2232
+ function signArgs(reportPath, secretKey) {
2233
+ return ["-Sm", reportPath, "-s", secretKey];
2234
+ }
2235
+ function verifyArgs(reportPath, pubKey) {
2236
+ return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
2237
+ }
2238
+ function hasMinisign() {
2239
+ try {
2240
+ execFileSync5("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
2241
+ return true;
2242
+ } catch {
2243
+ return false;
2244
+ }
2245
+ }
2246
+ function defaultKey(flag) {
2247
+ return join19(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
2248
+ }
2249
+ function generatePureKey() {
2250
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
2251
+ const privJwk = privateKey.export({ format: "jwk" });
2252
+ const pubJwk = publicKey.export({ format: "jwk" });
2253
+ return {
2254
+ key: Buffer.from(privJwk.d, "base64url").toString("base64"),
2255
+ pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
2256
+ };
2257
+ }
2258
+ function ensurePureKey(homeDir) {
2259
+ const dir = join19(homeDir, ".mugiwara");
2260
+ const keyPath = join19(dir, "mugiwara.key");
2261
+ const pubPath = join19(dir, "mugiwara.pub");
2262
+ if (!existsSync14(keyPath) || !existsSync14(pubPath)) {
2263
+ mkdirSync9(dir, { recursive: true });
2264
+ const { key, pub } = generatePureKey();
2265
+ if (!existsSync14(keyPath)) {
2266
+ writeFileSync9(keyPath, key + `
2267
+ `, { mode: 384 });
2268
+ }
2269
+ if (!existsSync14(pubPath))
2270
+ writeFileSync9(pubPath, pub + `
2271
+ `);
2272
+ }
2273
+ try {
2274
+ chmodSync2(keyPath, 384);
2275
+ } catch {}
2276
+ return dir;
2277
+ }
2278
+ function pureSign(content, seedBase64, opts) {
2279
+ const seed = Buffer.from(seedBase64.trim(), "base64");
2280
+ if (seed.length !== 32)
2281
+ return { ok: false, message: "invalid seed (want 32B base64)" };
2282
+ const pubBuf = Buffer.from(opts.pub.trim(), "base64");
2283
+ if (pubBuf.length !== 32)
2284
+ return { ok: false, message: "invalid pub (want 32B base64)" };
2285
+ const privateKey = createPrivateKey({
2286
+ key: {
2287
+ kty: "OKP",
2288
+ crv: "Ed25519",
2289
+ d: seed.toString("base64url"),
2290
+ x: pubBuf.toString("base64url")
2291
+ },
2292
+ format: "jwk"
2293
+ });
2294
+ const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
2295
+ const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
2296
+ if (opts.outputPath)
2297
+ writeFileSync9(opts.outputPath, JSON.stringify(out, null, 2) + `
2298
+ `);
2299
+ return { ...out, ok: true };
2300
+ }
2301
+ function pureVerify(content, sig) {
2302
+ try {
2303
+ const pub = Buffer.from(sig.pub, "base64");
2304
+ if (pub.length !== 32)
2305
+ return false;
2306
+ const publicKey = createPublicKey({
2307
+ key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
2308
+ format: "jwk"
2309
+ });
2310
+ return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
2311
+ } catch {
2312
+ return false;
2313
+ }
2314
+ }
2315
+ function resolveBackend(configured, env) {
2316
+ switch (configured) {
2317
+ case "off":
2318
+ return "off";
2319
+ case "minisign":
2320
+ return env.hasMinisign ? "minisign" : "minisign-fail";
2321
+ case "pure":
2322
+ return "pure";
2323
+ case "auto":
2324
+ default:
2325
+ return env.hasMinisign && env.hasKey ? "minisign" : "pure";
2326
+ }
2327
+ }
2328
+ function configuredBackend(projectDir) {
2329
+ return readConfig(projectDir).sign;
2330
+ }
2331
+ function missionMeta(projectDir, mission) {
2332
+ let commit = "unknown";
2333
+ try {
2334
+ commit = execFileSync5("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2335
+ } catch {}
2336
+ return { commit, ts: new Date().toISOString() };
2337
+ }
2338
+ function signReport(projectDir, missionDir) {
2339
+ const report = join19(missionDir, "report.md");
2340
+ if (!existsSync14(report))
2341
+ return { ok: false, message: "no report.md to sign — archive first" };
2342
+ const mission = missionDir.split(join19(".mugiwara", "missions", "")).pop() ?? "unknown";
2343
+ const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync14(defaultKey("secret")) });
2344
+ if (backend === "off")
2345
+ return { ok: false, message: "signing disabled (sign=off)" };
2346
+ if (backend === "minisign-fail")
2347
+ return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
2348
+ if (backend === "minisign") {
2349
+ const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
2350
+ try {
2351
+ execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
2352
+ return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
2353
+ } catch (e) {
2354
+ return { ok: false, message: `signing failed: ${e.message}` };
2355
+ }
2356
+ }
2357
+ const dir = ensurePureKey(homedir3());
2358
+ const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.key"));
2359
+ const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.pub"));
2360
+ if (!seed || !pub)
2361
+ return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
2362
+ const content = readFileSafe(report);
2363
+ if (content === null)
2364
+ return { ok: false, message: `cannot read ${report}` };
2365
+ const { commit, ts } = missionMeta(projectDir, mission);
2366
+ const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
2367
+ if (!sig.ok)
2368
+ return { ok: false, message: `signing failed: ${sig.message}` };
2369
+ return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join19(dir, "mugiwara.key")})` };
2370
+ }
2371
+ function verifyReport(projectDir, missionDir) {
2372
+ const report = join19(missionDir, "report.md");
2373
+ const minisig = `${report}.minisig`;
2374
+ const mugisig = `${report}.mugisig`;
2375
+ if (!existsSync14(minisig) && !existsSync14(mugisig)) {
2376
+ return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
2377
+ }
2378
+ if (existsSync14(minisig)) {
2379
+ if (!hasMinisign())
2380
+ return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
2381
+ const pubKey = existsSync14(defaultKey("public")) ? defaultKey("public") : null;
2382
+ try {
2383
+ execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
2384
+ return { ok: true, message: "signature verifies against report.md (minisig)" };
2385
+ } catch {
2386
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
2387
+ }
2388
+ }
2389
+ try {
2390
+ const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
2391
+ const content = readFileSafe(report);
2392
+ if (content === null || parsed.algo !== "ed25519-pure")
2393
+ return { ok: false, message: "invalid .mugisig file" };
2394
+ return pureVerify(content, parsed) ? { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" } : { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
2395
+ } catch {
2396
+ return { ok: false, message: "invalid .mugisig file" };
2397
+ }
2398
+ }
2399
+ function readFileSafe(p) {
2400
+ try {
2401
+ return readFileSync13(p, "utf8");
2402
+ } catch {
2403
+ return null;
2404
+ }
2405
+ }
2406
+ function readFileSyncSafe2(p) {
2407
+ try {
2408
+ return readFileSync13(p, "utf8").trim();
2409
+ } catch {
2410
+ return "";
2411
+ }
2412
+ }
2413
+
2414
+ // src/slop.ts
2415
+ function detectHealingSlop(input) {
2416
+ const kind = "healing";
2417
+ const max = input.max_cycles ?? 3;
2418
+ const hasZeroHistory = input.history_fixes.some((n) => n === 0);
2419
+ if (input.fixes_in_cycle === 0 && hasZeroHistory) {
2420
+ return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
2421
+ }
2422
+ if (input.cycle >= max && input.fixes_in_cycle === 0) {
2423
+ return { slop: true, reason: `slop: healing — cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
2424
+ }
2425
+ return { slop: false, reason: "no slop — healing making progress", kind };
2426
+ }
2427
+ function computeLiveSlop(input) {
2428
+ const rows = [];
2429
+ const thr = input.repeated_read_threshold ?? 3;
2430
+ const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
2431
+ if (heal.slop)
2432
+ rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
2433
+ if (input.repeated_reads >= thr)
2434
+ rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ≥ ${thr}` });
2435
+ const perRole = {};
2436
+ for (const r of rows)
2437
+ perRole[r.role] = (perRole[r.role] ?? 0) + 1;
2438
+ return { interventions: rows.length, perRole, rows };
2439
+ }
2440
+
1481
2441
  // src/cli.ts
1482
2442
  var str = (v) => typeof v === "string" ? v : undefined;
1483
2443
  var flag = (v) => v === true;
@@ -1489,6 +2449,16 @@ async function run(argv) {
1489
2449
  console.log(`mugiwara ${VERSION}`);
1490
2450
  return;
1491
2451
  }
2452
+ if (command === "continue" || command === "status") {
2453
+ return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
2454
+ }
2455
+ const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
2456
+ if (!isDryRunInstall) {
2457
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
2458
+ if (ensureConfig(projectDir)) {
2459
+ console.log(`default .mugiwara/config written at ${join20(projectDir, ".mugiwara", "config")} (edit it to customise)`);
2460
+ }
2461
+ }
1492
2462
  switch (command) {
1493
2463
  case "install":
1494
2464
  return install(flags);
@@ -1502,20 +2472,24 @@ async function run(argv) {
1502
2472
  return resetCmd(flags);
1503
2473
  case "archive":
1504
2474
  return archive(flags, _);
1505
- case "onboard":
1506
- return onboardCmd(flags);
2475
+ case "clean":
2476
+ return cleanCmd(flags);
1507
2477
  case "continue":
1508
2478
  return continueCmd(flags, _);
1509
2479
  case "status":
1510
2480
  return statusCmd(flags);
2481
+ case "cost":
2482
+ return costCmd(flags, _);
1511
2483
  case "run":
1512
2484
  return runCmd(flags, _);
1513
2485
  case "savepoint":
1514
2486
  return runCmd(flags, ["run", "savepoint.sh", ..._.slice(1)]);
1515
- case "initiative":
1516
- return initiativeCmd(flags, _);
1517
- case "start":
1518
- return startCmd(flags, _);
2487
+ case "blame":
2488
+ return blameCmd(flags, _);
2489
+ case "handoff":
2490
+ return handoffCmd(flags, _);
2491
+ case "sign":
2492
+ return signCmd(flags, _);
1519
2493
  default:
1520
2494
  throw new Error(`Unknown command: ${command}`);
1521
2495
  }
@@ -1545,6 +2519,8 @@ function archive(flags, positionals) {
1545
2519
  const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
1546
2520
  if (result.report)
1547
2521
  console.log(`archive target: ${result.report}`);
2522
+ else
2523
+ console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
1548
2524
  if (result.removed.length)
1549
2525
  console.log(`${flag(flags.dryRun) ? "would remove" : "removed"}: ${result.removed.join(", ")}`);
1550
2526
  if (result.kept.length)
@@ -1552,11 +2528,59 @@ function archive(flags, positionals) {
1552
2528
  if (result.index)
1553
2529
  console.log(`index updated: ${result.index}`);
1554
2530
  }
1555
- async function onboardCmd(flags) {
2531
+ function cleanCmd(flags) {
1556
2532
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1557
- if (!existsSync10(projectDir))
1558
- throw new Error(`Project dir not found: ${projectDir}`);
1559
- await runOnboard(projectDir);
2533
+ const dryRun = flag(flags.dryRun);
2534
+ const root = join20(projectDir, ".mugiwara", "missions");
2535
+ if (!existsSync15(root)) {
2536
+ console.log("nothing to clean (.mugiwara/missions/ does not exist).");
2537
+ return;
2538
+ }
2539
+ const before = str(flags.before);
2540
+ const beforeMs = before ? Date.parse(before) : NaN;
2541
+ if (before && !Number.isFinite(beforeMs)) {
2542
+ console.error(`invalid --before date: ${before}`);
2543
+ process.exit(1);
2544
+ }
2545
+ let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
2546
+ const stateFiles = (m) => readdirSync10(join20(root, m)).filter((f) => {
2547
+ const stem = f.replace(/\.json$/, "");
2548
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
2549
+ });
2550
+ const hasLiveState = (m) => stateFiles(m).length > 0;
2551
+ const staleBefore = (m) => {
2552
+ if (!Number.isFinite(beforeMs))
2553
+ return false;
2554
+ for (const f of stateFiles(m)) {
2555
+ try {
2556
+ const ts = Date.parse(JSON.parse(readFileSync14(join20(root, m, f), "utf8")).updated_at ?? "") || 0;
2557
+ if (ts === 0 || ts >= beforeMs)
2558
+ return false;
2559
+ } catch {
2560
+ return false;
2561
+ }
2562
+ }
2563
+ return true;
2564
+ };
2565
+ if (!flag(flags.all)) {
2566
+ candidates = candidates.filter((m) => existsSync15(join20(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
2567
+ } else if (!flag(flags.force)) {
2568
+ const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
2569
+ if (live.length) {
2570
+ console.error(`✗ in-flight mission(s): ${live.join(", ")}. Use --force to archive them anyway.`);
2571
+ process.exit(1);
2572
+ }
2573
+ }
2574
+ if (!candidates.length) {
2575
+ console.log("nothing to clean.");
2576
+ return;
2577
+ }
2578
+ for (const m of candidates) {
2579
+ const r = archiveMission(projectDir, m, { dryRun });
2580
+ console.log(`${dryRun ? "would clean" : "cleaned"} ${m}${r.report ? ` → ${r.report}` : ""}`);
2581
+ if (r.index)
2582
+ console.log(`index updated: ${r.index}`);
2583
+ }
1560
2584
  }
1561
2585
  async function resolveOptions(flags) {
1562
2586
  const interactive = !flag(flags.yes);
@@ -1573,7 +2597,7 @@ async function resolveOptions(flags) {
1573
2597
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
1574
2598
  }
1575
2599
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1576
- if (scope === "project" && !existsSync10(projectDir))
2600
+ if (scope === "project" && !existsSync15(projectDir))
1577
2601
  throw new Error(`Project dir not found: ${projectDir}`);
1578
2602
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
1579
2603
  if (targetIds && targetIds.includes("all"))
@@ -1598,7 +2622,7 @@ async function resolveOptions(flags) {
1598
2622
  }
1599
2623
  async function install(flags) {
1600
2624
  const { scope, projectDir, targetIds } = await resolveOptions(flags);
1601
- const home = homedir2();
2625
+ const home = homedir4();
1602
2626
  const allFiles = [];
1603
2627
  const allNotes = [];
1604
2628
  const installed = [];
@@ -1640,34 +2664,19 @@ Dry run — nothing written.`);
1640
2664
  OK mugiwara ${VERSION} installed (manifest: ${file})`);
1641
2665
  if (allNotes.length)
1642
2666
  console.log(`${allNotes.length} note(s) above may need attention.`);
1643
- if (scope === "project") {
1644
- const cfg = join11(projectDir, ".mugiwara", "config");
1645
- let mode = "guided", enforce = "block";
1646
- if (existsSync10(cfg)) {
1647
- for (const line of readFileSync8(cfg, "utf8").split(/\r?\n/)) {
1648
- const [k, v] = line.split("=").map((s) => s.trim());
1649
- if (k === "mode")
1650
- mode = v;
1651
- if (k === "enforce")
1652
- enforce = v;
1653
- }
1654
- }
1655
- console.log(`
1656
- Next: run \`mugiwara onboard\` to customise (mode=${mode}, enforce=${enforce} now).`);
1657
- } else {
1658
- console.log("\nNext: run `mugiwara onboard` in a project to write its .mugiwara/config.");
1659
- }
2667
+ console.log(`
2668
+ Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
1660
2669
  }
1661
2670
  async function uninstall(flags) {
1662
2671
  const scope = flag(flags.global) ? "global" : "project";
1663
2672
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1664
- const home = homedir2();
2673
+ const home = homedir4();
1665
2674
  const file = manifestPath({ scope, projectDir, home });
1666
2675
  const manifest = readManifest(file);
1667
2676
  if (!manifest) {
1668
2677
  if (scope === "project") {
1669
2678
  const globalFile = manifestPath({ scope: "global", projectDir, home });
1670
- if (existsSync10(globalFile)) {
2679
+ if (existsSync15(globalFile)) {
1671
2680
  console.log(`No project manifest found, but a global install exists. Try:
1672
2681
  mugiwara uninstall --global`);
1673
2682
  return;
@@ -1704,13 +2713,13 @@ async function uninstall(flags) {
1704
2713
  console.log(` note: ${n}`);
1705
2714
  }
1706
2715
  rmSync3(file);
1707
- const mugiDir = join11(scope === "global" ? home : projectDir, ".mugiwara");
1708
- if (existsSync10(mugiDir) && readdirSync7(mugiDir).length === 0) {
2716
+ const mugiDir = join20(scope === "global" ? home : projectDir, ".mugiwara");
2717
+ if (existsSync15(mugiDir) && readdirSync10(mugiDir).length === 0) {
1709
2718
  rmSync3(mugiDir, { recursive: true, force: true });
1710
2719
  }
1711
2720
  if (manifest.targets.includes("opencode")) {
1712
- const opencodeCache = join11(home, ".cache", "opencode", "packages", "@ionivetech");
1713
- if (existsSync10(opencodeCache)) {
2721
+ const opencodeCache = join20(home, ".cache", "opencode", "packages", "@ionivetech");
2722
+ if (existsSync15(opencodeCache)) {
1714
2723
  rmSync3(opencodeCache, { recursive: true, force: true });
1715
2724
  console.log(" cleared opencode npm cache (stale plugin versions)");
1716
2725
  }
@@ -1719,7 +2728,7 @@ async function uninstall(flags) {
1719
2728
  console.log(`OK removed ${removed.length} files`);
1720
2729
  }
1721
2730
  function list(flags) {
1722
- const home = homedir2();
2731
+ const home = homedir4();
1723
2732
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1724
2733
  let found = false;
1725
2734
  for (const [label, file] of [
@@ -1731,7 +2740,7 @@ function list(flags) {
1731
2740
  continue;
1732
2741
  found = true;
1733
2742
  if (flag(flags.check)) {
1734
- const missing = m.files.filter((f) => !existsSync10(f));
2743
+ const missing = m.files.filter((f) => !existsSync15(f));
1735
2744
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
1736
2745
  } else {
1737
2746
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -1753,6 +2762,10 @@ function continueCmd(flags, positionals) {
1753
2762
  const r = resolveContinue(entries, mission, member);
1754
2763
  if (r.kind === "resume") {
1755
2764
  console.log(formatResume(r.entry));
2765
+ const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
2766
+ const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
2767
+ if (stale)
2768
+ console.log(stale);
1756
2769
  return;
1757
2770
  }
1758
2771
  if (r.kind === "none") {
@@ -1797,6 +2810,58 @@ function statusCmd(flags) {
1797
2810
  console.log(` evidence: ${s.evidence.join(", ")}`);
1798
2811
  }
1799
2812
  }
2813
+ function costCmd(flags, positionals) {
2814
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
2815
+ const mission = str(flags.mission) ?? positionals[1] ?? (() => {
2816
+ const states2 = readState(projectDir);
2817
+ if (states2.length === 1)
2818
+ return states2[0].mission;
2819
+ if (states2.length > 1) {
2820
+ console.error("multiple missions in flight — specify --mission <id>");
2821
+ process.exit(1);
2822
+ }
2823
+ return null;
2824
+ })();
2825
+ if (!mission) {
2826
+ console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
2827
+ process.exit(1);
2828
+ }
2829
+ const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2830
+ if (!existsSync15(missionDir)) {
2831
+ console.error(`No cost ledger found for mission "${mission}"`);
2832
+ process.exit(1);
2833
+ }
2834
+ const states = readState(projectDir).filter((s) => s.mission === mission);
2835
+ const envelope = states.length ? costEnvelope({ lane: states[0].lane, budget: states[0].budget, tokens_est: states[0].tokens_est }) : costEnvelope({ lane: "full", tokens_est: 0 });
2836
+ const state0 = states[0];
2837
+ let repeatedReads = 0;
2838
+ try {
2839
+ const reg = loadRegistry(missionDir);
2840
+ repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
2841
+ } catch {
2842
+ repeatedReads = 0;
2843
+ }
2844
+ const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
2845
+ const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
2846
+ if (flag(flags.json)) {
2847
+ console.log(toCostJSON(ledger));
2848
+ return;
2849
+ }
2850
+ console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
2851
+ console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
2852
+ console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
2853
+ if (ledger.avoided.slop_interventions > 0) {
2854
+ const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(", ");
2855
+ console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
2856
+ }
2857
+ console.log(`Trail: ${ledger.trail.length} decisions`);
2858
+ if (flag(flags.ledger) && ledger.trail.length) {
2859
+ for (const t of ledger.trail.slice(0, 20))
2860
+ console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`);
2861
+ if (ledger.trail.length > 20)
2862
+ console.log(`… ${ledger.trail.length - 20} more`);
2863
+ }
2864
+ }
1800
2865
  function runCmd(flags, positionals) {
1801
2866
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1802
2867
  const name = positionals[1];
@@ -1809,85 +2874,125 @@ function runCmd(flags, positionals) {
1809
2874
  if (code !== 0)
1810
2875
  process.exit(code);
1811
2876
  }
1812
- function initiativeCmd(flags, positionals) {
2877
+ function blameCmd(flags, positionals) {
1813
2878
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1814
- const sub = positionals[1];
1815
- if (!sub) {
1816
- console.error("usage: mugiwara initiative <status|conflict-check|set-status> <plan-file> [--id <id> --status <s>]");
2879
+ const path = positionals[1];
2880
+ if (!path) {
2881
+ console.error("usage: mugiwara blame <file-path>");
1817
2882
  process.exit(1);
1818
2883
  }
1819
- const script = join11(import.meta.dirname, "..", "scripts", "initiative.ts");
1820
- const bun = process.platform === "win32" ? "bun.exe" : "bun";
1821
- const r = spawnSync2(bun, [script, ...positionals.slice(1)], {
1822
- cwd: projectDir,
1823
- stdio: "inherit",
1824
- env: process.env
1825
- });
1826
- if (r.error)
1827
- throw r.error;
1828
- if (r.status)
1829
- process.exit(r.status);
2884
+ console.log(blamePath(projectDir, path));
2885
+ }
2886
+ function stalenessLine(projectDir, baseSha) {
2887
+ if (!baseSha || baseSha === "unknown")
2888
+ return null;
2889
+ const git3 = (args) => {
2890
+ try {
2891
+ return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2892
+ } catch {
2893
+ return "";
2894
+ }
2895
+ };
2896
+ let main = "";
2897
+ for (const ref of ["main", "master"]) {
2898
+ main = git3(["rev-parse", "--verify", ref]);
2899
+ if (main)
2900
+ break;
2901
+ }
2902
+ if (!main)
2903
+ return null;
2904
+ try {
2905
+ const behind = Number(git3(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
2906
+ 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;
2907
+ } catch {
2908
+ return null;
2909
+ }
1830
2910
  }
1831
- function startCmd(flags, positionals) {
2911
+ function handoffCmd(flags, positionals) {
1832
2912
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1833
2913
  const mission = positionals[1];
1834
2914
  if (!mission) {
1835
- console.error("usage: mugiwara start <mission> [member]");
2915
+ console.error("usage: mugiwara handoff <mission> [--project <dir>]");
1836
2916
  process.exit(1);
1837
2917
  }
1838
- const plansDir = join11(projectDir, ".mugiwara", "plans");
1839
- let planFile = "";
1840
- if (existsSync10(plansDir)) {
1841
- const bare = join11(plansDir, `${mission}.md`);
1842
- if (existsSync10(bare))
1843
- planFile = bare;
1844
- else {
1845
- const dated = readdirSync7(plansDir).filter((f) => f.endsWith(`-${mission}.md`)).sort().pop();
1846
- if (dated)
1847
- planFile = join11(plansDir, dated);
1848
- }
1849
- }
1850
- if (!planFile) {
1851
- console.error(`mugiwara start: no plan found for "${mission}" in ${plansDir}`);
2918
+ const states = readState(projectDir).filter((s) => s.mission === mission);
2919
+ if (!states.length) {
2920
+ console.error(`no in-flight mission "${mission}"`);
1852
2921
  process.exit(1);
1853
2922
  }
1854
- const plan = readFileSync8(planFile, "utf8");
1855
- const subMatch = plan.match(/##\s*Sub-missions/i);
1856
- let member = positionals[2] ?? "";
1857
- if (subMatch && !member) {
1858
- const actor = gitActor(projectDir);
1859
- const name = actor.split("<")[0].trim();
1860
- const rowRe = /^\|\s*([\w.-]+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm;
1861
- let m;
1862
- let found = "";
1863
- while (m = rowRe.exec(plan)) {
1864
- const assignee = m[3].trim();
1865
- if (assignee === name || assignee === actor) {
1866
- found = m[1].trim();
1867
- break;
2923
+ const lines = [
2924
+ `# Handoff: ${mission}`,
2925
+ "",
2926
+ `Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
2927
+ "",
2928
+ "| | |",
2929
+ "|---|---|"
2930
+ ];
2931
+ for (const s of states) {
2932
+ const scope = s.member ? ` [${s.member}]` : "";
2933
+ lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? " (rose)" : ""}, mode ${s.mode} |`);
2934
+ lines.push(`| Branch | \`${s.branch}\` |`);
2935
+ lines.push(`| Actor | ${s.actor || "(unknown)"} |`);
2936
+ if (s.next_action)
2937
+ lines.push(`| Next action | ${s.next_action} |`);
2938
+ if (s.blockers_open)
2939
+ lines.push(`| Open blockers | ${s.blockers_open} |`);
2940
+ if (s.heal_cycle)
2941
+ lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? " — HALTED" : ""} |`);
2942
+ if (s.evidence.length)
2943
+ lines.push(`| Evidence | ${s.evidence.join(", ")} |`);
2944
+ const stale = stalenessLine(projectDir, s.base_sha);
2945
+ if (stale)
2946
+ lines.push(`| Staleness | ${stale.replace("⚠ stale base: ", "")} |`);
2947
+ }
2948
+ lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
2949
+ lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
2950
+ const out = join20(".mugiwara", "missions", mission, "handoff.md");
2951
+ writeFileSync10(resolve2(projectDir, out), lines.join(`
2952
+ `) + `
2953
+ `);
2954
+ console.log(lines.join(`
2955
+ `));
2956
+ console.log(`
2957
+ written: ${out}`);
2958
+ }
2959
+ function signCmd(flags, _) {
2960
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
2961
+ if (flag(flags.genKey)) {
2962
+ const backend = str(flags.backend) ?? "auto";
2963
+ const home = homedir4();
2964
+ if (backend === "minisign") {
2965
+ if (!hasMinisign()) {
2966
+ console.error("✗ minisign not installed — cannot generate keys with this backend");
2967
+ process.exit(1);
2968
+ }
2969
+ try {
2970
+ execFileSync6("minisign", ["-G"], { stdio: "inherit" });
2971
+ console.log("✓ minisign key pair generated in ~/.mugiwara/");
2972
+ return;
2973
+ } catch {
2974
+ console.error("✗ key generation failed");
2975
+ process.exit(1);
1868
2976
  }
1869
2977
  }
1870
- if (!found) {
1871
- console.error(`mugiwara start: no sub-mission assigned to "${name}" in ${planFile}. Pass a member explicitly: mugiwara start ${mission} <member>`);
1872
- process.exit(1);
1873
- }
1874
- member = found;
2978
+ const dir = ensurePureKey(home);
2979
+ console.log(`✓ pure ed25519 key pair ready: ${join20(dir, "mugiwara.key")} / ${join20(dir, "mugiwara.pub")}`);
2980
+ return;
1875
2981
  }
1876
- const code = runScript("savepoint.sh", [mission, member, "0", "auto", "full"], projectDir);
1877
- if (code !== 0)
1878
- process.exit(code);
1879
- if (subMatch && member) {
1880
- const init = join11(import.meta.dirname, "..", "scripts", "initiative.ts");
1881
- const bun = process.platform === "win32" ? "bun.exe" : "bun";
1882
- const r = spawnSync2(bun, [init, "set-status", planFile, "--id", member, "--status", "in-progress"], {
1883
- cwd: projectDir,
1884
- stdio: "inherit",
1885
- env: process.env
1886
- });
1887
- if (r.status)
1888
- process.exit(r.status);
2982
+ const mission = _[1];
2983
+ if (!mission) {
2984
+ console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
2985
+ process.exit(1);
1889
2986
  }
1890
- console.log(`mugiwara start: ${mission}${member ? ` [${member}]` : ""} begun at Flow 0. Resume anytime with: mugiwara continue ${mission}${member ? ` ${member}` : ""}`);
2987
+ const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2988
+ if (!existsSync15(missionDir)) {
2989
+ console.error(`no mission dir: ${missionDir}`);
2990
+ process.exit(1);
2991
+ }
2992
+ const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
2993
+ console.log(`${r.ok ? "✓" : "✗"} ${r.message}`);
2994
+ if (!r.ok)
2995
+ process.exit(1);
1891
2996
  }
1892
2997
  function help() {
1893
2998
  console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
@@ -1898,21 +3003,27 @@ Usage:
1898
3003
  mugiwara uninstall remove installed files via manifest
1899
3004
  mugiwara list show installations
1900
3005
  mugiwara list --check health check: show installations + missing files
1901
- mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
1902
- mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
1903
- mugiwara onboard run the zero-LLM onboarding wizard (writes .mugiwara/config)
3006
+ mugiwara reset wipe mission state (missions/ + legacy dirs)
3007
+ mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
3008
+ mugiwara clean [--all] [--before <date>]
3009
+ batch-archive every closed mission (report.md present, no live state)
1904
3010
  mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
1905
3011
  mugiwara continue <m> [member]
1906
3012
  print the exact resume point for that mission/member
1907
3013
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
3014
+ mugiwara cost [--mission <id>] [--json] [--ledger]
3015
+ show cost ledger, avoided work, efficiency, trail (human + JSON)
3016
+ mugiwara blame <path> provenance note on the last commit touching <path>
3017
+ (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
3018
+ mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
3019
+ engineer can act on (computed state + staleness check)
3020
+ mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
3021
+ mugiwara sign --gen-key [--backend pure|minisign]
3022
+ create signing keys (pure ed25519 default)
1908
3023
  mugiwara run <script> [args...]
1909
3024
  run a bundled harness script here (${RUNNABLE.join(", ")})
1910
- mugiwara savepoint <mission> [member] [wave] [mode]
3025
+ mugiwara savepoint <mission> [member] [flow] [mode]
1911
3026
  shorthand for: mugiwara run savepoint.sh ...
1912
- mugiwara initiative <status|conflict-check|set-status> <plan> [args]
1913
- team sub-mission coordination (status / conflict-check / set-status)
1914
- mugiwara start <mission> [member]
1915
- begin a mission from its plan (solo, or team sub-mission by git actor)
1916
3027
  mugiwara --help this help
1917
3028
  mugiwara --version print version
1918
3029
 
@@ -1924,8 +3035,10 @@ Flags:
1924
3035
  --force overwrite differing files (with backup)
1925
3036
  --dry-run print actions without writing
1926
3037
  --check with list: report missing files (health check)
1927
- --all with continue/status: every actor, not just yours
1928
- --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
3038
+ --all with continue/status: every actor; with clean: include in-flight missions
3039
+ --force with clean --all: archive in-flight missions anyway
3040
+ --before <date> with clean: also archive missions untouched since this date
3041
+ --keep-logs with reset: keep lessons.md (lessons ledger survives)`);
1929
3042
  }
1930
3043
  var isMain = process.argv[1] !== undefined && fileURLToPath5(import.meta.url) === resolve2(process.argv[1]);
1931
3044
  if (isMain) {
@@ -1935,5 +3048,6 @@ if (isMain) {
1935
3048
  });
1936
3049
  }
1937
3050
  export {
1938
- run
3051
+ run,
3052
+ stalenessLine
1939
3053
  };