@mstar-harness/dsh 3.2.6 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/dist/index.js +994 -994
  5. package/harness-skills/mstar-audit/SKILL.md +3 -2
  6. package/harness-skills/mstar-audit/references/audit-playbook.md +19 -1
  7. package/harness-skills/mstar-audit/references/codebase-audit.md +19 -3
  8. package/harness-skills/mstar-audit/references/finding-format.md +5 -1
  9. package/harness-skills/mstar-audit/references/pr-review.md +54 -1
  10. package/harness-skills/mstar-audit/references/security-review.md +221 -0
  11. package/harness-skills/mstar-conventions/SKILL.md +4 -2
  12. package/harness-skills/mstar-conventions/references/harness-bootstrap-and-agents-layering.md +5 -1
  13. package/harness-skills/mstar-harness-core/SKILL.md +1 -1
  14. package/harness-skills/mstar-roles/SKILL.md +17 -13
  15. package/harness-skills/mstar-roles/references/architect.md +9 -11
  16. package/harness-skills/mstar-roles/references/code-reviewer.md +12 -15
  17. package/harness-skills/mstar-roles/references/frontend-dev.md +9 -11
  18. package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +9 -11
  19. package/harness-skills/mstar-roles/references/ops-engineer.md +9 -11
  20. package/harness-skills/mstar-roles/references/product-manager.md +9 -11
  21. package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +2 -0
  22. package/harness-skills/mstar-roles/references/prompt-engineer.md +10 -13
  23. package/harness-skills/mstar-roles/references/qa-engineer.md +8 -12
  24. package/harness-skills/mstar-roles/references/qc-specialist-shared.md +10 -17
  25. package/harness-skills/mstar-roles/references/writing-specialist.md +8 -9
  26. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1106,7 +1106,7 @@ var require_lib2 = __commonJS((exports, module) => {
1106
1106
  });
1107
1107
 
1108
1108
  // src/index.ts
1109
- import { existsSync as existsSync22, readFileSync as readFileSync18, readdirSync as readdirSync14 } from "node:fs";
1109
+ import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync14 } from "node:fs";
1110
1110
  import { join as join23 } from "node:path";
1111
1111
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1112
1112
  import {
@@ -1122,20 +1122,20 @@ import { basename, dirname, join, resolve } from "node:path";
1122
1122
  import { fileURLToPath } from "node:url";
1123
1123
  import { readFileSync as readFileSync2, statSync } from "node:fs";
1124
1124
  import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
1125
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
1125
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
1126
1126
  import { execFileSync } from "node:child_process";
1127
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
1128
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
1129
- import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
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 resolve5 } from "node:path";
1128
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
1129
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
1130
+ import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
1131
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
1131
1132
  import { AsyncLocalStorage } from "node:async_hooks";
1132
1133
  import { execFileSync as execFileSync2 } from "node:child_process";
1133
- import { existsSync as existsSync4 } from "node:fs";
1134
+ import { existsSync as existsSync6 } from "node:fs";
1134
1135
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
1135
1136
  import { execFileSync as execFileSync3 } from "node:child_process";
1136
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
1137
- import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
1138
- import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
1137
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
1138
+ import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve7 } from "node:path";
1139
1139
  import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
1140
1140
  import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
1141
1141
  import { existsSync as existsSync9 } from "node:fs";
@@ -1276,220 +1276,380 @@ function isAtOrBelow(dir, root) {
1276
1276
  const rel = relative(root, dir);
1277
1277
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1278
1278
  }
