@mstar-harness/engine 3.4.1 → 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/engine.js +534 -200
- 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>"';
|
|
@@ -1008,7 +1166,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1008
1166
|
|
|
1009
1167
|
// src/workflow.ts
|
|
1010
1168
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
1011
|
-
import { join as
|
|
1169
|
+
import { join as join6 } from "node:path";
|
|
1012
1170
|
var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
1013
1171
|
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
1014
1172
|
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
@@ -1125,10 +1283,12 @@ async function writeWorkflowSnapshot(snapshot, dir) {
|
|
|
1125
1283
|
const detail = gate.violations.map((v) => v.message).join("; ");
|
|
1126
1284
|
throw new Error(`refusing to write invalid workflow snapshot: ${detail}`);
|
|
1127
1285
|
}
|
|
1128
|
-
const snapshotPath =
|
|
1286
|
+
const snapshotPath = join6(dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1287
|
+
const store = getArtifactStore();
|
|
1288
|
+
assertFsStorePath(store, { kind: "snapshot", key: snapshot.id }, snapshotPath);
|
|
1129
1289
|
mkdirSync3(dir, { recursive: true });
|
|
1130
|
-
await withStatusWriteLock(snapshotPath, () => {
|
|
1131
|
-
|
|
1290
|
+
await withStatusWriteLock(snapshotPath, async () => {
|
|
1291
|
+
await store.put({ kind: "snapshot", key: snapshot.id, payload: snapshot });
|
|
1132
1292
|
});
|
|
1133
1293
|
}
|
|
1134
1294
|
|
|
@@ -1295,7 +1455,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1295
1455
|
if (typeof docOrPath === "string") {
|
|
1296
1456
|
try {
|
|
1297
1457
|
doc = readJson(docOrPath);
|
|
1298
|
-
harnessDir = dirname4(
|
|
1458
|
+
harnessDir = dirname4(resolve5(docOrPath));
|
|
1299
1459
|
} catch (error) {
|
|
1300
1460
|
return {
|
|
1301
1461
|
ok: false,
|
|
@@ -1362,8 +1522,8 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1362
1522
|
for (const entry of doc.workflows) {
|
|
1363
1523
|
if (!isPlainObject4(entry) || typeof entry.dir !== "string")
|
|
1364
1524
|
continue;
|
|
1365
|
-
const relSnapshot =
|
|
1366
|
-
const snapshotPath =
|
|
1525
|
+
const relSnapshot = join7(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1526
|
+
const snapshotPath = join7(harnessDir, relSnapshot);
|
|
1367
1527
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
1368
1528
|
let physical;
|
|
1369
1529
|
try {
|
|
@@ -1397,8 +1557,10 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1397
1557
|
return { ok: violations.length === 0, violations };
|
|
1398
1558
|
}
|
|
1399
1559
|
var validateStatus = validateStatusV2;
|
|
1400
|
-
function registerWorkflowEntryLocked(statusPath, entry) {
|
|
1560
|
+
async function registerWorkflowEntryLocked(statusPath, entry) {
|
|
1401
1561
|
const harnessDir = dirname4(statusPath);
|
|
1562
|
+
const store = getArtifactStore();
|
|
1563
|
+
assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
|
|
1402
1564
|
const current = readJson(statusPath);
|
|
1403
1565
|
const fresh = Object.keys(current).length === 0;
|
|
1404
1566
|
const doc = fresh ? { version: 2, updated_at: todayString(), workflows: [] } : current;
|
|
@@ -1416,7 +1578,7 @@ function registerWorkflowEntryLocked(statusPath, entry) {
|
|
|
1416
1578
|
if (!gate.ok) {
|
|
1417
1579
|
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1418
1580
|
}
|
|
1419
|
-
|
|
1581
|
+
await store.put({ kind: "status", key: "root", payload: doc });
|
|
1420
1582
|
return doc;
|
|
1421
1583
|
}
|
|
1422
1584
|
async function registerWorkflow(root, entry) {
|
|
@@ -1424,16 +1586,18 @@ async function registerWorkflow(root, entry) {
|
|
|
1424
1586
|
if (!entryGate.ok) {
|
|
1425
1587
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
1426
1588
|
}
|
|
1427
|
-
const statusPath =
|
|
1589
|
+
const statusPath = resolve5(root);
|
|
1428
1590
|
return withStatusWriteLock(statusPath, () => registerWorkflowEntryLocked(statusPath, entry));
|
|
1429
1591
|
}
|
|
1430
1592
|
async function unregisterWorkflow(root, id) {
|
|
1431
1593
|
if (typeof id !== "string" || id.trim() === "") {
|
|
1432
1594
|
throw new Error("refusing to unregister workflow: id must be a non-empty string");
|
|
1433
1595
|
}
|
|
1434
|
-
const statusPath =
|
|
1596
|
+
const statusPath = resolve5(root);
|
|
1597
|
+
const store = getArtifactStore();
|
|
1598
|
+
assertFsStorePath(store, { kind: "status", key: "root" }, statusPath);
|
|
1435
1599
|
const harnessDir = dirname4(statusPath);
|
|
1436
|
-
return withStatusWriteLock(statusPath, () => {
|
|
1600
|
+
return withStatusWriteLock(statusPath, async () => {
|
|
1437
1601
|
const current = readJson(statusPath);
|
|
1438
1602
|
if (Object.keys(current).length === 0) {
|
|
1439
1603
|
return { version: 2, updated_at: todayString(), workflows: [] };
|
|
@@ -1452,25 +1616,25 @@ async function unregisterWorkflow(root, id) {
|
|
|
1452
1616
|
if (!gate.ok) {
|
|
1453
1617
|
throw new Error(`refusing to write invalid status.json: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1454
1618
|
}
|
|
1455
|
-
|
|
1619
|
+
await store.put({ kind: "status", key: "root", payload: doc });
|
|
1456
1620
|
return doc;
|
|
1457
1621
|
});
|
|
1458
1622
|
}
|
|
1459
1623
|
function resolveCompassEnforcement(harnessDir) {
|
|
1460
1624
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
1461
|
-
if (!
|
|
1625
|
+
if (!existsSync4(iterationsDir))
|
|
1462
1626
|
return { hard: false, source: "none" };
|
|
1463
1627
|
let entries;
|
|
1464
1628
|
try {
|
|
1465
|
-
entries =
|
|
1629
|
+
entries = readdirSync3(iterationsDir, { withFileTypes: true });
|
|
1466
1630
|
} catch {
|
|
1467
1631
|
return { hard: false, source: "none" };
|
|
1468
1632
|
}
|
|
1469
1633
|
for (const entry of entries) {
|
|
1470
1634
|
if (!entry.isDirectory())
|
|
1471
1635
|
continue;
|
|
1472
|
-
const compassPath =
|
|
1473
|
-
if (!
|
|
1636
|
+
const compassPath = join7(iterationsDir, entry.name, "delivery-compass.md");
|
|
1637
|
+
if (!existsSync4(compassPath))
|
|
1474
1638
|
continue;
|
|
1475
1639
|
let content;
|
|
1476
1640
|
try {
|
|
@@ -1489,7 +1653,7 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1489
1653
|
return { hard: false, source: "none" };
|
|
1490
1654
|
}
|
|
1491
1655
|
function resolveMstarcEnforcement(harnessDir) {
|
|
1492
|
-
const dir =
|
|
1656
|
+
const dir = resolve5(harnessDir);
|
|
1493
1657
|
const rc = loadMstarc(dir, dirname4(dir));
|
|
1494
1658
|
const value = rc?.config.enforcement;
|
|
1495
1659
|
if (value === "hard")
|
|
@@ -1641,9 +1805,12 @@ async function appendProjectRegisterEntries(opts) {
|
|
|
1641
1805
|
if (opts.entries.length === 0) {
|
|
1642
1806
|
throw new Error("refusing to append residual entries: entries must not be empty");
|
|
1643
1807
|
}
|
|
1644
|
-
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);
|
|
1645
1812
|
mkdirSync4(opts.projectDir, { recursive: true });
|
|
1646
|
-
return withStatusWriteLock(registerPath, () => {
|
|
1813
|
+
return withStatusWriteLock(registerPath, async () => {
|
|
1647
1814
|
const doc = readJson(registerPath);
|
|
1648
1815
|
const entriesMap = doc.entries ?? {};
|
|
1649
1816
|
let key = opts.basePlanKey;
|
|
@@ -1678,14 +1845,17 @@ async function appendProjectRegisterEntries(opts) {
|
|
|
1678
1845
|
if (!gate.ok) {
|
|
1679
1846
|
throw new Error(`refusing to write invalid project register: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1680
1847
|
}
|
|
1681
|
-
|
|
1848
|
+
await store.put({ kind: "residuals", key: projectKey, payload: register });
|
|
1682
1849
|
return { ok: true, key };
|
|
1683
1850
|
});
|
|
1684
1851
|
}
|
|
1685
1852
|
async function closeProjectRegisterEntry(opts) {
|
|
1686
|
-
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);
|
|
1687
1857
|
mkdirSync4(opts.projectDir, { recursive: true });
|
|
1688
|
-
return withStatusWriteLock(registerPath, () => {
|
|
1858
|
+
return withStatusWriteLock(registerPath, async () => {
|
|
1689
1859
|
const doc = readJson(registerPath);
|
|
1690
1860
|
const planEntries = doc.entries?.[opts.planKey];
|
|
1691
1861
|
if (!Array.isArray(planEntries)) {
|
|
@@ -1705,7 +1875,7 @@ async function closeProjectRegisterEntry(opts) {
|
|
|
1705
1875
|
if (!gate.ok) {
|
|
1706
1876
|
throw new Error(`refusing to write invalid project register: ${gate.violations.map((v) => v.message).join("; ")}`);
|
|
1707
1877
|
}
|
|
1708
|
-
|
|
1878
|
+
await store.put({ kind: "residuals", key: projectKey, payload: register });
|
|
1709
1879
|
return { ok: true };
|
|
1710
1880
|
});
|
|
1711
1881
|
}
|
|
@@ -1758,15 +1928,15 @@ function techDebtRollup(projectDir) {
|
|
|
1758
1928
|
const items = [];
|
|
1759
1929
|
let entries;
|
|
1760
1930
|
try {
|
|
1761
|
-
entries =
|
|
1931
|
+
entries = readdirSync4(projectDir, { withFileTypes: true });
|
|
1762
1932
|
} catch {
|
|
1763
1933
|
entries = [];
|
|
1764
1934
|
}
|
|
1765
1935
|
for (const project of entries) {
|
|
1766
1936
|
if (!project.isDirectory())
|
|
1767
1937
|
continue;
|
|
1768
|
-
const registerPath =
|
|
1769
|
-
if (!
|
|
1938
|
+
const registerPath = join8(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
1939
|
+
if (!existsSync5(registerPath))
|
|
1770
1940
|
continue;
|
|
1771
1941
|
let register;
|
|
1772
1942
|
try {
|
|
@@ -1802,10 +1972,10 @@ function techDebtRollup(projectDir) {
|
|
|
1802
1972
|
return { computed, stored, checks, overall };
|
|
1803
1973
|
}
|
|
1804
1974
|
function listProjectReferenceFiles(projectDir) {
|
|
1805
|
-
const root =
|
|
1975
|
+
const root = join8(projectDir, PROJECT_REFERENCES_DIR);
|
|
1806
1976
|
let entries;
|
|
1807
1977
|
try {
|
|
1808
|
-
entries =
|
|
1978
|
+
entries = readdirSync4(root, { withFileTypes: true });
|
|
1809
1979
|
} catch {
|
|
1810
1980
|
return [];
|
|
1811
1981
|
}
|
|
@@ -1818,7 +1988,7 @@ function listProjectReferenceFiles(projectDir) {
|
|
|
1818
1988
|
} else if (entry.isDirectory()) {
|
|
1819
1989
|
let nested;
|
|
1820
1990
|
try {
|
|
1821
|
-
nested =
|
|
1991
|
+
nested = readdirSync4(join8(root, entry.name), { withFileTypes: true });
|
|
1822
1992
|
} catch {
|
|
1823
1993
|
continue;
|
|
1824
1994
|
}
|
|
@@ -1833,19 +2003,19 @@ function listProjectReferenceFiles(projectDir) {
|
|
|
1833
2003
|
|
|
1834
2004
|
// src/path.ts
|
|
1835
2005
|
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
1836
|
-
const start =
|
|
2006
|
+
const start = resolve7(startDir);
|
|
1837
2007
|
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
1838
2008
|
if (explicit)
|
|
1839
|
-
return
|
|
1840
|
-
const boundary =
|
|
2009
|
+
return resolve7(start, explicit);
|
|
2010
|
+
const boundary = resolve7(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
1841
2011
|
const rc = loadMstarc(start, boundary);
|
|
1842
2012
|
if (rc !== null && rc.config.harnessDir)
|
|
1843
|
-
return
|
|
2013
|
+
return resolve7(rc.dir, rc.config.harnessDir);
|
|
1844
2014
|
let dir = start;
|
|
1845
2015
|
for (;; ) {
|
|
1846
2016
|
if (!isAtOrBelow2(dir, boundary))
|
|
1847
2017
|
return null;
|
|
1848
|
-
for (const candidate of [
|
|
2018
|
+
for (const candidate of [join9(dir, ".mstar"), join9(dir, ".agents"), join9(dir, ".plans"), join9(dir, "plans")]) {
|
|
1849
2019
|
if (isDirectory(candidate))
|
|
1850
2020
|
return candidate;
|
|
1851
2021
|
}
|
|
@@ -1871,19 +2041,19 @@ function defaultWorkspaceRoot(startDir) {
|
|
|
1871
2041
|
if (segment && segment !== ".")
|
|
1872
2042
|
boundary = dirname5(boundary);
|
|
1873
2043
|
}
|
|
1874
|
-
return
|
|
2044
|
+
return resolve7(boundary);
|
|
1875
2045
|
} catch {}
|
|
1876
2046
|
return startDir;
|
|
1877
2047
|
}
|
|
1878
2048
|
function isAtOrBelow2(dir, root) {
|
|
1879
2049
|
const rel = relative2(root, dir);
|
|
1880
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
2050
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
1881
2051
|
}
|
|
1882
2052
|
function mstarcDirOverride(harnessDir, key) {
|
|
1883
|
-
const dir =
|
|
2053
|
+
const dir = resolve7(harnessDir);
|
|
1884
2054
|
const rc = loadMstarc(dir, dirname5(dir));
|
|
1885
2055
|
const declared = rc?.config[key];
|
|
1886
|
-
return declared ?
|
|
2056
|
+
return declared ? resolve7(rc.dir, declared) : null;
|
|
1887
2057
|
}
|
|
1888
2058
|
function resolveSpecsDir(harnessDir, opts = {}) {
|
|
1889
2059
|
const declared = mstarcDirOverride(harnessDir, "specsDir");
|
|
@@ -1892,20 +2062,20 @@ function resolveSpecsDir(harnessDir, opts = {}) {
|
|
|
1892
2062
|
mkdirSync5(declared, { recursive: true });
|
|
1893
2063
|
return declared;
|
|
1894
2064
|
}
|
|
1895
|
-
const harness =
|
|
2065
|
+
const harness = resolve7(harnessDir);
|
|
1896
2066
|
const repoRoot = dirname5(harness);
|
|
1897
2067
|
const candidates = [
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2068
|
+
join9(harness, "specs"),
|
|
2069
|
+
join9(repoRoot, "docs", "specs"),
|
|
2070
|
+
join9(repoRoot, "specs"),
|
|
2071
|
+
join9(harness, "designs"),
|
|
2072
|
+
join9(repoRoot, "designs")
|
|
1903
2073
|
];
|
|
1904
2074
|
for (const candidate of candidates) {
|
|
1905
2075
|
if (isDirectory(candidate) && hasFiles(candidate))
|
|
1906
2076
|
return candidate;
|
|
1907
2077
|
}
|
|
1908
|
-
const fallback =
|
|
2078
|
+
const fallback = join9(harness, "specs");
|
|
1909
2079
|
if (opts.create !== false)
|
|
1910
2080
|
mkdirSync5(fallback, { recursive: true });
|
|
1911
2081
|
return fallback;
|
|
@@ -1914,11 +2084,11 @@ function resolvePlanDir(harnessDir) {
|
|
|
1914
2084
|
const declared = mstarcDirOverride(harnessDir, "planDir");
|
|
1915
2085
|
if (declared !== null)
|
|
1916
2086
|
return declared;
|
|
1917
|
-
const dir =
|
|
1918
|
-
const name =
|
|
2087
|
+
const dir = resolve7(harnessDir);
|
|
2088
|
+
const name = basename3(dir);
|
|
1919
2089
|
if (name === ".plans" || name === "plans")
|
|
1920
2090
|
return dir;
|
|
1921
|
-
return
|
|
2091
|
+
return join9(dir, "plans");
|
|
1922
2092
|
}
|
|
1923
2093
|
function assertSafePathComponent(value, what) {
|
|
1924
2094
|
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
@@ -1927,30 +2097,30 @@ function assertSafePathComponent(value, what) {
|
|
|
1927
2097
|
}
|
|
1928
2098
|
function resolveSddDir(harnessDir, planId) {
|
|
1929
2099
|
assertSafePathComponent(planId, "planId");
|
|
1930
|
-
const base =
|
|
2100
|
+
const base = resolve7(harnessDir);
|
|
1931
2101
|
const declared = mstarcDirOverride(base, "sddDir");
|
|
1932
|
-
const sddBase = declared !== null ? declared :
|
|
1933
|
-
return
|
|
2102
|
+
const sddBase = declared !== null ? declared : join9(base, "sdd");
|
|
2103
|
+
return join9(sddBase, planId);
|
|
1934
2104
|
}
|
|
1935
2105
|
function resolveIterationDir(harnessDir) {
|
|
1936
2106
|
const declared = mstarcDirOverride(harnessDir, "iterationDir");
|
|
1937
2107
|
if (declared !== null)
|
|
1938
2108
|
return declared;
|
|
1939
|
-
return
|
|
2109
|
+
return join9(resolve7(harnessDir), "iterations");
|
|
1940
2110
|
}
|
|
1941
2111
|
function resolveKnowledgeDir(harnessDir) {
|
|
1942
2112
|
const declared = mstarcDirOverride(harnessDir, "knowledgeDir");
|
|
1943
2113
|
if (declared !== null)
|
|
1944
2114
|
return declared;
|
|
1945
|
-
return
|
|
2115
|
+
return join9(resolve7(harnessDir), "knowledge");
|
|
1946
2116
|
}
|
|
1947
2117
|
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
1948
2118
|
const harness = resolveHarnessDir(startDir, opts);
|
|
1949
2119
|
if (harness === null) {
|
|
1950
|
-
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)`);
|
|
1951
2121
|
}
|
|
1952
2122
|
const declared = mstarcDirOverride(harness, key);
|
|
1953
|
-
return declared !== null ? declared :
|
|
2123
|
+
return declared !== null ? declared : join9(resolve7(harness), fallback);
|
|
1954
2124
|
}
|
|
1955
2125
|
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
1956
2126
|
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
@@ -1965,13 +2135,13 @@ var EMPTY_STATUS_TEMPLATE = {
|
|
|
1965
2135
|
};
|
|
1966
2136
|
var SCAFFOLD_DIRS = ["plans", "iterations", "knowledge", "specs", "sdd"];
|
|
1967
2137
|
function resolveScaffoldDirs(root) {
|
|
1968
|
-
const start =
|
|
1969
|
-
const boundary =
|
|
2138
|
+
const start = resolve7(root);
|
|
2139
|
+
const boundary = resolve7(start, defaultWorkspaceRoot(start));
|
|
1970
2140
|
const rc = loadMstarc(start, boundary);
|
|
1971
2141
|
const explicit = process.env.MSTAR_HARNESS_DIR;
|
|
1972
|
-
const harnessDir = explicit ?
|
|
2142
|
+
const harnessDir = explicit ? resolve7(start, explicit) : rc !== null && rc.config.harnessDir ? resolve7(rc.dir, rc.config.harnessDir) : join9(start, ".mstar");
|
|
1973
2143
|
const declaredProjectDir = mstarcDirOverride(harnessDir, "projectDir");
|
|
1974
|
-
const projectDir = declaredProjectDir !== null ? declaredProjectDir :
|
|
2144
|
+
const projectDir = declaredProjectDir !== null ? declaredProjectDir : join9(harnessDir, "projects");
|
|
1975
2145
|
return { harnessDir, projectDir };
|
|
1976
2146
|
}
|
|
1977
2147
|
var ROADMAP_TEMPLATE = `---
|
|
@@ -1993,18 +2163,18 @@ var EMPTY_REGISTER_TEMPLATE = {
|
|
|
1993
2163
|
function scaffoldHarness(root) {
|
|
1994
2164
|
const { harnessDir, projectDir } = resolveScaffoldDirs(root);
|
|
1995
2165
|
for (const dir of SCAFFOLD_DIRS)
|
|
1996
|
-
mkdirSync5(
|
|
1997
|
-
const statusPath =
|
|
2166
|
+
mkdirSync5(join9(harnessDir, dir), { recursive: true });
|
|
2167
|
+
const statusPath = join9(harnessDir, "status.json");
|
|
1998
2168
|
if (Object.keys(readJson(statusPath)).length === 0)
|
|
1999
2169
|
writeJson(statusPath, EMPTY_STATUS_TEMPLATE);
|
|
2000
|
-
const defaultProjectDir =
|
|
2170
|
+
const defaultProjectDir = join9(projectDir, _DEFAULT_PROJECT);
|
|
2001
2171
|
mkdirSync5(defaultProjectDir, { recursive: true });
|
|
2002
|
-
const roadmapPath =
|
|
2003
|
-
if (!
|
|
2172
|
+
const roadmapPath = join9(defaultProjectDir, PROJECT_ROADMAP_FILE);
|
|
2173
|
+
if (!existsSync6(roadmapPath)) {
|
|
2004
2174
|
const created = new Date().toISOString().slice(0, 10);
|
|
2005
2175
|
writeFileSync3(roadmapPath, ROADMAP_TEMPLATE.replace("{created_at}", created), "utf8");
|
|
2006
2176
|
}
|
|
2007
|
-
const registerPath =
|
|
2177
|
+
const registerPath = join9(defaultProjectDir, PROJECT_REGISTER_FILE);
|
|
2008
2178
|
if (Object.keys(readJson(registerPath)).length === 0)
|
|
2009
2179
|
writeJson(registerPath, EMPTY_REGISTER_TEMPLATE);
|
|
2010
2180
|
return harnessDir;
|
|
@@ -2042,7 +2212,7 @@ function emitGitignoreSnippet(kind) {
|
|
|
2042
2212
|
return `${GITIGNORE_SNIPPET}${GITIGNORE_SNIPPET_AGENTS}`;
|
|
2043
2213
|
}
|
|
2044
2214
|
function validateGitignore(root) {
|
|
2045
|
-
const gitignorePath =
|
|
2215
|
+
const gitignorePath = join9(resolve7(root), ".gitignore");
|
|
2046
2216
|
const kind = detectHarnessKind(resolveHarnessDir(root));
|
|
2047
2217
|
let content;
|
|
2048
2218
|
try {
|
|
@@ -2090,7 +2260,7 @@ function validateGitignore(root) {
|
|
|
2090
2260
|
function detectHarnessKind(harnessDir) {
|
|
2091
2261
|
if (!harnessDir)
|
|
2092
2262
|
return null;
|
|
2093
|
-
const name =
|
|
2263
|
+
const name = basename3(resolve7(harnessDir));
|
|
2094
2264
|
if (name === ".mstar")
|
|
2095
2265
|
return "mstar";
|
|
2096
2266
|
if (name === ".agents")
|
|
@@ -2098,7 +2268,7 @@ function detectHarnessKind(harnessDir) {
|
|
|
2098
2268
|
return null;
|
|
2099
2269
|
}
|
|
2100
2270
|
function assertPlanWritingPath(planPath, harnessDir) {
|
|
2101
|
-
const planAbs =
|
|
2271
|
+
const planAbs = resolve7(planPath);
|
|
2102
2272
|
if (!harnessDir) {
|
|
2103
2273
|
return {
|
|
2104
2274
|
ok: false,
|
|
@@ -2110,7 +2280,7 @@ function assertPlanWritingPath(planPath, harnessDir) {
|
|
|
2110
2280
|
}
|
|
2111
2281
|
const planDir = resolvePlanDir(harnessDir);
|
|
2112
2282
|
const rel = relative2(planDir, planAbs);
|
|
2113
|
-
const inside = rel === "" || !rel.startsWith("..") && !
|
|
2283
|
+
const inside = rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
2114
2284
|
if (!inside) {
|
|
2115
2285
|
return {
|
|
2116
2286
|
ok: false,
|
|
@@ -2120,12 +2290,12 @@ function assertPlanWritingPath(planPath, harnessDir) {
|
|
|
2120
2290
|
fix: `write the plan under ${planDir}`
|
|
2121
2291
|
};
|
|
2122
2292
|
}
|
|
2123
|
-
if (
|
|
2293
|
+
if (existsSync6(planAbs)) {
|
|
2124
2294
|
try {
|
|
2125
2295
|
const canonicalPlan = realpathSync2(planAbs);
|
|
2126
|
-
const canonicalPlanDir =
|
|
2296
|
+
const canonicalPlanDir = existsSync6(planDir) ? realpathSync2(planDir) : resolve7(planDir);
|
|
2127
2297
|
const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
|
|
2128
|
-
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !
|
|
2298
|
+
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute4(canonicalRel);
|
|
2129
2299
|
if (!canonicalInside) {
|
|
2130
2300
|
return {
|
|
2131
2301
|
ok: false,
|
|
@@ -2153,9 +2323,9 @@ function isDirectory(dir) {
|
|
|
2153
2323
|
}
|
|
2154
2324
|
function hasFiles(dir) {
|
|
2155
2325
|
try {
|
|
2156
|
-
for (const entry of
|
|
2326
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
2157
2327
|
if (entry.isDirectory()) {
|
|
2158
|
-
if (hasFiles(
|
|
2328
|
+
if (hasFiles(join9(dir, entry.name)))
|
|
2159
2329
|
return true;
|
|
2160
2330
|
} else if (entry.isFile()) {
|
|
2161
2331
|
return true;
|
|
@@ -2168,8 +2338,8 @@ function hasFiles(dir) {
|
|
|
2168
2338
|
}
|
|
2169
2339
|
// src/worktree.ts
|
|
2170
2340
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2171
|
-
import { existsSync as
|
|
2172
|
-
import { isAbsolute as
|
|
2341
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2342
|
+
import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
|
|
2173
2343
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
2174
2344
|
function probeTimeoutMs() {
|
|
2175
2345
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
@@ -2220,10 +2390,10 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
2220
2390
|
if (leaseWorkingBranch.trim() === "") {
|
|
2221
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"));
|
|
2222
2392
|
}
|
|
2223
|
-
if (controlWorktreePath !== "" && leaseWorktreePath !== "" &&
|
|
2393
|
+
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(leaseWorktreePath)) {
|
|
2224
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"));
|
|
2225
2395
|
}
|
|
2226
|
-
if (leaseWorktreePath !== "" && !
|
|
2396
|
+
if (leaseWorktreePath !== "" && !existsSync7(leaseWorktreePath)) {
|
|
2227
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>`));
|
|
2228
2398
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
2229
2399
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
@@ -2247,17 +2417,17 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2247
2417
|
violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2248
2418
|
return;
|
|
2249
2419
|
}
|
|
2250
|
-
if (!
|
|
2420
|
+
if (!isAbsolute5(track.worktreePath)) {
|
|
2251
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>)`));
|
|
2252
2422
|
return;
|
|
2253
2423
|
}
|
|
2254
|
-
const normalized =
|
|
2424
|
+
const normalized = resolve8(track.worktreePath);
|
|
2255
2425
|
if (seenPaths.has(normalized)) {
|
|
2256
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"));
|
|
2257
2427
|
return;
|
|
2258
2428
|
}
|
|
2259
2429
|
seenPaths.add(normalized);
|
|
2260
|
-
if (!
|
|
2430
|
+
if (!existsSync7(track.worktreePath)) {
|
|
2261
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}`));
|
|
2262
2432
|
return;
|
|
2263
2433
|
}
|
|
@@ -2272,7 +2442,7 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2272
2442
|
}
|
|
2273
2443
|
function assertControlVsFeaturePath(controlWorktreePath, featureWorktreePath) {
|
|
2274
2444
|
const violations = [];
|
|
2275
|
-
const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" &&
|
|
2445
|
+
const samePath = controlWorktreePath === "" && featureWorktreePath === "" || controlWorktreePath !== "" && featureWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(featureWorktreePath);
|
|
2276
2446
|
if (samePath) {
|
|
2277
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"));
|
|
2278
2448
|
}
|
|
@@ -2320,8 +2490,8 @@ function singleReviewSnapshot(assignments) {
|
|
|
2320
2490
|
}
|
|
2321
2491
|
// src/sdd.ts
|
|
2322
2492
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2323
|
-
import { mkdirSync as mkdirSync6, readdirSync as
|
|
2324
|
-
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";
|
|
2325
2495
|
class SddScriptError extends Error {
|
|
2326
2496
|
exitCode;
|
|
2327
2497
|
constructor(message, exitCode) {
|
|
@@ -2358,14 +2528,14 @@ function gitOut(cwd, args) {
|
|
|
2358
2528
|
}
|
|
2359
2529
|
}
|
|
2360
2530
|
function probeHarnessWithStatus(root) {
|
|
2361
|
-
if (isFile2(
|
|
2362
|
-
return
|
|
2363
|
-
if (isFile2(
|
|
2364
|
-
return
|
|
2365
|
-
if (hasWorkflowSnapshot(
|
|
2366
|
-
return
|
|
2367
|
-
if (hasWorkflowSnapshot(
|
|
2368
|
-
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");
|
|
2369
2539
|
return null;
|
|
2370
2540
|
}
|
|
2371
2541
|
function hasWorkflowSnapshot(harnessDir) {
|
|
@@ -2373,13 +2543,13 @@ function hasWorkflowSnapshot(harnessDir) {
|
|
|
2373
2543
|
try {
|
|
2374
2544
|
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2375
2545
|
} catch {
|
|
2376
|
-
workflowsDir =
|
|
2546
|
+
workflowsDir = join10(harnessDir, "workflows");
|
|
2377
2547
|
}
|
|
2378
2548
|
if (!isDirectory2(workflowsDir))
|
|
2379
2549
|
return false;
|
|
2380
2550
|
try {
|
|
2381
|
-
for (const entry of
|
|
2382
|
-
if (entry.isDirectory() && isFile2(
|
|
2551
|
+
for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
|
|
2552
|
+
if (entry.isDirectory() && isFile2(join10(workflowsDir, entry.name, "snapshot.json")))
|
|
2383
2553
|
return true;
|
|
2384
2554
|
}
|
|
2385
2555
|
} catch {
|
|
@@ -2392,14 +2562,14 @@ function isLinkedWorktree(root) {
|
|
|
2392
2562
|
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2393
2563
|
if (gitDirRaw === null || commonRaw === null)
|
|
2394
2564
|
return false;
|
|
2395
|
-
const gitDir =
|
|
2396
|
-
const common =
|
|
2565
|
+
const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw : join10(root, gitDirRaw);
|
|
2566
|
+
const common = isAbsolute6(commonRaw) ? commonRaw : join10(root, commonRaw);
|
|
2397
2567
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2398
2568
|
return true;
|
|
2399
2569
|
try {
|
|
2400
2570
|
const gdParent = realpathSync3(dirname6(gitDir));
|
|
2401
2571
|
const cmAbs = realpathSync3(common);
|
|
2402
|
-
return
|
|
2572
|
+
return join10(gdParent, basename4(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2403
2573
|
} catch {
|
|
2404
2574
|
return false;
|
|
2405
2575
|
}
|
|
@@ -2430,28 +2600,28 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2430
2600
|
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2431
2601
|
let harnessDir;
|
|
2432
2602
|
if (harnessOverride) {
|
|
2433
|
-
harnessDir =
|
|
2603
|
+
harnessDir = resolve9(root, harnessOverride);
|
|
2434
2604
|
} else {
|
|
2435
2605
|
const rc = findMstarc(root, root);
|
|
2436
2606
|
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
|
|
2437
2607
|
if (rcHarnessDir) {
|
|
2438
|
-
harnessDir =
|
|
2608
|
+
harnessDir = resolve9(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
2439
2609
|
} else {
|
|
2440
2610
|
const probed = probeHarnessWithStatus(root);
|
|
2441
2611
|
if (probed) {
|
|
2442
2612
|
harnessDir = probed;
|
|
2443
|
-
} else if (isDirectory2(
|
|
2444
|
-
harnessDir =
|
|
2445
|
-
} else if (isDirectory2(
|
|
2446
|
-
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");
|
|
2447
2617
|
} else {
|
|
2448
|
-
harnessDir =
|
|
2618
|
+
harnessDir = join10(root, ".mstar");
|
|
2449
2619
|
}
|
|
2450
2620
|
}
|
|
2451
2621
|
}
|
|
2452
2622
|
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2453
2623
|
mkdirSync6(sddDir, { recursive: true });
|
|
2454
|
-
writeFileSync4(
|
|
2624
|
+
writeFileSync4(join10(sddDir, ".gitignore"), `*
|
|
2455
2625
|
`);
|
|
2456
2626
|
return realpathSync3(sddDir);
|
|
2457
2627
|
}
|
|
@@ -2474,7 +2644,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2474
2644
|
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2475
2645
|
}
|
|
2476
2646
|
mkdirSync6(sddDir, { recursive: true });
|
|
2477
|
-
out =
|
|
2647
|
+
out = join10(sddDir, `task-${taskN}-brief.md`);
|
|
2478
2648
|
}
|
|
2479
2649
|
const records = content.endsWith(`
|
|
2480
2650
|
`) ? content.split(`
|
|
@@ -2527,7 +2697,7 @@ function reviewPackage(base, head, outFile, opts = {}) {
|
|
|
2527
2697
|
mkdirSync6(sddDir, { recursive: true });
|
|
2528
2698
|
const shortBase = gitOut(cwd, ["rev-parse", "--short", base]) ?? base;
|
|
2529
2699
|
const shortHead = gitOut(cwd, ["rev-parse", "--short", head]) ?? head;
|
|
2530
|
-
out =
|
|
2700
|
+
out = join10(sddDir, `review-${shortBase}..${shortHead}.diff`);
|
|
2531
2701
|
}
|
|
2532
2702
|
const run = (args) => execFileSync3("git", args, { cwd, maxBuffer: GIT_CAPTURE_MAX_BYTES });
|
|
2533
2703
|
const parts = [
|
|
@@ -2563,7 +2733,7 @@ function assertBaseSha(ref, opts = {}) {
|
|
|
2563
2733
|
}
|
|
2564
2734
|
function taskReportExists(sddDir, taskN) {
|
|
2565
2735
|
try {
|
|
2566
|
-
const st = statSync4(
|
|
2736
|
+
const st = statSync4(join10(sddDir, `task-${taskN}-report.md`));
|
|
2567
2737
|
return st.isFile() && st.size > 0;
|
|
2568
2738
|
} catch {
|
|
2569
2739
|
return false;
|
|
@@ -2572,7 +2742,7 @@ function taskReportExists(sddDir, taskN) {
|
|
|
2572
2742
|
function readProgressLedger(sddDir) {
|
|
2573
2743
|
let content;
|
|
2574
2744
|
try {
|
|
2575
|
-
content = readFileSync7(
|
|
2745
|
+
content = readFileSync7(join10(sddDir, "progress.md"), "utf8");
|
|
2576
2746
|
} catch {
|
|
2577
2747
|
return [];
|
|
2578
2748
|
}
|
|
@@ -2605,8 +2775,8 @@ function implementerSessionStickyRules(input) {
|
|
|
2605
2775
|
return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
|
|
2606
2776
|
}
|
|
2607
2777
|
// src/migrate.ts
|
|
2608
|
-
import { copyFileSync, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as
|
|
2609
|
-
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";
|
|
2610
2780
|
var MIGRATE_STATUS_FILE = "status.json";
|
|
2611
2781
|
var ARCHIVED_STATUS_V1_FILE = "archived/status.v1.json";
|
|
2612
2782
|
var NOTES_LEDGER_FILE = "notes.jsonl";
|
|
@@ -2646,18 +2816,18 @@ function todayString3() {
|
|
|
2646
2816
|
return `${now.getFullYear()}-${month}-${day}`;
|
|
2647
2817
|
}
|
|
2648
2818
|
function scanCompasses(harnessDir) {
|
|
2649
|
-
const iterationsDir =
|
|
2819
|
+
const iterationsDir = join11(harnessDir, "iterations");
|
|
2650
2820
|
const out = [];
|
|
2651
2821
|
let entries;
|
|
2652
2822
|
try {
|
|
2653
|
-
entries =
|
|
2823
|
+
entries = readdirSync7(iterationsDir, { withFileTypes: true });
|
|
2654
2824
|
} catch {
|
|
2655
2825
|
return out;
|
|
2656
2826
|
}
|
|
2657
2827
|
for (const entry of entries) {
|
|
2658
2828
|
if (!entry.isDirectory())
|
|
2659
2829
|
continue;
|
|
2660
|
-
const compassPath =
|
|
2830
|
+
const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
|
|
2661
2831
|
let content;
|
|
2662
2832
|
try {
|
|
2663
2833
|
content = readFileSync8(compassPath, "utf8");
|
|
@@ -2737,8 +2907,8 @@ function buildIterationSnapshot(compass, rowById, rootUpdatedAt) {
|
|
|
2737
2907
|
id: compass.id,
|
|
2738
2908
|
type: "iteration",
|
|
2739
2909
|
status,
|
|
2740
|
-
file:
|
|
2741
|
-
source:
|
|
2910
|
+
file: join11("workflows", compass.id, WORKFLOW_SNAPSHOT_FILE),
|
|
2911
|
+
source: join11("iterations", compass.id, "delivery-compass.md"),
|
|
2742
2912
|
data: snapshot
|
|
2743
2913
|
};
|
|
2744
2914
|
}
|
|
@@ -2773,7 +2943,7 @@ function buildStandaloneSnapshot(row, rootUpdatedAt, migrationNotes) {
|
|
|
2773
2943
|
id,
|
|
2774
2944
|
type: "plan",
|
|
2775
2945
|
status,
|
|
2776
|
-
file:
|
|
2946
|
+
file: join11("workflows", id, WORKFLOW_SNAPSHOT_FILE),
|
|
2777
2947
|
source: "status.json plans[] row",
|
|
2778
2948
|
data: snapshot
|
|
2779
2949
|
};
|
|
@@ -2881,7 +3051,7 @@ function buildRegister(residualFindings, byPlan, projectId, migratedAt) {
|
|
|
2881
3051
|
return null;
|
|
2882
3052
|
const doc = { entries };
|
|
2883
3053
|
return {
|
|
2884
|
-
file:
|
|
3054
|
+
file: join11("projects", projectId, PROJECT_REGISTER_FILE),
|
|
2885
3055
|
source: "status.json residual_findings",
|
|
2886
3056
|
data: doc
|
|
2887
3057
|
};
|
|
@@ -2912,7 +3082,7 @@ function collectNotesFiles(snapshots) {
|
|
|
2912
3082
|
if (lines.length === 0)
|
|
2913
3083
|
continue;
|
|
2914
3084
|
out.push({
|
|
2915
|
-
file:
|
|
3085
|
+
file: join11(dirname7(snapshot.file), NOTES_LEDGER_FILE),
|
|
2916
3086
|
source,
|
|
2917
3087
|
lines
|
|
2918
3088
|
});
|
|
@@ -2920,11 +3090,11 @@ function collectNotesFiles(snapshots) {
|
|
|
2920
3090
|
return out;
|
|
2921
3091
|
}
|
|
2922
3092
|
function migrateHarnessTree(root, opts = {}) {
|
|
2923
|
-
const harnessDir =
|
|
3093
|
+
const harnessDir = resolve10(root);
|
|
2924
3094
|
const workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2925
3095
|
const projectDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
2926
3096
|
const projectId = opts.projectId ?? _DEFAULT_PROJECT;
|
|
2927
|
-
const statusPath =
|
|
3097
|
+
const statusPath = join11(harnessDir, MIGRATE_STATUS_FILE);
|
|
2928
3098
|
const legacy = readJson(statusPath);
|
|
2929
3099
|
if (legacy.version === 2) {
|
|
2930
3100
|
const updatedAt = typeof legacy.updated_at === "string" && legacy.updated_at !== "" ? legacy.updated_at : "1970-01-01";
|
|
@@ -3020,7 +3190,7 @@ function migrateHarnessTree(root, opts = {}) {
|
|
|
3020
3190
|
migrationNotes.push(`roadmap title sanitized for frontmatter (line breaks replaced with spaces): ${JSON.stringify(rawTitle)}`);
|
|
3021
3191
|
}
|
|
3022
3192
|
roadmap = {
|
|
3023
|
-
file:
|
|
3193
|
+
file: join11("projects", projectId, PROJECT_ROADMAP_FILE),
|
|
3024
3194
|
source: "status.json metadata.program_roadmap",
|
|
3025
3195
|
content: buildRoadmap({ ...programRoadmap, title: sanitizedTitle }, projectId, migratedAt)
|
|
3026
3196
|
};
|
|
@@ -3069,19 +3239,19 @@ async function applyMigratePlan(plan) {
|
|
|
3069
3239
|
if (plan.dryRun) {
|
|
3070
3240
|
return { applied: false, message: `dry-run: ${plan.steps.length} steps planned (source → destination), zero writes` };
|
|
3071
3241
|
}
|
|
3072
|
-
const statusPath =
|
|
3242
|
+
const statusPath = join11(plan.root, MIGRATE_STATUS_FILE);
|
|
3073
3243
|
const current = readJson(statusPath);
|
|
3074
3244
|
if (current.version === 2) {
|
|
3075
3245
|
return { applied: false, message: "no-op: status.json already at schema version 2 (migrated) — nothing to do" };
|
|
3076
3246
|
}
|
|
3077
|
-
const harnessRoot =
|
|
3078
|
-
const workflowRoot =
|
|
3079
|
-
const projectRoot =
|
|
3080
|
-
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)) {
|
|
3081
3251
|
throw new Error(`refusing to apply migration: plan workflowDir/projectDir must be absolute (got ${JSON.stringify(plan.workflowDir)} / ${JSON.stringify(plan.projectDir)})`);
|
|
3082
3252
|
}
|
|
3083
|
-
const workflowTargetOf = (canonicalFile) =>
|
|
3084
|
-
const projectTargetOf = (canonicalFile) =>
|
|
3253
|
+
const workflowTargetOf = (canonicalFile) => join11(workflowRoot, relative3("workflows", canonicalFile));
|
|
3254
|
+
const projectTargetOf = (canonicalFile) => join11(projectRoot, relative3("projects", canonicalFile));
|
|
3085
3255
|
const allDestinations = [
|
|
3086
3256
|
plan.archive.file,
|
|
3087
3257
|
...plan.snapshots.map((snapshot) => snapshot.file),
|
|
@@ -3090,14 +3260,14 @@ async function applyMigratePlan(plan) {
|
|
|
3090
3260
|
...plan.roadmap !== null ? [plan.roadmap.file] : []
|
|
3091
3261
|
];
|
|
3092
3262
|
for (const destination of allDestinations) {
|
|
3093
|
-
const resolvedDest =
|
|
3263
|
+
const resolvedDest = resolve10(join11(plan.root, destination));
|
|
3094
3264
|
const inside = (dir) => resolvedDest === dir || resolvedDest.startsWith(`${dir}${sep2}`);
|
|
3095
3265
|
if (!inside(harnessRoot) && !inside(workflowRoot) && !inside(projectRoot)) {
|
|
3096
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)})`);
|
|
3097
3267
|
}
|
|
3098
3268
|
}
|
|
3099
|
-
mkdirSync7(
|
|
3100
|
-
copyFileSync(statusPath,
|
|
3269
|
+
mkdirSync7(join11(plan.root, dirname7(plan.archive.file)), { recursive: true });
|
|
3270
|
+
copyFileSync(statusPath, join11(plan.root, plan.archive.file));
|
|
3101
3271
|
for (const snapshot of plan.snapshots) {
|
|
3102
3272
|
await writeWorkflowSnapshot(snapshot.data, dirname7(workflowTargetOf(snapshot.file)));
|
|
3103
3273
|
}
|
|
@@ -3546,8 +3716,8 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
3546
3716
|
}
|
|
3547
3717
|
// src/audit.ts
|
|
3548
3718
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
3549
|
-
import { existsSync as
|
|
3550
|
-
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";
|
|
3551
3721
|
function violation9(severity, code, message, fix) {
|
|
3552
3722
|
return { ok: false, severity, code, message, fix };
|
|
3553
3723
|
}
|
|
@@ -3848,7 +4018,7 @@ function scanSecrets(files) {
|
|
|
3848
4018
|
unreadableFiles++;
|
|
3849
4019
|
continue;
|
|
3850
4020
|
}
|
|
3851
|
-
const base =
|
|
4021
|
+
const base = basename5(file);
|
|
3852
4022
|
for (const entry of NEVER_COMMIT_FILENAMES) {
|
|
3853
4023
|
if (entry.re.test(base))
|
|
3854
4024
|
findings.push({ file, line: 1, type: entry.type });
|
|
@@ -3900,12 +4070,12 @@ var LOCKFILE_NAMES = [
|
|
|
3900
4070
|
function rootLockfiles(root) {
|
|
3901
4071
|
let entries;
|
|
3902
4072
|
try {
|
|
3903
|
-
entries =
|
|
4073
|
+
entries = readdirSync8(root, { withFileTypes: true });
|
|
3904
4074
|
} catch {
|
|
3905
4075
|
return [];
|
|
3906
4076
|
}
|
|
3907
4077
|
const names = new Set(LOCKFILE_NAMES);
|
|
3908
|
-
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));
|
|
3909
4079
|
if (present.length === 0)
|
|
3910
4080
|
return [];
|
|
3911
4081
|
try {
|
|
@@ -3914,7 +4084,7 @@ function rootLockfiles(root) {
|
|
|
3914
4084
|
encoding: "utf8",
|
|
3915
4085
|
stdio: ["ignore", "pipe", "ignore"]
|
|
3916
4086
|
}).split("\x00").filter((f) => f !== ""));
|
|
3917
|
-
return present.filter((p) => tracked.has(
|
|
4087
|
+
return present.filter((p) => tracked.has(basename5(p)));
|
|
3918
4088
|
} catch {
|
|
3919
4089
|
return present;
|
|
3920
4090
|
}
|
|
@@ -3930,17 +4100,17 @@ function supplyChainChecks(repoRoot) {
|
|
|
3930
4100
|
findings.push({ kind: "lockfile-duplicate", file: lockfiles.map((f) => f.replace(`${repoRoot}/`, "")).join(", ") });
|
|
3931
4101
|
violations.push(violation9("medium", "audit.supply.lockfile-duplicate", `multiple lockfiles at ${repoRoot}: ${lockfiles.join(", ")}`, "keep exactly one lockfile per package manager"));
|
|
3932
4102
|
}
|
|
3933
|
-
const workflowsDir =
|
|
4103
|
+
const workflowsDir = join12(repoRoot, ".github", "workflows");
|
|
3934
4104
|
let wfEntries = [];
|
|
3935
4105
|
try {
|
|
3936
|
-
wfEntries =
|
|
4106
|
+
wfEntries = readdirSync8(workflowsDir, { withFileTypes: true });
|
|
3937
4107
|
} catch {
|
|
3938
4108
|
wfEntries = [];
|
|
3939
4109
|
}
|
|
3940
4110
|
for (const entry of wfEntries) {
|
|
3941
4111
|
if (!entry.isFile() || !/\.(?:ya?ml)$/.test(entry.name))
|
|
3942
4112
|
continue;
|
|
3943
|
-
const wfPath =
|
|
4113
|
+
const wfPath = join12(workflowsDir, entry.name);
|
|
3944
4114
|
const relPath = `.github/workflows/${entry.name}`;
|
|
3945
4115
|
let text;
|
|
3946
4116
|
try {
|
|
@@ -4097,9 +4267,9 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4097
4267
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
4098
4268
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
4099
4269
|
mkdirSync8(outDir, { recursive: true });
|
|
4100
|
-
const existingReadme =
|
|
4101
|
-
const carried =
|
|
4102
|
-
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));
|
|
4103
4273
|
let next = existing.reduce((max, f) => Math.max(max, Number(f.slice(0, 3))), 0) + 1;
|
|
4104
4274
|
const redactedFindings = findings.map(redactFinding);
|
|
4105
4275
|
const written = [];
|
|
@@ -4115,13 +4285,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4115
4285
|
}
|
|
4116
4286
|
usedSlugs.add(slug);
|
|
4117
4287
|
const file = `${num}-${slug}.md`;
|
|
4118
|
-
writeFileSync6(
|
|
4288
|
+
writeFileSync6(join12(outDir, file), renderPlanFile(finding, plannedAt));
|
|
4119
4289
|
written.push(file);
|
|
4120
4290
|
next++;
|
|
4121
4291
|
}
|
|
4122
4292
|
const all = [...existing, ...written].sort();
|
|
4123
4293
|
const rows = all.map((file) => {
|
|
4124
|
-
const summary = readPlanFileSummary(
|
|
4294
|
+
const summary = readPlanFileSummary(join12(outDir, file));
|
|
4125
4295
|
const fields = summary.fields;
|
|
4126
4296
|
return {
|
|
4127
4297
|
num: file.slice(0, 3),
|
|
@@ -4155,7 +4325,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4155
4325
|
});
|
|
4156
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;
|
|
4157
4327
|
const hardeningCheckedLines = options.hardeningChecked !== undefined ? options.hardeningChecked.map((hc) => `- ${hc.kind}: ${escapeCell(redactText(hc.text))}`) : carried.hardeningChecked;
|
|
4158
|
-
writeFileSync6(
|
|
4328
|
+
writeFileSync6(join12(outDir, "README.md"), renderIndex({
|
|
4159
4329
|
date,
|
|
4160
4330
|
repoName: options.repoName ?? "repo",
|
|
4161
4331
|
repoShortSha: options.repoShortSha ?? "unknown",
|
|
@@ -4164,7 +4334,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
4164
4334
|
needsVerification: needsVerificationLines,
|
|
4165
4335
|
hardeningChecked: hardeningCheckedLines
|
|
4166
4336
|
}));
|
|
4167
|
-
return { outDir:
|
|
4337
|
+
return { outDir: resolve11(outDir), date, files: written, nextNumber: next };
|
|
4168
4338
|
}
|
|
4169
4339
|
async function promoteAuditPlans(outDir, selected, options) {
|
|
4170
4340
|
if (selected.length === 0) {
|
|
@@ -4173,19 +4343,19 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4173
4343
|
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
4174
4344
|
throw new Error("promoteAuditPlans: options.harnessDir is required (must contain status.json + workflows/)");
|
|
4175
4345
|
}
|
|
4176
|
-
const workflowId = options.workflowId ??
|
|
4346
|
+
const workflowId = options.workflowId ?? basename5(resolve11(outDir));
|
|
4177
4347
|
assertSafePathComponent(workflowId, "workflow id");
|
|
4178
|
-
const harnessDir =
|
|
4179
|
-
const statusPath =
|
|
4180
|
-
const workflowDir =
|
|
4181
|
-
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);
|
|
4182
4352
|
const planFiles = resolveSelectedPlanFiles(outDir, selected);
|
|
4183
4353
|
const indexRows = readExecutionOrderIndex(outDir);
|
|
4184
4354
|
const plans = planFiles.map((planFile) => {
|
|
4185
4355
|
const stem = planFile.replace(/\.md$/, "");
|
|
4186
4356
|
const num = stem.slice(0, 3);
|
|
4187
4357
|
const indexRow = indexRows.get(num);
|
|
4188
|
-
const title = indexRow?.title ?? readPlanFileSummary(
|
|
4358
|
+
const title = indexRow?.title ?? readPlanFileSummary(join12(outDir, planFile)).title;
|
|
4189
4359
|
return {
|
|
4190
4360
|
id: stem,
|
|
4191
4361
|
title,
|
|
@@ -4213,18 +4383,18 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4213
4383
|
if (!entryGate.ok) {
|
|
4214
4384
|
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
4215
4385
|
}
|
|
4216
|
-
await withStatusWriteLock(statusPath, () => {
|
|
4217
|
-
if (
|
|
4386
|
+
await withStatusWriteLock(statusPath, async () => {
|
|
4387
|
+
if (existsSync8(snapshotPath)) {
|
|
4218
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`);
|
|
4219
4389
|
}
|
|
4220
4390
|
mkdirSync8(workflowDir, { recursive: true });
|
|
4221
4391
|
try {
|
|
4222
4392
|
writeJson(snapshotPath, snapshot);
|
|
4223
|
-
registerWorkflowEntryLocked(statusPath, entry);
|
|
4393
|
+
await registerWorkflowEntryLocked(statusPath, entry);
|
|
4224
4394
|
} catch (error) {
|
|
4225
4395
|
rmSync(snapshotPath, { force: true });
|
|
4226
4396
|
try {
|
|
4227
|
-
if (
|
|
4397
|
+
if (readdirSync8(workflowDir).length === 0) {
|
|
4228
4398
|
rmdirSync2(workflowDir);
|
|
4229
4399
|
}
|
|
4230
4400
|
} catch {}
|
|
@@ -4235,7 +4405,7 @@ async function promoteAuditPlans(outDir, selected, options) {
|
|
|
4235
4405
|
return { workflowId, snapshotPath };
|
|
4236
4406
|
}
|
|
4237
4407
|
function resolveSelectedPlanFiles(outDir, selected) {
|
|
4238
|
-
const files =
|
|
4408
|
+
const files = readdirSync8(outDir).filter((f) => /^\d{3}-.*\.md$/.test(f)).sort();
|
|
4239
4409
|
const byNum = new Map;
|
|
4240
4410
|
const byStem = new Map;
|
|
4241
4411
|
for (const file of files) {
|
|
@@ -4250,7 +4420,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
4250
4420
|
for (const id of selected) {
|
|
4251
4421
|
const file = byNum.get(id) ?? byStem.get(id) ?? byStem.get(id.replace(/\.md$/, ""));
|
|
4252
4422
|
if (file === undefined) {
|
|
4253
|
-
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)}`);
|
|
4254
4424
|
}
|
|
4255
4425
|
if (!seen.has(file)) {
|
|
4256
4426
|
seen.add(file);
|
|
@@ -4260,7 +4430,7 @@ function resolveSelectedPlanFiles(outDir, selected) {
|
|
|
4260
4430
|
return resolved;
|
|
4261
4431
|
}
|
|
4262
4432
|
function readExecutionOrderIndex(outDir) {
|
|
4263
|
-
const readmePath =
|
|
4433
|
+
const readmePath = join12(outDir, "README.md");
|
|
4264
4434
|
let text;
|
|
4265
4435
|
try {
|
|
4266
4436
|
text = readFileSync9(readmePath, "utf8");
|
|
@@ -4289,7 +4459,7 @@ function readExecutionOrderIndex(outDir) {
|
|
|
4289
4459
|
return rows;
|
|
4290
4460
|
}
|
|
4291
4461
|
function planFileRel(outDir, planFile) {
|
|
4292
|
-
const resolved =
|
|
4462
|
+
const resolved = resolve11(outDir);
|
|
4293
4463
|
const parts = resolved.split(sep3);
|
|
4294
4464
|
const plansIdx = parts.lastIndexOf("plans");
|
|
4295
4465
|
if (plansIdx >= 0) {
|
|
@@ -4298,8 +4468,8 @@ function planFileRel(outDir, planFile) {
|
|
|
4298
4468
|
return planFile;
|
|
4299
4469
|
}
|
|
4300
4470
|
// src/compound.ts
|
|
4301
|
-
import { existsSync as
|
|
4302
|
-
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";
|
|
4303
4473
|
function violation10(severity, code, message, fix) {
|
|
4304
4474
|
return { ok: false, severity, code, message, fix };
|
|
4305
4475
|
}
|
|
@@ -4566,7 +4736,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4566
4736
|
if (ref === "" || seen.has(ref))
|
|
4567
4737
|
continue;
|
|
4568
4738
|
seen.add(ref);
|
|
4569
|
-
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)) {
|
|
4570
4740
|
continue;
|
|
4571
4741
|
}
|
|
4572
4742
|
if (ref.includes("/") || REF_EXT_RE.test(ref)) {
|
|
@@ -4585,7 +4755,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4585
4755
|
const dir = stack.pop();
|
|
4586
4756
|
let entries;
|
|
4587
4757
|
try {
|
|
4588
|
-
entries =
|
|
4758
|
+
entries = readdirSync9(dir, { withFileTypes: true });
|
|
4589
4759
|
} catch {
|
|
4590
4760
|
continue;
|
|
4591
4761
|
}
|
|
@@ -4594,7 +4764,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4594
4764
|
break;
|
|
4595
4765
|
if (entry.isDirectory()) {
|
|
4596
4766
|
if (!WALK_SKIP_DIRS.has(entry.name))
|
|
4597
|
-
stack.push(
|
|
4767
|
+
stack.push(join13(dir, entry.name));
|
|
4598
4768
|
} else if (!entry.isSymbolicLink()) {
|
|
4599
4769
|
const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
4600
4770
|
if (moduleNames.has(base))
|
|
@@ -4606,7 +4776,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
4606
4776
|
for (const { ref, isSymbol, module } of refs) {
|
|
4607
4777
|
if (!isSymbol || module === undefined) {
|
|
4608
4778
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
4609
|
-
if (
|
|
4779
|
+
if (existsSync9(resolve12(repoRoot, candidate))) {
|
|
4610
4780
|
checked++;
|
|
4611
4781
|
} else {
|
|
4612
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"));
|
|
@@ -4626,14 +4796,14 @@ function collectKnowledgeDocs(dir) {
|
|
|
4626
4796
|
const current = stack.pop();
|
|
4627
4797
|
let entries;
|
|
4628
4798
|
try {
|
|
4629
|
-
entries =
|
|
4799
|
+
entries = readdirSync9(current, { withFileTypes: true });
|
|
4630
4800
|
} catch {
|
|
4631
4801
|
continue;
|
|
4632
4802
|
}
|
|
4633
4803
|
for (const entry of entries) {
|
|
4634
4804
|
if (entry.isSymbolicLink())
|
|
4635
4805
|
continue;
|
|
4636
|
-
const full =
|
|
4806
|
+
const full = join13(current, entry.name);
|
|
4637
4807
|
if (entry.isDirectory()) {
|
|
4638
4808
|
stack.push(full);
|
|
4639
4809
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
@@ -4653,8 +4823,8 @@ function normalizeIndexRef(cell) {
|
|
|
4653
4823
|
}
|
|
4654
4824
|
function assertIndexRows(knowledgeDir) {
|
|
4655
4825
|
const violations = [];
|
|
4656
|
-
const readmePath =
|
|
4657
|
-
if (!
|
|
4826
|
+
const readmePath = join13(knowledgeDir, "README.md");
|
|
4827
|
+
if (!existsSync9(readmePath)) {
|
|
4658
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"));
|
|
4659
4829
|
return { ok: false, violations };
|
|
4660
4830
|
}
|
|
@@ -4679,19 +4849,19 @@ function assertIndexRows(knowledgeDir) {
|
|
|
4679
4849
|
}
|
|
4680
4850
|
function compoundRefreshScope(harnessDir, projectRoot) {
|
|
4681
4851
|
return [
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4852
|
+
join13(harnessDir, "knowledge"),
|
|
4853
|
+
join13(harnessDir, "knowledge", "README.md"),
|
|
4854
|
+
join13(projectRoot, "CONCEPTS.md"),
|
|
4855
|
+
join13(harnessDir, "status.json")
|
|
4686
4856
|
];
|
|
4687
4857
|
}
|
|
4688
4858
|
function isFileLikeRoot(root) {
|
|
4689
|
-
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(
|
|
4859
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename6(root));
|
|
4690
4860
|
}
|
|
4691
4861
|
function scopeGuard(path, allowedRoots) {
|
|
4692
|
-
const resolved =
|
|
4862
|
+
const resolved = resolve12(path);
|
|
4693
4863
|
for (const root of allowedRoots) {
|
|
4694
|
-
const r =
|
|
4864
|
+
const r = resolve12(root);
|
|
4695
4865
|
if (isFileLikeRoot(r)) {
|
|
4696
4866
|
if (resolved === r)
|
|
4697
4867
|
return { ok: true, violations: [] };
|
|
@@ -4939,8 +5109,8 @@ function lintStrategySections(docText) {
|
|
|
4939
5109
|
return { ok: violations.length === 0, violations };
|
|
4940
5110
|
}
|
|
4941
5111
|
// src/roles.ts
|
|
4942
|
-
import { existsSync as
|
|
4943
|
-
import { join as
|
|
5112
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
5113
|
+
import { join as join14 } from "node:path";
|
|
4944
5114
|
function violation12(severity, code, message, fix) {
|
|
4945
5115
|
return { ok: false, severity, code, message, fix };
|
|
4946
5116
|
}
|
|
@@ -4996,8 +5166,8 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
4996
5166
|
const violations = [];
|
|
4997
5167
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
4998
5168
|
for (const { agentId, reference } of mapping) {
|
|
4999
|
-
if (!
|
|
5000
|
-
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`));
|
|
5001
5171
|
}
|
|
5002
5172
|
}
|
|
5003
5173
|
for (const { family, memberIds } of families) {
|
|
@@ -5202,8 +5372,8 @@ function resolveAssetPath(skillName, relPath, host) {
|
|
|
5202
5372
|
return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
|
|
5203
5373
|
}
|
|
5204
5374
|
// src/prreview.ts
|
|
5205
|
-
import { readdirSync as
|
|
5206
|
-
import { isAbsolute as
|
|
5375
|
+
import { readdirSync as readdirSync10 } from "node:fs";
|
|
5376
|
+
import { isAbsolute as isAbsolute9, join as join15 } from "node:path";
|
|
5207
5377
|
var MERGE_CLASSES = ["must-fix", "should-fix", "nit"];
|
|
5208
5378
|
var PR_VERDICTS = ["ship it", "needs fixes", "blocked"];
|
|
5209
5379
|
var REVIEW_EMOJI = {
|
|
@@ -5240,6 +5410,162 @@ function computePrTally(input) {
|
|
|
5240
5410
|
` + `must-fix=${mustFix} should-fix=${shouldFix} nit=${nit} unverified=${unverified}`;
|
|
5241
5411
|
return { verdict, scorePct, tally: { mustFix, shouldFix, nit, unverified }, chatHeader };
|
|
5242
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
|
+
}
|
|
5243
5569
|
var SHORT_SHA_WIDTH = 7;
|
|
5244
5570
|
var DATE_RE6 = /^\d{4}-\d{2}-\d{2}$/;
|
|
5245
5571
|
function todayString4() {
|
|
@@ -5287,7 +5613,7 @@ function prReviewReportPath(opts) {
|
|
|
5287
5613
|
const sameStem = new RegExp(`^${escaped}(?:-r([0-9]+))?\\.md$`);
|
|
5288
5614
|
let dirents;
|
|
5289
5615
|
try {
|
|
5290
|
-
dirents =
|
|
5616
|
+
dirents = readdirSync10(opts.reportsDir, { withFileTypes: true });
|
|
5291
5617
|
} catch (error) {
|
|
5292
5618
|
if (error.code === "ENOENT")
|
|
5293
5619
|
dirents = [];
|
|
@@ -5303,7 +5629,7 @@ function prReviewReportPath(opts) {
|
|
|
5303
5629
|
}
|
|
5304
5630
|
const revision = maxRevision + 1;
|
|
5305
5631
|
const name = revision === 1 ? `${finalStem}.md` : `${finalStem}-r${revision}.md`;
|
|
5306
|
-
return
|
|
5632
|
+
return join15(opts.reportsDir, name);
|
|
5307
5633
|
}
|
|
5308
5634
|
var PR_TIERS = ["quick", "default", "deep"];
|
|
5309
5635
|
function violation14(severity, code, message, fix) {
|
|
@@ -5573,10 +5899,10 @@ function prReviewSeatPrompt(opts) {
|
|
|
5573
5899
|
}
|
|
5574
5900
|
const skillRoot = opts.skillRoot.trim();
|
|
5575
5901
|
const worktreePath = opts.worktreePath.trim();
|
|
5576
|
-
if (!
|
|
5902
|
+
if (!isAbsolute9(skillRoot)) {
|
|
5577
5903
|
throw new TypeError(`prReviewSeatPrompt: skillRoot must be an absolute path - got ${JSON.stringify(opts.skillRoot)}`);
|
|
5578
5904
|
}
|
|
5579
|
-
if (!
|
|
5905
|
+
if (!isAbsolute9(worktreePath)) {
|
|
5580
5906
|
throw new TypeError(`prReviewSeatPrompt: worktreePath must be an absolute path - got ${JSON.stringify(opts.worktreePath)}`);
|
|
5581
5907
|
}
|
|
5582
5908
|
const slug = `${domain}-${seat}`;
|
|
@@ -5600,14 +5926,14 @@ function prReviewSeatPrompt(opts) {
|
|
|
5600
5926
|
lines.push("");
|
|
5601
5927
|
lines.push("## Read first");
|
|
5602
5928
|
lines.push("");
|
|
5603
|
-
const prReviewRef =
|
|
5929
|
+
const prReviewRef = join15(skillRoot, "references", "pr-review.md");
|
|
5604
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";
|
|
5605
5931
|
lines.push(`1. \`${prReviewRef}\` — read at least these sections: ${sections}.`);
|
|
5606
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).`);
|
|
5607
5933
|
if (opts.stage === 2) {
|
|
5608
|
-
lines.push(`3. \`${
|
|
5934
|
+
lines.push(`3. \`${join15(skillRoot, "references", "finding-format.md")}\` — the template every finding follows.`);
|
|
5609
5935
|
if (opts.securitySeat === true) {
|
|
5610
|
-
lines.push(`4. \`${
|
|
5936
|
+
lines.push(`4. \`${join15(skillRoot, "references", "security-review.md")}\` — the security lens.`);
|
|
5611
5937
|
}
|
|
5612
5938
|
}
|
|
5613
5939
|
lines.push("");
|
|
@@ -5791,6 +6117,7 @@ export {
|
|
|
5791
6117
|
validateProjectRegister,
|
|
5792
6118
|
validatePrReviewReport,
|
|
5793
6119
|
validatePlanRow,
|
|
6120
|
+
validateMstarReviewV1,
|
|
5794
6121
|
validateIntegrationMergeLease,
|
|
5795
6122
|
validateGitignore,
|
|
5796
6123
|
validateFindingDoc,
|
|
@@ -5803,9 +6130,11 @@ export {
|
|
|
5803
6130
|
techDebtRollup,
|
|
5804
6131
|
taskReportExists,
|
|
5805
6132
|
taskBrief,
|
|
6133
|
+
synthesizeReview,
|
|
5806
6134
|
supplyChainChecks,
|
|
5807
6135
|
stripFrontmatter,
|
|
5808
6136
|
singleReviewSnapshot,
|
|
6137
|
+
setArtifactStore,
|
|
5809
6138
|
sddWorkspace,
|
|
5810
6139
|
scopeGuard,
|
|
5811
6140
|
scanSecrets,
|
|
@@ -5829,6 +6158,7 @@ export {
|
|
|
5829
6158
|
resolveHarnessDir,
|
|
5830
6159
|
resolveCompassEnforcement,
|
|
5831
6160
|
resolveAssetPath,
|
|
6161
|
+
resolveArtifactPath,
|
|
5832
6162
|
releaseLease,
|
|
5833
6163
|
registerWorkflow,
|
|
5834
6164
|
referenceExists,
|
|
@@ -5855,6 +6185,7 @@ export {
|
|
|
5855
6185
|
parseAssignmentBranchForms,
|
|
5856
6186
|
normalizeSeverity,
|
|
5857
6187
|
migrateHarnessTree,
|
|
6188
|
+
loadStoreModule,
|
|
5858
6189
|
listProjectReferenceFiles,
|
|
5859
6190
|
lintStrategySections,
|
|
5860
6191
|
lintSkillFrontmatter,
|
|
@@ -5865,6 +6196,7 @@ export {
|
|
|
5865
6196
|
l1PreDispatchCheck,
|
|
5866
6197
|
isReadOnlyAssignmentRole,
|
|
5867
6198
|
implementerSessionStickyRules,
|
|
6199
|
+
getArtifactStore,
|
|
5868
6200
|
findingsCleanupGate,
|
|
5869
6201
|
findTemporaryMarkers,
|
|
5870
6202
|
findSimplifyMarkers,
|
|
@@ -5875,6 +6207,7 @@ export {
|
|
|
5875
6207
|
emitGitignoreSnippet,
|
|
5876
6208
|
detectHost,
|
|
5877
6209
|
detectHarnessKind,
|
|
6210
|
+
createFsStore,
|
|
5878
6211
|
computePrTally,
|
|
5879
6212
|
compoundRefreshScope,
|
|
5880
6213
|
composeDispatchGate,
|
|
@@ -5890,6 +6223,7 @@ export {
|
|
|
5890
6223
|
assertLightDarkParity,
|
|
5891
6224
|
assertIndexRows,
|
|
5892
6225
|
assertIndexRowObligations,
|
|
6226
|
+
assertFsStorePath,
|
|
5893
6227
|
assertDefaultBranchProtected,
|
|
5894
6228
|
assertControlVsFeaturePath,
|
|
5895
6229
|
assertBranchAlignment,
|