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