@ionivetech/mugiwara 0.7.0 → 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 (70) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/README.md +194 -328
  8. package/content/agents/franky-gates.md +1 -1
  9. package/content/agents/luffy-orchestrator.md +2 -2
  10. package/content/skills/mugiwara-backend/SKILL.md +52 -43
  11. package/content/skills/mugiwara-checkpoint/SKILL.md +19 -8
  12. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  13. package/content/skills/mugiwara-execution/SKILL.md +32 -32
  14. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  15. package/content/skills/mugiwara-frontend/SKILL.md +44 -44
  16. package/content/skills/mugiwara-gates/SKILL.md +22 -16
  17. package/content/skills/mugiwara-healing/SKILL.md +26 -25
  18. package/content/skills/mugiwara-orchestration/SKILL.md +6 -6
  19. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  20. package/content/skills/mugiwara-planning/SKILL.md +26 -14
  21. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  22. package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
  23. package/content/skills/mugiwara-quality/SKILL.md +19 -13
  24. package/content/skills/mugiwara-resume/SKILL.md +6 -1
  25. package/content/skills/mugiwara-review/SKILL.md +17 -12
  26. package/content/skills/mugiwara-security/SKILL.md +46 -35
  27. package/content/skills/mugiwara-workflow/SKILL.md +6 -9
  28. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +5 -0
  29. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +53 -0
  30. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +5 -0
  31. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  32. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +14 -0
  33. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +14 -0
  34. package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
  35. package/dist/mugiwara.js +925 -253
  36. package/gemini-extension.json +1 -1
  37. package/hooks/pipeline-guard.js +1 -1
  38. package/hooks/pipeline-guard.ts +2 -1
  39. package/package.json +2 -2
  40. package/plugin.json +1 -1
  41. package/references/multi-actor.md +21 -0
  42. package/references/posture-routing.md +31 -0
  43. package/scripts/benchmark-governor.ts +516 -0
  44. package/scripts/benchmark-thresholds.json +47 -0
  45. package/scripts/check-doc-links.ts +8 -2
  46. package/scripts/gate-selftest.ts +20 -0
  47. package/scripts/lib/lane-base.sh +4 -4
  48. package/scripts/retrieval-eval.ts +9 -3
  49. package/scripts/savepoint.sh +20 -1
  50. package/scripts/validate-content.ts +22 -3
  51. package/src/adaptive-budget.ts +178 -0
  52. package/src/args.ts +3 -2
  53. package/src/budget.ts +7 -16
  54. package/src/check-artifacts.ts +45 -0
  55. package/src/cli.ts +102 -4
  56. package/src/cognition.ts +234 -0
  57. package/src/config.ts +107 -0
  58. package/src/context.ts +72 -0
  59. package/src/cost.ts +186 -0
  60. package/src/evidence.ts +160 -0
  61. package/src/installer.ts +2 -16
  62. package/src/integrity.ts +1 -1
  63. package/src/investigation.ts +72 -0
  64. package/src/mission.ts +124 -10
  65. package/src/posture.ts +86 -0
  66. package/src/reporting.ts +225 -0
  67. package/src/scope.ts +321 -0
  68. package/src/sign.ts +194 -20
  69. package/src/slop.ts +306 -0
  70. package/src/work.ts +273 -0
package/dist/mugiwara.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
4
+ import { existsSync as existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync10 } from "node:fs";
5
5
  import { execFileSync as execFileSync6 } from "node:child_process";
6
6
  import { homedir as homedir4 } from "node:os";
7
- import { join as join15, resolve as resolve2 } from "node:path";
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", "--before": "before" };
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",
@@ -19,10 +19,13 @@ var BOOL_FLAGS = {
19
19
  "--check": "check",
20
20
  "--all": "all",
21
21
  "--verify": "verify",
22
+ "--gen-key": "genKey",
22
23
  "--help": "help",
23
24
  "-h": "help",
24
25
  "--version": "version",
25
- "-v": "version"
26
+ "-v": "version",
27
+ "--json": "json",
28
+ "--ledger": "ledger"
26
29
  };
27
30
  function parseArgs(argv) {
28
31
  const out = { _: [], flags: {}, command: "install" };
@@ -620,50 +623,118 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
620
623
  var TARGET_IDS = Object.keys(targets);
621
624
 
622
625
  // src/installer.ts
623
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync3, copyFileSync as copyFileSync3, rmSync, lstatSync } from "node:fs";
624
- import { dirname as dirname3, join as join5 } from "node:path";
625
- 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";
626
629
  import { fileURLToPath as fileURLToPath3 } from "node:url";
627
- var CONTENT_DIR = join5(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
628
- 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"));
629
700
  var VERSION = pkg.version;
630
701
  function collectContent() {
631
- 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);
632
703
  const skills = skillNames.map((name) => {
633
- const { data, body } = parseFrontmatter(readFileSync3(join5(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
634
- 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)) };
635
706
  });
636
- const agents = readdirSync3(join5(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
637
- 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"));
638
709
  return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
639
710
  });
640
- const sharedRefsDir = join5(dirname3(CONTENT_DIR), "references");
711
+ const sharedRefsDir = join6(dirname3(CONTENT_DIR), "references");
641
712
  const sharedRefs = [];
642
- if (existsSync4(sharedRefsDir)) {
713
+ if (existsSync5(sharedRefsDir)) {
643
714
  for (const f of readdirSync3(sharedRefsDir)) {
644
715
  if (!f.endsWith(".md"))
645
716
  continue;
646
- sharedRefs.push({ relPath: f, text: readFileSync3(join5(sharedRefsDir, f), "utf8") });
717
+ sharedRefs.push({ relPath: f, text: readFileSync4(join6(sharedRefsDir, f), "utf8") });
647
718
  }
648
719
  }
649
720
  return { skills, agents, sharedRefs };
650
721
  }
651
722
  function collectRefs(skillDir) {
652
- const refsDir = join5(skillDir, "references");
653
- if (!existsSync4(refsDir))
723
+ const refsDir = join6(skillDir, "references");
724
+ if (!existsSync5(refsDir))
654
725
  return [];
655
- 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") }));
656
727
  }
657
728
  function installTo(target10, opts) {
658
729
  const { scope, projectDir, dryRun = false, force = false } = opts;
659
- const home = opts.home ?? homedir();
730
+ const home = opts.home ?? homedir2();
660
731
  const { skills, agents, sharedRefs } = collectContent();
661
732
  const dirs = target10.paths({ scope, projectDir, home });
662
- const backupRoot = join5(scope === "global" ? home : projectDir, ".mugiwara");
733
+ const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
663
734
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
664
735
  const writeOne = (absPath, text) => {
665
- if (existsSync4(absPath)) {
666
- if (readFileSync3(absPath, "utf8") === text) {
736
+ if (existsSync5(absPath)) {
737
+ if (readFileSync4(absPath, "utf8") === text) {
667
738
  result.skipped.push(absPath);
668
739
  return;
669
740
  }
@@ -674,17 +745,17 @@ function installTo(target10, opts) {
674
745
  }
675
746
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
676
747
  const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
677
- const backupDir = join5(backupRoot, "backup", `${ts}-${target10.id}`);
678
- const backupFile = join5(backupDir, fileName);
748
+ const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
749
+ const backupFile = join6(backupDir, fileName);
679
750
  if (!dryRun) {
680
- mkdirSync3(backupDir, { recursive: true });
751
+ mkdirSync4(backupDir, { recursive: true });
681
752
  copyFileSync3(absPath, backupFile);
682
753
  }
683
754
  result.backedUp.push(absPath);
684
755
  }
685
756
  if (!dryRun) {
686
- mkdirSync3(dirname3(absPath), { recursive: true });
687
- writeFileSync3(absPath, text);
757
+ mkdirSync4(dirname3(absPath), { recursive: true });
758
+ writeFileSync4(absPath, text);
688
759
  }
689
760
  result.written.push(absPath);
690
761
  };
@@ -695,7 +766,7 @@ function installTo(target10, opts) {
695
766
  if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
696
767
  text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
697
768
  }
698
- writeOne(join5(dirs.skillsDir, out.relPath), text);
769
+ writeOne(join6(dirs.skillsDir, out.relPath), text);
699
770
  }
700
771
  if (target10.transformSkillFull) {
701
772
  const full = target10.transformSkillFull(s.data, s.body);
@@ -704,29 +775,29 @@ function installTo(target10, opts) {
704
775
  if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
705
776
  text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
706
777
  }
707
- 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);
708
779
  }
709
780
  }
710
781
  if (s.refs.length && target10.refsDir) {
711
782
  const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
712
783
  for (const r of s.refs)
713
- writeOne(join5(refsRoot, r.relPath), r.text);
784
+ writeOne(join6(refsRoot, r.relPath), r.text);
714
785
  }
715
786
  }
