@mstar-harness/engine 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +326 -46
- package/dist/dispatch.d.ts +37 -18
- package/dist/engine.js +539 -203
- package/dist/index.d.ts +4 -2
- package/dist/project.d.ts +12 -5
- package/dist/prreview.d.ts +65 -0
- package/dist/status.d.ts +13 -1
- package/dist/store.d.ts +76 -0
- package/dist/store.test.d.ts +1 -0
- package/dist/workflow.d.ts +9 -1
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -170,13 +170,13 @@ function isAtOrBelow(dir, root) {
|
|
|
170
170
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
171
171
|
}
|
|
172
172
|
// src/path.ts
|
|
173
|
-
import { existsSync as
|
|
173
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
174
174
|
import { execFileSync } from "node:child_process";
|
|
175
|
-
import { basename as
|
|
175
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve7 } from "node:path";
|
|
176
176
|
|
|
177
177
|
// src/project.ts
|
|
178
|
-
import { existsSync as
|
|
179
|
-
import { join as
|
|
178
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "node:fs";
|
|
179
|
+
import { basename as basename2, join as join8, resolve as resolve6 } from "node:path";
|
|
180
180
|
|
|
181
181
|
// src/iteration.ts
|
|
182
182
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -757,9 +757,167 @@ async function withStatusWriteLock(statusPath, fn, opts = {}) {
|
|
|
757
757
|
}
|
|
758
758
|
}
|
|
759
759
|
|
|
760
|
+
// src/store.ts
|
|
761
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "node:fs";
|
|
762
|
+
import { isAbsolute as isAbsolute3, join as join5, resolve as resolve4 } from "node:path";
|
|
763
|
+
var PLAN_SHAPED_KEY_RE = /^[0-9]{8}-[a-z0-9-]+$/;
|
|
764
|
+
function resolveArtifactPath(harnessRoot, ref) {
|
|
765
|
+
const { kind, key } = ref;
|
|
766
|
+
if (kind === "json") {
|
|
767
|
+
if (!isAbsolute3(key)) {
|
|
768
|
+
throw new Error(`ArtifactStore json key must be an absolute path — got ${JSON.stringify(key)}`);
|
|
769
|
+
}
|
|
770
|
+
if (key.split(/[\\/]+/).includes("..")) {
|
|
771
|
+
throw new Error(`ArtifactStore json key must not contain ".." segments — got ${JSON.stringify(key)}`);
|
|
772
|
+
}
|
|
773
|
+
return key;
|
|
774
|
+
}
|
|
775
|
+
assertSafePathComponent(key, "ArtifactStore key");
|
|
776
|
+
if (kind === "status") {
|
|
777
|
+
if (key !== "root") {
|
|
778
|
+
throw new Error(`ArtifactStore status key must be "root" — got ${JSON.stringify(key)}`);
|
|
779
|
+
}
|
|
780
|
+
return join5(harnessRoot, "status.json");
|
|
781
|
+
}
|
|
782
|
+
if (kind === "snapshot") {
|
|
783
|
+
return join5(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), key, "snapshot.json");
|
|
784
|
+
}
|
|
785
|
+
if (kind === "residuals") {
|
|
786
|
+
return join5(resolveProjectDir(harnessRoot, { harnessDir: harnessRoot }), key, "residuals.json");
|
|
787
|
+
}
|
|
788
|
+
if (PLAN_SHAPED_KEY_RE.test(key)) {
|
|
789
|
+
return join5(harnessRoot, "sdd", key, "review", "report.json");
|
|
790
|
+
}
|
|
791
|
+
return join5(harnessRoot, "sdd", "_reviews", `${key}.json`);
|
|
792
|
+
}
|
|
793
|
+
function listDirNames(dir) {
|
|
794
|
+
return readDirEntries(dir).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
795
|
+
}
|
|
796
|
+
function listJsonKeys(dir) {
|
|
797
|
+
return readDirEntries(dir).filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name.slice(0, -".json".length));
|
|
798
|
+
}
|
|
799
|
+
function readDirEntries(dir) {
|
|
800
|
+
try {
|
|
801
|
+
return readdirSync2(dir, { withFileTypes: true });
|
|
802
|
+
} catch (error) {
|
|
803
|
+
const code = error.code;
|
|
804
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
805
|
+
return [];
|
|
806
|
+
throw error;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function tryResolveGetPath(root, kind, key) {
|
|
810
|
+
try {
|
|
811
|
+
return resolveArtifactPath(root, { kind, key });
|
|
812
|
+
} catch {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function createFsStore(harnessRoot) {
|
|
817
|
+
const root = resolve4(harnessRoot);
|
|
818
|
+
return {
|
|
819
|
+
root,
|
|
820
|
+
async put(doc) {
|
|
821
|
+
if (doc.schema !== undefined) {
|
|
822
|
+
throw new Error("FsStore does not persist schema ids — omit --schema or inject a store module that persists it");
|
|
823
|
+
}
|
|
824
|
+
writeJson(resolveArtifactPath(root, doc), doc.payload);
|
|
825
|
+
},
|
|
826
|
+
async get(ref) {
|
|
827
|
+
const filePath = resolveArtifactPath(root, ref);
|
|
828
|
+
if (!existsSync3(filePath))
|
|
829
|
+
return;
|
|
830
|
+
return readJson(filePath);
|
|
831
|
+
},
|
|
832
|
+
async delete(ref) {
|
|
833
|
+
const filePath = resolveArtifactPath(root, ref);
|
|
834
|
+
if (existsSync3(filePath))
|
|
835
|
+
unlinkSync3(filePath);
|
|
836
|
+
},
|
|
837
|
+
async list(kind) {
|
|
838
|
+
if (kind === "json") {
|
|
839
|
+
throw new Error("ArtifactStore json keys are absolute paths and cannot be listed");
|
|
840
|
+
}
|
|
841
|
+
const keys = [];
|
|
842
|
+
if (kind === "status") {
|
|
843
|
+
if (existsSync3(resolveArtifactPath(root, { kind, key: "root" })))
|
|
844
|
+
keys.push("root");
|
|
845
|
+
} else if (kind === "snapshot" || kind === "residuals") {
|
|
846
|
+
const baseDir = kind === "snapshot" ? resolveWorkflowDir(root, { harnessDir: root }) : resolveProjectDir(root, { harnessDir: root });
|
|
847
|
+
for (const name of listDirNames(baseDir)) {
|
|
848
|
+
const getPath = tryResolveGetPath(root, kind, name);
|
|
849
|
+
if (getPath !== undefined && existsSync3(getPath))
|
|
850
|
+
keys.push(name);
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
const sddDir = join5(root, "sdd");
|
|
854
|
+
for (const key of listJsonKeys(join5(sddDir, "_reviews"))) {
|
|
855
|
+
if (!PLAN_SHAPED_KEY_RE.test(key) && tryResolveGetPath(root, kind, key) !== undefined) {
|
|
856
|
+
keys.push(key);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
for (const name of listDirNames(sddDir)) {
|
|
860
|
+
if (PLAN_SHAPED_KEY_RE.test(name)) {
|
|
861
|
+
const getPath = tryResolveGetPath(root, kind, name);
|
|
862
|
+
if (getPath !== undefined && existsSync3(getPath))
|
|
863
|
+
keys.push(name);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return keys.sort().map((key) => ({ kind, key }));
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
var injectedStore;
|
|
872
|
+
function setArtifactStore(store) {
|
|
873
|
+
injectedStore = store;
|
|
874
|
+
}
|
|
875
|
+
function getArtifactStore() {
|
|
876
|
+
if (injectedStore !== undefined)
|
|
877
|
+
return injectedStore;
|
|
878
|
+
const root = resolveHarnessDir(process.cwd());
|
|
879
|
+
if (root === null) {
|
|
880
|
+
throw new Error(`harness dir not found from ${resolve4(process.cwd())} — cannot create the default FsStore (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
881
|
+
}
|
|
882
|
+
return createFsStore(root);
|
|
883
|
+
}
|
|
884
|
+
function assertFsStorePath(store, ref, expectedPath) {
|
|
885
|
+
const root = store.root;
|
|
886
|
+
if (typeof root !== "string")
|
|
887
|
+
return;
|
|
888
|
+
const storePath = resolveArtifactPath(root, ref);
|
|
889
|
+
const expected = resolve4(expectedPath);
|
|
890
|
+
if (storePath !== expected) {
|
|
891
|
+
throw new Error(`routed writer path mismatch: the active FsStore resolves ${ref.kind}/${JSON.stringify(ref.key)} to ${JSON.stringify(storePath)} but the caller's target is ${JSON.stringify(expected)} — call setArtifactStore(createFsStore(<root>)) first when the write target differs from the active store's root`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
var URI_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
895
|
+
function isArtifactStore(value) {
|
|
896
|
+
return typeof value === "object" && value !== null && typeof value.put === "function" && typeof value.get === "function";
|
|
897
|
+
}
|
|
898
|
+
async function loadStoreModule(modulePath) {
|
|
899
|
+
if (modulePath === "") {
|
|
900
|
+
throw new Error("loadStoreModule: module path must not be empty");
|
|
901
|
+
}
|
|
902
|
+
if (URI_SCHEME_RE.test(modulePath)) {
|
|
903
|
+
throw new Error(`loadStoreModule: only filesystem paths are allowed — got ${JSON.stringify(modulePath)} (URI schemes such as http:/https:/file: are rejected)`);
|
|
904
|
+
}
|
|
905
|
+
const resolved = resolve4(modulePath);
|
|
906
|
+
if (!existsSync3(resolved)) {
|
|
907
|
+
throw new Error(`loadStoreModule: module file not found — ${JSON.stringify(resolved)}`);
|
|
908
|
+
}
|
|
909
|
+
const mod = await import(resolved);
|
|
910
|
+
const candidate = mod.createArtifactStore ?? mod.default ?? mod;
|
|
911
|
+
const store = typeof candidate === "function" ? await candidate() : candidate;
|
|
912
|
+
if (!isArtifactStore(store)) {
|
|
913
|
+
throw new Error(`loadStoreModule: module ${JSON.stringify(resolved)} does not export an ArtifactStore — expected a createArtifactStore named export, a default factory, or a default object with put() and get() functions`);
|
|
914
|
+
}
|
|
915
|
+
return store;
|
|
916
|
+
}
|
|
917
|
+
|
|
760
918
|
// src/status.ts
|
|
761
|
-
import { existsSync as
|
|
762
|
-
import { dirname as dirname4, join as
|
|
919
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, realpathSync } from "node:fs";
|
|
920
|
+
import { dirname as dirname4, join as join7, resolve as resolve5, sep } from "node:path";
|
|
763
921
|
|
|
764
922
|
// src/dispatch.ts
|
|
765
923
|
var BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"';
|
|
@@ -965,8 +1123,10 @@ function composeDispatchGate(text, opts = {}) {
|
|
|
965
1123
|
const violations = [];
|
|
966
1124
|
const writable = opts.writable !== false;
|
|
967
1125
|
violations.push(...validateAssignmentFields(text, { writable }).violations);
|
|
968
|
-
const
|
|
969
|
-
|
|
1126
|
+
const caller = opts.caller ?? "";
|
|
1127
|
+
if (caller.trim() !== "" || opts.callerRequired === true) {
|
|
1128
|
+
violations.push(...antiRecursionPrecheck(caller, parseAssignmentFields(text).executeAs ?? "").violations);
|
|
1129
|
+
}
|
|
970
1130
|
if (writable) {
|
|
971
1131
|
const forms = parseAssignmentBranchForms(text);
|
|
972
1132
|
const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
|
|
@@ -989,7 +1149,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
989
1149
|
return {
|
|
990
1150
|
ok: false,
|
|
991
1151
|
violations: [
|
|
992
|
-
violation3("critical", "dispatch.anti-recursion.empty-binding", `empty
|
|
1152
|
+
violation3("critical", "dispatch.anti-recursion.empty-binding", `empty caller role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "declare the dispatching agent's own role binding (dsh Config `dispatchBinding`) before dispatching")
|
|
993
1153
|
]
|
|
994
1154
|
};
|
|
995
1155
|
}
|
|
@@ -1006,7 +1166,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1006
1166
|
|
|
1007
1167
|
// src/workflow.ts
|
|
1008
1168
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
1009
|
-
import { join as
|
|
1169
|
+
import { join as join6 } from "node:path";
|
|
1010
1170
|
var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
1011
1171
|
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
1012
1172
|
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
@@ -1123,10 +1283,12 @@ async function writeWorkflowSnapshot(snapshot, dir) {
|
|
|
1123
1283
|
const detail = gate.violations.map((v) => v.message).join("; ");
|
|
1124
1284
|
throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
|
|
1125
1285
|
}
|
|
1126
|
-
const snapshotPath =
|
|
1286
|
+
const snapshotPath = join6(dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1287
|
+
const store = getArtifactStore();
|
|
1288
|
+
assertFsStorePath(store, { kind: "snapshot", key: snapshot.id }, snapshotPath);
|
|
1127
1289
|
mkdirSync3(dir, { recursive: true });
|
|
1128
|
-
await withStatusWriteLock(snapshotPath, () => {
|
|
1129
|
-
|
|
1290
|
+
await withStatusWriteLock(snapshotPath, async () => {
|
|
1291
|
+
await store.put({ kind: "snapshot", key: snapshot.id, payload: snapshot });
|
|
1130
1292
|
});
|
|
1131
1293
|
}
|
|
1132
1294
|
|
|
@@ -1293,7 +1455,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1293
1455
|
if (typeof docOrPath === "string") {
|
|
1294
1456
|
try {
|
|
1295
1457
|
doc = readJson(docOrPath);
|
|
1296
|
-
harnessDir = dirname4(
|
|
1458
|
+
harnessDir = dirname4(resolve5(docOrPath));
|
|
1297
1459
|
} catch (error) {
|
|
1298
1460
|
return {
|
|
1299
1461
|
ok: false,
|
|
@@ -1360,8 +1522,8 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1360
1522
|
for (const entry of doc.workflows) {
|
|
1361
1523
|
if (!isPlainObject4(entry) || typeof entry.dir !== "string")
|
|
1362
1524
|
continue;
|
|
1363
|
-
const relSnapshot =
|
|
1364
|
-
const snapshotPath =
|
|
1525
|
+
const relSnapshot = join7(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1526
|
+
const snapshotPath = join7(harnessDir, relSnapshot);
|
|
1365
1527
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
1366
1528
|
let physical;
|
|
1367
1529
|
try {
|
|
@@ -1395,8 +1557,10 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1395
1557
|
return { ok: violations.length === 0, violations };
|
|
1396
1558
|
}
|
|
1397
1559
|
var validateStatus = validateStatusV2;
|
|
1398
|
-
function registerWorkflowEntryLocked(statusPath, entry) {
|
|
1560
|
+
async function registerWorkflowEntryLocked(statusPath, entry) {
|
|
1399
1561
|
const harnessDir = dirname4(statusPath);
|
|
1562
|
+
const store = getArtifactStore();
|
|
1563
|
+
assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
|
|
1400
1564
|
const current = readJson(statusPath);
|
|
1401
1565
|
const fresh = Object.keys(current).length === 0;
|
|
1402
1566
|
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
@@ -1414,7 +1578,7 @@ function registerWorkflowEntryLocked(statusPath, entry) {
|
|
|
1414
1578
|
if (!gate.ok) {
|
|
1415
1579
|
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1416
1580
|
}
|
|
1417
|
-
|
|
1581
|
+
await store.put({ kind: "status", key: "root", payload: doc });
|
|
1418
1582
|
return doc;
|
|
1419
1583
|
}
|
|
1420
1584
|
async function registerWorkflow(root, entry) {
|
|
@@ -1422,16 +1586,18 @@ async function registerWorkflow(root, entry) {
|
|
|
1422
1586
|
if (!entryGate.ok) {
|
|
1423
1587
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
1424
1588
|
}
|
|
1425
|
-
const statusPath =
|
|
1589
|
+
const statusPath = resolve5(root);
|
|
1426
1590
|
return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
|
|
1427
1591
|
}
|
|
1428
1592
|
async function unregisterWorkflow(root, id) {
|
|
1429
1593
|
if (typeof id !== "string" || id.trim() === "") {
|
|
1430
1594
|
throw new Error("refusing to unregister workflow: id must be a non-empty string");
|
|
1431
1595
|
}
|
|
1432
|
-
const statusPath =
|
|
1596
|
+
const statusPath = resolve5(root);
|
|
1597
|
+
const store = getArtifactStore();
|
|
1598
|
+
assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
|
|
1433
1599
|
const harnessDir = dirname4(statusPath);
|
|
1434
|
-
return withStatusWriteLock(statusPath, () => {
|
|
1600
|
+
return withStatusWriteLock(statusPath, async () => {
|
|
1435
1601
|
const current = readJson(statusPath);
|
|
1436
1602
|
if (Object.keys(current).length === 0) {
|
|
1437
1603
|
return { version: 2, updated_at: todayString(), workflows: [] };
|
|
@@ -1450,25 +1616,25 @@ async function unregisterWorkflow(root, id) {
|
|
|
1450
1616
|
if (!gate.ok) {
|
|
1451
1617
|
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1452
1618
|
}
|
|
1453
|
-
|
|
1619
|
+
await store.put({ kind: "status", key: "root", payload: doc });
|
|
1454
1620
|
return doc;
|
|
1455
1621
|
});
|
|
1456
1622
|
}
|
|
1457
1623
|
function resolveCompassEnforcement(harnessDir) {
|
|
1458
1624
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
1459
|
-
if (!
|
|
1625
|
+
if (!existsSync4(iterationsDir))
|
|
1460
1626
|
return { hard: false, source: "none" };
|
|
1461
1627
|
let entries;
|
|
1462
1628
|
try {
|
|
1463
|
-
entries =
|
|
1629
|
+
entries = readdirSync3(iterationsDir, { withFileTypes: true });
|
|
1464
1630
|
} catch {
|
|
1465
1631
|
return { hard: false, source: "none" };
|
|
1466
1632
|
}
|
|
1467
1633
|
for (const entry of entries) {
|
|
1468
1634
|
if (!entry.isDirectory())
|
|
1469
1635
|
continue;
|
|
1470
|
-
const compassPath =
|
|
1471
|
-
if (!
|
|
1636
|
+
const compassPath = join7(iterationsDir, entry.name, "delivery-compass.md");
|
|
1637
|
+
if (!existsSync4(compassPath))
|
|
1472
1638
|
continue;
|
|
1473
1639
|
let content;
|
|
1474
1640
|
try {
|
|
@@ -1487,7 +1653,7 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1487
1653
|
return { hard: false, source: "none" };
|
|
1488
1654
|
}
|
|
1489
1655
|
function resolveMstarcEnforcement(harnessDir) {
|
|
1490
|
-
const dir =
|
|
1656
|
+
const dir = resolve5(harnessDir);
|
|
1491
1657
|
const rc = loadMstarc(dir, dirname4(dir));
|
|
1492
1658
|
const value = rc?.config.enforcement;
|
|
1493
1659
|
if (value === "hard")
|
|
@@ -1639,9 +1805,12 @@ async function appendProjectRegisterEntries(opts) {
|
|
|
1639
1805
|
if (opts.entries.length === 0) {
|
|
1640
1806
|
throw new Error("refusing to append residual entries: entries must not be empty");
|
|
1641
1807
|
}
|
|
1642
|
-
const registerPath =
|
|
1808
|
+
const registerPath = resolve6(join8(opts.projectDir, PROJECT_REGISTER_FILE));
|
|
1809
|
+
const projectKey = basename2(resolve6(opts.projectDir));
|
|
1810
|
+
const store = getArtifactStore();
|
|
1811
|
+
assertFsStorePath(store, { kind: "residuals", key: projectKey }, registerPath);
|
|
1643
1812
|
mkdirSync4(opts.projectDir, { recursive: true });
|
|
1644
|
-
return withStatusWriteLock(registerPath, () => {
|
|
1813
|
+
return withStatusWriteLock(registerPath, async () => {
|
|
1645
1814
|
const doc = readJson(registerPath);
|
|
1646
1815
|
const entriesMap = doc.entries ?? {};
|
|
1647
1816
|
let key = opts.basePlanKey;
|
|
@@ -1676,14 +1845,17 @@ async function appendProjectRegisterEntries(opts) {
|
|
|
1676
1845
|
if (!gate.ok) {
|
|
1677
1846
|
throw new Error(`refusing to write invalid project register: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1678
1847
|
}
|
|
1679
|
-
|
|
1848
|
+
await store.put({ kind: "residuals", key: projectKey, payload: register });
|
|
1680
1849
|
return { ok: true, key };
|
|
1681
1850
|
});
|
|
1682
1851
|
}
|
|
1683
1852
|
async function closeProjectRegisterEntry(opts) {
|
|
1684
|
-
const registerPath =
|
|
1853
|
+
const registerPath = resolve6(join8(opts.projectDir, PROJECT_REGISTER_FILE));
|
|
1854
|
+
const projectKey = basename2(resolve6(opts.projectDir));
|
|
1855
|
+
const store = getArtifactStore();
|
|
1856
|
+
assertFsStorePath(store, { kind: "residuals", key: projectKey }, registerPath);
|
|
1685
1857
|
mkdirSync4(opts.projectDir, { recursive: true });
|
|
1686
|
-
return withStatusWriteLock(registerPath, () => {
|
|
1858
|
+
return withStatusWriteLock(registerPath, async () => {
|
|
1687
1859
|
const doc = readJson(registerPath);
|
|
1688
1860
|
const planEntries = doc.entries?.[opts.planKey];
|
|
1689
1861
|
if (!Array.isArray(planEntries)) {
|
|
@@ -1703,7 +1875,7 @@ async function closeProjectRegisterEntry(opts) {
|
|
|
1703
1875
|
if (!gate.ok) {
|
|
1704
1876
|
throw new Error(`refusing to write invalid project register: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1705
1877
|
}
|
|
1706
|
-
|
|
1878
|
+
await store.put({ kind: "residuals", key: projectKey, payload: register });
|
|
1707
1879
|
return { ok: true };
|
|
1708
1880
|
});
|
|
1709
1881
|
}
|
|
@@ -1756,15 +1928,15 @@ function techDebtRollup(projectDir) {
|
|
|
1756
1928
|
const items = [];
|
|
1757
1929
|
let entries;
|
|
1758
1930
|
try {
|
|
1759
|
-
entries =
|
|
1931
|
+
entries = readdirSync4(projectDir, { withFileTypes: true });
|
|
1760
1932
|
} catch {
|
|
1761
1933
|
entries = [];
|
|
1762
1934
|
}
|
|
1763
1935
|
for (const project of entries) {
|
|
1764
1936
|
if (!project.isDirectory())
|
|
1765
1937
|
continue;
|
|
1766
|
-
const registerPath =
|
|
1767
|
-
if (!
|
|
1938
|
+
const registerPath = join8(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
1939
|
+
if (!existsSync5(registerPath))
|
|
1768
1940
|
continue;
|
|
1769
1941
|
let register;
|
|
1770
1942
|
try {
|
|
@@ -1800,10 +1972,10 @@ function techDebtRollup(projectDir) {
|
|
|
1800
1972
|
return { computed, stored, checks, overall };
|
|
1801
1973
|
}
|
|
1802
1974
|
function listProjectReferenceFiles(projectDir) {
|
|
1803
|
-
const root =
|
|
1975
|
+
const root = join8(projectDir, PROJECT_REFERENCES_DIR);
|
|
1804
1976
|
let entries;
|
|
1805
1977
|
try {
|
|
1806
|
-
entries =
|
|
1978
|
+
entries = readdirSync4(root, { withFileTypes: true });
|
|
1807
1979
|
} catch {
|
|
1808
1980
|
return [];
|
|
1809
1981
|
}
|
|
@@ -1816,7 +1988,7 @@ function listProjectReferenceFiles(projectDir) {
|
|
|
1816
1988
|
} else if (entry.isDirectory()) {
|
|
1817
1989
|
let nested;
|
|
1818
1990
|
try {
|
|
1819
|
-
nested =
|
|
1991
|
+
nested = readdirSync4(join8(root, entry.name), { withFileTypes: true });
|
|
1820
1992
|
} catch {
|
|
1821
1993
|
continue;
|
|
1822
1994
|
}
|
|
@@ -1831,19 +2003,19 @@ function listProjectReferenceFiles(projectDir) {
|
|
|
1831
2003
|
|
|
1832
2004
|
// src/path.ts
|
|
1833
2005
|
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
1834
|
-
const start =
|
|
2006
|
+
const start = resolve7(startDir);
|
|
1835
2007
|
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
1836
2008
|
if (explicit)
|
|
1837
|
-
return
|
|
1838
|
-
const boundary =
|
|
2009
|
+
return resolve7(start, explicit);
|
|
2010
|
+
const boundary = resolve7(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
1839
2011
|
const rc = loadMstarc(start, boundary);
|
|
1840
2012
|
if (rc !== null && rc.config.harnessDir)
|
|
1841
|
-
return
|
|
2013
|
+
return resolve7(rc.dir, rc.config.harnessDir);
|
|
1842
2014
|
let dir = start;
|
|
1843
2015
|
for (;; ) {
|
|
1844
2016
|
if (!isAtOrBelow2(dir, boundary))
|
|
1845
2017
|
return null;
|
|
1846
|
-
for (const candidate of [
|
|
2018
|
+
for (const candidate of [join9(dir, ".mstar"), join9(dir, ".agents"), join9(dir, ".plans"), join9(dir, "plans")]) {
|
|
1847
2019
|
if (isDirectory(candidate))
|
|
1848
2020
|
return candidate;
|
|
1849
2021
|
}
|
|
@@ -1869,19 +2041,19 @@ function defaultWorkspaceRoot(startDir) {
|
|
|
1869
2041
|
if (segment && segment !== ".")
|
|
1870
2042
|
boundary = dirname5(boundary);
|
|
1871
2043
|
}
|
|
1872
|
-
return
|
|
2044
|
+
return resolve7(boundary);
|
|
1873
2045
|
} catch {}
|
|
1874
2046
|
return startDir;
|
|
1875
2047
|
}
|
|
1876
2048
|
function isAtOrBelow2(dir, root) {
|
|
1877
2049
|
const rel = relative2(root, dir);
|
|
1878
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
2050
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
1879
2051
|
}
|
|
1880
2052
|
function mstarcDirOverride(harnessDir, key) {
|
|
1881
|
-
const dir =
|
|
2053
|
+
const dir = resolve7(harnessDir);
|
|
1882
2054
|
const rc = loadMstarc(dir, dirname5(dir));
|
|
1883
2055
|
const declared = rc?.config[key];
|
|
1884
|
-
return declared ?
|
|
2056
|
+
return declared ? resolve7(rc.dir, declared) : null;
|
|
1885
2057
|
}
|
|
1886
2058
|
function resolveSpecsDir(harnessDir, opts = {}) {
|
|
1887
2059
|
const declared = mstarcDirOverride(harnessDir, "specsDir");
|
|
@@ -1890,20 +2062,20 @@ function resolveSpecsDir(harnessDir, opts = {}) {
|
|
|
1890
2062
|
mkdirSync5(declared, { recursive: true });
|
|
1891
2063
|
return declared;
|
|
1892
2064
|
}
|
|
1893
|
-
const harness =
|
|
2065
|
+
const harness = resolve7(harnessDir);
|
|
1894
2066
|
const repoRoot = dirname5(harness);
|
|
1895
2067
|
const candidates = [
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2068
|
+
join9(harness, "specs"),
|
|
2069
|
+
join9(repoRoot, "docs", "specs"),
|
|
2070
|
+
join9(repoRoot, "specs"),
|
|
2071
|
+
join9(harness, "designs"),
|
|
2072
|
+
join9(repoRoot, "designs")
|
|
1901
2073
|
];
|
|
1902
2074
|
for (const candidate of candidates) {
|
|
1903
2075
|
if (isDirectory(candidate) && hasFiles(candidate))
|
|
1904
2076
|
return candidate;
|
|
1905
2077
|
}
|
|
1906
|
-
const fallback =
|
|
2078
|
+
const fallback = join9(harness, "specs");
|
|
1907
2079
|
if (opts.create !== false)
|
|
1908
2080
|
mkdirSync5(fallback, { recursive: true });
|
|
1909
2081
|
return fallback;
|
|
@@ -1912,11 +2084,11 @@ function resolvePlanDir(harnessDir) {
|
|
|
1912
2084
|
const declared = mstarcDirOverride(harnessDir, "planDir");
|
|
1913
2085
|
if (declared !== null)
|
|
1914
2086
|
return declared;
|
|
1915
|
-
const dir =
|
|
1916
|
-
const name =
|
|
2087
|
+
const dir = resolve7(harnessDir);
|
|
2088
|
+
const name = basename3(dir);
|
|
1917
2089
|
if (name === ".plans" || name === "plans")
|
|
1918
2090
|
return dir;
|
|
1919
|
-
return
|
|
2091
|
+
return join9(dir, "plans");
|
|
1920
2092
|
}
|
|
1921
2093
|
function assertSafePathComponent(value, what) {
|
|
1922
2094
|
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
@@ -1925,30 +2097,30 @@ function assertSafePathComponent(value, what) {
|
|
|
1925
2097
|
}
|
|
1926
2098
|
function resolveSddDir(harnessDir, planId) {
|
|
1927
2099
|
assertSafePathComponent(planId, "planId");
|
|
1928
|
-
const base =
|
|
2100
|
+
const base = resolve7(harnessDir);
|
|
1929
2101
|
const declared = mstarcDirOverride(base, "sddDir");
|
|
1930
|
-
const sddBase = declared !== null ? declared :
|
|
1931
|
-
return
|
|
2102
|
+
const sddBase = declared !== null ? declared : join9(base, "sdd");
|
|
2103
|
+
return join9(sddBase, planId);
|
|
1932
2104
|
}
|
|
1933
2105
|
function resolveIterationDir(harnessDir) {
|
|
1934
2106
|
const declared = mstarcDirOverride(harnessDir, "iterationDir");
|
|
1935
2107
|
if (declared !== null)
|
|
1936
2108
|
return declared;
|
|
1937
|
-
return
|
|
2109
|
+
return join9(resolve7(harnessDir), "iterations");
|
|
1938
2110
|
}
|
|
1939
2111
|
function resolveKnowledgeDir(harnessDir) {
|
|
1940
2112
|
const declared = mstarcDirOverride(harnessDir, "knowledgeDir");
|
|
1941
2113
|
if (declared !== null)
|
|
1942
2114
|
return declared;
|
|
1943
|
-
return
|
|
2115
|
+
return join9(resolve7(harnessDir), "knowledge");
|
|
1944
2116
|
}
|
|
1945
2117
|
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
1946
2118
|
const harness = resolveHarnessDir(startDir, opts);
|
|
1947
2119
|
if (harness === null) {
|
|
1948
|
-
throw new Error(`harness dir not found from ${
|
|
2120
|
+
throw new Error(`harness dir not found from ${resolve7(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
1949
2121
|
}
|
|
1950
2122
|
const declared = mstarcDirOverride(harness, key);
|
|
1951
|
-
return declared !== null ? declared :
|
|
2123
|
+
return declared !== null ? declared : join9(resolve7(harness), fallback);
|
|
1952
2124
|
}
|
|
1953
2125
|
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
1954
2126
|
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
@@ -1963,13 +2135,13 @@ var EMPTY_STATUS_TEMPLATE = {
|
|
|
1963
2135
|
};
|
|
1964
2136
|
var SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
|
|
1965
2137
|
function resolveScaffoldDirs(root) {
|
|
1966
|
-
const start =
|
|
1967
|
-
const boundary =
|
|
2138
|
+
const start = resolve7(root);
|
|
2139
|
+
const boundary = resolve7(start, defaultWorkspaceRoot(start));
|
|
1968
2140
|
const rc = loadMstarc(start, boundary);
|
|
1969
2141
|
const explicit = process.env.MSTAR_HARNESS_DIR;
|
|
1970
|
-
const harnessDir = explicit ?
|
|
2142
|
+
const harnessDir = explicit ? resolve7(start, explicit) : rc !== null && rc.config.harnessDir ? resolve7(rc.dir, rc.config.harnessDir) : join9(start, ".mstar");
|
|
1971
2143
|
const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
|
|
1972
|
-
const projectDir = declaredProjectDir !== null ? declaredProjectDir :
|
|
2144
|
+
const projectDir = declaredProjectDir !== null ? declaredProjectDir : join9(harnessDir, "projects");
|
|
1973
2145
|
return { harnessDir, projectDir };
|
|
1974
2146
|
}
|
|
1975
2147
|
var ROADMAP_TEMPLATE = `---
|
|
@@ -1991,18 +2163,18 @@ var EMPTY_REGISTER_TEMPLATE = {
|
|
|
1991
2163
|
function scaffoldHarness(root) {
|
|
1992
2164
|
const { harnessDir, projectDir } = resolveScaffoldDirs(root);
|
|
1993
2165
|
for (const dir of SCAFFOLD_DIRS)
|
|
1994
|
-
mkdirSync5(
|
|
1995
|
-
const statusPath =
|
|
2166
|
+
mkdirSync5(join9(harnessDir, dir), { recursive: true });
|
|
2167
|
+
const statusPath = join9(harnessDir, "status.json");
|
|
1996
2168
|
if (Object.keys(readJson(statusPath)).length === 0)
|
|
1997
2169
|
writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
|
|
1998
|
-
const defaultProjectDir =
|
|
2170
|
+
const defaultProjectDir = join9(projectDir, _DEFAULT_PROJECT);
|
|
1999
2171
|
mkdirSync5(defaultProjectDir, { recursive: true });
|
|
2000
|
-
const roadmapPath =
|
|
2001
|
-
if (!
|
|
2172
|
+
const roadmapPath = join9(defaultProjectDir, PROJECT_ROADMAP_FILE);
|
|
2173
|
+
if (!existsSync6(roadmapPath)) {
|
|
2002
2174
|
const created = new Date().toISOString().slice(0, 10);
|
|
2003
2175
|
writeFileSync3(roadmapPath, ROADMAP_TEMPLATE.replace("{created_at}", created), "utf8");
|
|
2004
2176
|
}
|
|
2005
|
-
const registerPath =
|
|
2177
|
+
const registerPath = join9(defaultProjectDir, PROJECT_REGISTER_FILE);
|
|
2006
2178
|
if (Object.keys(readJson(registerPath)).length === 0)
|
|
2007
2179
|
writeJson(registerPath, EMPTY_REGISTER_TEMPLATE);
|
|
2008
2180
|
return harnessDir;
|
|
@@ -2040,7 +2212,7 @@ function emitGitignoreSnippet(kind) {
|
|
|
2040
2212
|
return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
|
|
2041
2213
|
}
|
|
2042
2214
|
function validateGitignore(root) {
|
|
2043
|
-
const gitignorePath =
|
|
2215
|
+
const gitignorePath = join9(resolve7(root), ".gitignore");
|
|
2044
2216
|
const kind = detectHarnessKind(resolveHarnessDir(root));
|
|
2045
2217
|
let content;
|
|
2046
2218
|
try {
|
|
@@ -2088,7 +2260,7 @@ function validateGitignore(root) {
|
|
|
2088
2260
|
function detectHarnessKind(harnessDir) {
|
|
2089
2261
|
if (!harnessDir)
|
|
2090
2262
|
return null;
|
|
2091
|
-
const name =
|
|
2263
|
+
const name = basename3(resolve7(harnessDir));
|
|
2092
2264
|
if (name === ".mstar")
|
|
2093
2265
|
return "mstar";
|
|
2094
2266
|
if (name === ".agents")
|
|
@@ -2096,7 +2268,7 @@ function detectHarnessKind(harnessDir) {
|
|
|
2096
2268
|
return null;
|
|
2097
2269
|
}
|
|
2098
2270
|
function assertPlanWritingPath(planPath, harnessDir) {
|
|
2099
|
-
const planAbs =
|
|
2271
|
+
const planAbs = resolve7(planPath);
|
|
2100
2272
|
if (!harnessDir) {
|
|
2101
2273
|
return {
|
|
2102
2274
|
ok: false,
|
|
@@ -2108,7 +2280,7 @@ function assertPlanWritingPath(planPath, harnessDir) {
|
|
|
2108
2280
|
}
|
|
2109
2281
|
const planDir = resolvePlanDir(harnessDir);
|
|
2110
2282
|
const rel = relative2(planDir, planAbs);
|
|
2111
|
-
const inside = rel === "" || !rel.startsWith("..") && !
|
|
2283
|
+
const inside = rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
2112
2284
|
if (!inside) {
|
|
2113
2285
|
return {
|
|
2114
2286
|
ok: false,
|
|
@@ -2118,12 +2290,12 @@ function assertPlanWritingPath(planPath, harnessDir) {
|
|
|
2118
2290
|
fix: `write the plan under ${planDir}`
|
|
2119
2291
|
};
|
|
2120
2292
|
}
|
|
2121
|
-
if (
|
|
2293
|
+
if (existsSync6(planAbs)) {
|
|
2122
2294
|
try {
|
|
2123
2295
|
const canonicalPlan = realpathSync2(planAbs);
|
|
2124
|
-
const canonicalPlanDir =
|
|
2296
|
+
const canonicalPlanDir = existsSync6(planDir) ? realpathSync2(planDir) : resolve7(planDir);
|
|
2125
2297
|
const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
|
|
2126
|
-
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !
|
|
2298
|
+
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute4(canonicalRel);
|
|
2127
2299
|
if (!canonicalInside) {
|
|
2128
2300
|
return {
|
|
2129
2301
|
ok: false,
|
|
@@ -2151,9 +2323,9 @@ function isDirectory(dir) {
|
|
|
2151
2323
|
}
|
|
2152
2324
|
function hasFiles(dir) {
|
|
2153
2325
|
try {
|
|
2154
|
-
for (const entry of
|
|
2326
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2155
2327
|
if (entry.isDirectory()) {
|
|
2156
|
-
if (hasFiles(
|
|
2328
|
+
if (hasFiles(join9(dir, entry.name)))
|
|
2157
2329
|
return true;
|
|
2158
2330
|
} else if (entry.isFile()) {
|
|
2159
2331
|
return true;
|
|
@@ -2166,8 +2338,8 @@ function hasFiles(dir) {
|
|
|
2166
2338
|
}
|
|
2167
2339
|
// src/worktree.ts
|
|
2168
2340
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2169
|
-
import { existsSync as
|
|
2170
|
-
import { isAbsolute as
|
|
2341
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2342
|
+
import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
|
|
2171
2343
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
2172
2344
|
function probeTimeoutMs() {
|
|
2173
2345
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
@@ -2218,10 +2390,10 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
2218
2390
|
if (leaseWorkingBranch.trim() === "") {
|
|
2219
2391
|
violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
|
|
2220
2392
|
}
|
|
2221
|
-
if (controlWorktreePath !== "" && leaseWorktreePath !== "" &&
|
|
2393
|
+
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(leaseWorktreePath)) {
|
|
2222
2394
|
violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
|
|
2223
2395
|
}
|
|
2224
|
-
if (leaseWorktreePath !== "" && !
|
|
2396
|
+
if (leaseWorktreePath !== "" && !existsSync7(leaseWorktreePath)) {
|
|
2225
2397
|
violations.push(violation7("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
|
|
2226
2398
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
2227
2399
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
@@ -2245,17 +2417,17 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2245
2417
|
violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2246
2418
|
return;
|
|
2247
2419
|
}
|
|
2248
|
-
if (!
|
|
2420
|
+
if (!isAbsolute5(track.worktreePath)) {
|
|
2249
2421
|
violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
|
|
2250
2422
|
return;
|
|
2251
2423
|
}
|
|
2252
|
-
const normalized =
|
|
2424
|
+
const normalized = resolve8(track.worktreePath);
|
|
2253
2425
|
if (seenPaths.has(normalized)) {
|
|
2254
2426
|
violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
|
|
2255
2427
|
return;
|
|
2256
2428
|
}
|
|
2257
2429
|
seenPaths.add(normalized);
|
|
2258
|
-
if (!
|
|
2430
|
+
if (!existsSync7(track.worktreePath)) {
|
|
2259
2431
|
violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
2260
2432
|
return;
|
|
2261
2433
|
}
|
|
@@ -2270,7 +2442,7 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2270
2442
|
}
|
|
2271
2443
|
function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
|
|
2272
2444
|
const violations = [];
|
|
2273
|
-
const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" &&
|
|
2445
|
+
const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(featureWorktreePath);
|
|
2274
2446
|
if (samePath) {
|
|
2275
2447
|
violations.push(violation7("critical", "worktree.control-feature.same", `control worktree path equals feature/lease worktree path "${controlWorktreePath}" — execution_lease.worktree_path MUST differ from metadata.control_worktree_path`, "use a distinct feature worktree for the plan's product edits"));
|
|
2276
2448
|
}
|
|
@@ -2318,8 +2490,8 @@ function singleReviewSnapshot(assignments) {
|
|
|
2318
2490
|
}
|
|
2319
2491
|
// src/sdd.ts
|
|
2320
2492
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2321
|
-
import { mkdirSync as mkdirSync6, readdirSync as
|
|
2322
|
-
import { basename as
|
|
2493
|
+
import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2494
|
+
import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "node:path";
|
|
2323
2495
|
class SddScriptError extends Error {
|
|
2324
2496
|
exitCode;
|
|
2325
2497
|
constructor(message, exitCode) {
|
|
@@ -2356,14 +2528,14 @@ function gitOut(cwd, args) {
|
|
|
2356
2528
|
}
|
|
2357
2529
|
}
|
|
2358
2530
|
function probeHarnessWithStatus(root) {
|
|
2359
|
-
if (isFile2(
|
|
2360
|
-
return
|
|
2361
|
-
if (isFile2(
|
|
2362
|
-
return
|
|
2363
|
-
if (hasWorkflowSnapshot(
|
|
2364
|
-
return
|
|
2365
|
-
if (hasWorkflowSnapshot(
|
|
2366
|
-
return
|
|
2531
|
+
if (isFile2(join10(root, ".mstar", "status.json")))
|
|
2532
|
+
return join10(root, ".mstar");
|
|
2533
|
+
if (isFile2(join10(root, ".agents", "status.json")))
|
|
2534
|
+
return join10(root, ".agents");
|
|
2535
|
+
if (hasWorkflowSnapshot(join10(root, ".mstar")))
|
|
2536
|
+
return join10(root, ".mstar");
|
|
2537
|
+
if (hasWorkflowSnapshot(join10(root, ".agents")))
|
|
2538
|
+
return join10(root, ".agents");
|
|
2367
2539
|
return null;
|
|
2368
2540
|
}
|
|
2369
2541
|
function hasWorkflowSnapshot(harnessDir) {
|
|
@@ -2371,13 +2543,13 @@ function hasWorkflowSnapshot(harnessDir) {
|
|
|
2371
2543
|
try {
|
|
2372
2544
|
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2373
2545
|
} catch {
|
|
2374
|
-
workflowsDir =
|
|
2546
|
+
workflowsDir = join10(harnessDir, "workflows");
|
|
2375
2547
|
}
|
|
2376
2548
|
if (!isDirectory2(workflowsDir))
|
|
2377
2549
|
return false;
|
|
2378
2550
|
try {
|
|
2379
|
-
for (const entry of
|
|
2380
|
-
if (entry.isDirectory() && isFile2(
|
|
2551
|
+
for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
|
|
2552
|
+
if (entry.isDirectory() && isFile2(join10(workflowsDir, entry.name, "snapshot.json")))
|
|
2381
2553
|
return true;
|
|
2382
2554
|
}
|
|
2383
2555
|
} catch {
|
|
@@ -2390,14 +2562,14 @@ function isLinkedWorktree(root) {
|
|
|
2390
2562
|
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2391
2563
|
if (gitDirRaw === null || commonRaw === null)
|
|
2392
2564
|
return false;
|
|
2393
|
-
const gitDir =
|
|
2394
|
-
const common =
|
|
2565
|
+
const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw : join10(root, gitDirRaw);
|
|
2566
|
+
const common = isAbsolute6(commonRaw) ? commonRaw : join10(root, commonRaw);
|
|
2395
2567
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2396
2568
|
return true;
|
|
2397
2569
|
try {
|
|
2398
2570
|
const gdParent = realpathSync3(dirname6(gitDir));
|
|
2399
2571
|
const cmAbs = realpathSync3(common);
|
|
2400
|
-
return
|
|
2572
|
+
return join10(gdParent, basename4(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2401
2573
|
} catch {
|
|
2402
2574
|
return false;
|
|
2403
2575
|
}
|
|
@@ -2428,28 +2600,28 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2428
2600
|
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2429
2601
|
let harnessDir;
|
|
2430
2602
|
if (harnessOverride) {
|
|
2431
|
-
harnessDir =
|
|
2603
|
+
harnessDir = resolve9(root, harnessOverride);
|
|
2432
2604
|
} else {
|
|
2433
2605
|
const rc = findMstarc(root, root);
|
|
2434
2606
|
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
|
|
2435
2607
|
if (rcHarnessDir) {
|
|
2436
|
-
harnessDir =
|
|
2608
|
+
harnessDir = resolve9(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
2437
2609
|
} else {
|
|
2438
2610
|
const probed = probeHarnessWithStatus(root);
|
|
2439
2611
|
if (probed) {
|
|
2440
2612
|
harnessDir = probed;
|
|
2441
|
-
} else if (isDirectory2(
|
|
2442
|
-
harnessDir =
|
|
2443
|
-
} else if (isDirectory2(
|
|
2444
|
-
harnessDir =
|
|
2613
|
+
} else if (isDirectory2(join10(root, ".mstar"))) {
|
|
2614
|
+
harnessDir = join10(root, ".mstar");
|
|
2615
|
+
} else if (isDirectory2(join10(root, ".agents"))) {
|
|
2616
|
+
harnessDir = join10(root, ".agents");
|
|
2445
2617
|
} else {
|
|
2446
|
-
harnessDir =
|
|
2618
|
+
harnessDir = join10(root, ".mstar");
|
|
2447
2619
|
}
|
|
2448
2620
|
}
|
|
2449
2621
|
}
|
|
2450
2622
|
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2451
2623
|
mkdirSync6(sddDir, { recursive: true });
|
|
2452
|
-
writeFileSync4(
|
|
2624
|
+
writeFileSync4(join10(sddDir, ".gitignore"), `*
|
|
2453
2625
|
`);
|
|
2454
2626
|
return realpathSync3(sddDir);
|
|
2455
2627
|
}
|
|
@@ -2472,7 +2644,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2472
2644
|
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2473
2645
|
}
|
|
2474
2646
|
mkdirSync6(sddDir, { recursive: true });
|
|
2475
|
-
out =
|
|
2647
|
+
out = join10(sddDir, `task-${taskN}-brief.md`);
|
|
2476
2648
|
}
|
|
2477
2649
|
const records = content.endsWith(`
|
|
2478
2650
|
`) ? content.split(`
|
|
@@ -2525,7 +2697,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
|
|
|
2525
2697
|
mkdirSync6(sddDir, { recursive: true });
|
|
2526
2698
|
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
2527
2699
|
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
2528
|
-
out =
|
|
2700
|
+
out = join10(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
2529
2701
|
}
|
|
2530
2702
|
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
2531
2703
|
const parts = [
|
|
@@ -2561,7 +2733,7 @@ function assertBaseSha(ref, opts = {}) {
|
|
|
2561
2733
|
}
|
|
2562
2734
|
function taskReportExists(sddDir, taskN) {
|
|
2563
2735
|
try {
|
|
2564
|
-
const st = statSync4(
|
|
2736
|
+
const st = statSync4(join10(sddDir, `task-${taskN}-report.md`));
|
|
2565
2737
|
return st.isFile() && st.size > 0;
|
|
2566
2738
|
} catch {
|
|
2567
2739
|
return false;
|
|
@@ -2570,7 +2742,7 @@ function taskReportExists(sddDir, taskN) {
|
|
|
2570
2742
|
function readProgressLedger(sddDir) {
|
|
2571
2743
|
let content;
|
|
2572
2744
|
try {
|
|
2573
|
-
content = readFileSync7(
|
|
2745
|
+
content = readFileSync7(join10(sddDir, "progress.md"), "utf8");
|
|
2574
2746
|
} catch {
|
|
2575
2747
|
return [];
|
|
2576
2748
|
}
|
|
@@ -2603,8 +2775,8 @@ function implementerSessionStickyRules(input) {
|
|
|
2603
2775
|
return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
|
|
2604
2776
|
}
|
|
2605
2777
|
// src/migrate.ts
|
|
2606
|
-
import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as
|
|
2607
|
-
import { dirname as dirname7, isAbsolute as
|
|
2778
|
+
import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2779
|
+
import { dirname as dirname7, isAbsolute as isAbsolute7, join as join11, relative as relative3, resolve as resolve10, sep as sep2 } from "node:path";
|
|
2608
2780
|
var MIGRATE_STATUS_FILE = "status.json";
|
|
2609
2781
|
var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
|
|
2610
2782
|
var NOTES_LEDGER_FILE = "notes.jsonl";
|
|
@@ -2644,18 +2816,18 @@ function todayString3() {
|
|
|
2644
2816
|
return `${now.getFullYear()}-${month}-${day}`;
|
|
2645
2817
|
}
|
|
2646
2818
|
function scanCompasses(harnessDir) {
|
|
2647
|
-
const iterationsDir =
|
|
2819
|
+
const iterationsDir = join11(harnessDir, "iterations");
|
|
2648
2820
|
const out = [];
|
|
2649
2821
|
let entries;
|
|
2650
2822
|
try {
|
|
2651
|
-
entries =
|
|
2823
|
+
entries = readdirSync7(iterationsDir, { withFileTypes: true });
|
|
2652
2824
|
} catch {
|
|
2653
2825
|
return out;
|
|
2654
2826
|
}
|
|
2655
2827
|
for (const entry of entries) {
|
|
2656
2828
|
if (!entry.isDirectory())
|
|
2657
2829
|
continue;
|
|
2658
|
-
const compassPath =
|
|
2830
|
+
const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
|
|
2659
2831
|
let content;
|
|
2660
2832
|
try {
|
|
2661
2833
|
content = readFileSync8(compassPath, "utf8");
|
|
@@ -2735,8 +2907,8 @@ function buildIterationSnapshot(compass, rowById, rootUpdatedAt) {
|
|
|
2735
2907
|
id: compass.id,
|
|
2736
2908
|
type: "iteration",
|
|
2737
2909
|
status,
|
|
2738
|
-
file:
|
|
2739
|
-
source:
|
|
2910
|
+
file: join11("workflows", compass.id, WORKFLOW_SNAPSHOT_FILE),
|
|
2911
|
+
source: join11("iterations", compass.id, "delivery-compass.md"),
|
|
2740
2912
|
data: snapshot
|
|
2741
2913
|
};
|
|
2742
2914
|
}
|
|
@@ -2771,7 +2943,7 @@ function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes) {
|
|
|
2771
2943
|
id,
|
|
2772
2944
|
type: "plan",
|
|
2773
2945
|
status,
|
|
2774
|
-
file:
|
|
2946
|
+
file: join11("workflows", id, WORKFLOW_SNAPSHOT_FILE),
|
|
2775
2947
|
source: "status.json plans[] row",
|
|
2776
2948
|
data: snapshot
|
|
2777
2949
|
};
|
|
@@ -2879,7 +3051,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
|
|
|
2879
3051
|
return null;
|
|
2880
3052
|
const doc = { entries };
|
|
2881
3053
|
return {
|
|
2882
|
-
file:
|
|
3054
|
+
file: join11("projects", projectId, PROJECT_REGISTER_FILE),
|
|
2883
3055
|
source: "status.json residual_findings",
|
|
2884
3056
|
data: doc
|
|
2885
3057
|
};
|
|
@@ -2910,7 +3082,7 @@ function collectNotesFiles(snapshots) {
|
|
|
2910
3082
|
if (lines.length === 0)
|
|
2911
3083
|
continue;
|
|
2912
3084
|
out.push({
|
|
2913
|
-
file:
|
|
3085
|
+
file: join11(dirname7(snapshot.file), NOTES_LEDGER_FILE),
|
|
2914
3086
|
source,
|
|
2915
3087
|
lines
|
|
2916
3088
|
});
|
|
@@ -2918,11 +3090,11 @@ function collectNotesFiles(snapshots) {
|
|
|
2918
3090
|
return out;
|
|
2919
3091
|
}
|
|
2920
3092
|
function migrateHarnessTree(root, opts = {}) {
|
|
2921
|
-
const harnessDir =
|
|
3093
|
+
const harnessDir = resolve10(root);
|
|
2922
3094
|
const workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2923
3095
|
const projectDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
2924
3096
|
const projectId = opts.projectId ?? _DEFAULT_PROJECT;
|
|
2925
|
-
const statusPath =
|
|
3097
|
+
const statusPath = join11(harnessDir, MIGRATE_STATUS_FILE);
|
|
2926
3098
|
const legacy = readJson(statusPath);
|
|
2927
3099
|
if (legacy.version === 2) {
|
|
2928
3100
|
const updatedAt = typeof legacy.updated_at === "string" && legacy.updated_at !== "" ? legacy.updated_at : "1970-01-01";
|
|
@@ -3018,7 +3190,7 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
3018
3190
|
migrationNotes.push(`roadmap title sanitized for frontmatter (line breaks replaced with spaces): ${JSON.stringify(rawTitle)}`);
|
|
3019
3191
|
}
|
|
3020
3192
|
roadmap = {
|
|
3021
|
-
file:
|
|
3193
|
+
file: join11("projects", projectId, PROJECT_ROADMAP_FILE),
|
|
3022
3194
|
source: "status.json metadata.program_roadmap",
|
|
3023
3195
|
content: buildRoadmap({ ...programRoadmap, title: sanitizedTitle }, projectId, migratedAt)
|
|
3024
3196
|
};
|
|
@@ -3067,19 +3239,19 @@ async function applyMigratePlan(plan) {
|
|
|
3067
3239
|
if (plan.dryRun) {
|
|
3068
3240
|
return { applied: false, message: `dry-run: ${plan.steps.length} steps planned (source → destination), zero writes` };
|
|
3069
3241
|
}
|
|
3070
|
-
const statusPath =
|
|
3242
|
+
const statusPath = join11(plan.root, MIGRATE_STATUS_FILE);
|
|
3071
3243
|
const current = readJson(statusPath);
|
|
3072
3244
|
if (current.version === 2) {
|
|
3073
3245
|
return { applied: false, message: "no-op: status.json already at schema version 2 (migrated) — nothing to do" };
|
|
3074
3246
|
}
|
|
3075
|
-
const harnessRoot =
|
|
3076
|
-
const workflowRoot =
|
|
3077
|
-
const projectRoot =
|
|
3078
|
-
if (!
|
|
3247
|
+
const harnessRoot = resolve10(plan.root);
|
|
3248
|
+
const workflowRoot = resolve10(plan.workflowDir);
|
|
3249
|
+
const projectRoot = resolve10(plan.projectDir);
|
|
3250
|
+
if (!isAbsolute7(plan.workflowDir) || !isAbsolute7(plan.projectDir)) {
|
|
3079
3251
|
throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
|
|
3080
3252
|
}
|
|
3081
|
-
const workflowTargetOf = (canonicalFile) =>
|
|
3082
|
-
const projectTargetOf = (canonicalFile) =>
|
|
3253
|
+
const workflowTargetOf = (canonicalFile) => join11(workflowRoot, relative3("workflows", canonicalFile));
|
|
3254
|
+
const projectTargetOf = (canonicalFile) => join11(projectRoot, relative3("projects", canonicalFile));
|
|
3083
3255
|
const allDestinations = [
|
|
3084
3256
|
plan.archive.file,
|
|
3085
3257
|
...plan.snapshots.map((snapshot) => snapshot.file),
|
|
@@ -3088,14 +3260,14 @@ async function applyMigratePlan(plan) {
|
|
|
3088
3260
|
...plan.roadmap !== null ? [plan.roadmap.file] : []
|
|
3089
3261
|
];
|
|
3090
3262
|
for (const destination of allDestinations) {
|
|
3091
|
-
const resolvedDest =
|
|
3263
|
+
const resolvedDest = resolve10(join11(plan.root, destination));
|
|
3092
3264
|
const inside = (dir) => resolvedDest === dir || resolvedDest.startsWith(`${dir}${sep2}`);
|
|
3093
3265
|
if (!inside(harnessRoot) && !inside(workflowRoot) && !inside(projectRoot)) {
|
|
3094
3266
|
throw new Error(`refusing to apply migration: destination escapes the harness dir (${JSON.stringify(destination)}) — every write must stay under ${JSON.stringify(plan.root)}, the workflow dir (${JSON.stringify(plan.workflowDir)}) or the project dir (${JSON.stringify(plan.projectDir)})`);
|
|
3095
3267
|
}
|
|
3096
3268
|
}
|
|
3097
|
-
mkdirSync7(
|
|
3098
|
-
copyFileSync(statusPath,
|
|
3269
|
+
mkdirSync7(join11(plan.root, dirname7(plan.archive.file)), { recursive: true });
|
|
3270
|
+
copyFileSync(statusPath, join11(plan.root, plan.archive.file));
|
|
3099
3271
|
for (const snapshot of plan.snapshots) {
|
|
3100
3272
|
await writeWorkflowSnapshot(snapshot.data, dirname7(workflowTargetOf(snapshot.file)));
|
|
3101
3273
|
}
|
|
@@ -3544,8 +3716,8 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
3544
3716
|
}
|
|
3545
3717
|
// src/audit.ts
|
|
3546
3718
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3547
|
-
import { existsSync as
|
|
3548
|
-
import { basename as
|
|
3719
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readdirSync as readdirSync8, readFileSync as readFileSync9, rmdirSync as rmdirSync2, rmSync, writeFileSync as writeFileSync6 } from "node:fs";
|
|
3720
|
+
import { basename as basename5, join as join12, resolve as resolve11, sep as sep3 } from "node:path";
|
|
3549
3721
|
function violation9(severity, code, message, fix) {
|
|
3550
3722
|
return { ok: false, severity, code, message, fix };
|
|
3551
3723
|
}
|
|
@@ -3846,7 +4018,7 @@ function scanSecrets(files) {
|
|
|
3846
4018
|
unreadableFiles++;
|
|
3847
4019
|
continue;
|
|
3848
4020
|
}
|
|
3849
|
-
const base =
|
|
4021
|
+
const base = basename5(file);
|
|
3850
4022
|
for (const entry of NEVER_COMMIT_FILENAMES) {
|
|
3851
4023
|
if (entry.re.test(base))
|
|
3852
4024
|
findings.push({ file, line: 1, type: entry.type });
|
|
@@ -3898,12 +4070,12 @@ var LOCKFILE_NAMES = [
|
|
|
3898
4070
|
function rootLockfiles(root) {
|
|
3899
4071
|
let entries;
|
|
3900
4072
|
try {
|
|
3901
|
-
entries =
|
|
4073
|
+
entries = readdirSync8(root, { withFileTypes: true });
|
|
3902
4074
|
} catch {
|
|
3903
4075
|
return [];
|
|
3904
4076
|
}
|
|
3905
4077
|
const names = new Set(LOCKFILE_NAMES);
|
|
3906
|
-
const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) =>
|
|
4078
|
+
const present = entries.filter((entry) => entry.isFile() && names.has(entry.name)).map((entry) => join12(root, entry.name));
|
|
3907
4079
|
if (present.length === 0)
|
|
3908
4080
|
return [];
|
|
3909
4081
|
try {
|
|
@@ -3912,7 +4084,7 @@ function rootLockfiles(root) {
|
|
|
3912
4084
|
encoding: "utf8",
|
|
3913
4085
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3914
4086
|
}).split("\x00").filter((f) => f !== ""));
|
|
3915
|
-
return present.filter((p) => tracked.has(
|
|
4087
|
+
return present.filter((p) => tracked.has(basename5(p)));
|
|
3916
4088
|
} catch {
|
|
3917
4089
|
return present;
|
|
3918
4090
|
}
|
|
@@ -3928,17 +4100,17 @@ function supplyChainChecks(repoRoot) {
|
|
|
3928
4100
|
findings.push({ kind: "lockfile-duplicate", file: lockfiles.map((f) => f.replace(`${repoRoot}/`, "")).join(", ") });
|
|
3929
4101
|
violations.push(violation9("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
|
|
3930
4102
|
}
|
|
3931
|
-
const workflowsDir =
|
|
4103
|
+
const workflowsDir = join12(repoRoot, ".github", "workflows");
|
|
3932
4104
|
let wfEntries = [];
|
|
3933
4105
|
try {
|
|
3934
|
-
wfEntries =
|
|
4106
|
+
wfEntries = readdirSync8(workflowsDir, { withFileTypes: true });
|
|
3935
4107
|
} catch {
|
|
3936
4108
|
wfEntries = [];
|
|
3937
4109
|
}
|
|
3938
4110
|
for (const entry of wfEntries) {
|
|
3939
4111
|
if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
|
|
3940
4112
|
continue;
|
|
3941
|
-
const wfPath =
|
|
4113
|
+
const wfPath = join12(workflowsDir, entry.name);
|
|
3942
4114
|
const relPath = `.github/workflows/${entry.name}`;
|
|
3943
4115
|
let text;
|
|
3944
4116
|
try {
|
|
@@ -4095,9 +4267,9 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4095
4267
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
4096
4268
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
4097
4269
|
mkdirSync8(outDir, { recursive: true });
|
|
4098
|
-
const existingReadme =
|
|
4099
|
-
const carried =
|
|
4100
|
-
const existing =
|
|
4270
|
+
const existingReadme = join12(outDir, "README.md");
|
|
4271
|
+
const carried = existsSync8(existingReadme) ? extractSecurityDispositionSections(readFileSync9(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
4272
|
+
const existing = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f));
|
|
4101
4273
|
let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
|
|
4102
4274
|
const redactedFindings = findings.map(redactFinding);
|
|
4103
4275
|
const written = [];
|
|
@@ -4113,13 +4285,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4113
4285
|
}
|
|
4114
4286
|
usedSlugs.add(slug);
|
|
4115
4287
|
const file = `${num}-${slug}.md`;
|
|
4116
|
-
writeFileSync6(
|
|
4288
|
+
writeFileSync6(join12(outDir, file), renderPlanFile(finding, plannedAt));
|
|
4117
4289
|
written.push(file);
|
|
4118
4290
|
next++;
|
|
4119
4291
|
}
|
|
4120
4292
|
const all = [...existing, ...written].sort();
|
|
4121
4293
|
const rows = all.map((file) => {
|
|
4122
|
-
const summary = readPlanFileSummary(
|
|
4294
|
+
const summary = readPlanFileSummary(join12(outDir, file));
|
|
4123
4295
|
const fields = summary.fields;
|
|
4124
4296
|
return {
|
|
4125
4297
|
num: file.slice(0, 3),
|
|
@@ -4153,7 +4325,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4153
4325
|
});
|
|
4154
4326
|
const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
|
|
4155
4327
|
const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
|
|
4156
|
-
writeFileSync6(
|
|
4328
|
+
writeFileSync6(join12(outDir, "README.md"), renderIndex({
|
|
4157
4329
|
date,
|
|
4158
4330
|
repoName: options.repoName ?? "repo",
|
|
4159
4331
|
repoShortSha: options.repoShortSha ?? "unknown",
|
|
@@ -4162,7 +4334,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4162
4334
|
needsVerification: needsVerificationLines,
|
|
4163
4335
|
hardeningChecked: hardeningCheckedLines
|
|
4164
4336
|
}));
|
|
4165
|
-
return { outDir:
|
|
4337
|
+
return { outDir: resolve11(outDir), date, files: written, nextNumber: next };
|
|
4166
4338
|
}
|
|
4167
4339
|
async function promoteAuditPlans(outDir, selected, options) {
|
|
4168
4340
|
if (selected.length === 0) {
|
|
@@ -4171,19 +4343,19 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4171
4343
|
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
4172
4344
|
throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
|
|
4173
4345
|
}
|
|
4174
|
-
const workflowId = options.workflowId ??
|
|
4346
|
+
const workflowId = options.workflowId ?? basename5(resolve11(outDir));
|
|
4175
4347
|
assertSafePathComponent(workflowId, "workflow id");
|
|
4176
|
-
const harnessDir =
|
|
4177
|
-
const statusPath =
|
|
4178
|
-
const workflowDir =
|
|
4179
|
-
const snapshotPath =
|
|
4348
|
+
const harnessDir = resolve11(options.harnessDir);
|
|
4349
|
+
const statusPath = join12(harnessDir, "status.json");
|
|
4350
|
+
const workflowDir = join12(harnessDir, "workflows", workflowId);
|
|
4351
|
+
const snapshotPath = join12(workflowDir, WORKFLOW_SNAPSHOT_FILE);
|
|
4180
4352
|
const planFiles = resolveSelectedPlanFiles(outDir, selected);
|
|
4181
4353
|
const indexRows = readExecutionOrderIndex(outDir);
|
|
4182
4354
|
const plans = planFiles.map((planFile) => {
|
|
4183
4355
|
const stem = planFile.replace(/\.md$/, "");
|
|
4184
4356
|
const num = stem.slice(0, 3);
|
|
4185
4357
|
const indexRow = indexRows.get(num);
|
|
4186
|
-
const title = indexRow?.title ?? readPlanFileSummary(
|
|
4358
|
+
const title = indexRow?.title ?? readPlanFileSummary(join12(outDir, planFile)).title;
|
|
4187
4359
|
return {
|
|
4188
4360
|
id: stem,
|
|
4189
4361
|
title,
|
|
@@ -4211,18 +4383,18 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4211
4383
|
if (!entryGate.ok) {
|
|
4212
4384
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
4213
4385
|
}
|
|
4214
|
-
await withStatusWriteLock(statusPath, () => {
|
|
4215
|
-
if (
|
|
4386
|
+
await withStatusWriteLock(statusPath, async () => {
|
|
4387
|
+
if (existsSync8(snapshotPath)) {
|
|
4216
4388
|
throw new Error(`refusing to promote audit plans: workflow ${JSON.stringify(workflowId)} already exists ` + `(snapshot at ${snapshotPath}) — re-promote would drop its registered plan rows; ` + `remove that workflow before promoting again`);
|
|
4217
4389
|
}
|
|
4218
4390
|
mkdirSync8(workflowDir, { recursive: true });
|
|
4219
4391
|
try {
|
|
4220
4392
|
writeJson(snapshotPath, snapshot);
|
|
4221
|
-
registerWorkflowEntryLocked(statusPath, entry);
|
|
4393
|
+
await registerWorkflowEntryLocked(statusPath, entry);
|
|
4222
4394
|
} catch (error) {
|
|
4223
4395
|
rmSync(snapshotPath, { force: true });
|
|
4224
4396
|
try {
|
|
4225
|
-
if (
|
|
4397
|
+
if (readdirSync8(workflowDir).length === 0) {
|
|
4226
4398
|
rmdirSync2(workflowDir);
|
|
4227
4399
|
}
|
|
4228
4400
|
} catch {}
|
|
@@ -4233,7 +4405,7 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4233
4405
|
return { workflowId, snapshotPath };
|
|
4234
4406
|
}
|
|
4235
4407
|
function resolveSelectedPlanFiles(outDir, selected) {
|
|
4236
|
-
const files =
|
|
4408
|
+
const files = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
|
|
4237
4409
|
const byNum = new Map;
|
|
4238
4410
|
const byStem = new Map;
|
|
4239
4411
|
for (const file of files) {
|
|
@@ -4248,7 +4420,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
4248
4420
|
for (const id of selected) {
|
|
4249
4421
|
const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
|
|
4250
4422
|
if (file === undefined) {
|
|
4251
|
-
throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${
|
|
4423
|
+
throw new Error(`promoteAuditPlans: selected plan ${JSON.stringify(id)} does not match any NNN-*.md file in ${resolve11(outDir)}`);
|
|
4252
4424
|
}
|
|
4253
4425
|
if (!seen.has(file)) {
|
|
4254
4426
|
seen.add(file);
|
|
@@ -4258,7 +4430,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
4258
4430
|
return resolved;
|
|
4259
4431
|
}
|
|
4260
4432
|
function readExecutionOrderIndex(outDir) {
|
|
4261
|
-
const readmePath =
|
|
4433
|
+
const readmePath = join12(outDir, "README.md");
|
|
4262
4434
|
let text;
|
|
4263
4435
|
try {
|
|
4264
4436
|
text = readFileSync9(readmePath, "utf8");
|
|
@@ -4287,7 +4459,7 @@ function readExecutionOrderIndex(outDir) {
|
|
|
4287
4459
|
return rows;
|
|
4288
4460
|
}
|
|
4289
4461
|
function planFileRel(outDir, planFile) {
|
|
4290
|
-
const resolved =
|
|
4462
|
+
const resolved = resolve11(outDir);
|
|
4291
4463
|
const parts = resolved.split(sep3);
|
|
4292
4464
|
const plansIdx = parts.lastIndexOf("plans");
|
|
4293
4465
|
if (plansIdx >= 0) {
|
|
@@ -4296,8 +4468,8 @@ function planFileRel(outDir, planFile) {
|
|
|
4296
4468
|
return planFile;
|
|
4297
4469
|
}
|
|
4298
4470
|
// src/compound.ts
|
|
4299
|
-
import { existsSync as
|
|
4300
|
-
import { basename as
|
|
4471
|
+
import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync10 } from "node:fs";
|
|
4472
|
+
import { basename as basename6, isAbsolute as isAbsolute8, join as join13, relative as relative4, resolve as resolve12, sep as sep4 } from "node:path";
|
|
4301
4473
|
function violation10(severity, code, message, fix) {
|
|
4302
4474
|
return { ok: false, severity, code, message, fix };
|
|
4303
4475
|
}
|
|
@@ -4564,7 +4736,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4564
4736
|
if (ref === "" || seen.has(ref))
|
|
4565
4737
|
continue;
|
|
4566
4738
|
seen.add(ref);
|
|
4567
|
-
if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") ||
|
|
4739
|
+
if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute8(ref)) {
|
|
4568
4740
|
continue;
|
|
4569
4741
|
}
|
|
4570
4742
|
if (ref.includes("/") || REF_EXT_RE.test(ref)) {
|
|
@@ -4583,7 +4755,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4583
4755
|
const dir = stack.pop();
|
|
4584
4756
|
let entries;
|
|
4585
4757
|
try {
|
|
4586
|
-
entries =
|
|
4758
|
+
entries = readdirSync9(dir, { withFileTypes: true });
|
|
4587
4759
|
} catch {
|
|
4588
4760
|
continue;
|
|
4589
4761
|
}
|
|
@@ -4592,7 +4764,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4592
4764
|
break;
|
|
4593
4765
|
if (entry.isDirectory()) {
|
|
4594
4766
|
if (!WALK_SKIP_DIRS.has(entry.name))
|
|
4595
|
-
stack.push(
|
|
4767
|
+
stack.push(join13(dir, entry.name));
|
|
4596
4768
|
} else if (!entry.isSymbolicLink()) {
|
|
4597
4769
|
const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
4598
4770
|
if (moduleNames.has(base))
|
|
@@ -4604,7 +4776,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4604
4776
|
for (const { ref, isSymbol, module } of refs) {
|
|
4605
4777
|
if (!isSymbol || module === undefined) {
|
|
4606
4778
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
4607
|
-
if (
|
|
4779
|
+
if (existsSync9(resolve12(repoRoot, candidate))) {
|
|
4608
4780
|
checked++;
|
|
4609
4781
|
} else {
|
|
4610
4782
|
violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
|
|
@@ -4624,14 +4796,14 @@ function collectKnowledgeDocs(dir) {
|
|
|
4624
4796
|
const current = stack.pop();
|
|
4625
4797
|
let entries;
|
|
4626
4798
|
try {
|
|
4627
|
-
entries =
|
|
4799
|
+
entries = readdirSync9(current, { withFileTypes: true });
|
|
4628
4800
|
} catch {
|
|
4629
4801
|
continue;
|
|
4630
4802
|
}
|
|
4631
4803
|
for (const entry of entries) {
|
|
4632
4804
|
if (entry.isSymbolicLink())
|
|
4633
4805
|
continue;
|
|
4634
|
-
const full =
|
|
4806
|
+
const full = join13(current, entry.name);
|
|
4635
4807
|
if (entry.isDirectory()) {
|
|
4636
4808
|
stack.push(full);
|
|
4637
4809
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
@@ -4651,8 +4823,8 @@ function normalizeIndexRef(cell) {
|
|
|
4651
4823
|
}
|
|
4652
4824
|
function assertIndexRows(knowledgeDir) {
|
|
4653
4825
|
const violations = [];
|
|
4654
|
-
const readmePath =
|
|
4655
|
-
if (!
|
|
4826
|
+
const readmePath = join13(knowledgeDir, "README.md");
|
|
4827
|
+
if (!existsSync9(readmePath)) {
|
|
4656
4828
|
violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
|
|
4657
4829
|
return { ok: false, violations };
|
|
4658
4830
|
}
|
|
@@ -4677,19 +4849,19 @@ function assertIndexRows(knowledgeDir) {
|
|
|
4677
4849
|
}
|
|
4678
4850
|
function compoundRefreshScope(harnessDir, projectRoot) {
|
|
4679
4851
|
return [
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4852
|
+
join13(harnessDir, "knowledge"),
|
|
4853
|
+
join13(harnessDir, "knowledge", "README.md"),
|
|
4854
|
+
join13(projectRoot, "CONCEPTS.md"),
|
|
4855
|
+
join13(harnessDir, "status.json")
|
|
4684
4856
|
];
|
|
4685
4857
|
}
|
|
4686
4858
|
function isFileLikeRoot(root) {
|
|
4687
|
-
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(
|
|
4859
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename6(root));
|
|
4688
4860
|
}
|
|
4689
4861
|
function scopeGuard(path, allowedRoots) {
|
|
4690
|
-
const resolved =
|
|
4862
|
+
const resolved = resolve12(path);
|
|
4691
4863
|
for (const root of allowedRoots) {
|
|
4692
|
-
const r =
|
|
4864
|
+
const r = resolve12(root);
|
|
4693
4865
|
if (isFileLikeRoot(r)) {
|
|
4694
4866
|
if (resolved === r)
|
|
4695
4867
|
return { ok: true, violations: [] };
|
|
@@ -4937,8 +5109,8 @@ function lintStrategySections(docText) {
|
|
|
4937
5109
|
return { ok: violations.length === 0, violations };
|
|
4938
5110
|
}
|
|
4939
5111
|
// src/roles.ts
|
|
4940
|
-
import { existsSync as
|
|
4941
|
-
import { join as
|
|
5112
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
5113
|
+
import { join as join14 } from "node:path";
|
|
4942
5114
|
function violation12(severity, code, message, fix) {
|
|
4943
5115
|
return { ok: false, severity, code, message, fix };
|
|
4944
5116
|
}
|
|
@@ -4994,8 +5166,8 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
4994
5166
|
const violations = [];
|
|
4995
5167
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
4996
5168
|
for (const { agentId, reference } of mapping) {
|
|
4997
|
-
if (!
|
|
4998
|
-
violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${
|
|
5169
|
+
if (!existsSync10(join14(rolesDir, reference))) {
|
|
5170
|
+
violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join14(rolesDir, reference)} or fix the mapping row`));
|
|
4999
5171
|
}
|
|
5000
5172
|
}
|
|
5001
5173
|
for (const { family, memberIds } of families) {
|
|
@@ -5200,8 +5372,8 @@ function resolveAssetPath(skillName, relPath, host) {
|
|
|
5200
5372
|
return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
|
|
5201
5373
|
}
|
|
5202
5374
|
// src/prreview.ts
|
|
5203
|
-
import { readdirSync as
|
|
5204
|
-
import { isAbsolute as
|
|
5375
|
+
import { readdirSync as readdirSync10 } from "node:fs";
|
|
5376
|
+
import { isAbsolute as isAbsolute9, join as join15 } from "node:path";
|
|
5205
5377
|
var MERGE_CLASSES = ["must-fix", "should-fix", "nit"];
|
|
5206
5378
|
var PR_VERDICTS = ["ship it", "needs fixes", "blocked"];
|
|
5207
5379
|
var REVIEW_EMOJI = {
|
|
@@ -5238,6 +5410,162 @@ function computePrTally(input) {
|
|
|
5238
5410
|
` + `must-fix=${mustFix} should-fix=${shouldFix} nit=${nit} unverified=${unverified}`;
|
|
5239
5411
|
return { verdict, scorePct, tally: { mustFix, shouldFix, nit, unverified }, chatHeader };
|
|
5240
5412
|
}
|
|
5413
|
+
var REVIEW_SCHEMA_ID = "mstar.review/v1";
|
|
5414
|
+
var INSPECTOR_VERDICTS = ["comment", "request_changes", "approve"];
|
|
5415
|
+
var INSPECTOR_SEVERITIES = ["critical", "warning", "suggestion", "info"];
|
|
5416
|
+
function isPlainObject7(value) {
|
|
5417
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5418
|
+
}
|
|
5419
|
+
var TALLY_COUNT_KEYS = ["mustFix", "shouldFix", "nit", "unverified"];
|
|
5420
|
+
function checkProvidedTallyShape(tally, violations) {
|
|
5421
|
+
if (typeof tally.verdict !== "string" || !PR_VERDICTS.includes(tally.verdict)) {
|
|
5422
|
+
violations.push(violation14("high", "review.tally-malformed", `tally.verdict "${String(tally.verdict)}" is not one of ${JSON.stringify(PR_VERDICTS)}`, `use one of: ${PR_VERDICTS.join(" | ")}`));
|
|
5423
|
+
}
|
|
5424
|
+
if (typeof tally.scorePct !== "number" || !Number.isInteger(tally.scorePct) || tally.scorePct < 0 || tally.scorePct > 100) {
|
|
5425
|
+
violations.push(violation14("high", "review.tally-malformed", `tally.scorePct must be an integer in [0, 100] - got ${String(tally.scorePct)}`));
|
|
5426
|
+
}
|
|
5427
|
+
if (!isPlainObject7(tally.tally)) {
|
|
5428
|
+
violations.push(violation14("high", "review.tally-malformed", "tally.tally must be an object carrying the four class counts"));
|
|
5429
|
+
} else {
|
|
5430
|
+
for (const key of TALLY_COUNT_KEYS) {
|
|
5431
|
+
const count = tally.tally[key];
|
|
5432
|
+
if (typeof count !== "number" || !Number.isInteger(count) || count < 0) {
|
|
5433
|
+
violations.push(violation14("high", "review.tally-malformed", `tally.tally.${key} must be a non-negative integer - got ${String(count)}`));
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
if (typeof tally.chatHeader !== "string") {
|
|
5438
|
+
violations.push(violation14("high", "review.tally-malformed", `tally.chatHeader must be a string - got ${typeof tally.chatHeader}`));
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
function validateMstarReviewV1(doc) {
|
|
5442
|
+
const violations = [];
|
|
5443
|
+
if (!isPlainObject7(doc)) {
|
|
5444
|
+
return {
|
|
5445
|
+
ok: false,
|
|
5446
|
+
violations: [violation14("high", "review.not-object", "review document must be a JSON object")]
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
if (doc.schema === undefined) {
|
|
5450
|
+
violations.push(violation14("high", "review.missing-schema", "missing required field: schema"));
|
|
5451
|
+
} else if (doc.schema !== REVIEW_SCHEMA_ID) {
|
|
5452
|
+
violations.push(violation14("high", "review.invalid-schema", `schema "${String(doc.schema)}" is not "${REVIEW_SCHEMA_ID}"`));
|
|
5453
|
+
}
|
|
5454
|
+
if (doc.verdict === undefined) {
|
|
5455
|
+
violations.push(violation14("high", "review.missing-verdict", "missing required field: verdict"));
|
|
5456
|
+
} else if (typeof doc.verdict !== "string") {
|
|
5457
|
+
violations.push(violation14("high", "review.invalid-verdict", `verdict must be a string - got ${typeof doc.verdict}`));
|
|
5458
|
+
} else if (INSPECTOR_VERDICTS.includes(doc.verdict)) {
|
|
5459
|
+
violations.push(violation14("high", "review.inspector-vocab", `verdict "${doc.verdict}" is inspector M1 vocab - harness verdicts are ${JSON.stringify(PR_VERDICTS)}`, `use one of: ${PR_VERDICTS.join(" | ")}`));
|
|
5460
|
+
} else if (!PR_VERDICTS.includes(doc.verdict)) {
|
|
5461
|
+
violations.push(violation14("high", "review.invalid-verdict", `verdict "${doc.verdict}" is not one of ${JSON.stringify(PR_VERDICTS)}`, `use one of: ${PR_VERDICTS.join(" | ")}`));
|
|
5462
|
+
}
|
|
5463
|
+
if (doc.summary_md === undefined || typeof doc.summary_md !== "string" || doc.summary_md.trim() === "") {
|
|
5464
|
+
violations.push(violation14("high", "review.missing-summary", "summary_md must be a non-empty string"));
|
|
5465
|
+
}
|
|
5466
|
+
if (doc.findings === undefined || !Array.isArray(doc.findings)) {
|
|
5467
|
+
violations.push(violation14("high", "review.findings-not-array", "findings must be an array"));
|
|
5468
|
+
} else {
|
|
5469
|
+
doc.findings.forEach((finding, index) => {
|
|
5470
|
+
if (!isPlainObject7(finding)) {
|
|
5471
|
+
violations.push(violation14("high", "review.invalid-finding", `findings[${index}] must be an object`));
|
|
5472
|
+
return;
|
|
5473
|
+
}
|
|
5474
|
+
if (finding.severity !== undefined) {
|
|
5475
|
+
violations.push(violation14("high", "review.inspector-vocab", `findings[${index}] carries inspector M1 field "severity" - harness merge classes are ${JSON.stringify(MERGE_CLASSES)}`, "use mergeClass: must-fix | should-fix | nit"));
|
|
5476
|
+
}
|
|
5477
|
+
if (finding.mergeClass === undefined) {
|
|
5478
|
+
violations.push(violation14("high", "review.missing-merge-class", `findings[${index}] missing required field: mergeClass`));
|
|
5479
|
+
} else if (typeof finding.mergeClass !== "string") {
|
|
5480
|
+
violations.push(violation14("high", "review.invalid-merge-class", `findings[${index}].mergeClass must be a string - got ${typeof finding.mergeClass}`));
|
|
5481
|
+
} else if (INSPECTOR_SEVERITIES.includes(finding.mergeClass)) {
|
|
5482
|
+
violations.push(violation14("high", "review.inspector-vocab", `findings[${index}].mergeClass "${finding.mergeClass}" is inspector M1 severity vocab - harness merge classes are ${JSON.stringify(MERGE_CLASSES)}`, `use one of: ${MERGE_CLASSES.join(" | ")}`));
|
|
5483
|
+
} else if (!MERGE_CLASSES.includes(finding.mergeClass)) {
|
|
5484
|
+
violations.push(violation14("high", "review.invalid-merge-class", `findings[${index}].mergeClass "${finding.mergeClass}" is not one of ${JSON.stringify(MERGE_CLASSES)}`, `use one of: ${MERGE_CLASSES.join(" | ")}`));
|
|
5485
|
+
}
|
|
5486
|
+
if (finding.title === undefined || typeof finding.title !== "string" || finding.title.trim() === "") {
|
|
5487
|
+
violations.push(violation14("high", "review.empty-title", `findings[${index}].title must be a non-empty string`));
|
|
5488
|
+
}
|
|
5489
|
+
if (finding.body === undefined || typeof finding.body !== "string" || finding.body.trim() === "") {
|
|
5490
|
+
violations.push(violation14("high", "review.empty-body", `findings[${index}].body must be a non-empty string`));
|
|
5491
|
+
}
|
|
5492
|
+
if (finding.category !== undefined && typeof finding.category !== "string") {
|
|
5493
|
+
violations.push(violation14("high", "review.invalid-category", `findings[${index}].category must be a string`));
|
|
5494
|
+
}
|
|
5495
|
+
if (finding.file_path !== undefined && finding.file_path !== null && typeof finding.file_path !== "string") {
|
|
5496
|
+
violations.push(violation14("high", "review.invalid-file-path", `findings[${index}].file_path must be a string or null`));
|
|
5497
|
+
}
|
|
5498
|
+
if (finding.line_start !== undefined && finding.line_start !== null && typeof finding.line_start !== "number") {
|
|
5499
|
+
violations.push(violation14("high", "review.invalid-line-start", `findings[${index}].line_start must be a number or null`));
|
|
5500
|
+
}
|
|
5501
|
+
if (finding.line_end !== undefined && finding.line_end !== null && typeof finding.line_end !== "number") {
|
|
5502
|
+
violations.push(violation14("high", "review.invalid-line-end", `findings[${index}].line_end must be a number or null`));
|
|
5503
|
+
}
|
|
5504
|
+
if (finding.fingerprint_hint !== undefined && typeof finding.fingerprint_hint !== "string") {
|
|
5505
|
+
violations.push(violation14("high", "review.invalid-fingerprint-hint", `findings[${index}].fingerprint_hint must be a string`));
|
|
5506
|
+
}
|
|
5507
|
+
});
|
|
5508
|
+
}
|
|
5509
|
+
if (doc.tally !== undefined) {
|
|
5510
|
+
if (!isPlainObject7(doc.tally)) {
|
|
5511
|
+
violations.push(violation14("high", "review.invalid-tally", "tally must be a PrTallyResult object"));
|
|
5512
|
+
} else {
|
|
5513
|
+
checkProvidedTallyShape(doc.tally, violations);
|
|
5514
|
+
if (doc.tally.verdict !== doc.verdict) {
|
|
5515
|
+
violations.push(violation14("high", "review.verdict-tally-mismatch", `tally.verdict "${String(doc.tally.verdict)}" does not equal the top-level verdict "${String(doc.verdict)}" - the envelope verdict and tally must agree (consistency rule)`));
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
if (doc.target !== undefined) {
|
|
5520
|
+
if (!isPlainObject7(doc.target)) {
|
|
5521
|
+
violations.push(violation14("high", "review.invalid-target", "target must be an object"));
|
|
5522
|
+
} else {
|
|
5523
|
+
if (doc.target.owner !== undefined && typeof doc.target.owner !== "string") {
|
|
5524
|
+
violations.push(violation14("high", "review.invalid-target", "target.owner must be a string"));
|
|
5525
|
+
}
|
|
5526
|
+
if (doc.target.repo !== undefined && typeof doc.target.repo !== "string") {
|
|
5527
|
+
violations.push(violation14("high", "review.invalid-target", "target.repo must be a string"));
|
|
5528
|
+
}
|
|
5529
|
+
if (doc.target.pr !== undefined && typeof doc.target.pr !== "number") {
|
|
5530
|
+
violations.push(violation14("high", "review.invalid-target", "target.pr must be a number"));
|
|
5531
|
+
}
|
|
5532
|
+
if (doc.target.head_sha !== undefined && typeof doc.target.head_sha !== "string") {
|
|
5533
|
+
violations.push(violation14("high", "review.invalid-target", "target.head_sha must be a string"));
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
return { ok: violations.length === 0, violations };
|
|
5538
|
+
}
|
|
5539
|
+
function defaultReviewSummary(tally, findings) {
|
|
5540
|
+
const lines = [
|
|
5541
|
+
`## Verdict: ${tally.verdict} · ${tally.scorePct}%`,
|
|
5542
|
+
"",
|
|
5543
|
+
`must-fix=${tally.tally.mustFix} should-fix=${tally.tally.shouldFix} nit=${tally.tally.nit} unverified=${tally.tally.unverified}`
|
|
5544
|
+
];
|
|
5545
|
+
if (findings.length > 0) {
|
|
5546
|
+
lines.push("");
|
|
5547
|
+
for (const finding of findings) {
|
|
5548
|
+
lines.push(`- ${finding.mergeClass}: ${finding.title}`);
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
return lines.join(`
|
|
5552
|
+
`);
|
|
5553
|
+
}
|
|
5554
|
+
function synthesizeReview(input) {
|
|
5555
|
+
const tally = computePrTally({
|
|
5556
|
+
findings: input.findings,
|
|
5557
|
+
unverifiedCount: input.unverifiedCount,
|
|
5558
|
+
unmetAc: input.unmetAc
|
|
5559
|
+
});
|
|
5560
|
+
return {
|
|
5561
|
+
schema: "mstar.review/v1",
|
|
5562
|
+
verdict: tally.verdict,
|
|
5563
|
+
summary_md: input.summary_md ?? defaultReviewSummary(tally, input.findings),
|
|
5564
|
+
tally,
|
|
5565
|
+
findings: input.findings,
|
|
5566
|
+
...input.target !== undefined ? { target: input.target } : {}
|
|
5567
|
+
};
|
|
5568
|
+
}
|
|
5241
5569
|
var SHORT_SHA_WIDTH = 7;
|
|
5242
5570
|
var DATE_RE6 = /^\d{4}-\d{2}-\d{2}$/;
|
|
5243
5571
|
function todayString4() {
|
|
@@ -5285,7 +5613,7 @@ function prReviewReportPath(opts) {
|
|
|
5285
5613
|
const sameStem = new RegExp(`^${escaped}(?:-r([0-9]+))?\\.md$`);
|
|
5286
5614
|
let dirents;
|
|
5287
5615
|
try {
|
|
5288
|
-
dirents =
|
|
5616
|
+
dirents = readdirSync10(opts.reportsDir, { withFileTypes: true });
|
|
5289
5617
|
} catch (error) {
|
|
5290
5618
|
if (error.code === "ENOENT")
|
|
5291
5619
|
dirents = [];
|
|
@@ -5301,7 +5629,7 @@ function prReviewReportPath(opts) {
|
|
|
5301
5629
|
}
|
|
5302
5630
|
const revision = maxRevision + 1;
|
|
5303
5631
|
const name = revision === 1 ? `${finalStem}.md` : `${finalStem}-r${revision}.md`;
|
|
5304
|
-
return
|
|
5632
|
+
return join15(opts.reportsDir, name);
|
|
5305
5633
|
}
|
|
5306
5634
|
var PR_TIERS = ["quick", "default", "deep"];
|
|
5307
5635
|
function violation14(severity, code, message, fix) {
|
|
@@ -5571,10 +5899,10 @@ function prReviewSeatPrompt(opts) {
|
|
|
5571
5899
|
}
|
|
5572
5900
|
const skillRoot = opts.skillRoot.trim();
|
|
5573
5901
|
const worktreePath = opts.worktreePath.trim();
|
|
5574
|
-
if (!
|
|
5902
|
+
if (!isAbsolute9(skillRoot)) {
|
|
5575
5903
|
throw new TypeError(`prReviewSeatPrompt: skillRoot must be an absolute path - got ${JSON.stringify(opts.skillRoot)}`);
|
|
5576
5904
|
}
|
|
5577
|
-
if (!
|
|
5905
|
+
if (!isAbsolute9(worktreePath)) {
|
|
5578
5906
|
throw new TypeError(`prReviewSeatPrompt: worktreePath must be an absolute path - got ${JSON.stringify(opts.worktreePath)}`);
|
|
5579
5907
|
}
|
|
5580
5908
|
const slug = `${domain}-${seat}`;
|
|
@@ -5598,14 +5926,14 @@ function prReviewSeatPrompt(opts) {
|
|
|
5598
5926
|
lines.push("");
|
|
5599
5927
|
lines.push("## Read first");
|
|
5600
5928
|
lines.push("");
|
|
5601
|
-
const prReviewRef =
|
|
5929
|
+
const prReviewRef = join15(skillRoot, "references", "pr-review.md");
|
|
5602
5930
|
const sections = opts.stage === 1 ? tier === "quick" ? "Scoping, Evidence rules" : "Review pipeline, Worktree isolation, Scoping, Evidence rules" : "Merge class, Attack and vet, Evidence rules, Sizing & change shape";
|
|
5603
5931
|
lines.push(`1. \`${prReviewRef}\` — read at least these sections: ${sections}.`);
|
|
5604
5932
|
lines.push(`2. The review worktree: \`${worktreePath}\` — your ONLY working directory this session; read-only (no edits, no fixes, no stash, no commits, no posts).`);
|
|
5605
5933
|
if (opts.stage === 2) {
|
|
5606
|
-
lines.push(`3. \`${
|
|
5934
|
+
lines.push(`3. \`${join15(skillRoot, "references", "finding-format.md")}\` — the template every finding follows.`);
|
|
5607
5935
|
if (opts.securitySeat === true) {
|
|
5608
|
-
lines.push(`4. \`${
|
|
5936
|
+
lines.push(`4. \`${join15(skillRoot, "references", "security-review.md")}\` — the security lens.`);
|
|
5609
5937
|
}
|
|
5610
5938
|
}
|
|
5611
5939
|
lines.push("");
|
|
@@ -5789,6 +6117,7 @@ export {
|
|
|
5789
6117
|
validateProjectRegister,
|
|
5790
6118
|
validatePrReviewReport,
|
|
5791
6119
|
validatePlanRow,
|
|
6120
|
+
validateMstarReviewV1,
|
|
5792
6121
|
validateIntegrationMergeLease,
|
|
5793
6122
|
validateGitignore,
|
|
5794
6123
|
validateFindingDoc,
|
|
@@ -5801,9 +6130,11 @@ export {
|
|
|
5801
6130
|
techDebtRollup,
|
|
5802
6131
|
taskReportExists,
|
|
5803
6132
|
taskBrief,
|
|
6133
|
+
synthesizeReview,
|
|
5804
6134
|
supplyChainChecks,
|
|
5805
6135
|
stripFrontmatter,
|
|
5806
6136
|
singleReviewSnapshot,
|
|
6137
|
+
setArtifactStore,
|
|
5807
6138
|
sddWorkspace,
|
|
5808
6139
|
scopeGuard,
|
|
5809
6140
|
scanSecrets,
|
|
@@ -5827,6 +6158,7 @@ export {
|
|
|
5827
6158
|
resolveHarnessDir,
|
|
5828
6159
|
resolveCompassEnforcement,
|
|
5829
6160
|
resolveAssetPath,
|
|
6161
|
+
resolveArtifactPath,
|
|
5830
6162
|
releaseLease,
|
|
5831
6163
|
registerWorkflow,
|
|
5832
6164
|
referenceExists,
|
|
@@ -5853,6 +6185,7 @@ export {
|
|
|
5853
6185
|
parseAssignmentBranchForms,
|
|
5854
6186
|
normalizeSeverity,
|
|
5855
6187
|
migrateHarnessTree,
|
|
6188
|
+
loadStoreModule,
|
|
5856
6189
|
listProjectReferenceFiles,
|
|
5857
6190
|
lintStrategySections,
|
|
5858
6191
|
lintSkillFrontmatter,
|
|
@@ -5863,6 +6196,7 @@ export {
|
|
|
5863
6196
|
l1PreDispatchCheck,
|
|
5864
6197
|
isReadOnlyAssignmentRole,
|
|
5865
6198
|
implementerSessionStickyRules,
|
|
6199
|
+
getArtifactStore,
|
|
5866
6200
|
findingsCleanupGate,
|
|
5867
6201
|
findTemporaryMarkers,
|
|
5868
6202
|
findSimplifyMarkers,
|
|
@@ -5873,6 +6207,7 @@ export {
|
|
|
5873
6207
|
emitGitignoreSnippet,
|
|
5874
6208
|
detectHost,
|
|
5875
6209
|
detectHarnessKind,
|
|
6210
|
+
createFsStore,
|
|
5876
6211
|
computePrTally,
|
|
5877
6212
|
compoundRefreshScope,
|
|
5878
6213
|
composeDispatchGate,
|
|
@@ -5888,6 +6223,7 @@ export {
|
|
|
5888
6223
|
assertLightDarkParity,
|
|
5889
6224
|
assertIndexRows,
|
|
5890
6225
|
assertIndexRowObligations,
|
|
6226
|
+
assertFsStorePath,
|
|
5891
6227
|
assertDefaultBranchProtected,
|
|
5892
6228
|
assertControlVsFeaturePath,
|
|
5893
6229
|
assertBranchAlignment,
|