@ionivetech/mugiwara 0.6.6 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -7
  3. package/.codex-plugin/plugin.json +2 -7
  4. package/.cursor-plugin/plugin.json +2 -7
  5. package/.kimi-plugin/plugin.json +2 -7
  6. package/.opencode/plugins/mugiwara.mjs +3 -18
  7. package/AGENTS.md +5 -4
  8. package/GEMINI.md +3 -3
  9. package/README.md +69 -63
  10. package/content/agents/brook-healing.md +8 -4
  11. package/content/agents/chopper-checkpoint.md +7 -3
  12. package/content/agents/eval-runner.md +4 -4
  13. package/content/agents/franky-gates.md +3 -3
  14. package/content/agents/jinbe-security.md +6 -7
  15. package/content/agents/luffy-orchestrator.md +11 -13
  16. package/content/agents/memory-keeper.md +4 -4
  17. package/content/agents/nami-planner.md +4 -4
  18. package/content/agents/resume-coordinator.md +7 -7
  19. package/content/agents/robin-reviewer.md +7 -3
  20. package/content/agents/sanji-quality.md +2 -2
  21. package/content/agents/skeptic-verifier.md +2 -2
  22. package/content/agents/usopp-brainstorm.md +3 -3
  23. package/content/agents/zoro-execution.md +6 -6
  24. package/content/skills/mugiwara-backend/SKILL.md +2 -0
  25. package/content/skills/mugiwara-backend/references/database.md +61 -0
  26. package/content/skills/mugiwara-brainstorm/SKILL.md +1 -1
  27. package/content/skills/mugiwara-checkpoint/SKILL.md +6 -5
  28. package/content/skills/mugiwara-checkpoint/references/ledger-format.md +1 -1
  29. package/content/skills/mugiwara-execution/SKILL.md +8 -8
  30. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  31. package/content/skills/mugiwara-execution/references/resume-batching.md +2 -2
  32. package/content/skills/mugiwara-frontend/SKILL.md +3 -2
  33. package/content/skills/mugiwara-frontend/references/checklist.md +14 -0
  34. package/content/skills/mugiwara-gates/SKILL.md +8 -5
  35. package/content/skills/mugiwara-healing/SKILL.md +2 -2
  36. package/content/skills/mugiwara-lessons/SKILL.md +2 -2
  37. package/content/skills/mugiwara-orchestration/SKILL.md +7 -7
  38. package/content/skills/mugiwara-orchestration/references/check-ins.md +2 -2
  39. package/content/skills/mugiwara-orchestration/references/closure.md +17 -12
  40. package/content/skills/mugiwara-orchestration/references/triage-escalation.md +11 -1
  41. package/content/skills/mugiwara-planning/SKILL.md +3 -18
  42. package/content/skills/mugiwara-planning/references/plan-template.md +2 -2
  43. package/content/skills/mugiwara-quality/SKILL.md +5 -2
  44. package/content/skills/mugiwara-resume/SKILL.md +2 -2
  45. package/content/skills/mugiwara-review/SKILL.md +3 -2
  46. package/content/skills/mugiwara-security/SKILL.md +1 -1
  47. package/content/skills/mugiwara-ship/SKILL.md +1 -1
  48. package/content/skills/mugiwara-ship/references/cleanup.md +17 -16
  49. package/content/skills/mugiwara-workflow/SKILL.md +9 -12
  50. package/content/skills/mugiwara-workflow/references/workspace-layout.md +39 -38
  51. package/dist/mugiwara.js +904 -462
  52. package/gemini-extension.json +1 -1
  53. package/hooks/auto-savepoint.js +18 -10
  54. package/hooks/auto-savepoint.ts +23 -23
  55. package/hooks/engagement-marker.js +1 -1
  56. package/hooks/engagement-marker.ts +1 -1
  57. package/hooks/pipeline-guard.js +16 -12
  58. package/hooks/pipeline-guard.ts +22 -19
  59. package/hooks/session-start.js +13 -10
  60. package/hooks/session-start.ts +24 -19
  61. package/package.json +2 -2
  62. package/plugin.json +1 -1
  63. package/references/complexity.md +27 -4
  64. package/references/definition-of-done.md +1 -1
  65. package/references/multi-actor.md +0 -3
  66. package/references/prose-style.md +54 -0
  67. package/scripts/check-doc-links.ts +40 -0
  68. package/scripts/conformance.ts +6 -16
  69. package/scripts/coverage-gate.ts +20 -6
  70. package/scripts/gate-selftest.ts +118 -7
  71. package/scripts/lane.sh +54 -8
  72. package/scripts/lib/patterns.sh +5 -0
  73. package/scripts/policy-force.ts +22 -0
  74. package/scripts/retrieval-eval.ts +1 -1
  75. package/scripts/savepoint.sh +136 -58
  76. package/scripts/validate-content.ts +22 -9
  77. package/scripts/verify-install.ts +9 -7
  78. package/src/args.ts +2 -2
  79. package/src/budget.ts +56 -0
  80. package/src/cli.ts +161 -110
  81. package/src/continue.ts +29 -10
  82. package/src/installer.ts +25 -20
  83. package/src/integrity.ts +158 -0
  84. package/src/mission.ts +228 -95
  85. package/src/policy.ts +156 -0
  86. package/src/provenance.ts +116 -0
  87. package/src/rollback.ts +95 -0
  88. package/src/routing.ts +69 -0
  89. package/src/run.ts +2 -2
  90. package/src/sign.ts +61 -0
  91. package/src/targets/claude.ts +2 -2
  92. package/.opencode/commands/mugiwara-execute.md +0 -13
  93. package/.opencode/commands/mugiwara-heal.md +0 -13
  94. package/.opencode/commands/mugiwara-onboard.md +0 -14
  95. package/.opencode/commands/mugiwara-plan.md +0 -17
  96. package/.opencode/commands/mugiwara-ship.md +0 -13
  97. package/.opencode/commands/using-mugiwara.md +0 -20
  98. package/content/skills/mugiwara-agent-security/SKILL.md +0 -58
  99. package/content/skills/mugiwara-agent-security/references/checklist.md +0 -20
  100. package/content/skills/mugiwara-context-budget/SKILL.md +0 -66
  101. package/content/skills/mugiwara-context-budget/references/context-budget.md +0 -37
  102. package/content/skills/mugiwara-pr/SKILL.md +0 -69
  103. package/content/skills/mugiwara-pr/references/verdict-format.md +0 -31
  104. package/content/skills/mugiwara-sunset/SKILL.md +0 -82
  105. package/content/skills/using-mugiwara/SKILL.md +0 -51
  106. package/references/token-budget.md +0 -56
  107. package/scripts/evidence.sh +0 -81
  108. package/scripts/initiative.ts +0 -296
  109. package/scripts/mission-report.sh +0 -293
  110. package/src/onboard.ts +0 -207
package/dist/mugiwara.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync7, rmSync as rmSync3 } from "node:fs";
5
- import { spawnSync as spawnSync2 } from "node:child_process";
6
- import { homedir as homedir2 } from "node:os";
7
- import { join as join11, resolve as resolve2 } from "node:path";
4
+ import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync9, rmSync as rmSync3, writeFileSync as writeFileSync8 } from "node:fs";
5
+ import { execFileSync as execFileSync6 } from "node:child_process";
6
+ import { homedir as homedir4 } from "node:os";
7
+ import { join as join15, resolve as resolve2 } from "node:path";
8
8
  import { fileURLToPath as fileURLToPath5 } from "node:url";
9
9
 
10
10
  // src/args.ts
