@mstar-harness/dsh 3.8.3 → 3.9.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/README.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/client/panel/locale.d.ts +1 -1
- package/dist/client.js +5 -5
- package/dist/index.js +1293 -955
- package/dist/types.d.ts +2 -2
- package/harness-commands/iteration-drive.md +12 -6
- package/harness-commands/iteration-loop.md +10 -10
- package/harness-commands/iteration-start.md +10 -10
- package/harness-skills/mstar-artifacts/SKILL.md +3 -3
- package/harness-skills/mstar-artifacts/references/status-and-residuals.md +12 -8
- package/harness-skills/mstar-artifacts/templates/plan.main.md +2 -0
- package/harness-skills/mstar-branch-worktree/SKILL.md +69 -28
- package/harness-skills/mstar-branch-worktree/references/parallel-writable-pre-dispatch.md +9 -8
- package/harness-skills/mstar-conventions/SKILL.md +2 -2
- package/harness-skills/mstar-dispatch-gates/SKILL.md +1 -1
- package/harness-skills/mstar-engine-legacy/references/lease-protocol.md +12 -9
- package/harness-skills/mstar-engine-legacy/references/status-field-history.md +3 -3
- package/harness-skills/mstar-host/references/codex.md +6 -4
- package/harness-skills/mstar-host/references/dsh.md +1 -1
- package/harness-skills/mstar-iteration/SKILL.md +18 -12
- package/harness-skills/mstar-iteration/references/command-shared-invariants.md +2 -1
- package/harness-skills/mstar-iteration/references/phase-1-prepare.md +2 -0
- package/harness-skills/mstar-iteration/references/phase-2-worktree-lease.md +70 -46
- package/harness-skills/mstar-iteration/references/phase-3-iteration-close.md +4 -4
- package/harness-skills/mstar-iteration/references/phase-4-5-pr-delivery.md +6 -5
- package/harness-skills/mstar-iteration/references/phase-6-post-merge-close.md +71 -0
- package/harness-skills/mstar-review-qc/SKILL.md +1 -0
- package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +10 -8
- package/harness-skills/mstar-roles/references/project-manager.md +3 -2
- package/harness-skills/mstar-roles/references/prompt-engineer.md +1 -1
- package/harness-skills/mstar-roles/references/qa-engineer/acceptance-gate.md +15 -0
- package/harness-skills/mstar-roles/references/qa-engineer.md +3 -1
- package/harness-skills/mstar-sdd/SKILL.md +3 -1
- package/harness-skills/mstar-sdd/references/file-handoffs.md +21 -3
- package/harness-skills/mstar-sdd/references/implementer-continuation-prompt.md +1 -1
- package/harness-skills/mstar-sdd/references/implementer-prompt.md +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1122,8 +1122,8 @@ var require_lib2 = __commonJS(function(exports, module) {
|
|
|
1122
1122
|
});
|
|
1123
1123
|
|
|
1124
1124
|
// src/index.ts
|
|
1125
|
-
import { existsSync as
|
|
1126
|
-
import { join as
|
|
1125
|
+
import { existsSync as existsSync24, readFileSync as readFileSync19, readdirSync as readdirSync14 } from "node:fs";
|
|
1126
|
+
import { join as join28 } from "node:path";
|
|
1127
1127
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1128
1128
|
import {
|
|
1129
1129
|
apply as applySkillLocal,
|
|
@@ -1138,24 +1138,28 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
1138
1138
|
import { fileURLToPath } from "node:url";
|
|
1139
1139
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
1140
1140
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
1141
|
-
import { existsSync as
|
|
1141
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1142
1142
|
import { execFileSync } from "node:child_process";
|
|
1143
|
-
import { basename as basename3, dirname as dirname5, isAbsolute as
|
|
1144
|
-
import { existsSync as
|
|
1145
|
-
import {
|
|
1143
|
+
import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute5, join as join9, relative as relative2, resolve as resolve7 } from "node:path";
|
|
1144
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
1145
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
1146
|
+
import { dirname as dirname4, join as join6, resolve as resolve5, sep } from "node:path";
|
|
1147
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "node:path";
|
|
1146
1148
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1147
|
-
import { existsSync as
|
|
1148
|
-
import {
|
|
1149
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1150
|
+
import { isAbsolute as isAbsolute4, join as join5 } from "node:path";
|
|
1149
1151
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1150
|
-
import { existsSync as
|
|
1151
|
-
import { isAbsolute as
|
|
1152
|
+
import { existsSync as existsSync8, realpathSync as realpathSync3 } from "node:fs";
|
|
1153
|
+
import { isAbsolute as isAbsolute6, join as join10, resolve as resolve8 } from "node:path";
|
|
1154
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1155
|
+
import { join as join11, dirname as dirname7 } from "node:path";
|
|
1152
1156
|
import { execFileSync as execFileSync3, spawn } from "node:child_process";
|
|
1153
|
-
import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as
|
|
1154
|
-
import { basename as
|
|
1155
|
-
import { existsSync as
|
|
1156
|
-
import { basename as
|
|
1157
|
-
import { existsSync as
|
|
1158
|
-
import { join as
|
|
1157
|
+
import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync8, realpathSync as realpathSync4, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1158
|
+
import { basename as basename5, dirname as dirname8, isAbsolute as isAbsolute7, join as join12, relative as relative3, resolve as resolve9 } from "node:path";
|
|
1159
|
+
import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync11 } from "node:fs";
|
|
1160
|
+
import { basename as basename8, isAbsolute as isAbsolute9, join as join16, relative as relative6, resolve as resolve13, sep as sep4 } from "node:path";
|
|
1161
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
1162
|
+
import { join as join17 } from "node:path";
|
|
1159
1163
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
|
|
1160
1164
|
function applyEnforcement(gate, opts) {
|
|
1161
1165
|
return { ...gate, hardBlocked: opts.hard && gate.violations.length > 0 };
|
|
@@ -1292,445 +1296,168 @@ function isAtOrBelow(dir, root) {
|
|
|
1292
1296
|
const rel = relative(root, dir);
|
|
1293
1297
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1294
1298
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
var PLAN_STATUS_DONE = "Done";
|
|
1298
|
-
function typeName(value) {
|
|
1299
|
-
if (value === null)
|
|
1300
|
-
return "null";
|
|
1301
|
-
if (Array.isArray(value))
|
|
1302
|
-
return "array";
|
|
1303
|
-
return typeof value;
|
|
1304
|
-
}
|
|
1305
|
-
function validateCompassShape(doc) {
|
|
1306
|
-
const issues = [];
|
|
1307
|
-
const expectString = (key, opts = {}) => {
|
|
1308
|
-
const value = doc[key];
|
|
1309
|
-
if (typeof value !== "string") {
|
|
1310
|
-
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
if (opts.min !== undefined && value.length < opts.min) {
|
|
1314
|
-
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
1318
|
-
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
1319
|
-
}
|
|
1320
|
-
};
|
|
1321
|
-
expectString("iteration_id", { min: 1 });
|
|
1322
|
-
expectString("start_date", { regex: DATE_RE });
|
|
1323
|
-
const status = doc.status;
|
|
1324
|
-
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
1325
|
-
issues.push({
|
|
1326
|
-
path: ["status"],
|
|
1327
|
-
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
1328
|
-
});
|
|
1329
|
-
}
|
|
1330
|
-
expectString("iteration_base_branch", { min: 1 });
|
|
1331
|
-
expectString("target_branch", { min: 1 });
|
|
1332
|
-
const plans = doc.plans;
|
|
1333
|
-
if (plans !== undefined) {
|
|
1334
|
-
if (!Array.isArray(plans)) {
|
|
1335
|
-
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
1336
|
-
} else {
|
|
1337
|
-
plans.forEach((entry, index) => {
|
|
1338
|
-
if (typeof entry !== "string") {
|
|
1339
|
-
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
1340
|
-
} else if (entry.length < 1) {
|
|
1341
|
-
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
1342
|
-
}
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
const end_date = doc.end_date;
|
|
1347
|
-
if (end_date !== undefined) {
|
|
1348
|
-
if (typeof end_date !== "string") {
|
|
1349
|
-
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
1350
|
-
} else if (!DATE_RE.test(end_date)) {
|
|
1351
|
-
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE}` });
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
if (issues.length > 0)
|
|
1355
|
-
return { ok: false, issues };
|
|
1356
|
-
return {
|
|
1357
|
-
ok: true,
|
|
1358
|
-
data: {
|
|
1359
|
-
iteration_id: doc.iteration_id,
|
|
1360
|
-
start_date: doc.start_date,
|
|
1361
|
-
status,
|
|
1362
|
-
iteration_base_branch: doc.iteration_base_branch,
|
|
1363
|
-
target_branch: doc.target_branch,
|
|
1364
|
-
...plans !== undefined ? { plans } : {},
|
|
1365
|
-
...end_date !== undefined ? { end_date } : {}
|
|
1366
|
-
}
|
|
1367
|
-
};
|
|
1299
|
+
function isPlainObject(value) {
|
|
1300
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1368
1301
|
}
|
|
1369
1302
|
function violation(severity, code, message, fix) {
|
|
1370
1303
|
return { ok: false, severity, code, message, fix };
|
|
1371
1304
|
}
|
|
1372
|
-
function
|
|
1373
|
-
|
|
1305
|
+
function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
|
|
1306
|
+
if (value === undefined) {
|
|
1307
|
+
violations.push(violation("high", missingCode, `missing required field: ${field}`));
|
|
1308
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1309
|
+
violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1310
|
+
}
|
|
1374
1311
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1312
|
+
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
1313
|
+
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
1314
|
+
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
1315
|
+
function isValidClaimedAt(value) {
|
|
1316
|
+
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
1317
|
+
}
|
|
1318
|
+
function validateExecutionLease(lease) {
|
|
1319
|
+
const violations = [];
|
|
1320
|
+
if (!isPlainObject(lease)) {
|
|
1377
1321
|
return {
|
|
1378
1322
|
ok: false,
|
|
1379
1323
|
violations: [
|
|
1380
|
-
violation("
|
|
1324
|
+
violation("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
|
|
1381
1325
|
]
|
|
1382
1326
|
};
|
|
1383
1327
|
}
|
|
1384
|
-
|
|
1385
|
-
if (
|
|
1328
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
1329
|
+
if (lease.claimed_at === undefined) {
|
|
1330
|
+
violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1331
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1332
|
+
violations.push(violation("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
|
|
1333
|
+
}
|
|
1334
|
+
if (lease.worktree_path === undefined) {
|
|
1335
|
+
violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
1336
|
+
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
1337
|
+
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
1338
|
+
} else if (!isAbsolute2(lease.worktree_path)) {
|
|
1339
|
+
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (a Git checkout distinct from the main worktree and the integration worktree)"));
|
|
1340
|
+
}
|
|
1341
|
+
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
1342
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1343
|
+
violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1344
|
+
}
|
|
1345
|
+
return { ok: violations.length === 0, violations };
|
|
1346
|
+
}
|
|
1347
|
+
function validateIntegrationMergeLease(lease) {
|
|
1348
|
+
const violations = [];
|
|
1349
|
+
if (!isPlainObject(lease)) {
|
|
1386
1350
|
return {
|
|
1387
1351
|
ok: false,
|
|
1388
|
-
violations:
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
})
|
|
1352
|
+
violations: [
|
|
1353
|
+
violation("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
|
|
1354
|
+
]
|
|
1392
1355
|
};
|
|
1393
1356
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1357
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
1358
|
+
if (lease.claimed_at === undefined) {
|
|
1359
|
+
violations.push(violation("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1360
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1361
|
+
violations.push(violation("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
|
|
1398
1362
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1363
|
+
validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
1364
|
+
validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
1365
|
+
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
1366
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1367
|
+
violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1401
1368
|
}
|
|
1402
1369
|
return { ok: violations.length === 0, violations };
|
|
1403
1370
|
}
|
|
1404
|
-
function
|
|
1405
|
-
|
|
1406
|
-
return [];
|
|
1407
|
-
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
1371
|
+
function planExecutionLeaseLocations(row) {
|
|
1372
|
+
return { row: row.execution_lease };
|
|
1408
1373
|
}
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
if (
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1374
|
+
function verifyPlanExecutionLease(row, planId) {
|
|
1375
|
+
const { row: rowLease } = planExecutionLeaseLocations(row);
|
|
1376
|
+
const lease = rowLease;
|
|
1377
|
+
if (lease === undefined) {
|
|
1378
|
+
if (row.status === "InProgress") {
|
|
1379
|
+
return {
|
|
1380
|
+
ok: false,
|
|
1381
|
+
violations: [
|
|
1382
|
+
violation("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
|
|
1383
|
+
]
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
return {
|
|
1387
|
+
ok: false,
|
|
1388
|
+
violations: [
|
|
1389
|
+
violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
1390
|
+
]
|
|
1391
|
+
};
|
|
1418
1392
|
}
|
|
1419
|
-
return null;
|
|
1420
|
-
}
|
|
1421
|
-
function entryPlansAllDone(snapshotDoc, registered) {
|
|
1422
1393
|
const violations = [];
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1394
|
+
violations.push(...validateExecutionLease(lease).violations);
|
|
1395
|
+
return { ok: violations.length === 0, violations, lease };
|
|
1396
|
+
}
|
|
1397
|
+
var heldLockDirs = new AsyncLocalStorage;
|
|
1398
|
+
var BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"';
|
|
1399
|
+
var REQUIRED_FIELDS = [
|
|
1400
|
+
{ key: "executeAs", label: "Execute as", code: "execute-as" },
|
|
1401
|
+
{ key: "delegation", label: "Delegation", code: "delegation" },
|
|
1402
|
+
{ key: "taskCategory", label: "Task category", code: "task-category" }
|
|
1403
|
+
];
|
|
1404
|
+
function violation2(severity, code, message, fix) {
|
|
1405
|
+
return { ok: false, severity, code, message, fix };
|
|
1406
|
+
}
|
|
1407
|
+
function parseAssignmentFields(assignmentText) {
|
|
1408
|
+
const fields = {};
|
|
1409
|
+
for (const line of assignmentText.split(/\r?\n/)) {
|
|
1410
|
+
const match = line.match(/^[ \t]*(?:[-*][ \t]+)?\*\*\s*([^*:]+?)\s*\*\*\s*:\s*(.*)$/) ?? line.match(/^[ \t]*(?:[-*][ \t]+)?([A-Za-z][A-Za-z -]*?)\s*:\s*(.*)$/);
|
|
1411
|
+
if (!match)
|
|
1412
|
+
continue;
|
|
1413
|
+
const label = match[1].trim();
|
|
1414
|
+
const value = match[2].trim();
|
|
1415
|
+
const known = REQUIRED_FIELDS.find((f) => f.label === label);
|
|
1416
|
+
if (known) {
|
|
1417
|
+
fields[known.key] = value;
|
|
1431
1418
|
continue;
|
|
1432
1419
|
}
|
|
1433
|
-
if (
|
|
1434
|
-
|
|
1435
|
-
|
|
1420
|
+
if (label === "Working branch")
|
|
1421
|
+
fields.workingBranch = value;
|
|
1422
|
+
else if (label === "Branch policy")
|
|
1423
|
+
fields.branchPolicy = value;
|
|
1436
1424
|
}
|
|
1437
|
-
return
|
|
1425
|
+
return fields;
|
|
1438
1426
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1427
|
+
var ASSIGNMENT_ENFORCEMENT_BOLD_RE = /^[ \t]*(?:[-*][ \t]+)?\*\*\s*Enforcement\s*\*\*\s*:\s*(.*)$/m;
|
|
1428
|
+
var ASSIGNMENT_ENFORCEMENT_PLAIN_RE = /^[ \t]*(?:[-*][ \t]+)?Enforcement\s*:\s*(.*)$/m;
|
|
1429
|
+
var COMPASS_ENFORCEMENT_RE = /^enforcement\s*:\s*(.*)$/m;
|
|
1430
|
+
function enforcementValue(raw) {
|
|
1431
|
+
const value = raw.trim();
|
|
1432
|
+
const unquoted = value.replace(/^(['"])(.*)\1$/, "$2");
|
|
1433
|
+
return unquoted.trim().toLowerCase();
|
|
1441
1434
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1447
|
-
const endDate = compassDoc.end_date;
|
|
1448
|
-
if (typeof endDate !== "string" || !DATE_RE.test(endDate)) {
|
|
1449
|
-
violations.push(violation("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
|
|
1450
|
-
}
|
|
1451
|
-
return violations;
|
|
1435
|
+
var ASSIGNMENT_BODY_START_RE = /^(?:#{1,6}[ \t]+Task\b|-{3,}[ \t]*$|#[ \t])/m;
|
|
1436
|
+
function assignmentHeaderRegion(assignmentText) {
|
|
1437
|
+
const marker = assignmentText.match(ASSIGNMENT_BODY_START_RE);
|
|
1438
|
+
return marker !== null ? assignmentText.slice(0, marker.index) : assignmentText;
|
|
1452
1439
|
}
|
|
1453
|
-
function
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1440
|
+
function parseEnforcementFlag(text) {
|
|
1441
|
+
const bold = text.match(ASSIGNMENT_ENFORCEMENT_BOLD_RE);
|
|
1442
|
+
if (bold !== null)
|
|
1443
|
+
return { hard: enforcementValue(bold[1]) === "hard", source: "assignment" };
|
|
1444
|
+
const plain = text.match(ASSIGNMENT_ENFORCEMENT_PLAIN_RE);
|
|
1445
|
+
if (plain !== null)
|
|
1446
|
+
return { hard: enforcementValue(plain[1]) === "hard", source: "assignment" };
|
|
1447
|
+
const compass = text.match(COMPASS_ENFORCEMENT_RE);
|
|
1448
|
+
if (compass !== null)
|
|
1449
|
+
return { hard: enforcementValue(compass[1]) === "hard", source: "compass" };
|
|
1450
|
+
return { hard: false, source: "none" };
|
|
1462
1451
|
}
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
return violations;
|
|
1473
|
-
}
|
|
1474
|
-
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
1475
|
-
const registered = registeredPlanIds(compassDoc);
|
|
1476
|
-
const entryViolations = [
|
|
1477
|
-
...entryPlansAllDone(snapshotDoc, registered),
|
|
1478
|
-
...entryFrontmatterComplete(compassDoc)
|
|
1479
|
-
];
|
|
1480
|
-
const exitViolations = [
|
|
1481
|
-
...exitFrontmatterClosed(compassDoc),
|
|
1482
|
-
...exitBranchCheck(opts),
|
|
1483
|
-
...exitPrBaseCheck(compassDoc, opts)
|
|
1484
|
-
];
|
|
1485
|
-
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
1486
|
-
const row = findPlanRow(snapshotDoc, planId);
|
|
1487
|
-
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
1488
|
-
});
|
|
1489
|
-
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
1490
|
-
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
1491
|
-
let transition;
|
|
1492
|
-
if (!allPlansDone)
|
|
1493
|
-
transition = "phase-2-execute";
|
|
1494
|
-
else if (entry.ok && exit.ok)
|
|
1495
|
-
transition = "phase-4-pr-delivery";
|
|
1496
|
-
else
|
|
1497
|
-
transition = "phase-3-close";
|
|
1498
|
-
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
1499
|
-
return {
|
|
1500
|
-
transition,
|
|
1501
|
-
allPlansDone,
|
|
1502
|
-
entry,
|
|
1503
|
-
exit,
|
|
1504
|
-
ok: gateBlocking.length === 0,
|
|
1505
|
-
violations: gateBlocking
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
function parseCompassFrontmatter(filePath) {
|
|
1509
|
-
return parseCompassFrontmatterText(readFileSync3(filePath, "utf8"), filePath);
|
|
1510
|
-
}
|
|
1511
|
-
function parseCompassFrontmatterText(content, filePath) {
|
|
1512
|
-
const lines = content.split(/\r?\n/);
|
|
1513
|
-
if (lines[0]?.trim() !== "---") {
|
|
1514
|
-
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
1515
|
-
}
|
|
1516
|
-
const end = lines.indexOf("---", 1);
|
|
1517
|
-
if (end === -1) {
|
|
1518
|
-
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
1519
|
-
}
|
|
1520
|
-
const doc = {};
|
|
1521
|
-
let listKey = null;
|
|
1522
|
-
for (let i = 1;i < end; i += 1) {
|
|
1523
|
-
const line = lines[i] ?? "";
|
|
1524
|
-
if (!line.trim() || line.trim().startsWith("#"))
|
|
1525
|
-
continue;
|
|
1526
|
-
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
1527
|
-
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
1528
|
-
if (!Array.isArray(doc[listKey]))
|
|
1529
|
-
doc[listKey] = [];
|
|
1530
|
-
doc[listKey].push(item);
|
|
1531
|
-
continue;
|
|
1532
|
-
}
|
|
1533
|
-
listKey = null;
|
|
1534
|
-
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
1535
|
-
if (!kv) {
|
|
1536
|
-
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
1537
|
-
}
|
|
1538
|
-
const value = kv[2].trim();
|
|
1539
|
-
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
1540
|
-
listKey = value === "" ? kv[1] : null;
|
|
1541
|
-
}
|
|
1542
|
-
return doc;
|
|
1543
|
-
}
|
|
1544
|
-
function parseFlowArray(raw, filePath) {
|
|
1545
|
-
const inner = raw.slice(1, -1);
|
|
1546
|
-
if (/[[\]]/.test(inner)) {
|
|
1547
|
-
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
|
|
1548
|
-
}
|
|
1549
|
-
let quote = null;
|
|
1550
|
-
for (const ch of inner) {
|
|
1551
|
-
if (ch === '"' || ch === "'") {
|
|
1552
|
-
if (quote === null)
|
|
1553
|
-
quote = ch;
|
|
1554
|
-
else if (quote === ch)
|
|
1555
|
-
quote = null;
|
|
1556
|
-
} else if (ch === "," && quote !== null) {
|
|
1557
|
-
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
if (quote !== null) {
|
|
1561
|
-
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
1562
|
-
}
|
|
1563
|
-
const items = [];
|
|
1564
|
-
for (const part of inner.split(",")) {
|
|
1565
|
-
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
1566
|
-
if (item === "")
|
|
1567
|
-
continue;
|
|
1568
|
-
items.push(item);
|
|
1569
|
-
}
|
|
1570
|
-
return items;
|
|
1571
|
-
}
|
|
1572
|
-
function isPlainObject2(value) {
|
|
1573
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1574
|
-
}
|
|
1575
|
-
function violation2(severity, code, message, fix) {
|
|
1576
|
-
return { ok: false, severity, code, message, fix };
|
|
1577
|
-
}
|
|
1578
|
-
function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
|
|
1579
|
-
if (value === undefined) {
|
|
1580
|
-
violations.push(violation2("high", missingCode, `missing required field: ${field}`));
|
|
1581
|
-
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1582
|
-
violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
1586
|
-
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
1587
|
-
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
1588
|
-
function isValidClaimedAt(value) {
|
|
1589
|
-
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
1590
|
-
}
|
|
1591
|
-
function validateExecutionLease(lease) {
|
|
1592
|
-
const violations = [];
|
|
1593
|
-
if (!isPlainObject2(lease)) {
|
|
1594
|
-
return {
|
|
1595
|
-
ok: false,
|
|
1596
|
-
violations: [
|
|
1597
|
-
violation2("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
|
|
1598
|
-
]
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
1602
|
-
if (lease.claimed_at === undefined) {
|
|
1603
|
-
violations.push(violation2("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1604
|
-
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1605
|
-
violations.push(violation2("medium", "lease.execution-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T02:30:00Z) or a YYYY-MM-DD date"));
|
|
1606
|
-
}
|
|
1607
|
-
if (lease.worktree_path === undefined) {
|
|
1608
|
-
violations.push(violation2("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
1609
|
-
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
1610
|
-
violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
1611
|
-
} else if (!isAbsolute2(lease.worktree_path)) {
|
|
1612
|
-
violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be an absolute path — it identifies the dedicated feature-worktree root (and MUST differ from metadata.control_worktree_path)"));
|
|
1613
|
-
}
|
|
1614
|
-
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
1615
|
-
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1616
|
-
violations.push(violation2("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1617
|
-
}
|
|
1618
|
-
return { ok: violations.length === 0, violations };
|
|
1619
|
-
}
|
|
1620
|
-
function validateIntegrationMergeLease(lease) {
|
|
1621
|
-
const violations = [];
|
|
1622
|
-
if (!isPlainObject2(lease)) {
|
|
1623
|
-
return {
|
|
1624
|
-
ok: false,
|
|
1625
|
-
violations: [
|
|
1626
|
-
violation2("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
|
|
1627
|
-
]
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
1631
|
-
if (lease.claimed_at === undefined) {
|
|
1632
|
-
violations.push(violation2("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1633
|
-
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1634
|
-
violations.push(violation2("medium", "lease.merge-lease.invalid-claimed-at", "claimed_at must be an RFC 3339 UTC timestamp with explicit Z (e.g. 2026-07-22T04:00:00Z) or a YYYY-MM-DD date"));
|
|
1635
|
-
}
|
|
1636
|
-
validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
1637
|
-
validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
1638
|
-
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
1639
|
-
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1640
|
-
violations.push(violation2("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1641
|
-
}
|
|
1642
|
-
return { ok: violations.length === 0, violations };
|
|
1643
|
-
}
|
|
1644
|
-
function planExecutionLeaseLocations(row) {
|
|
1645
|
-
return { row: row.execution_lease };
|
|
1646
|
-
}
|
|
1647
|
-
function verifyPlanExecutionLease(row, planId) {
|
|
1648
|
-
const { row: rowLease } = planExecutionLeaseLocations(row);
|
|
1649
|
-
const lease = rowLease;
|
|
1650
|
-
if (lease === undefined) {
|
|
1651
|
-
if (row.status === "InProgress") {
|
|
1652
|
-
return {
|
|
1653
|
-
ok: false,
|
|
1654
|
-
violations: [
|
|
1655
|
-
violation2("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
|
|
1656
|
-
]
|
|
1657
|
-
};
|
|
1658
|
-
}
|
|
1659
|
-
return {
|
|
1660
|
-
ok: false,
|
|
1661
|
-
violations: [
|
|
1662
|
-
violation2("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
1663
|
-
]
|
|
1664
|
-
};
|
|
1665
|
-
}
|
|
1666
|
-
const violations = [];
|
|
1667
|
-
violations.push(...validateExecutionLease(lease).violations);
|
|
1668
|
-
return { ok: violations.length === 0, violations, lease };
|
|
1669
|
-
}
|
|
1670
|
-
var heldLockDirs = new AsyncLocalStorage;
|
|
1671
|
-
var BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"';
|
|
1672
|
-
var REQUIRED_FIELDS = [
|
|
1673
|
-
{ key: "executeAs", label: "Execute as", code: "execute-as" },
|
|
1674
|
-
{ key: "delegation", label: "Delegation", code: "delegation" },
|
|
1675
|
-
{ key: "taskCategory", label: "Task category", code: "task-category" }
|
|
1676
|
-
];
|
|
1677
|
-
function violation3(severity, code, message, fix) {
|
|
1678
|
-
return { ok: false, severity, code, message, fix };
|
|
1679
|
-
}
|
|
1680
|
-
function parseAssignmentFields(assignmentText) {
|
|
1681
|
-
const fields = {};
|
|
1682
|
-
for (const line of assignmentText.split(/\r?\n/)) {
|
|
1683
|
-
const match = line.match(/^[ \t]*(?:[-*][ \t]+)?\*\*\s*([^*:]+?)\s*\*\*\s*:\s*(.*)$/) ?? line.match(/^[ \t]*(?:[-*][ \t]+)?([A-Za-z][A-Za-z -]*?)\s*:\s*(.*)$/);
|
|
1684
|
-
if (!match)
|
|
1685
|
-
continue;
|
|
1686
|
-
const label = match[1].trim();
|
|
1687
|
-
const value = match[2].trim();
|
|
1688
|
-
const known = REQUIRED_FIELDS.find((f) => f.label === label);
|
|
1689
|
-
if (known) {
|
|
1690
|
-
fields[known.key] = value;
|
|
1691
|
-
continue;
|
|
1692
|
-
}
|
|
1693
|
-
if (label === "Working branch")
|
|
1694
|
-
fields.workingBranch = value;
|
|
1695
|
-
else if (label === "Branch policy")
|
|
1696
|
-
fields.branchPolicy = value;
|
|
1697
|
-
}
|
|
1698
|
-
return fields;
|
|
1699
|
-
}
|
|
1700
|
-
var ASSIGNMENT_ENFORCEMENT_BOLD_RE = /^[ \t]*(?:[-*][ \t]+)?\*\*\s*Enforcement\s*\*\*\s*:\s*(.*)$/m;
|
|
1701
|
-
var ASSIGNMENT_ENFORCEMENT_PLAIN_RE = /^[ \t]*(?:[-*][ \t]+)?Enforcement\s*:\s*(.*)$/m;
|
|
1702
|
-
var COMPASS_ENFORCEMENT_RE = /^enforcement\s*:\s*(.*)$/m;
|
|
1703
|
-
function enforcementValue(raw) {
|
|
1704
|
-
const value = raw.trim();
|
|
1705
|
-
const unquoted = value.replace(/^(['"])(.*)\1$/, "$2");
|
|
1706
|
-
return unquoted.trim().toLowerCase();
|
|
1707
|
-
}
|
|
1708
|
-
var ASSIGNMENT_BODY_START_RE = /^(?:#{1,6}[ \t]+Task\b|-{3,}[ \t]*$|#[ \t])/m;
|
|
1709
|
-
function assignmentHeaderRegion(assignmentText) {
|
|
1710
|
-
const marker = assignmentText.match(ASSIGNMENT_BODY_START_RE);
|
|
1711
|
-
return marker !== null ? assignmentText.slice(0, marker.index) : assignmentText;
|
|
1712
|
-
}
|
|
1713
|
-
function parseEnforcementFlag(text) {
|
|
1714
|
-
const bold = text.match(ASSIGNMENT_ENFORCEMENT_BOLD_RE);
|
|
1715
|
-
if (bold !== null)
|
|
1716
|
-
return { hard: enforcementValue(bold[1]) === "hard", source: "assignment" };
|
|
1717
|
-
const plain = text.match(ASSIGNMENT_ENFORCEMENT_PLAIN_RE);
|
|
1718
|
-
if (plain !== null)
|
|
1719
|
-
return { hard: enforcementValue(plain[1]) === "hard", source: "assignment" };
|
|
1720
|
-
const compass = text.match(COMPASS_ENFORCEMENT_RE);
|
|
1721
|
-
if (compass !== null)
|
|
1722
|
-
return { hard: enforcementValue(compass[1]) === "hard", source: "compass" };
|
|
1723
|
-
return { hard: false, source: "none" };
|
|
1724
|
-
}
|
|
1725
|
-
function requireField(violations, value, label, code) {
|
|
1726
|
-
if (value === undefined) {
|
|
1727
|
-
const v = violation3("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
|
|
1728
|
-
v.aliases = [`assignment.presence.missing-${code}`];
|
|
1729
|
-
violations.push(v);
|
|
1730
|
-
} else if (value === "") {
|
|
1731
|
-
const v = violation3("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
|
|
1732
|
-
v.aliases = [`assignment.presence.missing-${code}`];
|
|
1733
|
-
violations.push(v);
|
|
1452
|
+
function requireField(violations, value, label, code) {
|
|
1453
|
+
if (value === undefined) {
|
|
1454
|
+
const v = violation2("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
|
|
1455
|
+
v.aliases = [`assignment.presence.missing-${code}`];
|
|
1456
|
+
violations.push(v);
|
|
1457
|
+
} else if (value === "") {
|
|
1458
|
+
const v = violation2("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
|
|
1459
|
+
v.aliases = [`assignment.presence.missing-${code}`];
|
|
1460
|
+
violations.push(v);
|
|
1734
1461
|
}
|
|
1735
1462
|
}
|
|
1736
1463
|
function parseWorkingBranchValue(value) {
|
|
@@ -1787,20 +1514,20 @@ function validateAssignmentFields(assignmentText, opts = {}) {
|
|
|
1787
1514
|
const formCount = Number(workingPresent) + Number(policyPresent);
|
|
1788
1515
|
const forms = parseAssignmentBranchForms(assignmentText);
|
|
1789
1516
|
if (formCount === 0) {
|
|
1790
|
-
violations.push(
|
|
1517
|
+
violations.push(violation2("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
1791
1518
|
} else if (formCount > 1) {
|
|
1792
|
-
violations.push(
|
|
1519
|
+
violations.push(violation2("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) — exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
1793
1520
|
} else if (workingPresent) {
|
|
1794
1521
|
const create = forms.createForm;
|
|
1795
1522
|
if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
|
|
1796
|
-
violations.push(
|
|
1523
|
+
violations.push(violation2("high", "assignment.field.branch-missing-base", `create-form Working branch is incomplete: "${fields.workingBranch}" (expected "create <new-branch> from <base>")`, "write both the new branch name and the ancestor branch after `from` (main / existing feature branch / remote-tracking branch / `current`)"));
|
|
1797
1524
|
}
|
|
1798
1525
|
} else if (policyPresent) {
|
|
1799
1526
|
const direct = forms.directOn;
|
|
1800
1527
|
if (direct === undefined) {
|
|
1801
|
-
violations.push(
|
|
1528
|
+
violations.push(violation2("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
|
|
1802
1529
|
} else if (direct.reason === "") {
|
|
1803
|
-
violations.push(
|
|
1530
|
+
violations.push(violation2("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
|
|
1804
1531
|
}
|
|
1805
1532
|
}
|
|
1806
1533
|
}
|
|
@@ -1811,7 +1538,7 @@ function assertDefaultBranchProtected(branch, opts = {}) {
|
|
|
1811
1538
|
const violations = [];
|
|
1812
1539
|
const normalized = branch.trim();
|
|
1813
1540
|
if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
|
|
1814
|
-
violations.push(
|
|
1541
|
+
violations.push(violation2("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} — <reason>" to the Assignment, or use a feature branch`));
|
|
1815
1542
|
}
|
|
1816
1543
|
return { ok: violations.length === 0, violations };
|
|
1817
1544
|
}
|
|
@@ -1820,7 +1547,7 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
1820
1547
|
const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
|
|
1821
1548
|
let n;
|
|
1822
1549
|
if (mode === "") {
|
|
1823
|
-
violations.push(
|
|
1550
|
+
violations.push(violation2("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
|
|
1824
1551
|
} else if (mode === "sdd") {
|
|
1825
1552
|
n = 3;
|
|
1826
1553
|
} else if (mode === "inline") {
|
|
@@ -1828,14 +1555,14 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
1828
1555
|
} else if (mode === "targeted") {
|
|
1829
1556
|
const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
|
|
1830
1557
|
if (seats.length === 0) {
|
|
1831
|
-
violations.push(
|
|
1558
|
+
violations.push(violation2("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted — reviewers: <role-id>, …" to the Assignment and pass the seats'));
|
|
1832
1559
|
} else if (seats.length > 3) {
|
|
1833
|
-
violations.push(
|
|
1560
|
+
violations.push(violation2("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats — at most 3 (targeted re-review seats are the tri seats, N = 1–3)`, "list at most three reviewer seats for the targeted re-review"));
|
|
1834
1561
|
} else {
|
|
1835
1562
|
n = seats.length;
|
|
1836
1563
|
}
|
|
1837
1564
|
} else {
|
|
1838
|
-
violations.push(
|
|
1565
|
+
violations.push(violation2("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
|
|
1839
1566
|
}
|
|
1840
1567
|
return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
|
|
1841
1568
|
}
|
|
@@ -1883,7 +1610,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1883
1610
|
return {
|
|
1884
1611
|
ok: false,
|
|
1885
1612
|
violations: [
|
|
1886
|
-
|
|
1613
|
+
violation2("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")
|
|
1887
1614
|
]
|
|
1888
1615
|
};
|
|
1889
1616
|
}
|
|
@@ -1891,7 +1618,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1891
1618
|
return {
|
|
1892
1619
|
ok: false,
|
|
1893
1620
|
violations: [
|
|
1894
|
-
|
|
1621
|
+
violation2("critical", "dispatch.anti-recursion.self-type", `recursive dispatch refused: role binding "${subagentType}" equals Execute as "${executeAs}" (leaf executors must not re-invoke their own role)`, "complete the work in this session, or return Blocked to project-manager")
|
|
1895
1622
|
]
|
|
1896
1623
|
};
|
|
1897
1624
|
}
|
|
@@ -1901,45 +1628,50 @@ var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
|
1901
1628
|
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
1902
1629
|
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
1903
1630
|
var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
1904
|
-
function
|
|
1631
|
+
function isPlainObject2(value) {
|
|
1905
1632
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1906
1633
|
}
|
|
1907
|
-
function
|
|
1634
|
+
function violation3(severity, code, message, fix) {
|
|
1908
1635
|
return { ok: false, severity, code, message, fix };
|
|
1909
1636
|
}
|
|
1910
1637
|
function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
|
|
1911
1638
|
if (value === undefined) {
|
|
1912
|
-
violations.push(
|
|
1639
|
+
violations.push(violation3("high", missingCode, `missing required field: ${field}`));
|
|
1913
1640
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1914
|
-
violations.push(
|
|
1641
|
+
violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function validateWorktreePathValue(violations, value, field) {
|
|
1645
|
+
if (typeof value !== "string" || value.trim() === "" || !isAbsolute4(value)) {
|
|
1646
|
+
violations.push(violation3("high", "workflow.snapshot.invalid-integration-worktree-path", `${field} must be a non-empty absolute path — got ${JSON.stringify(value)}`, "record the absolute integration checkout path (integration_worktree_path)"));
|
|
1915
1647
|
}
|
|
1916
1648
|
}
|
|
1917
1649
|
function validateWorkflowSnapshot(doc) {
|
|
1918
1650
|
const violations = [];
|
|
1919
|
-
if (!
|
|
1651
|
+
if (!isPlainObject2(doc)) {
|
|
1920
1652
|
return {
|
|
1921
1653
|
ok: false,
|
|
1922
|
-
violations: [
|
|
1654
|
+
violations: [violation3("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
|
|
1923
1655
|
};
|
|
1924
1656
|
}
|
|
1925
1657
|
if (doc.schema_version === undefined) {
|
|
1926
|
-
violations.push(
|
|
1658
|
+
violations.push(violation3("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
|
|
1927
1659
|
} else if (doc.schema_version !== 1) {
|
|
1928
|
-
violations.push(
|
|
1660
|
+
violations.push(violation3("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 — got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
|
|
1929
1661
|
}
|
|
1930
1662
|
if (doc.version !== undefined) {
|
|
1931
|
-
violations.push(
|
|
1663
|
+
violations.push(violation3("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator — snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
|
|
1932
1664
|
}
|
|
1933
1665
|
validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
|
|
1934
1666
|
if (doc.type === undefined) {
|
|
1935
|
-
violations.push(
|
|
1667
|
+
violations.push(violation3("high", "workflow.snapshot.missing-type", "missing required field: type"));
|
|
1936
1668
|
} else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
|
|
1937
|
-
violations.push(
|
|
1669
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
|
|
1938
1670
|
}
|
|
1939
1671
|
if (doc.status === undefined) {
|
|
1940
|
-
violations.push(
|
|
1672
|
+
violations.push(violation3("high", "workflow.snapshot.missing-status", "missing required field: status"));
|
|
1941
1673
|
} else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
|
|
1942
|
-
violations.push(
|
|
1674
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
|
|
1943
1675
|
}
|
|
1944
1676
|
validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
|
|
1945
1677
|
validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
|
|
@@ -1947,44 +1679,54 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1947
1679
|
validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
|
|
1948
1680
|
}
|
|
1949
1681
|
if (doc.phase !== undefined && typeof doc.phase !== "string") {
|
|
1950
|
-
violations.push(
|
|
1682
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
|
|
1951
1683
|
}
|
|
1952
1684
|
if (doc.plans === undefined) {
|
|
1953
|
-
violations.push(
|
|
1685
|
+
violations.push(violation3("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
|
|
1954
1686
|
} else if (!Array.isArray(doc.plans)) {
|
|
1955
|
-
violations.push(
|
|
1687
|
+
violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
1956
1688
|
} else {
|
|
1957
1689
|
for (const row of doc.plans) {
|
|
1958
1690
|
violations.push(...validatePlanRow(row).violations);
|
|
1959
|
-
if (
|
|
1691
|
+
if (isPlainObject2(row) && row.execution_lease !== undefined) {
|
|
1960
1692
|
violations.push(...validateExecutionLease(row.execution_lease).violations);
|
|
1961
1693
|
}
|
|
1962
1694
|
}
|
|
1963
1695
|
}
|
|
1964
1696
|
if (doc.execution_policy !== undefined) {
|
|
1965
|
-
if (!
|
|
1966
|
-
violations.push(
|
|
1697
|
+
if (!isPlainObject2(doc.execution_policy)) {
|
|
1698
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
|
|
1967
1699
|
}
|
|
1968
1700
|
}
|
|
1969
1701
|
if (doc.integration_merge_lease !== undefined) {
|
|
1970
1702
|
violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
|
|
1971
1703
|
}
|
|
1972
1704
|
if (doc.branch !== undefined) {
|
|
1973
|
-
if (!
|
|
1974
|
-
violations.push(
|
|
1705
|
+
if (!isPlainObject2(doc.branch)) {
|
|
1706
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
1975
1707
|
} else {
|
|
1976
1708
|
for (const key of ["base", "integration", "target"]) {
|
|
1977
1709
|
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
1978
|
-
violations.push(
|
|
1710
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
1979
1711
|
}
|
|
1980
1712
|
}
|
|
1981
1713
|
}
|
|
1982
1714
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1715
|
+
const legacyWorktreePath = doc.control_worktree_path;
|
|
1716
|
+
const canonicalWorktreePath = doc.integration_worktree_path;
|
|
1717
|
+
if (legacyWorktreePath !== undefined && canonicalWorktreePath !== undefined) {
|
|
1718
|
+
violations.push(violation3("high", "workflow.snapshot.conflicting-worktree-paths", "both integration_worktree_path and the legacy control_worktree_path key are present — the canonical snapshot carries only integration_worktree_path (refused even when the values are equal)", "remove the legacy control_worktree_path key"));
|
|
1719
|
+
} else {
|
|
1720
|
+
if (canonicalWorktreePath !== undefined) {
|
|
1721
|
+
validateWorktreePathValue(violations, canonicalWorktreePath, "integration_worktree_path");
|
|
1722
|
+
}
|
|
1723
|
+
if (legacyWorktreePath !== undefined) {
|
|
1724
|
+
violations.push(violation3("medium", "workflow.snapshot.legacy-control-worktree-path", "legacy control_worktree_path is present — the canonical reader normalizes it to integration_worktree_path in memory; migrate on the next authorized write (writers emit only the canonical key)", "rename control_worktree_path to integration_worktree_path on the next authorized write"));
|
|
1725
|
+
validateWorktreePathValue(violations, legacyWorktreePath, "control_worktree_path (legacy alias)");
|
|
1726
|
+
}
|
|
1985
1727
|
}
|
|
1986
|
-
if (doc.legacy_metadata !== undefined && !
|
|
1987
|
-
violations.push(
|
|
1728
|
+
if (doc.legacy_metadata !== undefined && !isPlainObject2(doc.legacy_metadata)) {
|
|
1729
|
+
violations.push(violation3("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
|
|
1988
1730
|
}
|
|
1989
1731
|
if (doc.compass_ref !== undefined) {
|
|
1990
1732
|
validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
@@ -1992,30 +1734,73 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1992
1734
|
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
|
|
1993
1735
|
if (terminal) {
|
|
1994
1736
|
if (doc.ended_at === undefined) {
|
|
1995
|
-
violations.push(
|
|
1737
|
+
violations.push(violation3("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
|
|
1996
1738
|
}
|
|
1997
1739
|
if (Array.isArray(doc.plans)) {
|
|
1998
1740
|
for (const row of doc.plans) {
|
|
1999
|
-
if (
|
|
2000
|
-
violations.push(
|
|
1741
|
+
if (isPlainObject2(row) && row.execution_lease !== undefined) {
|
|
1742
|
+
violations.push(violation3("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) — release every lease before the lifecycle ends`));
|
|
2001
1743
|
}
|
|
2002
1744
|
}
|
|
2003
1745
|
}
|
|
2004
1746
|
if (doc.integration_merge_lease !== undefined) {
|
|
2005
|
-
violations.push(
|
|
1747
|
+
violations.push(violation3("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) — release the merge lease before the lifecycle ends"));
|
|
2006
1748
|
}
|
|
2007
1749
|
}
|
|
2008
1750
|
return { ok: violations.length === 0, violations };
|
|
2009
1751
|
}
|
|
2010
|
-
var
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1752
|
+
var LEGACY_WORKTREE_PATH_CODE = "workflow.snapshot.legacy-control-worktree-path";
|
|
1753
|
+
|
|
1754
|
+
class WorkflowSnapshotValidationError extends Error {
|
|
1755
|
+
violations;
|
|
1756
|
+
constructor(message, violations) {
|
|
1757
|
+
super(message);
|
|
1758
|
+
this.violations = violations;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function readWorkflowSnapshot(dir) {
|
|
1762
|
+
const snapshotPath = join5(dir, WORKFLOW_SNAPSHOT_FILE);
|
|
1763
|
+
if (!existsSync3(snapshotPath)) {
|
|
1764
|
+
throw new Error(`workflow snapshot not found: ${snapshotPath}`);
|
|
1765
|
+
}
|
|
1766
|
+
let doc;
|
|
1767
|
+
try {
|
|
1768
|
+
doc = JSON.parse(readFileSync3(snapshotPath, "utf8"));
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
throw new Error(`Invalid JSON in ${snapshotPath}: ${error.message}`);
|
|
1771
|
+
}
|
|
1772
|
+
return normalizeWorkflowSnapshot(doc, snapshotPath);
|
|
1773
|
+
}
|
|
1774
|
+
function normalizeWorkflowSnapshot(doc, snapshotPath) {
|
|
1775
|
+
const gate = validateWorkflowSnapshot(doc);
|
|
1776
|
+
const migration = gate.violations.filter((v) => v.code === LEGACY_WORKTREE_PATH_CODE);
|
|
1777
|
+
const blocking = gate.violations.filter((v) => v.code !== LEGACY_WORKTREE_PATH_CODE);
|
|
1778
|
+
if (blocking.length > 0) {
|
|
1779
|
+
const detail = blocking.map((v) => `${v.code}: ${v.message}`).join("; ");
|
|
1780
|
+
throw new WorkflowSnapshotValidationError(`refusing to read invalid workflow snapshot ${snapshotPath}: ${detail}`, blocking);
|
|
1781
|
+
}
|
|
1782
|
+
if (migration.length === 0) {
|
|
1783
|
+
return { snapshot: doc, diagnostics: [] };
|
|
1784
|
+
}
|
|
1785
|
+
const raw = doc;
|
|
1786
|
+
const { control_worktree_path: _legacy, ...rest } = raw;
|
|
1787
|
+
const normalized = { ...rest, integration_worktree_path: raw.control_worktree_path };
|
|
1788
|
+
const revalidated = validateWorkflowSnapshot(normalized);
|
|
1789
|
+
if (!revalidated.ok) {
|
|
1790
|
+
const detail = revalidated.violations.map((v) => `${v.code}: ${v.message}`).join("; ");
|
|
1791
|
+
throw new Error(`refusing to read workflow snapshot ${snapshotPath}: legacy normalization produced an invalid document: ${detail}`);
|
|
1792
|
+
}
|
|
1793
|
+
return { snapshot: normalized, diagnostics: migration };
|
|
1794
|
+
}
|
|
1795
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1796
|
+
var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
1797
|
+
var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
|
|
1798
|
+
var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
|
|
1799
|
+
function isPlainObject3(value) {
|
|
1800
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1801
|
+
}
|
|
1802
|
+
function violation4(severity, code, message, fix) {
|
|
1803
|
+
return { ok: false, severity, code, message, fix };
|
|
2019
1804
|
}
|
|
2020
1805
|
function normalizeSeverity(value) {
|
|
2021
1806
|
if (value === "warning")
|
|
@@ -2031,19 +1816,19 @@ function isOpenResidual(entry) {
|
|
|
2031
1816
|
}
|
|
2032
1817
|
function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
|
|
2033
1818
|
if (value === undefined) {
|
|
2034
|
-
violations.push(
|
|
1819
|
+
violations.push(violation4("high", missingCode, `missing required field: ${field}`));
|
|
2035
1820
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
2036
|
-
violations.push(
|
|
1821
|
+
violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
|
|
2037
1822
|
}
|
|
2038
1823
|
}
|
|
2039
1824
|
function validatePlanRow(row) {
|
|
2040
1825
|
const violations = [];
|
|
2041
|
-
if (!
|
|
2042
|
-
return { ok: false, violations: [
|
|
1826
|
+
if (!isPlainObject3(row)) {
|
|
1827
|
+
return { ok: false, violations: [violation4("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
2043
1828
|
}
|
|
2044
1829
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
2045
1830
|
if (id === undefined && planId === undefined) {
|
|
2046
|
-
violations.push(
|
|
1831
|
+
violations.push(violation4("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
|
|
2047
1832
|
} else {
|
|
2048
1833
|
if (id !== undefined) {
|
|
2049
1834
|
validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
|
|
@@ -2052,31 +1837,31 @@ function validatePlanRow(row) {
|
|
|
2052
1837
|
validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
|
|
2053
1838
|
}
|
|
2054
1839
|
if (id !== undefined && planId !== undefined && id !== planId) {
|
|
2055
|
-
violations.push(
|
|
1840
|
+
violations.push(violation4("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
|
|
2056
1841
|
}
|
|
2057
1842
|
}
|
|
2058
1843
|
validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
|
|
2059
1844
|
validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
|
|
2060
1845
|
if (status === undefined) {
|
|
2061
|
-
violations.push(
|
|
1846
|
+
violations.push(violation4("high", "status.plan-row.missing-status", "missing required field: status"));
|
|
2062
1847
|
} else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
|
|
2063
|
-
violations.push(
|
|
1848
|
+
violations.push(violation4("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
|
|
2064
1849
|
}
|
|
2065
|
-
if (metadata !== undefined && !
|
|
2066
|
-
violations.push(
|
|
1850
|
+
if (metadata !== undefined && !isPlainObject3(metadata)) {
|
|
1851
|
+
violations.push(violation4("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
2067
1852
|
}
|
|
2068
|
-
if (execution_lease !== undefined && !
|
|
2069
|
-
violations.push(
|
|
1853
|
+
if (execution_lease !== undefined && !isPlainObject3(execution_lease)) {
|
|
1854
|
+
violations.push(violation4("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
2070
1855
|
}
|
|
2071
1856
|
if (status === "Done" && execution_lease !== undefined) {
|
|
2072
|
-
violations.push(
|
|
1857
|
+
violations.push(violation4("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
|
|
2073
1858
|
}
|
|
2074
1859
|
return { ok: violations.length === 0, violations };
|
|
2075
1860
|
}
|
|
2076
1861
|
function validateResidual(entry) {
|
|
2077
1862
|
const violations = [];
|
|
2078
|
-
if (!
|
|
2079
|
-
return { ok: false, violations: [
|
|
1863
|
+
if (!isPlainObject3(entry)) {
|
|
1864
|
+
return { ok: false, violations: [violation4("high", "status.residual.invalid", "residual entry must be an object")] };
|
|
2080
1865
|
}
|
|
2081
1866
|
const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
|
|
2082
1867
|
validateNonEmptyString3(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
|
|
@@ -2085,42 +1870,42 @@ function validateResidual(entry) {
|
|
|
2085
1870
|
validateNonEmptyString3(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
|
|
2086
1871
|
validateNonEmptyString3(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
|
|
2087
1872
|
if (severity === undefined) {
|
|
2088
|
-
violations.push(
|
|
1873
|
+
violations.push(violation4("high", "status.residual.missing-severity", "missing required field: severity"));
|
|
2089
1874
|
} else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
|
|
2090
|
-
violations.push(
|
|
1875
|
+
violations.push(violation4("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
|
|
2091
1876
|
} else if (severity === "warning") {
|
|
2092
|
-
violations.push(
|
|
1877
|
+
violations.push(violation4("low", "status.residual.legacy-warning", `severity "warning" is legacy — forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' → 'low')`));
|
|
2093
1878
|
}
|
|
2094
1879
|
if (decision === undefined) {
|
|
2095
|
-
violations.push(
|
|
1880
|
+
violations.push(violation4("high", "status.residual.missing-decision", "missing required field: decision"));
|
|
2096
1881
|
} else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
|
|
2097
|
-
violations.push(
|
|
1882
|
+
violations.push(violation4("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
|
|
2098
1883
|
}
|
|
2099
1884
|
if (target === undefined) {
|
|
2100
|
-
violations.push(
|
|
1885
|
+
violations.push(violation4("high", "status.residual.missing-target", "missing required field: target"));
|
|
2101
1886
|
} else if (typeof target !== "string" && target !== null) {
|
|
2102
|
-
violations.push(
|
|
1887
|
+
violations.push(violation4("medium", "status.residual.invalid-target", "target must be a string or null"));
|
|
2103
1888
|
}
|
|
2104
1889
|
if (tracking === undefined) {
|
|
2105
|
-
violations.push(
|
|
1890
|
+
violations.push(violation4("high", "status.residual.missing-tracking", "missing required field: tracking"));
|
|
2106
1891
|
} else if (typeof tracking !== "string" && tracking !== null) {
|
|
2107
|
-
violations.push(
|
|
1892
|
+
violations.push(violation4("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
|
|
2108
1893
|
}
|
|
2109
1894
|
if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
|
|
2110
|
-
violations.push(
|
|
1895
|
+
violations.push(violation4("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
|
|
2111
1896
|
}
|
|
2112
|
-
if (closed_at !== undefined && (typeof closed_at !== "string" || !
|
|
2113
|
-
violations.push(
|
|
1897
|
+
if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE.test(closed_at))) {
|
|
1898
|
+
violations.push(violation4("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
|
|
2114
1899
|
}
|
|
2115
1900
|
if (lifecycle !== undefined) {
|
|
2116
1901
|
if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
|
|
2117
|
-
violations.push(
|
|
1902
|
+
violations.push(violation4("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
|
|
2118
1903
|
} else if (lifecycle !== "open") {
|
|
2119
1904
|
if (closed_at === undefined) {
|
|
2120
|
-
violations.push(
|
|
1905
|
+
violations.push(violation4("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
|
|
2121
1906
|
}
|
|
2122
1907
|
if (entry.closure_note === undefined) {
|
|
2123
|
-
violations.push(
|
|
1908
|
+
violations.push(violation4("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
|
|
2124
1909
|
}
|
|
2125
1910
|
}
|
|
2126
1911
|
}
|
|
@@ -2135,25 +1920,25 @@ function isHarnessRelativePath(dir) {
|
|
|
2135
1920
|
}
|
|
2136
1921
|
function validateWorkflowEntry(entry) {
|
|
2137
1922
|
const violations = [];
|
|
2138
|
-
if (!
|
|
1923
|
+
if (!isPlainObject3(entry)) {
|
|
2139
1924
|
return {
|
|
2140
1925
|
ok: false,
|
|
2141
|
-
violations: [
|
|
1926
|
+
violations: [violation4("high", "status.workflow.invalid", "workflow entry must be an object")]
|
|
2142
1927
|
};
|
|
2143
1928
|
}
|
|
2144
1929
|
validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
|
|
2145
1930
|
if (entry.type === undefined) {
|
|
2146
|
-
violations.push(
|
|
1931
|
+
violations.push(violation4("high", "status.workflow.missing-type", "missing required field: type"));
|
|
2147
1932
|
} else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
|
|
2148
|
-
violations.push(
|
|
1933
|
+
violations.push(violation4("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
|
|
2149
1934
|
}
|
|
2150
1935
|
validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
|
|
2151
1936
|
if (entry.dir === undefined) {
|
|
2152
|
-
violations.push(
|
|
1937
|
+
violations.push(violation4("high", "status.workflow.missing-dir", "missing required field: dir"));
|
|
2153
1938
|
} else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
|
|
2154
|
-
violations.push(
|
|
1939
|
+
violations.push(violation4("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
|
|
2155
1940
|
} else if (!isHarnessRelativePath(entry.dir)) {
|
|
2156
|
-
violations.push(
|
|
1941
|
+
violations.push(violation4("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
|
|
2157
1942
|
}
|
|
2158
1943
|
return { ok: violations.length === 0, violations };
|
|
2159
1944
|
}
|
|
@@ -2167,20 +1952,20 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
2167
1952
|
} catch (error) {
|
|
2168
1953
|
return {
|
|
2169
1954
|
ok: false,
|
|
2170
|
-
violations: [
|
|
1955
|
+
violations: [violation4("high", "status.invalid-json", error.message)]
|
|
2171
1956
|
};
|
|
2172
1957
|
}
|
|
2173
1958
|
} else {
|
|
2174
1959
|
doc = docOrPath;
|
|
2175
1960
|
}
|
|
2176
|
-
if (!
|
|
2177
|
-
return { ok: false, violations: [
|
|
1961
|
+
if (!isPlainObject3(doc)) {
|
|
1962
|
+
return { ok: false, violations: [violation4("high", "status.invalid-doc", "status document must be an object")] };
|
|
2178
1963
|
}
|
|
2179
1964
|
if (doc.version !== 2) {
|
|
2180
1965
|
return {
|
|
2181
1966
|
ok: false,
|
|
2182
1967
|
violations: [
|
|
2183
|
-
|
|
1968
|
+
violation4("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
|
|
2184
1969
|
]
|
|
2185
1970
|
};
|
|
2186
1971
|
}
|
|
@@ -2188,7 +1973,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
2188
1973
|
return {
|
|
2189
1974
|
ok: false,
|
|
2190
1975
|
violations: [
|
|
2191
|
-
|
|
1976
|
+
violation4("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
2192
1977
|
]
|
|
2193
1978
|
};
|
|
2194
1979
|
}
|
|
@@ -2196,122 +1981,399 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
2196
1981
|
return {
|
|
2197
1982
|
ok: false,
|
|
2198
1983
|
violations: [
|
|
2199
|
-
|
|
1984
|
+
violation4("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
|
|
2200
1985
|
]
|
|
2201
1986
|
};
|
|
2202
1987
|
}
|
|
2203
1988
|
const violations = [];
|
|
2204
1989
|
if (doc.updated_at === undefined) {
|
|
2205
|
-
violations.push(
|
|
2206
|
-
} else if (typeof doc.updated_at !== "string" || !
|
|
2207
|
-
violations.push(
|
|
1990
|
+
violations.push(violation4("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
1991
|
+
} else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
|
|
1992
|
+
violations.push(violation4("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
2208
1993
|
}
|
|
2209
1994
|
if (doc.workflows === undefined) {
|
|
2210
|
-
violations.push(
|
|
1995
|
+
violations.push(violation4("high", "status.missing-workflows", "missing required field: workflows"));
|
|
2211
1996
|
} else if (!Array.isArray(doc.workflows)) {
|
|
2212
|
-
violations.push(
|
|
1997
|
+
violations.push(violation4("high", "status.invalid-workflows", "workflows must be an array"));
|
|
2213
1998
|
} else {
|
|
2214
1999
|
const seen = new Set;
|
|
2215
2000
|
for (const entry of doc.workflows) {
|
|
2216
2001
|
violations.push(...validateWorkflowEntry(entry).violations);
|
|
2217
|
-
if (
|
|
2002
|
+
if (isPlainObject3(entry) && typeof entry.id === "string") {
|
|
2218
2003
|
if (seen.has(entry.id)) {
|
|
2219
|
-
violations.push(
|
|
2004
|
+
violations.push(violation4("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
|
|
2220
2005
|
}
|
|
2221
2006
|
seen.add(entry.id);
|
|
2222
2007
|
}
|
|
2223
2008
|
}
|
|
2224
2009
|
}
|
|
2225
|
-
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
2226
|
-
let realHarnessDir = null;
|
|
2227
|
-
try {
|
|
2228
|
-
realHarnessDir = realpathSync(harnessDir);
|
|
2229
|
-
} catch {}
|
|
2230
|
-
for (const entry of doc.workflows) {
|
|
2231
|
-
if (!
|
|
2232
|
-
continue;
|
|
2233
|
-
const relSnapshot =
|
|
2234
|
-
const snapshotPath =
|
|
2235
|
-
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
2236
|
-
let physical;
|
|
2237
|
-
try {
|
|
2238
|
-
physical = realpathSync(snapshotPath);
|
|
2239
|
-
} catch {
|
|
2240
|
-
violations.push(
|
|
2241
|
-
continue;
|
|
2242
|
-
}
|
|
2243
|
-
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
|
|
2244
|
-
violations.push(
|
|
2245
|
-
continue;
|
|
2246
|
-
}
|
|
2247
|
-
let snapshot;
|
|
2248
|
-
try {
|
|
2249
|
-
snapshot = readJson(snapshotPath);
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
violations.push(
|
|
2252
|
-
continue;
|
|
2253
|
-
}
|
|
2254
|
-
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
2255
|
-
violations.push(
|
|
2256
|
-
}
|
|
2257
|
-
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
2258
|
-
violations.push(
|
|
2259
|
-
}
|
|
2260
|
-
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
2261
|
-
violations.push(
|
|
2262
|
-
}
|
|
2010
|
+
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
2011
|
+
let realHarnessDir = null;
|
|
2012
|
+
try {
|
|
2013
|
+
realHarnessDir = realpathSync(harnessDir);
|
|
2014
|
+
} catch {}
|
|
2015
|
+
for (const entry of doc.workflows) {
|
|
2016
|
+
if (!isPlainObject3(entry) || typeof entry.dir !== "string")
|
|
2017
|
+
continue;
|
|
2018
|
+
const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
2019
|
+
const snapshotPath = join6(harnessDir, relSnapshot);
|
|
2020
|
+
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
2021
|
+
let physical;
|
|
2022
|
+
try {
|
|
2023
|
+
physical = realpathSync(snapshotPath);
|
|
2024
|
+
} catch {
|
|
2025
|
+
violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
|
|
2029
|
+
violations.push(violation4("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
let snapshot;
|
|
2033
|
+
try {
|
|
2034
|
+
snapshot = readJson(snapshotPath);
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
violations.push(violation4("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
2040
|
+
violations.push(violation4("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
|
|
2041
|
+
}
|
|
2042
|
+
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
2043
|
+
violations.push(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
|
|
2044
|
+
}
|
|
2045
|
+
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
2046
|
+
violations.push(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot — align them or remove the colliding workflow`));
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return { ok: violations.length === 0, violations };
|
|
2051
|
+
}
|
|
2052
|
+
var validateStatus = validateStatusV2;
|
|
2053
|
+
function resolveCompassEnforcement(harnessDir) {
|
|
2054
|
+
const iterationsDir = resolveIterationDir(harnessDir);
|
|
2055
|
+
if (!existsSync4(iterationsDir))
|
|
2056
|
+
return { hard: false, source: "none" };
|
|
2057
|
+
let entries;
|
|
2058
|
+
try {
|
|
2059
|
+
entries = readdirSync2(iterationsDir, { withFileTypes: true });
|
|
2060
|
+
} catch {
|
|
2061
|
+
return { hard: false, source: "none" };
|
|
2062
|
+
}
|
|
2063
|
+
for (const entry of entries) {
|
|
2064
|
+
if (!entry.isDirectory())
|
|
2065
|
+
continue;
|
|
2066
|
+
const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
|
|
2067
|
+
if (!existsSync4(compassPath))
|
|
2068
|
+
continue;
|
|
2069
|
+
let content;
|
|
2070
|
+
try {
|
|
2071
|
+
content = readFileSync4(compassPath, "utf8");
|
|
2072
|
+
} catch {
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2076
|
+
const fm = frontmatter !== null ? frontmatter[1] : "";
|
|
2077
|
+
if (!/^status[ \t]*:[ \t]*(?:active|locked)[ \t]*$/m.test(fm))
|
|
2078
|
+
continue;
|
|
2079
|
+
const flag = parseEnforcementFlag(fm);
|
|
2080
|
+
if (flag.hard)
|
|
2081
|
+
return flag;
|
|
2082
|
+
}
|
|
2083
|
+
return { hard: false, source: "none" };
|
|
2084
|
+
}
|
|
2085
|
+
function resolveMstarcEnforcement(harnessDir) {
|
|
2086
|
+
const dir = resolve5(harnessDir);
|
|
2087
|
+
const rc = loadMstarc(dir, dirname4(dir));
|
|
2088
|
+
const value = rc?.config.enforcement;
|
|
2089
|
+
if (value === "hard")
|
|
2090
|
+
return { hard: true, source: "mstarc" };
|
|
2091
|
+
if (value === "soft")
|
|
2092
|
+
return { hard: false, source: "mstarc" };
|
|
2093
|
+
return { hard: false, source: "none" };
|
|
2094
|
+
}
|
|
2095
|
+
function resolveRepoEnforcement(harnessDir) {
|
|
2096
|
+
const rc = resolveMstarcEnforcement(harnessDir);
|
|
2097
|
+
if (rc.source !== "none")
|
|
2098
|
+
return rc;
|
|
2099
|
+
return resolveCompassEnforcement(harnessDir);
|
|
2100
|
+
}
|
|
2101
|
+
var COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
2102
|
+
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2103
|
+
var PLAN_STATUS_DONE = "Done";
|
|
2104
|
+
function typeName(value) {
|
|
2105
|
+
if (value === null)
|
|
2106
|
+
return "null";
|
|
2107
|
+
if (Array.isArray(value))
|
|
2108
|
+
return "array";
|
|
2109
|
+
return typeof value;
|
|
2110
|
+
}
|
|
2111
|
+
function validateCompassShape(doc) {
|
|
2112
|
+
const issues = [];
|
|
2113
|
+
const expectString = (key, opts = {}) => {
|
|
2114
|
+
const value = doc[key];
|
|
2115
|
+
if (typeof value !== "string") {
|
|
2116
|
+
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (opts.min !== undefined && value.length < opts.min) {
|
|
2120
|
+
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
2124
|
+
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
expectString("iteration_id", { min: 1 });
|
|
2128
|
+
expectString("start_date", { regex: DATE_RE2 });
|
|
2129
|
+
const status = doc.status;
|
|
2130
|
+
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
2131
|
+
issues.push({
|
|
2132
|
+
path: ["status"],
|
|
2133
|
+
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
expectString("iteration_base_branch", { min: 1 });
|
|
2137
|
+
expectString("target_branch", { min: 1 });
|
|
2138
|
+
const plans = doc.plans;
|
|
2139
|
+
if (plans !== undefined) {
|
|
2140
|
+
if (!Array.isArray(plans)) {
|
|
2141
|
+
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
2142
|
+
} else {
|
|
2143
|
+
plans.forEach((entry, index) => {
|
|
2144
|
+
if (typeof entry !== "string") {
|
|
2145
|
+
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
2146
|
+
} else if (entry.length < 1) {
|
|
2147
|
+
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
const end_date = doc.end_date;
|
|
2153
|
+
if (end_date !== undefined) {
|
|
2154
|
+
if (typeof end_date !== "string") {
|
|
2155
|
+
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
2156
|
+
} else if (!DATE_RE2.test(end_date)) {
|
|
2157
|
+
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (issues.length > 0)
|
|
2161
|
+
return { ok: false, issues };
|
|
2162
|
+
return {
|
|
2163
|
+
ok: true,
|
|
2164
|
+
data: {
|
|
2165
|
+
iteration_id: doc.iteration_id,
|
|
2166
|
+
start_date: doc.start_date,
|
|
2167
|
+
status,
|
|
2168
|
+
iteration_base_branch: doc.iteration_base_branch,
|
|
2169
|
+
target_branch: doc.target_branch,
|
|
2170
|
+
...plans !== undefined ? { plans } : {},
|
|
2171
|
+
...end_date !== undefined ? { end_date } : {}
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
function violation5(severity, code, message, fix) {
|
|
2176
|
+
return { ok: false, severity, code, message, fix };
|
|
2177
|
+
}
|
|
2178
|
+
function isPlainObject4(value) {
|
|
2179
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2180
|
+
}
|
|
2181
|
+
function validateCompassFrontmatter(doc) {
|
|
2182
|
+
if (!isPlainObject4(doc)) {
|
|
2183
|
+
return {
|
|
2184
|
+
ok: false,
|
|
2185
|
+
violations: [
|
|
2186
|
+
violation5("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
|
|
2187
|
+
]
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
const parsed = validateCompassShape(doc);
|
|
2191
|
+
if (!parsed.ok) {
|
|
2192
|
+
return {
|
|
2193
|
+
ok: false,
|
|
2194
|
+
violations: parsed.issues.map((issue) => {
|
|
2195
|
+
const field = issue.path.join(".") || "(root)";
|
|
2196
|
+
return violation5("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration §1.3)`);
|
|
2197
|
+
})
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
const violations = [];
|
|
2201
|
+
const { status, end_date } = parsed.data;
|
|
2202
|
+
if (status === "completed" && end_date === undefined) {
|
|
2203
|
+
violations.push(violation5("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
|
|
2204
|
+
}
|
|
2205
|
+
if (status !== "completed" && end_date !== undefined) {
|
|
2206
|
+
violations.push(violation5("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
|
|
2207
|
+
}
|
|
2208
|
+
return { ok: violations.length === 0, violations };
|
|
2209
|
+
}
|
|
2210
|
+
function registeredPlanIds(compassDoc) {
|
|
2211
|
+
if (!Array.isArray(compassDoc.plans))
|
|
2212
|
+
return [];
|
|
2213
|
+
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
2214
|
+
}
|
|
2215
|
+
function findPlanRow(snapshotDoc, planId) {
|
|
2216
|
+
if (!Array.isArray(snapshotDoc.plans))
|
|
2217
|
+
return null;
|
|
2218
|
+
for (const row of snapshotDoc.plans) {
|
|
2219
|
+
if (!isPlainObject4(row))
|
|
2220
|
+
continue;
|
|
2221
|
+
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
2222
|
+
if (rowId === planId)
|
|
2223
|
+
return row;
|
|
2224
|
+
}
|
|
2225
|
+
return null;
|
|
2226
|
+
}
|
|
2227
|
+
function entryPlansAllDone(snapshotDoc, registered) {
|
|
2228
|
+
const violations = [];
|
|
2229
|
+
if (registered.length === 0) {
|
|
2230
|
+
violations.push(violation5("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
|
|
2231
|
+
return violations;
|
|
2232
|
+
}
|
|
2233
|
+
for (const planId of registered) {
|
|
2234
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
2235
|
+
if (row === null) {
|
|
2236
|
+
violations.push(violation5("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
if (row.status !== PLAN_STATUS_DONE) {
|
|
2240
|
+
violations.push(violation5("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return violations;
|
|
2244
|
+
}
|
|
2245
|
+
function entryFrontmatterComplete(compassDoc) {
|
|
2246
|
+
return validateCompassFrontmatter(compassDoc).violations;
|
|
2247
|
+
}
|
|
2248
|
+
function exitFrontmatterClosed(compassDoc) {
|
|
2249
|
+
const violations = [];
|
|
2250
|
+
if (compassDoc.status !== "completed") {
|
|
2251
|
+
violations.push(violation5("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
|
|
2252
|
+
}
|
|
2253
|
+
const endDate = compassDoc.end_date;
|
|
2254
|
+
if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
|
|
2255
|
+
violations.push(violation5("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
|
|
2256
|
+
}
|
|
2257
|
+
return violations;
|
|
2258
|
+
}
|
|
2259
|
+
function exitBranchCheck(opts) {
|
|
2260
|
+
const violations = [];
|
|
2261
|
+
const { currentBranch, specIntegrationBranch } = opts;
|
|
2262
|
+
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
2263
|
+
violations.push(violation5("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
|
|
2264
|
+
} else if (currentBranch !== specIntegrationBranch) {
|
|
2265
|
+
violations.push(violation5("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
|
|
2266
|
+
}
|
|
2267
|
+
return violations;
|
|
2268
|
+
}
|
|
2269
|
+
function exitPrBaseCheck(compassDoc, opts) {
|
|
2270
|
+
const violations = [];
|
|
2271
|
+
const target = compassDoc.target_branch;
|
|
2272
|
+
const { prBaseBranch } = opts;
|
|
2273
|
+
if (prBaseBranch === undefined) {
|
|
2274
|
+
violations.push(violation5("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
|
|
2275
|
+
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
2276
|
+
violations.push(violation5("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
|
|
2277
|
+
}
|
|
2278
|
+
return violations;
|
|
2279
|
+
}
|
|
2280
|
+
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
2281
|
+
const registered = registeredPlanIds(compassDoc);
|
|
2282
|
+
const entryViolations = [
|
|
2283
|
+
...entryPlansAllDone(snapshotDoc, registered),
|
|
2284
|
+
...entryFrontmatterComplete(compassDoc)
|
|
2285
|
+
];
|
|
2286
|
+
const exitViolations = [
|
|
2287
|
+
...exitFrontmatterClosed(compassDoc),
|
|
2288
|
+
...exitBranchCheck(opts),
|
|
2289
|
+
...exitPrBaseCheck(compassDoc, opts)
|
|
2290
|
+
];
|
|
2291
|
+
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
2292
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
2293
|
+
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
2294
|
+
});
|
|
2295
|
+
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
2296
|
+
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
2297
|
+
let transition;
|
|
2298
|
+
if (!allPlansDone)
|
|
2299
|
+
transition = "phase-2-execute";
|
|
2300
|
+
else if (entry.ok && exit.ok)
|
|
2301
|
+
transition = "phase-4-pr-delivery";
|
|
2302
|
+
else
|
|
2303
|
+
transition = "phase-3-close";
|
|
2304
|
+
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
2305
|
+
return {
|
|
2306
|
+
transition,
|
|
2307
|
+
allPlansDone,
|
|
2308
|
+
entry,
|
|
2309
|
+
exit,
|
|
2310
|
+
ok: gateBlocking.length === 0,
|
|
2311
|
+
violations: gateBlocking
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
function parseCompassFrontmatter(filePath) {
|
|
2315
|
+
return parseCompassFrontmatterText(readFileSync5(filePath, "utf8"), filePath);
|
|
2316
|
+
}
|
|
2317
|
+
function parseCompassFrontmatterText(content, filePath) {
|
|
2318
|
+
const lines = content.split(/\r?\n/);
|
|
2319
|
+
if (lines[0]?.trim() !== "---") {
|
|
2320
|
+
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
2321
|
+
}
|
|
2322
|
+
const end = lines.indexOf("---", 1);
|
|
2323
|
+
if (end === -1) {
|
|
2324
|
+
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
2325
|
+
}
|
|
2326
|
+
const doc = {};
|
|
2327
|
+
let listKey = null;
|
|
2328
|
+
for (let i = 1;i < end; i += 1) {
|
|
2329
|
+
const line = lines[i] ?? "";
|
|
2330
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
2331
|
+
continue;
|
|
2332
|
+
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
2333
|
+
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
2334
|
+
if (!Array.isArray(doc[listKey]))
|
|
2335
|
+
doc[listKey] = [];
|
|
2336
|
+
doc[listKey].push(item);
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
listKey = null;
|
|
2340
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
2341
|
+
if (!kv) {
|
|
2342
|
+
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
2263
2343
|
}
|
|
2344
|
+
const value = kv[2].trim();
|
|
2345
|
+
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
2346
|
+
listKey = value === "" ? kv[1] : null;
|
|
2264
2347
|
}
|
|
2265
|
-
return
|
|
2348
|
+
return doc;
|
|
2266
2349
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
return { hard: false, source: "none" };
|
|
2272
|
-
let entries;
|
|
2273
|
-
try {
|
|
2274
|
-
entries = readdirSync3(iterationsDir, { withFileTypes: true });
|
|
2275
|
-
} catch {
|
|
2276
|
-
return { hard: false, source: "none" };
|
|
2350
|
+
function parseFlowArray(raw, filePath) {
|
|
2351
|
+
const inner = raw.slice(1, -1);
|
|
2352
|
+
if (/[[\]]/.test(inner)) {
|
|
2353
|
+
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
|
|
2277
2354
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
} catch {
|
|
2288
|
-
continue;
|
|
2355
|
+
let quote = null;
|
|
2356
|
+
for (const ch of inner) {
|
|
2357
|
+
if (ch === '"' || ch === "'") {
|
|
2358
|
+
if (quote === null)
|
|
2359
|
+
quote = ch;
|
|
2360
|
+
else if (quote === ch)
|
|
2361
|
+
quote = null;
|
|
2362
|
+
} else if (ch === "," && quote !== null) {
|
|
2363
|
+
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
2289
2364
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2365
|
+
}
|
|
2366
|
+
if (quote !== null) {
|
|
2367
|
+
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
2368
|
+
}
|
|
2369
|
+
const items = [];
|
|
2370
|
+
for (const part of inner.split(",")) {
|
|
2371
|
+
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
2372
|
+
if (item === "")
|
|
2293
2373
|
continue;
|
|
2294
|
-
|
|
2295
|
-
if (flag.hard)
|
|
2296
|
-
return flag;
|
|
2374
|
+
items.push(item);
|
|
2297
2375
|
}
|
|
2298
|
-
return
|
|
2299
|
-
}
|
|
2300
|
-
function resolveMstarcEnforcement(harnessDir) {
|
|
2301
|
-
const dir = resolve5(harnessDir);
|
|
2302
|
-
const rc = loadMstarc(dir, dirname4(dir));
|
|
2303
|
-
const value = rc?.config.enforcement;
|
|
2304
|
-
if (value === "hard")
|
|
2305
|
-
return { hard: true, source: "mstarc" };
|
|
2306
|
-
if (value === "soft")
|
|
2307
|
-
return { hard: false, source: "mstarc" };
|
|
2308
|
-
return { hard: false, source: "none" };
|
|
2309
|
-
}
|
|
2310
|
-
function resolveRepoEnforcement(harnessDir) {
|
|
2311
|
-
const rc = resolveMstarcEnforcement(harnessDir);
|
|
2312
|
-
if (rc.source !== "none")
|
|
2313
|
-
return rc;
|
|
2314
|
-
return resolveCompassEnforcement(harnessDir);
|
|
2376
|
+
return items;
|
|
2315
2377
|
}
|
|
2316
2378
|
var PROJECT_ROADMAP_FILE = "roadmap.md";
|
|
2317
2379
|
var PROJECT_REGISTER_FILE = "residuals.json";
|
|
@@ -2454,7 +2516,7 @@ function defaultWorkspaceRoot(startDir) {
|
|
|
2454
2516
|
}
|
|
2455
2517
|
function isAtOrBelow2(dir, root) {
|
|
2456
2518
|
const rel = relative2(root, dir);
|
|
2457
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
2519
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
2458
2520
|
}
|
|
2459
2521
|
function mstarcDirOverride(harnessDir, key) {
|
|
2460
2522
|
const dir = resolve7(harnessDir);
|
|
@@ -2472,7 +2534,7 @@ function canonicalizeNearestExisting(path) {
|
|
|
2472
2534
|
let dir = abs;
|
|
2473
2535
|
const tail = [];
|
|
2474
2536
|
for (;; ) {
|
|
2475
|
-
if (
|
|
2537
|
+
if (existsSync7(dir)) {
|
|
2476
2538
|
try {
|
|
2477
2539
|
return join9(realpathSync2(dir), ...tail);
|
|
2478
2540
|
} catch {
|
|
@@ -2545,15 +2607,74 @@ function isDirectory(dir) {
|
|
|
2545
2607
|
return false;
|
|
2546
2608
|
}
|
|
2547
2609
|
}
|
|
2610
|
+
var INPUT_ENTRIES_CAP = 1e4;
|
|
2611
|
+
var MAX_RETAINED_ENTRIES = INPUT_ENTRIES_CAP / 2;
|
|
2548
2612
|
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
2549
|
-
function
|
|
2613
|
+
function gitProbeTimeoutMs() {
|
|
2550
2614
|
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
2551
2615
|
if (raw === undefined || raw.trim() === "")
|
|
2552
2616
|
return DEFAULT_PROBE_TIMEOUT_MS;
|
|
2553
2617
|
const parsed = Number(raw);
|
|
2554
2618
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
2555
2619
|
}
|
|
2556
|
-
function
|
|
2620
|
+
function parseMainWorktree(out) {
|
|
2621
|
+
const tokens = out.split("\x00");
|
|
2622
|
+
const first = tokens.findIndex((t) => t.startsWith("worktree "));
|
|
2623
|
+
if (first === -1)
|
|
2624
|
+
return null;
|
|
2625
|
+
const rawPath = tokens[first].slice("worktree ".length);
|
|
2626
|
+
if (rawPath.trim() === "")
|
|
2627
|
+
return null;
|
|
2628
|
+
let branch = null;
|
|
2629
|
+
let detached = false;
|
|
2630
|
+
for (let i = first + 1;i < tokens.length; i++) {
|
|
2631
|
+
const token = tokens[i];
|
|
2632
|
+
if (token.startsWith("worktree "))
|
|
2633
|
+
break;
|
|
2634
|
+
if (token === "bare")
|
|
2635
|
+
return null;
|
|
2636
|
+
if (token === "detached")
|
|
2637
|
+
detached = true;
|
|
2638
|
+
else if (token.startsWith("branch ")) {
|
|
2639
|
+
const ref = token.slice("branch ".length).trim();
|
|
2640
|
+
if (ref === "")
|
|
2641
|
+
return null;
|
|
2642
|
+
branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
if (detached)
|
|
2646
|
+
branch = "";
|
|
2647
|
+
if (branch === null)
|
|
2648
|
+
return null;
|
|
2649
|
+
try {
|
|
2650
|
+
return { root: realpathSync3(rawPath), branch };
|
|
2651
|
+
} catch {
|
|
2652
|
+
return null;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
function readMainWorktree(cwd) {
|
|
2656
|
+
const start = cwd ?? process.cwd();
|
|
2657
|
+
try {
|
|
2658
|
+
const stdout = execFileSync2("git", ["-C", start, "worktree", "list", "--porcelain", "-z"], {
|
|
2659
|
+
encoding: "utf8",
|
|
2660
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2661
|
+
timeout: gitProbeTimeoutMs()
|
|
2662
|
+
});
|
|
2663
|
+
return parseMainWorktree(stdout);
|
|
2664
|
+
} catch {
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function assertMainWorktreeResidency(main, expectedBranch) {
|
|
2669
|
+
const violations = [];
|
|
2670
|
+
if (main.branch === "") {
|
|
2671
|
+
violations.push(violation8("high", "worktree.main.residency-switched", `main worktree "${main.root}" is detached (no branch checked out) — residency cannot match the recorded expectation ${JSON.stringify(expectedBranch)}`, "check out the recorded main-worktree branch in the main worktree"));
|
|
2672
|
+
} else if (main.branch !== expectedBranch) {
|
|
2673
|
+
violations.push(violation8("high", "worktree.main.residency-switched", `main worktree "${main.root}" is on branch "${main.branch}", expected the recorded branch "${expectedBranch}" — residency is checked against the value recorded at lifecycle start, never re-pointed at the observed branch`, "restore the recorded branch in the main worktree; re-record at lifecycle start only through the owning workflow"));
|
|
2674
|
+
}
|
|
2675
|
+
return gate(violations);
|
|
2676
|
+
}
|
|
2677
|
+
function violation8(severity, code, message, fix) {
|
|
2557
2678
|
return { ok: false, severity, code, message, fix };
|
|
2558
2679
|
}
|
|
2559
2680
|
function gate(violations) {
|
|
@@ -2563,7 +2684,7 @@ function probeBranch(worktreePath, opts) {
|
|
|
2563
2684
|
const precomputed = opts.branchOf?.(worktreePath);
|
|
2564
2685
|
if (precomputed !== undefined)
|
|
2565
2686
|
return { branch: precomputed };
|
|
2566
|
-
const timeout = opts.timeoutMs ??
|
|
2687
|
+
const timeout = opts.timeoutMs ?? gitProbeTimeoutMs();
|
|
2567
2688
|
try {
|
|
2568
2689
|
const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
|
|
2569
2690
|
encoding: "utf8",
|
|
@@ -2584,7 +2705,7 @@ function probeBranch(worktreePath, opts) {
|
|
|
2584
2705
|
}
|
|
2585
2706
|
}
|
|
2586
2707
|
function probeCheckout(worktreePath, opts) {
|
|
2587
|
-
const timeout = opts.timeoutMs ??
|
|
2708
|
+
const timeout = opts.timeoutMs ?? gitProbeTimeoutMs();
|
|
2588
2709
|
try {
|
|
2589
2710
|
const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "rev-parse", "--git-dir"], {
|
|
2590
2711
|
encoding: "utf8",
|
|
@@ -2594,7 +2715,7 @@ function probeCheckout(worktreePath, opts) {
|
|
|
2594
2715
|
const raw = stdout.trim();
|
|
2595
2716
|
if (raw === "")
|
|
2596
2717
|
return { error: `no git dir reported at "${worktreePath}"` };
|
|
2597
|
-
const abs =
|
|
2718
|
+
const abs = isAbsolute6(raw) ? raw : join10(worktreePath, raw);
|
|
2598
2719
|
return { gitDir: realpathSync3(abs) };
|
|
2599
2720
|
} catch (err) {
|
|
2600
2721
|
const e = err;
|
|
@@ -2605,41 +2726,118 @@ function probeCheckout(worktreePath, opts) {
|
|
|
2605
2726
|
return { error: detail };
|
|
2606
2727
|
}
|
|
2607
2728
|
}
|
|
2729
|
+
function probeCheckoutRoot(path, opts = {}) {
|
|
2730
|
+
const timeout = opts.timeoutMs ?? gitProbeTimeoutMs();
|
|
2731
|
+
try {
|
|
2732
|
+
const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", path, "rev-parse", "--show-toplevel"], {
|
|
2733
|
+
encoding: "utf8",
|
|
2734
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2735
|
+
timeout
|
|
2736
|
+
});
|
|
2737
|
+
const raw = stdout.trim();
|
|
2738
|
+
if (raw === "")
|
|
2739
|
+
return null;
|
|
2740
|
+
return realpathSync3(raw);
|
|
2741
|
+
} catch {
|
|
2742
|
+
return null;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2608
2745
|
function l1PreDispatchCheck(input, opts = {}) {
|
|
2609
2746
|
const violations = [];
|
|
2610
|
-
const
|
|
2611
|
-
|
|
2612
|
-
|
|
2747
|
+
const checkoutCache = new Map;
|
|
2748
|
+
const checkout = (path) => {
|
|
2749
|
+
const key = resolve8(path);
|
|
2750
|
+
if (!checkoutCache.has(key))
|
|
2751
|
+
checkoutCache.set(key, probeCheckout(path, opts));
|
|
2752
|
+
return checkoutCache.get(key);
|
|
2753
|
+
};
|
|
2754
|
+
const {
|
|
2755
|
+
workflowType,
|
|
2756
|
+
integrationWorktreePath,
|
|
2757
|
+
integrationBranch,
|
|
2758
|
+
mainWorktree,
|
|
2759
|
+
expectedMainBranch,
|
|
2760
|
+
lifecycleBranches,
|
|
2761
|
+
leaseWorktreePath,
|
|
2762
|
+
leaseWorkingBranch,
|
|
2763
|
+
planId
|
|
2764
|
+
} = input;
|
|
2765
|
+
if (mainWorktree === null) {
|
|
2766
|
+
violations.push(violation8("high", "worktree.main.unresolved", `main worktree identity cannot be proved for plan "${planId}" (git worktree discovery failed or unavailable) — L1 refuses instead of skipping the residency row`, "run from inside the repository (or pass a resolvable cwd) so the main worktree can be discovered"));
|
|
2767
|
+
} else {
|
|
2768
|
+
if (expectedMainBranch.trim() === "") {
|
|
2769
|
+
violations.push(violation8("high", "worktree.main.expected-branch-missing", `neither a recorded main-worktree branch nor an explicit branch.base is available for plan "${planId}" — the expectation is never the branch observed at check time`, "record the main worktree branch at lifecycle start (plan header / --main-branch) or carry the explicit branch.base"));
|
|
2770
|
+
} else {
|
|
2771
|
+
violations.push(...assertMainWorktreeResidency(mainWorktree, expectedMainBranch).violations);
|
|
2772
|
+
}
|
|
2773
|
+
if (mainWorktree.branch !== "" && lifecycleBranches.includes(mainWorktree.branch)) {
|
|
2774
|
+
violations.push(violation8("high", "worktree.main.residency-switched", `main worktree "${mainWorktree.root}" is on branch "${mainWorktree.branch}", which is owned by an active lifecycle — main must not carry any lifecycle-owned branch, even when the recorded expectation matches it`, "restore the recorded main-worktree branch; never switch main to satisfy a check"));
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
const integrationRequired = workflowType === "iteration" || integrationWorktreePath.trim() !== "" || integrationBranch.trim() !== "";
|
|
2778
|
+
if (integrationRequired && (integrationWorktreePath.trim() === "" || integrationBranch.trim() === "")) {
|
|
2779
|
+
violations.push(violation8("high", "worktree.l1.integration-missing", `integration worktree path and branch are both required (workflow type "${workflowType}") for plan "${planId}" — got path ${JSON.stringify(integrationWorktreePath)}, branch ${JSON.stringify(integrationBranch)}`, "record snapshot integration_worktree_path + branch.integration (the dedicated integration checkout), or pass both empty for a standalone plan"));
|
|
2613
2780
|
}
|
|
2614
2781
|
if (leaseWorktreePath.trim() === "") {
|
|
2615
|
-
violations.push(
|
|
2782
|
+
violations.push(violation8("high", "worktree.l1.lease-missing", `execution_lease.worktree_path is empty for plan "${planId}" — no verified execution_lease to dispatch against`, "claim the execution_lease with an absolute feature worktree path before dispatch"));
|
|
2616
2783
|
}
|
|
2617
2784
|
if (leaseWorkingBranch.trim() === "") {
|
|
2618
|
-
violations.push(
|
|
2619
|
-
}
|
|
2620
|
-
if (
|
|
2621
|
-
if (
|
|
2622
|
-
violations.push(
|
|
2623
|
-
} else
|
|
2624
|
-
const
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
violations.push(violation7("high", "worktree.l1.checkout-probe-failed", `cannot establish the lease worktree "${leaseWorktreePath}" is a distinct Git checkout from the control worktree "${controlWorktreePath}" for plan "${planId}": ${leaseProbe.error}`, "verify the lease worktree path is a git checkout (git worktree add <path> <branch>)"));
|
|
2630
|
-
} else if (controlProbe.gitDir === leaseProbe.gitDir) {
|
|
2631
|
-
violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" is the same Git checkout as metadata.control_worktree_path "${controlWorktreePath}" — a plain subdirectory or symlink alias of the control checkout is not isolation; the feature worktree MUST be a distinct checkout`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
|
|
2785
|
+
violations.push(violation8("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
|
|
2786
|
+
}
|
|
2787
|
+
if (integrationRequired && integrationWorktreePath.trim() !== "" && integrationBranch.trim() !== "") {
|
|
2788
|
+
if (!existsSync8(integrationWorktreePath)) {
|
|
2789
|
+
violations.push(violation8("high", "worktree.l1.integration-missing", `integration worktree "${integrationWorktreePath}" does not exist for plan "${planId}" — the integration checkout must exist before dispatch`, `create it before dispatch: git worktree add ${shellQuote(integrationWorktreePath)} ${shellQuote(integrationBranch)}`));
|
|
2790
|
+
} else {
|
|
2791
|
+
const probe = probeBranch(integrationWorktreePath, opts);
|
|
2792
|
+
if ("error" in probe) {
|
|
2793
|
+
violations.push(violation8("high", "worktree.branch-probe-failed", `cannot probe branch at the integration worktree "${integrationWorktreePath}" for plan "${planId}": ${probe.error}`, "verify the path is a git worktree checkout on the integration branch (not detached)"));
|
|
2794
|
+
} else if (probe.branch !== integrationBranch) {
|
|
2795
|
+
violations.push(violation8("high", "worktree.branch-mismatch", `integration worktree "${integrationWorktreePath}" is on branch "${probe.branch}", expected branch.integration "${integrationBranch}" (plan "${planId}")`, `checkout ${shellQuote(integrationBranch)} in the integration worktree`));
|
|
2632
2796
|
}
|
|
2633
2797
|
}
|
|
2634
2798
|
}
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2799
|
+
const identityViolation = (aPath, bPath, code, describe, fix) => {
|
|
2800
|
+
if (resolve8(aPath) === resolve8(bPath)) {
|
|
2801
|
+
return violation8("critical", code, `${describe} — the same checkout (normalized path equality) is not isolation`, fix);
|
|
2802
|
+
}
|
|
2803
|
+
if (!existsSync8(aPath) || !existsSync8(bPath))
|
|
2804
|
+
return null;
|
|
2805
|
+
const a = checkout(aPath);
|
|
2806
|
+
const b = checkout(bPath);
|
|
2807
|
+
if ("error" in a) {
|
|
2808
|
+
return violation8("high", "worktree.l1.checkout-probe-failed", `cannot establish the Git-checkout identity of "${aPath}" for plan "${planId}": ${a.error}`, "verify the path is a git checkout");
|
|
2809
|
+
}
|
|
2810
|
+
if ("error" in b) {
|
|
2811
|
+
return violation8("high", "worktree.l1.checkout-probe-failed", `cannot establish the Git-checkout identity of "${bPath}" for plan "${planId}": ${b.error}`, "verify the path is a git checkout");
|
|
2812
|
+
}
|
|
2813
|
+
if (a.gitDir === b.gitDir) {
|
|
2814
|
+
return violation8("critical", code, `${describe} — a plain subdirectory or symlink alias of the other checkout is not isolation (canonical per-worktree git dirs match)`, fix);
|
|
2815
|
+
}
|
|
2816
|
+
return null;
|
|
2817
|
+
};
|
|
2818
|
+
if (mainWorktree !== null && leaseWorktreePath.trim() !== "") {
|
|
2819
|
+
const v = identityViolation(mainWorktree.root, leaseWorktreePath, "worktree.l1.lease-equals-main", `execution_lease.worktree_path "${leaseWorktreePath}" is the same Git checkout as the main worktree "${mainWorktree.root}" (plan "${planId}")`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease");
|
|
2820
|
+
if (v !== null)
|
|
2821
|
+
violations.push(v);
|
|
2822
|
+
}
|
|
2823
|
+
if (mainWorktree !== null && integrationRequired && integrationWorktreePath.trim() !== "") {
|
|
2824
|
+
const v = identityViolation(mainWorktree.root, integrationWorktreePath, "worktree.l1.integration-equals-main", `integration worktree "${integrationWorktreePath}" is the same Git checkout as the main worktree "${mainWorktree.root}" (plan "${planId}") — the integration checkout is the sole merge cwd and must be dedicated`, "use a dedicated integration checkout on branch.integration (git worktree add <path> <integration-branch>) and record it as integration_worktree_path");
|
|
2825
|
+
if (v !== null)
|
|
2826
|
+
violations.push(v);
|
|
2827
|
+
}
|
|
2828
|
+
if (integrationRequired && integrationWorktreePath.trim() !== "" && leaseWorktreePath.trim() !== "") {
|
|
2829
|
+
const v = identityViolation(integrationWorktreePath, leaseWorktreePath, "worktree.l1.lease-equals-integration", `execution_lease.worktree_path "${leaseWorktreePath}" is the same Git checkout as the integration worktree "${integrationWorktreePath}" (plan "${planId}") — the feature worktree must be distinct from both main and integration`, "use a distinct feature worktree for the plan (git worktree add <path> <working-branch>) and update the lease");
|
|
2830
|
+
if (v !== null)
|
|
2831
|
+
violations.push(v);
|
|
2832
|
+
}
|
|
2833
|
+
if (leaseWorktreePath.trim() !== "" && !existsSync8(leaseWorktreePath)) {
|
|
2834
|
+
violations.push(violation8("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${shellQuote(leaseWorktreePath)} <working-branch>`));
|
|
2835
|
+
} else if (leaseWorktreePath.trim() !== "" && leaseWorkingBranch.trim() !== "") {
|
|
2638
2836
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
2639
2837
|
if ("error" in probe) {
|
|
2640
|
-
violations.push(
|
|
2838
|
+
violations.push(violation8("high", "worktree.l1.branch-probe-failed", `cannot probe branch at "${leaseWorktreePath}" for plan "${planId}": ${probe.error}`, "verify the path is a git worktree checkout on the lease working branch (not detached)"));
|
|
2641
2839
|
} else if (probe.branch !== leaseWorkingBranch) {
|
|
2642
|
-
violations.push(
|
|
2840
|
+
violations.push(violation8("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${shellQuote(leaseWorkingBranch)} in the feature worktree`));
|
|
2643
2841
|
}
|
|
2644
2842
|
}
|
|
2645
2843
|
return gate(violations);
|
|
@@ -2649,32 +2847,32 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2649
2847
|
const tracks = input.tracks ?? [];
|
|
2650
2848
|
const seenPaths = new Set;
|
|
2651
2849
|
if (tracks.length < 1) {
|
|
2652
|
-
violations.push(
|
|
2850
|
+
violations.push(violation8("high", "worktree.l2.no-tracks", "no parallel writable tracks — the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
|
|
2653
2851
|
}
|
|
2654
2852
|
tracks.forEach((track, index) => {
|
|
2655
2853
|
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
2656
|
-
violations.push(
|
|
2854
|
+
violations.push(violation8("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2657
2855
|
return;
|
|
2658
2856
|
}
|
|
2659
|
-
if (!
|
|
2660
|
-
violations.push(
|
|
2857
|
+
if (!isAbsolute6(track.worktreePath)) {
|
|
2858
|
+
violations.push(violation8("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>)`));
|
|
2661
2859
|
return;
|
|
2662
2860
|
}
|
|
2663
2861
|
const normalized = resolve8(track.worktreePath);
|
|
2664
2862
|
if (seenPaths.has(normalized)) {
|
|
2665
|
-
violations.push(
|
|
2863
|
+
violations.push(violation8("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"));
|
|
2666
2864
|
return;
|
|
2667
2865
|
}
|
|
2668
2866
|
seenPaths.add(normalized);
|
|
2669
|
-
if (!
|
|
2670
|
-
violations.push(
|
|
2867
|
+
if (!existsSync8(track.worktreePath)) {
|
|
2868
|
+
violations.push(violation8("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
2671
2869
|
return;
|
|
2672
2870
|
}
|
|
2673
2871
|
const probe = probeBranch(track.worktreePath, opts);
|
|
2674
2872
|
if ("error" in probe) {
|
|
2675
|
-
violations.push(
|
|
2873
|
+
violations.push(violation8("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
|
|
2676
2874
|
} else if (probe.branch !== track.workingBranch) {
|
|
2677
|
-
violations.push(
|
|
2875
|
+
violations.push(violation8("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
|
|
2678
2876
|
}
|
|
2679
2877
|
});
|
|
2680
2878
|
return gate(violations);
|
|
@@ -2683,12 +2881,108 @@ function assertBranchAlignment(worktreePath, expectedBranch, opts = {}) {
|
|
|
2683
2881
|
const violations = [];
|
|
2684
2882
|
const probe = probeBranch(worktreePath, opts);
|
|
2685
2883
|
if ("error" in probe) {
|
|
2686
|
-
violations.push(
|
|
2884
|
+
violations.push(violation8("high", "worktree.branch-probe-failed", `cannot probe branch at "${worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on the expected branch (not detached)"));
|
|
2687
2885
|
} else if (probe.branch !== expectedBranch) {
|
|
2688
|
-
violations.push(
|
|
2886
|
+
violations.push(violation8("high", "worktree.branch-mismatch", `worktree "${worktreePath}" is on branch "${probe.branch}", expected "${expectedBranch}" (Assignment Working branch)`, `checkout ${expectedBranch} in that worktree`));
|
|
2689
2887
|
}
|
|
2690
2888
|
return gate(violations);
|
|
2691
2889
|
}
|
|
2890
|
+
function shellQuote(value) {
|
|
2891
|
+
return "'" + value.replaceAll("'", `'"'"'`) + "'";
|
|
2892
|
+
}
|
|
2893
|
+
function isPlainObject8(value) {
|
|
2894
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2895
|
+
}
|
|
2896
|
+
function collectActiveLifecycleBranches(snapshots) {
|
|
2897
|
+
const owned = new Set;
|
|
2898
|
+
for (const doc of snapshots) {
|
|
2899
|
+
const branch = doc.branch;
|
|
2900
|
+
if (isPlainObject8(branch) && typeof branch.integration === "string" && branch.integration.trim() !== "") {
|
|
2901
|
+
owned.add(branch.integration);
|
|
2902
|
+
}
|
|
2903
|
+
if (!Array.isArray(doc.plans))
|
|
2904
|
+
continue;
|
|
2905
|
+
for (const row of doc.plans) {
|
|
2906
|
+
if (!isPlainObject8(row))
|
|
2907
|
+
continue;
|
|
2908
|
+
const lease = row.execution_lease;
|
|
2909
|
+
if (isPlainObject8(lease) && typeof lease.working_branch === "string" && lease.working_branch.trim() !== "") {
|
|
2910
|
+
owned.add(lease.working_branch);
|
|
2911
|
+
}
|
|
2912
|
+
const meta = row.metadata;
|
|
2913
|
+
if (isPlainObject8(meta) && Array.isArray(meta.track_branches)) {
|
|
2914
|
+
for (const branch2 of meta.track_branches) {
|
|
2915
|
+
if (typeof branch2 === "string" && branch2.trim() !== "")
|
|
2916
|
+
owned.add(branch2);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (isPlainObject8(meta) && typeof meta.working_branch === "string" && meta.working_branch.trim() !== "") {
|
|
2920
|
+
owned.add(meta.working_branch);
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
return [...owned];
|
|
2925
|
+
}
|
|
2926
|
+
function scanActiveLifecycleBranches(harnessDir, governingWorkflowId) {
|
|
2927
|
+
const registerPath = join11(harnessDir, "status.json");
|
|
2928
|
+
if (!existsSync9(registerPath))
|
|
2929
|
+
return { kind: "ok", branches: [], notes: [] };
|
|
2930
|
+
let register;
|
|
2931
|
+
try {
|
|
2932
|
+
register = readJson(registerPath);
|
|
2933
|
+
} catch (error) {
|
|
2934
|
+
return { kind: "refusal", code: "worktree.l1.lifecycle-register-unreadable", detail: `${registerPath}: ${error.message}` };
|
|
2935
|
+
}
|
|
2936
|
+
if (!isPlainObject8(register) || register.version !== 2 || !Array.isArray(register.workflows)) {
|
|
2937
|
+
return {
|
|
2938
|
+
kind: "refusal",
|
|
2939
|
+
code: "worktree.l1.lifecycle-register-unreadable",
|
|
2940
|
+
detail: `${registerPath}: not a readable v2 root register (version 2 + workflows[]) — the active lifecycle set cannot be enumerated`
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
let workflowsDir;
|
|
2944
|
+
try {
|
|
2945
|
+
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2946
|
+
} catch (error) {
|
|
2947
|
+
return { kind: "refusal", code: "worktree.l1.lifecycle-register-unreadable", detail: `${registerPath}: ${error.message}` };
|
|
2948
|
+
}
|
|
2949
|
+
const owned = new Set;
|
|
2950
|
+
const notes = [];
|
|
2951
|
+
for (const entry of register.workflows) {
|
|
2952
|
+
if (typeof entry !== "object" || entry === null || typeof entry.id !== "string") {
|
|
2953
|
+
return {
|
|
2954
|
+
kind: "refusal",
|
|
2955
|
+
code: "worktree.l1.lifecycle-register-unreadable",
|
|
2956
|
+
detail: `${registerPath}: malformed workflows[] entry — a registered active lifecycle cannot be identified`
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
const id = entry.id;
|
|
2960
|
+
try {
|
|
2961
|
+
assertSafePathComponent(id, "workflow id");
|
|
2962
|
+
} catch (error) {
|
|
2963
|
+
return { kind: "refusal", code: "worktree.l1.lifecycle-register-unreadable", detail: `${registerPath}: ${error.message}` };
|
|
2964
|
+
}
|
|
2965
|
+
if (id === governingWorkflowId)
|
|
2966
|
+
continue;
|
|
2967
|
+
const snapshotPath = join11(workflowsDir, id, WORKFLOW_SNAPSHOT_FILE);
|
|
2968
|
+
let snapshot;
|
|
2969
|
+
try {
|
|
2970
|
+
const read = readWorkflowSnapshot(dirname7(snapshotPath));
|
|
2971
|
+
snapshot = read.snapshot;
|
|
2972
|
+
notes.push(...read.diagnostics);
|
|
2973
|
+
} catch (error) {
|
|
2974
|
+
return {
|
|
2975
|
+
kind: "refusal",
|
|
2976
|
+
code: "worktree.l1.lifecycle-snapshot-unreadable",
|
|
2977
|
+
detail: `${snapshotPath}: ${error.message}`
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
for (const branch of collectActiveLifecycleBranches([snapshot]))
|
|
2981
|
+
owned.add(branch);
|
|
2982
|
+
}
|
|
2983
|
+
return { kind: "ok", branches: [...owned], notes };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2692
2986
|
class SddScriptError extends Error {
|
|
2693
2987
|
exitCode;
|
|
2694
2988
|
constructor(message, exitCode) {
|
|
@@ -2718,21 +3012,22 @@ function gitOut(cwd, args) {
|
|
|
2718
3012
|
cwd,
|
|
2719
3013
|
encoding: "utf8",
|
|
2720
3014
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2721
|
-
maxBuffer: GIT_CAPTURE_MAX_BYTES
|
|
3015
|
+
maxBuffer: GIT_CAPTURE_MAX_BYTES,
|
|
3016
|
+
timeout: gitProbeTimeoutMs()
|
|
2722
3017
|
}).trim();
|
|
2723
3018
|
} catch {
|
|
2724
3019
|
return null;
|
|
2725
3020
|
}
|
|
2726
3021
|
}
|
|
2727
3022
|
function probeHarnessWithStatus(root) {
|
|
2728
|
-
if (isFile2(
|
|
2729
|
-
return
|
|
2730
|
-
if (isFile2(
|
|
2731
|
-
return
|
|
2732
|
-
if (hasWorkflowSnapshot(
|
|
2733
|
-
return
|
|
2734
|
-
if (hasWorkflowSnapshot(
|
|
2735
|
-
return
|
|
3023
|
+
if (isFile2(join12(root, ".mstar", "status.json")))
|
|
3024
|
+
return join12(root, ".mstar");
|
|
3025
|
+
if (isFile2(join12(root, ".agents", "status.json")))
|
|
3026
|
+
return join12(root, ".agents");
|
|
3027
|
+
if (hasWorkflowSnapshot(join12(root, ".mstar")))
|
|
3028
|
+
return join12(root, ".mstar");
|
|
3029
|
+
if (hasWorkflowSnapshot(join12(root, ".agents")))
|
|
3030
|
+
return join12(root, ".agents");
|
|
2736
3031
|
return null;
|
|
2737
3032
|
}
|
|
2738
3033
|
function hasWorkflowSnapshot(harnessDir) {
|
|
@@ -2740,13 +3035,13 @@ function hasWorkflowSnapshot(harnessDir) {
|
|
|
2740
3035
|
try {
|
|
2741
3036
|
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2742
3037
|
} catch {
|
|
2743
|
-
workflowsDir =
|
|
3038
|
+
workflowsDir = join12(harnessDir, "workflows");
|
|
2744
3039
|
}
|
|
2745
3040
|
if (!isDirectory2(workflowsDir))
|
|
2746
3041
|
return false;
|
|
2747
3042
|
try {
|
|
2748
3043
|
for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
|
|
2749
|
-
if (entry.isDirectory() && isFile2(
|
|
3044
|
+
if (entry.isDirectory() && isFile2(join12(workflowsDir, entry.name, "snapshot.json")))
|
|
2750
3045
|
return true;
|
|
2751
3046
|
}
|
|
2752
3047
|
} catch {
|
|
@@ -2754,27 +3049,28 @@ function hasWorkflowSnapshot(harnessDir) {
|
|
|
2754
3049
|
}
|
|
2755
3050
|
return false;
|
|
2756
3051
|
}
|
|
2757
|
-
function
|
|
2758
|
-
const
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
3052
|
+
function checkoutRootNearestExisting(dir, boundary) {
|
|
3053
|
+
const stop = resolve9(boundary);
|
|
3054
|
+
const startInside = isInside(resolve9(dir), stop);
|
|
3055
|
+
let current = resolve9(dir);
|
|
3056
|
+
for (;; ) {
|
|
3057
|
+
const probed = probeCheckoutRoot(current);
|
|
3058
|
+
if (probed !== null)
|
|
3059
|
+
return probed;
|
|
3060
|
+
if (startInside && current === stop)
|
|
3061
|
+
return null;
|
|
3062
|
+
const parent = dirname8(current);
|
|
3063
|
+
if (parent === current)
|
|
3064
|
+
return null;
|
|
3065
|
+
if (startInside && !isInside(parent, stop))
|
|
3066
|
+
return null;
|
|
3067
|
+
current = parent;
|
|
2772
3068
|
}
|
|
2773
3069
|
}
|
|
2774
3070
|
function sddWorkspace(planId, opts = {}) {
|
|
2775
3071
|
if (!planId) {
|
|
2776
3072
|
throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
|
|
2777
|
-
Set MSTAR_CONTROL_ROOT=<
|
|
3073
|
+
Set MSTAR_CONTROL_ROOT=<main-repo-root> when running from a feature worktree.`, 2);
|
|
2778
3074
|
}
|
|
2779
3075
|
const cwd = opts.cwd ?? process.cwd();
|
|
2780
3076
|
const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
|
|
@@ -2783,16 +3079,29 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2783
3079
|
if (!isDirectory2(controlRoot)) {
|
|
2784
3080
|
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
2785
3081
|
}
|
|
2786
|
-
|
|
3082
|
+
const supplied = realpathSync4(controlRoot);
|
|
3083
|
+
const main = readMainWorktree(supplied);
|
|
3084
|
+
if (main !== null) {
|
|
3085
|
+
if (main.root !== supplied) {
|
|
3086
|
+
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT "${supplied}" is a linked (integration/foreign) checkout of the main worktree "${main.root}".
|
|
3087
|
+
` + ` The process control root is the main worktree itself — refusing to redirect the process SSOT.
|
|
3088
|
+
` + ` Re-run with MSTAR_CONTROL_ROOT=${main.root}`, 1);
|
|
3089
|
+
}
|
|
3090
|
+
root = main.root;
|
|
3091
|
+
} else if (isFile2(join12(supplied, ".git")) || gitOut(supplied, ["rev-parse", "--is-inside-work-tree"]) === "true") {
|
|
3092
|
+
throw new SddScriptError(`mstar sdd workspace: cannot verify the main worktree for CONTROL_ROOT "${supplied}" (git worktree discovery failed).
|
|
3093
|
+
Refusing to resolve or create any SDD tree without a verified main worktree.`, 1);
|
|
3094
|
+
} else {
|
|
3095
|
+
root = supplied;
|
|
3096
|
+
}
|
|
2787
3097
|
} else {
|
|
2788
|
-
const
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
|
|
3098
|
+
const main = readMainWorktree(cwd);
|
|
3099
|
+
if (main === null) {
|
|
3100
|
+
throw new SddScriptError(`mstar sdd workspace: cannot verify the main worktree from cwd ${cwd} (git worktree discovery failed, git is unavailable, or the directory is not a Git worktree).
|
|
3101
|
+
` + ` Refusing to resolve or create any SDD tree without a verified main worktree — no second process-SSOT tree is ever created under a linked checkout.
|
|
3102
|
+
` + ` Re-run with MSTAR_CONTROL_ROOT=<main-repo-root> or: mstar sdd workspace ${planId} <main-repo-root>`, 1);
|
|
3103
|
+
}
|
|
3104
|
+
root = main.root;
|
|
2796
3105
|
}
|
|
2797
3106
|
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2798
3107
|
let harnessDir;
|
|
@@ -2800,25 +3109,30 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
2800
3109
|
harnessDir = resolve9(root, harnessOverride);
|
|
2801
3110
|
} else {
|
|
2802
3111
|
const rc = findMstarc(root, root);
|
|
2803
|
-
const rcHarnessDir = rc !== null ? parseMstarc(
|
|
3112
|
+
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync8(rc, "utf8")).harnessDir : undefined;
|
|
2804
3113
|
if (rcHarnessDir) {
|
|
2805
|
-
harnessDir = resolve9(rc !== null ?
|
|
3114
|
+
harnessDir = resolve9(rc !== null ? dirname8(rc) : root, rcHarnessDir);
|
|
2806
3115
|
} else {
|
|
2807
3116
|
const probed = probeHarnessWithStatus(root);
|
|
2808
3117
|
if (probed) {
|
|
2809
3118
|
harnessDir = probed;
|
|
2810
|
-
} else if (isDirectory2(
|
|
2811
|
-
harnessDir =
|
|
2812
|
-
} else if (isDirectory2(
|
|
2813
|
-
harnessDir =
|
|
3119
|
+
} else if (isDirectory2(join12(root, ".mstar"))) {
|
|
3120
|
+
harnessDir = join12(root, ".mstar");
|
|
3121
|
+
} else if (isDirectory2(join12(root, ".agents"))) {
|
|
3122
|
+
harnessDir = join12(root, ".agents");
|
|
2814
3123
|
} else {
|
|
2815
|
-
harnessDir =
|
|
3124
|
+
harnessDir = join12(root, ".mstar");
|
|
2816
3125
|
}
|
|
2817
3126
|
}
|
|
2818
3127
|
}
|
|
3128
|
+
const harnessCheckout = checkoutRootNearestExisting(harnessDir, root);
|
|
3129
|
+
if (harnessCheckout !== null && harnessCheckout !== root) {
|
|
3130
|
+
throw new SddScriptError(`mstar sdd workspace: harness root "${harnessDir}" resolves inside Git checkout "${harnessCheckout}", not the verified main worktree "${root}".
|
|
3131
|
+
` + ` Refusing to redirect the process SSOT into a linked checkout — resolve MSTAR_HARNESS_DIR / .mstarc harness_dir relative to the main worktree.`, 1);
|
|
3132
|
+
}
|
|
2819
3133
|
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2820
3134
|
mkdirSync6(sddDir, { recursive: true });
|
|
2821
|
-
writeFileSync4(
|
|
3135
|
+
writeFileSync4(join12(sddDir, ".gitignore"), `*
|
|
2822
3136
|
`);
|
|
2823
3137
|
return realpathSync4(sddDir);
|
|
2824
3138
|
}
|
|
@@ -2838,7 +3152,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2838
3152
|
}
|
|
2839
3153
|
let content;
|
|
2840
3154
|
try {
|
|
2841
|
-
content =
|
|
3155
|
+
content = readFileSync8(planFile, "utf8");
|
|
2842
3156
|
} catch {
|
|
2843
3157
|
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
2844
3158
|
}
|
|
@@ -2847,7 +3161,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2847
3161
|
if (outFile) {
|
|
2848
3162
|
out = bound ? resolve9(observedCwd, outFile) : outFile;
|
|
2849
3163
|
} else if (bound) {
|
|
2850
|
-
out =
|
|
3164
|
+
out = join12(bound.sddDir, `task-${taskN}-brief.md`);
|
|
2851
3165
|
mkdirAfterGate = bound.sddDir;
|
|
2852
3166
|
} else {
|
|
2853
3167
|
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
@@ -2855,7 +3169,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2855
3169
|
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2856
3170
|
}
|
|
2857
3171
|
mkdirSync6(sddDir, { recursive: true });
|
|
2858
|
-
out =
|
|
3172
|
+
out = join12(sddDir, `task-${taskN}-brief.md`);
|
|
2859
3173
|
}
|
|
2860
3174
|
if (bound) {
|
|
2861
3175
|
const gate2 = checkSddAction(bound, { kind: "artifact", cwd: observedCwd, target: out });
|
|
@@ -2892,7 +3206,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
|
2892
3206
|
}
|
|
2893
3207
|
function isInside(child, ancestor) {
|
|
2894
3208
|
const rel = relative3(ancestor, child);
|
|
2895
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
3209
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute7(rel);
|
|
2896
3210
|
}
|
|
2897
3211
|
function canonicalDir(path) {
|
|
2898
3212
|
try {
|
|
@@ -2981,7 +3295,7 @@ function checkSddAction(context, action) {
|
|
|
2981
3295
|
}
|
|
2982
3296
|
return { ok: violations.length === 0, violations };
|
|
2983
3297
|
}
|
|
2984
|
-
function
|
|
3298
|
+
function violation9(severity, code, message, fix) {
|
|
2985
3299
|
return { ok: false, severity, code, message, fix };
|
|
2986
3300
|
}
|
|
2987
3301
|
var RAW_GROUP = "__raw";
|
|
@@ -3094,7 +3408,7 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
3094
3408
|
const violations = [];
|
|
3095
3409
|
const fm = parseDesignFrontmatter(frontmatterText);
|
|
3096
3410
|
if (fm === null) {
|
|
3097
|
-
violations.push(
|
|
3411
|
+
violations.push(violation9("medium", "design-md.tokens.missing-frontmatter", "no `---` YAML frontmatter block found — DESIGN.md must open with a fenced frontmatter holding the token SSOT (design-md-spec §1.5)", "add a `---` fenced frontmatter with version, name, description, and the colors/typography/spacing/rounded groups"));
|
|
3098
3412
|
return { ok: false, violations };
|
|
3099
3413
|
}
|
|
3100
3414
|
const groupEntries = (group) => {
|
|
@@ -3109,61 +3423,61 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
3109
3423
|
};
|
|
3110
3424
|
for (const group of ["colors", "typography", "spacing", "rounded"]) {
|
|
3111
3425
|
if (!isMap(fm[group])) {
|
|
3112
|
-
violations.push(
|
|
3426
|
+
violations.push(violation9("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
|
|
3113
3427
|
} else if (!groupIsMap(group)) {
|
|
3114
|
-
violations.push(
|
|
3428
|
+
violations.push(violation9("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
|
|
3115
3429
|
} else if (groupEntries(group).length === 0) {
|
|
3116
|
-
violations.push(
|
|
3430
|
+
violations.push(violation9("medium", "design-md.tokens.missing-group", `missing required token group "${group}" — colors/typography/spacing/rounded are required by the frontmatter SSOT (design-md-spec §1.5)`, `add an active \`${group}:\` block with concrete token values`));
|
|
3117
3431
|
}
|
|
3118
3432
|
}
|
|
3119
3433
|
if (!isMap(fm.components) || !groupIsMap("components")) {
|
|
3120
|
-
violations.push(
|
|
3434
|
+
violations.push(violation9("medium", "design-md.tokens.group-not-map", `token group "components" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`components\` as a nested map`));
|
|
3121
3435
|
}
|
|
3122
|
-
const placeholder = (group, name, value) => violations.push(
|
|
3436
|
+
const placeholder = (group, name, value) => violations.push(violation9("low", "design-md.tokens.placeholder", `token "${group}.${name}" uses a "[...]" template value — placeholders never count as concrete tokens (completeness-checklist § How to use item 5)`, `replace \`${value}\` with a concrete value`));
|
|
3123
3437
|
for (const [name, value] of groupEntries("colors")) {
|
|
3124
3438
|
if (typeof value !== "string") {
|
|
3125
|
-
violations.push(
|
|
3439
|
+
violations.push(violation9("medium", "design-md.tokens.color-format", `color "${name}" must be a string value (design-md-spec §2.2)`, "quote the color value"));
|
|
3126
3440
|
continue;
|
|
3127
3441
|
}
|
|
3128
3442
|
if (isPlaceholder(value)) {
|
|
3129
3443
|
placeholder("colors", name, value);
|
|
3130
3444
|
} else if (!HEX_RE.test(value) && !OKLCH_RE.test(value)) {
|
|
3131
|
-
violations.push(
|
|
3445
|
+
violations.push(violation9("medium", "design-md.tokens.color-format", `color "${name}" = "${value}" is not a hex (\`#rrggbb\`/\`#rrggbbaa\`) or oklch() value (design-md-spec §2.2)`, "use an sRGB hex value, optionally with a `-p3` oklch() twin"));
|
|
3132
3446
|
}
|
|
3133
3447
|
}
|
|
3134
3448
|
for (const [name, value] of groupEntries("typography")) {
|
|
3135
3449
|
if (!isMap(value)) {
|
|
3136
|
-
violations.push(
|
|
3450
|
+
violations.push(violation9("medium", "design-md.tokens.typography-shape", `typography token "${name}" must be a map of the five properties (design-md-spec §1.5)`, "give it fontFamily/fontSize/fontWeight/lineHeight/letterSpacing"));
|
|
3137
3451
|
continue;
|
|
3138
3452
|
}
|
|
3139
3453
|
const keys = Object.keys(value);
|
|
3140
3454
|
const missing = TYPOGRAPHY_PROPS.filter((p) => !keys.includes(p));
|
|
3141
3455
|
const extra = keys.filter((k) => !TYPOGRAPHY_PROPS.includes(k));
|
|
3142
3456
|
if (missing.length > 0 || extra.length > 0) {
|
|
3143
|
-
violations.push(
|
|
3457
|
+
violations.push(violation9("medium", "design-md.tokens.typography-shape", `typography token "${name}" must have exactly the five properties fontFamily/fontSize/fontWeight/lineHeight/letterSpacing (design-md-spec §1.5)${missing.length > 0 ? ` — missing: ${missing.join(", ")}` : ""}${extra.length > 0 ? ` — extra: ${extra.join(", ")}` : ""}`, "align the token with the five-property shape"));
|
|
3144
3458
|
}
|
|
3145
3459
|
for (const prop of ["fontFamily", "fontSize"]) {
|
|
3146
3460
|
const v = value[prop];
|
|
3147
3461
|
if (typeof v === "string" && isPlaceholder(v))
|
|
3148
3462
|
placeholder("typography", name, v);
|
|
3149
3463
|
else if (typeof v !== "string" || v.trim() === "") {
|
|
3150
|
-
violations.push(
|
|
3464
|
+
violations.push(violation9("medium", "design-md.tokens.typography-shape", `typography token "${name}" has an empty \`${prop}\` (design-md-spec §1.5)`, `fill \`${prop}\` with a concrete value`));
|
|
3151
3465
|
}
|
|
3152
3466
|
}
|
|
3153
3467
|
}
|
|
3154
3468
|
if (groupIsMap("spacing")) {
|
|
3155
3469
|
const spacing = fm.spacing;
|
|
3156
3470
|
if (!Object.prototype.hasOwnProperty.call(spacing, "base")) {
|
|
3157
|
-
violations.push(
|
|
3471
|
+
violations.push(violation9("medium", "design-md.tokens.spacing-base", "spacing must declare the base unit as `base` (design-md-spec §2.4)", "add `base: 4px` (or 8px) to the spacing group"));
|
|
3158
3472
|
}
|
|
3159
3473
|
for (const [name, value] of Object.entries(spacing)) {
|
|
3160
3474
|
if (name !== "base" && name !== RAW_GROUP && !/^\d+$/.test(name)) {
|
|
3161
|
-
violations.push(
|
|
3475
|
+
violations.push(violation9("medium", "design-md.tokens.spacing-key", `spacing key "${name}" must be \`base\` or a numeric multiplier (design-md-spec §1.5)`, "use numeric scale-step keys or `base`"));
|
|
3162
3476
|
}
|
|
3163
3477
|
if (typeof value === "string" && isPlaceholder(value)) {
|
|
3164
3478
|
placeholder("spacing", name, value);
|
|
3165
3479
|
} else if (typeof value !== "string" || !PX_RE.test(value)) {
|
|
3166
|
-
violations.push(
|
|
3480
|
+
violations.push(violation9("medium", "design-md.tokens.spacing-format", `spacing value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `4px`"));
|
|
3167
3481
|
}
|
|
3168
3482
|
}
|
|
3169
3483
|
}
|
|
@@ -3171,12 +3485,12 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
3171
3485
|
if (typeof value === "string" && isPlaceholder(value)) {
|
|
3172
3486
|
placeholder("rounded", name, value);
|
|
3173
3487
|
} else if (typeof value !== "string" || !PX_RE.test(value)) {
|
|
3174
|
-
violations.push(
|
|
3488
|
+
violations.push(violation9("medium", "design-md.tokens.rounded-format", `rounded value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `6px`"));
|
|
3175
3489
|
}
|
|
3176
3490
|
}
|
|
3177
3491
|
for (const [name, value] of groupEntries("components")) {
|
|
3178
3492
|
if (!isMap(value)) {
|
|
3179
|
-
violations.push(
|
|
3493
|
+
violations.push(violation9("medium", "design-md.tokens.components-shape", `component token "${name}" must be a map of properties (design-md-spec §2.8)`, "give it backgroundColor/textColor/typography/rounded/padding/height"));
|
|
3180
3494
|
continue;
|
|
3181
3495
|
}
|
|
3182
3496
|
for (const [prop, v] of Object.entries(value)) {
|
|
@@ -3192,7 +3506,7 @@ function validateDesignTokenFrontmatter(frontmatterText) {
|
|
|
3192
3506
|
const [, refGroup, refKey] = ref;
|
|
3193
3507
|
const resolves = REF_GROUPS.includes(refGroup) && groupIsMap(refGroup) && Object.prototype.hasOwnProperty.call(fm[refGroup], refKey);
|
|
3194
3508
|
if (!resolves) {
|
|
3195
|
-
violations.push(
|
|
3509
|
+
violations.push(violation9("medium", "design-md.tokens.ref-unresolved", `component "${name}" references "${v}" which does not resolve to an active token in this frontmatter (design-md-spec §6 — {path} refs MUST trace back to a key)`, `add the referenced token or use a literal value`));
|
|
3196
3510
|
}
|
|
3197
3511
|
}
|
|
3198
3512
|
}
|
|
@@ -3204,7 +3518,7 @@ function assertLightDarkParity(lightFm, darkFm) {
|
|
|
3204
3518
|
const light = parseDesignFrontmatter(lightFm);
|
|
3205
3519
|
const dark = parseDesignFrontmatter(darkFm);
|
|
3206
3520
|
if (light === null || dark === null) {
|
|
3207
|
-
violations.push(
|
|
3521
|
+
violations.push(violation9("medium", "design-md.parity.missing-frontmatter", `light/dark parity needs a YAML frontmatter in both files — ${light === null ? "DESIGN.md" : "DESIGN.dark.md"} has none (design-md-spec §4 rules 1–2)`, "add the fenced frontmatter to both theme files"));
|
|
3208
3522
|
return { ok: false, violations };
|
|
3209
3523
|
}
|
|
3210
3524
|
const activeKeys = (fm) => {
|
|
@@ -3222,12 +3536,12 @@ function assertLightDarkParity(lightFm, darkFm) {
|
|
|
3222
3536
|
const darkKeys = activeKeys(dark);
|
|
3223
3537
|
for (const key of lightKeys) {
|
|
3224
3538
|
if (!darkKeys.has(key)) {
|
|
3225
|
-
violations.push(
|
|
3539
|
+
violations.push(violation9("medium", "design-md.parity.missing-dark", `token "${key}" is active in DESIGN.md but missing from DESIGN.dark.md — both files must define the same token set (design-md-spec §4 rule 3)`, "add the token to DESIGN.dark.md with a dark-appropriate value"));
|
|
3226
3540
|
}
|
|
3227
3541
|
}
|
|
3228
3542
|
for (const key of darkKeys) {
|
|
3229
3543
|
if (!lightKeys.has(key)) {
|
|
3230
|
-
violations.push(
|
|
3544
|
+
violations.push(violation9("medium", "design-md.parity.missing-light", `token "${key}" is active in DESIGN.dark.md but missing from DESIGN.md — DESIGN.md is the SSOT for token names (design-md-spec §4 rules 3–4)`, "add the token to DESIGN.md, or remove it from the dark file"));
|
|
3231
3545
|
}
|
|
3232
3546
|
}
|
|
3233
3547
|
return { ok: violations.length === 0, violations };
|
|
@@ -3380,7 +3694,7 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
3380
3694
|
const upgradeTo = candidate === undefined ? null : candidate;
|
|
3381
3695
|
return { level, items, missing, placeholders, upgradeTo, bodyUnverified };
|
|
3382
3696
|
}
|
|
3383
|
-
function
|
|
3697
|
+
function violation10(severity, code, message, fix) {
|
|
3384
3698
|
return { ok: false, severity, code, message, fix };
|
|
3385
3699
|
}
|
|
3386
3700
|
var AUDIT_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
@@ -3413,14 +3727,14 @@ function validateAuditStatusBlocks(planText) {
|
|
|
3413
3727
|
const violations = [];
|
|
3414
3728
|
const blocks = parseStatusBlocks(planText);
|
|
3415
3729
|
if (blocks.length === 0) {
|
|
3416
|
-
violations.push(
|
|
3730
|
+
violations.push(violation10("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL.md § Plan output)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
|
|
3417
3731
|
return { ok: false, violations };
|
|
3418
3732
|
}
|
|
3419
3733
|
blocks.forEach((block, index) => {
|
|
3420
3734
|
const label = blocks.length > 1 ? ` #${index + 1}` : "";
|
|
3421
3735
|
for (const field of AUDIT_STATUS_FIELDS) {
|
|
3422
3736
|
if (!block.fields.has(field)) {
|
|
3423
|
-
violations.push(
|
|
3737
|
+
violations.push(violation10("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL.md § Plan output)`, `add \`- **${field}**: <value>\` to the Status block`));
|
|
3424
3738
|
}
|
|
3425
3739
|
}
|
|
3426
3740
|
const check = (field, pattern, code, expected) => {
|
|
@@ -3428,7 +3742,7 @@ function validateAuditStatusBlocks(planText) {
|
|
|
3428
3742
|
if (value === undefined)
|
|
3429
3743
|
return;
|
|
3430
3744
|
if (!pattern.test(value)) {
|
|
3431
|
-
violations.push(
|
|
3745
|
+
violations.push(violation10("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL.md § Plan output)`, `fix \`- **${field}**:\` to one of: ${expected}`));
|
|
3432
3746
|
}
|
|
3433
3747
|
};
|
|
3434
3748
|
check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
|
|
@@ -3440,7 +3754,7 @@ function validateAuditStatusBlocks(planText) {
|
|
|
3440
3754
|
});
|
|
3441
3755
|
return { ok: violations.length === 0, violations };
|
|
3442
3756
|
}
|
|
3443
|
-
function
|
|
3757
|
+
function violation11(severity, code, message, fix) {
|
|
3444
3758
|
return { ok: false, severity, code, message, fix };
|
|
3445
3759
|
}
|
|
3446
3760
|
var KNOWLEDGE_REQUIRED_FIELDS = ["module", "date", "problem_type", "category", "severity"];
|
|
@@ -3605,36 +3919,36 @@ function validateSchemaYaml(frontmatterText) {
|
|
|
3605
3919
|
const violations = [];
|
|
3606
3920
|
const doc = parseYamlLite(frontmatterText);
|
|
3607
3921
|
if (doc === null) {
|
|
3608
|
-
violations.push(
|
|
3922
|
+
violations.push(violation11("medium", "compound.schema.missing-frontmatter", "no `---` YAML frontmatter block found — knowledge docs must open with the schema.yaml contract (mstar-compound/references/schema.yaml)", "add the fenced frontmatter with module, date, problem_type, category, severity"));
|
|
3609
3923
|
return { ok: false, violations };
|
|
3610
3924
|
}
|
|
3611
3925
|
const isStr = (v) => typeof v === "string";
|
|
3612
|
-
const missing = (field) => violations.push(
|
|
3926
|
+
const missing = (field) => violations.push(violation11("medium", "compound.schema.missing-field", `missing required frontmatter field "${field}" (schema.yaml required_fields)`, `add \`${field}: <value>\` to the frontmatter`));
|
|
3613
3927
|
for (const field of KNOWLEDGE_REQUIRED_FIELDS) {
|
|
3614
3928
|
if (!(field in doc) || doc[field] === "")
|
|
3615
3929
|
missing(field);
|
|
3616
3930
|
}
|
|
3617
3931
|
if (doc.date !== undefined && (!isStr(doc.date) || !DATE_RE5.test(doc.date))) {
|
|
3618
|
-
violations.push(
|
|
3932
|
+
violations.push(violation11("medium", "compound.schema.invalid-date", `date "${String(doc.date)}" must be a YYYY-MM-DD string (schema.yaml required_fields.date)`, "use `YYYY-MM-DD`"));
|
|
3619
3933
|
}
|
|
3620
3934
|
const problemType = doc.problem_type;
|
|
3621
3935
|
if (problemType !== undefined && !isStr(problemType)) {
|
|
3622
|
-
violations.push(
|
|
3936
|
+
violations.push(violation11("medium", "compound.schema.invalid-problem-type", `problem_type "${String(problemType)}" must be a string — one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
|
|
3623
3937
|
}
|
|
3624
3938
|
const problemTypeValid = isStr(problemType) && KNOWLEDGE_PROBLEM_TYPES.includes(problemType);
|
|
3625
3939
|
if (isStr(problemType) && !problemTypeValid) {
|
|
3626
|
-
violations.push(
|
|
3940
|
+
violations.push(violation11("medium", "compound.schema.invalid-problem-type", `problem_type "${problemType}" is not one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
|
|
3627
3941
|
}
|
|
3628
3942
|
if (doc.severity !== undefined && !isStr(doc.severity)) {
|
|
3629
|
-
violations.push(
|
|
3943
|
+
violations.push(violation11("medium", "compound.schema.invalid-severity", `severity "${String(doc.severity)}" must be a string — critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
|
|
3630
3944
|
}
|
|
3631
3945
|
if (isStr(doc.severity) && !KNOWLEDGE_SEVERITIES.includes(doc.severity)) {
|
|
3632
|
-
violations.push(
|
|
3946
|
+
violations.push(violation11("medium", "compound.schema.invalid-severity", `severity "${doc.severity}" must be critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
|
|
3633
3947
|
}
|
|
3634
3948
|
if (problemTypeValid && isStr(doc.category)) {
|
|
3635
3949
|
const expected = KNOWLEDGE_CATEGORY_MAP[problemType];
|
|
3636
3950
|
if (doc.category !== expected) {
|
|
3637
|
-
violations.push(
|
|
3951
|
+
violations.push(violation11("medium", "compound.schema.category-mismatch", `category "${doc.category}" does not match problem_type "${problemType}" — category-mapping.md rule 1 maps it to "${expected}"`, `set \`category: ${expected}\` (the directory name under {KNOWLEDGE_DIR})`));
|
|
3638
3952
|
}
|
|
3639
3953
|
}
|
|
3640
3954
|
if (problemTypeValid) {
|
|
@@ -3642,37 +3956,37 @@ function validateSchemaYaml(frontmatterText) {
|
|
|
3642
3956
|
if (isBug) {
|
|
3643
3957
|
for (const field of ["symptoms", "root_cause", "resolution_type"]) {
|
|
3644
3958
|
if (!(field in doc)) {
|
|
3645
|
-
violations.push(
|
|
3959
|
+
violations.push(violation11("medium", "compound.schema.missing-track-field", `bug-track doc missing required field "${field}" (schema.yaml track_rules.bug)`, `add \`${field}:\` to the frontmatter`));
|
|
3646
3960
|
}
|
|
3647
3961
|
}
|
|
3648
3962
|
if (doc.symptoms !== undefined && !Array.isArray(doc.symptoms)) {
|
|
3649
|
-
violations.push(
|
|
3963
|
+
violations.push(violation11("medium", "compound.schema.invalid-symptoms", "bug-track `symptoms` must be a YAML list (schema.yaml track_rules.bug)", "list the observable symptoms under `symptoms:`"));
|
|
3650
3964
|
}
|
|
3651
3965
|
if (doc.root_cause !== undefined && !isStr(doc.root_cause)) {
|
|
3652
|
-
violations.push(
|
|
3966
|
+
violations.push(violation11("medium", "compound.schema.invalid-root-cause", "bug-track `root_cause` must be a string (schema.yaml track_rules.bug)", "write the fundamental technical cause as a string"));
|
|
3653
3967
|
}
|
|
3654
3968
|
if (isStr(doc.resolution_type) && !KNOWLEDGE_RESOLUTION_TYPES.includes(doc.resolution_type)) {
|
|
3655
|
-
violations.push(
|
|
3969
|
+
violations.push(violation11("medium", "compound.schema.invalid-resolution-type", `resolution_type "${doc.resolution_type}" is not one of the schema.yaml track_rules.bug enum values`, "use code_fix | migration | config_change | test_fix | dependency_update | environment_setup | workflow_improvement | documentation_update | tooling_addition"));
|
|
3656
3970
|
}
|
|
3657
3971
|
} else if (doc.applies_when !== undefined && !Array.isArray(doc.applies_when)) {
|
|
3658
|
-
violations.push(
|
|
3972
|
+
violations.push(violation11("low", "compound.schema.invalid-applies-when", "knowledge-track `applies_when` must be a YAML list when present (schema.yaml track_rules.knowledge)", "list the conditions under `applies_when:`"));
|
|
3659
3973
|
}
|
|
3660
3974
|
}
|
|
3661
3975
|
if (doc.plan_id !== undefined && !isStr(doc.plan_id)) {
|
|
3662
|
-
violations.push(
|
|
3976
|
+
violations.push(violation11("low", "compound.schema.invalid-plan-id", "optional `plan_id` must be a string (schema.yaml optional_fields.plan_id)", "reference the status.json plan id as a string"));
|
|
3663
3977
|
}
|
|
3664
3978
|
if (doc.tags !== undefined) {
|
|
3665
3979
|
if (!Array.isArray(doc.tags)) {
|
|
3666
|
-
violations.push(
|
|
3980
|
+
violations.push(violation11("low", "compound.schema.invalid-tags", "optional `tags` must be a YAML list (schema.yaml optional_fields.tags)", "list lowercase, hyphen-separated keywords"));
|
|
3667
3981
|
} else if (doc.tags.length > 8) {
|
|
3668
|
-
violations.push(
|
|
3982
|
+
violations.push(violation11("low", "compound.schema.tags-too-many", `tags has ${doc.tags.length} entries — max 8 (schema.yaml optional_fields.tags.max_items)`, "trim the tag list to at most 8 keywords"));
|
|
3669
3983
|
}
|
|
3670
3984
|
}
|
|
3671
3985
|
if (doc.last_updated !== undefined && (!isStr(doc.last_updated) || !DATE_RE5.test(doc.last_updated))) {
|
|
3672
|
-
violations.push(
|
|
3986
|
+
violations.push(violation11("low", "compound.schema.invalid-last-updated", `last_updated "${String(doc.last_updated)}" must be YYYY-MM-DD (schema.yaml optional_fields.last_updated)`, "use `YYYY-MM-DD`"));
|
|
3673
3987
|
}
|
|
3674
3988
|
if (doc.related_components !== undefined && !Array.isArray(doc.related_components)) {
|
|
3675
|
-
violations.push(
|
|
3989
|
+
violations.push(violation11("low", "compound.schema.invalid-related-components", "optional `related_components` must be a YAML list (schema.yaml optional_fields.related_components)", "list the other components involved"));
|
|
3676
3990
|
}
|
|
3677
3991
|
return { ok: violations.length === 0, violations };
|
|
3678
3992
|
}
|
|
@@ -3694,7 +4008,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3694
4008
|
if (ref === "" || seen.has(ref))
|
|
3695
4009
|
continue;
|
|
3696
4010
|
seen.add(ref);
|
|
3697
|
-
if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") ||
|
|
4011
|
+
if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute9(ref)) {
|
|
3698
4012
|
continue;
|
|
3699
4013
|
}
|
|
3700
4014
|
if (ref.includes("/") || REF_EXT_RE.test(ref)) {
|
|
@@ -3722,7 +4036,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3722
4036
|
break;
|
|
3723
4037
|
if (entry.isDirectory()) {
|
|
3724
4038
|
if (!WALK_SKIP_DIRS.has(entry.name))
|
|
3725
|
-
stack.push(
|
|
4039
|
+
stack.push(join16(dir, entry.name));
|
|
3726
4040
|
} else if (!entry.isSymbolicLink()) {
|
|
3727
4041
|
const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
3728
4042
|
if (moduleNames.has(base))
|
|
@@ -3734,15 +4048,15 @@ function referenceExists(repoRoot, docText) {
|
|
|
3734
4048
|
for (const { ref, isSymbol, module } of refs) {
|
|
3735
4049
|
if (!isSymbol || module === undefined) {
|
|
3736
4050
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
3737
|
-
if (
|
|
4051
|
+
if (existsSync12(resolve13(repoRoot, candidate))) {
|
|
3738
4052
|
checked++;
|
|
3739
4053
|
} else {
|
|
3740
|
-
violations.push(
|
|
4054
|
+
violations.push(violation11("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"));
|
|
3741
4055
|
}
|
|
3742
4056
|
} else if (foundModules.has(module)) {
|
|
3743
4057
|
checked++;
|
|
3744
4058
|
} else {
|
|
3745
|
-
violations.push(
|
|
4059
|
+
violations.push(violation11("low", "compound.reference.module-missing", `symbol ref \`${ref}\` — heuristic: no ${module}.ts|tsx|js|jsx|mjs|cjs module file found under ${repoRoot} (compound-refresh Phase 2)`, "verify the module file exists, or update the reference"));
|
|
3746
4060
|
}
|
|
3747
4061
|
}
|
|
3748
4062
|
return { ok: violations.length === 0, violations, checked };
|
|
@@ -3761,7 +4075,7 @@ function collectKnowledgeDocs(dir) {
|
|
|
3761
4075
|
for (const entry of entries) {
|
|
3762
4076
|
if (entry.isSymbolicLink())
|
|
3763
4077
|
continue;
|
|
3764
|
-
const full =
|
|
4078
|
+
const full = join16(current, entry.name);
|
|
3765
4079
|
if (entry.isDirectory()) {
|
|
3766
4080
|
stack.push(full);
|
|
3767
4081
|
} else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
|
|
@@ -3781,14 +4095,14 @@ function normalizeIndexRef(cell) {
|
|
|
3781
4095
|
}
|
|
3782
4096
|
function assertIndexRows(knowledgeDir) {
|
|
3783
4097
|
const violations = [];
|
|
3784
|
-
const readmePath =
|
|
3785
|
-
if (!
|
|
3786
|
-
violations.push(
|
|
4098
|
+
const readmePath = join16(knowledgeDir, "README.md");
|
|
4099
|
+
if (!existsSync12(readmePath)) {
|
|
4100
|
+
violations.push(violation11("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"));
|
|
3787
4101
|
return { ok: false, violations };
|
|
3788
4102
|
}
|
|
3789
4103
|
const docs = collectKnowledgeDocs(knowledgeDir);
|
|
3790
4104
|
const rows = new Set;
|
|
3791
|
-
for (const line of
|
|
4105
|
+
for (const line of readFileSync11(readmePath, "utf8").split(/\r?\n/)) {
|
|
3792
4106
|
if (!line.trim().startsWith("|"))
|
|
3793
4107
|
continue;
|
|
3794
4108
|
const cells = line.split("|").map((c) => c.trim());
|
|
@@ -3800,13 +4114,13 @@ function assertIndexRows(knowledgeDir) {
|
|
|
3800
4114
|
}
|
|
3801
4115
|
for (const doc of docs) {
|
|
3802
4116
|
if (!rows.has(doc)) {
|
|
3803
|
-
violations.push(
|
|
4117
|
+
violations.push(violation11("medium", "compound.index.missing-row", `knowledge doc "${doc}" has no row in knowledge/README.md index (mstar-compound Phase 6 index obligations)`, `add a row \`| [<title>](${doc}) | <source plan> | <description> | <status> |\` to knowledge/README.md`));
|
|
3804
4118
|
}
|
|
3805
4119
|
}
|
|
3806
4120
|
return { ok: violations.length === 0, violations };
|
|
3807
4121
|
}
|
|
3808
4122
|
function isFileLikeRoot(root) {
|
|
3809
|
-
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(
|
|
4123
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename8(root));
|
|
3810
4124
|
}
|
|
3811
4125
|
function scopeGuard(path, allowedRoots) {
|
|
3812
4126
|
const resolved = resolve13(path);
|
|
@@ -3822,11 +4136,11 @@ function scopeGuard(path, allowedRoots) {
|
|
|
3822
4136
|
return {
|
|
3823
4137
|
ok: false,
|
|
3824
4138
|
violations: [
|
|
3825
|
-
|
|
4139
|
+
violation11("medium", "compound.scope.outside", `path "${path}" is outside the compound-refresh scope (allowed: ${allowedRoots.join(", ")}) — compound-refresh operates only on {HARNESS_DIR}/knowledge/**, {HARNESS_DIR}/knowledge/README.md, <repo-root>/CONCEPTS.md, {HARNESS_DIR}/status.json (mstar-compound-refresh SKILL.md § 产物与操作路径)`, "point the operation at one of the allowed paths")
|
|
3826
4140
|
]
|
|
3827
4141
|
};
|
|
3828
4142
|
}
|
|
3829
|
-
function
|
|
4143
|
+
function violation12(severity, code, message, fix) {
|
|
3830
4144
|
return { ok: false, severity, code, message, fix };
|
|
3831
4145
|
}
|
|
3832
4146
|
var TASK_ARTIFACT_RE = /\btask-\d+(?:-(?:brief|report|fix-report|diff)|\.diff)\b/g;
|
|
@@ -3856,18 +4170,18 @@ function lintSkillFrontmatter(frontmatterText) {
|
|
|
3856
4170
|
const violations = [];
|
|
3857
4171
|
const fm = parseFrontmatter(frontmatterText);
|
|
3858
4172
|
if (fm === null) {
|
|
3859
|
-
violations.push(
|
|
4173
|
+
violations.push(violation12("medium", "lint.frontmatter.missing", "no YAML frontmatter block found — a skill file must open with a `---` fenced frontmatter (mstar-skill-authoring § Frontmatter Contract)", "add a frontmatter block with `name` and `description` at the top of the file"));
|
|
3860
4174
|
return { ok: false, violations };
|
|
3861
4175
|
}
|
|
3862
4176
|
const name = fm.name ?? "";
|
|
3863
4177
|
if (name === "") {
|
|
3864
|
-
violations.push(
|
|
4178
|
+
violations.push(violation12("medium", "lint.frontmatter.name.missing", "frontmatter `name` is missing — required (mstar-skill-authoring § Frontmatter Contract)", "add `name: <lowercase-hyphen-id>` to the frontmatter"));
|
|
3865
4179
|
} else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
3866
|
-
violations.push(
|
|
4180
|
+
violations.push(violation12("medium", "lint.frontmatter.name.format", `frontmatter \`name\` must be lowercase-hyphen ("${name}") — e.g. example-skill (mstar-skill-authoring § Frontmatter Contract)`, "rename to a stable lowercase-hyphen id, e.g. `name: example-skill`"));
|
|
3867
4181
|
}
|
|
3868
4182
|
const description = fm.description ?? "";
|
|
3869
4183
|
if (description === "") {
|
|
3870
|
-
violations.push(
|
|
4184
|
+
violations.push(violation12("medium", "lint.frontmatter.description.missing", "frontmatter `description` is missing — the trigger contract is required (mstar-skill-authoring § Frontmatter Contract)", "add a `description:` that states when the skill loads (symptoms, context, roles, exclusions)"));
|
|
3871
4185
|
} else {
|
|
3872
4186
|
const stripped = description.replace(/`[^`]*`/g, " ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
|
|
3873
4187
|
let pronoun = null;
|
|
@@ -3878,15 +4192,15 @@ function lintSkillFrontmatter(frontmatterText) {
|
|
|
3878
4192
|
break;
|
|
3879
4193
|
}
|
|
3880
4194
|
if (pronoun !== null) {
|
|
3881
|
-
violations.push(
|
|
4195
|
+
violations.push(violation12("low", "lint.frontmatter.description.person", `description uses first/second-person pronoun "${pronoun[0]}" — keep the trigger contract third person (mstar-skill-authoring § Frontmatter Contract)`, 'rewrite without I/we/you/my/our/your/us, e.g. "Use when the user asks …"'));
|
|
3882
4196
|
}
|
|
3883
4197
|
const start = description.trim().replace(/^[*_#>]+/, "").replace(/^["'`]+/, "").trim();
|
|
3884
4198
|
if (WORKFLOW_VERB_START_RE.test(start)) {
|
|
3885
|
-
violations.push(
|
|
4199
|
+
violations.push(violation12("low", "lint.frontmatter.description.workflow", 'description reads as a workflow summary ("Explains/Describes/Covers …") — the description is the trigger contract, not a summary of steps (mstar-skill-authoring § Frontmatter Contract)', "describe when to load the skill (symptoms, context, roles, exclusions) instead of summarizing its steps"));
|
|
3886
4200
|
} else {
|
|
3887
4201
|
const words = description.trim().split(/\s+/).filter(Boolean).length;
|
|
3888
4202
|
if (words > DESCRIPTION_MAX_WORDS) {
|
|
3889
|
-
violations.push(
|
|
4203
|
+
violations.push(violation12("low", "lint.frontmatter.description.workflow", `description is ${words} words — paragraph-length summaries bury the trigger contract (threshold ${DESCRIPTION_MAX_WORDS}, above the longest corpus description at 114 words, mstar-design-md; mstar-skill-authoring § Frontmatter Contract)`, "trim the description to a scannable trigger contract and move detail into the body"));
|
|
3890
4204
|
}
|
|
3891
4205
|
}
|
|
3892
4206
|
}
|
|
@@ -3914,7 +4228,7 @@ function parseFrontmatter(text) {
|
|
|
3914
4228
|
return null;
|
|
3915
4229
|
return fields;
|
|
3916
4230
|
}
|
|
3917
|
-
function
|
|
4231
|
+
function violation13(severity, code, message, fix) {
|
|
3918
4232
|
return { ok: false, severity, code, message, fix };
|
|
3919
4233
|
}
|
|
3920
4234
|
var ROLE_MAPPING = [
|
|
@@ -3969,19 +4283,19 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
3969
4283
|
const violations = [];
|
|
3970
4284
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
3971
4285
|
for (const { agentId, reference } of mapping) {
|
|
3972
|
-
if (!
|
|
3973
|
-
violations.push(
|
|
4286
|
+
if (!existsSync13(join17(rolesDir, reference))) {
|
|
4287
|
+
violations.push(violation13("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join17(rolesDir, reference)} or fix the mapping row`));
|
|
3974
4288
|
}
|
|
3975
4289
|
}
|
|
3976
4290
|
for (const { family, memberIds } of families) {
|
|
3977
4291
|
const absent = memberIds.filter((id) => !referenceById.has(id));
|
|
3978
4292
|
for (const id of absent) {
|
|
3979
|
-
violations.push(
|
|
4293
|
+
violations.push(violation13("medium", "roles.mapping.family.member.missing", `shared family "${family}" member "${id}" is absent from the role mapping (mstar-roles § Role Reference Mapping)`, `add "${id}" to the mapping`));
|
|
3980
4294
|
}
|
|
3981
4295
|
if (absent.length === 0) {
|
|
3982
4296
|
const refs = new Set(memberIds.map((id) => referenceById.get(id)));
|
|
3983
4297
|
if (refs.size !== 1) {
|
|
3984
|
-
violations.push(
|
|
4298
|
+
violations.push(violation13("medium", "roles.mapping.family.shared", `shared family "${family}" (${memberIds.join(", ")}) must resolve to ONE shared reference file — got ${[...refs].join(", ")} (mstar-roles § Maintenance Rules: "Keep shared-family roles on one shared reference file")`, `point every "${family}" member at the same references/<role>-shared.md`));
|
|
3985
4299
|
}
|
|
3986
4300
|
}
|
|
3987
4301
|
}
|
|
@@ -3990,12 +4304,12 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
3990
4304
|
for (const row of rows) {
|
|
3991
4305
|
const existing = tableByRole.get(row.roleId);
|
|
3992
4306
|
if (existing !== undefined) {
|
|
3993
|
-
violations.push(
|
|
4307
|
+
violations.push(violation13("medium", "roles.param.role.duplicate", `role "${row.roleId}" appears in both the ${existing} and ${table} parameter rows (mstar-roles § Parameter Table (SSOT))`, "remove the duplicate row"));
|
|
3994
4308
|
} else {
|
|
3995
4309
|
tableByRole.set(row.roleId, table);
|
|
3996
4310
|
}
|
|
3997
4311
|
if (!referenceById.has(row.roleId)) {
|
|
3998
|
-
violations.push(
|
|
4312
|
+
violations.push(violation13("medium", "roles.param.role.missing", `${table} parameter row references unknown role "${row.roleId}" (mstar-roles § Parameter Table (SSOT))`, `add "${row.roleId}" to the role mapping or drop the row`));
|
|
3999
4313
|
}
|
|
4000
4314
|
}
|
|
4001
4315
|
};
|
|
@@ -4003,20 +4317,20 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
4003
4317
|
checkParamRoles(qcReviewers, "QC reviewer");
|
|
4004
4318
|
for (const row of devTrack) {
|
|
4005
4319
|
if (row.track !== "primary" && row.track !== "parallel_secondary") {
|
|
4006
|
-
violations.push(
|
|
4320
|
+
violations.push(violation13("medium", "roles.param.track", `dev track for "${row.roleId}" is "${String(row.track)}" — must be primary or parallel_secondary (mstar-roles § Parameter Table (SSOT))`, 'set track to "primary" or "parallel_secondary"'));
|
|
4007
4321
|
}
|
|
4008
4322
|
}
|
|
4009
4323
|
const indices = qcReviewers.map((r) => r.reviewerIndex).sort((a, b) => a - b);
|
|
4010
4324
|
const unique = new Set(indices);
|
|
4011
4325
|
if (indices.length !== 3 || unique.size !== 3 || indices[0] !== 1 || indices[1] !== 2 || indices[2] !== 3) {
|
|
4012
|
-
violations.push(
|
|
4326
|
+
violations.push(violation13("high", "roles.param.qc.index.set", `QC reviewer_index must be exactly {1, 2, 3} across the three qc-specialist* seats — got [${indices.join(", ")}] (mstar-roles § Parameter Table (SSOT))`, "assign reviewer_index 1/2/3 to qc-specialist / qc-specialist-2 / qc-specialist-3"));
|
|
4013
4327
|
}
|
|
4014
4328
|
for (const row of qcReviewers) {
|
|
4015
4329
|
if (row.focus.trim() === "") {
|
|
4016
|
-
violations.push(
|
|
4330
|
+
violations.push(violation13("medium", "roles.param.qc.focus.missing", `QC seat "${row.roleId}" (reviewer_index ${row.reviewerIndex}) has an empty focus (mstar-roles § Parameter Table (SSOT))`, "add the review focus"));
|
|
4017
4331
|
}
|
|
4018
4332
|
if (row.reportSuffix !== `qc${row.reviewerIndex}`) {
|
|
4019
|
-
violations.push(
|
|
4333
|
+
violations.push(violation13("medium", "roles.param.qc.suffix", `QC seat "${row.roleId}" report_suffix "${row.reportSuffix}" must equal qc${row.reviewerIndex} — tri reports land at {SDD_DIR}/review/qc1.md…qc3.md (mstar-roles § Parameter Table (SSOT))`, `set report_suffix to qc${row.reviewerIndex}`));
|
|
4020
4334
|
}
|
|
4021
4335
|
}
|
|
4022
4336
|
return { ok: violations.length === 0, violations };
|
|
@@ -4063,22 +4377,22 @@ function lintLoadOrder(skillTexts) {
|
|
|
4063
4377
|
continue;
|
|
4064
4378
|
const section = extractLoadOrderSection(text);
|
|
4065
4379
|
if (section === null) {
|
|
4066
|
-
violations.push(
|
|
4380
|
+
violations.push(violation13("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section — every mstar-* topic skill must declare its first read (mstar-harness-core § 加载契约; mstar-roles § Load Order = single load-selection authority)`, `add a "## Load Order" section: topics name mstar-harness-core first; mstar-roles declares the Skill presets decision matrix`));
|
|
4067
4381
|
continue;
|
|
4068
4382
|
}
|
|
4069
4383
|
if (name === "mstar-roles") {
|
|
4070
4384
|
const lower = section.toLowerCase();
|
|
4071
4385
|
const missing = HUB_BOOTSTRAP_ASSERTIONS.filter((a) => !lower.includes(a.marker.toLowerCase()));
|
|
4072
4386
|
if (missing.length > 0) {
|
|
4073
|
-
violations.push(
|
|
4387
|
+
violations.push(violation13("medium", "roles.loadorder.hub.bootstrap.missing", `skill "${name}" Load Order section is missing the hub bootstrap decision matrix: ${missing.map((m) => `"${m.marker}" (${m.why})`).join("; ")} (mstar-roles § Load Order = single load-selection authority)`, "declare identity-first, the Skill presets none/standard decision, role-owned methods, and the unknown-preset / missing-identity refusal in the Load Order section"));
|
|
4074
4388
|
}
|
|
4075
4389
|
if (!section.includes("mstar-harness-core")) {
|
|
4076
|
-
violations.push(
|
|
4390
|
+
violations.push(violation13("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not point at mstar-harness-core — the hub bootstrap is exempt from core-first, but core remains the lifecycle/authorization conflict authority and global entry whenever loaded`, "keep the conditional mstar-harness-core pointer (global entry / conflict authority) in the Load Order section"));
|
|
4077
4391
|
}
|
|
4078
4392
|
continue;
|
|
4079
4393
|
}
|
|
4080
4394
|
if (!section.includes("mstar-harness-core")) {
|
|
4081
|
-
violations.push(
|
|
4395
|
+
violations.push(violation13("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core § 加载契约: directly-invoked topics keep core as first dependency; the mstar-roles hub bootstrap is the only exception)`, "name mstar-harness-core first in the Load Order section"));
|
|
4082
4396
|
}
|
|
4083
4397
|
}
|
|
4084
4398
|
return { ok: violations.length === 0, violations };
|
|
@@ -4104,7 +4418,7 @@ function resolveSkillRoot(host, paths) {
|
|
|
4104
4418
|
return `deferred: pi has no plugin API in v1 — skill-root resolution lands with its adapter (roadmap §8.4)`;
|
|
4105
4419
|
}
|
|
4106
4420
|
}
|
|
4107
|
-
function
|
|
4421
|
+
function violation14(severity, code, message, fix) {
|
|
4108
4422
|
return { ok: false, severity, code, message, fix };
|
|
4109
4423
|
}
|
|
4110
4424
|
var FIVE_QUESTION_SECTIONS = [
|
|
@@ -4175,7 +4489,7 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
|
4175
4489
|
const aliases = mode === "runtime" ? RUNTIME_HEADING_ALIASES[section.key] ?? [] : [];
|
|
4176
4490
|
const covered = headings.some((heading) => heading.includes(label) || aliases.some((alias) => heading.includes(alias)));
|
|
4177
4491
|
if (!covered) {
|
|
4178
|
-
violations.push(
|
|
4492
|
+
violations.push(violation14("low", `skill-authoring.five-question.${section.key}`, `body does not answer "${section.question}" — no "${section.label}" section (mstar-skill-authoring § Body 必须回答的 5 问 / § 默认 Body 结构)`, `add a "## ${section.label}" section covering ${section.question}`));
|
|
4179
4493
|
}
|
|
4180
4494
|
}
|
|
4181
4495
|
return { ok: violations.length === 0, violations };
|
|
@@ -4228,7 +4542,7 @@ class DshMstar extends Service {
|
|
|
4228
4542
|
|
|
4229
4543
|
// src/gates/_shared.ts
|
|
4230
4544
|
var import_schemastery = __toESM(require_lib2(), 1);
|
|
4231
|
-
import { existsSync as
|
|
4545
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
4232
4546
|
import { resolve as resolve4 } from "node:path";
|
|
4233
4547
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4234
4548
|
var STATUS_FILE = "status.json";
|
|
@@ -4265,8 +4579,8 @@ var Config = import_schemastery.default.object({
|
|
|
4265
4579
|
workflowGate: import_schemastery.default.union(["off", "warn", "ask", "hard"]).default("warn"),
|
|
4266
4580
|
workflowNames: import_schemastery.default.array(import_schemastery.default.string()).default(undefined)
|
|
4267
4581
|
});
|
|
4268
|
-
function formatViolation(
|
|
4269
|
-
return `[${
|
|
4582
|
+
function formatViolation(violation7) {
|
|
4583
|
+
return `[${violation7.severity}] ${violation7.code}: ${violation7.message}${violation7.fix !== undefined ? ` (fix: ${violation7.fix})` : ""}`;
|
|
4270
4584
|
}
|
|
4271
4585
|
function asRecord(value) {
|
|
4272
4586
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
|
|
@@ -4289,7 +4603,7 @@ function resolvePackagedSkillsDir(fileUrl) {
|
|
|
4289
4603
|
for (const rel of ["../harness-skills", "../../harness-skills"]) {
|
|
4290
4604
|
try {
|
|
4291
4605
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
4292
|
-
if (
|
|
4606
|
+
if (existsSync2(dir))
|
|
4293
4607
|
return dir;
|
|
4294
4608
|
} catch {}
|
|
4295
4609
|
}
|
|
@@ -4308,7 +4622,7 @@ function resolvePackagedAgentsDir(fileUrl) {
|
|
|
4308
4622
|
for (const rel of ["../harness-agents", "../../harness-agents"]) {
|
|
4309
4623
|
try {
|
|
4310
4624
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
4311
|
-
if (
|
|
4625
|
+
if (existsSync2(dir))
|
|
4312
4626
|
return dir;
|
|
4313
4627
|
} catch {}
|
|
4314
4628
|
}
|
|
@@ -4406,21 +4720,21 @@ function skillLocalConfig(config) {
|
|
|
4406
4720
|
}
|
|
4407
4721
|
|
|
4408
4722
|
// src/gates/catalog.ts
|
|
4409
|
-
import { existsSync as
|
|
4410
|
-
import { basename as
|
|
4723
|
+
import { existsSync as existsSync16, readFileSync as readFileSync12, readdirSync as readdirSync4, realpathSync as realpathSync7 } from "node:fs";
|
|
4724
|
+
import { basename as basename4, isAbsolute as isAbsolute8, join as join18, relative as relative7, sep as sep3 } from "node:path";
|
|
4411
4725
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4412
4726
|
|
|
4413
4727
|
// src/gates/agent-flow.ts
|
|
4414
|
-
import { appendFileSync, closeSync, existsSync as
|
|
4415
|
-
import { join as
|
|
4728
|
+
import { appendFileSync, closeSync, existsSync as existsSync14, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync6, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4729
|
+
import { join as join13 } from "node:path";
|
|
4416
4730
|
|
|
4417
4731
|
// src/gates/dispatch.ts
|
|
4418
|
-
import { existsSync as
|
|
4419
|
-
import { basename as basename2, join as
|
|
4732
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
4733
|
+
import { basename as basename2, join as join7, resolve as resolve6 } from "node:path";
|
|
4420
4734
|
|
|
4421
4735
|
// src/gates/workflow-selection.ts
|
|
4422
|
-
import { existsSync as
|
|
4423
|
-
import { dirname as
|
|
4736
|
+
import { existsSync as existsSync6, readdirSync, realpathSync as realpathSync5, statSync as statSync2 } from "node:fs";
|
|
4737
|
+
import { dirname as dirname6, isAbsolute as isAbsolute3, join as join4, normalize, relative as relative4, sep as sep2 } from "node:path";
|
|
4424
4738
|
var TERMINAL_STATUS_CACHE_MAX = 64;
|
|
4425
4739
|
var terminalStatusCache = new Map;
|
|
4426
4740
|
function terminalStatusOf(snapshotPath) {
|
|
@@ -4501,11 +4815,20 @@ function validLeases(snapshot) {
|
|
|
4501
4815
|
return leases;
|
|
4502
4816
|
}
|
|
4503
4817
|
function readSnapshot(harnessDir, entry) {
|
|
4504
|
-
const snapshotPath =
|
|
4505
|
-
if (!
|
|
4818
|
+
const snapshotPath = join4(harnessDir, entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4819
|
+
if (!existsSync6(snapshotPath))
|
|
4506
4820
|
return;
|
|
4507
4821
|
try {
|
|
4508
|
-
|
|
4822
|
+
const snapshot = asRecord(readJson(snapshotPath));
|
|
4823
|
+
if (snapshot === undefined)
|
|
4824
|
+
return;
|
|
4825
|
+
if ("control_worktree_path" in snapshot && "integration_worktree_path" in snapshot)
|
|
4826
|
+
return;
|
|
4827
|
+
if ("control_worktree_path" in snapshot) {
|
|
4828
|
+
const { control_worktree_path, ...rest } = snapshot;
|
|
4829
|
+
return { ...rest, integration_worktree_path: control_worktree_path };
|
|
4830
|
+
}
|
|
4831
|
+
return snapshot;
|
|
4509
4832
|
} catch {
|
|
4510
4833
|
return;
|
|
4511
4834
|
}
|
|
@@ -4537,8 +4860,8 @@ function automaticBinding(harnessDir, entries, hint) {
|
|
|
4537
4860
|
if (holder !== undefined && leases.some((lease) => lease.holder === holder) || cwd !== undefined && leases.some((lease) => within(String(lease.worktree_path), cwd))) {
|
|
4538
4861
|
leaseMatches.set(entry.id, entry);
|
|
4539
4862
|
}
|
|
4540
|
-
const
|
|
4541
|
-
if (cwd !== undefined && typeof
|
|
4863
|
+
const integrationWorktree = snapshot.integration_worktree_path;
|
|
4864
|
+
if (cwd !== undefined && typeof integrationWorktree === "string" && within(integrationWorktree, cwd)) {
|
|
4542
4865
|
cwdMatches.set(entry.id, entry);
|
|
4543
4866
|
}
|
|
4544
4867
|
}
|
|
@@ -4549,8 +4872,8 @@ function automaticBinding(harnessDir, entries, hint) {
|
|
|
4549
4872
|
return byCwd === undefined ? undefined : { kind: "active", workflowId: byCwd.id, dir: byCwd.dir };
|
|
4550
4873
|
}
|
|
4551
4874
|
function resolveActiveWorkflow(harnessDir, hint) {
|
|
4552
|
-
const statusPath =
|
|
4553
|
-
if (!
|
|
4875
|
+
const statusPath = join4(harnessDir, STATUS_FILE);
|
|
4876
|
+
if (!existsSync6(statusPath)) {
|
|
4554
4877
|
return {
|
|
4555
4878
|
kind: "error",
|
|
4556
4879
|
code: "status.missing",
|
|
@@ -4597,7 +4920,7 @@ function resolveActiveWorkflow(harnessDir, hint) {
|
|
|
4597
4920
|
return {
|
|
4598
4921
|
kind: "error",
|
|
4599
4922
|
code: "workflow.selection.invalid-entry",
|
|
4600
|
-
message: `workflows[] entry fails the v2 workflow-entry contract: ${gate2.violations.map((
|
|
4923
|
+
message: `workflows[] entry fails the v2 workflow-entry contract: ${gate2.violations.map((violation7) => violation7.message).join("; ")}`
|
|
4601
4924
|
};
|
|
4602
4925
|
}
|
|
4603
4926
|
const entry = raw;
|
|
@@ -4640,7 +4963,7 @@ function resolveReadWorkflow(harnessDir, hint) {
|
|
|
4640
4963
|
if (active.kind === "error" && active.code !== "workflow.selection.no-active")
|
|
4641
4964
|
return active;
|
|
4642
4965
|
const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
4643
|
-
if (!
|
|
4966
|
+
if (!existsSync6(workflowsDir)) {
|
|
4644
4967
|
return {
|
|
4645
4968
|
kind: "error",
|
|
4646
4969
|
code: "workflow.selection.no-snapshot",
|
|
@@ -4649,7 +4972,7 @@ function resolveReadWorkflow(harnessDir, hint) {
|
|
|
4649
4972
|
}
|
|
4650
4973
|
let entries;
|
|
4651
4974
|
try {
|
|
4652
|
-
entries =
|
|
4975
|
+
entries = readdirSync(workflowsDir, { withFileTypes: true });
|
|
4653
4976
|
} catch {
|
|
4654
4977
|
return {
|
|
4655
4978
|
kind: "error",
|
|
@@ -4661,8 +4984,8 @@ function resolveReadWorkflow(harnessDir, hint) {
|
|
|
4661
4984
|
for (const entry of entries) {
|
|
4662
4985
|
if (!entry.isDirectory())
|
|
4663
4986
|
continue;
|
|
4664
|
-
const snapshotPath =
|
|
4665
|
-
if (!
|
|
4987
|
+
const snapshotPath = join4(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
|
|
4988
|
+
if (!existsSync6(snapshotPath)) {
|
|
4666
4989
|
terminalStatusCache.delete(snapshotPath);
|
|
4667
4990
|
continue;
|
|
4668
4991
|
}
|
|
@@ -4670,11 +4993,11 @@ function resolveReadWorkflow(harnessDir, hint) {
|
|
|
4670
4993
|
if (verdict === undefined || !verdict.terminal)
|
|
4671
4994
|
continue;
|
|
4672
4995
|
if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
|
|
4673
|
-
best = { workflowId: entry.name, dir:
|
|
4996
|
+
best = { workflowId: entry.name, dir: join4(relative4(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
|
|
4674
4997
|
}
|
|
4675
4998
|
}
|
|
4676
4999
|
for (const cachedPath of terminalStatusCache.keys()) {
|
|
4677
|
-
if (!
|
|
5000
|
+
if (!existsSync6(join4(dirname6(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
|
|
4678
5001
|
terminalStatusCache.delete(cachedPath);
|
|
4679
5002
|
}
|
|
4680
5003
|
}
|
|
@@ -4873,12 +5196,12 @@ function leaseGateViolations(harnessDir, exec, writable, prompt, hint) {
|
|
|
4873
5196
|
if (!sdd)
|
|
4874
5197
|
return [];
|
|
4875
5198
|
if (selection.kind === "error" && selection.code === "status.unreadable") {
|
|
4876
|
-
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${
|
|
5199
|
+
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${join7(harnessDir, STATUS_FILE)}: the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid status.json (the status gate refuses invalid writes)")];
|
|
4877
5200
|
}
|
|
4878
|
-
return [leaseViolation("lease.dispatch.unverifiable", `${
|
|
5201
|
+
return [leaseViolation("lease.dispatch.unverifiable", `${join7(harnessDir, STATUS_FILE)}: ${selection.kind === "error" ? selection.message : "no active workflow"} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid v2 status.json registering an active workflow (first implement dispatch requires a plan row + lease)")];
|
|
4879
5202
|
}
|
|
4880
|
-
const snapshotPath =
|
|
4881
|
-
if (!
|
|
5203
|
+
const snapshotPath = join7(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5204
|
+
if (!existsSync10(snapshotPath)) {
|
|
4882
5205
|
if (!sdd)
|
|
4883
5206
|
return [];
|
|
4884
5207
|
return [leaseViolation("lease.dispatch.unverifiable", `${snapshotPath} is missing — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid workflow snapshot registering the plan row (first implement dispatch requires a plan row)")];
|
|
@@ -4952,30 +5275,29 @@ function worktreeL2Violations(header) {
|
|
|
4952
5275
|
return violations;
|
|
4953
5276
|
}
|
|
4954
5277
|
function activeSnapshotRows(harnessDir, hint) {
|
|
4955
|
-
if (!
|
|
4956
|
-
return { rows: null,
|
|
5278
|
+
if (!existsSync10(join7(harnessDir, STATUS_FILE)))
|
|
5279
|
+
return { rows: null, snapshot: null, workflowId: null, unreadable: false };
|
|
4957
5280
|
const selection = resolveActiveWorkflow(harnessDir, hint);
|
|
4958
5281
|
if (selection.kind !== "active") {
|
|
4959
5282
|
return {
|
|
4960
5283
|
rows: null,
|
|
4961
|
-
|
|
5284
|
+
snapshot: null,
|
|
5285
|
+
workflowId: null,
|
|
4962
5286
|
unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
|
|
4963
5287
|
};
|
|
4964
5288
|
}
|
|
4965
|
-
const
|
|
4966
|
-
if (!
|
|
4967
|
-
return { rows: null,
|
|
4968
|
-
|
|
5289
|
+
const snapshotDir = join7(harnessDir, selection.dir);
|
|
5290
|
+
if (!existsSync10(join7(snapshotDir, WORKFLOW_SNAPSHOT_FILE))) {
|
|
5291
|
+
return { rows: null, snapshot: null, workflowId: selection.workflowId, unreadable: true };
|
|
5292
|
+
}
|
|
5293
|
+
let snapshot;
|
|
4969
5294
|
try {
|
|
4970
|
-
|
|
5295
|
+
snapshot = readWorkflowSnapshot(snapshotDir).snapshot;
|
|
4971
5296
|
} catch {
|
|
4972
|
-
return { rows: null,
|
|
5297
|
+
return { rows: null, snapshot: null, workflowId: selection.workflowId, unreadable: true };
|
|
4973
5298
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
const controlWorktreePath = typeof doc.control_worktree_path === "string" ? doc.control_worktree_path : undefined;
|
|
4977
|
-
const rows = doc.plans.map((row) => asRecord(row)).filter((row) => row !== undefined);
|
|
4978
|
-
return { rows, controlWorktreePath, unreadable: false };
|
|
5299
|
+
const rows = (Array.isArray(snapshot.plans) ? snapshot.plans : []).map((row) => asRecord(row)).filter((row) => row !== undefined);
|
|
5300
|
+
return { rows, snapshot, workflowId: selection.workflowId, unreadable: false };
|
|
4979
5301
|
}
|
|
4980
5302
|
function worktreeL1Violations(harnessDir, header, hint) {
|
|
4981
5303
|
if (harnessDir === null)
|
|
@@ -4984,10 +5306,7 @@ function worktreeL1Violations(harnessDir, header, hint) {
|
|
|
4984
5306
|
if (planId === undefined || planId === "")
|
|
4985
5307
|
return [];
|
|
4986
5308
|
const read = activeSnapshotRows(harnessDir, hint);
|
|
4987
|
-
if (read.rows === null)
|
|
4988
|
-
return [];
|
|
4989
|
-
const controlWorktreePath = read.controlWorktreePath;
|
|
4990
|
-
if (controlWorktreePath === undefined || controlWorktreePath.trim() === "")
|
|
5309
|
+
if (read.rows === null || read.snapshot === null)
|
|
4991
5310
|
return [];
|
|
4992
5311
|
const row = read.rows.find((r) => r?.id === planId || r?.plan_id === planId);
|
|
4993
5312
|
const lease = asRecord(row?.execution_lease);
|
|
@@ -4996,7 +5315,26 @@ function worktreeL1Violations(harnessDir, header, hint) {
|
|
|
4996
5315
|
if (leaseWorktreePath === undefined || leaseWorktreePath.trim() === "" || leaseWorkingBranch === undefined || leaseWorkingBranch.trim() === "") {
|
|
4997
5316
|
return [];
|
|
4998
5317
|
}
|
|
4999
|
-
|
|
5318
|
+
const snapshot = read.snapshot;
|
|
5319
|
+
const lifecycleBranches = new Set(collectActiveLifecycleBranches([snapshot]));
|
|
5320
|
+
const siblingScan = scanActiveLifecycleBranches(harnessDir, read.workflowId);
|
|
5321
|
+
if (siblingScan.kind === "refusal") {
|
|
5322
|
+
return [worktreeViolation(siblingScan.code, siblingScan.detail, "repair or unregister the unreadable lifecycle state before dispatch")];
|
|
5323
|
+
}
|
|
5324
|
+
for (const branch of siblingScan.branches)
|
|
5325
|
+
lifecycleBranches.add(branch);
|
|
5326
|
+
const expectedMainBranch = assignmentHeaderValue(header, "Main worktree branch") ?? (typeof snapshot.branch?.base === "string" ? snapshot.branch.base : "");
|
|
5327
|
+
return l1PreDispatchCheck({
|
|
5328
|
+
workflowType: snapshot.type,
|
|
5329
|
+
integrationWorktreePath: typeof snapshot.integration_worktree_path === "string" ? snapshot.integration_worktree_path : "",
|
|
5330
|
+
integrationBranch: typeof snapshot.branch?.integration === "string" ? snapshot.branch.integration : "",
|
|
5331
|
+
mainWorktree: readMainWorktree(harnessDir),
|
|
5332
|
+
expectedMainBranch,
|
|
5333
|
+
lifecycleBranches: [...lifecycleBranches],
|
|
5334
|
+
leaseWorktreePath,
|
|
5335
|
+
leaseWorkingBranch,
|
|
5336
|
+
planId
|
|
5337
|
+
}).violations;
|
|
5000
5338
|
}
|
|
5001
5339
|
function writableFanOutUncovered(harnessDir, hint) {
|
|
5002
5340
|
if (harnessDir === null)
|
|
@@ -5053,7 +5391,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec, hintReadOf) {
|
|
|
5053
5391
|
}
|
|
5054
5392
|
const pb = writableFanOutUncovered(harnessDir, hintReadOf().hint);
|
|
5055
5393
|
if (pb.unreadable) {
|
|
5056
|
-
const statusLabel = harnessDir === null ? STATUS_FILE :
|
|
5394
|
+
const statusLabel = harnessDir === null ? STATUS_FILE : join7(harnessDir, STATUS_FILE);
|
|
5057
5395
|
ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
|
|
5058
5396
|
}
|
|
5059
5397
|
const verdict = workflowPolicy(config, adapter.workflowAskCache, {
|
|
@@ -5258,7 +5596,7 @@ function callPairingKey(exec) {
|
|
|
5258
5596
|
function resolveAgentFlowWriteTarget(harnessDir, hint) {
|
|
5259
5597
|
const selection = resolveActiveWorkflow(harnessDir, hint);
|
|
5260
5598
|
if (selection.kind === "active")
|
|
5261
|
-
return { dir:
|
|
5599
|
+
return { dir: join13(harnessDir, selection.dir), selection };
|
|
5262
5600
|
const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
|
|
5263
5601
|
if (!noActiveWarned) {
|
|
5264
5602
|
noActiveWarned = true;
|
|
@@ -5278,7 +5616,7 @@ function sleepSync(ms) {
|
|
|
5278
5616
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
5279
5617
|
}
|
|
5280
5618
|
function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
5281
|
-
const lockDir =
|
|
5619
|
+
const lockDir = join13(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
|
|
5282
5620
|
if (heldWorkflowLocks.has(lockDir)) {
|
|
5283
5621
|
throw new Error(`${lockDir} is already held by this process — withWorkflowDirLock is not reentrant; a nested acquisition on the same workflow dir is a bug`);
|
|
5284
5622
|
}
|
|
@@ -5304,7 +5642,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
5304
5642
|
heldWorkflowLocks.add(lockDir);
|
|
5305
5643
|
try {
|
|
5306
5644
|
try {
|
|
5307
|
-
writeFileSync2(
|
|
5645
|
+
writeFileSync2(join13(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
|
|
5308
5646
|
} catch {}
|
|
5309
5647
|
return fn();
|
|
5310
5648
|
} finally {
|
|
@@ -5313,7 +5651,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
5313
5651
|
const current = statSync5(lockDir);
|
|
5314
5652
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
5315
5653
|
try {
|
|
5316
|
-
unlinkSync2(
|
|
5654
|
+
unlinkSync2(join13(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
|
|
5317
5655
|
} catch {}
|
|
5318
5656
|
rmdirSync(lockDir);
|
|
5319
5657
|
}
|
|
@@ -5321,13 +5659,13 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
5321
5659
|
}
|
|
5322
5660
|
}
|
|
5323
5661
|
function appendEvent(workflowDir, event) {
|
|
5324
|
-
const file =
|
|
5662
|
+
const file = join13(workflowDir, AGENT_FLOW_FILE);
|
|
5325
5663
|
withWorkflowDirLock(workflowDir, () => {
|
|
5326
5664
|
appendFileSync(file, `${JSON.stringify(event)}
|
|
5327
5665
|
`);
|
|
5328
5666
|
if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
|
|
5329
5667
|
return;
|
|
5330
|
-
const content =
|
|
5668
|
+
const content = readFileSync6(file, "utf8");
|
|
5331
5669
|
const lines = content.replace(/\n$/, "").split(`
|
|
5332
5670
|
`);
|
|
5333
5671
|
if (lines.length > AGENT_FLOW_MAX_EVENTS) {
|
|
@@ -5731,7 +6069,7 @@ function summaryOf(events) {
|
|
|
5731
6069
|
function ledgerContent(file, n) {
|
|
5732
6070
|
const size = statSync5(file).size;
|
|
5733
6071
|
if (size <= AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES) {
|
|
5734
|
-
return
|
|
6072
|
+
return readFileSync6(file, "utf8");
|
|
5735
6073
|
}
|
|
5736
6074
|
let windowBytes = AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES;
|
|
5737
6075
|
const fd = openSync(file, "r");
|
|
@@ -5763,8 +6101,8 @@ function ledgerContent(file, n) {
|
|
|
5763
6101
|
}
|
|
5764
6102
|
}
|
|
5765
6103
|
function readAgentFlow(workflowDir, limit) {
|
|
5766
|
-
const file =
|
|
5767
|
-
if (!
|
|
6104
|
+
const file = join13(workflowDir, AGENT_FLOW_FILE);
|
|
6105
|
+
if (!existsSync14(file)) {
|
|
5768
6106
|
return { events: [], summary: [] };
|
|
5769
6107
|
}
|
|
5770
6108
|
const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
|
|
@@ -6087,8 +6425,8 @@ function registerSubagentCatalogListener(ctx, pairing) {
|
|
|
6087
6425
|
}
|
|
6088
6426
|
|
|
6089
6427
|
// src/engine-status-store.ts
|
|
6090
|
-
import { mkdirSync as
|
|
6091
|
-
import { dirname as dirname9, join as
|
|
6428
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync9, renameSync as renameSync3, rmSync, statSync as statSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6429
|
+
import { dirname as dirname9, join as join15 } from "node:path";
|
|
6092
6430
|
var ENGINE_STATUS_SNAPSHOT_VERSION = 1;
|
|
6093
6431
|
var ENGINE_STATUS_SNAPSHOT_ENTRY_VERSION = 1;
|
|
6094
6432
|
var ENGINE_STATUS_SNAPSHOT_MAX_PER_SESSION = 50;
|
|
@@ -6097,7 +6435,7 @@ var ENGINE_STATUS_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
|
6097
6435
|
var ENGINE_STATUS_SNAPSHOT_LOCK_TIMEOUT_MS = 250;
|
|
6098
6436
|
var ENGINE_STATUS_SNAPSHOT_RELATIVE_PATH = "snapshots/engine-status.json";
|
|
6099
6437
|
function engineStatusSnapshotPath(harnessDir) {
|
|
6100
|
-
return
|
|
6438
|
+
return join15(harnessDir, ENGINE_STATUS_SNAPSHOT_RELATIVE_PATH);
|
|
6101
6439
|
}
|
|
6102
6440
|
function engineStatusUnavailable(reason) {
|
|
6103
6441
|
return { kind: "unavailable", reason };
|
|
@@ -6155,7 +6493,7 @@ function readEngineStatusSnapshot(harnessDir, sessionId) {
|
|
|
6155
6493
|
return engineStatusUnavailable("no-session-entry");
|
|
6156
6494
|
let raw;
|
|
6157
6495
|
try {
|
|
6158
|
-
raw =
|
|
6496
|
+
raw = readFileSync9(engineStatusSnapshotPath(harnessDir), "utf8");
|
|
6159
6497
|
} catch (error) {
|
|
6160
6498
|
const code = error.code;
|
|
6161
6499
|
return engineStatusUnavailable(code === "ENOENT" ? "absent" : "unreadable");
|
|
@@ -6195,7 +6533,7 @@ function serializeEnvelope(entries, bindings) {
|
|
|
6195
6533
|
function loadForWrite(harnessDir) {
|
|
6196
6534
|
let raw;
|
|
6197
6535
|
try {
|
|
6198
|
-
raw =
|
|
6536
|
+
raw = readFileSync9(engineStatusSnapshotPath(harnessDir), "utf8");
|
|
6199
6537
|
} catch (error) {
|
|
6200
6538
|
if (error.code === "ENOENT") {
|
|
6201
6539
|
return {
|
|
@@ -6294,7 +6632,7 @@ function writeEngineStatusSnapshot(harnessDir, input) {
|
|
|
6294
6632
|
const file = engineStatusSnapshotPath(harnessDir);
|
|
6295
6633
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
6296
6634
|
try {
|
|
6297
|
-
|
|
6635
|
+
mkdirSync4(dir, { recursive: true });
|
|
6298
6636
|
return withWorkflowDirLock(dir, () => {
|
|
6299
6637
|
const loaded = loadForWrite(harnessDir);
|
|
6300
6638
|
if (loaded.kind === "refused")
|
|
@@ -6353,7 +6691,7 @@ var bindingStoreMemos = new Map;
|
|
|
6353
6691
|
function parseBindingStore(file) {
|
|
6354
6692
|
let raw;
|
|
6355
6693
|
try {
|
|
6356
|
-
raw =
|
|
6694
|
+
raw = readFileSync9(file, "utf8");
|
|
6357
6695
|
} catch {
|
|
6358
6696
|
return { kind: "unavailable", reason: "store-unreadable" };
|
|
6359
6697
|
}
|
|
@@ -6430,7 +6768,7 @@ function updateWorkflowSessionBinding(harnessDir, sessionId, cwd, update) {
|
|
|
6430
6768
|
const dir = dirname9(file);
|
|
6431
6769
|
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
6432
6770
|
try {
|
|
6433
|
-
|
|
6771
|
+
mkdirSync4(dir, { recursive: true });
|
|
6434
6772
|
return withWorkflowDirLock(dir, () => {
|
|
6435
6773
|
const loaded = loadForWrite(harnessDir);
|
|
6436
6774
|
if (loaded.kind === "refused")
|
|
@@ -6479,7 +6817,7 @@ function residualSeverityIndex(severity) {
|
|
|
6479
6817
|
function pluginVersion() {
|
|
6480
6818
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
6481
6819
|
try {
|
|
6482
|
-
const pkg = JSON.parse(
|
|
6820
|
+
const pkg = JSON.parse(readFileSync12(new URL(rel, import.meta.url), "utf8"));
|
|
6483
6821
|
if (typeof pkg.version === "string" && pkg.version !== "")
|
|
6484
6822
|
return pkg.version;
|
|
6485
6823
|
} catch {}
|
|
@@ -6606,7 +6944,7 @@ function renderEngineStatusCatalog(source) {
|
|
|
6606
6944
|
const policy = [
|
|
6607
6945
|
state.pushPolicy !== null ? `push ${state.pushPolicy}` : null,
|
|
6608
6946
|
state.worktreeMode !== null ? `worktree ${state.worktreeMode}` : null,
|
|
6609
|
-
state.
|
|
6947
|
+
state.integrationWorktreePath !== null ? `integration ${state.integrationWorktreePath}` : null
|
|
6610
6948
|
].filter((part) => part !== null).join("; ");
|
|
6611
6949
|
if (policy !== "")
|
|
6612
6950
|
lines.push(`policy: ${policy}`);
|
|
@@ -6645,8 +6983,8 @@ function hhmm(ts) {
|
|
|
6645
6983
|
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
6646
6984
|
}
|
|
6647
6985
|
function harnessStateSource(harnessDir, selection) {
|
|
6648
|
-
const statusPath =
|
|
6649
|
-
if (!
|
|
6986
|
+
const statusPath = join18(harnessDir, STATUS_FILE);
|
|
6987
|
+
if (!existsSync16(statusPath))
|
|
6650
6988
|
return null;
|
|
6651
6989
|
try {
|
|
6652
6990
|
const rollup = residualRollup(harnessDir);
|
|
@@ -6655,9 +6993,9 @@ function harnessStateSource(harnessDir, selection) {
|
|
|
6655
6993
|
if (selection.kind === "error") {
|
|
6656
6994
|
return selectionErrorState(selection, rollup, harnessDir);
|
|
6657
6995
|
}
|
|
6658
|
-
const snapshotPath =
|
|
6996
|
+
const snapshotPath = join18(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
6659
6997
|
let snapshot;
|
|
6660
|
-
if (!
|
|
6998
|
+
if (!existsSync16(snapshotPath)) {
|
|
6661
6999
|
return selectionErrorState({
|
|
6662
7000
|
kind: "error",
|
|
6663
7001
|
code: "workflow.selection.snapshot-unreadable",
|
|
@@ -6665,7 +7003,7 @@ function harnessStateSource(harnessDir, selection) {
|
|
|
6665
7003
|
}, rollup, harnessDir);
|
|
6666
7004
|
}
|
|
6667
7005
|
try {
|
|
6668
|
-
snapshot =
|
|
7006
|
+
snapshot = readWorkflowSnapshot(join18(harnessDir, selection.dir)).snapshot;
|
|
6669
7007
|
} catch {
|
|
6670
7008
|
return selectionErrorState({
|
|
6671
7009
|
kind: "error",
|
|
@@ -6717,11 +7055,11 @@ function harnessStateSource(harnessDir, selection) {
|
|
|
6717
7055
|
specIntegrationBranch: str(branch?.integration),
|
|
6718
7056
|
pushPolicy: str(executionPolicy?.push_policy),
|
|
6719
7057
|
worktreeMode: str(executionPolicy?.worktree_mode),
|
|
6720
|
-
|
|
7058
|
+
integrationWorktreePath: str(snapshot.integration_worktree_path),
|
|
6721
7059
|
leases,
|
|
6722
7060
|
knowledge: knowledgeDigest(harnessDir),
|
|
6723
7061
|
direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
|
|
6724
|
-
agentFlow: readAgentFlow(
|
|
7062
|
+
agentFlow: readAgentFlow(join18(harnessDir, selection.dir), 50)
|
|
6725
7063
|
};
|
|
6726
7064
|
} catch {
|
|
6727
7065
|
return null;
|
|
@@ -6741,7 +7079,7 @@ function selectionErrorState(selection, rollup, harnessDir) {
|
|
|
6741
7079
|
specIntegrationBranch: null,
|
|
6742
7080
|
pushPolicy: null,
|
|
6743
7081
|
worktreeMode: null,
|
|
6744
|
-
|
|
7082
|
+
integrationWorktreePath: null,
|
|
6745
7083
|
leases: [],
|
|
6746
7084
|
knowledge: knowledgeDigest(harnessDir),
|
|
6747
7085
|
direction: null,
|
|
@@ -6751,7 +7089,7 @@ function selectionErrorState(selection, rollup, harnessDir) {
|
|
|
6751
7089
|
function projectRollupSource(harnessDir, residuals) {
|
|
6752
7090
|
const milestones = [];
|
|
6753
7091
|
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
6754
|
-
if (
|
|
7092
|
+
if (existsSync16(projectsDir)) {
|
|
6755
7093
|
let entries;
|
|
6756
7094
|
try {
|
|
6757
7095
|
entries = readdirSync4(projectsDir, { withFileTypes: true });
|
|
@@ -6761,11 +7099,11 @@ function projectRollupSource(harnessDir, residuals) {
|
|
|
6761
7099
|
for (const entry of entries) {
|
|
6762
7100
|
if (!entry.isDirectory())
|
|
6763
7101
|
continue;
|
|
6764
|
-
const roadmapPath =
|
|
6765
|
-
if (!
|
|
7102
|
+
const roadmapPath = join18(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
|
|
7103
|
+
if (!existsSync16(roadmapPath))
|
|
6766
7104
|
continue;
|
|
6767
7105
|
try {
|
|
6768
|
-
const doc = parseCompassFrontmatterText(
|
|
7106
|
+
const doc = parseCompassFrontmatterText(readFileSync12(roadmapPath, "utf8"), roadmapPath);
|
|
6769
7107
|
if (Array.isArray(doc.milestones)) {
|
|
6770
7108
|
for (const milestone of doc.milestones) {
|
|
6771
7109
|
if (typeof milestone === "string" && milestone.trim() !== "")
|
|
@@ -6827,7 +7165,7 @@ function residualRollup(harnessDir) {
|
|
|
6827
7165
|
}
|
|
6828
7166
|
function projectRegisters(harnessDir) {
|
|
6829
7167
|
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
6830
|
-
if (!
|
|
7168
|
+
if (!existsSync16(projectsDir))
|
|
6831
7169
|
return [];
|
|
6832
7170
|
let entries;
|
|
6833
7171
|
try {
|
|
@@ -6839,8 +7177,8 @@ function projectRegisters(harnessDir) {
|
|
|
6839
7177
|
for (const entry of entries) {
|
|
6840
7178
|
if (!entry.isDirectory())
|
|
6841
7179
|
continue;
|
|
6842
|
-
const registerPath =
|
|
6843
|
-
if (!
|
|
7180
|
+
const registerPath = join18(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
7181
|
+
if (!existsSync16(registerPath))
|
|
6844
7182
|
continue;
|
|
6845
7183
|
try {
|
|
6846
7184
|
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
@@ -6851,13 +7189,13 @@ function projectRegisters(harnessDir) {
|
|
|
6851
7189
|
return registers;
|
|
6852
7190
|
}
|
|
6853
7191
|
function knowledgeDigest(harnessDir) {
|
|
6854
|
-
const indexPath =
|
|
6855
|
-
if (!
|
|
7192
|
+
const indexPath = join18(harnessDir, "knowledge", "README.md");
|
|
7193
|
+
if (!existsSync16(indexPath))
|
|
6856
7194
|
return null;
|
|
6857
7195
|
try {
|
|
6858
7196
|
const categories = new Set;
|
|
6859
7197
|
let docCount = 0;
|
|
6860
|
-
for (const line of
|
|
7198
|
+
for (const line of readFileSync12(indexPath, "utf8").split(/\r?\n/)) {
|
|
6861
7199
|
const row = line.trim().match(/^\|(.+)\|$/);
|
|
6862
7200
|
if (row === null)
|
|
6863
7201
|
continue;
|
|
@@ -6880,7 +7218,7 @@ function knowledgeDigest(harnessDir) {
|
|
|
6880
7218
|
}
|
|
6881
7219
|
function compassDirection(compassPath) {
|
|
6882
7220
|
try {
|
|
6883
|
-
const content =
|
|
7221
|
+
const content = readFileSync12(compassPath, "utf8");
|
|
6884
7222
|
const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
|
|
6885
7223
|
if (section === null)
|
|
6886
7224
|
return null;
|
|
@@ -6897,14 +7235,14 @@ function compassDirection(compassPath) {
|
|
|
6897
7235
|
}
|
|
6898
7236
|
function selectedCompass(harnessDir, workflowId, snapshot) {
|
|
6899
7237
|
const ref = snapshot.compass_ref;
|
|
6900
|
-
if (typeof ref !== "string" || ref.trim() === "" ||
|
|
7238
|
+
if (typeof ref !== "string" || ref.trim() === "" || isAbsolute8(ref))
|
|
6901
7239
|
return;
|
|
6902
|
-
const compassPath =
|
|
7240
|
+
const compassPath = join18(harnessDir, ref);
|
|
6903
7241
|
if (!pathInside(harnessDir, compassPath))
|
|
6904
7242
|
return;
|
|
6905
|
-
if (
|
|
7243
|
+
if (basename4(compassPath) !== "delivery-compass.md")
|
|
6906
7244
|
return;
|
|
6907
|
-
if (!
|
|
7245
|
+
if (!existsSync16(compassPath))
|
|
6908
7246
|
return;
|
|
6909
7247
|
let resolvedHarness;
|
|
6910
7248
|
let resolvedCompass;
|
|
@@ -6930,15 +7268,15 @@ function selectedCompass(harnessDir, workflowId, snapshot) {
|
|
|
6930
7268
|
}
|
|
6931
7269
|
function pathInside(root, candidate) {
|
|
6932
7270
|
const rel = relative7(root, candidate);
|
|
6933
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep3}`) && !
|
|
7271
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute8(rel);
|
|
6934
7272
|
}
|
|
6935
7273
|
function iterationGateSource(harnessDir, selection) {
|
|
6936
7274
|
if (harnessDir === null || selection === undefined || selection.kind === "error")
|
|
6937
7275
|
return;
|
|
6938
|
-
const statusPath =
|
|
6939
|
-
if (!
|
|
7276
|
+
const statusPath = join18(harnessDir, STATUS_FILE);
|
|
7277
|
+
if (!existsSync16(statusPath))
|
|
6940
7278
|
return;
|
|
6941
|
-
const snapshotPath =
|
|
7279
|
+
const snapshotPath = join18(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
6942
7280
|
try {
|
|
6943
7281
|
const snapshotDoc = readJson(snapshotPath);
|
|
6944
7282
|
const compass = selectedCompass(harnessDir, selection.workflowId, snapshotDoc);
|
|
@@ -7028,7 +7366,7 @@ function persistEngineStatusSnapshot(ctx, harnessDir, cwd, turn, catalogPayload,
|
|
|
7028
7366
|
}
|
|
7029
7367
|
|
|
7030
7368
|
// src/engine-status-endpoint.ts
|
|
7031
|
-
import { isAbsolute as
|
|
7369
|
+
import { isAbsolute as isAbsolute10 } from "node:path";
|
|
7032
7370
|
|
|
7033
7371
|
// ../../node_modules/.bun/@deepseek-ai+dsh-typert-protocol@0.1.5-rc.2+1e4e7adc57ac8890/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
|
|
7034
7372
|
import { Service as Service2 } from "@deepseek-ai/cordis";
|
|
@@ -7084,7 +7422,7 @@ function liveSeqOf(value) {
|
|
|
7084
7422
|
function isRefusedPath(cwd) {
|
|
7085
7423
|
if (cwd.includes("\x00"))
|
|
7086
7424
|
return true;
|
|
7087
|
-
if (!
|
|
7425
|
+
if (!isAbsolute10(cwd))
|
|
7088
7426
|
return true;
|
|
7089
7427
|
return cwd.split(/[\\/]+/).includes("..");
|
|
7090
7428
|
}
|
|
@@ -7320,12 +7658,12 @@ function installEngineStatusEndpoint(ctx, options) {
|
|
|
7320
7658
|
}
|
|
7321
7659
|
|
|
7322
7660
|
// src/gates/status.ts
|
|
7323
|
-
import { existsSync as
|
|
7324
|
-
import { basename as
|
|
7661
|
+
import { existsSync as existsSync17, readdirSync as readdirSync7 } from "node:fs";
|
|
7662
|
+
import { basename as basename6, join as join20, relative as relative9, resolve as resolve10 } from "node:path";
|
|
7325
7663
|
var LOGGER_NAME = "mstar/status-gate";
|
|
7326
7664
|
function harnessDocKindOfTarget(harnessDir, targetPath) {
|
|
7327
7665
|
const resolved = resolve10(targetPath);
|
|
7328
|
-
const name =
|
|
7666
|
+
const name = basename6(resolved);
|
|
7329
7667
|
if (name !== STATUS_FILE && name !== WORKFLOW_SNAPSHOT_FILE && name !== PROJECT_REGISTER_FILE)
|
|
7330
7668
|
return null;
|
|
7331
7669
|
const rel = relative9(harnessDir, resolved);
|
|
@@ -7337,8 +7675,8 @@ function harnessDocKindOfTarget(harnessDir, targetPath) {
|
|
|
7337
7675
|
workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
7338
7676
|
projectDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
7339
7677
|
} catch {
|
|
7340
|
-
workflowDir =
|
|
7341
|
-
projectDir =
|
|
7678
|
+
workflowDir = join20(harnessDir, "workflows");
|
|
7679
|
+
projectDir = join20(harnessDir, "projects");
|
|
7342
7680
|
}
|
|
7343
7681
|
if (name === WORKFLOW_SNAPSHOT_FILE && /^[^/]+\/snapshot\.json$/.test(relative9(workflowDir, resolved)))
|
|
7344
7682
|
return "snapshot";
|
|
@@ -7402,8 +7740,8 @@ function projectRegisterDocs(harnessDir) {
|
|
|
7402
7740
|
for (const entry of entries) {
|
|
7403
7741
|
if (!entry.isDirectory())
|
|
7404
7742
|
continue;
|
|
7405
|
-
const registerPath =
|
|
7406
|
-
if (!
|
|
7743
|
+
const registerPath = join20(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
7744
|
+
if (!existsSync17(registerPath))
|
|
7407
7745
|
continue;
|
|
7408
7746
|
try {
|
|
7409
7747
|
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
@@ -7476,8 +7814,8 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
|
|
|
7476
7814
|
}
|
|
7477
7815
|
|
|
7478
7816
|
// src/gates/skill-lint.ts
|
|
7479
|
-
import { existsSync as
|
|
7480
|
-
import { basename as
|
|
7817
|
+
import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
|
|
7818
|
+
import { basename as basename7, dirname as dirname11, resolve as resolve12, sep as sep5 } from "node:path";
|
|
7481
7819
|
var SKILL_LINT_LOGGER = "mstar/skill-lint";
|
|
7482
7820
|
|
|
7483
7821
|
class SkillLintVetoError extends Error {
|
|
@@ -7518,7 +7856,7 @@ function lintSkillDoc(doc, options = {}) {
|
|
|
7518
7856
|
return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
|
|
7519
7857
|
}
|
|
7520
7858
|
function lintSkillWrite(doc, options) {
|
|
7521
|
-
const skillId =
|
|
7859
|
+
const skillId = basename7(dirname11(resolve12(options.target)));
|
|
7522
7860
|
const result = lintSkillDoc(doc, { skillId });
|
|
7523
7861
|
if (options.hard && !result.ok) {
|
|
7524
7862
|
throw new SkillLintVetoError(options.target, result.violations);
|
|
@@ -7531,13 +7869,13 @@ function skillRootsOf(config) {
|
|
|
7531
7869
|
return roots.map((root) => root.trim()).filter((root) => root !== "");
|
|
7532
7870
|
}
|
|
7533
7871
|
function isSkillTarget(roots, target) {
|
|
7534
|
-
if (
|
|
7872
|
+
if (basename7(target.displayPath) !== "SKILL.md")
|
|
7535
7873
|
return false;
|
|
7536
7874
|
const resolvedPath = resolve12(target.displayPath);
|
|
7537
7875
|
return roots.some((root) => resolvedPath.startsWith(resolve12(root) + sep5));
|
|
7538
7876
|
}
|
|
7539
7877
|
function skillNameOf(target) {
|
|
7540
|
-
return
|
|
7878
|
+
return basename7(dirname11(target.displayPath));
|
|
7541
7879
|
}
|
|
7542
7880
|
function skillCanonicalForm(target) {
|
|
7543
7881
|
return resolveSkillRoot("dsh", { skill: skillNameOf(target), rel: "SKILL.md" });
|
|
@@ -7550,11 +7888,11 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
|
|
|
7550
7888
|
if (!isSkillTarget(roots, target))
|
|
7551
7889
|
return;
|
|
7552
7890
|
const skillPath = resolve12(target.displayPath);
|
|
7553
|
-
if (!
|
|
7891
|
+
if (!existsSync18(skillPath))
|
|
7554
7892
|
return;
|
|
7555
7893
|
let doc;
|
|
7556
7894
|
try {
|
|
7557
|
-
doc =
|
|
7895
|
+
doc = readFileSync14(skillPath, "utf8");
|
|
7558
7896
|
} catch (error) {
|
|
7559
7897
|
ctx.logger(SKILL_LINT_LOGGER).error(`skill lint degraded to allow (cannot read ${skillPath}): ${error.message}`);
|
|
7560
7898
|
ctx.emit("mstar/skill-lint", {
|
|
@@ -7567,7 +7905,7 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
|
|
|
7567
7905
|
});
|
|
7568
7906
|
return;
|
|
7569
7907
|
}
|
|
7570
|
-
const result = lintSkillDoc(doc, { skillId:
|
|
7908
|
+
const result = lintSkillDoc(doc, { skillId: basename7(dirname11(skillPath)) });
|
|
7571
7909
|
if (result.ok)
|
|
7572
7910
|
return;
|
|
7573
7911
|
const hard = resolveSeamHard(harnessDir, config);
|
|
@@ -7601,8 +7939,8 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
|
|
|
7601
7939
|
}
|
|
7602
7940
|
|
|
7603
7941
|
// src/gates/seams.ts
|
|
7604
|
-
import { existsSync as
|
|
7605
|
-
import { basename as
|
|
7942
|
+
import { existsSync as existsSync19, readFileSync as readFileSync15, readdirSync as readdirSync10 } from "node:fs";
|
|
7943
|
+
import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve14, sep as sep7 } from "node:path";
|
|
7606
7944
|
|
|
7607
7945
|
// ../engine/dist/audit.js
|
|
7608
7946
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
@@ -7801,7 +8139,7 @@ var SEAM_LOGGERS = {
|
|
|
7801
8139
|
roles: "mstar/roles-lint"
|
|
7802
8140
|
};
|
|
7803
8141
|
function isIndexFile(path) {
|
|
7804
|
-
const base =
|
|
8142
|
+
const base = basename9(path);
|
|
7805
8143
|
return base === "README.md" || base === "index.md";
|
|
7806
8144
|
}
|
|
7807
8145
|
function isMarkdownDoc(path) {
|
|
@@ -7834,14 +8172,14 @@ function rolesDirOf(path) {
|
|
|
7834
8172
|
const segments = resolve14(path).split(sep7);
|
|
7835
8173
|
const idx = segments.indexOf("mstar-roles");
|
|
7836
8174
|
if (idx === -1)
|
|
7837
|
-
return
|
|
7838
|
-
return idx === 0 ? sep7 :
|
|
8175
|
+
return dirname12(path);
|
|
8176
|
+
return idx === 0 ? sep7 : join21(sep7, ...segments.slice(1, idx + 1));
|
|
7839
8177
|
}
|
|
7840
8178
|
function isSeamTarget(seam, harnessDir, target) {
|
|
7841
8179
|
const path = resolve14(target.displayPath);
|
|
7842
8180
|
switch (seam) {
|
|
7843
8181
|
case "design-md": {
|
|
7844
|
-
const base =
|
|
8182
|
+
const base = basename9(path);
|
|
7845
8183
|
return base === "DESIGN.md" || base === "DESIGN.dark.md";
|
|
7846
8184
|
}
|
|
7847
8185
|
case "audit":
|
|
@@ -7849,7 +8187,7 @@ function isSeamTarget(seam, harnessDir, target) {
|
|
|
7849
8187
|
case "compound": {
|
|
7850
8188
|
if (harnessDir === null)
|
|
7851
8189
|
return false;
|
|
7852
|
-
return path.startsWith(resolve14(
|
|
8190
|
+
return path.startsWith(resolve14(join21(harnessDir, "knowledge")) + sep7) && isMarkdownDoc(path);
|
|
7853
8191
|
}
|
|
7854
8192
|
case "roles":
|
|
7855
8193
|
return isRolesTarget(path);
|
|
@@ -7866,11 +8204,11 @@ function auditSecretsViolations(findings, file) {
|
|
|
7866
8204
|
}
|
|
7867
8205
|
function validateDesignDoc(doc, path) {
|
|
7868
8206
|
const violations = [...validateDesignTokenFrontmatter(doc).violations];
|
|
7869
|
-
const isDark =
|
|
7870
|
-
const sibling =
|
|
7871
|
-
if (
|
|
7872
|
-
const light = isDark ?
|
|
7873
|
-
const dark = isDark ? doc :
|
|
8207
|
+
const isDark = basename9(path) === "DESIGN.dark.md";
|
|
8208
|
+
const sibling = join21(dirname12(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
|
|
8209
|
+
if (existsSync19(sibling)) {
|
|
8210
|
+
const light = isDark ? readFileSync15(sibling, "utf8") : doc;
|
|
8211
|
+
const dark = isDark ? doc : readFileSync15(sibling, "utf8");
|
|
7874
8212
|
violations.push(...assertLightDarkParity(light, dark).violations);
|
|
7875
8213
|
}
|
|
7876
8214
|
return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
|
|
@@ -7884,21 +8222,21 @@ function validateAuditDoc(doc, path) {
|
|
|
7884
8222
|
function validateCompoundDoc(doc, _path, harnessDir) {
|
|
7885
8223
|
const violations = [...validateSchemaYaml(doc).violations];
|
|
7886
8224
|
if (harnessDir !== null) {
|
|
7887
|
-
violations.push(...referenceExists(
|
|
8225
|
+
violations.push(...referenceExists(dirname12(harnessDir), doc).violations);
|
|
7888
8226
|
}
|
|
7889
8227
|
return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
|
|
7890
8228
|
}
|
|
7891
|
-
function validateRolesState(rolesDir, skillsRoot =
|
|
8229
|
+
function validateRolesState(rolesDir, skillsRoot = dirname12(rolesDir)) {
|
|
7892
8230
|
const violations = [...validateRoleMapping(rolesDir).violations];
|
|
7893
8231
|
const skillTexts = {};
|
|
7894
8232
|
for (const entry of readdirSync10(skillsRoot, { withFileTypes: true })) {
|
|
7895
8233
|
if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
|
|
7896
8234
|
continue;
|
|
7897
|
-
const skillFile =
|
|
7898
|
-
if (!
|
|
8235
|
+
const skillFile = join21(skillsRoot, entry.name, "SKILL.md");
|
|
8236
|
+
if (!existsSync19(skillFile))
|
|
7899
8237
|
continue;
|
|
7900
8238
|
try {
|
|
7901
|
-
skillTexts[entry.name] =
|
|
8239
|
+
skillTexts[entry.name] = readFileSync15(skillFile, "utf8");
|
|
7902
8240
|
} catch {}
|
|
7903
8241
|
}
|
|
7904
8242
|
violations.push(...lintLoadOrder(skillTexts).violations);
|
|
@@ -7929,11 +8267,11 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
|
|
|
7929
8267
|
if (!isSeamTarget(seam, harnessDir, target))
|
|
7930
8268
|
return;
|
|
7931
8269
|
const path = resolve14(target.displayPath);
|
|
7932
|
-
if (!
|
|
8270
|
+
if (!existsSync19(path))
|
|
7933
8271
|
return;
|
|
7934
8272
|
let doc;
|
|
7935
8273
|
try {
|
|
7936
|
-
doc =
|
|
8274
|
+
doc = readFileSync15(path, "utf8");
|
|
7937
8275
|
} catch (error) {
|
|
7938
8276
|
logger.error(`seam lint degraded to allow (cannot read ${path}): ${error.message}`);
|
|
7939
8277
|
emitSeamAdvisory(ctx, seam, target.displayPath, { ok: true, violations: [] }, false, { degraded: true });
|
|
@@ -7945,12 +8283,12 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
|
|
|
7945
8283
|
const hard = resolveSeamHard(harnessDir, config);
|
|
7946
8284
|
const verdict = applyEnforcement(result, { hard });
|
|
7947
8285
|
if (verdict.hardBlocked) {
|
|
7948
|
-
logger.error(`${
|
|
8286
|
+
logger.error(`${basename9(path)} write to ${target.displayPath} ALLOWED as repair (Enforcement: hard; the current on-disk document is already invalid — the intent carries no incoming content, so the vetoable signal is only the pre-write state):
|
|
7949
8287
|
${verdict.violations.map(formatViolation).join(`
|
|
7950
8288
|
`)}`);
|
|
7951
8289
|
emitSeamAdvisory(ctx, seam, target.displayPath, verdict, hard, { repair: true });
|
|
7952
8290
|
} else {
|
|
7953
|
-
logger.warn(`${
|
|
8291
|
+
logger.warn(`${basename9(path)} write to ${target.displayPath} (advisory):
|
|
7954
8292
|
${verdict.violations.map(formatViolation).join(`
|
|
7955
8293
|
`)}`);
|
|
7956
8294
|
emitSeamAdvisory(ctx, seam, target.displayPath, verdict, hard);
|
|
@@ -7999,8 +8337,8 @@ function lintRolesWrite(doc, options) {
|
|
|
7999
8337
|
}
|
|
8000
8338
|
|
|
8001
8339
|
// src/gates/tools.ts
|
|
8002
|
-
import { existsSync as
|
|
8003
|
-
import { join as
|
|
8340
|
+
import { existsSync as existsSync20, readFileSync as readFileSync16 } from "node:fs";
|
|
8341
|
+
import { join as join22, resolve as resolve16 } from "node:path";
|
|
8004
8342
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8005
8343
|
var ITERATION_VIOLATION_SCHEMA = {
|
|
8006
8344
|
type: "object",
|
|
@@ -8165,9 +8503,9 @@ function registerSddIterationTools(ctx, resolver) {
|
|
|
8165
8503
|
presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
|
|
8166
8504
|
isConcurrencySafe: () => false,
|
|
8167
8505
|
async execute(args) {
|
|
8168
|
-
if (!
|
|
8506
|
+
if (!existsSync20(args.snapshot_path))
|
|
8169
8507
|
throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
|
|
8170
|
-
if (!
|
|
8508
|
+
if (!existsSync20(args.compass_path))
|
|
8171
8509
|
throw new Error(`compass file not found: ${args.compass_path}`);
|
|
8172
8510
|
const snapshotDoc = readJson(args.snapshot_path);
|
|
8173
8511
|
const compassDoc = parseCompassFrontmatter(args.compass_path);
|
|
@@ -8221,10 +8559,10 @@ function registerSeamTools(ctx, resolver) {
|
|
|
8221
8559
|
isConcurrencySafe: () => false,
|
|
8222
8560
|
async execute(args) {
|
|
8223
8561
|
const abs = resolve16(args.dir);
|
|
8224
|
-
const lightPath =
|
|
8225
|
-
if (!
|
|
8562
|
+
const lightPath = join22(abs, "DESIGN.md");
|
|
8563
|
+
if (!existsSync20(lightPath))
|
|
8226
8564
|
throw new Error(`design file not found: ${lightPath}`);
|
|
8227
|
-
const light =
|
|
8565
|
+
const light = readFileSync16(lightPath, "utf8");
|
|
8228
8566
|
const result = validateDesignDoc(light, lightPath);
|
|
8229
8567
|
const level = completenessLevel(light);
|
|
8230
8568
|
return {
|
|
@@ -8275,9 +8613,9 @@ function registerSeamTools(ctx, resolver) {
|
|
|
8275
8613
|
isConcurrencySafe: () => false,
|
|
8276
8614
|
async execute(args) {
|
|
8277
8615
|
const abs = resolve16(args.plan_path);
|
|
8278
|
-
if (!
|
|
8616
|
+
if (!existsSync20(abs))
|
|
8279
8617
|
throw new Error(`plan file not found: ${abs}`);
|
|
8280
|
-
const text =
|
|
8618
|
+
const text = readFileSync16(abs, "utf8");
|
|
8281
8619
|
const result = validateAuditDoc(text, abs);
|
|
8282
8620
|
return { ok: result.ok, violations: result.violations.map(iterationViolationView), secrets: result.findings };
|
|
8283
8621
|
}
|
|
@@ -8315,9 +8653,9 @@ function registerSeamTools(ctx, resolver) {
|
|
|
8315
8653
|
isConcurrencySafe: () => false,
|
|
8316
8654
|
async execute(args, exec) {
|
|
8317
8655
|
const abs = resolve16(args.doc_path);
|
|
8318
|
-
if (!
|
|
8656
|
+
if (!existsSync20(abs))
|
|
8319
8657
|
throw new Error(`knowledge doc not found: ${abs}`);
|
|
8320
|
-
const text =
|
|
8658
|
+
const text = readFileSync16(abs, "utf8");
|
|
8321
8659
|
const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
|
|
8322
8660
|
const violations = [...base.violations];
|
|
8323
8661
|
if (args.repo_root !== undefined) {
|
|
@@ -8360,7 +8698,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
8360
8698
|
isConcurrencySafe: () => false,
|
|
8361
8699
|
async execute(args) {
|
|
8362
8700
|
const rolesDir = resolve16(args.roles_dir);
|
|
8363
|
-
if (!
|
|
8701
|
+
if (!existsSync20(join22(rolesDir, "SKILL.md")))
|
|
8364
8702
|
throw new Error(`roles dir not found: ${rolesDir}`);
|
|
8365
8703
|
const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
|
|
8366
8704
|
return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
|
|
@@ -8370,8 +8708,8 @@ function registerSeamTools(ctx, resolver) {
|
|
|
8370
8708
|
}
|
|
8371
8709
|
|
|
8372
8710
|
// src/gates/adapter.ts
|
|
8373
|
-
import { existsSync as
|
|
8374
|
-
import { join as
|
|
8711
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
8712
|
+
import { join as join23 } from "node:path";
|
|
8375
8713
|
import { Service as Service3 } from "@deepseek-ai/cordis";
|
|
8376
8714
|
var HOST_LOGGER = "mstar/host-adapter";
|
|
8377
8715
|
function mergeLeasesMatch(left, right) {
|
|
@@ -8407,7 +8745,7 @@ class DshHostAdapter extends Service3 {
|
|
|
8407
8745
|
this.logSink(level, msg);
|
|
8408
8746
|
}
|
|
8409
8747
|
statusGate(path, kind, harnessDir) {
|
|
8410
|
-
if (!
|
|
8748
|
+
if (!existsSync21(path))
|
|
8411
8749
|
return { ok: true, violations: [] };
|
|
8412
8750
|
return validateStatusDoc(path, kind, harnessDir);
|
|
8413
8751
|
}
|
|
@@ -8486,7 +8824,7 @@ class DshHostAdapter extends Service3 {
|
|
|
8486
8824
|
if (harnessDir !== null) {
|
|
8487
8825
|
const selection = resolveActiveWorkflow(harnessDir);
|
|
8488
8826
|
if (selection.kind === "active") {
|
|
8489
|
-
const snapshotPath =
|
|
8827
|
+
const snapshotPath = join23(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
8490
8828
|
try {
|
|
8491
8829
|
const snapshot = readJson(snapshotPath);
|
|
8492
8830
|
const stored = snapshot?.integration_merge_lease;
|
|
@@ -8512,8 +8850,8 @@ class DshHostAdapter extends Service3 {
|
|
|
8512
8850
|
}
|
|
8513
8851
|
|
|
8514
8852
|
// src/gates/agent-personas.ts
|
|
8515
|
-
import { readFileSync as
|
|
8516
|
-
import { join as
|
|
8853
|
+
import { readFileSync as readFileSync17, readdirSync as readdirSync12, statSync as statSync8 } from "node:fs";
|
|
8854
|
+
import { join as join24, resolve as resolve17, sep as sep8 } from "node:path";
|
|
8517
8855
|
var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
8518
8856
|
var defaultCache = new Map;
|
|
8519
8857
|
function personaFor(roleId, lookup, warn) {
|
|
@@ -8541,7 +8879,7 @@ function subagentRoleIds(agentsDir) {
|
|
|
8541
8879
|
const roleId = name.slice(0, -3);
|
|
8542
8880
|
let content;
|
|
8543
8881
|
try {
|
|
8544
|
-
content =
|
|
8882
|
+
content = readFileSync17(join24(agentsDir, name), "utf8");
|
|
8545
8883
|
} catch {
|
|
8546
8884
|
continue;
|
|
8547
8885
|
}
|
|
@@ -8571,7 +8909,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
|
|
|
8571
8909
|
return hit.text;
|
|
8572
8910
|
let content;
|
|
8573
8911
|
try {
|
|
8574
|
-
content =
|
|
8912
|
+
content = readFileSync17(file, "utf8");
|
|
8575
8913
|
} catch {
|
|
8576
8914
|
return;
|
|
8577
8915
|
}
|
|
@@ -8802,8 +9140,8 @@ function log2(level, message) {
|
|
|
8802
9140
|
}
|
|
8803
9141
|
|
|
8804
9142
|
// src/gates/workflow-ledger.ts
|
|
8805
|
-
import { readFileSync as
|
|
8806
|
-
import { join as
|
|
9143
|
+
import { readFileSync as readFileSync18, renameSync as renameSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9144
|
+
import { join as join25 } from "node:path";
|
|
8807
9145
|
var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
|
|
8808
9146
|
var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
|
|
8809
9147
|
var TOOL_WORKFLOW_AGENT_START = "tool-workflow/agent-start";
|
|
@@ -8863,7 +9201,7 @@ function loadWatermark(workflowDir, fresh = false) {
|
|
|
8863
9201
|
return cached;
|
|
8864
9202
|
const watermark = new Map;
|
|
8865
9203
|
try {
|
|
8866
|
-
const raw =
|
|
9204
|
+
const raw = readFileSync18(join25(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE), "utf8");
|
|
8867
9205
|
const record = asRecord(JSON.parse(raw));
|
|
8868
9206
|
const cursors = asRecord(record?.cursors);
|
|
8869
9207
|
if (cursors !== undefined) {
|
|
@@ -8886,7 +9224,7 @@ function loadWatermark(workflowDir, fresh = false) {
|
|
|
8886
9224
|
}
|
|
8887
9225
|
function saveWatermark(workflowDir, watermark) {
|
|
8888
9226
|
try {
|
|
8889
|
-
const file =
|
|
9227
|
+
const file = join25(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
|
|
8890
9228
|
const tmp = `${file}.tmp`;
|
|
8891
9229
|
writeFileSync7(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
|
|
8892
9230
|
renameSync4(tmp, file);
|
|
@@ -9205,8 +9543,8 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
|
|
|
9205
9543
|
}
|
|
9206
9544
|
|
|
9207
9545
|
// src/gates/goal-bridge.ts
|
|
9208
|
-
import { existsSync as
|
|
9209
|
-
import { join as
|
|
9546
|
+
import { existsSync as existsSync22, readdirSync as readdirSync13 } from "node:fs";
|
|
9547
|
+
import { join as join26 } from "node:path";
|
|
9210
9548
|
var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
|
|
9211
9549
|
var GOAL_CHANGE_EVENT_TYPE = "goal/change";
|
|
9212
9550
|
var GOAL_CHANGE_VERSION = 1;
|
|
@@ -9231,12 +9569,12 @@ function projectRegisterPointer(harnessDir) {
|
|
|
9231
9569
|
for (const entry of readdirSync13(projectsDir, { withFileTypes: true })) {
|
|
9232
9570
|
if (!entry.isDirectory())
|
|
9233
9571
|
continue;
|
|
9234
|
-
const registerPath =
|
|
9235
|
-
if (
|
|
9572
|
+
const registerPath = join26(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
9573
|
+
if (existsSync22(registerPath))
|
|
9236
9574
|
return registerPath;
|
|
9237
9575
|
}
|
|
9238
9576
|
} catch {}
|
|
9239
|
-
return
|
|
9577
|
+
return join26(projectsDir, _DEFAULT_PROJECT, PROJECT_REGISTER_FILE);
|
|
9240
9578
|
}
|
|
9241
9579
|
function blockedAdvisoryOf(envelope) {
|
|
9242
9580
|
const record = asRecord(envelope);
|
|
@@ -9287,8 +9625,8 @@ function registerGoalBridge(ctx, resolver) {
|
|
|
9287
9625
|
}
|
|
9288
9626
|
|
|
9289
9627
|
// src/gates/plan-mode-bridge.ts
|
|
9290
|
-
import { existsSync as
|
|
9291
|
-
import { isAbsolute as
|
|
9628
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
9629
|
+
import { isAbsolute as isAbsolute11, join as join27, relative as relative10, sep as sep9 } from "node:path";
|
|
9292
9630
|
|
|
9293
9631
|
// src/gates/steering.ts
|
|
9294
9632
|
function isRootLikeAgent(agent) {
|
|
@@ -9332,7 +9670,7 @@ function errorMessage5(error) {
|
|
|
9332
9670
|
return error instanceof Error ? error.message : String(error);
|
|
9333
9671
|
}
|
|
9334
9672
|
function selectedLifecycle(harnessDir, hint) {
|
|
9335
|
-
if (!
|
|
9673
|
+
if (!existsSync23(join27(harnessDir, STATUS_FILE)))
|
|
9336
9674
|
return { kind: "none" };
|
|
9337
9675
|
const selection = resolveReadWorkflow(harnessDir, hint);
|
|
9338
9676
|
if (selection.kind === "error") {
|
|
@@ -9340,7 +9678,7 @@ function selectedLifecycle(harnessDir, hint) {
|
|
|
9340
9678
|
}
|
|
9341
9679
|
let doc;
|
|
9342
9680
|
try {
|
|
9343
|
-
doc = readJson(
|
|
9681
|
+
doc = readJson(join27(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE));
|
|
9344
9682
|
} catch {
|
|
9345
9683
|
return { kind: "none" };
|
|
9346
9684
|
}
|
|
@@ -9350,13 +9688,13 @@ function selectedLifecycle(harnessDir, hint) {
|
|
|
9350
9688
|
}
|
|
9351
9689
|
function selectedCompassSteers(harnessDir, lifecycle) {
|
|
9352
9690
|
const ref = lifecycle.doc.compass_ref;
|
|
9353
|
-
if (typeof ref !== "string" || ref.trim() === "" ||
|
|
9691
|
+
if (typeof ref !== "string" || ref.trim() === "" || isAbsolute11(ref))
|
|
9354
9692
|
return false;
|
|
9355
|
-
const compassPath =
|
|
9693
|
+
const compassPath = join27(harnessDir, ref);
|
|
9356
9694
|
const inside = relative10(harnessDir, compassPath);
|
|
9357
|
-
if (inside === "" || inside === ".." || inside.startsWith(`..${sep9}`) ||
|
|
9695
|
+
if (inside === "" || inside === ".." || inside.startsWith(`..${sep9}`) || isAbsolute11(inside))
|
|
9358
9696
|
return false;
|
|
9359
|
-
if (!
|
|
9697
|
+
if (!existsSync23(compassPath))
|
|
9360
9698
|
return false;
|
|
9361
9699
|
let fields;
|
|
9362
9700
|
try {
|
|
@@ -9869,7 +10207,7 @@ var inject = ["loader"];
|
|
|
9869
10207
|
function packagedCommandsDir() {
|
|
9870
10208
|
try {
|
|
9871
10209
|
const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
|
|
9872
|
-
return
|
|
10210
|
+
return existsSync24(dir) ? dir : undefined;
|
|
9873
10211
|
} catch {
|
|
9874
10212
|
return;
|
|
9875
10213
|
}
|
|
@@ -9925,7 +10263,7 @@ function registerMstarCommands(ctx) {
|
|
|
9925
10263
|
for (const file of readdirSync14(dir).sort()) {
|
|
9926
10264
|
if (!file.endsWith(".md"))
|
|
9927
10265
|
continue;
|
|
9928
|
-
const parsed = parseCommandMarkdown(
|
|
10266
|
+
const parsed = parseCommandMarkdown(readFileSync19(join28(dir, file), "utf8"));
|
|
9929
10267
|
if (parsed === undefined)
|
|
9930
10268
|
continue;
|
|
9931
10269
|
commandsCtx.commands.register({
|