@mstar-harness/dsh 3.2.6 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +4 -4
  3. package/README.zh.md +4 -4
  4. package/bundle/README.md +1 -1
  5. package/dist/gates/_shared.d.ts +3 -3
  6. package/dist/index.js +1072 -997
  7. package/harness-commands/amazing-pr-review.md +54 -0
  8. package/harness-skills/mstar-audit/SKILL.md +7 -6
  9. package/harness-skills/mstar-audit/references/audit-playbook.md +19 -1
  10. package/harness-skills/mstar-audit/references/codebase-audit.md +21 -3
  11. package/harness-skills/mstar-audit/references/finding-format.md +5 -1
  12. package/harness-skills/mstar-audit/references/pr-review-seat-evidence.md +28 -0
  13. package/harness-skills/mstar-audit/references/pr-review.md +169 -69
  14. package/harness-skills/mstar-audit/references/security-review.md +219 -0
  15. package/harness-skills/mstar-conventions/SKILL.md +4 -2
  16. package/harness-skills/mstar-conventions/references/harness-bootstrap-and-agents-layering.md +5 -1
  17. package/harness-skills/mstar-harness-core/SKILL.md +1 -1
  18. package/harness-skills/mstar-roles/SKILL.md +17 -13
  19. package/harness-skills/mstar-roles/references/_shared/leaf-executor-core.md +5 -7
  20. package/harness-skills/mstar-roles/references/architect.md +9 -11
  21. package/harness-skills/mstar-roles/references/code-reviewer.md +19 -22
  22. package/harness-skills/mstar-roles/references/frontend-dev.md +10 -11
  23. package/harness-skills/mstar-roles/references/fullstack-dev-shared.md +12 -13
  24. package/harness-skills/mstar-roles/references/ops-engineer.md +10 -12
  25. package/harness-skills/mstar-roles/references/product-manager.md +9 -11
  26. package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +2 -0
  27. package/harness-skills/mstar-roles/references/prompt-engineer.md +11 -14
  28. package/harness-skills/mstar-roles/references/qa-engineer.md +9 -13
  29. package/harness-skills/mstar-roles/references/qc-specialist-shared.md +10 -17
  30. package/harness-skills/mstar-roles/references/writing-specialist.md +8 -9
  31. package/package.json +1 -1
  32. package/harness-commands/pr-deep-review.md +0 -39
package/dist/index.js CHANGED
@@ -1106,7 +1106,7 @@ var require_lib2 = __commonJS((exports, module) => {
1106
1106
  });
1107
1107
 
1108
1108
  // src/index.ts
1109
- import { existsSync as 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,22 +1122,22 @@ import { basename, dirname, join, resolve } from "node:path";
1122
1122
  import { fileURLToPath } from "node:url";
1123
1123
  import { readFileSync as readFileSync2, statSync } from "node:fs";
1124
1124
  import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
1125
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
1125
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
1126
1126
  import { execFileSync } from "node:child_process";
1127
- import { basename as basename2, dirname as 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 resolve6 } from "node:path";
1128
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
1129
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
1131
1130
  import { AsyncLocalStorage } from "node:async_hooks";
1131
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
1132
+ import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
1132
1133
  import { execFileSync as execFileSync2 } from "node:child_process";
1133
- import { existsSync as existsSync4 } from "node:fs";
1134
- import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
1134
+ import { existsSync as existsSync6 } from "node:fs";
1135
+ import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
1135
1136
  import { execFileSync as execFileSync3 } from "node:child_process";
1136
- import { mkdirSync as 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 mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
1138
+ import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve8 } from "node:path";
1139
1139
  import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
1140
- import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep4 } from "node:path";
1140
+ import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve11, sep as sep4 } from "node:path";
1141
1141
  import { existsSync as existsSync9 } from "node:fs";
1142
1142
  import { join as join13 } from "node:path";
1143
1143
  var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
@@ -1276,220 +1276,380 @@ function isAtOrBelow(dir, root) {
1276
1276
  const rel = relative(root, dir);
1277
1277
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1278
1278
  }