11
- var VALUE_FLAGS = { "--project": "project", "--target": "target" };
11
+ var VALUE_FLAGS = { "--project": "project", "--target": "target", "--before": "before" };
12
12
  var BOOL_FLAGS = {
13
13
  "--global": "global",
14
14
  "--yes": "yes",
@@ -18,6 +18,7 @@ var BOOL_FLAGS = {
18
18
  "--keep-logs": "keepLogs",
19
19
  "--check": "check",
20
20
  "--all": "all",
21
+ "--verify": "verify",
21
22
  "--help": "help",
22
23
  "-h": "help",
23
24
  "--version": "version",
@@ -130,7 +131,7 @@ function copyScripts(dstDir) {
130
131
  if (!existsSync(src))
131
132
  return;
132
133
  for (const f of readdirSync(src)) {
133
- if (!f.endsWith(".sh") && f !== "initiative.ts" || f === "install.sh")
134
+ if (!f.endsWith(".sh") || f === "install.sh")
134
135
  continue;
135
136
  const to = join(dst, f);
136
137
  mkdirSync(dst, { recursive: true });
@@ -750,9 +751,11 @@ function installTo(target10, opts) {
750
751
  "coverage_modified=80",
751
752
  "review_depth=full",
752
753
  "quality_depth=full",
754
+ "verify_merged=off",
753
755
  "delegate_threshold=60",
754
756
  "heal_max_cycles=3",
755
- "verbosity=normal"
757
+ "verbosity=normal",
758
+ "# context_budget_chars=150000 # optional: fail archive if trail exceeds this (measured in report Cost section)"
756
759
  ].join(`
757
760
  `) + `
758
761
  `;
@@ -761,7 +764,7 @@ function installTo(target10, opts) {
761
764
  writeFileSync3(configPath, body);
762
765
  }
763
766
  result.written.push(configPath);
764
- result.notes.push(`default config written: ${configPath} (run \`mugiwara onboard\` to customise)`);
767
+ result.notes.push(`default config written: ${configPath} (edit it to customise)`);
765
768
  }
766
769
  }
767
770
  return result;
@@ -802,11 +805,11 @@ function assertNotSymlink(file) {
802
805
  }
803
806
  var GITIGNORE_BLOCK_START = "# >>> mugiwara >>>";
804
807
  var GITIGNORE_BLOCK_END = "# <<< mugiwara <<<";
805
- var GITIGNORE_MARKER = "# mugiwara";
806
- var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit reports/, results/, logs/, spec/, plans/.
807
- # Ignore session state and regenerated files.
808
- .mugiwara/state/
809
- .mugiwara/continue/
808
+ var GITIGNORE_LEGACY = [".mugiwara/state/", "# mugiwara"];
809
+ var GITIGNORE_BLOCK = `# >>> mugiwara >>> — audit trail is the product: commit plan.md, flows/, review.md,
810
+ # decisions.md, blockers.md, report.md under .mugiwara/missions/. Ignore session state.
811
+ .mugiwara/missions/**/*.json
812
+ .mugiwara/index.md
810
813
  .mugiwara/config
811
814
  .mugiwara/refs/
812
815
  # <<< mugiwara <<<
@@ -817,10 +820,10 @@ function ensureProjectGitignore(projectDir, opts = {}) {
817
820
  assertNotSymlink(path);
818
821
  let existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
819
822
  if (existing) {
820
- if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/state/")) {
823
+ if (existing.includes(GITIGNORE_BLOCK_START) && existing.includes(".mugiwara/missions/**/*.json")) {
821
824
  return { appended: false, notes: [] };
822
825
  }
823
- const hadOld = existing.includes(GITIGNORE_MARKER) || existing.includes(GITIGNORE_BLOCK_START);
826
+ const hadOld = GITIGNORE_LEGACY.some((m) => existing.includes(m)) || existing.includes(GITIGNORE_BLOCK_START);
824
827
  if (hadOld) {
825
828
  const clean = removeProjectGitignore(projectDir, { dryRun });
826
829
  existing = clean.removed ? existsSync4(path) ? readFileSync3(path, "utf8") : "" : existing;
@@ -849,18 +852,20 @@ function removeProjectGitignore(projectDir, { dryRun = false } = {}) {
849
852
  if (end < start)
850
853
  return { removed: false, notes: ["delimiter mismatch — .gitignore left untouched"] };
851
854
  cleaned = current.slice(0, start) + current.slice(end + GITIGNORE_BLOCK_END.length);
852
- } else if (current.includes(GITIGNORE_MARKER)) {
855
+ } else if (current.includes("# mugiwara")) {
853
856
  const LEGACY_LINES = new Set([
854
857
  ".mugiwara/state.json",
855
858
  ".mugiwara/state-*.json",
856
859
  ".mugiwara/config",
860
+ ".mugiwara/state/",
861
+ ".mugiwara/continue/",
857
862
  ".mugiwara/continue.md",
858
863
  ".mugiwara/refs/"
859
864
  ]);
860
865
  const lines = current.split(`
861
866
  `).filter((l) => {
862
867
  const t = l.trim();
863
- if (t.startsWith(GITIGNORE_MARKER))
868
+ if (t.startsWith("# mugiwara"))
864
869
  return false;
865
870
  return !LEGACY_LINES.has(t);
866
871
  });
@@ -898,19 +903,436 @@ function writeManifest(file, data) {
898
903
  }
899
904
 
900
905
  // src/mission.ts
901
- import { existsSync as existsSync6, rmSync as rmSync2, readFileSync as readFileSync5, readdirSync as readdirSync4, mkdirSync as mkdirSync5, appendFileSync } from "node:fs";
902
- import { join as join7 } from "node:path";
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";
909
+
910
+ // 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
+ 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"]
921
+ ];
922
+ var ALLOW_SECRET = "mugiwara:allow-secret";
923
+ function findSecrets(body) {
924
+ const out = [];
925
+ for (const line of body.split(/\r?\n/)) {
926
+ if (line.includes(ALLOW_SECRET))
927
+ continue;
928
+ for (const [re, label] of SECRET_PATTERNS) {
929
+ const hit = line.match(re);
930
+ if (hit)
931
+ out.push({ label, hit: hit[0] });
932
+ }
933
+ }
934
+ return out;
935
+ }
936
+ var TRAIL_EXTS = new Set([".md", ".json", ".sh"]);
937
+ function trailFiles(dir) {
938
+ const out = [];
939
+ const walk = (d) => {
940
+ for (const e of readdirSync4(d, { withFileTypes: true })) {
941
+ const p = join7(d, e.name);
942
+ if (e.isDirectory())
943
+ walk(p);
944
+ else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
945
+ out.push(p);
946
+ }
947
+ };
948
+ walk(dir);
949
+ return out;
950
+ }
951
+ function linkedPaths(md) {
952
+ const out = [];
953
+ for (const m of md.matchAll(/\]\(([^)\s]+)\)/g)) {
954
+ const t = m[1];
955
+ if (/^(https?:|mailto:|#|\/\/)/.test(t))
956
+ continue;
957
+ out.push(t.split("#")[0]);
958
+ }
959
+ return out.filter(Boolean);
960
+ }
961
+ function hasCommandOutputShape(body) {
962
+ return /`[^`]+`/.test(body) || /\b(exit\s+[01]|✓|✗|\bPASS\b|\bFAIL\b|passed|failed|\d+\s+(passed|failed))\b/i.test(body);
963
+ }
964
+ function collectPassCitedPaths(missionDir) {
965
+ const out = [];
966
+ for (const f of trailFiles(missionDir)) {
967
+ let body;
968
+ try {
969
+ body = readFileSync5(f, "utf8");
970
+ } catch {
971
+ continue;
972
+ }
973
+ for (const line of body.split(/\r?\n/)) {
974
+ if (!/\bPASS\b/.test(line))
975
+ continue;
976
+ for (const p of linkedPaths(line))
977
+ out.push(p);
978
+ for (const m of line.matchAll(/(?:^|[\s"'(])([a-zA-Z0-9._\/-]+\.(?:md|txt|log|json))\b/g)) {
979
+ const cand = m[1];
980
+ if (cand.includes("/"))
981
+ out.push(cand);
982
+ }
983
+ }
984
+ }
985
+ return [...new Set(out)];
986
+ }
987
+ function checkTrail(missionDir, projectRoot) {
988
+ const issues = [];
989
+ const files = trailFiles(missionDir);
990
+ for (const f of files) {
991
+ let body;
992
+ try {
993
+ body = readFileSync5(f, "utf8");
994
+ } catch {
995
+ continue;
996
+ }
997
+ for (const target10 of linkedPaths(body)) {
998
+ if (isAbsolute(target10))
999
+ continue;
1000
+ const fromMission = join7(missionDir, target10);
1001
+ const fromRoot = join7(projectRoot, target10);
1002
+ if (!existsSync6(fromMission) && !existsSync6(fromRoot)) {
1003
+ issues.push({
1004
+ kind: "dangling-path",
1005
+ detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
1006
+ });
1007
+ }
1008
+ }
1009
+ for (const { label, hit } of findSecrets(body)) {
1010
+ issues.push({
1011
+ kind: "secret",
1012
+ detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
1013
+ });
1014
+ }
1015
+ }
1016
+ const evidencePaths = [];
1017
+ const evidenceFile = join7(missionDir, "state.json");
1018
+ if (existsSync6(evidenceFile)) {
1019
+ try {
1020
+ const s = JSON.parse(readFileSync5(evidenceFile, "utf8"));
1021
+ if (Array.isArray(s.evidence)) {
1022
+ for (const e of s.evidence) {
1023
+ if (typeof e !== "string" || !e.trim())
1024
+ continue;
1025
+ evidencePaths.push(e);
1026
+ const cand = join7(projectRoot, e);
1027
+ if (!isAbsolute(e) && !existsSync6(cand) && !existsSync6(join7(missionDir, e))) {
1028
+ issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1029
+ }
1030
+ }
1031
+ }
1032
+ } catch {}
1033
+ }
1034
+ const passCited = collectPassCitedPaths(missionDir);
1035
+ for (const e of passCited) {
1036
+ if (!e.trim() || isAbsolute(e))
1037
+ continue;
1038
+ const candMission = join7(missionDir, e);
1039
+ const candRoot = join7(projectRoot, e);
1040
+ const resolved = existsSync6(candMission) ? candMission : existsSync6(candRoot) ? candRoot : null;
1041
+ if (!resolved)
1042
+ continue;
1043
+ let body;
1044
+ try {
1045
+ body = readFileSync5(resolved, "utf8");
1046
+ } catch {
1047
+ continue;
1048
+ }
1049
+ if (!hasCommandOutputShape(body)) {
1050
+ issues.push({ kind: "evidence-thin", detail: `evidence "${e}" exists but lacks command output (no backticked command or exit-status token)` });
1051
+ }
1052
+ }
1053
+ return issues;
1054
+ }
1055
+ function formatIssues(issues) {
1056
+ return issues.map((i) => ` ✗ [${i.kind}] ${i.detail}`).join(`
1057
+ `);
1058
+ }
1059
+
1060
+ // src/rollback.ts
1061
+ import { execFileSync } from "node:child_process";
1062
+ import { writeFileSync as writeFileSync5 } from "node:fs";
1063
+ import { join as join8 } from "node:path";
1064
+ function git(cwd, args) {
1065
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1066
+ }
1067
+ function buildRollback(input, commitsNewestFirst, filesTouched) {
1068
+ const lines = [
1069
+ "#!/usr/bin/env bash",
1070
+ `# Rollback map for mission "${input.mission}" — generated at closure.`,
1071
+ "# Human-executed. Review before running; mugiwara never runs this.",
1072
+ `# Branch: ${input.branch}`,
1073
+ `# Base: ${input.baseSha}`,
1074
+ "",
1075
+ "set -euo pipefail",
1076
+ ""
1077
+ ];
1078
+ if (!commitsNewestFirst.length && !filesTouched.length) {
1079
+ lines.push("# No commits between base and HEAD on this branch — nothing to revert.", "");
1080
+ return lines.join(`
1081
+ `);
1082
+ }
1083
+ if (!commitsNewestFirst.length) {
1084
+ lines.push("# UNRESOLVED: squash-merged state detected.", `# git rev-list ${input.baseSha}..${input.branch} is empty, but the diff`, `# ${input.baseSha}..${input.branch} touches ${filesTouched.length} file(s):`, "# the mission's changes were collapsed into commit(s) already on the base", "# ref. No per-commit revert list can be derived automatically.", "# Locate the squash commit, review it, then revert it:", `# git log --oneline ${input.baseSha}..HEAD --grep="${input.mission}"`, "# git revert <squash-commit>", "# Files carrying the squashed changes (verify each after reverting):", ...filesTouched.map((f) => `# ${f}`), "", 'echo "ROLLBACK INCOMPLETE: squash-merged state — locate and revert the squash commit manually" >&2', "exit 1", "");
1085
+ return lines.join(`
1086
+ `);
1087
+ }
1088
+ lines.push("# Revert newest-first so earlier reverts never conflict with later ones.", "git revert --no-edit \\", commitsNewestFirst.map((c) => ` ${c}`).join(" \\\n"), "");
1089
+ if (filesTouched.length) {
1090
+ lines.push("# Files this mission touched (verify the working tree is clean afterwards):", ...filesTouched.map((f) => `# ${f}`), "");
1091
+ }
1092
+ return lines.join(`
1093
+ `);
1094
+ }
1095
+ function generateRollback(projectDir, missionDir, input) {
1096
+ try {
1097
+ const range = `${input.baseSha}..${input.branch}`;
1098
+ const revList = git(projectDir, ["rev-list", "--reverse", range]).split(/\r?\n/).filter(Boolean);
1099
+ const commitsNewestFirst = [...revList].reverse();
1100
+ const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
1101
+ const body = buildRollback(input, commitsNewestFirst, filesTouched);
1102
+ const file = join8(missionDir, "rollback.sh");
1103
+ writeFileSync5(file, body, { mode: 493 });
1104
+ return { file: "rollback.sh", commits: commitsNewestFirst.length };
1105
+ } catch {
1106
+ return null;
1107
+ }
1108
+ }
1109
+
1110
+ // src/provenance.ts
1111
+ import { execFileSync as execFileSync2 } from "node:child_process";
1112
+ import { writeFileSync as writeFileSync6 } from "node:fs";
1113
+ import { join as join9 } from "node:path";
1114
+ function git2(cwd, args) {
1115
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1116
+ }
1117
+ function modelLabel() {
1118
+ return process.env.MUGIWARA_MODEL?.trim() || process.env.ANTHROPIC_MODEL?.trim() || "model-unrecorded (set MUGIWARA_MODEL to attribute)";
1119
+ }
1120
+ function buildNote(s) {
1121
+ const gates = s.evidence.length ? s.evidence.join(" · ") : "no evidence recorded";
1122
+ const uniqModels = [...new Set((s.models ?? []).filter(Boolean))];
1123
+ const modelPart = uniqModels.length ? `model(s): ${uniqModels.join(", ")}` : s.model ?? modelLabel();
1124
+ return [
1125
+ `mission: ${s.mission}`,
1126
+ `agent: ${s.actor || "unknown"} · ${modelPart} · lane ${s.lane} · mode ${s.mode}`,
1127
+ `tasks: ${s.tasks_done}/${s.tasks_total}`,
1128
+ `gates/evidence: ${gates}`,
1129
+ `branch: ${s.branch}`,
1130
+ "human review: pending (PR review is the terminal gate)"
1131
+ ].join(`
1132
+ `);
1133
+ }
1134
+ function renderProvenanceMd(note, sha) {
1135
+ const lines = [
1136
+ "# Provenance",
1137
+ "",
1138
+ "<!-- paste below into the PR description or a PR comment -->",
1139
+ "",
1140
+ "```",
1141
+ note,
1142
+ "```",
1143
+ "",
1144
+ sha ? `Commit: ${sha}` : "Commit: not recorded (no git head resolved at closure)",
1145
+ "",
1146
+ "Query locally after pushing notes:",
1147
+ "`git fetch origin refs/notes/mugiwara:refs/notes/mugiwara` then `mugiwara blame <path>`."
1148
+ ];
1149
+ return lines.join(`
1150
+ `) + `
1151
+ `;
1152
+ }
1153
+ function attachGitNote(projectDir, branch, note) {
1154
+ try {
1155
+ let sha;
1156
+ try {
1157
+ sha = git2(projectDir, ["rev-parse", "--verify", branch]);
1158
+ } catch {
1159
+ sha = git2(projectDir, ["rev-parse", "HEAD"]);
1160
+ }
1161
+ git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1162
+ return { sha };
1163
+ } catch {
1164
+ return null;
1165
+ }
1166
+ }
1167
+ function blamePath(projectDir, path) {
1168
+ let sha;
1169
+ try {
1170
+ sha = git2(projectDir, ["log", "-1", "--format=%H", "--", path]);
1171
+ } catch {
1172
+ return `blame: not a git repository (${projectDir})`;
1173
+ }
1174
+ if (!sha)
1175
+ return `blame: no commit touches "${path}"`;
1176
+ try {
1177
+ const note = git2(projectDir, ["notes", "--ref=mugiwara", "show", sha]);
1178
+ return `${path} @ ${sha.slice(0, 7)}
1179
+ ${note}`;
1180
+ } catch {
1181
+ return `${path} @ ${sha.slice(0, 7)}
1182
+ (no mugiwara provenance note on this commit)`;
1183
+ }
1184
+ }
1185
+ function writeProvenance(projectDir, missionDir, state) {
1186
+ const note = buildNote(state);
1187
+ const attached = attachGitNote(projectDir, state.branch, note);
1188
+ writeFileSync6(join9(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1189
+ }
1190
+
1191
+ // src/routing.ts
1192
+ var DOC_PAT = /\.(md|txt|rst)$|^docs\/|^(CHANGELOG|LICENSE|README)/;
1193
+ var TEST_PAT = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)(tests?|__tests__|specs?)\//;
1194
+ function scorePath(path, sensitivePaths, evidenceJoined) {
1195
+ const reasons = [];
1196
+ let score = 0;
1197
+ const sensitiveHit = sensitivePaths.some((s) => s && (path === s || path.startsWith(s.replace(/\/?$/, "/")) || new RegExp(s.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")).test(path)));
1198
+ if (sensitiveHit) {
1199
+ score += 100;
1200
+ reasons.push("sensitive path");
1201
+ }
1202
+ if (!DOC_PAT.test(path) && !TEST_PAT.test(path)) {
1203
+ score += 50;
1204
+ reasons.push("production code");
1205
+ } else if (TEST_PAT.test(path)) {
1206
+ score += 10;
1207
+ reasons.push("test scaffolding — skim unless behavior changed");
1208
+ } else {
1209
+ score += 5;
1210
+ reasons.push("docs/config");
1211
+ }
1212
+ if (!evidenceJoined.includes(path)) {
1213
+ score += 20;
1214
+ reasons.push("not covered by recorded evidence");
1215
+ }
1216
+ return { score, reasons };
1217
+ }
1218
+ function rankFiles(paths, state) {
1219
+ const sensitive = state.sensitive_paths ?? [];
1220
+ const ev = state.evidence.join(" ");
1221
+ return paths.map((path) => {
1222
+ const { score, reasons } = scorePath(path, sensitive, ev);
1223
+ return { path, score, reasons };
1224
+ }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
1225
+ }
1226
+ function renderRouting(ranked, mission) {
1227
+ if (!ranked.length)
1228
+ return "";
1229
+ const lines = [
1230
+ "",
1231
+ "## Review routing",
1232
+ "",
1233
+ `Ranked reading order for \`${mission}\` (heuristic ordering — it decides where to look first, never correctness):`,
1234
+ ""
1235
+ ];
1236
+ ranked.forEach((r, i) => {
1237
+ lines.push(`${i + 1}. \`${r.path}\` — ${r.reasons.join("; ")}`);
1238
+ });
1239
+ lines.push("");
1240
+ return lines.join(`
1241
+ `);
1242
+ }
1243
+
1244
+ // 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";
1248
+ 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;
1267
+ }
1268
+ function measureContextChars(missionDir) {
1269
+ let total = 0;
1270
+ const add = (p) => {
1271
+ try {
1272
+ total += statSync(p).size;
1273
+ } catch {}
1274
+ };
1275
+ for (const f of readdirSync5(missionDir)) {
1276
+ if (/\.md$/.test(f))
1277
+ add(join10(missionDir, f));
1278
+ }
1279
+ 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));
1284
+ }
1285
+ }
1286
+ return total;
1287
+ }
1288
+ function formatFootprint(chars, budget) {
1289
+ const base = `Context footprint: ${chars} chars`;
1290
+ if (!budget)
1291
+ return `${base} (no budget configured)`;
1292
+ return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
1293
+ }
1294
+
1295
+ // src/mission.ts
1296
+ function isStateFile(f) {
1297
+ const stem = f.replace(/\.json$/, "");
1298
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
1299
+ }
1300
+ function primaryState(dir, files) {
1301
+ const name = files.includes("state.json") ? "state.json" : files.find((f) => f.endsWith(".json") && f !== "continue.json" && !f.startsWith("continue-"));
1302
+ if (!name)
1303
+ return null;
1304
+ try {
1305
+ return JSON.parse(readFileSync7(join11(dir, name), "utf8"));
1306
+ } catch {
1307
+ return null;
1308
+ }
1309
+ }
1310
+ function changedFiles(projectDir, state) {
1311
+ const base = typeof state?.base_sha === "string" ? state.base_sha : "";
1312
+ const branch = typeof state?.branch === "string" ? state.branch : "";
1313
+ if (!base || base === "unknown" || !branch)
1314
+ return [];
1315
+ try {
1316
+ return execFileSync3("git", ["diff", "--name-only", base, branch], {
1317
+ cwd: projectDir,
1318
+ encoding: "utf8",
1319
+ stdio: ["ignore", "pipe", "ignore"]
1320
+ }).split(/\r?\n/).filter(Boolean);
1321
+ } catch {
1322
+ return [];
1323
+ }
1324
+ }
903
1325
  function activeActor(projectDir) {
904
- const stateDir = join7(projectDir, ".mugiwara", "state");
905
- if (!existsSync6(stateDir))
1326
+ const missionsDir = join11(projectDir, ".mugiwara", "missions");
1327
+ if (!existsSync8(missionsDir))
906
1328
  return null;
907
- const missions = readdirSync4(stateDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1329
+ const missions = readdirSync6(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
908
1330
  let latest = null;
909
1331
  for (const mission of missions) {
910
- const d = join7(stateDir, mission);
911
- for (const f of readdirSync4(d).filter((f2) => f2.endsWith(".json"))) {
1332
+ const d = join11(missionsDir, mission);
1333
+ for (const f of readdirSync6(d).filter(isStateFile)) {
912
1334
  try {
913
- const s = JSON.parse(readFileSync5(join7(d, f), "utf8"));
1335
+ const s = JSON.parse(readFileSync7(join11(d, f), "utf8"));
914
1336
  const t = Date.parse(s.updated_at || "") || 0;
915
1337
  if (!latest || t > latest.updated)
916
1338
  latest = { actor: s.actor || null, updated: t };
@@ -920,8 +1342,8 @@ function activeActor(projectDir) {
920
1342
  return latest ? latest.actor : null;
921
1343
  }
922
1344
  function resetMission(projectDir, keepLogs, force) {
923
- const root = join7(projectDir, ".mugiwara");
924
- if (!existsSync6(root))
1345
+ const root = join11(projectDir, ".mugiwara");
1346
+ if (!existsSync8(root))
925
1347
  return { removed: [], kept: [] };
926
1348
  if (!force) {
927
1349
  const actor = activeActor(projectDir);
@@ -931,361 +1353,249 @@ function resetMission(projectDir, keepLogs, force) {
931
1353
  }
932
1354
  const removed = [];
933
1355
  const kept = [];
934
- for (const dir of ["spec", "plans", "results", "review", "issues", "reports"]) {
935
- const p = join7(root, dir);
936
- if (existsSync6(p)) {
937
- rmSync2(p, { recursive: true, force: true });
938
- removed.push(dir);
939
- }
940
- }
941
- for (const dir of ["state", "continue"]) {
942
- const p = join7(root, dir);
943
- if (existsSync6(p)) {
1356
+ for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
1357
+ const p = join11(root, dir);
1358
+ if (existsSync8(p)) {
944
1359
  rmSync2(p, { recursive: true, force: true });
945
1360
  removed.push(dir);
946
1361
  }
947
1362
  }
948
- for (const f of readdirSync4(root)) {
1363
+ for (const f of readdirSync6(root)) {
949
1364
  if (/^state(-.+)?\.json$/.test(f)) {
950
- const p = join7(root, f);
951
- if (existsSync6(p)) {
1365
+ const p = join11(root, f);
1366
+ if (existsSync8(p)) {
952
1367
  rmSync2(p);
953
1368
  removed.push(f);
954
1369
  }
955
1370
  }
956
1371
  }
957
1372
  if (!keepLogs) {
958
- const p = join7(root, "logs");
959
- if (existsSync6(p)) {
960
- rmSync2(p, { recursive: true, force: true });
961
- removed.push("logs");
1373
+ for (const p of [join11(root, "lessons.md"), join11(root, "logs")]) {
1374
+ if (existsSync8(p)) {
1375
+ rmSync2(p, { recursive: true, force: true });
1376
+ removed.push(p.startsWith(join11(root, "logs")) ? "logs" : "lessons.md");
1377
+ }
962
1378
  }
963
- } else if (existsSync6(join7(root, "logs"))) {
964
- kept.push("logs");
1379
+ } else {
1380
+ if (existsSync8(join11(root, "lessons.md")))
1381
+ kept.push("lessons.md");
1382
+ else if (existsSync8(join11(root, join11("logs", "lessons.md"))))
1383
+ kept.push(join11("logs", "lessons.md"));
965
1384
  }
966
1385
  for (const f of ["config", "manifest.json", "backup"]) {
967
- if (existsSync6(join7(root, f)))
1386
+ if (existsSync8(join11(root, f)))
968
1387
  kept.push(f);
969
1388
  }
970
1389
  return { removed, kept };
971
1390
  }
972
1391
  function archiveMission(projectDir, mission, opts = {}) {
973
1392
  const { dryRun = false } = opts;
974
- const root = join7(projectDir, ".mugiwara");
1393
+ const root = join11(projectDir, ".mugiwara");
975
1394
  if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
976
1395
  throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
977
1396
  const removed = [];
978
1397
  const kept = [];
979
- const belongs = (f) => {
980
- const base = f.replace(/^\d{4}-\d{2}-\d{2}-/, "");
981
- return base === `${mission}.md` || base.startsWith(`${mission}-`);
982
- };
983
- let report = null;
984
- const reportsDir = join7(root, "reports");
985
- if (existsSync6(reportsDir)) {
986
- const f = readdirSync4(reportsDir).find((n) => {
987
- const m = n.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
988
- return !!m && m[2] === mission;
989
- });
990
- if (f)
991
- report = join7("reports", f);
992
- }
993
- const resultsDir = join7(root, "results", mission);
994
- if (existsSync6(resultsDir)) {
995
- for (const f of readdirSync4(resultsDir)) {
996
- kept.push(join7("results", mission, f));
1398
+ const dir = join11(root, "missions", mission);
1399
+ if (!existsSync8(dir))
1400
+ return { report: null, removed, kept };
1401
+ if (!dryRun) {
1402
+ const issues = checkTrail(dir, projectDir);
1403
+ if (issues.length) {
1404
+ throw new Error(`closure integrity gate failed — fix these before archiving:
1405
+ ${formatIssues(issues)}`);
997
1406
  }
998
1407
  }
999
- const specDir = join7(root, "spec");
1000
- if (existsSync6(specDir)) {
1001
- for (const f of readdirSync4(specDir)) {
1002
- if (!belongs(f))
1003
- continue;
1004
- const p = join7(specDir, f);
1005
- if (!dryRun)
1006
- rmSync2(p);
1007
- removed.push(join7("spec", f));
1408
+ const files = readdirSync6(dir);
1409
+ const state = primaryState(dir, files);
1410
+ const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
1411
+ try {
1412
+ const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
1413
+ return typeof s.model === "string" ? s.model : "";
1414
+ } catch {
1415
+ return "";
1008
1416
  }
1009
- }
1010
- for (const dir of ["review", "issues"]) {
1011
- const d = join7(root, dir);
1012
- if (!existsSync6(d))
1013
- continue;
1014
- for (const f of readdirSync4(d)) {
1015
- if (!belongs(f))
1016
- continue;
1017
- const p = join7(d, f);
1018
- if (!dryRun)
1019
- rmSync2(p, { force: true });
1020
- removed.push(join7(dir, f));
1417
+ }).filter(Boolean))];
1418
+ let costSection = "";
1419
+ if (!dryRun && state) {
1420
+ const chars = measureContextChars(dir);
1421
+ const budget = readBudgetConfig(projectDir);
1422
+ 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.`);
1021
1425
  }
1022
- }
1023
- const logsDir = join7(root, "logs");
1024
- if (existsSync6(logsDir)) {
1025
- for (const f of readdirSync4(logsDir)) {
1026
- if (!belongs(f))
1027
- continue;
1028
- const p = join7(logsDir, f);
1029
- if (!dryRun)
1030
- rmSync2(p);
1031
- removed.push(join7("logs", f));
1426
+ const est = typeof state.tokens_est === "number" ? state.tokens_est : 0;
1427
+ const src = typeof state.tokens_source === "string" ? state.tokens_source : "computed";
1428
+ 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;
1430
+ const effBudget = budget || laneBudget;
1431
+ const pct = effBudget ? Math.round(est / effBudget * 100) : 0;
1432
+ const delta = effBudget ? est <= effBudget ? `${(effBudget - est).toLocaleString()} under` : `${(est - effBudget).toLocaleString()} over` : "no budget configured";
1433
+ const srcLabel = src === "reported" ? "provider-reported" : "estimator";
1434
+ let reportedTotal = 0;
1435
+ let hasReported = false;
1436
+ for (const f of files.filter(isStateFile)) {
1437
+ try {
1438
+ const s = JSON.parse(readFileSync7(join11(dir, f), "utf8"));
1439
+ if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
1440
+ reportedTotal += s.tokens_est;
1441
+ hasReported = true;
1442
+ }
1443
+ } catch {}
1032
1444
  }
1445
+ if (!hasReported && src === "reported" && est > 0) {
1446
+ reportedTotal = est;
1447
+ hasReported = true;
1448
+ }
1449
+ costSection = [
1450
+ "## Cost",
1451
+ "",
1452
+ "| Metric | Value |",
1453
+ "|--------|-------|",
1454
+ `| **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)"} |`
1458
+ ].join(`
1459
+ `);
1460
+ if (hasReported) {
1461
+ costSection += `
1462
+ | **Provider total** | ${reportedTotal.toLocaleString()} (provider-reported — sum of reported stages) |`;
1463
+ }
1464
+ costSection += `
1465
+ `;
1033
1466
  }
1034
- const contDir = join7(root, "continue", mission);
1035
- if (existsSync6(contDir)) {
1036
- if (!dryRun)
1037
- rmSync2(contDir, { recursive: true, force: true });
1038
- removed.push(join7("continue", mission));
1039
- }
1040
- const stateDir = join7(root, "state", mission);
1041
- if (existsSync6(stateDir)) {
1042
- if (!dryRun)
1043
- rmSync2(stateDir, { recursive: true, force: true });
1044
- removed.push(join7("state", mission));
1045
- }
1046
- if (report)
1047
- kept.push(report);
1048
- for (const k of ["plans", "config", join7("logs", "lessons.md")]) {
1049
- if (existsSync6(join7(root, k)))
1050
- kept.push(k);
1051
- }
1467
+ const FOLD_TOP = ["decisions.md", "blockers.md", "review.md", "security.md", "spec.md"];
1468
+ const fold = [];
1469
+ for (const f of FOLD_TOP) {
1470
+ if (files.includes(f))
1471
+ fold.push(f);
1472
+ }
1473
+ const flowsDir = join11(dir, "flows");
1474
+ const legacyWavesDir = join11(dir, "waves");
1475
+ const artDir = existsSync8(flowsDir) ? flowsDir : existsSync8(legacyWavesDir) ? legacyWavesDir : flowsDir;
1476
+ const artRel = artDir === legacyWavesDir ? "waves" : "flows";
1477
+ if (existsSync8(artDir)) {
1478
+ for (const f of readdirSync6(artDir).sort())
1479
+ fold.push(join11(artRel, f));
1480
+ }
1481
+ let report = "";
1482
+ const reportPath = join11(dir, "report.md");
1483
+ 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");
1487
+ if (!dryRun) {
1488
+ mkdirSync5(dir, { recursive: true });
1489
+ if (fold.length) {
1490
+ const sections = fold.map((f) => {
1491
+ const body = readFileSync7(join11(dir, f), "utf8").trim();
1492
+ const name = f.includes("/") ? f.split("/").pop() ?? f : f;
1493
+ return `
1494
+
1495
+ ## Archived: ${name}
1496
+
1497
+ ${body}`;
1498
+ }).join("");
1499
+ const tmp = `${reportPath}.tmp`;
1500
+ const routingSection = state ? renderRouting(rankFiles(changedFiles(projectDir, state), {
1501
+ mission,
1502
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
1503
+ sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
1504
+ }), mission) : "";
1505
+ writeFileSync7(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1506
+ ${costSection}
1507
+ ` : "") + `
1508
+ `);
1509
+ renameSync(tmp, reportPath);
1510
+ }
1511
+ for (const f of fold) {
1512
+ rmSync2(join11(dir, f), { force: true, recursive: true });
1513
+ removed.push(join11("missions", mission, f));
1514
+ }
1515
+ 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)
1518
+ rmSync2(artDir, { recursive: true, force: true });
1519
+ if (!existsSync8(reportPath)) {
1520
+ writeFileSync7(reportPath, `# Mission: ${mission}
1521
+
1522
+ Archived before closure — no wave artifacts were present.
1523
+ `);
1524
+ }
1525
+ if (state && typeof state.branch === "string") {
1526
+ const rb = generateRollback(projectDir, dir, {
1527
+ mission,
1528
+ branch: state.branch,
1529
+ baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
1530
+ });
1531
+ if (rb)
1532
+ kept.push(join11("missions", mission, rb.file));
1533
+ try {
1534
+ writeProvenance(projectDir, dir, {
1535
+ mission,
1536
+ actor: typeof state.actor === "string" ? state.actor : "",
1537
+ lane: typeof state.lane === "string" ? state.lane : "",
1538
+ mode: typeof state.mode === "string" ? state.mode : "",
1539
+ branch: state.branch,
1540
+ tasks_done: Number(state.tasks_done) || 0,
1541
+ tasks_total: Number(state.tasks_total) || 0,
1542
+ evidence: Array.isArray(state.evidence) ? state.evidence : [],
1543
+ models: stageModels
1544
+ });
1545
+ kept.push(join11("missions", mission, "provenance.md"));
1546
+ } catch {}
1547
+ }
1548
+ } else {
1549
+ for (const f of fold)
1550
+ removed.push(join11("missions", mission, f));
1551
+ }
1552
+ removed.push(join11("missions", mission, "<session state>"));
1553
+ if (files.includes("plan.md"))
1554
+ kept.push(join11("missions", mission, "plan.md"));
1555
+ if (files.includes("handoff.md"))
1556
+ kept.push(join11("missions", mission, "handoff.md"));
1557
+ kept.push(join11("missions", mission, "report.md"));
1052
1558
  let index;
1053
- const indexFile = join7(root, "reports", "index.md");
1054
- const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}${report ? ` → ${report}` : ""}
1559
+ const indexFile = join11(root, "index.md");
1560
+ const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
1055
1561
  `;
1056
1562
  if (!dryRun) {
1057
- mkdirSync5(join7(root, "reports"), { recursive: true });
1058
- const existing = existsSync6(indexFile) ? readFileSync5(indexFile, "utf8") : "";
1563
+ const existing = existsSync8(indexFile) ? readFileSync7(indexFile, "utf8") : "";
1059
1564
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
1060
1565
  const header = existing ? "" : `# Mission index
1061
1566
 
1062
1567
  `;
1063
- appendFileSync(indexFile, header + line);
1064
- }
1065
- index = join7("reports", "index.md");
1066
- }
1067
- return { report, removed, kept, index };
1068
- }
1069
-
1070
- // src/onboard.ts
1071
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
1072
- import { join as join8 } from "node:path";
1073
- var DEFAULT_ORDER = [
1074
- "mode",
1075
- "branch",
1076
- "commit",
1077
- "auto_commit",
1078
- "coverage_new",
1079
- "coverage_modified",
1080
- "review_depth",
1081
- "quality_depth",
1082
- "delegate_threshold",
1083
- "heal_max_cycles",
1084
- "verbosity"
1085
- ];
1086
- var DEFAULTS = {
1087
- auto_commit: "on",
1088
- delegate_threshold: "60",
1089
- heal_max_cycles: "3",
1090
- verbosity: "normal"
1091
- };
1092
- var BRANCH_PRESETS = {
1093
- 1: "feature/{type}-{issue}-{slug}",
1094
- 2: "feature/{slug}",
1095
- 3: "feat/{slug}"
1096
- };
1097
- var COMMIT_PRESETS = {
1098
- 1: "conventional",
1099
- 2: "gitmoji",
1100
- 3: "plain"
1101
- };
1102
- function parseConfig(lines) {
1103
- const map = new Map;
1104
- for (const line of lines) {
1105
- const t = line.trim();
1106
- if (!t || t.startsWith("#"))
1107
- continue;
1108
- const eq = t.indexOf("=");
1109
- if (eq === -1)
1110
- continue;
1111
- const k = t.slice(0, eq).trim();
1112
- const v = t.slice(eq + 1).trim();
1113
- if (!map.has(k))
1114
- map.set(k, v);
1115
- }
1116
- return map;
1117
- }
1118
- function buildConfig(existing, updates) {
1119
- const merged = new Map(existing);
1120
- for (const [k, v] of Object.entries(updates))
1121
- merged.set(k, v);
1122
- const lines = [];
1123
- for (const k of DEFAULT_ORDER) {
1124
- const v = merged.get(k) ?? DEFAULTS[k];
1125
- if (v === undefined)
1126
- continue;
1127
- lines.push(`${k}=${v}`);
1128
- merged.delete(k);
1129
- }
1130
- for (const [k, v] of merged)
1131
- lines.push(`${k}=${v}`);
1132
- return lines.join(`
1133
- `) + `
1134
- `;
1135
- }
1136
- async function askFreeText(rl, prompt) {
1137
- for (;; ) {
1138
- const v = (await rl.question(prompt)).trim();
1139
- if (v)
1140
- return v;
1141
- console.log(" Cannot be empty.");
1142
- }
1143
- }
1144
- function assertNotSymlink2(file) {
1145
- if (!existsSync7(file))
1146
- return;
1147
- try {
1148
- if (lstatSync2(file).isSymbolicLink())
1149
- throw new Error(`refusing to follow symlink: ${file}`);
1150
- } catch (e) {
1151
- if (e.code === "ENOENT")
1152
- return;
1153
- throw e;
1154
- }
1155
- }
1156
- async function runOnboard(projectDir) {
1157
- if (!process.stdin.isTTY) {
1158
- throw new Error("Not a terminal. Run `mugiwara onboard` in a terminal, or edit .mugiwara/config directly.");
1159
- }
1160
- const mugiwaraDir = join8(projectDir, ".mugiwara");
1161
- const configPath = join8(mugiwaraDir, "config");
1162
- assertNotSymlink2(configPath);
1163
- const existing = parseConfig(existsSync7(configPath) ? readFileSync6(configPath, "utf8").split(/\r?\n/) : []);
1164
- const rl = createRl();
1165
- try {
1166
- console.log("");
1167
- console.log("╔══════════════════════════════════════╗");
1168
- console.log("║ Mugiwara Onboarding Wizard ║");
1169
- console.log("╚══════════════════════════════════════╝");
1170
- console.log("");
1171
- if (existing.size > 0) {
1172
- const redo = await confirm(rl, "Existing .mugiwara/config found. Re-onboard?");
1173
- if (!redo) {
1174
- console.log("Onboarding cancelled. Config preserved.");
1175
- return;
1568
+ const fd = openSync(indexFile, "a");
1569
+ try {
1570
+ writeSync(fd, header + line);
1571
+ } finally {
1572
+ closeSync(fd);
1176
1573
  }
1177
- console.log("");
1178
- }
1179
- const branchChoice = await choose(rl, "Q1 — Branch naming pattern:", [
1180
- "Trunk-based — feature/{type}-{issue}-{slug}",
1181
- "GitFlow — feature/{slug}",
1182
- "GitHub Flow — feat/{slug}",
1183
- "Custom — enter your own pattern"
1184
- ]);
1185
- let branch;
1186
- if (branchChoice === 3) {
1187
- branch = await askFreeText(rl, " Branch pattern (placeholders: {type} {issue} {slug}, e.g. {issue}-{slug}): ");
1188
- } else {
1189
- branch = BRANCH_PRESETS[branchChoice + 1];
1190
1574
  }
1191
- const modeIdx = await choose(rl, "Q2 — Autonomy mode:", [
1192
- "guided — ask before every wave transition",
1193
- "semi — auto-advance through waves, pause on failures",
1194
- "auto — full auto-pilot"
1195
- ]);
1196
- const mode = ["guided", "semi", "auto"][modeIdx];
1197
- const reviewIdx = await choose(rl, "Q3 — Code review depth:", [
1198
- "full — breaking-change map, five-axis review, <=3 cycles",
1199
- "standard — five-axis review, 1 cycle",
1200
- "quick — diff-only, no caller-map"
1201
- ]);
1202
- const reviewDepth = ["full", "standard", "quick"][reviewIdx];
1203
- const qualityIdx = await choose(rl, "Q4 — Quality check depth:", [
1204
- "full — format, lint, typecheck, test, build",
1205
- "standard — lint, typecheck, test",
1206
- "quick — test only"
1207
- ]);
1208
- const qualityDepth = ["full", "standard", "quick"][qualityIdx];
1209
- const covIdx = await choose(rl, "Q5 — Test coverage threshold:", [
1210
- "90/80 — new code 90%, modified 80%",
1211
- "80/70 — new code 80%, modified 70%",
1212
- "custom — enter your own values",
1213
- "none — 0/0, no coverage enforcement"
1214
- ]);
1215
- let coverageNew = 90;
1216
- let coverageModified = 80;
1217
- if (covIdx === 1) {
1218
- coverageNew = 80;
1219
- coverageModified = 70;
1220
- } else if (covIdx === 2) {
1221
- coverageNew = Number.parseInt(await askFreeText(rl, " Coverage for new code (%): "), 10) || 0;
1222
- coverageModified = Number.parseInt(await askFreeText(rl, " Coverage for modified code (%): "), 10) || 0;
1223
- } else if (covIdx === 3) {
1224
- coverageNew = 0;
1225
- coverageModified = 0;
1226
- }
1227
- const commitChoice = await choose(rl, "Q6 — Commit style:", [
1228
- "Conventional Commits — feat:, fix:, chore:, docs:",
1229
- "Gitmoji — emoji-prefixed conventional",
1230
- "Plain — no prefix, imperative sentence",
1231
- "Custom template — enter your own (placeholders: {type} {issue} {title})"
1232
- ]);
1233
- let commit;
1234
- if (commitChoice === 3) {
1235
- commit = await askFreeText(rl, " Commit template (e.g. {issue}: {title}): ");
1236
- } else {
1237
- commit = COMMIT_PRESETS[commitChoice + 1];
1238
- }
1239
- if (!existsSync7(mugiwaraDir))
1240
- mkdirSync6(mugiwaraDir, { recursive: true });
1241
- const body = buildConfig(existing, {
1242
- mode,
1243
- branch,
1244
- commit,
1245
- coverage_new: String(coverageNew),
1246
- coverage_modified: String(coverageModified),
1247
- review_depth: reviewDepth,
1248
- quality_depth: qualityDepth
1249
- });
1250
- writeFileSync5(configPath, `# .mugiwara/config — written by mugiwara onboard on ${new Date().toISOString().slice(0, 10)}
1251
- ${body}`);
1252
- console.log("╔══════════════════════════════════════╗");
1253
- console.log("║ Onboarding Complete ║");
1254
- console.log("╚══════════════════════════════════════╝");
1255
- console.log("");
1256
- console.log(` Branch pattern: ${branch}`);
1257
- console.log(` Autonomy mode: ${mode}`);
1258
- console.log(` Review depth: ${reviewDepth}`);
1259
- console.log(` Quality depth: ${qualityDepth}`);
1260
- console.log(` Coverage: ${coverageNew}/${coverageModified}`);
1261
- console.log(` Commit style: ${commit}`);
1262
- console.log("");
1263
- console.log(` Config written: ${configPath}`);
1264
- console.log("");
1265
- } finally {
1266
- rl.close();
1575
+ index = "index.md";
1267
1576
  }
1577
+ return { report: join11("missions", mission, "report.md"), removed, kept, index };
1268
1578
  }
1269
1579
 
1270
1580
  // src/run.ts
1271
- import { existsSync as existsSync8, readdirSync as readdirSync5 } from "node:fs";
1581
+ import { existsSync as existsSync9, readdirSync as readdirSync7 } from "node:fs";
1272
1582
  import { spawnSync } from "node:child_process";
1273
- import { dirname as dirname5, join as join9, resolve } from "node:path";
1583
+ import { dirname as dirname5, join as join12, resolve } from "node:path";
1274
1584
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1275
1585
  var here3 = dirname5(fileURLToPath4(import.meta.url));
1276
- var SCRIPTS_DIR = join9(here3, "..", "scripts");
1277
- var RUNNABLE = ["savepoint.sh", "lane.sh", "evidence.sh", "mission-report.sh"];
1586
+ var SCRIPTS_DIR = join12(here3, "..", "scripts");
1587
+ var RUNNABLE = ["savepoint.sh", "lane.sh"];
1278
1588
  function findBash() {
1279
1589
  const explicit = process.env.MUGIWARA_BASH?.trim();
1280
1590
  if (explicit)
1281
- return existsSync8(explicit) ? explicit : null;
1591
+ return existsSync9(explicit) ? explicit : null;
1282
1592
  const candidates = process.platform === "win32" ? [
1283
1593
  "C:\\Program Files\\Git\\bin\\bash.exe",
1284
1594
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
1285
- join9(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
1595
+ join12(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
1286
1596
  ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
1287
1597
  for (const p of candidates) {
1288
- if (p && existsSync8(p))
1598
+ if (p && existsSync9(p))
1289
1599
  return p;
1290
1600
  }
1291
1601
  const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
@@ -1302,9 +1612,9 @@ function runScript(name, args, projectDir) {
1302
1612
  if (!/^[a-z0-9-]+\.sh$/.test(name)) {
1303
1613
  throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
1304
1614
  }
1305
- const script = join9(SCRIPTS_DIR, name);
1306
- if (!existsSync8(script)) {
1307
- const have = existsSync8(SCRIPTS_DIR) ? readdirSync5(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
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")) : [];
1308
1618
  throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
1309
1619
  }
1310
1620
  const bash = findBash();
@@ -1321,9 +1631,9 @@ function runScript(name, args, projectDir) {
1321
1631
  }
1322
1632
 
1323
1633
  // src/continue.ts
1324
- import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "node:fs";
1325
- import { execFileSync } from "node:child_process";
1326
- import { join as join10 } from "node:path";
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";
1327
1637
  var SAFE = /^[A-Za-z0-9._-]+$/;
1328
1638
  var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
1329
1639
  var num = (v) => {
@@ -1338,34 +1648,46 @@ function gitActor(cwd) {
1338
1648
  const author = (process.env.GIT_AUTHOR_NAME ?? "").trim();
1339
1649
  if (author)
1340
1650
  return author;
1341
- const git = (args) => {
1651
+ const git3 = (args) => {
1342
1652
  try {
1343
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1653
+ return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1344
1654
  } catch {
1345
1655
  return "";
1346
1656
  }
1347
1657
  };
1348
- const name = git(["config", "user.name"]);
1349
- const email = git(["config", "user.email"]);
1658
+ const name = git3(["config", "user.name"]);
1659
+ const email = git3(["config", "user.email"]);
1350
1660
  if (name && email)
1351
1661
  return `${name} <${email}>`;
1352
1662
  return name || process.env.USER || process.env.USERNAME || "";
1353
1663
  }
1354
- function scan(projectDir, root, map) {
1355
- const base = join10(projectDir, ".mugiwara", root);
1356
- if (!existsSync9(base))
1664
+ function scan(projectDir, kind, map) {
1665
+ const base = join13(projectDir, ".mugiwara", "missions");
1666
+ if (!existsSync10(base))
1357
1667
  return [];
1358
1668
  const out = [];
1359
- const missions = readdirSync6(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
1669
+ const missions = readdirSync8(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
1360
1670
  for (const mission of missions) {
1361
- const dir = join10(base, mission);
1362
- for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
1671
+ const dir = join13(base, mission);
1672
+ for (const f of readdirSync8(dir).filter((n) => n.endsWith(".json")).sort()) {
1363
1673
  const stem = f.slice(0, -".json".length);
1364
- const member = stem === "state" ? null : stem;
1674
+ let member;
1675
+ if (kind === "state") {
1676
+ if (stem === "continue" || stem.startsWith("continue-"))
1677
+ continue;
1678
+ member = stem === "state" ? null : stem;
1679
+ } else {
1680
+ if (stem === "continue")
1681
+ member = null;
1682
+ else if (stem.startsWith("continue-"))
1683
+ member = stem.slice("continue-".length);
1684
+ else
1685
+ continue;
1686
+ }
1365
1687
  if (member !== null && !isSafeKey(member))
1366
1688
  continue;
1367
1689
  try {
1368
- const raw = JSON.parse(readFileSync7(join10(dir, f), "utf8"));
1690
+ const raw = JSON.parse(readFileSync8(join13(dir, f), "utf8"));
1369
1691
  if (text(raw.mission) !== mission)
1370
1692
  continue;
1371
1693
  out.push(map(raw, member));
@@ -1407,8 +1729,10 @@ function readState(projectDir) {
1407
1729
  lane_rose: r.lane_rose === true,
1408
1730
  lane_prev: text(r.lane_prev),
1409
1731
  lane_peak: text(r.lane_peak),
1410
- next_action: "",
1411
- next_session_prompt: "",
1732
+ base_sha: text(r.base_sha),
1733
+ sensitive_paths: Array.isArray(r.sensitive_paths) ? r.sensitive_paths.map(text).filter(Boolean) : [],
1734
+ next_action: text(r.next_action),
1735
+ next_session_prompt: text(r.next_session_prompt),
1412
1736
  updated_at: text(r.updated_at),
1413
1737
  blockers_open: num(r.blockers_open),
1414
1738
  heal_cycle: num(r.heal_cycle),
@@ -1478,6 +1802,59 @@ function formatResume(e) {
1478
1802
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
1479
1803
  }
1480
1804
 
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
+ }
1856
+ }
1857
+
1481
1858
  // src/cli.ts
1482
1859
  var str = (v) => typeof v === "string" ? v : undefined;
1483
1860
  var flag = (v) => v === true;
@@ -1502,8 +1879,8 @@ async function run(argv) {
1502
1879
  return resetCmd(flags);
1503
1880
  case "archive":
1504
1881
  return archive(flags, _);
1505
- case "onboard":
1506
- return onboardCmd(flags);
1882
+ case "clean":
1883
+ return cleanCmd(flags);
1507
1884
  case "continue":
1508
1885
  return continueCmd(flags, _);
1509
1886
  case "status":
@@ -1512,10 +1889,12 @@ async function run(argv) {
1512
1889
  return runCmd(flags, _);
1513
1890
  case "savepoint":
1514
1891
  return runCmd(flags, ["run", "savepoint.sh", ..._.slice(1)]);
1515
- case "initiative":
1516
- return initiativeCmd(flags, _);
1517
- case "start":
1518
- return startCmd(flags, _);
1892
+ case "blame":
1893
+ return blameCmd(flags, _);
1894
+ case "handoff":
1895
+ return handoffCmd(flags, _);
1896
+ case "sign":
1897
+ return signCmd(flags, _);
1519
1898
  default:
1520
1899
  throw new Error(`Unknown command: ${command}`);
1521
1900
  }
@@ -1545,6 +1924,8 @@ function archive(flags, positionals) {
1545
1924
  const result = archiveMission(projectDir, mission, { dryRun: flag(flags.dryRun) });
1546
1925
  if (result.report)
1547
1926
  console.log(`archive target: ${result.report}`);
1927
+ else
1928
+ console.error(`no mission dir for "${mission}" under .mugiwara/missions/`);
1548
1929
  if (result.removed.length)
1549
1930
  console.log(`${flag(flags.dryRun) ? "would remove" : "removed"}: ${result.removed.join(", ")}`);
1550
1931
  if (result.kept.length)
@@ -1552,11 +1933,59 @@ function archive(flags, positionals) {
1552
1933
  if (result.index)
1553
1934
  console.log(`index updated: ${result.index}`);
1554
1935
  }
1555
- async function onboardCmd(flags) {
1936
+ function cleanCmd(flags) {
1556
1937
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1557
- if (!existsSync10(projectDir))
1558
- throw new Error(`Project dir not found: ${projectDir}`);
1559
- await runOnboard(projectDir);
1938
+ const dryRun = flag(flags.dryRun);
1939
+ const root = join15(projectDir, ".mugiwara", "missions");
1940
+ if (!existsSync12(root)) {
1941
+ console.log("nothing to clean (.mugiwara/missions/ does not exist).");
1942
+ return;
1943
+ }
1944
+ const before = str(flags.before);
1945
+ const beforeMs = before ? Date.parse(before) : NaN;
1946
+ if (before && !Number.isFinite(beforeMs)) {
1947
+ console.error(`invalid --before date: ${before}`);
1948
+ process.exit(1);
1949
+ }
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) => {
1952
+ const stem = f.replace(/\.json$/, "");
1953
+ return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
1954
+ });
1955
+ const hasLiveState = (m) => stateFiles(m).length > 0;
1956
+ const staleBefore = (m) => {
1957
+ if (!Number.isFinite(beforeMs))
1958
+ return false;
1959
+ for (const f of stateFiles(m)) {
1960
+ try {
1961
+ const ts = Date.parse(JSON.parse(readFileSync9(join15(root, m, f), "utf8")).updated_at ?? "") || 0;
1962
+ if (ts === 0 || ts >= beforeMs)
1963
+ return false;
1964
+ } catch {
1965
+ return false;
1966
+ }
1967
+ }
1968
+ return true;
1969
+ };
1970
+ if (!flag(flags.all)) {
1971
+ candidates = candidates.filter((m) => existsSync12(join15(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
1972
+ } else if (!flag(flags.force)) {
1973
+ const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
1974
+ if (live.length) {
1975
+ console.error(`✗ in-flight mission(s): ${live.join(", ")}. Use --force to archive them anyway.`);
1976
+ process.exit(1);
1977
+ }
1978
+ }
1979
+ if (!candidates.length) {
1980
+ console.log("nothing to clean.");
1981
+ return;
1982
+ }
1983
+ for (const m of candidates) {
1984
+ const r = archiveMission(projectDir, m, { dryRun });
1985
+ console.log(`${dryRun ? "would clean" : "cleaned"} ${m}${r.report ? ` → ${r.report}` : ""}`);
1986
+ if (r.index)
1987
+ console.log(`index updated: ${r.index}`);
1988
+ }
1560
1989
  }
1561
1990
  async function resolveOptions(flags) {
1562
1991
  const interactive = !flag(flags.yes);
@@ -1573,7 +2002,7 @@ async function resolveOptions(flags) {
1573
2002
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
1574
2003
  }
1575
2004
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1576
- if (scope === "project" && !existsSync10(projectDir))
2005
+ if (scope === "project" && !existsSync12(projectDir))
1577
2006
  throw new Error(`Project dir not found: ${projectDir}`);
1578
2007
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
1579
2008
  if (targetIds && targetIds.includes("all"))
@@ -1598,7 +2027,7 @@ async function resolveOptions(flags) {
1598
2027
  }
1599
2028
  async function install(flags) {
1600
2029
  const { scope, projectDir, targetIds } = await resolveOptions(flags);
1601
- const home = homedir2();
2030
+ const home = homedir4();
1602
2031
  const allFiles = [];
1603
2032
  const allNotes = [];
1604
2033
  const installed = [];
@@ -1640,34 +2069,19 @@ Dry run — nothing written.`);
1640
2069
  OK mugiwara ${VERSION} installed (manifest: ${file})`);
1641
2070
  if (allNotes.length)
1642
2071
  console.log(`${allNotes.length} note(s) above may need attention.`);
1643
- if (scope === "project") {
1644
- const cfg = join11(projectDir, ".mugiwara", "config");
1645
- let mode = "guided", enforce = "block";
1646
- if (existsSync10(cfg)) {
1647
- for (const line of readFileSync8(cfg, "utf8").split(/\r?\n/)) {
1648
- const [k, v] = line.split("=").map((s) => s.trim());
1649
- if (k === "mode")
1650
- mode = v;
1651
- if (k === "enforce")
1652
- enforce = v;
1653
- }
1654
- }
1655
- console.log(`
1656
- Next: run \`mugiwara onboard\` to customise (mode=${mode}, enforce=${enforce} now).`);
1657
- } else {
1658
- console.log("\nNext: run `mugiwara onboard` in a project to write its .mugiwara/config.");
1659
- }
2072
+ console.log(`
2073
+ Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
1660
2074
  }
1661
2075
  async function uninstall(flags) {
1662
2076
  const scope = flag(flags.global) ? "global" : "project";
1663
2077
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1664
- const home = homedir2();
2078
+ const home = homedir4();
1665
2079
  const file = manifestPath({ scope, projectDir, home });
1666
2080
  const manifest = readManifest(file);
1667
2081
  if (!manifest) {
1668
2082
  if (scope === "project") {
1669
2083
  const globalFile = manifestPath({ scope: "global", projectDir, home });
1670
- if (existsSync10(globalFile)) {
2084
+ if (existsSync12(globalFile)) {
1671
2085
  console.log(`No project manifest found, but a global install exists. Try:
1672
2086
  mugiwara uninstall --global`);
1673
2087
  return;
@@ -1704,13 +2118,13 @@ async function uninstall(flags) {
1704
2118
  console.log(` note: ${n}`);
1705
2119
  }
1706
2120
  rmSync3(file);
1707
- const mugiDir = join11(scope === "global" ? home : projectDir, ".mugiwara");
1708
- if (existsSync10(mugiDir) && readdirSync7(mugiDir).length === 0) {
2121
+ const mugiDir = join15(scope === "global" ? home : projectDir, ".mugiwara");
2122
+ if (existsSync12(mugiDir) && readdirSync9(mugiDir).length === 0) {
1709
2123
  rmSync3(mugiDir, { recursive: true, force: true });
1710
2124
  }
1711
2125
  if (manifest.targets.includes("opencode")) {
1712
- const opencodeCache = join11(home, ".cache", "opencode", "packages", "@ionivetech");
1713
- if (existsSync10(opencodeCache)) {
2126
+ const opencodeCache = join15(home, ".cache", "opencode", "packages", "@ionivetech");
2127
+ if (existsSync12(opencodeCache)) {
1714
2128
  rmSync3(opencodeCache, { recursive: true, force: true });
1715
2129
  console.log(" cleared opencode npm cache (stale plugin versions)");
1716
2130
  }
@@ -1719,7 +2133,7 @@ async function uninstall(flags) {
1719
2133
  console.log(`OK removed ${removed.length} files`);
1720
2134
  }
1721
2135
  function list(flags) {
1722
- const home = homedir2();
2136
+ const home = homedir4();
1723
2137
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1724
2138
  let found = false;
1725
2139
  for (const [label, file] of [
@@ -1731,7 +2145,7 @@ function list(flags) {
1731
2145
  continue;
1732
2146
  found = true;
1733
2147
  if (flag(flags.check)) {
1734
- const missing = m.files.filter((f) => !existsSync10(f));
2148
+ const missing = m.files.filter((f) => !existsSync12(f));
1735
2149
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
1736
2150
  } else {
1737
2151
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -1753,6 +2167,10 @@ function continueCmd(flags, positionals) {
1753
2167
  const r = resolveContinue(entries, mission, member);
1754
2168
  if (r.kind === "resume") {
1755
2169
  console.log(formatResume(r.entry));
2170
+ const st = readState(projectDir).find((s) => s.mission === r.entry.mission && s.member === r.entry.member);
2171
+ const stale = st ? stalenessLine(projectDir, st.base_sha) : null;
2172
+ if (stale)
2173
+ console.log(stale);
1756
2174
  return;
1757
2175
  }
1758
2176
  if (r.kind === "none") {
@@ -1809,85 +2227,104 @@ function runCmd(flags, positionals) {
1809
2227
  if (code !== 0)
1810
2228
  process.exit(code);
1811
2229
  }
1812
- function initiativeCmd(flags, positionals) {
2230
+ function blameCmd(flags, positionals) {
1813
2231
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1814
- const sub = positionals[1];
1815
- if (!sub) {
1816
- console.error("usage: mugiwara initiative <status|conflict-check|set-status> <plan-file> [--id <id> --status <s>]");
2232
+ const path = positionals[1];
2233
+ if (!path) {
2234
+ console.error("usage: mugiwara blame <file-path>");
1817
2235
  process.exit(1);
1818
2236
  }
1819
- const script = join11(import.meta.dirname, "..", "scripts", "initiative.ts");
1820
- const bun = process.platform === "win32" ? "bun.exe" : "bun";
1821
- const r = spawnSync2(bun, [script, ...positionals.slice(1)], {
1822
- cwd: projectDir,
1823
- stdio: "inherit",
1824
- env: process.env
1825
- });
1826
- if (r.error)
1827
- throw r.error;
1828
- if (r.status)
1829
- process.exit(r.status);
2237
+ console.log(blamePath(projectDir, path));
2238
+ }
2239
+ function stalenessLine(projectDir, baseSha) {
2240
+ if (!baseSha || baseSha === "unknown")
2241
+ return null;
2242
+ const git3 = (args) => {
2243
+ try {
2244
+ return execFileSync6("git", args, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2245
+ } catch {
2246
+ return "";
2247
+ }
2248
+ };
2249
+ let main = "";
2250
+ for (const ref of ["main", "master"]) {
2251
+ main = git3(["rev-parse", "--verify", ref]);
2252
+ if (main)
2253
+ break;
2254
+ }
2255
+ if (!main)
2256
+ return null;
2257
+ try {
2258
+ const behind = Number(git3(["rev-list", "--count", `${baseSha}..${main}`])) || 0;
2259
+ return behind > 0 ? `⚠ stale base: main is ${behind} commit(s) ahead of this mission's base ${baseSha.slice(0, 7)} — rebase check before continuing` : null;
2260
+ } catch {
2261
+ return null;
2262
+ }
1830
2263
  }
1831
- function startCmd(flags, positionals) {
2264
+ function handoffCmd(flags, positionals) {
1832
2265
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
1833
2266
  const mission = positionals[1];
1834
2267
  if (!mission) {
1835
- console.error("usage: mugiwara start <mission> [member]");
2268
+ console.error("usage: mugiwara handoff <mission> [--project <dir>]");
1836
2269
  process.exit(1);
1837
2270
  }
1838
- const plansDir = join11(projectDir, ".mugiwara", "plans");
1839
- let planFile = "";
1840
- if (existsSync10(plansDir)) {
1841
- const bare = join11(plansDir, `${mission}.md`);
1842
- if (existsSync10(bare))
1843
- planFile = bare;
1844
- else {
1845
- const dated = readdirSync7(plansDir).filter((f) => f.endsWith(`-${mission}.md`)).sort().pop();
1846
- if (dated)
1847
- planFile = join11(plansDir, dated);
1848
- }
1849
- }
1850
- if (!planFile) {
1851
- console.error(`mugiwara start: no plan found for "${mission}" in ${plansDir}`);
2271
+ const states = readState(projectDir).filter((s) => s.mission === mission);
2272
+ if (!states.length) {
2273
+ console.error(`no in-flight mission "${mission}"`);
1852
2274
  process.exit(1);
1853
2275
  }
1854
- const plan = readFileSync8(planFile, "utf8");
1855
- const subMatch = plan.match(/##\s*Sub-missions/i);
1856
- let member = positionals[2] ?? "";
1857
- if (subMatch && !member) {
1858
- const actor = gitActor(projectDir);
1859
- const name = actor.split("<")[0].trim();
1860
- const rowRe = /^\|\s*([\w.-]+)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/gm;
1861
- let m;
1862
- let found = "";
1863
- while (m = rowRe.exec(plan)) {
1864
- const assignee = m[3].trim();
1865
- if (assignee === name || assignee === actor) {
1866
- found = m[1].trim();
1867
- break;
1868
- }
1869
- }
1870
- if (!found) {
1871
- console.error(`mugiwara start: no sub-mission assigned to "${name}" in ${planFile}. Pass a member explicitly: mugiwara start ${mission} <member>`);
1872
- process.exit(1);
1873
- }
1874
- member = found;
2276
+ const lines = [
2277
+ `# Handoff: ${mission}`,
2278
+ "",
2279
+ `Generated ${new Date().toISOString()} by \`mugiwara handoff\`.`,
2280
+ "",
2281
+ "| | |",
2282
+ "|---|---|"
2283
+ ];
2284
+ for (const s of states) {
2285
+ const scope = s.member ? ` [${s.member}]` : "";
2286
+ lines.push(`| Mission${scope} | flow ${s.flow}, tasks ${s.tasks_done}/${s.tasks_total}, lane ${s.lane}${s.lane_rose ? " (rose)" : ""}, mode ${s.mode} |`);
2287
+ lines.push(`| Branch | \`${s.branch}\` |`);
2288
+ lines.push(`| Actor | ${s.actor || "(unknown)"} |`);
2289
+ if (s.next_action)
2290
+ lines.push(`| Next action | ${s.next_action} |`);
2291
+ if (s.blockers_open)
2292
+ lines.push(`| Open blockers | ${s.blockers_open} |`);
2293
+ if (s.heal_cycle)
2294
+ lines.push(`| Heal cycles | ${s.heal_cycle}/${s.heal_max_cycles}${s.heal_halt ? " — HALTED" : ""} |`);
2295
+ if (s.evidence.length)
2296
+ lines.push(`| Evidence | ${s.evidence.join(", ")} |`);
2297
+ const stale = stalenessLine(projectDir, s.base_sha);
2298
+ if (stale)
2299
+ lines.push(`| Staleness | ${stale.replace("⚠ stale base: ", "")} |`);
2300
+ }
2301
+ lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
2302
+ 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(`
2305
+ `) + `
2306
+ `);
2307
+ console.log(lines.join(`
2308
+ `));
2309
+ console.log(`
2310
+ written: ${out}`);
2311
+ }
2312
+ function signCmd(flags, _) {
2313
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
2314
+ const mission = _[1];
2315
+ if (!mission) {
2316
+ console.error("usage: mugiwara sign <mission> [--verify] [--project <dir>]");
2317
+ process.exit(1);
1875
2318
  }
1876
- const code = runScript("savepoint.sh", [mission, member, "0", "auto", "full"], projectDir);
1877
- if (code !== 0)
1878
- process.exit(code);
1879
- if (subMatch && member) {
1880
- const init = join11(import.meta.dirname, "..", "scripts", "initiative.ts");
1881
- const bun = process.platform === "win32" ? "bun.exe" : "bun";
1882
- const r = spawnSync2(bun, [init, "set-status", planFile, "--id", member, "--status", "in-progress"], {
1883
- cwd: projectDir,
1884
- stdio: "inherit",
1885
- env: process.env
1886
- });
1887
- if (r.status)
1888
- process.exit(r.status);
2319
+ const missionDir = join15(projectDir, ".mugiwara", "missions", mission);
2320
+ if (!existsSync12(missionDir)) {
2321
+ console.error(`no mission dir: ${missionDir}`);
2322
+ process.exit(1);
1889
2323
  }
1890
- console.log(`mugiwara start: ${mission}${member ? ` [${member}]` : ""} begun at Flow 0. Resume anytime with: mugiwara continue ${mission}${member ? ` ${member}` : ""}`);
2324
+ const r = flag(flags.verify) ? verifyReport(projectDir, missionDir) : signReport(projectDir, missionDir);
2325
+ console.log(`${r.ok ? "✓" : "✗"} ${r.message}`);
2326
+ if (!r.ok)
2327
+ process.exit(1);
1891
2328
  }
1892
2329
  function help() {
1893
2330
  console.log(`mugiwara ${VERSION} — the Straw Hat crew for AI agents
@@ -1898,21 +2335,23 @@ Usage:
1898
2335
  mugiwara uninstall remove installed files via manifest
1899
2336
  mugiwara list show installations
1900
2337
  mugiwara list --check health check: show installations + missing files
1901
- mugiwara reset wipe mission state (spec/plans/results/review/issues[/logs])
1902
- mugiwara archive <m> fold a closed mission's evidence into its report, then remove loose files
1903
- mugiwara onboard run the zero-LLM onboarding wizard (writes .mugiwara/config)
2338
+ mugiwara reset wipe mission state (missions/ + legacy dirs)
2339
+ mugiwara archive <m> fold a closed mission's waves into its report, then remove loose files
2340
+ mugiwara clean [--all] [--before <date>]
2341
+ batch-archive every closed mission (report.md present, no live state)
1904
2342
  mugiwara continue list in-flight missions (exit 2 = pick one, nothing resumed)
1905
2343
  mugiwara continue <m> [member]
1906
2344
  print the exact resume point for that mission/member
1907
2345
  mugiwara status computed mission state: wave, tasks, lane, blockers, budget
2346
+ mugiwara blame <path> provenance note on the last commit touching <path>
2347
+ (fetch notes first: git fetch origin 'refs/notes/mugiwara:refs/notes/mugiwara')
2348
+ mugiwara handoff <m> write .mugiwara/missions/<m>/handoff.md — a report the next
2349
+ engineer can act on (computed state + staleness check)
2350
+ mugiwara sign <m> optional attestation: minisign-sign report.md (--verify to check)
1908
2351
  mugiwara run <script> [args...]
1909
2352
  run a bundled harness script here (${RUNNABLE.join(", ")})
1910
- mugiwara savepoint <mission> [member] [wave] [mode]
2353
+ mugiwara savepoint <mission> [member] [flow] [mode]
1911
2354
  shorthand for: mugiwara run savepoint.sh ...
1912
- mugiwara initiative <status|conflict-check|set-status> <plan> [args]
1913
- team sub-mission coordination (status / conflict-check / set-status)
1914
- mugiwara start <mission> [member]
1915
- begin a mission from its plan (solo, or team sub-mission by git actor)
1916
2355
  mugiwara --help this help
1917
2356
  mugiwara --version print version
1918
2357
 
@@ -1924,8 +2363,10 @@ Flags:
1924
2363
  --force overwrite differing files (with backup)
1925
2364
  --dry-run print actions without writing
1926
2365
  --check with list: report missing files (health check)
1927
- --all with continue/status: every actor, not just yours
1928
- --keep-logs with reset: keep .mugiwara/logs (lessons ledger survives)`);
2366
+ --all with continue/status: every actor; with clean: include in-flight missions
2367
+ --force with clean --all: archive in-flight missions anyway
2368
+ --before <date> with clean: also archive missions untouched since this date
2369
+ --keep-logs with reset: keep lessons.md (lessons ledger survives)`);
1929
2370
  }
1930
2371
  var isMain = process.argv[1] !== undefined && fileURLToPath5(import.meta.url) === resolve2(process.argv[1]);
1931
2372
  if (isMain) {
@@ -1935,5 +2376,6 @@ if (isMain) {
1935
2376
  });
1936
2377
  }
1937
2378
  export {
1938
- run
2379
+ run,
2380
+ stalenessLine
1939
2381
  };