@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.kimi-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/content/agents/brook-healing.md +1 -1
- package/content/agents/memory-keeper.md +5 -0
- package/content/agents/usopp-brainstorm.md +3 -2
- package/content/agents/zoro-execution.md +4 -3
- package/content/skills/mugiwara-brainstorm/SKILL.md +5 -3
- package/content/skills/mugiwara-checkpoint/SKILL.md +2 -0
- package/content/skills/mugiwara-execution/SKILL.md +4 -3
- package/content/skills/mugiwara-execution/references/dispatch.md +1 -1
- package/content/skills/mugiwara-gates/SKILL.md +6 -0
- package/content/skills/mugiwara-healing/SKILL.md +5 -1
- package/content/skills/mugiwara-lessons/SKILL.md +3 -0
- package/content/skills/mugiwara-orchestration/SKILL.md +5 -4
- package/content/skills/mugiwara-planning/SKILL.md +2 -0
- package/content/skills/mugiwara-quality/SKILL.md +3 -14
- package/content/skills/mugiwara-quality/references/order-checklist.md +18 -0
- package/content/skills/mugiwara-resume/SKILL.md +3 -14
- package/content/skills/mugiwara-resume/references/resume-protocol.md +16 -0
- package/content/skills/mugiwara-review/SKILL.md +3 -15
- package/content/skills/mugiwara-review/references/red-flags-review.md +17 -0
- package/content/skills/mugiwara-security/SKILL.md +1 -0
- package/content/skills/mugiwara-ship/SKILL.md +2 -0
- package/content/skills/mugiwara-workflow/SKILL.md +10 -7
- package/dist/mugiwara.js +1190 -376
- package/gemini-extension.json +1 -1
- package/hooks/mugiwara-mode-tracker.js +24 -4
- package/hooks/mugiwara-mode-tracker.ts +36 -7
- package/hooks/session-start.js +6 -1
- package/hooks/session-start.ts +8 -1
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/references/cost-governor.md +104 -0
- package/references/wave-banners.md +1 -2
- package/scripts/gate-selftest.ts +84 -21
- package/scripts/savepoint.sh +22 -2
- package/scripts/validate-content.ts +60 -0
- package/scripts/verify-install.ts +20 -0
- package/scripts/write-metrics.ts +73 -0
- package/src/budget.ts +11 -0
- package/src/cli.ts +128 -13
- package/src/config.ts +6 -0
- package/src/continue.ts +29 -0
- package/src/cost.ts +3 -0
- package/src/integrity.ts +64 -15
- package/src/mission.ts +123 -7
- package/src/policy.ts +355 -2
- package/src/provenance.ts +29 -9
- package/src/sign.ts +45 -3
- package/content/skills/mugiwara-workflow/references/adaptive-budget-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/benchmark-governor.md +0 -53
- package/content/skills/mugiwara-workflow/references/cognitive-output-governor.md +0 -5
- package/content/skills/mugiwara-workflow/references/scope-code-governor.md +0 -14
- 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
|
|
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
|
|
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
|
|
961
|
-
import { execFileSync as
|
|
962
|
-
import { join as
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1055
|
-
const fromRoot =
|
|
1056
|
-
if (!
|
|
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 =
|
|
1072
|
-
if (
|
|
1556
|
+
const evidenceFile = join9(missionDir, "state.json");
|
|
1557
|
+
if (existsSync8(evidenceFile)) {
|
|
1073
1558
|
try {
|
|
1074
|
-
const s = JSON.parse(
|
|
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 =
|
|
1081
|
-
if (!isAbsolute(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
|
-
|
|
1093
|
-
|
|
1094
|
-
const
|
|
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 =
|
|
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
|
|
1116
|
-
import { join as
|
|
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 =
|
|
1120
|
-
if (!
|
|
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 (!
|
|
1620
|
+
if (!existsSync9(join10(missionDir, "plan.md")))
|
|
1134
1621
|
missing.push("plan.md");
|
|
1135
|
-
const flowsDir =
|
|
1136
|
-
const hasFlows =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
1731
|
+
const range = baseSha ? `${baseSha}..${branch}` : branch;
|
|
1732
|
+
let shas = [];
|
|
1245
1733
|
try {
|
|
1246
|
-
|
|
1734
|
+
const raw = git2(projectDir, ["rev-list", range]);
|
|
1735
|
+
shas = raw.split(`
|
|
1736
|
+
`).filter(Boolean);
|
|
1247
1737
|
} catch {
|
|
1248
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
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
|
-
|
|
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
|
|
1277
|
-
|
|
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
|
|
1335
|
-
import { join as
|
|
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(
|
|
2087
|
+
add(join14(missionDir, f));
|
|
1354
2088
|
}
|
|
1355
2089
|
for (const sub of ["flows", "waves"]) {
|
|
1356
|
-
const dir =
|
|
1357
|
-
if (
|
|
2090
|
+
const dir = join14(missionDir, sub);
|
|
2091
|
+
if (existsSync11(dir)) {
|
|
1358
2092
|
for (const f of readdirSync6(dir))
|
|
1359
|
-
add(
|
|
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
|
|
1373
|
-
import { join as
|
|
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
|
-
|
|
2170
|
+
mkdirSync7(missionDir, { recursive: true });
|
|
1430
2171
|
const line = Object.assign({ ts: new Date().toISOString() }, event);
|
|
1431
|
-
appendFileSync(
|
|
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
|
|
1437
|
-
import { join as
|
|
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 =
|
|
2198
|
+
const file = join16(missionDir, REGISTRY_FILE);
|
|
1457
2199
|
try {
|
|
1458
2200
|
const out = [];
|
|
1459
|
-
for (const line of
|
|
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
|
|
1501
|
-
import { join as
|
|
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 =
|
|
1520
|
-
if (!
|
|
2261
|
+
const file = join17(missionDir, "decisions.md");
|
|
2262
|
+
if (!existsSync12(file))
|
|
1521
2263
|
return [];
|
|
1522
|
-
const raw =
|
|
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 =
|
|
1539
|
-
if (!
|
|
2280
|
+
const file = join17(missionDir, "cost-events.jsonl");
|
|
2281
|
+
if (!existsSync12(file))
|
|
1540
2282
|
return [];
|
|
1541
2283
|
const out = [];
|
|
1542
|
-
for (const line of
|
|
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(
|
|
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
|
|
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 =
|
|
1671
|
-
if (!
|
|
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 =
|
|
2465
|
+
const d = join18(missionsDir, mission);
|
|
1677
2466
|
for (const f of readdirSync7(d).filter(isStateFile)) {
|
|
1678
2467
|
try {
|
|
1679
|
-
const s = JSON.parse(
|
|
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 =
|
|
1690
|
-
if (!
|
|
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 =
|
|
1702
|
-
if (
|
|
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 =
|
|
1710
|
-
if (
|
|
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 [
|
|
1718
|
-
if (
|
|
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(
|
|
2509
|
+
removed.push(p.startsWith(join18(root, "logs")) ? "logs" : "lessons.md");
|
|
1721
2510
|
}
|
|
1722
2511
|
}
|
|
1723
2512
|
} else {
|
|
1724
|
-
if (
|
|
2513
|
+
if (existsSync13(join18(root, "lessons.md")))
|
|
1725
2514
|
kept.push("lessons.md");
|
|
1726
|
-
else if (
|
|
1727
|
-
kept.push(
|
|
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 (
|
|
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 =
|
|
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 =
|
|
1743
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
1877
|
-
const flowsDir =
|
|
1878
|
-
const legacyWavesDir =
|
|
1879
|
-
const artDir =
|
|
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 (
|
|
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(
|
|
2736
|
+
fold.push(join18(artRel, f));
|
|
1886
2737
|
}
|
|
1887
2738
|
}
|
|
1888
|
-
if (
|
|
2739
|
+
if (existsSync13(join18(dir, "cost-events.jsonl")))
|
|
1889
2740
|
fold.push("cost-events.jsonl");
|
|
1890
|
-
if (
|
|
2741
|
+
if (existsSync13(join18(dir, "context-registry.jsonl")))
|
|
1891
2742
|
fold.push("context-registry.jsonl");
|
|
1892
2743
|
let report = "";
|
|
1893
|
-
const reportPath =
|
|
2744
|
+
const reportPath = join18(dir, "report.md");
|
|
1894
2745
|
if (files.includes("report.md"))
|
|
1895
|
-
report =
|
|
1896
|
-
else if (
|
|
1897
|
-
report =
|
|
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
|
-
|
|
1900
|
-
const prVerdictSrc =
|
|
1901
|
-
if (
|
|
1902
|
-
const prVerdictPath =
|
|
1903
|
-
|
|
1904
|
-
kept.push(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
1930
|
-
removed.push(
|
|
2780
|
+
rmSync2(join18(dir, f), { force: true, recursive: true });
|
|
2781
|
+
removed.push(join18("missions", mission, f));
|
|
1931
2782
|
}
|
|
1932
|
-
if (
|
|
1933
|
-
rmSync2(
|
|
1934
|
-
removed.push(
|
|
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(
|
|
1938
|
-
if (
|
|
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 (!
|
|
1941
|
-
|
|
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(
|
|
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
|
|
1962
|
-
tasks_total
|
|
2817
|
+
tasks_done,
|
|
2818
|
+
tasks_total,
|
|
1963
2819
|
evidence: Array.isArray(state.evidence) ? state.evidence : [],
|
|
1964
|
-
models: stageModels
|
|
1965
|
-
|
|
1966
|
-
|
|
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(
|
|
2828
|
+
removed.push(join18("missions", mission, f));
|
|
1972
2829
|
}
|
|
1973
|
-
removed.push(
|
|
2830
|
+
removed.push(join18("missions", mission, "<session state>"));
|
|
1974
2831
|
if (files.includes("plan.md"))
|
|
1975
|
-
kept.push(
|
|
2832
|
+
kept.push(join18("missions", mission, "plan.md"));
|
|
1976
2833
|
if (files.includes("handoff.md"))
|
|
1977
|
-
kept.push(
|
|
1978
|
-
kept.push(
|
|
2834
|
+
kept.push(join18("missions", mission, "handoff.md"));
|
|
2835
|
+
kept.push(join18("missions", mission, "report.md"));
|
|
1979
2836
|
let index;
|
|
1980
|
-
const indexFile =
|
|
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 =
|
|
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:
|
|
2855
|
+
return { report: join18("missions", mission, "report.md"), removed, kept, index };
|
|
1999
2856
|
}
|
|
2000
2857
|
|
|
2001
2858
|
// src/run.ts
|
|
2002
|
-
import { existsSync as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
2037
|
-
if (!
|
|
2038
|
-
const have =
|
|
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
|
|
2056
|
-
import { execFileSync as
|
|
2057
|
-
import { join as
|
|
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
|
|
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 =
|
|
2087
|
-
if (!
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
2453
|
-
|
|
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 ${
|
|
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 =
|
|
2535
|
-
if (!
|
|
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(
|
|
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(
|
|
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) =>
|
|
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" && !
|
|
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 (
|
|
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 =
|
|
2717
|
-
if (
|
|
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 =
|
|
2722
|
-
if (
|
|
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) => !
|
|
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 =
|
|
2830
|
-
if (!
|
|
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 =
|
|
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: ${
|
|
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 =
|
|
2988
|
-
if (!
|
|
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
|
-
|
|
3837
|
+
run a bundled harness script here (${RUNNABLE.join(", ")})
|
|
3025
3838
|
mugiwara savepoint <mission> [member] [flow] [mode]
|
|
3026
|
-
|
|
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
|
};
|