1279
- 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,30 +2295,243 @@ 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"],
2161
- timeout
2315
+ function validateProjectRegister(doc) {
2316
+ const violations = [];
2317
+ if (!isPlainObject5(doc)) {
2318
+ return {
2319
+ ok: false,
2320
+ violations: [violation6("high", "project.register.invalid", "project register must be an object")]
2321
+ };
2322
+ }
2323
+ if (doc.entries === undefined) {
2324
+ violations.push(violation6("high", "project.register.missing-entries", "missing required field: entries"));
2325
+ } else if (!isPlainObject5(doc.entries)) {
2326
+ violations.push(violation6("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
2327
+ } else {
2328
+ for (const [key, entries] of Object.entries(doc.entries)) {
2329
+ if (key.trim() === "") {
2330
+ violations.push(violation6("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
2331
+ }
2332
+ if (!Array.isArray(entries)) {
2333
+ violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(key)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
2334
+ continue;
2335
+ }
2336
+ for (const entry of entries) {
2337
+ violations.push(...validateResidual(entry).violations);
2338
+ if (!isPlainObject5(entry))
2339
+ continue;
2340
+ validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
2341
+ if (entry.registered_at === undefined) {
2342
+ violations.push(violation6("high", "project.register.missing-registered-at", "missing required field: registered_at"));
2343
+ } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
2344
+ violations.push(violation6("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
2345
+ }
2346
+ if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
2347
+ violations.push(violation6("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
2348
+ }
2349
+ if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
2350
+ violations.push(violation6("medium", "project.register.mismatched-source-plan", `source_plan ${JSON.stringify(entry.source_plan)} does not match the entries key ${JSON.stringify(key)} — entries are keyed by plan id`));
2351
+ }
2352
+ }
2353
+ }
2354
+ }
2355
+ return { ok: violations.length === 0, violations };
2356
+ }
2357
+ function findingsCleanupGate(register, planId, opts) {
2358
+ const mode = opts?.mode ?? "allow-residual";
2359
+ const violations = [];
2360
+ const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
2361
+ if (entries === undefined) {
2362
+ return { ok: true, violations };
2363
+ }
2364
+ if (!Array.isArray(entries)) {
2365
+ violations.push(violation6("high", "project.register.invalid-entry-list", `entries[${JSON.stringify(planId)}] must be an array of residual entries (one entry per residual; v1 multi-finding semantics)`));
2366
+ return { ok: false, violations };
2367
+ }
2368
+ if (entries.length === 0) {
2369
+ return { ok: true, violations };
2370
+ }
2371
+ for (const entry of entries) {
2372
+ if (!isOpenResidual(entry))
2373
+ continue;
2374
+ const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
2375
+ const label = `R#${id}`;
2376
+ if (mode === "zero-residual") {
2377
+ if (entry.severity === "nit") {
2378
+ violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
2379
+ } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
2380
+ violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
2381
+ } else if (entry.decision === "defer") {
2382
+ if (typeof entry.target !== "string" || entry.target.trim() === "") {
2383
+ violations.push(violation6("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
2384
+ }
2385
+ } else {
2386
+ violations.push(violation6("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
2387
+ }
2388
+ } else if (normalizeSeverity(entry.severity) === "critical") {
2389
+ violations.push(violation6("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
2390
+ }
2391
+ }
2392
+ return { ok: violations.length === 0, violations };
2393
+ }
2394
+ function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
2395
+ const start = resolve6(startDir);
2396
+ const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
2397
+ if (explicit)
2398
+ return resolve6(start, explicit);
2399
+ const boundary = resolve6(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
2400
+ const rc = loadMstarc(start, boundary);
2401
+ if (rc !== null && rc.config.harnessDir)
2402
+ return resolve6(rc.dir, rc.config.harnessDir);
2403
+ let dir = start;
2404
+ for (;; ) {
2405
+ if (!isAtOrBelow2(dir, boundary))
2406
+ return null;
2407
+ for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
2408
+ if (isDirectory(candidate))
2409
+ return candidate;
2410
+ }
2411
+ if (dir === boundary)
2412
+ return null;
2413
+ const parent = dirname5(dir);
2414
+ if (parent === dir)
2415
+ return null;
2416
+ dir = parent;
2417
+ }
2418
+ }
2419
+ function defaultWorkspaceRoot(startDir) {
2420
+ try {
2421
+ const cdup = execFileSync("git", ["rev-parse", "--show-cdup"], {
2422
+ cwd: startDir,
2423
+ encoding: "utf8",
2424
+ stdio: ["ignore", "pipe", "ignore"]
2425
+ }).trim();
2426
+ if (!cdup)
2427
+ return startDir;
2428
+ let boundary = startDir;
2429
+ for (const segment of cdup.split(/[\\/]/)) {
2430
+ if (segment && segment !== ".")
2431
+ boundary = dirname5(boundary);
2432
+ }
2433
+ return resolve6(boundary);
2434
+ } catch {}
2435
+ return startDir;
2436
+ }
2437
+ function isAtOrBelow2(dir, root) {
2438
+ const rel = relative2(root, dir);
2439
+ return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
2440
+ }
2441
+ function mstarcDirOverride(harnessDir, key) {
2442
+ const dir = resolve6(harnessDir);
2443
+ const rc = loadMstarc(dir, dirname5(dir));
2444
+ const declared = rc?.config[key];
2445
+ return declared ? resolve6(rc.dir, declared) : null;
2446
+ }
2447
+ function assertSafePathComponent(value, what) {
2448
+ if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
2449
+ throw new Error(`${what} must be a single safe path component ([A-Za-z0-9._-]+; not "", ".", "..", or containing "/" or "\\") — got ${JSON.stringify(value)}`);
2450
+ }
2451
+ }
2452
+ function resolveSddDir(harnessDir, planId) {
2453
+ assertSafePathComponent(planId, "planId");
2454
+ const base = resolve6(harnessDir);
2455
+ const declared = mstarcDirOverride(base, "sddDir");
2456
+ const sddBase = declared !== null ? declared : join8(base, "sdd");
2457
+ return join8(sddBase, planId);
2458
+ }
2459
+ function resolveIterationDir(harnessDir) {
2460
+ const declared = mstarcDirOverride(harnessDir, "iterationDir");
2461
+ if (declared !== null)
2462
+ return declared;
2463
+ return join8(resolve6(harnessDir), "iterations");
2464
+ }
2465
+ function resolveHarnessSubdir(startDir, opts, key, fallback) {
2466
+ const harness = resolveHarnessDir(startDir, opts);
2467
+ if (harness === null) {
2468
+ throw new Error(`harness dir not found from ${resolve6(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
2469
+ }
2470
+ const declared = mstarcDirOverride(harness, key);
2471
+ return declared !== null ? declared : join8(resolve6(harness), fallback);
2472
+ }
2473
+ function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
2474
+ return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
2475
+ }
2476
+ function resolveProjectDir(startDir = process.cwd(), opts = {}) {
2477
+ return resolveHarnessSubdir(startDir, opts, "projectDir", "projects");
2478
+ }
2479
+ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
2480
+ # Principle: process stays local; results are shared with the team.
2481
+ # Default-ignore everything under .mstar/, then re-include the tracked results.
2482
+ .mstar/**
2483
+ !.mstar/AGENTS.md
2484
+ !.mstar/knowledge/
2485
+ !.mstar/knowledge/**
2486
+ !.mstar/specs/
2487
+ !.mstar/specs/**
2488
+ # .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
2489
+ .mstarc
2490
+ `;
2491
+ var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
2492
+ # Default-ignore everything under .agents/, then re-include the tracked results.
2493
+ .agents/**
2494
+ !.agents/AGENTS.md
2495
+ !.agents/knowledge/
2496
+ !.agents/knowledge/**
2497
+ !.agents/specs/
2498
+ !.agents/specs/**
2499
+ `;
2500
+ var GITIGNORE_PROCESS_ENTRIES = GITIGNORE_SNIPPET.split(`
2501
+ `).filter((line) => line.startsWith(".mstar/") || line.startsWith("!.mstar/")).map((line) => line.trim());
2502
+ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
2503
+ `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
2504
+ function isDirectory(dir) {
2505
+ try {
2506
+ return statSync3(dir).isDirectory();
2507
+ } catch {
2508
+ return false;
2509
+ }
2510
+ }
2511
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
2512
+ function probeTimeoutMs() {
2513
+ const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
2514
+ if (raw === undefined || raw.trim() === "")
2515
+ return DEFAULT_PROBE_TIMEOUT_MS;
2516
+ const parsed = Number(raw);
2517
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
2518
+ }
2519
+ function violation7(severity, code, message, fix) {
2520
+ return { ok: false, severity, code, message, fix };
2521
+ }
2522
+ function gate(violations) {
2523
+ return { ok: violations.length === 0, violations };
2524
+ }
2525
+ function probeBranch(worktreePath, opts) {
2526
+ const precomputed = opts.branchOf?.(worktreePath);
2527
+ if (precomputed !== undefined)
2528
+ return { branch: precomputed };
2529
+ const timeout = opts.timeoutMs ?? probeTimeoutMs();
2530
+ try {
2531
+ const stdout = execFileSync2(opts.gitPath ?? "git", ["-C", worktreePath, "branch", "--show-current"], {
2532
+ encoding: "utf8",
2533
+ stdio: ["ignore", "pipe", "pipe"],
2534
+ timeout
2162
2535
  });
2163
2536
  const branch = stdout.trim();
2164
2537
  if (branch === "")
@@ -2177,25 +2550,25 @@ function l1PreDispatchCheck(input, opts = {}) {
2177
2550
  const violations = [];
2178
2551
  const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
2179
2552
  if (controlWorktreePath.trim() === "") {
2180
- violations.push(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
- 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"));
2561
+ if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve7(controlWorktreePath) === resolve7(leaseWorktreePath)) {
2562
+ violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
2190
2563
  }
2191
- if (leaseWorktreePath !== "" && !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`));
2571
+ violations.push(violation7("high", "worktree.l1.branch-mismatch", `feature worktree "${leaseWorktreePath}" is on branch "${probe.branch}", expected execution_lease.working_branch "${leaseWorkingBranch}" (plan "${planId}")`, `checkout ${leaseWorkingBranch} in the feature worktree`));
2199
2572
  }
2200
2573
  }
2201
2574
  return gate(violations);
@@ -2205,588 +2578,215 @@ function l2PreDispatchCheck(input, opts = {}) {
2205
2578
  const tracks = input.tracks ?? [];
2206
2579
  const seenPaths = new Set;
2207
2580
  if (tracks.length < 1) {
2208
- violations.push(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"));
2581
+ violations.push(violation7("high", "worktree.l2.no-tracks", "no parallel writable tracks — the L2 pre-dispatch checklist requires at least one track with an absolute worktreePath and Working branch", "pass each track's absolute Worktree path and PM-approved Working branch"));
2209
2582
  }
2210
2583
  tracks.forEach((track, index) => {
2211
2584
  if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
2212
- violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2585
+ violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2213
2586
  return;
2214
2587
  }
2215
2588
  if (!isAbsolute4(track.worktreePath)) {
2216
- violations.push(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>)`));
2589
+ violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
2217
2590
  return;
2218
2591
  }
2219
- const normalized = resolve6(track.worktreePath);
2592
+ const normalized = resolve7(track.worktreePath);
2220
2593
  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"));
2594
+ violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
2222
2595
  return;
2223
2596
  }
2224
2597
  seenPaths.add(normalized);
2225
- if (!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}`));
2598
+ if (!existsSync6(track.worktreePath)) {
2599
+ violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
2227
2600
  return;
2228
2601
  }
2229
- const probe = probeBranch(track.worktreePath, opts);
2230
- if ("error" in probe) {
2231
- violations.push(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`));
2234
- }
2235
- });
2236
- return gate(violations);
2237
- }
2238
- class SddScriptError extends Error {
2239
- exitCode;
2240
- constructor(message, exitCode) {
2241
- super(message);
2242
- this.name = "SddScriptError";
2243
- this.exitCode = exitCode;
2244
- }
2245
- }
2246
- function isDirectory2(dir) {
2247
- try {
2248
- return statSync4(dir).isDirectory();
2249
- } catch {
2250
- return false;
2251
- }
2252
- }
2253
- function isFile2(file) {
2254
- try {
2255
- return statSync4(file).isFile();
2256
- } catch {
2257
- return false;
2258
- }
2259
- }
2260
- var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
2261
- function gitOut(cwd, args) {
2262
- try {
2263
- return execFileSync3("git", args, {
2264
- cwd,
2265
- encoding: "utf8",
2266
- stdio: ["ignore", "pipe", "pipe"],
2267
- maxBuffer: GIT_CAPTURE_MAX_BYTES
2268
- }).trim();
2269
- } catch {
2270
- return null;
2271
- }
2272
- }
2273
- function probeHarnessWithStatus(root) {
2274
- if (isFile2(join7(root, ".mstar", "status.json")))
2275
- return join7(root, ".mstar");
2276
- if (isFile2(join7(root, ".agents", "status.json")))
2277
- return join7(root, ".agents");
2278
- if (hasWorkflowSnapshot(join7(root, ".mstar")))
2279
- return join7(root, ".mstar");
2280
- if (hasWorkflowSnapshot(join7(root, ".agents")))
2281
- return join7(root, ".agents");
2282
- return null;
2283
- }
2284
- function hasWorkflowSnapshot(harnessDir) {
2285
- let workflowsDir;
2286
- try {
2287
- workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2288
- } catch {
2289
- workflowsDir = join7(harnessDir, "workflows");
2290
- }
2291
- if (!isDirectory2(workflowsDir))
2292
- return false;
2293
- try {
2294
- for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
2295
- if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
2296
- return true;
2297
- }
2298
- } catch {
2299
- return false;
2300
- }
2301
- return false;
2302
- }
2303
- function isLinkedWorktree(root) {
2304
- const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
2305
- const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2306
- if (gitDirRaw === null || commonRaw === null)
2307
- return false;
2308
- const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
2309
- const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
2310
- if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2311
- return true;
2312
- try {
2313
- const gdParent = realpathSync3(dirname6(gitDir));
2314
- const cmAbs = realpathSync3(common);
2315
- return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2316
- } catch {
2317
- return false;
2318
- }
2319
- }
2320
- function sddWorkspace(planId, opts = {}) {
2321
- if (!planId) {
2322
- throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
2323
- ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
2324
- }
2325
- const cwd = opts.cwd ?? process.cwd();
2326
- const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
2327
- let root;
2328
- if (controlRoot) {
2329
- if (!isDirectory2(controlRoot)) {
2330
- throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
2331
- }
2332
- root = realpathSync3(controlRoot);
2333
- } else {
2334
- const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
2335
- root = realpathSync3(topLevel ?? cwd);
2336
- }
2337
- if (!controlRoot && isLinkedWorktree(root)) {
2338
- throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
2339
- ` + ` Refusing to create a second SDD tree under the feature checkout.
2340
- ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
2341
- ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
2342
- }
2343
- const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
2344
- let harnessDir;
2345
- if (harnessOverride) {
2346
- harnessDir = resolve7(root, harnessOverride);
2347
- } else {
2348
- const rc = findMstarc(root, root);
2349
- const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
2350
- if (rcHarnessDir) {
2351
- harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2352
- } else {
2353
- const probed = probeHarnessWithStatus(root);
2354
- if (probed) {
2355
- harnessDir = probed;
2356
- } else if (isDirectory2(join7(root, ".mstar"))) {
2357
- harnessDir = join7(root, ".mstar");
2358
- } else if (isDirectory2(join7(root, ".agents"))) {
2359
- harnessDir = join7(root, ".agents");
2360
- } else {
2361
- harnessDir = join7(root, ".mstar");
2362
- }
2363
- }
2364
- }
2365
- const sddDir = resolveSddDir(harnessDir, planId);
2366
- mkdirSync5(sddDir, { recursive: true });
2367
- writeFileSync3(join7(sddDir, ".gitignore"), `*
2368
- `);
2369
- return realpathSync3(sddDir);
2370
- }
2371
- function taskBrief(planFile, taskN, outFile, opts = {}) {
2372
- if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
2373
- throw new SddScriptError("usage: mstar sdd task-brief PLAN_FILE TASK_NUMBER [OUTFILE]", 2);
2374
- }
2375
- let content;
2376
- try {
2377
- content = readFileSync5(planFile, "utf8");
2378
- } catch {
2379
- throw new SddScriptError(`no such plan file: ${planFile}`, 2);
2380
- }
2381
- let out;
2382
- if (outFile) {
2383
- out = outFile;
2384
- } else {
2385
- const sddDir = opts.sddDir ?? process.env.SDD_DIR;
2386
- if (!sddDir) {
2387
- throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2388
- }
2389
- mkdirSync5(sddDir, { recursive: true });
2390
- out = join7(sddDir, `task-${taskN}-brief.md`);
2391
- }
2392
- const records = content.endsWith(`
2393
- `) ? content.split(`
2394
- `).slice(0, -1) : content.split(`
2395
- `);
2396
- const headingRe = /^#+[ \t]+Task[ \t]+[0-9]+/;
2397
- const targetRe = new RegExp(`^#+[ ]+Task[ ]+${taskN}([^0-9]|$)`);
2398
- let infence = false;
2399
- let intask = false;
2400
- const printed = [];
2401
- for (const line of records) {
2402
- if (/^```/.test(line))
2403
- infence = !infence;
2404
- if (!infence && headingRe.test(line))
2405
- intask = targetRe.test(line);
2406
- if (intask)
2407
- printed.push(line);
2408
- }
2409
- const output = printed.length > 0 ? `${printed.join(`
2410
- `)}
2411
- ` : "";
2412
- writeFileSync3(out, output);
2413
- if (printed.length === 0) {
2414
- throw new SddScriptError(`task ${taskN} not found in ${planFile} (no heading matching Task ${taskN})`, 3);
2415
- }
2416
- return out;
2417
- }
2418
- var COMPASS_STATUSES = ["active", "locked", "completed"];
2419
- var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
2420
- var PLAN_STATUS_DONE = "Done";
2421
- function typeName(value) {
2422
- if (value === null)
2423
- return "null";
2424
- if (Array.isArray(value))
2425
- return "array";
2426
- return typeof value;
2427
- }
2428
- function validateCompassShape(doc) {
2429
- const issues = [];
2430
- const expectString = (key, opts = {}) => {
2431
- const value = doc[key];
2432
- if (typeof value !== "string") {
2433
- issues.push({ path: [key], message: `expected string, received ${typeName(value)}` });
2434
- return;
2435
- }
2436
- if (opts.min !== undefined && value.length < opts.min) {
2437
- issues.push({ path: [key], message: `string must contain at least ${opts.min} character(s)` });
2438
- return;
2439
- }
2440
- if (opts.regex !== undefined && !opts.regex.test(value)) {
2441
- issues.push({ path: [key], message: `string must match ${opts.regex}` });
2442
- }
2443
- };
2444
- expectString("iteration_id", { min: 1 });
2445
- expectString("start_date", { regex: DATE_RE2 });
2446
- const status = doc.status;
2447
- if (typeof status !== "string" || !COMPASS_STATUSES.includes(status)) {
2448
- issues.push({
2449
- path: ["status"],
2450
- message: `expected one of ${COMPASS_STATUSES.map((s) => `'${s}'`).join(" | ")}, received ${typeName(status)}`
2451
- });
2452
- }
2453
- expectString("iteration_base_branch", { min: 1 });
2454
- expectString("target_branch", { min: 1 });
2455
- const plans = doc.plans;
2456
- if (plans !== undefined) {
2457
- if (!Array.isArray(plans)) {
2458
- issues.push({ path: ["plans"], message: `expected array, received ${typeName(plans)}` });
2459
- } else {
2460
- plans.forEach((entry, index) => {
2461
- if (typeof entry !== "string") {
2462
- issues.push({ path: ["plans", index], message: `expected string, received ${typeName(entry)}` });
2463
- } else if (entry.length < 1) {
2464
- issues.push({ path: ["plans", index], message: "string must contain at least 1 character(s)" });
2465
- }
2466
- });
2467
- }
2468
- }
2469
- const end_date = doc.end_date;
2470
- if (end_date !== undefined) {
2471
- if (typeof end_date !== "string") {
2472
- issues.push({ path: ["end_date"], message: `expected string, received ${typeName(end_date)}` });
2473
- } else if (!DATE_RE2.test(end_date)) {
2474
- issues.push({ path: ["end_date"], message: `string must match ${DATE_RE2}` });
2475
- }
2476
- }
2477
- if (issues.length > 0)
2478
- return { ok: false, issues };
2479
- return {
2480
- ok: true,
2481
- data: {
2482
- iteration_id: doc.iteration_id,
2483
- start_date: doc.start_date,
2484
- status,
2485
- iteration_base_branch: doc.iteration_base_branch,
2486
- target_branch: doc.target_branch,
2487
- ...plans !== undefined ? { plans } : {},
2488
- ...end_date !== undefined ? { end_date } : {}
2489
- }
2490
- };
2491
- }
2492
- function violation6(severity, code, message, fix) {
2493
- return { ok: false, severity, code, message, fix };
2494
- }
2495
- function isPlainObject4(value) {
2496
- return typeof value === "object" && value !== null && !Array.isArray(value);
2497
- }
2498
- function validateCompassFrontmatter(doc) {
2499
- if (!isPlainObject4(doc)) {
2500
- return {
2501
- ok: false,
2502
- violations: [
2503
- violation6("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
2504
- ]
2505
- };
2506
- }
2507
- const parsed = validateCompassShape(doc);
2508
- if (!parsed.ok) {
2509
- return {
2510
- ok: false,
2511
- violations: parsed.issues.map((issue) => {
2512
- const field = issue.path.join(".") || "(root)";
2513
- return violation6("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration §1.3)`);
2514
- })
2515
- };
2516
- }
2517
- const violations = [];
2518
- const { status, end_date } = parsed.data;
2519
- if (status === "completed" && end_date === undefined) {
2520
- violations.push(violation6("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
2521
- }
2522
- if (status !== "completed" && end_date !== undefined) {
2523
- violations.push(violation6("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
2524
- }
2525
- return { ok: violations.length === 0, violations };
2526
- }
2527
- function registeredPlanIds(compassDoc) {
2528
- if (!Array.isArray(compassDoc.plans))
2529
- return [];
2530
- return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
2531
- }
2532
- function findPlanRow(snapshotDoc, planId) {
2533
- if (!Array.isArray(snapshotDoc.plans))
2534
- return null;
2535
- for (const row of snapshotDoc.plans) {
2536
- if (!isPlainObject4(row))
2537
- continue;
2538
- const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
2539
- if (rowId === planId)
2540
- return row;
2541
- }
2542
- return null;
2543
- }
2544
- function entryPlansAllDone(snapshotDoc, registered) {
2545
- const violations = [];
2546
- if (registered.length === 0) {
2547
- violations.push(violation6("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
2548
- return violations;
2549
- }
2550
- for (const planId of registered) {
2551
- const row = findPlanRow(snapshotDoc, planId);
2552
- if (row === null) {
2553
- violations.push(violation6("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in the workflow snapshot plans[] (mstar-iteration §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/workflows/<id>/snapshot.json"));
2554
- continue;
2555
- }
2556
- if (row.status !== PLAN_STATUS_DONE) {
2557
- violations.push(violation6("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in the workflow snapshot — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
2558
- }
2559
- }
2560
- return violations;
2561
- }
2562
- function entryFrontmatterComplete(compassDoc) {
2563
- return validateCompassFrontmatter(compassDoc).violations;
2564
- }
2565
- function exitFrontmatterClosed(compassDoc) {
2566
- const violations = [];
2567
- if (compassDoc.status !== "completed") {
2568
- violations.push(violation6("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
2569
- }
2570
- const endDate = compassDoc.end_date;
2571
- if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
2572
- violations.push(violation6("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
2573
- }
2574
- return violations;
2575
- }
2576
- function exitBranchCheck(opts) {
2577
- const violations = [];
2578
- const { currentBranch, specIntegrationBranch } = opts;
2579
- if (currentBranch === undefined || specIntegrationBranch === undefined) {
2580
- violations.push(violation6("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
2581
- } else if (currentBranch !== specIntegrationBranch) {
2582
- violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
2583
- }
2584
- return violations;
2585
- }
2586
- function exitPrBaseCheck(compassDoc, opts) {
2587
- const violations = [];
2588
- const target = compassDoc.target_branch;
2589
- const { prBaseBranch } = opts;
2590
- if (prBaseBranch === undefined) {
2591
- violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
2592
- } else if (typeof target !== "string" || prBaseBranch !== target) {
2593
- violations.push(violation6("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
2594
- }
2595
- return violations;
2596
- }
2597
- function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
2598
- const registered = registeredPlanIds(compassDoc);
2599
- const entryViolations = [
2600
- ...entryPlansAllDone(snapshotDoc, registered),
2601
- ...entryFrontmatterComplete(compassDoc)
2602
- ];
2603
- const exitViolations = [
2604
- ...exitFrontmatterClosed(compassDoc),
2605
- ...exitBranchCheck(opts),
2606
- ...exitPrBaseCheck(compassDoc, opts)
2607
- ];
2608
- const allPlansDone = registered.length > 0 && registered.every((planId) => {
2609
- const row = findPlanRow(snapshotDoc, planId);
2610
- return row !== null && row.status === PLAN_STATUS_DONE;
2611
- });
2612
- const entry = { ok: entryViolations.length === 0, violations: entryViolations };
2613
- const exit = { ok: exitViolations.length === 0, violations: exitViolations };
2614
- let transition;
2615
- if (!allPlansDone)
2616
- transition = "phase-2-execute";
2617
- else if (entry.ok && exit.ok)
2618
- transition = "phase-4-pr-delivery";
2619
- else
2620
- transition = "phase-3-close";
2621
- const gateBlocking = allPlansDone ? [...entryViolations, ...exitViolations] : [];
2622
- return {
2623
- transition,
2624
- allPlansDone,
2625
- entry,
2626
- exit,
2627
- ok: gateBlocking.length === 0,
2628
- violations: gateBlocking
2629
- };
2630
- }
2631
- function parseCompassFrontmatter(filePath) {
2632
- return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
2633
- }
2634
- function parseCompassFrontmatterText(content, filePath) {
2635
- const lines = content.split(/\r?\n/);
2636
- if (lines[0]?.trim() !== "---") {
2637
- throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
2638
- }
2639
- const end = lines.indexOf("---", 1);
2640
- if (end === -1) {
2641
- throw new Error(`unterminated YAML frontmatter in ${filePath} (no closing "---")`);
2642
- }
2643
- const doc = {};
2644
- let listKey = null;
2645
- for (let i = 1;i < end; i += 1) {
2646
- const line = lines[i] ?? "";
2647
- if (!line.trim() || line.trim().startsWith("#"))
2648
- continue;
2649
- if (listKey !== null && /^\s*-\s+/.test(line)) {
2650
- const item = line.replace(/^\s*-\s+/, "").trim().replace(/^["']|["']$/g, "");
2651
- if (!Array.isArray(doc[listKey]))
2652
- doc[listKey] = [];
2653
- doc[listKey].push(item);
2654
- continue;
2655
- }
2656
- listKey = null;
2657
- const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
2658
- if (!kv) {
2659
- throw new Error(`unsupported frontmatter line in ${filePath}: ${JSON.stringify(line)}`);
2660
- }
2661
- const value = kv[2].trim();
2662
- doc[kv[1]] = value === "" ? null : /^\[.*\]$/.test(value) ? parseFlowArray(value, filePath) : value.replace(/^["']|["']$/g, "");
2663
- listKey = value === "" ? kv[1] : null;
2664
- }
2665
- return doc;
2602
+ const probe = probeBranch(track.worktreePath, opts);
2603
+ if ("error" in probe) {
2604
+ violations.push(violation7("high", "worktree.l2.branch-probe-failed", `cannot probe branch at "${track.worktreePath}": ${probe.error}`, "verify the path is a git worktree checkout on its Working branch (not detached)"));
2605
+ } else if (probe.branch !== track.workingBranch) {
2606
+ violations.push(violation7("high", "worktree.l2.branch-mismatch", `track worktree "${track.worktreePath}" is on branch "${probe.branch}", expected Working branch "${track.workingBranch}"`, `checkout ${track.workingBranch} in that worktree`));
2607
+ }
2608
+ });
2609
+ return gate(violations);
2666
2610
  }
2667
- 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])`);
2611
+ class SddScriptError extends Error {
2612
+ exitCode;
2613
+ constructor(message, exitCode) {
2614
+ super(message);
2615
+ this.name = "SddScriptError";
2616
+ this.exitCode = exitCode;
2671
2617
  }
2672
- 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)`);
2681
- }
2618
+ }
2619
+ function isDirectory2(dir) {
2620
+ try {
2621
+ return statSync4(dir).isDirectory();
2622
+ } catch {
2623
+ return false;
2682
2624
  }
2683
- if (quote !== null) {
2684
- throw new Error(`unterminated ${quote} quote in flow-style array in ${filePath}: ${JSON.stringify(raw)}`);
2625
+ }
2626
+ function isFile2(file) {
2627
+ try {
2628
+ return statSync4(file).isFile();
2629
+ } catch {
2630
+ return false;
2685
2631
  }
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);
2632
+ }
2633
+ var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
2634
+ function gitOut(cwd, args) {
2635
+ try {
2636
+ return execFileSync3("git", args, {
2637
+ cwd,
2638
+ encoding: "utf8",
2639
+ stdio: ["ignore", "pipe", "pipe"],
2640
+ maxBuffer: GIT_CAPTURE_MAX_BYTES
2641
+ }).trim();
2642
+ } catch {
2643
+ return null;
2692
2644
  }
2693
- return items;
2694
2645
  }
2695
- 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);
2646
+ function probeHarnessWithStatus(root) {
2647
+ if (isFile2(join9(root, ".mstar", "status.json")))
2648
+ return join9(root, ".mstar");
2649
+ if (isFile2(join9(root, ".agents", "status.json")))
2650
+ return join9(root, ".agents");
2651
+ if (hasWorkflowSnapshot(join9(root, ".mstar")))
2652
+ return join9(root, ".mstar");
2653
+ if (hasWorkflowSnapshot(join9(root, ".agents")))
2654
+ return join9(root, ".agents");
2655
+ return null;
2701
2656
  }
2702
- function violation7(severity, code, message, fix) {
2703
- return { ok: false, severity, code, message, fix };
2657
+ function hasWorkflowSnapshot(harnessDir) {
2658
+ let workflowsDir;
2659
+ try {
2660
+ workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2661
+ } catch {
2662
+ workflowsDir = join9(harnessDir, "workflows");
2663
+ }
2664
+ if (!isDirectory2(workflowsDir))
2665
+ return false;
2666
+ try {
2667
+ for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
2668
+ if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
2669
+ return true;
2670
+ }
2671
+ } catch {
2672
+ return false;
2673
+ }
2674
+ return false;
2704
2675
  }
2705
- function 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`));
2676
+ function isLinkedWorktree(root) {
2677
+ const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
2678
+ const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2679
+ if (gitDirRaw === null || commonRaw === null)
2680
+ return false;
2681
+ const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
2682
+ const common = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
2683
+ if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2684
+ return true;
2685
+ try {
2686
+ const gdParent = realpathSync3(dirname6(gitDir));
2687
+ const cmAbs = realpathSync3(common);
2688
+ return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2689
+ } catch {
2690
+ return false;
2710
2691
  }
2711
2692
  }
2712
- function 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
- };
2693
+ function sddWorkspace(planId, opts = {}) {
2694
+ if (!planId) {
2695
+ throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
2696
+ ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
2719
2697
  }
2720
- 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"));
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 = resolve8(root, harnessOverride);
2720
+ } else {
2721
+ const rc = findMstarc(root, root);
2722
+ const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
2723
+ if (rcHarnessDir) {
2724
+ harnessDir = resolve8(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2725
+ } else {
2726
+ const probed = probeHarnessWithStatus(root);
2727
+ if (probed) {
2728
+ harnessDir = probed;
2729
+ } else if (isDirectory2(join9(root, ".mstar"))) {
2730
+ harnessDir = join9(root, ".mstar");
2731
+ } else if (isDirectory2(join9(root, ".agents"))) {
2732
+ harnessDir = join9(root, ".agents");
2733
+ } else {
2734
+ harnessDir = join9(root, ".mstar");
2749
2735
  }
2750
2736
  }
2751
2737
  }
2752
- return { ok: violations.length === 0, violations };
2738
+ const sddDir = resolveSddDir(harnessDir, planId);
2739
+ mkdirSync6(sddDir, { recursive: true });
2740
+ writeFileSync4(join9(sddDir, ".gitignore"), `*
2741
+ `);
2742
+ return realpathSync3(sddDir);
2753
2743
  }
2754
- function 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
+ mkdirSync6(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 };
@@ -3190,6 +3190,9 @@ function completenessLevel(frontmatterText, checklist) {
3190
3190
  function violation9(severity, code, message, fix) {
3191
3191
  return { ok: false, severity, code, message, fix };
3192
3192
  }
3193
+ var AUDIT_EFFORTS = ["XS", "S", "M", "L", "XL"];
3194
+ var AUDIT_RISKS = ["LOW", "MED", "HIGH"];
3195
+ var AUDIT_CONFIDENCES = ["HIGH", "MED", "LOW"];
3193
3196
  var AUDIT_STATUS_FIELDS = ["Priority", "Effort", "Risk", "Depends on", "Category", "Planned at"];
3194
3197
  function parseStatusBlocks(planText) {
3195
3198
  const blocks = [];
@@ -3538,7 +3541,7 @@ function referenceExists(repoRoot, docText) {
3538
3541
  for (const { ref, isSymbol, module } of refs) {
3539
3542
  if (!isSymbol || module === undefined) {
3540
3543
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
3541
- if (existsSync8(resolve10(repoRoot, candidate))) {
3544
+ if (existsSync8(resolve11(repoRoot, candidate))) {
3542
3545
  checked++;
3543
3546
  } else {
3544
3547
  violations.push(violation10("medium", "compound.reference.missing-file", `referenced path \`${ref}\` does not exist under ${repoRoot} (compound-refresh Phase 2: referenced code still exists?)`, "update the doc to reference an existing path, or delete the stale reference"));
@@ -3613,9 +3616,9 @@ function isFileLikeRoot(root) {
3613
3616
  return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
3614
3617
  }
3615
3618
  function scopeGuard(path, allowedRoots) {
3616
- const resolved = resolve10(path);
3619
+ const resolved = resolve11(path);
3617
3620
  for (const root of allowedRoots) {
3618
- const r = resolve10(root);
3621
+ const r = resolve11(root);
3619
3622
  if (isFileLikeRoot(r)) {
3620
3623
  if (resolved === r)
3621
3624
  return { ok: true, violations: [] };
@@ -3947,6 +3950,9 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
3947
3950
  function resolveAssetPath(skillName, relPath, host) {
3948
3951
  return `skill \`${skillName}\` → ${relPath} (${resolveSkillRoot(host, { skill: skillName, rel: relPath })})`;
3949
3952
  }
3953
+ var EFFORT_ENUM_RE = new RegExp(`^(?:${AUDIT_EFFORTS.join("|")})(?:\\s*\\(|$)`);
3954
+ var RISK_ENUM_RE = new RegExp(`^(?:${AUDIT_RISKS.join("|")})(?:\\b|$)`);
3955
+ var CONFIDENCE_ENUM_RE = new RegExp(`^(${[...AUDIT_CONFIDENCES, "MEDIUM"].join("|")})\\b`, "i");
3950
3956
 
3951
3957
  // src/index.ts
3952
3958
  import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
@@ -3984,8 +3990,8 @@ class DshMstar extends Service {
3984
3990
 
3985
3991
  // src/gates/_shared.ts
3986
3992
  var import_schemastery = __toESM(require_lib2(), 1);
3987
- import { existsSync as existsSync6 } from "node:fs";
3988
- import { resolve as resolve8 } from "node:path";
3993
+ import { existsSync as existsSync4 } from "node:fs";
3994
+ import { resolve as resolve5 } from "node:path";
3989
3995
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3990
3996
  var STATUS_FILE = "status.json";
3991
3997
  var PERSONA_INTERPOLATION_HAZARD = /\{\{[\s\S]*\}\}/;
@@ -4039,7 +4045,7 @@ function resolvePackagedSkillsDir(fileUrl) {
4039
4045
  for (const rel of ["../harness-skills", "../../harness-skills"]) {
4040
4046
  try {
4041
4047
  const dir = fileURLToPath2(new URL(rel, fileUrl));
4042
- if (existsSync6(dir))
4048
+ if (existsSync4(dir))
4043
4049
  return dir;
4044
4050
  } catch {}
4045
4051
  }
@@ -4058,7 +4064,7 @@ function resolvePackagedAgentsDir(fileUrl) {
4058
4064
  for (const rel of ["../harness-agents", "../../harness-agents"]) {
4059
4065
  try {
4060
4066
  const dir = fileURLToPath2(new URL(rel, fileUrl));
4061
- if (existsSync6(dir))
4067
+ if (existsSync4(dir))
4062
4068
  return dir;
4063
4069
  } catch {}
4064
4070
  }
@@ -4078,14 +4084,14 @@ class HarnessResolver {
4078
4084
  explicit;
4079
4085
  cache = new Map;
4080
4086
  constructor(explicit) {
4081
- this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve8(process.cwd(), explicit) : null;
4087
+ this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve5(process.cwd(), explicit) : null;
4082
4088
  }
4083
4089
  forWorkspace(cwd) {
4084
4090
  if (this.explicit !== null)
4085
4091
  return this.explicit;
4086
4092
  if (cwd === undefined || cwd.trim() === "")
4087
4093
  return null;
4088
- const abs = resolve8(cwd);
4094
+ const abs = resolve5(cwd);
4089
4095
  const hit = this.cache.get(abs);
4090
4096
  if (hit !== undefined)
4091
4097
  return hit;
@@ -4126,27 +4132,27 @@ function skillLocalConfig(config) {
4126
4132
  }
4127
4133
 
4128
4134
  // src/gates/catalog.ts
4129
- import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "node:fs";
4130
- import { join as join11 } from "node:path";
4135
+ import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "node:fs";
4136
+ import { join as join10 } from "node:path";
4131
4137
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
4132
4138
 
4133
4139
  // src/gates/agent-flow.ts
4134
- import { appendFileSync, closeSync, existsSync as 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";
4140
+ import { appendFileSync, closeSync, existsSync as existsSync12, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4141
+ import { join as join7 } from "node:path";
4136
4142
 
4137
4143
  // src/gates/dispatch.ts
4138
- import { existsSync as existsSync12 } from "node:fs";
4139
- import { basename as basename4, join as join8, resolve as resolve11 } from "node:path";
4144
+ import { existsSync as existsSync10 } from "node:fs";
4145
+ import { basename as basename4, join as join5, resolve as resolve9 } from "node:path";
4140
4146
 
4141
4147
  // src/gates/workflow-selection.ts
4142
- import { existsSync as existsSync10, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
4143
- import { dirname as dirname7, join as join5, relative as relative3 } from "node:path";
4148
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4149
+ import { dirname as dirname7, join as join3, relative as relative3 } from "node:path";
4144
4150
  var TERMINAL_STATUS_CACHE_MAX = 64;
4145
4151
  var terminalStatusCache = new Map;
4146
4152
  function terminalStatusOf(snapshotPath) {
4147
4153
  let mtimeMs;
4148
4154
  try {
4149
- mtimeMs = statSync3(snapshotPath).mtimeMs;
4155
+ mtimeMs = statSync2(snapshotPath).mtimeMs;
4150
4156
  } catch {
4151
4157
  terminalStatusCache.delete(snapshotPath);
4152
4158
  return;
@@ -4173,8 +4179,8 @@ function terminalStatusOf(snapshotPath) {
4173
4179
  return verdict;
4174
4180
  }
4175
4181
  function resolveActiveWorkflow(harnessDir) {
4176
- const statusPath = join5(harnessDir, STATUS_FILE);
4177
- if (!existsSync10(statusPath)) {
4182
+ const statusPath = join3(harnessDir, STATUS_FILE);
4183
+ if (!existsSync7(statusPath)) {
4178
4184
  return {
4179
4185
  kind: "error",
4180
4186
  code: "status.missing",
@@ -4232,7 +4238,7 @@ function resolveReadWorkflow(harnessDir) {
4232
4238
  if (active.kind === "error" && active.code !== "workflow.selection.no-active")
4233
4239
  return active;
4234
4240
  const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
4235
- if (!existsSync10(workflowsDir)) {
4241
+ if (!existsSync7(workflowsDir)) {
4236
4242
  return {
4237
4243
  kind: "error",
4238
4244
  code: "workflow.selection.no-snapshot",
@@ -4241,7 +4247,7 @@ function resolveReadWorkflow(harnessDir) {
4241
4247
  }
4242
4248
  let entries;
4243
4249
  try {
4244
- entries = readdirSync5(workflowsDir, { withFileTypes: true });
4250
+ entries = readdirSync3(workflowsDir, { withFileTypes: true });
4245
4251
  } catch {
4246
4252
  return {
4247
4253
  kind: "error",
@@ -4253,8 +4259,8 @@ function resolveReadWorkflow(harnessDir) {
4253
4259
  for (const entry of entries) {
4254
4260
  if (!entry.isDirectory())
4255
4261
  continue;
4256
- const snapshotPath = join5(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
4257
- if (!existsSync10(snapshotPath)) {
4262
+ const snapshotPath = join3(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
4263
+ if (!existsSync7(snapshotPath)) {
4258
4264
  terminalStatusCache.delete(snapshotPath);
4259
4265
  continue;
4260
4266
  }
@@ -4262,11 +4268,11 @@ function resolveReadWorkflow(harnessDir) {
4262
4268
  if (verdict === undefined || !verdict.terminal)
4263
4269
  continue;
4264
4270
  if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
4265
- best = { workflowId: entry.name, dir: join5(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
4271
+ best = { workflowId: entry.name, dir: join3(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
4266
4272
  }
4267
4273
  }
4268
4274
  for (const cachedPath of terminalStatusCache.keys()) {
4269
- if (!existsSync10(join5(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
4275
+ if (!existsSync7(join3(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
4270
4276
  terminalStatusCache.delete(cachedPath);
4271
4277
  }
4272
4278
  }
@@ -4459,12 +4465,12 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4459
4465
  if (!sdd)
4460
4466
  return [];
4461
4467
  if (selection.kind === "error" && selection.code === "status.unreadable") {
4462
- return [leaseViolation("lease.dispatch.unreadable", `cannot read ${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)")];
4468
+ return [leaseViolation("lease.dispatch.unreadable", `cannot read ${join5(harnessDir, STATUS_FILE)}: the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid status.json (the status gate refuses invalid writes)")];
4463
4469
  }
4464
- return [leaseViolation("lease.dispatch.unverifiable", `${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)")];
4470
+ return [leaseViolation("lease.dispatch.unverifiable", `${join5(harnessDir, STATUS_FILE)}: ${selection.kind === "error" ? selection.message : "no active workflow"} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid v2 status.json registering an active workflow (first implement dispatch requires a plan row + lease)")];
4465
4471
  }
4466
- const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4467
- if (!existsSync12(snapshotPath)) {
4472
+ const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4473
+ if (!existsSync10(snapshotPath)) {
4468
4474
  if (!sdd)
4469
4475
  return [];
4470
4476
  return [leaseViolation("lease.dispatch.unverifiable", `${snapshotPath} is missing — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid workflow snapshot registering the plan row (first implement dispatch requires a plan row)")];
@@ -4497,7 +4503,7 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4497
4503
  const wt = worktree === undefined ? undefined : firstToken(worktree);
4498
4504
  if (isNaValue(wt)) {
4499
4505
  violations.push(leaseViolation("lease.dispatch.worktree-mismatch", "Assignment declares no Worktree path — cannot confirm this dispatch matches execution_lease.worktree_path", "add the absolute Worktree path to the Assignment (must equal the lease worktree_path)"));
4500
- } else if (resolve11(wt) !== resolve11(String(lease.worktree_path ?? ""))) {
4506
+ } else if (resolve9(wt) !== resolve9(String(lease.worktree_path ?? ""))) {
4501
4507
  violations.push(leaseViolation("lease.dispatch.worktree-mismatch", `Assignment Worktree path "${wt}" differs from execution_lease.worktree_path "${String(lease.worktree_path)}"`, "align the Assignment with the lease worktree path (or update the lease)"));
4502
4508
  }
4503
4509
  const forms = parseAssignmentBranchForms(header);
@@ -4538,7 +4544,7 @@ function worktreeL2Violations(header) {
4538
4544
  return violations;
4539
4545
  }
4540
4546
  function activeSnapshotRows(harnessDir) {
4541
- if (!existsSync12(join8(harnessDir, STATUS_FILE)))
4547
+ if (!existsSync10(join5(harnessDir, STATUS_FILE)))
4542
4548
  return { rows: null, controlWorktreePath: undefined, unreadable: false };
4543
4549
  const selection = resolveActiveWorkflow(harnessDir);
4544
4550
  if (selection.kind !== "active") {
@@ -4548,8 +4554,8 @@ function activeSnapshotRows(harnessDir) {
4548
4554
  unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
4549
4555
  };
4550
4556
  }
4551
- const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4552
- if (!existsSync12(snapshotPath))
4557
+ const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4558
+ if (!existsSync10(snapshotPath))
4553
4559
  return { rows: null, controlWorktreePath: undefined, unreadable: true };
4554
4560
  let doc;
4555
4561
  try {
@@ -4639,7 +4645,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec) {
4639
4645
  }
4640
4646
  const pb = writableFanOutUncovered(harnessDir);
4641
4647
  if (pb.unreadable) {
4642
- const statusLabel = harnessDir === null ? STATUS_FILE : join8(harnessDir, STATUS_FILE);
4648
+ const statusLabel = harnessDir === null ? STATUS_FILE : join5(harnessDir, STATUS_FILE);
4643
4649
  ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
4644
4650
  }
4645
4651
  const verdict = workflowPolicy(config, adapter.workflowAskCache, {
@@ -4829,7 +4835,7 @@ function callPairingKey(exec) {
4829
4835
  function resolveAgentFlowWriteDir(harnessDir) {
4830
4836
  const selection = resolveActiveWorkflow(harnessDir);
4831
4837
  if (selection.kind === "active")
4832
- return join10(harnessDir, selection.dir);
4838
+ return join7(harnessDir, selection.dir);
4833
4839
  const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
4834
4840
  if (!noActiveWarned) {
4835
4841
  noActiveWarned = true;
@@ -4846,7 +4852,7 @@ function sleepSync(ms) {
4846
4852
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
4847
4853
  }
4848
4854
  function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4849
- const lockDir = join10(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4855
+ const lockDir = join7(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4850
4856
  if (heldWorkflowLocks.has(lockDir)) {
4851
4857
  throw new Error(`${lockDir} is already held by this process — withWorkflowDirLock is not reentrant; a nested acquisition on the same workflow dir is a bug`);
4852
4858
  }
@@ -4856,7 +4862,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4856
4862
  let acquired = null;
4857
4863
  for (;; ) {
4858
4864
  try {
4859
- mkdirSync3(lockDir);
4865
+ mkdirSync2(lockDir);
4860
4866
  const st = statSync5(lockDir);
4861
4867
  acquired = { dev: st.dev, ino: st.ino };
4862
4868
  break;
@@ -4872,7 +4878,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4872
4878
  heldWorkflowLocks.add(lockDir);
4873
4879
  try {
4874
4880
  try {
4875
- writeFileSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4881
+ writeFileSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4876
4882
  } catch {}
4877
4883
  return fn();
4878
4884
  } finally {
@@ -4881,7 +4887,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4881
4887
  const current = statSync5(lockDir);
4882
4888
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
4883
4889
  try {
4884
- unlinkSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4890
+ unlinkSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4885
4891
  } catch {}
4886
4892
  rmdirSync(lockDir);
4887
4893
  }
@@ -4889,13 +4895,13 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4889
4895
  }
4890
4896
  }
4891
4897
  function appendEvent(workflowDir, event) {
4892
- const file = join10(workflowDir, AGENT_FLOW_FILE);
4898
+ const file = join7(workflowDir, AGENT_FLOW_FILE);
4893
4899
  withWorkflowDirLock(workflowDir, () => {
4894
4900
  appendFileSync(file, `${JSON.stringify(event)}
4895
4901
  `);
4896
4902
  if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
4897
4903
  return;
4898
- const content = readFileSync7(file, "utf8");
4904
+ const content = readFileSync5(file, "utf8");
4899
4905
  const lines = content.replace(/\n$/, "").split(`
4900
4906
  `);
4901
4907
  if (lines.length > AGENT_FLOW_MAX_EVENTS) {
@@ -5246,7 +5252,7 @@ function summaryOf(events) {
5246
5252
  function ledgerContent(file, n) {
5247
5253
  const size = statSync5(file).size;
5248
5254
  if (size <= AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES) {
5249
- return readFileSync7(file, "utf8");
5255
+ return readFileSync5(file, "utf8");
5250
5256
  }
5251
5257
  let windowBytes = AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES;
5252
5258
  const fd = openSync(file, "r");
@@ -5278,8 +5284,8 @@ function ledgerContent(file, n) {
5278
5284
  }
5279
5285
  }
5280
5286
  function readAgentFlow(workflowDir, limit) {
5281
- const file = join10(workflowDir, AGENT_FLOW_FILE);
5282
- if (!existsSync13(file)) {
5287
+ const file = join7(workflowDir, AGENT_FLOW_FILE);
5288
+ if (!existsSync12(file)) {
5283
5289
  return { events: [], summary: [] };
5284
5290
  }
5285
5291
  const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
@@ -5404,7 +5410,7 @@ function residualSeverityIndex(severity) {
5404
5410
  function pluginVersion() {
5405
5411
  for (const rel of ["../package.json", "../../package.json"]) {
5406
5412
  try {
5407
- const pkg = JSON.parse(readFileSync9(new URL(rel, import.meta.url), "utf8"));
5413
+ const pkg = JSON.parse(readFileSync8(new URL(rel, import.meta.url), "utf8"));
5408
5414
  if (typeof pkg.version === "string" && pkg.version !== "")
5409
5415
  return pkg.version;
5410
5416
  } catch {}
@@ -5533,8 +5539,8 @@ function hhmm(ts) {
5533
5539
  function harnessStateSource(harnessDir) {
5534
5540
  if (harnessDir === null)
5535
5541
  return null;
5536
- const statusPath = join11(harnessDir, STATUS_FILE);
5537
- if (!existsSync14(statusPath))
5542
+ const statusPath = join10(harnessDir, STATUS_FILE);
5543
+ if (!existsSync13(statusPath))
5538
5544
  return null;
5539
5545
  try {
5540
5546
  const selection = resolveReadWorkflow(harnessDir);
@@ -5553,9 +5559,9 @@ function harnessStateSource(harnessDir) {
5553
5559
  if (selection.kind === "error") {
5554
5560
  return selectionErrorState(selection, rollup, harnessDir, compass);
5555
5561
  }
5556
- const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5562
+ const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5557
5563
  let snapshot;
5558
- if (!existsSync14(snapshotPath)) {
5564
+ if (!existsSync13(snapshotPath)) {
5559
5565
  return selectionErrorState({
5560
5566
  kind: "error",
5561
5567
  code: "workflow.selection.snapshot-unreadable",
@@ -5617,7 +5623,7 @@ function harnessStateSource(harnessDir) {
5617
5623
  leases,
5618
5624
  knowledge: knowledgeDigest(harnessDir),
5619
5625
  direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
5620
- agentFlow: readAgentFlow(join11(harnessDir, selection.dir), 50)
5626
+ agentFlow: readAgentFlow(join10(harnessDir, selection.dir), 50)
5621
5627
  };
5622
5628
  } catch {
5623
5629
  return null;
@@ -5647,21 +5653,21 @@ function selectionErrorState(selection, rollup, harnessDir, compass) {
5647
5653
  function projectRollupSource(harnessDir, residuals) {
5648
5654
  const milestones = [];
5649
5655
  const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5650
- if (existsSync14(projectsDir)) {
5656
+ if (existsSync13(projectsDir)) {
5651
5657
  let entries;
5652
5658
  try {
5653
- entries = readdirSync7(projectsDir, { withFileTypes: true });
5659
+ entries = readdirSync6(projectsDir, { withFileTypes: true });
5654
5660
  } catch {
5655
5661
  entries = [];
5656
5662
  }
5657
5663
  for (const entry of entries) {
5658
5664
  if (!entry.isDirectory())
5659
5665
  continue;
5660
- const roadmapPath = join11(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5661
- if (!existsSync14(roadmapPath))
5666
+ const roadmapPath = join10(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5667
+ if (!existsSync13(roadmapPath))
5662
5668
  continue;
5663
5669
  try {
5664
- const doc = parseCompassFrontmatterText(readFileSync9(roadmapPath, "utf8"), roadmapPath);
5670
+ const doc = parseCompassFrontmatterText(readFileSync8(roadmapPath, "utf8"), roadmapPath);
5665
5671
  if (Array.isArray(doc.milestones)) {
5666
5672
  for (const milestone of doc.milestones) {
5667
5673
  if (typeof milestone === "string" && milestone.trim() !== "")
@@ -5723,11 +5729,11 @@ function residualRollup(harnessDir) {
5723
5729
  }
5724
5730
  function projectRegisters(harnessDir) {
5725
5731
  const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5726
- if (!existsSync14(projectsDir))
5732
+ if (!existsSync13(projectsDir))
5727
5733
  return [];
5728
5734
  let entries;
5729
5735
  try {
5730
- entries = readdirSync7(projectsDir, { withFileTypes: true });
5736
+ entries = readdirSync6(projectsDir, { withFileTypes: true });
5731
5737
  } catch {
5732
5738
  return [];
5733
5739
  }
@@ -5735,8 +5741,8 @@ function projectRegisters(harnessDir) {
5735
5741
  for (const entry of entries) {
5736
5742
  if (!entry.isDirectory())
5737
5743
  continue;
5738
- const registerPath = join11(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5739
- if (!existsSync14(registerPath))
5744
+ const registerPath = join10(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5745
+ if (!existsSync13(registerPath))
5740
5746
  continue;
5741
5747
  try {
5742
5748
  registers.push({ projectId: entry.name, doc: readJson(registerPath) });
@@ -5747,13 +5753,13 @@ function projectRegisters(harnessDir) {
5747
5753
  return registers;
5748
5754
  }
5749
5755
  function knowledgeDigest(harnessDir) {
5750
- const indexPath = join11(harnessDir, "knowledge", "README.md");
5751
- if (!existsSync14(indexPath))
5756
+ const indexPath = join10(harnessDir, "knowledge", "README.md");
5757
+ if (!existsSync13(indexPath))
5752
5758
  return null;
5753
5759
  try {
5754
5760
  const categories = new Set;
5755
5761
  let docCount = 0;
5756
- for (const line of readFileSync9(indexPath, "utf8").split(/\r?\n/)) {
5762
+ for (const line of readFileSync8(indexPath, "utf8").split(/\r?\n/)) {
5757
5763
  const row = line.trim().match(/^\|(.+)\|$/);
5758
5764
  if (row === null)
5759
5765
  continue;
@@ -5776,7 +5782,7 @@ function knowledgeDigest(harnessDir) {
5776
5782
  }
5777
5783
  function compassDirection(compassPath) {
5778
5784
  try {
5779
- const content = readFileSync9(compassPath, "utf8");
5785
+ const content = readFileSync8(compassPath, "utf8");
5780
5786
  const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
5781
5787
  if (section === null)
5782
5788
  return null;
@@ -5793,23 +5799,23 @@ function compassDirection(compassPath) {
5793
5799
  }
5794
5800
  function steeringCompassPath(harnessDir) {
5795
5801
  const iterationsDir = resolveIterationDir(harnessDir);
5796
- if (!existsSync14(iterationsDir))
5802
+ if (!existsSync13(iterationsDir))
5797
5803
  return;
5798
5804
  let entries;
5799
5805
  try {
5800
- entries = readdirSync7(iterationsDir, { withFileTypes: true });
5806
+ entries = readdirSync6(iterationsDir, { withFileTypes: true });
5801
5807
  } catch {
5802
5808
  return;
5803
5809
  }
5804
5810
  for (const entry of entries) {
5805
5811
  if (!entry.isDirectory())
5806
5812
  continue;
5807
- const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
5808
- if (!existsSync14(compassPath))
5813
+ const compassPath = join10(iterationsDir, entry.name, "delivery-compass.md");
5814
+ if (!existsSync13(compassPath))
5809
5815
  continue;
5810
5816
  let content;
5811
5817
  try {
5812
- content = readFileSync9(compassPath, "utf8");
5818
+ content = readFileSync8(compassPath, "utf8");
5813
5819
  } catch {
5814
5820
  continue;
5815
5821
  }
@@ -5823,8 +5829,8 @@ function steeringCompassPath(harnessDir) {
5823
5829
  function iterationGateSource(harnessDir) {
5824
5830
  if (harnessDir === null)
5825
5831
  return;
5826
- const statusPath = join11(harnessDir, STATUS_FILE);
5827
- if (!existsSync14(statusPath))
5832
+ const statusPath = join10(harnessDir, STATUS_FILE);
5833
+ if (!existsSync13(statusPath))
5828
5834
  return;
5829
5835
  const compass = steeringCompassPath(harnessDir);
5830
5836
  if (compass === undefined)
@@ -5832,7 +5838,7 @@ function iterationGateSource(harnessDir) {
5832
5838
  const selection = resolveReadWorkflow(harnessDir);
5833
5839
  if (selection.kind === "error")
5834
5840
  return;
5835
- const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5841
+ const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5836
5842
  try {
5837
5843
  const snapshotDoc = readJson(snapshotPath);
5838
5844
  const compassDoc = parseCompassFrontmatter(compass.compassPath);
@@ -5886,11 +5892,11 @@ function agentDigestKey(agent, cwd) {
5886
5892
  }
5887
5893
 
5888
5894
  // src/gates/status.ts
5889
- import { existsSync as existsSync15, readdirSync as readdirSync9 } from "node:fs";
5890
- import { basename as basename6, join as join14, relative as relative5, resolve as resolve13 } from "node:path";
5895
+ import { existsSync as existsSync14, readdirSync as readdirSync9 } from "node:fs";
5896
+ import { basename as basename6, join as join14, relative as relative5, resolve as resolve12 } from "node:path";
5891
5897
  var LOGGER_NAME = "mstar/status-gate";
5892
5898
  function harnessDocKindOfTarget(harnessDir, targetPath) {
5893
- const resolved = resolve13(targetPath);
5899
+ const resolved = resolve12(targetPath);
5894
5900
  const name = basename6(resolved);
5895
5901
  if (name !== STATUS_FILE && name !== WORKFLOW_SNAPSHOT_FILE && name !== PROJECT_REGISTER_FILE)
5896
5902
  return null;
@@ -5969,7 +5975,7 @@ function projectRegisterDocs(harnessDir) {
5969
5975
  if (!entry.isDirectory())
5970
5976
  continue;
5971
5977
  const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5972
- if (!existsSync15(registerPath))
5978
+ if (!existsSync14(registerPath))
5973
5979
  continue;
5974
5980
  try {
5975
5981
  registers.push({ projectId: entry.name, doc: readJson(registerPath) });
@@ -6042,7 +6048,7 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
6042
6048
  }
6043
6049
 
6044
6050
  // src/gates/skill-lint.ts
6045
- import { existsSync as existsSync16, readFileSync as readFileSync11 } from "node:fs";
6051
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
6046
6052
  import { basename as basename7, dirname as dirname9, resolve as resolve14, sep as sep2 } from "node:path";
6047
6053
  var SKILL_LINT_LOGGER = "mstar/skill-lint";
6048
6054
 
@@ -6123,7 +6129,7 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
6123
6129
  if (!isSkillTarget(roots, target))
6124
6130
  return;
6125
6131
  const skillPath = resolve14(target.displayPath);
6126
- if (!existsSync16(skillPath))
6132
+ if (!existsSync15(skillPath))
6127
6133
  return;
6128
6134
  let doc;
6129
6135
  try {
@@ -6174,7 +6180,7 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
6174
6180
  }
6175
6181
 
6176
6182
  // src/gates/seams.ts
6177
- import { existsSync as existsSync17, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
6183
+ import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
6178
6184
  import { basename as basename8, dirname as dirname10, join as join16, resolve as resolve15, sep as sep3 } from "node:path";
6179
6185
 
6180
6186
  // ../engine/dist/audit.js
@@ -6229,9 +6235,11 @@ var GITIGNORE_PROCESS_ENTRIES2 = GITIGNORE_SNIPPET2.split(`
6229
6235
  var GITIGNORE_PROCESS_ENTRIES_AGENTS2 = GITIGNORE_SNIPPET_AGENTS2.split(`
6230
6236
  `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
6231
6237
  var WHOLE_MATCH_PATTERNS = [
6232
- { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
6238
+ { type: "private-key", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g },
6233
6239
  { type: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
6234
6240
  { type: "github-token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g },
6241
+ { type: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
6242
+ { type: "stripe-live-key", re: /\bsk_live_[A-Za-z0-9]{16,}\b/g },
6235
6243
  { type: "slack-token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
6236
6244
  { type: "jwt", re: /\beyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}\b/g },
6237
6245
  { type: "api-secret-key", re: /\bsk-[A-Za-z0-9-]{20,}\b/g }
@@ -6242,6 +6250,28 @@ var VALUE_PATTERNS = [
6242
6250
  re: /(["']?)\b(password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|secret|token)\b(["']?)(\s*[:=]\s*)("[^"\n]{8,}"|'[^'\n]{8,}'|[A-Za-z0-9_./+\-=]{16,})/gi
6243
6251
  }
6244
6252
  ];
6253
+ var CI_IAC_LEAK_SHAPES = [
6254
+ {
6255
+ kind: "actions-plaintext-env",
6256
+ description: "GitHub Actions env assignment with plaintext literal",
6257
+ re: /^\s*(?:-\s+)?env:\s*[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|KEY)[A-Z0-9_]*\s*[:=]\s*["']?[A-Za-z0-9_/+=-]{8,}["']?\s*$/
6258
+ },
6259
+ {
6260
+ kind: "actions-secret-echo",
6261
+ description: "echo of a GitHub Actions secrets context value",
6262
+ re: /\becho\b[^#\n]*\$\{\{\s*secrets\.[A-Za-z0-9_]+\s*\}\}/
6263
+ },
6264
+ {
6265
+ kind: "dockerfile-credential-env",
6266
+ description: "Dockerfile ENV/ARG with credential-looking name",
6267
+ re: /^\s*(?:ENV|ARG)\s+[A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|APIKEY|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*\b/i
6268
+ },
6269
+ {
6270
+ kind: "terraform-hardcoded-password",
6271
+ description: "Terraform hardcoded password attribute",
6272
+ re: /^\s*password\s*=\s*"[^$\{][^"]*"\s*$/
6273
+ }
6274
+ ];
6245
6275
  function buildLineStarts(text) {
6246
6276
  const starts = [0];
6247
6277
  for (let i = 0;i < text.length; i++) {
@@ -6263,17 +6293,25 @@ function lineAt(starts, index) {
6263
6293
  }
6264
6294
  return lo + 1;
6265
6295
  }
6296
+ function lineStartOf(text, index) {
6297
+ return text.lastIndexOf(`
6298
+ `, index - 1) + 1;
6299
+ }
6266
6300
  function redactSecrets(text, filePath) {
6267
6301
  const starts = buildLineStarts(text);
6268
6302
  const marker = (type, index) => `[REDACTED ${type}@${lineAt(starts, index)}${filePath === undefined ? "" : ` in ${filePath}`}]`;
6269
- const replacements = [];
6270
- const findings = [];
6303
+ const spans = [];
6271
6304
  for (const pattern of WHOLE_MATCH_PATTERNS) {
6272
6305
  for (const match of text.matchAll(pattern.re)) {
6273
6306
  if (match.index === undefined)
6274
6307
  continue;
6275
- replacements.push({ index: match.index, length: match[0].length, text: marker(pattern.type, match.index) });
6276
- findings.push({ line: lineAt(starts, match.index), type: pattern.type });
6308
+ spans.push({
6309
+ start: match.index,
6310
+ end: match.index + match[0].length,
6311
+ priority: 0,
6312
+ text: marker(pattern.type, match.index),
6313
+ type: pattern.type
6314
+ });
6277
6315
  }
6278
6316
  }
6279
6317
  for (const pattern of VALUE_PATTERNS) {
@@ -6282,14 +6320,51 @@ function redactSecrets(text, filePath) {
6282
6320
  continue;
6283
6321
  const type = pattern.typeOf(match[2]);
6284
6322
  const replacement = `${match[1]}${match[2]}${match[3]}${match[4]}${marker(type, match.index)}`;
6285
- replacements.push({ index: match.index, length: match[0].length, text: replacement });
6286
- findings.push({ line: lineAt(starts, match.index), type });
6323
+ spans.push({ start: match.index, end: match.index + match[0].length, priority: 1, text: replacement, type });
6324
+ }
6325
+ }
6326
+ for (const shape of CI_IAC_LEAK_SHAPES) {
6327
+ const lineScoped = new RegExp(shape.re.source, shape.re.ignoreCase ? "gim" : "gm");
6328
+ for (const match of text.matchAll(lineScoped)) {
6329
+ if (match.index === undefined)
6330
+ continue;
6331
+ const lineEnd = text.indexOf(`
6332
+ `, match.index);
6333
+ const end = lineEnd === -1 ? text.length : lineEnd;
6334
+ spans.push({
6335
+ start: match.index,
6336
+ end,
6337
+ priority: 2,
6338
+ text: `${" ".repeat(match.index - lineStartOf(text, match.index))}${marker(shape.kind, match.index)}`,
6339
+ type: shape.kind
6340
+ });
6287
6341
  }
6288
6342
  }
6289
- replacements.sort((a, b) => b.index - a.index);
6343
+ spans.sort((a, b) => a.start - b.start || b.end - a.end || b.priority - a.priority);
6344
+ const merged = [];
6345
+ let groupMaxEnd = -1;
6346
+ let best = null;
6347
+ for (const span of spans) {
6348
+ if (span.start < groupMaxEnd) {
6349
+ if (groupMaxEnd < span.end)
6350
+ groupMaxEnd = span.end;
6351
+ if (span.end - span.start > best.end - best.start)
6352
+ best = span;
6353
+ } else {
6354
+ if (best !== null)
6355
+ merged.push(best);
6356
+ groupMaxEnd = span.end;
6357
+ best = span;
6358
+ }
6359
+ }
6360
+ if (best !== null)
6361
+ merged.push(best);
6290
6362
  let out = text;
6291
- for (const r of replacements)
6292
- out = out.slice(0, r.index) + r.text + out.slice(r.index + r.length);
6363
+ for (let i = merged.length - 1;i >= 0; i--) {
6364
+ const r = merged[i];
6365
+ out = out.slice(0, r.start) + r.text + out.slice(r.end);
6366
+ }
6367
+ const findings = merged.map((r) => ({ line: lineAt(starts, r.start), type: r.type }));
6293
6368
  const deduped = new Map;
6294
6369
  for (const f of findings)
6295
6370
  deduped.set(`${f.line}:${f.type}`, f);
@@ -6372,7 +6447,7 @@ function validateDesignDoc(doc, path) {
6372
6447
  const violations = [...validateDesignTokenFrontmatter(doc).violations];
6373
6448
  const isDark = basename8(path) === "DESIGN.dark.md";
6374
6449
  const sibling = join16(dirname10(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
6375
- if (existsSync17(sibling)) {
6450
+ if (existsSync16(sibling)) {
6376
6451
  const light = isDark ? readFileSync13(sibling, "utf8") : doc;
6377
6452
  const dark = isDark ? doc : readFileSync13(sibling, "utf8");
6378
6453
  violations.push(...assertLightDarkParity(light, dark).violations);
@@ -6399,7 +6474,7 @@ function validateRolesState(rolesDir, skillsRoot = dirname10(rolesDir)) {
6399
6474
  if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
6400
6475
  continue;
6401
6476
  const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
6402
- if (!existsSync17(skillFile))
6477
+ if (!existsSync16(skillFile))
6403
6478
  continue;
6404
6479
  try {
6405
6480
  skillTexts[entry.name] = readFileSync13(skillFile, "utf8");
@@ -6433,7 +6508,7 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
6433
6508
  if (!isSeamTarget(seam, harnessDir, target))
6434
6509
  return;
6435
6510
  const path = resolve15(target.displayPath);
6436
- if (!existsSync17(path))
6511
+ if (!existsSync16(path))
6437
6512
  return;
6438
6513
  let doc;
6439
6514
  try {
@@ -6503,7 +6578,7 @@ function lintRolesWrite(doc, options) {
6503
6578
  }
6504
6579
 
6505
6580
  // src/gates/tools.ts
6506
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
6581
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
6507
6582
  import { join as join17, resolve as resolve16 } from "node:path";
6508
6583
  import { defineTool } from "@deepseek-ai/dsh-tools";
6509
6584
  var ITERATION_VIOLATION_SCHEMA = {
@@ -6669,9 +6744,9 @@ function registerSddIterationTools(ctx, resolver) {
6669
6744
  presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
6670
6745
  isConcurrencySafe: () => false,
6671
6746
  async execute(args) {
6672
- if (!existsSync18(args.snapshot_path))
6747
+ if (!existsSync17(args.snapshot_path))
6673
6748
  throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
6674
- if (!existsSync18(args.compass_path))
6749
+ if (!existsSync17(args.compass_path))
6675
6750
  throw new Error(`compass file not found: ${args.compass_path}`);
6676
6751
  const snapshotDoc = readJson(args.snapshot_path);
6677
6752
  const compassDoc = parseCompassFrontmatter(args.compass_path);
@@ -6726,7 +6801,7 @@ function registerSeamTools(ctx, resolver) {
6726
6801
  async execute(args) {
6727
6802
  const abs = resolve16(args.dir);
6728
6803
  const lightPath = join17(abs, "DESIGN.md");
6729
- if (!existsSync18(lightPath))
6804
+ if (!existsSync17(lightPath))
6730
6805
  throw new Error(`design file not found: ${lightPath}`);
6731
6806
  const light = readFileSync14(lightPath, "utf8");
6732
6807
  const result = validateDesignDoc(light, lightPath);
@@ -6779,7 +6854,7 @@ function registerSeamTools(ctx, resolver) {
6779
6854
  isConcurrencySafe: () => false,
6780
6855
  async execute(args) {
6781
6856
  const abs = resolve16(args.plan_path);
6782
- if (!existsSync18(abs))
6857
+ if (!existsSync17(abs))
6783
6858
  throw new Error(`plan file not found: ${abs}`);
6784
6859
  const text = readFileSync14(abs, "utf8");
6785
6860
  const result = validateAuditDoc(text, abs);
@@ -6819,7 +6894,7 @@ function registerSeamTools(ctx, resolver) {
6819
6894
  isConcurrencySafe: () => false,
6820
6895
  async execute(args, exec) {
6821
6896
  const abs = resolve16(args.doc_path);
6822
- if (!existsSync18(abs))
6897
+ if (!existsSync17(abs))
6823
6898
  throw new Error(`knowledge doc not found: ${abs}`);
6824
6899
  const text = readFileSync14(abs, "utf8");
6825
6900
  const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
@@ -6864,7 +6939,7 @@ function registerSeamTools(ctx, resolver) {
6864
6939
  isConcurrencySafe: () => false,
6865
6940
  async execute(args) {
6866
6941
  const rolesDir = resolve16(args.roles_dir);
6867
- if (!existsSync18(join17(rolesDir, "SKILL.md")))
6942
+ if (!existsSync17(join17(rolesDir, "SKILL.md")))
6868
6943
  throw new Error(`roles dir not found: ${rolesDir}`);
6869
6944
  const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
6870
6945
  return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
@@ -6874,7 +6949,7 @@ function registerSeamTools(ctx, resolver) {
6874
6949
  }
6875
6950
 
6876
6951
  // src/gates/adapter.ts
6877
- import { existsSync as existsSync19 } from "node:fs";
6952
+ import { existsSync as existsSync18 } from "node:fs";
6878
6953
  import { join as join18 } from "node:path";
6879
6954
  import { Service as Service2 } from "@deepseek-ai/cordis";
6880
6955
  var HOST_LOGGER = "mstar/host-adapter";
@@ -6911,7 +6986,7 @@ class DshHostAdapter extends Service2 {
6911
6986
  this.logSink(level, msg);
6912
6987
  }
6913
6988
  statusGate(path, kind, harnessDir) {
6914
- if (!existsSync19(path))
6989
+ if (!existsSync18(path))
6915
6990
  return { ok: true, violations: [] };
6916
6991
  return validateStatusDoc(path, kind, harnessDir);
6917
6992
  }
@@ -6992,7 +7067,7 @@ class DshHostAdapter extends Service2 {
6992
7067
  }
6993
7068
 
6994
7069
  // src/gates/agent-personas.ts
6995
- import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync6 } from "node:fs";
7070
+ import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync7 } from "node:fs";
6996
7071
  import { join as join19, resolve as resolve17, sep as sep5 } from "node:path";
6997
7072
  var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
6998
7073
  var defaultCache = new Map;
@@ -7040,7 +7115,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
7040
7115
  return;
7041
7116
  let stat;
7042
7117
  try {
7043
- stat = statSync6(file);
7118
+ stat = statSync7(file);
7044
7119
  } catch {
7045
7120
  return;
7046
7121
  }
@@ -7239,7 +7314,7 @@ function log2(level, message) {
7239
7314
  }
7240
7315
 
7241
7316
  // src/gates/workflow-ledger.ts
7242
- import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
7317
+ import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
7243
7318
  import { join as join20 } from "node:path";
7244
7319
  var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
7245
7320
  var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
@@ -7309,7 +7384,7 @@ function saveWatermark(workflowDir, watermark) {
7309
7384
  try {
7310
7385
  const file = join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
7311
7386
  const tmp = `${file}.tmp`;
7312
- writeFileSync4(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
7387
+ writeFileSync5(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
7313
7388
  renameSync3(tmp, file);
7314
7389
  } catch (error) {
7315
7390
  log3("warn", `workflow-ledger watermark write failed for ${workflowDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
@@ -7523,7 +7598,7 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
7523
7598
  }
7524
7599
 
7525
7600
  // src/gates/goal-bridge.ts
7526
- import { existsSync as existsSync20, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
7601
+ import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
7527
7602
  import { join as join21 } from "node:path";
7528
7603
  var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
7529
7604
  var DEFAULT_MAX_GOAL_ROUNDS = 256;
@@ -7552,7 +7627,7 @@ function projectRegisterPointer(harnessDir) {
7552
7627
  if (!entry.isDirectory())
7553
7628
  continue;
7554
7629
  const registerPath = join21(projectsDir, entry.name, PROJECT_REGISTER_FILE);
7555
- if (existsSync20(registerPath))
7630
+ if (existsSync19(registerPath))
7556
7631
  return registerPath;
7557
7632
  }
7558
7633
  } catch {}
@@ -7567,7 +7642,7 @@ function iterationGoalObjective(iterationId) {
7567
7642
  }
7568
7643
  function steeringCompass(harnessDir) {
7569
7644
  const iterationsDir = resolveIterationDir(harnessDir);
7570
- if (!existsSync20(iterationsDir))
7645
+ if (!existsSync19(iterationsDir))
7571
7646
  return;
7572
7647
  let entries;
7573
7648
  try {
@@ -7579,7 +7654,7 @@ function steeringCompass(harnessDir) {
7579
7654
  if (!entry.isDirectory())
7580
7655
  continue;
7581
7656
  const compassPath = join21(iterationsDir, entry.name, "delivery-compass.md");
7582
- if (!existsSync20(compassPath))
7657
+ if (!existsSync19(compassPath))
7583
7658
  continue;
7584
7659
  let content;
7585
7660
  try {
@@ -7759,7 +7834,7 @@ function registerGoalBridge(ctx, resolver, config) {
7759
7834
  }
7760
7835
 
7761
7836
  // src/gates/plan-mode-bridge.ts
7762
- import { existsSync as existsSync21 } from "node:fs";
7837
+ import { existsSync as existsSync20 } from "node:fs";
7763
7838
  import { join as join22 } from "node:path";
7764
7839
  var PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
7765
7840
  var PLAN_STATUS_TODO = "Todo";
@@ -7777,7 +7852,7 @@ function errorMessage5(error) {
7777
7852
  }
7778
7853
  function hasPrepareWindow(harnessDir) {
7779
7854
  const statusPath = join22(harnessDir, STATUS_FILE);
7780
- if (!existsSync21(statusPath))
7855
+ if (!existsSync20(statusPath))
7781
7856
  return false;
7782
7857
  const selection = resolveReadWorkflow(harnessDir);
7783
7858
  if (selection.kind === "error")
@@ -7852,7 +7927,7 @@ function errorMessage6(error) {
7852
7927
  return error instanceof Error ? error.message : String(error);
7853
7928
  }
7854
7929
  function mstarLoadLine(roleId) {
7855
- return `Load mstar-roles (references/${roleId}.md) and the role's Required Skill Dependencies before acting.`;
7930
+ return `Load mstar-roles (references/${roleId}.md) first — identity comes before skills; load topic skills only when the Assignment activates them via its Skill presets field.`;
7856
7931
  }
7857
7932
  async function declareMstarSeeds(service, options) {
7858
7933
  const { agentsDir, log: log6 } = options;
@@ -8236,7 +8311,7 @@ var inject = ["loader"];
8236
8311
  function packagedCommandsDir() {
8237
8312
  try {
8238
8313
  const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
8239
- return existsSync22(dir) ? dir : undefined;
8314
+ return existsSync21(dir) ? dir : undefined;
8240
8315
  } catch {
8241
8316
  return;
8242
8317
  }