@ionivetech/mugiwara 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) 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 +5 -4
  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 +10 -7
  29. package/dist/mugiwara.js +1190 -376
  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 +84 -21
  40. package/scripts/savepoint.sh +22 -2
  41. package/scripts/validate-content.ts +60 -0
  42. package/scripts/verify-install.ts +20 -0
  43. package/scripts/write-metrics.ts +73 -0
  44. package/src/budget.ts +11 -0
  45. package/src/cli.ts +128 -13
  46. package/src/config.ts +6 -0
  47. package/src/continue.ts +29 -0
  48. package/src/cost.ts +3 -0
  49. package/src/integrity.ts +64 -15
  50. package/src/mission.ts +123 -7
  51. package/src/policy.ts +355 -2
  52. package/src/provenance.ts +29 -9
  53. package/src/sign.ts +45 -3
  54. package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
  55. package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
  56. package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
  57. package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
  58. 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
@@ -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))
@@ -957,32 +968,503 @@ function writeManifest(file, data) {
957
968
  }
958
969
 
959
970
  // 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";
971
+ import { existsSync as existsSync13, rmSync as rmSync2, readFileSync as readFileSync13, readdirSync as readdirSync7, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, renameSync, openSync, writeSync, closeSync } from "node:fs";
972
+ import { execFileSync as execFileSync4 } from "node:child_process";
973
+ import { join as join18 } from "node:path";
974
+
975
+ // src/integrity.ts
976
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
977
+ import { isAbsolute, join as join9, relative } from "node:path";
978
+
979
+ // src/policy.ts
980
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
981
+ import { join as join8 } from "node:path";
982
+ var POLICY_FILES = ["mugiwara.policy.yml", "mugiwara.policy.yaml"];
983
+ var KNOWN_ROOTS = ["lanes", "gates", "evidence", "integrity", "attestation", "harness"];
984
+ function parsePolicyYaml(text) {
985
+ const root = {};
986
+ const stack = [{ keyIndent: -1, obj: root }];
987
+ const lines = text.split(/\r?\n/);
988
+ const process2 = (i) => {
989
+ if (i >= lines.length)
990
+ return;
991
+ const noComment = lines[i].replace(/(^|\s)#.*$/, "");
992
+ if (!noComment.trim())
993
+ return process2(i + 1);
994
+ const indent = noComment.length - noComment.trimStart().length;
995
+ const line = noComment.trim();
996
+ while (stack.length > 1 && indent <= stack[stack.length - 1].keyIndent)
997
+ stack.pop();
998
+ const top = stack[stack.length - 1];
999
+ if (top.pending && !line.startsWith("- ") && indent > top.pending.indent && !Array.isArray(top.obj[top.pending.key])) {
1000
+ const { key: key2, indent: pIndent } = top.pending;
1001
+ delete top.pending;
1002
+ const created = {};
1003
+ top.obj[key2] = created;
1004
+ stack.push({ keyIndent: pIndent, obj: created });
1005
+ return process2(i);
1006
+ }
1007
+ if (line.startsWith("- ")) {
1008
+ if (top.pending) {
1009
+ const existing = top.obj[top.pending.key];
1010
+ const arr = Array.isArray(existing) ? existing : [];
1011
+ arr.push(scalar(line.slice(2)));
1012
+ top.obj[top.pending.key] = arr;
1013
+ }
1014
+ return process2(i + 1);
1015
+ }
1016
+ const colon = line.indexOf(":");
1017
+ if (colon === -1)
1018
+ return process2(i + 1);
1019
+ const key = line.slice(0, colon).trim();
1020
+ const rest = line.slice(colon + 1).trim();
1021
+ if (rest === "") {
1022
+ top.pending = { key, indent };
1023
+ } else {
1024
+ top.obj[key] = scalar(rest);
1025
+ delete top.pending;
1026
+ }
1027
+ process2(i + 1);
1028
+ };
1029
+ process2(0);
1030
+ return root;
1031
+ }
1032
+ function scalar(v) {
1033
+ const t = v.trim().replace(/^["']|["']$/g, "");
1034
+ if (/^-?\d+(\.\d+)?$/.test(t))
1035
+ return Number(t);
1036
+ if (t === "true")
1037
+ return true;
1038
+ if (t === "false")
1039
+ return false;
1040
+ return t;
1041
+ }
1042
+ function extractExtraSecretPatterns(text) {
1043
+ const lines = text.split(/\r?\n/);
1044
+ let inBlock = false;
1045
+ let baseIndent = -1;
1046
+ const out = [];
1047
+ let current = null;
1048
+ let currentIndent = -1;
1049
+ for (const rawLine of lines) {
1050
+ const noComment = rawLine.replace(/(^|\s)#.*$/, "");
1051
+ if (!noComment.trim())
1052
+ continue;
1053
+ const indent = noComment.length - noComment.trimStart().length;
1054
+ const trimmed = noComment.trim();
1055
+ if (!inBlock) {
1056
+ if (trimmed.startsWith("extra_secret_patterns:")) {
1057
+ inBlock = true;
1058
+ baseIndent = indent;
1059
+ continue;
1060
+ }
1061
+ } else {
1062
+ if (indent <= baseIndent && !trimmed.startsWith("-") && trimmed.includes(":")) {
1063
+ break;
1064
+ }
1065
+ if (trimmed.startsWith("-")) {
1066
+ if (current)
1067
+ out.push(current);
1068
+ current = {};
1069
+ currentIndent = indent;
1070
+ const afterDash = trimmed.slice(1).trim();
1071
+ if (!afterDash)
1072
+ continue;
1073
+ if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
1074
+ const inner = afterDash.slice(1, -1);
1075
+ for (const part of inner.split(",")) {
1076
+ const colon = part.indexOf(":");
1077
+ if (colon === -1)
1078
+ continue;
1079
+ const k = part.slice(0, colon).trim();
1080
+ const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1081
+ if (k && v)
1082
+ current[k] = v;
1083
+ }
1084
+ } else if (afterDash.includes(":")) {
1085
+ const colon = afterDash.indexOf(":");
1086
+ const k = afterDash.slice(0, colon).trim();
1087
+ const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1088
+ if (k && v)
1089
+ current[k] = v;
1090
+ } else {
1091
+ const v = afterDash.replace(/^["']|["']$/g, "");
1092
+ if (v)
1093
+ current["pattern"] = v;
1094
+ }
1095
+ } else if (current && trimmed.includes(":")) {
1096
+ if (indent > currentIndent) {
1097
+ const colon = trimmed.indexOf(":");
1098
+ const k = trimmed.slice(0, colon).trim();
1099
+ const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1100
+ if (k && v)
1101
+ current[k] = v;
1102
+ } else if (indent <= baseIndent) {
1103
+ break;
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ if (current)
1109
+ out.push(current);
1110
+ return out.filter((o) => typeof o.pattern === "string" && typeof o.label === "string" && o.pattern.length > 0);
1111
+ }
1112
+ function extractAttestation(text) {
1113
+ const lines = text.split(/\r?\n/);
1114
+ let attBase = -1;
1115
+ let inAtt = false;
1116
+ let required;
1117
+ const trusted = [];
1118
+ const revoked = [];
1119
+ let collecting = null;
1120
+ let collectBase = -1;
1121
+ let current = null;
1122
+ let curIndent = -1;
1123
+ const flush = () => {
1124
+ if (!current)
1125
+ return;
1126
+ if (collecting === "trusted")
1127
+ trusted.push(current);
1128
+ else if (collecting === "revoked")
1129
+ revoked.push(current);
1130
+ current = null;
1131
+ };
1132
+ for (let idx = 0;idx < lines.length; idx++) {
1133
+ const rawLine = lines[idx];
1134
+ const noComment = rawLine.replace(/(^|\s)#.*$/, "");
1135
+ if (!noComment.trim())
1136
+ continue;
1137
+ const indent = noComment.length - noComment.trimStart().length;
1138
+ const trimmed = noComment.trim();
1139
+ if (!inAtt) {
1140
+ if (trimmed === "attestation:" || trimmed.startsWith("attestation:")) {
1141
+ const after = trimmed.slice("attestation:".length).trim();
1142
+ if (after.startsWith("{")) {
1143
+ return null;
1144
+ }
1145
+ inAtt = true;
1146
+ attBase = indent;
1147
+ continue;
1148
+ }
1149
+ continue;
1150
+ }
1151
+ if (indent <= attBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
1152
+ flush();
1153
+ break;
1154
+ }
1155
+ if (collecting) {
1156
+ if (indent <= collectBase && !trimmed.startsWith("-") && trimmed.includes(":")) {
1157
+ flush();
1158
+ collecting = null;
1159
+ } else if (trimmed.startsWith("-")) {
1160
+ flush();
1161
+ current = {};
1162
+ curIndent = indent;
1163
+ const afterDash = trimmed.slice(1).trim();
1164
+ if (!afterDash)
1165
+ continue;
1166
+ if (afterDash.startsWith("{") && afterDash.endsWith("}")) {
1167
+ const inner = afterDash.slice(1, -1);
1168
+ for (const part of inner.split(",")) {
1169
+ const colon = part.indexOf(":");
1170
+ if (colon === -1)
1171
+ continue;
1172
+ const k = part.slice(0, colon).trim();
1173
+ const v = part.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1174
+ if (k && v)
1175
+ current[k] = v;
1176
+ }
1177
+ } else if (afterDash.includes(":")) {
1178
+ const colon = afterDash.indexOf(":");
1179
+ const k = afterDash.slice(0, colon).trim();
1180
+ const v = afterDash.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1181
+ if (k && v)
1182
+ current[k] = v;
1183
+ } else {
1184
+ const v = afterDash.replace(/^["']|["']$/g, "");
1185
+ if (v)
1186
+ current["id"] = v;
1187
+ }
1188
+ continue;
1189
+ } else if (current && trimmed.includes(":")) {
1190
+ if (indent > curIndent) {
1191
+ const colon = trimmed.indexOf(":");
1192
+ const k = trimmed.slice(0, colon).trim();
1193
+ const v = trimmed.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1194
+ if (k && v)
1195
+ current[k] = v;
1196
+ continue;
1197
+ }
1198
+ }
1199
+ if (collecting)
1200
+ continue;
1201
+ }
1202
+ if (trimmed.startsWith("required:")) {
1203
+ const v = trimmed.slice("required:".length).trim().replace(/^["']|["']$/g, "");
1204
+ if (v === "true")
1205
+ required = true;
1206
+ else if (v === "false")
1207
+ required = false;
1208
+ continue;
1209
+ }
1210
+ if (trimmed.startsWith("trusted_keys:")) {
1211
+ const after = trimmed.slice("trusted_keys:".length).trim();
1212
+ if (after === "[]")
1213
+ continue;
1214
+ collecting = "trusted";
1215
+ collectBase = indent;
1216
+ current = null;
1217
+ continue;
1218
+ }
1219
+ if (trimmed.startsWith("revoked:")) {
1220
+ const after = trimmed.slice("revoked:".length).trim();
1221
+ if (after === "[]")
1222
+ continue;
1223
+ collecting = "revoked";
1224
+ collectBase = indent;
1225
+ current = null;
1226
+ continue;
1227
+ }
1228
+ }
1229
+ flush();
1230
+ if (required === undefined && trusted.length === 0 && revoked.length === 0)
1231
+ return null;
1232
+ const out = {};
1233
+ if (required !== undefined)
1234
+ out.required = required;
1235
+ if (trusted.length)
1236
+ out.trusted_keys = trusted;
1237
+ if (revoked.length)
1238
+ out.revoked = revoked;
1239
+ return out;
1240
+ }
1241
+ function loadPolicy(projectDir) {
1242
+ for (const name of POLICY_FILES) {
1243
+ const file = join8(projectDir, name);
1244
+ if (!existsSync7(file))
1245
+ continue;
1246
+ const text = readFileSync6(file, "utf8");
1247
+ const raw = parsePolicyYaml(text);
1248
+ const extra = extractExtraSecretPatterns(text);
1249
+ if (extra.length) {
1250
+ if (!raw.integrity || typeof raw.integrity !== "object" || Array.isArray(raw.integrity))
1251
+ raw.integrity = {};
1252
+ raw.integrity.extra_secret_patterns = extra;
1253
+ }
1254
+ const att = extractAttestation(text);
1255
+ if (att) {
1256
+ if (!raw.attestation || typeof raw.attestation !== "object" || Array.isArray(raw.attestation))
1257
+ raw.attestation = {};
1258
+ const a = raw.attestation;
1259
+ if (att.required !== undefined)
1260
+ a.required = att.required;
1261
+ if (att.trusted_keys)
1262
+ a.trusted_keys = att.trusted_keys;
1263
+ if (att.revoked)
1264
+ a.revoked = att.revoked;
1265
+ }
1266
+ return normalize(raw);
1267
+ }
1268
+ return null;
1269
+ }
1270
+ function normalize(raw) {
1271
+ for (const k of Object.keys(raw)) {
1272
+ if (!KNOWN_ROOTS.includes(k))
1273
+ throw new Error(`unknown policy key "${k}" (known: ${KNOWN_ROOTS.join(", ")})`);
1274
+ }
1275
+ const out = {};
1276
+ const lanes = raw.lanes;
1277
+ if (lanes && Array.isArray(lanes.force_full))
1278
+ out.lanes = { force_full: strings(lanes.force_full) };
1279
+ const gates = raw.gates;
1280
+ if (gates) {
1281
+ out.gates = {};
1282
+ const cov = gates.coverage;
1283
+ if (cov) {
1284
+ out.gates.coverage = {};
1285
+ if (typeof cov.new === "number")
1286
+ out.gates.coverage.new = cov.new;
1287
+ if (typeof cov.modified === "number")
1288
+ out.gates.coverage.modified = cov.modified;
1289
+ }
1290
+ if (Array.isArray(gates.require_human_approval))
1291
+ out.gates.require_human_approval = strings(gates.require_human_approval);
1292
+ }
1293
+ const evidence = raw.evidence;
1294
+ if (evidence && Array.isArray(evidence.required))
1295
+ out.evidence = { required: strings(evidence.required) };
1296
+ const integrity = raw.integrity;
1297
+ if (integrity && Array.isArray(integrity.extra_secret_patterns)) {
1298
+ const arr = integrity.extra_secret_patterns;
1299
+ const cleaned = [];
1300
+ for (const e of arr) {
1301
+ if (!e || typeof e !== "object")
1302
+ continue;
1303
+ const rec = e;
1304
+ if (typeof rec.pattern !== "string" || typeof rec.label !== "string")
1305
+ continue;
1306
+ const sev = rec.severity === "warn" ? "warn" : rec.severity === "block" ? "block" : undefined;
1307
+ const entry = { pattern: rec.pattern, label: rec.label };
1308
+ if (sev)
1309
+ entry.severity = sev;
1310
+ cleaned.push(entry);
1311
+ }
1312
+ if (cleaned.length)
1313
+ out.integrity = { extra_secret_patterns: cleaned };
1314
+ }
1315
+ const att = raw.attestation;
1316
+ if (att) {
1317
+ const a = {};
1318
+ if (typeof att.required === "boolean")
1319
+ a.required = att.required;
1320
+ if (Array.isArray(att.trusted_keys)) {
1321
+ const cleanedTk = [];
1322
+ for (const e of att.trusted_keys) {
1323
+ if (!e || typeof e !== "object")
1324
+ continue;
1325
+ const rec = e;
1326
+ if (typeof rec.id !== "string" || typeof rec.pubkey !== "string")
1327
+ continue;
1328
+ if (!rec.id.trim() || !rec.pubkey.trim())
1329
+ continue;
1330
+ const entry = { id: rec.id.trim(), pubkey: rec.pubkey.trim() };
1331
+ if (typeof rec.added === "string" && rec.added.trim())
1332
+ entry.added = rec.added.trim();
1333
+ cleanedTk.push(entry);
1334
+ }
1335
+ if (cleanedTk.length)
1336
+ a.trusted_keys = cleanedTk;
1337
+ }
1338
+ if (Array.isArray(att.revoked)) {
1339
+ const cleanedRv = [];
1340
+ for (const e of att.revoked) {
1341
+ if (!e || typeof e !== "object")
1342
+ continue;
1343
+ const rec = e;
1344
+ if (typeof rec.id !== "string" || !rec.id.trim())
1345
+ continue;
1346
+ const entry = { id: rec.id.trim() };
1347
+ if (typeof rec.revoked === "string" && rec.revoked.trim())
1348
+ entry.revoked = rec.revoked.trim();
1349
+ if (typeof rec.reason === "string" && rec.reason.trim())
1350
+ entry.reason = rec.reason.trim();
1351
+ if (typeof rec.pubkey === "string" && rec.pubkey.trim())
1352
+ entry.pubkey = rec.pubkey.trim();
1353
+ cleanedRv.push(entry);
1354
+ }
1355
+ if (cleanedRv.length)
1356
+ a.revoked = cleanedRv;
1357
+ }
1358
+ if (a.required !== undefined || a.trusted_keys || a.revoked)
1359
+ out.attestation = a;
1360
+ }
1361
+ const harness = raw.harness;
1362
+ if (harness && typeof harness.require_enforcement === "boolean") {
1363
+ out.harness = { require_enforcement: harness.require_enforcement };
1364
+ }
1365
+ return out;
1366
+ }
1367
+ function strings(a) {
1368
+ return a.filter((x) => typeof x === "string" && x.length > 0);
1369
+ }
1370
+ function detectHarness(projectDir) {
1371
+ const e = process.env;
1372
+ const has = (v) => v !== undefined && v !== "";
1373
+ if (has(e.CLAUDECODE) || has(e.CLAUDE_CODE_ENTRYPOINT) || typeof e.ANTHROPIC_MODEL === "string" && /claude/i.test(e.ANTHROPIC_MODEL))
1374
+ return "claude";
1375
+ if (has(e.OPENCODE) || has(e.OPENCODE_TOKENS_FILE))
1376
+ return "opencode";
1377
+ const candidates = [
1378
+ projectDir ? join8(projectDir, ".opencode", "config.json") : null,
1379
+ join8(process.cwd(), ".opencode", "config.json")
1380
+ ].filter(Boolean);
1381
+ for (const p of candidates) {
1382
+ try {
1383
+ if (existsSync7(p))
1384
+ return "opencode";
1385
+ } catch {}
1386
+ }
1387
+ if (has(e.CURSOR) || has(e.VSCODE_GIT_ASKPASS_NODE))
1388
+ return "cursor";
1389
+ return "unknown";
1390
+ }
1391
+ function isEnforcedHarness(projectDir) {
1392
+ return detectHarness(projectDir) === "opencode";
1393
+ }
1394
+ function getHarnessEnforcementError(projectDir) {
1395
+ let policy;
1396
+ try {
1397
+ policy = loadPolicy(projectDir);
1398
+ } catch (e) {
1399
+ throw e;
1400
+ }
1401
+ if (!policy?.harness?.require_enforcement)
1402
+ return null;
1403
+ if (isEnforcedHarness(projectDir))
1404
+ return null;
1405
+ const h = detectHarness(projectDir);
1406
+ return `harness enforcement required but current harness is rules-based only — use opencode or set harness.require_enforcement:false (detected: ${h})`;
1407
+ }
1408
+ function enforceHarnessPolicy(projectDir) {
1409
+ const err = getHarnessEnforcementError(projectDir);
1410
+ if (!err)
1411
+ return;
1412
+ console.error(`✗ ${err}`);
1413
+ process.exit(1);
1414
+ }
963
1415
 
964
1416
  // 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
1417
  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"]
1418
+ [/AKIA[0-9A-Z]{16}/, "AWS access key id", "block"],
1419
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "private key block", "block"],
1420
+ [/gh[pousr]_[A-Za-z0-9]{20,}/, "GitHub token", "block"],
1421
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/, "Slack token", "block"],
1422
+ [/sk-[A-Za-z0-9]{32,}/, "API key (sk-…)", "block"],
1423
+ [/eyJhbGciOi[A-Za-z0-9_.-]{20,}/, "JWT pasted verbatim", "block"],
1424
+ [/(api[_-]?key|secret|passwd|password)\s*[=:]\s*["'][^"'\s]{8,}["']/i, "credential assignment", "block"],
1425
+ [/AIza[0-9A-Za-z_-]{35}/, "Google API key", "block"],
1426
+ [/ya29\.[0-9A-Za-z_-]{20,}/, "Google OAuth token", "block"],
1427
+ [/\b[a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:[^\s@/]{4,}@[^\s/]+/i, "connection string with inline credential", "block"],
1428
+ [/\bAC[a-f0-9]{32}\b/, "Twilio account SID", "block"],
1429
+ [/\bSK[a-f0-9]{32}\b/, "Twilio API key", "block"],
1430
+ [/\bglpat-[A-Za-z0-9_-]{20,}/, "GitLab token", "block"],
1431
+ [/\bnpm_[A-Za-z0-9]{36}\b/, "npm token", "block"],
1432
+ [/\bdop_v1_[a-f0-9]{64}\b/, "DigitalOcean token", "block"],
1433
+ [/\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})\b/, "card-number shape (verify before committing)", "warn"]
975
1434
  ];
976
1435
  var ALLOW_SECRET = "mugiwara:allow-secret";
977
- function findSecrets(body) {
1436
+ function loadExtraPatterns(projectRoot) {
1437
+ try {
1438
+ const policy = loadPolicy(projectRoot);
1439
+ const extras = policy?.integrity?.extra_secret_patterns;
1440
+ if (!extras || !Array.isArray(extras))
1441
+ return [];
1442
+ const out = [];
1443
+ for (const e of extras) {
1444
+ if (!e || typeof e.pattern !== "string" || typeof e.label !== "string")
1445
+ continue;
1446
+ const rec = e;
1447
+ const sev = rec.severity === "warn" ? "warn" : "block";
1448
+ try {
1449
+ const re = new RegExp(rec.pattern);
1450
+ out.push([re, rec.label, sev]);
1451
+ } catch {}
1452
+ }
1453
+ return out;
1454
+ } catch {
1455
+ return [];
1456
+ }
1457
+ }
1458
+ function findSecrets(body, extra) {
978
1459
  const out = [];
1460
+ const patterns = extra ? [...SECRET_PATTERNS, ...extra] : SECRET_PATTERNS;
979
1461
  for (const line of body.split(/\r?\n/)) {
980
1462
  if (line.includes(ALLOW_SECRET))
981
1463
  continue;
982
- for (const [re, label] of SECRET_PATTERNS) {
1464
+ for (const [re, label, severity] of patterns) {
983
1465
  const hit = line.match(re);
984
1466
  if (hit)
985
- out.push({ label, hit: hit[0] });
1467
+ out.push({ label, hit: hit[0], severity: severity ?? "block" });
986
1468
  }
987
1469
  }
988
1470
  return out;
@@ -992,7 +1474,7 @@ function trailFiles(dir) {
992
1474
  const out = [];
993
1475
  const walk = (d) => {
994
1476
  for (const e of readdirSync4(d, { withFileTypes: true })) {
995
- const p = join8(d, e.name);
1477
+ const p = join9(d, e.name);
996
1478
  if (e.isDirectory())
997
1479
  walk(p);
998
1480
  else if (TRAIL_EXTS.has(e.name.slice(e.name.lastIndexOf(".")) || ""))
@@ -1020,7 +1502,7 @@ function collectPassCitedPaths(missionDir) {
1020
1502
  for (const f of trailFiles(missionDir)) {
1021
1503
  let body;
1022
1504
  try {
1023
- body = readFileSync6(f, "utf8");
1505
+ body = readFileSync7(f, "utf8");
1024
1506
  } catch {
1025
1507
  continue;
1026
1508
  }
@@ -1041,44 +1523,47 @@ function collectPassCitedPaths(missionDir) {
1041
1523
  function checkTrail(missionDir, projectRoot) {
1042
1524
  const issues = [];
1043
1525
  const files = trailFiles(missionDir);
1526
+ const extraPatterns = loadExtraPatterns(projectRoot);
1044
1527
  for (const f of files) {
1045
1528
  let body;
1046
1529
  try {
1047
- body = readFileSync6(f, "utf8");
1530
+ body = readFileSync7(f, "utf8");
1048
1531
  } catch {
1049
1532
  continue;
1050
1533
  }
1051
1534
  for (const target10 of linkedPaths(body)) {
1052
1535
  if (isAbsolute(target10))
1053
1536
  continue;
1054
- const fromMission = join8(missionDir, target10);
1055
- const fromRoot = join8(projectRoot, target10);
1056
- if (!existsSync7(fromMission) && !existsSync7(fromRoot)) {
1537
+ const fromMission = join9(missionDir, target10);
1538
+ const fromRoot = join9(projectRoot, target10);
1539
+ if (!existsSync8(fromMission) && !existsSync8(fromRoot)) {
1057
1540
  issues.push({
1058
1541
  kind: "dangling-path",
1059
1542
  detail: `${relative(projectRoot, f)} links "${target10}" — no such file (mission dir or repo root)`
1060
1543
  });
1061
1544
  }
1062
1545
  }
1063
- for (const { label, hit } of findSecrets(body)) {
1546
+ for (const { label, hit, severity } of findSecrets(body, extraPatterns.length ? extraPatterns : undefined)) {
1547
+ const isWarn = severity === "warn";
1064
1548
  issues.push({
1065
- kind: "secret",
1066
- detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`
1549
+ kind: isWarn ? "secret-warn" : "secret",
1550
+ detail: `${relative(projectRoot, f)} matches ${label}: ${hit.slice(0, 12)}…`,
1551
+ severity: isWarn ? "warn" : "block"
1067
1552
  });
1068
1553
  }
1069
1554
  }
1070
1555
  const evidencePaths = [];
1071
- const evidenceFile = join8(missionDir, "state.json");
1072
- if (existsSync7(evidenceFile)) {
1556
+ const evidenceFile = join9(missionDir, "state.json");
1557
+ if (existsSync8(evidenceFile)) {
1073
1558
  try {
1074
- const s = JSON.parse(readFileSync6(evidenceFile, "utf8"));
1559
+ const s = JSON.parse(readFileSync7(evidenceFile, "utf8"));
1075
1560
  if (Array.isArray(s.evidence)) {
1076
1561
  for (const e of s.evidence) {
1077
1562
  if (typeof e !== "string" || !e.trim())
1078
1563
  continue;
1079
1564
  evidencePaths.push(e);
1080
- const cand = join8(projectRoot, e);
1081
- if (!isAbsolute(e) && !existsSync7(cand) && !existsSync7(join8(missionDir, e))) {
1565
+ const cand = join9(projectRoot, e);
1566
+ if (!isAbsolute(e) && !existsSync8(cand) && !existsSync8(join9(missionDir, e))) {
1082
1567
  issues.push({ kind: "evidence", detail: `state.json evidence "${e}" does not exist` });
1083
1568
  }
1084
1569
  }
@@ -1089,14 +1574,16 @@ function checkTrail(missionDir, projectRoot) {
1089
1574
  for (const e of passCited) {
1090
1575
  if (!e.trim() || isAbsolute(e))
1091
1576
  continue;
1092
- const candMission = join8(missionDir, e);
1093
- const candRoot = join8(projectRoot, e);
1094
- const resolved = existsSync7(candMission) ? candMission : existsSync7(candRoot) ? candRoot : null;
1577
+ if (/(?:^|\/)(state|continue)(-[^\/]*)?\.json$/.test(e))
1578
+ continue;
1579
+ const candMission = join9(missionDir, e);
1580
+ const candRoot = join9(projectRoot, e);
1581
+ const resolved = existsSync8(candMission) ? candMission : existsSync8(candRoot) ? candRoot : null;
1095
1582
  if (!resolved)
1096
1583
  continue;
1097
1584
  let body;
1098
1585
  try {
1099
- body = readFileSync6(resolved, "utf8");
1586
+ body = readFileSync7(resolved, "utf8");
1100
1587
  } catch {
1101
1588
  continue;
1102
1589
  }
@@ -1112,12 +1599,12 @@ function formatIssues(issues) {
1112
1599
  }
1113
1600
 
1114
1601
  // 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";
1602
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "node:fs";
1603
+ import { join as join10 } from "node:path";
1117
1604
  var LANE_MIN = new Set(["standard", "full", "spike"]);
1118
1605
  function checkMissionArtifacts(missionDir) {
1119
- const statePath = join9(missionDir, "state.json");
1120
- if (!existsSync8(statePath)) {
1606
+ const statePath = join10(missionDir, "state.json");
1607
+ if (!existsSync9(statePath)) {
1121
1608
  return { ok: true, missing: [], lane: null };
1122
1609
  }
1123
1610
  let lane = "unknown";
@@ -1130,17 +1617,17 @@ function checkMissionArtifacts(missionDir) {
1130
1617
  return { ok: true, missing: [], lane };
1131
1618
  }
1132
1619
  const missing = [];
1133
- if (!existsSync8(join9(missionDir, "plan.md")))
1620
+ if (!existsSync9(join10(missionDir, "plan.md")))
1134
1621
  missing.push("plan.md");
1135
- const flowsDir = join9(missionDir, "flows");
1136
- const hasFlows = existsSync8(flowsDir) && readdirSync5(flowsDir).length > 0;
1622
+ const flowsDir = join10(missionDir, "flows");
1623
+ const hasFlows = existsSync9(flowsDir) && readdirSync5(flowsDir).length > 0;
1137
1624
  if (!hasFlows)
1138
1625
  missing.push("flows/ (no execution evidence)");
1139
1626
  return { ok: missing.length === 0, missing, lane };
1140
1627
  }
1141
1628
  function readFileSyncSafe(p) {
1142
1629
  try {
1143
- return readFileSync7(p, "utf8");
1630
+ return readFileSync8(p, "utf8");
1144
1631
  } catch {
1145
1632
  return "";
1146
1633
  }
@@ -1149,7 +1636,7 @@ function readFileSyncSafe(p) {
1149
1636
  // src/rollback.ts
1150
1637
  import { execFileSync } from "node:child_process";
1151
1638
  import { writeFileSync as writeFileSync6 } from "node:fs";
1152
- import { join as join10 } from "node:path";
1639
+ import { join as join11 } from "node:path";
1153
1640
  function git(cwd, args) {
1154
1641
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1155
1642
  }
@@ -1188,7 +1675,7 @@ function generateRollback(projectDir, missionDir, input) {
1188
1675
  const commitsNewestFirst = [...revList].reverse();
1189
1676
  const filesTouched = input.baseSha === "unknown" ? [] : git(projectDir, ["diff", "--name-only", input.baseSha, input.branch]).split(/\r?\n/).filter(Boolean);
1190
1677
  const body = buildRollback(input, commitsNewestFirst, filesTouched);
1191
- const file = join10(missionDir, "rollback.sh");
1678
+ const file = join11(missionDir, "rollback.sh");
1192
1679
  writeFileSync6(file, body, { mode: 493 });
1193
1680
  return { file: "rollback.sh", commits: commitsNewestFirst.length };
1194
1681
  } catch {
@@ -1199,7 +1686,7 @@ function generateRollback(projectDir, missionDir, input) {
1199
1686
  // src/provenance.ts
1200
1687
  import { execFileSync as execFileSync2 } from "node:child_process";
1201
1688
  import { writeFileSync as writeFileSync7 } from "node:fs";
1202
- import { join as join11 } from "node:path";
1689
+ import { join as join12 } from "node:path";
1203
1690
  function git2(cwd, args) {
1204
1691
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1205
1692
  }
@@ -1239,16 +1726,33 @@ function renderProvenanceMd(note, sha) {
1239
1726
  `) + `
1240
1727
  `;
1241
1728
  }
1242
- function attachGitNote(projectDir, branch, note) {
1729
+ function attachGitNote(projectDir, branch, note, baseSha) {
1243
1730
  try {
1244
- let sha;
1731
+ const range = baseSha ? `${baseSha}..${branch}` : branch;
1732
+ let shas = [];
1245
1733
  try {
1246
- sha = git2(projectDir, ["rev-parse", "--verify", branch]);
1734
+ const raw = git2(projectDir, ["rev-list", range]);
1735
+ shas = raw.split(`
1736
+ `).filter(Boolean);
1247
1737
  } catch {
1248
- sha = git2(projectDir, ["rev-parse", "HEAD"]);
1738
+ shas = [];
1739
+ }
1740
+ if (shas.length > 200) {
1741
+ console.warn(`attachGitNote: range ${shas.length} >200, falling back to head-only`);
1742
+ shas = [];
1249
1743
  }
1250
- git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1251
- return { sha };
1744
+ let targets2 = shas;
1745
+ if (!targets2.length) {
1746
+ try {
1747
+ targets2 = [git2(projectDir, ["rev-parse", "--verify", branch])];
1748
+ } catch {
1749
+ targets2 = [git2(projectDir, ["rev-parse", "HEAD"])];
1750
+ }
1751
+ }
1752
+ for (const sha of targets2) {
1753
+ git2(projectDir, ["notes", "--ref=mugiwara", "add", "-f", "-m", note, sha]);
1754
+ }
1755
+ return { sha: targets2[0], count: targets2.length };
1252
1756
  } catch {
1253
1757
  return null;
1254
1758
  }
@@ -1268,13 +1772,243 @@ function blamePath(projectDir, path) {
1268
1772
  ${note}`;
1269
1773
  } catch {
1270
1774
  return `${path} @ ${sha.slice(0, 7)}
1271
- (no mugiwara provenance note on this commit)`;
1775
+ no per-commit note see .mugiwara/missions/<m>/provenance.md`;
1272
1776
  }
1273
1777
  }
1274
- function writeProvenance(projectDir, missionDir, state) {
1778
+ function writeProvenance(projectDir, missionDir, state, baseSha) {
1275
1779
  const note = buildNote(state);
1276
- const attached = attachGitNote(projectDir, state.branch, note);
1277
- writeFileSync7(join11(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1780
+ const resolvedBase = baseSha ?? (typeof state.base_sha === "string" ? state.base_sha : undefined);
1781
+ const cleanBase = resolvedBase && resolvedBase !== "unknown" ? resolvedBase : undefined;
1782
+ const attached = attachGitNote(projectDir, state.branch, note, cleanBase);
1783
+ writeFileSync7(join12(missionDir, "provenance.md"), renderProvenanceMd(note, attached ? attached.sha : null));
1784
+ }
1785
+
1786
+ // src/sign.ts
1787
+ import { execFileSync as execFileSync3 } from "node:child_process";
1788
+ import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
1789
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
1790
+ import { homedir as homedir3 } from "node:os";
1791
+ import { join as join13 } from "node:path";
1792
+ function signArgs(reportPath, secretKey) {
1793
+ return ["-Sm", reportPath, "-s", secretKey];
1794
+ }
1795
+ function verifyArgs(reportPath, pubKey) {
1796
+ return pubKey ? ["-Vm", reportPath, "-p", pubKey] : ["-Vm", reportPath];
1797
+ }
1798
+ function hasMinisign() {
1799
+ try {
1800
+ execFileSync3("minisign", ["-v"], { stdio: ["ignore", "pipe", "ignore"] });
1801
+ return true;
1802
+ } catch {
1803
+ return false;
1804
+ }
1805
+ }
1806
+ function defaultKey(flag) {
1807
+ return join13(homedir3(), ".mugiwara", flag === "secret" ? "minisign.key" : "minisign.pub");
1808
+ }
1809
+ function generatePureKey() {
1810
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
1811
+ const privJwk = privateKey.export({ format: "jwk" });
1812
+ const pubJwk = publicKey.export({ format: "jwk" });
1813
+ return {
1814
+ key: Buffer.from(privJwk.d, "base64url").toString("base64"),
1815
+ pub: Buffer.from(pubJwk.x, "base64url").toString("base64")
1816
+ };
1817
+ }
1818
+ function ensurePureKey(homeDir) {
1819
+ const dir = join13(homeDir, ".mugiwara");
1820
+ const keyPath = join13(dir, "mugiwara.key");
1821
+ const pubPath = join13(dir, "mugiwara.pub");
1822
+ if (!existsSync10(keyPath) || !existsSync10(pubPath)) {
1823
+ mkdirSync6(dir, { recursive: true });
1824
+ const { key, pub } = generatePureKey();
1825
+ if (!existsSync10(keyPath)) {
1826
+ writeFileSync8(keyPath, key + `
1827
+ `, { mode: 384 });
1828
+ }
1829
+ if (!existsSync10(pubPath))
1830
+ writeFileSync8(pubPath, pub + `
1831
+ `);
1832
+ }
1833
+ try {
1834
+ chmodSync2(keyPath, 384);
1835
+ } catch {}
1836
+ return dir;
1837
+ }
1838
+ function pureSign(content, seedBase64, opts) {
1839
+ const seed = Buffer.from(seedBase64.trim(), "base64");
1840
+ if (seed.length !== 32)
1841
+ return { ok: false, message: "invalid seed (want 32B base64)" };
1842
+ const pubBuf = Buffer.from(opts.pub.trim(), "base64");
1843
+ if (pubBuf.length !== 32)
1844
+ return { ok: false, message: "invalid pub (want 32B base64)" };
1845
+ const privateKey = createPrivateKey({
1846
+ key: {
1847
+ kty: "OKP",
1848
+ crv: "Ed25519",
1849
+ d: seed.toString("base64url"),
1850
+ x: pubBuf.toString("base64url")
1851
+ },
1852
+ format: "jwk"
1853
+ });
1854
+ const sig = sign(null, Buffer.from(content, "utf8"), privateKey).toString("base64");
1855
+ const out = { algo: "ed25519-pure", sig, pub: opts.pub, mission: opts.mission, commit: opts.commit, ts: opts.ts };
1856
+ if (opts.outputPath)
1857
+ writeFileSync8(opts.outputPath, JSON.stringify(out, null, 2) + `
1858
+ `);
1859
+ return { ...out, ok: true };
1860
+ }
1861
+ function pureVerify(content, sig) {
1862
+ try {
1863
+ const pub = Buffer.from(sig.pub, "base64");
1864
+ if (pub.length !== 32)
1865
+ return false;
1866
+ const publicKey = createPublicKey({
1867
+ key: { kty: "OKP", crv: "Ed25519", x: pub.toString("base64url") },
1868
+ format: "jwk"
1869
+ });
1870
+ return verify(null, Buffer.from(content, "utf8"), publicKey, Buffer.from(sig.sig, "base64"));
1871
+ } catch {
1872
+ return false;
1873
+ }
1874
+ }
1875
+ function resolveBackend(configured, env) {
1876
+ switch (configured) {
1877
+ case "off":
1878
+ return "off";
1879
+ case "minisign":
1880
+ return env.hasMinisign ? "minisign" : "minisign-fail";
1881
+ case "pure":
1882
+ return "pure";
1883
+ case "auto":
1884
+ default:
1885
+ return env.hasMinisign && env.hasKey ? "minisign" : "pure";
1886
+ }
1887
+ }
1888
+ function configuredBackend(projectDir) {
1889
+ return readConfig(projectDir).sign;
1890
+ }
1891
+ function missionMeta(projectDir, mission) {
1892
+ let commit = "unknown";
1893
+ try {
1894
+ commit = execFileSync3("git", ["rev-parse", "HEAD"], { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1895
+ } catch {}
1896
+ return { commit, ts: new Date().toISOString() };
1897
+ }
1898
+ function signReport(projectDir, missionDir) {
1899
+ const report = join13(missionDir, "report.md");
1900
+ if (!existsSync10(report))
1901
+ return { ok: false, message: "no report.md to sign — archive first" };
1902
+ const mission = missionDir.split(join13(".mugiwara", "missions", "")).pop() ?? "unknown";
1903
+ const backend = resolveBackend(configuredBackend(projectDir), { hasMinisign: hasMinisign(), hasKey: existsSync10(defaultKey("secret")) });
1904
+ if (backend === "off")
1905
+ return { ok: false, message: "signing disabled (sign=off)" };
1906
+ if (backend === "minisign-fail")
1907
+ return { ok: false, message: "sign=minisign but minisign not installed — install it or set sign=pure" };
1908
+ if (backend === "minisign") {
1909
+ const secretKey = process.env.MUGIWARA_SIGN_KEY?.trim() || defaultKey("secret");
1910
+ try {
1911
+ execFileSync3("minisign", signArgs(report, secretKey), { cwd: projectDir, stdio: "pipe", input: process.env.MUGIWARA_SIGN_PASSWORD ?? "" });
1912
+ return { ok: true, message: `signed ${report}.minisig (minisign, key: ${secretKey})` };
1913
+ } catch (e) {
1914
+ return { ok: false, message: `signing failed: ${e.message}` };
1915
+ }
1916
+ }
1917
+ const dir = ensurePureKey(homedir3());
1918
+ const seed = process.env.MUGIWARA_SIGN_KEY?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.key"));
1919
+ const pub = process.env.MUGIWARA_SIGN_PUB?.trim() || readFileSyncSafe2(join13(dir, "mugiwara.pub"));
1920
+ if (!seed || !pub)
1921
+ return { ok: false, message: "pure keys missing — run `mugiwara sign --gen-key --backend pure`" };
1922
+ const content = readFileSafe(report);
1923
+ if (content === null)
1924
+ return { ok: false, message: `cannot read ${report}` };
1925
+ const { commit, ts } = missionMeta(projectDir, mission);
1926
+ const sig = pureSign(content, seed, { mission, commit, ts, pub, outputPath: `${report}.mugisig` });
1927
+ if (!sig.ok)
1928
+ return { ok: false, message: `signing failed: ${sig.message}` };
1929
+ return { ok: true, message: `signed ${report}.mugisig (pure ed25519, key: ${join13(dir, "mugiwara.key")})` };
1930
+ }
1931
+ function normalizePubkey(pk) {
1932
+ const t = pk.trim();
1933
+ return t.startsWith("ed25519:") ? t.slice("ed25519:".length).trim() : t;
1934
+ }
1935
+ function checkTrust(projectDir, signerPubB64) {
1936
+ try {
1937
+ const policy = loadPolicy(projectDir);
1938
+ const att = policy?.attestation;
1939
+ if (!att)
1940
+ return { ok: true };
1941
+ const trusted = att.trusted_keys ?? [];
1942
+ const revoked = att.revoked ?? [];
1943
+ const signer = signerPubB64.trim();
1944
+ const directlyRevoked = revoked.some((r) => {
1945
+ const rpk = r.pubkey;
1946
+ if (!rpk)
1947
+ return false;
1948
+ return normalizePubkey(rpk) === signer;
1949
+ });
1950
+ if (directlyRevoked)
1951
+ return { ok: false, message: "signature valid but signer revoked (pubkey in revoked list)" };
1952
+ if (trusted.length === 0) {
1953
+ return { ok: true };
1954
+ }
1955
+ const match = trusted.find((e) => normalizePubkey(e.pubkey) === signer);
1956
+ if (!match)
1957
+ return { ok: false, message: "signature valid but signer untrusted (pub not in trusted_keys)" };
1958
+ const revokedById = revoked.some((r) => r.id === match.id);
1959
+ if (revokedById)
1960
+ return { ok: false, message: `signature valid but signer revoked (id: ${match.id})` };
1961
+ return { ok: true };
1962
+ } catch {
1963
+ return { ok: true };
1964
+ }
1965
+ }
1966
+ function verifyReport(projectDir, missionDir) {
1967
+ const report = join13(missionDir, "report.md");
1968
+ const minisig = `${report}.minisig`;
1969
+ const mugisig = `${report}.mugisig`;
1970
+ if (!existsSync10(minisig) && !existsSync10(mugisig)) {
1971
+ return { ok: false, message: "not signed (no .minisig or .mugisig beside report.md)" };
1972
+ }
1973
+ if (existsSync10(minisig)) {
1974
+ if (!hasMinisign())
1975
+ return { ok: false, message: "minisig present but minisign not installed — cannot verify that signature" };
1976
+ const pubKey = existsSync10(defaultKey("public")) ? defaultKey("public") : null;
1977
+ try {
1978
+ execFileSync3("minisign", verifyArgs(report, pubKey), { cwd: projectDir, stdio: "pipe" });
1979
+ return { ok: true, message: "signature verifies against report.md (minisig)" };
1980
+ } catch {
1981
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (minisig)" };
1982
+ }
1983
+ }
1984
+ try {
1985
+ const parsed = JSON.parse(readFileSafe(mugisig) ?? "{}");
1986
+ const content = readFileSafe(report);
1987
+ if (content === null || parsed.algo !== "ed25519-pure")
1988
+ return { ok: false, message: "invalid .mugisig file" };
1989
+ if (!pureVerify(content, parsed))
1990
+ return { ok: false, message: "SIGNATURE INVALID — report.md changed after signing (mugisig)" };
1991
+ const trust = checkTrust(projectDir, parsed.pub);
1992
+ if (!trust.ok)
1993
+ return { ok: false, message: trust.message };
1994
+ return { ok: true, message: "signature verifies against report.md (mugisig, ed25519-pure)" };
1995
+ } catch {
1996
+ return { ok: false, message: "invalid .mugisig file" };
1997
+ }
1998
+ }
1999
+ function readFileSafe(p) {
2000
+ try {
2001
+ return readFileSync9(p, "utf8");
2002
+ } catch {
2003
+ return null;
2004
+ }
2005
+ }
2006
+ function readFileSyncSafe2(p) {
2007
+ try {
2008
+ return readFileSync9(p, "utf8").trim();
2009
+ } catch {
2010
+ return "";
2011
+ }
1278
2012
  }
1279
2013
 
1280
2014
  // src/routing.ts
@@ -1331,8 +2065,8 @@ function renderRouting(ranked, mission) {
1331
2065
  }
1332
2066
 
1333
2067
  // src/budget.ts
1334
- import { existsSync as existsSync9, readdirSync as readdirSync6, statSync } from "node:fs";
1335
- import { join as join12 } from "node:path";
2068
+ import { existsSync as existsSync11, readdirSync as readdirSync6, statSync } from "node:fs";
2069
+ import { join as join14 } from "node:path";
1336
2070
  function readBudgetConfig(projectDir) {
1337
2071
  const cfg = readConfig(projectDir);
1338
2072
  const raw = cfg.context_budget_chars;
@@ -1350,13 +2084,13 @@ function measureContextChars(missionDir) {
1350
2084
  };
1351
2085
  for (const f of readdirSync6(missionDir)) {
1352
2086
  if (/\.md$/.test(f))
1353
- add(join12(missionDir, f));
2087
+ add(join14(missionDir, f));
1354
2088
  }
1355
2089
  for (const sub of ["flows", "waves"]) {
1356
- const dir = join12(missionDir, sub);
1357
- if (existsSync9(dir)) {
2090
+ const dir = join14(missionDir, sub);
2091
+ if (existsSync11(dir)) {
1358
2092
  for (const f of readdirSync6(dir))
1359
- add(join12(dir, f));
2093
+ add(join14(dir, f));
1360
2094
  }
1361
2095
  }
1362
2096
  return total;
@@ -1367,10 +2101,17 @@ function formatFootprint(chars, budget) {
1367
2101
  return `${base} (no budget configured)`;
1368
2102
  return chars > budget ? `${base} — OVER budget ${budget}` : `${base} (budget ${budget})`;
1369
2103
  }
2104
+ var COMPRESS_THRESHOLD_PCT = 0.8;
2105
+ function shouldCompress(budget, chars) {
2106
+ return budget > 0 && chars > Math.floor(budget * COMPRESS_THRESHOLD_PCT);
2107
+ }
2108
+ function compressThreshold(budget) {
2109
+ return Math.floor(budget * COMPRESS_THRESHOLD_PCT);
2110
+ }
1370
2111
 
1371
2112
  // src/cost.ts
1372
- import { appendFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8 } from "node:fs";
1373
- import { join as join13 } from "node:path";
2113
+ import { appendFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync10 } from "node:fs";
2114
+ import { join as join15 } from "node:path";
1374
2115
  var LANE_BUDGET = {
1375
2116
  lean: 12000,
1376
2117
  standard: 25000,
@@ -1426,15 +2167,16 @@ function assertMissionDir(dir) {
1426
2167
  }
1427
2168
  function appendCostEvent(missionDir, event) {
1428
2169
  assertMissionDir(missionDir);
1429
- mkdirSync6(missionDir, { recursive: true });
2170
+ mkdirSync7(missionDir, { recursive: true });
1430
2171
  const line = Object.assign({ ts: new Date().toISOString() }, event);
1431
- appendFileSync(join13(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
2172
+ appendFileSync(join15(missionDir, COST_EVENTS_FILE), JSON.stringify(line) + `
1432
2173
  `, "utf8");
1433
2174
  }
2175
+ var COMPRESSED_KIND = "compressed";
1434
2176
 
1435
2177
  // 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";
2178
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync11 } from "node:fs";
2179
+ import { join as join16 } from "node:path";
1438
2180
  var REGISTRY_FILE = "context-registry.jsonl";
1439
2181
  function isAllowedMissionDir2(dir) {
1440
2182
  if (!dir || dir.includes(".."))
@@ -1453,10 +2195,10 @@ function assertMissionDir2(dir) {
1453
2195
  }
1454
2196
  function loadRegistry(missionDir) {
1455
2197
  assertMissionDir2(missionDir);
1456
- const file = join14(missionDir, REGISTRY_FILE);
2198
+ const file = join16(missionDir, REGISTRY_FILE);
1457
2199
  try {
1458
2200
  const out = [];
1459
- for (const line of readFileSync9(file, "utf8").split(/\r?\n/)) {
2201
+ for (const line of readFileSync11(file, "utf8").split(/\r?\n/)) {
1460
2202
  if (!line.trim())
1461
2203
  continue;
1462
2204
  let e;
@@ -1497,8 +2239,8 @@ function computeContextMetrics(stats) {
1497
2239
  }
1498
2240
 
1499
2241
  // src/reporting.ts
1500
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
1501
- import { join as join15 } from "node:path";
2242
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
2243
+ import { join as join17 } from "node:path";
1502
2244
  function isAllowedMissionDir3(dir) {
1503
2245
  if (!dir || dir.includes(".."))
1504
2246
  return false;
@@ -1516,10 +2258,10 @@ function assertMissionDir3(dir) {
1516
2258
  }
1517
2259
  function parseDecisionTrail(missionDir) {
1518
2260
  assertMissionDir3(missionDir);
1519
- const file = join15(missionDir, "decisions.md");
1520
- if (!existsSync10(file))
2261
+ const file = join17(missionDir, "decisions.md");
2262
+ if (!existsSync12(file))
1521
2263
  return [];
1522
- const raw = readFileSync10(file, "utf8");
2264
+ const raw = readFileSync12(file, "utf8");
1523
2265
  const lines = raw.split(/\r?\n/);
1524
2266
  const headerIdx = lines.findIndex((l) => l.trim() === "## Cost governor decisions" || l.trim() === "## Budget");
1525
2267
  const scan = headerIdx >= 0 ? lines.slice(headerIdx) : lines;
@@ -1535,11 +2277,11 @@ function parseDecisionTrail(missionDir) {
1535
2277
  }
1536
2278
  function loadCostEvents(missionDir) {
1537
2279
  assertMissionDir3(missionDir);
1538
- const file = join15(missionDir, "cost-events.jsonl");
1539
- if (!existsSync10(file))
2280
+ const file = join17(missionDir, "cost-events.jsonl");
2281
+ if (!existsSync12(file))
1540
2282
  return [];
1541
2283
  const out = [];
1542
- for (const line of readFileSync10(file, "utf8").split(/\r?\n/)) {
2284
+ for (const line of readFileSync12(file, "utf8").split(/\r?\n/)) {
1543
2285
  if (!line.trim())
1544
2286
  continue;
1545
2287
  try {
@@ -1646,18 +2388,65 @@ function primaryState(dir, files) {
1646
2388
  if (!name)
1647
2389
  return null;
1648
2390
  try {
1649
- return JSON.parse(readFileSync11(join16(dir, name), "utf8"));
2391
+ return JSON.parse(readFileSync13(join18(dir, name), "utf8"));
1650
2392
  } catch {
1651
2393
  return null;
1652
2394
  }
1653
2395
  }
2396
+ function tasksFromState(state) {
2397
+ if (!state)
2398
+ return { done: 0, total: 0 };
2399
+ const t = state.tasks;
2400
+ if (t && typeof t.done === "number" && typeof t.total === "number")
2401
+ return { done: t.done, total: t.total };
2402
+ const done = Number(state.tasks_done);
2403
+ const total = Number(state.tasks_total);
2404
+ return { done: Number.isFinite(done) ? done : 0, total: Number.isFinite(total) ? total : 0 };
2405
+ }
2406
+ function countPlanTasks(missionDir) {
2407
+ let total = 0;
2408
+ let done = 0;
2409
+ const planFile = join18(missionDir, "plan.md");
2410
+ if (existsSync13(planFile)) {
2411
+ try {
2412
+ const text = readFileSync13(planFile, "utf8");
2413
+ for (const line of text.split(/\r?\n/)) {
2414
+ if (/^\s*-\s*\[[ xX]\]/.test(line)) {
2415
+ total++;
2416
+ if (/^\s*-\s*\[[xX]\]/.test(line))
2417
+ done++;
2418
+ }
2419
+ }
2420
+ } catch {}
2421
+ }
2422
+ if (total === 0) {
2423
+ const subPlanDir = join18(missionDir, "sub-plan");
2424
+ if (existsSync13(subPlanDir)) {
2425
+ try {
2426
+ for (const f of readdirSync7(subPlanDir)) {
2427
+ if (!f.endsWith(".md"))
2428
+ continue;
2429
+ const text = readFileSync13(join18(subPlanDir, f), "utf8");
2430
+ for (const line of text.split(/\r?\n/)) {
2431
+ if (/^\s*-\s*\[[ xX]\]/.test(line)) {
2432
+ total++;
2433
+ if (/^\s*-\s*\[[xX]\]/.test(line))
2434
+ done++;
2435
+ }
2436
+ }
2437
+ }
2438
+ } catch {}
2439
+ }
2440
+ }
2441
+ return { done, total };
2442
+ }
1654
2443
  function changedFiles(projectDir, state) {
1655
2444
  const base = typeof state?.base_sha === "string" ? state.base_sha : "";
1656
2445
  const branch = typeof state?.branch === "string" ? state.branch : "";
1657
2446
  if (!base || base === "unknown" || !branch)
1658
2447
  return [];
1659
2448
  try {
1660
- return execFileSync3("git", ["diff", "--name-only", base, branch], {
2449
+ return execFileSync4("git", ["diff", "--name-only", base, branch], {
1661
2450
  cwd: projectDir,
1662
2451
  encoding: "utf8",
1663
2452
  stdio: ["ignore", "pipe", "ignore"]
@@ -1667,16 +2456,16 @@ function changedFiles(projectDir, state) {
1667
2456
  }
1668
2457
  }
1669
2458
  function activeActor(projectDir) {
1670
- const missionsDir = join16(projectDir, ".mugiwara", "missions");
1671
- if (!existsSync11(missionsDir))
2459
+ const missionsDir = join18(projectDir, ".mugiwara", "missions");
2460
+ if (!existsSync13(missionsDir))
1672
2461
  return null;
1673
2462
  const missions = readdirSync7(missionsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1674
2463
  let latest = null;
1675
2464
  for (const mission of missions) {
1676
- const d = join16(missionsDir, mission);
2465
+ const d = join18(missionsDir, mission);
1677
2466
  for (const f of readdirSync7(d).filter(isStateFile)) {
1678
2467
  try {
1679
- const s = JSON.parse(readFileSync11(join16(d, f), "utf8"));
2468
+ const s = JSON.parse(readFileSync13(join18(d, f), "utf8"));
1680
2469
  const t = Date.parse(s.updated_at || "") || 0;
1681
2470
  if (!latest || t > latest.updated)
1682
2471
  latest = { actor: s.actor || null, updated: t };
@@ -1686,8 +2475,8 @@ function activeActor(projectDir) {
1686
2475
  return latest ? latest.actor : null;
1687
2476
  }
1688
2477
  function resetMission(projectDir, keepLogs, force) {
1689
- const root = join16(projectDir, ".mugiwara");
1690
- if (!existsSync11(root))
2478
+ const root = join18(projectDir, ".mugiwara");
2479
+ if (!existsSync13(root))
1691
2480
  return { removed: [], kept: [] };
1692
2481
  if (!force) {
1693
2482
  const actor = activeActor(projectDir);
@@ -1698,66 +2487,93 @@ function resetMission(projectDir, keepLogs, force) {
1698
2487
  const removed = [];
1699
2488
  const kept = [];
1700
2489
  for (const dir of ["missions", "spec", "plans", "results", "review", "issues", "reports", "state", "continue"]) {
1701
- const p = join16(root, dir);
1702
- if (existsSync11(p)) {
2490
+ const p = join18(root, dir);
2491
+ if (existsSync13(p)) {
1703
2492
  rmSync2(p, { recursive: true, force: true });
1704
2493
  removed.push(dir);
1705
2494
  }
1706
2495
  }
1707
2496
  for (const f of readdirSync7(root)) {
1708
2497
  if (/^state(-.+)?\.json$/.test(f)) {
1709
- const p = join16(root, f);
1710
- if (existsSync11(p)) {
2498
+ const p = join18(root, f);
2499
+ if (existsSync13(p)) {
1711
2500
  rmSync2(p);
1712
2501
  removed.push(f);
1713
2502
  }
1714
2503
  }
1715
2504
  }
1716
2505
  if (!keepLogs) {
1717
- for (const p of [join16(root, "lessons.md"), join16(root, "logs")]) {
1718
- if (existsSync11(p)) {
2506
+ for (const p of [join18(root, "lessons.md"), join18(root, "logs")]) {
2507
+ if (existsSync13(p)) {
1719
2508
  rmSync2(p, { recursive: true, force: true });
1720
- removed.push(p.startsWith(join16(root, "logs")) ? "logs" : "lessons.md");
2509
+ removed.push(p.startsWith(join18(root, "logs")) ? "logs" : "lessons.md");
1721
2510
  }
1722
2511
  }
1723
2512
  } else {
1724
- if (existsSync11(join16(root, "lessons.md")))
2513
+ if (existsSync13(join18(root, "lessons.md")))
1725
2514
  kept.push("lessons.md");
1726
- else if (existsSync11(join16(root, join16("logs", "lessons.md"))))
1727
- kept.push(join16("logs", "lessons.md"));
2515
+ else if (existsSync13(join18(root, join18("logs", "lessons.md"))))
2516
+ kept.push(join18("logs", "lessons.md"));
2517
+ }
2518
+ if (removed.includes("missions") && existsSync13(join18(root, "index.md"))) {
2519
+ rmSync2(join18(root, "index.md"));
2520
+ removed.push("index.md");
1728
2521
  }
1729
2522
  for (const f of ["config", "manifest.json", "backup"]) {
1730
- if (existsSync11(join16(root, f)))
2523
+ if (existsSync13(join18(root, f)))
1731
2524
  kept.push(f);
1732
2525
  }
1733
2526
  return { removed, kept };
1734
2527
  }
1735
2528
  function archiveMission(projectDir, mission, opts = {}) {
1736
2529
  const { dryRun = false } = opts;
1737
- const root = join16(projectDir, ".mugiwara");
2530
+ const root = join18(projectDir, ".mugiwara");
1738
2531
  if (!mission || /[^a-zA-Z0-9._-]/.test(mission) || /^\.+$/.test(mission))
1739
2532
  throw new Error(`invalid mission name "${mission}" (allowlist: [a-zA-Z0-9._-], not a dot-path)`);
1740
2533
  const removed = [];
1741
2534
  const kept = [];
1742
- const dir = join16(root, "missions", mission);
1743
- if (!existsSync11(dir))
2535
+ const dir = join18(root, "missions", mission);
2536
+ if (!existsSync13(dir))
1744
2537
  return { report: null, removed, kept };
1745
2538
  if (!dryRun) {
1746
2539
  const issues = checkTrail(dir, projectDir);
1747
- if (issues.length) {
2540
+ const blocking = issues.filter((i) => i.kind !== "secret-warn" && i.severity !== "warn");
2541
+ if (blocking.length) {
1748
2542
  throw new Error(`closure integrity gate failed — fix these before archiving:
1749
- ${formatIssues(issues)}`);
2543
+ ${formatIssues(blocking)}`);
2544
+ }
2545
+ if (issues.length && blocking.length === 0) {
2546
+ try {
2547
+ const warnText = formatIssues(issues);
2548
+ if (warnText)
2549
+ console.warn(`closure integrity warnings (non-blocking):
2550
+ ${warnText}`);
2551
+ } catch {}
1750
2552
  }
1751
2553
  const artifacts = checkMissionArtifacts(dir);
1752
2554
  if (!artifacts.ok) {
1753
2555
  throw new Error(`archive artifact gate failed — missing: ${artifacts.missing.join(", ")} (lane ${artifacts.lane}). Write the evidence trail before archiving.`);
1754
2556
  }
2557
+ try {
2558
+ const policy = loadPolicy(projectDir);
2559
+ if (policy?.attestation?.required) {
2560
+ const v = verifyReport(projectDir, dir);
2561
+ if (!v.ok) {
2562
+ throw new Error(`closure integrity gate failed — attestation required but report not signed/trusted: ${v.message}`);
2563
+ }
2564
+ }
2565
+ } catch (e) {
2566
+ if (e.message.startsWith("closure integrity gate failed — attestation required"))
2567
+ throw e;
2568
+ if (e.message.startsWith("unknown policy key"))
2569
+ throw e;
2570
+ }
1755
2571
  }
1756
2572
  const files = readdirSync7(dir);
1757
2573
  const state = primaryState(dir, files);
1758
2574
  const stageModels = [...new Set(files.filter(isStateFile).map((f) => {
1759
2575
  try {
1760
- const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
2576
+ const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
1761
2577
  return typeof s.model === "string" ? s.model : "";
1762
2578
  } catch {
1763
2579
  return "";
@@ -1797,7 +2613,7 @@ ${formatIssues(issues)}`);
1797
2613
  let hasReported = false;
1798
2614
  for (const f of files.filter(isStateFile)) {
1799
2615
  try {
1800
- const s = JSON.parse(readFileSync11(join16(dir, f), "utf8"));
2616
+ const s = JSON.parse(readFileSync13(join18(dir, f), "utf8"));
1801
2617
  if (s.tokens_source === "reported" && typeof s.tokens_est === "number") {
1802
2618
  reportedTotal += s.tokens_est;
1803
2619
  hasReported = true;
@@ -1852,6 +2668,41 @@ ${formatIssues(issues)}`);
1852
2668
  try {
1853
2669
  costSection += renderAdaptationSection(dir);
1854
2670
  } catch {}
2671
+ if (shouldCompress(budget, chars)) {
2672
+ try {
2673
+ const flowsDir2 = join18(dir, "flows");
2674
+ const wavesDir = join18(dir, "waves");
2675
+ const targetDir = existsSync13(flowsDir2) ? flowsDir2 : existsSync13(wavesDir) ? wavesDir : null;
2676
+ if (targetDir && existsSync13(targetDir)) {
2677
+ const flowFiles = readdirSync7(targetDir).filter((f) => f.endsWith(".md"));
2678
+ if (flowFiles.length) {
2679
+ const pct = Math.round(chars / budget * 100);
2680
+ const stub = `# Compressed trail
2681
+
2682
+ Trail ${chars} chars exceeds ${pct}% of budget ${budget} (threshold ${compressThreshold(budget)}) — flows archived as stub to preserve budget. Original flows: ${flowFiles.join(", ")}
2683
+ `;
2684
+ for (const f of flowFiles) {
2685
+ try {
2686
+ rmSync2(join18(targetDir, f), { force: true });
2687
+ } catch {}
2688
+ }
2689
+ writeFileSync9(join18(targetDir, "00-compressed.md"), stub);
2690
+ }
2691
+ }
2692
+ appendCostEvent(dir, {
2693
+ kind: COMPRESSED_KIND,
2694
+ mission,
2695
+ tokens_est: est,
2696
+ budget: laneBudget,
2697
+ status: "compressed",
2698
+ context_chars: chars,
2699
+ context_status: ctxStatus,
2700
+ context_metrics: metrics
2701
+ });
2702
+ costSection += `
2703
+ | **Compressed** | yes — ${chars} chars >80% of ${budget} — flows stubbed as 00-compressed.md |`;
2704
+ } catch {}
2705
+ }
1855
2706
  appendCostEvent(dir, {
1856
2707
  kind: "closure",
1857
2708
  mission,
@@ -1873,39 +2724,39 @@ ${formatIssues(issues)}`);
1873
2724
  fold.push(f);
1874
2725
  }
1875
2726
  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;
2727
+ const PR_VERDICT_SRC = join18("flows", "07-pr-verdict.md");
2728
+ const flowsDir = join18(dir, "flows");
2729
+ const legacyWavesDir = join18(dir, "waves");
2730
+ const artDir = existsSync13(flowsDir) ? flowsDir : existsSync13(legacyWavesDir) ? legacyWavesDir : flowsDir;
1880
2731
  const artRel = artDir === legacyWavesDir ? "waves" : "flows";
1881
- if (existsSync11(artDir)) {
2732
+ if (existsSync13(artDir)) {
1882
2733
  for (const f of readdirSync7(artDir).sort()) {
1883
2734
  if (artRel === "flows" && f === "07-pr-verdict.md")
1884
2735
  continue;
1885
- fold.push(join16(artRel, f));
2736
+ fold.push(join18(artRel, f));
1886
2737
  }
1887
2738
  }
1888
- if (existsSync11(join16(dir, "cost-events.jsonl")))
2739
+ if (existsSync13(join18(dir, "cost-events.jsonl")))
1889
2740
  fold.push("cost-events.jsonl");
1890
- if (existsSync11(join16(dir, "context-registry.jsonl")))
2741
+ if (existsSync13(join18(dir, "context-registry.jsonl")))
1891
2742
  fold.push("context-registry.jsonl");
1892
2743
  let report = "";
1893
- const reportPath = join16(dir, "report.md");
2744
+ const reportPath = join18(dir, "report.md");
1894
2745
  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");
2746
+ report = readFileSync13(reportPath, "utf8");
2747
+ else if (existsSync13(join18(artDir, "06-closure.md")))
2748
+ report = readFileSync13(join18(artDir, "06-closure.md"), "utf8");
1898
2749
  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));
2750
+ mkdirSync9(dir, { recursive: true });
2751
+ const prVerdictSrc = join18(dir, PR_VERDICT_SRC);
2752
+ if (existsSync13(prVerdictSrc)) {
2753
+ const prVerdictPath = join18(dir, PR_VERDICT);
2754
+ writeFileSync9(prVerdictPath, readFileSync13(prVerdictSrc, "utf8"));
2755
+ kept.push(join18("missions", mission, PR_VERDICT));
1905
2756
  }
1906
2757
  if (fold.length) {
1907
2758
  const sections = fold.map((f) => {
1908
- const body = readFileSync11(join16(dir, f), "utf8").trim();
2759
+ const body = readFileSync13(join18(dir, f), "utf8").trim();
1909
2760
  const name = f.includes("/") ? f.split("/").pop() ?? f : f;
1910
2761
  return `
1911
2762
 
@@ -1919,26 +2770,26 @@ ${body}`;
1919
2770
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1920
2771
  sensitive_paths: Array.isArray(state.sensitive_paths) ? state.sensitive_paths : []
1921
2772
  }), mission) : "";
1922
- writeFileSync8(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
2773
+ writeFileSync9(tmp, report.trimEnd() + sections + (routingSection || "") + (costSection ? `
1923
2774
  ${costSection}
1924
2775
  ` : "") + `
1925
2776
  `);
1926
2777
  renameSync(tmp, reportPath);
1927
2778
  }
1928
2779
  for (const f of fold) {
1929
- rmSync2(join16(dir, f), { force: true, recursive: true });
1930
- removed.push(join16("missions", mission, f));
2780
+ rmSync2(join18(dir, f), { force: true, recursive: true });
2781
+ removed.push(join18("missions", mission, f));
1931
2782
  }
1932
- if (existsSync11(prVerdictSrc)) {
1933
- rmSync2(join16(dir, PR_VERDICT_SRC), { force: true });
1934
- removed.push(join16("missions", mission, PR_VERDICT_SRC));
2783
+ if (existsSync13(prVerdictSrc)) {
2784
+ rmSync2(join18(dir, PR_VERDICT_SRC), { force: true });
2785
+ removed.push(join18("missions", mission, PR_VERDICT_SRC));
1935
2786
  }
1936
2787
  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)
2788
+ rmSync2(join18(dir, f), { force: true });
2789
+ if (existsSync13(artDir) && readdirSync7(artDir).length === 0)
1939
2790
  rmSync2(artDir, { recursive: true, force: true });
1940
- if (!existsSync11(reportPath)) {
1941
- writeFileSync8(reportPath, `# Mission: ${mission}
2791
+ if (!existsSync13(reportPath)) {
2792
+ writeFileSync9(reportPath, `# Mission: ${mission}
1942
2793
 
1943
2794
  Archived before closure — no wave artifacts were present.
1944
2795
  `);
@@ -1950,38 +2801,44 @@ Archived before closure — no wave artifacts were present.
1950
2801
  baseSha: typeof state.base_sha === "string" ? state.base_sha : "unknown"
1951
2802
  });
1952
2803
  if (rb)
1953
- kept.push(join16("missions", mission, rb.file));
2804
+ kept.push(join18("missions", mission, rb.file));
1954
2805
  try {
2806
+ const st = tasksFromState(state);
2807
+ const pt = countPlanTasks(dir);
2808
+ const tasks_done = pt.total > 0 ? pt.done : st.done;
2809
+ const tasks_total = pt.total > 0 ? pt.total : st.total;
2810
+ const baseShaForNote = typeof state.base_sha === "string" ? state.base_sha : undefined;
1955
2811
  writeProvenance(projectDir, dir, {
1956
2812
  mission,
1957
2813
  actor: typeof state.actor === "string" ? state.actor : "",
1958
2814
  lane: typeof state.lane === "string" ? state.lane : "",
1959
2815
  mode: typeof state.mode === "string" ? state.mode : "",
1960
2816
  branch: state.branch,
1961
- tasks_done: Number(state.tasks_done) || 0,
1962
- tasks_total: Number(state.tasks_total) || 0,
2817
+ tasks_done,
2818
+ tasks_total,
1963
2819
  evidence: Array.isArray(state.evidence) ? state.evidence : [],
1964
- models: stageModels
1965
- });
1966
- kept.push(join16("missions", mission, "provenance.md"));
2820
+ models: stageModels,
2821
+ base_sha: baseShaForNote
2822
+ }, baseShaForNote);
2823
+ kept.push(join18("missions", mission, "provenance.md"));
1967
2824
  } catch {}
1968
2825
  }
1969
2826
  } else {
1970
2827
  for (const f of fold)
1971
- removed.push(join16("missions", mission, f));
2828
+ removed.push(join18("missions", mission, f));
1972
2829
  }
1973
- removed.push(join16("missions", mission, "<session state>"));
2830
+ removed.push(join18("missions", mission, "<session state>"));
1974
2831
  if (files.includes("plan.md"))
1975
- kept.push(join16("missions", mission, "plan.md"));
2832
+ kept.push(join18("missions", mission, "plan.md"));
1976
2833
  if (files.includes("handoff.md"))
1977
- kept.push(join16("missions", mission, "handoff.md"));
1978
- kept.push(join16("missions", mission, "report.md"));
2834
+ kept.push(join18("missions", mission, "handoff.md"));
2835
+ kept.push(join18("missions", mission, "report.md"));
1979
2836
  let index;
1980
- const indexFile = join16(root, "index.md");
2837
+ const indexFile = join18(root, "index.md");
1981
2838
  const line = `- ${mission} — ${new Date().toISOString().slice(0, 10)}
1982
2839
  `;
1983
2840
  if (!dryRun) {
1984
- const existing = existsSync11(indexFile) ? readFileSync11(indexFile, "utf8") : "";
2841
+ const existing = existsSync13(indexFile) ? readFileSync13(indexFile, "utf8") : "";
1985
2842
  if (!existing.split(/\r?\n/).some((l) => l.startsWith(`- ${mission} —`))) {
1986
2843
  const header = existing ? "" : `# Mission index
1987
2844
 
@@ -1995,28 +2852,28 @@ Archived before closure — no wave artifacts were present.
1995
2852
  }
1996
2853
  index = "index.md";
1997
2854
  }
1998
- return { report: join16("missions", mission, "report.md"), removed, kept, index };
2855
+ return { report: join18("missions", mission, "report.md"), removed, kept, index };
1999
2856
  }
2000
2857
 
2001
2858
  // src/run.ts
2002
- import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
2859
+ import { existsSync as existsSync14, readdirSync as readdirSync8 } from "node:fs";
2003
2860
  import { spawnSync } from "node:child_process";
2004
- import { dirname as dirname5, join as join17, resolve } from "node:path";
2861
+ import { dirname as dirname5, join as join19, resolve } from "node:path";
2005
2862
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2006
2863
  var here3 = dirname5(fileURLToPath4(import.meta.url));
2007
- var SCRIPTS_DIR = join17(here3, "..", "scripts");
2864
+ var SCRIPTS_DIR = join19(here3, "..", "scripts");
2008
2865
  var RUNNABLE = ["savepoint.sh", "lane.sh"];
2009
2866
  function findBash() {
2010
2867
  const explicit = process.env.MUGIWARA_BASH?.trim();
2011
2868
  if (explicit)
2012
- return existsSync12(explicit) ? explicit : null;
2869
+ return existsSync14(explicit) ? explicit : null;
2013
2870
  const candidates = process.platform === "win32" ? [
2014
2871
  "C:\\Program Files\\Git\\bin\\bash.exe",
2015
2872
  "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
2016
- join17(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2873
+ join19(process.env.LOCALAPPDATA ?? "", "Programs", "Git", "bin", "bash.exe")
2017
2874
  ] : ["/bin/bash", "/usr/bin/bash", "/usr/local/bin/bash", "/opt/homebrew/bin/bash"];
2018
2875
  for (const p of candidates) {
2019
- if (p && existsSync12(p))
2876
+ if (p && existsSync14(p))
2020
2877
  return p;
2021
2878
  }
2022
2879
  const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bash"], {
@@ -2033,9 +2890,9 @@ function runScript(name, args, projectDir) {
2033
2890
  if (!/^[a-z0-9-]+\.sh$/.test(name)) {
2034
2891
  throw new Error(`invalid script name "${name}" (expected one of: ${RUNNABLE.join(", ")})`);
2035
2892
  }
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")) : [];
2893
+ const script = join19(SCRIPTS_DIR, name);
2894
+ if (!existsSync14(script)) {
2895
+ const have = existsSync14(SCRIPTS_DIR) ? readdirSync8(SCRIPTS_DIR).filter((f) => f.endsWith(".sh")) : [];
2039
2896
  throw new Error(`script not found: ${name}${have.length ? ` (available: ${have.join(", ")})` : ""}`);
2040
2897
  }
2041
2898
  const bash = findBash();
@@ -2052,9 +2909,30 @@ function runScript(name, args, projectDir) {
2052
2909
  }
2053
2910
 
2054
2911
  // 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";
2912
+ import { existsSync as existsSync15, readFileSync as readFileSync14, readdirSync as readdirSync9 } from "node:fs";
2913
+ import { execFileSync as execFileSync5 } from "node:child_process";
2914
+ import { join as join20 } from "node:path";
2915
+ var CURRENT_SCHEMA_VERSION = 2;
2916
+ function hasLegacyLayout(projectDir) {
2917
+ for (const legacy of [join20(projectDir, ".mugiwara", "state"), join20(projectDir, ".mugiwara", "continue")]) {
2918
+ if (!existsSync15(legacy))
2919
+ continue;
2920
+ try {
2921
+ const walk = (dir) => {
2922
+ for (const e of readdirSync9(dir, { withFileTypes: true })) {
2923
+ if (e.isFile() && e.name.endsWith(".json"))
2924
+ return true;
2925
+ if (e.isDirectory() && walk(join20(dir, e.name)))
2926
+ return true;
2927
+ }
2928
+ return false;
2929
+ };
2930
+ if (walk(legacy))
2931
+ return true;
2932
+ } catch {}
2933
+ }
2934
+ return false;
2935
+ }
2058
2936
  var SAFE = /^[A-Za-z0-9._-]+$/;
2059
2937
  var isSafeKey = (s) => SAFE.test(s) && !/^\.+$/.test(s);
2060
2938
  var num = (v) => {
@@ -2071,7 +2949,7 @@ function gitActor(cwd) {
2071
2949
  return author;
2072
2950
  const git3 = (args) => {
2073
2951
  try {
2074
- return execFileSync4("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2952
+ return execFileSync5("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2075
2953
  } catch {
2076
2954
  return "";
2077
2955
  }
@@ -2083,13 +2961,13 @@ function gitActor(cwd) {
2083
2961
  return name || process.env.USER || process.env.USERNAME || "";
2084
2962
  }
2085
2963
  function scan(projectDir, kind, map) {
2086
- const base = join18(projectDir, ".mugiwara", "missions");
2087
- if (!existsSync13(base))
2964
+ const base = join20(projectDir, ".mugiwara", "missions");
2965
+ if (!existsSync15(base))
2088
2966
  return [];
2089
2967
  const out = [];
2090
2968
  const missions = readdirSync9(base, { withFileTypes: true }).filter((e) => e.isDirectory() && isSafeKey(e.name)).map((e) => e.name).sort();
2091
2969
  for (const mission of missions) {
2092
- const dir = join18(base, mission);
2970
+ const dir = join20(base, mission);
2093
2971
  for (const f of readdirSync9(dir).filter((n) => n.endsWith(".json")).sort()) {
2094
2972
  const stem = f.slice(0, -".json".length);
2095
2973
  let member;
@@ -2108,7 +2986,7 @@ function scan(projectDir, kind, map) {
2108
2986
  if (member !== null && !isSafeKey(member))
2109
2987
  continue;
2110
2988
  try {
2111
- const raw = JSON.parse(readFileSync12(join18(dir, f), "utf8"));
2989
+ const raw = JSON.parse(readFileSync14(join20(dir, f), "utf8"));
2112
2990
  if (text(raw.mission) !== mission)
2113
2991
  continue;
2114
2992
  out.push(map(raw, member));
@@ -2136,6 +3014,7 @@ function readContinue(projectDir) {
2136
3014
  function readState(projectDir) {
2137
3015
  return scan(projectDir, "state", (r, member) => {
2138
3016
  const tasks = r.tasks ?? {};
3017
+ const sv = r.schema_version;
2139
3018
  return {
2140
3019
  mission: text(r.mission),
2141
3020
  member,
@@ -2165,7 +3044,8 @@ function readState(projectDir) {
2165
3044
  budget: num(r.budget),
2166
3045
  budget_status: text(r.budget_status) || "ok",
2167
3046
  files_touched: num(r.files_touched),
2168
- evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : []
3047
+ evidence: Array.isArray(r.evidence) ? r.evidence.map(text).filter(Boolean) : [],
3048
+ schema_version: typeof sv === "number" || typeof sv === "string" ? sv : null
2169
3049
  };
2170
3050
  });
2171
3051
  }
@@ -2223,194 +3103,6 @@ function formatResume(e) {
2223
3103
  return `Resumed: ${e.mission}${scope}, Flow ${e.flow}, ${e.tasks_done}/${e.tasks_total} tasks — next_action: ${e.next_action} — run: ${next}`;
2224
3104
  }
2225
3105
 
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
3106
  // src/slop.ts
2415
3107
  function detectHealingSlop(input) {
2416
3108
  const kind = "healing";
@@ -2449,16 +3141,23 @@ async function run(argv) {
2449
3141
  console.log(`mugiwara ${VERSION}`);
2450
3142
  return;
2451
3143
  }
2452
- if (command === "continue" || command === "status") {
2453
- return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
3144
+ {
3145
+ const bypass = new Set(["install", "update", "uninstall", "list"]);
3146
+ if (!bypass.has(command)) {
3147
+ const projectDirForHarness = resolve2(str(flags.project) ?? process.cwd());
3148
+ enforceHarnessPolicy(projectDirForHarness);
3149
+ }
2454
3150
  }
2455
3151
  const isDryRunInstall = (command === "install" || command === "update") && flag(flags.dryRun);
2456
3152
  if (!isDryRunInstall) {
2457
3153
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2458
3154
  if (ensureConfig(projectDir)) {
2459
- console.log(`default .mugiwara/config written at ${join20(projectDir, ".mugiwara", "config")} (edit it to customise)`);
3155
+ console.log(`default .mugiwara/config written at ${join21(projectDir, ".mugiwara", "config")} (edit it to customise)`);
2460
3156
  }
2461
3157
  }
3158
+ if (command === "continue" || command === "status") {
3159
+ return command === "continue" ? continueCmd(flags, _) : statusCmd(flags);
3160
+ }
2462
3161
  switch (command) {
2463
3162
  case "install":
2464
3163
  return install(flags);
@@ -2490,6 +3189,8 @@ async function run(argv) {
2490
3189
  return handoffCmd(flags, _);
2491
3190
  case "sign":
2492
3191
  return signCmd(flags, _);
3192
+ case "migrate":
3193
+ return migrateCmd(flags);
2493
3194
  default:
2494
3195
  throw new Error(`Unknown command: ${command}`);
2495
3196
  }
@@ -2531,8 +3232,8 @@ function archive(flags, positionals) {
2531
3232
  function cleanCmd(flags) {
2532
3233
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2533
3234
  const dryRun = flag(flags.dryRun);
2534
- const root = join20(projectDir, ".mugiwara", "missions");
2535
- if (!existsSync15(root)) {
3235
+ const root = join21(projectDir, ".mugiwara", "missions");
3236
+ if (!existsSync16(root)) {
2536
3237
  console.log("nothing to clean (.mugiwara/missions/ does not exist).");
2537
3238
  return;
2538
3239
  }
@@ -2543,7 +3244,7 @@ function cleanCmd(flags) {
2543
3244
  process.exit(1);
2544
3245
  }
2545
3246
  let candidates = readdirSync10(root, { withFileTypes: true }).filter((e) => e.isDirectory() && /^[A-Za-z0-9._-]+$/.test(e.name) && !/^\.+$/.test(e.name)).map((e) => e.name);
2546
- const stateFiles = (m) => readdirSync10(join20(root, m)).filter((f) => {
3247
+ const stateFiles = (m) => readdirSync10(join21(root, m)).filter((f) => {
2547
3248
  const stem = f.replace(/\.json$/, "");
2548
3249
  return f.endsWith(".json") && stem !== "continue" && !stem.startsWith("continue-");
2549
3250
  });
@@ -2553,7 +3254,7 @@ function cleanCmd(flags) {
2553
3254
  return false;
2554
3255
  for (const f of stateFiles(m)) {
2555
3256
  try {
2556
- const ts = Date.parse(JSON.parse(readFileSync14(join20(root, m, f), "utf8")).updated_at ?? "") || 0;
3257
+ const ts = Date.parse(JSON.parse(readFileSync15(join21(root, m, f), "utf8")).updated_at ?? "") || 0;
2557
3258
  if (ts === 0 || ts >= beforeMs)
2558
3259
  return false;
2559
3260
  } catch {
@@ -2563,7 +3264,7 @@ function cleanCmd(flags) {
2563
3264
  return true;
2564
3265
  };
2565
3266
  if (!flag(flags.all)) {
2566
- candidates = candidates.filter((m) => existsSync15(join20(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
3267
+ candidates = candidates.filter((m) => existsSync16(join21(root, m, "report.md")) && !hasLiveState(m) || staleBefore(m));
2567
3268
  } else if (!flag(flags.force)) {
2568
3269
  const live = candidates.filter((m) => hasLiveState(m) && !staleBefore(m));
2569
3270
  if (live.length) {
@@ -2597,7 +3298,7 @@ async function resolveOptions(flags) {
2597
3298
  scope = await choose(rl, "Install scope?", ["global (user-wide)", "project (this repo)"]) === 0 ? "global" : "project";
2598
3299
  }
2599
3300
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2600
- if (scope === "project" && !existsSync15(projectDir))
3301
+ if (scope === "project" && !existsSync16(projectDir))
2601
3302
  throw new Error(`Project dir not found: ${projectDir}`);
2602
3303
  let targetIds = str(flags.target)?.split(",").map((s) => s.trim()) ?? null;
2603
3304
  if (targetIds && targetIds.includes("all"))
@@ -2676,7 +3377,7 @@ async function uninstall(flags) {
2676
3377
  if (!manifest) {
2677
3378
  if (scope === "project") {
2678
3379
  const globalFile = manifestPath({ scope: "global", projectDir, home });
2679
- if (existsSync15(globalFile)) {
3380
+ if (existsSync16(globalFile)) {
2680
3381
  console.log(`No project manifest found, but a global install exists. Try:
2681
3382
  mugiwara uninstall --global`);
2682
3383
  return;
@@ -2713,13 +3414,13 @@ async function uninstall(flags) {
2713
3414
  console.log(` note: ${n}`);
2714
3415
  }
2715
3416
  rmSync3(file);
2716
- const mugiDir = join20(scope === "global" ? home : projectDir, ".mugiwara");
2717
- if (existsSync15(mugiDir) && readdirSync10(mugiDir).length === 0) {
3417
+ const mugiDir = join21(scope === "global" ? home : projectDir, ".mugiwara");
3418
+ if (existsSync16(mugiDir) && readdirSync10(mugiDir).length === 0) {
2718
3419
  rmSync3(mugiDir, { recursive: true, force: true });
2719
3420
  }
2720
3421
  if (manifest.targets.includes("opencode")) {
2721
- const opencodeCache = join20(home, ".cache", "opencode", "packages", "@ionivetech");
2722
- if (existsSync15(opencodeCache)) {
3422
+ const opencodeCache = join21(home, ".cache", "opencode", "packages", "@ionivetech");
3423
+ if (existsSync16(opencodeCache)) {
2723
3424
  rmSync3(opencodeCache, { recursive: true, force: true });
2724
3425
  console.log(" cleared opencode npm cache (stale plugin versions)");
2725
3426
  }
@@ -2727,9 +3428,25 @@ async function uninstall(flags) {
2727
3428
  }
2728
3429
  console.log(`OK removed ${removed.length} files`);
2729
3430
  }
3431
+ function legacyWarning(projectDir) {
3432
+ if (hasLegacyLayout(projectDir)) {
3433
+ console.error("⚠ legacy layout detected (v0.6 .mugiwara/state/ — run `mugiwara migrate` to move to missions/)");
3434
+ }
3435
+ }
3436
+ function schemaWarnings(projectDir) {
3437
+ const states = readState(projectDir);
3438
+ for (const s of states) {
3439
+ const v = s.schema_version;
3440
+ if (v !== CURRENT_SCHEMA_VERSION) {
3441
+ const wrote = v === null || v === undefined || v === "" ? "unknown" : String(v);
3442
+ console.error(`⚠ state written by v${wrote} (mission ${s.mission}${s.member ? `/${s.member}` : ""}) — current expects v${CURRENT_SCHEMA_VERSION} — run \`mugiwara migrate\``);
3443
+ }
3444
+ }
3445
+ }
2730
3446
  function list(flags) {
2731
3447
  const home = homedir4();
2732
3448
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3449
+ legacyWarning(projectDir);
2733
3450
  let found = false;
2734
3451
  for (const [label, file] of [
2735
3452
  ["project", manifestPath({ scope: "project", projectDir, home })],
@@ -2740,7 +3457,7 @@ function list(flags) {
2740
3457
  continue;
2741
3458
  found = true;
2742
3459
  if (flag(flags.check)) {
2743
- const missing = m.files.filter((f) => !existsSync15(f));
3460
+ const missing = m.files.filter((f) => !existsSync16(f));
2744
3461
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} missing=${missing.length} installed=${m.installedAt}`);
2745
3462
  } else {
2746
3463
  console.log(`${label}: v${m.version} targets=${m.targets.join(",")} files=${m.files.length} installed=${m.installedAt}`);
@@ -2751,6 +3468,8 @@ function list(flags) {
2751
3468
  }
2752
3469
  function continueCmd(flags, positionals) {
2753
3470
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3471
+ legacyWarning(projectDir);
3472
+ schemaWarnings(projectDir);
2754
3473
  const [mission, member] = positionals.slice(1);
2755
3474
  let entries = readContinue(projectDir);
2756
3475
  if (!flag(flags.all)) {
@@ -2791,6 +3510,8 @@ Pick one: mugiwara continue ${r.mission} <member>`);
2791
3510
  }
2792
3511
  function statusCmd(flags) {
2793
3512
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
3513
+ legacyWarning(projectDir);
3514
+ schemaWarnings(projectDir);
2794
3515
  const states = readState(projectDir);
2795
3516
  if (!states.length) {
2796
3517
  console.log("No mission state on disk.");
@@ -2826,8 +3547,8 @@ function costCmd(flags, positionals) {
2826
3547
  console.error("usage: mugiwara cost [--mission <id>] [--json] [--ledger] [--project <dir>]");
2827
3548
  process.exit(1);
2828
3549
  }
2829
- const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2830
- if (!existsSync15(missionDir)) {
3550
+ const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3551
+ if (!existsSync16(missionDir)) {
2831
3552
  console.error(`No cost ledger found for mission "${mission}"`);
2832
3553
  process.exit(1);
2833
3554
  }
@@ -2947,7 +3668,7 @@ function handoffCmd(flags, positionals) {
2947
3668
  }
2948
3669
  lines.push("", "## Resuming", "", `\`mugiwara continue ${mission}\` prints the exact resume point.`);
2949
3670
  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");
3671
+ const out = join21(".mugiwara", "missions", mission, "handoff.md");
2951
3672
  writeFileSync10(resolve2(projectDir, out), lines.join(`
2952
3673
  `) + `
2953
3674
  `);
@@ -2956,6 +3677,96 @@ function handoffCmd(flags, positionals) {
2956
3677
  console.log(`
2957
3678
  written: ${out}`);
2958
3679
  }
3680
+ function migrateCmd(flags) {
3681
+ const projectDir = resolve2(str(flags.project) ?? process.cwd());
3682
+ const dryRun = flag(flags.dryRun);
3683
+ const legacyState = join21(projectDir, ".mugiwara", "state");
3684
+ const legacyContinue = join21(projectDir, ".mugiwara", "continue");
3685
+ const missionsRoot = join21(projectDir, ".mugiwara", "missions");
3686
+ const moves = [];
3687
+ const collect = (srcRoot, isContinue) => {
3688
+ if (!existsSync16(srcRoot))
3689
+ return;
3690
+ const walk = (dir) => {
3691
+ for (const e of readdirSync10(dir, { withFileTypes: true })) {
3692
+ const full = join21(dir, e.name);
3693
+ if (e.isDirectory())
3694
+ walk(full);
3695
+ else if (e.isFile() && e.name.endsWith(".json")) {
3696
+ const rel = full.slice(srcRoot.length + 1);
3697
+ let destRel;
3698
+ if (isContinue) {
3699
+ const parts = rel.split("/");
3700
+ const file = parts.pop();
3701
+ const mission = parts.join("/");
3702
+ const stem = file.slice(0, -".json".length);
3703
+ let destFile;
3704
+ if (stem === "state")
3705
+ destFile = "continue.json";
3706
+ else
3707
+ destFile = `continue-${stem}.json`;
3708
+ destRel = mission ? join21(mission, destFile) : destFile;
3709
+ } else {
3710
+ destRel = rel;
3711
+ }
3712
+ moves.push({ src: full, dest: join21(missionsRoot, destRel) });
3713
+ }
3714
+ }
3715
+ };
3716
+ walk(srcRoot);
3717
+ };
3718
+ collect(legacyState, false);
3719
+ collect(legacyContinue, true);
3720
+ if (!moves.length) {
3721
+ if (!existsSync16(legacyState) && !existsSync16(legacyContinue)) {
3722
+ console.log("no legacy layout found (.mugiwara/state/ does not exist)");
3723
+ } else {
3724
+ console.log("no legacy state files to migrate");
3725
+ }
3726
+ return;
3727
+ }
3728
+ for (const m of moves) {
3729
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${m.src} → ${m.dest}`);
3730
+ if (!dryRun) {
3731
+ mkdirSync10(dirname6(m.dest), { recursive: true });
3732
+ try {
3733
+ const raw = JSON.parse(readFileSync15(m.src, "utf8"));
3734
+ raw.schema_version = CURRENT_SCHEMA_VERSION;
3735
+ writeFileSync10(m.dest, JSON.stringify(raw, null, 2) + `
3736
+ `);
3737
+ rmSync3(m.src, { force: true });
3738
+ } catch {
3739
+ try {
3740
+ renameSync2(m.src, m.dest);
3741
+ } catch {}
3742
+ }
3743
+ }
3744
+ }
3745
+ if (!dryRun) {
3746
+ const prune = (root) => {
3747
+ if (!existsSync16(root))
3748
+ return;
3749
+ const walkPrune = (dir) => {
3750
+ for (const e of readdirSync10(dir, { withFileTypes: true })) {
3751
+ if (e.isDirectory())
3752
+ walkPrune(join21(dir, e.name));
3753
+ }
3754
+ try {
3755
+ if (readdirSync10(dir).length === 0)
3756
+ rmSync3(dir, { recursive: true, force: true });
3757
+ } catch {}
3758
+ };
3759
+ walkPrune(root);
3760
+ try {
3761
+ if (existsSync16(root) && readdirSync10(root).length === 0)
3762
+ rmSync3(root, { recursive: true, force: true });
3763
+ } catch {}
3764
+ };
3765
+ prune(legacyState);
3766
+ prune(legacyContinue);
3767
+ }
3768
+ console.log(`${dryRun ? "would migrate" : "migrated"} ${moves.length} file(s)${dryRun ? " (dry run)" : ""}`);
3769
+ }
2959
3770
  function signCmd(flags, _) {
2960
3771
  const projectDir = resolve2(str(flags.project) ?? process.cwd());
2961
3772
  if (flag(flags.genKey)) {
@@ -2976,7 +3787,7 @@ function signCmd(flags, _) {
2976
3787
  }
2977
3788
  }
2978
3789
  const dir = ensurePureKey(home);
2979
- console.log(`✓ pure ed25519 key pair ready: ${join20(dir, "mugiwara.key")} / ${join20(dir, "mugiwara.pub")}`);
3790
+ console.log(`✓ pure ed25519 key pair ready: ${join21(dir, "mugiwara.key")} / ${join21(dir, "mugiwara.pub")}`);
2980
3791
  return;
2981
3792
  }
2982
3793
  const mission = _[1];
@@ -2984,8 +3795,8 @@ function signCmd(flags, _) {
2984
3795
  console.error("usage: mugiwara sign <mission> [--verify] [--gen-key [--backend pure|minisign]] [--project <dir>]");
2985
3796
  process.exit(1);
2986
3797
  }
2987
- const missionDir = join20(projectDir, ".mugiwara", "missions", mission);
2988
- if (!existsSync15(missionDir)) {
3798
+ const missionDir = join21(projectDir, ".mugiwara", "missions", mission);
3799
+ if (!existsSync16(missionDir)) {
2989
3800
  console.error(`no mission dir: ${missionDir}`);
2990
3801
  process.exit(1);
2991
3802
  }
@@ -3020,10 +3831,12 @@ Usage:
3020
3831
  mugiwara sign <m> attestation: sign report.md (auto/minisign/pure/off; --verify to check)
3021
3832
  mugiwara sign --gen-key [--backend pure|minisign]
3022
3833
  create signing keys (pure ed25519 default)
3834
+ mugiwara migrate [--dry-run] [--project <dir>]
3835
+ move legacy .mugiwara/state/ layout to .mugiwara/missions/
3023
3836
  mugiwara run <script> [args...]
3024
- run a bundled harness script here (${RUNNABLE.join(", ")})
3837
+ run a bundled harness script here (${RUNNABLE.join(", ")})
3025
3838
  mugiwara savepoint <mission> [member] [flow] [mode]
3026
- shorthand for: mugiwara run savepoint.sh ...
3839
+ shorthand for: mugiwara run savepoint.sh ...
3027
3840
  mugiwara --help this help
3028
3841
  mugiwara --version print version
3029
3842
 
@@ -3048,6 +3861,7 @@ if (isMain) {
3048
3861
  });
3049
3862
  }
3050
3863
  export {
3864
+ migrateCmd,
3051
3865
  run,
3052
3866
  stalenessLine
3053
3867
  };