@ionivetech/mugiwara 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/.opencode/mugiwara-helpers.mjs +2 -2
  7. package/README.md +196 -330
  8. package/content/agents/brook-healing.md +1 -1
  9. package/content/agents/franky-gates.md +1 -1
  10. package/content/agents/luffy-orchestrator.md +2 -2
  11. package/content/agents/memory-keeper.md +5 -0
  12. package/content/agents/usopp-brainstorm.md +3 -2
  13. package/content/agents/zoro-execution.md +4 -3
  14. package/content/skills/mugiwara-backend/SKILL.md +52 -43
  15. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  16. package/content/skills/mugiwara-checkpoint/SKILL.md +21 -8
  17. package/content/skills/mugiwara-contract-first/SKILL.md +46 -1
  18. package/content/skills/mugiwara-execution/SKILL.md +34 -33
  19. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  20. package/content/skills/mugiwara-execution/references/execution-phase-flows.md +18 -0
  21. package/content/skills/mugiwara-frontend/SKILL.md +44 -44
  22. package/content/skills/mugiwara-gates/SKILL.md +28 -16
  23. package/content/skills/mugiwara-healing/SKILL.md +30 -25
  24. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  25. package/content/skills/mugiwara-orchestration/SKILL.md +10 -9
  26. package/content/skills/mugiwara-orchestration/references/control-commands.md +14 -0
  27. package/content/skills/mugiwara-planning/SKILL.md +28 -14
  28. package/content/skills/mugiwara-planning/references/large-campaign-subplan.md +41 -0
  29. package/content/skills/mugiwara-planning/references/plan-template.md +22 -0
  30. package/content/skills/mugiwara-quality/SKILL.md +8 -13
  31. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  32. package/content/skills/mugiwara-resume/SKILL.md +3 -9
  33. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  34. package/content/skills/mugiwara-review/SKILL.md +17 -24
  35. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  36. package/content/skills/mugiwara-security/SKILL.md +47 -35
  37. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  38. package/content/skills/mugiwara-workflow/SKILL.md +13 -13
  39. package/content/skills/mugiwara-workflow/references/large-campaign-subplan.md +29 -0
  40. package/content/skills/mugiwara-workflow/references/workspace-layout.md +6 -3
  41. package/dist/mugiwara.js +1802 -316
  42. package/gemini-extension.json +1 -1
  43. package/hooks/mugiwara-mode-tracker.js +24 -4
  44. package/hooks/mugiwara-mode-tracker.ts +36 -7
  45. package/hooks/pipeline-guard.js +1 -1
  46. package/hooks/pipeline-guard.ts +2 -1
  47. package/hooks/session-start.js +6 -1
  48. package/hooks/session-start.ts +8 -1
  49. package/package.json +2 -2
  50. package/plugin.json +1 -1
  51. package/references/cost-governor.md +104 -0
  52. package/references/multi-actor.md +21 -0
  53. package/references/posture-routing.md +31 -0
  54. package/references/wave-banners.md +1 -2
  55. package/scripts/benchmark-governor.ts +516 -0
  56. package/scripts/benchmark-thresholds.json +47 -0
  57. package/scripts/check-doc-links.ts +8 -2
  58. package/scripts/gate-selftest.ts +104 -21
  59. package/scripts/lib/lane-base.sh +4 -4
  60. package/scripts/retrieval-eval.ts +9 -3
  61. package/scripts/savepoint.sh +41 -2
  62. package/scripts/validate-content.ts +82 -3
  63. package/scripts/verify-install.ts +20 -0
  64. package/scripts/write-metrics.ts +73 -0
  65. package/src/adaptive-budget.ts +178 -0
  66. package/src/args.ts +3 -2
  67. package/src/budget.ts +18 -16
  68. package/src/check-artifacts.ts +45 -0
  69. package/src/cli.ts +221 -8
  70. package/src/cognition.ts +234 -0
  71. package/src/config.ts +113 -0
  72. package/src/context.ts +72 -0
  73. package/src/continue.ts +29 -0
  74. package/src/cost.ts +189 -0
  75. package/src/evidence.ts +160 -0
  76. package/src/installer.ts +2 -16
  77. package/src/integrity.ts +65 -16
  78. package/src/investigation.ts +72 -0
  79. package/src/mission.ts +246 -16
  80. package/src/policy.ts +355 -2
  81. package/src/posture.ts +86 -0
  82. package/src/provenance.ts +29 -9
  83. package/src/reporting.ts +225 -0
  84. package/src/scope.ts +321 -0
  85. package/src/sign.ts +234 -18
  86. package/src/slop.ts +306 -0
  87. package/src/work.ts +273 -0
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 existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync15, rmSync as rmSync3, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, renameSync as renameSync2 } from "node:fs";
5
5
  import { execFileSync as execFileSync6 } from "node:child_process";
6
6
  import { homedir as homedir4 } from "node:os";
7
- import { join as join15, resolve as resolve2 } from "node:path";
7
+ import { dirname as dirname6, join as join21, resolve as resolve2 } from "node:path";
8
8
  import { fileURLToPath as fileURLToPath5 } from "node:url";
9
9
 
10
10
  // src/args.ts
11
- var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before" };
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,129 @@ 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
+ try {
662
+ const file = join5(projectDir, ".mugiwara", "config");
663
+ let exists = false;
664
+ try {
665
+ exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
666
+ } catch {
667
+ exists = false;
668
+ }
669
+ if (!exists)
670
+ ensureConfig(projectDir);
671
+ } catch {}
672
+ const out = {};
673
+ for (const file of configPaths(projectDir)) {
674
+ if (!existsSync4(file))
675
+ continue;
676
+ for (const line of readFileSync3(file, "utf8").split(/\r?\n/)) {
677
+ const t = line.trim();
678
+ if (!t || t.startsWith("#"))
679
+ continue;
680
+ const eq = t.indexOf("=");
681
+ if (eq === -1)
682
+ continue;
683
+ const key = t.slice(0, eq).trim();
684
+ if (!key)
685
+ continue;
686
+ if (key in out)
687
+ continue;
688
+ out[key] = t.slice(eq + 1).trim();
689
+ }
690
+ }
691
+ return out;
692
+ }
693
+ function ensureConfig(projectDir) {
694
+ const file = join5(projectDir, ".mugiwara", "config");
695
+ let exists = false;
696
+ try {
697
+ exists = lstatSync(file).isFile() || lstatSync(file).isSymbolicLink();
698
+ } catch {
699
+ exists = false;
700
+ }
701
+ if (exists)
702
+ return false;
703
+ mkdirSync3(join5(projectDir, ".mugiwara"), { recursive: true });
704
+ writeFileSync3(file, DEFAULT_CONFIG);
705
+ return true;
706
+ }
707
+
708
+ // src/installer.ts
709
+ var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
710
+ var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
629
711
  var VERSION = pkg.version;
630
712
  function collectContent() {
631
- const skillNames = readdirSync3(join5(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
713
+ const skillNames = readdirSync3(join6(CONTENT_DIR, "skills"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
632
714
  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)) };
715
+ const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "skills", name, "SKILL.md"), "utf8"));
716
+ return { name, data, body, refs: collectRefs(join6(CONTENT_DIR, "skills", name)) };
635
717
  });
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"));
718
+ const agents = readdirSync3(join6(CONTENT_DIR, "agents")).filter((f) => f.endsWith(".md")).map((f) => {
719
+ const { data, body } = parseFrontmatter(readFileSync4(join6(CONTENT_DIR, "agents", f), "utf8"));
638
720
  return { name: f.replace(/\.md$/, ""), data, body, refs: [], internal: data.internal === "true" };
639
721
  });
640
- const sharedRefsDir = join5(dirname3(CONTENT_DIR), "references");
722
+ const sharedRefsDir = join6(dirname3(CONTENT_DIR), "references");
641
723
  const sharedRefs = [];
642
- if (existsSync4(sharedRefsDir)) {
724
+ if (existsSync5(sharedRefsDir)) {
643
725
  for (const f of readdirSync3(sharedRefsDir)) {
644
726
  if (!f.endsWith(".md"))
645
727
  continue;
646
- sharedRefs.push({ relPath: f, text: readFileSync3(join5(sharedRefsDir, f), "utf8") });
728
+ sharedRefs.push({ relPath: f, text: readFileSync4(join6(sharedRefsDir, f), "utf8") });
647
729
  }
648
730
  }
649
731
  return { skills, agents, sharedRefs };
650
732
  }
651
733
  function collectRefs(skillDir) {
652
- const refsDir = join5(skillDir, "references");
653
- if (!existsSync4(refsDir))
734
+ const refsDir = join6(skillDir, "references");
735
+ if (!existsSync5(refsDir))
654
736
  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") }));
737
+ 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
738
  }