716
787
  for (const a of agents) {
717
788
  const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
718
789
  if (out)
719
- writeOne(join5(dirs.agentsDir, out.relPath), out.text);
790
+ writeOne(join6(dirs.agentsDir, out.relPath), out.text);
720
791
  if (target10.transformAgentFull) {
721
792
  const full = target10.transformAgentFull(a.data, a.body);
722
793
  if (full && target10.refsDir)
723
- 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);
724
795
  }
725
796
  }
726
797
  if (sharedRefs.length) {
727
- const sharedRoot = join5(dirs.skillsDir, "_shared", "references");
798
+ const sharedRoot = join6(dirs.skillsDir, "_shared", "references");
728
799
  for (const r of sharedRefs)
729
- writeOne(join5(sharedRoot, r.relPath), r.text);
800
+ writeOne(join6(sharedRoot, r.relPath), r.text);
730
801
  }
731
802
  if (target10.postInstall) {
732
803
  const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
@@ -734,34 +805,17 @@ function installTo(target10, opts) {
734
805
  result.notes.push(...post.notes);
735
806
  }
736
807
  if (scope === "project") {
737
- const configPath = join5(projectDir, ".mugiwara", "config");
808
+ const configPath = join6(projectDir, ".mugiwara", "config");
738
809
  let configExists = false;
739
810
  try {
740
- configExists = lstatSync(configPath).isFile() || lstatSync(configPath).isSymbolicLink();
811
+ configExists = lstatSync2(configPath).isFile() || lstatSync2(configPath).isSymbolicLink();
741
812
  } catch {
742
813
  configExists = false;
743
814
  }
744
815
  if (!configExists) {
745
- const body = [
746
- "mode=guided",
747
- "branch=feature/{type}-{issue}-{slug}",
748
- "commit=conventional",
749
- "auto_commit=on",
750
- "coverage_new=90",
751
- "coverage_modified=80",
752
- "review_depth=full",
753
- "quality_depth=full",
754
- "verify_merged=off",
755
- "delegate_threshold=60",
756
- "heal_max_cycles=3",
757
- "verbosity=normal",
758
- "# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)"
759
- ].join(`
760
- `) + `
761
- `;
762
816
  if (!dryRun) {
763
- mkdirSync3(dirname3(configPath), { recursive: true });
764
- writeFileSync3(configPath, body);
817
+ mkdirSync4(dirname3(configPath), { recursive: true });
818
+ writeFileSync4(configPath, DEFAULT_CONFIG);
765
819
  }
766
820
  result.written.push(configPath);
767
821
  result.notes.push(`default config written: ${configPath} (edit it to customise)`);
@@ -772,7 +826,7 @@ function installTo(target10, opts) {
772
826
  function removeInstalled(manifest, { dryRun = false } = {}) {
773
827
  const removed = [];
774
828
  for (const f of manifest.files) {
775
- if (existsSync4(f)) {
829
+ if (existsSync5(f)) {
776
830
  if (!dryRun)
777
831
  rmSync(f);
778
832
  removed.push(f);
@@ -781,7 +835,7 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
781
835
  if (!dryRun) {
782
836
  for (const f of manifest.files) {
783
837
  let d = dirname3(f);
784
- while (existsSync4(d) && readdirSync3(d).length === 0) {
838
+ while (existsSync5(d) && readdirSync3(d).length === 0) {
785
839
  if (d.endsWith("/.mugiwara") || d === dirname3(d))
786
840
  break;
787
841
  rmSync(d, { recursive: true, force: true });
@@ -792,10 +846,10 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
792
846
  return removed;
793
847
  }
794
848
  function assertNotSymlink(file) {
795
- if (!existsSync4(file))
849
+ if (!existsSync5(file))
796
850
  return;
797
851
  try {
798
- if (lstatSync(file).isSymbolicLink())
852
+ if (lstatSync2(file).isSymbolicLink())
799
853
  throw new Error(`refusing to follow symlink: ${file}`);
800
854
  } catch (e) {
801
855
  if (e.code === "ENOENT")
@@ -816,9 +870,9 @@ var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit
816
870
  `;
817
871
  function ensureProjectGitignore(projectDir, opts = {}) {
818
872
  const { dryRun = false } = opts;
819
- const path = join5(projectDir, ".gitignore");
873
+ const path = join6(projectDir, ".gitignore");
820
874
  assertNotSymlink(path);
821
- let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
875
+ let existing = existsSync5(path) ? readFileSync4(path, "utf8") : "";
822
876
  if (existing) {
823
877
  if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
824
878
  return { appended: false, notes: [] };
@@ -826,24 +880,24 @@ function ensureProjectGitignore(projectDir, opts = {}) {
826
880
  const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
827
881
  if (hadOld) {
828
882
  const clean = removeProjectGitignore(projectDir, { dryRun });
829
- existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
883
+ existing = clean.removed ? existsSync5(path) ? readFileSync4(path, "utf8") : "" : existing;
830
884
  }
831
885
  }
832
886
  const separator = existing.length && !existing.endsWith(`
833
887
  `) ? `
834
888
  ` : "";
835
889
  if (!dryRun) {
836
- mkdirSync3(dirname3(path), { recursive: true });
837
- writeFileSync3(path, existing + separator + GITIGNORE_BLOCK);
890
+ mkdirSync4(dirname3(path), { recursive: true });
891
+ writeFileSync4(path, existing + separator + GITIGNORE_BLOCK);
838
892
  }
839
893
  return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
840
894
  }
841
895
  function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
842
- const path = join5(projectDir, ".gitignore");
896
+ const path = join6(projectDir, ".gitignore");
843
897
  assertNotSymlink(path);
844
- if (!existsSync4(path))
898
+ if (!existsSync5(path))
845
899
  return { removed: false, notes: [] };
846
- const current = readFileSync3(path, "utf8");
900
+ const current = readFileSync4(path, "utf8");
847
901
  const delimited = current.includes(GITIGNORE_BLOCK_START);
848
902
  let cleaned;
849
903
  if (delimited) {
@@ -883,33 +937,33 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
883
937
  if (cleaned === current)
884
938
  return { removed: false, notes: [] };
885
939
  if (!dryRun)
886
- writeFileSync3(path, cleaned);
940
+ writeFileSync4(path, cleaned);
887
941
  return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
888
942
  }
889
943
 
890
944
  // src/manifest.ts
891
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
892
- 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";
893
947
  function manifestPath({ scope, projectDir, home }) {
894
- 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");
895
949
  }
896
950
  function readManifest(file) {
897
- return existsSync5(file) ? JSON.parse(readFileSync4(file, "utf8")) : null;
951
+ return existsSync6(file) ? JSON.parse(readFileSync5(file, "utf8")) : null;
898
952
  }
899
953
  function writeManifest(file, data) {
900
- mkdirSync4(dirname4(file), { recursive: true });
901
- writeFileSync4(file, JSON.stringify(data, null, 2) + `
954
+ mkdirSync5(dirname4(file), { recursive: true });
955
+ writeFileSync5(file, JSON.stringify(data, null, 2) + `
902
956
  `);
903
957
  }
904
958
 
905
959
  // src/mission.ts
906
- import { existsSync as existsSync8, rmSync as rmSync2, readFileSync as readFileSync7, readdirSync as readdirSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7, renameSync, openSync, writeSync, closeSync } from "node:fs";
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";
907
961
  import { execFileSync as execFileSync3 } from "node:child_process";
908
- import { join as join11 } from "node:path";
962
+ import { join as join16 } from "node:path";
909
963
 
910
964
  // src/integrity.ts
911
- import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
912
- import { isAbsolute, join as join7, relative } from "node:path";
965
+ import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
966
+ import { isAbsolute, join as join8, relative } from "node:path";
913
967
  var SECRET_PATTERNS = [
914
968
  [/AKIA[0-9A-Z]{16}/, "AWS access key id"],
915
969
  [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
@@ -933,12 +987,12 @@ function findSecrets(body) {
933
987
  }
934
988
  return out;
935
989
  }
936
- var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
990
+ var TRAIL_EXTS = new Set([".md", ".json", ".sh", ".jsonl"]);
937
991
  function trailFiles(dir) {
938
992
  const out = [];
939
993
  const walk = (d) => {
940
994
  for (const e of readdirSync4(d, { withFileTypes: true })) {
941
- const p = join7(d, e.name);
995
+ const p = join8(d, e.name);
942
996
  if (e.isDirectory())
943
997
  walk(p);
944
998
  else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
@@ -966,7 +1020,7 @@ function collectPassCitedPaths(missionDir) {
966
1020
  for (const f of trailFiles(missionDir)) {
967
1021
  let body;
968
1022
  try {
969
- body = readFileSync5(f, "utf8");
1023
+ body = readFileSync6(f, "utf8");
970
1024
  } catch {
971
1025
  continue;
972
1026
  }
@@ -990,16 +1044,16 @@ function checkTrail(missionDir, projectRoot) {
990
1044
  for (const f of files) {
991
1045
  let body;
992
1046
  try {
993
- body = readFileSync5(f, "utf8");
1047
+ body = readFileSync6(f, "utf8");
994
1048
  } catch {
995
1049
  continue;
996
1050
  }
997
1051
  for (const target10 of linkedPaths(body)) {
998
1052
  if (isAbsolute(target10))
999
1053
  continue;
1000
- const fromMission = join7(missionDir, target10);
1001
- const fromRoot = join7(projectRoot, target10);
1002
- if (!existsSync6(fromMission) && !existsSync6(fromRoot)) {
1054
+ const fromMission = join8(missionDir, target10);
1055
+ const fromRoot = join8(projectRoot, target10);
1056
+ if (!existsSync7(fromMission) && !existsSync7(fromRoot)) {
1003
1057
  issues.push({
1004
1058
  kind: "dangling-path",
1005
1059
  detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
@@ -1014,17 +1068,17 @@ function checkTrail(missionDir, projectRoot) {
1014
1068
  }
1015
1069
  }
1016
1070
  const evidencePaths = [];
1017
- const evidenceFile = join7(missionDir, "state.json");
1018
- if (existsSync6(evidenceFile)) {
1071
+ const evidenceFile = join8(missionDir, "state.json");
1072
+ if (existsSync7(evidenceFile)) {
1019
1073
  try {
1020
- const s = JSON.parse(readFileSync5(evidenceFile, "utf8"));
1074
+ const s = JSON.parse(readFileSync6(evidenceFile, "utf8"));
1021
1075
  if (Array.isArray(s.evidence)) {
1022
1076
  for (const e of s.evidence) {
1023
1077
  if (typeof e !== "string" || !e.trim())
1024
1078
  continue;
1025
1079
  evidencePaths.push(e);
1026
- const cand = join7(projectRoot, e);
1027
- if (!isAbsolute(e) && !existsSync6(cand) && !existsSync6(join7(missionDir, e))) {
1080
+ const cand = join8(projectRoot, e);
1081
+ if (!isAbsolute(e) && !existsSync7(cand) && !existsSync7(join8(missionDir, e))) {
1028
1082
  issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1029
1083
  }
1030
1084
  }
@@ -1035,14 +1089,14 @@ function checkTrail(missionDir, projectRoot) {
1035
1089
  for (const e of passCited) {
1036
1090
  if (!e.trim() || isAbsolute(e))
1037
1091
  continue;
1038
- const candMission = join7(missionDir, e);
1039
- const candRoot = join7(projectRoot, e);
1040
- const resolved = existsSync6(candMission) ? candMission : existsSync6(candRoot) ? candRoot : null;
1092
+ const candMission = join8(missionDir, e);
1093
+ const candRoot = join8(projectRoot, e);
1094
+ const resolved = existsSync7(candMission) ? candMission : existsSync7(candRoot) ? candRoot : null;
1041
1095
  if (!resolved)
1042
1096
  continue;
1043
1097
  let body;
1044
1098
  try {
1045
- body = readFileSync5(resolved, "utf8");
1099
+ body = readFileSync6(resolved, "utf8");
1046
1100
  } catch {
1047
1101
  continue;
1048
1102
  }
@@ -1057,10 +1111,45 @@ function formatIssues(issues) {
1057
1111
  `);
1058
1112
  }
1059
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
+
1060
1149
  // src/rollback.ts
1061
1150
  import { execFileSync } from "node:child_process";
1062
- import { writeFileSync as writeFileSync5 } from "node:fs";
1063
- import { join as join8 } from "node:path";
1151
+ import { writeFileSync as writeFileSync6 } from "node:fs";
1152
+ import { join as join10 } from "node:path";
1064
1153
  function git(cwd, args) {
1065
1154
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1066
1155
  }
@@ -1099,8 +1188,8 @@ function generateRollback(projectDir, missionDir, input) {
1099
1188
  const commitsNewestFirst = [...revList].reverse();
1100
1189
  const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
1101
1190
  const body = buildRollback(input, commitsNewestFirst, filesTouched);
1102
- const file = join8(missionDir, "rollback.sh");
1103
- writeFileSync5(file, body, { mode: 493 });
1191
+ const file = join10(missionDir, "rollback.sh");
1192
+ writeFileSync6(file, body, { mode: 493 });
1104
1193
  return { file: "rollback.sh", commits: commitsNewestFirst.length };
1105
1194
  } catch {
1106
1195
  return null;
@@ -1109,8 +1198,8 @@ function generateRollback(projectDir, missionDir, input) {
1109
1198
 
1110
1199
  // src/provenance.ts
1111
1200
  import { execFileSync as execFileSync2 } from "node:child_process";
1112
- import { writeFileSync as writeFileSync6 } from "node:fs";
1113
- import { join as join9 } from "node:path";
1201
+ import { writeFileSync as writeFileSync7 } from "node:fs";
1202
+ import { join as join11 } from "node:path";
1114
1203
  function git2(cwd, args) {
1115
1204
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1116
1205
  }
@@ -1185,7 +1274,7 @@ ${note}`;
1185
1274
  function writeProvenance(projectDir, missionDir, state) {
1186
1275
  const note = buildNote(state);
1187
1276
  const attached = attachGitNote(projectDir, state.branch, note);
1188
- writeFileSync6(join9(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1277
+ writeFileSync7(join11(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1189
1278
  }
1190
1279
 
1191
1280
  // src/routing.ts
@@ -1242,28 +1331,15 @@ function renderRouting(ranked, mission) {
1242
1331
  }
1243
1332
 
1244
1333
  // src/budget.ts
1245
- import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync } from "node:fs";
1246
- import { homedir as homedir2 } from "node:os";
1247
- import { join as join10 } from "node:path";
1334
+ import { existsSync as existsSync9, readdirSync as readdirSync6, statSync } from "node:fs";
1335
+ import { join as join12 } from "node:path";
1248
1336
  function readBudgetConfig(projectDir) {
1249
- for (const base of [projectDir, homedir2()]) {
1250
- const file = join10(base, ".mugiwara", "config");
1251
- if (!existsSync7(file))
1252
- continue;
1253
- for (const line of readFileSync6(file, "utf8").split(/\r?\n/)) {
1254
- const t = line.trim();
1255
- if (!t || t.startsWith("#"))
1256
- continue;
1257
- const eq = t.indexOf("=");
1258
- if (eq === -1)
1259
- continue;
1260
- if (t.slice(0, eq).trim() !== "context_budget_chars")
1261
- continue;
1262
- const n = Number(t.slice(eq + 1).trim());
1263
- return Number.isFinite(n) && n > 0 ? n : 0;
1264
- }
1265
- }
1266
- return 0;
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;
1267
1343
  }
1268
1344
  function measureContextChars(missionDir) {
1269
1345
  let total = 0;
@@ -1272,15 +1348,15 @@ function measureContextChars(missionDir) {
1272
1348
  total += statSync(p).size;
1273
1349
  } catch {}
1274
1350
  };
1275
- for (const f of readdirSync5(missionDir)) {
1351
+ for (const f of readdirSync6(missionDir)) {
1276
1352
  if (/\.md$/.test(f))
1277
- add(join10(missionDir, f));
1353
+ add(join12(missionDir, f));
1278
1354
  }
1279
1355
  for (const sub of ["flows", "waves"]) {
1280
- const dir = join10(missionDir, sub);
1281
- if (existsSync7(dir)) {
1282
- for (const f of readdirSync5(dir))
1283
- add(join10(dir, f));
1356
+ const dir = join12(missionDir, sub);
1357
+ if (existsSync9(dir)) {
1358
+ for (const f of readdirSync6(dir))
1359
+ add(join12(dir, f));
1284
1360
  }
1285
1361
  }
1286
1362
  return total;
@@ -1292,6 +1368,274 @@ function formatFootprint(chars, budget) {
1292
1368
  return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
1293
1369
  }
1294
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
+
1295
1639
  // src/mission.ts
1296
1640
  function isStateFile(f) {
1297
1641
  const stem = f.replace(/\.json$/, "");
@@ -1302,7 +1646,7 @@ function primaryState(dir, files) {
1302
1646
  if (!name)
1303
1647
  return null;
1304
1648
  try {
1305
- return JSON.parse(readFileSync7(join11(dir, name), "utf8"));
1649
+ return JSON.parse(readFileSync11(join16(dir, name), "utf8"));
1306
1650
  } catch {
1307
1651
  return null;
1308
1652
  }
@@ -1323,16 +1667,16 @@ function changedFiles(projectDir, state) {
1323
1667
  }
1324
1668
  }
1325
1669
  function activeActor(projectDir) {
1326
- const missionsDir = join11(projectDir, ".mugiwara", "missions");
1327
- if (!existsSync8(missionsDir))
1670
+ const missionsDir = join16(projectDir, ".mugiwara", "missions");
1671
+ if (!existsSync11(missionsDir))
1328
1672
  return null;
1329
- const missions = readdirSync6(missionsDir, { 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);
1330
1674
  let latest = null;
1331
1675
  for (const mission of missions) {
1332
- const d = join11(missionsDir, mission);
1333
- for (const f of readdirSync6(d).filter(isStateFile)) {
1676
+ const d = join16(missionsDir, mission);
1677
+ for (const f of readdirSync7(d).filter(isStateFile)) {
1334
1678
  try {
1335
- const s = JSON.parse(readFileSync7(join11(d, f), "utf8"));
1679
+ const s = JSON.parse(readFileSync11(join16(d, f), "utf8"));
1336
1680
  const t = Date.parse(s.updated_at || "") || 0;
1337
1681
  if (!latest || t > latest.updated)
1338
1682
  latest = { actor: s.actor || null, updated: t };
@@ -1342,8 +1686,8 @@ function activeActor(projectDir) {
1342
1686
  return latest ? latest.actor : null;
1343
1687
  }
1344
1688
  function resetMission(projectDir, keepLogs, force) {
1345
- const root = join11(projectDir, ".mugiwara");
1346
- if (!existsSync8(root))
1689
+ const root = join16(projectDir, ".mugiwara");
1690
+ if (!existsSync11(root))
1347
1691
  return { removed: [], kept: [] };
1348
1692
  if (!force) {
1349
1693
  const actor = activeActor(projectDir);
@@ -1354,49 +1698,49 @@ function resetMission(projectDir, keepLogs, force) {
1354
1698
  const removed = [];
1355
1699
  const kept = [];
1356
1700
  for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
1357
- const p = join11(root, dir);
1358
- if (existsSync8(p)) {
1701
+ const p = join16(root, dir);
1702
+ if (existsSync11(p)) {
1359
1703
  rmSync2(p, { recursive: true, force: true });
1360
1704
  removed.push(dir);
1361
1705
  }
1362
1706
  }
1363
- for (const f of readdirSync6(root)) {
1707
+ for (const f of readdirSync7(root)) {
1364
1708
  if (/^state(-.+)?\.json$/.test(f)) {
1365
- const p = join11(root, f);
1366
- if (existsSync8(p)) {
1709
+ const p = join16(root, f);
1710
+ if (existsSync11(p)) {
1367
1711
  rmSync2(p);
1368
1712
  removed.push(f);
1369
1713
  }
1370
1714
  }
1371
1715
  }
1372
1716
  if (!keepLogs) {
1373
- for (const p of [join11(root, "lessons.md"), join11(root, "logs")]) {
1374
- if (existsSync8(p)) {
1717
+ for (const p of [join16(root, "lessons.md"), join16(root, "logs")]) {
1718
+ if (existsSync11(p)) {
1375
1719
  rmSync2(p, { recursive: true, force: true });
1376
- removed.push(p.startsWith(join11(root, "logs")) ? "logs" : "lessons.md");
1720
+ removed.push(p.startsWith(join16(root, "logs")) ? "logs" : "lessons.md");
1377
1721
  }
1378
1722
  }
1379
1723
  } else {
1380
- if (existsSync8(join11(root, "lessons.md")))
1724
+ if (existsSync11(join16(root, "lessons.md")))
1381
1725
  kept.push("lessons.md");
1382
- else if (existsSync8(join11(root, join11("logs", "lessons.md"))))
1383
- kept.push(join11("logs", "lessons.md"));
1726
+ else if (existsSync11(join16(root, join16("logs", "lessons.md"))))
1727
+ kept.push(join16("logs", "lessons.md"));
1384
1728
  }
1385
1729
  for (const f of ["config", "manifest.json", "backup"]) {
1386
- if (existsSync8(join11(root, f)))
1730
+ if (existsSync11(join16(root, f)))
1387
1731
  kept.push(f);
1388
1732
  }
1389
1733
  return { removed, kept };
1390
1734
  }
1391
1735
  function archiveMission(projectDir, mission, opts = {}) {
1392
1736
  const { dryRun = false } = opts;
1393
- const root = join11(projectDir, ".mugiwara");
1737
+ const root = join16(projectDir, ".mugiwara");
1394
1738
  if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
1395
1739
  throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
1396
1740
  const removed = [];
1397
1741
  const kept = [];
1398
- const dir = join11(root, "missions", mission);
1399
- if (!existsSync8(dir))
1742
+ const dir = join16(root, "missions", mission);
1743
+ if (!existsSync11(dir))
1400
1744
  return { report: null, removed, kept };
1401
1745
  if (!dryRun) {
1402
1746
  const issues = checkTrail(dir, projectDir);
@@ -1404,12 +1748,16 @@ function archiveMission(projectDir, mission, opts = {}) {
1404
1748
  throw new Error(`closure integrity gate failed — fix these before archiving:
1405
1749
  ${formatIssues(issues)}`);
1406
1750
  }
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.`);
1754
+ }
1407
1755
  }
1408
- const files = readdirSync6(dir);
1756
+ const files = readdirSync7(dir);
1409
1757
  const state = primaryState(dir, files);
1410
1758
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
1411
1759
  try {
1412
- const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
1760
+ const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
1413
1761
  return typeof s.model === "string" ? s.model : "";
1414
1762
  } catch {
1415
1763
  return "";
@@ -1420,22 +1768,36 @@ ${formatIssues(issues)}`);
1420
1768
  const chars = measureContextChars(dir);
1421
1769
  const budget = readBudgetConfig(projectDir);
1422
1770
  const footprintLine = formatFootprint(chars, budget);
1423
- if (budget && chars > budget) {
1424
- throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
1425
- }
1426
1771
  const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
1427
1772
  const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
1428
1773
  const lane = typeof state.lane === "string" ? state.lane : "unknown";
1429
- const laneBudget = lane === "lean" ? 12000 : lane === "standard" ? 25000 : lane === "full" ? 50000 : lane === "spike" ? 3000 : 0;
1774
+ const laneBudget = budgetForLane(lane);
1775
+ const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
1430
1776
  const effBudget = budget || laneBudget;
1431
- const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
1432
1777
  const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
1433
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)";
1434
1796
  let reportedTotal = 0;
1435
1797
  let hasReported = false;
1436
1798
  for (const f of files.filter(isStateFile)) {
1437
1799
  try {
1438
- const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
1800
+ const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
1439
1801
  if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
1440
1802
  reportedTotal += s.tokens_est;
1441
1803
  hasReported = true;
@@ -1452,17 +1814,57 @@ ${formatIssues(issues)}`);
1452
1814
  "| Metric | Value |",
1453
1815
  "|--------|-------|",
1454
1816
  `| **Tokens used** | ${est.toLocaleString()} (${srcLabel}) |`,
1455
- `| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? Math.round(effBudget * 1.5).toLocaleString() : "—"} · stop ${effBudget ? (effBudget * 3).toLocaleString() : "—"}) |`,
1456
- `| **Budget status** | ${effBudget ? `${pct}% of budget · ${delta} · ${est >= effBudget * 3 ? "STOP" : est >= effBudget * 1.5 ? "WARN" : "OK"}` : "no lane budget"} |`,
1457
- `| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`
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} |`
1458
1822
  ].join(`
1459
1823
  `);
1460
1824
  if (hasReported) {
1461
1825
  costSection += `
1462
1826
  | **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
1463
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 {}
1464
1850
  costSection += `
1465
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.`);
1867
+ }
1466
1868
  }
1467
1869
  const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
1468
1870
  const fold = [];
@@ -1470,25 +1872,40 @@ ${formatIssues(issues)}`);
1470
1872
  if (files.includes(f))
1471
1873
  fold.push(f);
1472
1874
  }
1473
- const flowsDir = join11(dir, "flows");
1474
- const legacyWavesDir = join11(dir, "waves");
1475
- const artDir = existsSync8(flowsDir) ? flowsDir : existsSync8(legacyWavesDir) ? legacyWavesDir : flowsDir;
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;
1476
1880
  const artRel = artDir === legacyWavesDir ? "waves" : "flows";
1477
- if (existsSync8(artDir)) {
1478
- for (const f of readdirSync6(artDir).sort())
1479
- fold.push(join11(artRel, f));
1881
+ if (existsSync11(artDir)) {
1882
+ for (const f of readdirSync7(artDir).sort()) {
1883
+ if (artRel === "flows" && f === "07-pr-verdict.md")
1884
+ continue;
1885
+ fold.push(join16(artRel, f));
1886
+ }
1480
1887
  }
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");
1481
1892
  let report = "";
1482
- const reportPath = join11(dir, "report.md");
1893
+ const reportPath = join16(dir, "report.md");
1483
1894
  if (files.includes("report.md"))
1484
- report = readFileSync7(reportPath, "utf8");
1485
- else if (existsSync8(join11(artDir, "06-closure.md")))
1486
- report = readFileSync7(join11(artDir, "06-closure.md"), "utf8");
1895
+ report = readFileSync11(reportPath, "utf8");
1896
+ else if (existsSync11(join16(artDir, "06-closure.md")))
1897
+ report = readFileSync11(join16(artDir, "06-closure.md"), "utf8");
1487
1898
  if (!dryRun) {
1488
- mkdirSync5(dir, { recursive: true });
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
+ }
1489
1906
  if (fold.length) {
1490
1907
  const sections = fold.map((f) => {
1491
- const body = readFileSync7(join11(dir, f), "utf8").trim();
1908
+ const body = readFileSync11(join16(dir, f), "utf8").trim();
1492
1909
  const name = f.includes("/") ? f.split("/").pop() ?? f : f;
1493
1910
  return `
1494
1911
 
@@ -1502,22 +1919,26 @@ ${body}`;
1502
1919
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1503
1920
  sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
1504
1921
  }), mission) : "";
1505
- writeFileSync7(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1922
+ writeFileSync8(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1506
1923
  ${costSection}
1507
1924
  ` : "") + `
1508
1925
  `);
1509
1926
  renameSync(tmp, reportPath);
1510
1927
  }
1511
1928
  for (const f of fold) {
1512
- rmSync2(join11(dir, f), { force: true, recursive: true });
1513
- removed.push(join11("missions", mission, f));
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));
1514
1935
  }
1515
1936
  for (const f of files.filter((f2) => f2.endsWith(".json")))
1516
- rmSync2(join11(dir, f), { force: true });
1517
- if (existsSync8(artDir) && readdirSync6(artDir).length === 0)
1937
+ rmSync2(join16(dir, f), { force: true });
1938
+ if (existsSync11(artDir) && readdirSync7(artDir).length === 0)
1518
1939
  rmSync2(artDir, { recursive: true, force: true });
1519
- if (!existsSync8(reportPath)) {
1520
- writeFileSync7(reportPath, `# Mission: ${mission}
1940
+ if (!existsSync11(reportPath)) {
1941
+ writeFileSync8(reportPath, `# Mission: ${mission}
1521
1942
 
1522
1943
  Archived before closure — no wave artifacts were present.
1523
1944
  `);
@@ -1529,7 +1950,7 @@ Archived before closure — no wave artifacts were present.
1529
1950
  baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
1530
1951
  });
1531
1952
  if (rb)
1532
- kept.push(join11("missions", mission, rb.file));
1953
+ kept.push(join16("missions", mission, rb.file));
1533
1954
  try {
1534
1955
  writeProvenance(projectDir, dir, {
1535
1956
  mission,
@@ -1542,25 +1963,25 @@ Archived before closure — no wave artifacts were present.
1542
1963
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1543
1964
  models: stageModels
1544
1965
  });
1545
- kept.push(join11("missions", mission, "provenance.md"));
1966
+ kept.push(join16("missions", mission, "provenance.md"));
1546
1967
  } catch {}
1547
1968
  }
1548
1969
  } else {
1549
1970
  for (const f of fold)
1550
- removed.push(join11("missions", mission, f));
1971
+ removed.push(join16("missions", mission, f));
1551
1972
  }
1552
- removed.push(join11("missions", mission, "<session state>"));
1973
+ removed.push(join16("missions", mission, "<session state>"));
1553
1974
  if (files.includes("plan.md"))
1554
- kept.push(join11("missions", mission, "plan.md"));
1975
+ kept.push(join16("missions", mission, "plan.md"));
1555
1976
  if (files.includes("handoff.md"))
1556
- kept.push(join11("missions", mission, "handoff.md"));
1557
- kept.push(join11("missions", mission, "report.md"));
1977
+ kept.push(join16("missions", mission, "handoff.md"));
1978
+ kept.push(join16("missions", mission, "report.md"));
1558
1979
  let index;
1559
- const indexFile = join11(root, "index.md");
1980
+ const indexFile = join16(root, "index.md");
1560
1981
  const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
1561
1982
  `;
1562
1983
  if (!dryRun) {
1563
- const existing = existsSync8(indexFile) ? readFileSync7(indexFile, "utf8") : "";
1984
+ const existing = existsSync11(indexFile) ? readFileSync11(indexFile, "utf8") : "";
1564
1985
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
1565
1986
  const header = existing ? "" : `# Mission index
1566
1987
 
@@ -1574,28 +1995,28 @@ Archived before closure — no wave artifacts were present.
1574
1995
  }
1575
1996
  index = "index.md";
1576
1997
  }
1577
- return { report: join11("missions", mission, "report.md"), removed, kept, index };
1998
+ return { report: join16("missions", mission, "report.md"), removed, kept, index };
1578
1999
  }
1579
2000
 
1580
2001
  // src/run.ts
1581
- import { existsSync as existsSync9, readdirSync as readdirSync7 } from "node:fs";
2002
+ import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
1582
2003
  import { spawnSync } from "node:child_process";
1583
- import { dirname as dirname5, join as join12, resolve } from "node:path";
2004
+ import { dirname as dirname5, join as join17, resolve } from "node:path";
1584
2005
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1585
2006
  var here3 = dirname5(fileURLToPath4(import.meta.url));
1586
- var SCRIPTS_DIR = join12(here3, "..", "scripts");
2007
+ var SCRIPTS_DIR = join17(here3, "..", "scripts");
1587
2008
  var RUNNABLE = ["savepoint.sh", "lane.sh"];
1588
2009
  function findBash() {
1589
2010
  const explicit = process.env.MUGIWARA_BASH?.trim();
1590
2011
  if (explicit)
1591
- return existsSync9(explicit) ? explicit : null;
2012
+ return existsSync12(explicit) ? explicit : null;
1592
2013
  const candidates = process.platform === "win32" ? [
1593
2014
  "C:\\Program Files\\Git\\bin\\bash.exe",
1594
2015
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
1595
- join12(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2016
+ join17(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
1596
2017
  ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
1597
2018
  for (const p of candidates) {
1598
- if (p && existsSync9(p))
2019
+ if (p && existsSync12(p))
1599
2020
  return p;
1600
2021
  }
1601
2022
  const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
@@ -1612,9 +2033,9 @@ function runScript(name, args, projectDir) {
1612
2033
  if (!/^[a-z0-9-]+\.sh$/.test(name)) {
1613
2034
  throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
1614
2035
  }
1615
- const script = join12(SCRIPTS_DIR, name);
1616
- if (!existsSync9(script)) {
1617
- const have = existsSync9(SCRIPTS_DIR) ? readdirSync7(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")) : [];
1618
2039
  throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
1619
2040
  }
1620
2041
  const bash = findBash();
@@ -1631,9 +2052,9 @@ function runScript(name, args, projectDir) {
1631
2052
  }
1632
2053
 
1633
2054
  // src/continue.ts
1634
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync8 } from "node:fs";
2055
+ import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync9 } from "node:fs";
1635
2056
  import { execFileSync as execFileSync4 } from "node:child_process";
1636
- import { join as join13 } from "node:path";
2057
+ import { join as join18 } from "node:path";
1637
2058
  var SAFE = /^[A-Za-z0-9._-]+$/;
1638
2059
  var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
1639
2060
  var num = (v) => {
@@ -1662,14 +2083,14 @@ function gitActor(cwd) {
1662
2083
  return name || process.env.USER || process.env.USERNAME || "";
1663
2084
  }
1664
2085
  function scan(projectDir, kind, map) {
1665
- const base = join13(projectDir, ".mugiwara", "missions");
1666
- if (!existsSync10(base))
2086
+ const base = join18(projectDir, ".mugiwara", "missions");
2087
+ if (!existsSync13(base))
1667
2088
  return [];
1668
2089
  const out = [];
1669
- const missions = readdirSync8(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();
1670
2091
  for (const mission of missions) {
1671
- const dir = join13(base, mission);
1672
- for (const f of readdirSync8(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()) {
1673
2094
  const stem = f.slice(0, -".json".length);
1674
2095
  let member;
1675
2096
  if (kind === "state") {
@@ -1687,7 +2108,7 @@ function scan(projectDir, kind, map) {
1687
2108
  if (member !== null && !isSafeKey(member))
1688
2109
  continue;
1689
2110
  try {
1690
- const raw = JSON.parse(readFileSync8(join13(dir, f), "utf8"));
2111
+ const raw = JSON.parse(readFileSync12(join18(dir, f), "utf8"));
1691
2112
  if (text(raw.mission) !== mission)
1692
2113
  continue;
1693
2114
  out.push(map(raw, member));
@@ -1804,9 +2225,10 @@ function formatResume(e) {
1804
2225
 
1805
2226
  // src/sign.ts
1806
2227
  import { execFileSync as execFileSync5 } from "node:child_process";
1807
- import { existsSync as existsSync11 } from "node:fs";
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";
1808
2230
  import { homedir as homedir3 } from "node:os";
1809
- import { join as join14 } from "node:path";
2231
+ import { join as join19 } from "node:path";
1810
2232
  function signArgs(reportPath, secretKey) {
1811
2233
  return ["-Sm", reportPath, "-s", secretKey];
1812
2234
  }
@@ -1822,39 +2244,200 @@ function hasMinisign() {
1822
2244
  }
1823
2245
  }
1824
2246
  function defaultKey(flag) {
1825
- return join14(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
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() };
1826
2337
  }
1827
2338
  function signReport(projectDir, missionDir) {
1828
- const report = join14(missionDir, "report.md");
1829
- if (!existsSync11(report))
2339
+ const report = join19(missionDir, "report.md");
2340
+ if (!existsSync14(report))
1830
2341
  return { ok: false, message: "no report.md to sign — archive first" };
1831
- if (!hasMinisign()) {
1832
- return { ok: false, message: "minisign not installed — signing skipped (install minisign to enable attestation)" };
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
+ }
1833
2388
  }
1834
- const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
1835
2389
  try {
1836
- execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
1837
- return { ok: true, message: `signed ${report}.minisig (key: ${secretKey})` };
1838
- } catch (e) {
1839
- return { ok: false, message: `signing failed: ${e.message}` };
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" };
1840
2397
  }
1841
2398
  }
1842
- function verifyReport(projectDir, missionDir) {
1843
- const report = join14(missionDir, "report.md");
1844
- const sig = `${report}.minisig`;
1845
- if (!existsSync11(sig))
1846
- return { ok: false, message: "not signed (no .minisig beside report.md)" };
1847
- if (!hasMinisign())
1848
- return { ok: false, message: "minisign not installed — cannot verify" };
1849
- const pubKey = existsSync11(defaultKey("public")) ? defaultKey("public") : null;
2399
+ function readFileSafe(p) {
2400
+ try {
2401
+ return readFileSync13(p, "utf8");
2402
+ } catch {
2403
+ return null;
2404
+ }
2405
+ }
2406
+ function readFileSyncSafe2(p) {
1850
2407
  try {
1851
- execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
1852
- return { ok: true, message: "signature verifies against report.md" };
2408
+ return readFileSync13(p, "utf8").trim();
1853
2409
  } catch {
1854
- return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing" };
2410
+ return "";
1855
2411
  }
1856
2412
  }
1857
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
+
1858
2441
  // src/cli.ts
1859
2442
  var str = (v) => typeof v === "string" ? v : undefined;
1860
2443
  var flag = (v) => v === true;
@@ -1866,6 +2449,16 @@ async function run(argv) {
1866
2449
  console.log(`mugiwara ${VERSION}`);
1867
2450
  return;
1868
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
+ }
1869
2462
  switch (command) {
1870
2463
  case "install":
1871
2464
  return install(flags);
@@ -1885,6 +2478,8 @@ async function run(argv) {
1885
2478
  return continueCmd(flags, _);
1886
2479
  case "status":
1887
2480
  return statusCmd(flags);
2481
+ case "cost":
2482
+ return costCmd(flags, _);
1888
2483
  case "run":
1889
2484
  return runCmd(flags, _);
1890
2485
  case "savepoint":
@@ -1936,8 +2531,8 @@ function archive(flags, positionals) {
1936
2531
  function cleanCmd(flags) {
1937
2532
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1938
2533
  const dryRun = flag(flags.dryRun);
1939
- const root = join15(projectDir, ".mugiwara", "missions");
1940
- if (!existsSync12(root)) {
2534
+ const root = join20(projectDir, ".mugiwara", "missions");
2535
+ if (!existsSync15(root)) {
1941
2536
  console.log("nothing to clean (.mugiwara/missions/ does not exist).");
1942
2537
  return;
1943
2538
  }
@@ -1947,8 +2542,8 @@ function cleanCmd(flags) {
1947
2542
  console.error(`invalid --before date: ${before}`);
1948
2543
  process.exit(1);
1949
2544
  }
1950
- let candidates = readdirSync9(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
1951
- const stateFiles = (m) => readdirSync9(join15(root, m)).filter((f) => {
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) => {
1952
2547
  const stem = f.replace(/\.json$/, "");
1953
2548
  return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
1954
2549
  });
@@ -1958,7 +2553,7 @@ function cleanCmd(flags) {
1958
2553
  return false;
1959
2554
  for (const f of stateFiles(m)) {
1960
2555
  try {
1961
- const ts = Date.parse(JSON.parse(readFileSync9(join15(root, m, f), "utf8")).updated_at ?? "") || 0;
2556
+ const ts = Date.parse(JSON.parse(readFileSync14(join20(root, m, f), "utf8")).updated_at ?? "") || 0;
1962
2557
  if (ts === 0 || ts >= beforeMs)
1963
2558
  return false;
1964
2559
  } catch {
@@ -1968,7 +2563,7 @@ function cleanCmd(flags) {
1968
2563
  return true;
1969
2564
  };
1970
2565
  if (!flag(flags.all)) {
1971
- candidates = candidates.filter((m) => existsSync12(join15(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
2566
+ candidates = candidates.filter((m) => existsSync15(join20(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
1972
2567
  } else if (!flag(flags.force)) {
1973
2568
  const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
1974
2569
  if (live.length) {
@@ -2002,7 +2597,7 @@ async function resolveOptions(flags) {
2002
2597
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
2003
2598
  }
2004
2599
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2005
- if (scope === "project" && !existsSync12(projectDir))
2600
+ if (scope === "project" && !existsSync15(projectDir))
2006
2601
  throw new Error(`Project dir not found: ${projectDir}`);
2007
2602
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
2008
2603
  if (targetIds && targetIds.includes("all"))
@@ -2081,7 +2676,7 @@ async function uninstall(flags) {
2081
2676
  if (!manifest) {
2082
2677
  if (scope === "project") {
2083
2678
  const globalFile = manifestPath({ scope: "global", projectDir, home });
2084
- if (existsSync12(globalFile)) {
2679
+ if (existsSync15(globalFile)) {
2085
2680
  console.log(`No project manifest found, but a global install exists. Try:
2086
2681
  mugiwara uninstall --global`);
2087
2682
  return;
@@ -2118,13 +2713,13 @@ async function uninstall(flags) {
2118
2713
  console.log(` note: ${n}`);
2119
2714
  }
2120
2715
  rmSync3(file);
2121
- const mugiDir = join15(scope === "global" ? home : projectDir, ".mugiwara");
2122
- if (existsSync12(mugiDir) && readdirSync9(mugiDir).length === 0) {
2716
+ const mugiDir = join20(scope === "global" ? home : projectDir, ".mugiwara");
2717
+ if (existsSync15(mugiDir) && readdirSync10(mugiDir).length === 0) {
2123
2718
  rmSync3(mugiDir, { recursive: true, force: true });
2124
2719
  }
2125
2720
  if (manifest.targets.includes("opencode")) {
2126
- const opencodeCache = join15(home, ".cache", "opencode", "packages", "@ionivetech");
2127
- if (existsSync12(opencodeCache)) {
2721
+ const opencodeCache = join20(home, ".cache", "opencode", "packages", "@ionivetech");
2722
+ if (existsSync15(opencodeCache)) {
2128
2723
  rmSync3(opencodeCache, { recursive: true, force: true });
2129
2724
  console.log(" cleared opencode npm cache (stale plugin versions)");
2130
2725
  }
@@ -2145,7 +2740,7 @@ function list(flags) {
2145
2740
  continue;
2146
2741
  found = true;
2147
2742
  if (flag(flags.check)) {
2148
- const missing = m.files.filter((f) => !existsSync12(f));
2743
+ const missing = m.files.filter((f) => !existsSync15(f));
2149
2744
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
2150
2745
  } else {
2151
2746
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -2215,6 +2810,58 @@ function statusCmd(flags) {
2215
2810
  console.log(` evidence: ${s.evidence.join(", ")}`);
2216
2811
  }
2217
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
+ }
2218
2865
  function runCmd(flags, positionals) {
2219
2866
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2220
2867
  const name = positionals[1];
@@ -2300,8 +2947,8 @@ function handoffCmd(flags, positionals) {
2300
2947
  }
2301
2948
  lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
2302
2949
  lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
2303
- const out = join15(".mugiwara", "missions", mission, "handoff.md");
2304
- writeFileSync8(resolve2(projectDir, out), lines.join(`
2950
+ const out = join20(".mugiwara", "missions", mission, "handoff.md");
2951
+ writeFileSync10(resolve2(projectDir, out), lines.join(`
2305
2952
  `) + `
2306
2953
  `);
2307
2954
  console.log(lines.join(`
@@ -2311,13 +2958,34 @@ written: ${out}`);
2311
2958
  }
2312
2959
  function signCmd(flags, _) {
2313
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);
2976
+ }
2977
+ }
2978
+ const dir = ensurePureKey(home);
2979
+ console.log(`✓ pure ed25519 key pair ready: ${join20(dir, "mugiwara.key")} / ${join20(dir, "mugiwara.pub")}`);
2980
+ return;
2981
+ }
2314
2982
  const mission = _[1];
2315
2983
  if (!mission) {
2316
- console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
2984
+ console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
2317
2985
  process.exit(1);
2318
2986
  }
2319
- const missionDir = join15(projectDir, ".mugiwara", "missions", mission);
2320
- if (!existsSync12(missionDir)) {
2987
+ const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2988
+ if (!existsSync15(missionDir)) {
2321
2989
  console.error(`no mission dir: ${missionDir}`);
2322
2990
  process.exit(1);
2323
2991
  }
@@ -2343,11 +3011,15 @@ Usage:
2343
3011
  mugiwara continue <m> [member]
2344
3012
  print the exact resume point for that mission/member
2345
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)
2346
3016
  mugiwara blame <path> provenance note on the last commit touching <path>
2347
3017
  (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
2348
3018
  mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
2349
3019
  engineer can act on (computed state + staleness check)
2350
- mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to 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)
2351
3023
  mugiwara run <script> [args...]
2352
3024
  run a bundled harness script here (${RUNNABLE.join(", ")})
2353
3025
  mugiwara savepoint <mission> [member] [flow] [mode]