@ionivetech/mugiwara 0.8.0 → 0.8.2

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 (62) 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/README.md +2 -2
  7. package/content/agents/brook-healing.md +1 -1
  8. package/content/agents/memory-keeper.md +5 -0
  9. package/content/agents/usopp-brainstorm.md +3 -2
  10. package/content/agents/zoro-execution.md +4 -3
  11. package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
  12. package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
  13. package/content/skills/mugiwara-execution/SKILL.md +4 -3
  14. package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
  15. package/content/skills/mugiwara-gates/SKILL.md +6 -0
  16. package/content/skills/mugiwara-healing/SKILL.md +5 -1
  17. package/content/skills/mugiwara-lessons/SKILL.md +3 -0
  18. package/content/skills/mugiwara-orchestration/SKILL.md +7 -6
  19. package/content/skills/mugiwara-planning/SKILL.md +2 -0
  20. package/content/skills/mugiwara-quality/SKILL.md +3 -14
  21. package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
  22. package/content/skills/mugiwara-resume/SKILL.md +3 -14
  23. package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
  24. package/content/skills/mugiwara-review/SKILL.md +3 -15
  25. package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
  26. package/content/skills/mugiwara-security/SKILL.md +1 -0
  27. package/content/skills/mugiwara-ship/SKILL.md +2 -0
  28. package/content/skills/mugiwara-workflow/SKILL.md +28 -25
  29. package/dist/mugiwara.js +1323 -402
  30. package/gemini-extension.json +1 -1
  31. package/hooks/mugiwara-mode-tracker.js +24 -4
  32. package/hooks/mugiwara-mode-tracker.ts +36 -7
  33. package/hooks/session-start.js +6 -1
  34. package/hooks/session-start.ts +8 -1
  35. package/package.json +2 -2
  36. package/plugin.json +1 -1
  37. package/references/cost-governor.md +104 -0
  38. package/references/wave-banners.md +1 -2
  39. package/scripts/gate-selftest.ts +239 -21
  40. package/scripts/lane-base.ts +16 -0
  41. package/scripts/lane.sh +5 -1
  42. package/scripts/lib/lane-base.sh +1 -1
  43. package/scripts/savepoint.sh +48 -5
  44. package/scripts/validate-content.ts +60 -0
  45. package/scripts/verify-install.ts +20 -0
  46. package/scripts/write-metrics.ts +73 -0
  47. package/src/budget.ts +11 -0
  48. package/src/cli.ts +185 -28
  49. package/src/config.ts +6 -0
  50. package/src/continue.ts +36 -1
  51. package/src/cost.ts +4 -1
  52. package/src/installer.ts +27 -4
  53. package/src/integrity.ts +105 -25
  54. package/src/mission.ts +123 -7
  55. package/src/policy.ts +372 -4
  56. package/src/provenance.ts +29 -9
  57. package/src/sign.ts +45 -3
  58. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
  59. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
  60. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
  61. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
  62. package/content/skills/mugiwara-workflow/references/stop-slop-governor.md +0 -14
package/dist/mugiwara.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync15, readdirSync as readdirSync10, readFileSync as readFileSync14, rmSync as rmSync3, writeFileSync as writeFileSync10 } 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 join20, 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
@@ -623,7 +623,7 @@ var targets = { claude: target, opencode: target2, copilot: target3, gemini: tar
623
623
  var TARGET_IDS = Object.keys(targets);
624
624
 
625
625
  // src/installer.ts
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";
626
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync4, copyFileSync as copyFileSync3, rmSync, lstatSync as lstatSync2, chmodSync as chmodSync2 } from "node:fs";
627
627
  import { dirname as dirname3, join as join6 } from "node:path";
628
628
  import { homedir as homedir2 } from "node:os";
629
629
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -658,6 +658,17 @@ function configPaths(projectDir) {
658
658
  return [join5(projectDir, ".mugiwara", "config"), join5(homedir(), ".mugiwara", "config")];
659
659
  }
660
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 {}
661
672
  const out = {};
