@mstar-harness/dsh 3.2.6 → 3.3.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/index.js +994 -994
- package/harness-skills/mstar-audit/SKILL.md +3 -2
- package/harness-skills/mstar-audit/references/audit-playbook.md +19 -1
- package/harness-skills/mstar-audit/references/codebase-audit.md +19 -3
- package/harness-skills/mstar-audit/references/finding-format.md +5 -1
- package/harness-skills/mstar-audit/references/pr-review.md +54 -1
- package/harness-skills/mstar-audit/references/security-review.md +221 -0
- package/harness-skills/mstar-conventions/SKILL.md +4 -2
- package/harness-skills/mstar-conventions/references/harness-bootstrap-and-agents-layering.md +5 -1
- package/harness-skills/mstar-harness-core/SKILL.md +1 -1
- package/harness-skills/mstar-roles/SKILL.md +17 -13
- package/harness-skills/mstar-roles/references/architect.md +9 -11
- package/harness-skills/mstar-roles/references/code-reviewer.md +12 -15
- package/harness-skills/mstar-roles/references/frontend-dev.md +9 -11
- package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +9 -11
- package/harness-skills/mstar-roles/references/ops-engineer.md +9 -11
- package/harness-skills/mstar-roles/references/product-manager.md +9 -11
- package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +2 -0
- package/harness-skills/mstar-roles/references/prompt-engineer.md +10 -13
- package/harness-skills/mstar-roles/references/qa-engineer.md +8 -12
- package/harness-skills/mstar-roles/references/qc-specialist-shared.md +10 -17
- package/harness-skills/mstar-roles/references/writing-specialist.md +8 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1106,7 +1106,7 @@ var require_lib2 = __commonJS((exports, module) => {
|
|
|
1106
1106
|
});
|
|
1107
1107
|
|
|
1108
1108
|
// src/index.ts
|
|
1109
|
-
import { existsSync as
|
|
1109
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync14 } from "node:fs";
|
|
1110
1110
|
import { join as join23 } from "node:path";
|
|
1111
1111
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1112
1112
|
import {
|
|
@@ -1122,20 +1122,20 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
1122
1122
|
import { fileURLToPath } from "node:url";
|
|
1123
1123
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
1124
1124
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
1125
|
-
import { existsSync as
|
|
1125
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1126
1126
|
import { execFileSync } from "node:child_process";
|
|
1127
|
-
import { basename as basename2, dirname as
|
|
1128
|
-
import { existsSync as
|
|
1129
|
-
import {
|
|
1130
|
-
import { dirname as dirname4,
|
|
1127
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve5 } from "node:path";
|
|
1128
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
1129
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
1130
|
+
import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
|
|
1131
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
|
|
1131
1132
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1132
1133
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1133
|
-
import { existsSync as
|
|
1134
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1134
1135
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
1135
1136
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1136
|
-
import { mkdirSync as mkdirSync5, readdirSync as
|
|
1137
|
-
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as
|
|
1138
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
1137
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1138
|
+
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve7 } from "node:path";
|
|
1139
1139
|
import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
1140
1140
|
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
|
|
1141
1141
|
import { existsSync as existsSync9 } from "node:fs";
|
|
@@ -1276,220 +1276,380 @@ function isAtOrBelow(dir, root) {
|
|
|
1276
1276
|
const rel = relative(root, dir);
|
|
1277
1277
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
1278
1278
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
let dir = start;
|
|
1289
|
-
for (;; ) {
|
|
1290
|
-
if (!isAtOrBelow2(dir, boundary))
|
|
1291
|
-
return null;
|
|
1292
|
-
for (const candidate of [join3(dir, ".mstar"), join3(dir, ".agents"), join3(dir, ".plans"), join3(dir, "plans")]) {
|
|
1293
|
-
if (isDirectory(candidate))
|
|
1294
|
-
return candidate;
|
|
1295
|
-
}
|
|
1296
|
-
if (dir === boundary)
|
|
1297
|
-
return null;
|
|
1298
|
-
const parent = dirname3(dir);
|
|
1299
|
-
if (parent === dir)
|
|
1300
|
-
return null;
|
|
1301
|
-
dir = parent;
|
|
1302
|
-
}
|
|
1279
|
+
var COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
1280
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1281
|
+
var PLAN_STATUS_DONE = "Done";
|
|
1282
|
+
function typeName(value) {
|
|
1283
|
+
if (value === null)
|
|
1284
|
+
return "null";
|
|
1285
|
+
if (Array.isArray(value))
|
|
1286
|
+
return "array";
|
|
1287
|
+
return typeof value;
|
|
1303
1288
|
}
|
|
1304
|
-
function
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
if (!cdup)
|
|
1312
|
-
return startDir;
|
|
1313
|
-
let boundary = startDir;
|
|
1314
|
-
for (const segment of cdup.split(/[\\/]/)) {
|
|
1315
|
-
if (segment && segment !== ".")
|
|
1316
|
-
boundary = dirname3(boundary);
|
|
1289
|
+
function validateCompassShape(doc) {
|
|
1290
|
+
const issues = [];
|
|
1291
|
+
const expectString = (key, opts = {}) => {
|
|
1292
|
+
const value = doc[key];
|
|
1293
|
+
if (typeof value !== "string") {
|
|
1294
|
+
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
1295
|
+
return;
|
|
1317
1296
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
|
|
1297
|
+
if (opts.min !== undefined && value.length < opts.min) {
|
|
1298
|
+
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
1302
|
+
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
expectString("iteration_id", { min: 1 });
|
|
1306
|
+
expectString("start_date", { regex: DATE_RE });
|
|
1307
|
+
const status = doc.status;
|
|
1308
|
+
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
1309
|
+
issues.push({
|
|
1310
|
+
path: ["status"],
|
|
1311
|
+
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
1312
|
+
});
|
|
1335
1313
|
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
const harness = resolveHarnessDir(startDir, opts);
|
|
1352
|
-
if (harness === null) {
|
|
1353
|
-
throw new Error(`harness dir not found from ${resolve3(startDir)} — cannot resolve the ${fallback} dir (run \`mstar init\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
1314
|
+
expectString("iteration_base_branch", { min: 1 });
|
|
1315
|
+
expectString("target_branch", { min: 1 });
|
|
1316
|
+
const plans = doc.plans;
|
|
1317
|
+
if (plans !== undefined) {
|
|
1318
|
+
if (!Array.isArray(plans)) {
|
|
1319
|
+
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
1320
|
+
} else {
|
|
1321
|
+
plans.forEach((entry, index) => {
|
|
1322
|
+
if (typeof entry !== "string") {
|
|
1323
|
+
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
1324
|
+
} else if (entry.length < 1) {
|
|
1325
|
+
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1354
1329
|
}
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
|
|
1363
|
-
}
|
|
1364
|
-
var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
|
|
1365
|
-
# Principle: process stays local; results are shared with the team.
|
|
1366
|
-
# Default-ignore everything under .mstar/, then re-include the tracked results.
|
|
1367
|
-
.mstar/**
|
|
1368
|
-
!.mstar/AGENTS.md
|
|
1369
|
-
!.mstar/knowledge/
|
|
1370
|
-
!.mstar/knowledge/**
|
|
1371
|
-
!.mstar/specs/
|
|
1372
|
-
!.mstar/specs/**
|
|
1373
|
-
# .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
|
|
1374
|
-
.mstarc
|
|
1375
|
-
`;
|
|
1376
|
-
var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
|
|
1377
|
-
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
1378
|
-
.agents/**
|
|
1379
|
-
!.agents/AGENTS.md
|
|
1380
|
-
!.agents/knowledge/
|
|
1381
|
-
!.agents/knowledge/**
|
|
1382
|
-
!.agents/specs/
|
|
1383
|
-
!.agents/specs/**
|
|
1384
|
-
`;
|
|
1385
|
-
var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
|
|
1386
|
-
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
1387
|
-
var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
1388
|
-
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
1389
|
-
function isDirectory(dir) {
|
|
1390
|
-
try {
|
|
1391
|
-
return statSync2(dir).isDirectory();
|
|
1392
|
-
} catch {
|
|
1393
|
-
return false;
|
|
1330
|
+
const end_date = doc.end_date;
|
|
1331
|
+
if (end_date !== undefined) {
|
|
1332
|
+
if (typeof end_date !== "string") {
|
|
1333
|
+
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
1334
|
+
} else if (!DATE_RE.test(end_date)) {
|
|
1335
|
+
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE}` });
|
|
1336
|
+
}
|
|
1394
1337
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
return
|
|
1338
|
+
if (issues.length > 0)
|
|
1339
|
+
return { ok: false, issues };
|
|
1340
|
+
return {
|
|
1341
|
+
ok: true,
|
|
1342
|
+
data: {
|
|
1343
|
+
iteration_id: doc.iteration_id,
|
|
1344
|
+
start_date: doc.start_date,
|
|
1345
|
+
status,
|
|
1346
|
+
iteration_base_branch: doc.iteration_base_branch,
|
|
1347
|
+
target_branch: doc.target_branch,
|
|
1348
|
+
...plans !== undefined ? { plans } : {},
|
|
1349
|
+
...end_date !== undefined ? { end_date } : {}
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1398
1352
|
}
|
|
1399
1353
|
function violation(severity, code, message, fix) {
|
|
1400
1354
|
return { ok: false, severity, code, message, fix };
|
|
1401
1355
|
}
|
|
1402
|
-
function
|
|
1403
|
-
|
|
1404
|
-
violations.push(violation("high", missingCode, `missing required field: ${field}`));
|
|
1405
|
-
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1406
|
-
violations.push(violation("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
1410
|
-
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
1411
|
-
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
1412
|
-
function isValidClaimedAt(value) {
|
|
1413
|
-
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
1356
|
+
function isPlainObject(value) {
|
|
1357
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1414
1358
|
}
|
|
1415
|
-
function
|
|
1416
|
-
|
|
1417
|
-
if (!isPlainObject(lease)) {
|
|
1359
|
+
function validateCompassFrontmatter(doc) {
|
|
1360
|
+
if (!isPlainObject(doc)) {
|
|
1418
1361
|
return {
|
|
1419
1362
|
ok: false,
|
|
1420
1363
|
violations: [
|
|
1421
|
-
violation("
|
|
1364
|
+
violation("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")
|
|
1422
1365
|
]
|
|
1423
1366
|
};
|
|
1424
1367
|
}
|
|
1425
|
-
|
|
1426
|
-
if (
|
|
1427
|
-
violations.push(violation("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1428
|
-
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1429
|
-
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"));
|
|
1430
|
-
}
|
|
1431
|
-
if (lease.worktree_path === undefined) {
|
|
1432
|
-
violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
1433
|
-
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
1434
|
-
violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
1435
|
-
} else if (!isAbsolute3(lease.worktree_path)) {
|
|
1436
|
-
violations.push(violation("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)"));
|
|
1437
|
-
}
|
|
1438
|
-
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
1439
|
-
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1440
|
-
violations.push(violation("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1441
|
-
}
|
|
1442
|
-
return { ok: violations.length === 0, violations };
|
|
1443
|
-
}
|
|
1444
|
-
function validateIntegrationMergeLease(lease) {
|
|
1445
|
-
const violations = [];
|
|
1446
|
-
if (!isPlainObject(lease)) {
|
|
1368
|
+
const parsed = validateCompassShape(doc);
|
|
1369
|
+
if (!parsed.ok) {
|
|
1447
1370
|
return {
|
|
1448
1371
|
ok: false,
|
|
1449
|
-
violations:
|
|
1450
|
-
|
|
1451
|
-
|
|
1372
|
+
violations: parsed.issues.map((issue) => {
|
|
1373
|
+
const field = issue.path.join(".") || "(root)";
|
|
1374
|
+
return violation("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)`);
|
|
1375
|
+
})
|
|
1452
1376
|
};
|
|
1453
1377
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
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"));
|
|
1378
|
+
const violations = [];
|
|
1379
|
+
const { status, end_date } = parsed.data;
|
|
1380
|
+
if (status === "completed" && end_date === undefined) {
|
|
1381
|
+
violations.push(violation("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"));
|
|
1459
1382
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
1463
|
-
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1464
|
-
violations.push(violation("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1383
|
+
if (status !== "completed" && end_date !== undefined) {
|
|
1384
|
+
violations.push(violation("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"));
|
|
1465
1385
|
}
|
|
1466
1386
|
return { ok: violations.length === 0, violations };
|
|
1467
1387
|
}
|
|
1468
|
-
function
|
|
1469
|
-
|
|
1388
|
+
function registeredPlanIds(compassDoc) {
|
|
1389
|
+
if (!Array.isArray(compassDoc.plans))
|
|
1390
|
+
return [];
|
|
1391
|
+
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
1470
1392
|
}
|
|
1471
|
-
function
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
if (row
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
]
|
|
1481
|
-
};
|
|
1482
|
-
}
|
|
1483
|
-
return {
|
|
1484
|
-
ok: false,
|
|
1485
|
-
violations: [
|
|
1486
|
-
violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
1487
|
-
]
|
|
1488
|
-
};
|
|
1393
|
+
function findPlanRow(snapshotDoc, planId) {
|
|
1394
|
+
if (!Array.isArray(snapshotDoc.plans))
|
|
1395
|
+
return null;
|
|
1396
|
+
for (const row of snapshotDoc.plans) {
|
|
1397
|
+
if (!isPlainObject(row))
|
|
1398
|
+
continue;
|
|
1399
|
+
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
1400
|
+
if (rowId === planId)
|
|
1401
|
+
return row;
|
|
1489
1402
|
}
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
function entryPlansAllDone(snapshotDoc, registered) {
|
|
1490
1406
|
const violations = [];
|
|
1491
|
-
|
|
1492
|
-
|
|
1407
|
+
if (registered.length === 0) {
|
|
1408
|
+
violations.push(violation("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`"));
|
|
1409
|
+
return violations;
|
|
1410
|
+
}
|
|
1411
|
+
for (const planId of registered) {
|
|
1412
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
1413
|
+
if (row === null) {
|
|
1414
|
+
violations.push(violation("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"));
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (row.status !== PLAN_STATUS_DONE) {
|
|
1418
|
+
violations.push(violation("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)`));
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return violations;
|
|
1422
|
+
}
|
|
1423
|
+
function entryFrontmatterComplete(compassDoc) {
|
|
1424
|
+
return validateCompassFrontmatter(compassDoc).violations;
|
|
1425
|
+
}
|
|
1426
|
+
function exitFrontmatterClosed(compassDoc) {
|
|
1427
|
+
const violations = [];
|
|
1428
|
+
if (compassDoc.status !== "completed") {
|
|
1429
|
+
violations.push(violation("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)`));
|
|
1430
|
+
}
|
|
1431
|
+
const endDate = compassDoc.end_date;
|
|
1432
|
+
if (typeof endDate !== "string" || !DATE_RE.test(endDate)) {
|
|
1433
|
+
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)"));
|
|
1434
|
+
}
|
|
1435
|
+
return violations;
|
|
1436
|
+
}
|
|
1437
|
+
function exitBranchCheck(opts) {
|
|
1438
|
+
const violations = [];
|
|
1439
|
+
const { currentBranch, specIntegrationBranch } = opts;
|
|
1440
|
+
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
1441
|
+
violations.push(violation("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)"));
|
|
1442
|
+
} else if (currentBranch !== specIntegrationBranch) {
|
|
1443
|
+
violations.push(violation("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
|
|
1444
|
+
}
|
|
1445
|
+
return violations;
|
|
1446
|
+
}
|
|
1447
|
+
function exitPrBaseCheck(compassDoc, opts) {
|
|
1448
|
+
const violations = [];
|
|
1449
|
+
const target = compassDoc.target_branch;
|
|
1450
|
+
const { prBaseBranch } = opts;
|
|
1451
|
+
if (prBaseBranch === undefined) {
|
|
1452
|
+
violations.push(violation("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
|
|
1453
|
+
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
1454
|
+
violations.push(violation("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)`));
|
|
1455
|
+
}
|
|
1456
|
+
return violations;
|
|
1457
|
+
}
|
|
1458
|
+
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
1459
|
+
const registered = registeredPlanIds(compassDoc);
|
|
1460
|
+
const entryViolations = [
|
|
1461
|
+
...entryPlansAllDone(snapshotDoc, registered),
|
|
1462
|
+
...entryFrontmatterComplete(compassDoc)
|
|
1463
|
+
];
|
|
1464
|
+
const exitViolations = [
|
|
1465
|
+
...exitFrontmatterClosed(compassDoc),
|
|
1466
|
+
...exitBranchCheck(opts),
|
|
1467
|
+
...exitPrBaseCheck(compassDoc, opts)
|
|
1468
|
+
];
|
|
1469
|
+
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
1470
|
+
const row = findPlanRow(snapshotDoc, planId);
|
|
1471
|
+
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
1472
|
+
});
|
|
1473
|
+
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
1474
|
+
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
1475
|
+
let transition;
|
|
1476
|
+
if (!allPlansDone)
|
|
1477
|
+
transition = "phase-2-execute";
|
|
1478
|
+
else if (entry.ok && exit.ok)
|
|
1479
|
+
transition = "phase-4-pr-delivery";
|
|
1480
|
+
else
|
|
1481
|
+
transition = "phase-3-close";
|
|
1482
|
+
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
1483
|
+
return {
|
|
1484
|
+
transition,
|
|
1485
|
+
allPlansDone,
|
|
1486
|
+
entry,
|
|
1487
|
+
exit,
|
|
1488
|
+
ok: gateBlocking.length === 0,
|
|
1489
|
+
violations: gateBlocking
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
function parseCompassFrontmatter(filePath) {
|
|
1493
|
+
return parseCompassFrontmatterText(readFileSync3(filePath, "utf8"), filePath);
|
|
1494
|
+
}
|
|
1495
|
+
function parseCompassFrontmatterText(content, filePath) {
|
|
1496
|
+
const lines = content.split(/\r?\n/);
|
|
1497
|
+
if (lines[0]?.trim() !== "---") {
|
|
1498
|
+
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
1499
|
+
}
|
|
1500
|
+
const end = lines.indexOf("---", 1);
|
|
1501
|
+
if (end === -1) {
|
|
1502
|
+
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
1503
|
+
}
|
|
1504
|
+
const doc = {};
|
|
1505
|
+
let listKey = null;
|
|
1506
|
+
for (let i = 1;i < end; i += 1) {
|
|
1507
|
+
const line = lines[i] ?? "";
|
|
1508
|
+
if (!line.trim() || line.trim().startsWith("#"))
|
|
1509
|
+
continue;
|
|
1510
|
+
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
1511
|
+
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
1512
|
+
if (!Array.isArray(doc[listKey]))
|
|
1513
|
+
doc[listKey] = [];
|
|
1514
|
+
doc[listKey].push(item);
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
listKey = null;
|
|
1518
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
1519
|
+
if (!kv) {
|
|
1520
|
+
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
1521
|
+
}
|
|
1522
|
+
const value = kv[2].trim();
|
|
1523
|
+
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
1524
|
+
listKey = value === "" ? kv[1] : null;
|
|
1525
|
+
}
|
|
1526
|
+
return doc;
|
|
1527
|
+
}
|
|
1528
|
+
function parseFlowArray(raw, filePath) {
|
|
1529
|
+
const inner = raw.slice(1, -1);
|
|
1530
|
+
if (/[[\]]/.test(inner)) {
|
|
1531
|
+
throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
|
|
1532
|
+
}
|
|
1533
|
+
let quote = null;
|
|
1534
|
+
for (const ch of inner) {
|
|
1535
|
+
if (ch === '"' || ch === "'") {
|
|
1536
|
+
if (quote === null)
|
|
1537
|
+
quote = ch;
|
|
1538
|
+
else if (quote === ch)
|
|
1539
|
+
quote = null;
|
|
1540
|
+
} else if (ch === "," && quote !== null) {
|
|
1541
|
+
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (quote !== null) {
|
|
1545
|
+
throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
|
|
1546
|
+
}
|
|
1547
|
+
const items = [];
|
|
1548
|
+
for (const part of inner.split(",")) {
|
|
1549
|
+
const item = part.trim().replace(/^["']|["']$/g, "");
|
|
1550
|
+
if (item === "")
|
|
1551
|
+
continue;
|
|
1552
|
+
items.push(item);
|
|
1553
|
+
}
|
|
1554
|
+
return items;
|
|
1555
|
+
}
|
|
1556
|
+
function isPlainObject2(value) {
|
|
1557
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1558
|
+
}
|
|
1559
|
+
function violation2(severity, code, message, fix) {
|
|
1560
|
+
return { ok: false, severity, code, message, fix };
|
|
1561
|
+
}
|
|
1562
|
+
function validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
|
|
1563
|
+
if (value === undefined) {
|
|
1564
|
+
violations.push(violation2("high", missingCode, `missing required field: ${field}`));
|
|
1565
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1566
|
+
violations.push(violation2("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
var DATE_PART = String.raw`\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])`;
|
|
1570
|
+
var RFC3339_Z_RE = new RegExp(String.raw`^${DATE_PART}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$`);
|
|
1571
|
+
var DATE_ONLY_RE = new RegExp(String.raw`^${DATE_PART}$`);
|
|
1572
|
+
function isValidClaimedAt(value) {
|
|
1573
|
+
return typeof value === "string" && (RFC3339_Z_RE.test(value) || DATE_ONLY_RE.test(value));
|
|
1574
|
+
}
|
|
1575
|
+
function validateExecutionLease(lease) {
|
|
1576
|
+
const violations = [];
|
|
1577
|
+
if (!isPlainObject2(lease)) {
|
|
1578
|
+
return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
violations: [
|
|
1581
|
+
violation2("high", "lease.execution-lease.invalid", "execution_lease must be an object — null and tombstone objects are invalid; writers delete the key on release")
|
|
1582
|
+
]
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
|
|
1586
|
+
if (lease.claimed_at === undefined) {
|
|
1587
|
+
violations.push(violation2("high", "lease.execution-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1588
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1589
|
+
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"));
|
|
1590
|
+
}
|
|
1591
|
+
if (lease.worktree_path === undefined) {
|
|
1592
|
+
violations.push(violation2("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
|
|
1593
|
+
} else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
|
|
1594
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
|
|
1595
|
+
} else if (!isAbsolute2(lease.worktree_path)) {
|
|
1596
|
+
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)"));
|
|
1597
|
+
}
|
|
1598
|
+
validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
|
|
1599
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1600
|
+
violations.push(violation2("medium", "lease.execution-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1601
|
+
}
|
|
1602
|
+
return { ok: violations.length === 0, violations };
|
|
1603
|
+
}
|
|
1604
|
+
function validateIntegrationMergeLease(lease) {
|
|
1605
|
+
const violations = [];
|
|
1606
|
+
if (!isPlainObject2(lease)) {
|
|
1607
|
+
return {
|
|
1608
|
+
ok: false,
|
|
1609
|
+
violations: [
|
|
1610
|
+
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")
|
|
1611
|
+
]
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
|
|
1615
|
+
if (lease.claimed_at === undefined) {
|
|
1616
|
+
violations.push(violation2("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
|
|
1617
|
+
} else if (!isValidClaimedAt(lease.claimed_at)) {
|
|
1618
|
+
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"));
|
|
1619
|
+
}
|
|
1620
|
+
validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
|
|
1621
|
+
validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
|
|
1622
|
+
validateNonEmptyString(violations, lease.target_branch, "target_branch", "lease.merge-lease.missing-target-branch", "lease.merge-lease.invalid-target-branch");
|
|
1623
|
+
if (lease.session_label !== undefined && typeof lease.session_label !== "string") {
|
|
1624
|
+
violations.push(violation2("medium", "lease.merge-lease.invalid-session-label", "session_label must be a string (display only — never used for ownership comparison)"));
|
|
1625
|
+
}
|
|
1626
|
+
return { ok: violations.length === 0, violations };
|
|
1627
|
+
}
|
|
1628
|
+
function planExecutionLeaseLocations(row) {
|
|
1629
|
+
return { row: row.execution_lease };
|
|
1630
|
+
}
|
|
1631
|
+
function verifyPlanExecutionLease(row, planId) {
|
|
1632
|
+
const { row: rowLease } = planExecutionLeaseLocations(row);
|
|
1633
|
+
const lease = rowLease;
|
|
1634
|
+
if (lease === undefined) {
|
|
1635
|
+
if (row.status === "InProgress") {
|
|
1636
|
+
return {
|
|
1637
|
+
ok: false,
|
|
1638
|
+
violations: [
|
|
1639
|
+
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)")
|
|
1640
|
+
]
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
return {
|
|
1644
|
+
ok: false,
|
|
1645
|
+
violations: [
|
|
1646
|
+
violation2("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
|
|
1647
|
+
]
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
const violations = [];
|
|
1651
|
+
violations.push(...validateExecutionLease(lease).violations);
|
|
1652
|
+
return { ok: violations.length === 0, violations, lease };
|
|
1493
1653
|
}
|
|
1494
1654
|
var heldLockDirs = new AsyncLocalStorage;
|
|
1495
1655
|
var BRANCH_FORMS_HINT = '"Working branch: <existing>" | "Working branch: create <new> from <base>" | "Branch policy: direct on <branch> — <reason>"';
|
|
@@ -1498,7 +1658,7 @@ var REQUIRED_FIELDS = [
|
|
|
1498
1658
|
{ key: "delegation", label: "Delegation", code: "delegation" },
|
|
1499
1659
|
{ key: "taskCategory", label: "Task category", code: "task-category" }
|
|
1500
1660
|
];
|
|
1501
|
-
function
|
|
1661
|
+
function violation3(severity, code, message, fix) {
|
|
1502
1662
|
return { ok: false, severity, code, message, fix };
|
|
1503
1663
|
}
|
|
1504
1664
|
function parseAssignmentFields(assignmentText) {
|
|
@@ -1548,11 +1708,11 @@ function parseEnforcementFlag(text) {
|
|
|
1548
1708
|
}
|
|
1549
1709
|
function requireField(violations, value, label, code) {
|
|
1550
1710
|
if (value === undefined) {
|
|
1551
|
-
const v =
|
|
1711
|
+
const v = violation3("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
|
|
1552
1712
|
v.aliases = [`assignment.presence.missing-${code}`];
|
|
1553
1713
|
violations.push(v);
|
|
1554
1714
|
} else if (value === "") {
|
|
1555
|
-
const v =
|
|
1715
|
+
const v = violation3("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
|
|
1556
1716
|
v.aliases = [`assignment.presence.missing-${code}`];
|
|
1557
1717
|
violations.push(v);
|
|
1558
1718
|
}
|
|
@@ -1611,20 +1771,20 @@ function validateAssignmentFields(assignmentText, opts = {}) {
|
|
|
1611
1771
|
const formCount = Number(workingPresent) + Number(policyPresent);
|
|
1612
1772
|
const forms = parseAssignmentBranchForms(assignmentText);
|
|
1613
1773
|
if (formCount === 0) {
|
|
1614
|
-
violations.push(
|
|
1774
|
+
violations.push(violation3("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
|
|
1615
1775
|
} else if (formCount > 1) {
|
|
1616
|
-
violations.push(
|
|
1776
|
+
violations.push(violation3("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}`));
|
|
1617
1777
|
} else if (workingPresent) {
|
|
1618
1778
|
const create = forms.createForm;
|
|
1619
1779
|
if (create !== undefined && (create.base === undefined || create.base.trim() === "" || create.name.trim() === "")) {
|
|
1620
|
-
violations.push(
|
|
1780
|
+
violations.push(violation3("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`)"));
|
|
1621
1781
|
}
|
|
1622
1782
|
} else if (policyPresent) {
|
|
1623
1783
|
const direct = forms.directOn;
|
|
1624
1784
|
if (direct === undefined) {
|
|
1625
|
-
violations.push(
|
|
1785
|
+
violations.push(violation3("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
|
|
1626
1786
|
} else if (direct.reason === "") {
|
|
1627
|
-
violations.push(
|
|
1787
|
+
violations.push(violation3("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
|
|
1628
1788
|
}
|
|
1629
1789
|
}
|
|
1630
1790
|
}
|
|
@@ -1635,7 +1795,7 @@ function assertDefaultBranchProtected(branch, opts = {}) {
|
|
|
1635
1795
|
const violations = [];
|
|
1636
1796
|
const normalized = branch.trim();
|
|
1637
1797
|
if (normalized !== "" && defaultBranches.includes(normalized) && opts.directOnException !== true) {
|
|
1638
|
-
violations.push(
|
|
1798
|
+
violations.push(violation3("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`));
|
|
1639
1799
|
}
|
|
1640
1800
|
return { ok: violations.length === 0, violations };
|
|
1641
1801
|
}
|
|
@@ -1644,7 +1804,7 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
1644
1804
|
const mode = executionMode.trim().toLowerCase().split(/\s+/)[0] ?? "";
|
|
1645
1805
|
let n;
|
|
1646
1806
|
if (mode === "") {
|
|
1647
|
-
violations.push(
|
|
1807
|
+
violations.push(violation3("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
|
|
1648
1808
|
} else if (mode === "sdd") {
|
|
1649
1809
|
n = 3;
|
|
1650
1810
|
} else if (mode === "inline") {
|
|
@@ -1652,14 +1812,14 @@ function executionModeToN(executionMode, opts = {}) {
|
|
|
1652
1812
|
} else if (mode === "targeted") {
|
|
1653
1813
|
const seats = [...new Set((opts.seats ?? []).map((s) => s.trim()).filter((s) => s !== ""))];
|
|
1654
1814
|
if (seats.length === 0) {
|
|
1655
|
-
violations.push(
|
|
1815
|
+
violations.push(violation3("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'));
|
|
1656
1816
|
} else if (seats.length > 3) {
|
|
1657
|
-
violations.push(
|
|
1817
|
+
violations.push(violation3("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"));
|
|
1658
1818
|
} else {
|
|
1659
1819
|
n = seats.length;
|
|
1660
1820
|
}
|
|
1661
1821
|
} else {
|
|
1662
|
-
violations.push(
|
|
1822
|
+
violations.push(violation3("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
|
|
1663
1823
|
}
|
|
1664
1824
|
return n === undefined ? { ok: false, violations } : { ok: true, violations, n };
|
|
1665
1825
|
}
|
|
@@ -1705,7 +1865,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1705
1865
|
return {
|
|
1706
1866
|
ok: false,
|
|
1707
1867
|
violations: [
|
|
1708
|
-
|
|
1868
|
+
violation3("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
|
|
1709
1869
|
]
|
|
1710
1870
|
};
|
|
1711
1871
|
}
|
|
@@ -1713,7 +1873,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
|
|
|
1713
1873
|
return {
|
|
1714
1874
|
ok: false,
|
|
1715
1875
|
violations: [
|
|
1716
|
-
|
|
1876
|
+
violation3("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")
|
|
1717
1877
|
]
|
|
1718
1878
|
};
|
|
1719
1879
|
}
|
|
@@ -1723,45 +1883,45 @@ var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
|
|
|
1723
1883
|
var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
|
|
1724
1884
|
var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
|
|
1725
1885
|
var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
|
|
1726
|
-
function
|
|
1886
|
+
function isPlainObject3(value) {
|
|
1727
1887
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1728
1888
|
}
|
|
1729
|
-
function
|
|
1889
|
+
function violation4(severity, code, message, fix) {
|
|
1730
1890
|
return { ok: false, severity, code, message, fix };
|
|
1731
1891
|
}
|
|
1732
1892
|
function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
|
|
1733
1893
|
if (value === undefined) {
|
|
1734
|
-
violations.push(
|
|
1894
|
+
violations.push(violation4("high", missingCode, `missing required field: ${field}`));
|
|
1735
1895
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1736
|
-
violations.push(
|
|
1896
|
+
violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1737
1897
|
}
|
|
1738
1898
|
}
|
|
1739
1899
|
function validateWorkflowSnapshot(doc) {
|
|
1740
1900
|
const violations = [];
|
|
1741
|
-
if (!
|
|
1901
|
+
if (!isPlainObject3(doc)) {
|
|
1742
1902
|
return {
|
|
1743
1903
|
ok: false,
|
|
1744
|
-
violations: [
|
|
1904
|
+
violations: [violation4("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
|
|
1745
1905
|
};
|
|
1746
1906
|
}
|
|
1747
1907
|
if (doc.schema_version === undefined) {
|
|
1748
|
-
violations.push(
|
|
1908
|
+
violations.push(violation4("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
|
|
1749
1909
|
} else if (doc.schema_version !== 1) {
|
|
1750
|
-
violations.push(
|
|
1910
|
+
violations.push(violation4("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)`));
|
|
1751
1911
|
}
|
|
1752
1912
|
if (doc.version !== undefined) {
|
|
1753
|
-
violations.push(
|
|
1913
|
+
violations.push(violation4("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"));
|
|
1754
1914
|
}
|
|
1755
1915
|
validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
|
|
1756
1916
|
if (doc.type === undefined) {
|
|
1757
|
-
violations.push(
|
|
1917
|
+
violations.push(violation4("high", "workflow.snapshot.missing-type", "missing required field: type"));
|
|
1758
1918
|
} else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
|
|
1759
|
-
violations.push(
|
|
1919
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
|
|
1760
1920
|
}
|
|
1761
1921
|
if (doc.status === undefined) {
|
|
1762
|
-
violations.push(
|
|
1922
|
+
violations.push(violation4("high", "workflow.snapshot.missing-status", "missing required field: status"));
|
|
1763
1923
|
} else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
|
|
1764
|
-
violations.push(
|
|
1924
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
|
|
1765
1925
|
}
|
|
1766
1926
|
validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
|
|
1767
1927
|
validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
|
|
@@ -1769,35 +1929,35 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1769
1929
|
validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
|
|
1770
1930
|
}
|
|
1771
1931
|
if (doc.phase !== undefined && typeof doc.phase !== "string") {
|
|
1772
|
-
violations.push(
|
|
1932
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
|
|
1773
1933
|
}
|
|
1774
1934
|
if (doc.plans === undefined) {
|
|
1775
|
-
violations.push(
|
|
1935
|
+
violations.push(violation4("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
|
|
1776
1936
|
} else if (!Array.isArray(doc.plans)) {
|
|
1777
|
-
violations.push(
|
|
1937
|
+
violations.push(violation4("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
|
|
1778
1938
|
} else {
|
|
1779
1939
|
for (const row of doc.plans) {
|
|
1780
1940
|
violations.push(...validatePlanRow(row).violations);
|
|
1781
|
-
if (
|
|
1941
|
+
if (isPlainObject3(row) && row.execution_lease !== undefined) {
|
|
1782
1942
|
violations.push(...validateExecutionLease(row.execution_lease).violations);
|
|
1783
1943
|
}
|
|
1784
1944
|
}
|
|
1785
1945
|
}
|
|
1786
1946
|
if (doc.execution_policy !== undefined) {
|
|
1787
|
-
if (!
|
|
1788
|
-
violations.push(
|
|
1947
|
+
if (!isPlainObject3(doc.execution_policy)) {
|
|
1948
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
|
|
1789
1949
|
}
|
|
1790
1950
|
}
|
|
1791
1951
|
if (doc.integration_merge_lease !== undefined) {
|
|
1792
1952
|
violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
|
|
1793
1953
|
}
|
|
1794
1954
|
if (doc.branch !== undefined) {
|
|
1795
|
-
if (!
|
|
1796
|
-
violations.push(
|
|
1955
|
+
if (!isPlainObject3(doc.branch)) {
|
|
1956
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
|
|
1797
1957
|
} else {
|
|
1798
1958
|
for (const key of ["base", "integration", "target"]) {
|
|
1799
1959
|
if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
|
|
1800
|
-
violations.push(
|
|
1960
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
|
|
1801
1961
|
}
|
|
1802
1962
|
}
|
|
1803
1963
|
}
|
|
@@ -1805,8 +1965,8 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1805
1965
|
if (doc.control_worktree_path !== undefined) {
|
|
1806
1966
|
validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
|
|
1807
1967
|
}
|
|
1808
|
-
if (doc.legacy_metadata !== undefined && !
|
|
1809
|
-
violations.push(
|
|
1968
|
+
if (doc.legacy_metadata !== undefined && !isPlainObject3(doc.legacy_metadata)) {
|
|
1969
|
+
violations.push(violation4("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
|
|
1810
1970
|
}
|
|
1811
1971
|
if (doc.compass_ref !== undefined) {
|
|
1812
1972
|
validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
|
|
@@ -1814,29 +1974,29 @@ function validateWorkflowSnapshot(doc) {
|
|
|
1814
1974
|
const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
|
|
1815
1975
|
if (terminal) {
|
|
1816
1976
|
if (doc.ended_at === undefined) {
|
|
1817
|
-
violations.push(
|
|
1977
|
+
violations.push(violation4("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
|
|
1818
1978
|
}
|
|
1819
1979
|
if (Array.isArray(doc.plans)) {
|
|
1820
1980
|
for (const row of doc.plans) {
|
|
1821
|
-
if (
|
|
1822
|
-
violations.push(
|
|
1981
|
+
if (isPlainObject3(row) && row.execution_lease !== undefined) {
|
|
1982
|
+
violations.push(violation4("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`));
|
|
1823
1983
|
}
|
|
1824
1984
|
}
|
|
1825
1985
|
}
|
|
1826
1986
|
if (doc.integration_merge_lease !== undefined) {
|
|
1827
|
-
violations.push(
|
|
1987
|
+
violations.push(violation4("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"));
|
|
1828
1988
|
}
|
|
1829
1989
|
}
|
|
1830
1990
|
return { ok: violations.length === 0, violations };
|
|
1831
1991
|
}
|
|
1832
|
-
var
|
|
1992
|
+
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
1833
1993
|
var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
|
|
1834
1994
|
var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
|
|
1835
1995
|
var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
|
|
1836
|
-
function
|
|
1996
|
+
function isPlainObject4(value) {
|
|
1837
1997
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1838
1998
|
}
|
|
1839
|
-
function
|
|
1999
|
+
function violation5(severity, code, message, fix) {
|
|
1840
2000
|
return { ok: false, severity, code, message, fix };
|
|
1841
2001
|
}
|
|
1842
2002
|
function normalizeSeverity(value) {
|
|
@@ -1853,19 +2013,19 @@ function isOpenResidual(entry) {
|
|
|
1853
2013
|
}
|
|
1854
2014
|
function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
|
|
1855
2015
|
if (value === undefined) {
|
|
1856
|
-
violations.push(
|
|
2016
|
+
violations.push(violation5("high", missingCode, `missing required field: ${field}`));
|
|
1857
2017
|
} else if (typeof value !== "string" || value.trim() === "") {
|
|
1858
|
-
violations.push(
|
|
2018
|
+
violations.push(violation5("medium", invalidCode, `${field} must be a non-empty string`));
|
|
1859
2019
|
}
|
|
1860
2020
|
}
|
|
1861
2021
|
function validatePlanRow(row) {
|
|
1862
2022
|
const violations = [];
|
|
1863
|
-
if (!
|
|
1864
|
-
return { ok: false, violations: [
|
|
2023
|
+
if (!isPlainObject4(row)) {
|
|
2024
|
+
return { ok: false, violations: [violation5("high", "status.plan-row.invalid", "plan row must be an object")] };
|
|
1865
2025
|
}
|
|
1866
2026
|
const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
|
|
1867
2027
|
if (id === undefined && planId === undefined) {
|
|
1868
|
-
violations.push(
|
|
2028
|
+
violations.push(violation5("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
|
|
1869
2029
|
} else {
|
|
1870
2030
|
if (id !== undefined) {
|
|
1871
2031
|
validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
|
|
@@ -1874,31 +2034,31 @@ function validatePlanRow(row) {
|
|
|
1874
2034
|
validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
|
|
1875
2035
|
}
|
|
1876
2036
|
if (id !== undefined && planId !== undefined && id !== planId) {
|
|
1877
|
-
violations.push(
|
|
2037
|
+
violations.push(violation5("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
|
|
1878
2038
|
}
|
|
1879
2039
|
}
|
|
1880
2040
|
validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
|
|
1881
2041
|
validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
|
|
1882
2042
|
if (status === undefined) {
|
|
1883
|
-
violations.push(
|
|
2043
|
+
violations.push(violation5("high", "status.plan-row.missing-status", "missing required field: status"));
|
|
1884
2044
|
} else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
|
|
1885
|
-
violations.push(
|
|
2045
|
+
violations.push(violation5("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
|
|
1886
2046
|
}
|
|
1887
|
-
if (metadata !== undefined && !
|
|
1888
|
-
violations.push(
|
|
2047
|
+
if (metadata !== undefined && !isPlainObject4(metadata)) {
|
|
2048
|
+
violations.push(violation5("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
|
|
1889
2049
|
}
|
|
1890
|
-
if (execution_lease !== undefined && !
|
|
1891
|
-
violations.push(
|
|
2050
|
+
if (execution_lease !== undefined && !isPlainObject4(execution_lease)) {
|
|
2051
|
+
violations.push(violation5("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
|
|
1892
2052
|
}
|
|
1893
2053
|
if (status === "Done" && execution_lease !== undefined) {
|
|
1894
|
-
violations.push(
|
|
2054
|
+
violations.push(violation5("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"'));
|
|
1895
2055
|
}
|
|
1896
2056
|
return { ok: violations.length === 0, violations };
|
|
1897
2057
|
}
|
|
1898
2058
|
function validateResidual(entry) {
|
|
1899
2059
|
const violations = [];
|
|
1900
|
-
if (!
|
|
1901
|
-
return { ok: false, violations: [
|
|
2060
|
+
if (!isPlainObject4(entry)) {
|
|
2061
|
+
return { ok: false, violations: [violation5("high", "status.residual.invalid", "residual entry must be an object")] };
|
|
1902
2062
|
}
|
|
1903
2063
|
const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
|
|
1904
2064
|
validateNonEmptyString3(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
|
|
@@ -1907,42 +2067,42 @@ function validateResidual(entry) {
|
|
|
1907
2067
|
validateNonEmptyString3(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
|
|
1908
2068
|
validateNonEmptyString3(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
|
|
1909
2069
|
if (severity === undefined) {
|
|
1910
|
-
violations.push(
|
|
2070
|
+
violations.push(violation5("high", "status.residual.missing-severity", "missing required field: severity"));
|
|
1911
2071
|
} else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
|
|
1912
|
-
violations.push(
|
|
2072
|
+
violations.push(violation5("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
|
|
1913
2073
|
} else if (severity === "warning") {
|
|
1914
|
-
violations.push(
|
|
2074
|
+
violations.push(violation5("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')`));
|
|
1915
2075
|
}
|
|
1916
2076
|
if (decision === undefined) {
|
|
1917
|
-
violations.push(
|
|
2077
|
+
violations.push(violation5("high", "status.residual.missing-decision", "missing required field: decision"));
|
|
1918
2078
|
} else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
|
|
1919
|
-
violations.push(
|
|
2079
|
+
violations.push(violation5("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
|
|
1920
2080
|
}
|
|
1921
2081
|
if (target === undefined) {
|
|
1922
|
-
violations.push(
|
|
2082
|
+
violations.push(violation5("high", "status.residual.missing-target", "missing required field: target"));
|
|
1923
2083
|
} else if (typeof target !== "string" && target !== null) {
|
|
1924
|
-
violations.push(
|
|
2084
|
+
violations.push(violation5("medium", "status.residual.invalid-target", "target must be a string or null"));
|
|
1925
2085
|
}
|
|
1926
2086
|
if (tracking === undefined) {
|
|
1927
|
-
violations.push(
|
|
2087
|
+
violations.push(violation5("high", "status.residual.missing-tracking", "missing required field: tracking"));
|
|
1928
2088
|
} else if (typeof tracking !== "string" && tracking !== null) {
|
|
1929
|
-
violations.push(
|
|
2089
|
+
violations.push(violation5("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
|
|
1930
2090
|
}
|
|
1931
2091
|
if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
|
|
1932
|
-
violations.push(
|
|
2092
|
+
violations.push(violation5("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
|
|
1933
2093
|
}
|
|
1934
|
-
if (closed_at !== undefined && (typeof closed_at !== "string" || !
|
|
1935
|
-
violations.push(
|
|
2094
|
+
if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE2.test(closed_at))) {
|
|
2095
|
+
violations.push(violation5("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
|
|
1936
2096
|
}
|
|
1937
2097
|
if (lifecycle !== undefined) {
|
|
1938
2098
|
if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
|
|
1939
|
-
violations.push(
|
|
2099
|
+
violations.push(violation5("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
|
|
1940
2100
|
} else if (lifecycle !== "open") {
|
|
1941
2101
|
if (closed_at === undefined) {
|
|
1942
|
-
violations.push(
|
|
2102
|
+
violations.push(violation5("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
|
|
1943
2103
|
}
|
|
1944
2104
|
if (entry.closure_note === undefined) {
|
|
1945
|
-
violations.push(
|
|
2105
|
+
violations.push(violation5("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
|
|
1946
2106
|
}
|
|
1947
2107
|
}
|
|
1948
2108
|
}
|
|
@@ -1957,25 +2117,25 @@ function isHarnessRelativePath(dir) {
|
|
|
1957
2117
|
}
|
|
1958
2118
|
function validateWorkflowEntry(entry) {
|
|
1959
2119
|
const violations = [];
|
|
1960
|
-
if (!
|
|
2120
|
+
if (!isPlainObject4(entry)) {
|
|
1961
2121
|
return {
|
|
1962
2122
|
ok: false,
|
|
1963
|
-
violations: [
|
|
2123
|
+
violations: [violation5("high", "status.workflow.invalid", "workflow entry must be an object")]
|
|
1964
2124
|
};
|
|
1965
2125
|
}
|
|
1966
2126
|
validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
|
|
1967
2127
|
if (entry.type === undefined) {
|
|
1968
|
-
violations.push(
|
|
2128
|
+
violations.push(violation5("high", "status.workflow.missing-type", "missing required field: type"));
|
|
1969
2129
|
} else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
|
|
1970
|
-
violations.push(
|
|
2130
|
+
violations.push(violation5("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
|
|
1971
2131
|
}
|
|
1972
2132
|
validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
|
|
1973
2133
|
if (entry.dir === undefined) {
|
|
1974
|
-
violations.push(
|
|
2134
|
+
violations.push(violation5("high", "status.workflow.missing-dir", "missing required field: dir"));
|
|
1975
2135
|
} else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
|
|
1976
|
-
violations.push(
|
|
2136
|
+
violations.push(violation5("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
|
|
1977
2137
|
} else if (!isHarnessRelativePath(entry.dir)) {
|
|
1978
|
-
violations.push(
|
|
2138
|
+
violations.push(violation5("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
|
|
1979
2139
|
}
|
|
1980
2140
|
return { ok: violations.length === 0, violations };
|
|
1981
2141
|
}
|
|
@@ -1985,24 +2145,24 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1985
2145
|
if (typeof docOrPath === "string") {
|
|
1986
2146
|
try {
|
|
1987
2147
|
doc = readJson(docOrPath);
|
|
1988
|
-
harnessDir =
|
|
2148
|
+
harnessDir = dirname4(resolve4(docOrPath));
|
|
1989
2149
|
} catch (error) {
|
|
1990
2150
|
return {
|
|
1991
2151
|
ok: false,
|
|
1992
|
-
violations: [
|
|
2152
|
+
violations: [violation5("high", "status.invalid-json", error.message)]
|
|
1993
2153
|
};
|
|
1994
2154
|
}
|
|
1995
2155
|
} else {
|
|
1996
2156
|
doc = docOrPath;
|
|
1997
2157
|
}
|
|
1998
|
-
if (!
|
|
1999
|
-
return { ok: false, violations: [
|
|
2158
|
+
if (!isPlainObject4(doc)) {
|
|
2159
|
+
return { ok: false, violations: [violation5("high", "status.invalid-doc", "status document must be an object")] };
|
|
2000
2160
|
}
|
|
2001
2161
|
if (doc.version !== 2) {
|
|
2002
2162
|
return {
|
|
2003
2163
|
ok: false,
|
|
2004
2164
|
violations: [
|
|
2005
|
-
|
|
2165
|
+
violation5("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`")
|
|
2006
2166
|
]
|
|
2007
2167
|
};
|
|
2008
2168
|
}
|
|
@@ -2010,7 +2170,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
2010
2170
|
return {
|
|
2011
2171
|
ok: false,
|
|
2012
2172
|
violations: [
|
|
2013
|
-
|
|
2173
|
+
violation5("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`")
|
|
2014
2174
|
]
|
|
2015
2175
|
};
|
|
2016
2176
|
}
|
|
@@ -2018,27 +2178,27 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
2018
2178
|
return {
|
|
2019
2179
|
ok: false,
|
|
2020
2180
|
violations: [
|
|
2021
|
-
|
|
2181
|
+
violation5("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`")
|
|
2022
2182
|
]
|
|
2023
2183
|
};
|
|
2024
2184
|
}
|
|
2025
2185
|
const violations = [];
|
|
2026
2186
|
if (doc.updated_at === undefined) {
|
|
2027
|
-
violations.push(
|
|
2028
|
-
} else if (typeof doc.updated_at !== "string" || !
|
|
2029
|
-
violations.push(
|
|
2187
|
+
violations.push(violation5("high", "status.missing-updated-at", "missing required field: updated_at"));
|
|
2188
|
+
} else if (typeof doc.updated_at !== "string" || !DATE_RE2.test(doc.updated_at)) {
|
|
2189
|
+
violations.push(violation5("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
|
|
2030
2190
|
}
|
|
2031
2191
|
if (doc.workflows === undefined) {
|
|
2032
|
-
violations.push(
|
|
2192
|
+
violations.push(violation5("high", "status.missing-workflows", "missing required field: workflows"));
|
|
2033
2193
|
} else if (!Array.isArray(doc.workflows)) {
|
|
2034
|
-
violations.push(
|
|
2194
|
+
violations.push(violation5("high", "status.invalid-workflows", "workflows must be an array"));
|
|
2035
2195
|
} else {
|
|
2036
2196
|
const seen = new Set;
|
|
2037
2197
|
for (const entry of doc.workflows) {
|
|
2038
2198
|
violations.push(...validateWorkflowEntry(entry).violations);
|
|
2039
|
-
if (
|
|
2199
|
+
if (isPlainObject4(entry) && typeof entry.id === "string") {
|
|
2040
2200
|
if (seen.has(entry.id)) {
|
|
2041
|
-
violations.push(
|
|
2201
|
+
violations.push(violation5("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
|
|
2042
2202
|
}
|
|
2043
2203
|
seen.add(entry.id);
|
|
2044
2204
|
}
|
|
@@ -2047,40 +2207,40 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
2047
2207
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
2048
2208
|
let realHarnessDir = null;
|
|
2049
2209
|
try {
|
|
2050
|
-
realHarnessDir =
|
|
2210
|
+
realHarnessDir = realpathSync(harnessDir);
|
|
2051
2211
|
} catch {}
|
|
2052
2212
|
for (const entry of doc.workflows) {
|
|
2053
|
-
if (!
|
|
2213
|
+
if (!isPlainObject4(entry) || typeof entry.dir !== "string")
|
|
2054
2214
|
continue;
|
|
2055
2215
|
const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
2056
2216
|
const snapshotPath = join6(harnessDir, relSnapshot);
|
|
2057
2217
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
2058
2218
|
let physical;
|
|
2059
2219
|
try {
|
|
2060
|
-
physical =
|
|
2220
|
+
physical = realpathSync(snapshotPath);
|
|
2061
2221
|
} catch {
|
|
2062
|
-
violations.push(
|
|
2222
|
+
violations.push(violation5("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`));
|
|
2063
2223
|
continue;
|
|
2064
2224
|
}
|
|
2065
2225
|
if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
|
|
2066
|
-
violations.push(
|
|
2226
|
+
violations.push(violation5("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)}`));
|
|
2067
2227
|
continue;
|
|
2068
2228
|
}
|
|
2069
2229
|
let snapshot;
|
|
2070
2230
|
try {
|
|
2071
2231
|
snapshot = readJson(snapshotPath);
|
|
2072
2232
|
} catch (error) {
|
|
2073
|
-
violations.push(
|
|
2233
|
+
violations.push(violation5("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
|
|
2074
2234
|
continue;
|
|
2075
2235
|
}
|
|
2076
2236
|
if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
|
|
2077
|
-
violations.push(
|
|
2237
|
+
violations.push(violation5("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`));
|
|
2078
2238
|
}
|
|
2079
2239
|
if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
|
|
2080
|
-
violations.push(
|
|
2240
|
+
violations.push(violation5("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`));
|
|
2081
2241
|
}
|
|
2082
2242
|
if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
|
|
2083
|
-
violations.push(
|
|
2243
|
+
violations.push(violation5("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`));
|
|
2084
2244
|
}
|
|
2085
2245
|
}
|
|
2086
2246
|
}
|
|
@@ -2120,8 +2280,8 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
2120
2280
|
return { hard: false, source: "none" };
|
|
2121
2281
|
}
|
|
2122
2282
|
function resolveMstarcEnforcement(harnessDir) {
|
|
2123
|
-
const dir =
|
|
2124
|
-
const rc = loadMstarc(dir,
|
|
2283
|
+
const dir = resolve4(harnessDir);
|
|
2284
|
+
const rc = loadMstarc(dir, dirname4(dir));
|
|
2125
2285
|
const value = rc?.config.enforcement;
|
|
2126
2286
|
if (value === "hard")
|
|
2127
2287
|
return { hard: true, source: "mstarc" };
|
|
@@ -2135,29 +2295,242 @@ function resolveRepoEnforcement(harnessDir) {
|
|
|
2135
2295
|
return rc;
|
|
2136
2296
|
return resolveCompassEnforcement(harnessDir);
|
|
2137
2297
|
}
|
|
2138
|
-
var
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
2298
|
+
var PROJECT_ROADMAP_FILE = "roadmap.md";
|
|
2299
|
+
var PROJECT_REGISTER_FILE = "residuals.json";
|
|
2300
|
+
var _DEFAULT_PROJECT = "_default";
|
|
2301
|
+
var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2302
|
+
function isPlainObject5(value) {
|
|
2303
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2145
2304
|
}
|
|
2146
|
-
function
|
|
2305
|
+
function violation6(severity, code, message, fix) {
|
|
2147
2306
|
return { ok: false, severity, code, message, fix };
|
|
2148
2307
|
}
|
|
2149
|
-
function
|
|
2150
|
-
|
|
2308
|
+
function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
|
|
2309
|
+
if (value === undefined) {
|
|
2310
|
+
violations.push(violation6("high", missingCode, `missing required field: ${field}`));
|
|
2311
|
+
} else if (typeof value !== "string" || value.trim() === "") {
|
|
2312
|
+
violations.push(violation6("medium", invalidCode, `${field} must be a non-empty string`));
|
|
2313
|
+
}
|
|
2151
2314
|
}
|
|
2152
|
-
function
|
|
2153
|
-
const
|
|
2154
|
-
if (
|
|
2155
|
-
return {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2315
|
+
function validateProjectRegister(doc) {
|
|
2316
|
+
const violations = [];
|
|
2317
|
+
if (!isPlainObject5(doc)) {
|
|
2318
|
+
return {
|
|
2319
|
+
ok: false,
|
|
2320
|
+
violations: [violation6("high", "project.register.invalid", "project register must be an object")]
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
if (doc.entries === undefined) {
|
|
2324
|
+
violations.push(violation6("high", "project.register.missing-entries", "missing required field: entries"));
|
|
2325
|
+
} else if (!isPlainObject5(doc.entries)) {
|
|
2326
|
+
violations.push(violation6("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
|
|
2327
|
+
} else {
|
|
2328
|
+
for (const [key, entries] of Object.entries(doc.entries)) {
|
|
2329
|
+
if (key.trim() === "") {
|
|
2330
|
+
violations.push(violation6("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
|
|
2331
|
+
}
|
|
2332
|
+
if (!Array.isArray(entries)) {
|
|
2333
|
+
violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
for (const entry of entries) {
|
|
2337
|
+
violations.push(...validateResidual(entry).violations);
|
|
2338
|
+
if (!isPlainObject5(entry))
|
|
2339
|
+
continue;
|
|
2340
|
+
validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
|
|
2341
|
+
if (entry.registered_at === undefined) {
|
|
2342
|
+
violations.push(violation6("high", "project.register.missing-registered-at", "missing required field: registered_at"));
|
|
2343
|
+
} else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
|
|
2344
|
+
violations.push(violation6("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
|
|
2345
|
+
}
|
|
2346
|
+
if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
|
|
2347
|
+
violations.push(violation6("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
|
|
2348
|
+
}
|
|
2349
|
+
if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
|
|
2350
|
+
violations.push(violation6("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} — entries are keyed by plan id`));
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return { ok: violations.length === 0, violations };
|
|
2356
|
+
}
|
|
2357
|
+
function findingsCleanupGate(register, planId, opts) {
|
|
2358
|
+
const mode = opts?.mode ?? "allow-residual";
|
|
2359
|
+
const violations = [];
|
|
2360
|
+
const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
|
|
2361
|
+
if (entries === undefined) {
|
|
2362
|
+
return { ok: true, violations };
|
|
2363
|
+
}
|
|
2364
|
+
if (!Array.isArray(entries)) {
|
|
2365
|
+
violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
2366
|
+
return { ok: false, violations };
|
|
2367
|
+
}
|
|
2368
|
+
if (entries.length === 0) {
|
|
2369
|
+
return { ok: true, violations };
|
|
2370
|
+
}
|
|
2371
|
+
for (const entry of entries) {
|
|
2372
|
+
if (!isOpenResidual(entry))
|
|
2373
|
+
continue;
|
|
2374
|
+
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
2375
|
+
const label = `R#${id}`;
|
|
2376
|
+
if (mode === "zero-residual") {
|
|
2377
|
+
if (entry.severity === "nit") {
|
|
2378
|
+
violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
|
|
2379
|
+
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
2380
|
+
violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
2381
|
+
} else if (entry.decision === "defer") {
|
|
2382
|
+
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
2383
|
+
violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
2384
|
+
}
|
|
2385
|
+
} else {
|
|
2386
|
+
violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
|
|
2387
|
+
}
|
|
2388
|
+
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
2389
|
+
violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
return { ok: violations.length === 0, violations };
|
|
2393
|
+
}
|
|
2394
|
+
function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
|
|
2395
|
+
const start = resolve5(startDir);
|
|
2396
|
+
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
2397
|
+
if (explicit)
|
|
2398
|
+
return resolve5(start, explicit);
|
|
2399
|
+
const boundary = resolve5(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
2400
|
+
const rc = loadMstarc(start, boundary);
|
|
2401
|
+
if (rc !== null && rc.config.harnessDir)
|
|
2402
|
+
return resolve5(rc.dir, rc.config.harnessDir);
|
|
2403
|
+
let dir = start;
|
|
2404
|
+
for (;; ) {
|
|
2405
|
+
if (!isAtOrBelow2(dir, boundary))
|
|
2406
|
+
return null;
|
|
2407
|
+
for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
|
|
2408
|
+
if (isDirectory(candidate))
|
|
2409
|
+
return candidate;
|
|
2410
|
+
}
|
|
2411
|
+
if (dir === boundary)
|
|
2412
|
+
return null;
|
|
2413
|
+
const parent = dirname5(dir);
|
|
2414
|
+
if (parent === dir)
|
|
2415
|
+
return null;
|
|
2416
|
+
dir = parent;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
function defaultWorkspaceRoot(startDir) {
|
|
2420
|
+
try {
|
|
2421
|
+
const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
|
|
2422
|
+
cwd: startDir,
|
|
2423
|
+
encoding: "utf8",
|
|
2424
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2425
|
+
}).trim();
|
|
2426
|
+
if (!cdup)
|
|
2427
|
+
return startDir;
|
|
2428
|
+
let boundary = startDir;
|
|
2429
|
+
for (const segment of cdup.split(/[\\/]/)) {
|
|
2430
|
+
if (segment && segment !== ".")
|
|
2431
|
+
boundary = dirname5(boundary);
|
|
2432
|
+
}
|
|
2433
|
+
return resolve5(boundary);
|
|
2434
|
+
} catch {}
|
|
2435
|
+
return startDir;
|
|
2436
|
+
}
|
|
2437
|
+
function isAtOrBelow2(dir, root) {
|
|
2438
|
+
const rel = relative2(root, dir);
|
|
2439
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
2440
|
+
}
|
|
2441
|
+
function mstarcDirOverride(harnessDir, key) {
|
|
2442
|
+
const dir = resolve5(harnessDir);
|
|
2443
|
+
const rc = loadMstarc(dir, dirname5(dir));
|
|
2444
|
+
const declared = rc?.config[key];
|
|
2445
|
+
return declared ? resolve5(rc.dir, declared) : null;
|
|
2446
|
+
}
|
|
2447
|
+
function assertSafePathComponent(value, what) {
|
|
2448
|
+
if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
2449
|
+
throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
function resolveSddDir(harnessDir, planId) {
|
|
2453
|
+
assertSafePathComponent(planId, "planId");
|
|
2454
|
+
const base = resolve5(harnessDir);
|
|
2455
|
+
const declared = mstarcDirOverride(base, "sddDir");
|
|
2456
|
+
const sddBase = declared !== null ? declared : join8(base, "sdd");
|
|
2457
|
+
return join8(sddBase, planId);
|
|
2458
|
+
}
|
|
2459
|
+
function resolveIterationDir(harnessDir) {
|
|
2460
|
+
const declared = mstarcDirOverride(harnessDir, "iterationDir");
|
|
2461
|
+
if (declared !== null)
|
|
2462
|
+
return declared;
|
|
2463
|
+
return join8(resolve5(harnessDir), "iterations");
|
|
2464
|
+
}
|
|
2465
|
+
function resolveHarnessSubdir(startDir, opts, key, fallback) {
|
|
2466
|
+
const harness = resolveHarnessDir(startDir, opts);
|
|
2467
|
+
if (harness === null) {
|
|
2468
|
+
throw new Error(`harness dir not found from ${resolve5(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
|
|
2469
|
+
}
|
|
2470
|
+
const declared = mstarcDirOverride(harness, key);
|
|
2471
|
+
return declared !== null ? declared : join8(resolve5(harness), fallback);
|
|
2472
|
+
}
|
|
2473
|
+
function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
|
|
2474
|
+
return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
|
|
2475
|
+
}
|
|
2476
|
+
function resolveProjectDir(startDir = process.cwd(), opts = {}) {
|
|
2477
|
+
return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
|
|
2478
|
+
}
|
|
2479
|
+
var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
|
|
2480
|
+
# Principle: process stays local; results are shared with the team.
|
|
2481
|
+
# Default-ignore everything under .mstar/, then re-include the tracked results.
|
|
2482
|
+
.mstar/**
|
|
2483
|
+
!.mstar/AGENTS.md
|
|
2484
|
+
!.mstar/knowledge/
|
|
2485
|
+
!.mstar/knowledge/**
|
|
2486
|
+
!.mstar/specs/
|
|
2487
|
+
!.mstar/specs/**
|
|
2488
|
+
# .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
|
|
2489
|
+
.mstarc
|
|
2490
|
+
`;
|
|
2491
|
+
var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
|
|
2492
|
+
# Default-ignore everything under .agents/, then re-include the tracked results.
|
|
2493
|
+
.agents/**
|
|
2494
|
+
!.agents/AGENTS.md
|
|
2495
|
+
!.agents/knowledge/
|
|
2496
|
+
!.agents/knowledge/**
|
|
2497
|
+
!.agents/specs/
|
|
2498
|
+
!.agents/specs/**
|
|
2499
|
+
`;
|
|
2500
|
+
var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
|
|
2501
|
+
`).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
|
|
2502
|
+
var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
|
|
2503
|
+
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
2504
|
+
function isDirectory(dir) {
|
|
2505
|
+
try {
|
|
2506
|
+
return statSync3(dir).isDirectory();
|
|
2507
|
+
} catch {
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
|
|
2512
|
+
function probeTimeoutMs() {
|
|
2513
|
+
const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
|
|
2514
|
+
if (raw === undefined || raw.trim() === "")
|
|
2515
|
+
return DEFAULT_PROBE_TIMEOUT_MS;
|
|
2516
|
+
const parsed = Number(raw);
|
|
2517
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
|
|
2518
|
+
}
|
|
2519
|
+
function violation7(severity, code, message, fix) {
|
|
2520
|
+
return { ok: false, severity, code, message, fix };
|
|
2521
|
+
}
|
|
2522
|
+
function gate(violations) {
|
|
2523
|
+
return { ok: violations.length === 0, violations };
|
|
2524
|
+
}
|
|
2525
|
+
function probeBranch(worktreePath, opts) {
|
|
2526
|
+
const precomputed = opts.branchOf?.(worktreePath);
|
|
2527
|
+
if (precomputed !== undefined)
|
|
2528
|
+
return { branch: precomputed };
|
|
2529
|
+
const timeout = opts.timeoutMs ?? probeTimeoutMs();
|
|
2530
|
+
try {
|
|
2531
|
+
const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
|
|
2532
|
+
encoding: "utf8",
|
|
2533
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2161
2534
|
timeout
|
|
2162
2535
|
});
|
|
2163
2536
|
const branch = stdout.trim();
|
|
@@ -2177,616 +2550,243 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
2177
2550
|
const violations = [];
|
|
2178
2551
|
const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
|
|
2179
2552
|
if (controlWorktreePath.trim() === "") {
|
|
2180
|
-
violations.push(
|
|
2553
|
+
violations.push(violation7("high", "worktree.l1.control-missing", "metadata.control_worktree_path is not recorded — the L1 control worktree (integration-branch checkout) must be recorded in status.json before writable dispatch", "record the control worktree path in status.json metadata.control_worktree_path"));
|
|
2181
2554
|
}
|
|
2182
2555
|
if (leaseWorktreePath.trim() === "") {
|
|
2183
|
-
violations.push(
|
|
2556
|
+
violations.push(violation7("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"));
|
|
2184
2557
|
}
|
|
2185
2558
|
if (leaseWorkingBranch.trim() === "") {
|
|
2186
|
-
violations.push(
|
|
2559
|
+
violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
|
|
2187
2560
|
}
|
|
2188
2561
|
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
2189
|
-
violations.push(
|
|
2562
|
+
violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
|
|
2190
2563
|
}
|
|
2191
|
-
if (leaseWorktreePath !== "" && !
|
|
2192
|
-
violations.push(
|
|
2564
|
+
if (leaseWorktreePath !== "" && !existsSync6(leaseWorktreePath)) {
|
|
2565
|
+
violations.push(violation7("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
|
|
2193
2566
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
2194
2567
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
2195
2568
|
if ("error" in probe) {
|
|
2196
|
-
violations.push(
|
|
2569
|
+
violations.push(violation7("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)"));
|
|
2197
2570
|
} else if (probe.branch !== leaseWorkingBranch) {
|
|
2198
|
-
violations.push(
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
return gate(violations);
|
|
2202
|
-
}
|
|
2203
|
-
function l2PreDispatchCheck(input, opts = {}) {
|
|
2204
|
-
const violations = [];
|
|
2205
|
-
const tracks = input.tracks ?? [];
|
|
2206
|
-
const seenPaths = new Set;
|
|
2207
|
-
if (tracks.length < 1) {
|
|
2208
|
-
violations.push(violation5("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"));
|
|
2209
|
-
}
|
|
2210
|
-
tracks.forEach((track, index) => {
|
|
2211
|
-
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
2212
|
-
violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2213
|
-
return;
|
|
2214
|
-
}
|
|
2215
|
-
if (!isAbsolute4(track.worktreePath)) {
|
|
2216
|
-
violations.push(violation5("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>)`));
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
const normalized = resolve6(track.worktreePath);
|
|
2220
|
-
if (seenPaths.has(normalized)) {
|
|
2221
|
-
violations.push(violation5("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"));
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
seenPaths.add(normalized);
|
|
2225
|
-
if (!existsSync4(track.worktreePath)) {
|
|
2226
|
-
violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
2227
|
-
return;
|
|
2228
|
-
}
|
|
2229
|
-
const probe = probeBranch(track.worktreePath, opts);
|
|
2230
|
-
if ("error" in probe) {
|
|
2231
|
-
violations.push(violation5("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)"));
|
|
2232
|
-
} else if (probe.branch !== track.workingBranch) {
|
|
2233
|
-
violations.push(violation5("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`));
|
|
2571
|
+
violations.push(violation7("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
|
|
2234
2572
|
}
|
|
2235
|
-
});
|
|
2236
|
-
return gate(violations);
|
|
2237
|
-
}
|
|
2238
|
-
class SddScriptError extends Error {
|
|
2239
|
-
exitCode;
|
|
2240
|
-
constructor(message, exitCode) {
|
|
2241
|
-
super(message);
|
|
2242
|
-
this.name = "SddScriptError";
|
|
2243
|
-
this.exitCode = exitCode;
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
function isDirectory2(dir) {
|
|
2247
|
-
try {
|
|
2248
|
-
return statSync4(dir).isDirectory();
|
|
2249
|
-
} catch {
|
|
2250
|
-
return false;
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
function isFile2(file) {
|
|
2254
|
-
try {
|
|
2255
|
-
return statSync4(file).isFile();
|
|
2256
|
-
} catch {
|
|
2257
|
-
return false;
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
|
|
2261
|
-
function gitOut(cwd, args) {
|
|
2262
|
-
try {
|
|
2263
|
-
return execFileSync3("git", args, {
|
|
2264
|
-
cwd,
|
|
2265
|
-
encoding: "utf8",
|
|
2266
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2267
|
-
maxBuffer: GIT_CAPTURE_MAX_BYTES
|
|
2268
|
-
}).trim();
|
|
2269
|
-
} catch {
|
|
2270
|
-
return null;
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
function probeHarnessWithStatus(root) {
|
|
2274
|
-
if (isFile2(join7(root, ".mstar", "status.json")))
|
|
2275
|
-
return join7(root, ".mstar");
|
|
2276
|
-
if (isFile2(join7(root, ".agents", "status.json")))
|
|
2277
|
-
return join7(root, ".agents");
|
|
2278
|
-
if (hasWorkflowSnapshot(join7(root, ".mstar")))
|
|
2279
|
-
return join7(root, ".mstar");
|
|
2280
|
-
if (hasWorkflowSnapshot(join7(root, ".agents")))
|
|
2281
|
-
return join7(root, ".agents");
|
|
2282
|
-
return null;
|
|
2283
|
-
}
|
|
2284
|
-
function hasWorkflowSnapshot(harnessDir) {
|
|
2285
|
-
let workflowsDir;
|
|
2286
|
-
try {
|
|
2287
|
-
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2288
|
-
} catch {
|
|
2289
|
-
workflowsDir = join7(harnessDir, "workflows");
|
|
2290
|
-
}
|
|
2291
|
-
if (!isDirectory2(workflowsDir))
|
|
2292
|
-
return false;
|
|
2293
|
-
try {
|
|
2294
|
-
for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
|
|
2295
|
-
if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
|
|
2296
|
-
return true;
|
|
2297
|
-
}
|
|
2298
|
-
} catch {
|
|
2299
|
-
return false;
|
|
2300
|
-
}
|
|
2301
|
-
return false;
|
|
2302
|
-
}
|
|
2303
|
-
function isLinkedWorktree(root) {
|
|
2304
|
-
const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
|
|
2305
|
-
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2306
|
-
if (gitDirRaw === null || commonRaw === null)
|
|
2307
|
-
return false;
|
|
2308
|
-
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
|
|
2309
|
-
const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
|
|
2310
|
-
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2311
|
-
return true;
|
|
2312
|
-
try {
|
|
2313
|
-
const gdParent = realpathSync3(dirname6(gitDir));
|
|
2314
|
-
const cmAbs = realpathSync3(common);
|
|
2315
|
-
return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2316
|
-
} catch {
|
|
2317
|
-
return false;
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
function sddWorkspace(planId, opts = {}) {
|
|
2321
|
-
if (!planId) {
|
|
2322
|
-
throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
|
|
2323
|
-
` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
|
|
2324
|
-
}
|
|
2325
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
2326
|
-
const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
|
|
2327
|
-
let root;
|
|
2328
|
-
if (controlRoot) {
|
|
2329
|
-
if (!isDirectory2(controlRoot)) {
|
|
2330
|
-
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
2331
|
-
}
|
|
2332
|
-
root = realpathSync3(controlRoot);
|
|
2333
|
-
} else {
|
|
2334
|
-
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2335
|
-
root = realpathSync3(topLevel ?? cwd);
|
|
2336
|
-
}
|
|
2337
|
-
if (!controlRoot && isLinkedWorktree(root)) {
|
|
2338
|
-
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
2339
|
-
` + ` Refusing to create a second SDD tree under the feature checkout.
|
|
2340
|
-
` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
|
|
2341
|
-
` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
|
|
2342
|
-
}
|
|
2343
|
-
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2344
|
-
let harnessDir;
|
|
2345
|
-
if (harnessOverride) {
|
|
2346
|
-
harnessDir = resolve7(root, harnessOverride);
|
|
2347
|
-
} else {
|
|
2348
|
-
const rc = findMstarc(root, root);
|
|
2349
|
-
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
|
|
2350
|
-
if (rcHarnessDir) {
|
|
2351
|
-
harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
2352
|
-
} else {
|
|
2353
|
-
const probed = probeHarnessWithStatus(root);
|
|
2354
|
-
if (probed) {
|
|
2355
|
-
harnessDir = probed;
|
|
2356
|
-
} else if (isDirectory2(join7(root, ".mstar"))) {
|
|
2357
|
-
harnessDir = join7(root, ".mstar");
|
|
2358
|
-
} else if (isDirectory2(join7(root, ".agents"))) {
|
|
2359
|
-
harnessDir = join7(root, ".agents");
|
|
2360
|
-
} else {
|
|
2361
|
-
harnessDir = join7(root, ".mstar");
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
|
-
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2366
|
-
mkdirSync5(sddDir, { recursive: true });
|
|
2367
|
-
writeFileSync3(join7(sddDir, ".gitignore"), `*
|
|
2368
|
-
`);
|
|
2369
|
-
return realpathSync3(sddDir);
|
|
2370
|
-
}
|
|
2371
|
-
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
2372
|
-
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
2373
|
-
throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
|
|
2374
|
-
}
|
|
2375
|
-
let content;
|
|
2376
|
-
try {
|
|
2377
|
-
content = readFileSync5(planFile, "utf8");
|
|
2378
|
-
} catch {
|
|
2379
|
-
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
2380
|
-
}
|
|
2381
|
-
let out;
|
|
2382
|
-
if (outFile) {
|
|
2383
|
-
out = outFile;
|
|
2384
|
-
} else {
|
|
2385
|
-
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
2386
|
-
if (!sddDir) {
|
|
2387
|
-
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2388
|
-
}
|
|
2389
|
-
mkdirSync5(sddDir, { recursive: true });
|
|
2390
|
-
out = join7(sddDir, `task-${taskN}-brief.md`);
|
|
2391
|
-
}
|
|
2392
|
-
const records = content.endsWith(`
|
|
2393
|
-
`) ? content.split(`
|
|
2394
|
-
`).slice(0, -1) : content.split(`
|
|
2395
|
-
`);
|
|
2396
|
-
const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
|
|
2397
|
-
const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
|
|
2398
|
-
let infence = false;
|
|
2399
|
-
let intask = false;
|
|
2400
|
-
const printed = [];
|
|
2401
|
-
for (const line of records) {
|
|
2402
|
-
if (/^```/.test(line))
|
|
2403
|
-
infence = !infence;
|
|
2404
|
-
if (!infence && headingRe.test(line))
|
|
2405
|
-
intask = targetRe.test(line);
|
|
2406
|
-
if (intask)
|
|
2407
|
-
printed.push(line);
|
|
2408
|
-
}
|
|
2409
|
-
const output = printed.length > 0 ? `${printed.join(`
|
|
2410
|
-
`)}
|
|
2411
|
-
` : "";
|
|
2412
|
-
writeFileSync3(out, output);
|
|
2413
|
-
if (printed.length === 0) {
|
|
2414
|
-
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
2415
|
-
}
|
|
2416
|
-
return out;
|
|
2417
|
-
}
|
|
2418
|
-
var COMPASS_STATUSES = ["active", "locked", "completed"];
|
|
2419
|
-
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
2420
|
-
var PLAN_STATUS_DONE = "Done";
|
|
2421
|
-
function typeName(value) {
|
|
2422
|
-
if (value === null)
|
|
2423
|
-
return "null";
|
|
2424
|
-
if (Array.isArray(value))
|
|
2425
|
-
return "array";
|
|
2426
|
-
return typeof value;
|
|
2427
|
-
}
|
|
2428
|
-
function validateCompassShape(doc) {
|
|
2429
|
-
const issues = [];
|
|
2430
|
-
const expectString = (key, opts = {}) => {
|
|
2431
|
-
const value = doc[key];
|
|
2432
|
-
if (typeof value !== "string") {
|
|
2433
|
-
issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
|
|
2434
|
-
return;
|
|
2435
|
-
}
|
|
2436
|
-
if (opts.min !== undefined && value.length < opts.min) {
|
|
2437
|
-
issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
|
|
2438
|
-
return;
|
|
2439
|
-
}
|
|
2440
|
-
if (opts.regex !== undefined && !opts.regex.test(value)) {
|
|
2441
|
-
issues.push({ path: [key], message: `string must match ${opts.regex}` });
|
|
2442
|
-
}
|
|
2443
|
-
};
|
|
2444
|
-
expectString("iteration_id", { min: 1 });
|
|
2445
|
-
expectString("start_date", { regex: DATE_RE2 });
|
|
2446
|
-
const status = doc.status;
|
|
2447
|
-
if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
|
|
2448
|
-
issues.push({
|
|
2449
|
-
path: ["status"],
|
|
2450
|
-
message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
expectString("iteration_base_branch", { min: 1 });
|
|
2454
|
-
expectString("target_branch", { min: 1 });
|
|
2455
|
-
const plans = doc.plans;
|
|
2456
|
-
if (plans !== undefined) {
|
|
2457
|
-
if (!Array.isArray(plans)) {
|
|
2458
|
-
issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
|
|
2459
|
-
} else {
|
|
2460
|
-
plans.forEach((entry, index) => {
|
|
2461
|
-
if (typeof entry !== "string") {
|
|
2462
|
-
issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
|
|
2463
|
-
} else if (entry.length < 1) {
|
|
2464
|
-
issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
|
|
2465
|
-
}
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
const end_date = doc.end_date;
|
|
2470
|
-
if (end_date !== undefined) {
|
|
2471
|
-
if (typeof end_date !== "string") {
|
|
2472
|
-
issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
|
|
2473
|
-
} else if (!DATE_RE2.test(end_date)) {
|
|
2474
|
-
issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
if (issues.length > 0)
|
|
2478
|
-
return { ok: false, issues };
|
|
2479
|
-
return {
|
|
2480
|
-
ok: true,
|
|
2481
|
-
data: {
|
|
2482
|
-
iteration_id: doc.iteration_id,
|
|
2483
|
-
start_date: doc.start_date,
|
|
2484
|
-
status,
|
|
2485
|
-
iteration_base_branch: doc.iteration_base_branch,
|
|
2486
|
-
target_branch: doc.target_branch,
|
|
2487
|
-
...plans !== undefined ? { plans } : {},
|
|
2488
|
-
...end_date !== undefined ? { end_date } : {}
|
|
2489
|
-
}
|
|
2490
|
-
};
|
|
2491
|
-
}
|
|
2492
|
-
function violation6(severity, code, message, fix) {
|
|
2493
|
-
return { ok: false, severity, code, message, fix };
|
|
2494
|
-
}
|
|
2495
|
-
function isPlainObject4(value) {
|
|
2496
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2497
|
-
}
|
|
2498
|
-
function validateCompassFrontmatter(doc) {
|
|
2499
|
-
if (!isPlainObject4(doc)) {
|
|
2500
|
-
return {
|
|
2501
|
-
ok: false,
|
|
2502
|
-
violations: [
|
|
2503
|
-
violation6("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")
|
|
2504
|
-
]
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2507
|
-
const parsed = validateCompassShape(doc);
|
|
2508
|
-
if (!parsed.ok) {
|
|
2509
|
-
return {
|
|
2510
|
-
ok: false,
|
|
2511
|
-
violations: parsed.issues.map((issue) => {
|
|
2512
|
-
const field = issue.path.join(".") || "(root)";
|
|
2513
|
-
return violation6("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)`);
|
|
2514
|
-
})
|
|
2515
|
-
};
|
|
2516
|
-
}
|
|
2517
|
-
const violations = [];
|
|
2518
|
-
const { status, end_date } = parsed.data;
|
|
2519
|
-
if (status === "completed" && end_date === undefined) {
|
|
2520
|
-
violations.push(violation6("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"));
|
|
2521
|
-
}
|
|
2522
|
-
if (status !== "completed" && end_date !== undefined) {
|
|
2523
|
-
violations.push(violation6("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"));
|
|
2524
|
-
}
|
|
2525
|
-
return { ok: violations.length === 0, violations };
|
|
2526
|
-
}
|
|
2527
|
-
function registeredPlanIds(compassDoc) {
|
|
2528
|
-
if (!Array.isArray(compassDoc.plans))
|
|
2529
|
-
return [];
|
|
2530
|
-
return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
|
|
2531
|
-
}
|
|
2532
|
-
function findPlanRow(snapshotDoc, planId) {
|
|
2533
|
-
if (!Array.isArray(snapshotDoc.plans))
|
|
2534
|
-
return null;
|
|
2535
|
-
for (const row of snapshotDoc.plans) {
|
|
2536
|
-
if (!isPlainObject4(row))
|
|
2537
|
-
continue;
|
|
2538
|
-
const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
|
|
2539
|
-
if (rowId === planId)
|
|
2540
|
-
return row;
|
|
2541
|
-
}
|
|
2542
|
-
return null;
|
|
2543
|
-
}
|
|
2544
|
-
function entryPlansAllDone(snapshotDoc, registered) {
|
|
2545
|
-
const violations = [];
|
|
2546
|
-
if (registered.length === 0) {
|
|
2547
|
-
violations.push(violation6("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`"));
|
|
2548
|
-
return violations;
|
|
2549
|
-
}
|
|
2550
|
-
for (const planId of registered) {
|
|
2551
|
-
const row = findPlanRow(snapshotDoc, planId);
|
|
2552
|
-
if (row === null) {
|
|
2553
|
-
violations.push(violation6("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"));
|
|
2554
|
-
continue;
|
|
2555
|
-
}
|
|
2556
|
-
if (row.status !== PLAN_STATUS_DONE) {
|
|
2557
|
-
violations.push(violation6("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)`));
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
return violations;
|
|
2561
|
-
}
|
|
2562
|
-
function entryFrontmatterComplete(compassDoc) {
|
|
2563
|
-
return validateCompassFrontmatter(compassDoc).violations;
|
|
2564
|
-
}
|
|
2565
|
-
function exitFrontmatterClosed(compassDoc) {
|
|
2566
|
-
const violations = [];
|
|
2567
|
-
if (compassDoc.status !== "completed") {
|
|
2568
|
-
violations.push(violation6("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)`));
|
|
2569
|
-
}
|
|
2570
|
-
const endDate = compassDoc.end_date;
|
|
2571
|
-
if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
|
|
2572
|
-
violations.push(violation6("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)"));
|
|
2573
|
-
}
|
|
2574
|
-
return violations;
|
|
2575
|
-
}
|
|
2576
|
-
function exitBranchCheck(opts) {
|
|
2577
|
-
const violations = [];
|
|
2578
|
-
const { currentBranch, specIntegrationBranch } = opts;
|
|
2579
|
-
if (currentBranch === undefined || specIntegrationBranch === undefined) {
|
|
2580
|
-
violations.push(violation6("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)"));
|
|
2581
|
-
} else if (currentBranch !== specIntegrationBranch) {
|
|
2582
|
-
violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
|
|
2583
|
-
}
|
|
2584
|
-
return violations;
|
|
2585
|
-
}
|
|
2586
|
-
function exitPrBaseCheck(compassDoc, opts) {
|
|
2587
|
-
const violations = [];
|
|
2588
|
-
const target = compassDoc.target_branch;
|
|
2589
|
-
const { prBaseBranch } = opts;
|
|
2590
|
-
if (prBaseBranch === undefined) {
|
|
2591
|
-
violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
|
|
2592
|
-
} else if (typeof target !== "string" || prBaseBranch !== target) {
|
|
2593
|
-
violations.push(violation6("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)`));
|
|
2594
|
-
}
|
|
2595
|
-
return violations;
|
|
2596
|
-
}
|
|
2597
|
-
function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
|
|
2598
|
-
const registered = registeredPlanIds(compassDoc);
|
|
2599
|
-
const entryViolations = [
|
|
2600
|
-
...entryPlansAllDone(snapshotDoc, registered),
|
|
2601
|
-
...entryFrontmatterComplete(compassDoc)
|
|
2602
|
-
];
|
|
2603
|
-
const exitViolations = [
|
|
2604
|
-
...exitFrontmatterClosed(compassDoc),
|
|
2605
|
-
...exitBranchCheck(opts),
|
|
2606
|
-
...exitPrBaseCheck(compassDoc, opts)
|
|
2607
|
-
];
|
|
2608
|
-
const allPlansDone = registered.length > 0 && registered.every((planId) => {
|
|
2609
|
-
const row = findPlanRow(snapshotDoc, planId);
|
|
2610
|
-
return row !== null && row.status === PLAN_STATUS_DONE;
|
|
2611
|
-
});
|
|
2612
|
-
const entry = { ok: entryViolations.length === 0, violations: entryViolations };
|
|
2613
|
-
const exit = { ok: exitViolations.length === 0, violations: exitViolations };
|
|
2614
|
-
let transition;
|
|
2615
|
-
if (!allPlansDone)
|
|
2616
|
-
transition = "phase-2-execute";
|
|
2617
|
-
else if (entry.ok && exit.ok)
|
|
2618
|
-
transition = "phase-4-pr-delivery";
|
|
2619
|
-
else
|
|
2620
|
-
transition = "phase-3-close";
|
|
2621
|
-
const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
|
|
2622
|
-
return {
|
|
2623
|
-
transition,
|
|
2624
|
-
allPlansDone,
|
|
2625
|
-
entry,
|
|
2626
|
-
exit,
|
|
2627
|
-
ok: gateBlocking.length === 0,
|
|
2628
|
-
violations: gateBlocking
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
function parseCompassFrontmatter(filePath) {
|
|
2632
|
-
return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
|
|
2633
|
-
}
|
|
2634
|
-
function parseCompassFrontmatterText(content, filePath) {
|
|
2635
|
-
const lines = content.split(/\r?\n/);
|
|
2636
|
-
if (lines[0]?.trim() !== "---") {
|
|
2637
|
-
throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
|
|
2638
|
-
}
|
|
2639
|
-
const end = lines.indexOf("---", 1);
|
|
2640
|
-
if (end === -1) {
|
|
2641
|
-
throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
|
|
2642
|
-
}
|
|
2643
|
-
const doc = {};
|
|
2644
|
-
let listKey = null;
|
|
2645
|
-
for (let i = 1;i < end; i += 1) {
|
|
2646
|
-
const line = lines[i] ?? "";
|
|
2647
|
-
if (!line.trim() || line.trim().startsWith("#"))
|
|
2648
|
-
continue;
|
|
2649
|
-
if (listKey !== null && /^\s*-\s+/.test(line)) {
|
|
2650
|
-
const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
|
|
2651
|
-
if (!Array.isArray(doc[listKey]))
|
|
2652
|
-
doc[listKey] = [];
|
|
2653
|
-
doc[listKey].push(item);
|
|
2654
|
-
continue;
|
|
2655
|
-
}
|
|
2656
|
-
listKey = null;
|
|
2657
|
-
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
2658
|
-
if (!kv) {
|
|
2659
|
-
throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
|
|
2660
|
-
}
|
|
2661
|
-
const value = kv[2].trim();
|
|
2662
|
-
doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
|
|
2663
|
-
listKey = value === "" ? kv[1] : null;
|
|
2664
2573
|
}
|
|
2665
|
-
return
|
|
2574
|
+
return gate(violations);
|
|
2666
2575
|
}
|
|
2667
|
-
function
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
|
|
2576
|
+
function l2PreDispatchCheck(input, opts = {}) {
|
|
2577
|
+
const violations = [];
|
|
2578
|
+
const tracks = input.tracks ?? [];
|
|
2579
|
+
const seenPaths = new Set;
|
|
2580
|
+
if (tracks.length < 1) {
|
|
2581
|
+
violations.push(violation7("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"));
|
|
2671
2582
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2583
|
+
tracks.forEach((track, index) => {
|
|
2584
|
+
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
2585
|
+
violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2586
|
+
return;
|
|
2587
|
+
}
|
|
2588
|
+
if (!isAbsolute4(track.worktreePath)) {
|
|
2589
|
+
violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
const normalized = resolve6(track.worktreePath);
|
|
2593
|
+
if (seenPaths.has(normalized)) {
|
|
2594
|
+
violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
seenPaths.add(normalized);
|
|
2598
|
+
if (!existsSync6(track.worktreePath)) {
|
|
2599
|
+
violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const probe = probeBranch(track.worktreePath, opts);
|
|
2603
|
+
if ("error" in probe) {
|
|
2604
|
+
violations.push(violation7("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)"));
|
|
2605
|
+
} else if (probe.branch !== track.workingBranch) {
|
|
2606
|
+
violations.push(violation7("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`));
|
|
2681
2607
|
}
|
|
2608
|
+
});
|
|
2609
|
+
return gate(violations);
|
|
2610
|
+
}
|
|
2611
|
+
class SddScriptError extends Error {
|
|
2612
|
+
exitCode;
|
|
2613
|
+
constructor(message, exitCode) {
|
|
2614
|
+
super(message);
|
|
2615
|
+
this.name = "SddScriptError";
|
|
2616
|
+
this.exitCode = exitCode;
|
|
2682
2617
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2618
|
+
}
|
|
2619
|
+
function isDirectory2(dir) {
|
|
2620
|
+
try {
|
|
2621
|
+
return statSync4(dir).isDirectory();
|
|
2622
|
+
} catch {
|
|
2623
|
+
return false;
|
|
2685
2624
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2625
|
+
}
|
|
2626
|
+
function isFile2(file) {
|
|
2627
|
+
try {
|
|
2628
|
+
return statSync4(file).isFile();
|
|
2629
|
+
} catch {
|
|
2630
|
+
return false;
|
|
2692
2631
|
}
|
|
2693
|
-
return items;
|
|
2694
2632
|
}
|
|
2695
|
-
var
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2633
|
+
var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
|
|
2634
|
+
function gitOut(cwd, args) {
|
|
2635
|
+
try {
|
|
2636
|
+
return execFileSync3("git", args, {
|
|
2637
|
+
cwd,
|
|
2638
|
+
encoding: "utf8",
|
|
2639
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2640
|
+
maxBuffer: GIT_CAPTURE_MAX_BYTES
|
|
2641
|
+
}).trim();
|
|
2642
|
+
} catch {
|
|
2643
|
+
return null;
|
|
2644
|
+
}
|
|
2701
2645
|
}
|
|
2702
|
-
function
|
|
2703
|
-
|
|
2646
|
+
function probeHarnessWithStatus(root) {
|
|
2647
|
+
if (isFile2(join9(root, ".mstar", "status.json")))
|
|
2648
|
+
return join9(root, ".mstar");
|
|
2649
|
+
if (isFile2(join9(root, ".agents", "status.json")))
|
|
2650
|
+
return join9(root, ".agents");
|
|
2651
|
+
if (hasWorkflowSnapshot(join9(root, ".mstar")))
|
|
2652
|
+
return join9(root, ".mstar");
|
|
2653
|
+
if (hasWorkflowSnapshot(join9(root, ".agents")))
|
|
2654
|
+
return join9(root, ".agents");
|
|
2655
|
+
return null;
|
|
2704
2656
|
}
|
|
2705
|
-
function
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2657
|
+
function hasWorkflowSnapshot(harnessDir) {
|
|
2658
|
+
let workflowsDir;
|
|
2659
|
+
try {
|
|
2660
|
+
workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
2661
|
+
} catch {
|
|
2662
|
+
workflowsDir = join9(harnessDir, "workflows");
|
|
2663
|
+
}
|
|
2664
|
+
if (!isDirectory2(workflowsDir))
|
|
2665
|
+
return false;
|
|
2666
|
+
try {
|
|
2667
|
+
for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
|
|
2668
|
+
if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
|
|
2669
|
+
return true;
|
|
2670
|
+
}
|
|
2671
|
+
} catch {
|
|
2672
|
+
return false;
|
|
2710
2673
|
}
|
|
2674
|
+
return false;
|
|
2711
2675
|
}
|
|
2712
|
-
function
|
|
2713
|
-
const
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2676
|
+
function isLinkedWorktree(root) {
|
|
2677
|
+
const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
|
|
2678
|
+
const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
|
|
2679
|
+
if (gitDirRaw === null || commonRaw === null)
|
|
2680
|
+
return false;
|
|
2681
|
+
const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
|
|
2682
|
+
const common = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
|
|
2683
|
+
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
2684
|
+
return true;
|
|
2685
|
+
try {
|
|
2686
|
+
const gdParent = realpathSync3(dirname6(gitDir));
|
|
2687
|
+
const cmAbs = realpathSync3(common);
|
|
2688
|
+
return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
2689
|
+
} catch {
|
|
2690
|
+
return false;
|
|
2719
2691
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2692
|
+
}
|
|
2693
|
+
function sddWorkspace(planId, opts = {}) {
|
|
2694
|
+
if (!planId) {
|
|
2695
|
+
throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
|
|
2696
|
+
` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
|
|
2697
|
+
}
|
|
2698
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2699
|
+
const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
|
|
2700
|
+
let root;
|
|
2701
|
+
if (controlRoot) {
|
|
2702
|
+
if (!isDirectory2(controlRoot)) {
|
|
2703
|
+
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
2704
|
+
}
|
|
2705
|
+
root = realpathSync3(controlRoot);
|
|
2724
2706
|
} else {
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2707
|
+
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2708
|
+
root = realpathSync3(topLevel ?? cwd);
|
|
2709
|
+
}
|
|
2710
|
+
if (!controlRoot && isLinkedWorktree(root)) {
|
|
2711
|
+
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
2712
|
+
` + ` Refusing to create a second SDD tree under the feature checkout.
|
|
2713
|
+
` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
|
|
2714
|
+
` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
|
|
2715
|
+
}
|
|
2716
|
+
const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
|
|
2717
|
+
let harnessDir;
|
|
2718
|
+
if (harnessOverride) {
|
|
2719
|
+
harnessDir = resolve7(root, harnessOverride);
|
|
2720
|
+
} else {
|
|
2721
|
+
const rc = findMstarc(root, root);
|
|
2722
|
+
const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
|
|
2723
|
+
if (rcHarnessDir) {
|
|
2724
|
+
harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
|
|
2725
|
+
} else {
|
|
2726
|
+
const probed = probeHarnessWithStatus(root);
|
|
2727
|
+
if (probed) {
|
|
2728
|
+
harnessDir = probed;
|
|
2729
|
+
} else if (isDirectory2(join9(root, ".mstar"))) {
|
|
2730
|
+
harnessDir = join9(root, ".mstar");
|
|
2731
|
+
} else if (isDirectory2(join9(root, ".agents"))) {
|
|
2732
|
+
harnessDir = join9(root, ".agents");
|
|
2733
|
+
} else {
|
|
2734
|
+
harnessDir = join9(root, ".mstar");
|
|
2749
2735
|
}
|
|
2750
2736
|
}
|
|
2751
2737
|
}
|
|
2752
|
-
|
|
2738
|
+
const sddDir = resolveSddDir(harnessDir, planId);
|
|
2739
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2740
|
+
writeFileSync4(join9(sddDir, ".gitignore"), `*
|
|
2741
|
+
`);
|
|
2742
|
+
return realpathSync3(sddDir);
|
|
2753
2743
|
}
|
|
2754
|
-
function
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
|
|
2758
|
-
if (entries === undefined) {
|
|
2759
|
-
return { ok: true, violations };
|
|
2760
|
-
}
|
|
2761
|
-
if (!Array.isArray(entries)) {
|
|
2762
|
-
violations.push(violation7("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
|
|
2763
|
-
return { ok: false, violations };
|
|
2744
|
+
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
2745
|
+
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
2746
|
+
throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
|
|
2764
2747
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2748
|
+
let content;
|
|
2749
|
+
try {
|
|
2750
|
+
content = readFileSync7(planFile, "utf8");
|
|
2751
|
+
} catch {
|
|
2752
|
+
throw new SddScriptError(`no such plan file: ${planFile}`, 2);
|
|
2767
2753
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
const
|
|
2773
|
-
if (
|
|
2774
|
-
|
|
2775
|
-
violations.push(violation7("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
|
|
2776
|
-
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
2777
|
-
violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
2778
|
-
} else if (entry.decision === "defer") {
|
|
2779
|
-
if (typeof entry.target !== "string" || entry.target.trim() === "") {
|
|
2780
|
-
violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
|
|
2781
|
-
}
|
|
2782
|
-
} else {
|
|
2783
|
-
violations.push(violation7("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
|
|
2784
|
-
}
|
|
2785
|
-
} else if (normalizeSeverity(entry.severity) === "critical") {
|
|
2786
|
-
violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
|
|
2754
|
+
let out;
|
|
2755
|
+
if (outFile) {
|
|
2756
|
+
out = outFile;
|
|
2757
|
+
} else {
|
|
2758
|
+
const sddDir = opts.sddDir ?? process.env.SDD_DIR;
|
|
2759
|
+
if (!sddDir) {
|
|
2760
|
+
throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
|
|
2787
2761
|
}
|
|
2762
|
+
mkdirSync5(sddDir, { recursive: true });
|
|
2763
|
+
out = join9(sddDir, `task-${taskN}-brief.md`);
|
|
2788
2764
|
}
|
|
2789
|
-
|
|
2765
|
+
const records = content.endsWith(`
|
|
2766
|
+
`) ? content.split(`
|
|
2767
|
+
`).slice(0, -1) : content.split(`
|
|
2768
|
+
`);
|
|
2769
|
+
const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
|
|
2770
|
+
const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
|
|
2771
|
+
let infence = false;
|
|
2772
|
+
let intask = false;
|
|
2773
|
+
const printed = [];
|
|
2774
|
+
for (const line of records) {
|
|
2775
|
+
if (/^```/.test(line))
|
|
2776
|
+
infence = !infence;
|
|
2777
|
+
if (!infence && headingRe.test(line))
|
|
2778
|
+
intask = targetRe.test(line);
|
|
2779
|
+
if (intask)
|
|
2780
|
+
printed.push(line);
|
|
2781
|
+
}
|
|
2782
|
+
const output = printed.length > 0 ? `${printed.join(`
|
|
2783
|
+
`)}
|
|
2784
|
+
` : "";
|
|
2785
|
+
writeFileSync4(out, output);
|
|
2786
|
+
if (printed.length === 0) {
|
|
2787
|
+
throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
|
|
2788
|
+
}
|
|
2789
|
+
return out;
|
|
2790
2790
|
}
|
|
2791
2791
|
function violation8(severity, code, message, fix) {
|
|
2792
2792
|
return { ok: false, severity, code, message, fix };
|
|
@@ -3984,7 +3984,7 @@ class DshMstar extends Service {
|
|
|
3984
3984
|
|
|
3985
3985
|
// src/gates/_shared.ts
|
|
3986
3986
|
var import_schemastery = __toESM(require_lib2(), 1);
|
|
3987
|
-
import { existsSync as
|
|
3987
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
3988
3988
|
import { resolve as resolve8 } from "node:path";
|
|
3989
3989
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3990
3990
|
var STATUS_FILE = "status.json";
|
|
@@ -4039,7 +4039,7 @@ function resolvePackagedSkillsDir(fileUrl) {
|
|
|
4039
4039
|
for (const rel of ["../harness-skills", "../../harness-skills"]) {
|
|
4040
4040
|
try {
|
|
4041
4041
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
4042
|
-
if (
|
|
4042
|
+
if (existsSync4(dir))
|
|
4043
4043
|
return dir;
|
|
4044
4044
|
} catch {}
|
|
4045
4045
|
}
|
|
@@ -4058,7 +4058,7 @@ function resolvePackagedAgentsDir(fileUrl) {
|
|
|
4058
4058
|
for (const rel of ["../harness-agents", "../../harness-agents"]) {
|
|
4059
4059
|
try {
|
|
4060
4060
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
4061
|
-
if (
|
|
4061
|
+
if (existsSync4(dir))
|
|
4062
4062
|
return dir;
|
|
4063
4063
|
} catch {}
|
|
4064
4064
|
}
|
|
@@ -4126,27 +4126,27 @@ function skillLocalConfig(config) {
|
|
|
4126
4126
|
}
|
|
4127
4127
|
|
|
4128
4128
|
// src/gates/catalog.ts
|
|
4129
|
-
import { existsSync as
|
|
4130
|
-
import { join as
|
|
4129
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "node:fs";
|
|
4130
|
+
import { join as join10 } from "node:path";
|
|
4131
4131
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4132
4132
|
|
|
4133
4133
|
// src/gates/agent-flow.ts
|
|
4134
|
-
import { appendFileSync, closeSync, existsSync as
|
|
4135
|
-
import { join as
|
|
4134
|
+
import { appendFileSync, closeSync, existsSync as existsSync12, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
4135
|
+
import { join as join7 } from "node:path";
|
|
4136
4136
|
|
|
4137
4137
|
// src/gates/dispatch.ts
|
|
4138
|
-
import { existsSync as
|
|
4139
|
-
import { basename as basename4, join as
|
|
4138
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
4139
|
+
import { basename as basename4, join as join5, resolve as resolve11 } from "node:path";
|
|
4140
4140
|
|
|
4141
4141
|
// src/gates/workflow-selection.ts
|
|
4142
|
-
import { existsSync as
|
|
4143
|
-
import { dirname as dirname7, join as
|
|
4142
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
4143
|
+
import { dirname as dirname7, join as join3, relative as relative3 } from "node:path";
|
|
4144
4144
|
var TERMINAL_STATUS_CACHE_MAX = 64;
|
|
4145
4145
|
var terminalStatusCache = new Map;
|
|
4146
4146
|
function terminalStatusOf(snapshotPath) {
|
|
4147
4147
|
let mtimeMs;
|
|
4148
4148
|
try {
|
|
4149
|
-
mtimeMs =
|
|
4149
|
+
mtimeMs = statSync2(snapshotPath).mtimeMs;
|
|
4150
4150
|
} catch {
|
|
4151
4151
|
terminalStatusCache.delete(snapshotPath);
|
|
4152
4152
|
return;
|
|
@@ -4173,8 +4173,8 @@ function terminalStatusOf(snapshotPath) {
|
|
|
4173
4173
|
return verdict;
|
|
4174
4174
|
}
|
|
4175
4175
|
function resolveActiveWorkflow(harnessDir) {
|
|
4176
|
-
const statusPath =
|
|
4177
|
-
if (!
|
|
4176
|
+
const statusPath = join3(harnessDir, STATUS_FILE);
|
|
4177
|
+
if (!existsSync7(statusPath)) {
|
|
4178
4178
|
return {
|
|
4179
4179
|
kind: "error",
|
|
4180
4180
|
code: "status.missing",
|
|
@@ -4232,7 +4232,7 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4232
4232
|
if (active.kind === "error" && active.code !== "workflow.selection.no-active")
|
|
4233
4233
|
return active;
|
|
4234
4234
|
const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
4235
|
-
if (!
|
|
4235
|
+
if (!existsSync7(workflowsDir)) {
|
|
4236
4236
|
return {
|
|
4237
4237
|
kind: "error",
|
|
4238
4238
|
code: "workflow.selection.no-snapshot",
|
|
@@ -4241,7 +4241,7 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4241
4241
|
}
|
|
4242
4242
|
let entries;
|
|
4243
4243
|
try {
|
|
4244
|
-
entries =
|
|
4244
|
+
entries = readdirSync3(workflowsDir, { withFileTypes: true });
|
|
4245
4245
|
} catch {
|
|
4246
4246
|
return {
|
|
4247
4247
|
kind: "error",
|
|
@@ -4253,8 +4253,8 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4253
4253
|
for (const entry of entries) {
|
|
4254
4254
|
if (!entry.isDirectory())
|
|
4255
4255
|
continue;
|
|
4256
|
-
const snapshotPath =
|
|
4257
|
-
if (!
|
|
4256
|
+
const snapshotPath = join3(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
|
|
4257
|
+
if (!existsSync7(snapshotPath)) {
|
|
4258
4258
|
terminalStatusCache.delete(snapshotPath);
|
|
4259
4259
|
continue;
|
|
4260
4260
|
}
|
|
@@ -4262,11 +4262,11 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4262
4262
|
if (verdict === undefined || !verdict.terminal)
|
|
4263
4263
|
continue;
|
|
4264
4264
|
if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
|
|
4265
|
-
best = { workflowId: entry.name, dir:
|
|
4265
|
+
best = { workflowId: entry.name, dir: join3(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
|
|
4266
4266
|
}
|
|
4267
4267
|
}
|
|
4268
4268
|
for (const cachedPath of terminalStatusCache.keys()) {
|
|
4269
|
-
if (!
|
|
4269
|
+
if (!existsSync7(join3(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
|
|
4270
4270
|
terminalStatusCache.delete(cachedPath);
|
|
4271
4271
|
}
|
|
4272
4272
|
}
|
|
@@ -4459,12 +4459,12 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
|
|
|
4459
4459
|
if (!sdd)
|
|
4460
4460
|
return [];
|
|
4461
4461
|
if (selection.kind === "error" && selection.code === "status.unreadable") {
|
|
4462
|
-
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${
|
|
4462
|
+
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${join5(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)")];
|
|
4463
4463
|
}
|
|
4464
|
-
return [leaseViolation("lease.dispatch.unverifiable", `${
|
|
4464
|
+
return [leaseViolation("lease.dispatch.unverifiable", `${join5(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)")];
|
|
4465
4465
|
}
|
|
4466
|
-
const snapshotPath =
|
|
4467
|
-
if (!
|
|
4466
|
+
const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4467
|
+
if (!existsSync10(snapshotPath)) {
|
|
4468
4468
|
if (!sdd)
|
|
4469
4469
|
return [];
|
|
4470
4470
|
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)")];
|
|
@@ -4538,7 +4538,7 @@ function worktreeL2Violations(header) {
|
|
|
4538
4538
|
return violations;
|
|
4539
4539
|
}
|
|
4540
4540
|
function activeSnapshotRows(harnessDir) {
|
|
4541
|
-
if (!
|
|
4541
|
+
if (!existsSync10(join5(harnessDir, STATUS_FILE)))
|
|
4542
4542
|
return { rows: null, controlWorktreePath: undefined, unreadable: false };
|
|
4543
4543
|
const selection = resolveActiveWorkflow(harnessDir);
|
|
4544
4544
|
if (selection.kind !== "active") {
|
|
@@ -4548,8 +4548,8 @@ function activeSnapshotRows(harnessDir) {
|
|
|
4548
4548
|
unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
|
|
4549
4549
|
};
|
|
4550
4550
|
}
|
|
4551
|
-
const snapshotPath =
|
|
4552
|
-
if (!
|
|
4551
|
+
const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4552
|
+
if (!existsSync10(snapshotPath))
|
|
4553
4553
|
return { rows: null, controlWorktreePath: undefined, unreadable: true };
|
|
4554
4554
|
let doc;
|
|
4555
4555
|
try {
|
|
@@ -4639,7 +4639,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec) {
|
|
|
4639
4639
|
}
|
|
4640
4640
|
const pb = writableFanOutUncovered(harnessDir);
|
|
4641
4641
|
if (pb.unreadable) {
|
|
4642
|
-
const statusLabel = harnessDir === null ? STATUS_FILE :
|
|
4642
|
+
const statusLabel = harnessDir === null ? STATUS_FILE : join5(harnessDir, STATUS_FILE);
|
|
4643
4643
|
ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
|
|
4644
4644
|
}
|
|
4645
4645
|
const verdict = workflowPolicy(config, adapter.workflowAskCache, {
|
|
@@ -4829,7 +4829,7 @@ function callPairingKey(exec) {
|
|
|
4829
4829
|
function resolveAgentFlowWriteDir(harnessDir) {
|
|
4830
4830
|
const selection = resolveActiveWorkflow(harnessDir);
|
|
4831
4831
|
if (selection.kind === "active")
|
|
4832
|
-
return
|
|
4832
|
+
return join7(harnessDir, selection.dir);
|
|
4833
4833
|
const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
|
|
4834
4834
|
if (!noActiveWarned) {
|
|
4835
4835
|
noActiveWarned = true;
|
|
@@ -4846,7 +4846,7 @@ function sleepSync(ms) {
|
|
|
4846
4846
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
4847
4847
|
}
|
|
4848
4848
|
function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
4849
|
-
const lockDir =
|
|
4849
|
+
const lockDir = join7(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
|
|
4850
4850
|
if (heldWorkflowLocks.has(lockDir)) {
|
|
4851
4851
|
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`);
|
|
4852
4852
|
}
|
|
@@ -4856,7 +4856,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4856
4856
|
let acquired = null;
|
|
4857
4857
|
for (;; ) {
|
|
4858
4858
|
try {
|
|
4859
|
-
|
|
4859
|
+
mkdirSync2(lockDir);
|
|
4860
4860
|
const st = statSync5(lockDir);
|
|
4861
4861
|
acquired = { dev: st.dev, ino: st.ino };
|
|
4862
4862
|
break;
|
|
@@ -4872,7 +4872,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4872
4872
|
heldWorkflowLocks.add(lockDir);
|
|
4873
4873
|
try {
|
|
4874
4874
|
try {
|
|
4875
|
-
writeFileSync2(
|
|
4875
|
+
writeFileSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
|
|
4876
4876
|
} catch {}
|
|
4877
4877
|
return fn();
|
|
4878
4878
|
} finally {
|
|
@@ -4881,7 +4881,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4881
4881
|
const current = statSync5(lockDir);
|
|
4882
4882
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
4883
4883
|
try {
|
|
4884
|
-
unlinkSync2(
|
|
4884
|
+
unlinkSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
|
|
4885
4885
|
} catch {}
|
|
4886
4886
|
rmdirSync(lockDir);
|
|
4887
4887
|
}
|
|
@@ -4889,13 +4889,13 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4889
4889
|
}
|
|
4890
4890
|
}
|
|
4891
4891
|
function appendEvent(workflowDir, event) {
|
|
4892
|
-
const file =
|
|
4892
|
+
const file = join7(workflowDir, AGENT_FLOW_FILE);
|
|
4893
4893
|
withWorkflowDirLock(workflowDir, () => {
|
|
4894
4894
|
appendFileSync(file, `${JSON.stringify(event)}
|
|
4895
4895
|
`);
|
|
4896
4896
|
if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
|
|
4897
4897
|
return;
|
|
4898
|
-
const content =
|
|
4898
|
+
const content = readFileSync5(file, "utf8");
|
|
4899
4899
|
const lines = content.replace(/\n$/, "").split(`
|
|
4900
4900
|
`);
|
|
4901
4901
|
if (lines.length > AGENT_FLOW_MAX_EVENTS) {
|
|
@@ -5246,7 +5246,7 @@ function summaryOf(events) {
|
|
|
5246
5246
|
function ledgerContent(file, n) {
|
|
5247
5247
|
const size = statSync5(file).size;
|
|
5248
5248
|
if (size <= AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES) {
|
|
5249
|
-
return
|
|
5249
|
+
return readFileSync5(file, "utf8");
|
|
5250
5250
|
}
|
|
5251
5251
|
let windowBytes = AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES;
|
|
5252
5252
|
const fd = openSync(file, "r");
|
|
@@ -5278,8 +5278,8 @@ function ledgerContent(file, n) {
|
|
|
5278
5278
|
}
|
|
5279
5279
|
}
|
|
5280
5280
|
function readAgentFlow(workflowDir, limit) {
|
|
5281
|
-
const file =
|
|
5282
|
-
if (!
|
|
5281
|
+
const file = join7(workflowDir, AGENT_FLOW_FILE);
|
|
5282
|
+
if (!existsSync12(file)) {
|
|
5283
5283
|
return { events: [], summary: [] };
|
|
5284
5284
|
}
|
|
5285
5285
|
const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
|
|
@@ -5404,7 +5404,7 @@ function residualSeverityIndex(severity) {
|
|
|
5404
5404
|
function pluginVersion() {
|
|
5405
5405
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
5406
5406
|
try {
|
|
5407
|
-
const pkg = JSON.parse(
|
|
5407
|
+
const pkg = JSON.parse(readFileSync8(new URL(rel, import.meta.url), "utf8"));
|
|
5408
5408
|
if (typeof pkg.version === "string" && pkg.version !== "")
|
|
5409
5409
|
return pkg.version;
|
|
5410
5410
|
} catch {}
|
|
@@ -5533,8 +5533,8 @@ function hhmm(ts) {
|
|
|
5533
5533
|
function harnessStateSource(harnessDir) {
|
|
5534
5534
|
if (harnessDir === null)
|
|
5535
5535
|
return null;
|
|
5536
|
-
const statusPath =
|
|
5537
|
-
if (!
|
|
5536
|
+
const statusPath = join10(harnessDir, STATUS_FILE);
|
|
5537
|
+
if (!existsSync13(statusPath))
|
|
5538
5538
|
return null;
|
|
5539
5539
|
try {
|
|
5540
5540
|
const selection = resolveReadWorkflow(harnessDir);
|
|
@@ -5553,9 +5553,9 @@ function harnessStateSource(harnessDir) {
|
|
|
5553
5553
|
if (selection.kind === "error") {
|
|
5554
5554
|
return selectionErrorState(selection, rollup, harnessDir, compass);
|
|
5555
5555
|
}
|
|
5556
|
-
const snapshotPath =
|
|
5556
|
+
const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5557
5557
|
let snapshot;
|
|
5558
|
-
if (!
|
|
5558
|
+
if (!existsSync13(snapshotPath)) {
|
|
5559
5559
|
return selectionErrorState({
|
|
5560
5560
|
kind: "error",
|
|
5561
5561
|
code: "workflow.selection.snapshot-unreadable",
|
|
@@ -5617,7 +5617,7 @@ function harnessStateSource(harnessDir) {
|
|
|
5617
5617
|
leases,
|
|
5618
5618
|
knowledge: knowledgeDigest(harnessDir),
|
|
5619
5619
|
direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
|
|
5620
|
-
agentFlow: readAgentFlow(
|
|
5620
|
+
agentFlow: readAgentFlow(join10(harnessDir, selection.dir), 50)
|
|
5621
5621
|
};
|
|
5622
5622
|
} catch {
|
|
5623
5623
|
return null;
|
|
@@ -5647,21 +5647,21 @@ function selectionErrorState(selection, rollup, harnessDir, compass) {
|
|
|
5647
5647
|
function projectRollupSource(harnessDir, residuals) {
|
|
5648
5648
|
const milestones = [];
|
|
5649
5649
|
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5650
|
-
if (
|
|
5650
|
+
if (existsSync13(projectsDir)) {
|
|
5651
5651
|
let entries;
|
|
5652
5652
|
try {
|
|
5653
|
-
entries =
|
|
5653
|
+
entries = readdirSync6(projectsDir, { withFileTypes: true });
|
|
5654
5654
|
} catch {
|
|
5655
5655
|
entries = [];
|
|
5656
5656
|
}
|
|
5657
5657
|
for (const entry of entries) {
|
|
5658
5658
|
if (!entry.isDirectory())
|
|
5659
5659
|
continue;
|
|
5660
|
-
const roadmapPath =
|
|
5661
|
-
if (!
|
|
5660
|
+
const roadmapPath = join10(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
|
|
5661
|
+
if (!existsSync13(roadmapPath))
|
|
5662
5662
|
continue;
|
|
5663
5663
|
try {
|
|
5664
|
-
const doc = parseCompassFrontmatterText(
|
|
5664
|
+
const doc = parseCompassFrontmatterText(readFileSync8(roadmapPath, "utf8"), roadmapPath);
|
|
5665
5665
|
if (Array.isArray(doc.milestones)) {
|
|
5666
5666
|
for (const milestone of doc.milestones) {
|
|
5667
5667
|
if (typeof milestone === "string" && milestone.trim() !== "")
|
|
@@ -5723,11 +5723,11 @@ function residualRollup(harnessDir) {
|
|
|
5723
5723
|
}
|
|
5724
5724
|
function projectRegisters(harnessDir) {
|
|
5725
5725
|
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5726
|
-
if (!
|
|
5726
|
+
if (!existsSync13(projectsDir))
|
|
5727
5727
|
return [];
|
|
5728
5728
|
let entries;
|
|
5729
5729
|
try {
|
|
5730
|
-
entries =
|
|
5730
|
+
entries = readdirSync6(projectsDir, { withFileTypes: true });
|
|
5731
5731
|
} catch {
|
|
5732
5732
|
return [];
|
|
5733
5733
|
}
|
|
@@ -5735,8 +5735,8 @@ function projectRegisters(harnessDir) {
|
|
|
5735
5735
|
for (const entry of entries) {
|
|
5736
5736
|
if (!entry.isDirectory())
|
|
5737
5737
|
continue;
|
|
5738
|
-
const registerPath =
|
|
5739
|
-
if (!
|
|
5738
|
+
const registerPath = join10(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
5739
|
+
if (!existsSync13(registerPath))
|
|
5740
5740
|
continue;
|
|
5741
5741
|
try {
|
|
5742
5742
|
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
@@ -5747,13 +5747,13 @@ function projectRegisters(harnessDir) {
|
|
|
5747
5747
|
return registers;
|
|
5748
5748
|
}
|
|
5749
5749
|
function knowledgeDigest(harnessDir) {
|
|
5750
|
-
const indexPath =
|
|
5751
|
-
if (!
|
|
5750
|
+
const indexPath = join10(harnessDir, "knowledge", "README.md");
|
|
5751
|
+
if (!existsSync13(indexPath))
|
|
5752
5752
|
return null;
|
|
5753
5753
|
try {
|
|
5754
5754
|
const categories = new Set;
|
|
5755
5755
|
let docCount = 0;
|
|
5756
|
-
for (const line of
|
|
5756
|
+
for (const line of readFileSync8(indexPath, "utf8").split(/\r?\n/)) {
|
|
5757
5757
|
const row = line.trim().match(/^\|(.+)\|$/);
|
|
5758
5758
|
if (row === null)
|
|
5759
5759
|
continue;
|
|
@@ -5776,7 +5776,7 @@ function knowledgeDigest(harnessDir) {
|
|
|
5776
5776
|
}
|
|
5777
5777
|
function compassDirection(compassPath) {
|
|
5778
5778
|
try {
|
|
5779
|
-
const content =
|
|
5779
|
+
const content = readFileSync8(compassPath, "utf8");
|
|
5780
5780
|
const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
|
|
5781
5781
|
if (section === null)
|
|
5782
5782
|
return null;
|
|
@@ -5793,23 +5793,23 @@ function compassDirection(compassPath) {
|
|
|
5793
5793
|
}
|
|
5794
5794
|
function steeringCompassPath(harnessDir) {
|
|
5795
5795
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
5796
|
-
if (!
|
|
5796
|
+
if (!existsSync13(iterationsDir))
|
|
5797
5797
|
return;
|
|
5798
5798
|
let entries;
|
|
5799
5799
|
try {
|
|
5800
|
-
entries =
|
|
5800
|
+
entries = readdirSync6(iterationsDir, { withFileTypes: true });
|
|
5801
5801
|
} catch {
|
|
5802
5802
|
return;
|
|
5803
5803
|
}
|
|
5804
5804
|
for (const entry of entries) {
|
|
5805
5805
|
if (!entry.isDirectory())
|
|
5806
5806
|
continue;
|
|
5807
|
-
const compassPath =
|
|
5808
|
-
if (!
|
|
5807
|
+
const compassPath = join10(iterationsDir, entry.name, "delivery-compass.md");
|
|
5808
|
+
if (!existsSync13(compassPath))
|
|
5809
5809
|
continue;
|
|
5810
5810
|
let content;
|
|
5811
5811
|
try {
|
|
5812
|
-
content =
|
|
5812
|
+
content = readFileSync8(compassPath, "utf8");
|
|
5813
5813
|
} catch {
|
|
5814
5814
|
continue;
|
|
5815
5815
|
}
|
|
@@ -5823,8 +5823,8 @@ function steeringCompassPath(harnessDir) {
|
|
|
5823
5823
|
function iterationGateSource(harnessDir) {
|
|
5824
5824
|
if (harnessDir === null)
|
|
5825
5825
|
return;
|
|
5826
|
-
const statusPath =
|
|
5827
|
-
if (!
|
|
5826
|
+
const statusPath = join10(harnessDir, STATUS_FILE);
|
|
5827
|
+
if (!existsSync13(statusPath))
|
|
5828
5828
|
return;
|
|
5829
5829
|
const compass = steeringCompassPath(harnessDir);
|
|
5830
5830
|
if (compass === undefined)
|
|
@@ -5832,7 +5832,7 @@ function iterationGateSource(harnessDir) {
|
|
|
5832
5832
|
const selection = resolveReadWorkflow(harnessDir);
|
|
5833
5833
|
if (selection.kind === "error")
|
|
5834
5834
|
return;
|
|
5835
|
-
const snapshotPath =
|
|
5835
|
+
const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5836
5836
|
try {
|
|
5837
5837
|
const snapshotDoc = readJson(snapshotPath);
|
|
5838
5838
|
const compassDoc = parseCompassFrontmatter(compass.compassPath);
|
|
@@ -5886,7 +5886,7 @@ function agentDigestKey(agent, cwd) {
|
|
|
5886
5886
|
}
|
|
5887
5887
|
|
|
5888
5888
|
// src/gates/status.ts
|
|
5889
|
-
import { existsSync as
|
|
5889
|
+
import { existsSync as existsSync14, readdirSync as readdirSync9 } from "node:fs";
|
|
5890
5890
|
import { basename as basename6, join as join14, relative as relative5, resolve as resolve13 } from "node:path";
|
|
5891
5891
|
var LOGGER_NAME = "mstar/status-gate";
|
|
5892
5892
|
function harnessDocKindOfTarget(harnessDir, targetPath) {
|
|
@@ -5969,7 +5969,7 @@ function projectRegisterDocs(harnessDir) {
|
|
|
5969
5969
|
if (!entry.isDirectory())
|
|
5970
5970
|
continue;
|
|
5971
5971
|
const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
5972
|
-
if (!
|
|
5972
|
+
if (!existsSync14(registerPath))
|
|
5973
5973
|
continue;
|
|
5974
5974
|
try {
|
|
5975
5975
|
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
@@ -6042,7 +6042,7 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
|
|
|
6042
6042
|
}
|
|
6043
6043
|
|
|
6044
6044
|
// src/gates/skill-lint.ts
|
|
6045
|
-
import { existsSync as
|
|
6045
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
|
|
6046
6046
|
import { basename as basename7, dirname as dirname9, resolve as resolve14, sep as sep2 } from "node:path";
|
|
6047
6047
|
var SKILL_LINT_LOGGER = "mstar/skill-lint";
|
|
6048
6048
|
|
|
@@ -6123,7 +6123,7 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
|
|
|
6123
6123
|
if (!isSkillTarget(roots, target))
|
|
6124
6124
|
return;
|
|
6125
6125
|
const skillPath = resolve14(target.displayPath);
|
|
6126
|
-
if (!
|
|
6126
|
+
if (!existsSync15(skillPath))
|
|
6127
6127
|
return;
|
|
6128
6128
|
let doc;
|
|
6129
6129
|
try {
|
|
@@ -6174,7 +6174,7 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
|
|
|
6174
6174
|
}
|
|
6175
6175
|
|
|
6176
6176
|
// src/gates/seams.ts
|
|
6177
|
-
import { existsSync as
|
|
6177
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
|
|
6178
6178
|
import { basename as basename8, dirname as dirname10, join as join16, resolve as resolve15, sep as sep3 } from "node:path";
|
|
6179
6179
|
|
|
6180
6180
|
// ../engine/dist/audit.js
|
|
@@ -6372,7 +6372,7 @@ function validateDesignDoc(doc, path) {
|
|
|
6372
6372
|
const violations = [...validateDesignTokenFrontmatter(doc).violations];
|
|
6373
6373
|
const isDark = basename8(path) === "DESIGN.dark.md";
|
|
6374
6374
|
const sibling = join16(dirname10(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
|
|
6375
|
-
if (
|
|
6375
|
+
if (existsSync16(sibling)) {
|
|
6376
6376
|
const light = isDark ? readFileSync13(sibling, "utf8") : doc;
|
|
6377
6377
|
const dark = isDark ? doc : readFileSync13(sibling, "utf8");
|
|
6378
6378
|
violations.push(...assertLightDarkParity(light, dark).violations);
|
|
@@ -6399,7 +6399,7 @@ function validateRolesState(rolesDir, skillsRoot = dirname10(rolesDir)) {
|
|
|
6399
6399
|
if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
|
|
6400
6400
|
continue;
|
|
6401
6401
|
const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
|
|
6402
|
-
if (!
|
|
6402
|
+
if (!existsSync16(skillFile))
|
|
6403
6403
|
continue;
|
|
6404
6404
|
try {
|
|
6405
6405
|
skillTexts[entry.name] = readFileSync13(skillFile, "utf8");
|
|
@@ -6433,7 +6433,7 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
|
|
|
6433
6433
|
if (!isSeamTarget(seam, harnessDir, target))
|
|
6434
6434
|
return;
|
|
6435
6435
|
const path = resolve15(target.displayPath);
|
|
6436
|
-
if (!
|
|
6436
|
+
if (!existsSync16(path))
|
|
6437
6437
|
return;
|
|
6438
6438
|
let doc;
|
|
6439
6439
|
try {
|
|
@@ -6503,7 +6503,7 @@ function lintRolesWrite(doc, options) {
|
|
|
6503
6503
|
}
|
|
6504
6504
|
|
|
6505
6505
|
// src/gates/tools.ts
|
|
6506
|
-
import { existsSync as
|
|
6506
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
|
|
6507
6507
|
import { join as join17, resolve as resolve16 } from "node:path";
|
|
6508
6508
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6509
6509
|
var ITERATION_VIOLATION_SCHEMA = {
|
|
@@ -6669,9 +6669,9 @@ function registerSddIterationTools(ctx, resolver) {
|
|
|
6669
6669
|
presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
|
|
6670
6670
|
isConcurrencySafe: () => false,
|
|
6671
6671
|
async execute(args) {
|
|
6672
|
-
if (!
|
|
6672
|
+
if (!existsSync17(args.snapshot_path))
|
|
6673
6673
|
throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
|
|
6674
|
-
if (!
|
|
6674
|
+
if (!existsSync17(args.compass_path))
|
|
6675
6675
|
throw new Error(`compass file not found: ${args.compass_path}`);
|
|
6676
6676
|
const snapshotDoc = readJson(args.snapshot_path);
|
|
6677
6677
|
const compassDoc = parseCompassFrontmatter(args.compass_path);
|
|
@@ -6726,7 +6726,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6726
6726
|
async execute(args) {
|
|
6727
6727
|
const abs = resolve16(args.dir);
|
|
6728
6728
|
const lightPath = join17(abs, "DESIGN.md");
|
|
6729
|
-
if (!
|
|
6729
|
+
if (!existsSync17(lightPath))
|
|
6730
6730
|
throw new Error(`design file not found: ${lightPath}`);
|
|
6731
6731
|
const light = readFileSync14(lightPath, "utf8");
|
|
6732
6732
|
const result = validateDesignDoc(light, lightPath);
|
|
@@ -6779,7 +6779,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6779
6779
|
isConcurrencySafe: () => false,
|
|
6780
6780
|
async execute(args) {
|
|
6781
6781
|
const abs = resolve16(args.plan_path);
|
|
6782
|
-
if (!
|
|
6782
|
+
if (!existsSync17(abs))
|
|
6783
6783
|
throw new Error(`plan file not found: ${abs}`);
|
|
6784
6784
|
const text = readFileSync14(abs, "utf8");
|
|
6785
6785
|
const result = validateAuditDoc(text, abs);
|
|
@@ -6819,7 +6819,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6819
6819
|
isConcurrencySafe: () => false,
|
|
6820
6820
|
async execute(args, exec) {
|
|
6821
6821
|
const abs = resolve16(args.doc_path);
|
|
6822
|
-
if (!
|
|
6822
|
+
if (!existsSync17(abs))
|
|
6823
6823
|
throw new Error(`knowledge doc not found: ${abs}`);
|
|
6824
6824
|
const text = readFileSync14(abs, "utf8");
|
|
6825
6825
|
const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
|
|
@@ -6864,7 +6864,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6864
6864
|
isConcurrencySafe: () => false,
|
|
6865
6865
|
async execute(args) {
|
|
6866
6866
|
const rolesDir = resolve16(args.roles_dir);
|
|
6867
|
-
if (!
|
|
6867
|
+
if (!existsSync17(join17(rolesDir, "SKILL.md")))
|
|
6868
6868
|
throw new Error(`roles dir not found: ${rolesDir}`);
|
|
6869
6869
|
const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
|
|
6870
6870
|
return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
|
|
@@ -6874,7 +6874,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6874
6874
|
}
|
|
6875
6875
|
|
|
6876
6876
|
// src/gates/adapter.ts
|
|
6877
|
-
import { existsSync as
|
|
6877
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
6878
6878
|
import { join as join18 } from "node:path";
|
|
6879
6879
|
import { Service as Service2 } from "@deepseek-ai/cordis";
|
|
6880
6880
|
var HOST_LOGGER = "mstar/host-adapter";
|
|
@@ -6911,7 +6911,7 @@ class DshHostAdapter extends Service2 {
|
|
|
6911
6911
|
this.logSink(level, msg);
|
|
6912
6912
|
}
|
|
6913
6913
|
statusGate(path, kind, harnessDir) {
|
|
6914
|
-
if (!
|
|
6914
|
+
if (!existsSync18(path))
|
|
6915
6915
|
return { ok: true, violations: [] };
|
|
6916
6916
|
return validateStatusDoc(path, kind, harnessDir);
|
|
6917
6917
|
}
|
|
@@ -6992,7 +6992,7 @@ class DshHostAdapter extends Service2 {
|
|
|
6992
6992
|
}
|
|
6993
6993
|
|
|
6994
6994
|
// src/gates/agent-personas.ts
|
|
6995
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as
|
|
6995
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync7 } from "node:fs";
|
|
6996
6996
|
import { join as join19, resolve as resolve17, sep as sep5 } from "node:path";
|
|
6997
6997
|
var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
6998
6998
|
var defaultCache = new Map;
|
|
@@ -7040,7 +7040,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
|
|
|
7040
7040
|
return;
|
|
7041
7041
|
let stat;
|
|
7042
7042
|
try {
|
|
7043
|
-
stat =
|
|
7043
|
+
stat = statSync7(file);
|
|
7044
7044
|
} catch {
|
|
7045
7045
|
return;
|
|
7046
7046
|
}
|
|
@@ -7239,7 +7239,7 @@ function log2(level, message) {
|
|
|
7239
7239
|
}
|
|
7240
7240
|
|
|
7241
7241
|
// src/gates/workflow-ledger.ts
|
|
7242
|
-
import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as
|
|
7242
|
+
import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7243
7243
|
import { join as join20 } from "node:path";
|
|
7244
7244
|
var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
|
|
7245
7245
|
var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
|
|
@@ -7309,7 +7309,7 @@ function saveWatermark(workflowDir, watermark) {
|
|
|
7309
7309
|
try {
|
|
7310
7310
|
const file = join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
|
|
7311
7311
|
const tmp = `${file}.tmp`;
|
|
7312
|
-
|
|
7312
|
+
writeFileSync5(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
|
|
7313
7313
|
renameSync3(tmp, file);
|
|
7314
7314
|
} catch (error) {
|
|
7315
7315
|
log3("warn", `workflow-ledger watermark write failed for ${workflowDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
|
|
@@ -7523,7 +7523,7 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
|
|
|
7523
7523
|
}
|
|
7524
7524
|
|
|
7525
7525
|
// src/gates/goal-bridge.ts
|
|
7526
|
-
import { existsSync as
|
|
7526
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
|
|
7527
7527
|
import { join as join21 } from "node:path";
|
|
7528
7528
|
var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
|
|
7529
7529
|
var DEFAULT_MAX_GOAL_ROUNDS = 256;
|
|
@@ -7552,7 +7552,7 @@ function projectRegisterPointer(harnessDir) {
|
|
|
7552
7552
|
if (!entry.isDirectory())
|
|
7553
7553
|
continue;
|
|
7554
7554
|
const registerPath = join21(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
7555
|
-
if (
|
|
7555
|
+
if (existsSync19(registerPath))
|
|
7556
7556
|
return registerPath;
|
|
7557
7557
|
}
|
|
7558
7558
|
} catch {}
|
|
@@ -7567,7 +7567,7 @@ function iterationGoalObjective(iterationId) {
|
|
|
7567
7567
|
}
|
|
7568
7568
|
function steeringCompass(harnessDir) {
|
|
7569
7569
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
7570
|
-
if (!
|
|
7570
|
+
if (!existsSync19(iterationsDir))
|
|
7571
7571
|
return;
|
|
7572
7572
|
let entries;
|
|
7573
7573
|
try {
|
|
@@ -7579,7 +7579,7 @@ function steeringCompass(harnessDir) {
|
|
|
7579
7579
|
if (!entry.isDirectory())
|
|
7580
7580
|
continue;
|
|
7581
7581
|
const compassPath = join21(iterationsDir, entry.name, "delivery-compass.md");
|
|
7582
|
-
if (!
|
|
7582
|
+
if (!existsSync19(compassPath))
|
|
7583
7583
|
continue;
|
|
7584
7584
|
let content;
|
|
7585
7585
|
try {
|
|
@@ -7759,7 +7759,7 @@ function registerGoalBridge(ctx, resolver, config) {
|
|
|
7759
7759
|
}
|
|
7760
7760
|
|
|
7761
7761
|
// src/gates/plan-mode-bridge.ts
|
|
7762
|
-
import { existsSync as
|
|
7762
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
7763
7763
|
import { join as join22 } from "node:path";
|
|
7764
7764
|
var PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
|
|
7765
7765
|
var PLAN_STATUS_TODO = "Todo";
|
|
@@ -7777,7 +7777,7 @@ function errorMessage5(error) {
|
|
|
7777
7777
|
}
|
|
7778
7778
|
function hasPrepareWindow(harnessDir) {
|
|
7779
7779
|
const statusPath = join22(harnessDir, STATUS_FILE);
|
|
7780
|
-
if (!
|
|
7780
|
+
if (!existsSync20(statusPath))
|
|
7781
7781
|
return false;
|
|
7782
7782
|
const selection = resolveReadWorkflow(harnessDir);
|
|
7783
7783
|
if (selection.kind === "error")
|
|
@@ -7852,7 +7852,7 @@ function errorMessage6(error) {
|
|
|
7852
7852
|
return error instanceof Error ? error.message : String(error);
|
|
7853
7853
|
}
|
|
7854
7854
|
function mstarLoadLine(roleId) {
|
|
7855
|
-
return `Load mstar-roles (references/${roleId}.md)
|
|
7855
|
+
return `Load mstar-roles (references/${roleId}.md) first — identity comes before skills; load topic skills only when the Assignment activates them via its Skill presets field.`;
|
|
7856
7856
|
}
|
|
7857
7857
|
async function declareMstarSeeds(service, options) {
|
|
7858
7858
|
const { agentsDir, log: log6 } = options;
|
|
@@ -8236,7 +8236,7 @@ var inject = ["loader"];
|
|
|
8236
8236
|
function packagedCommandsDir() {
|
|
8237
8237
|
try {
|
|
8238
8238
|
const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
|
|
8239
|
-
return
|
|
8239
|
+
return existsSync21(dir) ? dir : undefined;
|
|
8240
8240
|
} catch {
|
|
8241
8241
|
return;
|
|
8242
8242
|
}
|