1279
- function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
1280
- const start = resolve3(startDir);
1281
- const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
1282
- if (explicit)
1283
- return resolve3(start, explicit);
1284
- const boundary = resolve3(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
1285
- const rc = loadMstarc(start, boundary);
1286
- if (rc !== null && rc.config.harnessDir)
1287
- return resolve3(rc.dir, rc.config.harnessDir);
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 defaultWorkspaceRoot(startDir) {
1305
- try {
1306
- const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
1307
- cwd: startDir,
1308
- encoding: "utf8",
1309
- stdio: ["ignore", "pipe", "ignore"]
1310
- }).trim();
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
- return resolve3(boundary);
1319
- } catch {}
1320
- return startDir;
1321
- }
1322
- function isAtOrBelow2(dir, root) {
1323
- const rel = relative2(root, dir);
1324
- return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
1325
- }
1326
- function mstarcDirOverride(harnessDir, key) {
1327
- const dir = resolve3(harnessDir);
1328
- const rc = loadMstarc(dir, dirname3(dir));
1329
- const declared = rc?.config[key];
1330
- return declared ? resolve3(rc.dir, declared) : null;
1331
- }
1332
- function assertSafePathComponent(value, what) {
1333
- if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
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
- function resolveSddDir(harnessDir, planId) {
1338
- assertSafePathComponent(planId, "planId");
1339
- const base = resolve3(harnessDir);
1340
- const declared = mstarcDirOverride(base, "sddDir");
1341
- const sddBase = declared !== null ? declared : join3(base, "sdd");
1342
- return join3(sddBase, planId);
1343
- }
1344
- function resolveIterationDir(harnessDir) {
1345
- const declared = mstarcDirOverride(harnessDir, "iterationDir");
1346
- if (declared !== null)
1347
- return declared;
1348
- return join3(resolve3(harnessDir), "iterations");
1349
- }
1350
- function resolveHarnessSubdir(startDir, opts, key, fallback) {
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 declared = mstarcDirOverride(harness, key);
1356
- return declared !== null ? declared : join3(resolve3(harness), fallback);
1357
- }
1358
- function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
1359
- return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
1360
- }
1361
- function resolveProjectDir(startDir = process.cwd(), opts = {}) {
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
- function isPlainObject(value) {
1397
- return typeof value === "object" && value !== null && !Array.isArray(value);
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 validateNonEmptyString(violations, value, field, missingCode, invalidCode) {
1403
- if (value === undefined) {
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 validateExecutionLease(lease) {
1416
- const violations = [];
1417
- if (!isPlainObject(lease)) {
1359
+ function validateCompassFrontmatter(doc) {
1360
+ if (!isPlainObject(doc)) {
1418
1361
  return {
1419
1362
  ok: false,
1420
1363
  violations: [
1421
- violation("high", "lease.execution-lease.invalid", "execution_lease must be an object null and tombstone objects are invalid; writers delete the key on release")
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
- validateNonEmptyString(violations, lease.holder, "holder", "lease.execution-lease.missing-holder", "lease.execution-lease.invalid-holder");
1426
- if (lease.claimed_at === undefined) {
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
- violation("high", "lease.merge-lease.invalid", "integration_merge_lease must be an object — absent means unclaimed; null and tombstone objects are invalid; writers delete the key on release")
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
- validateNonEmptyString(violations, lease.holder, "holder", "lease.merge-lease.missing-holder", "lease.merge-lease.invalid-holder");
1455
- if (lease.claimed_at === undefined) {
1456
- violations.push(violation("high", "lease.merge-lease.missing-claimed-at", "missing required field: claimed_at"));
1457
- } else if (!isValidClaimedAt(lease.claimed_at)) {
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
- validateNonEmptyString(violations, lease.plan_id, "plan_id", "lease.merge-lease.missing-plan-id", "lease.merge-lease.invalid-plan-id");
1461
- validateNonEmptyString(violations, lease.source_branch, "source_branch", "lease.merge-lease.missing-source-branch", "lease.merge-lease.invalid-source-branch");
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 planExecutionLeaseLocations(row) {
1469
- return { row: row.execution_lease };
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 verifyPlanExecutionLease(row, planId) {
1472
- const { row: rowLease } = planExecutionLeaseLocations(row);
1473
- const lease = rowLease;
1474
- if (lease === undefined) {
1475
- if (row.status === "InProgress") {
1476
- return {
1477
- ok: false,
1478
- violations: [
1479
- violation("high", "lease.verify.orphan", "plan is InProgress without an execution_lease — orphan: STOP, no writable dispatch until recovery (status-and-residuals.md § Orphan recovery)")
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
- violations.push(...validateExecutionLease(lease).violations);
1492
- return { ok: violations.length === 0, violations, lease };
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 violation2(severity, code, message, fix) {
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 = violation2("high", `assignment.field.missing-${code}`, `missing required Assignment field: ${label}`, `add "**${label}**: <value>" to the Assignment`);
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 = violation2("high", `assignment.field.invalid-${code}`, `${label} must be non-empty`, `fill in "**${label}**: <value>"`);
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(violation2("high", "assignment.field.branch-missing", "writable assignment must contain exactly one branch form", `add exactly one of: ${BRANCH_FORMS_HINT}`));
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(violation2("high", "assignment.field.branch-multiple", `writable assignment contains ${formCount} branch forms (Working branch + Branch policy) — exactly one required`, `keep exactly one of: ${BRANCH_FORMS_HINT}`));
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(violation2("high", "assignment.field.branch-missing-base", `create-form Working branch is incomplete: "${fields.workingBranch}" (expected "create <new-branch> from <base>")`, "write both the new branch name and the ancestor branch after `from` (main / existing feature branch / remote-tracking branch / `current`)"));
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(violation2("high", "assignment.field.branch-policy-missing-branch", `unparseable Branch policy: "${fields.branchPolicy}" (expected "direct on <branch> — <reason>")`, "start the field with `direct on <branch>`"));
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(violation2("high", "assignment.field.branch-policy-missing-reason", `Branch policy "direct on ${direct.branch}" is missing the reason`, 'append "— <reason>" after the branch name'));
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(violation2("high", "dispatch.default-branch.protected", `writable work on default protected branch "${normalized}" requires an explicit direct-on exception`, `add "Branch policy: direct on ${normalized} — <reason>" to the Assignment, or use a feature branch`));
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(violation2("high", "dispatch.execution-mode.missing", "missing required Assignment field: Execution mode", 'add "**Execution mode**: sdd | inline | targeted"'));
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(violation2("high", "dispatch.execution-mode.missing-seats", 'execution mode "targeted" requires listed reviewer seats', 'add "QC re-review: targeted — reviewers: <role-id>, …" to the Assignment and pass the seats'));
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(violation2("high", "dispatch.execution-mode.too-many-seats", `execution mode "targeted" lists ${seats.length} reviewer seats — at most 3 (targeted re-review seats are the tri seats, N = 1–3)`, "list at most three reviewer seats for the targeted re-review"));
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(violation2("high", "dispatch.execution-mode.unknown", `unknown execution mode "${executionMode.trim()}" (expected sdd | inline | targeted)`, "fix the Execution mode field"));
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
- violation2("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")
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
- violation2("critical", "dispatch.anti-recursion.self-type", `recursive dispatch refused: role binding "${subagentType}" equals Execute as "${executeAs}" (leaf executors must not re-invoke their own role)`, "complete the work in this session, or return Blocked to project-manager")
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 isPlainObject2(value) {
1886
+ function isPlainObject3(value) {
1727
1887
  return typeof value === "object" && value !== null && !Array.isArray(value);
1728
1888
  }
1729
- function violation3(severity, code, message, fix) {
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(violation3("high", missingCode, `missing required field: ${field}`));
1894
+ violations.push(violation4("high", missingCode, `missing required field: ${field}`));
1735
1895
  } else if (typeof value !== "string" || value.trim() === "") {
1736
- violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
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 (!isPlainObject2(doc)) {
1901
+ if (!isPlainObject3(doc)) {
1742
1902
  return {
1743
1903
  ok: false,
1744
- violations: [violation3("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
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(violation3("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
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(violation3("high", "workflow.snapshot.invalid-schema-version", `schema_version must be 1 — got ${JSON.stringify(doc.schema_version)} (version is reserved for the root file discriminator)`));
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(violation3("medium", "workflow.snapshot.reserved-version", `top-level version is reserved for the root status.json discriminator — snapshots use schema_version; remove the version key (got ${JSON.stringify(doc.version)})`, "remove the version key from the snapshot"));
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(violation3("high", "workflow.snapshot.missing-type", "missing required field: type"));
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(violation3("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
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(violation3("high", "workflow.snapshot.missing-status", "missing required field: status"));
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(violation3("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
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(violation3("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
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(violation3("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
1935
+ violations.push(violation4("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
1776
1936
  } else if (!Array.isArray(doc.plans)) {
1777
- violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
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 (isPlainObject2(row) && row.execution_lease !== undefined) {
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 (!isPlainObject2(doc.execution_policy)) {
1788
- violations.push(violation3("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
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 (!isPlainObject2(doc.branch)) {
1796
- violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
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(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
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 && !isPlainObject2(doc.legacy_metadata)) {
1809
- violations.push(violation3("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
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(violation3("high", "workflow.snapshot.missing-ended-at", `terminal status ${JSON.stringify(doc.status)} requires ended_at — a terminal snapshot must record when the lifecycle ended`));
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 (isPlainObject2(row) && row.execution_lease !== undefined) {
1822
- violations.push(violation3("high", "workflow.snapshot.terminal-dangling-execution-lease", `terminal snapshot must not carry a row execution_lease (dangling lease) — release every lease before the lifecycle ends`));
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(violation3("high", "workflow.snapshot.terminal-dangling-merge-lease", "terminal snapshot must not carry integration_merge_lease (dangling lease) — release the merge lease before the lifecycle ends"));
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 DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
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 isPlainObject3(value) {
1996
+ function isPlainObject4(value) {
1837
1997
  return typeof value === "object" && value !== null && !Array.isArray(value);
1838
1998
  }
1839
- function violation4(severity, code, message, fix) {
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(violation4("high", missingCode, `missing required field: ${field}`));
2016
+ violations.push(violation5("high", missingCode, `missing required field: ${field}`));
1857
2017
  } else if (typeof value !== "string" || value.trim() === "") {
1858
- violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
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 (!isPlainObject3(row)) {
1864
- return { ok: false, violations: [violation4("high", "status.plan-row.invalid", "plan row must be an object")] };
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(violation4("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
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(violation4("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
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(violation4("high", "status.plan-row.missing-status", "missing required field: status"));
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(violation4("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
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 && !isPlainObject3(metadata)) {
1888
- violations.push(violation4("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
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 && !isPlainObject3(execution_lease)) {
1891
- violations.push(violation4("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
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(violation4("medium", "status.plan-row.done-with-lease", 'plan status Done must not carry an execution_lease — the Done authority deletes the lease in the same complete-file update as status: "Done" (status-and-residuals.md § Hold, release, and override)', 'delete plans[].execution_lease in the same update that sets status: "Done"'));
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 (!isPlainObject3(entry)) {
1901
- return { ok: false, violations: [violation4("high", "status.residual.invalid", "residual entry must be an object")] };
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(violation4("high", "status.residual.missing-severity", "missing required field: severity"));
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(violation4("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
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(violation4("low", "status.residual.legacy-warning", `severity "warning" is legacy — forbidden on new entries; read paths normalize it to "low"`, `use "low" (normalizeSeverity maps 'warning' → 'low')`));
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(violation4("high", "status.residual.missing-decision", "missing required field: decision"));
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(violation4("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
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(violation4("high", "status.residual.missing-target", "missing required field: target"));
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(violation4("medium", "status.residual.invalid-target", "target must be a string or null"));
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(violation4("high", "status.residual.missing-tracking", "missing required field: tracking"));
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(violation4("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
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(violation4("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
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" || !DATE_RE.test(closed_at))) {
1935
- violations.push(violation4("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
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(violation4("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
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(violation4("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
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(violation4("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
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 (!isPlainObject3(entry)) {
2120
+ if (!isPlainObject4(entry)) {
1961
2121
  return {
1962
2122
  ok: false,
1963
- violations: [violation4("high", "status.workflow.invalid", "workflow entry must be an object")]
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(violation4("high", "status.workflow.missing-type", "missing required field: type"));
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(violation4("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
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(violation4("high", "status.workflow.missing-dir", "missing required field: dir"));
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(violation4("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
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(violation4("medium", "status.workflow.invalid-dir", `dir must be a harness-relative path (no absolute paths, no ".." segments) — got ${JSON.stringify(entry.dir)}`));
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 = dirname5(resolve5(docOrPath));
2148
+ harnessDir = dirname4(resolve4(docOrPath));
1989
2149
  } catch (error) {
1990
2150
  return {
1991
2151
  ok: false,
1992
- violations: [violation4("high", "status.invalid-json", error.message)]
2152
+ violations: [violation5("high", "status.invalid-json", error.message)]
1993
2153
  };
1994
2154
  }
1995
2155
  } else {
1996
2156
  doc = docOrPath;
1997
2157
  }
1998
- if (!isPlainObject3(doc)) {
1999
- return { ok: false, violations: [violation4("high", "status.invalid-doc", "status document must be an object")] };
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
- violation4("high", "status.migration-required", `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`, "run `mstar migrate`")
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
- violation4("high", "status.migration-required", "v1-shaped status.json (root plans[]) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
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
- violation4("high", "status.migration-required", "v1-shaped status.json (root residual_findings) is not a v2 document — run `mstar migrate` to convert the tree", "run `mstar migrate`")
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(violation4("high", "status.missing-updated-at", "missing required field: updated_at"));
2028
- } else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
2029
- violations.push(violation4("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
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(violation4("high", "status.missing-workflows", "missing required field: workflows"));
2192
+ violations.push(violation5("high", "status.missing-workflows", "missing required field: workflows"));
2033
2193
  } else if (!Array.isArray(doc.workflows)) {
2034
- violations.push(violation4("high", "status.invalid-workflows", "workflows must be an array"));
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 (isPlainObject3(entry) && typeof entry.id === "string") {
2199
+ if (isPlainObject4(entry) && typeof entry.id === "string") {
2040
2200
  if (seen.has(entry.id)) {
2041
- violations.push(violation4("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
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 = realpathSync2(harnessDir);
2210
+ realHarnessDir = realpathSync(harnessDir);
2051
2211
  } catch {}
2052
2212
  for (const entry of doc.workflows) {
2053
- if (!isPlainObject3(entry) || typeof entry.dir !== "string")
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 = realpathSync2(snapshotPath);
2220
+ physical = realpathSync(snapshotPath);
2061
2221
  } catch {
2062
- violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
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(violation4("high", "status.workflow.snapshot-outside-harness", `workflows[] lists ${JSON.stringify(label)} but its snapshot resolves outside the harness dir (${JSON.stringify(physical)}) — symlinked snapshot paths are rejected; the snapshot must physically live under ${JSON.stringify(harnessDir)}`));
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(violation4("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
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(violation4("high", "status.workflow.terminal-listed", `workflows[] lists ${JSON.stringify(label)} whose snapshot status is terminal (${snapshot.status}) — removal-at-terminal: terminal writers unregister AFTER the snapshot write`));
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(violation4("medium", "status.workflow.mismatched-type", `workflows[] entry ${JSON.stringify(label)} type ${JSON.stringify(entry.type)} does not match its snapshot type ${JSON.stringify(snapshot.type)} — the root entry mirrors the snapshot; align them`));
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(violation4("medium", "status.workflow.mismatched-started-at", `workflows[] entry ${JSON.stringify(label)} started_at ${JSON.stringify(entry.started_at)} does not match its snapshot started_at ${JSON.stringify(snapshot.started_at)} — workflow ${JSON.stringify(label)} collided with another writer (e.g. a concurrent/re-run \`audit promote\` with the same workflow id rewrote the snapshot); the root entry mirrors the snapshot — align them or remove the colliding workflow`));
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 = resolve5(harnessDir);
2124
- const rc = loadMstarc(dir, dirname5(dir));
2283
+ const dir = resolve4(harnessDir);
2284
+ const rc = loadMstarc(dir, dirname4(dir));
2125
2285
  const value = rc?.config.enforcement;
2126
2286
  if (value === "hard")
2127
2287
  return { hard: true, source: "mstarc" };
@@ -2135,29 +2295,242 @@ function resolveRepoEnforcement(harnessDir) {
2135
2295
  return rc;
2136
2296
  return resolveCompassEnforcement(harnessDir);
2137
2297
  }
2138
- var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
2139
- function probeTimeoutMs() {
2140
- const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
2141
- if (raw === undefined || raw.trim() === "")
2142
- return DEFAULT_PROBE_TIMEOUT_MS;
2143
- const parsed = Number(raw);
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 violation5(severity, code, message, fix) {
2305
+ function violation6(severity, code, message, fix) {
2147
2306
  return { ok: false, severity, code, message, fix };
2148
2307
  }
2149
- function gate(violations) {
2150
- return { ok: violations.length === 0, violations };
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 probeBranch(worktreePath, opts) {
2153
- const precomputed = opts.branchOf?.(worktreePath);
2154
- if (precomputed !== undefined)
2155
- return { branch: precomputed };
2156
- const timeout = opts.timeoutMs ?? probeTimeoutMs();
2157
- try {
2158
- const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
2159
- encoding: "utf8",
2160
- stdio: ["ignore", "pipe", "pipe"],
2315
+ function validateProjectRegister(doc) {
2316
+ const violations = [];
2317
+ if (!isPlainObject5(doc)) {
2318
+ return {
2319
+ ok: false,
2320
+ violations: [violation6("high", "project.register.invalid", "project register must be an object")]
2321
+ };
2322
+ }
2323
+ if (doc.entries === undefined) {
2324
+ violations.push(violation6("high", "project.register.missing-entries", "missing required field: entries"));
2325
+ } else if (!isPlainObject5(doc.entries)) {
2326
+ violations.push(violation6("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
2327
+ } else {
2328
+ for (const [key, entries] of Object.entries(doc.entries)) {
2329
+ if (key.trim() === "") {
2330
+ violations.push(violation6("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
2331
+ }
2332
+ if (!Array.isArray(entries)) {
2333
+ violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
2334
+ continue;
2335
+ }
2336
+ for (const entry of entries) {
2337
+ violations.push(...validateResidual(entry).violations);
2338
+ if (!isPlainObject5(entry))
2339
+ continue;
2340
+ validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
2341
+ if (entry.registered_at === undefined) {
2342
+ violations.push(violation6("high", "project.register.missing-registered-at", "missing required field: registered_at"));
2343
+ } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
2344
+ violations.push(violation6("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
2345
+ }
2346
+ if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
2347
+ violations.push(violation6("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
2348
+ }
2349
+ if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
2350
+ violations.push(violation6("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} — entries are keyed by plan id`));
2351
+ }
2352
+ }
2353
+ }
2354
+ }
2355
+ return { ok: violations.length === 0, violations };
2356
+ }
2357
+ function findingsCleanupGate(register, planId, opts) {
2358
+ const mode = opts?.mode ?? "allow-residual";
2359
+ const violations = [];
2360
+ const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
2361
+ if (entries === undefined) {
2362
+ return { ok: true, violations };
2363
+ }
2364
+ if (!Array.isArray(entries)) {
2365
+ violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
2366
+ return { ok: false, violations };
2367
+ }
2368
+ if (entries.length === 0) {
2369
+ return { ok: true, violations };
2370
+ }
2371
+ for (const entry of entries) {
2372
+ if (!isOpenResidual(entry))
2373
+ continue;
2374
+ const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
2375
+ const label = `R#${id}`;
2376
+ if (mode === "zero-residual") {
2377
+ if (entry.severity === "nit") {
2378
+ violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
2379
+ } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
2380
+ violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
2381
+ } else if (entry.decision === "defer") {
2382
+ if (typeof entry.target !== "string" || entry.target.trim() === "") {
2383
+ violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
2384
+ }
2385
+ } else {
2386
+ violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
2387
+ }
2388
+ } else if (normalizeSeverity(entry.severity) === "critical") {
2389
+ violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
2390
+ }
2391
+ }
2392
+ return { ok: violations.length === 0, violations };
2393
+ }
2394
+ function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
2395
+ const start = resolve5(startDir);
2396
+ const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
2397
+ if (explicit)
2398
+ return resolve5(start, explicit);
2399
+ const boundary = resolve5(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
2400
+ const rc = loadMstarc(start, boundary);
2401
+ if (rc !== null && rc.config.harnessDir)
2402
+ return resolve5(rc.dir, rc.config.harnessDir);
2403
+ let dir = start;
2404
+ for (;; ) {
2405
+ if (!isAtOrBelow2(dir, boundary))
2406
+ return null;
2407
+ for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
2408
+ if (isDirectory(candidate))
2409
+ return candidate;
2410
+ }
2411
+ if (dir === boundary)
2412
+ return null;
2413
+ const parent = dirname5(dir);
2414
+ if (parent === dir)
2415
+ return null;
2416
+ dir = parent;
2417
+ }
2418
+ }
2419
+ function defaultWorkspaceRoot(startDir) {
2420
+ try {
2421
+ const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
2422
+ cwd: startDir,
2423
+ encoding: "utf8",
2424
+ stdio: ["ignore", "pipe", "ignore"]
2425
+ }).trim();
2426
+ if (!cdup)
2427
+ return startDir;
2428
+ let boundary = startDir;
2429
+ for (const segment of cdup.split(/[\\/]/)) {
2430
+ if (segment && segment !== ".")
2431
+ boundary = dirname5(boundary);
2432
+ }
2433
+ return resolve5(boundary);
2434
+ } catch {}
2435
+ return startDir;
2436
+ }
2437
+ function isAtOrBelow2(dir, root) {
2438
+ const rel = relative2(root, dir);
2439
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
2440
+ }
2441
+ function mstarcDirOverride(harnessDir, key) {
2442
+ const dir = resolve5(harnessDir);
2443
+ const rc = loadMstarc(dir, dirname5(dir));
2444
+ const declared = rc?.config[key];
2445
+ return declared ? resolve5(rc.dir, declared) : null;
2446
+ }
2447
+ function assertSafePathComponent(value, what) {
2448
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
2449
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
2450
+ }
2451
+ }
2452
+ function resolveSddDir(harnessDir, planId) {
2453
+ assertSafePathComponent(planId, "planId");
2454
+ const base = resolve5(harnessDir);
2455
+ const declared = mstarcDirOverride(base, "sddDir");
2456
+ const sddBase = declared !== null ? declared : join8(base, "sdd");
2457
+ return join8(sddBase, planId);
2458
+ }
2459
+ function resolveIterationDir(harnessDir) {
2460
+ const declared = mstarcDirOverride(harnessDir, "iterationDir");
2461
+ if (declared !== null)
2462
+ return declared;
2463
+ return join8(resolve5(harnessDir), "iterations");
2464
+ }
2465
+ function resolveHarnessSubdir(startDir, opts, key, fallback) {
2466
+ const harness = resolveHarnessDir(startDir, opts);
2467
+ if (harness === null) {
2468
+ throw new Error(`harness dir not found from ${resolve5(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
2469
+ }
2470
+ const declared = mstarcDirOverride(harness, key);
2471
+ return declared !== null ? declared : join8(resolve5(harness), fallback);
2472
+ }
2473
+ function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
2474
+ return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
2475
+ }
2476
+ function resolveProjectDir(startDir = process.cwd(), opts = {}) {
2477
+ return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
2478
+ }
2479
+ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
2480
+ # Principle: process stays local; results are shared with the team.
2481
+ # Default-ignore everything under .mstar/, then re-include the tracked results.
2482
+ .mstar/**
2483
+ !.mstar/AGENTS.md
2484
+ !.mstar/knowledge/
2485
+ !.mstar/knowledge/**
2486
+ !.mstar/specs/
2487
+ !.mstar/specs/**
2488
+ # .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
2489
+ .mstarc
2490
+ `;
2491
+ var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
2492
+ # Default-ignore everything under .agents/, then re-include the tracked results.
2493
+ .agents/**
2494
+ !.agents/AGENTS.md
2495
+ !.agents/knowledge/
2496
+ !.agents/knowledge/**
2497
+ !.agents/specs/
2498
+ !.agents/specs/**
2499
+ `;
2500
+ var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
2501
+ `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
2502
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
2503
+ `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
2504
+ function isDirectory(dir) {
2505
+ try {
2506
+ return statSync3(dir).isDirectory();
2507
+ } catch {
2508
+ return false;
2509
+ }
2510
+ }
2511
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
2512
+ function probeTimeoutMs() {
2513
+ const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
2514
+ if (raw === undefined || raw.trim() === "")
2515
+ return DEFAULT_PROBE_TIMEOUT_MS;
2516
+ const parsed = Number(raw);
2517
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
2518
+ }
2519
+ function violation7(severity, code, message, fix) {
2520
+ return { ok: false, severity, code, message, fix };
2521
+ }
2522
+ function gate(violations) {
2523
+ return { ok: violations.length === 0, violations };
2524
+ }
2525
+ function probeBranch(worktreePath, opts) {
2526
+ const precomputed = opts.branchOf?.(worktreePath);
2527
+ if (precomputed !== undefined)
2528
+ return { branch: precomputed };
2529
+ const timeout = opts.timeoutMs ?? probeTimeoutMs();
2530
+ try {
2531
+ const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
2532
+ encoding: "utf8",
2533
+ stdio: ["ignore", "pipe", "pipe"],
2161
2534
  timeout
2162
2535
  });
2163
2536
  const branch = stdout.trim();
@@ -2177,616 +2550,243 @@ function l1PreDispatchCheck(input, opts = {}) {
2177
2550
  const violations = [];
2178
2551
  const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
2179
2552
  if (controlWorktreePath.trim() === "") {
2180
- violations.push(violation5("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"));
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(violation5("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"));
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(violation5("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
2559
+ violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
2187
2560
  }
2188
2561
  if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
2189
- violations.push(violation5("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"));
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 !== "" && !existsSync4(leaseWorktreePath)) {
2192
- violations.push(violation5("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>`));
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(violation5("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)"));
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(violation5("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
- }
2200
- }
2201
- return gate(violations);
2202
- }
2203
- function l2PreDispatchCheck(input, opts = {}) {
2204
- const violations = [];
2205
- const tracks = input.tracks ?? [];
2206
- const seenPaths = new Set;
2207
- if (tracks.length < 1) {
2208
- violations.push(violation5("high", "worktree.l2.no-tracks", "no parallel writable tracks — the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
2209
- }
2210
- tracks.forEach((track, index) => {
2211
- if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
2212
- violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2213
- return;
2214
- }
2215
- if (!isAbsolute4(track.worktreePath)) {
2216
- violations.push(violation5("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
2217
- return;
2218
- }
2219
- const normalized = resolve6(track.worktreePath);
2220
- if (seenPaths.has(normalized)) {
2221
- violations.push(violation5("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
2222
- return;
2223
- }
2224
- seenPaths.add(normalized);
2225
- if (!existsSync4(track.worktreePath)) {
2226
- violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
2227
- return;
2228
- }
2229
- const probe = probeBranch(track.worktreePath, opts);
2230
- if ("error" in probe) {
2231
- violations.push(violation5("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
2232
- } else if (probe.branch !== track.workingBranch) {
2233
- violations.push(violation5("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
2571
+ violations.push(violation7("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
2234
2572
  }
2235
- });
2236
- return gate(violations);
2237
- }
2238
- class SddScriptError extends Error {
2239
- exitCode;
2240
- constructor(message, exitCode) {
2241
- super(message);
2242
- this.name = "SddScriptError";
2243
- this.exitCode = exitCode;
2244
- }
2245
- }
2246
- function isDirectory2(dir) {
2247
- try {
2248
- return statSync4(dir).isDirectory();
2249
- } catch {
2250
- return false;
2251
- }
2252
- }
2253
- function isFile2(file) {
2254
- try {
2255
- return statSync4(file).isFile();
2256
- } catch {
2257
- return false;
2258
- }
2259
- }
2260
- var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
2261
- function gitOut(cwd, args) {
2262
- try {
2263
- return execFileSync3("git", args, {
2264
- cwd,
2265
- encoding: "utf8",
2266
- stdio: ["ignore", "pipe", "pipe"],
2267
- maxBuffer: GIT_CAPTURE_MAX_BYTES
2268
- }).trim();
2269
- } catch {
2270
- return null;
2271
- }
2272
- }
2273
- function probeHarnessWithStatus(root) {
2274
- if (isFile2(join7(root, ".mstar", "status.json")))
2275
- return join7(root, ".mstar");
2276
- if (isFile2(join7(root, ".agents", "status.json")))
2277
- return join7(root, ".agents");
2278
- if (hasWorkflowSnapshot(join7(root, ".mstar")))
2279
- return join7(root, ".mstar");
2280
- if (hasWorkflowSnapshot(join7(root, ".agents")))
2281
- return join7(root, ".agents");
2282
- return null;
2283
- }
2284
- function hasWorkflowSnapshot(harnessDir) {
2285
- let workflowsDir;
2286
- try {
2287
- workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2288
- } catch {
2289
- workflowsDir = join7(harnessDir, "workflows");
2290
- }
2291
- if (!isDirectory2(workflowsDir))
2292
- return false;
2293
- try {
2294
- for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
2295
- if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
2296
- return true;
2297
- }
2298
- } catch {
2299
- return false;
2300
- }
2301
- return false;
2302
- }
2303
- function isLinkedWorktree(root) {
2304
- const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
2305
- const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2306
- if (gitDirRaw === null || commonRaw === null)
2307
- return false;
2308
- const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
2309
- const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
2310
- if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2311
- return true;
2312
- try {
2313
- const gdParent = realpathSync3(dirname6(gitDir));
2314
- const cmAbs = realpathSync3(common);
2315
- return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2316
- } catch {
2317
- return false;
2318
- }
2319
- }
2320
- function sddWorkspace(planId, opts = {}) {
2321
- if (!planId) {
2322
- throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
2323
- ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
2324
- }
2325
- const cwd = opts.cwd ?? process.cwd();
2326
- const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
2327
- let root;
2328
- if (controlRoot) {
2329
- if (!isDirectory2(controlRoot)) {
2330
- throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
2331
- }
2332
- root = realpathSync3(controlRoot);
2333
- } else {
2334
- const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
2335
- root = realpathSync3(topLevel ?? cwd);
2336
- }
2337
- if (!controlRoot && isLinkedWorktree(root)) {
2338
- throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
2339
- ` + ` Refusing to create a second SDD tree under the feature checkout.
2340
- ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
2341
- ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
2342
- }
2343
- const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
2344
- let harnessDir;
2345
- if (harnessOverride) {
2346
- harnessDir = resolve7(root, harnessOverride);
2347
- } else {
2348
- const rc = findMstarc(root, root);
2349
- const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
2350
- if (rcHarnessDir) {
2351
- harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2352
- } else {
2353
- const probed = probeHarnessWithStatus(root);
2354
- if (probed) {
2355
- harnessDir = probed;
2356
- } else if (isDirectory2(join7(root, ".mstar"))) {
2357
- harnessDir = join7(root, ".mstar");
2358
- } else if (isDirectory2(join7(root, ".agents"))) {
2359
- harnessDir = join7(root, ".agents");
2360
- } else {
2361
- harnessDir = join7(root, ".mstar");
2362
- }
2363
- }
2364
- }
2365
- const sddDir = resolveSddDir(harnessDir, planId);
2366
- mkdirSync5(sddDir, { recursive: true });
2367
- writeFileSync3(join7(sddDir, ".gitignore"), `*
2368
- `);
2369
- return realpathSync3(sddDir);
2370
- }
2371
- function taskBrief(planFile, taskN, outFile, opts = {}) {
2372
- if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
2373
- throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
2374
- }
2375
- let content;
2376
- try {
2377
- content = readFileSync5(planFile, "utf8");
2378
- } catch {
2379
- throw new SddScriptError(`no such plan file: ${planFile}`, 2);
2380
- }
2381
- let out;
2382
- if (outFile) {
2383
- out = outFile;
2384
- } else {
2385
- const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2386
- if (!sddDir) {
2387
- throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2388
- }
2389
- mkdirSync5(sddDir, { recursive: true });
2390
- out = join7(sddDir, `task-${taskN}-brief.md`);
2391
- }
2392
- const records = content.endsWith(`
2393
- `) ? content.split(`
2394
- `).slice(0, -1) : content.split(`
2395
- `);
2396
- const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
2397
- const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
2398
- let infence = false;
2399
- let intask = false;
2400
- const printed = [];
2401
- for (const line of records) {
2402
- if (/^```/.test(line))
2403
- infence = !infence;
2404
- if (!infence && headingRe.test(line))
2405
- intask = targetRe.test(line);
2406
- if (intask)
2407
- printed.push(line);
2408
- }
2409
- const output = printed.length > 0 ? `${printed.join(`
2410
- `)}
2411
- ` : "";
2412
- writeFileSync3(out, output);
2413
- if (printed.length === 0) {
2414
- throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
2415
- }
2416
- return out;
2417
- }
2418
- var COMPASS_STATUSES = ["active", "locked", "completed"];
2419
- var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
2420
- var PLAN_STATUS_DONE = "Done";
2421
- function typeName(value) {
2422
- if (value === null)
2423
- return "null";
2424
- if (Array.isArray(value))
2425
- return "array";
2426
- return typeof value;
2427
- }
2428
- function validateCompassShape(doc) {
2429
- const issues = [];
2430
- const expectString = (key, opts = {}) => {
2431
- const value = doc[key];
2432
- if (typeof value !== "string") {
2433
- issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
2434
- return;
2435
- }
2436
- if (opts.min !== undefined && value.length < opts.min) {
2437
- issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
2438
- return;
2439
- }
2440
- if (opts.regex !== undefined && !opts.regex.test(value)) {
2441
- issues.push({ path: [key], message: `string must match ${opts.regex}` });
2442
- }
2443
- };
2444
- expectString("iteration_id", { min: 1 });
2445
- expectString("start_date", { regex: DATE_RE2 });
2446
- const status = doc.status;
2447
- if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
2448
- issues.push({
2449
- path: ["status"],
2450
- message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
2451
- });
2452
- }
2453
- expectString("iteration_base_branch", { min: 1 });
2454
- expectString("target_branch", { min: 1 });
2455
- const plans = doc.plans;
2456
- if (plans !== undefined) {
2457
- if (!Array.isArray(plans)) {
2458
- issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
2459
- } else {
2460
- plans.forEach((entry, index) => {
2461
- if (typeof entry !== "string") {
2462
- issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
2463
- } else if (entry.length < 1) {
2464
- issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
2465
- }
2466
- });
2467
- }
2468
- }
2469
- const end_date = doc.end_date;
2470
- if (end_date !== undefined) {
2471
- if (typeof end_date !== "string") {
2472
- issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
2473
- } else if (!DATE_RE2.test(end_date)) {
2474
- issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
2475
- }
2476
- }
2477
- if (issues.length > 0)
2478
- return { ok: false, issues };
2479
- return {
2480
- ok: true,
2481
- data: {
2482
- iteration_id: doc.iteration_id,
2483
- start_date: doc.start_date,
2484
- status,
2485
- iteration_base_branch: doc.iteration_base_branch,
2486
- target_branch: doc.target_branch,
2487
- ...plans !== undefined ? { plans } : {},
2488
- ...end_date !== undefined ? { end_date } : {}
2489
- }
2490
- };
2491
- }
2492
- function violation6(severity, code, message, fix) {
2493
- return { ok: false, severity, code, message, fix };
2494
- }
2495
- function isPlainObject4(value) {
2496
- return typeof value === "object" && value !== null && !Array.isArray(value);
2497
- }
2498
- function validateCompassFrontmatter(doc) {
2499
- if (!isPlainObject4(doc)) {
2500
- return {
2501
- ok: false,
2502
- violations: [
2503
- violation6("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
2504
- ]
2505
- };
2506
- }
2507
- const parsed = validateCompassShape(doc);
2508
- if (!parsed.ok) {
2509
- return {
2510
- ok: false,
2511
- violations: parsed.issues.map((issue) => {
2512
- const field = issue.path.join(".") || "(root)";
2513
- return violation6("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration §1.3)`);
2514
- })
2515
- };
2516
- }
2517
- const violations = [];
2518
- const { status, end_date } = parsed.data;
2519
- if (status === "completed" && end_date === undefined) {
2520
- violations.push(violation6("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
2521
- }
2522
- if (status !== "completed" && end_date !== undefined) {
2523
- violations.push(violation6("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
2524
- }
2525
- return { ok: violations.length === 0, violations };
2526
- }
2527
- function registeredPlanIds(compassDoc) {
2528
- if (!Array.isArray(compassDoc.plans))
2529
- return [];
2530
- return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
2531
- }
2532
- function findPlanRow(snapshotDoc, planId) {
2533
- if (!Array.isArray(snapshotDoc.plans))
2534
- return null;
2535
- for (const row of snapshotDoc.plans) {
2536
- if (!isPlainObject4(row))
2537
- continue;
2538
- const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
2539
- if (rowId === planId)
2540
- return row;
2541
- }
2542
- return null;
2543
- }
2544
- function entryPlansAllDone(snapshotDoc, registered) {
2545
- const violations = [];
2546
- if (registered.length === 0) {
2547
- violations.push(violation6("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
2548
- return violations;
2549
- }
2550
- for (const planId of registered) {
2551
- const row = findPlanRow(snapshotDoc, planId);
2552
- if (row === null) {
2553
- violations.push(violation6("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
2554
- continue;
2555
- }
2556
- if (row.status !== PLAN_STATUS_DONE) {
2557
- violations.push(violation6("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
2558
- }
2559
- }
2560
- return violations;
2561
- }
2562
- function entryFrontmatterComplete(compassDoc) {
2563
- return validateCompassFrontmatter(compassDoc).violations;
2564
- }
2565
- function exitFrontmatterClosed(compassDoc) {
2566
- const violations = [];
2567
- if (compassDoc.status !== "completed") {
2568
- violations.push(violation6("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
2569
- }
2570
- const endDate = compassDoc.end_date;
2571
- if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
2572
- violations.push(violation6("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
2573
- }
2574
- return violations;
2575
- }
2576
- function exitBranchCheck(opts) {
2577
- const violations = [];
2578
- const { currentBranch, specIntegrationBranch } = opts;
2579
- if (currentBranch === undefined || specIntegrationBranch === undefined) {
2580
- violations.push(violation6("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
2581
- } else if (currentBranch !== specIntegrationBranch) {
2582
- violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
2583
- }
2584
- return violations;
2585
- }
2586
- function exitPrBaseCheck(compassDoc, opts) {
2587
- const violations = [];
2588
- const target = compassDoc.target_branch;
2589
- const { prBaseBranch } = opts;
2590
- if (prBaseBranch === undefined) {
2591
- violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
2592
- } else if (typeof target !== "string" || prBaseBranch !== target) {
2593
- violations.push(violation6("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
2594
- }
2595
- return violations;
2596
- }
2597
- function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
2598
- const registered = registeredPlanIds(compassDoc);
2599
- const entryViolations = [
2600
- ...entryPlansAllDone(snapshotDoc, registered),
2601
- ...entryFrontmatterComplete(compassDoc)
2602
- ];
2603
- const exitViolations = [
2604
- ...exitFrontmatterClosed(compassDoc),
2605
- ...exitBranchCheck(opts),
2606
- ...exitPrBaseCheck(compassDoc, opts)
2607
- ];
2608
- const allPlansDone = registered.length > 0 && registered.every((planId) => {
2609
- const row = findPlanRow(snapshotDoc, planId);
2610
- return row !== null && row.status === PLAN_STATUS_DONE;
2611
- });
2612
- const entry = { ok: entryViolations.length === 0, violations: entryViolations };
2613
- const exit = { ok: exitViolations.length === 0, violations: exitViolations };
2614
- let transition;
2615
- if (!allPlansDone)
2616
- transition = "phase-2-execute";
2617
- else if (entry.ok && exit.ok)
2618
- transition = "phase-4-pr-delivery";
2619
- else
2620
- transition = "phase-3-close";
2621
- const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
2622
- return {
2623
- transition,
2624
- allPlansDone,
2625
- entry,
2626
- exit,
2627
- ok: gateBlocking.length === 0,
2628
- violations: gateBlocking
2629
- };
2630
- }
2631
- function parseCompassFrontmatter(filePath) {
2632
- return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
2633
- }
2634
- function parseCompassFrontmatterText(content, filePath) {
2635
- const lines = content.split(/\r?\n/);
2636
- if (lines[0]?.trim() !== "---") {
2637
- throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
2638
- }
2639
- const end = lines.indexOf("---", 1);
2640
- if (end === -1) {
2641
- throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
2642
- }
2643
- const doc = {};
2644
- let listKey = null;
2645
- for (let i = 1;i < end; i += 1) {
2646
- const line = lines[i] ?? "";
2647
- if (!line.trim() || line.trim().startsWith("#"))
2648
- continue;
2649
- if (listKey !== null && /^\s*-\s+/.test(line)) {
2650
- const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
2651
- if (!Array.isArray(doc[listKey]))
2652
- doc[listKey] = [];
2653
- doc[listKey].push(item);
2654
- continue;
2655
- }
2656
- listKey = null;
2657
- const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
2658
- if (!kv) {
2659
- throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
2660
- }
2661
- const value = kv[2].trim();
2662
- doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
2663
- listKey = value === "" ? kv[1] : null;
2664
2573
  }
2665
- return doc;
2574
+ return gate(violations);
2666
2575
  }
2667
- function parseFlowArray(raw, filePath) {
2668
- const inner = raw.slice(1, -1);
2669
- if (/[[\]]/.test(inner)) {
2670
- throw new Error(`nested flow-style array in ${filePath}: ${JSON.stringify(raw)} — only flat scalar items are supported (e.g. [a, b])`);
2576
+ function l2PreDispatchCheck(input, opts = {}) {
2577
+ const violations = [];
2578
+ const tracks = input.tracks ?? [];
2579
+ const seenPaths = new Set;
2580
+ if (tracks.length < 1) {
2581
+ violations.push(violation7("high", "worktree.l2.no-tracks", "no parallel writable tracks — the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
2671
2582
  }
2672
- let quote = null;
2673
- for (const ch of inner) {
2674
- if (ch === '"' || ch === "'") {
2675
- if (quote === null)
2676
- quote = ch;
2677
- else if (quote === ch)
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)`);
2583
+ tracks.forEach((track, index) => {
2584
+ if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
2585
+ violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2586
+ return;
2587
+ }
2588
+ if (!isAbsolute4(track.worktreePath)) {
2589
+ violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
2590
+ return;
2591
+ }
2592
+ const normalized = resolve6(track.worktreePath);
2593
+ if (seenPaths.has(normalized)) {
2594
+ violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
2595
+ return;
2596
+ }
2597
+ seenPaths.add(normalized);
2598
+ if (!existsSync6(track.worktreePath)) {
2599
+ violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
2600
+ return;
2601
+ }
2602
+ const probe = probeBranch(track.worktreePath, opts);
2603
+ if ("error" in probe) {
2604
+ violations.push(violation7("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
2605
+ } else if (probe.branch !== track.workingBranch) {
2606
+ violations.push(violation7("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
2681
2607
  }
2608
+ });
2609
+ return gate(violations);
2610
+ }
2611
+ class SddScriptError extends Error {
2612
+ exitCode;
2613
+ constructor(message, exitCode) {
2614
+ super(message);
2615
+ this.name = "SddScriptError";
2616
+ this.exitCode = exitCode;
2682
2617
  }
2683
- if (quote !== null) {
2684
- throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
2618
+ }
2619
+ function isDirectory2(dir) {
2620
+ try {
2621
+ return statSync4(dir).isDirectory();
2622
+ } catch {
2623
+ return false;
2685
2624
  }
2686
- const items = [];
2687
- for (const part of inner.split(",")) {
2688
- const item = part.trim().replace(/^["']|["']$/g, "");
2689
- if (item === "")
2690
- continue;
2691
- items.push(item);
2625
+ }
2626
+ function isFile2(file) {
2627
+ try {
2628
+ return statSync4(file).isFile();
2629
+ } catch {
2630
+ return false;
2692
2631
  }
2693
- return items;
2694
2632
  }
2695
- var PROJECT_ROADMAP_FILE = "roadmap.md";
2696
- var PROJECT_REGISTER_FILE = "residuals.json";
2697
- var _DEFAULT_PROJECT = "_default";
2698
- var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
2699
- function isPlainObject5(value) {
2700
- return typeof value === "object" && value !== null && !Array.isArray(value);
2633
+ var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
2634
+ function gitOut(cwd, args) {
2635
+ try {
2636
+ return execFileSync3("git", args, {
2637
+ cwd,
2638
+ encoding: "utf8",
2639
+ stdio: ["ignore", "pipe", "pipe"],
2640
+ maxBuffer: GIT_CAPTURE_MAX_BYTES
2641
+ }).trim();
2642
+ } catch {
2643
+ return null;
2644
+ }
2701
2645
  }
2702
- function violation7(severity, code, message, fix) {
2703
- return { ok: false, severity, code, message, fix };
2646
+ function probeHarnessWithStatus(root) {
2647
+ if (isFile2(join9(root, ".mstar", "status.json")))
2648
+ return join9(root, ".mstar");
2649
+ if (isFile2(join9(root, ".agents", "status.json")))
2650
+ return join9(root, ".agents");
2651
+ if (hasWorkflowSnapshot(join9(root, ".mstar")))
2652
+ return join9(root, ".mstar");
2653
+ if (hasWorkflowSnapshot(join9(root, ".agents")))
2654
+ return join9(root, ".agents");
2655
+ return null;
2704
2656
  }
2705
- function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
2706
- if (value === undefined) {
2707
- violations.push(violation7("high", missingCode, `missing required field: ${field}`));
2708
- } else if (typeof value !== "string" || value.trim() === "") {
2709
- violations.push(violation7("medium", invalidCode, `${field} must be a non-empty string`));
2657
+ function hasWorkflowSnapshot(harnessDir) {
2658
+ let workflowsDir;
2659
+ try {
2660
+ workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2661
+ } catch {
2662
+ workflowsDir = join9(harnessDir, "workflows");
2663
+ }
2664
+ if (!isDirectory2(workflowsDir))
2665
+ return false;
2666
+ try {
2667
+ for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
2668
+ if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
2669
+ return true;
2670
+ }
2671
+ } catch {
2672
+ return false;
2710
2673
  }
2674
+ return false;
2711
2675
  }
2712
- function validateProjectRegister(doc) {
2713
- const violations = [];
2714
- if (!isPlainObject5(doc)) {
2715
- return {
2716
- ok: false,
2717
- violations: [violation7("high", "project.register.invalid", "project register must be an object")]
2718
- };
2676
+ function isLinkedWorktree(root) {
2677
+ const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
2678
+ const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2679
+ if (gitDirRaw === null || commonRaw === null)
2680
+ return false;
2681
+ const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
2682
+ const common = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
2683
+ if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2684
+ return true;
2685
+ try {
2686
+ const gdParent = realpathSync3(dirname6(gitDir));
2687
+ const cmAbs = realpathSync3(common);
2688
+ return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2689
+ } catch {
2690
+ return false;
2719
2691
  }
2720
- if (doc.entries === undefined) {
2721
- violations.push(violation7("high", "project.register.missing-entries", "missing required field: entries"));
2722
- } else if (!isPlainObject5(doc.entries)) {
2723
- violations.push(violation7("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
2692
+ }
2693
+ function sddWorkspace(planId, opts = {}) {
2694
+ if (!planId) {
2695
+ throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
2696
+ ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
2697
+ }
2698
+ const cwd = opts.cwd ?? process.cwd();
2699
+ const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
2700
+ let root;
2701
+ if (controlRoot) {
2702
+ if (!isDirectory2(controlRoot)) {
2703
+ throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
2704
+ }
2705
+ root = realpathSync3(controlRoot);
2724
2706
  } else {
2725
- for (const [key, entries] of Object.entries(doc.entries)) {
2726
- if (key.trim() === "") {
2727
- violations.push(violation7("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
2728
- }
2729
- if (!Array.isArray(entries)) {
2730
- violations.push(violation7("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)`));
2731
- continue;
2732
- }
2733
- for (const entry of entries) {
2734
- violations.push(...validateResidual(entry).violations);
2735
- if (!isPlainObject5(entry))
2736
- continue;
2737
- validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
2738
- if (entry.registered_at === undefined) {
2739
- violations.push(violation7("high", "project.register.missing-registered-at", "missing required field: registered_at"));
2740
- } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
2741
- violations.push(violation7("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
2742
- }
2743
- if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
2744
- violations.push(violation7("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
2745
- }
2746
- if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
2747
- violations.push(violation7("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`));
2748
- }
2707
+ const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
2708
+ root = realpathSync3(topLevel ?? cwd);
2709
+ }
2710
+ if (!controlRoot && isLinkedWorktree(root)) {
2711
+ throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
2712
+ ` + ` Refusing to create a second SDD tree under the feature checkout.
2713
+ ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
2714
+ ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
2715
+ }
2716
+ const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
2717
+ let harnessDir;
2718
+ if (harnessOverride) {
2719
+ harnessDir = resolve7(root, harnessOverride);
2720
+ } else {
2721
+ const rc = findMstarc(root, root);
2722
+ const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
2723
+ if (rcHarnessDir) {
2724
+ harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2725
+ } else {
2726
+ const probed = probeHarnessWithStatus(root);
2727
+ if (probed) {
2728
+ harnessDir = probed;
2729
+ } else if (isDirectory2(join9(root, ".mstar"))) {
2730
+ harnessDir = join9(root, ".mstar");
2731
+ } else if (isDirectory2(join9(root, ".agents"))) {
2732
+ harnessDir = join9(root, ".agents");
2733
+ } else {
2734
+ harnessDir = join9(root, ".mstar");
2749
2735
  }
2750
2736
  }
2751
2737
  }
2752
- return { ok: violations.length === 0, violations };
2738
+ const sddDir = resolveSddDir(harnessDir, planId);
2739
+ mkdirSync5(sddDir, { recursive: true });
2740
+ writeFileSync4(join9(sddDir, ".gitignore"), `*
2741
+ `);
2742
+ return realpathSync3(sddDir);
2753
2743
  }
2754
- function findingsCleanupGate(register, planId, opts) {
2755
- const mode = opts?.mode ?? "allow-residual";
2756
- const violations = [];
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
- if (entries.length === 0) {
2766
- return { ok: true, violations };
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
- for (const entry of entries) {
2769
- if (!isOpenResidual(entry))
2770
- continue;
2771
- const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
2772
- const label = `R#${id}`;
2773
- if (mode === "zero-residual") {
2774
- if (entry.severity === "nit") {
2775
- violations.push(violation7("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
2776
- } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
2777
- violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
2778
- } else if (entry.decision === "defer") {
2779
- if (typeof entry.target !== "string" || entry.target.trim() === "") {
2780
- violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
2781
- }
2782
- } else {
2783
- violations.push(violation7("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
2784
- }
2785
- } else if (normalizeSeverity(entry.severity) === "critical") {
2786
- violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
2754
+ let out;
2755
+ if (outFile) {
2756
+ out = outFile;
2757
+ } else {
2758
+ const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2759
+ if (!sddDir) {
2760
+ throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2787
2761
  }
2762
+ mkdirSync5(sddDir, { recursive: true });
2763
+ out = join9(sddDir, `task-${taskN}-brief.md`);
2788
2764
  }
2789
- return { ok: violations.length === 0, violations };
2765
+ const records = content.endsWith(`
2766
+ `) ? content.split(`
2767
+ `).slice(0, -1) : content.split(`
2768
+ `);
2769
+ const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
2770
+ const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
2771
+ let infence = false;
2772
+ let intask = false;
2773
+ const printed = [];
2774
+ for (const line of records) {
2775
+ if (/^```/.test(line))
2776
+ infence = !infence;
2777
+ if (!infence && headingRe.test(line))
2778
+ intask = targetRe.test(line);
2779
+ if (intask)
2780
+ printed.push(line);
2781
+ }
2782
+ const output = printed.length > 0 ? `${printed.join(`
2783
+ `)}
2784
+ ` : "";
2785
+ writeFileSync4(out, output);
2786
+ if (printed.length === 0) {
2787
+ throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
2788
+ }
2789
+ return out;
2790
2790
  }
2791
2791
  function violation8(severity, code, message, fix) {
2792
2792
  return { ok: false, severity, code, message, fix };
@@ -3984,7 +3984,7 @@ class DshMstar extends Service {
3984
3984
 
3985
3985
  // src/gates/_shared.ts
3986
3986
  var import_schemastery = __toESM(require_lib2(), 1);
3987
- import { existsSync as existsSync6 } from "node:fs";
3987
+ import { existsSync as existsSync4 } from "node:fs";
3988
3988
  import { resolve as resolve8 } from "node:path";
3989
3989
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3990
3990
  var STATUS_FILE = "status.json";
@@ -4039,7 +4039,7 @@ function resolvePackagedSkillsDir(fileUrl) {
4039
4039
  for (const rel of ["../harness-skills", "../../harness-skills"]) {
4040
4040
  try {
4041
4041
  const dir = fileURLToPath2(new URL(rel, fileUrl));
4042
- if (existsSync6(dir))
4042
+ if (existsSync4(dir))
4043
4043
  return dir;
4044
4044
  } catch {}
4045
4045
  }
@@ -4058,7 +4058,7 @@ function resolvePackagedAgentsDir(fileUrl) {
4058
4058
  for (const rel of ["../harness-agents", "../../harness-agents"]) {
4059
4059
  try {
4060
4060
  const dir = fileURLToPath2(new URL(rel, fileUrl));
4061
- if (existsSync6(dir))
4061
+ if (existsSync4(dir))
4062
4062
  return dir;
4063
4063
  } catch {}
4064
4064
  }
@@ -4126,27 +4126,27 @@ function skillLocalConfig(config) {
4126
4126
  }
4127
4127
 
4128
4128
  // src/gates/catalog.ts
4129
- import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "node:fs";
4130
- import { join as join11 } from "node:path";
4129
+ import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "node:fs";
4130
+ import { join as join10 } from "node:path";
4131
4131
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
4132
4132
 
4133
4133
  // src/gates/agent-flow.ts
4134
- import { appendFileSync, closeSync, existsSync as existsSync13, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync7, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4135
- import { join as join10 } from "node:path";
4134
+ import { appendFileSync, closeSync, existsSync as existsSync12, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4135
+ import { join as join7 } from "node:path";
4136
4136
 
4137
4137
  // src/gates/dispatch.ts
4138
- import { existsSync as existsSync12 } from "node:fs";
4139
- import { basename as basename4, join as join8, resolve as resolve11 } from "node:path";
4138
+ import { existsSync as existsSync10 } from "node:fs";
4139
+ import { basename as basename4, join as join5, resolve as resolve11 } from "node:path";
4140
4140
 
4141
4141
  // src/gates/workflow-selection.ts
4142
- import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
4143
- import { dirname as dirname7, join as join5, relative as relative3 } from "node:path";
4142
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4143
+ import { dirname as dirname7, join as join3, relative as relative3 } from "node:path";
4144
4144
  var TERMINAL_STATUS_CACHE_MAX = 64;
4145
4145
  var terminalStatusCache = new Map;
4146
4146
  function terminalStatusOf(snapshotPath) {
4147
4147
  let mtimeMs;
4148
4148
  try {
4149
- mtimeMs = statSync3(snapshotPath).mtimeMs;
4149
+ mtimeMs = statSync2(snapshotPath).mtimeMs;
4150
4150
  } catch {
4151
4151
  terminalStatusCache.delete(snapshotPath);
4152
4152
  return;
@@ -4173,8 +4173,8 @@ function terminalStatusOf(snapshotPath) {
4173
4173
  return verdict;
4174
4174
  }
4175
4175
  function resolveActiveWorkflow(harnessDir) {
4176
- const statusPath = join5(harnessDir, STATUS_FILE);
4177
- if (!existsSync10(statusPath)) {
4176
+ const statusPath = join3(harnessDir, STATUS_FILE);
4177
+ if (!existsSync7(statusPath)) {
4178
4178
  return {
4179
4179
  kind: "error",
4180
4180
  code: "status.missing",
@@ -4232,7 +4232,7 @@ function resolveReadWorkflow(harnessDir) {
4232
4232
  if (active.kind === "error" && active.code !== "workflow.selection.no-active")
4233
4233
  return active;
4234
4234
  const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
4235
- if (!existsSync10(workflowsDir)) {
4235
+ if (!existsSync7(workflowsDir)) {
4236
4236
  return {
4237
4237
  kind: "error",
4238
4238
  code: "workflow.selection.no-snapshot",
@@ -4241,7 +4241,7 @@ function resolveReadWorkflow(harnessDir) {
4241
4241
  }
4242
4242
  let entries;
4243
4243
  try {
4244
- entries = readdirSync5(workflowsDir, { withFileTypes: true });
4244
+ entries = readdirSync3(workflowsDir, { withFileTypes: true });
4245
4245
  } catch {
4246
4246
  return {
4247
4247
  kind: "error",
@@ -4253,8 +4253,8 @@ function resolveReadWorkflow(harnessDir) {
4253
4253
  for (const entry of entries) {
4254
4254
  if (!entry.isDirectory())
4255
4255
  continue;
4256
- const snapshotPath = join5(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
4257
- if (!existsSync10(snapshotPath)) {
4256
+ const snapshotPath = join3(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
4257
+ if (!existsSync7(snapshotPath)) {
4258
4258
  terminalStatusCache.delete(snapshotPath);
4259
4259
  continue;
4260
4260
  }
@@ -4262,11 +4262,11 @@ function resolveReadWorkflow(harnessDir) {
4262
4262
  if (verdict === undefined || !verdict.terminal)
4263
4263
  continue;
4264
4264
  if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
4265
- best = { workflowId: entry.name, dir: join5(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
4265
+ best = { workflowId: entry.name, dir: join3(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
4266
4266
  }
4267
4267
  }
4268
4268
  for (const cachedPath of terminalStatusCache.keys()) {
4269
- if (!existsSync10(join5(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
4269
+ if (!existsSync7(join3(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
4270
4270
  terminalStatusCache.delete(cachedPath);
4271
4271
  }
4272
4272
  }
@@ -4459,12 +4459,12 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4459
4459
  if (!sdd)
4460
4460
  return [];
4461
4461
  if (selection.kind === "error" && selection.code === "status.unreadable") {
4462
- return [leaseViolation("lease.dispatch.unreadable", `cannot read ${join8(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)")];
4462
+ return [leaseViolation("lease.dispatch.unreadable", `cannot read ${join5(harnessDir, STATUS_FILE)}: the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid status.json (the status gate refuses invalid writes)")];
4463
4463
  }
4464
- return [leaseViolation("lease.dispatch.unverifiable", `${join8(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)")];
4464
+ return [leaseViolation("lease.dispatch.unverifiable", `${join5(harnessDir, STATUS_FILE)}: ${selection.kind === "error" ? selection.message : "no active workflow"} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid v2 status.json registering an active workflow (first implement dispatch requires a plan row + lease)")];
4465
4465
  }
4466
- const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4467
- if (!existsSync12(snapshotPath)) {
4466
+ const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4467
+ if (!existsSync10(snapshotPath)) {
4468
4468
  if (!sdd)
4469
4469
  return [];
4470
4470
  return [leaseViolation("lease.dispatch.unverifiable", `${snapshotPath} is missing — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid workflow snapshot registering the plan row (first implement dispatch requires a plan row)")];
@@ -4538,7 +4538,7 @@ function worktreeL2Violations(header) {
4538
4538
  return violations;
4539
4539
  }
4540
4540
  function activeSnapshotRows(harnessDir) {
4541
- if (!existsSync12(join8(harnessDir, STATUS_FILE)))
4541
+ if (!existsSync10(join5(harnessDir, STATUS_FILE)))
4542
4542
  return { rows: null, controlWorktreePath: undefined, unreadable: false };
4543
4543
  const selection = resolveActiveWorkflow(harnessDir);
4544
4544
  if (selection.kind !== "active") {
@@ -4548,8 +4548,8 @@ function activeSnapshotRows(harnessDir) {
4548
4548
  unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
4549
4549
  };
4550
4550
  }
4551
- const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4552
- if (!existsSync12(snapshotPath))
4551
+ const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4552
+ if (!existsSync10(snapshotPath))
4553
4553
  return { rows: null, controlWorktreePath: undefined, unreadable: true };
4554
4554
  let doc;
4555
4555
  try {
@@ -4639,7 +4639,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec) {
4639
4639
  }
4640
4640
  const pb = writableFanOutUncovered(harnessDir);
4641
4641
  if (pb.unreadable) {
4642
- const statusLabel = harnessDir === null ? STATUS_FILE : join8(harnessDir, STATUS_FILE);
4642
+ const statusLabel = harnessDir === null ? STATUS_FILE : join5(harnessDir, STATUS_FILE);
4643
4643
  ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
4644
4644
  }
4645
4645
  const verdict = workflowPolicy(config, adapter.workflowAskCache, {
@@ -4829,7 +4829,7 @@ function callPairingKey(exec) {
4829
4829
  function resolveAgentFlowWriteDir(harnessDir) {
4830
4830
  const selection = resolveActiveWorkflow(harnessDir);
4831
4831
  if (selection.kind === "active")
4832
- return join10(harnessDir, selection.dir);
4832
+ return join7(harnessDir, selection.dir);
4833
4833
  const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
4834
4834
  if (!noActiveWarned) {
4835
4835
  noActiveWarned = true;
@@ -4846,7 +4846,7 @@ function sleepSync(ms) {
4846
4846
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
4847
4847
  }
4848
4848
  function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4849
- const lockDir = join10(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4849
+ const lockDir = join7(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4850
4850
  if (heldWorkflowLocks.has(lockDir)) {
4851
4851
  throw new Error(`${lockDir} is already held by this process — withWorkflowDirLock is not reentrant; a nested acquisition on the same workflow dir is a bug`);
4852
4852
  }
@@ -4856,7 +4856,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4856
4856
  let acquired = null;
4857
4857
  for (;; ) {
4858
4858
  try {
4859
- mkdirSync3(lockDir);
4859
+ mkdirSync2(lockDir);
4860
4860
  const st = statSync5(lockDir);
4861
4861
  acquired = { dev: st.dev, ino: st.ino };
4862
4862
  break;
@@ -4872,7 +4872,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4872
4872
  heldWorkflowLocks.add(lockDir);
4873
4873
  try {
4874
4874
  try {
4875
- writeFileSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4875
+ writeFileSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4876
4876
  } catch {}
4877
4877
  return fn();
4878
4878
  } finally {
@@ -4881,7 +4881,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4881
4881
  const current = statSync5(lockDir);
4882
4882
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
4883
4883
  try {
4884
- unlinkSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4884
+ unlinkSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4885
4885
  } catch {}
4886
4886
  rmdirSync(lockDir);
4887
4887
  }
@@ -4889,13 +4889,13 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4889
4889
  }
4890
4890
  }
4891
4891
  function appendEvent(workflowDir, event) {
4892
- const file = join10(workflowDir, AGENT_FLOW_FILE);
4892
+ const file = join7(workflowDir, AGENT_FLOW_FILE);
4893
4893
  withWorkflowDirLock(workflowDir, () => {
4894
4894
  appendFileSync(file, `${JSON.stringify(event)}
4895
4895
  `);
4896
4896
  if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
4897
4897
  return;
4898
- const content = readFileSync7(file, "utf8");
4898
+ const content = readFileSync5(file, "utf8");
4899
4899
  const lines = content.replace(/\n$/, "").split(`
4900
4900
  `);
4901
4901
  if (lines.length > AGENT_FLOW_MAX_EVENTS) {
@@ -5246,7 +5246,7 @@ function summaryOf(events) {
5246
5246
  function ledgerContent(file, n) {
5247
5247
  const size = statSync5(file).size;
5248
5248
  if (size <= AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES) {
5249
- return readFileSync7(file, "utf8");
5249
+ return readFileSync5(file, "utf8");
5250
5250
  }
5251
5251
  let windowBytes = AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES;
5252
5252
  const fd = openSync(file, "r");
@@ -5278,8 +5278,8 @@ function ledgerContent(file, n) {
5278
5278
  }
5279
5279
  }
5280
5280
  function readAgentFlow(workflowDir, limit) {
5281
- const file = join10(workflowDir, AGENT_FLOW_FILE);
5282
- if (!existsSync13(file)) {
5281
+ const file = join7(workflowDir, AGENT_FLOW_FILE);
5282
+ if (!existsSync12(file)) {
5283
5283
  return { events: [], summary: [] };
5284
5284
  }
5285
5285
  const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
@@ -5404,7 +5404,7 @@ function residualSeverityIndex(severity) {
5404
5404
  function pluginVersion() {
5405
5405
  for (const rel of ["../package.json", "../../package.json"]) {
5406
5406
  try {
5407
- const pkg = JSON.parse(readFileSync9(new URL(rel, import.meta.url), "utf8"));
5407
+ const pkg = JSON.parse(readFileSync8(new URL(rel, import.meta.url), "utf8"));
5408
5408
  if (typeof pkg.version === "string" && pkg.version !== "")
5409
5409
  return pkg.version;
5410
5410
  } catch {}
@@ -5533,8 +5533,8 @@ function hhmm(ts) {
5533
5533
  function harnessStateSource(harnessDir) {
5534
5534
  if (harnessDir === null)
5535
5535
  return null;
5536
- const statusPath = join11(harnessDir, STATUS_FILE);
5537
- if (!existsSync14(statusPath))
5536
+ const statusPath = join10(harnessDir, STATUS_FILE);
5537
+ if (!existsSync13(statusPath))
5538
5538
  return null;
5539
5539
  try {
5540
5540
  const selection = resolveReadWorkflow(harnessDir);
@@ -5553,9 +5553,9 @@ function harnessStateSource(harnessDir) {
5553
5553
  if (selection.kind === "error") {
5554
5554
  return selectionErrorState(selection, rollup, harnessDir, compass);
5555
5555
  }
5556
- const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5556
+ const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5557
5557
  let snapshot;
5558
- if (!existsSync14(snapshotPath)) {
5558
+ if (!existsSync13(snapshotPath)) {
5559
5559
  return selectionErrorState({
5560
5560
  kind: "error",
5561
5561
  code: "workflow.selection.snapshot-unreadable",
@@ -5617,7 +5617,7 @@ function harnessStateSource(harnessDir) {
5617
5617
  leases,
5618
5618
  knowledge: knowledgeDigest(harnessDir),
5619
5619
  direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
5620
- agentFlow: readAgentFlow(join11(harnessDir, selection.dir), 50)
5620
+ agentFlow: readAgentFlow(join10(harnessDir, selection.dir), 50)
5621
5621
  };
5622
5622
  } catch {
5623
5623
  return null;
@@ -5647,21 +5647,21 @@ function selectionErrorState(selection, rollup, harnessDir, compass) {
5647
5647
  function projectRollupSource(harnessDir, residuals) {
5648
5648
  const milestones = [];
5649
5649
  const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5650
- if (existsSync14(projectsDir)) {
5650
+ if (existsSync13(projectsDir)) {
5651
5651
  let entries;
5652
5652
  try {
5653
- entries = readdirSync7(projectsDir, { withFileTypes: true });
5653
+ entries = readdirSync6(projectsDir, { withFileTypes: true });
5654
5654
  } catch {
5655
5655
  entries = [];
5656
5656
  }
5657
5657
  for (const entry of entries) {
5658
5658
  if (!entry.isDirectory())
5659
5659
  continue;
5660
- const roadmapPath = join11(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5661
- if (!existsSync14(roadmapPath))
5660
+ const roadmapPath = join10(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5661
+ if (!existsSync13(roadmapPath))
5662
5662
  continue;
5663
5663
  try {
5664
- const doc = parseCompassFrontmatterText(readFileSync9(roadmapPath, "utf8"), roadmapPath);
5664
+ const doc = parseCompassFrontmatterText(readFileSync8(roadmapPath, "utf8"), roadmapPath);
5665
5665
  if (Array.isArray(doc.milestones)) {
5666
5666
  for (const milestone of doc.milestones) {
5667
5667
  if (typeof milestone === "string" && milestone.trim() !== "")
@@ -5723,11 +5723,11 @@ function residualRollup(harnessDir) {
5723
5723
  }
5724
5724
  function projectRegisters(harnessDir) {
5725
5725
  const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5726
- if (!existsSync14(projectsDir))
5726
+ if (!existsSync13(projectsDir))
5727
5727
  return [];
5728
5728
  let entries;
5729
5729
  try {
5730
- entries = readdirSync7(projectsDir, { withFileTypes: true });
5730
+ entries = readdirSync6(projectsDir, { withFileTypes: true });
5731
5731
  } catch {
5732
5732
  return [];
5733
5733
  }
@@ -5735,8 +5735,8 @@ function projectRegisters(harnessDir) {
5735
5735
  for (const entry of entries) {
5736
5736
  if (!entry.isDirectory())
5737
5737
  continue;
5738
- const registerPath = join11(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5739
- if (!existsSync14(registerPath))
5738
+ const registerPath = join10(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5739
+ if (!existsSync13(registerPath))
5740
5740
  continue;
5741
5741
  try {
5742
5742
  registers.push({ projectId: entry.name, doc: readJson(registerPath) });
@@ -5747,13 +5747,13 @@ function projectRegisters(harnessDir) {
5747
5747
  return registers;
5748
5748
  }
5749
5749
  function knowledgeDigest(harnessDir) {
5750
- const indexPath = join11(harnessDir, "knowledge", "README.md");
5751
- if (!existsSync14(indexPath))
5750
+ const indexPath = join10(harnessDir, "knowledge", "README.md");
5751
+ if (!existsSync13(indexPath))
5752
5752
  return null;
5753
5753
  try {
5754
5754
  const categories = new Set;
5755
5755
  let docCount = 0;
5756
- for (const line of readFileSync9(indexPath, "utf8").split(/\r?\n/)) {
5756
+ for (const line of readFileSync8(indexPath, "utf8").split(/\r?\n/)) {
5757
5757
  const row = line.trim().match(/^\|(.+)\|$/);
5758
5758
  if (row === null)
5759
5759
  continue;
@@ -5776,7 +5776,7 @@ function knowledgeDigest(harnessDir) {
5776
5776
  }
5777
5777
  function compassDirection(compassPath) {
5778
5778
  try {
5779
- const content = readFileSync9(compassPath, "utf8");
5779
+ const content = readFileSync8(compassPath, "utf8");
5780
5780
  const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
5781
5781
  if (section === null)
5782
5782
  return null;
@@ -5793,23 +5793,23 @@ function compassDirection(compassPath) {
5793
5793
  }
5794
5794
  function steeringCompassPath(harnessDir) {
5795
5795
  const iterationsDir = resolveIterationDir(harnessDir);
5796
- if (!existsSync14(iterationsDir))
5796
+ if (!existsSync13(iterationsDir))
5797
5797
  return;
5798
5798
  let entries;
5799
5799
  try {
5800
- entries = readdirSync7(iterationsDir, { withFileTypes: true });
5800
+ entries = readdirSync6(iterationsDir, { withFileTypes: true });
5801
5801
  } catch {
5802
5802
  return;
5803
5803
  }
5804
5804
  for (const entry of entries) {
5805
5805
  if (!entry.isDirectory())
5806
5806
  continue;
5807
- const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
5808
- if (!existsSync14(compassPath))
5807
+ const compassPath = join10(iterationsDir, entry.name, "delivery-compass.md");
5808
+ if (!existsSync13(compassPath))
5809
5809
  continue;
5810
5810
  let content;
5811
5811
  try {
5812
- content = readFileSync9(compassPath, "utf8");
5812
+ content = readFileSync8(compassPath, "utf8");
5813
5813
  } catch {
5814
5814
  continue;
5815
5815
  }
@@ -5823,8 +5823,8 @@ function steeringCompassPath(harnessDir) {
5823
5823
  function iterationGateSource(harnessDir) {
5824
5824
  if (harnessDir === null)
5825
5825
  return;
5826
- const statusPath = join11(harnessDir, STATUS_FILE);
5827
- if (!existsSync14(statusPath))
5826
+ const statusPath = join10(harnessDir, STATUS_FILE);
5827
+ if (!existsSync13(statusPath))
5828
5828
  return;
5829
5829
  const compass = steeringCompassPath(harnessDir);
5830
5830
  if (compass === undefined)
@@ -5832,7 +5832,7 @@ function iterationGateSource(harnessDir) {
5832
5832
  const selection = resolveReadWorkflow(harnessDir);
5833
5833
  if (selection.kind === "error")
5834
5834
  return;
5835
- const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5835
+ const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5836
5836
  try {
5837
5837
  const snapshotDoc = readJson(snapshotPath);
5838
5838
  const compassDoc = parseCompassFrontmatter(compass.compassPath);
@@ -5886,7 +5886,7 @@ function agentDigestKey(agent, cwd) {
5886
5886
  }
5887
5887
 
5888
5888
  // src/gates/status.ts
5889
- import { existsSync as existsSync15, readdirSync as readdirSync9 } from "node:fs";
5889
+ import { existsSync as existsSync14, readdirSync as readdirSync9 } from "node:fs";
5890
5890
  import { basename as basename6, join as join14, relative as relative5, resolve as resolve13 } from "node:path";
5891
5891
  var LOGGER_NAME = "mstar/status-gate";
5892
5892
  function harnessDocKindOfTarget(harnessDir, targetPath) {
@@ -5969,7 +5969,7 @@ function projectRegisterDocs(harnessDir) {
5969
5969
  if (!entry.isDirectory())
5970
5970
  continue;
5971
5971
  const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5972
- if (!existsSync15(registerPath))
5972
+ if (!existsSync14(registerPath))
5973
5973
  continue;
5974
5974
  try {
5975
5975
  registers.push({ projectId: entry.name, doc: readJson(registerPath) });
@@ -6042,7 +6042,7 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
6042
6042
  }
6043
6043
 
6044
6044
  // src/gates/skill-lint.ts
6045
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
6045
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
6046
6046
  import { basename as basename7, dirname as dirname9, resolve as resolve14, sep as sep2 } from "node:path";
6047
6047
  var SKILL_LINT_LOGGER = "mstar/skill-lint";
6048
6048
 
@@ -6123,7 +6123,7 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
6123
6123
  if (!isSkillTarget(roots, target))
6124
6124
  return;
6125
6125
  const skillPath = resolve14(target.displayPath);
6126
- if (!existsSync16(skillPath))
6126
+ if (!existsSync15(skillPath))
6127
6127
  return;
6128
6128
  let doc;
6129
6129
  try {
@@ -6174,7 +6174,7 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
6174
6174
  }
6175
6175
 
6176
6176
  // src/gates/seams.ts
6177
- import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
6177
+ import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
6178
6178
  import { basename as basename8, dirname as dirname10, join as join16, resolve as resolve15, sep as sep3 } from "node:path";
6179
6179
 
6180
6180
  // ../engine/dist/audit.js
@@ -6372,7 +6372,7 @@ function validateDesignDoc(doc, path) {
6372
6372
  const violations = [...validateDesignTokenFrontmatter(doc).violations];
6373
6373
  const isDark = basename8(path) === "DESIGN.dark.md";
6374
6374
  const sibling = join16(dirname10(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
6375
- if (existsSync17(sibling)) {
6375
+ if (existsSync16(sibling)) {
6376
6376
  const light = isDark ? readFileSync13(sibling, "utf8") : doc;
6377
6377
  const dark = isDark ? doc : readFileSync13(sibling, "utf8");
6378
6378
  violations.push(...assertLightDarkParity(light, dark).violations);
@@ -6399,7 +6399,7 @@ function validateRolesState(rolesDir, skillsRoot = dirname10(rolesDir)) {
6399
6399
  if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
6400
6400
  continue;
6401
6401
  const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
6402
- if (!existsSync17(skillFile))
6402
+ if (!existsSync16(skillFile))
6403
6403
  continue;
6404
6404
  try {
6405
6405
  skillTexts[entry.name] = readFileSync13(skillFile, "utf8");
@@ -6433,7 +6433,7 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
6433
6433
  if (!isSeamTarget(seam, harnessDir, target))
6434
6434
  return;
6435
6435
  const path = resolve15(target.displayPath);
6436
- if (!existsSync17(path))
6436
+ if (!existsSync16(path))
6437
6437
  return;
6438
6438
  let doc;
6439
6439
  try {
@@ -6503,7 +6503,7 @@ function lintRolesWrite(doc, options) {
6503
6503
  }
6504
6504
 
6505
6505
  // src/gates/tools.ts
6506
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
6506
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
6507
6507
  import { join as join17, resolve as resolve16 } from "node:path";
6508
6508
  import { defineTool } from "@deepseek-ai/dsh-tools";
6509
6509
  var ITERATION_VIOLATION_SCHEMA = {
@@ -6669,9 +6669,9 @@ function registerSddIterationTools(ctx, resolver) {
6669
6669
  presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
6670
6670
  isConcurrencySafe: () => false,
6671
6671
  async execute(args) {
6672
- if (!existsSync18(args.snapshot_path))
6672
+ if (!existsSync17(args.snapshot_path))
6673
6673
  throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
6674
- if (!existsSync18(args.compass_path))
6674
+ if (!existsSync17(args.compass_path))
6675
6675
  throw new Error(`compass file not found: ${args.compass_path}`);
6676
6676
  const snapshotDoc = readJson(args.snapshot_path);
6677
6677
  const compassDoc = parseCompassFrontmatter(args.compass_path);
@@ -6726,7 +6726,7 @@ function registerSeamTools(ctx, resolver) {
6726
6726
  async execute(args) {
6727
6727
  const abs = resolve16(args.dir);
6728
6728
  const lightPath = join17(abs, "DESIGN.md");
6729
- if (!existsSync18(lightPath))
6729
+ if (!existsSync17(lightPath))
6730
6730
  throw new Error(`design file not found: ${lightPath}`);
6731
6731
  const light = readFileSync14(lightPath, "utf8");
6732
6732
  const result = validateDesignDoc(light, lightPath);
@@ -6779,7 +6779,7 @@ function registerSeamTools(ctx, resolver) {
6779
6779
  isConcurrencySafe: () => false,
6780
6780
  async execute(args) {
6781
6781
  const abs = resolve16(args.plan_path);
6782
- if (!existsSync18(abs))
6782
+ if (!existsSync17(abs))
6783
6783
  throw new Error(`plan file not found: ${abs}`);
6784
6784
  const text = readFileSync14(abs, "utf8");
6785
6785
  const result = validateAuditDoc(text, abs);
@@ -6819,7 +6819,7 @@ function registerSeamTools(ctx, resolver) {
6819
6819
  isConcurrencySafe: () => false,
6820
6820
  async execute(args, exec) {
6821
6821
  const abs = resolve16(args.doc_path);
6822
- if (!existsSync18(abs))
6822
+ if (!existsSync17(abs))
6823
6823
  throw new Error(`knowledge doc not found: ${abs}`);
6824
6824
  const text = readFileSync14(abs, "utf8");
6825
6825
  const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
@@ -6864,7 +6864,7 @@ function registerSeamTools(ctx, resolver) {
6864
6864
  isConcurrencySafe: () => false,
6865
6865
  async execute(args) {
6866
6866
  const rolesDir = resolve16(args.roles_dir);
6867
- if (!existsSync18(join17(rolesDir, "SKILL.md")))
6867
+ if (!existsSync17(join17(rolesDir, "SKILL.md")))
6868
6868
  throw new Error(`roles dir not found: ${rolesDir}`);
6869
6869
  const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
6870
6870
  return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
@@ -6874,7 +6874,7 @@ function registerSeamTools(ctx, resolver) {
6874
6874
  }
6875
6875
 
6876
6876
  // src/gates/adapter.ts
6877
- import { existsSync as existsSync19 } from "node:fs";
6877
+ import { existsSync as existsSync18 } from "node:fs";
6878
6878
  import { join as join18 } from "node:path";
6879
6879
  import { Service as Service2 } from "@deepseek-ai/cordis";
6880
6880
  var HOST_LOGGER = "mstar/host-adapter";
@@ -6911,7 +6911,7 @@ class DshHostAdapter extends Service2 {
6911
6911
  this.logSink(level, msg);
6912
6912
  }
6913
6913
  statusGate(path, kind, harnessDir) {
6914
- if (!existsSync19(path))
6914
+ if (!existsSync18(path))
6915
6915
  return { ok: true, violations: [] };
6916
6916
  return validateStatusDoc(path, kind, harnessDir);
6917
6917
  }
@@ -6992,7 +6992,7 @@ class DshHostAdapter extends Service2 {
6992
6992
  }
6993
6993
 
6994
6994
  // src/gates/agent-personas.ts
6995
- import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync6 } from "node:fs";
6995
+ import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync7 } from "node:fs";
6996
6996
  import { join as join19, resolve as resolve17, sep as sep5 } from "node:path";
6997
6997
  var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
6998
6998
  var defaultCache = new Map;
@@ -7040,7 +7040,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
7040
7040
  return;
7041
7041
  let stat;
7042
7042
  try {
7043
- stat = statSync6(file);
7043
+ stat = statSync7(file);
7044
7044
  } catch {
7045
7045
  return;
7046
7046
  }
@@ -7239,7 +7239,7 @@ function log2(level, message) {
7239
7239
  }
7240
7240
 
7241
7241
  // src/gates/workflow-ledger.ts
7242
- import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
7242
+ import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
7243
7243
  import { join as join20 } from "node:path";
7244
7244
  var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
7245
7245
  var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
@@ -7309,7 +7309,7 @@ function saveWatermark(workflowDir, watermark) {
7309
7309
  try {
7310
7310
  const file = join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
7311
7311
  const tmp = `${file}.tmp`;
7312
- writeFileSync4(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
7312
+ writeFileSync5(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
7313
7313
  renameSync3(tmp, file);
7314
7314
  } catch (error) {
7315
7315
  log3("warn", `workflow-ledger watermark write failed for ${workflowDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
@@ -7523,7 +7523,7 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
7523
7523
  }
7524
7524
 
7525
7525
  // src/gates/goal-bridge.ts
7526
- import { existsSync as existsSync20, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
7526
+ import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
7527
7527
  import { join as join21 } from "node:path";
7528
7528
  var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
7529
7529
  var DEFAULT_MAX_GOAL_ROUNDS = 256;
@@ -7552,7 +7552,7 @@ function projectRegisterPointer(harnessDir) {
7552
7552
  if (!entry.isDirectory())
7553
7553
  continue;
7554
7554
  const registerPath = join21(projectsDir, entry.name, PROJECT_REGISTER_FILE);
7555
- if (existsSync20(registerPath))
7555
+ if (existsSync19(registerPath))
7556
7556
  return registerPath;
7557
7557
  }
7558
7558
  } catch {}
@@ -7567,7 +7567,7 @@ function iterationGoalObjective(iterationId) {
7567
7567
  }
7568
7568
  function steeringCompass(harnessDir) {
7569
7569
  const iterationsDir = resolveIterationDir(harnessDir);
7570
- if (!existsSync20(iterationsDir))
7570
+ if (!existsSync19(iterationsDir))
7571
7571
  return;
7572
7572
  let entries;
7573
7573
  try {
@@ -7579,7 +7579,7 @@ function steeringCompass(harnessDir) {
7579
7579
  if (!entry.isDirectory())
7580
7580
  continue;
7581
7581
  const compassPath = join21(iterationsDir, entry.name, "delivery-compass.md");
7582
- if (!existsSync20(compassPath))
7582
+ if (!existsSync19(compassPath))
7583
7583
  continue;
7584
7584
  let content;
7585
7585
  try {
@@ -7759,7 +7759,7 @@ function registerGoalBridge(ctx, resolver, config) {
7759
7759
  }
7760
7760
 
7761
7761
  // src/gates/plan-mode-bridge.ts
7762
- import { existsSync as existsSync21 } from "node:fs";
7762
+ import { existsSync as existsSync20 } from "node:fs";
7763
7763
  import { join as join22 } from "node:path";
7764
7764
  var PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
7765
7765
  var PLAN_STATUS_TODO = "Todo";
@@ -7777,7 +7777,7 @@ function errorMessage5(error) {
7777
7777
  }
7778
7778
  function hasPrepareWindow(harnessDir) {
7779
7779
  const statusPath = join22(harnessDir, STATUS_FILE);
7780
- if (!existsSync21(statusPath))
7780
+ if (!existsSync20(statusPath))
7781
7781
  return false;
7782
7782
  const selection = resolveReadWorkflow(harnessDir);
7783
7783
  if (selection.kind === "error")
@@ -7852,7 +7852,7 @@ function errorMessage6(error) {
7852
7852
  return error instanceof Error ? error.message : String(error);
7853
7853
  }
7854
7854
  function mstarLoadLine(roleId) {
7855
- return `Load mstar-roles (references/${roleId}.md) and the role's Required Skill Dependencies before acting.`;
7855
+ return `Load mstar-roles (references/${roleId}.md) first — identity comes before skills; load topic skills only when the Assignment activates them via its Skill presets field.`;
7856
7856
  }
7857
7857
  async function declareMstarSeeds(service, options) {
7858
7858
  const { agentsDir, log: log6 } = options;
@@ -8236,7 +8236,7 @@ var inject = ["loader"];
8236
8236
  function packagedCommandsDir() {
8237
8237
  try {
8238
8238
  const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
8239
- return existsSync22(dir) ? dir : undefined;
8239
+ return existsSync21(dir) ? dir : undefined;
8240
8240
  } catch {
8241
8241
  return;
8242
8242
  }