662
673
  for (const file of configPaths(projectDir)) {
663
674
  if (!existsSync4(file))
@@ -696,6 +707,7 @@ function ensureConfig(projectDir) {
696
707
 
697
708
  // src/installer.ts
698
709
  var CONTENT_DIR = join6(dirname3(fileURLToPath3(import.meta.url)), "..", "content");
710
+ var REPO_ROOT = join6(dirname3(fileURLToPath3(import.meta.url)), "..");
699
711
  var pkg = JSON.parse(readFileSync4(join6(dirname3(fileURLToPath3(import.meta.url)), "..", "package.json"), "utf8"));
700
712
  var VERSION = pkg.version;
701
713
  function collectContent() {
@@ -732,9 +744,14 @@ function installTo(target10, opts) {
732
744
  const dirs = target10.paths({ scope, projectDir, home });
733
745
  const backupRoot = join6(scope === "global" ? home : projectDir, ".mugiwara");
734
746
  const result = { written: [], skipped: [], backedUp: [], notes: [] };
735
- const writeOne = (absPath, text) => {
747
+ const writeOne = (absPath, text, mode) => {
736
748
  if (existsSync5(absPath)) {
737
749
  if (readFileSync4(absPath, "utf8") === text) {
750
+ if (!dryRun && mode !== undefined) {
751
+ try {
752
+ chmodSync2(absPath, mode);
753
+ } catch {}
754
+ }
738
755
  result.skipped.push(absPath);
739
756
  return;
740
757
  }
@@ -756,6 +773,11 @@ function installTo(target10, opts) {
756
773
  if (!dryRun) {
757
774
  mkdirSync4(dirname3(absPath), { recursive: true });
758
775
  writeFileSync4(absPath, text);
776
+ if (mode !== undefined) {
777
+ try {
778
+ chmodSync2(absPath, mode);
779
+ } catch {}
780
+ }
759
781
  }
760
782
  result.written.push(absPath);
761
783
  };
@@ -799,6 +821,18 @@ function installTo(target10, opts) {
799
821
  for (const r of sharedRefs)
800
822
  writeOne(join6(sharedRoot, r.relPath), r.text);
801
823
  }
824
+ {
825
+ const SHELL_FALLBACKS = ["lane.sh", "savepoint.sh", "lib/patterns.sh", "lib/lane-base.sh"];
826
+ const mugiwaraDir = join6(scope === "global" ? home : projectDir, ".mugiwara");
827
+ for (const rel of SHELL_FALLBACKS) {
828
+ const src = join6(REPO_ROOT, "scripts", rel);
829
+ if (!existsSync5(src))
830
+ continue;
831
+ const text = readFileSync4(src, "utf8");
832
+ const dest = join6(mugiwaraDir, "bin", rel);
833
+ writeOne(dest, text, 493);
834
+ }
835
+ }
802
836
  if (target10.postInstall) {
803
837
  const post = target10.postInstall({ scope, projectDir, home, dryRun, files: result.written });
804
838
  result.written.push(...post.written);
@@ -957,32 +991,524 @@ function writeManifest(file, data) {
957
991
  }
958
992
 
959
993
  // src/mission.ts
960
- import { existsSync as existsSync11, rmSync as rmSync2, readFileSync as readFileSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8, renameSync, openSync, writeSync, closeSync } from "node:fs";
961
- import { execFileSync as execFileSync3 } from "node:child_process";
962
- import { join as join16 } from "node:path";
994
+ 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";
995
+ import { execFileSync as execFileSync4 } from "node:child_process";
996
+ import { join as join18 } from "node:path";
997
+
998
+ // src/integrity.ts
999
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
1000
+ import { isAbsolute, join as join9, relative } from "node:path";
1001
+
1002
+ // src/policy.ts
1003
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
1004
+ import { join as join8 } from "node:path";
1005
+ var POLICY_FILES = ["mugiwara.policy.yml", "mugiwara.policy.yaml"];
1006
+ var KNOWN_ROOTS = ["lanes", "gates", "evidence", "integrity", "attestation", "harness"];
1007
+ function parsePolicyYaml(text) {
1008
+ const root = {};
1009
+ const stack = [{ keyIndent: -1, obj: root }];
1010
+ const lines = text.split(/\r?\n/);
1011
+ const process2 = (i) => {
1012
+ if (i >= lines.length)
1013
+ return;
1014
+ const noComment = lines[i].replace(/(^|\s)#.*$/, "");
1015
+ if (!noComment.trim())
1016
+ return process2(i + 1);
1017
+ const indent = noComment.length - noComment.trimStart().length;
1018
+ const line = noComment.trim();
1019
+ while (stack.length > 1 && indent <= stack[stack.length - 1].keyIndent)
1020
+ stack.pop();
1021
+ const top = stack[stack.length - 1];
1022
+ if (top.pending && !line.startsWith("- ") && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
1023
+ const { key: key2, indent: pIndent } = top.pending;
1024
+ delete top.pending;
1025
+ const created = {};
1026
+ top.obj[key2] = created;
1027
+ stack.push({ keyIndent: pIndent, obj: created });
1028
+ return process2(i);
1029
+ }
1030
+ if (line.startsWith("- ")) {
1031
+ if (top.pending) {
1032
+ const existing = top.obj[top.pending.key];
1033
+ const arr = Array.isArray(existing) ? existing : [];
1034
+ arr.push(scalar(line.slice(2)));
1035
+ top.obj[top.pending.key] = arr;
1036
+ }
1037
+ return process2(i + 1);
1038
+ }
1039
+ const colon = line.indexOf(":");
1040
+ if (colon === -1)
1041
+ return process2(i + 1);
1042
+ const key = line.slice(0, colon).trim();
1043
+ const rest = line.slice(colon + 1).trim();
1044
+ if (rest === "") {
1045
+ top.pending = { key, indent };
1046
+ } else {
1047
+ top.obj[key] = scalar(rest);
1048
+ delete top.pending;
1049
+ }
1050
+ process2(i + 1);
1051
+ };
1052
+ process2(0);
1053
+ return root;
1054
+ }
1055
+ function scalar(v) {
1056
+ const t = v.trim().replace(/^["']|["']$/g, "");
1057
+ if (/^-?\d+(\.\d+)?$/.test(t))
1058
+ return Number(t);
1059
+ if (t === "true")
1060
+ return true;
1061
+ if (t === "false")
1062
+ return false;
1063
+ return t;
1064
+ }
1065
+ function extractExtraSecretPatterns(text) {
1066
+ const lines = text.split(/\r?\n/);
1067
+ let inBlock = false;
1068
+ let baseIndent = -1;
1069
+ const out = [];
1070
+ let current = null;
1071
+ let currentIndent = -1;
1072
+ for (const rawLine of lines) {
1073
+ const noComment = rawLine.replace(/(^|\s)#.*$/, "");
1074
+ if (!noComment.trim())
1075
+ continue;
1076
+ const indent = noComment.length - noComment.trimStart().length;
1077
+ const trimmed = noComment.trim();
1078
+ if (!inBlock) {
1079
+ if (trimmed.startsWith("extra_secret_patterns:")) {
1080
+ inBlock = true;
1081
+ baseIndent = indent;
1082
+ continue;
1083
+ }
1084
+ } else {
1085
+ if (indent <= baseIndent && !trimmed.startsWith("-") && trimmed.includes(":")) {
1086
+ break;
1087
+ }
1088
+ if (trimmed.startsWith("-")) {
1089
+ if (current)
1090
+ out.push(current);
1091
+ current = {};
1092
+ currentIndent = indent;
1093
+ const afterDash = trimmed.slice(1).trim();
1094
+ if (!afterDash)
1095
+ continue;
1096
+ if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
1097
+ const inner = afterDash.slice(1, -1);
1098
+ for (const part of inner.split(",")) {
1099
+ const colon = part.indexOf(":");
1100
+ if (colon === -1)
1101
+ continue;
1102
+ const k = part.slice(0, colon).trim();
1103
+ const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1104
+ if (k && v)
1105
+ current[k] = v;
1106
+ }
1107
+ } else if (afterDash.includes(":")) {
1108
+ const colon = afterDash.indexOf(":");
1109
+ const k = afterDash.slice(0, colon).trim();
1110
+ const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1111
+ if (k && v)
1112
+ current[k] = v;
1113
+ } else {
1114
+ const v = afterDash.replace(/^["']|["']$/g, "");
1115
+ if (v)
1116
+ current["pattern"] = v;
1117
+ }
1118
+ } else if (current && trimmed.includes(":")) {
1119
+ if (indent > currentIndent) {
1120
+ const colon = trimmed.indexOf(":");
1121
+ const k = trimmed.slice(0, colon).trim();
1122
+ const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1123
+ if (k && v)
1124
+ current[k] = v;
1125
+ } else if (indent <= baseIndent) {
1126
+ break;
1127
+ }
1128
+ }
1129
+ }
1130
+ }
1131
+ if (current)
1132
+ out.push(current);
1133
+ return out.filter((o) => typeof o.pattern === "string" && typeof o.label === "string" && o.pattern.length > 0);
1134
+ }
1135
+ function extractAttestation(text) {
1136
+ const lines = text.split(/\r?\n/);
1137
+ let attBase = -1;
1138
+ let inAtt = false;
1139
+ let required;
1140
+ const trusted = [];
1141
+ const revoked = [];
1142
+ let collecting = null;
1143
+ let collectBase = -1;
1144
+ let current = null;
1145
+ let curIndent = -1;
1146
+ const flush = () => {
1147
+ if (!current)
1148
+ return;
1149
+ if (collecting === "trusted")
1150
+ trusted.push(current);
1151
+ else if (collecting === "revoked")
1152
+ revoked.push(current);
1153
+ current = null;
1154
+ };
1155
+ for (let idx = 0;idx < lines.length; idx++) {
1156
+ const rawLine = lines[idx];
1157
+ const noComment = rawLine.replace(/(^|\s)#.*$/, "");
1158
+ if (!noComment.trim())
1159
+ continue;
1160
+ const indent = noComment.length - noComment.trimStart().length;
1161
+ const trimmed = noComment.trim();
1162
+ if (!inAtt) {
1163
+ if (trimmed === "attestation:" || trimmed.startsWith("attestation:")) {
1164
+ const after = trimmed.slice("attestation:".length).trim();
1165
+ if (after.startsWith("{")) {
1166
+ return null;
1167
+ }
1168
+ inAtt = true;
1169
+ attBase = indent;
1170
+ continue;
1171
+ }
1172
+ continue;
1173
+ }
1174
+ if (indent <= attBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
1175
+ flush();
1176
+ break;
1177
+ }
1178
+ if (collecting) {
1179
+ if (indent <= collectBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
1180
+ flush();
1181
+ collecting = null;
1182
+ } else if (trimmed.startsWith("-")) {
1183
+ flush();
1184
+ current = {};
1185
+ curIndent = indent;
1186
+ const afterDash = trimmed.slice(1).trim();
1187
+ if (!afterDash)
1188
+ continue;
1189
+ if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
1190
+ const inner = afterDash.slice(1, -1);
1191
+ for (const part of inner.split(",")) {
1192
+ const colon = part.indexOf(":");
1193
+ if (colon === -1)
1194
+ continue;
1195
+ const k = part.slice(0, colon).trim();
1196
+ const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1197
+ if (k && v)
1198
+ current[k] = v;
1199
+ }
1200
+ } else if (afterDash.includes(":")) {
1201
+ const colon = afterDash.indexOf(":");
1202
+ const k = afterDash.slice(0, colon).trim();
1203
+ const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1204
+ if (k && v)
1205
+ current[k] = v;
1206
+ } else {
1207
+ const v = afterDash.replace(/^["']|["']$/g, "");
1208
+ if (v)
1209
+ current["id"] = v;
1210
+ }
1211
+ continue;
1212
+ } else if (current && trimmed.includes(":")) {
1213
+ if (indent > curIndent) {
1214
+ const colon = trimmed.indexOf(":");
1215
+ const k = trimmed.slice(0, colon).trim();
1216
+ const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1217
+ if (k && v)
1218
+ current[k] = v;
1219
+ continue;
1220
+ }
1221
+ }
1222
+ if (collecting)
1223
+ continue;
1224
+ }
1225
+ if (trimmed.startsWith("required:")) {
1226
+ const v = trimmed.slice("required:".length).trim().replace(/^["']|["']$/g, "");
1227
+ if (v === "true")
1228
+ required = true;
1229
+ else if (v === "false")
1230
+ required = false;
1231
+ continue;
1232
+ }
1233
+ if (trimmed.startsWith("trusted_keys:")) {
1234
+ const after = trimmed.slice("trusted_keys:".length).trim();
1235
+ if (after === "[]")
1236
+ continue;
1237
+ collecting = "trusted";
1238
+ collectBase = indent;
1239
+ current = null;
1240
+ continue;
1241
+ }
1242
+ if (trimmed.startsWith("revoked:")) {
1243
+ const after = trimmed.slice("revoked:".length).trim();
1244
+ if (after === "[]")
1245
+ continue;
1246
+ collecting = "revoked";
1247
+ collectBase = indent;
1248
+ current = null;
1249
+ continue;
1250
+ }
1251
+ }
1252
+ flush();
1253
+ if (required === undefined && trusted.length === 0 && revoked.length === 0)
1254
+ return null;
1255
+ const out = {};
1256
+ if (required !== undefined)
1257
+ out.required = required;
1258
+ if (trusted.length)
1259
+ out.trusted_keys = trusted;
1260
+ if (revoked.length)
1261
+ out.revoked = revoked;
1262
+ return out;
1263
+ }
1264
+ function loadPolicy(projectDir) {
1265
+ for (const name of POLICY_FILES) {
1266
+ const file = join8(projectDir, name);
1267
+ if (!existsSync7(file))
1268
+ continue;
1269
+ const text = readFileSync6(file, "utf8");
1270
+ const raw = parsePolicyYaml(text);
1271
+ const extra = extractExtraSecretPatterns(text);
1272
+ if (extra.length) {
1273
+ if (!raw.integrity || typeof raw.integrity !== "object" || Array.isArray(raw.integrity))
1274
+ raw.integrity = {};
1275
+ raw.integrity.extra_secret_patterns = extra;
1276
+ }
1277
+ const att = extractAttestation(text);
1278
+ if (att) {
1279
+ if (!raw.attestation || typeof raw.attestation !== "object" || Array.isArray(raw.attestation))
1280
+ raw.attestation = {};
1281
+ const a = raw.attestation;
1282
+ if (att.required !== undefined)
1283
+ a.required = att.required;
1284
+ if (att.trusted_keys)
1285
+ a.trusted_keys = att.trusted_keys;
1286
+ if (att.revoked)
1287
+ a.revoked = att.revoked;
1288
+ }
1289
+ return normalize(raw);
1290
+ }
1291
+ return null;
1292
+ }
1293
+ function normalize(raw) {
1294
+ for (const k of Object.keys(raw)) {
1295
+ if (!KNOWN_ROOTS.includes(k))
1296
+ throw new Error(`unknown policy key "${k}" (known: ${KNOWN_ROOTS.join(", ")})`);
1297
+ }
1298
+ const out = {};
1299
+ const lanes = raw.lanes;
1300
+ if (lanes && Array.isArray(lanes.force_full))
1301
+ out.lanes = { force_full: strings(lanes.force_full) };
1302
+ const gates = raw.gates;
1303
+ if (gates) {
1304
+ out.gates = {};
1305
+ const cov = gates.coverage;
1306
+ if (cov) {
1307
+ out.gates.coverage = {};
1308
+ if (typeof cov.new === "number")
1309
+ out.gates.coverage.new = cov.new;
1310
+ if (typeof cov.modified === "number")
1311
+ out.gates.coverage.modified = cov.modified;
1312
+ }
1313
+ if (Array.isArray(gates.require_human_approval))
1314
+ out.gates.require_human_approval = strings(gates.require_human_approval);
1315
+ }
1316
+ const evidence = raw.evidence;
1317
+ if (evidence) {
1318
+ const ev = {};
1319
+ if (Array.isArray(evidence.required))
1320
+ ev.required = strings(evidence.required);
1321
+ else if (typeof evidence.required === "string" && evidence.required.trim().startsWith("[")) {
1322
+ try {
1323
+ const p = JSON.parse(evidence.required);
1324
+ if (Array.isArray(p))
1325
+ ev.required = strings(p);
1326
+ } catch {}
1327
+ }
1328
+ if (Array.isArray(evidence.require_nonempty_for_lanes))
1329
+ ev.require_nonempty_for_lanes = strings(evidence.require_nonempty_for_lanes);
1330
+ else if (typeof evidence.require_nonempty_for_lanes === "string" && evidence.require_nonempty_for_lanes.trim().startsWith("[")) {
1331
+ try {
1332
+ const p = JSON.parse(evidence.require_nonempty_for_lanes);
1333
+ if (Array.isArray(p))
1334
+ ev.require_nonempty_for_lanes = strings(p);
1335
+ } catch {}
1336
+ }
1337
+ if (ev.required || ev.require_nonempty_for_lanes)
1338
+ out.evidence = ev;
1339
+ }
1340
+ const integrity = raw.integrity;
1341
+ if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
1342
+ const arr = integrity.extra_secret_patterns;
1343
+ const cleaned = [];
1344
+ for (const e of arr) {
1345
+ if (!e || typeof e !== "object")
1346
+ continue;
1347
+ const rec = e;
1348
+ if (typeof rec.pattern !== "string" || typeof rec.label !== "string")
1349
+ continue;
1350
+ const sev = rec.severity === "warn" ? "warn" : rec.severity === "block" ? "block" : undefined;
1351
+ const entry = { pattern: rec.pattern, label: rec.label };
1352
+ if (sev)
1353
+ entry.severity = sev;
1354
+ cleaned.push(entry);
1355
+ }
1356
+ if (cleaned.length)
1357
+ out.integrity = { extra_secret_patterns: cleaned };
1358
+ }
1359
+ const att = raw.attestation;
1360
+ if (att) {
1361
+ const a = {};
1362
+ if (typeof att.required === "boolean")
1363
+ a.required = att.required;
1364
+ if (Array.isArray(att.trusted_keys)) {
1365
+ const cleanedTk = [];
1366
+ for (const e of att.trusted_keys) {
1367
+ if (!e || typeof e !== "object")
1368
+ continue;
1369
+ const rec = e;
1370
+ if (typeof rec.id !== "string" || typeof rec.pubkey !== "string")
1371
+ continue;
1372
+ if (!rec.id.trim() || !rec.pubkey.trim())
1373
+ continue;
1374
+ const entry = { id: rec.id.trim(), pubkey: rec.pubkey.trim() };
1375
+ if (typeof rec.added === "string" && rec.added.trim())
1376
+ entry.added = rec.added.trim();
1377
+ cleanedTk.push(entry);
1378
+ }
1379
+ if (cleanedTk.length)
1380
+ a.trusted_keys = cleanedTk;
1381
+ }
1382
+ if (Array.isArray(att.revoked)) {
1383
+ const cleanedRv = [];
1384
+ for (const e of att.revoked) {
1385
+ if (!e || typeof e !== "object")
1386
+ continue;
1387
+ const rec = e;
1388
+ if (typeof rec.id !== "string" || !rec.id.trim())
1389
+ continue;
1390
+ const entry = { id: rec.id.trim() };
1391
+ if (typeof rec.revoked === "string" && rec.revoked.trim())
1392
+ entry.revoked = rec.revoked.trim();
1393
+ if (typeof rec.reason === "string" && rec.reason.trim())
1394
+ entry.reason = rec.reason.trim();
1395
+ if (typeof rec.pubkey === "string" && rec.pubkey.trim())
1396
+ entry.pubkey = rec.pubkey.trim();
1397
+ cleanedRv.push(entry);
1398
+ }
1399
+ if (cleanedRv.length)
1400
+ a.revoked = cleanedRv;
1401
+ }
1402
+ if (a.required !== undefined || a.trusted_keys || a.revoked)
1403
+ out.attestation = a;
1404
+ }
1405
+ const harness = raw.harness;
1406
+ if (harness && typeof harness.require_enforcement === "boolean") {
1407
+ out.harness = { require_enforcement: harness.require_enforcement };
1408
+ }
1409
+ return out;
1410
+ }
1411
+ function strings(a) {
1412
+ return a.filter((x) => typeof x === "string" && x.length > 0);
1413
+ }
1414
+ function detectHarness(projectDir) {
1415
+ const e = process.env;
1416
+ const has = (v) => v !== undefined && v !== "";
1417
+ if (has(e.CLAUDECODE) || has(e.CLAUDE_CODE_ENTRYPOINT) || typeof e.ANTHROPIC_MODEL === "string" && /claude/i.test(e.ANTHROPIC_MODEL))
1418
+ return "claude";
1419
+ if (has(e.OPENCODE) || has(e.OPENCODE_TOKENS_FILE))
1420
+ return "opencode";
1421
+ const candidates = [
1422
+ projectDir ? join8(projectDir, ".opencode", "config.json") : null,
1423
+ join8(process.cwd(), ".opencode", "config.json")
1424
+ ].filter(Boolean);
1425
+ for (const p of candidates) {
1426
+ try {
1427
+ if (existsSync7(p))
1428
+ return "opencode";
1429
+ } catch {}
1430
+ }
1431
+ if (has(e.CURSOR) || has(e.VSCODE_GIT_ASKPASS_NODE))
1432
+ return "cursor";
1433
+ return "unknown";
1434
+ }
1435
+ function isEnforcedHarness(projectDir) {
1436
+ return detectHarness(projectDir) === "opencode";
1437
+ }
1438
+ function getHarnessEnforcementError(projectDir) {
1439
+ let policy;
1440
+ try {
1441
+ policy = loadPolicy(projectDir);
1442
+ } catch (e) {
1443
+ throw e;
1444
+ }
1445
+ if (!policy?.harness?.require_enforcement)
1446
+ return null;
1447
+ if (isEnforcedHarness(projectDir))
1448
+ return null;
1449
+ const h = detectHarness(projectDir);
1450
+ return `harness enforcement required but current harness is rules-based only — use opencode or set harness.require_enforcement:false (detected: ${h})`;
1451
+ }
1452
+ function enforceHarnessPolicy(projectDir) {
1453
+ const err = getHarnessEnforcementError(projectDir);
1454
+ if (!err)
1455
+ return;
1456
+ console.error(`✗ ${err}`);
1457
+ process.exit(1);
1458
+ }
963
1459
 
964
1460
  // src/integrity.ts
965
- import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
966
- import { isAbsolute, join as join8, relative } from "node:path";
967
1461
  var SECRET_PATTERNS = [
968
- [/AKIA[0-9A-Z]{16}/, "AWS access key id"],
969
- [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block"],
970
- [/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token"],
971
- [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token"],
972
- [/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)"],
973
- [/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim"],
974
- [/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment"]
1462
+ [/AKIA[0-9A-Z]{16}/, "AWS access key id", "block"],
1463
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block", "block"],
1464
+ [/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token", "block"],
1465
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token", "block"],
1466
+ [/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)", "block"],
1467
+ [/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim", "block"],
1468
+ [/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment", "block"],
1469
+ [/AIza[0-9A-Za-z_-]{35}/, "Google API key", "block"],
1470
+ [/ya29\.[0-9A-Za-z_-]{20,}/, "Google OAuth token", "block"],
1471
+ [/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s@/]{4,}@[^\s/]+/i, "connection string with inline credential", "block"],
1472
+ [/\bAC[a-f0-9]{32}\b/, "Twilio account SID", "block"],
1473
+ [/\bSK[a-f0-9]{32}\b/, "Twilio API key", "block"],
1474
+ [/\bglpat-[A-Za-z0-9_-]{20,}/, "GitLab token", "block"],
1475
+ [/\bnpm_[A-Za-z0-9]{36}\b/, "npm token", "block"],
1476
+ [/\bdop_v1_[a-f0-9]{64}\b/, "DigitalOcean token", "block"],
1477
+ [/\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"]
975
1478
  ];
976
1479
  var ALLOW_SECRET = "mugiwara:allow-secret";
977
- function findSecrets(body) {
1480
+ function loadExtraPatterns(projectRoot) {
1481
+ try {
1482
+ const policy = loadPolicy(projectRoot);
1483
+ const extras = policy?.integrity?.extra_secret_patterns;
1484
+ if (!extras || !Array.isArray(extras))
1485
+ return [];
1486
+ const out = [];
1487
+ for (const e of extras) {
1488
+ if (!e || typeof e.pattern !== "string" || typeof e.label !== "string")
1489
+ continue;
1490
+ const rec = e;
1491
+ const sev = rec.severity === "warn" ? "warn" : "block";
1492
+ try {
1493
+ const re = new RegExp(rec.pattern);
1494
+ out.push([re, rec.label, sev]);
1495
+ } catch {}
1496
+ }
1497
+ return out;
1498
+ } catch {
1499
+ return [];
1500
+ }
1501
+ }
1502
+ function findSecrets(body, extra) {
978
1503
  const out = [];
1504
+ const patterns = extra ? [...SECRET_PATTERNS, ...extra] : SECRET_PATTERNS;
979
1505
  for (const line of body.split(/\r?\n/)) {
980
1506
  if (line.includes(ALLOW_SECRET))
981
1507
  continue;
982
- for (const [re, label] of SECRET_PATTERNS) {
1508
+ for (const [re, label, severity] of patterns) {
983
1509
  const hit = line.match(re);
984
1510
  if (hit)
985
- out.push({ label, hit: hit[0] });
1511
+ out.push({ label, hit: hit[0], severity: severity ?? "block" });
986
1512
  }
987
1513
  }
988
1514
  return out;
@@ -992,7 +1518,7 @@ function trailFiles(dir) {
992
1518
  const out = [];
993
1519
  const walk = (d) => {
994
1520
  for (const e of readdirSync4(d, { withFileTypes: true })) {
995
- const p = join8(d, e.name);
1521
+ const p = join9(d, e.name);
996
1522
  if (e.isDirectory())
997
1523
  walk(p);
998
1524
  else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
@@ -1020,7 +1546,7 @@ function collectPassCitedPaths(missionDir) {
1020
1546
  for (const f of trailFiles(missionDir)) {
1021
1547
  let body;
1022
1548
  try {
1023
- body = readFileSync6(f, "utf8");
1549
+ body = readFileSync7(f, "utf8");
1024
1550
  } catch {
1025
1551
  continue;
1026
1552
  }
@@ -1041,62 +1567,92 @@ function collectPassCitedPaths(missionDir) {
1041
1567
  function checkTrail(missionDir, projectRoot) {
1042
1568
  const issues = [];
1043
1569
  const files = trailFiles(missionDir);
1570
+ const extraPatterns = loadExtraPatterns(projectRoot);
1044
1571
  for (const f of files) {
1045
1572
  let body;
1046
1573
  try {
1047
- body = readFileSync6(f, "utf8");
1574
+ body = readFileSync7(f, "utf8");
1048
1575
  } catch {
1049
1576
  continue;
1050
1577
  }
1051
1578
  for (const target10 of linkedPaths(body)) {
1052
1579
  if (isAbsolute(target10))
1053
1580
  continue;
1054
- const fromMission = join8(missionDir, target10);
1055
- const fromRoot = join8(projectRoot, target10);
1056
- if (!existsSync7(fromMission) && !existsSync7(fromRoot)) {
1581
+ const fromMission = join9(missionDir, target10);
1582
+ const fromRoot = join9(projectRoot, target10);
1583
+ if (!existsSync8(fromMission) && !existsSync8(fromRoot)) {
1057
1584
  issues.push({
1058
1585
  kind: "dangling-path",
1059
1586
  detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
1060
1587
  });
1061
1588
  }
1062
1589
  }
1063
- for (const { label, hit } of findSecrets(body)) {
1590
+ for (const { label, hit, severity } of findSecrets(body, extraPatterns.length ? extraPatterns : undefined)) {
1591
+ const isWarn = severity === "warn";
1064
1592
  issues.push({
1065
- kind: "secret",
1066
- detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
1593
+ kind: isWarn ? "secret-warn" : "secret",
1594
+ detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`,
1595
+ severity: isWarn ? "warn" : "block"
1067
1596
  });
1068
1597
  }
1069
1598
  }
1070
1599
  const evidencePaths = [];
1071
- const evidenceFile = join8(missionDir, "state.json");
1072
- if (existsSync7(evidenceFile)) {
1600
+ const stateFiles = existsSync8(missionDir) ? readdirSync4(missionDir).filter((n) => n.endsWith(".json") && n !== "continue.json" && !n.startsWith("continue-")).sort() : [];
1601
+ for (const name of stateFiles) {
1602
+ const evidenceFile = join9(missionDir, name);
1073
1603
  try {
1074
- const s = JSON.parse(readFileSync6(evidenceFile, "utf8"));
1075
- if (Array.isArray(s.evidence)) {
1076
- for (const e of s.evidence) {
1077
- if (typeof e !== "string" || !e.trim())
1078
- continue;
1079
- evidencePaths.push(e);
1080
- const cand = join8(projectRoot, e);
1081
- if (!isAbsolute(e) && !existsSync7(cand) && !existsSync7(join8(missionDir, e))) {
1082
- issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1083
- }
1604
+ const s = JSON.parse(readFileSync7(evidenceFile, "utf8"));
1605
+ if (!Array.isArray(s.evidence))
1606
+ continue;
1607
+ for (const e of s.evidence) {
1608
+ if (typeof e !== "string" || !e.trim())
1609
+ continue;
1610
+ evidencePaths.push(e);
1611
+ const cand = join9(projectRoot, e);
1612
+ if (!isAbsolute(e) && !existsSync8(cand) && !existsSync8(join9(missionDir, e))) {
1613
+ issues.push({ kind: "evidence", detail: `${name} evidence "${e}" does not exist` });
1614
+ }
1615
+ }
1616
+ } catch {}
1617
+ }
1618
+ if (evidencePaths.length === 0) {
1619
+ let severity = "warn";
1620
+ try {
1621
+ const policy = loadPolicy(projectRoot);
1622
+ const lanes = policy?.evidence?.require_nonempty_for_lanes;
1623
+ if (Array.isArray(lanes) && lanes.length) {
1624
+ const stateLanes = new Set;
1625
+ for (const name of stateFiles) {
1626
+ try {
1627
+ const s = JSON.parse(readFileSync7(join9(missionDir, name), "utf8"));
1628
+ if (typeof s.lane === "string")
1629
+ stateLanes.add(s.lane);
1630
+ } catch {}
1084
1631
  }
1632
+ if ([...stateLanes].some((l) => lanes.includes(l)))
1633
+ severity = "block";
1085
1634
  }
1086
1635
  } catch {}
1636
+ issues.push({
1637
+ kind: "evidence",
1638
+ severity,
1639
+ detail: "mission declares no evidence — closing with zero recorded checks"
1640
+ });
1087
1641
  }
1088
1642
  const passCited = collectPassCitedPaths(missionDir);
1089
1643
  for (const e of passCited) {
1090
1644
  if (!e.trim() || isAbsolute(e))
1091
1645
  continue;
1092
- const candMission = join8(missionDir, e);
1093
- const candRoot = join8(projectRoot, e);
1094
- const resolved = existsSync7(candMission) ? candMission : existsSync7(candRoot) ? candRoot : null;
1646
+ if (/(?:^|\/)(state|continue)(-[^\/]*)?\.json$/.test(e))
1647
+ continue;
1648
+ const candMission = join9(missionDir, e);
1649
+ const candRoot = join9(projectRoot, e);
1650
+ const resolved = existsSync8(candMission) ? candMission : existsSync8(candRoot) ? candRoot : null;
1095
1651
  if (!resolved)
1096
1652
  continue;
1097
1653
  let body;
1098
1654
  try {
1099
- body = readFileSync6(resolved, "utf8");
1655
+ body = readFileSync7(resolved, "utf8");
1100
1656
  } catch {
1101
1657
  continue;
1102
1658
  }
@@ -1112,12 +1668,12 @@ function formatIssues(issues) {
1112
1668
  }
1113
1669
 
1114
1670
  // src/check-artifacts.ts
1115
- import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
1116
- import { join as join9 } from "node:path";
1671
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
1672
+ import { join as join10 } from "node:path";
1117
1673
  var LANE_MIN = new Set(["standard", "full", "spike"]);
1118
1674
  function checkMissionArtifacts(missionDir) {
1119
- const statePath = join9(missionDir, "state.json");
1120
- if (!existsSync8(statePath)) {
1675
+ const statePath = join10(missionDir, "state.json");
1676
+ if (!existsSync9(statePath)) {
1121
1677
  return { ok: true, missing: [], lane: null };
1122
1678
  }
1123
1679
  let lane = "unknown";
@@ -1130,17 +1686,17 @@ function checkMissionArtifacts(missionDir) {
1130
1686
  return { ok: true, missing: [], lane };
1131
1687
  }
1132
1688
  const missing = [];
1133
- if (!existsSync8(join9(missionDir, "plan.md")))
1689
+ if (!existsSync9(join10(missionDir, "plan.md")))
1134
1690
  missing.push("plan.md");
1135
- const flowsDir = join9(missionDir, "flows");
1136
- const hasFlows = existsSync8(flowsDir) && readdirSync5(flowsDir).length > 0;
1691
+ const flowsDir = join10(missionDir, "flows");
1692
+ const hasFlows = existsSync9(flowsDir) && readdirSync5(flowsDir).length > 0;
1137
1693
  if (!hasFlows)
1138
1694
  missing.push("flows/ (no execution evidence)");
1139
1695
  return { ok: missing.length === 0, missing, lane };
1140
1696
  }
1141
1697
  function readFileSyncSafe(p) {
1142
1698
  try {
1143
- return readFileSync7(p, "utf8");
1699
+ return readFileSync8(p, "utf8");
1144
1700
  } catch {
1145
1701
  return "";
1146
1702
  }
@@ -1149,7 +1705,7 @@ function readFileSyncSafe(p) {
1149
1705
  // src/rollback.ts
1150
1706
  import { execFileSync } from "node:child_process";
1151
1707
  import { writeFileSync as writeFileSync6 } from "node:fs";
1152
- import { join as join10 } from "node:path";
1708
+ import { join as join11 } from "node:path";
1153
1709
  function git(cwd, args) {
1154
1710
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1155
1711
  }
@@ -1188,7 +1744,7 @@ function generateRollback(projectDir, missionDir, input) {
1188
1744
  const commitsNewestFirst = [...revList].reverse();
1189
1745
  const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
1190
1746
  const body = buildRollback(input, commitsNewestFirst, filesTouched);
1191
- const file = join10(missionDir, "rollback.sh");
1747
+ const file = join11(missionDir, "rollback.sh");
1192
1748
  writeFileSync6(file, body, { mode: 493 });
1193
1749
  return { file: "rollback.sh", commits: commitsNewestFirst.length };
1194
1750
  } catch {
@@ -1199,7 +1755,7 @@ function generateRollback(projectDir, missionDir, input) {
1199
1755
  // src/provenance.ts
1200
1756
  import { execFileSync as execFileSync2 } from "node:child_process";
1201
1757
  import { writeFileSync as writeFileSync7 } from "node:fs";
1202
- import { join as join11 } from "node:path";
1758
+ import { join as join12 } from "node:path";
1203
1759
  function git2(cwd, args) {
1204
1760
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1205
1761
  }
@@ -1239,16 +1795,33 @@ function renderProvenanceMd(note, sha) {
1239
1795
  `) + `
1240
1796
  `;
1241
1797
  }
1242
- function attachGitNote(projectDir, branch, note) {
1798
+ function attachGitNote(projectDir, branch, note, baseSha) {
1243
1799
  try {
1244
- let sha;
1800
+ const range = baseSha ? `${baseSha}..${branch}` : branch;
1801
+ let shas = [];
1245
1802
  try {
1246
- sha = git2(projectDir, ["rev-parse", "--verify", branch]);
1803
+ const raw = git2(projectDir, ["rev-list", range]);
1804
+ shas = raw.split(`
1805
+ `).filter(Boolean);
1247
1806
  } catch {
1248
- sha = git2(projectDir, ["rev-parse", "HEAD"]);
1807
+ shas = [];
1808
+ }
1809
+ if (shas.length > 200) {
1810
+ console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
1811
+ shas = [];
1249
1812
  }
1250
- git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1251
- return { sha };
1813
+ let targets2 = shas;
1814
+ if (!targets2.length) {
1815
+ try {
1816
+ targets2 = [git2(projectDir, ["rev-parse", "--verify", branch])];
1817
+ } catch {
1818
+ targets2 = [git2(projectDir, ["rev-parse", "HEAD"])];
1819
+ }
1820
+ }
1821
+ for (const sha of targets2) {
1822
+ git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1823
+ }
1824
+ return { sha: targets2[0], count: targets2.length };
1252
1825
  } catch {
1253
1826
  return null;
1254
1827
  }
@@ -1268,13 +1841,243 @@ function blamePath(projectDir, path) {
1268
1841
  ${note}`;
1269
1842
  } catch {
1270
1843
  return `${path} @ ${sha.slice(0, 7)}
1271
- (no mugiwara provenance note on this commit)`;
1844
+ no per-commit note see .mugiwara/missions/<m>/provenance.md`;
1272
1845
  }
1273
1846
  }
1274
- function writeProvenance(projectDir, missionDir, state) {
1847
+ function writeProvenance(projectDir, missionDir, state, baseSha) {
1275
1848
  const note = buildNote(state);
1276
- const attached = attachGitNote(projectDir, state.branch, note);
1277
- writeFileSync7(join11(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1849
+ const resolvedBase = baseSha ?? (typeof state.base_sha === "string" ? state.base_sha : undefined);
1850
+ const cleanBase = resolvedBase && resolvedBase !== "unknown" ? resolvedBase : undefined;
1851
+ const attached = attachGitNote(projectDir, state.branch, note, cleanBase);
1852
+ writeFileSync7(join12(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1853
+ }
1854
+
1855
+ // src/sign.ts
1856
+ import { execFileSync as execFileSync3 } from "node:child_process";
1857
+ import { chmodSync as chmodSync3, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
1858
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
1859
+ import { homedir as homedir3 } from "node:os";
1860
+ import { join as join13 } from "node:path";
1861
+ function signArgs(reportPath, secretKey) {
1862
+ return ["-Sm", reportPath, "-s", secretKey];
1863
+ }
1864
+ function verifyArgs(reportPath, pubKey) {
1865
+ return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
1866
+ }
1867
+ function hasMinisign() {
1868
+ try {
1869
+ execFileSync3("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
1870
+ return true;
1871
+ } catch {
1872
+ return false;
1873
+ }
1874
+ }
1875
+ function defaultKey(flag) {
1876
+ return join13(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
1877
+ }
1878
+ function generatePureKey() {
1879
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
1880
+ const privJwk = privateKey.export({ format: "jwk" });
1881
+ const pubJwk = publicKey.export({ format: "jwk" });
1882
+ return {
1883
+ key: Buffer.from(privJwk.d, "base64url").toString("base64"),
1884
+ pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
1885
+ };
1886
+ }
1887
+ function ensurePureKey(homeDir) {
1888
+ const dir = join13(homeDir, ".mugiwara");
1889
+ const keyPath = join13(dir, "mugiwara.key");
1890
+ const pubPath = join13(dir, "mugiwara.pub");
1891
+ if (!existsSync10(keyPath) || !existsSync10(pubPath)) {
1892
+ mkdirSync6(dir, { recursive: true });
1893
+ const { key, pub } = generatePureKey();
1894
+ if (!existsSync10(keyPath)) {
1895
+ writeFileSync8(keyPath, key + `
1896
+ `, { mode: 384 });
1897
+ }
1898
+ if (!existsSync10(pubPath))
1899
+ writeFileSync8(pubPath, pub + `
1900
+ `);
1901
+ }
1902
+ try {
1903
+ chmodSync3(keyPath, 384);
1904
+ } catch {}
1905
+ return dir;
1906
+ }
1907
+ function pureSign(content, seedBase64, opts) {
1908
+ const seed = Buffer.from(seedBase64.trim(), "base64");
1909
+ if (seed.length !== 32)
1910
+ return { ok: false, message: "invalid seed (want 32B base64)" };
1911
+ const pubBuf = Buffer.from(opts.pub.trim(), "base64");
1912
+ if (pubBuf.length !== 32)
1913
+ return { ok: false, message: "invalid pub (want 32B base64)" };
1914
+ const privateKey = createPrivateKey({
1915
+ key: {
1916
+ kty: "OKP",
1917
+ crv: "Ed25519",
1918
+ d: seed.toString("base64url"),
1919
+ x: pubBuf.toString("base64url")
1920
+ },
1921
+ format: "jwk"
1922
+ });
1923
+ const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
1924
+ const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
1925
+ if (opts.outputPath)
1926
+ writeFileSync8(opts.outputPath, JSON.stringify(out, null, 2) + `
1927
+ `);
1928
+ return { ...out, ok: true };
1929
+ }
1930
+ function pureVerify(content, sig) {
1931
+ try {
1932
+ const pub = Buffer.from(sig.pub, "base64");
1933
+ if (pub.length !== 32)
1934
+ return false;
1935
+ const publicKey = createPublicKey({
1936
+ key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
1937
+ format: "jwk"
1938
+ });
1939
+ return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
1940
+ } catch {
1941
+ return false;
1942
+ }
1943
+ }
1944
+ function resolveBackend(configured, env) {
1945
+ switch (configured) {
1946
+ case "off":
1947
+ return "off";
1948
+ case "minisign":
1949
+ return env.hasMinisign ? "minisign" : "minisign-fail";
1950
+ case "pure":
1951
+ return "pure";
1952
+ case "auto":
1953
+ default:
1954
+ return env.hasMinisign && env.hasKey ? "minisign" : "pure";
1955
+ }
1956
+ }
1957
+ function configuredBackend(projectDir) {
1958
+ return readConfig(projectDir).sign;
1959
+ }
1960
+ function missionMeta(projectDir, mission) {
1961
+ let commit = "unknown";
1962
+ try {
1963
+ commit = execFileSync3("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1964
+ } catch {}
1965
+ return { commit, ts: new Date().toISOString() };
1966
+ }
1967
+ function signReport(projectDir, missionDir) {
1968
+ const report = join13(missionDir, "report.md");
1969
+ if (!existsSync10(report))
1970
+ return { ok: false, message: "no report.md to sign — archive first" };
1971
+ const mission = missionDir.split(join13(".mugiwara", "missions", "")).pop() ?? "unknown";
1972
+ const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync10(defaultKey("secret")) });
1973
+ if (backend === "off")
1974
+ return { ok: false, message: "signing disabled (sign=off)" };
1975
+ if (backend === "minisign-fail")
1976
+ return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
1977
+ if (backend === "minisign") {
1978
+ const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
1979
+ try {
1980
+ execFileSync3("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
1981
+ return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
1982
+ } catch (e) {
1983
+ return { ok: false, message: `signing failed: ${e.message}` };
1984
+ }
1985
+ }
1986
+ const dir = ensurePureKey(homedir3());
1987
+ const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.key"));
1988
+ const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.pub"));
1989
+ if (!seed || !pub)
1990
+ return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
1991
+ const content = readFileSafe(report);
1992
+ if (content === null)
1993
+ return { ok: false, message: `cannot read ${report}` };
1994
+ const { commit, ts } = missionMeta(projectDir, mission);
1995
+ const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
1996
+ if (!sig.ok)
1997
+ return { ok: false, message: `signing failed: ${sig.message}` };
1998
+ return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join13(dir, "mugiwara.key")})` };
1999
+ }
2000
+ function normalizePubkey(pk) {
2001
+ const t = pk.trim();
2002
+ return t.startsWith("ed25519:") ? t.slice("ed25519:".length).trim() : t;
2003
+ }
2004
+ function checkTrust(projectDir, signerPubB64) {
2005
+ try {
2006
+ const policy = loadPolicy(projectDir);
2007
+ const att = policy?.attestation;
2008
+ if (!att)
2009
+ return { ok: true };
2010
+ const trusted = att.trusted_keys ?? [];
2011
+ const revoked = att.revoked ?? [];
2012
+ const signer = signerPubB64.trim();
2013
+ const directlyRevoked = revoked.some((r) => {
2014
+ const rpk = r.pubkey;
2015
+ if (!rpk)
2016
+ return false;
2017
+ return normalizePubkey(rpk) === signer;
2018
+ });
2019
+ if (directlyRevoked)
2020
+ return { ok: false, message: "signature valid but signer revoked (pubkey in revoked list)" };
2021
+ if (trusted.length === 0) {
2022
+ return { ok: true };
2023
+ }
2024
+ const match = trusted.find((e) => normalizePubkey(e.pubkey) === signer);
2025
+ if (!match)
2026
+ return { ok: false, message: "signature valid but signer untrusted (pub not in trusted_keys)" };
2027
+ const revokedById = revoked.some((r) => r.id === match.id);
2028
+ if (revokedById)
2029
+ return { ok: false, message: `signature valid but signer revoked (id: ${match.id})` };
2030
+ return { ok: true };
2031
+ } catch {
2032
+ return { ok: true };
2033
+ }
2034
+ }
2035
+ function verifyReport(projectDir, missionDir) {
2036
+ const report = join13(missionDir, "report.md");
2037
+ const minisig = `${report}.minisig`;
2038
+ const mugisig = `${report}.mugisig`;
2039
+ if (!existsSync10(minisig) && !existsSync10(mugisig)) {
2040
+ return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
2041
+ }
2042
+ if (existsSync10(minisig)) {
2043
+ if (!hasMinisign())
2044
+ return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
2045
+ const pubKey = existsSync10(defaultKey("public")) ? defaultKey("public") : null;
2046
+ try {
2047
+ execFileSync3("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
2048
+ return { ok: true, message: "signature verifies against report.md (minisig)" };
2049
+ } catch {
2050
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
2051
+ }
2052
+ }
2053
+ try {
2054
+ const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
2055
+ const content = readFileSafe(report);
2056
+ if (content === null || parsed.algo !== "ed25519-pure")
2057
+ return { ok: false, message: "invalid .mugisig file" };
2058
+ if (!pureVerify(content, parsed))
2059
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
2060
+ const trust = checkTrust(projectDir, parsed.pub);
2061
+ if (!trust.ok)
2062
+ return { ok: false, message: trust.message };
2063
+ return { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" };
2064
+ } catch {
2065
+ return { ok: false, message: "invalid .mugisig file" };
2066
+ }
2067
+ }
2068
+ function readFileSafe(p) {
2069
+ try {
2070
+ return readFileSync9(p, "utf8");
2071
+ } catch {
2072
+ return null;
2073
+ }
2074
+ }
2075
+ function readFileSyncSafe2(p) {
2076
+ try {
2077
+ return readFileSync9(p, "utf8").trim();
2078
+ } catch {
2079
+ return "";
2080
+ }
1278
2081
  }
1279
2082
 
1280
2083
  // src/routing.ts
@@ -1331,8 +2134,8 @@ function renderRouting(ranked, mission) {
1331
2134
  }
1332
2135
 
1333
2136
  // src/budget.ts
1334
- import { existsSync as existsSync9, readdirSync as readdirSync6, statSync } from "node:fs";
1335
- import { join as join12 } from "node:path";
2137
+ import { existsSync as existsSync11, readdirSync as readdirSync6, statSync } from "node:fs";
2138
+ import { join as join14 } from "node:path";
1336
2139
  function readBudgetConfig(projectDir) {
1337
2140
  const cfg = readConfig(projectDir);
1338
2141
  const raw = cfg.context_budget_chars;
@@ -1350,13 +2153,13 @@ function measureContextChars(missionDir) {
1350
2153
  };
1351
2154
  for (const f of readdirSync6(missionDir)) {
1352
2155
  if (/\.md$/.test(f))
1353
- add(join12(missionDir, f));
2156
+ add(join14(missionDir, f));
1354
2157
  }
1355
2158
  for (const sub of ["flows", "waves"]) {
1356
- const dir = join12(missionDir, sub);
1357
- if (existsSync9(dir)) {
2159
+ const dir = join14(missionDir, sub);
2160
+ if (existsSync11(dir)) {
1358
2161
  for (const f of readdirSync6(dir))
1359
- add(join12(dir, f));
2162
+ add(join14(dir, f));
1360
2163
  }
1361
2164
  }
1362
2165
  return total;
@@ -1367,15 +2170,22 @@ function formatFootprint(chars, budget) {
1367
2170
  return `${base} (no budget configured)`;
1368
2171
  return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
1369
2172
  }
2173
+ var COMPRESS_THRESHOLD_PCT = 0.8;
2174
+ function shouldCompress(budget, chars) {
2175
+ return budget > 0 && chars > Math.floor(budget * COMPRESS_THRESHOLD_PCT);
2176
+ }
2177
+ function compressThreshold(budget) {
2178
+ return Math.floor(budget * COMPRESS_THRESHOLD_PCT);
2179
+ }
1370
2180
 
1371
2181
  // src/cost.ts
1372
- import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
1373
- import { join as join13 } from "node:path";
2182
+ import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10 } from "node:fs";
2183
+ import { join as join15 } from "node:path";
1374
2184
  var LANE_BUDGET = {
1375
2185
  lean: 12000,
1376
2186
  standard: 25000,
1377
2187
  full: 50000,
1378
- spike: 3000
2188
+ spike: 9000
1379
2189
  };
1380
2190
  function budgetForLane(lane) {
1381
2191
  return LANE_BUDGET[lane] ?? 0;
@@ -1426,15 +2236,16 @@ function assertMissionDir(dir) {
1426
2236
  }
1427
2237
  function appendCostEvent(missionDir, event) {
1428
2238
  assertMissionDir(missionDir);
1429
- mkdirSync6(missionDir, { recursive: true });
2239
+ mkdirSync7(missionDir, { recursive: true });
1430
2240
  const line = Object.assign({ ts: new Date().toISOString() }, event);
1431
- appendFileSync(join13(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
2241
+ appendFileSync(join15(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
1432
2242
  `, "utf8");
1433
2243
  }
2244
+ var COMPRESSED_KIND = "compressed";
1434
2245
 
1435
2246
  // src/evidence.ts
1436
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9 } from "node:fs";
1437
- import { join as join14 } from "node:path";
2247
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
2248
+ import { join as join16 } from "node:path";
1438
2249
  var REGISTRY_FILE = "context-registry.jsonl";
1439
2250
  function isAllowedMissionDir2(dir) {
1440
2251
  if (!dir || dir.includes(".."))
@@ -1453,10 +2264,10 @@ function assertMissionDir2(dir) {
1453
2264
  }
1454
2265
  function loadRegistry(missionDir) {
1455
2266
  assertMissionDir2(missionDir);
1456
- const file = join14(missionDir, REGISTRY_FILE);
2267
+ const file = join16(missionDir, REGISTRY_FILE);
1457
2268
  try {
1458
2269
  const out = [];
1459
- for (const line of readFileSync9(file, "utf8").split(/\r?\n/)) {
2270
+ for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
1460
2271
  if (!line.trim())
1461
2272
  continue;
1462
2273
  let e;
@@ -1497,8 +2308,8 @@ function computeContextMetrics(stats) {
1497
2308
  }
1498
2309
 
1499
2310
  // src/reporting.ts
1500
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
1501
- import { join as join15 } from "node:path";
2311
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
2312
+ import { join as join17 } from "node:path";
1502
2313
  function isAllowedMissionDir3(dir) {
1503
2314
  if (!dir || dir.includes(".."))
1504
2315
  return false;
@@ -1516,10 +2327,10 @@ function assertMissionDir3(dir) {
1516
2327
  }
1517
2328
  function parseDecisionTrail(missionDir) {
1518
2329
  assertMissionDir3(missionDir);
1519
- const file = join15(missionDir, "decisions.md");
1520
- if (!existsSync10(file))
2330
+ const file = join17(missionDir, "decisions.md");
2331
+ if (!existsSync12(file))
1521
2332
  return [];
1522
- const raw = readFileSync10(file, "utf8");
2333
+ const raw = readFileSync12(file, "utf8");
1523
2334
  const lines = raw.split(/\r?\n/);
1524
2335
  const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
1525
2336
  const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
@@ -1535,11 +2346,11 @@ function parseDecisionTrail(missionDir) {
1535
2346
  }
1536
2347
  function loadCostEvents(missionDir) {
1537
2348
  assertMissionDir3(missionDir);
1538
- const file = join15(missionDir, "cost-events.jsonl");
1539
- if (!existsSync10(file))
2349
+ const file = join17(missionDir, "cost-events.jsonl");
2350
+ if (!existsSync12(file))
1540
2351
  return [];
1541
2352
  const out = [];
1542
- for (const line of readFileSync10(file, "utf8").split(/\r?\n/)) {
2353
+ for (const line of readFileSync12(file, "utf8").split(/\r?\n/)) {
1543
2354
  if (!line.trim())
1544
2355
  continue;
1545
2356
  try {
@@ -1646,18 +2457,65 @@ function primaryState(dir, files) {
1646
2457
  if (!name)
1647
2458
  return null;
1648
2459
  try {
1649
- return JSON.parse(readFileSync11(join16(dir, name), "utf8"));
2460
+ return JSON.parse(readFileSync13(join18(dir, name), "utf8"));
1650
2461
  } catch {
1651
2462
  return null;
1652
2463
  }
1653
2464
  }
2465
+ function tasksFromState(state) {
2466
+ if (!state)
2467
+ return { done: 0, total: 0 };
2468
+ const t = state.tasks;
2469
+ if (t && typeof t.done === "number" && typeof t.total === "number")
2470
+ return { done: t.done, total: t.total };
2471
+ const done = Number(state.tasks_done);
2472
+ const total = Number(state.tasks_total);
2473
+ return { done: Number.isFinite(done) ? done : 0, total: Number.isFinite(total) ? total : 0 };
2474
+ }
2475
+ function countPlanTasks(missionDir) {
2476
+ let total = 0;
2477
+ let done = 0;
2478
+ const planFile = join18(missionDir, "plan.md");
2479
+ if (existsSync13(planFile)) {
2480
+ try {
2481
+ const text = readFileSync13(planFile, "utf8");
2482
+ for (const line of text.split(/\r?\n/)) {
2483
+ if (/^\s*-\s*\[[ xX]\]/.test(line)) {
2484
+ total++;
2485
+ if (/^\s*-\s*\[[xX]\]/.test(line))
2486
+ done++;
2487
+ }
2488
+ }
2489
+ } catch {}
2490
+ }
2491
+ if (total === 0) {
2492
+ const subPlanDir = join18(missionDir, "sub-plan");
2493
+ if (existsSync13(subPlanDir)) {
2494
+ try {
2495
+ for (const f of readdirSync7(subPlanDir)) {
2496
+ if (!f.endsWith(".md"))
2497
+ continue;
2498
+ const text = readFileSync13(join18(subPlanDir, f), "utf8");
2499
+ for (const line of text.split(/\r?\n/)) {
2500
+ if (/^\s*-\s*\[[ xX]\]/.test(line)) {
2501
+ total++;
2502
+ if (/^\s*-\s*\[[xX]\]/.test(line))
2503
+ done++;
2504
+ }
2505
+ }
2506
+ }
2507
+ } catch {}
2508
+ }
2509
+ }
2510
+ return { done, total };
2511
+ }
1654
2512
  function changedFiles(projectDir, state) {
1655
2513
  const base = typeof state?.base_sha === "string" ? state.base_sha : "";
1656
2514
  const branch = typeof state?.branch === "string" ? state.branch : "";
1657
2515
  if (!base || base === "unknown" || !branch)
1658
2516
  return [];
1659
2517
  try {
1660
- return execFileSync3("git", ["diff", "--name-only", base, branch], {
2518
+ return execFileSync4("git", ["diff", "--name-only", base, branch], {
1661
2519
  cwd: projectDir,
1662
2520
  encoding: "utf8",
1663
2521
  stdio: ["ignore", "pipe", "ignore"]
@@ -1667,16 +2525,16 @@ function changedFiles(projectDir, state) {
1667
2525
  }
1668
2526
  }
1669
2527
  function activeActor(projectDir) {
1670
- const missionsDir = join16(projectDir, ".mugiwara", "missions");
1671
- if (!existsSync11(missionsDir))
2528
+ const missionsDir = join18(projectDir, ".mugiwara", "missions");
2529
+ if (!existsSync13(missionsDir))
1672
2530
  return null;
1673
2531
  const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1674
2532
  let latest = null;
1675
2533
  for (const mission of missions) {
1676
- const d = join16(missionsDir, mission);
2534
+ const d = join18(missionsDir, mission);
1677
2535
  for (const f of readdirSync7(d).filter(isStateFile)) {
1678
2536
  try {
1679
- const s = JSON.parse(readFileSync11(join16(d, f), "utf8"));
2537
+ const s = JSON.parse(readFileSync13(join18(d, f), "utf8"));
1680
2538
  const t = Date.parse(s.updated_at || "") || 0;
1681
2539
  if (!latest || t > latest.updated)
1682
2540
  latest = { actor: s.actor || null, updated: t };
@@ -1686,8 +2544,8 @@ function activeActor(projectDir) {
1686
2544
  return latest ? latest.actor : null;
1687
2545
  }
1688
2546
  function resetMission(projectDir, keepLogs, force) {
1689
- const root = join16(projectDir, ".mugiwara");
1690
- if (!existsSync11(root))
2547
+ const root = join18(projectDir, ".mugiwara");
2548
+ if (!existsSync13(root))
1691
2549
  return { removed: [], kept: [] };
1692
2550
  if (!force) {
1693
2551
  const actor = activeActor(projectDir);
@@ -1698,66 +2556,93 @@ function resetMission(projectDir, keepLogs, force) {
1698
2556
  const removed = [];
1699
2557
  const kept = [];
1700
2558
  for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
1701
- const p = join16(root, dir);
1702
- if (existsSync11(p)) {
2559
+ const p = join18(root, dir);
2560
+ if (existsSync13(p)) {
1703
2561
  rmSync2(p, { recursive: true, force: true });
1704
2562
  removed.push(dir);
1705
2563
  }
1706
2564
  }
1707
2565
  for (const f of readdirSync7(root)) {
1708
2566
  if (/^state(-.+)?\.json$/.test(f)) {
1709
- const p = join16(root, f);
1710
- if (existsSync11(p)) {
2567
+ const p = join18(root, f);
2568
+ if (existsSync13(p)) {
1711
2569
  rmSync2(p);
1712
2570
  removed.push(f);
1713
2571
  }
1714
2572
  }
1715
2573
  }
1716
2574
  if (!keepLogs) {
1717
- for (const p of [join16(root, "lessons.md"), join16(root, "logs")]) {
1718
- if (existsSync11(p)) {
2575
+ for (const p of [join18(root, "lessons.md"), join18(root, "logs")]) {
2576
+ if (existsSync13(p)) {
1719
2577
  rmSync2(p, { recursive: true, force: true });
1720
- removed.push(p.startsWith(join16(root, "logs")) ? "logs" : "lessons.md");
2578
+ removed.push(p.startsWith(join18(root, "logs")) ? "logs" : "lessons.md");
1721
2579
  }
1722
2580
  }
1723
2581
  } else {
1724
- if (existsSync11(join16(root, "lessons.md")))
2582
+ if (existsSync13(join18(root, "lessons.md")))
1725
2583
  kept.push("lessons.md");
1726
- else if (existsSync11(join16(root, join16("logs", "lessons.md"))))
1727
- kept.push(join16("logs", "lessons.md"));
2584
+ else if (existsSync13(join18(root, join18("logs", "lessons.md"))))
2585
+ kept.push(join18("logs", "lessons.md"));
2586
+ }
2587
+ if (removed.includes("missions") && existsSync13(join18(root, "index.md"))) {
2588
+ rmSync2(join18(root, "index.md"));
2589
+ removed.push("index.md");
1728
2590
  }
1729
2591
  for (const f of ["config", "manifest.json", "backup"]) {
1730
- if (existsSync11(join16(root, f)))
2592
+ if (existsSync13(join18(root, f)))
1731
2593
  kept.push(f);
1732
2594
  }
1733
2595
  return { removed, kept };
1734
2596
  }
1735
2597
  function archiveMission(projectDir, mission, opts = {}) {
1736
2598
  const { dryRun = false } = opts;
1737
- const root = join16(projectDir, ".mugiwara");
2599
+ const root = join18(projectDir, ".mugiwara");
1738
2600
  if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
1739
2601
  throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
1740
2602
  const removed = [];
1741
2603
  const kept = [];
1742
- const dir = join16(root, "missions", mission);
1743
- if (!existsSync11(dir))
2604
+ const dir = join18(root, "missions", mission);
2605
+ if (!existsSync13(dir))
1744
2606
  return { report: null, removed, kept };
1745
2607
  if (!dryRun) {
1746
2608
  const issues = checkTrail(dir, projectDir);
1747
- if (issues.length) {
2609
+ const blocking = issues.filter((i) => i.kind !== "secret-warn" && i.severity !== "warn");
2610
+ if (blocking.length) {
1748
2611
  throw new Error(`closure integrity gate failed — fix these before archiving:
1749
- ${formatIssues(issues)}`);
2612
+ ${formatIssues(blocking)}`);
2613
+ }
2614
+ if (issues.length && blocking.length === 0) {
2615
+ try {
2616
+ const warnText = formatIssues(issues);
2617
+ if (warnText)
2618
+ console.warn(`closure integrity warnings (non-blocking):
2619
+ ${warnText}`);
2620
+ } catch {}
1750
2621
  }
1751
2622
  const artifacts = checkMissionArtifacts(dir);
1752
2623
  if (!artifacts.ok) {
1753
2624
  throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
1754
2625
  }
2626
+ try {
2627
+ const policy = loadPolicy(projectDir);
2628
+ if (policy?.attestation?.required) {
2629
+ const v = verifyReport(projectDir, dir);
2630
+ if (!v.ok) {
2631
+ throw new Error(`closure integrity gate failed — attestation required but report not signed/trusted: ${v.message}`);
2632
+ }
2633
+ }
2634
+ } catch (e) {
2635
+ if (e.message.startsWith("closure integrity gate failed — attestation required"))
2636
+ throw e;
2637
+ if (e.message.startsWith("unknown policy key"))
2638
+ throw e;
2639
+ }
1755
2640
  }
1756
2641
  const files = readdirSync7(dir);
1757
2642
  const state = primaryState(dir, files);
1758
2643
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
1759
2644
  try {
1760
- const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
2645
+ const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
1761
2646
  return typeof s.model === "string" ? s.model : "";
1762
2647
  } catch {
1763
2648
  return "";
@@ -1797,7 +2682,7 @@ ${formatIssues(issues)}`);
1797
2682
  let hasReported = false;
1798
2683
  for (const f of files.filter(isStateFile)) {
1799
2684
  try {
1800
- const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
2685
+ const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
1801
2686
  if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
1802
2687
  reportedTotal += s.tokens_est;
1803
2688
  hasReported = true;
@@ -1852,6 +2737,41 @@ ${formatIssues(issues)}`);
1852
2737
  try {
1853
2738
  costSection += renderAdaptationSection(dir);
1854
2739
  } catch {}
2740
+ if (shouldCompress(budget, chars)) {
2741
+ try {
2742
+ const flowsDir2 = join18(dir, "flows");
2743
+ const wavesDir = join18(dir, "waves");
2744
+ const targetDir = existsSync13(flowsDir2) ? flowsDir2 : existsSync13(wavesDir) ? wavesDir : null;
2745
+ if (targetDir && existsSync13(targetDir)) {
2746
+ const flowFiles = readdirSync7(targetDir).filter((f) => f.endsWith(".md"));
2747
+ if (flowFiles.length) {
2748
+ const pct = Math.round(chars / budget * 100);
2749
+ const stub = `# Compressed trail
2750
+
2751
+ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressThreshold(budget)}) — flows archived as stub to preserve budget. Original flows: ${flowFiles.join(", ")}
2752
+ `;
2753
+ for (const f of flowFiles) {
2754
+ try {
2755
+ rmSync2(join18(targetDir, f), { force: true });
2756
+ } catch {}
2757
+ }
2758
+ writeFileSync9(join18(targetDir, "00-compressed.md"), stub);
2759
+ }
2760
+ }
2761
+ appendCostEvent(dir, {
2762
+ kind: COMPRESSED_KIND,
2763
+ mission,
2764
+ tokens_est: est,
2765
+ budget: laneBudget,
2766
+ status: "compressed",
2767
+ context_chars: chars,
2768
+ context_status: ctxStatus,
2769
+ context_metrics: metrics
2770
+ });
2771
+ costSection += `
2772
+ | **Compressed** | yes — ${chars} chars >80% of ${budget} — flows stubbed as 00-compressed.md |`;
2773
+ } catch {}
2774
+ }
1855
2775
  appendCostEvent(dir, {
1856
2776
  kind: "closure",
1857
2777
  mission,
@@ -1873,39 +2793,39 @@ ${formatIssues(issues)}`);
1873
2793
  fold.push(f);
1874
2794
  }
1875
2795
  const PR_VERDICT = "pr-verdict.md";
1876
- const PR_VERDICT_SRC = join16("flows", "07-pr-verdict.md");
1877
- const flowsDir = join16(dir, "flows");
1878
- const legacyWavesDir = join16(dir, "waves");
1879
- const artDir = existsSync11(flowsDir) ? flowsDir : existsSync11(legacyWavesDir) ? legacyWavesDir : flowsDir;
2796
+ const PR_VERDICT_SRC = join18("flows", "07-pr-verdict.md");
2797
+ const flowsDir = join18(dir, "flows");
2798
+ const legacyWavesDir = join18(dir, "waves");
2799
+ const artDir = existsSync13(flowsDir) ? flowsDir : existsSync13(legacyWavesDir) ? legacyWavesDir : flowsDir;
1880
2800
  const artRel = artDir === legacyWavesDir ? "waves" : "flows";
1881
- if (existsSync11(artDir)) {
2801
+ if (existsSync13(artDir)) {
1882
2802
  for (const f of readdirSync7(artDir).sort()) {
1883
2803
  if (artRel === "flows" && f === "07-pr-verdict.md")
1884
2804
  continue;
1885
- fold.push(join16(artRel, f));
2805
+ fold.push(join18(artRel, f));
1886
2806
  }
1887
2807
  }
1888
- if (existsSync11(join16(dir, "cost-events.jsonl")))
2808
+ if (existsSync13(join18(dir, "cost-events.jsonl")))
1889
2809
  fold.push("cost-events.jsonl");
1890
- if (existsSync11(join16(dir, "context-registry.jsonl")))
2810
+ if (existsSync13(join18(dir, "context-registry.jsonl")))
1891
2811
  fold.push("context-registry.jsonl");
1892
2812
  let report = "";
1893
- const reportPath = join16(dir, "report.md");
2813
+ const reportPath = join18(dir, "report.md");
1894
2814
  if (files.includes("report.md"))
1895
- report = readFileSync11(reportPath, "utf8");
1896
- else if (existsSync11(join16(artDir, "06-closure.md")))
1897
- report = readFileSync11(join16(artDir, "06-closure.md"), "utf8");
2815
+ report = readFileSync13(reportPath, "utf8");
2816
+ else if (existsSync13(join18(artDir, "06-closure.md")))
2817
+ report = readFileSync13(join18(artDir, "06-closure.md"), "utf8");
1898
2818
  if (!dryRun) {
1899
- mkdirSync8(dir, { recursive: true });
1900
- const prVerdictSrc = join16(dir, PR_VERDICT_SRC);
1901
- if (existsSync11(prVerdictSrc)) {
1902
- const prVerdictPath = join16(dir, PR_VERDICT);
1903
- writeFileSync8(prVerdictPath, readFileSync11(prVerdictSrc, "utf8"));
1904
- kept.push(join16("missions", mission, PR_VERDICT));
2819
+ mkdirSync9(dir, { recursive: true });
2820
+ const prVerdictSrc = join18(dir, PR_VERDICT_SRC);
2821
+ if (existsSync13(prVerdictSrc)) {
2822
+ const prVerdictPath = join18(dir, PR_VERDICT);
2823
+ writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
2824
+ kept.push(join18("missions", mission, PR_VERDICT));
1905
2825
  }
1906
2826
  if (fold.length) {
1907
2827
  const sections = fold.map((f) => {
1908
- const body = readFileSync11(join16(dir, f), "utf8").trim();
2828
+ const body = readFileSync13(join18(dir, f), "utf8").trim();
1909
2829
  const name = f.includes("/") ? f.split("/").pop() ?? f : f;
1910
2830
  return `
1911
2831
 
@@ -1919,26 +2839,26 @@ ${body}`;
1919
2839
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1920
2840
  sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
1921
2841
  }), mission) : "";
1922
- writeFileSync8(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
2842
+ writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1923
2843
  ${costSection}
1924
2844
  ` : "") + `
1925
2845
  `);
1926
2846
  renameSync(tmp, reportPath);
1927
2847
  }
1928
2848
  for (const f of fold) {
1929
- rmSync2(join16(dir, f), { force: true, recursive: true });
1930
- removed.push(join16("missions", mission, f));
2849
+ rmSync2(join18(dir, f), { force: true, recursive: true });
2850
+ removed.push(join18("missions", mission, f));
1931
2851
  }
1932
- if (existsSync11(prVerdictSrc)) {
1933
- rmSync2(join16(dir, PR_VERDICT_SRC), { force: true });
1934
- removed.push(join16("missions", mission, PR_VERDICT_SRC));
2852
+ if (existsSync13(prVerdictSrc)) {
2853
+ rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
2854
+ removed.push(join18("missions", mission, PR_VERDICT_SRC));
1935
2855
  }
1936
2856
  for (const f of files.filter((f2) => f2.endsWith(".json")))
1937
- rmSync2(join16(dir, f), { force: true });
1938
- if (existsSync11(artDir) && readdirSync7(artDir).length === 0)
2857
+ rmSync2(join18(dir, f), { force: true });
2858
+ if (existsSync13(artDir) && readdirSync7(artDir).length === 0)
1939
2859
  rmSync2(artDir, { recursive: true, force: true });
1940
- if (!existsSync11(reportPath)) {
1941
- writeFileSync8(reportPath, `# Mission: ${mission}
2860
+ if (!existsSync13(reportPath)) {
2861
+ writeFileSync9(reportPath, `# Mission: ${mission}
1942
2862
 
1943
2863
  Archived before closure — no wave artifacts were present.
1944
2864
  `);
@@ -1950,38 +2870,44 @@ Archived before closure — no wave artifacts were present.
1950
2870
  baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
1951
2871
  });
1952
2872
  if (rb)
1953
- kept.push(join16("missions", mission, rb.file));
2873
+ kept.push(join18("missions", mission, rb.file));
1954
2874
  try {
2875
+ const st = tasksFromState(state);
2876
+ const pt = countPlanTasks(dir);
2877
+ const tasks_done = pt.total > 0 ? pt.done : st.done;
2878
+ const tasks_total = pt.total > 0 ? pt.total : st.total;
2879
+ const baseShaForNote = typeof state.base_sha === "string" ? state.base_sha : undefined;
1955
2880
  writeProvenance(projectDir, dir, {
1956
2881
  mission,
1957
2882
  actor: typeof state.actor === "string" ? state.actor : "",
1958
2883
  lane: typeof state.lane === "string" ? state.lane : "",
1959
2884
  mode: typeof state.mode === "string" ? state.mode : "",
1960
2885
  branch: state.branch,
1961
- tasks_done: Number(state.tasks_done) || 0,
1962
- tasks_total: Number(state.tasks_total) || 0,
2886
+ tasks_done,
2887
+ tasks_total,
1963
2888
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1964
- models: stageModels
1965
- });
1966
- kept.push(join16("missions", mission, "provenance.md"));
2889
+ models: stageModels,
2890
+ base_sha: baseShaForNote
2891
+ }, baseShaForNote);
2892
+ kept.push(join18("missions", mission, "provenance.md"));
1967
2893
  } catch {}
1968
2894
  }
1969
2895
  } else {
1970
2896
  for (const f of fold)
1971
- removed.push(join16("missions", mission, f));
2897
+ removed.push(join18("missions", mission, f));
1972
2898
  }
1973
- removed.push(join16("missions", mission, "<session state>"));
2899
+ removed.push(join18("missions", mission, "<session state>"));
1974
2900
  if (files.includes("plan.md"))
1975
- kept.push(join16("missions", mission, "plan.md"));
2901
+ kept.push(join18("missions", mission, "plan.md"));
1976
2902
  if (files.includes("handoff.md"))
1977
- kept.push(join16("missions", mission, "handoff.md"));
1978
- kept.push(join16("missions", mission, "report.md"));
2903
+ kept.push(join18("missions", mission, "handoff.md"));
2904
+ kept.push(join18("missions", mission, "report.md"));
1979
2905
  let index;
1980
- const indexFile = join16(root, "index.md");
2906
+ const indexFile = join18(root, "index.md");
1981
2907
  const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
1982
2908
  `;
1983
2909
  if (!dryRun) {
1984
- const existing = existsSync11(indexFile) ? readFileSync11(indexFile, "utf8") : "";
2910
+ const existing = existsSync13(indexFile) ? readFileSync13(indexFile, "utf8") : "";
1985
2911
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
1986
2912
  const header = existing ? "" : `# Mission index
1987
2913
 
@@ -1995,28 +2921,28 @@ Archived before closure — no wave artifacts were present.
1995
2921
  }
1996
2922
  index = "index.md";
1997
2923
  }
1998
- return { report: join16("missions", mission, "report.md"), removed, kept, index };
2924
+ return { report: join18("missions", mission, "report.md"), removed, kept, index };
1999
2925
  }
2000
2926
 
2001
2927
  // src/run.ts
2002
- import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
2928
+ import { existsSync as existsSync14, readdirSync as readdirSync8 } from "node:fs";
2003
2929
  import { spawnSync } from "node:child_process";
2004
- import { dirname as dirname5, join as join17, resolve } from "node:path";
2930
+ import { dirname as dirname5, join as join19, resolve } from "node:path";
2005
2931
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2006
2932
  var here3 = dirname5(fileURLToPath4(import.meta.url));
2007
- var SCRIPTS_DIR = join17(here3, "..", "scripts");
2933
+ var SCRIPTS_DIR = join19(here3, "..", "scripts");
2008
2934
  var RUNNABLE = ["savepoint.sh", "lane.sh"];
2009
2935
  function findBash() {
2010
2936
  const explicit = process.env.MUGIWARA_BASH?.trim();
2011
2937
  if (explicit)
2012
- return existsSync12(explicit) ? explicit : null;
2938
+ return existsSync14(explicit) ? explicit : null;
2013
2939
  const candidates = process.platform === "win32" ? [
2014
2940
  "C:\\Program Files\\Git\\bin\\bash.exe",
2015
2941
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
2016
- join17(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2942
+ join19(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2017
2943
  ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
2018
2944
  for (const p of candidates) {
2019
- if (p && existsSync12(p))
2945
+ if (p && existsSync14(p))
2020
2946
  return p;
2021
2947
  }
2022
2948
  const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
@@ -2033,9 +2959,9 @@ function runScript(name, args, projectDir) {
2033
2959
  if (!/^[a-z0-9-]+\.sh$/.test(name)) {
2034
2960
  throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
2035
2961
  }
2036
- const script = join17(SCRIPTS_DIR, name);
2037
- if (!existsSync12(script)) {
2038
- const have = existsSync12(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
2962
+ const script = join19(SCRIPTS_DIR, name);
2963
+ if (!existsSync14(script)) {
2964
+ const have = existsSync14(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
2039
2965
  throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
2040
2966
  }
2041
2967
  const bash = findBash();
@@ -2052,9 +2978,30 @@ function runScript(name, args, projectDir) {
2052
2978
  }
2053
2979
 
2054
2980
  // src/continue.ts
2055
- import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync9 } from "node:fs";
2056
- import { execFileSync as execFileSync4 } from "node:child_process";
2057
- import { join as join18 } from "node:path";
2981
+ import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "node:fs";
2982
+ import { execFileSync as execFileSync5 } from "node:child_process";
2983
+ import { join as join20 } from "node:path";
2984
+ var CURRENT_SCHEMA_VERSION = 2;
2985
+ function hasLegacyLayout(projectDir) {
2986
+ for (const legacy of [join20(projectDir, ".mugiwara", "state"), join20(projectDir, ".mugiwara", "continue")]) {
2987
+ if (!existsSync15(legacy))
2988
+ continue;
2989
+ try {
2990
+ const walk = (dir) => {
2991
+ for (const e of readdirSync9(dir, { withFileTypes: true })) {
2992
+ if (e.isFile() && e.name.endsWith(".json"))
2993
+ return true;
2994
+ if (e.isDirectory() && walk(join20(dir, e.name)))
2995
+ return true;
2996
+ }
2997
+ return false;
2998
+ };
2999
+ if (walk(legacy))
3000
+ return true;
3001
+ } catch {}
3002
+ }
3003
+ return false;
3004
+ }
2058
3005
  var SAFE = /^[A-Za-z0-9._-]+$/;
2059
3006
  var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
2060
3007
  var num = (v) => {
@@ -2071,7 +3018,7 @@ function gitActor(cwd) {
2071
3018
  return author;
2072
3019
  const git3 = (args) => {
2073
3020
  try {
2074
- return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3021
+ return execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2075
3022
  } catch {
2076
3023
  return "";
2077
3024
  }
@@ -2082,14 +3029,19 @@ function gitActor(cwd) {
2082
3029
  return `${name} <${email}>`;
2083
3030
  return name || process.env.USER || process.env.USERNAME || "";
2084
3031
  }
3032
+ var unreadable = [];
3033
+ function unreadableStateFiles() {
3034
+ return [...unreadable];
3035
+ }
2085
3036
  function scan(projectDir, kind, map) {
2086
- const base = join18(projectDir, ".mugiwara", "missions");
2087
- if (!existsSync13(base))
3037
+ unreadable.length = 0;
3038
+ const base = join20(projectDir, ".mugiwara", "missions");
3039
+ if (!existsSync15(base))
2088
3040
  return [];
2089
3041
  const out = [];
2090
3042
  const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
2091
3043
  for (const mission of missions) {
2092
- const dir = join18(base, mission);
3044
+ const dir = join20(base, mission);
2093
3045
  for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
2094
3046
  const stem = f.slice(0, -".json".length);
2095
3047
  let member;
@@ -2108,11 +3060,13 @@ function scan(projectDir, kind, map) {
2108
3060
  if (member !== null && !isSafeKey(member))
2109
3061
  continue;
2110
3062
  try {
2111
- const raw = JSON.parse(readFileSync12(join18(dir, f), "utf8"));
3063
+ const raw = JSON.parse(readFileSync14(join20(dir, f), "utf8"));
2112
3064
  if (text(raw.mission) !== mission)
2113
3065
  continue;
2114
3066
  out.push(map(raw, member));
2115
- } catch {}
3067
+ } catch {
3068
+ unreadable.push(join20(mission, f));
3069
+ }
2116
3070
  }
2117
3071
  }
2118
3072
  return out;
@@ -2136,6 +3090,7 @@ function readContinue(projectDir) {
2136
3090
  function readState(projectDir) {
2137
3091
  return scan(projectDir, "state", (r, member) => {
2138
3092
  const tasks = r.tasks ?? {};
3093
+ const sv = r.schema_version;
2139
3094
  return {
2140
3095
  mission: text(r.mission),
2141
3096
  member,
@@ -2165,7 +3120,8 @@ function readState(projectDir) {
2165
3120
  budget: num(r.budget),
2166
3121
  budget_status: text(r.budget_status) || "ok",
2167
3122
  files_touched: num(r.files_touched),
2168
- evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : []
3123
+ evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : [],
3124
+ schema_version: typeof sv === "number" || typeof sv === "string" ? sv : null
2169
3125
  };
2170
3126
  });
2171
3127
  }
@@ -2223,194 +3179,6 @@ function formatResume(e) {
2223
3179
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
2224
3180
  }
2225
3181
 
2226
- // src/sign.ts
2227
- import { execFileSync as execFileSync5 } from "node:child_process";
2228
- import { chmodSync as chmodSync2, existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
2229
- import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
2230
- import { homedir as homedir3 } from "node:os";
2231
- import { join as join19 } from "node:path";
2232
- function signArgs(reportPath, secretKey) {
2233
- return ["-Sm", reportPath, "-s", secretKey];
2234
- }
2235
- function verifyArgs(reportPath, pubKey) {
2236
- return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
2237
- }
2238
- function hasMinisign() {
2239
- try {
2240
- execFileSync5("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
2241
- return true;
2242
- } catch {
2243
- return false;
2244
- }
2245
- }
2246
- function defaultKey(flag) {
2247
- return join19(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
2248
- }
2249
- function generatePureKey() {
2250
- const { privateKey, publicKey } = generateKeyPairSync("ed25519");
2251
- const privJwk = privateKey.export({ format: "jwk" });
2252
- const pubJwk = publicKey.export({ format: "jwk" });
2253
- return {
2254
- key: Buffer.from(privJwk.d, "base64url").toString("base64"),
2255
- pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
2256
- };
2257
- }
2258
- function ensurePureKey(homeDir) {
2259
- const dir = join19(homeDir, ".mugiwara");
2260
- const keyPath = join19(dir, "mugiwara.key");
2261
- const pubPath = join19(dir, "mugiwara.pub");
2262
- if (!existsSync14(keyPath) || !existsSync14(pubPath)) {
2263
- mkdirSync9(dir, { recursive: true });
2264
- const { key, pub } = generatePureKey();
2265
- if (!existsSync14(keyPath)) {
2266
- writeFileSync9(keyPath, key + `
2267
- `, { mode: 384 });
2268
- }
2269
- if (!existsSync14(pubPath))
2270
- writeFileSync9(pubPath, pub + `
2271
- `);
2272
- }
2273
- try {
2274
- chmodSync2(keyPath, 384);
2275
- } catch {}
2276
- return dir;
2277
- }
2278
- function pureSign(content, seedBase64, opts) {
2279
- const seed = Buffer.from(seedBase64.trim(), "base64");
2280
- if (seed.length !== 32)
2281
- return { ok: false, message: "invalid seed (want 32B base64)" };
2282
- const pubBuf = Buffer.from(opts.pub.trim(), "base64");
2283
- if (pubBuf.length !== 32)
2284
- return { ok: false, message: "invalid pub (want 32B base64)" };
2285
- const privateKey = createPrivateKey({
2286
- key: {
2287
- kty: "OKP",
2288
- crv: "Ed25519",
2289
- d: seed.toString("base64url"),
2290
- x: pubBuf.toString("base64url")
2291
- },
2292
- format: "jwk"
2293
- });
2294
- const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
2295
- const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
2296
- if (opts.outputPath)
2297
- writeFileSync9(opts.outputPath, JSON.stringify(out, null, 2) + `
2298
- `);
2299
- return { ...out, ok: true };
2300
- }
2301
- function pureVerify(content, sig) {
2302
- try {
2303
- const pub = Buffer.from(sig.pub, "base64");
2304
- if (pub.length !== 32)
2305
- return false;
2306
- const publicKey = createPublicKey({
2307
- key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
2308
- format: "jwk"
2309
- });
2310
- return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
2311
- } catch {
2312
- return false;
2313
- }
2314
- }
2315
- function resolveBackend(configured, env) {
2316
- switch (configured) {
2317
- case "off":
2318
- return "off";
2319
- case "minisign":
2320
- return env.hasMinisign ? "minisign" : "minisign-fail";
2321
- case "pure":
2322
- return "pure";
2323
- case "auto":
2324
- default:
2325
- return env.hasMinisign && env.hasKey ? "minisign" : "pure";
2326
- }
2327
- }
2328
- function configuredBackend(projectDir) {
2329
- return readConfig(projectDir).sign;
2330
- }
2331
- function missionMeta(projectDir, mission) {
2332
- let commit = "unknown";
2333
- try {
2334
- commit = execFileSync5("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2335
- } catch {}
2336
- return { commit, ts: new Date().toISOString() };
2337
- }
2338
- function signReport(projectDir, missionDir) {
2339
- const report = join19(missionDir, "report.md");
2340
- if (!existsSync14(report))
2341
- return { ok: false, message: "no report.md to sign — archive first" };
2342
- const mission = missionDir.split(join19(".mugiwara", "missions", "")).pop() ?? "unknown";
2343
- const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync14(defaultKey("secret")) });
2344
- if (backend === "off")
2345
- return { ok: false, message: "signing disabled (sign=off)" };
2346
- if (backend === "minisign-fail")
2347
- return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
2348
- if (backend === "minisign") {
2349
- const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
2350
- try {
2351
- execFileSync5("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
2352
- return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
2353
- } catch (e) {
2354
- return { ok: false, message: `signing failed: ${e.message}` };
2355
- }
2356
- }
2357
- const dir = ensurePureKey(homedir3());
2358
- const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.key"));
2359
- const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join19(dir, "mugiwara.pub"));
2360
- if (!seed || !pub)
2361
- return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
2362
- const content = readFileSafe(report);
2363
- if (content === null)
2364
- return { ok: false, message: `cannot read ${report}` };
2365
- const { commit, ts } = missionMeta(projectDir, mission);
2366
- const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
2367
- if (!sig.ok)
2368
- return { ok: false, message: `signing failed: ${sig.message}` };
2369
- return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join19(dir, "mugiwara.key")})` };
2370
- }
2371
- function verifyReport(projectDir, missionDir) {
2372
- const report = join19(missionDir, "report.md");
2373
- const minisig = `${report}.minisig`;
2374
- const mugisig = `${report}.mugisig`;
2375
- if (!existsSync14(minisig) && !existsSync14(mugisig)) {
2376
- return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
2377
- }
2378
- if (existsSync14(minisig)) {
2379
- if (!hasMinisign())
2380
- return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
2381
- const pubKey = existsSync14(defaultKey("public")) ? defaultKey("public") : null;
2382
- try {
2383
- execFileSync5("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
2384
- return { ok: true, message: "signature verifies against report.md (minisig)" };
2385
- } catch {
2386
- return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
2387
- }
2388
- }
2389
- try {
2390
- const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
2391
- const content = readFileSafe(report);
2392
- if (content === null || parsed.algo !== "ed25519-pure")
2393
- return { ok: false, message: "invalid .mugisig file" };
2394
- return pureVerify(content, parsed) ? { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" } : { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
2395
- } catch {
2396
- return { ok: false, message: "invalid .mugisig file" };
2397
- }
2398
- }
2399
- function readFileSafe(p) {
2400
- try {
2401
- return readFileSync13(p, "utf8");
2402
- } catch {
2403
- return null;
2404
- }
2405
- }
2406
- function readFileSyncSafe2(p) {
2407
- try {
2408
- return readFileSync13(p, "utf8").trim();
2409
- } catch {
2410
- return "";
2411
- }
2412
- }
2413
-
2414
3182
  // src/slop.ts
2415
3183
  function detectHealingSlop(input) {
2416
3184
  const kind = "healing";
@@ -2441,6 +3209,16 @@ function computeLiveSlop(input) {
2441
3209
  // src/cli.ts
2442
3210
  var str = (v) => typeof v === "string" ? v : undefined;
2443
3211
  var flag = (v) => v === true;
3212
+ function resolveProjectDir(explicit) {
3213
+ if (explicit)
3214
+ return resolve2(explicit);
3215
+ try {
3216
+ const root = execFileSync6("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
3217
+ if (root)
3218
+ return root;
3219
+ } catch {}
3220
+ return process.cwd();
3221
+ }
2444
3222
  async function run(argv) {
2445
3223
  const { command, flags, _ } = parseArgs(argv);
2446
3224
  if (flag(flags.help) || command === "help")
@@ -2449,16 +3227,23 @@ async function run(argv) {
2449
3227
  console.log(`mugiwara ${VERSION}`);
2450
3228
  return;
2451
3229
  }
2452
- if (command === "continue" || command === "status") {
2453
- return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
3230
+ {
3231
+ const bypass = new Set(["install", "update", "uninstall", "list"]);
3232
+ if (!bypass.has(command)) {
3233
+ const projectDirForHarness = resolveProjectDir(str(flags.project));
3234
+ enforceHarnessPolicy(projectDirForHarness);
3235
+ }
2454
3236
  }
2455
3237
  const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
2456
3238
  if (!isDryRunInstall) {
2457
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3239
+ const projectDir = resolveProjectDir(str(flags.project));
2458
3240
  if (ensureConfig(projectDir)) {
2459
- console.log(`default .mugiwara/config written at ${join20(projectDir, ".mugiwara", "config")} (edit it to customise)`);
3241
+ console.log(`default .mugiwara/config written at ${join21(projectDir, ".mugiwara", "config")} (edit it to customise)`);
2460
3242
  }
2461
3243
  }
3244
+ if (command === "continue" || command === "status") {
3245
+ return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
3246
+ }
2462
3247
  switch (command) {
2463
3248
  case "install":
2464
3249
  return install(flags);
@@ -2490,12 +3275,14 @@ async function run(argv) {
2490
3275
  return handoffCmd(flags, _);
2491
3276
  case "sign":
2492
3277
  return signCmd(flags, _);
3278
+ case "migrate":
3279
+ return migrateCmd(flags);
2493
3280
  default:
2494
3281
  throw new Error(`Unknown command: ${command}`);
2495
3282
  }
2496
3283
  }
2497
3284
  function resetCmd(flags) {
2498
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3285
+ const projectDir = resolveProjectDir(str(flags.project));
2499
3286
  const force = flag(flags.force);
2500
3287
  const result = resetMission(projectDir, flag(flags.keepLogs), force);
2501
3288
  if (result.blocked) {
@@ -2510,7 +3297,7 @@ function resetCmd(flags) {
2510
3297
  console.log(`kept: ${result.kept.join(", ")}`);
2511
3298
  }
2512
3299
  function archive(flags, positionals) {
2513
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3300
+ const projectDir = resolveProjectDir(str(flags.project));
2514
3301
  const mission = positionals[1];
2515
3302
  if (!mission) {
2516
3303
  console.error("usage: mugiwara archive <mission> [--project <dir>] [--dry-run]");
@@ -2529,10 +3316,10 @@ function archive(flags, positionals) {
2529
3316
  console.log(`index updated: ${result.index}`);
2530
3317
  }
2531
3318
  function cleanCmd(flags) {
2532
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3319
+ const projectDir = resolveProjectDir(str(flags.project));
2533
3320
  const dryRun = flag(flags.dryRun);
2534
- const root = join20(projectDir, ".mugiwara", "missions");
2535
- if (!existsSync15(root)) {
3321
+ const root = join21(projectDir, ".mugiwara", "missions");
3322
+ if (!existsSync16(root)) {
2536
3323
  console.log("nothing to clean (.mugiwara/missions/ does not exist).");
2537
3324
  return;
2538
3325
  }
@@ -2543,7 +3330,7 @@ function cleanCmd(flags) {
2543
3330
  process.exit(1);
2544
3331
  }
2545
3332
  let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
2546
- const stateFiles = (m) => readdirSync10(join20(root, m)).filter((f) => {
3333
+ const stateFiles = (m) => readdirSync10(join21(root, m)).filter((f) => {
2547
3334
  const stem = f.replace(/\.json$/, "");
2548
3335
  return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
2549
3336
  });
@@ -2553,7 +3340,7 @@ function cleanCmd(flags) {
2553
3340
  return false;
2554
3341
  for (const f of stateFiles(m)) {
2555
3342
  try {
2556
- const ts = Date.parse(JSON.parse(readFileSync14(join20(root, m, f), "utf8")).updated_at ?? "") || 0;
3343
+ const ts = Date.parse(JSON.parse(readFileSync15(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
2557
3344
  if (ts === 0 || ts >= beforeMs)
2558
3345
  return false;
2559
3346
  } catch {
@@ -2563,7 +3350,7 @@ function cleanCmd(flags) {
2563
3350
  return true;
2564
3351
  };
2565
3352
  if (!flag(flags.all)) {
2566
- candidates = candidates.filter((m) => existsSync15(join20(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
3353
+ candidates = candidates.filter((m) => existsSync16(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
2567
3354
  } else if (!flag(flags.force)) {
2568
3355
  const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
2569
3356
  if (live.length) {
@@ -2596,8 +3383,8 @@ async function resolveOptions(flags) {
2596
3383
  } else
2597
3384
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
2598
3385
  }
2599
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
2600
- if (scope === "project" && !existsSync15(projectDir))
3386
+ const projectDir = resolveProjectDir(str(flags.project));
3387
+ if (scope === "project" && !existsSync16(projectDir))
2601
3388
  throw new Error(`Project dir not found: ${projectDir}`);
2602
3389
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
2603
3390
  if (targetIds && targetIds.includes("all"))
@@ -2664,19 +3451,21 @@ Dry run — nothing written.`);
2664
3451
  OK mugiwara ${VERSION} installed (manifest: ${file})`);
2665
3452
  if (allNotes.length)
2666
3453
  console.log(`${allNotes.length} note(s) above may need attention.`);
3454
+ console.log("CLI: run `npm i -g @ionivetech/mugiwara` so the crew can call `mugiwara savepoint/archive/continue`.");
3455
+ console.log(" Without it the crew degrades to inline-only — no state, no resume, no closure gate.");
2667
3456
  console.log(`
2668
3457
  Next: edit .mugiwara/config to customise (mode, branch, coverage, depths).`);
2669
3458
  }
2670
3459
  async function uninstall(flags) {
2671
3460
  const scope = flag(flags.global) ? "global" : "project";
2672
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3461
+ const projectDir = resolveProjectDir(str(flags.project));
2673
3462
  const home = homedir4();
2674
3463
  const file = manifestPath({ scope, projectDir, home });
2675
3464
  const manifest = readManifest(file);
2676
3465
  if (!manifest) {
2677
3466
  if (scope === "project") {
2678
3467
  const globalFile = manifestPath({ scope: "global", projectDir, home });
2679
- if (existsSync15(globalFile)) {
3468
+ if (existsSync16(globalFile)) {
2680
3469
  console.log(`No project manifest found, but a global install exists. Try:
2681
3470
  mugiwara uninstall --global`);
2682
3471
  return;
@@ -2713,13 +3502,13 @@ async function uninstall(flags) {
2713
3502
  console.log(` note: ${n}`);
2714
3503
  }
2715
3504
  rmSync3(file);
2716
- const mugiDir = join20(scope === "global" ? home : projectDir, ".mugiwara");
2717
- if (existsSync15(mugiDir) && readdirSync10(mugiDir).length === 0) {
3505
+ const mugiDir = join21(scope === "global" ? home : projectDir, ".mugiwara");
3506
+ if (existsSync16(mugiDir) && readdirSync10(mugiDir).length === 0) {
2718
3507
  rmSync3(mugiDir, { recursive: true, force: true });
2719
3508
  }
2720
3509
  if (manifest.targets.includes("opencode")) {
2721
- const opencodeCache = join20(home, ".cache", "opencode", "packages", "@ionivetech");
2722
- if (existsSync15(opencodeCache)) {
3510
+ const opencodeCache = join21(home, ".cache", "opencode", "packages", "@ionivetech");
3511
+ if (existsSync16(opencodeCache)) {
2723
3512
  rmSync3(opencodeCache, { recursive: true, force: true });
2724
3513
  console.log(" cleared opencode npm cache (stale plugin versions)");
2725
3514
  }
@@ -2727,9 +3516,25 @@ async function uninstall(flags) {
2727
3516
  }
2728
3517
  console.log(`OK removed ${removed.length} files`);
2729
3518
  }
3519
+ function legacyWarning(projectDir) {
3520
+ if (hasLegacyLayout(projectDir)) {
3521
+ console.error("⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)");
3522
+ }
3523
+ }
3524
+ function schemaWarnings(projectDir) {
3525
+ const states = readState(projectDir);
3526
+ for (const s of states) {
3527
+ const v = s.schema_version;
3528
+ if (v !== CURRENT_SCHEMA_VERSION) {
3529
+ const wrote = v === null || v === undefined || v === "" ? "unknown" : String(v);
3530
+ console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ""}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
3531
+ }
3532
+ }
3533
+ }
2730
3534
  function list(flags) {
2731
3535
  const home = homedir4();
2732
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3536
+ const projectDir = resolveProjectDir(str(flags.project));
3537
+ legacyWarning(projectDir);
2733
3538
  let found = false;
2734
3539
  for (const [label, file] of [
2735
3540
  ["project", manifestPath({ scope: "project", projectDir, home })],
@@ -2740,7 +3545,7 @@ function list(flags) {
2740
3545
  continue;
2741
3546
  found = true;
2742
3547
  if (flag(flags.check)) {
2743
- const missing = m.files.filter((f) => !existsSync15(f));
3548
+ const missing = m.files.filter((f) => !existsSync16(f));
2744
3549
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
2745
3550
  } else {
2746
3551
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -2750,9 +3555,20 @@ function list(flags) {
2750
3555
  console.log("No mugiwara installation found.");
2751
3556
  }
2752
3557
  function continueCmd(flags, positionals) {
2753
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3558
+ const projectDir = resolveProjectDir(str(flags.project));
3559
+ legacyWarning(projectDir);
3560
+ schemaWarnings(projectDir);
2754
3561
  const [mission, member] = positionals.slice(1);
2755
3562
  let entries = readContinue(projectDir);
3563
+ if (mission) {
3564
+ readState(projectDir);
3565
+ const badState = unreadableStateFiles();
3566
+ const target10 = member ? `${mission}/${member}.json` : `${mission}/state.json`;
3567
+ if (badState.includes(target10)) {
3568
+ console.error(`✗ mission "${mission}"${member ? ` member "${member}"` : ""} has unreadable state: ${target10}`);
3569
+ process.exit(1);
3570
+ }
3571
+ }
2756
3572
  if (!flag(flags.all)) {
2757
3573
  const actor = gitActor(projectDir);
2758
3574
  const mine = entries.filter((e) => e.actor === actor);
@@ -2790,10 +3606,17 @@ Pick one: mugiwara continue ${r.mission} <member>`);
2790
3606
  process.exit(2);
2791
3607
  }
2792
3608
  function statusCmd(flags) {
2793
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3609
+ const projectDir = resolveProjectDir(str(flags.project));
3610
+ legacyWarning(projectDir);
3611
+ schemaWarnings(projectDir);
2794
3612
  const states = readState(projectDir);
3613
+ const bad = unreadableStateFiles();
3614
+ if (bad.length) {
3615
+ console.error(`⚠ ${bad.length} unreadable state file(s): ${bad.join(", ")}`);
3616
+ console.error(' These are not "no mission" — they are corrupt. Inspect or delete them.');
3617
+ }
2795
3618
  if (!states.length) {
2796
- console.log("No mission state on disk.");
3619
+ console.log(bad.length ? "No readable mission state on disk." : "No mission state on disk.");
2797
3620
  return;
2798
3621
  }
2799
3622
  const actor = flag(flags.all) ? null : gitActor(projectDir);
@@ -2811,7 +3634,7 @@ function statusCmd(flags) {
2811
3634
  }
2812
3635
  }
2813
3636
  function costCmd(flags, positionals) {
2814
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3637
+ const projectDir = resolveProjectDir(str(flags.project));
2815
3638
  const mission = str(flags.mission) ?? positionals[1] ?? (() => {
2816
3639
  const states2 = readState(projectDir);
2817
3640
  if (states2.length === 1)
@@ -2826,8 +3649,8 @@ function costCmd(flags, positionals) {
2826
3649
  console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
2827
3650
  process.exit(1);
2828
3651
  }
2829
- const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2830
- if (!existsSync15(missionDir)) {
3652
+ const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3653
+ if (!existsSync16(missionDir)) {
2831
3654
  console.error(`No cost ledger found for mission "${mission}"`);
2832
3655
  process.exit(1);
2833
3656
  }
@@ -2863,7 +3686,7 @@ function costCmd(flags, positionals) {
2863
3686
  }
2864
3687
  }
2865
3688
  function runCmd(flags, positionals) {
2866
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3689
+ const projectDir = resolveProjectDir(str(flags.project));
2867
3690
  const name = positionals[1];
2868
3691
  if (!name) {
2869
3692
  console.error(`usage: mugiwara run <script> [args...]
@@ -2875,7 +3698,7 @@ function runCmd(flags, positionals) {
2875
3698
  process.exit(code);
2876
3699
  }
2877
3700
  function blameCmd(flags, positionals) {
2878
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3701
+ const projectDir = resolveProjectDir(str(flags.project));
2879
3702
  const path = positionals[1];
2880
3703
  if (!path) {
2881
3704
  console.error("usage: mugiwara blame <file-path>");
@@ -2909,13 +3732,18 @@ function stalenessLine(projectDir, baseSha) {
2909
3732
  }
2910
3733
  }
2911
3734
  function handoffCmd(flags, positionals) {
2912
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3735
+ const projectDir = resolveProjectDir(str(flags.project));
2913
3736
  const mission = positionals[1];
2914
3737
  if (!mission) {
2915
3738
  console.error("usage: mugiwara handoff <mission> [--project <dir>]");
2916
3739
  process.exit(1);
2917
3740
  }
2918
3741
  const states = readState(projectDir).filter((s) => s.mission === mission);
3742
+ const bad = unreadableStateFiles().filter((p) => p.startsWith(`${mission}/`));
3743
+ if (bad.length) {
3744
+ console.error(`✗ mission "${mission}" has unreadable state: ${bad.join(", ")}`);
3745
+ process.exit(1);
3746
+ }
2919
3747
  if (!states.length) {
2920
3748
  console.error(`no in-flight mission "${mission}"`);
2921
3749
  process.exit(1);
@@ -2947,7 +3775,7 @@ function handoffCmd(flags, positionals) {
2947
3775
  }
2948
3776
  lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
2949
3777
  lines.push("Verify `next_action` against plan.md before executing — the table above is computed state, not judgement.");
2950
- const out = join20(".mugiwara", "missions", mission, "handoff.md");
3778
+ const out = join21(".mugiwara", "missions", mission, "handoff.md");
2951
3779
  writeFileSync10(resolve2(projectDir, out), lines.join(`
2952
3780
  `) + `
2953
3781
  `);
@@ -2956,8 +3784,98 @@ function handoffCmd(flags, positionals) {
2956
3784
  console.log(`
2957
3785
  written: ${out}`);
2958
3786
  }
3787
+ function migrateCmd(flags) {
3788
+ const projectDir = resolveProjectDir(str(flags.project));
3789
+ const dryRun = flag(flags.dryRun);
3790
+ const legacyState = join21(projectDir, ".mugiwara", "state");
3791
+ const legacyContinue = join21(projectDir, ".mugiwara", "continue");
3792
+ const missionsRoot = join21(projectDir, ".mugiwara", "missions");
3793
+ const moves = [];
3794
+ const collect = (srcRoot, isContinue) => {
3795
+ if (!existsSync16(srcRoot))
3796
+ return;
3797
+ const walk = (dir) => {
3798
+ for (const e of readdirSync10(dir, { withFileTypes: true })) {
3799
+ const full = join21(dir, e.name);
3800
+ if (e.isDirectory())
3801
+ walk(full);
3802
+ else if (e.isFile() && e.name.endsWith(".json")) {
3803
+ const rel = full.slice(srcRoot.length + 1);
3804
+ let destRel;
3805
+ if (isContinue) {
3806
+ const parts = rel.split("/");
3807
+ const file = parts.pop();
3808
+ const mission = parts.join("/");
3809
+ const stem = file.slice(0, -".json".length);
3810
+ let destFile;
3811
+ if (stem === "state")
3812
+ destFile = "continue.json";
3813
+ else
3814
+ destFile = `continue-${stem}.json`;
3815
+ destRel = mission ? join21(mission, destFile) : destFile;
3816
+ } else {
3817
+ destRel = rel;
3818
+ }
3819
+ moves.push({ src: full, dest: join21(missionsRoot, destRel) });
3820
+ }
3821
+ }
3822
+ };
3823
+ walk(srcRoot);
3824
+ };
3825
+ collect(legacyState, false);
3826
+ collect(legacyContinue, true);
3827
+ if (!moves.length) {
3828
+ if (!existsSync16(legacyState) && !existsSync16(legacyContinue)) {
3829
+ console.log("no legacy layout found (.mugiwara/state/ does not exist)");
3830
+ } else {
3831
+ console.log("no legacy state files to migrate");
3832
+ }
3833
+ return;
3834
+ }
3835
+ for (const m of moves) {
3836
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
3837
+ if (!dryRun) {
3838
+ mkdirSync10(dirname6(m.dest), { recursive: true });
3839
+ try {
3840
+ const raw = JSON.parse(readFileSync15(m.src, "utf8"));
3841
+ raw.schema_version = CURRENT_SCHEMA_VERSION;
3842
+ writeFileSync10(m.dest, JSON.stringify(raw, null, 2) + `
3843
+ `);
3844
+ rmSync3(m.src, { force: true });
3845
+ } catch {
3846
+ try {
3847
+ renameSync2(m.src, m.dest);
3848
+ } catch {}
3849
+ }
3850
+ }
3851
+ }
3852
+ if (!dryRun) {
3853
+ const prune = (root) => {
3854
+ if (!existsSync16(root))
3855
+ return;
3856
+ const walkPrune = (dir) => {
3857
+ for (const e of readdirSync10(dir, { withFileTypes: true })) {
3858
+ if (e.isDirectory())
3859
+ walkPrune(join21(dir, e.name));
3860
+ }
3861
+ try {
3862
+ if (readdirSync10(dir).length === 0)
3863
+ rmSync3(dir, { recursive: true, force: true });
3864
+ } catch {}
3865
+ };
3866
+ walkPrune(root);
3867
+ try {
3868
+ if (existsSync16(root) && readdirSync10(root).length === 0)
3869
+ rmSync3(root, { recursive: true, force: true });
3870
+ } catch {}
3871
+ };
3872
+ prune(legacyState);
3873
+ prune(legacyContinue);
3874
+ }
3875
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${moves.length} file(s)${dryRun ? " (dry run)" : ""}`);
3876
+ }
2959
3877
  function signCmd(flags, _) {
2960
- const projectDir = resolve2(str(flags.project) ?? process.cwd());
3878
+ const projectDir = resolveProjectDir(str(flags.project));
2961
3879
  if (flag(flags.genKey)) {
2962
3880
  const backend = str(flags.backend) ?? "auto";
2963
3881
  const home = homedir4();
@@ -2976,7 +3894,7 @@ function signCmd(flags, _) {
2976
3894
  }
2977
3895
  }
2978
3896
  const dir = ensurePureKey(home);
2979
- console.log(`✓ pure ed25519 key pair ready: ${join20(dir, "mugiwara.key")} / ${join20(dir, "mugiwara.pub")}`);
3897
+ console.log(`✓ pure ed25519 key pair ready: ${join21(dir, "mugiwara.key")} / ${join21(dir, "mugiwara.pub")}`);
2980
3898
  return;
2981
3899
  }
2982
3900
  const mission = _[1];
@@ -2984,8 +3902,8 @@ function signCmd(flags, _) {
2984
3902
  console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
2985
3903
  process.exit(1);
2986
3904
  }
2987
- const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2988
- if (!existsSync15(missionDir)) {
3905
+ const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3906
+ if (!existsSync16(missionDir)) {
2989
3907
  console.error(`no mission dir: ${missionDir}`);
2990
3908
  process.exit(1);
2991
3909
  }
@@ -3020,10 +3938,12 @@ Usage:
3020
3938
  mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
3021
3939
  mugiwara sign --gen-key [--backend pure|minisign]
3022
3940
  create signing keys (pure ed25519 default)
3941
+ mugiwara migrate [--dry-run] [--project <dir>]
3942
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
3023
3943
  mugiwara run <script> [args...]
3024
- run a bundled harness script here (${RUNNABLE.join(", ")})
3944
+ run a bundled harness script here (${RUNNABLE.join(", ")})
3025
3945
  mugiwara savepoint <mission> [member] [flow] [mode]
3026
- shorthand for: mugiwara run savepoint.sh ...
3946
+ shorthand for: mugiwara run savepoint.sh ...
3027
3947
  mugiwara --help this help
3028
3948
  mugiwara --version print version
3029
3949
 
@@ -3048,6 +3968,7 @@ if (isMain) {
3048
3968
  });
3049
3969
  }
3050
3970
  export {
3971
+ migrateCmd,
3051
3972
  run,
3052
3973
  stalenessLine
3053
3974
  };