657
739
  function installTo(target10, opts) {
658
740
  const { scope, projectDir, dryRun = false, force = false } = opts;
659
- const home = opts.home ?? homedir();
741
+ const home = opts.home ?? homedir2();
660
742
  const { skills, agents, sharedRefs } = collectContent();
661
743
  const dirs = target10.paths({ scope, projectDir, home });
662
- const backupRoot = join5(scope === "global" ? home : projectDir, ".mugiwara");
744
+ const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
663
745
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
664
746
  const writeOne = (absPath, text) => {
665
- if (existsSync4(absPath)) {
666
- if (readFileSync3(absPath, "utf8") === text) {
747
+ if (existsSync5(absPath)) {
748
+ if (readFileSync4(absPath, "utf8") === text) {
667
749
  result.skipped.push(absPath);
668
750
  return;
669
751
  }
@@ -674,17 +756,17 @@ function installTo(target10, opts) {
674
756
  }
675
757
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
676
758
  const fileName = absPath.replace(/[^a-zA-Z0-9]+/g, "_");
677
- const backupDir = join5(backupRoot, "backup", `${ts}-${target10.id}`);
678
- const backupFile = join5(backupDir, fileName);
759
+ const backupDir = join6(backupRoot, "backup", `${ts}-${target10.id}`);
760
+ const backupFile = join6(backupDir, fileName);
679
761
  if (!dryRun) {
680
- mkdirSync3(backupDir, { recursive: true });
762
+ mkdirSync4(backupDir, { recursive: true });
681
763
  copyFileSync3(absPath, backupFile);
682
764
  }
683
765
  result.backedUp.push(absPath);
684
766
  }
685
767
  if (!dryRun) {
686
- mkdirSync3(dirname3(absPath), { recursive: true });
687
- writeFileSync3(absPath, text);
768
+ mkdirSync4(dirname3(absPath), { recursive: true });
769
+ writeFileSync4(absPath, text);
688
770
  }
689
771
  result.written.push(absPath);
690
772
  };
@@ -695,7 +777,7 @@ function installTo(target10, opts) {
695
777
  if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
696
778
  text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
697
779
  }
698
- writeOne(join5(dirs.skillsDir, out.relPath), text);
780
+ writeOne(join6(dirs.skillsDir, out.relPath), text);
699
781
  }
700
782
  if (target10.transformSkillFull) {
701
783
  const full = target10.transformSkillFull(s.data, s.body);
@@ -704,29 +786,29 @@ function installTo(target10, opts) {
704
786
  if (target10.refPointerPrefix !== undefined && target10.refPointerPrefix !== "") {
705
787
  text = text.replace(/`_shared\/references\//g, "`" + target10.refPointerPrefix + "_shared/references/");
706
788
  }
707
- writeOne(join5(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
789
+ writeOne(join6(target10.refsDir({ scope, projectDir, home }, s.name), full.relPath), text);
708
790
  }
709
791
  }
710
792
  if (s.refs.length && target10.refsDir) {
711
793
  const refsRoot = target10.refsDir({ scope, projectDir, home }, s.name);
712
794
  for (const r of s.refs)
713
- writeOne(join5(refsRoot, r.relPath), r.text);
795
+ writeOne(join6(refsRoot, r.relPath), r.text);
714
796
  }
715
797
  }
716
798
  for (const a of agents) {
717
799
  const out = target10.transformAgent({ ...a.data, ...a.internal ? { "internal-agent": "true" } : {} }, a.body);
718
800
  if (out)
719
- writeOne(join5(dirs.agentsDir, out.relPath), out.text);
801
+ writeOne(join6(dirs.agentsDir, out.relPath), out.text);
720
802
  if (target10.transformAgentFull) {
721
803
  const full = target10.transformAgentFull(a.data, a.body);
722
804
  if (full && target10.refsDir)
723
- writeOne(join5(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
805
+ writeOne(join6(target10.refsDir({ scope, projectDir, home }, a.name), full.relPath), full.text);
724
806
  }
725
807
  }
726
808
  if (sharedRefs.length) {
727
- const sharedRoot = join5(dirs.skillsDir, "_shared", "references");
809
+ const sharedRoot = join6(dirs.skillsDir, "_shared", "references");
728
810
  for (const r of sharedRefs)
729
- writeOne(join5(sharedRoot, r.relPath), r.text);
811
+ writeOne(join6(sharedRoot, r.relPath), r.text);
730
812
  }
731
813
  if (target10.postInstall) {
732
814
  const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
@@ -734,34 +816,17 @@ function installTo(target10, opts) {
734
816
  result.notes.push(...post.notes);
735
817
  }
736
818
  if (scope === "project") {
737
- const configPath = join5(projectDir, ".mugiwara", "config");
819
+ const configPath = join6(projectDir, ".mugiwara", "config");
738
820
  let configExists = false;
739
821
  try {
740
- configExists = lstatSync(configPath).isFile() || lstatSync(configPath).isSymbolicLink();
822
+ configExists = lstatSync2(configPath).isFile() || lstatSync2(configPath).isSymbolicLink();
741
823
  } catch {
742
824
  configExists = false;
743
825
  }
744
826
  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
827
  if (!dryRun) {
763
- mkdirSync3(dirname3(configPath), { recursive: true });
764
- writeFileSync3(configPath, body);
828
+ mkdirSync4(dirname3(configPath), { recursive: true });
829
+ writeFileSync4(configPath, DEFAULT_CONFIG);
765
830
  }
766
831
  result.written.push(configPath);
767
832
  result.notes.push(`default config written: ${configPath} (edit it to customise)`);
@@ -772,7 +837,7 @@ function installTo(target10, opts) {
772
837
  function removeInstalled(manifest, { dryRun = false } = {}) {
773
838
  const removed = [];
774
839
  for (const f of manifest.files) {
775
- if (existsSync4(f)) {
840
+ if (existsSync5(f)) {
776
841
  if (!dryRun)
777
842
  rmSync(f);
778
843
  removed.push(f);
@@ -781,7 +846,7 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
781
846
  if (!dryRun) {
782
847
  for (const f of manifest.files) {
783
848
  let d = dirname3(f);
784
- while (existsSync4(d) && readdirSync3(d).length === 0) {
849
+ while (existsSync5(d) && readdirSync3(d).length === 0) {
785
850
  if (d.endsWith("/.mugiwara") || d === dirname3(d))
786
851
  break;
787
852
  rmSync(d, { recursive: true, force: true });
@@ -792,10 +857,10 @@ function removeInstalled(manifest, { dryRun = false } = {}) {
792
857
  return removed;
793
858
  }
794
859
  function assertNotSymlink(file) {
795
- if (!existsSync4(file))
860
+ if (!existsSync5(file))
796
861
  return;
797
862
  try {
798
- if (lstatSync(file).isSymbolicLink())
863
+ if (lstatSync2(file).isSymbolicLink())
799
864
  throw new Error(`refusing to follow symlink: ${file}`);
800
865
  } catch (e) {
801
866
  if (e.code === "ENOENT")
@@ -816,9 +881,9 @@ var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit
816
881
  `;
817
882
  function ensureProjectGitignore(projectDir, opts = {}) {
818
883
  const { dryRun = false } = opts;
819
- const path = join5(projectDir, ".gitignore");
884
+ const path = join6(projectDir, ".gitignore");
820
885
  assertNotSymlink(path);
821
- let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
886
+ let existing = existsSync5(path) ? readFileSync4(path, "utf8") : "";
822
887
  if (existing) {
823
888
  if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
824
889
  return { appended: false, notes: [] };
@@ -826,24 +891,24 @@ function ensureProjectGitignore(projectDir, opts = {}) {
826
891
  const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
827
892
  if (hadOld) {
828
893
  const clean = removeProjectGitignore(projectDir, { dryRun });
829
- existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
894
+ existing = clean.removed ? existsSync5(path) ? readFileSync4(path, "utf8") : "" : existing;
830
895
  }
831
896
  }
832
897
  const separator = existing.length && !existing.endsWith(`
833
898
  `) ? `
834
899
  ` : "";
835
900
  if (!dryRun) {
836
- mkdirSync3(dirname3(path), { recursive: true });
837
- writeFileSync3(path, existing + separator + GITIGNORE_BLOCK);
901
+ mkdirSync4(dirname3(path), { recursive: true });
902
+ writeFileSync4(path, existing + separator + GITIGNORE_BLOCK);
838
903
  }
839
904
  return { appended: true, notes: [`.gitignore ${dryRun ? "would upgrade+append" : "upgraded"} mugiwara audit-trail block`] };
840
905
  }
841
906
  function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
842
- const path = join5(projectDir, ".gitignore");
907
+ const path = join6(projectDir, ".gitignore");
843
908
  assertNotSymlink(path);
844
- if (!existsSync4(path))
909
+ if (!existsSync5(path))
845
910
  return { removed: false, notes: [] };
846
- const current = readFileSync3(path, "utf8");
911
+ const current = readFileSync4(path, "utf8");
847
912
  const delimited = current.includes(GITIGNORE_BLOCK_START);
848
913
  let cleaned;
849
914
  if (delimited) {
@@ -883,62 +948,533 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
883
948
  if (cleaned === current)
884
949
  return { removed: false, notes: [] };
885
950
  if (!dryRun)
886
- writeFileSync3(path, cleaned);
951
+ writeFileSync4(path, cleaned);
887
952
  return { removed: true, notes: [`.gitignore ${dryRun ? "would strip" : "stripped"} mugiwara block`] };
888
953
  }
889
954
 
890
955
  // 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";
956
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
957
+ import { dirname as dirname4, join as join7 } from "node:path";
893
958
  function manifestPath({ scope, projectDir, home }) {
894
- return scope === "global" ? join6(home, ".mugiwara", "manifest.json") : join6(projectDir, ".mugiwara", "manifest.json");
959
+ return scope === "global" ? join7(home, ".mugiwara", "manifest.json") : join7(projectDir, ".mugiwara", "manifest.json");
895
960
  }
896
961
  function readManifest(file) {
897
- return existsSync5(file) ? JSON.parse(readFileSync4(file, "utf8")) : null;
962
+ return existsSync6(file) ? JSON.parse(readFileSync5(file, "utf8")) : null;
898
963
  }
899
964
  function writeManifest(file, data) {
900
- mkdirSync4(dirname4(file), { recursive: true });
901
- writeFileSync4(file, JSON.stringify(data, null, 2) + `
965
+ mkdirSync5(dirname4(file), { recursive: true });
966
+ writeFileSync5(file, JSON.stringify(data, null, 2) + `
902
967
  `);
903
968
  }
904
969
 
905
970
  // 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";
907
- import { execFileSync as execFileSync3 } from "node:child_process";
908
- import { join as join11 } from "node:path";
971
+ import { existsSync as existsSync13, rmSync as rmSync2, readFileSync as readFileSync13, readdirSync as readdirSync7, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, renameSync, openSync, writeSync, closeSync } from "node:fs";
972
+ import { execFileSync as execFileSync4 } from "node:child_process";
973
+ import { join as join18 } from "node:path";
974
+
975
+ // src/integrity.ts
976
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
977
+ import { isAbsolute, join as join9, relative } from "node:path";
978
+
979
+ // src/policy.ts
980
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
981
+ import { join as join8 } from "node:path";
982
+ var POLICY_FILES = ["mugiwara.policy.yml", "mugiwara.policy.yaml"];
983
+ var KNOWN_ROOTS = ["lanes", "gates", "evidence", "integrity", "attestation", "harness"];
984
+ function parsePolicyYaml(text) {
985
+ const root = {};
986
+ const stack = [{ keyIndent: -1, obj: root }];
987
+ const lines = text.split(/\r?\n/);
988
+ const process2 = (i) => {
989
+ if (i >= lines.length)
990
+ return;
991
+ const noComment = lines[i].replace(/(^|\s)#.*$/, "");
992
+ if (!noComment.trim())
993
+ return process2(i + 1);
994
+ const indent = noComment.length - noComment.trimStart().length;
995
+ const line = noComment.trim();
996
+ while (stack.length > 1 && indent <= stack[stack.length - 1].keyIndent)
997
+ stack.pop();
998
+ const top = stack[stack.length - 1];
999
+ if (top.pending && !line.startsWith("- ") && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
1000
+ const { key: key2, indent: pIndent } = top.pending;
1001
+ delete top.pending;
1002
+ const created = {};
1003
+ top.obj[key2] = created;
1004
+ stack.push({ keyIndent: pIndent, obj: created });
1005
+ return process2(i);
1006
+ }
1007
+ if (line.startsWith("- ")) {
1008
+ if (top.pending) {
1009
+ const existing = top.obj[top.pending.key];
1010
+ const arr = Array.isArray(existing) ? existing : [];
1011
+ arr.push(scalar(line.slice(2)));
1012
+ top.obj[top.pending.key] = arr;
1013
+ }
1014
+ return process2(i + 1);
1015
+ }
1016
+ const colon = line.indexOf(":");
1017
+ if (colon === -1)
1018
+ return process2(i + 1);
1019
+ const key = line.slice(0, colon).trim();
1020
+ const rest = line.slice(colon + 1).trim();
1021
+ if (rest === "") {
1022
+ top.pending = { key, indent };
1023
+ } else {
1024
+ top.obj[key] = scalar(rest);
1025
+ delete top.pending;
1026
+ }
1027
+ process2(i + 1);
1028
+ };
1029
+ process2(0);
1030
+ return root;
1031
+ }
1032
+ function scalar(v) {
1033
+ const t = v.trim().replace(/^["']|["']$/g, "");
1034
+ if (/^-?\d+(\.\d+)?$/.test(t))
1035
+ return Number(t);
1036
+ if (t === "true")
1037
+ return true;
1038
+ if (t === "false")
1039
+ return false;
1040
+ return t;
1041
+ }
1042
+ function extractExtraSecretPatterns(text) {
1043
+ const lines = text.split(/\r?\n/);
1044
+ let inBlock = false;
1045
+ let baseIndent = -1;
1046
+ const out = [];
1047
+ let current = null;
1048
+ let currentIndent = -1;
1049
+ for (const rawLine of lines) {
1050
+ const noComment = rawLine.replace(/(^|\s)#.*$/, "");
1051
+ if (!noComment.trim())
1052
+ continue;
1053
+ const indent = noComment.length - noComment.trimStart().length;
1054
+ const trimmed = noComment.trim();
1055
+ if (!inBlock) {
1056
+ if (trimmed.startsWith("extra_secret_patterns:")) {
1057
+ inBlock = true;
1058
+ baseIndent = indent;
1059
+ continue;
1060
+ }
1061
+ } else {
1062
+ if (indent <= baseIndent && !trimmed.startsWith("-") && trimmed.includes(":")) {
1063
+ break;
1064
+ }
1065
+ if (trimmed.startsWith("-")) {
1066
+ if (current)
1067
+ out.push(current);
1068
+ current = {};
1069
+ currentIndent = indent;
1070
+ const afterDash = trimmed.slice(1).trim();
1071
+ if (!afterDash)
1072
+ continue;
1073
+ if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
1074
+ const inner = afterDash.slice(1, -1);
1075
+ for (const part of inner.split(",")) {
1076
+ const colon = part.indexOf(":");
1077
+ if (colon === -1)
1078
+ continue;
1079
+ const k = part.slice(0, colon).trim();
1080
+ const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1081
+ if (k && v)
1082
+ current[k] = v;
1083
+ }
1084
+ } else if (afterDash.includes(":")) {
1085
+ const colon = afterDash.indexOf(":");
1086
+ const k = afterDash.slice(0, colon).trim();
1087
+ const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1088
+ if (k && v)
1089
+ current[k] = v;
1090
+ } else {
1091
+ const v = afterDash.replace(/^["']|["']$/g, "");
1092
+ if (v)
1093
+ current["pattern"] = v;
1094
+ }
1095
+ } else if (current && trimmed.includes(":")) {
1096
+ if (indent > currentIndent) {
1097
+ const colon = trimmed.indexOf(":");
1098
+ const k = trimmed.slice(0, colon).trim();
1099
+ const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1100
+ if (k && v)
1101
+ current[k] = v;
1102
+ } else if (indent <= baseIndent) {
1103
+ break;
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ if (current)
1109
+ out.push(current);
1110
+ return out.filter((o) => typeof o.pattern === "string" && typeof o.label === "string" && o.pattern.length > 0);
1111
+ }
1112
+ function extractAttestation(text) {
1113
+ const lines = text.split(/\r?\n/);
1114
+ let attBase = -1;
1115
+ let inAtt = false;
1116
+ let required;
1117
+ const trusted = [];
1118
+ const revoked = [];
1119
+ let collecting = null;
1120
+ let collectBase = -1;
1121
+ let current = null;
1122
+ let curIndent = -1;
1123
+ const flush = () => {
1124
+ if (!current)
1125
+ return;
1126
+ if (collecting === "trusted")
1127
+ trusted.push(current);
1128
+ else if (collecting === "revoked")
1129
+ revoked.push(current);
1130
+ current = null;
1131
+ };
1132
+ for (let idx = 0;idx < lines.length; idx++) {
1133
+ const rawLine = lines[idx];
1134
+ const noComment = rawLine.replace(/(^|\s)#.*$/, "");
1135
+ if (!noComment.trim())
1136
+ continue;
1137
+ const indent = noComment.length - noComment.trimStart().length;
1138
+ const trimmed = noComment.trim();
1139
+ if (!inAtt) {
1140
+ if (trimmed === "attestation:" || trimmed.startsWith("attestation:")) {
1141
+ const after = trimmed.slice("attestation:".length).trim();
1142
+ if (after.startsWith("{")) {
1143
+ return null;
1144
+ }
1145
+ inAtt = true;
1146
+ attBase = indent;
1147
+ continue;
1148
+ }
1149
+ continue;
1150
+ }
1151
+ if (indent <= attBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
1152
+ flush();
1153
+ break;
1154
+ }
1155
+ if (collecting) {
1156
+ if (indent <= collectBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
1157
+ flush();
1158
+ collecting = null;
1159
+ } else if (trimmed.startsWith("-")) {
1160
+ flush();
1161
+ current = {};
1162
+ curIndent = indent;
1163
+ const afterDash = trimmed.slice(1).trim();
1164
+ if (!afterDash)
1165
+ continue;
1166
+ if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
1167
+ const inner = afterDash.slice(1, -1);
1168
+ for (const part of inner.split(",")) {
1169
+ const colon = part.indexOf(":");
1170
+ if (colon === -1)
1171
+ continue;
1172
+ const k = part.slice(0, colon).trim();
1173
+ const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1174
+ if (k && v)
1175
+ current[k] = v;
1176
+ }
1177
+ } else if (afterDash.includes(":")) {
1178
+ const colon = afterDash.indexOf(":");
1179
+ const k = afterDash.slice(0, colon).trim();
1180
+ const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1181
+ if (k && v)
1182
+ current[k] = v;
1183
+ } else {
1184
+ const v = afterDash.replace(/^["']|["']$/g, "");
1185
+ if (v)
1186
+ current["id"] = v;
1187
+ }
1188
+ continue;
1189
+ } else if (current && trimmed.includes(":")) {
1190
+ if (indent > curIndent) {
1191
+ const colon = trimmed.indexOf(":");
1192
+ const k = trimmed.slice(0, colon).trim();
1193
+ const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1194
+ if (k && v)
1195
+ current[k] = v;
1196
+ continue;
1197
+ }
1198
+ }
1199
+ if (collecting)
1200
+ continue;
1201
+ }
1202
+ if (trimmed.startsWith("required:")) {
1203
+ const v = trimmed.slice("required:".length).trim().replace(/^["']|["']$/g, "");
1204
+ if (v === "true")
1205
+ required = true;
1206
+ else if (v === "false")
1207
+ required = false;
1208
+ continue;
1209
+ }
1210
+ if (trimmed.startsWith("trusted_keys:")) {
1211
+ const after = trimmed.slice("trusted_keys:".length).trim();
1212
+ if (after === "[]")
1213
+ continue;
1214
+ collecting = "trusted";
1215
+ collectBase = indent;
1216
+ current = null;
1217
+ continue;
1218
+ }
1219
+ if (trimmed.startsWith("revoked:")) {
1220
+ const after = trimmed.slice("revoked:".length).trim();
1221
+ if (after === "[]")
1222
+ continue;
1223
+ collecting = "revoked";
1224
+ collectBase = indent;
1225
+ current = null;
1226
+ continue;
1227
+ }
1228
+ }
1229
+ flush();
1230
+ if (required === undefined && trusted.length === 0 && revoked.length === 0)
1231
+ return null;
1232
+ const out = {};
1233
+ if (required !== undefined)
1234
+ out.required = required;
1235
+ if (trusted.length)
1236
+ out.trusted_keys = trusted;
1237
+ if (revoked.length)
1238
+ out.revoked = revoked;
1239
+ return out;
1240
+ }
1241
+ function loadPolicy(projectDir) {
1242
+ for (const name of POLICY_FILES) {
1243
+ const file = join8(projectDir, name);
1244
+ if (!existsSync7(file))
1245
+ continue;
1246
+ const text = readFileSync6(file, "utf8");
1247
+ const raw = parsePolicyYaml(text);
1248
+ const extra = extractExtraSecretPatterns(text);
1249
+ if (extra.length) {
1250
+ if (!raw.integrity || typeof raw.integrity !== "object" || Array.isArray(raw.integrity))
1251
+ raw.integrity = {};
1252
+ raw.integrity.extra_secret_patterns = extra;
1253
+ }
1254
+ const att = extractAttestation(text);
1255
+ if (att) {
1256
+ if (!raw.attestation || typeof raw.attestation !== "object" || Array.isArray(raw.attestation))
1257
+ raw.attestation = {};
1258
+ const a = raw.attestation;
1259
+ if (att.required !== undefined)
1260
+ a.required = att.required;
1261
+ if (att.trusted_keys)
1262
+ a.trusted_keys = att.trusted_keys;
1263
+ if (att.revoked)
1264
+ a.revoked = att.revoked;
1265
+ }
1266
+ return normalize(raw);
1267
+ }
1268
+ return null;
1269
+ }
1270
+ function normalize(raw) {
1271
+ for (const k of Object.keys(raw)) {
1272
+ if (!KNOWN_ROOTS.includes(k))
1273
+ throw new Error(`unknown policy key "${k}" (known: ${KNOWN_ROOTS.join(", ")})`);
1274
+ }
1275
+ const out = {};
1276
+ const lanes = raw.lanes;
1277
+ if (lanes && Array.isArray(lanes.force_full))
1278
+ out.lanes = { force_full: strings(lanes.force_full) };
1279
+ const gates = raw.gates;
1280
+ if (gates) {
1281
+ out.gates = {};
1282
+ const cov = gates.coverage;
1283
+ if (cov) {
1284
+ out.gates.coverage = {};
1285
+ if (typeof cov.new === "number")
1286
+ out.gates.coverage.new = cov.new;
1287
+ if (typeof cov.modified === "number")
1288
+ out.gates.coverage.modified = cov.modified;
1289
+ }
1290
+ if (Array.isArray(gates.require_human_approval))
1291
+ out.gates.require_human_approval = strings(gates.require_human_approval);
1292
+ }
1293
+ const evidence = raw.evidence;
1294
+ if (evidence && Array.isArray(evidence.required))
1295
+ out.evidence = { required: strings(evidence.required) };
1296
+ const integrity = raw.integrity;
1297
+ if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
1298
+ const arr = integrity.extra_secret_patterns;
1299
+ const cleaned = [];
1300
+ for (const e of arr) {
1301
+ if (!e || typeof e !== "object")
1302
+ continue;
1303
+ const rec = e;
1304
+ if (typeof rec.pattern !== "string" || typeof rec.label !== "string")
1305
+ continue;
1306
+ const sev = rec.severity === "warn" ? "warn" : rec.severity === "block" ? "block" : undefined;
1307
+ const entry = { pattern: rec.pattern, label: rec.label };
1308
+ if (sev)
1309
+ entry.severity = sev;
1310
+ cleaned.push(entry);
1311
+ }
1312
+ if (cleaned.length)
1313
+ out.integrity = { extra_secret_patterns: cleaned };
1314
+ }
1315
+ const att = raw.attestation;
1316
+ if (att) {
1317
+ const a = {};
1318
+ if (typeof att.required === "boolean")
1319
+ a.required = att.required;
1320
+ if (Array.isArray(att.trusted_keys)) {
1321
+ const cleanedTk = [];
1322
+ for (const e of att.trusted_keys) {
1323
+ if (!e || typeof e !== "object")
1324
+ continue;
1325
+ const rec = e;
1326
+ if (typeof rec.id !== "string" || typeof rec.pubkey !== "string")
1327
+ continue;
1328
+ if (!rec.id.trim() || !rec.pubkey.trim())
1329
+ continue;
1330
+ const entry = { id: rec.id.trim(), pubkey: rec.pubkey.trim() };
1331
+ if (typeof rec.added === "string" && rec.added.trim())
1332
+ entry.added = rec.added.trim();
1333
+ cleanedTk.push(entry);
1334
+ }
1335
+ if (cleanedTk.length)
1336
+ a.trusted_keys = cleanedTk;
1337
+ }
1338
+ if (Array.isArray(att.revoked)) {
1339
+ const cleanedRv = [];
1340
+ for (const e of att.revoked) {
1341
+ if (!e || typeof e !== "object")
1342
+ continue;
1343
+ const rec = e;
1344
+ if (typeof rec.id !== "string" || !rec.id.trim())
1345
+ continue;
1346
+ const entry = { id: rec.id.trim() };
1347
+ if (typeof rec.revoked === "string" && rec.revoked.trim())
1348
+ entry.revoked = rec.revoked.trim();
1349
+ if (typeof rec.reason === "string" && rec.reason.trim())
1350
+ entry.reason = rec.reason.trim();
1351
+ if (typeof rec.pubkey === "string" && rec.pubkey.trim())
1352
+ entry.pubkey = rec.pubkey.trim();
1353
+ cleanedRv.push(entry);
1354
+ }
1355
+ if (cleanedRv.length)
1356
+ a.revoked = cleanedRv;
1357
+ }
1358
+ if (a.required !== undefined || a.trusted_keys || a.revoked)
1359
+ out.attestation = a;
1360
+ }
1361
+ const harness = raw.harness;
1362
+ if (harness && typeof harness.require_enforcement === "boolean") {
1363
+ out.harness = { require_enforcement: harness.require_enforcement };
1364
+ }
1365
+ return out;
1366
+ }
1367
+ function strings(a) {
1368
+ return a.filter((x) => typeof x === "string" && x.length > 0);
1369
+ }
1370
+ function detectHarness(projectDir) {
1371
+ const e = process.env;
1372
+ const has = (v) => v !== undefined && v !== "";
1373
+ if (has(e.CLAUDECODE) || has(e.CLAUDE_CODE_ENTRYPOINT) || typeof e.ANTHROPIC_MODEL === "string" && /claude/i.test(e.ANTHROPIC_MODEL))
1374
+ return "claude";
1375
+ if (has(e.OPENCODE) || has(e.OPENCODE_TOKENS_FILE))
1376
+ return "opencode";
1377
+ const candidates = [
1378
+ projectDir ? join8(projectDir, ".opencode", "config.json") : null,
1379
+ join8(process.cwd(), ".opencode", "config.json")
1380
+ ].filter(Boolean);
1381
+ for (const p of candidates) {
1382
+ try {
1383
+ if (existsSync7(p))
1384
+ return "opencode";
1385
+ } catch {}
1386
+ }
1387
+ if (has(e.CURSOR) || has(e.VSCODE_GIT_ASKPASS_NODE))
1388
+ return "cursor";
1389
+ return "unknown";
1390
+ }
1391
+ function isEnforcedHarness(projectDir) {
1392
+ return detectHarness(projectDir) === "opencode";
1393
+ }
1394
+ function getHarnessEnforcementError(projectDir) {
1395
+ let policy;
1396
+ try {
1397
+ policy = loadPolicy(projectDir);
1398
+ } catch (e) {
1399
+ throw e;
1400
+ }
1401
+ if (!policy?.harness?.require_enforcement)
1402
+ return null;
1403
+ if (isEnforcedHarness(projectDir))
1404
+ return null;
1405
+ const h = detectHarness(projectDir);
1406
+ return `harness enforcement required but current harness is rules-based only — use opencode or set harness.require_enforcement:false (detected: ${h})`;
1407
+ }
1408
+ function enforceHarnessPolicy(projectDir) {
1409
+ const err = getHarnessEnforcementError(projectDir);
1410
+ if (!err)
1411
+ return;
1412
+ console.error(`✗ ${err}`);
1413
+ process.exit(1);
1414
+ }
909
1415
 
910
1416
  // 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";
913
1417
  var SECRET_PATTERNS = [
914
- [/AKIA[0-9A-Z]{16}/, "AWS access key id"],
915
- [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
916
- [/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
917
- [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
918
- [/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
919
- [/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
920
- [/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
1418
+ [/AKIA[0-9A-Z]{16}/, "AWS access key id", "block"],
1419
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block", "block"],
1420
+ [/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token", "block"],
1421
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token", "block"],
1422
+ [/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)", "block"],
1423
+ [/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim", "block"],
1424
+ [/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment", "block"],
1425
+ [/AIza[0-9A-Za-z_-]{35}/, "Google API key", "block"],
1426
+ [/ya29\.[0-9A-Za-z_-]{20,}/, "Google OAuth token", "block"],
1427
+ [/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s@/]{4,}@[^\s/]+/i, "connection string with inline credential", "block"],
1428
+ [/\bAC[a-f0-9]{32}\b/, "Twilio account SID", "block"],
1429
+ [/\bSK[a-f0-9]{32}\b/, "Twilio API key", "block"],
1430
+ [/\bglpat-[A-Za-z0-9_-]{20,}/, "GitLab token", "block"],
1431
+ [/\bnpm_[A-Za-z0-9]{36}\b/, "npm token", "block"],
1432
+ [/\bdop_v1_[a-f0-9]{64}\b/, "DigitalOcean token", "block"],
1433
+ [/\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/, "card-number shape (verify before committing)", "warn"]
921
1434
  ];
922
1435
  var ALLOW_SECRET = "mugiwara:allow-secret";
923
- function findSecrets(body) {
1436
+ function loadExtraPatterns(projectRoot) {
1437
+ try {
1438
+ const policy = loadPolicy(projectRoot);
1439
+ const extras = policy?.integrity?.extra_secret_patterns;
1440
+ if (!extras || !Array.isArray(extras))
1441
+ return [];
1442
+ const out = [];
1443
+ for (const e of extras) {
1444
+ if (!e || typeof e.pattern !== "string" || typeof e.label !== "string")
1445
+ continue;
1446
+ const rec = e;
1447
+ const sev = rec.severity === "warn" ? "warn" : "block";
1448
+ try {
1449
+ const re = new RegExp(rec.pattern);
1450
+ out.push([re, rec.label, sev]);
1451
+ } catch {}
1452
+ }
1453
+ return out;
1454
+ } catch {
1455
+ return [];
1456
+ }
1457
+ }
1458
+ function findSecrets(body, extra) {
924
1459
  const out = [];
1460
+ const patterns = extra ? [...SECRET_PATTERNS, ...extra] : SECRET_PATTERNS;
925
1461
  for (const line of body.split(/\r?\n/)) {
926
1462
  if (line.includes(ALLOW_SECRET))
927
1463
  continue;
928
- for (const [re, label] of SECRET_PATTERNS) {
1464
+ for (const [re, label, severity] of patterns) {
929
1465
  const hit = line.match(re);
930
1466
  if (hit)
931
- out.push({ label, hit: hit[0] });
1467
+ out.push({ label, hit: hit[0], severity: severity ?? "block" });
932
1468
  }
933
1469
  }
934
1470
  return out;
935
1471
  }
936
- var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
1472
+ var TRAIL_EXTS = new Set([".md", ".json", ".sh", ".jsonl"]);
937
1473
  function trailFiles(dir) {
938
1474
  const out = [];
939
1475
  const walk = (d) => {
940
1476
  for (const e of readdirSync4(d, { withFileTypes: true })) {
941
- const p = join7(d, e.name);
1477
+ const p = join9(d, e.name);
942
1478
  if (e.isDirectory())
943
1479
  walk(p);
944
1480
  else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
@@ -966,7 +1502,7 @@ function collectPassCitedPaths(missionDir) {
966
1502
  for (const f of trailFiles(missionDir)) {
967
1503
  let body;
968
1504
  try {
969
- body = readFileSync5(f, "utf8");
1505
+ body = readFileSync7(f, "utf8");
970
1506
  } catch {
971
1507
  continue;
972
1508
  }
@@ -987,44 +1523,47 @@ function collectPassCitedPaths(missionDir) {
987
1523
  function checkTrail(missionDir, projectRoot) {
988
1524
  const issues = [];
989
1525
  const files = trailFiles(missionDir);
1526
+ const extraPatterns = loadExtraPatterns(projectRoot);
990
1527
  for (const f of files) {
991
1528
  let body;
992
1529
  try {
993
- body = readFileSync5(f, "utf8");
1530
+ body = readFileSync7(f, "utf8");
994
1531
  } catch {
995
1532
  continue;
996
1533
  }
997
1534
  for (const target10 of linkedPaths(body)) {
998
1535
  if (isAbsolute(target10))
999
1536
  continue;
1000
- const fromMission = join7(missionDir, target10);
1001
- const fromRoot = join7(projectRoot, target10);
1002
- if (!existsSync6(fromMission) && !existsSync6(fromRoot)) {
1537
+ const fromMission = join9(missionDir, target10);
1538
+ const fromRoot = join9(projectRoot, target10);
1539
+ if (!existsSync8(fromMission) && !existsSync8(fromRoot)) {
1003
1540
  issues.push({
1004
1541
  kind: "dangling-path",
1005
1542
  detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
1006
1543
  });
1007
1544
  }
1008
1545
  }
1009
- for (const { label, hit } of findSecrets(body)) {
1546
+ for (const { label, hit, severity } of findSecrets(body, extraPatterns.length ? extraPatterns : undefined)) {
1547
+ const isWarn = severity === "warn";
1010
1548
  issues.push({
1011
- kind: "secret",
1012
- detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
1549
+ kind: isWarn ? "secret-warn" : "secret",
1550
+ detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`,
1551
+ severity: isWarn ? "warn" : "block"
1013
1552
  });
1014
1553
  }
1015
1554
  }
1016
1555
  const evidencePaths = [];
1017
- const evidenceFile = join7(missionDir, "state.json");
1018
- if (existsSync6(evidenceFile)) {
1556
+ const evidenceFile = join9(missionDir, "state.json");
1557
+ if (existsSync8(evidenceFile)) {
1019
1558
  try {
1020
- const s = JSON.parse(readFileSync5(evidenceFile, "utf8"));
1559
+ const s = JSON.parse(readFileSync7(evidenceFile, "utf8"));
1021
1560
  if (Array.isArray(s.evidence)) {
1022
1561
  for (const e of s.evidence) {
1023
1562
  if (typeof e !== "string" || !e.trim())
1024
1563
  continue;
1025
1564
  evidencePaths.push(e);
1026
- const cand = join7(projectRoot, e);
1027
- if (!isAbsolute(e) && !existsSync6(cand) && !existsSync6(join7(missionDir, e))) {
1565
+ const cand = join9(projectRoot, e);
1566
+ if (!isAbsolute(e) && !existsSync8(cand) && !existsSync8(join9(missionDir, e))) {
1028
1567
  issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1029
1568
  }
1030
1569
  }
@@ -1035,14 +1574,16 @@ function checkTrail(missionDir, projectRoot) {
1035
1574
  for (const e of passCited) {
1036
1575
  if (!e.trim() || isAbsolute(e))
1037
1576
  continue;
1038
- const candMission = join7(missionDir, e);
1039
- const candRoot = join7(projectRoot, e);
1040
- const resolved = existsSync6(candMission) ? candMission : existsSync6(candRoot) ? candRoot : null;
1577
+ if (/(?:^|\/)(state|continue)(-[^\/]*)?\.json$/.test(e))
1578
+ continue;
1579
+ const candMission = join9(missionDir, e);
1580
+ const candRoot = join9(projectRoot, e);
1581
+ const resolved = existsSync8(candMission) ? candMission : existsSync8(candRoot) ? candRoot : null;
1041
1582
  if (!resolved)
1042
1583
  continue;
1043
1584
  let body;
1044
1585
  try {
1045
- body = readFileSync5(resolved, "utf8");
1586
+ body = readFileSync7(resolved, "utf8");
1046
1587
  } catch {
1047
1588
  continue;
1048
1589
  }
@@ -1057,10 +1598,45 @@ function formatIssues(issues) {
1057
1598
  `);
1058
1599
  }
1059
1600
 
1601
+ // src/check-artifacts.ts
1602
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
1603
+ import { join as join10 } from "node:path";
1604
+ var LANE_MIN = new Set(["standard", "full", "spike"]);
1605
+ function checkMissionArtifacts(missionDir) {
1606
+ const statePath = join10(missionDir, "state.json");
1607
+ if (!existsSync9(statePath)) {
1608
+ return { ok: true, missing: [], lane: null };
1609
+ }
1610
+ let lane = "unknown";
1611
+ try {
1612
+ const s = JSON.parse(readFileSyncSafe(statePath));
1613
+ if (typeof s.lane === "string")
1614
+ lane = s.lane;
1615
+ } catch {}
1616
+ if (!LANE_MIN.has(lane)) {
1617
+ return { ok: true, missing: [], lane };
1618
+ }
1619
+ const missing = [];
1620
+ if (!existsSync9(join10(missionDir, "plan.md")))
1621
+ missing.push("plan.md");
1622
+ const flowsDir = join10(missionDir, "flows");
1623
+ const hasFlows = existsSync9(flowsDir) && readdirSync5(flowsDir).length > 0;
1624
+ if (!hasFlows)
1625
+ missing.push("flows/ (no execution evidence)");
1626
+ return { ok: missing.length === 0, missing, lane };
1627
+ }
1628
+ function readFileSyncSafe(p) {
1629
+ try {
1630
+ return readFileSync8(p, "utf8");
1631
+ } catch {
1632
+ return "";
1633
+ }
1634
+ }
1635
+
1060
1636
  // src/rollback.ts
1061
1637
  import { execFileSync } from "node:child_process";
1062
- import { writeFileSync as writeFileSync5 } from "node:fs";
1063
- import { join as join8 } from "node:path";
1638
+ import { writeFileSync as writeFileSync6 } from "node:fs";
1639
+ import { join as join11 } from "node:path";
1064
1640
  function git(cwd, args) {
1065
1641
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1066
1642
  }
@@ -1099,8 +1675,8 @@ function generateRollback(projectDir, missionDir, input) {
1099
1675
  const commitsNewestFirst = [...revList].reverse();
1100
1676
  const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
1101
1677
  const body = buildRollback(input, commitsNewestFirst, filesTouched);
1102
- const file = join8(missionDir, "rollback.sh");
1103
- writeFileSync5(file, body, { mode: 493 });
1678
+ const file = join11(missionDir, "rollback.sh");
1679
+ writeFileSync6(file, body, { mode: 493 });
1104
1680
  return { file: "rollback.sh", commits: commitsNewestFirst.length };
1105
1681
  } catch {
1106
1682
  return null;
@@ -1109,8 +1685,8 @@ function generateRollback(projectDir, missionDir, input) {
1109
1685
 
1110
1686
  // src/provenance.ts
1111
1687
  import { execFileSync as execFileSync2 } from "node:child_process";
1112
- import { writeFileSync as writeFileSync6 } from "node:fs";
1113
- import { join as join9 } from "node:path";
1688
+ import { writeFileSync as writeFileSync7 } from "node:fs";
1689
+ import { join as join12 } from "node:path";
1114
1690
  function git2(cwd, args) {
1115
1691
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1116
1692
  }
@@ -1150,16 +1726,33 @@ function renderProvenanceMd(note, sha) {
1150
1726
  `) + `
1151
1727
  `;
1152
1728
  }
1153
- function attachGitNote(projectDir, branch, note) {
1729
+ function attachGitNote(projectDir, branch, note, baseSha) {
1154
1730
  try {
1155
- let sha;
1731
+ const range = baseSha ? `${baseSha}..${branch}` : branch;
1732
+ let shas = [];
1156
1733
  try {
1157
- sha = git2(projectDir, ["rev-parse", "--verify", branch]);
1734
+ const raw = git2(projectDir, ["rev-list", range]);
1735
+ shas = raw.split(`
1736
+ `).filter(Boolean);
1158
1737
  } catch {
1159
- sha = git2(projectDir, ["rev-parse", "HEAD"]);
1738
+ shas = [];
1739
+ }
1740
+ if (shas.length > 200) {
1741
+ console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
1742
+ shas = [];
1160
1743
  }
1161
- git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1162
- return { sha };
1744
+ let targets2 = shas;
1745
+ if (!targets2.length) {
1746
+ try {
1747
+ targets2 = [git2(projectDir, ["rev-parse", "--verify", branch])];
1748
+ } catch {
1749
+ targets2 = [git2(projectDir, ["rev-parse", "HEAD"])];
1750
+ }
1751
+ }
1752
+ for (const sha of targets2) {
1753
+ git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1754
+ }
1755
+ return { sha: targets2[0], count: targets2.length };
1163
1756
  } catch {
1164
1757
  return null;
1165
1758
  }
@@ -1179,13 +1772,243 @@ function blamePath(projectDir, path) {
1179
1772
  ${note}`;
1180
1773
  } catch {
1181
1774
  return `${path} @ ${sha.slice(0, 7)}
1182
- (no mugiwara provenance note on this commit)`;
1775
+ no per-commit note see .mugiwara/missions/<m>/provenance.md`;
1183
1776
  }
1184
1777
  }
1185
- function writeProvenance(projectDir, missionDir, state) {
1778
+ function writeProvenance(projectDir, missionDir, state, baseSha) {
1186
1779
  const note = buildNote(state);
1187
- const attached = attachGitNote(projectDir, state.branch, note);
1188
- writeFileSync6(join9(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1780
+ const resolvedBase = baseSha ?? (typeof state.base_sha === "string" ? state.base_sha : undefined);
1781
+ const cleanBase = resolvedBase && resolvedBase !== "unknown" ? resolvedBase : undefined;
1782
+ const attached = attachGitNote(projectDir, state.branch, note, cleanBase);
1783
+ writeFileSync7(join12(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1784
+ }
1785
+
1786
+ // src/sign.ts
1787
+ import { execFileSync as execFileSync3 } from "node:child_process";
1788
+ import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
1789
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
1790
+ import { homedir as homedir3 } from "node:os";
1791
+ import { join as join13 } from "node:path";
1792
+ function signArgs(reportPath, secretKey) {
1793
+ return ["-Sm", reportPath, "-s", secretKey];
1794
+ }
1795
+ function verifyArgs(reportPath, pubKey) {
1796
+ return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
1797
+ }
1798
+ function hasMinisign() {
1799
+ try {
1800
+ execFileSync3("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
1801
+ return true;
1802
+ } catch {
1803
+ return false;
1804
+ }
1805
+ }
1806
+ function defaultKey(flag) {
1807
+ return join13(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
1808
+ }
1809
+ function generatePureKey() {
1810
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
1811
+ const privJwk = privateKey.export({ format: "jwk" });
1812
+ const pubJwk = publicKey.export({ format: "jwk" });
1813
+ return {
1814
+ key: Buffer.from(privJwk.d, "base64url").toString("base64"),
1815
+ pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
1816
+ };
1817
+ }
1818
+ function ensurePureKey(homeDir) {
1819
+ const dir = join13(homeDir, ".mugiwara");
1820
+ const keyPath = join13(dir, "mugiwara.key");
1821
+ const pubPath = join13(dir, "mugiwara.pub");
1822
+ if (!existsSync10(keyPath) || !existsSync10(pubPath)) {
1823
+ mkdirSync6(dir, { recursive: true });
1824
+ const { key, pub } = generatePureKey();
1825
+ if (!existsSync10(keyPath)) {
1826
+ writeFileSync8(keyPath, key + `
1827
+ `, { mode: 384 });
1828
+ }
1829
+ if (!existsSync10(pubPath))
1830
+ writeFileSync8(pubPath, pub + `
1831
+ `);
1832
+ }
1833
+ try {
1834
+ chmodSync2(keyPath, 384);
1835
+ } catch {}
1836
+ return dir;
1837
+ }
1838
+ function pureSign(content, seedBase64, opts) {
1839
+ const seed = Buffer.from(seedBase64.trim(), "base64");
1840
+ if (seed.length !== 32)
1841
+ return { ok: false, message: "invalid seed (want 32B base64)" };
1842
+ const pubBuf = Buffer.from(opts.pub.trim(), "base64");
1843
+ if (pubBuf.length !== 32)
1844
+ return { ok: false, message: "invalid pub (want 32B base64)" };
1845
+ const privateKey = createPrivateKey({
1846
+ key: {
1847
+ kty: "OKP",
1848
+ crv: "Ed25519",
1849
+ d: seed.toString("base64url"),
1850
+ x: pubBuf.toString("base64url")
1851
+ },
1852
+ format: "jwk"
1853
+ });
1854
+ const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
1855
+ const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
1856
+ if (opts.outputPath)
1857
+ writeFileSync8(opts.outputPath, JSON.stringify(out, null, 2) + `
1858
+ `);
1859
+ return { ...out, ok: true };
1860
+ }
1861
+ function pureVerify(content, sig) {
1862
+ try {
1863
+ const pub = Buffer.from(sig.pub, "base64");
1864
+ if (pub.length !== 32)
1865
+ return false;
1866
+ const publicKey = createPublicKey({
1867
+ key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
1868
+ format: "jwk"
1869
+ });
1870
+ return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
1871
+ } catch {
1872
+ return false;
1873
+ }
1874
+ }
1875
+ function resolveBackend(configured, env) {
1876
+ switch (configured) {
1877
+ case "off":
1878
+ return "off";
1879
+ case "minisign":
1880
+ return env.hasMinisign ? "minisign" : "minisign-fail";
1881
+ case "pure":
1882
+ return "pure";
1883
+ case "auto":
1884
+ default:
1885
+ return env.hasMinisign && env.hasKey ? "minisign" : "pure";
1886
+ }
1887
+ }
1888
+ function configuredBackend(projectDir) {
1889
+ return readConfig(projectDir).sign;
1890
+ }
1891
+ function missionMeta(projectDir, mission) {
1892
+ let commit = "unknown";
1893
+ try {
1894
+ commit = execFileSync3("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1895
+ } catch {}
1896
+ return { commit, ts: new Date().toISOString() };
1897
+ }
1898
+ function signReport(projectDir, missionDir) {
1899
+ const report = join13(missionDir, "report.md");
1900
+ if (!existsSync10(report))
1901
+ return { ok: false, message: "no report.md to sign — archive first" };
1902
+ const mission = missionDir.split(join13(".mugiwara", "missions", "")).pop() ?? "unknown";
1903
+ const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync10(defaultKey("secret")) });
1904
+ if (backend === "off")
1905
+ return { ok: false, message: "signing disabled (sign=off)" };
1906
+ if (backend === "minisign-fail")
1907
+ return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
1908
+ if (backend === "minisign") {
1909
+ const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
1910
+ try {
1911
+ execFileSync3("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
1912
+ return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
1913
+ } catch (e) {
1914
+ return { ok: false, message: `signing failed: ${e.message}` };
1915
+ }
1916
+ }
1917
+ const dir = ensurePureKey(homedir3());
1918
+ const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.key"));
1919
+ const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.pub"));
1920
+ if (!seed || !pub)
1921
+ return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
1922
+ const content = readFileSafe(report);
1923
+ if (content === null)
1924
+ return { ok: false, message: `cannot read ${report}` };
1925
+ const { commit, ts } = missionMeta(projectDir, mission);
1926
+ const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
1927
+ if (!sig.ok)
1928
+ return { ok: false, message: `signing failed: ${sig.message}` };
1929
+ return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join13(dir, "mugiwara.key")})` };
1930
+ }
1931
+ function normalizePubkey(pk) {
1932
+ const t = pk.trim();
1933
+ return t.startsWith("ed25519:") ? t.slice("ed25519:".length).trim() : t;
1934
+ }
1935
+ function checkTrust(projectDir, signerPubB64) {
1936
+ try {
1937
+ const policy = loadPolicy(projectDir);
1938
+ const att = policy?.attestation;
1939
+ if (!att)
1940
+ return { ok: true };
1941
+ const trusted = att.trusted_keys ?? [];
1942
+ const revoked = att.revoked ?? [];
1943
+ const signer = signerPubB64.trim();
1944
+ const directlyRevoked = revoked.some((r) => {
1945
+ const rpk = r.pubkey;
1946
+ if (!rpk)
1947
+ return false;
1948
+ return normalizePubkey(rpk) === signer;
1949
+ });
1950
+ if (directlyRevoked)
1951
+ return { ok: false, message: "signature valid but signer revoked (pubkey in revoked list)" };
1952
+ if (trusted.length === 0) {
1953
+ return { ok: true };
1954
+ }
1955
+ const match = trusted.find((e) => normalizePubkey(e.pubkey) === signer);
1956
+ if (!match)
1957
+ return { ok: false, message: "signature valid but signer untrusted (pub not in trusted_keys)" };
1958
+ const revokedById = revoked.some((r) => r.id === match.id);
1959
+ if (revokedById)
1960
+ return { ok: false, message: `signature valid but signer revoked (id: ${match.id})` };
1961
+ return { ok: true };
1962
+ } catch {
1963
+ return { ok: true };
1964
+ }
1965
+ }
1966
+ function verifyReport(projectDir, missionDir) {
1967
+ const report = join13(missionDir, "report.md");
1968
+ const minisig = `${report}.minisig`;
1969
+ const mugisig = `${report}.mugisig`;
1970
+ if (!existsSync10(minisig) && !existsSync10(mugisig)) {
1971
+ return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
1972
+ }
1973
+ if (existsSync10(minisig)) {
1974
+ if (!hasMinisign())
1975
+ return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
1976
+ const pubKey = existsSync10(defaultKey("public")) ? defaultKey("public") : null;
1977
+ try {
1978
+ execFileSync3("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
1979
+ return { ok: true, message: "signature verifies against report.md (minisig)" };
1980
+ } catch {
1981
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
1982
+ }
1983
+ }
1984
+ try {
1985
+ const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
1986
+ const content = readFileSafe(report);
1987
+ if (content === null || parsed.algo !== "ed25519-pure")
1988
+ return { ok: false, message: "invalid .mugisig file" };
1989
+ if (!pureVerify(content, parsed))
1990
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
1991
+ const trust = checkTrust(projectDir, parsed.pub);
1992
+ if (!trust.ok)
1993
+ return { ok: false, message: trust.message };
1994
+ return { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" };
1995
+ } catch {
1996
+ return { ok: false, message: "invalid .mugisig file" };
1997
+ }
1998
+ }
1999
+ function readFileSafe(p) {
2000
+ try {
2001
+ return readFileSync9(p, "utf8");
2002
+ } catch {
2003
+ return null;
2004
+ }
2005
+ }
2006
+ function readFileSyncSafe2(p) {
2007
+ try {
2008
+ return readFileSync9(p, "utf8").trim();
2009
+ } catch {
2010
+ return "";
2011
+ }
1189
2012
  }
1190
2013
 
1191
2014
  // src/routing.ts
@@ -1242,28 +2065,15 @@ function renderRouting(ranked, mission) {
1242
2065
  }
1243
2066
 
1244
2067
  // 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";
2068
+ import { existsSync as existsSync11, readdirSync as readdirSync6, statSync } from "node:fs";
2069
+ import { join as join14 } from "node:path";
1248
2070
  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;
2071
+ const cfg = readConfig(projectDir);
2072
+ const raw = cfg.context_budget_chars;
2073
+ if (raw === undefined || raw === "")
2074
+ return 0;
2075
+ const n = Number(raw);
2076
+ return Number.isFinite(n) && n > 0 ? n : 0;
1267
2077
  }
1268
2078
  function measureContextChars(missionDir) {
1269
2079
  let total = 0;
@@ -1272,15 +2082,15 @@ function measureContextChars(missionDir) {
1272
2082
  total += statSync(p).size;
1273
2083
  } catch {}
1274
2084
  };
1275
- for (const f of readdirSync5(missionDir)) {
2085
+ for (const f of readdirSync6(missionDir)) {
1276
2086
  if (/\.md$/.test(f))
1277
- add(join10(missionDir, f));
2087
+ add(join14(missionDir, f));
1278
2088
  }
1279
2089
  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));
2090
+ const dir = join14(missionDir, sub);
2091
+ if (existsSync11(dir)) {
2092
+ for (const f of readdirSync6(dir))
2093
+ add(join14(dir, f));
1284
2094
  }
1285
2095
  }
1286
2096
  return total;
@@ -1291,6 +2101,282 @@ function formatFootprint(chars, budget) {
1291
2101
  return `${base} (no budget configured)`;
1292
2102
  return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
1293
2103
  }
2104
+ var COMPRESS_THRESHOLD_PCT = 0.8;
2105
+ function shouldCompress(budget, chars) {
2106
+ return budget > 0 && chars > Math.floor(budget * COMPRESS_THRESHOLD_PCT);
2107
+ }
2108
+ function compressThreshold(budget) {
2109
+ return Math.floor(budget * COMPRESS_THRESHOLD_PCT);
2110
+ }
2111
+
2112
+ // src/cost.ts
2113
+ import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10 } from "node:fs";
2114
+ import { join as join15 } from "node:path";
2115
+ var LANE_BUDGET = {
2116
+ lean: 12000,
2117
+ standard: 25000,
2118
+ full: 50000,
2119
+ spike: 3000
2120
+ };
2121
+ function budgetForLane(lane) {
2122
+ return LANE_BUDGET[lane] ?? 0;
2123
+ }
2124
+ function warnAt(budget) {
2125
+ return Math.floor(budget * 3 / 2);
2126
+ }
2127
+ function stopAt(budget) {
2128
+ return budget * 3;
2129
+ }
2130
+ function budgetStatus(budget, tokens) {
2131
+ if (budget > 0 && tokens >= stopAt(budget))
2132
+ return "stop";
2133
+ if (budget > 0 && tokens >= warnAt(budget))
2134
+ return "warn";
2135
+ return "ok";
2136
+ }
2137
+ function costEnvelope(state) {
2138
+ const planned = typeof state.budget === "number" && state.budget > 0 ? state.budget : budgetForLane(state.lane ?? "");
2139
+ const used = typeof state.tokens_est === "number" ? state.tokens_est : 0;
2140
+ const remaining = Math.max(planned - used, 0);
2141
+ const pct = planned > 0 ? Math.round(used / planned * 100) : 0;
2142
+ return {
2143
+ planned,
2144
+ used,
2145
+ remaining,
2146
+ pct,
2147
+ warn_at: warnAt(planned),
2148
+ stop_at: stopAt(planned),
2149
+ status: budgetStatus(planned, used)
2150
+ };
2151
+ }
2152
+ var COST_EVENTS_FILE = "cost-events.jsonl";
2153
+ function isAllowedMissionDir(dir) {
2154
+ if (!dir || dir.includes(".."))
2155
+ return false;
2156
+ if (dir.includes(".mugiwara/missions"))
2157
+ return true;
2158
+ if (dir.includes("mugiwara-"))
2159
+ return true;
2160
+ if (dir.startsWith("/tmp/") && dir.includes("-"))
2161
+ return true;
2162
+ return false;
2163
+ }
2164
+ function assertMissionDir(dir) {
2165
+ if (!isAllowedMissionDir(dir))
2166
+ throw new Error(`Invalid missionDir: ${dir}`);
2167
+ }
2168
+ function appendCostEvent(missionDir, event) {
2169
+ assertMissionDir(missionDir);
2170
+ mkdirSync7(missionDir, { recursive: true });
2171
+ const line = Object.assign({ ts: new Date().toISOString() }, event);
2172
+ appendFileSync(join15(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
2173
+ `, "utf8");
2174
+ }
2175
+ var COMPRESSED_KIND = "compressed";
2176
+
2177
+ // src/evidence.ts
2178
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
2179
+ import { join as join16 } from "node:path";
2180
+ var REGISTRY_FILE = "context-registry.jsonl";
2181
+ function isAllowedMissionDir2(dir) {
2182
+ if (!dir || dir.includes(".."))
2183
+ return false;
2184
+ if (dir.includes(".mugiwara/missions"))
2185
+ return true;
2186
+ if (dir.includes("mugiwara-"))
2187
+ return true;
2188
+ if (dir.startsWith("/tmp/") && dir.includes("-"))
2189
+ return true;
2190
+ return false;
2191
+ }
2192
+ function assertMissionDir2(dir) {
2193
+ if (!isAllowedMissionDir2(dir))
2194
+ throw new Error(`Invalid missionDir: ${dir}`);
2195
+ }
2196
+ function loadRegistry(missionDir) {
2197
+ assertMissionDir2(missionDir);
2198
+ const file = join16(missionDir, REGISTRY_FILE);
2199
+ try {
2200
+ const out = [];
2201
+ for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
2202
+ if (!line.trim())
2203
+ continue;
2204
+ let e;
2205
+ try {
2206
+ e = JSON.parse(line);
2207
+ } catch {
2208
+ continue;
2209
+ }
2210
+ if (e === null || typeof e !== "object")
2211
+ continue;
2212
+ const entry = e;
2213
+ 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;
2214
+ if (!ok)
2215
+ continue;
2216
+ entry.reads = Math.floor(entry.reads);
2217
+ out.push(entry);
2218
+ }
2219
+ return out;
2220
+ } catch {
2221
+ return [];
2222
+ }
2223
+ }
2224
+
2225
+ // src/context.ts
2226
+ function contextStatus(budgetChars, chars) {
2227
+ return budgetChars > 0 && chars > budgetChars ? "over" : "ok";
2228
+ }
2229
+ function computeContextMetrics(stats) {
2230
+ const reuse_rate = stats.reads_total > 0 ? stats.reads_reused / stats.reads_total : 0;
2231
+ const duplicate_chars = stats.total_chars - stats.unique_chars;
2232
+ return {
2233
+ files_loaded: stats.files_loaded,
2234
+ repeated_reads: stats.repeated_reads,
2235
+ duplicate_chars,
2236
+ reuse_rate,
2237
+ read_avoidance_chars: duplicate_chars
2238
+ };
2239
+ }
2240
+
2241
+ // src/reporting.ts
2242
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
2243
+ import { join as join17 } from "node:path";
2244
+ function isAllowedMissionDir3(dir) {
2245
+ if (!dir || dir.includes(".."))
2246
+ return false;
2247
+ if (dir.includes(".mugiwara/missions"))
2248
+ return true;
2249
+ if (dir.includes("mugiwara-"))
2250
+ return true;
2251
+ if (dir.startsWith("/tmp/") && dir.includes("-"))
2252
+ return true;
2253
+ return false;
2254
+ }
2255
+ function assertMissionDir3(dir) {
2256
+ if (!isAllowedMissionDir3(dir))
2257
+ throw new Error(`Invalid missionDir: ${dir}`);
2258
+ }
2259
+ function parseDecisionTrail(missionDir) {
2260
+ assertMissionDir3(missionDir);
2261
+ const file = join17(missionDir, "decisions.md");
2262
+ if (!existsSync12(file))
2263
+ return [];
2264
+ const raw = readFileSync12(file, "utf8");
2265
+ const lines = raw.split(/\r?\n/);
2266
+ const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
2267
+ const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
2268
+ const out = [];
2269
+ const bulletRe = /^-\s+(.+?)\s+—\s+(.+?):\s+(.+?)\s+—\s+reason:\s+(.+?)(?:\s+—\s+evidence:\s+(.+))?\s*$/;
2270
+ for (const line of scan) {
2271
+ const m = bulletRe.exec(line.trim());
2272
+ if (!m)
2273
+ continue;
2274
+ out.push({ ts: m[1], actor: m[2], decision: m[3], reason: m[4], ...m[5] ? { evidence: m[5] } : {} });
2275
+ }
2276
+ return out;
2277
+ }
2278
+ function loadCostEvents(missionDir) {
2279
+ assertMissionDir3(missionDir);
2280
+ const file = join17(missionDir, "cost-events.jsonl");
2281
+ if (!existsSync12(file))
2282
+ return [];
2283
+ const out = [];
2284
+ for (const line of readFileSync12(file, "utf8").split(/\r?\n/)) {
2285
+ if (!line.trim())
2286
+ continue;
2287
+ try {
2288
+ const e = JSON.parse(line);
2289
+ if (e && typeof e === "object" && typeof e.mission === "string")
2290
+ out.push(e);
2291
+ } catch {
2292
+ continue;
2293
+ }
2294
+ }
2295
+ return out;
2296
+ }
2297
+ function computeAvoidedMetrics(input) {
2298
+ const dup = input.registryMetrics?.duplicateCount ?? 0;
2299
+ const rep = input.registryMetrics?.repeatedReads ?? 0;
2300
+ const contexts_avoided = dup + rep;
2301
+ const stages_avoided = input.workMetrics?.stagesAvoided ?? 0;
2302
+ const slop_interventions = input.slopMetrics?.interventions ?? 0;
2303
+ const tokens_avoided_est = contexts_avoided * 150;
2304
+ return { stages_avoided, contexts_avoided, slop_interventions, tokens_avoided_est };
2305
+ }
2306
+ function computeEfficiencyMetrics(input) {
2307
+ const reuse_rate = input.totalReads > 0 ? Math.round(input.reuseHits / input.totalReads * 100) / 100 : 0;
2308
+ const duplicate_avoidance_chars = input.duplicateChars;
2309
+ const budget_efficiency_pct = input.budget > 0 ? Math.round(input.used / input.budget * 100) : 0;
2310
+ return { reuse_rate, duplicate_avoidance_chars, budget_efficiency_pct };
2311
+ }
2312
+ function buildCostLedger(input) {
2313
+ assertMissionDir3(input.missionDir);
2314
+ const events = loadCostEvents(input.missionDir);
2315
+ let registrySize = 0;
2316
+ let totalReads = 0;
2317
+ let reuseHits = 0;
2318
+ let duplicateChars = 0;
2319
+ try {
2320
+ const reg = loadRegistry(input.missionDir);
2321
+ registrySize = reg.length;
2322
+ totalReads = reg.reduce((s, e) => s + e.reads, 0);
2323
+ reuseHits = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
2324
+ const unique = reg.reduce((s, e) => s + (e.chars ?? 0), 0);
2325
+ const total = reg.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
2326
+ duplicateChars = total - unique;
2327
+ if (duplicateChars < 0)
2328
+ duplicateChars = 0;
2329
+ } catch {
2330
+ registrySize = 0;
2331
+ }
2332
+ const decisions = parseDecisionTrail(input.missionDir);
2333
+ const repeatedReads = reuseHits;
2334
+ const duplicateCount = duplicateChars > 0 ? 1 : 0;
2335
+ const avoided = computeAvoidedMetrics({
2336
+ registryMetrics: { duplicateCount: 0, repeatedReads: reuseHits },
2337
+ workMetrics: { stagesAvoided: input.workSummary?.stagesAvoided ?? 0 },
2338
+ slopMetrics: { interventions: input.slopSummary?.interventions ?? 0 }
2339
+ });
2340
+ if (duplicateChars > 0 && avoided.contexts_avoided === 0) {
2341
+ avoided.contexts_avoided = 1;
2342
+ avoided.tokens_avoided_est = 150;
2343
+ }
2344
+ const efficiency = computeEfficiencyMetrics({
2345
+ totalReads,
2346
+ reuseHits,
2347
+ duplicateChars,
2348
+ budget: input.envelope.planned,
2349
+ used: input.envelope.used
2350
+ });
2351
+ return {
2352
+ envelope: input.envelope,
2353
+ ledger: { events, registrySize, decisions },
2354
+ avoided,
2355
+ efficiency,
2356
+ trail: decisions
2357
+ };
2358
+ }
2359
+ function toCostJSON(ledger) {
2360
+ return JSON.stringify({ envelope: ledger.envelope, ledger: ledger.ledger, avoided: ledger.avoided, efficiency: ledger.efficiency, trail: ledger.trail }, null, 2);
2361
+ }
2362
+ function summarizeAdaptation(missionDir) {
2363
+ const trail = parseDecisionTrail(missionDir);
2364
+ const postureRe = /posture|switch|adapt|pause|parallel-workers|context-relief|phase-isolated|team-scoped/i;
2365
+ const rows = trail.filter((t) => postureRe.test(`${t.decision} ${t.reason}`)).map((t) => ({ ts: t.ts, decision: t.decision, reason: t.reason }));
2366
+ return { count: rows.length, rows };
2367
+ }
2368
+ function renderAdaptationSection(missionDir) {
2369
+ const { count, rows } = summarizeAdaptation(missionDir);
2370
+ if (count === 0)
2371
+ return "";
2372
+ const lines = ["", "## Adaptation", "", "| Time | Decision | Reason |", "|---|---|---|"];
2373
+ for (const r of rows)
2374
+ lines.push(`| ${r.ts} | ${r.decision} | ${r.reason} |`);
2375
+ if (rows.length === 0)
2376
+ lines.push("| — | (posture rows recorded in decisions.md) | |");
2377
+ return lines.join(`
2378
+ `);
2379
+ }
1294
2380
 
1295
2381
  // src/mission.ts
1296
2382
  function isStateFile(f) {
@@ -1302,18 +2388,65 @@ function primaryState(dir, files) {
1302
2388
  if (!name)
1303
2389
  return null;
1304
2390
  try {
1305
- return JSON.parse(readFileSync7(join11(dir, name), "utf8"));
2391
+ return JSON.parse(readFileSync13(join18(dir, name), "utf8"));
1306
2392
  } catch {
1307
2393
  return null;
1308
2394
  }
1309
2395
  }
2396
+ function tasksFromState(state) {
2397
+ if (!state)
2398
+ return { done: 0, total: 0 };
2399
+ const t = state.tasks;
2400
+ if (t && typeof t.done === "number" && typeof t.total === "number")
2401
+ return { done: t.done, total: t.total };
2402
+ const done = Number(state.tasks_done);
2403
+ const total = Number(state.tasks_total);
2404
+ return { done: Number.isFinite(done) ? done : 0, total: Number.isFinite(total) ? total : 0 };
2405
+ }
2406
+ function countPlanTasks(missionDir) {
2407
+ let total = 0;
2408
+ let done = 0;
2409
+ const planFile = join18(missionDir, "plan.md");
2410
+ if (existsSync13(planFile)) {
2411
+ try {
2412
+ const text = readFileSync13(planFile, "utf8");
2413
+ for (const line of text.split(/\r?\n/)) {
2414
+ if (/^\s*-\s*\[[ xX]\]/.test(line)) {
2415
+ total++;
2416
+ if (/^\s*-\s*\[[xX]\]/.test(line))
2417
+ done++;
2418
+ }
2419
+ }
2420
+ } catch {}
2421
+ }
2422
+ if (total === 0) {
2423
+ const subPlanDir = join18(missionDir, "sub-plan");
2424
+ if (existsSync13(subPlanDir)) {
2425
+ try {
2426
+ for (const f of readdirSync7(subPlanDir)) {
2427
+ if (!f.endsWith(".md"))
2428
+ continue;
2429
+ const text = readFileSync13(join18(subPlanDir, f), "utf8");
2430
+ for (const line of text.split(/\r?\n/)) {
2431
+ if (/^\s*-\s*\[[ xX]\]/.test(line)) {
2432
+ total++;
2433
+ if (/^\s*-\s*\[[xX]\]/.test(line))
2434
+ done++;
2435
+ }
2436
+ }
2437
+ }
2438
+ } catch {}
2439
+ }
2440
+ }
2441
+ return { done, total };
2442
+ }
1310
2443
  function changedFiles(projectDir, state) {
1311
2444
  const base = typeof state?.base_sha === "string" ? state.base_sha : "";
1312
2445
  const branch = typeof state?.branch === "string" ? state.branch : "";
1313
2446
  if (!base || base === "unknown" || !branch)
1314
2447
  return [];
1315
2448
  try {
1316
- return execFileSync3("git", ["diff", "--name-only", base, branch], {
2449
+ return execFileSync4("git", ["diff", "--name-only", base, branch], {
1317
2450
  cwd: projectDir,
1318
2451
  encoding: "utf8",
1319
2452
  stdio: ["ignore", "pipe", "ignore"]
@@ -1323,16 +2456,16 @@ function changedFiles(projectDir, state) {
1323
2456
  }
1324
2457
  }
1325
2458
  function activeActor(projectDir) {
1326
- const missionsDir = join11(projectDir, ".mugiwara", "missions");
1327
- if (!existsSync8(missionsDir))
2459
+ const missionsDir = join18(projectDir, ".mugiwara", "missions");
2460
+ if (!existsSync13(missionsDir))
1328
2461
  return null;
1329
- const missions = readdirSync6(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
2462
+ const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1330
2463
  let latest = null;
1331
2464
  for (const mission of missions) {
1332
- const d = join11(missionsDir, mission);
1333
- for (const f of readdirSync6(d).filter(isStateFile)) {
2465
+ const d = join18(missionsDir, mission);
2466
+ for (const f of readdirSync7(d).filter(isStateFile)) {
1334
2467
  try {
1335
- const s = JSON.parse(readFileSync7(join11(d, f), "utf8"));
2468
+ const s = JSON.parse(readFileSync13(join18(d, f), "utf8"));
1336
2469
  const t = Date.parse(s.updated_at || "") || 0;
1337
2470
  if (!latest || t > latest.updated)
1338
2471
  latest = { actor: s.actor || null, updated: t };
@@ -1342,8 +2475,8 @@ function activeActor(projectDir) {
1342
2475
  return latest ? latest.actor : null;
1343
2476
  }
1344
2477
  function resetMission(projectDir, keepLogs, force) {
1345
- const root = join11(projectDir, ".mugiwara");
1346
- if (!existsSync8(root))
2478
+ const root = join18(projectDir, ".mugiwara");
2479
+ if (!existsSync13(root))
1347
2480
  return { removed: [], kept: [] };
1348
2481
  if (!force) {
1349
2482
  const actor = activeActor(projectDir);
@@ -1354,62 +2487,93 @@ function resetMission(projectDir, keepLogs, force) {
1354
2487
  const removed = [];
1355
2488
  const kept = [];
1356
2489
  for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
1357
- const p = join11(root, dir);
1358
- if (existsSync8(p)) {
2490
+ const p = join18(root, dir);
2491
+ if (existsSync13(p)) {
1359
2492
  rmSync2(p, { recursive: true, force: true });
1360
2493
  removed.push(dir);
1361
2494
  }
1362
2495
  }
1363
- for (const f of readdirSync6(root)) {
2496
+ for (const f of readdirSync7(root)) {
1364
2497
  if (/^state(-.+)?\.json$/.test(f)) {
1365
- const p = join11(root, f);
1366
- if (existsSync8(p)) {
2498
+ const p = join18(root, f);
2499
+ if (existsSync13(p)) {
1367
2500
  rmSync2(p);
1368
2501
  removed.push(f);
1369
2502
  }
1370
2503
  }
1371
2504
  }
1372
2505
  if (!keepLogs) {
1373
- for (const p of [join11(root, "lessons.md"), join11(root, "logs")]) {
1374
- if (existsSync8(p)) {
2506
+ for (const p of [join18(root, "lessons.md"), join18(root, "logs")]) {
2507
+ if (existsSync13(p)) {
1375
2508
  rmSync2(p, { recursive: true, force: true });
1376
- removed.push(p.startsWith(join11(root, "logs")) ? "logs" : "lessons.md");
2509
+ removed.push(p.startsWith(join18(root, "logs")) ? "logs" : "lessons.md");
1377
2510
  }
1378
2511
  }
1379
2512
  } else {
1380
- if (existsSync8(join11(root, "lessons.md")))
2513
+ if (existsSync13(join18(root, "lessons.md")))
1381
2514
  kept.push("lessons.md");
1382
- else if (existsSync8(join11(root, join11("logs", "lessons.md"))))
1383
- kept.push(join11("logs", "lessons.md"));
2515
+ else if (existsSync13(join18(root, join18("logs", "lessons.md"))))
2516
+ kept.push(join18("logs", "lessons.md"));
2517
+ }
2518
+ if (removed.includes("missions") && existsSync13(join18(root, "index.md"))) {
2519
+ rmSync2(join18(root, "index.md"));
2520
+ removed.push("index.md");
1384
2521
  }
1385
2522
  for (const f of ["config", "manifest.json", "backup"]) {
1386
- if (existsSync8(join11(root, f)))
2523
+ if (existsSync13(join18(root, f)))
1387
2524
  kept.push(f);
1388
2525
  }
1389
2526
  return { removed, kept };
1390
2527
  }
1391
2528
  function archiveMission(projectDir, mission, opts = {}) {
1392
2529
  const { dryRun = false } = opts;
1393
- const root = join11(projectDir, ".mugiwara");
2530
+ const root = join18(projectDir, ".mugiwara");
1394
2531
  if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
1395
2532
  throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
1396
2533
  const removed = [];
1397
2534
  const kept = [];
1398
- const dir = join11(root, "missions", mission);
1399
- if (!existsSync8(dir))
2535
+ const dir = join18(root, "missions", mission);
2536
+ if (!existsSync13(dir))
1400
2537
  return { report: null, removed, kept };
1401
2538
  if (!dryRun) {
1402
2539
  const issues = checkTrail(dir, projectDir);
1403
- if (issues.length) {
2540
+ const blocking = issues.filter((i) => i.kind !== "secret-warn" && i.severity !== "warn");
2541
+ if (blocking.length) {
1404
2542
  throw new Error(`closure integrity gate failed — fix these before archiving:
1405
- ${formatIssues(issues)}`);
2543
+ ${formatIssues(blocking)}`);
2544
+ }
2545
+ if (issues.length && blocking.length === 0) {
2546
+ try {
2547
+ const warnText = formatIssues(issues);
2548
+ if (warnText)
2549
+ console.warn(`closure integrity warnings (non-blocking):
2550
+ ${warnText}`);
2551
+ } catch {}
2552
+ }
2553
+ const artifacts = checkMissionArtifacts(dir);
2554
+ if (!artifacts.ok) {
2555
+ throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
2556
+ }
2557
+ try {
2558
+ const policy = loadPolicy(projectDir);
2559
+ if (policy?.attestation?.required) {
2560
+ const v = verifyReport(projectDir, dir);
2561
+ if (!v.ok) {
2562
+ throw new Error(`closure integrity gate failed — attestation required but report not signed/trusted: ${v.message}`);
2563
+ }
2564
+ }
2565
+ } catch (e) {
2566
+ if (e.message.startsWith("closure integrity gate failed — attestation required"))
2567
+ throw e;
2568
+ if (e.message.startsWith("unknown policy key"))
2569
+ throw e;
1406
2570
  }
1407
2571
  }
1408
- const files = readdirSync6(dir);
2572
+ const files = readdirSync7(dir);
1409
2573
  const state = primaryState(dir, files);
1410
2574
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
1411
2575
  try {
1412
- const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
2576
+ const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
1413
2577
  return typeof s.model === "string" ? s.model : "";
1414
2578
  } catch {
1415
2579
  return "";
@@ -1420,22 +2584,36 @@ ${formatIssues(issues)}`);
1420
2584
  const chars = measureContextChars(dir);
1421
2585
  const budget = readBudgetConfig(projectDir);
1422
2586
  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
2587
  const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
1427
2588
  const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
1428
2589
  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;
2590
+ const laneBudget = budgetForLane(lane);
2591
+ const env = costEnvelope({ lane, budget: laneBudget, tokens_est: est });
1430
2592
  const effBudget = budget || laneBudget;
1431
- const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
1432
2593
  const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
1433
2594
  const srcLabel = src === "reported" ? "provider-reported" : "estimator";
2595
+ const statusLabel = env.status.toUpperCase();
2596
+ const ctxStatus = contextStatus(budget, chars);
2597
+ const registry = loadRegistry(dir);
2598
+ const reads_total = registry.reduce((s, e) => s + e.reads, 0);
2599
+ const repeated_reads = registry.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
2600
+ const unique_chars = registry.reduce((s, e) => s + (e.chars ?? 0), 0);
2601
+ const total_chars = registry.reduce((s, e) => s + (e.chars ?? 0) * e.reads, 0);
2602
+ const charTracked = registry.length > 0 && total_chars > 0;
2603
+ const metrics = registry.length ? computeContextMetrics({
2604
+ files_loaded: registry.length,
2605
+ reads_total,
2606
+ reads_reused: repeated_reads,
2607
+ unique_chars,
2608
+ total_chars,
2609
+ repeated_reads
2610
+ }) : { files_loaded: 0, repeated_reads: 0, duplicate_chars: 0, reuse_rate: 0, read_avoidance_chars: 0 };
2611
+ const ctxNote = registry.length ? charTracked ? "" : " (char data not tracked)" : " (no registry — reads not tracked)";
1434
2612
  let reportedTotal = 0;
1435
2613
  let hasReported = false;
1436
2614
  for (const f of files.filter(isStateFile)) {
1437
2615
  try {
1438
- const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
2616
+ const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
1439
2617
  if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
1440
2618
  reportedTotal += s.tokens_est;
1441
2619
  hasReported = true;
@@ -1452,17 +2630,92 @@ ${formatIssues(issues)}`);
1452
2630
  "| Metric | Value |",
1453
2631
  "|--------|-------|",
1454
2632
  `| **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)"} |`
2633
+ `| **Lane** | ${lane} (budget ${effBudget ? effBudget.toLocaleString() : "—"} · warn ${effBudget ? env.warn_at.toLocaleString() : "—"} · stop ${effBudget ? env.stop_at.toLocaleString() : "—"}) |`,
2634
+ `| **Budget status** | ${effBudget ? `${env.pct}% of budget · ${delta} · ${statusLabel}` : "no lane budget"} |`,
2635
+ `| **Context footprint** | ${chars.toLocaleString()} chars${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
2636
+ `| **Context budget status** | ${ctxStatus.toUpperCase()}${budget ? ` (budget ${budget.toLocaleString()})` : " (no context budget configured)"} |`,
2637
+ `| **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
2638
  ].join(`
1459
2639
  `);
1460
2640
  if (hasReported) {
1461
2641
  costSection += `
1462
2642
  | **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
1463
2643
  }
2644
+ try {
2645
+ const ledger = buildCostLedger({ missionDir: dir, envelope: env });
2646
+ costSection += `
2647
+ | Budget | ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned}) |`;
2648
+ costSection += `
2649
+ | Context | ${chars.toLocaleString()} chars, reuse ${ledger.efficiency.reuse_rate} |`;
2650
+ costSection += `
2651
+ | Avoided | ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ${ledger.avoided.tokens_avoided_est} tokens est |`;
2652
+ costSection += `
2653
+ | Efficiency | reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}% |`;
2654
+ costSection += `
2655
+ | Trail | ${ledger.trail.length} decisions |`;
2656
+ if (ledger.trail.length) {
2657
+ const show = ledger.trail.slice(0, 5);
2658
+ for (const t of show)
2659
+ costSection += `
2660
+ - ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`;
2661
+ if (ledger.trail.length > 5)
2662
+ costSection += `
2663
+ … ${ledger.trail.length - 5} more`;
2664
+ }
2665
+ } catch {}
1464
2666
  costSection += `
1465
2667
  `;
2668
+ try {
2669
+ costSection += renderAdaptationSection(dir);
2670
+ } catch {}
2671
+ if (shouldCompress(budget, chars)) {
2672
+ try {
2673
+ const flowsDir2 = join18(dir, "flows");
2674
+ const wavesDir = join18(dir, "waves");
2675
+ const targetDir = existsSync13(flowsDir2) ? flowsDir2 : existsSync13(wavesDir) ? wavesDir : null;
2676
+ if (targetDir && existsSync13(targetDir)) {
2677
+ const flowFiles = readdirSync7(targetDir).filter((f) => f.endsWith(".md"));
2678
+ if (flowFiles.length) {
2679
+ const pct = Math.round(chars / budget * 100);
2680
+ const stub = `# Compressed trail
2681
+
2682
+ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressThreshold(budget)}) — flows archived as stub to preserve budget. Original flows: ${flowFiles.join(", ")}
2683
+ `;
2684
+ for (const f of flowFiles) {
2685
+ try {
2686
+ rmSync2(join18(targetDir, f), { force: true });
2687
+ } catch {}
2688
+ }
2689
+ writeFileSync9(join18(targetDir, "00-compressed.md"), stub);
2690
+ }
2691
+ }
2692
+ appendCostEvent(dir, {
2693
+ kind: COMPRESSED_KIND,
2694
+ mission,
2695
+ tokens_est: est,
2696
+ budget: laneBudget,
2697
+ status: "compressed",
2698
+ context_chars: chars,
2699
+ context_status: ctxStatus,
2700
+ context_metrics: metrics
2701
+ });
2702
+ costSection += `
2703
+ | **Compressed** | yes — ${chars} chars >80% of ${budget} — flows stubbed as 00-compressed.md |`;
2704
+ } catch {}
2705
+ }
2706
+ appendCostEvent(dir, {
2707
+ kind: "closure",
2708
+ mission,
2709
+ tokens_est: est,
2710
+ budget: laneBudget,
2711
+ status: env.status,
2712
+ context_chars: chars,
2713
+ context_status: ctxStatus,
2714
+ context_metrics: metrics
2715
+ });
2716
+ if (budget && chars > budget) {
2717
+ throw new Error(`closure context budget failed — ${footprintLine}. Trim the trail or raise context_budget_chars.`);
2718
+ }
1466
2719
  }
1467
2720
  const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
1468
2721
  const fold = [];
@@ -1470,25 +2723,40 @@ ${formatIssues(issues)}`);
1470
2723
  if (files.includes(f))
1471
2724
  fold.push(f);
1472
2725
  }
1473
- const flowsDir = join11(dir, "flows");
1474
- const legacyWavesDir = join11(dir, "waves");
1475
- const artDir = existsSync8(flowsDir) ? flowsDir : existsSync8(legacyWavesDir) ? legacyWavesDir : flowsDir;
2726
+ const PR_VERDICT = "pr-verdict.md";
2727
+ const PR_VERDICT_SRC = join18("flows", "07-pr-verdict.md");
2728
+ const flowsDir = join18(dir, "flows");
2729
+ const legacyWavesDir = join18(dir, "waves");
2730
+ const artDir = existsSync13(flowsDir) ? flowsDir : existsSync13(legacyWavesDir) ? legacyWavesDir : flowsDir;
1476
2731
  const artRel = artDir === legacyWavesDir ? "waves" : "flows";
1477
- if (existsSync8(artDir)) {
1478
- for (const f of readdirSync6(artDir).sort())
1479
- fold.push(join11(artRel, f));
2732
+ if (existsSync13(artDir)) {
2733
+ for (const f of readdirSync7(artDir).sort()) {
2734
+ if (artRel === "flows" && f === "07-pr-verdict.md")
2735
+ continue;
2736
+ fold.push(join18(artRel, f));
2737
+ }
1480
2738
  }
2739
+ if (existsSync13(join18(dir, "cost-events.jsonl")))
2740
+ fold.push("cost-events.jsonl");
2741
+ if (existsSync13(join18(dir, "context-registry.jsonl")))
2742
+ fold.push("context-registry.jsonl");
1481
2743
  let report = "";
1482
- const reportPath = join11(dir, "report.md");
2744
+ const reportPath = join18(dir, "report.md");
1483
2745
  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");
2746
+ report = readFileSync13(reportPath, "utf8");
2747
+ else if (existsSync13(join18(artDir, "06-closure.md")))
2748
+ report = readFileSync13(join18(artDir, "06-closure.md"), "utf8");
1487
2749
  if (!dryRun) {
1488
- mkdirSync5(dir, { recursive: true });
2750
+ mkdirSync9(dir, { recursive: true });
2751
+ const prVerdictSrc = join18(dir, PR_VERDICT_SRC);
2752
+ if (existsSync13(prVerdictSrc)) {
2753
+ const prVerdictPath = join18(dir, PR_VERDICT);
2754
+ writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
2755
+ kept.push(join18("missions", mission, PR_VERDICT));
2756
+ }
1489
2757
  if (fold.length) {
1490
2758
  const sections = fold.map((f) => {
1491
- const body = readFileSync7(join11(dir, f), "utf8").trim();
2759
+ const body = readFileSync13(join18(dir, f), "utf8").trim();
1492
2760
  const name = f.includes("/") ? f.split("/").pop() ?? f : f;
1493
2761
  return `
1494
2762
 
@@ -1502,22 +2770,26 @@ ${body}`;
1502
2770
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1503
2771
  sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
1504
2772
  }), mission) : "";
1505
- writeFileSync7(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
2773
+ writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1506
2774
  ${costSection}
1507
2775
  ` : "") + `
1508
2776
  `);
1509
2777
  renameSync(tmp, reportPath);
1510
2778
  }
1511
2779
  for (const f of fold) {
1512
- rmSync2(join11(dir, f), { force: true, recursive: true });
1513
- removed.push(join11("missions", mission, f));
2780
+ rmSync2(join18(dir, f), { force: true, recursive: true });
2781
+ removed.push(join18("missions", mission, f));
2782
+ }
2783
+ if (existsSync13(prVerdictSrc)) {
2784
+ rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
2785
+ removed.push(join18("missions", mission, PR_VERDICT_SRC));
1514
2786
  }
1515
2787
  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)
2788
+ rmSync2(join18(dir, f), { force: true });
2789
+ if (existsSync13(artDir) && readdirSync7(artDir).length === 0)
1518
2790
  rmSync2(artDir, { recursive: true, force: true });
1519
- if (!existsSync8(reportPath)) {
1520
- writeFileSync7(reportPath, `# Mission: ${mission}
2791
+ if (!existsSync13(reportPath)) {
2792
+ writeFileSync9(reportPath, `# Mission: ${mission}
1521
2793
 
1522
2794
  Archived before closure — no wave artifacts were present.
1523
2795
  `);
@@ -1529,38 +2801,44 @@ Archived before closure — no wave artifacts were present.
1529
2801
  baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
1530
2802
  });
1531
2803
  if (rb)
1532
- kept.push(join11("missions", mission, rb.file));
2804
+ kept.push(join18("missions", mission, rb.file));
1533
2805
  try {
2806
+ const st = tasksFromState(state);
2807
+ const pt = countPlanTasks(dir);
2808
+ const tasks_done = pt.total > 0 ? pt.done : st.done;
2809
+ const tasks_total = pt.total > 0 ? pt.total : st.total;
2810
+ const baseShaForNote = typeof state.base_sha === "string" ? state.base_sha : undefined;
1534
2811
  writeProvenance(projectDir, dir, {
1535
2812
  mission,
1536
2813
  actor: typeof state.actor === "string" ? state.actor : "",
1537
2814
  lane: typeof state.lane === "string" ? state.lane : "",
1538
2815
  mode: typeof state.mode === "string" ? state.mode : "",
1539
2816
  branch: state.branch,
1540
- tasks_done: Number(state.tasks_done) || 0,
1541
- tasks_total: Number(state.tasks_total) || 0,
2817
+ tasks_done,
2818
+ tasks_total,
1542
2819
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1543
- models: stageModels
1544
- });
1545
- kept.push(join11("missions", mission, "provenance.md"));
2820
+ models: stageModels,
2821
+ base_sha: baseShaForNote
2822
+ }, baseShaForNote);
2823
+ kept.push(join18("missions", mission, "provenance.md"));
1546
2824
  } catch {}
1547
2825
  }
1548
2826
  } else {
1549
2827
  for (const f of fold)
1550
- removed.push(join11("missions", mission, f));
2828
+ removed.push(join18("missions", mission, f));
1551
2829
  }
1552
- removed.push(join11("missions", mission, "<session state>"));
2830
+ removed.push(join18("missions", mission, "<session state>"));
1553
2831
  if (files.includes("plan.md"))
1554
- kept.push(join11("missions", mission, "plan.md"));
2832
+ kept.push(join18("missions", mission, "plan.md"));
1555
2833
  if (files.includes("handoff.md"))
1556
- kept.push(join11("missions", mission, "handoff.md"));
1557
- kept.push(join11("missions", mission, "report.md"));
2834
+ kept.push(join18("missions", mission, "handoff.md"));
2835
+ kept.push(join18("missions", mission, "report.md"));
1558
2836
  let index;
1559
- const indexFile = join11(root, "index.md");
2837
+ const indexFile = join18(root, "index.md");
1560
2838
  const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
1561
2839
  `;
1562
2840
  if (!dryRun) {
1563
- const existing = existsSync8(indexFile) ? readFileSync7(indexFile, "utf8") : "";
2841
+ const existing = existsSync13(indexFile) ? readFileSync13(indexFile, "utf8") : "";
1564
2842
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
1565
2843
  const header = existing ? "" : `# Mission index
1566
2844
 
@@ -1574,28 +2852,28 @@ Archived before closure — no wave artifacts were present.
1574
2852
  }
1575
2853
  index = "index.md";
1576
2854
  }
1577
- return { report: join11("missions", mission, "report.md"), removed, kept, index };
2855
+ return { report: join18("missions", mission, "report.md"), removed, kept, index };
1578
2856
  }
1579
2857
 
1580
2858
  // src/run.ts
1581
- import { existsSync as existsSync9, readdirSync as readdirSync7 } from "node:fs";
2859
+ import { existsSync as existsSync14, readdirSync as readdirSync8 } from "node:fs";
1582
2860
  import { spawnSync } from "node:child_process";
1583
- import { dirname as dirname5, join as join12, resolve } from "node:path";
2861
+ import { dirname as dirname5, join as join19, resolve } from "node:path";
1584
2862
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1585
2863
  var here3 = dirname5(fileURLToPath4(import.meta.url));
1586
- var SCRIPTS_DIR = join12(here3, "..", "scripts");
2864
+ var SCRIPTS_DIR = join19(here3, "..", "scripts");
1587
2865
  var RUNNABLE = ["savepoint.sh", "lane.sh"];
1588
2866
  function findBash() {
1589
2867
  const explicit = process.env.MUGIWARA_BASH?.trim();
1590
2868
  if (explicit)
1591
- return existsSync9(explicit) ? explicit : null;
2869
+ return existsSync14(explicit) ? explicit : null;
1592
2870
  const candidates = process.platform === "win32" ? [
1593
2871
  "C:\\Program Files\\Git\\bin\\bash.exe",
1594
2872
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
1595
- join12(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2873
+ join19(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
1596
2874
  ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
1597
2875
  for (const p of candidates) {
1598
- if (p && existsSync9(p))
2876
+ if (p && existsSync14(p))
1599
2877
  return p;
1600
2878
  }
1601
2879
  const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
@@ -1612,9 +2890,9 @@ function runScript(name, args, projectDir) {
1612
2890
  if (!/^[a-z0-9-]+\.sh$/.test(name)) {
1613
2891
  throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
1614
2892
  }
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")) : [];
2893
+ const script = join19(SCRIPTS_DIR, name);
2894
+ if (!existsSync14(script)) {
2895
+ const have = existsSync14(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
1618
2896
  throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
1619
2897
  }
1620
2898
  const bash = findBash();
@@ -1631,9 +2909,30 @@ function runScript(name, args, projectDir) {
1631
2909
  }
1632
2910
 
1633
2911
  // src/continue.ts
1634
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync8 } from "node:fs";
1635
- import { execFileSync as execFileSync4 } from "node:child_process";
1636
- import { join as join13 } from "node:path";
2912
+ import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "node:fs";
2913
+ import { execFileSync as execFileSync5 } from "node:child_process";
2914
+ import { join as join20 } from "node:path";
2915
+ var CURRENT_SCHEMA_VERSION = 2;
2916
+ function hasLegacyLayout(projectDir) {
2917
+ for (const legacy of [join20(projectDir, ".mugiwara", "state"), join20(projectDir, ".mugiwara", "continue")]) {
2918
+ if (!existsSync15(legacy))
2919
+ continue;
2920
+ try {
2921
+ const walk = (dir) => {
2922
+ for (const e of readdirSync9(dir, { withFileTypes: true })) {
2923
+ if (e.isFile() && e.name.endsWith(".json"))
2924
+ return true;
2925
+ if (e.isDirectory() && walk(join20(dir, e.name)))
2926
+ return true;
2927
+ }
2928
+ return false;
2929
+ };
2930
+ if (walk(legacy))
2931
+ return true;
2932
+ } catch {}
2933
+ }
2934
+ return false;
2935
+ }
1637
2936
  var SAFE = /^[A-Za-z0-9._-]+$/;
1638
2937
  var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
1639
2938
  var num = (v) => {
@@ -1650,7 +2949,7 @@ function gitActor(cwd) {
1650
2949
  return author;
1651
2950
  const git3 = (args) => {
1652
2951
  try {
1653
- return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2952
+ return execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1654
2953
  } catch {
1655
2954
  return "";
1656
2955
  }
@@ -1662,14 +2961,14 @@ function gitActor(cwd) {
1662
2961
  return name || process.env.USER || process.env.USERNAME || "";
1663
2962
  }
1664
2963
  function scan(projectDir, kind, map) {
1665
- const base = join13(projectDir, ".mugiwara", "missions");
1666
- if (!existsSync10(base))
2964
+ const base = join20(projectDir, ".mugiwara", "missions");
2965
+ if (!existsSync15(base))
1667
2966
  return [];
1668
2967
  const out = [];
1669
- const missions = readdirSync8(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
2968
+ const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
1670
2969
  for (const mission of missions) {
1671
- const dir = join13(base, mission);
1672
- for (const f of readdirSync8(dir).filter((n) => n.endsWith(".json")).sort()) {
2970
+ const dir = join20(base, mission);
2971
+ for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
1673
2972
  const stem = f.slice(0, -".json".length);
1674
2973
  let member;
1675
2974
  if (kind === "state") {
@@ -1687,7 +2986,7 @@ function scan(projectDir, kind, map) {
1687
2986
  if (member !== null && !isSafeKey(member))
1688
2987
  continue;
1689
2988
  try {
1690
- const raw = JSON.parse(readFileSync8(join13(dir, f), "utf8"));
2989
+ const raw = JSON.parse(readFileSync14(join20(dir, f), "utf8"));
1691
2990
  if (text(raw.mission) !== mission)
1692
2991
  continue;
1693
2992
  out.push(map(raw, member));
@@ -1715,6 +3014,7 @@ function readContinue(projectDir) {
1715
3014
  function readState(projectDir) {
1716
3015
  return scan(projectDir, "state", (r, member) => {
1717
3016
  const tasks = r.tasks ?? {};
3017
+ const sv = r.schema_version;
1718
3018
  return {
1719
3019
  mission: text(r.mission),
1720
3020
  member,
@@ -1744,7 +3044,8 @@ function readState(projectDir) {
1744
3044
  budget: num(r.budget),
1745
3045
  budget_status: text(r.budget_status) || "ok",
1746
3046
  files_touched: num(r.files_touched),
1747
- evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : []
3047
+ evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : [],
3048
+ schema_version: typeof sv === "number" || typeof sv === "string" ? sv : null
1748
3049
  };
1749
3050
  });
1750
3051
  }
@@ -1802,57 +3103,31 @@ function formatResume(e) {
1802
3103
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
1803
3104
  }
1804
3105
 
1805
- // src/sign.ts
1806
- import { execFileSync as execFileSync5 } from "node:child_process";
1807
- import { existsSync as existsSync11 } from "node:fs";
1808
- import { homedir as homedir3 } from "node:os";
1809
- import { join as join14 } from "node:path";
1810
- function signArgs(reportPath, secretKey) {
1811
- return ["-Sm", reportPath, "-s", secretKey];
1812
- }
1813
- function verifyArgs(reportPath, pubKey) {
1814
- return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
1815
- }
1816
- function hasMinisign() {
1817
- try {
1818
- execFileSync5("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
1819
- return true;
1820
- } catch {
1821
- return false;
1822
- }
1823
- }
1824
- function defaultKey(flag) {
1825
- return join14(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
1826
- }
1827
- function signReport(projectDir, missionDir) {
1828
- const report = join14(missionDir, "report.md");
1829
- if (!existsSync11(report))
1830
- 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)" };
1833
- }
1834
- const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
1835
- 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}` };
1840
- }
1841
- }
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;
1850
- try {
1851
- execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
1852
- return { ok: true, message: "signature verifies against report.md" };
1853
- } catch {
1854
- return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing" };
1855
- }
3106
+ // src/slop.ts
3107
+ function detectHealingSlop(input) {
3108
+ const kind = "healing";
3109
+ const max = input.max_cycles ?? 3;
3110
+ const hasZeroHistory = input.history_fixes.some((n) => n === 0);
3111
+ if (input.fixes_in_cycle === 0 && hasZeroHistory) {
3112
+ return { slop: true, reason: `slop: healing — no fixes in cycle ${input.cycle} with previous zero-fix cycle`, kind };
3113
+ }
3114
+ if (input.cycle >= max && input.fixes_in_cycle === 0) {
3115
+ return { slop: true, reason: `slop: healing cycle ${input.cycle} ≥ max ${max} with no fixes`, kind };
3116
+ }
3117
+ return { slop: false, reason: "no slop — healing making progress", kind };
3118
+ }
3119
+ function computeLiveSlop(input) {
3120
+ const rows = [];
3121
+ const thr = input.repeated_read_threshold ?? 3;
3122
+ const heal = detectHealingSlop({ cycle: input.heal_cycle, fixes_in_cycle: 0, history_fixes: [], max_cycles: input.max_heal_cycles ?? 3 });
3123
+ if (heal.slop)
3124
+ rows.push({ role: "Brook", kind: "healing", reason: heal.reason });
3125
+ if (input.repeated_reads >= thr)
3126
+ rows.push({ role: "all", kind: "context", reason: `repeated reads ${input.repeated_reads} ${thr}` });
3127
+ const perRole = {};
3128
+ for (const r of rows)
3129
+ perRole[r.role] = (perRole[r.role] ?? 0) + 1;
3130
+ return { interventions: rows.length, perRole, rows };
1856
3131
  }
1857
3132
 
1858
3133
  // src/cli.ts
@@ -1866,6 +3141,23 @@ async function run(argv) {
1866
3141
  console.log(`mugiwara ${VERSION}`);
1867
3142
  return;
1868
3143
  }
3144
+ {
3145
+ const bypass = new Set(["install", "update", "uninstall", "list"]);
3146
+ if (!bypass.has(command)) {
3147
+ const projectDirForHarness = resolve2(str(flags.project) ?? process.cwd());
3148
+ enforceHarnessPolicy(projectDirForHarness);
3149
+ }
3150
+ }
3151
+ const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
3152
+ if (!isDryRunInstall) {
3153
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
3154
+ if (ensureConfig(projectDir)) {
3155
+ console.log(`default .mugiwara/config written at ${join21(projectDir, ".mugiwara", "config")} (edit it to customise)`);
3156
+ }
3157
+ }
3158
+ if (command === "continue" || command === "status") {
3159
+ return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
3160
+ }
1869
3161
  switch (command) {
1870
3162
  case "install":
1871
3163
  return install(flags);
@@ -1885,6 +3177,8 @@ async function run(argv) {
1885
3177
  return continueCmd(flags, _);
1886
3178
  case "status":
1887
3179
  return statusCmd(flags);
3180
+ case "cost":
3181
+ return costCmd(flags, _);
1888
3182
  case "run":
1889
3183
  return runCmd(flags, _);
1890
3184
  case "savepoint":
@@ -1895,6 +3189,8 @@ async function run(argv) {
1895
3189
  return handoffCmd(flags, _);
1896
3190
  case "sign":
1897
3191
  return signCmd(flags, _);
3192
+ case "migrate":
3193
+ return migrateCmd(flags);
1898
3194
  default:
1899
3195
  throw new Error(`Unknown command: ${command}`);
1900
3196
  }
@@ -1936,8 +3232,8 @@ function archive(flags, positionals) {
1936
3232
  function cleanCmd(flags) {
1937
3233
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1938
3234
  const dryRun = flag(flags.dryRun);
1939
- const root = join15(projectDir, ".mugiwara", "missions");
1940
- if (!existsSync12(root)) {
3235
+ const root = join21(projectDir, ".mugiwara", "missions");
3236
+ if (!existsSync16(root)) {
1941
3237
  console.log("nothing to clean (.mugiwara/missions/ does not exist).");
1942
3238
  return;
1943
3239
  }
@@ -1947,8 +3243,8 @@ function cleanCmd(flags) {
1947
3243
  console.error(`invalid --before date: ${before}`);
1948
3244
  process.exit(1);
1949
3245
  }
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) => {
3246
+ let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
3247
+ const stateFiles = (m) => readdirSync10(join21(root, m)).filter((f) => {
1952
3248
  const stem = f.replace(/\.json$/, "");
1953
3249
  return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
1954
3250
  });
@@ -1958,7 +3254,7 @@ function cleanCmd(flags) {
1958
3254
  return false;
1959
3255
  for (const f of stateFiles(m)) {
1960
3256
  try {
1961
- const ts = Date.parse(JSON.parse(readFileSync9(join15(root, m, f), "utf8")).updated_at ?? "") || 0;
3257
+ const ts = Date.parse(JSON.parse(readFileSync15(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
1962
3258
  if (ts === 0 || ts >= beforeMs)
1963
3259
  return false;
1964
3260
  } catch {
@@ -1968,7 +3264,7 @@ function cleanCmd(flags) {
1968
3264
  return true;
1969
3265
  };
1970
3266
  if (!flag(flags.all)) {
1971
- candidates = candidates.filter((m) => existsSync12(join15(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
3267
+ candidates = candidates.filter((m) => existsSync16(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
1972
3268
  } else if (!flag(flags.force)) {
1973
3269
  const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
1974
3270
  if (live.length) {
@@ -2002,7 +3298,7 @@ async function resolveOptions(flags) {
2002
3298
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
2003
3299
  }
2004
3300
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2005
- if (scope === "project" && !existsSync12(projectDir))
3301
+ if (scope === "project" && !existsSync16(projectDir))
2006
3302
  throw new Error(`Project dir not found: ${projectDir}`);
2007
3303
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
2008
3304
  if (targetIds && targetIds.includes("all"))
@@ -2081,7 +3377,7 @@ async function uninstall(flags) {
2081
3377
  if (!manifest) {
2082
3378
  if (scope === "project") {
2083
3379
  const globalFile = manifestPath({ scope: "global", projectDir, home });
2084
- if (existsSync12(globalFile)) {
3380
+ if (existsSync16(globalFile)) {
2085
3381
  console.log(`No project manifest found, but a global install exists. Try:
2086
3382
  mugiwara uninstall --global`);
2087
3383
  return;
@@ -2118,13 +3414,13 @@ async function uninstall(flags) {
2118
3414
  console.log(` note: ${n}`);
2119
3415
  }
2120
3416
  rmSync3(file);
2121
- const mugiDir = join15(scope === "global" ? home : projectDir, ".mugiwara");
2122
- if (existsSync12(mugiDir) && readdirSync9(mugiDir).length === 0) {
3417
+ const mugiDir = join21(scope === "global" ? home : projectDir, ".mugiwara");
3418
+ if (existsSync16(mugiDir) && readdirSync10(mugiDir).length === 0) {
2123
3419
  rmSync3(mugiDir, { recursive: true, force: true });
2124
3420
  }
2125
3421
  if (manifest.targets.includes("opencode")) {
2126
- const opencodeCache = join15(home, ".cache", "opencode", "packages", "@ionivetech");
2127
- if (existsSync12(opencodeCache)) {
3422
+ const opencodeCache = join21(home, ".cache", "opencode", "packages", "@ionivetech");
3423
+ if (existsSync16(opencodeCache)) {
2128
3424
  rmSync3(opencodeCache, { recursive: true, force: true });
2129
3425
  console.log(" cleared opencode npm cache (stale plugin versions)");
2130
3426
  }
@@ -2132,9 +3428,25 @@ async function uninstall(flags) {
2132
3428
  }
2133
3429
  console.log(`OK removed ${removed.length} files`);
2134
3430
  }
3431
+ function legacyWarning(projectDir) {
3432
+ if (hasLegacyLayout(projectDir)) {
3433
+ console.error("⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)");
3434
+ }
3435
+ }
3436
+ function schemaWarnings(projectDir) {
3437
+ const states = readState(projectDir);
3438
+ for (const s of states) {
3439
+ const v = s.schema_version;
3440
+ if (v !== CURRENT_SCHEMA_VERSION) {
3441
+ const wrote = v === null || v === undefined || v === "" ? "unknown" : String(v);
3442
+ console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ""}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
3443
+ }
3444
+ }
3445
+ }
2135
3446
  function list(flags) {
2136
3447
  const home = homedir4();
2137
3448
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3449
+ legacyWarning(projectDir);
2138
3450
  let found = false;
2139
3451
  for (const [label, file] of [
2140
3452
  ["project", manifestPath({ scope: "project", projectDir, home })],
@@ -2145,7 +3457,7 @@ function list(flags) {
2145
3457
  continue;
2146
3458
  found = true;
2147
3459
  if (flag(flags.check)) {
2148
- const missing = m.files.filter((f) => !existsSync12(f));
3460
+ const missing = m.files.filter((f) => !existsSync16(f));
2149
3461
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
2150
3462
  } else {
2151
3463
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -2156,6 +3468,8 @@ function list(flags) {
2156
3468
  }
2157
3469
  function continueCmd(flags, positionals) {
2158
3470
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3471
+ legacyWarning(projectDir);
3472
+ schemaWarnings(projectDir);
2159
3473
  const [mission, member] = positionals.slice(1);
2160
3474
  let entries = readContinue(projectDir);
2161
3475
  if (!flag(flags.all)) {
@@ -2196,6 +3510,8 @@ Pick one: mugiwara continue ${r.mission} <member>`);
2196
3510
  }
2197
3511
  function statusCmd(flags) {
2198
3512
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3513
+ legacyWarning(projectDir);
3514
+ schemaWarnings(projectDir);
2199
3515
  const states = readState(projectDir);
2200
3516
  if (!states.length) {
2201
3517
  console.log("No mission state on disk.");
@@ -2215,6 +3531,58 @@ function statusCmd(flags) {
2215
3531
  console.log(` evidence: ${s.evidence.join(", ")}`);
2216
3532
  }
2217
3533
  }
3534
+ function costCmd(flags, positionals) {
3535
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
3536
+ const mission = str(flags.mission) ?? positionals[1] ?? (() => {
3537
+ const states2 = readState(projectDir);
3538
+ if (states2.length === 1)
3539
+ return states2[0].mission;
3540
+ if (states2.length > 1) {
3541
+ console.error("multiple missions in flight — specify --mission <id>");
3542
+ process.exit(1);
3543
+ }
3544
+ return null;
3545
+ })();
3546
+ if (!mission) {
3547
+ console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
3548
+ process.exit(1);
3549
+ }
3550
+ const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3551
+ if (!existsSync16(missionDir)) {
3552
+ console.error(`No cost ledger found for mission "${mission}"`);
3553
+ process.exit(1);
3554
+ }
3555
+ const states = readState(projectDir).filter((s) => s.mission === mission);
3556
+ 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 });
3557
+ const state0 = states[0];
3558
+ let repeatedReads = 0;
3559
+ try {
3560
+ const reg = loadRegistry(missionDir);
3561
+ repeatedReads = reg.reduce((s, e) => s + Math.max(e.reads - 1, 0), 0);
3562
+ } catch {
3563
+ repeatedReads = 0;
3564
+ }
3565
+ const liveSlop = computeLiveSlop({ heal_cycle: state0?.heal_cycle ?? 0, repeated_reads: repeatedReads });
3566
+ const ledger = buildCostLedger({ missionDir, envelope, slopSummary: { interventions: liveSlop.interventions } });
3567
+ if (flag(flags.json)) {
3568
+ console.log(toCostJSON(ledger));
3569
+ return;
3570
+ }
3571
+ console.log(`Cost envelope: ${ledger.envelope.status} ${ledger.envelope.pct}% (${ledger.envelope.used}/${ledger.envelope.planned})`);
3572
+ console.log(`Avoided: ${ledger.avoided.stages_avoided} stages, ${ledger.avoided.contexts_avoided} contexts, ~${ledger.avoided.tokens_avoided_est} tokens`);
3573
+ console.log(`Efficiency: reuse ${ledger.efficiency.reuse_rate}, dup ${ledger.efficiency.duplicate_avoidance_chars} chars, budget ${ledger.efficiency.budget_efficiency_pct}%`);
3574
+ if (ledger.avoided.slop_interventions > 0) {
3575
+ const roles = Object.entries(liveSlop.perRole).map(([r, n]) => `${r}:${n}`).join(", ");
3576
+ console.log(`Slop: ${ledger.avoided.slop_interventions} intervention(s) — ${roles}`);
3577
+ }
3578
+ console.log(`Trail: ${ledger.trail.length} decisions`);
3579
+ if (flag(flags.ledger) && ledger.trail.length) {
3580
+ for (const t of ledger.trail.slice(0, 20))
3581
+ console.log(`- ${t.ts} — ${t.actor}: ${t.decision} — reason: ${t.reason}${t.evidence ? ` — evidence: ${t.evidence}` : ""}`);
3582
+ if (ledger.trail.length > 20)
3583
+ console.log(`… ${ledger.trail.length - 20} more`);
3584
+ }
3585
+ }
2218
3586
  function runCmd(flags, positionals) {
2219
3587
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2220
3588
  const name = positionals[1];
@@ -2300,8 +3668,8 @@ function handoffCmd(flags, positionals) {
2300
3668
  }
2301
3669
  lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
2302
3670
  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(`
3671
+ const out = join21(".mugiwara", "missions", mission, "handoff.md");
3672
+ writeFileSync10(resolve2(projectDir, out), lines.join(`
2305
3673
  `) + `
2306
3674
  `);
2307
3675
  console.log(lines.join(`
@@ -2309,15 +3677,126 @@ function handoffCmd(flags, positionals) {
2309
3677
  console.log(`
2310
3678
  written: ${out}`);
2311
3679
  }
3680
+ function migrateCmd(flags) {
3681
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
3682
+ const dryRun = flag(flags.dryRun);
3683
+ const legacyState = join21(projectDir, ".mugiwara", "state");
3684
+ const legacyContinue = join21(projectDir, ".mugiwara", "continue");
3685
+ const missionsRoot = join21(projectDir, ".mugiwara", "missions");
3686
+ const moves = [];
3687
+ const collect = (srcRoot, isContinue) => {
3688
+ if (!existsSync16(srcRoot))
3689
+ return;
3690
+ const walk = (dir) => {
3691
+ for (const e of readdirSync10(dir, { withFileTypes: true })) {
3692
+ const full = join21(dir, e.name);
3693
+ if (e.isDirectory())
3694
+ walk(full);
3695
+ else if (e.isFile() && e.name.endsWith(".json")) {
3696
+ const rel = full.slice(srcRoot.length + 1);
3697
+ let destRel;
3698
+ if (isContinue) {
3699
+ const parts = rel.split("/");
3700
+ const file = parts.pop();
3701
+ const mission = parts.join("/");
3702
+ const stem = file.slice(0, -".json".length);
3703
+ let destFile;
3704
+ if (stem === "state")
3705
+ destFile = "continue.json";
3706
+ else
3707
+ destFile = `continue-${stem}.json`;
3708
+ destRel = mission ? join21(mission, destFile) : destFile;
3709
+ } else {
3710
+ destRel = rel;
3711
+ }
3712
+ moves.push({ src: full, dest: join21(missionsRoot, destRel) });
3713
+ }
3714
+ }
3715
+ };
3716
+ walk(srcRoot);
3717
+ };
3718
+ collect(legacyState, false);
3719
+ collect(legacyContinue, true);
3720
+ if (!moves.length) {
3721
+ if (!existsSync16(legacyState) && !existsSync16(legacyContinue)) {
3722
+ console.log("no legacy layout found (.mugiwara/state/ does not exist)");
3723
+ } else {
3724
+ console.log("no legacy state files to migrate");
3725
+ }
3726
+ return;
3727
+ }
3728
+ for (const m of moves) {
3729
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
3730
+ if (!dryRun) {
3731
+ mkdirSync10(dirname6(m.dest), { recursive: true });
3732
+ try {
3733
+ const raw = JSON.parse(readFileSync15(m.src, "utf8"));
3734
+ raw.schema_version = CURRENT_SCHEMA_VERSION;
3735
+ writeFileSync10(m.dest, JSON.stringify(raw, null, 2) + `
3736
+ `);
3737
+ rmSync3(m.src, { force: true });
3738
+ } catch {
3739
+ try {
3740
+ renameSync2(m.src, m.dest);
3741
+ } catch {}
3742
+ }
3743
+ }
3744
+ }
3745
+ if (!dryRun) {
3746
+ const prune = (root) => {
3747
+ if (!existsSync16(root))
3748
+ return;
3749
+ const walkPrune = (dir) => {
3750
+ for (const e of readdirSync10(dir, { withFileTypes: true })) {
3751
+ if (e.isDirectory())
3752
+ walkPrune(join21(dir, e.name));
3753
+ }
3754
+ try {
3755
+ if (readdirSync10(dir).length === 0)
3756
+ rmSync3(dir, { recursive: true, force: true });
3757
+ } catch {}
3758
+ };
3759
+ walkPrune(root);
3760
+ try {
3761
+ if (existsSync16(root) && readdirSync10(root).length === 0)
3762
+ rmSync3(root, { recursive: true, force: true });
3763
+ } catch {}
3764
+ };
3765
+ prune(legacyState);
3766
+ prune(legacyContinue);
3767
+ }
3768
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${moves.length} file(s)${dryRun ? " (dry run)" : ""}`);
3769
+ }
2312
3770
  function signCmd(flags, _) {
2313
3771
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3772
+ if (flag(flags.genKey)) {
3773
+ const backend = str(flags.backend) ?? "auto";
3774
+ const home = homedir4();
3775
+ if (backend === "minisign") {
3776
+ if (!hasMinisign()) {
3777
+ console.error("✗ minisign not installed — cannot generate keys with this backend");
3778
+ process.exit(1);
3779
+ }
3780
+ try {
3781
+ execFileSync6("minisign", ["-G"], { stdio: "inherit" });
3782
+ console.log("✓ minisign key pair generated in ~/.mugiwara/");
3783
+ return;
3784
+ } catch {
3785
+ console.error("✗ key generation failed");
3786
+ process.exit(1);
3787
+ }
3788
+ }
3789
+ const dir = ensurePureKey(home);
3790
+ console.log(`✓ pure ed25519 key pair ready: ${join21(dir, "mugiwara.key")} / ${join21(dir, "mugiwara.pub")}`);
3791
+ return;
3792
+ }
2314
3793
  const mission = _[1];
2315
3794
  if (!mission) {
2316
- console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
3795
+ console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
2317
3796
  process.exit(1);
2318
3797
  }
2319
- const missionDir = join15(projectDir, ".mugiwara", "missions", mission);
2320
- if (!existsSync12(missionDir)) {
3798
+ const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3799
+ if (!existsSync16(missionDir)) {
2321
3800
  console.error(`no mission dir: ${missionDir}`);
2322
3801
  process.exit(1);
2323
3802
  }
@@ -2343,15 +3822,21 @@ Usage:
2343
3822
  mugiwara continue <m> [member]
2344
3823
  print the exact resume point for that mission/member
2345
3824
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
3825
+ mugiwara cost [--mission <id>] [--json] [--ledger]
3826
+ show cost ledger, avoided work, efficiency, trail (human + JSON)
2346
3827
  mugiwara blame <path> provenance note on the last commit touching <path>
2347
3828
  (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
2348
3829
  mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
2349
3830
  engineer can act on (computed state + staleness check)
2350
- mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
3831
+ mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
3832
+ mugiwara sign --gen-key [--backend pure|minisign]
3833
+ create signing keys (pure ed25519 default)
3834
+ mugiwara migrate [--dry-run] [--project <dir>]
3835
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
2351
3836
  mugiwara run <script> [args...]
2352
- run a bundled harness script here (${RUNNABLE.join(", ")})
3837
+ run a bundled harness script here (${RUNNABLE.join(", ")})
2353
3838
  mugiwara savepoint <mission> [member] [flow] [mode]
2354
- shorthand for: mugiwara run savepoint.sh ...
3839
+ shorthand for: mugiwara run savepoint.sh ...
2355
3840
  mugiwara --help this help
2356
3841
  mugiwara --version print version
2357
3842
 
@@ -2376,6 +3861,7 @@ if (isMain) {
2376
3861
  });
2377
3862
  }
2378
3863
  export {
3864
+ migrateCmd,
2379
3865
  run,
2380
3866
  stalenessLine
2381
3867
  };