@mstar-harness/dsh 3.2.6 → 3.4.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 +4 -4
- package/README.zh.md +4 -4
- package/bundle/README.md +1 -1
- package/dist/gates/_shared.d.ts +3 -3
- package/dist/index.js +1072 -997
- package/harness-commands/amazing-pr-review.md +54 -0
- package/harness-skills/mstar-audit/SKILL.md +7 -6
- package/harness-skills/mstar-audit/references/audit-playbook.md +19 -1
- package/harness-skills/mstar-audit/references/codebase-audit.md +21 -3
- package/harness-skills/mstar-audit/references/finding-format.md +5 -1
- package/harness-skills/mstar-audit/references/pr-review-seat-evidence.md +28 -0
- package/harness-skills/mstar-audit/references/pr-review.md +169 -69
- package/harness-skills/mstar-audit/references/security-review.md +219 -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/_shared/leaf-executor-core.md +5 -7
- package/harness-skills/mstar-roles/references/architect.md +9 -11
- package/harness-skills/mstar-roles/references/code-reviewer.md +19 -22
- package/harness-skills/mstar-roles/references/frontend-dev.md +10 -11
- package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +12 -13
- package/harness-skills/mstar-roles/references/ops-engineer.md +10 -12
- 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 +11 -14
- package/harness-skills/mstar-roles/references/qa-engineer.md +9 -13
- 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/harness-commands/pr-deep-review.md +0 -39
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,22 +1122,22 @@ 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 mkdirSync5, 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 { dirname as
|
|
1130
|
-
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
|
|
1127
|
+
import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve6 } from "node:path";
|
|
1128
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
|
|
1129
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
|
|
1131
1130
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1131
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
|
|
1132
|
+
import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
|
|
1132
1133
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
1133
|
-
import { existsSync as
|
|
1134
|
-
import { isAbsolute as isAbsolute4, resolve as
|
|
1134
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1135
|
+
import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
|
|
1135
1136
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1136
|
-
import { mkdirSync 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 mkdirSync6, 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 resolve8 } from "node:path";
|
|
1139
1139
|
import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
1140
|
-
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as
|
|
1140
|
+
import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve11, sep as sep4 } from "node:path";
|
|
1141
1141
|
import { existsSync as existsSync9 } from "node:fs";
|
|
1142
1142
|
import { join as join13 } from "node:path";
|
|
1143
1143
|
var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
|
|
@@ -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,30 +2295,243 @@ 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
|
-
|
|
2161
|
-
|
|
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 = resolve6(startDir);
|
|
2396
|
+
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
2397
|
+
if (explicit)
|
|
2398
|
+
return resolve6(start, explicit);
|
|
2399
|
+
const boundary = resolve6(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
|
|
2400
|
+
const rc = loadMstarc(start, boundary);
|
|
2401
|
+
if (rc !== null && rc.config.harnessDir)
|
|
2402
|
+
return resolve6(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 resolve6(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 = resolve6(harnessDir);
|
|
2443
|
+
const rc = loadMstarc(dir, dirname5(dir));
|
|
2444
|
+
const declared = rc?.config[key];
|
|
2445
|
+
return declared ? resolve6(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 = resolve6(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(resolve6(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 ${resolve6(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(resolve6(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"],
|
|
2534
|
+
timeout
|
|
2162
2535
|
});
|
|
2163
2536
|
const branch = stdout.trim();
|
|
2164
2537
|
if (branch === "")
|
|
@@ -2177,25 +2550,25 @@ 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
|
-
if (controlWorktreePath !== "" && leaseWorktreePath !== "" &&
|
|
2189
|
-
violations.push(
|
|
2561
|
+
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve7(controlWorktreePath) === resolve7(leaseWorktreePath)) {
|
|
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(
|
|
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`));
|
|
2199
2572
|
}
|
|
2200
2573
|
}
|
|
2201
2574
|
return gate(violations);
|
|
@@ -2205,588 +2578,215 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
2205
2578
|
const tracks = input.tracks ?? [];
|
|
2206
2579
|
const seenPaths = new Set;
|
|
2207
2580
|
if (tracks.length < 1) {
|
|
2208
|
-
violations.push(
|
|
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"));
|
|
2209
2582
|
}
|
|
2210
2583
|
tracks.forEach((track, index) => {
|
|
2211
2584
|
if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
|
|
2212
|
-
violations.push(
|
|
2585
|
+
violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
|
|
2213
2586
|
return;
|
|
2214
2587
|
}
|
|
2215
2588
|
if (!isAbsolute4(track.worktreePath)) {
|
|
2216
|
-
violations.push(
|
|
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>)`));
|
|
2217
2590
|
return;
|
|
2218
2591
|
}
|
|
2219
|
-
const normalized =
|
|
2592
|
+
const normalized = resolve7(track.worktreePath);
|
|
2220
2593
|
if (seenPaths.has(normalized)) {
|
|
2221
|
-
violations.push(
|
|
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"));
|
|
2222
2595
|
return;
|
|
2223
2596
|
}
|
|
2224
2597
|
seenPaths.add(normalized);
|
|
2225
|
-
if (!
|
|
2226
|
-
violations.push(
|
|
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}`));
|
|
2227
2600
|
return;
|
|
2228
2601
|
}
|
|
2229
|
-
const probe = probeBranch(track.worktreePath, opts);
|
|
2230
|
-
if ("error" in probe) {
|
|
2231
|
-
violations.push(
|
|
2232
|
-
} else if (probe.branch !== track.workingBranch) {
|
|
2233
|
-
violations.push(
|
|
2234
|
-
}
|
|
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
|
-
}
|
|
2665
|
-
return doc;
|
|
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`));
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2609
|
+
return gate(violations);
|
|
2666
2610
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2611
|
+
class SddScriptError extends Error {
|
|
2612
|
+
exitCode;
|
|
2613
|
+
constructor(message, exitCode) {
|
|
2614
|
+
super(message);
|
|
2615
|
+
this.name = "SddScriptError";
|
|
2616
|
+
this.exitCode = exitCode;
|
|
2671
2617
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
quote = null;
|
|
2679
|
-
} else if (ch === "," && quote !== null) {
|
|
2680
|
-
throw new Error(`ambiguous flow-style array in ${filePath}: ${JSON.stringify(raw)} — quoted item containing comma cannot be split unambiguously (flat scalar items only)`);
|
|
2681
|
-
}
|
|
2618
|
+
}
|
|
2619
|
+
function isDirectory2(dir) {
|
|
2620
|
+
try {
|
|
2621
|
+
return statSync4(dir).isDirectory();
|
|
2622
|
+
} catch {
|
|
2623
|
+
return false;
|
|
2682
2624
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2625
|
+
}
|
|
2626
|
+
function isFile2(file) {
|
|
2627
|
+
try {
|
|
2628
|
+
return statSync4(file).isFile();
|
|
2629
|
+
} catch {
|
|
2630
|
+
return false;
|
|
2685
2631
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2632
|
+
}
|
|
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;
|
|
2692
2644
|
}
|
|
2693
|
-
return items;
|
|
2694
2645
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
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;
|
|
2701
2656
|
}
|
|
2702
|
-
function
|
|
2703
|
-
|
|
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;
|
|
2673
|
+
}
|
|
2674
|
+
return false;
|
|
2704
2675
|
}
|
|
2705
|
-
function
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
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;
|
|
2710
2691
|
}
|
|
2711
2692
|
}
|
|
2712
|
-
function
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
ok: false,
|
|
2717
|
-
violations: [violation7("high", "project.register.invalid", "project register must be an object")]
|
|
2718
|
-
};
|
|
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);
|
|
2719
2697
|
}
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
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 = resolve8(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 = resolve8(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
|
+
mkdirSync6(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
|
+
mkdirSync6(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 };
|
|
@@ -3190,6 +3190,9 @@ function completenessLevel(frontmatterText, checklist) {
|
|
|
3190
3190
|
function violation9(severity, code, message, fix) {
|
|
3191
3191
|
return { ok: false, severity, code, message, fix };
|
|
3192
3192
|
}
|
|
3193
|
+
var AUDIT_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
3194
|
+
var AUDIT_RISKS = ["LOW", "MED", "HIGH"];
|
|
3195
|
+
var AUDIT_CONFIDENCES = ["HIGH", "MED", "LOW"];
|
|
3193
3196
|
var AUDIT_STATUS_FIELDS = ["Priority", "Effort", "Risk", "Depends on", "Category", "Planned at"];
|
|
3194
3197
|
function parseStatusBlocks(planText) {
|
|
3195
3198
|
const blocks = [];
|
|
@@ -3538,7 +3541,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3538
3541
|
for (const { ref, isSymbol, module } of refs) {
|
|
3539
3542
|
if (!isSymbol || module === undefined) {
|
|
3540
3543
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
3541
|
-
if (existsSync8(
|
|
3544
|
+
if (existsSync8(resolve11(repoRoot, candidate))) {
|
|
3542
3545
|
checked++;
|
|
3543
3546
|
} else {
|
|
3544
3547
|
violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
|
|
@@ -3613,9 +3616,9 @@ function isFileLikeRoot(root) {
|
|
|
3613
3616
|
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
|
|
3614
3617
|
}
|
|
3615
3618
|
function scopeGuard(path, allowedRoots) {
|
|
3616
|
-
const resolved =
|
|
3619
|
+
const resolved = resolve11(path);
|
|
3617
3620
|
for (const root of allowedRoots) {
|
|
3618
|
-
const r =
|
|
3621
|
+
const r = resolve11(root);
|
|
3619
3622
|
if (isFileLikeRoot(r)) {
|
|
3620
3623
|
if (resolved === r)
|
|
3621
3624
|
return { ok: true, violations: [] };
|
|
@@ -3947,6 +3950,9 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
|
|
|
3947
3950
|
function resolveAssetPath(skillName, relPath, host) {
|
|
3948
3951
|
return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
|
|
3949
3952
|
}
|
|
3953
|
+
var EFFORT_ENUM_RE = new RegExp(`^(?:${AUDIT_EFFORTS.join("|")})(?:\\s*\\(|$)`);
|
|
3954
|
+
var RISK_ENUM_RE = new RegExp(`^(?:${AUDIT_RISKS.join("|")})(?:\\b|$)`);
|
|
3955
|
+
var CONFIDENCE_ENUM_RE = new RegExp(`^(${[...AUDIT_CONFIDENCES, "MEDIUM"].join("|")})\\b`, "i");
|
|
3950
3956
|
|
|
3951
3957
|
// src/index.ts
|
|
3952
3958
|
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
@@ -3984,8 +3990,8 @@ class DshMstar extends Service {
|
|
|
3984
3990
|
|
|
3985
3991
|
// src/gates/_shared.ts
|
|
3986
3992
|
var import_schemastery = __toESM(require_lib2(), 1);
|
|
3987
|
-
import { existsSync as
|
|
3988
|
-
import { resolve as
|
|
3993
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
3994
|
+
import { resolve as resolve5 } from "node:path";
|
|
3989
3995
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3990
3996
|
var STATUS_FILE = "status.json";
|
|
3991
3997
|
var PERSONA_INTERPOLATION_HAZARD = /\{\{[\s\S]*\}\}/;
|
|
@@ -4039,7 +4045,7 @@ function resolvePackagedSkillsDir(fileUrl) {
|
|
|
4039
4045
|
for (const rel of ["../harness-skills", "../../harness-skills"]) {
|
|
4040
4046
|
try {
|
|
4041
4047
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
4042
|
-
if (
|
|
4048
|
+
if (existsSync4(dir))
|
|
4043
4049
|
return dir;
|
|
4044
4050
|
} catch {}
|
|
4045
4051
|
}
|
|
@@ -4058,7 +4064,7 @@ function resolvePackagedAgentsDir(fileUrl) {
|
|
|
4058
4064
|
for (const rel of ["../harness-agents", "../../harness-agents"]) {
|
|
4059
4065
|
try {
|
|
4060
4066
|
const dir = fileURLToPath2(new URL(rel, fileUrl));
|
|
4061
|
-
if (
|
|
4067
|
+
if (existsSync4(dir))
|
|
4062
4068
|
return dir;
|
|
4063
4069
|
} catch {}
|
|
4064
4070
|
}
|
|
@@ -4078,14 +4084,14 @@ class HarnessResolver {
|
|
|
4078
4084
|
explicit;
|
|
4079
4085
|
cache = new Map;
|
|
4080
4086
|
constructor(explicit) {
|
|
4081
|
-
this.explicit = explicit !== undefined && explicit.trim() !== "" ?
|
|
4087
|
+
this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve5(process.cwd(), explicit) : null;
|
|
4082
4088
|
}
|
|
4083
4089
|
forWorkspace(cwd) {
|
|
4084
4090
|
if (this.explicit !== null)
|
|
4085
4091
|
return this.explicit;
|
|
4086
4092
|
if (cwd === undefined || cwd.trim() === "")
|
|
4087
4093
|
return null;
|
|
4088
|
-
const abs =
|
|
4094
|
+
const abs = resolve5(cwd);
|
|
4089
4095
|
const hit = this.cache.get(abs);
|
|
4090
4096
|
if (hit !== undefined)
|
|
4091
4097
|
return hit;
|
|
@@ -4126,27 +4132,27 @@ function skillLocalConfig(config) {
|
|
|
4126
4132
|
}
|
|
4127
4133
|
|
|
4128
4134
|
// src/gates/catalog.ts
|
|
4129
|
-
import { existsSync as
|
|
4130
|
-
import { join as
|
|
4135
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "node:fs";
|
|
4136
|
+
import { join as join10 } from "node:path";
|
|
4131
4137
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4132
4138
|
|
|
4133
4139
|
// src/gates/agent-flow.ts
|
|
4134
|
-
import { appendFileSync, closeSync, existsSync as
|
|
4135
|
-
import { join as
|
|
4140
|
+
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";
|
|
4141
|
+
import { join as join7 } from "node:path";
|
|
4136
4142
|
|
|
4137
4143
|
// src/gates/dispatch.ts
|
|
4138
|
-
import { existsSync as
|
|
4139
|
-
import { basename as basename4, join as
|
|
4144
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
4145
|
+
import { basename as basename4, join as join5, resolve as resolve9 } from "node:path";
|
|
4140
4146
|
|
|
4141
4147
|
// src/gates/workflow-selection.ts
|
|
4142
|
-
import { existsSync as
|
|
4143
|
-
import { dirname as dirname7, join as
|
|
4148
|
+
import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
4149
|
+
import { dirname as dirname7, join as join3, relative as relative3 } from "node:path";
|
|
4144
4150
|
var TERMINAL_STATUS_CACHE_MAX = 64;
|
|
4145
4151
|
var terminalStatusCache = new Map;
|
|
4146
4152
|
function terminalStatusOf(snapshotPath) {
|
|
4147
4153
|
let mtimeMs;
|
|
4148
4154
|
try {
|
|
4149
|
-
mtimeMs =
|
|
4155
|
+
mtimeMs = statSync2(snapshotPath).mtimeMs;
|
|
4150
4156
|
} catch {
|
|
4151
4157
|
terminalStatusCache.delete(snapshotPath);
|
|
4152
4158
|
return;
|
|
@@ -4173,8 +4179,8 @@ function terminalStatusOf(snapshotPath) {
|
|
|
4173
4179
|
return verdict;
|
|
4174
4180
|
}
|
|
4175
4181
|
function resolveActiveWorkflow(harnessDir) {
|
|
4176
|
-
const statusPath =
|
|
4177
|
-
if (!
|
|
4182
|
+
const statusPath = join3(harnessDir, STATUS_FILE);
|
|
4183
|
+
if (!existsSync7(statusPath)) {
|
|
4178
4184
|
return {
|
|
4179
4185
|
kind: "error",
|
|
4180
4186
|
code: "status.missing",
|
|
@@ -4232,7 +4238,7 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4232
4238
|
if (active.kind === "error" && active.code !== "workflow.selection.no-active")
|
|
4233
4239
|
return active;
|
|
4234
4240
|
const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
|
|
4235
|
-
if (!
|
|
4241
|
+
if (!existsSync7(workflowsDir)) {
|
|
4236
4242
|
return {
|
|
4237
4243
|
kind: "error",
|
|
4238
4244
|
code: "workflow.selection.no-snapshot",
|
|
@@ -4241,7 +4247,7 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4241
4247
|
}
|
|
4242
4248
|
let entries;
|
|
4243
4249
|
try {
|
|
4244
|
-
entries =
|
|
4250
|
+
entries = readdirSync3(workflowsDir, { withFileTypes: true });
|
|
4245
4251
|
} catch {
|
|
4246
4252
|
return {
|
|
4247
4253
|
kind: "error",
|
|
@@ -4253,8 +4259,8 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4253
4259
|
for (const entry of entries) {
|
|
4254
4260
|
if (!entry.isDirectory())
|
|
4255
4261
|
continue;
|
|
4256
|
-
const snapshotPath =
|
|
4257
|
-
if (!
|
|
4262
|
+
const snapshotPath = join3(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
|
|
4263
|
+
if (!existsSync7(snapshotPath)) {
|
|
4258
4264
|
terminalStatusCache.delete(snapshotPath);
|
|
4259
4265
|
continue;
|
|
4260
4266
|
}
|
|
@@ -4262,11 +4268,11 @@ function resolveReadWorkflow(harnessDir) {
|
|
|
4262
4268
|
if (verdict === undefined || !verdict.terminal)
|
|
4263
4269
|
continue;
|
|
4264
4270
|
if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
|
|
4265
|
-
best = { workflowId: entry.name, dir:
|
|
4271
|
+
best = { workflowId: entry.name, dir: join3(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
|
|
4266
4272
|
}
|
|
4267
4273
|
}
|
|
4268
4274
|
for (const cachedPath of terminalStatusCache.keys()) {
|
|
4269
|
-
if (!
|
|
4275
|
+
if (!existsSync7(join3(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
|
|
4270
4276
|
terminalStatusCache.delete(cachedPath);
|
|
4271
4277
|
}
|
|
4272
4278
|
}
|
|
@@ -4459,12 +4465,12 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
|
|
|
4459
4465
|
if (!sdd)
|
|
4460
4466
|
return [];
|
|
4461
4467
|
if (selection.kind === "error" && selection.code === "status.unreadable") {
|
|
4462
|
-
return [leaseViolation("lease.dispatch.unreadable", `cannot read ${
|
|
4468
|
+
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
4469
|
}
|
|
4464
|
-
return [leaseViolation("lease.dispatch.unverifiable", `${
|
|
4470
|
+
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
4471
|
}
|
|
4466
|
-
const snapshotPath =
|
|
4467
|
-
if (!
|
|
4472
|
+
const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4473
|
+
if (!existsSync10(snapshotPath)) {
|
|
4468
4474
|
if (!sdd)
|
|
4469
4475
|
return [];
|
|
4470
4476
|
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)")];
|
|
@@ -4497,7 +4503,7 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
|
|
|
4497
4503
|
const wt = worktree === undefined ? undefined : firstToken(worktree);
|
|
4498
4504
|
if (isNaValue(wt)) {
|
|
4499
4505
|
violations.push(leaseViolation("lease.dispatch.worktree-mismatch", "Assignment declares no Worktree path — cannot confirm this dispatch matches execution_lease.worktree_path", "add the absolute Worktree path to the Assignment (must equal the lease worktree_path)"));
|
|
4500
|
-
} else if (
|
|
4506
|
+
} else if (resolve9(wt) !== resolve9(String(lease.worktree_path ?? ""))) {
|
|
4501
4507
|
violations.push(leaseViolation("lease.dispatch.worktree-mismatch", `Assignment Worktree path "${wt}" differs from execution_lease.worktree_path "${String(lease.worktree_path)}"`, "align the Assignment with the lease worktree path (or update the lease)"));
|
|
4502
4508
|
}
|
|
4503
4509
|
const forms = parseAssignmentBranchForms(header);
|
|
@@ -4538,7 +4544,7 @@ function worktreeL2Violations(header) {
|
|
|
4538
4544
|
return violations;
|
|
4539
4545
|
}
|
|
4540
4546
|
function activeSnapshotRows(harnessDir) {
|
|
4541
|
-
if (!
|
|
4547
|
+
if (!existsSync10(join5(harnessDir, STATUS_FILE)))
|
|
4542
4548
|
return { rows: null, controlWorktreePath: undefined, unreadable: false };
|
|
4543
4549
|
const selection = resolveActiveWorkflow(harnessDir);
|
|
4544
4550
|
if (selection.kind !== "active") {
|
|
@@ -4548,8 +4554,8 @@ function activeSnapshotRows(harnessDir) {
|
|
|
4548
4554
|
unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
|
|
4549
4555
|
};
|
|
4550
4556
|
}
|
|
4551
|
-
const snapshotPath =
|
|
4552
|
-
if (!
|
|
4557
|
+
const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
4558
|
+
if (!existsSync10(snapshotPath))
|
|
4553
4559
|
return { rows: null, controlWorktreePath: undefined, unreadable: true };
|
|
4554
4560
|
let doc;
|
|
4555
4561
|
try {
|
|
@@ -4639,7 +4645,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec) {
|
|
|
4639
4645
|
}
|
|
4640
4646
|
const pb = writableFanOutUncovered(harnessDir);
|
|
4641
4647
|
if (pb.unreadable) {
|
|
4642
|
-
const statusLabel = harnessDir === null ? STATUS_FILE :
|
|
4648
|
+
const statusLabel = harnessDir === null ? STATUS_FILE : join5(harnessDir, STATUS_FILE);
|
|
4643
4649
|
ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
|
|
4644
4650
|
}
|
|
4645
4651
|
const verdict = workflowPolicy(config, adapter.workflowAskCache, {
|
|
@@ -4829,7 +4835,7 @@ function callPairingKey(exec) {
|
|
|
4829
4835
|
function resolveAgentFlowWriteDir(harnessDir) {
|
|
4830
4836
|
const selection = resolveActiveWorkflow(harnessDir);
|
|
4831
4837
|
if (selection.kind === "active")
|
|
4832
|
-
return
|
|
4838
|
+
return join7(harnessDir, selection.dir);
|
|
4833
4839
|
const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
|
|
4834
4840
|
if (!noActiveWarned) {
|
|
4835
4841
|
noActiveWarned = true;
|
|
@@ -4846,7 +4852,7 @@ function sleepSync(ms) {
|
|
|
4846
4852
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
4847
4853
|
}
|
|
4848
4854
|
function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
4849
|
-
const lockDir =
|
|
4855
|
+
const lockDir = join7(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
|
|
4850
4856
|
if (heldWorkflowLocks.has(lockDir)) {
|
|
4851
4857
|
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
4858
|
}
|
|
@@ -4856,7 +4862,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4856
4862
|
let acquired = null;
|
|
4857
4863
|
for (;; ) {
|
|
4858
4864
|
try {
|
|
4859
|
-
|
|
4865
|
+
mkdirSync2(lockDir);
|
|
4860
4866
|
const st = statSync5(lockDir);
|
|
4861
4867
|
acquired = { dev: st.dev, ino: st.ino };
|
|
4862
4868
|
break;
|
|
@@ -4872,7 +4878,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4872
4878
|
heldWorkflowLocks.add(lockDir);
|
|
4873
4879
|
try {
|
|
4874
4880
|
try {
|
|
4875
|
-
writeFileSync2(
|
|
4881
|
+
writeFileSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
|
|
4876
4882
|
} catch {}
|
|
4877
4883
|
return fn();
|
|
4878
4884
|
} finally {
|
|
@@ -4881,7 +4887,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4881
4887
|
const current = statSync5(lockDir);
|
|
4882
4888
|
if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
|
|
4883
4889
|
try {
|
|
4884
|
-
unlinkSync2(
|
|
4890
|
+
unlinkSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
|
|
4885
4891
|
} catch {}
|
|
4886
4892
|
rmdirSync(lockDir);
|
|
4887
4893
|
}
|
|
@@ -4889,13 +4895,13 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
|
|
|
4889
4895
|
}
|
|
4890
4896
|
}
|
|
4891
4897
|
function appendEvent(workflowDir, event) {
|
|
4892
|
-
const file =
|
|
4898
|
+
const file = join7(workflowDir, AGENT_FLOW_FILE);
|
|
4893
4899
|
withWorkflowDirLock(workflowDir, () => {
|
|
4894
4900
|
appendFileSync(file, `${JSON.stringify(event)}
|
|
4895
4901
|
`);
|
|
4896
4902
|
if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
|
|
4897
4903
|
return;
|
|
4898
|
-
const content =
|
|
4904
|
+
const content = readFileSync5(file, "utf8");
|
|
4899
4905
|
const lines = content.replace(/\n$/, "").split(`
|
|
4900
4906
|
`);
|
|
4901
4907
|
if (lines.length > AGENT_FLOW_MAX_EVENTS) {
|
|
@@ -5246,7 +5252,7 @@ function summaryOf(events) {
|
|
|
5246
5252
|
function ledgerContent(file, n) {
|
|
5247
5253
|
const size = statSync5(file).size;
|
|
5248
5254
|
if (size <= AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES) {
|
|
5249
|
-
return
|
|
5255
|
+
return readFileSync5(file, "utf8");
|
|
5250
5256
|
}
|
|
5251
5257
|
let windowBytes = AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES;
|
|
5252
5258
|
const fd = openSync(file, "r");
|
|
@@ -5278,8 +5284,8 @@ function ledgerContent(file, n) {
|
|
|
5278
5284
|
}
|
|
5279
5285
|
}
|
|
5280
5286
|
function readAgentFlow(workflowDir, limit) {
|
|
5281
|
-
const file =
|
|
5282
|
-
if (!
|
|
5287
|
+
const file = join7(workflowDir, AGENT_FLOW_FILE);
|
|
5288
|
+
if (!existsSync12(file)) {
|
|
5283
5289
|
return { events: [], summary: [] };
|
|
5284
5290
|
}
|
|
5285
5291
|
const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
|
|
@@ -5404,7 +5410,7 @@ function residualSeverityIndex(severity) {
|
|
|
5404
5410
|
function pluginVersion() {
|
|
5405
5411
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
5406
5412
|
try {
|
|
5407
|
-
const pkg = JSON.parse(
|
|
5413
|
+
const pkg = JSON.parse(readFileSync8(new URL(rel, import.meta.url), "utf8"));
|
|
5408
5414
|
if (typeof pkg.version === "string" && pkg.version !== "")
|
|
5409
5415
|
return pkg.version;
|
|
5410
5416
|
} catch {}
|
|
@@ -5533,8 +5539,8 @@ function hhmm(ts) {
|
|
|
5533
5539
|
function harnessStateSource(harnessDir) {
|
|
5534
5540
|
if (harnessDir === null)
|
|
5535
5541
|
return null;
|
|
5536
|
-
const statusPath =
|
|
5537
|
-
if (!
|
|
5542
|
+
const statusPath = join10(harnessDir, STATUS_FILE);
|
|
5543
|
+
if (!existsSync13(statusPath))
|
|
5538
5544
|
return null;
|
|
5539
5545
|
try {
|
|
5540
5546
|
const selection = resolveReadWorkflow(harnessDir);
|
|
@@ -5553,9 +5559,9 @@ function harnessStateSource(harnessDir) {
|
|
|
5553
5559
|
if (selection.kind === "error") {
|
|
5554
5560
|
return selectionErrorState(selection, rollup, harnessDir, compass);
|
|
5555
5561
|
}
|
|
5556
|
-
const snapshotPath =
|
|
5562
|
+
const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5557
5563
|
let snapshot;
|
|
5558
|
-
if (!
|
|
5564
|
+
if (!existsSync13(snapshotPath)) {
|
|
5559
5565
|
return selectionErrorState({
|
|
5560
5566
|
kind: "error",
|
|
5561
5567
|
code: "workflow.selection.snapshot-unreadable",
|
|
@@ -5617,7 +5623,7 @@ function harnessStateSource(harnessDir) {
|
|
|
5617
5623
|
leases,
|
|
5618
5624
|
knowledge: knowledgeDigest(harnessDir),
|
|
5619
5625
|
direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
|
|
5620
|
-
agentFlow: readAgentFlow(
|
|
5626
|
+
agentFlow: readAgentFlow(join10(harnessDir, selection.dir), 50)
|
|
5621
5627
|
};
|
|
5622
5628
|
} catch {
|
|
5623
5629
|
return null;
|
|
@@ -5647,21 +5653,21 @@ function selectionErrorState(selection, rollup, harnessDir, compass) {
|
|
|
5647
5653
|
function projectRollupSource(harnessDir, residuals) {
|
|
5648
5654
|
const milestones = [];
|
|
5649
5655
|
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5650
|
-
if (
|
|
5656
|
+
if (existsSync13(projectsDir)) {
|
|
5651
5657
|
let entries;
|
|
5652
5658
|
try {
|
|
5653
|
-
entries =
|
|
5659
|
+
entries = readdirSync6(projectsDir, { withFileTypes: true });
|
|
5654
5660
|
} catch {
|
|
5655
5661
|
entries = [];
|
|
5656
5662
|
}
|
|
5657
5663
|
for (const entry of entries) {
|
|
5658
5664
|
if (!entry.isDirectory())
|
|
5659
5665
|
continue;
|
|
5660
|
-
const roadmapPath =
|
|
5661
|
-
if (!
|
|
5666
|
+
const roadmapPath = join10(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
|
|
5667
|
+
if (!existsSync13(roadmapPath))
|
|
5662
5668
|
continue;
|
|
5663
5669
|
try {
|
|
5664
|
-
const doc = parseCompassFrontmatterText(
|
|
5670
|
+
const doc = parseCompassFrontmatterText(readFileSync8(roadmapPath, "utf8"), roadmapPath);
|
|
5665
5671
|
if (Array.isArray(doc.milestones)) {
|
|
5666
5672
|
for (const milestone of doc.milestones) {
|
|
5667
5673
|
if (typeof milestone === "string" && milestone.trim() !== "")
|
|
@@ -5723,11 +5729,11 @@ function residualRollup(harnessDir) {
|
|
|
5723
5729
|
}
|
|
5724
5730
|
function projectRegisters(harnessDir) {
|
|
5725
5731
|
const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
|
|
5726
|
-
if (!
|
|
5732
|
+
if (!existsSync13(projectsDir))
|
|
5727
5733
|
return [];
|
|
5728
5734
|
let entries;
|
|
5729
5735
|
try {
|
|
5730
|
-
entries =
|
|
5736
|
+
entries = readdirSync6(projectsDir, { withFileTypes: true });
|
|
5731
5737
|
} catch {
|
|
5732
5738
|
return [];
|
|
5733
5739
|
}
|
|
@@ -5735,8 +5741,8 @@ function projectRegisters(harnessDir) {
|
|
|
5735
5741
|
for (const entry of entries) {
|
|
5736
5742
|
if (!entry.isDirectory())
|
|
5737
5743
|
continue;
|
|
5738
|
-
const registerPath =
|
|
5739
|
-
if (!
|
|
5744
|
+
const registerPath = join10(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
5745
|
+
if (!existsSync13(registerPath))
|
|
5740
5746
|
continue;
|
|
5741
5747
|
try {
|
|
5742
5748
|
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
@@ -5747,13 +5753,13 @@ function projectRegisters(harnessDir) {
|
|
|
5747
5753
|
return registers;
|
|
5748
5754
|
}
|
|
5749
5755
|
function knowledgeDigest(harnessDir) {
|
|
5750
|
-
const indexPath =
|
|
5751
|
-
if (!
|
|
5756
|
+
const indexPath = join10(harnessDir, "knowledge", "README.md");
|
|
5757
|
+
if (!existsSync13(indexPath))
|
|
5752
5758
|
return null;
|
|
5753
5759
|
try {
|
|
5754
5760
|
const categories = new Set;
|
|
5755
5761
|
let docCount = 0;
|
|
5756
|
-
for (const line of
|
|
5762
|
+
for (const line of readFileSync8(indexPath, "utf8").split(/\r?\n/)) {
|
|
5757
5763
|
const row = line.trim().match(/^\|(.+)\|$/);
|
|
5758
5764
|
if (row === null)
|
|
5759
5765
|
continue;
|
|
@@ -5776,7 +5782,7 @@ function knowledgeDigest(harnessDir) {
|
|
|
5776
5782
|
}
|
|
5777
5783
|
function compassDirection(compassPath) {
|
|
5778
5784
|
try {
|
|
5779
|
-
const content =
|
|
5785
|
+
const content = readFileSync8(compassPath, "utf8");
|
|
5780
5786
|
const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
|
|
5781
5787
|
if (section === null)
|
|
5782
5788
|
return null;
|
|
@@ -5793,23 +5799,23 @@ function compassDirection(compassPath) {
|
|
|
5793
5799
|
}
|
|
5794
5800
|
function steeringCompassPath(harnessDir) {
|
|
5795
5801
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
5796
|
-
if (!
|
|
5802
|
+
if (!existsSync13(iterationsDir))
|
|
5797
5803
|
return;
|
|
5798
5804
|
let entries;
|
|
5799
5805
|
try {
|
|
5800
|
-
entries =
|
|
5806
|
+
entries = readdirSync6(iterationsDir, { withFileTypes: true });
|
|
5801
5807
|
} catch {
|
|
5802
5808
|
return;
|
|
5803
5809
|
}
|
|
5804
5810
|
for (const entry of entries) {
|
|
5805
5811
|
if (!entry.isDirectory())
|
|
5806
5812
|
continue;
|
|
5807
|
-
const compassPath =
|
|
5808
|
-
if (!
|
|
5813
|
+
const compassPath = join10(iterationsDir, entry.name, "delivery-compass.md");
|
|
5814
|
+
if (!existsSync13(compassPath))
|
|
5809
5815
|
continue;
|
|
5810
5816
|
let content;
|
|
5811
5817
|
try {
|
|
5812
|
-
content =
|
|
5818
|
+
content = readFileSync8(compassPath, "utf8");
|
|
5813
5819
|
} catch {
|
|
5814
5820
|
continue;
|
|
5815
5821
|
}
|
|
@@ -5823,8 +5829,8 @@ function steeringCompassPath(harnessDir) {
|
|
|
5823
5829
|
function iterationGateSource(harnessDir) {
|
|
5824
5830
|
if (harnessDir === null)
|
|
5825
5831
|
return;
|
|
5826
|
-
const statusPath =
|
|
5827
|
-
if (!
|
|
5832
|
+
const statusPath = join10(harnessDir, STATUS_FILE);
|
|
5833
|
+
if (!existsSync13(statusPath))
|
|
5828
5834
|
return;
|
|
5829
5835
|
const compass = steeringCompassPath(harnessDir);
|
|
5830
5836
|
if (compass === undefined)
|
|
@@ -5832,7 +5838,7 @@ function iterationGateSource(harnessDir) {
|
|
|
5832
5838
|
const selection = resolveReadWorkflow(harnessDir);
|
|
5833
5839
|
if (selection.kind === "error")
|
|
5834
5840
|
return;
|
|
5835
|
-
const snapshotPath =
|
|
5841
|
+
const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
|
|
5836
5842
|
try {
|
|
5837
5843
|
const snapshotDoc = readJson(snapshotPath);
|
|
5838
5844
|
const compassDoc = parseCompassFrontmatter(compass.compassPath);
|
|
@@ -5886,11 +5892,11 @@ function agentDigestKey(agent, cwd) {
|
|
|
5886
5892
|
}
|
|
5887
5893
|
|
|
5888
5894
|
// src/gates/status.ts
|
|
5889
|
-
import { existsSync as
|
|
5890
|
-
import { basename as basename6, join as join14, relative as relative5, resolve as
|
|
5895
|
+
import { existsSync as existsSync14, readdirSync as readdirSync9 } from "node:fs";
|
|
5896
|
+
import { basename as basename6, join as join14, relative as relative5, resolve as resolve12 } from "node:path";
|
|
5891
5897
|
var LOGGER_NAME = "mstar/status-gate";
|
|
5892
5898
|
function harnessDocKindOfTarget(harnessDir, targetPath) {
|
|
5893
|
-
const resolved =
|
|
5899
|
+
const resolved = resolve12(targetPath);
|
|
5894
5900
|
const name = basename6(resolved);
|
|
5895
5901
|
if (name !== STATUS_FILE && name !== WORKFLOW_SNAPSHOT_FILE && name !== PROJECT_REGISTER_FILE)
|
|
5896
5902
|
return null;
|
|
@@ -5969,7 +5975,7 @@ function projectRegisterDocs(harnessDir) {
|
|
|
5969
5975
|
if (!entry.isDirectory())
|
|
5970
5976
|
continue;
|
|
5971
5977
|
const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
5972
|
-
if (!
|
|
5978
|
+
if (!existsSync14(registerPath))
|
|
5973
5979
|
continue;
|
|
5974
5980
|
try {
|
|
5975
5981
|
registers.push({ projectId: entry.name, doc: readJson(registerPath) });
|
|
@@ -6042,7 +6048,7 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
|
|
|
6042
6048
|
}
|
|
6043
6049
|
|
|
6044
6050
|
// src/gates/skill-lint.ts
|
|
6045
|
-
import { existsSync as
|
|
6051
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
|
|
6046
6052
|
import { basename as basename7, dirname as dirname9, resolve as resolve14, sep as sep2 } from "node:path";
|
|
6047
6053
|
var SKILL_LINT_LOGGER = "mstar/skill-lint";
|
|
6048
6054
|
|
|
@@ -6123,7 +6129,7 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
|
|
|
6123
6129
|
if (!isSkillTarget(roots, target))
|
|
6124
6130
|
return;
|
|
6125
6131
|
const skillPath = resolve14(target.displayPath);
|
|
6126
|
-
if (!
|
|
6132
|
+
if (!existsSync15(skillPath))
|
|
6127
6133
|
return;
|
|
6128
6134
|
let doc;
|
|
6129
6135
|
try {
|
|
@@ -6174,7 +6180,7 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
|
|
|
6174
6180
|
}
|
|
6175
6181
|
|
|
6176
6182
|
// src/gates/seams.ts
|
|
6177
|
-
import { existsSync as
|
|
6183
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
|
|
6178
6184
|
import { basename as basename8, dirname as dirname10, join as join16, resolve as resolve15, sep as sep3 } from "node:path";
|
|
6179
6185
|
|
|
6180
6186
|
// ../engine/dist/audit.js
|
|
@@ -6229,9 +6235,11 @@ var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
|
|
|
6229
6235
|
var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
|
|
6230
6236
|
`).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
|
|
6231
6237
|
var WHOLE_MATCH_PATTERNS = [
|
|
6232
|
-
{ type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
|
|
6238
|
+
{ type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g },
|
|
6233
6239
|
{ type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
6234
6240
|
{ type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
|
|
6241
|
+
{ type: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
|
|
6242
|
+
{ type: "stripe-live-key", re: /\bsk_live_[A-Za-z0-9]{16,}\b/g },
|
|
6235
6243
|
{ type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
6236
6244
|
{ type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
|
|
6237
6245
|
{ type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
|
|
@@ -6242,6 +6250,28 @@ var VALUE_PATTERNS = [
|
|
|
6242
6250
|
re: /(["']?)\b(password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|secret|token)\b(["']?)(\s*[:=]\s*)("[^"\n]{8,}"|'[^'\n]{8,}'|[A-Za-z0-9_./+\-=]{16,})/gi
|
|
6243
6251
|
}
|
|
6244
6252
|
];
|
|
6253
|
+
var CI_IAC_LEAK_SHAPES = [
|
|
6254
|
+
{
|
|
6255
|
+
kind: "actions-plaintext-env",
|
|
6256
|
+
description: "GitHub Actions env assignment with plaintext literal",
|
|
6257
|
+
re: /^\s*(?:-\s+)?env:\s*[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY)[A-Z0-9_]*\s*[:=]\s*["']?[A-Za-z0-9_/+=-]{8,}["']?\s*$/
|
|
6258
|
+
},
|
|
6259
|
+
{
|
|
6260
|
+
kind: "actions-secret-echo",
|
|
6261
|
+
description: "echo of a GitHub Actions secrets context value",
|
|
6262
|
+
re: /\becho\b[^#\n]*\$\{\{\s*secrets\.[A-Za-z0-9_]+\s*\}\}/
|
|
6263
|
+
},
|
|
6264
|
+
{
|
|
6265
|
+
kind: "dockerfile-credential-env",
|
|
6266
|
+
description: "Dockerfile ENV/ARG with credential-looking name",
|
|
6267
|
+
re: /^\s*(?:ENV|ARG)\s+[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|APIKEY|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*\b/i
|
|
6268
|
+
},
|
|
6269
|
+
{
|
|
6270
|
+
kind: "terraform-hardcoded-password",
|
|
6271
|
+
description: "Terraform hardcoded password attribute",
|
|
6272
|
+
re: /^\s*password\s*=\s*"[^$\{][^"]*"\s*$/
|
|
6273
|
+
}
|
|
6274
|
+
];
|
|
6245
6275
|
function buildLineStarts(text) {
|
|
6246
6276
|
const starts = [0];
|
|
6247
6277
|
for (let i = 0;i < text.length; i++) {
|
|
@@ -6263,17 +6293,25 @@ function lineAt(starts, index) {
|
|
|
6263
6293
|
}
|
|
6264
6294
|
return lo + 1;
|
|
6265
6295
|
}
|
|
6296
|
+
function lineStartOf(text, index) {
|
|
6297
|
+
return text.lastIndexOf(`
|
|
6298
|
+
`, index - 1) + 1;
|
|
6299
|
+
}
|
|
6266
6300
|
function redactSecrets(text, filePath) {
|
|
6267
6301
|
const starts = buildLineStarts(text);
|
|
6268
6302
|
const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
|
|
6269
|
-
const
|
|
6270
|
-
const findings = [];
|
|
6303
|
+
const spans = [];
|
|
6271
6304
|
for (const pattern of WHOLE_MATCH_PATTERNS) {
|
|
6272
6305
|
for (const match of text.matchAll(pattern.re)) {
|
|
6273
6306
|
if (match.index === undefined)
|
|
6274
6307
|
continue;
|
|
6275
|
-
|
|
6276
|
-
|
|
6308
|
+
spans.push({
|
|
6309
|
+
start: match.index,
|
|
6310
|
+
end: match.index + match[0].length,
|
|
6311
|
+
priority: 0,
|
|
6312
|
+
text: marker(pattern.type, match.index),
|
|
6313
|
+
type: pattern.type
|
|
6314
|
+
});
|
|
6277
6315
|
}
|
|
6278
6316
|
}
|
|
6279
6317
|
for (const pattern of VALUE_PATTERNS) {
|
|
@@ -6282,14 +6320,51 @@ function redactSecrets(text, filePath) {
|
|
|
6282
6320
|
continue;
|
|
6283
6321
|
const type = pattern.typeOf(match[2]);
|
|
6284
6322
|
const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
|
|
6285
|
-
|
|
6286
|
-
|
|
6323
|
+
spans.push({ start: match.index, end: match.index + match[0].length, priority: 1, text: replacement, type });
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
for (const shape of CI_IAC_LEAK_SHAPES) {
|
|
6327
|
+
const lineScoped = new RegExp(shape.re.source, shape.re.ignoreCase ? "gim" : "gm");
|
|
6328
|
+
for (const match of text.matchAll(lineScoped)) {
|
|
6329
|
+
if (match.index === undefined)
|
|
6330
|
+
continue;
|
|
6331
|
+
const lineEnd = text.indexOf(`
|
|
6332
|
+
`, match.index);
|
|
6333
|
+
const end = lineEnd === -1 ? text.length : lineEnd;
|
|
6334
|
+
spans.push({
|
|
6335
|
+
start: match.index,
|
|
6336
|
+
end,
|
|
6337
|
+
priority: 2,
|
|
6338
|
+
text: `${" ".repeat(match.index - lineStartOf(text, match.index))}${marker(shape.kind, match.index)}`,
|
|
6339
|
+
type: shape.kind
|
|
6340
|
+
});
|
|
6287
6341
|
}
|
|
6288
6342
|
}
|
|
6289
|
-
|
|
6343
|
+
spans.sort((a, b) => a.start - b.start || b.end - a.end || b.priority - a.priority);
|
|
6344
|
+
const merged = [];
|
|
6345
|
+
let groupMaxEnd = -1;
|
|
6346
|
+
let best = null;
|
|
6347
|
+
for (const span of spans) {
|
|
6348
|
+
if (span.start < groupMaxEnd) {
|
|
6349
|
+
if (groupMaxEnd < span.end)
|
|
6350
|
+
groupMaxEnd = span.end;
|
|
6351
|
+
if (span.end - span.start > best.end - best.start)
|
|
6352
|
+
best = span;
|
|
6353
|
+
} else {
|
|
6354
|
+
if (best !== null)
|
|
6355
|
+
merged.push(best);
|
|
6356
|
+
groupMaxEnd = span.end;
|
|
6357
|
+
best = span;
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
if (best !== null)
|
|
6361
|
+
merged.push(best);
|
|
6290
6362
|
let out = text;
|
|
6291
|
-
for (
|
|
6292
|
-
|
|
6363
|
+
for (let i = merged.length - 1;i >= 0; i--) {
|
|
6364
|
+
const r = merged[i];
|
|
6365
|
+
out = out.slice(0, r.start) + r.text + out.slice(r.end);
|
|
6366
|
+
}
|
|
6367
|
+
const findings = merged.map((r) => ({ line: lineAt(starts, r.start), type: r.type }));
|
|
6293
6368
|
const deduped = new Map;
|
|
6294
6369
|
for (const f of findings)
|
|
6295
6370
|
deduped.set(`${f.line}:${f.type}`, f);
|
|
@@ -6372,7 +6447,7 @@ function validateDesignDoc(doc, path) {
|
|
|
6372
6447
|
const violations = [...validateDesignTokenFrontmatter(doc).violations];
|
|
6373
6448
|
const isDark = basename8(path) === "DESIGN.dark.md";
|
|
6374
6449
|
const sibling = join16(dirname10(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
|
|
6375
|
-
if (
|
|
6450
|
+
if (existsSync16(sibling)) {
|
|
6376
6451
|
const light = isDark ? readFileSync13(sibling, "utf8") : doc;
|
|
6377
6452
|
const dark = isDark ? doc : readFileSync13(sibling, "utf8");
|
|
6378
6453
|
violations.push(...assertLightDarkParity(light, dark).violations);
|
|
@@ -6399,7 +6474,7 @@ function validateRolesState(rolesDir, skillsRoot = dirname10(rolesDir)) {
|
|
|
6399
6474
|
if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
|
|
6400
6475
|
continue;
|
|
6401
6476
|
const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
|
|
6402
|
-
if (!
|
|
6477
|
+
if (!existsSync16(skillFile))
|
|
6403
6478
|
continue;
|
|
6404
6479
|
try {
|
|
6405
6480
|
skillTexts[entry.name] = readFileSync13(skillFile, "utf8");
|
|
@@ -6433,7 +6508,7 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
|
|
|
6433
6508
|
if (!isSeamTarget(seam, harnessDir, target))
|
|
6434
6509
|
return;
|
|
6435
6510
|
const path = resolve15(target.displayPath);
|
|
6436
|
-
if (!
|
|
6511
|
+
if (!existsSync16(path))
|
|
6437
6512
|
return;
|
|
6438
6513
|
let doc;
|
|
6439
6514
|
try {
|
|
@@ -6503,7 +6578,7 @@ function lintRolesWrite(doc, options) {
|
|
|
6503
6578
|
}
|
|
6504
6579
|
|
|
6505
6580
|
// src/gates/tools.ts
|
|
6506
|
-
import { existsSync as
|
|
6581
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
|
|
6507
6582
|
import { join as join17, resolve as resolve16 } from "node:path";
|
|
6508
6583
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6509
6584
|
var ITERATION_VIOLATION_SCHEMA = {
|
|
@@ -6669,9 +6744,9 @@ function registerSddIterationTools(ctx, resolver) {
|
|
|
6669
6744
|
presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
|
|
6670
6745
|
isConcurrencySafe: () => false,
|
|
6671
6746
|
async execute(args) {
|
|
6672
|
-
if (!
|
|
6747
|
+
if (!existsSync17(args.snapshot_path))
|
|
6673
6748
|
throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
|
|
6674
|
-
if (!
|
|
6749
|
+
if (!existsSync17(args.compass_path))
|
|
6675
6750
|
throw new Error(`compass file not found: ${args.compass_path}`);
|
|
6676
6751
|
const snapshotDoc = readJson(args.snapshot_path);
|
|
6677
6752
|
const compassDoc = parseCompassFrontmatter(args.compass_path);
|
|
@@ -6726,7 +6801,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6726
6801
|
async execute(args) {
|
|
6727
6802
|
const abs = resolve16(args.dir);
|
|
6728
6803
|
const lightPath = join17(abs, "DESIGN.md");
|
|
6729
|
-
if (!
|
|
6804
|
+
if (!existsSync17(lightPath))
|
|
6730
6805
|
throw new Error(`design file not found: ${lightPath}`);
|
|
6731
6806
|
const light = readFileSync14(lightPath, "utf8");
|
|
6732
6807
|
const result = validateDesignDoc(light, lightPath);
|
|
@@ -6779,7 +6854,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6779
6854
|
isConcurrencySafe: () => false,
|
|
6780
6855
|
async execute(args) {
|
|
6781
6856
|
const abs = resolve16(args.plan_path);
|
|
6782
|
-
if (!
|
|
6857
|
+
if (!existsSync17(abs))
|
|
6783
6858
|
throw new Error(`plan file not found: ${abs}`);
|
|
6784
6859
|
const text = readFileSync14(abs, "utf8");
|
|
6785
6860
|
const result = validateAuditDoc(text, abs);
|
|
@@ -6819,7 +6894,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6819
6894
|
isConcurrencySafe: () => false,
|
|
6820
6895
|
async execute(args, exec) {
|
|
6821
6896
|
const abs = resolve16(args.doc_path);
|
|
6822
|
-
if (!
|
|
6897
|
+
if (!existsSync17(abs))
|
|
6823
6898
|
throw new Error(`knowledge doc not found: ${abs}`);
|
|
6824
6899
|
const text = readFileSync14(abs, "utf8");
|
|
6825
6900
|
const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
|
|
@@ -6864,7 +6939,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6864
6939
|
isConcurrencySafe: () => false,
|
|
6865
6940
|
async execute(args) {
|
|
6866
6941
|
const rolesDir = resolve16(args.roles_dir);
|
|
6867
|
-
if (!
|
|
6942
|
+
if (!existsSync17(join17(rolesDir, "SKILL.md")))
|
|
6868
6943
|
throw new Error(`roles dir not found: ${rolesDir}`);
|
|
6869
6944
|
const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
|
|
6870
6945
|
return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
|
|
@@ -6874,7 +6949,7 @@ function registerSeamTools(ctx, resolver) {
|
|
|
6874
6949
|
}
|
|
6875
6950
|
|
|
6876
6951
|
// src/gates/adapter.ts
|
|
6877
|
-
import { existsSync as
|
|
6952
|
+
import { existsSync as existsSync18 } from "node:fs";
|
|
6878
6953
|
import { join as join18 } from "node:path";
|
|
6879
6954
|
import { Service as Service2 } from "@deepseek-ai/cordis";
|
|
6880
6955
|
var HOST_LOGGER = "mstar/host-adapter";
|
|
@@ -6911,7 +6986,7 @@ class DshHostAdapter extends Service2 {
|
|
|
6911
6986
|
this.logSink(level, msg);
|
|
6912
6987
|
}
|
|
6913
6988
|
statusGate(path, kind, harnessDir) {
|
|
6914
|
-
if (!
|
|
6989
|
+
if (!existsSync18(path))
|
|
6915
6990
|
return { ok: true, violations: [] };
|
|
6916
6991
|
return validateStatusDoc(path, kind, harnessDir);
|
|
6917
6992
|
}
|
|
@@ -6992,7 +7067,7 @@ class DshHostAdapter extends Service2 {
|
|
|
6992
7067
|
}
|
|
6993
7068
|
|
|
6994
7069
|
// src/gates/agent-personas.ts
|
|
6995
|
-
import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as
|
|
7070
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync7 } from "node:fs";
|
|
6996
7071
|
import { join as join19, resolve as resolve17, sep as sep5 } from "node:path";
|
|
6997
7072
|
var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
6998
7073
|
var defaultCache = new Map;
|
|
@@ -7040,7 +7115,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
|
|
|
7040
7115
|
return;
|
|
7041
7116
|
let stat;
|
|
7042
7117
|
try {
|
|
7043
|
-
stat =
|
|
7118
|
+
stat = statSync7(file);
|
|
7044
7119
|
} catch {
|
|
7045
7120
|
return;
|
|
7046
7121
|
}
|
|
@@ -7239,7 +7314,7 @@ function log2(level, message) {
|
|
|
7239
7314
|
}
|
|
7240
7315
|
|
|
7241
7316
|
// src/gates/workflow-ledger.ts
|
|
7242
|
-
import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as
|
|
7317
|
+
import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7243
7318
|
import { join as join20 } from "node:path";
|
|
7244
7319
|
var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
|
|
7245
7320
|
var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
|
|
@@ -7309,7 +7384,7 @@ function saveWatermark(workflowDir, watermark) {
|
|
|
7309
7384
|
try {
|
|
7310
7385
|
const file = join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
|
|
7311
7386
|
const tmp = `${file}.tmp`;
|
|
7312
|
-
|
|
7387
|
+
writeFileSync5(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
|
|
7313
7388
|
renameSync3(tmp, file);
|
|
7314
7389
|
} catch (error) {
|
|
7315
7390
|
log3("warn", `workflow-ledger watermark write failed for ${workflowDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
|
|
@@ -7523,7 +7598,7 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
|
|
|
7523
7598
|
}
|
|
7524
7599
|
|
|
7525
7600
|
// src/gates/goal-bridge.ts
|
|
7526
|
-
import { existsSync as
|
|
7601
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
|
|
7527
7602
|
import { join as join21 } from "node:path";
|
|
7528
7603
|
var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
|
|
7529
7604
|
var DEFAULT_MAX_GOAL_ROUNDS = 256;
|
|
@@ -7552,7 +7627,7 @@ function projectRegisterPointer(harnessDir) {
|
|
|
7552
7627
|
if (!entry.isDirectory())
|
|
7553
7628
|
continue;
|
|
7554
7629
|
const registerPath = join21(projectsDir, entry.name, PROJECT_REGISTER_FILE);
|
|
7555
|
-
if (
|
|
7630
|
+
if (existsSync19(registerPath))
|
|
7556
7631
|
return registerPath;
|
|
7557
7632
|
}
|
|
7558
7633
|
} catch {}
|
|
@@ -7567,7 +7642,7 @@ function iterationGoalObjective(iterationId) {
|
|
|
7567
7642
|
}
|
|
7568
7643
|
function steeringCompass(harnessDir) {
|
|
7569
7644
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
7570
|
-
if (!
|
|
7645
|
+
if (!existsSync19(iterationsDir))
|
|
7571
7646
|
return;
|
|
7572
7647
|
let entries;
|
|
7573
7648
|
try {
|
|
@@ -7579,7 +7654,7 @@ function steeringCompass(harnessDir) {
|
|
|
7579
7654
|
if (!entry.isDirectory())
|
|
7580
7655
|
continue;
|
|
7581
7656
|
const compassPath = join21(iterationsDir, entry.name, "delivery-compass.md");
|
|
7582
|
-
if (!
|
|
7657
|
+
if (!existsSync19(compassPath))
|
|
7583
7658
|
continue;
|
|
7584
7659
|
let content;
|
|
7585
7660
|
try {
|
|
@@ -7759,7 +7834,7 @@ function registerGoalBridge(ctx, resolver, config) {
|
|
|
7759
7834
|
}
|
|
7760
7835
|
|
|
7761
7836
|
// src/gates/plan-mode-bridge.ts
|
|
7762
|
-
import { existsSync as
|
|
7837
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
7763
7838
|
import { join as join22 } from "node:path";
|
|
7764
7839
|
var PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
|
|
7765
7840
|
var PLAN_STATUS_TODO = "Todo";
|
|
@@ -7777,7 +7852,7 @@ function errorMessage5(error) {
|
|
|
7777
7852
|
}
|
|
7778
7853
|
function hasPrepareWindow(harnessDir) {
|
|
7779
7854
|
const statusPath = join22(harnessDir, STATUS_FILE);
|
|
7780
|
-
if (!
|
|
7855
|
+
if (!existsSync20(statusPath))
|
|
7781
7856
|
return false;
|
|
7782
7857
|
const selection = resolveReadWorkflow(harnessDir);
|
|
7783
7858
|
if (selection.kind === "error")
|
|
@@ -7852,7 +7927,7 @@ function errorMessage6(error) {
|
|
|
7852
7927
|
return error instanceof Error ? error.message : String(error);
|
|
7853
7928
|
}
|
|
7854
7929
|
function mstarLoadLine(roleId) {
|
|
7855
|
-
return `Load mstar-roles (references/${roleId}.md)
|
|
7930
|
+
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
7931
|
}
|
|
7857
7932
|
async function declareMstarSeeds(service, options) {
|
|
7858
7933
|
const { agentsDir, log: log6 } = options;
|
|
@@ -8236,7 +8311,7 @@ var inject = ["loader"];
|
|
|
8236
8311
|
function packagedCommandsDir() {
|
|
8237
8312
|
try {
|
|
8238
8313
|
const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
|
|
8239
|
-
return
|
|
8314
|
+
return existsSync21(dir) ? dir : undefined;
|
|
8240
8315
|
} catch {
|
|
8241
8316
|
return;
|
|
8242
8317
|
}
|