@mstar-harness/dsh 2.4.1 → 3.1.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 (80) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +10 -8
  3. package/README.zh.md +7 -5
  4. package/bundle/README.md +4 -4
  5. package/dist/client/panel/graph/project-graph.d.ts +29 -3
  6. package/dist/client/panel/locale.d.ts +1 -1
  7. package/dist/client/panel/zones/ProjectRollup.d.ts +24 -0
  8. package/dist/client.js +270 -34
  9. package/dist/gates/_shared.d.ts +4 -4
  10. package/dist/gates/adapter.d.ts +38 -12
  11. package/dist/gates/agent-flow.d.ts +117 -12
  12. package/dist/gates/dispatch.d.ts +16 -10
  13. package/dist/gates/goal-bridge.d.ts +1 -1
  14. package/dist/gates/status.d.ts +64 -14
  15. package/dist/gates/system-prompt.d.ts +6 -2
  16. package/dist/gates/workflow-ledger.d.ts +88 -17
  17. package/dist/gates/workflow-policy.d.ts +2 -1
  18. package/dist/gates/workflow-selection.d.ts +31 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1573 -690
  21. package/dist/service.d.ts +14 -9
  22. package/dist/types.d.ts +64 -2
  23. package/harness-agents/code-reviewer.md +0 -8
  24. package/harness-skills/mstar-branch-worktree/SKILL.md +14 -14
  25. package/harness-skills/mstar-branch-worktree/references/parallel-writable-pre-dispatch.md +2 -2
  26. package/harness-skills/mstar-compound/SKILL.md +2 -2
  27. package/harness-skills/mstar-compound/references/compound-workflow.md +3 -3
  28. package/harness-skills/mstar-compound-refresh/SKILL.md +4 -4
  29. package/harness-skills/mstar-compound-refresh/references/project-knowledge-bootstrap.md +1 -1
  30. package/harness-skills/mstar-dispatch-gates/SKILL.md +2 -2
  31. package/harness-skills/mstar-engine-legacy/SKILL.md +53 -0
  32. package/harness-skills/mstar-engine-legacy/references/anti-recursion-checklists.md +58 -0
  33. package/harness-skills/mstar-engine-legacy/references/engine-check-boilerplate.md +26 -0
  34. package/harness-skills/mstar-engine-legacy/references/lease-protocol.md +92 -0
  35. package/harness-skills/mstar-engine-legacy/references/qc-seat-n-restatements.md +57 -0
  36. package/harness-skills/mstar-engine-legacy/references/status-field-history.md +186 -0
  37. package/harness-skills/mstar-harness-core/SKILL.md +6 -3
  38. package/harness-skills/mstar-host/references/_shared/plan-mode-bridge-core.md +8 -8
  39. package/harness-skills/mstar-host/references/codex.md +1 -1
  40. package/harness-skills/mstar-host/references/cursor-plan-mode-bridge.md +19 -8
  41. package/harness-skills/mstar-host/references/cursor.md +5 -5
  42. package/harness-skills/mstar-host/references/dsh.md +10 -7
  43. package/harness-skills/mstar-host/references/kimi-plan-mode-bridge.md +3 -3
  44. package/harness-skills/mstar-host/references/kimi.md +1 -1
  45. package/harness-skills/mstar-host/references/omp-plan-mode-bridge.md +2 -2
  46. package/harness-skills/mstar-host/references/omp.md +2 -2
  47. package/harness-skills/mstar-host/references/zcode-plan-mode-bridge.md +3 -3
  48. package/harness-skills/mstar-host/references/zcode.md +1 -1
  49. package/harness-skills/mstar-iteration/SKILL.md +38 -37
  50. package/harness-skills/mstar-iteration/references/autonomous-direction-lock.md +2 -2
  51. package/harness-skills/mstar-iteration/references/iteration-compass-template.md +3 -3
  52. package/harness-skills/mstar-iteration/references/phase-2-worktree-lease.md +50 -38
  53. package/harness-skills/mstar-iteration/references/phase-3-iteration-close.md +7 -7
  54. package/harness-skills/mstar-iteration/references/phase-4-5-pr-delivery.md +2 -2
  55. package/harness-skills/mstar-phase-gates/SKILL.md +3 -3
  56. package/harness-skills/mstar-plan-artifacts/SKILL.md +21 -23
  57. package/harness-skills/mstar-plan-artifacts/references/knowledge-and-designs.md +16 -6
  58. package/harness-skills/mstar-plan-artifacts/references/plan-files-and-reports.md +10 -10
  59. package/harness-skills/mstar-plan-artifacts/references/status-and-residuals.md +190 -309
  60. package/harness-skills/mstar-plan-artifacts/templates/README.md +1 -3
  61. package/harness-skills/mstar-plan-artifacts/templates/status.empty.json +2 -4
  62. package/harness-skills/mstar-plan-conventions/SKILL.md +53 -13
  63. package/harness-skills/mstar-plan-conventions/references/artifact-storage-paths.md +7 -3
  64. package/harness-skills/mstar-plan-conventions/references/harness-bootstrap-and-agents-layering.md +9 -10
  65. package/harness-skills/mstar-project-governance/SKILL.md +119 -0
  66. package/harness-skills/mstar-review-qc/SKILL.md +3 -3
  67. package/harness-skills/mstar-roles/references/project-manager/dispatch-and-assignment.md +1 -1
  68. package/harness-skills/mstar-roles/references/project-manager/plan-management.md +6 -8
  69. package/harness-skills/mstar-roles/references/project-manager/qc-and-residuals.md +6 -6
  70. package/harness-skills/mstar-roles/references/project-manager.md +7 -7
  71. package/harness-skills/mstar-roles/references/qc-specialist/deep-review-lenses.md +1 -1
  72. package/harness-skills/mstar-roles/references/qc-specialist/report-template.md +1 -1
  73. package/harness-skills/mstar-roles/references/qc-specialist/reviewer-workflow.md +1 -1
  74. package/harness-skills/mstar-roles/references/qc-specialist-shared.md +2 -2
  75. package/harness-skills/mstar-sdd/SKILL.md +2 -2
  76. package/harness-skills/mstar-sdd/references/file-handoffs.md +1 -1
  77. package/package.json +14 -14
  78. package/harness-skills/mstar-plan-artifacts/references/done-compaction.md +0 -88
  79. package/harness-skills/mstar-plan-artifacts/templates/notes.empty.json +0 -5
  80. package/harness-skills/mstar-plan-artifacts/templates/plans-done.empty.json +0 -3
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ var require_lib = __commonJS((exports, module) => {
59
59
  is: () => is,
60
60
  isNonNullable: () => isNonNullable,
61
61
  isNullable: () => isNullable,
62
- isPlainObject: () => isPlainObject4,
62
+ isPlainObject: () => isPlainObject6,
63
63
  makeArray: () => makeArray,
64
64
  mapValues: () => mapValues,
65
65
  noop: () => noop,
@@ -82,7 +82,7 @@ var require_lib = __commonJS((exports, module) => {
82
82
  function isNonNullable(value) {
83
83
  return !isNullable(value);
84
84
  }
85
- function isPlainObject4(data) {
85
+ function isPlainObject6(data) {
86
86
  return data && typeof data === "object" && !Array.isArray(data);
87
87
  }
88
88
  function filterKeys(object, filter) {
@@ -1106,8 +1106,8 @@ var require_lib2 = __commonJS((exports, module) => {
1106
1106
  });
1107
1107
 
1108
1108
  // src/index.ts
1109
- import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "node:fs";
1110
- import { join as join20 } from "node:path";
1109
+ import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync14 } from "node:fs";
1110
+ import { join as join23 } from "node:path";
1111
1111
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1112
1112
  import {
1113
1113
  apply as applySkillLocal,
@@ -1120,24 +1120,26 @@ import {
1120
1120
  import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
1121
1121
  import { basename, dirname, join, resolve } from "node:path";
1122
1122
  import { fileURLToPath } from "node:url";
1123
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
1123
+ import { readFileSync as readFileSync2, statSync } from "node:fs";
1124
+ import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
1125
+ import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
1124
1126
  import { execFileSync } from "node:child_process";
1125
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
1126
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
1127
- import { join as join4, resolve as resolve4 } from "node:path";
1128
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "node:path";
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 existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } 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";
1129
1131
  import { AsyncLocalStorage } from "node:async_hooks";
1130
1132
  import { execFileSync as execFileSync2 } from "node:child_process";
1131
1133
  import { existsSync as existsSync3 } from "node:fs";
1132
- import { isAbsolute as isAbsolute3, resolve as resolve5 } from "node:path";
1134
+ import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
1133
1135
  import { execFileSync as execFileSync3 } from "node:child_process";
1134
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
1135
- import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute4, join as join5, resolve as resolve6 } from "node:path";
1136
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
1137
- import { existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
1138
- import { basename as basename4, isAbsolute as isAbsolute5, join as join8, relative as relative2, resolve as resolve8, sep } from "node:path";
1139
- import { existsSync as existsSync6 } from "node:fs";
1140
- import { join as join9 } from "node:path";
1136
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, 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 existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
1139
+ import { existsSync as existsSync6, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
1140
+ import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
1141
+ import { existsSync as existsSync7 } from "node:fs";
1142
+ import { join as join13 } from "node:path";
1141
1143
  var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
1142
1144
  function applyEnforcement(gate, opts) {
1143
1145
  return { ...gate, hardBlocked: opts.hard && gate.violations.length > 0 };
@@ -1189,23 +1191,111 @@ function harnessVersionFrom(moduleDir) {
1189
1191
  function readHarnessVersion() {
1190
1192
  return harnessVersionFrom(dirname(fileURLToPath(import.meta.url)));
1191
1193
  }
1194
+ var MSTARC_FILE = ".mstarc";
1195
+ var MSTARC_SECTION = "config";
1196
+ var MSTARC_HARNESS_DIR_KEY = "harness_dir";
1197
+ var MSTARC_PLAN_DIR_KEY = "plan_dir";
1198
+ var MSTARC_SDD_DIR_KEY = "sdd_dir";
1199
+ var MSTARC_ITERATION_DIR_KEY = "iteration_dir";
1200
+ var MSTARC_KNOWLEDGE_DIR_KEY = "knowledge_dir";
1201
+ var MSTARC_SPECS_DIR_KEY = "specs_dir";
1202
+ var MSTARC_WORKFLOW_DIR_KEY = "workflow_dir";
1203
+ var MSTARC_PROJECT_DIR_KEY = "project_dir";
1204
+ var MSTARC_ENFORCEMENT_KEY = "enforcement";
1205
+ var CONFIG_KEYS = {
1206
+ [MSTARC_HARNESS_DIR_KEY]: "harnessDir",
1207
+ [MSTARC_PLAN_DIR_KEY]: "planDir",
1208
+ [MSTARC_SDD_DIR_KEY]: "sddDir",
1209
+ [MSTARC_ITERATION_DIR_KEY]: "iterationDir",
1210
+ [MSTARC_KNOWLEDGE_DIR_KEY]: "knowledgeDir",
1211
+ [MSTARC_SPECS_DIR_KEY]: "specsDir",
1212
+ [MSTARC_WORKFLOW_DIR_KEY]: "workflowDir",
1213
+ [MSTARC_PROJECT_DIR_KEY]: "projectDir",
1214
+ [MSTARC_ENFORCEMENT_KEY]: "enforcement"
1215
+ };
1216
+ function parseMstarc(text) {
1217
+ let section = null;
1218
+ const out = {};
1219
+ for (const raw of text.split(/\r?\n/)) {
1220
+ const line = raw.trim();
1221
+ if (line === "" || line.startsWith("#") || line.startsWith(";"))
1222
+ continue;
1223
+ const header = /^\[([^\]]+)\]$/.exec(line);
1224
+ if (header !== null) {
1225
+ section = header[1].trim();
1226
+ continue;
1227
+ }
1228
+ if (section !== MSTARC_SECTION)
1229
+ continue;
1230
+ const eq = line.indexOf("=");
1231
+ if (eq === -1)
1232
+ continue;
1233
+ const field = CONFIG_KEYS[line.slice(0, eq).trim()];
1234
+ if (field === undefined)
1235
+ continue;
1236
+ const value = line.slice(eq + 1).trim();
1237
+ if (value === "")
1238
+ continue;
1239
+ if (field === "enforcement" && value !== "hard" && value !== "soft")
1240
+ continue;
1241
+ out[field] = value;
1242
+ }
1243
+ return out;
1244
+ }
1245
+ function isFile(file) {
1246
+ try {
1247
+ return statSync(file).isFile();
1248
+ } catch {
1249
+ return false;
1250
+ }
1251
+ }
1252
+ function findMstarc(startDir, boundary) {
1253
+ let dir = resolve2(startDir);
1254
+ const bound = resolve2(boundary);
1255
+ for (;; ) {
1256
+ if (!isAtOrBelow(dir, bound))
1257
+ return null;
1258
+ const candidate = join2(dir, MSTARC_FILE);
1259
+ if (isFile(candidate))
1260
+ return candidate;
1261
+ if (dir === bound)
1262
+ return null;
1263
+ const parent = dirname2(dir);
1264
+ if (parent === dir)
1265
+ return null;
1266
+ dir = parent;
1267
+ }
1268
+ }
1269
+ function loadMstarc(startDir, boundary) {
1270
+ const file = findMstarc(startDir, boundary);
1271
+ if (file === null)
1272
+ return null;
1273
+ return { file, dir: dirname2(file), config: parseMstarc(readFileSync2(file, "utf8")) };
1274
+ }
1275
+ function isAtOrBelow(dir, root) {
1276
+ const rel = relative(root, dir);
1277
+ return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1278
+ }
1192
1279
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
1193
- const start = resolve2(startDir);
1280
+ const start = resolve3(startDir);
1194
1281
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
1195
1282
  if (explicit)
1196
- return resolve2(start, explicit);
1197
- const boundary = resolve2(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
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);
1198
1288
  let dir = start;
1199
1289
  for (;; ) {
1200
- if (!isAtOrBelow(dir, boundary))
1290
+ if (!isAtOrBelow2(dir, boundary))
1201
1291
  return null;
1202
- for (const candidate of [join2(dir, ".mstar"), join2(dir, ".agents"), join2(dir, ".plans"), join2(dir, "plans")]) {
1292
+ for (const candidate of [join3(dir, ".mstar"), join3(dir, ".agents"), join3(dir, ".plans"), join3(dir, "plans")]) {
1203
1293
  if (isDirectory(candidate))
1204
1294
  return candidate;
1205
1295
  }
1206
1296
  if (dir === boundary)
1207
1297
  return null;
1208
- const parent = dirname2(dir);
1298
+ const parent = dirname3(dir);
1209
1299
  if (parent === dir)
1210
1300
  return null;
1211
1301
  dir = parent;
@@ -1223,15 +1313,21 @@ function defaultWorkspaceRoot(startDir) {
1223
1313
  let boundary = startDir;
1224
1314
  for (const segment of cdup.split(/[\\/]/)) {
1225
1315
  if (segment && segment !== ".")
1226
- boundary = dirname2(boundary);
1316
+ boundary = dirname3(boundary);
1227
1317
  }
1228
- return resolve2(boundary);
1318
+ return resolve3(boundary);
1229
1319
  } catch {}
1230
1320
  return startDir;
1231
1321
  }
1232
- function isAtOrBelow(dir, root) {
1233
- const rel = relative(root, dir);
1234
- return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
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;
1235
1331
  }
1236
1332
  function assertSafePathComponent(value, what) {
1237
1333
  if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
@@ -1240,10 +1336,30 @@ function assertSafePathComponent(value, what) {
1240
1336
  }
1241
1337
  function resolveSddDir(harnessDir, planId) {
1242
1338
  assertSafePathComponent(planId, "planId");
1243
- return join2(resolve2(harnessDir), "sdd", 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);
1244
1343
  }
1245
1344
  function resolveIterationDir(harnessDir) {
1246
- return join2(resolve2(harnessDir), "iterations");
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)`);
1354
+ }
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");
1247
1363
  }
1248
1364
  var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
1249
1365
  # Principle: process stays local; results are shared with the team.
@@ -1254,6 +1370,8 @@ var GITIGNORE_SNIPPET = `# Morning Star harness (.mstar/)
1254
1370
  !.mstar/knowledge/**
1255
1371
  !.mstar/specs/
1256
1372
  !.mstar/specs/**
1373
+ # .mstarc — repo-local harness config (may declare [config] harness_dir=<name>)
1374
+ .mstarc
1257
1375
  `;
1258
1376
  var GITIGNORE_SNIPPET_AGENTS = `# Morning Star harness (.agents/) — legacy
1259
1377
  # Default-ignore everything under .agents/, then re-include the tracked results.
@@ -1270,7 +1388,7 @@ var GITIGNORE_PROCESS_ENTRIES_AGENTS = GITIGNORE_SNIPPET_AGENTS.split(`
1270
1388
  `).filter((line) => line.startsWith(".agents/") || line.startsWith("!.agents/")).map((line) => line.trim());
1271
1389
  function isDirectory(dir) {
1272
1390
  try {
1273
- return statSync(dir).isDirectory();
1391
+ return statSync2(dir).isDirectory();
1274
1392
  } catch {
1275
1393
  return false;
1276
1394
  }
@@ -1314,7 +1432,7 @@ function validateExecutionLease(lease) {
1314
1432
  violations.push(violation("high", "lease.execution-lease.missing-worktree-path", "missing required field: worktree_path"));
1315
1433
  } else if (typeof lease.worktree_path !== "string" || lease.worktree_path.trim() === "") {
1316
1434
  violations.push(violation("medium", "lease.execution-lease.invalid-worktree-path", "worktree_path must be a non-empty string"));
1317
- } else if (!isAbsolute2(lease.worktree_path)) {
1435
+ } else if (!isAbsolute3(lease.worktree_path)) {
1318
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)"));
1319
1437
  }
1320
1438
  validateNonEmptyString(violations, lease.working_branch, "working_branch", "lease.execution-lease.missing-working-branch", "lease.execution-lease.invalid-working-branch");
@@ -1348,13 +1466,11 @@ function validateIntegrationMergeLease(lease) {
1348
1466
  return { ok: violations.length === 0, violations };
1349
1467
  }
1350
1468
  function planExecutionLeaseLocations(row) {
1351
- const meta = row.metadata;
1352
- const metadataLease = meta && typeof meta === "object" && !Array.isArray(meta) ? meta.execution_lease : undefined;
1353
- return { row: row.execution_lease, metadata: metadataLease };
1469
+ return { row: row.execution_lease };
1354
1470
  }
1355
1471
  function verifyPlanExecutionLease(row, planId) {
1356
- const { row: rowLease, metadata: metadataLease } = planExecutionLeaseLocations(row);
1357
- const lease = rowLease !== undefined ? rowLease : metadataLease;
1472
+ const { row: rowLease } = planExecutionLeaseLocations(row);
1473
+ const lease = rowLease;
1358
1474
  if (lease === undefined) {
1359
1475
  if (row.status === "InProgress") {
1360
1476
  return {
@@ -1367,16 +1483,11 @@ function verifyPlanExecutionLease(row, planId) {
1367
1483
  return {
1368
1484
  ok: false,
1369
1485
  violations: [
1370
- violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease (neither plans[].execution_lease nor legacy plans[].metadata.execution_lease)`)
1486
+ violation("high", "lease.verify.missing", `plan ${planId} has no execution_lease at the SSOT location plans[].execution_lease`)
1371
1487
  ]
1372
1488
  };
1373
1489
  }
1374
1490
  const violations = [];
1375
- if (rowLease !== undefined && metadataLease !== undefined) {
1376
- violations.push(violation("high", "lease.verify.dual-write", "execution_lease present in BOTH plans[].execution_lease (SSOT) and plans[].metadata.execution_lease — the row-level lease wins; delete the metadata copy to remove the dual write"));
1377
- } else if (rowLease === undefined) {
1378
- violations.push(violation("high", "lease.verify.non-ssot-location", "execution_lease found only under plans[].metadata.execution_lease — the SSOT location is plans[].execution_lease; the metadata location is a legacy/hand-written read-compat fallback, not equivalent to SSOT success (migrate the lease to the plan row)"));
1379
- }
1380
1491
  violations.push(...validateExecutionLease(lease).violations);
1381
1492
  return { ok: violations.length === 0, violations, lease };
1382
1493
  }
@@ -1602,14 +1713,124 @@ function antiRecursionPrecheck(subagentType, executeAs) {
1602
1713
  }
1603
1714
  return { ok: true, violations: [] };
1604
1715
  }
1716
+ var WORKFLOW_SNAPSHOT_FILE = "snapshot.json";
1717
+ var WORKFLOW_LIFECYCLE_STATUSES = ["running", "paused", "completed", "failed", "stopped"];
1718
+ var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
1719
+ var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
1720
+ function isPlainObject2(value) {
1721
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1722
+ }
1723
+ function violation3(severity, code, message, fix) {
1724
+ return { ok: false, severity, code, message, fix };
1725
+ }
1726
+ function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
1727
+ if (value === undefined) {
1728
+ violations.push(violation3("high", missingCode, `missing required field: ${field}`));
1729
+ } else if (typeof value !== "string" || value.trim() === "") {
1730
+ violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
1731
+ }
1732
+ }
1733
+ function validateWorkflowSnapshot(doc) {
1734
+ const violations = [];
1735
+ if (!isPlainObject2(doc)) {
1736
+ return {
1737
+ ok: false,
1738
+ violations: [violation3("high", "workflow.snapshot.invalid", "workflow snapshot must be an object")]
1739
+ };
1740
+ }
1741
+ if (doc.schema_version === undefined) {
1742
+ violations.push(violation3("high", "workflow.snapshot.missing-schema-version", "missing required field: schema_version"));
1743
+ } else if (doc.schema_version !== 1) {
1744
+ 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)`));
1745
+ }
1746
+ if (doc.version !== undefined) {
1747
+ 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"));
1748
+ }
1749
+ validateNonEmptyString2(violations, doc.id, "id", "workflow.snapshot.missing-id", "workflow.snapshot.invalid-id");
1750
+ if (doc.type === undefined) {
1751
+ violations.push(violation3("high", "workflow.snapshot.missing-type", "missing required field: type"));
1752
+ } else if (typeof doc.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(doc.type)) {
1753
+ violations.push(violation3("medium", "workflow.snapshot.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(doc.type)}`));
1754
+ }
1755
+ if (doc.status === undefined) {
1756
+ violations.push(violation3("high", "workflow.snapshot.missing-status", "missing required field: status"));
1757
+ } else if (typeof doc.status !== "string" || !WORKFLOW_LIFECYCLE_STATUSES.includes(doc.status)) {
1758
+ violations.push(violation3("medium", "workflow.snapshot.invalid-status", `status must be one of ${WORKFLOW_LIFECYCLE_STATUSES.join(" | ")} — got ${JSON.stringify(doc.status)}`));
1759
+ }
1760
+ validateNonEmptyString2(violations, doc.started_at, "started_at", "workflow.snapshot.missing-started-at", "workflow.snapshot.invalid-started-at");
1761
+ validateNonEmptyString2(violations, doc.updated_at, "updated_at", "workflow.snapshot.missing-updated-at", "workflow.snapshot.invalid-updated-at");
1762
+ if (doc.ended_at !== undefined) {
1763
+ validateNonEmptyString2(violations, doc.ended_at, "ended_at", "workflow.snapshot.missing-ended-at", "workflow.snapshot.invalid-ended-at");
1764
+ }
1765
+ if (doc.phase !== undefined && typeof doc.phase !== "string") {
1766
+ violations.push(violation3("medium", "workflow.snapshot.invalid-phase", "phase must be a string (free-form phase machine label)"));
1767
+ }
1768
+ if (doc.plans === undefined) {
1769
+ violations.push(violation3("high", "workflow.snapshot.missing-plans", "missing required field: plans"));
1770
+ } else if (!Array.isArray(doc.plans)) {
1771
+ violations.push(violation3("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
1772
+ } else {
1773
+ for (const row of doc.plans) {
1774
+ violations.push(...validatePlanRow(row).violations);
1775
+ if (isPlainObject2(row) && row.execution_lease !== undefined) {
1776
+ violations.push(...validateExecutionLease(row.execution_lease).violations);
1777
+ }
1778
+ }
1779
+ }
1780
+ if (doc.execution_policy !== undefined) {
1781
+ if (!isPlainObject2(doc.execution_policy)) {
1782
+ violations.push(violation3("medium", "workflow.snapshot.invalid-execution-policy", "execution_policy must be an object"));
1783
+ }
1784
+ }
1785
+ if (doc.integration_merge_lease !== undefined) {
1786
+ violations.push(...validateIntegrationMergeLease(doc.integration_merge_lease).violations);
1787
+ }
1788
+ if (doc.branch !== undefined) {
1789
+ if (!isPlainObject2(doc.branch)) {
1790
+ violations.push(violation3("medium", "workflow.snapshot.invalid-branch", "branch must be an object"));
1791
+ } else {
1792
+ for (const key of ["base", "integration", "target"]) {
1793
+ if (doc.branch[key] !== undefined && (typeof doc.branch[key] !== "string" || doc.branch[key].trim() === "")) {
1794
+ violations.push(violation3("medium", "workflow.snapshot.invalid-branch", `branch.${key} must be a non-empty string`));
1795
+ }
1796
+ }
1797
+ }
1798
+ }
1799
+ if (doc.control_worktree_path !== undefined) {
1800
+ validateNonEmptyString2(violations, doc.control_worktree_path, "control_worktree_path", "workflow.snapshot.missing-control-worktree-path", "workflow.snapshot.invalid-control-worktree-path");
1801
+ }
1802
+ if (doc.legacy_metadata !== undefined && !isPlainObject2(doc.legacy_metadata)) {
1803
+ violations.push(violation3("medium", "workflow.snapshot.invalid-legacy-metadata", "legacy_metadata must be an object"));
1804
+ }
1805
+ if (doc.compass_ref !== undefined) {
1806
+ validateNonEmptyString2(violations, doc.compass_ref, "compass_ref", "workflow.snapshot.missing-compass-ref", "workflow.snapshot.invalid-compass-ref");
1807
+ }
1808
+ const terminal = typeof doc.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(doc.status);
1809
+ if (terminal) {
1810
+ if (doc.ended_at === undefined) {
1811
+ 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`));
1812
+ }
1813
+ if (Array.isArray(doc.plans)) {
1814
+ for (const row of doc.plans) {
1815
+ if (isPlainObject2(row) && row.execution_lease !== undefined) {
1816
+ 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`));
1817
+ }
1818
+ }
1819
+ }
1820
+ if (doc.integration_merge_lease !== undefined) {
1821
+ 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"));
1822
+ }
1823
+ }
1824
+ return { ok: violations.length === 0, violations };
1825
+ }
1605
1826
  var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1606
1827
  var PLAN_STATUSES = ["Todo", "InProgress", "InReview", "Blocked", "Done"];
1607
1828
  var RESIDUAL_DECISIONS = ["defer", "accept", "risk-accepted"];
1608
1829
  var RESIDUAL_LIFECYCLES = ["open", "resolved", "waived", "superseded", "duplicate"];
1609
- function isPlainObject2(value) {
1830
+ function isPlainObject3(value) {
1610
1831
  return typeof value === "object" && value !== null && !Array.isArray(value);
1611
1832
  }
1612
- function violation3(severity, code, message, fix) {
1833
+ function violation4(severity, code, message, fix) {
1613
1834
  return { ok: false, severity, code, message, fix };
1614
1835
  }
1615
1836
  function normalizeSeverity(value) {
@@ -1624,217 +1845,242 @@ function isOpenResidual(entry) {
1624
1845
  const effective = lifecycle === false || lifecycle === null || lifecycle === undefined ? "open" : lifecycle;
1625
1846
  return effective === "open";
1626
1847
  }
1627
- function validateNonEmptyString2(violations, value, field, missingCode, invalidCode) {
1848
+ function validateNonEmptyString3(violations, value, field, missingCode, invalidCode) {
1628
1849
  if (value === undefined) {
1629
- violations.push(violation3("high", missingCode, `missing required field: ${field}`));
1850
+ violations.push(violation4("high", missingCode, `missing required field: ${field}`));
1630
1851
  } else if (typeof value !== "string" || value.trim() === "") {
1631
- violations.push(violation3("medium", invalidCode, `${field} must be a non-empty string`));
1852
+ violations.push(violation4("medium", invalidCode, `${field} must be a non-empty string`));
1632
1853
  }
1633
1854
  }
1634
1855
  function validatePlanRow(row) {
1635
1856
  const violations = [];
1636
- if (!isPlainObject2(row)) {
1637
- return { ok: false, violations: [violation3("high", "status.plan-row.invalid", "plan row must be an object")] };
1857
+ if (!isPlainObject3(row)) {
1858
+ return { ok: false, violations: [violation4("high", "status.plan-row.invalid", "plan row must be an object")] };
1638
1859
  }
1639
1860
  const { id, plan_id: planId, title, file, status, metadata, execution_lease } = row;
1640
1861
  if (id === undefined && planId === undefined) {
1641
- violations.push(violation3("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
1862
+ violations.push(violation4("high", "status.plan-row.missing-id", "missing required field: id (or legacy plan_id)"));
1642
1863
  } else {
1643
1864
  if (id !== undefined) {
1644
- validateNonEmptyString2(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
1865
+ validateNonEmptyString3(violations, id, "id", "status.plan-row.missing-id", "status.plan-row.invalid-id");
1645
1866
  }
1646
1867
  if (planId !== undefined) {
1647
- validateNonEmptyString2(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
1868
+ validateNonEmptyString3(violations, planId, "plan_id", "status.plan-row.missing-plan-id", "status.plan-row.invalid-plan-id");
1648
1869
  }
1649
1870
  if (id !== undefined && planId !== undefined && id !== planId) {
1650
- violations.push(violation3("medium", "status.plan-row.dual-id", "row has both id and plan_id with different values — write one canonical key (prefer id)"));
1871
+ 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)"));
1651
1872
  }
1652
1873
  }
1653
- validateNonEmptyString2(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
1654
- validateNonEmptyString2(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
1874
+ validateNonEmptyString3(violations, title, "title", "status.plan-row.missing-title", "status.plan-row.invalid-title");
1875
+ validateNonEmptyString3(violations, file, "file", "status.plan-row.missing-file", "status.plan-row.invalid-file");
1655
1876
  if (status === undefined) {
1656
- violations.push(violation3("high", "status.plan-row.missing-status", "missing required field: status"));
1877
+ violations.push(violation4("high", "status.plan-row.missing-status", "missing required field: status"));
1657
1878
  } else if (typeof status !== "string" || !PLAN_STATUSES.includes(status)) {
1658
- violations.push(violation3("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
1879
+ violations.push(violation4("medium", "status.plan-row.invalid-status", `status must be one of ${PLAN_STATUSES.join(" | ")} — got ${JSON.stringify(status)}`));
1659
1880
  }
1660
- if (metadata !== undefined && !isPlainObject2(metadata)) {
1661
- violations.push(violation3("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
1881
+ if (metadata !== undefined && !isPlainObject3(metadata)) {
1882
+ violations.push(violation4("medium", "status.plan-row.invalid-metadata", "metadata must be an object"));
1662
1883
  }
1663
- if (execution_lease !== undefined && !isPlainObject2(execution_lease)) {
1664
- violations.push(violation3("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
1884
+ if (execution_lease !== undefined && !isPlainObject3(execution_lease)) {
1885
+ violations.push(violation4("medium", "status.plan-row.invalid-execution-lease", "execution_lease must be an object"));
1665
1886
  }
1666
1887
  if (status === "Done" && execution_lease !== undefined) {
1667
- violations.push(violation3("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"'));
1888
+ 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"'));
1668
1889
  }
1669
1890
  return { ok: violations.length === 0, violations };
1670
1891
  }
1671
1892
  function validateResidual(entry) {
1672
1893
  const violations = [];
1673
- if (!isPlainObject2(entry)) {
1674
- return { ok: false, violations: [violation3("high", "status.residual.invalid", "residual entry must be an object")] };
1894
+ if (!isPlainObject3(entry)) {
1895
+ return { ok: false, violations: [violation4("high", "status.residual.invalid", "residual entry must be an object")] };
1675
1896
  }
1676
1897
  const { id, title, severity, source, scope, decision, owner, target, tracking, detail_doc, lifecycle, closed_at } = entry;
1677
- validateNonEmptyString2(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
1678
- validateNonEmptyString2(violations, title, "title", "status.residual.missing-title", "status.residual.invalid-title");
1679
- validateNonEmptyString2(violations, source, "source", "status.residual.missing-source", "status.residual.invalid-source");
1680
- validateNonEmptyString2(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
1681
- validateNonEmptyString2(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
1898
+ validateNonEmptyString3(violations, id, "id", "status.residual.missing-id", "status.residual.invalid-id");
1899
+ validateNonEmptyString3(violations, title, "title", "status.residual.missing-title", "status.residual.invalid-title");
1900
+ validateNonEmptyString3(violations, source, "source", "status.residual.missing-source", "status.residual.invalid-source");
1901
+ validateNonEmptyString3(violations, scope, "scope", "status.residual.missing-scope", "status.residual.invalid-scope");
1902
+ validateNonEmptyString3(violations, owner, "owner", "status.residual.missing-owner", "status.residual.invalid-owner");
1682
1903
  if (severity === undefined) {
1683
- violations.push(violation3("high", "status.residual.missing-severity", "missing required field: severity"));
1904
+ violations.push(violation4("high", "status.residual.missing-severity", "missing required field: severity"));
1684
1905
  } else if (typeof severity !== "string" || !SEVERITY_ORDER.includes(severity) && severity !== "warning") {
1685
- violations.push(violation3("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
1906
+ violations.push(violation4("medium", "status.residual.invalid-severity", `severity must be one of ${SEVERITY_ORDER.join(" | ")} — got ${JSON.stringify(severity)}`));
1686
1907
  } else if (severity === "warning") {
1687
- violations.push(violation3("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')`));
1908
+ 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')`));
1688
1909
  }
1689
1910
  if (decision === undefined) {
1690
- violations.push(violation3("high", "status.residual.missing-decision", "missing required field: decision"));
1911
+ violations.push(violation4("high", "status.residual.missing-decision", "missing required field: decision"));
1691
1912
  } else if (typeof decision !== "string" || !RESIDUAL_DECISIONS.includes(decision)) {
1692
- violations.push(violation3("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
1913
+ violations.push(violation4("medium", "status.residual.invalid-decision", `decision must be one of ${RESIDUAL_DECISIONS.join(" | ")} — got ${JSON.stringify(decision)}`));
1693
1914
  }
1694
1915
  if (target === undefined) {
1695
- violations.push(violation3("high", "status.residual.missing-target", "missing required field: target"));
1916
+ violations.push(violation4("high", "status.residual.missing-target", "missing required field: target"));
1696
1917
  } else if (typeof target !== "string" && target !== null) {
1697
- violations.push(violation3("medium", "status.residual.invalid-target", "target must be a string or null"));
1918
+ violations.push(violation4("medium", "status.residual.invalid-target", "target must be a string or null"));
1698
1919
  }
1699
1920
  if (tracking === undefined) {
1700
- violations.push(violation3("high", "status.residual.missing-tracking", "missing required field: tracking"));
1921
+ violations.push(violation4("high", "status.residual.missing-tracking", "missing required field: tracking"));
1701
1922
  } else if (typeof tracking !== "string" && tracking !== null) {
1702
- violations.push(violation3("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
1923
+ violations.push(violation4("medium", "status.residual.invalid-tracking", "tracking must be a string or null"));
1703
1924
  }
1704
1925
  if (detail_doc !== undefined && typeof detail_doc !== "string" && detail_doc !== null) {
1705
- violations.push(violation3("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
1926
+ violations.push(violation4("medium", "status.residual.invalid-detail-doc", "detail_doc must be a string or null"));
1706
1927
  }
1707
1928
  if (closed_at !== undefined && (typeof closed_at !== "string" || !DATE_RE.test(closed_at))) {
1708
- violations.push(violation3("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
1929
+ violations.push(violation4("medium", "status.residual.invalid-closed-at", "closed_at must be YYYY-MM-DD"));
1709
1930
  }
1710
1931
  if (lifecycle !== undefined) {
1711
1932
  if (typeof lifecycle !== "string" || !RESIDUAL_LIFECYCLES.includes(lifecycle)) {
1712
- violations.push(violation3("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
1933
+ violations.push(violation4("medium", "status.residual.invalid-lifecycle", `lifecycle must be one of ${RESIDUAL_LIFECYCLES.join(" | ")} — got ${JSON.stringify(lifecycle)}`));
1713
1934
  } else if (lifecycle !== "open") {
1714
1935
  if (closed_at === undefined) {
1715
- violations.push(violation3("high", "status.residual.closed-missing-closed-at", `lifecycle "${lifecycle}" requires closed_at (YYYY-MM-DD)`, 'set closed_at (e.g. "2026-08-08")'));
1936
+ 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")'));
1716
1937
  }
1717
1938
  if (entry.closure_note === undefined) {
1718
- violations.push(violation3("medium", "status.residual.closed-missing-closure-note", `lifecycle "${lifecycle}" requires closure_note (what changed; how verified)`, "add closure_note explaining the close"));
1939
+ 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"));
1719
1940
  }
1720
1941
  }
1721
1942
  }
1722
1943
  return { ok: violations.length === 0, violations };
1723
1944
  }
1724
- function validateStatus(docOrPath) {
1945
+ function isHarnessRelativePath(dir) {
1946
+ if (dir.startsWith("/") || dir.startsWith("\\"))
1947
+ return false;
1948
+ if (/^[A-Za-z]:[\\/]/.test(dir))
1949
+ return false;
1950
+ return !dir.split(/[\\/]+/).includes("..");
1951
+ }
1952
+ function validateWorkflowEntry(entry) {
1953
+ const violations = [];
1954
+ if (!isPlainObject3(entry)) {
1955
+ return {
1956
+ ok: false,
1957
+ violations: [violation4("high", "status.workflow.invalid", "workflow entry must be an object")]
1958
+ };
1959
+ }
1960
+ validateNonEmptyString3(violations, entry.id, "id", "status.workflow.missing-id", "status.workflow.invalid-id");
1961
+ if (entry.type === undefined) {
1962
+ violations.push(violation4("high", "status.workflow.missing-type", "missing required field: type"));
1963
+ } else if (typeof entry.type !== "string" || !WORKFLOW_LIFECYCLE_TYPES.includes(entry.type)) {
1964
+ violations.push(violation4("medium", "status.workflow.invalid-type", `type must be one of ${WORKFLOW_LIFECYCLE_TYPES.join(" | ")} — got ${JSON.stringify(entry.type)}`));
1965
+ }
1966
+ validateNonEmptyString3(violations, entry.started_at, "started_at", "status.workflow.missing-started-at", "status.workflow.invalid-started-at");
1967
+ if (entry.dir === undefined) {
1968
+ violations.push(violation4("high", "status.workflow.missing-dir", "missing required field: dir"));
1969
+ } else if (typeof entry.dir !== "string" || entry.dir.trim() === "") {
1970
+ violations.push(violation4("medium", "status.workflow.invalid-dir", "dir must be a non-empty string"));
1971
+ } else if (!isHarnessRelativePath(entry.dir)) {
1972
+ 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)}`));
1973
+ }
1974
+ return { ok: violations.length === 0, violations };
1975
+ }
1976
+ function validateStatusV2(docOrPath, opts = {}) {
1725
1977
  let doc;
1978
+ let harnessDir = opts.harnessDir;
1726
1979
  if (typeof docOrPath === "string") {
1727
1980
  try {
1728
1981
  doc = readJson(docOrPath);
1982
+ harnessDir = dirname5(resolve5(docOrPath));
1729
1983
  } catch (error) {
1730
1984
  return {
1731
1985
  ok: false,
1732
- violations: [violation3("high", "status.invalid-json", error.message)]
1986
+ violations: [violation4("high", "status.invalid-json", error.message)]
1733
1987
  };
1734
1988
  }
1735
1989
  } else {
1736
1990
  doc = docOrPath;
1737
1991
  }
1738
- const violations = [];
1739
- const { version, updated_at, plans, residual_findings, metadata } = doc;
1740
- if (version === undefined) {
1741
- violations.push(violation3("high", "status.missing-version", "missing required field: version"));
1742
- } else if (typeof version !== "number" || !Number.isInteger(version)) {
1743
- violations.push(violation3("high", "status.invalid-version", "version must be an integer"));
1744
- } else if (version !== 1) {
1745
- violations.push(violation3("medium", "status.unsupported-version", `unsupported status.json schema version ${version} — expected 1`));
1746
- }
1747
- if (updated_at === undefined) {
1748
- violations.push(violation3("high", "status.missing-updated-at", "missing required field: updated_at"));
1749
- } else if (typeof updated_at !== "string" || !DATE_RE.test(updated_at)) {
1750
- violations.push(violation3("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
1751
- }
1752
- if (plans === undefined) {
1753
- violations.push(violation3("high", "status.missing-plans", "missing required field: plans"));
1754
- } else if (!Array.isArray(plans)) {
1755
- violations.push(violation3("high", "status.invalid-plans", "plans must be an array"));
1756
- } else {
1757
- for (const row of plans) {
1758
- violations.push(...validatePlanRow(row).violations);
1759
- }
1992
+ if (!isPlainObject3(doc)) {
1993
+ return { ok: false, violations: [violation4("high", "status.invalid-doc", "status document must be an object")] };
1760
1994
  }
1761
- if (residual_findings === undefined) {
1762
- violations.push(violation3("high", "status.missing-residual-findings", "missing required field: residual_findings (root-only canonical)"));
1763
- } else if (!isPlainObject2(residual_findings)) {
1764
- violations.push(violation3("high", "status.invalid-residual-findings", "residual_findings must be an object at root"));
1765
- } else {
1766
- for (const [planId, list] of Object.entries(residual_findings)) {
1767
- if (!Array.isArray(list)) {
1768
- violations.push(violation3("high", "status.residual.invalid-list", `residual_findings["${planId}"] must be an array`));
1769
- } else if (list.length === 0) {
1770
- violations.push(violation3("low", "status.residual.empty-key", `residual_findings["${planId}"] is empty — delete the key (no "plan-id": [])`));
1771
- } else {
1772
- for (const entry of list) {
1773
- violations.push(...validateResidual(entry).violations);
1774
- }
1775
- }
1776
- }
1995
+ if (doc.version !== 2) {
1996
+ return {
1997
+ ok: false,
1998
+ violations: [
1999
+ 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`")
2000
+ ]
2001
+ };
1777
2002
  }
1778
- if (metadata === undefined) {
1779
- violations.push(violation3("high", "status.missing-metadata", "missing required field: metadata"));
1780
- } else if (!isPlainObject2(metadata)) {
1781
- violations.push(violation3("high", "status.invalid-metadata", "metadata must be an object"));
1782
- } else if (Object.prototype.hasOwnProperty.call(metadata, "residual_findings")) {
1783
- violations.push(violation3("medium", "status.dual-write-residuals", "residual_findings must be root-only — metadata.residual_findings is legacy read-only; remove it (no dual-write)", "move entries to root residual_findings and delete metadata.residual_findings"));
2003
+ if (Array.isArray(doc.plans)) {
2004
+ return {
2005
+ ok: false,
2006
+ violations: [
2007
+ 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`")
2008
+ ]
2009
+ };
1784
2010
  }
1785
- return { ok: violations.length === 0, violations };
1786
- }
1787
- function planFindingsCleanup(doc, planId) {
1788
- if (!Array.isArray(doc.plans))
1789
- return;
1790
- for (const row of doc.plans) {
1791
- if (!isPlainObject2(row))
1792
- continue;
1793
- const rowId = row.id ?? row.plan_id;
1794
- if (rowId !== planId)
1795
- continue;
1796
- if (!isPlainObject2(row.metadata))
1797
- return;
1798
- const mode = row.metadata.findings_cleanup;
1799
- if (mode === "zero-residual" || mode === "allow-residual")
1800
- return mode;
1801
- return;
2011
+ if (doc.residual_findings !== undefined) {
2012
+ return {
2013
+ ok: false,
2014
+ violations: [
2015
+ 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`")
2016
+ ]
2017
+ };
1802
2018
  }
1803
- return;
1804
- }
1805
- function openResidualsOf(doc, planId) {
1806
- if (!isPlainObject2(doc.residual_findings))
1807
- return [];
1808
- const list = doc.residual_findings[planId];
1809
- if (!Array.isArray(list))
1810
- return [];
1811
- return list.filter((entry) => isPlainObject2(entry) && isOpenResidual(entry));
1812
- }
1813
- function findingsCleanupGate(doc, planId, opts) {
1814
- const mode = opts?.mode ?? planFindingsCleanup(doc, planId) ?? "allow-residual";
1815
2019
  const violations = [];
1816
- const residuals = openResidualsOf(doc, planId);
1817
- for (const entry of residuals) {
1818
- const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
1819
- const label = `R#${id}`;
1820
- if (mode === "zero-residual") {
1821
- if (entry.severity === "nit") {
1822
- violations.push(violation3("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
1823
- } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
1824
- violations.push(violation3("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
1825
- } else if (entry.decision === "defer") {
1826
- if (typeof entry.target !== "string" || entry.target.trim() === "") {
1827
- violations.push(violation3("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
2020
+ if (doc.updated_at === undefined) {
2021
+ violations.push(violation4("high", "status.missing-updated-at", "missing required field: updated_at"));
2022
+ } else if (typeof doc.updated_at !== "string" || !DATE_RE.test(doc.updated_at)) {
2023
+ violations.push(violation4("medium", "status.invalid-updated-at", "updated_at must be YYYY-MM-DD"));
2024
+ }
2025
+ if (doc.workflows === undefined) {
2026
+ violations.push(violation4("high", "status.missing-workflows", "missing required field: workflows"));
2027
+ } else if (!Array.isArray(doc.workflows)) {
2028
+ violations.push(violation4("high", "status.invalid-workflows", "workflows must be an array"));
2029
+ } else {
2030
+ const seen = new Set;
2031
+ for (const entry of doc.workflows) {
2032
+ violations.push(...validateWorkflowEntry(entry).violations);
2033
+ if (isPlainObject3(entry) && typeof entry.id === "string") {
2034
+ if (seen.has(entry.id)) {
2035
+ violations.push(violation4("medium", "status.workflow.duplicate-id", `duplicate workflow id in workflows[]: ${JSON.stringify(entry.id)}`));
1828
2036
  }
1829
- } else {
1830
- violations.push(violation3("medium", "findings.zero-residual-open-fixable", `${label}: fixable finding must not remain open under zero-residual — fix now or convert to a blocker-defer`));
2037
+ seen.add(entry.id);
2038
+ }
2039
+ }
2040
+ }
2041
+ if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
2042
+ let realHarnessDir = null;
2043
+ try {
2044
+ realHarnessDir = realpathSync(harnessDir);
2045
+ } catch {}
2046
+ for (const entry of doc.workflows) {
2047
+ if (!isPlainObject3(entry) || typeof entry.dir !== "string")
2048
+ continue;
2049
+ const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
2050
+ const snapshotPath = join6(harnessDir, relSnapshot);
2051
+ const label = typeof entry.id === "string" ? entry.id : relSnapshot;
2052
+ let physical;
2053
+ try {
2054
+ physical = realpathSync(snapshotPath);
2055
+ } catch {
2056
+ 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`));
2057
+ continue;
2058
+ }
2059
+ if (realHarnessDir !== null && physical !== realHarnessDir && !physical.startsWith(`${realHarnessDir}${sep}`)) {
2060
+ 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)}`));
2061
+ continue;
2062
+ }
2063
+ let snapshot;
2064
+ try {
2065
+ snapshot = readJson(snapshotPath);
2066
+ } catch (error) {
2067
+ violations.push(violation4("high", "status.workflow.snapshot-invalid", `snapshot at ${JSON.stringify(relSnapshot)} is not valid JSON: ${error.message}`));
2068
+ continue;
2069
+ }
2070
+ if (typeof snapshot.status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(snapshot.status)) {
2071
+ 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`));
2072
+ }
2073
+ if (typeof entry.type === "string" && typeof snapshot.type === "string" && entry.type !== snapshot.type) {
2074
+ 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`));
2075
+ }
2076
+ if (typeof entry.started_at === "string" && typeof snapshot.started_at === "string" && entry.started_at !== snapshot.started_at) {
2077
+ 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)} — the root entry mirrors the snapshot; align them`));
1831
2078
  }
1832
- } else if (normalizeSeverity(entry.severity) === "critical") {
1833
- violations.push(violation3("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
1834
2079
  }
1835
2080
  }
1836
2081
  return { ok: violations.length === 0, violations };
1837
2082
  }
2083
+ var validateStatus = validateStatusV2;
1838
2084
  function resolveCompassEnforcement(harnessDir) {
1839
2085
  const iterationsDir = resolveIterationDir(harnessDir);
1840
2086
  if (!existsSync2(iterationsDir))
@@ -1848,12 +2094,12 @@ function resolveCompassEnforcement(harnessDir) {
1848
2094
  for (const entry of entries) {
1849
2095
  if (!entry.isDirectory())
1850
2096
  continue;
1851
- const compassPath = join4(iterationsDir, entry.name, "delivery-compass.md");
2097
+ const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
1852
2098
  if (!existsSync2(compassPath))
1853
2099
  continue;
1854
2100
  let content;
1855
2101
  try {
1856
- content = readFileSync3(compassPath, "utf8");
2102
+ content = readFileSync4(compassPath, "utf8");
1857
2103
  } catch {
1858
2104
  continue;
1859
2105
  }
@@ -1867,6 +2113,22 @@ function resolveCompassEnforcement(harnessDir) {
1867
2113
  }
1868
2114
  return { hard: false, source: "none" };
1869
2115
  }
2116
+ function resolveMstarcEnforcement(harnessDir) {
2117
+ const dir = resolve5(harnessDir);
2118
+ const rc = loadMstarc(dir, dirname5(dir));
2119
+ const value = rc?.config.enforcement;
2120
+ if (value === "hard")
2121
+ return { hard: true, source: "mstarc" };
2122
+ if (value === "soft")
2123
+ return { hard: false, source: "mstarc" };
2124
+ return { hard: false, source: "none" };
2125
+ }
2126
+ function resolveRepoEnforcement(harnessDir) {
2127
+ const rc = resolveMstarcEnforcement(harnessDir);
2128
+ if (rc.source !== "none")
2129
+ return rc;
2130
+ return resolveCompassEnforcement(harnessDir);
2131
+ }
1870
2132
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
1871
2133
  function probeTimeoutMs() {
1872
2134
  const raw = process.env.MSTAR_GIT_PROBE_TIMEOUT_MS;
@@ -1875,7 +2137,7 @@ function probeTimeoutMs() {
1875
2137
  const parsed = Number(raw);
1876
2138
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_PROBE_TIMEOUT_MS;
1877
2139
  }
1878
- function violation4(severity, code, message, fix) {
2140
+ function violation5(severity, code, message, fix) {
1879
2141
  return { ok: false, severity, code, message, fix };
1880
2142
  }
1881
2143
  function gate(violations) {
@@ -1909,25 +2171,25 @@ function l1PreDispatchCheck(input, opts = {}) {
1909
2171
  const violations = [];
1910
2172
  const { controlWorktreePath, leaseWorktreePath, leaseWorkingBranch, planId } = input;
1911
2173
  if (controlWorktreePath.trim() === "") {
1912
- violations.push(violation4("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"));
2174
+ 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"));
1913
2175
  }
1914
2176
  if (leaseWorktreePath.trim() === "") {
1915
- violations.push(violation4("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"));
2177
+ 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"));
1916
2178
  }
1917
2179
  if (leaseWorkingBranch.trim() === "") {
1918
- violations.push(violation4("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
2180
+ 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"));
1919
2181
  }
1920
- if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve5(controlWorktreePath) === resolve5(leaseWorktreePath)) {
1921
- violations.push(violation4("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"));
2182
+ if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
2183
+ 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"));
1922
2184
  }
1923
2185
  if (leaseWorktreePath !== "" && !existsSync3(leaseWorktreePath)) {
1924
- violations.push(violation4("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>`));
2186
+ 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>`));
1925
2187
  } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
1926
2188
  const probe = probeBranch(leaseWorktreePath, opts);
1927
2189
  if ("error" in probe) {
1928
- violations.push(violation4("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)"));
2190
+ 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)"));
1929
2191
  } else if (probe.branch !== leaseWorkingBranch) {
1930
- violations.push(violation4("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`));
2192
+ 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`));
1931
2193
  }
1932
2194
  }
1933
2195
  return gate(violations);
@@ -1937,32 +2199,32 @@ function l2PreDispatchCheck(input, opts = {}) {
1937
2199
  const tracks = input.tracks ?? [];
1938
2200
  const seenPaths = new Set;
1939
2201
  if (tracks.length < 1) {
1940
- violations.push(violation4("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"));
2202
+ 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"));
1941
2203
  }
1942
2204
  tracks.forEach((track, index) => {
1943
2205
  if (track.worktreePath.trim() === "" || track.workingBranch.trim() === "") {
1944
- violations.push(violation4("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2206
+ violations.push(violation5("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
1945
2207
  return;
1946
2208
  }
1947
- if (!isAbsolute3(track.worktreePath)) {
1948
- violations.push(violation4("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>)`));
2209
+ if (!isAbsolute4(track.worktreePath)) {
2210
+ 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>)`));
1949
2211
  return;
1950
2212
  }
1951
- const normalized = resolve5(track.worktreePath);
2213
+ const normalized = resolve6(track.worktreePath);
1952
2214
  if (seenPaths.has(normalized)) {
1953
- violations.push(violation4("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"));
2215
+ 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"));
1954
2216
  return;
1955
2217
  }
1956
2218
  seenPaths.add(normalized);
1957
2219
  if (!existsSync3(track.worktreePath)) {
1958
- violations.push(violation4("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
2220
+ 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}`));
1959
2221
  return;
1960
2222
  }
1961
2223
  const probe = probeBranch(track.worktreePath, opts);
1962
2224
  if ("error" in probe) {
1963
- violations.push(violation4("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)"));
2225
+ 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)"));
1964
2226
  } else if (probe.branch !== track.workingBranch) {
1965
- violations.push(violation4("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`));
2227
+ 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`));
1966
2228
  }
1967
2229
  });
1968
2230
  return gate(violations);
@@ -1977,14 +2239,14 @@ class SddScriptError extends Error {
1977
2239
  }
1978
2240
  function isDirectory2(dir) {
1979
2241
  try {
1980
- return statSync3(dir).isDirectory();
2242
+ return statSync4(dir).isDirectory();
1981
2243
  } catch {
1982
2244
  return false;
1983
2245
  }
1984
2246
  }
1985
- function isFile(file) {
2247
+ function isFile2(file) {
1986
2248
  try {
1987
- return statSync3(file).isFile();
2249
+ return statSync4(file).isFile();
1988
2250
  } catch {
1989
2251
  return false;
1990
2252
  }
@@ -2003,25 +2265,48 @@ function gitOut(cwd, args) {
2003
2265
  }
2004
2266
  }
2005
2267
  function probeHarnessWithStatus(root) {
2006
- if (isFile(join5(root, ".mstar", "status.json")))
2007
- return join5(root, ".mstar");
2008
- if (isFile(join5(root, ".agents", "status.json")))
2009
- return join5(root, ".agents");
2268
+ if (isFile2(join7(root, ".mstar", "status.json")))
2269
+ return join7(root, ".mstar");
2270
+ if (isFile2(join7(root, ".agents", "status.json")))
2271
+ return join7(root, ".agents");
2272
+ if (hasWorkflowSnapshot(join7(root, ".mstar")))
2273
+ return join7(root, ".mstar");
2274
+ if (hasWorkflowSnapshot(join7(root, ".agents")))
2275
+ return join7(root, ".agents");
2010
2276
  return null;
2011
2277
  }
2278
+ function hasWorkflowSnapshot(harnessDir) {
2279
+ let workflowsDir;
2280
+ try {
2281
+ workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2282
+ } catch {
2283
+ workflowsDir = join7(harnessDir, "workflows");
2284
+ }
2285
+ if (!isDirectory2(workflowsDir))
2286
+ return false;
2287
+ try {
2288
+ for (const entry of readdirSync3(workflowsDir, { withFileTypes: true })) {
2289
+ if (entry.isDirectory() && isFile2(join7(workflowsDir, entry.name, "snapshot.json")))
2290
+ return true;
2291
+ }
2292
+ } catch {
2293
+ return false;
2294
+ }
2295
+ return false;
2296
+ }
2012
2297
  function isLinkedWorktree(root) {
2013
2298
  const gitDirRaw = gitOut(root, ["rev-parse", "--git-dir"]);
2014
2299
  const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2015
2300
  if (gitDirRaw === null || commonRaw === null)
2016
2301
  return false;
2017
- const gitDir = isAbsolute4(gitDirRaw) ? gitDirRaw : join5(root, gitDirRaw);
2018
- const common = isAbsolute4(commonRaw) ? commonRaw : join5(root, commonRaw);
2302
+ const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join7(root, gitDirRaw);
2303
+ const common = isAbsolute5(commonRaw) ? commonRaw : join7(root, commonRaw);
2019
2304
  if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2020
2305
  return true;
2021
2306
  try {
2022
- const gdParent = realpathSync(dirname4(gitDir));
2023
- const cmAbs = realpathSync(common);
2024
- return join5(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2307
+ const gdParent = realpathSync2(dirname6(gitDir));
2308
+ const cmAbs = realpathSync2(common);
2309
+ return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2025
2310
  } catch {
2026
2311
  return false;
2027
2312
  }
@@ -2038,10 +2323,10 @@ function sddWorkspace(planId, opts = {}) {
2038
2323
  if (!isDirectory2(controlRoot)) {
2039
2324
  throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
2040
2325
  }
2041
- root = realpathSync(controlRoot);
2326
+ root = realpathSync2(controlRoot);
2042
2327
  } else {
2043
2328
  const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
2044
- root = realpathSync(topLevel ?? cwd);
2329
+ root = realpathSync2(topLevel ?? cwd);
2045
2330
  }
2046
2331
  if (!controlRoot && isLinkedWorktree(root)) {
2047
2332
  throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
@@ -2052,24 +2337,30 @@ function sddWorkspace(planId, opts = {}) {
2052
2337
  const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
2053
2338
  let harnessDir;
2054
2339
  if (harnessOverride) {
2055
- harnessDir = resolve6(root, harnessOverride);
2340
+ harnessDir = resolve7(root, harnessOverride);
2056
2341
  } else {
2057
- const probed = probeHarnessWithStatus(root);
2058
- if (probed) {
2059
- harnessDir = probed;
2060
- } else if (isDirectory2(join5(root, ".mstar"))) {
2061
- harnessDir = join5(root, ".mstar");
2062
- } else if (isDirectory2(join5(root, ".agents"))) {
2063
- harnessDir = join5(root, ".agents");
2342
+ const rc = findMstarc(root, root);
2343
+ const rcHarnessDir = rc !== null ? parseMstarc(readFileSync5(rc, "utf8")).harnessDir : undefined;
2344
+ if (rcHarnessDir) {
2345
+ harnessDir = resolve7(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2064
2346
  } else {
2065
- harnessDir = join5(root, ".mstar");
2347
+ const probed = probeHarnessWithStatus(root);
2348
+ if (probed) {
2349
+ harnessDir = probed;
2350
+ } else if (isDirectory2(join7(root, ".mstar"))) {
2351
+ harnessDir = join7(root, ".mstar");
2352
+ } else if (isDirectory2(join7(root, ".agents"))) {
2353
+ harnessDir = join7(root, ".agents");
2354
+ } else {
2355
+ harnessDir = join7(root, ".mstar");
2356
+ }
2066
2357
  }
2067
2358
  }
2068
2359
  const sddDir = resolveSddDir(harnessDir, planId);
2069
- mkdirSync4(sddDir, { recursive: true });
2070
- writeFileSync3(join5(sddDir, ".gitignore"), `*
2360
+ mkdirSync5(sddDir, { recursive: true });
2361
+ writeFileSync3(join7(sddDir, ".gitignore"), `*
2071
2362
  `);
2072
- return realpathSync(sddDir);
2363
+ return realpathSync2(sddDir);
2073
2364
  }
2074
2365
  function taskBrief(planFile, taskN, outFile, opts = {}) {
2075
2366
  if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
@@ -2077,7 +2368,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2077
2368
  }
2078
2369
  let content;
2079
2370
  try {
2080
- content = readFileSync4(planFile, "utf8");
2371
+ content = readFileSync5(planFile, "utf8");
2081
2372
  } catch {
2082
2373
  throw new SddScriptError(`no such plan file: ${planFile}`, 2);
2083
2374
  }
@@ -2089,8 +2380,8 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2089
2380
  if (!sddDir) {
2090
2381
  throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2091
2382
  }
2092
- mkdirSync4(sddDir, { recursive: true });
2093
- out = join5(sddDir, `task-${taskN}-brief.md`);
2383
+ mkdirSync5(sddDir, { recursive: true });
2384
+ out = join7(sddDir, `task-${taskN}-brief.md`);
2094
2385
  }
2095
2386
  const records = content.endsWith(`
2096
2387
  `) ? content.split(`
@@ -2192,18 +2483,18 @@ function validateCompassShape(doc) {
2192
2483
  }
2193
2484
  };
2194
2485
  }
2195
- function violation5(severity, code, message, fix) {
2486
+ function violation6(severity, code, message, fix) {
2196
2487
  return { ok: false, severity, code, message, fix };
2197
2488
  }
2198
- function isPlainObject3(value) {
2489
+ function isPlainObject4(value) {
2199
2490
  return typeof value === "object" && value !== null && !Array.isArray(value);
2200
2491
  }
2201
2492
  function validateCompassFrontmatter(doc) {
2202
- if (!isPlainObject3(doc)) {
2493
+ if (!isPlainObject4(doc)) {
2203
2494
  return {
2204
2495
  ok: false,
2205
2496
  violations: [
2206
- violation5("medium", "COMPASS_INVALID_FIELD", "Compass frontmatter must be a YAML object with iteration_id / start_date / status / iteration_base_branch / target_branch (template: mstar-iteration §1.3)", "Fix the frontmatter of {ITERATION_DIR}/<iteration-id>/delivery-compass.md")
2497
+ 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")
2207
2498
  ]
2208
2499
  };
2209
2500
  }
@@ -2213,17 +2504,17 @@ function validateCompassFrontmatter(doc) {
2213
2504
  ok: false,
2214
2505
  violations: parsed.issues.map((issue) => {
2215
2506
  const field = issue.path.join(".") || "(root)";
2216
- return violation5("medium", "COMPASS_INVALID_FIELD", `Compass frontmatter field '${field}' is invalid: ${issue.message}`, `Fix '${field}' in {ITERATION_DIR}/<iteration-id>/delivery-compass.md frontmatter (template: mstar-iteration §1.3)`);
2507
+ 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)`);
2217
2508
  })
2218
2509
  };
2219
2510
  }
2220
2511
  const violations = [];
2221
2512
  const { status, end_date } = parsed.data;
2222
2513
  if (status === "completed" && end_date === undefined) {
2223
- violations.push(violation5("high", "COMPASS_END_DATE_REQUIRED", "Compass frontmatter status is 'completed' but end_date is missing — end_date is required at iteration-close (mstar-iteration §3.4, template Fields guide)", "Add `end_date: YYYY-MM-DD` to the frontmatter"));
2514
+ 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"));
2224
2515
  }
2225
2516
  if (status !== "completed" && end_date !== undefined) {
2226
- violations.push(violation5("medium", "COMPASS_END_DATE_NOT_ALLOWED", `Compass frontmatter sets end_date while status is '${status}' — end_date is only written at iteration-close (mstar-iteration §3.4)`, "Remove end_date until iteration-close"));
2517
+ 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"));
2227
2518
  }
2228
2519
  return { ok: violations.length === 0, violations };
2229
2520
  }
@@ -2232,11 +2523,11 @@ function registeredPlanIds(compassDoc) {
2232
2523
  return [];
2233
2524
  return compassDoc.plans.filter((plan) => typeof plan === "string" && plan.length > 0);
2234
2525
  }
2235
- function findPlanRow(statusDoc, planId) {
2236
- if (!Array.isArray(statusDoc.plans))
2526
+ function findPlanRow(snapshotDoc, planId) {
2527
+ if (!Array.isArray(snapshotDoc.plans))
2237
2528
  return null;
2238
- for (const row of statusDoc.plans) {
2239
- if (!isPlainObject3(row))
2529
+ for (const row of snapshotDoc.plans) {
2530
+ if (!isPlainObject4(row))
2240
2531
  continue;
2241
2532
  const rowId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : null;
2242
2533
  if (rowId === planId)
@@ -2244,65 +2535,35 @@ function findPlanRow(statusDoc, planId) {
2244
2535
  }
2245
2536
  return null;
2246
2537
  }
2247
- function entryPlansAllDone(statusDoc, registered) {
2538
+ function entryPlansAllDone(snapshotDoc, registered) {
2248
2539
  const violations = [];
2249
2540
  if (registered.length === 0) {
2250
- violations.push(violation5("medium", "COMPASS_NO_PLANS", "Compass frontmatter registers no plans — the all-plans-Done transition cannot be verified (mstar-iteration §1.3 / Phase transition gates)", "List the iteration's plan ids in the compass frontmatter `plans`"));
2541
+ 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`"));
2251
2542
  return violations;
2252
2543
  }
2253
2544
  for (const planId of registered) {
2254
- const row = findPlanRow(statusDoc, planId);
2545
+ const row = findPlanRow(snapshotDoc, planId);
2255
2546
  if (row === null) {
2256
- violations.push(violation5("high", "PLAN_NOT_IN_STATUS", `Plan '${planId}' is registered in the compass frontmatter but has no row in status.json plans[] (mstar-iteration §3.1 entry item 1)`, "Add the plan row to {HARNESS_DIR}/status.json"));
2547
+ 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"));
2257
2548
  continue;
2258
2549
  }
2259
2550
  if (row.status !== PLAN_STATUS_DONE) {
2260
- violations.push(violation5("high", "PLAN_NOT_DONE", `Plan '${planId}' status is ${JSON.stringify(row.status)} in status.json — all compass-registered plans must be 'Done' before iteration-close (mstar-iteration §3.1 entry item 1)`));
2551
+ 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)`));
2261
2552
  }
2262
2553
  }
2263
2554
  return violations;
2264
2555
  }
2265
- function entryResidualsOpen(statusDoc, planId) {
2266
- const violations = [];
2267
- const residualRoot = statusDoc.residual_findings;
2268
- if (residualRoot === undefined || residualRoot === null)
2269
- return violations;
2270
- if (!isPlainObject3(residualRoot)) {
2271
- violations.push(violation5("medium", "RESIDUAL_MALFORMED", "status.json residual_findings must be a plan-id → entries object (mstar-iteration §3.1 entry item 2)"));
2272
- return violations;
2273
- }
2274
- const entries = residualRoot[planId];
2275
- if (entries === undefined)
2276
- return violations;
2277
- if (!Array.isArray(entries)) {
2278
- violations.push(violation5("medium", "RESIDUAL_MALFORMED", `status.json residual_findings['${planId}'] must be an array of residual entries (mstar-iteration §3.1 entry item 2)`));
2279
- return violations;
2280
- }
2281
- const openIds = [];
2282
- for (const entry of entries) {
2283
- if (!isPlainObject3(entry) || !isOpenResidual(entry))
2284
- continue;
2285
- const isBlockerDefer = entry.decision === "defer" && typeof entry.target === "string" && entry.target.trim() !== "";
2286
- if (isBlockerDefer)
2287
- continue;
2288
- openIds.push(typeof entry.id === "string" ? entry.id : "<unnamed>");
2289
- }
2290
- if (openIds.length > 0) {
2291
- violations.push(violation5("high", "OPEN_RESIDUALS", `Plan '${planId}' has ${openIds.length} open residual finding(s) not exempted as blocker-defers (${openIds.join(", ")}) — residuals must be closed/archived before iteration-close; only zero-residual blocker-defers (decision: defer + target) may stay open (mstar-iteration §3.1 entry item 2)`, "Close or archive the open residuals, or convert them into blocker-defers (decision: defer + non-empty target) per mstar-plan-artifacts Findings cleanup modes"));
2292
- }
2293
- return violations;
2294
- }
2295
2556
  function entryFrontmatterComplete(compassDoc) {
2296
2557
  return validateCompassFrontmatter(compassDoc).violations;
2297
2558
  }
2298
2559
  function exitFrontmatterClosed(compassDoc) {
2299
2560
  const violations = [];
2300
2561
  if (compassDoc.status !== "completed") {
2301
- violations.push(violation5("high", "EXIT_STATUS_NOT_COMPLETED", `Compass frontmatter status must be 'completed' at close exit — current: ${JSON.stringify(compassDoc.status)} (mstar-iteration §3.4 / §3.5 exit item 4)`));
2562
+ 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)`));
2302
2563
  }
2303
2564
  const endDate = compassDoc.end_date;
2304
2565
  if (typeof endDate !== "string" || !DATE_RE2.test(endDate)) {
2305
- violations.push(violation5("high", "EXIT_END_DATE_REQUIRED", "Compass frontmatter end_date (YYYY-MM-DD) is required when closing (mstar-iteration §3.4 / §3.5 exit item 4)"));
2566
+ 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)"));
2306
2567
  }
2307
2568
  return violations;
2308
2569
  }
@@ -2310,9 +2571,9 @@ function exitBranchCheck(opts) {
2310
2571
  const violations = [];
2311
2572
  const { currentBranch, specIntegrationBranch } = opts;
2312
2573
  if (currentBranch === undefined || specIntegrationBranch === undefined) {
2313
- violations.push(violation5("medium", "EXIT_BRANCH_UNVERIFIABLE", "Cannot verify the current branch is spec_integration_branch — missing currentBranch / specIntegrationBranch probe inputs (mstar-iteration §3.5 exit item 5)"));
2574
+ 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)"));
2314
2575
  } else if (currentBranch !== specIntegrationBranch) {
2315
- violations.push(violation5("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
2576
+ violations.push(violation6("high", "EXIT_BRANCH_MISMATCH", `Current branch '${currentBranch}' is not the spec_integration_branch '${specIntegrationBranch}' (mstar-iteration §3.5 exit item 5)`));
2316
2577
  }
2317
2578
  return violations;
2318
2579
  }
@@ -2321,17 +2582,16 @@ function exitPrBaseCheck(compassDoc, opts) {
2321
2582
  const target = compassDoc.target_branch;
2322
2583
  const { prBaseBranch } = opts;
2323
2584
  if (prBaseBranch === undefined) {
2324
- violations.push(violation5("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
2585
+ violations.push(violation6("medium", "EXIT_PR_BASE_UNVERIFIABLE", "Cannot verify the PR base — missing prBaseBranch probe input (mstar-iteration §3.5 exit item 6)"));
2325
2586
  } else if (typeof target !== "string" || prBaseBranch !== target) {
2326
- violations.push(violation5("high", "EXIT_PR_BASE_MISMATCH", `PR base '${prBaseBranch}' must equal the compass target_branch '${String(target)}' — not an undocumented branch (mstar-iteration §3.5 exit item 6)`));
2587
+ 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)`));
2327
2588
  }
2328
2589
  return violations;
2329
2590
  }
2330
- function evaluatePhaseGate(statusDoc, compassDoc, opts = {}) {
2591
+ function evaluatePhaseGate(snapshotDoc, compassDoc, opts = {}) {
2331
2592
  const registered = registeredPlanIds(compassDoc);
2332
2593
  const entryViolations = [
2333
- ...entryPlansAllDone(statusDoc, registered),
2334
- ...registered.flatMap((planId) => entryResidualsOpen(statusDoc, planId)),
2594
+ ...entryPlansAllDone(snapshotDoc, registered),
2335
2595
  ...entryFrontmatterComplete(compassDoc)
2336
2596
  ];
2337
2597
  const exitViolations = [
@@ -2340,7 +2600,7 @@ function evaluatePhaseGate(statusDoc, compassDoc, opts = {}) {
2340
2600
  ...exitPrBaseCheck(compassDoc, opts)
2341
2601
  ];
2342
2602
  const allPlansDone = registered.length > 0 && registered.every((planId) => {
2343
- const row = findPlanRow(statusDoc, planId);
2603
+ const row = findPlanRow(snapshotDoc, planId);
2344
2604
  return row !== null && row.status === PLAN_STATUS_DONE;
2345
2605
  });
2346
2606
  const entry = { ok: entryViolations.length === 0, violations: entryViolations };
@@ -2363,7 +2623,9 @@ function evaluatePhaseGate(statusDoc, compassDoc, opts = {}) {
2363
2623
  };
2364
2624
  }
2365
2625
  function parseCompassFrontmatter(filePath) {
2366
- const content = readFileSync5(filePath, "utf8");
2626
+ return parseCompassFrontmatterText(readFileSync6(filePath, "utf8"), filePath);
2627
+ }
2628
+ function parseCompassFrontmatterText(content, filePath) {
2367
2629
  const lines = content.split(/\r?\n/);
2368
2630
  if (lines[0]?.trim() !== "---") {
2369
2631
  throw new Error(`no YAML frontmatter fence in ${filePath} (expected first line "---")`);
@@ -2424,7 +2686,103 @@ function parseFlowArray(raw, filePath) {
2424
2686
  }
2425
2687
  return items;
2426
2688
  }
2427
- function violation6(severity, code, message, fix) {
2689
+ var PROJECT_ROADMAP_FILE = "roadmap.md";
2690
+ var PROJECT_REGISTER_FILE = "residuals.json";
2691
+ var _DEFAULT_PROJECT = "_default";
2692
+ var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
2693
+ function isPlainObject5(value) {
2694
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2695
+ }
2696
+ function violation7(severity, code, message, fix) {
2697
+ return { ok: false, severity, code, message, fix };
2698
+ }
2699
+ function validateNonEmptyString4(violations, value, field, missingCode, invalidCode) {
2700
+ if (value === undefined) {
2701
+ violations.push(violation7("high", missingCode, `missing required field: ${field}`));
2702
+ } else if (typeof value !== "string" || value.trim() === "") {
2703
+ violations.push(violation7("medium", invalidCode, `${field} must be a non-empty string`));
2704
+ }
2705
+ }
2706
+ function validateProjectRegister(doc) {
2707
+ const violations = [];
2708
+ if (!isPlainObject5(doc)) {
2709
+ return {
2710
+ ok: false,
2711
+ violations: [violation7("high", "project.register.invalid", "project register must be an object")]
2712
+ };
2713
+ }
2714
+ if (doc.entries === undefined) {
2715
+ violations.push(violation7("high", "project.register.missing-entries", "missing required field: entries"));
2716
+ } else if (!isPlainObject5(doc.entries)) {
2717
+ violations.push(violation7("high", "project.register.invalid-entries", "entries must be an object keyed by plan id"));
2718
+ } else {
2719
+ for (const [key, entries] of Object.entries(doc.entries)) {
2720
+ if (key.trim() === "") {
2721
+ violations.push(violation7("medium", "project.register.invalid-key", "entries keys must be non-empty plan ids"));
2722
+ }
2723
+ if (!Array.isArray(entries)) {
2724
+ 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)`));
2725
+ continue;
2726
+ }
2727
+ for (const entry of entries) {
2728
+ violations.push(...validateResidual(entry).violations);
2729
+ if (!isPlainObject5(entry))
2730
+ continue;
2731
+ validateNonEmptyString4(violations, entry.source_plan, "source_plan", "project.register.missing-source-plan", "project.register.invalid-source-plan");
2732
+ if (entry.registered_at === undefined) {
2733
+ violations.push(violation7("high", "project.register.missing-registered-at", "missing required field: registered_at"));
2734
+ } else if (typeof entry.registered_at !== "string" || !DATE_RE3.test(entry.registered_at)) {
2735
+ violations.push(violation7("medium", "project.register.invalid-registered-at", "registered_at must be YYYY-MM-DD"));
2736
+ }
2737
+ if (entry.lifecycle_id !== undefined && (typeof entry.lifecycle_id !== "string" || entry.lifecycle_id.trim() === "")) {
2738
+ violations.push(violation7("medium", "project.register.invalid-lifecycle-id", "lifecycle_id must be a non-empty string"));
2739
+ }
2740
+ if (typeof entry.source_plan === "string" && entry.source_plan.trim() !== "" && entry.source_plan !== key) {
2741
+ 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`));
2742
+ }
2743
+ }
2744
+ }
2745
+ }
2746
+ return { ok: violations.length === 0, violations };
2747
+ }
2748
+ function findingsCleanupGate(register, planId, opts) {
2749
+ const mode = opts?.mode ?? "allow-residual";
2750
+ const violations = [];
2751
+ const entries = isPlainObject5(register.entries) ? register.entries[planId] : undefined;
2752
+ if (entries === undefined) {
2753
+ return { ok: true, violations };
2754
+ }
2755
+ if (!Array.isArray(entries)) {
2756
+ 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)`));
2757
+ return { ok: false, violations };
2758
+ }
2759
+ if (entries.length === 0) {
2760
+ return { ok: true, violations };
2761
+ }
2762
+ for (const entry of entries) {
2763
+ if (!isOpenResidual(entry))
2764
+ continue;
2765
+ const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
2766
+ const label = `R#${id}`;
2767
+ if (mode === "zero-residual") {
2768
+ if (entry.severity === "nit") {
2769
+ 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`));
2770
+ } else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
2771
+ violations.push(violation7("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
2772
+ } else if (entry.decision === "defer") {
2773
+ if (typeof entry.target !== "string" || entry.target.trim() === "") {
2774
+ violations.push(violation7("medium", "findings.zero-residual-defer-no-target", `${label}: blocker-defer requires a target (next iteration/milestone) under zero-residual`));
2775
+ }
2776
+ } else {
2777
+ 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`));
2778
+ }
2779
+ } else if (normalizeSeverity(entry.severity) === "critical") {
2780
+ violations.push(violation7("high", "findings.allow-residual-critical", `${label}: unresolved critical blocks Approve with residuals`));
2781
+ }
2782
+ }
2783
+ return { ok: violations.length === 0, violations };
2784
+ }
2785
+ function violation8(severity, code, message, fix) {
2428
2786
  return { ok: false, severity, code, message, fix };
2429
2787
  }
2430
2788
  var RAW_GROUP = "__raw";
@@ -2537,7 +2895,7 @@ function validateDesignTokenFrontmatter(frontmatterText) {
2537
2895
  const violations = [];
2538
2896
  const fm = parseDesignFrontmatter(frontmatterText);
2539
2897
  if (fm === null) {
2540
- violations.push(violation6("medium", "design-md.tokens.missing-frontmatter", "no `---` YAML frontmatter block found — DESIGN.md must open with a fenced frontmatter holding the token SSOT (design-md-spec §1.5)", "add a `---` fenced frontmatter with version, name, description, and the colors/typography/spacing/rounded groups"));
2898
+ violations.push(violation8("medium", "design-md.tokens.missing-frontmatter", "no `---` YAML frontmatter block found — DESIGN.md must open with a fenced frontmatter holding the token SSOT (design-md-spec §1.5)", "add a `---` fenced frontmatter with version, name, description, and the colors/typography/spacing/rounded groups"));
2541
2899
  return { ok: false, violations };
2542
2900
  }
2543
2901
  const groupEntries = (group) => {
@@ -2552,61 +2910,61 @@ function validateDesignTokenFrontmatter(frontmatterText) {
2552
2910
  };
2553
2911
  for (const group of ["colors", "typography", "spacing", "rounded"]) {
2554
2912
  if (!isMap(fm[group])) {
2555
- violations.push(violation6("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
2913
+ violations.push(violation8("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
2556
2914
  } else if (!groupIsMap(group)) {
2557
- violations.push(violation6("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
2915
+ violations.push(violation8("medium", "design-md.tokens.group-not-map", `token group "${group}" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`${group}\` as a nested map`));
2558
2916
  } else if (groupEntries(group).length === 0) {
2559
- violations.push(violation6("medium", "design-md.tokens.missing-group", `missing required token group "${group}" — colors/typography/spacing/rounded are required by the frontmatter SSOT (design-md-spec §1.5)`, `add an active \`${group}:\` block with concrete token values`));
2917
+ violations.push(violation8("medium", "design-md.tokens.missing-group", `missing required token group "${group}" — colors/typography/spacing/rounded are required by the frontmatter SSOT (design-md-spec §1.5)`, `add an active \`${group}:\` block with concrete token values`));
2560
2918
  }
2561
2919
  }
2562
2920
  if (!isMap(fm.components) || !groupIsMap("components")) {
2563
- violations.push(violation6("medium", "design-md.tokens.group-not-map", `token group "components" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`components\` as a nested map`));
2921
+ violations.push(violation8("medium", "design-md.tokens.group-not-map", `token group "components" must be a YAML map, not a scalar (design-md-spec §1.5)`, `rewrite \`components\` as a nested map`));
2564
2922
  }
2565
- const placeholder = (group, name, value) => violations.push(violation6("low", "design-md.tokens.placeholder", `token "${group}.${name}" uses a "[...]" template value — placeholders never count as concrete tokens (completeness-checklist § How to use item 5)`, `replace \`${value}\` with a concrete value`));
2923
+ const placeholder = (group, name, value) => violations.push(violation8("low", "design-md.tokens.placeholder", `token "${group}.${name}" uses a "[...]" template value — placeholders never count as concrete tokens (completeness-checklist § How to use item 5)`, `replace \`${value}\` with a concrete value`));
2566
2924
  for (const [name, value] of groupEntries("colors")) {
2567
2925
  if (typeof value !== "string") {
2568
- violations.push(violation6("medium", "design-md.tokens.color-format", `color "${name}" must be a string value (design-md-spec §2.2)`, "quote the color value"));
2926
+ violations.push(violation8("medium", "design-md.tokens.color-format", `color "${name}" must be a string value (design-md-spec §2.2)`, "quote the color value"));
2569
2927
  continue;
2570
2928
  }
2571
2929
  if (isPlaceholder(value)) {
2572
2930
  placeholder("colors", name, value);
2573
2931
  } else if (!HEX_RE.test(value) && !OKLCH_RE.test(value)) {
2574
- violations.push(violation6("medium", "design-md.tokens.color-format", `color "${name}" = "${value}" is not a hex (\`#rrggbb\`/\`#rrggbbaa\`) or oklch() value (design-md-spec §2.2)`, "use an sRGB hex value, optionally with a `-p3` oklch() twin"));
2932
+ violations.push(violation8("medium", "design-md.tokens.color-format", `color "${name}" = "${value}" is not a hex (\`#rrggbb\`/\`#rrggbbaa\`) or oklch() value (design-md-spec §2.2)`, "use an sRGB hex value, optionally with a `-p3` oklch() twin"));
2575
2933
  }
2576
2934
  }
2577
2935
  for (const [name, value] of groupEntries("typography")) {
2578
2936
  if (!isMap(value)) {
2579
- violations.push(violation6("medium", "design-md.tokens.typography-shape", `typography token "${name}" must be a map of the five properties (design-md-spec §1.5)`, "give it fontFamily/fontSize/fontWeight/lineHeight/letterSpacing"));
2937
+ violations.push(violation8("medium", "design-md.tokens.typography-shape", `typography token "${name}" must be a map of the five properties (design-md-spec §1.5)`, "give it fontFamily/fontSize/fontWeight/lineHeight/letterSpacing"));
2580
2938
  continue;
2581
2939
  }
2582
2940
  const keys = Object.keys(value);
2583
2941
  const missing = TYPOGRAPHY_PROPS.filter((p) => !keys.includes(p));
2584
2942
  const extra = keys.filter((k) => !TYPOGRAPHY_PROPS.includes(k));
2585
2943
  if (missing.length > 0 || extra.length > 0) {
2586
- violations.push(violation6("medium", "design-md.tokens.typography-shape", `typography token "${name}" must have exactly the five properties fontFamily/fontSize/fontWeight/lineHeight/letterSpacing (design-md-spec §1.5)${missing.length > 0 ? ` — missing: ${missing.join(", ")}` : ""}${extra.length > 0 ? ` — extra: ${extra.join(", ")}` : ""}`, "align the token with the five-property shape"));
2944
+ violations.push(violation8("medium", "design-md.tokens.typography-shape", `typography token "${name}" must have exactly the five properties fontFamily/fontSize/fontWeight/lineHeight/letterSpacing (design-md-spec §1.5)${missing.length > 0 ? ` — missing: ${missing.join(", ")}` : ""}${extra.length > 0 ? ` — extra: ${extra.join(", ")}` : ""}`, "align the token with the five-property shape"));
2587
2945
  }
2588
2946
  for (const prop of ["fontFamily", "fontSize"]) {
2589
2947
  const v = value[prop];
2590
2948
  if (typeof v === "string" && isPlaceholder(v))
2591
2949
  placeholder("typography", name, v);
2592
2950
  else if (typeof v !== "string" || v.trim() === "") {
2593
- violations.push(violation6("medium", "design-md.tokens.typography-shape", `typography token "${name}" has an empty \`${prop}\` (design-md-spec §1.5)`, `fill \`${prop}\` with a concrete value`));
2951
+ violations.push(violation8("medium", "design-md.tokens.typography-shape", `typography token "${name}" has an empty \`${prop}\` (design-md-spec §1.5)`, `fill \`${prop}\` with a concrete value`));
2594
2952
  }
2595
2953
  }
2596
2954
  }
2597
2955
  if (groupIsMap("spacing")) {
2598
2956
  const spacing = fm.spacing;
2599
2957
  if (!Object.prototype.hasOwnProperty.call(spacing, "base")) {
2600
- violations.push(violation6("medium", "design-md.tokens.spacing-base", "spacing must declare the base unit as `base` (design-md-spec §2.4)", "add `base: 4px` (or 8px) to the spacing group"));
2958
+ violations.push(violation8("medium", "design-md.tokens.spacing-base", "spacing must declare the base unit as `base` (design-md-spec §2.4)", "add `base: 4px` (or 8px) to the spacing group"));
2601
2959
  }
2602
2960
  for (const [name, value] of Object.entries(spacing)) {
2603
2961
  if (name !== "base" && name !== RAW_GROUP && !/^\d+$/.test(name)) {
2604
- violations.push(violation6("medium", "design-md.tokens.spacing-key", `spacing key "${name}" must be \`base\` or a numeric multiplier (design-md-spec §1.5)`, "use numeric scale-step keys or `base`"));
2962
+ violations.push(violation8("medium", "design-md.tokens.spacing-key", `spacing key "${name}" must be \`base\` or a numeric multiplier (design-md-spec §1.5)`, "use numeric scale-step keys or `base`"));
2605
2963
  }
2606
2964
  if (typeof value === "string" && isPlaceholder(value)) {
2607
2965
  placeholder("spacing", name, value);
2608
2966
  } else if (typeof value !== "string" || !PX_RE.test(value)) {
2609
- violations.push(violation6("medium", "design-md.tokens.spacing-format", `spacing value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `4px`"));
2967
+ violations.push(violation8("medium", "design-md.tokens.spacing-format", `spacing value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `4px`"));
2610
2968
  }
2611
2969
  }
2612
2970
  }
@@ -2614,12 +2972,12 @@ function validateDesignTokenFrontmatter(frontmatterText) {
2614
2972
  if (typeof value === "string" && isPlaceholder(value)) {
2615
2973
  placeholder("rounded", name, value);
2616
2974
  } else if (typeof value !== "string" || !PX_RE.test(value)) {
2617
- violations.push(violation6("medium", "design-md.tokens.rounded-format", `rounded value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `6px`"));
2975
+ violations.push(violation8("medium", "design-md.tokens.rounded-format", `rounded value "${name}" = "${String(value)}" is not a px length (design-md-spec §1.5)`, "use a pixel value like `6px`"));
2618
2976
  }
2619
2977
  }
2620
2978
  for (const [name, value] of groupEntries("components")) {
2621
2979
  if (!isMap(value)) {
2622
- violations.push(violation6("medium", "design-md.tokens.components-shape", `component token "${name}" must be a map of properties (design-md-spec §2.8)`, "give it backgroundColor/textColor/typography/rounded/padding/height"));
2980
+ violations.push(violation8("medium", "design-md.tokens.components-shape", `component token "${name}" must be a map of properties (design-md-spec §2.8)`, "give it backgroundColor/textColor/typography/rounded/padding/height"));
2623
2981
  continue;
2624
2982
  }
2625
2983
  for (const [prop, v] of Object.entries(value)) {
@@ -2635,7 +2993,7 @@ function validateDesignTokenFrontmatter(frontmatterText) {
2635
2993
  const [, refGroup, refKey] = ref;
2636
2994
  const resolves = REF_GROUPS.includes(refGroup) && groupIsMap(refGroup) && Object.prototype.hasOwnProperty.call(fm[refGroup], refKey);
2637
2995
  if (!resolves) {
2638
- violations.push(violation6("medium", "design-md.tokens.ref-unresolved", `component "${name}" references "${v}" which does not resolve to an active token in this frontmatter (design-md-spec §6 — {path} refs MUST trace back to a key)`, `add the referenced token or use a literal value`));
2996
+ violations.push(violation8("medium", "design-md.tokens.ref-unresolved", `component "${name}" references "${v}" which does not resolve to an active token in this frontmatter (design-md-spec §6 — {path} refs MUST trace back to a key)`, `add the referenced token or use a literal value`));
2639
2997
  }
2640
2998
  }
2641
2999
  }
@@ -2647,7 +3005,7 @@ function assertLightDarkParity(lightFm, darkFm) {
2647
3005
  const light = parseDesignFrontmatter(lightFm);
2648
3006
  const dark = parseDesignFrontmatter(darkFm);
2649
3007
  if (light === null || dark === null) {
2650
- violations.push(violation6("medium", "design-md.parity.missing-frontmatter", `light/dark parity needs a YAML frontmatter in both files — ${light === null ? "DESIGN.md" : "DESIGN.dark.md"} has none (design-md-spec §4 rules 1–2)`, "add the fenced frontmatter to both theme files"));
3008
+ violations.push(violation8("medium", "design-md.parity.missing-frontmatter", `light/dark parity needs a YAML frontmatter in both files — ${light === null ? "DESIGN.md" : "DESIGN.dark.md"} has none (design-md-spec §4 rules 1–2)`, "add the fenced frontmatter to both theme files"));
2651
3009
  return { ok: false, violations };
2652
3010
  }
2653
3011
  const activeKeys = (fm) => {
@@ -2665,12 +3023,12 @@ function assertLightDarkParity(lightFm, darkFm) {
2665
3023
  const darkKeys = activeKeys(dark);
2666
3024
  for (const key of lightKeys) {
2667
3025
  if (!darkKeys.has(key)) {
2668
- violations.push(violation6("medium", "design-md.parity.missing-dark", `token "${key}" is active in DESIGN.md but missing from DESIGN.dark.md — both files must define the same token set (design-md-spec §4 rule 3)`, "add the token to DESIGN.dark.md with a dark-appropriate value"));
3026
+ violations.push(violation8("medium", "design-md.parity.missing-dark", `token "${key}" is active in DESIGN.md but missing from DESIGN.dark.md — both files must define the same token set (design-md-spec §4 rule 3)`, "add the token to DESIGN.dark.md with a dark-appropriate value"));
2669
3027
  }
2670
3028
  }
2671
3029
  for (const key of darkKeys) {
2672
3030
  if (!lightKeys.has(key)) {
2673
- violations.push(violation6("medium", "design-md.parity.missing-light", `token "${key}" is active in DESIGN.dark.md but missing from DESIGN.md — DESIGN.md is the SSOT for token names (design-md-spec §4 rules 3–4)`, "add the token to DESIGN.md, or remove it from the dark file"));
3031
+ violations.push(violation8("medium", "design-md.parity.missing-light", `token "${key}" is active in DESIGN.dark.md but missing from DESIGN.md — DESIGN.md is the SSOT for token names (design-md-spec §4 rules 3–4)`, "add the token to DESIGN.md, or remove it from the dark file"));
2674
3032
  }
2675
3033
  }
2676
3034
  return { ok: violations.length === 0, violations };
@@ -2823,7 +3181,7 @@ function completenessLevel(frontmatterText, checklist) {
2823
3181
  const upgradeTo = candidate === undefined ? null : candidate;
2824
3182
  return { level, items, missing, placeholders, upgradeTo, bodyUnverified };
2825
3183
  }
2826
- function violation7(severity, code, message, fix) {
3184
+ function violation9(severity, code, message, fix) {
2827
3185
  return { ok: false, severity, code, message, fix };
2828
3186
  }
2829
3187
  var AUDIT_STATUS_FIELDS = ["Priority", "Effort", "Risk", "Depends on", "Category", "Planned at"];
@@ -2853,14 +3211,14 @@ function validateAuditStatusBlocks(planText) {
2853
3211
  const violations = [];
2854
3212
  const blocks = parseStatusBlocks(planText);
2855
3213
  if (blocks.length === 0) {
2856
- violations.push(violation7("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL § Plan files)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
3214
+ violations.push(violation9("medium", "audit.status.missing-block", "no `## Status` block found — audit plan files carry the Status block fields (mstar-audit SKILL § Plan files)", "add a `## Status` block with Priority, Effort, Risk, Depends on, Category, Planned at"));
2857
3215
  return { ok: false, violations };
2858
3216
  }
2859
3217
  blocks.forEach((block, index) => {
2860
3218
  const label = blocks.length > 1 ? ` #${index + 1}` : "";
2861
3219
  for (const field of AUDIT_STATUS_FIELDS) {
2862
3220
  if (!block.fields.has(field)) {
2863
- violations.push(violation7("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL § Plan files)`, `add \`- **${field}**: <value>\` to the Status block`));
3221
+ violations.push(violation9("medium", "audit.status.missing-field", `Status block${label} missing required field "${field}" (mstar-audit SKILL § Plan files)`, `add \`- **${field}**: <value>\` to the Status block`));
2864
3222
  }
2865
3223
  }
2866
3224
  const check = (field, pattern, code, expected) => {
@@ -2868,7 +3226,7 @@ function validateAuditStatusBlocks(planText) {
2868
3226
  if (value === undefined)
2869
3227
  return;
2870
3228
  if (!pattern.test(value)) {
2871
- violations.push(violation7("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL § Plan files)`, `fix \`- **${field}**:\` to one of: ${expected}`));
3229
+ violations.push(violation9("medium", code, `Status block${label} "${field}" = "${value}" — expected ${expected} (mstar-audit SKILL § Plan files)`, `fix \`- **${field}**:\` to one of: ${expected}`));
2872
3230
  }
2873
3231
  };
2874
3232
  check("Priority", /^P[123]$/, "audit.status.invalid-priority", "P1 | P2 | P3");
@@ -2948,7 +3306,7 @@ function redactSecrets(text, filePath) {
2948
3306
  const sorted = [...deduped.values()].sort((a, b) => a.line - b.line || a.type.localeCompare(b.type));
2949
3307
  return { text: out, findings: sorted };
2950
3308
  }
2951
- function violation8(severity, code, message, fix) {
3309
+ function violation10(severity, code, message, fix) {
2952
3310
  return { ok: false, severity, code, message, fix };
2953
3311
  }
2954
3312
  var KNOWLEDGE_REQUIRED_FIELDS = ["module", "date", "problem_type", "category", "severity"];
@@ -3020,7 +3378,7 @@ var KNOWLEDGE_CATEGORY_MAP = {
3020
3378
  developer_experience: "developer-experience",
3021
3379
  documentation_gap: "documentation"
3022
3380
  };
3023
- var DATE_RE3 = /^\d{4}-\d{2}-\d{2}$/;
3381
+ var DATE_RE5 = /^\d{4}-\d{2}-\d{2}$/;
3024
3382
  var isMap2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3025
3383
  function parseScalar2(raw) {
3026
3384
  const trimmed = raw.trim();
@@ -3113,36 +3471,36 @@ function validateSchemaYaml(frontmatterText) {
3113
3471
  const violations = [];
3114
3472
  const doc = parseYamlLite(frontmatterText);
3115
3473
  if (doc === null) {
3116
- violations.push(violation8("medium", "compound.schema.missing-frontmatter", "no `---` YAML frontmatter block found — knowledge docs must open with the schema.yaml contract (mstar-compound/references/schema.yaml)", "add the fenced frontmatter with module, date, problem_type, category, severity"));
3474
+ violations.push(violation10("medium", "compound.schema.missing-frontmatter", "no `---` YAML frontmatter block found — knowledge docs must open with the schema.yaml contract (mstar-compound/references/schema.yaml)", "add the fenced frontmatter with module, date, problem_type, category, severity"));
3117
3475
  return { ok: false, violations };
3118
3476
  }
3119
3477
  const isStr = (v) => typeof v === "string";
3120
- const missing = (field) => violations.push(violation8("medium", "compound.schema.missing-field", `missing required frontmatter field "${field}" (schema.yaml required_fields)`, `add \`${field}: <value>\` to the frontmatter`));
3478
+ const missing = (field) => violations.push(violation10("medium", "compound.schema.missing-field", `missing required frontmatter field "${field}" (schema.yaml required_fields)`, `add \`${field}: <value>\` to the frontmatter`));
3121
3479
  for (const field of KNOWLEDGE_REQUIRED_FIELDS) {
3122
3480
  if (!(field in doc) || doc[field] === "")
3123
3481
  missing(field);
3124
3482
  }
3125
- if (doc.date !== undefined && (!isStr(doc.date) || !DATE_RE3.test(doc.date))) {
3126
- violations.push(violation8("medium", "compound.schema.invalid-date", `date "${String(doc.date)}" must be a YYYY-MM-DD string (schema.yaml required_fields.date)`, "use `YYYY-MM-DD`"));
3483
+ if (doc.date !== undefined && (!isStr(doc.date) || !DATE_RE5.test(doc.date))) {
3484
+ violations.push(violation10("medium", "compound.schema.invalid-date", `date "${String(doc.date)}" must be a YYYY-MM-DD string (schema.yaml required_fields.date)`, "use `YYYY-MM-DD`"));
3127
3485
  }
3128
3486
  const problemType = doc.problem_type;
3129
3487
  if (problemType !== undefined && !isStr(problemType)) {
3130
- violations.push(violation8("medium", "compound.schema.invalid-problem-type", `problem_type "${String(problemType)}" must be a string — one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
3488
+ violations.push(violation10("medium", "compound.schema.invalid-problem-type", `problem_type "${String(problemType)}" must be a string — one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
3131
3489
  }
3132
3490
  const problemTypeValid = isStr(problemType) && KNOWLEDGE_PROBLEM_TYPES.includes(problemType);
3133
3491
  if (isStr(problemType) && !problemTypeValid) {
3134
- violations.push(violation8("medium", "compound.schema.invalid-problem-type", `problem_type "${problemType}" is not one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
3492
+ violations.push(violation10("medium", "compound.schema.invalid-problem-type", `problem_type "${problemType}" is not one of the schema.yaml enum values (bug: build_error…config_error; knowledge: developer_experience…testing_pattern)`, "pick the narrowest applicable problem_type from schema.yaml"));
3135
3493
  }
3136
3494
  if (doc.severity !== undefined && !isStr(doc.severity)) {
3137
- violations.push(violation8("medium", "compound.schema.invalid-severity", `severity "${String(doc.severity)}" must be a string — critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
3495
+ violations.push(violation10("medium", "compound.schema.invalid-severity", `severity "${String(doc.severity)}" must be a string — critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
3138
3496
  }
3139
3497
  if (isStr(doc.severity) && !KNOWLEDGE_SEVERITIES.includes(doc.severity)) {
3140
- violations.push(violation8("medium", "compound.schema.invalid-severity", `severity "${doc.severity}" must be critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
3498
+ violations.push(violation10("medium", "compound.schema.invalid-severity", `severity "${doc.severity}" must be critical | high | medium | low (schema.yaml required_fields.severity)`, "use one of the four severity values"));
3141
3499
  }
3142
3500
  if (problemTypeValid && isStr(doc.category)) {
3143
3501
  const expected = KNOWLEDGE_CATEGORY_MAP[problemType];
3144
3502
  if (doc.category !== expected) {
3145
- violations.push(violation8("medium", "compound.schema.category-mismatch", `category "${doc.category}" does not match problem_type "${problemType}" — category-mapping.md rule 1 maps it to "${expected}"`, `set \`category: ${expected}\` (the directory name under {KNOWLEDGE_DIR})`));
3503
+ violations.push(violation10("medium", "compound.schema.category-mismatch", `category "${doc.category}" does not match problem_type "${problemType}" — category-mapping.md rule 1 maps it to "${expected}"`, `set \`category: ${expected}\` (the directory name under {KNOWLEDGE_DIR})`));
3146
3504
  }
3147
3505
  }
3148
3506
  if (problemTypeValid) {
@@ -3150,37 +3508,37 @@ function validateSchemaYaml(frontmatterText) {
3150
3508
  if (isBug) {
3151
3509
  for (const field of ["symptoms", "root_cause", "resolution_type"]) {
3152
3510
  if (!(field in doc)) {
3153
- violations.push(violation8("medium", "compound.schema.missing-track-field", `bug-track doc missing required field "${field}" (schema.yaml track_rules.bug)`, `add \`${field}:\` to the frontmatter`));
3511
+ violations.push(violation10("medium", "compound.schema.missing-track-field", `bug-track doc missing required field "${field}" (schema.yaml track_rules.bug)`, `add \`${field}:\` to the frontmatter`));
3154
3512
  }
3155
3513
  }
3156
3514
  if (doc.symptoms !== undefined && !Array.isArray(doc.symptoms)) {
3157
- violations.push(violation8("medium", "compound.schema.invalid-symptoms", "bug-track `symptoms` must be a YAML list (schema.yaml track_rules.bug)", "list the observable symptoms under `symptoms:`"));
3515
+ violations.push(violation10("medium", "compound.schema.invalid-symptoms", "bug-track `symptoms` must be a YAML list (schema.yaml track_rules.bug)", "list the observable symptoms under `symptoms:`"));
3158
3516
  }
3159
3517
  if (doc.root_cause !== undefined && !isStr(doc.root_cause)) {
3160
- violations.push(violation8("medium", "compound.schema.invalid-root-cause", "bug-track `root_cause` must be a string (schema.yaml track_rules.bug)", "write the fundamental technical cause as a string"));
3518
+ violations.push(violation10("medium", "compound.schema.invalid-root-cause", "bug-track `root_cause` must be a string (schema.yaml track_rules.bug)", "write the fundamental technical cause as a string"));
3161
3519
  }
3162
3520
  if (isStr(doc.resolution_type) && !KNOWLEDGE_RESOLUTION_TYPES.includes(doc.resolution_type)) {
3163
- violations.push(violation8("medium", "compound.schema.invalid-resolution-type", `resolution_type "${doc.resolution_type}" is not one of the schema.yaml track_rules.bug enum values`, "use code_fix | migration | config_change | test_fix | dependency_update | environment_setup | workflow_improvement | documentation_update | tooling_addition"));
3521
+ violations.push(violation10("medium", "compound.schema.invalid-resolution-type", `resolution_type "${doc.resolution_type}" is not one of the schema.yaml track_rules.bug enum values`, "use code_fix | migration | config_change | test_fix | dependency_update | environment_setup | workflow_improvement | documentation_update | tooling_addition"));
3164
3522
  }
3165
3523
  } else if (doc.applies_when !== undefined && !Array.isArray(doc.applies_when)) {
3166
- violations.push(violation8("low", "compound.schema.invalid-applies-when", "knowledge-track `applies_when` must be a YAML list when present (schema.yaml track_rules.knowledge)", "list the conditions under `applies_when:`"));
3524
+ violations.push(violation10("low", "compound.schema.invalid-applies-when", "knowledge-track `applies_when` must be a YAML list when present (schema.yaml track_rules.knowledge)", "list the conditions under `applies_when:`"));
3167
3525
  }
3168
3526
  }
3169
3527
  if (doc.plan_id !== undefined && !isStr(doc.plan_id)) {
3170
- violations.push(violation8("low", "compound.schema.invalid-plan-id", "optional `plan_id` must be a string (schema.yaml optional_fields.plan_id)", "reference the status.json plan id as a string"));
3528
+ violations.push(violation10("low", "compound.schema.invalid-plan-id", "optional `plan_id` must be a string (schema.yaml optional_fields.plan_id)", "reference the status.json plan id as a string"));
3171
3529
  }
3172
3530
  if (doc.tags !== undefined) {
3173
3531
  if (!Array.isArray(doc.tags)) {
3174
- violations.push(violation8("low", "compound.schema.invalid-tags", "optional `tags` must be a YAML list (schema.yaml optional_fields.tags)", "list lowercase, hyphen-separated keywords"));
3532
+ violations.push(violation10("low", "compound.schema.invalid-tags", "optional `tags` must be a YAML list (schema.yaml optional_fields.tags)", "list lowercase, hyphen-separated keywords"));
3175
3533
  } else if (doc.tags.length > 8) {
3176
- violations.push(violation8("low", "compound.schema.tags-too-many", `tags has ${doc.tags.length} entries — max 8 (schema.yaml optional_fields.tags.max_items)`, "trim the tag list to at most 8 keywords"));
3534
+ violations.push(violation10("low", "compound.schema.tags-too-many", `tags has ${doc.tags.length} entries — max 8 (schema.yaml optional_fields.tags.max_items)`, "trim the tag list to at most 8 keywords"));
3177
3535
  }
3178
3536
  }
3179
- if (doc.last_updated !== undefined && (!isStr(doc.last_updated) || !DATE_RE3.test(doc.last_updated))) {
3180
- violations.push(violation8("low", "compound.schema.invalid-last-updated", `last_updated "${String(doc.last_updated)}" must be YYYY-MM-DD (schema.yaml optional_fields.last_updated)`, "use `YYYY-MM-DD`"));
3537
+ if (doc.last_updated !== undefined && (!isStr(doc.last_updated) || !DATE_RE5.test(doc.last_updated))) {
3538
+ violations.push(violation10("low", "compound.schema.invalid-last-updated", `last_updated "${String(doc.last_updated)}" must be YYYY-MM-DD (schema.yaml optional_fields.last_updated)`, "use `YYYY-MM-DD`"));
3181
3539
  }
3182
3540
  if (doc.related_components !== undefined && !Array.isArray(doc.related_components)) {
3183
- violations.push(violation8("low", "compound.schema.invalid-related-components", "optional `related_components` must be a YAML list (schema.yaml optional_fields.related_components)", "list the other components involved"));
3541
+ violations.push(violation10("low", "compound.schema.invalid-related-components", "optional `related_components` must be a YAML list (schema.yaml optional_fields.related_components)", "list the other components involved"));
3184
3542
  }
3185
3543
  return { ok: violations.length === 0, violations };
3186
3544
  }
@@ -3202,7 +3560,7 @@ function referenceExists(repoRoot, docText) {
3202
3560
  if (ref === "" || seen.has(ref))
3203
3561
  continue;
3204
3562
  seen.add(ref);
3205
- if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute5(ref)) {
3563
+ if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute7(ref)) {
3206
3564
  continue;
3207
3565
  }
3208
3566
  if (ref.includes("/") || REF_EXT_RE.test(ref)) {
@@ -3221,7 +3579,7 @@ function referenceExists(repoRoot, docText) {
3221
3579
  const dir = stack.pop();
3222
3580
  let entries;
3223
3581
  try {
3224
- entries = readdirSync5(dir, { withFileTypes: true });
3582
+ entries = readdirSync8(dir, { withFileTypes: true });
3225
3583
  } catch {
3226
3584
  continue;
3227
3585
  }
@@ -3230,7 +3588,7 @@ function referenceExists(repoRoot, docText) {
3230
3588
  break;
3231
3589
  if (entry.isDirectory()) {
3232
3590
  if (!WALK_SKIP_DIRS.has(entry.name))
3233
- stack.push(join8(dir, entry.name));
3591
+ stack.push(join12(dir, entry.name));
3234
3592
  } else if (!entry.isSymbolicLink()) {
3235
3593
  const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
3236
3594
  if (moduleNames.has(base))
@@ -3242,15 +3600,15 @@ function referenceExists(repoRoot, docText) {
3242
3600
  for (const { ref, isSymbol, module } of refs) {
3243
3601
  if (!isSymbol || module === undefined) {
3244
3602
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
3245
- if (existsSync5(resolve8(repoRoot, candidate))) {
3603
+ if (existsSync6(resolve10(repoRoot, candidate))) {
3246
3604
  checked++;
3247
3605
  } else {
3248
- violations.push(violation8("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"));
3606
+ 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"));
3249
3607
  }
3250
3608
  } else if (foundModules.has(module)) {
3251
3609
  checked++;
3252
3610
  } else {
3253
- violations.push(violation8("low", "compound.reference.module-missing", `symbol ref \`${ref}\` — heuristic: no ${module}.ts|tsx|js|jsx|mjs|cjs module file found under ${repoRoot} (compound-refresh Phase 2)`, "verify the module file exists, or update the reference"));
3611
+ violations.push(violation10("low", "compound.reference.module-missing", `symbol ref \`${ref}\` — heuristic: no ${module}.ts|tsx|js|jsx|mjs|cjs module file found under ${repoRoot} (compound-refresh Phase 2)`, "verify the module file exists, or update the reference"));
3254
3612
  }
3255
3613
  }
3256
3614
  return { ok: violations.length === 0, violations, checked };
@@ -3262,18 +3620,18 @@ function collectKnowledgeDocs(dir) {
3262
3620
  const current = stack.pop();
3263
3621
  let entries;
3264
3622
  try {
3265
- entries = readdirSync5(current, { withFileTypes: true });
3623
+ entries = readdirSync8(current, { withFileTypes: true });
3266
3624
  } catch {
3267
3625
  continue;
3268
3626
  }
3269
3627
  for (const entry of entries) {
3270
3628
  if (entry.isSymbolicLink())
3271
3629
  continue;
3272
- const full = join8(current, entry.name);
3630
+ const full = join12(current, entry.name);
3273
3631
  if (entry.isDirectory()) {
3274
3632
  stack.push(full);
3275
3633
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
3276
- docs.push(relative2(dir, full).split(sep).join("/"));
3634
+ docs.push(relative4(dir, full).split(sep3).join("/"));
3277
3635
  }
3278
3636
  }
3279
3637
  }
@@ -3289,14 +3647,14 @@ function normalizeIndexRef(cell) {
3289
3647
  }
3290
3648
  function assertIndexRows(knowledgeDir) {
3291
3649
  const violations = [];
3292
- const readmePath = join8(knowledgeDir, "README.md");
3293
- if (!existsSync5(readmePath)) {
3294
- violations.push(violation8("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
3650
+ const readmePath = join12(knowledgeDir, "README.md");
3651
+ if (!existsSync6(readmePath)) {
3652
+ violations.push(violation10("medium", "compound.index.missing-readme", `missing ${readmePath} — the knowledge index is required (mstar-compound Phase 6: every doc gets a README.md row)`, "create knowledge/README.md with a Document / Source Plan / Description / Status table"));
3295
3653
  return { ok: false, violations };
3296
3654
  }
3297
3655
  const docs = collectKnowledgeDocs(knowledgeDir);
3298
3656
  const rows = new Set;
3299
- for (const line of readFileSync7(readmePath, "utf8").split(/\r?\n/)) {
3657
+ for (const line of readFileSync10(readmePath, "utf8").split(/\r?\n/)) {
3300
3658
  if (!line.trim().startsWith("|"))
3301
3659
  continue;
3302
3660
  const cells = line.split("|").map((c) => c.trim());
@@ -3308,7 +3666,7 @@ function assertIndexRows(knowledgeDir) {
3308
3666
  }
3309
3667
  for (const doc of docs) {
3310
3668
  if (!rows.has(doc)) {
3311
- violations.push(violation8("medium", "compound.index.missing-row", `knowledge doc "${doc}" has no row in knowledge/README.md index (mstar-compound Phase 6 index obligations)`, `add a row \`| [<title>](${doc}) | <source plan> | <description> | <status> |\` to knowledge/README.md`));
3669
+ violations.push(violation10("medium", "compound.index.missing-row", `knowledge doc "${doc}" has no row in knowledge/README.md index (mstar-compound Phase 6 index obligations)`, `add a row \`| [<title>](${doc}) | <source plan> | <description> | <status> |\` to knowledge/README.md`));
3312
3670
  }
3313
3671
  }
3314
3672
  return { ok: violations.length === 0, violations };
@@ -3317,24 +3675,24 @@ function isFileLikeRoot(root) {
3317
3675
  return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename4(root));
3318
3676
  }
3319
3677
  function scopeGuard(path, allowedRoots) {
3320
- const resolved = resolve8(path);
3678
+ const resolved = resolve10(path);
3321
3679
  for (const root of allowedRoots) {
3322
- const r = resolve8(root);
3680
+ const r = resolve10(root);
3323
3681
  if (isFileLikeRoot(r)) {
3324
3682
  if (resolved === r)
3325
3683
  return { ok: true, violations: [] };
3326
- } else if (resolved === r || resolved.startsWith(r + sep)) {
3684
+ } else if (resolved === r || resolved.startsWith(r + sep3)) {
3327
3685
  return { ok: true, violations: [] };
3328
3686
  }
3329
3687
  }
3330
3688
  return {
3331
3689
  ok: false,
3332
3690
  violations: [
3333
- violation8("medium", "compound.scope.outside", `path "${path}" is outside the compound-refresh scope (allowed: ${allowedRoots.join(", ")}) — compound-refresh operates only on {HARNESS_DIR}/knowledge/**, {HARNESS_DIR}/knowledge/README.md, <repo-root>/CONCEPTS.md, {HARNESS_DIR}/status.json (mstar-compound-refresh SKILL.md § 产物与操作路径)`, "point the operation at one of the allowed paths")
3691
+ violation10("medium", "compound.scope.outside", `path "${path}" is outside the compound-refresh scope (allowed: ${allowedRoots.join(", ")}) — compound-refresh operates only on {HARNESS_DIR}/knowledge/**, {HARNESS_DIR}/knowledge/README.md, <repo-root>/CONCEPTS.md, {HARNESS_DIR}/status.json (mstar-compound-refresh SKILL.md § 产物与操作路径)`, "point the operation at one of the allowed paths")
3334
3692
  ]
3335
3693
  };
3336
3694
  }
3337
- function violation9(severity, code, message, fix) {
3695
+ function violation11(severity, code, message, fix) {
3338
3696
  return { ok: false, severity, code, message, fix };
3339
3697
  }
3340
3698
  var TASK_ARTIFACT_RE = /\btask-\d+(?:-(?:brief|report|fix-report|diff)|\.diff)\b/g;
@@ -3364,18 +3722,18 @@ function lintSkillFrontmatter(frontmatterText) {
3364
3722
  const violations = [];
3365
3723
  const fm = parseFrontmatter(frontmatterText);
3366
3724
  if (fm === null) {
3367
- violations.push(violation9("medium", "lint.frontmatter.missing", "no YAML frontmatter block found — a skill file must open with a `---` fenced frontmatter (mstar-skill-authoring § Frontmatter Contract)", "add a frontmatter block with `name` and `description` at the top of the file"));
3725
+ violations.push(violation11("medium", "lint.frontmatter.missing", "no YAML frontmatter block found — a skill file must open with a `---` fenced frontmatter (mstar-skill-authoring § Frontmatter Contract)", "add a frontmatter block with `name` and `description` at the top of the file"));
3368
3726
  return { ok: false, violations };
3369
3727
  }
3370
3728
  const name = fm.name ?? "";
3371
3729
  if (name === "") {
3372
- violations.push(violation9("medium", "lint.frontmatter.name.missing", "frontmatter `name` is missing — required (mstar-skill-authoring § Frontmatter Contract)", "add `name: <lowercase-hyphen-id>` to the frontmatter"));
3730
+ violations.push(violation11("medium", "lint.frontmatter.name.missing", "frontmatter `name` is missing — required (mstar-skill-authoring § Frontmatter Contract)", "add `name: <lowercase-hyphen-id>` to the frontmatter"));
3373
3731
  } else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
3374
- violations.push(violation9("medium", "lint.frontmatter.name.format", `frontmatter \`name\` must be lowercase-hyphen ("${name}") — e.g. example-skill (mstar-skill-authoring § Frontmatter Contract)`, "rename to a stable lowercase-hyphen id, e.g. `name: example-skill`"));
3732
+ violations.push(violation11("medium", "lint.frontmatter.name.format", `frontmatter \`name\` must be lowercase-hyphen ("${name}") — e.g. example-skill (mstar-skill-authoring § Frontmatter Contract)`, "rename to a stable lowercase-hyphen id, e.g. `name: example-skill`"));
3375
3733
  }
3376
3734
  const description = fm.description ?? "";
3377
3735
  if (description === "") {
3378
- violations.push(violation9("medium", "lint.frontmatter.description.missing", "frontmatter `description` is missing — the trigger contract is required (mstar-skill-authoring § Frontmatter Contract)", "add a `description:` that states when the skill loads (symptoms, context, roles, exclusions)"));
3736
+ violations.push(violation11("medium", "lint.frontmatter.description.missing", "frontmatter `description` is missing — the trigger contract is required (mstar-skill-authoring § Frontmatter Contract)", "add a `description:` that states when the skill loads (symptoms, context, roles, exclusions)"));
3379
3737
  } else {
3380
3738
  const stripped = description.replace(/`[^`]*`/g, " ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
3381
3739
  let pronoun = null;
@@ -3386,15 +3744,15 @@ function lintSkillFrontmatter(frontmatterText) {
3386
3744
  break;
3387
3745
  }
3388
3746
  if (pronoun !== null) {
3389
- violations.push(violation9("low", "lint.frontmatter.description.person", `description uses first/second-person pronoun "${pronoun[0]}" — keep the trigger contract third person (mstar-skill-authoring § Frontmatter Contract)`, 'rewrite without I/we/you/my/our/your/us, e.g. "Use when the user asks …"'));
3747
+ violations.push(violation11("low", "lint.frontmatter.description.person", `description uses first/second-person pronoun "${pronoun[0]}" — keep the trigger contract third person (mstar-skill-authoring § Frontmatter Contract)`, 'rewrite without I/we/you/my/our/your/us, e.g. "Use when the user asks …"'));
3390
3748
  }
3391
3749
  const start = description.trim().replace(/^[*_#>]+/, "").replace(/^["'`]+/, "").trim();
3392
3750
  if (WORKFLOW_VERB_START_RE.test(start)) {
3393
- violations.push(violation9("low", "lint.frontmatter.description.workflow", 'description reads as a workflow summary ("Explains/Describes/Covers …") — the description is the trigger contract, not a summary of steps (mstar-skill-authoring § Frontmatter Contract)', "describe when to load the skill (symptoms, context, roles, exclusions) instead of summarizing its steps"));
3751
+ violations.push(violation11("low", "lint.frontmatter.description.workflow", 'description reads as a workflow summary ("Explains/Describes/Covers …") — the description is the trigger contract, not a summary of steps (mstar-skill-authoring § Frontmatter Contract)', "describe when to load the skill (symptoms, context, roles, exclusions) instead of summarizing its steps"));
3394
3752
  } else {
3395
3753
  const words = description.trim().split(/\s+/).filter(Boolean).length;
3396
3754
  if (words > DESCRIPTION_MAX_WORDS) {
3397
- violations.push(violation9("low", "lint.frontmatter.description.workflow", `description is ${words} words — paragraph-length summaries bury the trigger contract (threshold ${DESCRIPTION_MAX_WORDS}, above the longest corpus description at 114 words, mstar-design-md; mstar-skill-authoring § Frontmatter Contract)`, "trim the description to a scannable trigger contract and move detail into the body"));
3755
+ violations.push(violation11("low", "lint.frontmatter.description.workflow", `description is ${words} words — paragraph-length summaries bury the trigger contract (threshold ${DESCRIPTION_MAX_WORDS}, above the longest corpus description at 114 words, mstar-design-md; mstar-skill-authoring § Frontmatter Contract)`, "trim the description to a scannable trigger contract and move detail into the body"));
3398
3756
  }
3399
3757
  }
3400
3758
  }
@@ -3422,7 +3780,7 @@ function parseFrontmatter(text) {
3422
3780
  return null;
3423
3781
  return fields;
3424
3782
  }
3425
- function violation10(severity, code, message, fix) {
3783
+ function violation12(severity, code, message, fix) {
3426
3784
  return { ok: false, severity, code, message, fix };
3427
3785
  }
3428
3786
  var ROLE_MAPPING = [
@@ -3477,19 +3835,19 @@ function validateRoleMapping(rolesDir, options = {}) {
3477
3835
  const violations = [];
3478
3836
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
3479
3837
  for (const { agentId, reference } of mapping) {
3480
- if (!existsSync6(join9(rolesDir, reference))) {
3481
- violations.push(violation10("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join9(rolesDir, reference)} or fix the mapping row`));
3838
+ if (!existsSync7(join13(rolesDir, reference))) {
3839
+ violations.push(violation12("medium", "roles.mapping.reference.missing", `role "${agentId}" maps to ${reference} which does not exist under ${rolesDir} (mstar-roles § Role Reference Mapping)`, `create ${join13(rolesDir, reference)} or fix the mapping row`));
3482
3840
  }
3483
3841
  }
3484
3842
  for (const { family, memberIds } of families) {
3485
3843
  const absent = memberIds.filter((id) => !referenceById.has(id));
3486
3844
  for (const id of absent) {
3487
- violations.push(violation10("medium", "roles.mapping.family.member.missing", `shared family "${family}" member "${id}" is absent from the role mapping (mstar-roles § Role Reference Mapping)`, `add "${id}" to the mapping`));
3845
+ violations.push(violation12("medium", "roles.mapping.family.member.missing", `shared family "${family}" member "${id}" is absent from the role mapping (mstar-roles § Role Reference Mapping)`, `add "${id}" to the mapping`));
3488
3846
  }
3489
3847
  if (absent.length === 0) {
3490
3848
  const refs = new Set(memberIds.map((id) => referenceById.get(id)));
3491
3849
  if (refs.size !== 1) {
3492
- violations.push(violation10("medium", "roles.mapping.family.shared", `shared family "${family}" (${memberIds.join(", ")}) must resolve to ONE shared reference file — got ${[...refs].join(", ")} (mstar-roles § Maintenance Rules: "Keep shared-family roles on one shared reference file")`, `point every "${family}" member at the same references/<role>-shared.md`));
3850
+ violations.push(violation12("medium", "roles.mapping.family.shared", `shared family "${family}" (${memberIds.join(", ")}) must resolve to ONE shared reference file — got ${[...refs].join(", ")} (mstar-roles § Maintenance Rules: "Keep shared-family roles on one shared reference file")`, `point every "${family}" member at the same references/<role>-shared.md`));
3493
3851
  }
3494
3852
  }
3495
3853
  }
@@ -3498,12 +3856,12 @@ function validateRoleMapping(rolesDir, options = {}) {
3498
3856
  for (const row of rows) {
3499
3857
  const existing = tableByRole.get(row.roleId);
3500
3858
  if (existing !== undefined) {
3501
- violations.push(violation10("medium", "roles.param.role.duplicate", `role "${row.roleId}" appears in both the ${existing} and ${table} parameter rows (mstar-roles § Parameter Table (SSOT))`, "remove the duplicate row"));
3859
+ violations.push(violation12("medium", "roles.param.role.duplicate", `role "${row.roleId}" appears in both the ${existing} and ${table} parameter rows (mstar-roles § Parameter Table (SSOT))`, "remove the duplicate row"));
3502
3860
  } else {
3503
3861
  tableByRole.set(row.roleId, table);
3504
3862
  }
3505
3863
  if (!referenceById.has(row.roleId)) {
3506
- violations.push(violation10("medium", "roles.param.role.missing", `${table} parameter row references unknown role "${row.roleId}" (mstar-roles § Parameter Table (SSOT))`, `add "${row.roleId}" to the role mapping or drop the row`));
3864
+ violations.push(violation12("medium", "roles.param.role.missing", `${table} parameter row references unknown role "${row.roleId}" (mstar-roles § Parameter Table (SSOT))`, `add "${row.roleId}" to the role mapping or drop the row`));
3507
3865
  }
3508
3866
  }
3509
3867
  };
@@ -3511,20 +3869,20 @@ function validateRoleMapping(rolesDir, options = {}) {
3511
3869
  checkParamRoles(qcReviewers, "QC reviewer");
3512
3870
  for (const row of devTrack) {
3513
3871
  if (row.track !== "primary" && row.track !== "parallel_secondary") {
3514
- violations.push(violation10("medium", "roles.param.track", `dev track for "${row.roleId}" is "${String(row.track)}" — must be primary or parallel_secondary (mstar-roles § Parameter Table (SSOT))`, 'set track to "primary" or "parallel_secondary"'));
3872
+ violations.push(violation12("medium", "roles.param.track", `dev track for "${row.roleId}" is "${String(row.track)}" — must be primary or parallel_secondary (mstar-roles § Parameter Table (SSOT))`, 'set track to "primary" or "parallel_secondary"'));
3515
3873
  }
3516
3874
  }
3517
3875
  const indices = qcReviewers.map((r) => r.reviewerIndex).sort((a, b) => a - b);
3518
3876
  const unique = new Set(indices);
3519
3877
  if (indices.length !== 3 || unique.size !== 3 || indices[0] !== 1 || indices[1] !== 2 || indices[2] !== 3) {
3520
- violations.push(violation10("high", "roles.param.qc.index.set", `QC reviewer_index must be exactly {1, 2, 3} across the three qc-specialist* seats — got [${indices.join(", ")}] (mstar-roles § Parameter Table (SSOT))`, "assign reviewer_index 1/2/3 to qc-specialist / qc-specialist-2 / qc-specialist-3"));
3878
+ violations.push(violation12("high", "roles.param.qc.index.set", `QC reviewer_index must be exactly {1, 2, 3} across the three qc-specialist* seats — got [${indices.join(", ")}] (mstar-roles § Parameter Table (SSOT))`, "assign reviewer_index 1/2/3 to qc-specialist / qc-specialist-2 / qc-specialist-3"));
3521
3879
  }
3522
3880
  for (const row of qcReviewers) {
3523
3881
  if (row.focus.trim() === "") {
3524
- violations.push(violation10("medium", "roles.param.qc.focus.missing", `QC seat "${row.roleId}" (reviewer_index ${row.reviewerIndex}) has an empty focus (mstar-roles § Parameter Table (SSOT))`, "add the review focus"));
3882
+ violations.push(violation12("medium", "roles.param.qc.focus.missing", `QC seat "${row.roleId}" (reviewer_index ${row.reviewerIndex}) has an empty focus (mstar-roles § Parameter Table (SSOT))`, "add the review focus"));
3525
3883
  }
3526
3884
  if (row.reportSuffix !== `qc${row.reviewerIndex}`) {
3527
- violations.push(violation10("medium", "roles.param.qc.suffix", `QC seat "${row.roleId}" report_suffix "${row.reportSuffix}" must equal qc${row.reviewerIndex} — tri reports land at {SDD_DIR}/review/qc1.md…qc3.md (mstar-roles § Parameter Table (SSOT))`, `set report_suffix to qc${row.reviewerIndex}`));
3885
+ violations.push(violation12("medium", "roles.param.qc.suffix", `QC seat "${row.roleId}" report_suffix "${row.reportSuffix}" must equal qc${row.reviewerIndex} — tri reports land at {SDD_DIR}/review/qc1.md…qc3.md (mstar-roles § Parameter Table (SSOT))`, `set report_suffix to qc${row.reviewerIndex}`));
3528
3886
  }
3529
3887
  }
3530
3888
  return { ok: violations.length === 0, violations };
@@ -3563,11 +3921,11 @@ function lintLoadOrder(skillTexts) {
3563
3921
  continue;
3564
3922
  const section = extractLoadOrderSection(text);
3565
3923
  if (section === null) {
3566
- violations.push(violation10("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section — every mstar-* topic skill must declare its first read (mstar-harness-core § 加载约定; mstar-roles § Load Order (Required))`, `add a "## Load Order" section naming mstar-harness-core as the first read`));
3924
+ violations.push(violation12("medium", "roles.loadorder.section.missing", `skill "${name}" has no Load Order / First action section — every mstar-* topic skill must declare its first read (mstar-harness-core § 加载约定; mstar-roles § Load Order (Required))`, `add a "## Load Order" section naming mstar-harness-core as the first read`));
3567
3925
  continue;
3568
3926
  }
3569
3927
  if (!section.includes("mstar-harness-core")) {
3570
- violations.push(violation10("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core § 加载约定: 凡 mstar-*(name ≠ mstar-harness-core)假定读者已 Read 本 skill)`, "name mstar-harness-core first in the Load Order section"));
3928
+ violations.push(violation12("medium", "roles.loadorder.core.missing", `skill "${name}" Load Order section does not declare mstar-harness-core as its first dependency (mstar-harness-core § 加载约定: 凡 mstar-*(name ≠ mstar-harness-core)假定读者已 Read 本 skill)`, "name mstar-harness-core first in the Load Order section"));
3571
3929
  }
3572
3930
  }
3573
3931
  return { ok: violations.length === 0, violations };
@@ -3593,7 +3951,7 @@ function resolveSkillRoot(host, paths) {
3593
3951
  return `deferred: pi has no plugin API in v1 — skill-root resolution lands with its adapter (roadmap §8.4)`;
3594
3952
  }
3595
3953
  }
3596
- function violation11(severity, code, message, fix) {
3954
+ function violation13(severity, code, message, fix) {
3597
3955
  return { ok: false, severity, code, message, fix };
3598
3956
  }
3599
3957
  var FIVE_QUESTION_SECTIONS = [
@@ -3643,7 +4001,7 @@ function lintFiveQuestion(bodyText, mode = "authoring") {
3643
4001
  const aliases = mode === "runtime" ? RUNTIME_HEADING_ALIASES[section.key] ?? [] : [];
3644
4002
  const covered = headings.some((heading) => heading.includes(label) || aliases.some((alias) => heading.includes(alias)));
3645
4003
  if (!covered) {
3646
- violations.push(violation11("low", `skill-authoring.five-question.${section.key}`, `body does not answer "${section.question}" — no "${section.label}" section (mstar-skill-authoring § Body 必须回答的 5 问 / § 默认 Body 结构)`, `add a "## ${section.label}" section covering ${section.question}`));
4004
+ violations.push(violation13("low", `skill-authoring.five-question.${section.key}`, `body does not answer "${section.question}" — no "${section.label}" section (mstar-skill-authoring § Body 必须回答的 5 问 / § 默认 Body 结构)`, `add a "## ${section.label}" section covering ${section.question}`));
3647
4005
  }
3648
4006
  }
3649
4007
  return { ok: violations.length === 0, violations };
@@ -3669,8 +4027,8 @@ class DshMstar extends Service {
3669
4027
  validateResidual(entry) {
3670
4028
  return validateResidual(entry);
3671
4029
  }
3672
- findingsCleanupGate(doc, planId, opts) {
3673
- return findingsCleanupGate(doc, planId, opts);
4030
+ findingsCleanupGate(register, planId, opts) {
4031
+ return findingsCleanupGate(register, planId, opts);
3674
4032
  }
3675
4033
  resolveCompassEnforcement(harnessDir) {
3676
4034
  return resolveCompassEnforcement(harnessDir);
@@ -3688,8 +4046,8 @@ class DshMstar extends Service {
3688
4046
 
3689
4047
  // src/gates/_shared.ts
3690
4048
  var import_schemastery = __toESM(require_lib2(), 1);
3691
- import { existsSync as existsSync7 } from "node:fs";
3692
- import { resolve as resolve7 } from "node:path";
4049
+ import { existsSync as existsSync5 } from "node:fs";
4050
+ import { resolve as resolve8 } from "node:path";
3693
4051
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3694
4052
  var STATUS_FILE = "status.json";
3695
4053
  var PERSONA_INTERPOLATION_HAZARD = /\{\{[\s\S]*\}\}/;
@@ -3726,8 +4084,8 @@ var Config = import_schemastery.default.object({
3726
4084
  workflowNames: import_schemastery.default.array(import_schemastery.default.string()).default(undefined),
3727
4085
  maxGoalRounds: import_schemastery.default.number().default(undefined)
3728
4086
  });
3729
- function formatViolation(violation12) {
3730
- return `[${violation12.severity}] ${violation12.code}: ${violation12.message}${violation12.fix !== undefined ? ` (fix: ${violation12.fix})` : ""}`;
4087
+ function formatViolation(violation14) {
4088
+ return `[${violation14.severity}] ${violation14.code}: ${violation14.message}${violation14.fix !== undefined ? ` (fix: ${violation14.fix})` : ""}`;
3731
4089
  }
3732
4090
  function asRecord(value) {
3733
4091
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
@@ -3737,13 +4095,13 @@ function resolveSeamHard(harnessDir, config) {
3737
4095
  return true;
3738
4096
  if (config.enforcement === "soft")
3739
4097
  return false;
3740
- return harnessDir !== null && resolveCompassEnforcement(harnessDir).hard;
4098
+ return harnessDir !== null && resolveRepoEnforcement(harnessDir).hard;
3741
4099
  }
3742
4100
  function resolvePackagedSkillsDir(fileUrl) {
3743
4101
  for (const rel of ["../harness-skills", "../../harness-skills"]) {
3744
4102
  try {
3745
4103
  const dir = fileURLToPath2(new URL(rel, fileUrl));
3746
- if (existsSync7(dir))
4104
+ if (existsSync5(dir))
3747
4105
  return dir;
3748
4106
  } catch {}
3749
4107
  }
@@ -3762,7 +4120,7 @@ function resolvePackagedAgentsDir(fileUrl) {
3762
4120
  for (const rel of ["../harness-agents", "../../harness-agents"]) {
3763
4121
  try {
3764
4122
  const dir = fileURLToPath2(new URL(rel, fileUrl));
3765
- if (existsSync7(dir))
4123
+ if (existsSync5(dir))
3766
4124
  return dir;
3767
4125
  } catch {}
3768
4126
  }
@@ -3782,14 +4140,14 @@ class HarnessResolver {
3782
4140
  explicit;
3783
4141
  cache = new Map;
3784
4142
  constructor(explicit) {
3785
- this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve7(process.cwd(), explicit) : null;
4143
+ this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve8(process.cwd(), explicit) : null;
3786
4144
  }
3787
4145
  forWorkspace(cwd) {
3788
4146
  if (this.explicit !== null)
3789
4147
  return this.explicit;
3790
4148
  if (cwd === undefined || cwd.trim() === "")
3791
4149
  return null;
3792
- const abs = resolve7(cwd);
4150
+ const abs = resolve8(cwd);
3793
4151
  const hit = this.cache.get(abs);
3794
4152
  if (hit !== undefined)
3795
4153
  return hit;
@@ -3821,27 +4179,161 @@ function skillLocalConfig(config) {
3821
4179
  if ((customSkillDirs === undefined || customSkillDirs.length === 0) && bundledSkillDir === undefined) {
3822
4180
  return;
3823
4181
  }
3824
- return {
3825
- providerName: "mstar",
3826
- includeDefaultRoots: false,
3827
- ...customSkillDirs !== undefined && customSkillDirs.length > 0 ? { customSkillDirs } : {},
3828
- ...bundledSkillDir !== undefined ? { bundledSkillDir } : {}
3829
- };
4182
+ return {
4183
+ providerName: "mstar",
4184
+ includeDefaultRoots: false,
4185
+ ...customSkillDirs !== undefined && customSkillDirs.length > 0 ? { customSkillDirs } : {},
4186
+ ...bundledSkillDir !== undefined ? { bundledSkillDir } : {}
4187
+ };
4188
+ }
4189
+
4190
+ // src/gates/catalog.ts
4191
+ import { existsSync as existsSync12, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "node:fs";
4192
+ import { join as join11 } from "node:path";
4193
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4194
+
4195
+ // src/gates/agent-flow.ts
4196
+ import { appendFileSync, closeSync, existsSync as existsSync11, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync7, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4197
+ import { join as join10 } from "node:path";
4198
+
4199
+ // src/gates/dispatch.ts
4200
+ import { existsSync as existsSync10 } from "node:fs";
4201
+ import { basename as basename5, join as join8, resolve as resolve11 } from "node:path";
4202
+
4203
+ // src/gates/workflow-selection.ts
4204
+ import { existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
4205
+ import { join as join5, relative as relative3 } from "node:path";
4206
+ var TERMINAL_STATUS_CACHE_MAX = 64;
4207
+ var terminalStatusCache = new Map;
4208
+ function terminalStatusOf(snapshotPath) {
4209
+ let mtimeMs;
4210
+ try {
4211
+ mtimeMs = statSync3(snapshotPath).mtimeMs;
4212
+ } catch {
4213
+ return;
4214
+ }
4215
+ const cached = terminalStatusCache.get(snapshotPath);
4216
+ if (cached !== undefined && cached.mtimeMs === mtimeMs)
4217
+ return cached;
4218
+ let terminal = false;
4219
+ try {
4220
+ const snapshot = readJson(snapshotPath);
4221
+ const status = snapshot.status;
4222
+ terminal = typeof status === "string" && WORKFLOW_TERMINAL_STATUSES.includes(status);
4223
+ } catch {
4224
+ return;
4225
+ }
4226
+ const verdict = { mtimeMs, terminal };
4227
+ terminalStatusCache.set(snapshotPath, verdict);
4228
+ while (terminalStatusCache.size > TERMINAL_STATUS_CACHE_MAX) {
4229
+ const oldest = terminalStatusCache.keys().next().value;
4230
+ if (oldest === undefined)
4231
+ break;
4232
+ terminalStatusCache.delete(oldest);
4233
+ }
4234
+ return verdict;
4235
+ }
4236
+ function resolveActiveWorkflow(harnessDir) {
4237
+ const statusPath = join5(harnessDir, STATUS_FILE);
4238
+ if (!existsSync8(statusPath)) {
4239
+ return {
4240
+ kind: "error",
4241
+ code: "status.missing",
4242
+ message: `no ${STATUS_FILE} at ${harnessDir} — the root v2 document is required`
4243
+ };
4244
+ }
4245
+ let doc;
4246
+ try {
4247
+ doc = readJson(statusPath);
4248
+ } catch {
4249
+ return { kind: "error", code: "status.unreadable", message: `cannot read ${statusPath}` };
4250
+ }
4251
+ if (doc.version !== 2) {
4252
+ return {
4253
+ kind: "error",
4254
+ code: "status.migration-required",
4255
+ message: `status.json schema version 2 required — got ${JSON.stringify(doc.version)} (v1 or unknown version); run \`mstar migrate\` to convert the tree`
4256
+ };
4257
+ }
4258
+ if (!Array.isArray(doc.workflows) || doc.workflows.length === 0) {
4259
+ return {
4260
+ kind: "error",
4261
+ code: "workflow.selection.no-active",
4262
+ message: "no active lifecycle in status.json workflows[] — the agent-flow writer appends only to an active lifecycle"
4263
+ };
4264
+ }
4265
+ const first = doc.workflows[0];
4266
+ const entry = asRecord(first);
4267
+ const workflowId = typeof entry?.id === "string" ? entry.id : undefined;
4268
+ const dir = typeof entry?.dir === "string" ? entry.dir : undefined;
4269
+ if (workflowId === undefined || dir === undefined) {
4270
+ return {
4271
+ kind: "error",
4272
+ code: "workflow.selection.invalid-entry",
4273
+ message: `workflows[] first entry is missing id/dir: ${JSON.stringify(first)}`
4274
+ };
4275
+ }
4276
+ if (doc.workflows.length > 1) {
4277
+ return {
4278
+ kind: "active",
4279
+ workflowId,
4280
+ dir,
4281
+ warning: {
4282
+ code: "workflow.selection.multi-active",
4283
+ message: `${doc.workflows.length} active lifecycles in status.json workflows[] — selected the first (${workflowId}); no silent pick`
4284
+ }
4285
+ };
4286
+ }
4287
+ return { kind: "active", workflowId, dir };
4288
+ }
4289
+ function resolveReadWorkflow(harnessDir) {
4290
+ const active = resolveActiveWorkflow(harnessDir);
4291
+ if (active.kind === "active")
4292
+ return active;
4293
+ if (active.kind === "error" && active.code !== "workflow.selection.no-active")
4294
+ return active;
4295
+ const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
4296
+ if (!existsSync8(workflowsDir)) {
4297
+ return {
4298
+ kind: "error",
4299
+ code: "workflow.selection.no-snapshot",
4300
+ message: `no workflow snapshots under ${workflowsDir} — run \`mstar migrate\` or start a lifecycle`
4301
+ };
4302
+ }
4303
+ let entries;
4304
+ try {
4305
+ entries = readdirSync5(workflowsDir, { withFileTypes: true });
4306
+ } catch {
4307
+ return {
4308
+ kind: "error",
4309
+ code: "workflow.selection.no-snapshot",
4310
+ message: `cannot read ${workflowsDir} — no workflow snapshot selected`
4311
+ };
4312
+ }
4313
+ let best;
4314
+ for (const entry of entries) {
4315
+ if (!entry.isDirectory())
4316
+ continue;
4317
+ const snapshotPath = join5(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
4318
+ if (!existsSync8(snapshotPath))
4319
+ continue;
4320
+ const verdict = terminalStatusOf(snapshotPath);
4321
+ if (verdict === undefined || !verdict.terminal)
4322
+ continue;
4323
+ if (best === undefined || verdict.mtimeMs > best.mtimeMs) {
4324
+ best = { workflowId: entry.name, dir: join5(relative3(harnessDir, workflowsDir), entry.name), mtimeMs: verdict.mtimeMs };
4325
+ }
4326
+ }
4327
+ if (best === undefined) {
4328
+ return {
4329
+ kind: "error",
4330
+ code: "workflow.selection.no-snapshot",
4331
+ message: `no terminal workflow snapshot under ${workflowsDir} — run \`mstar migrate\` or start a lifecycle`
4332
+ };
4333
+ }
4334
+ return { kind: "terminal", workflowId: best.workflowId, dir: best.dir };
3830
4335
  }
3831
4336
 
3832
- // src/gates/catalog.ts
3833
- import { existsSync as existsSync11, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
3834
- import { join as join12 } from "node:path";
3835
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
3836
-
3837
- // src/gates/agent-flow.ts
3838
- import { appendFileSync, existsSync as existsSync10, readFileSync as readFileSync6, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
3839
- import { join as join10 } from "node:path";
3840
-
3841
- // src/gates/dispatch.ts
3842
- import { existsSync as existsSync9 } from "node:fs";
3843
- import { basename as basename5, join as join6, resolve as resolve9 } from "node:path";
3844
-
3845
4337
  // src/gates/workflow-policy.ts
3846
4338
  var WORKFLOW_NAME_UNKNOWN_CODE = "workflow.name.unknown";
3847
4339
  var WORKFLOW_LEASE_UNCOVERED_CODE = "workflow.lease.uncovered";
@@ -3946,7 +4438,7 @@ function resolveDispatchHard(harnessDir, config, assignmentText) {
3946
4438
  return false;
3947
4439
  if (parseEnforcementFlag(assignmentHeaderRegion(assignmentText)).hard)
3948
4440
  return true;
3949
- return harnessDir !== null && resolveCompassEnforcement(harnessDir).hard;
4441
+ return harnessDir !== null && resolveRepoEnforcement(harnessDir).hard;
3950
4442
  }
3951
4443
  function denyReason(tool, verdict) {
3952
4444
  return [
@@ -4016,25 +4508,34 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4016
4508
  const planId = planIdOf(header);
4017
4509
  if (planId === undefined || planId === "")
4018
4510
  return [];
4019
- const statusPath = join6(harnessDir, STATUS_FILE);
4020
- if (!existsSync9(statusPath)) {
4511
+ const selection = resolveActiveWorkflow(harnessDir);
4512
+ if (selection.kind !== "active") {
4021
4513
  if (!sdd)
4022
4514
  return [];
4023
- return [leaseViolation("lease.dispatch.unverifiable", `${statusPath} is missing the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid status.json registering the plan row (first implement dispatch requires a plan row)")];
4515
+ if (selection.kind === "error" && selection.code === "status.unreadable") {
4516
+ 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)")];
4517
+ }
4518
+ 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)")];
4024
4519
  }
4025
- let doc;
4520
+ const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4521
+ if (!existsSync10(snapshotPath)) {
4522
+ if (!sdd)
4523
+ return [];
4524
+ 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)")];
4525
+ }
4526
+ let snapshot;
4026
4527
  try {
4027
- doc = readJson(statusPath);
4528
+ snapshot = readJson(snapshotPath);
4028
4529
  } catch (error) {
4029
4530
  if (!sdd)
4030
4531
  return [];
4031
- return [leaseViolation("lease.dispatch.unreadable", `cannot read ${statusPath}: ${error.message} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid status.json (the status gate refuses invalid writes)")];
4532
+ return [leaseViolation("lease.dispatch.unreadable", `cannot read ${snapshotPath}: ${error.message} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "restore a valid workflow snapshot (the status gate refuses invalid writes)")];
4032
4533
  }
4033
- const row = Array.isArray(doc.plans) ? doc.plans.map(asRecord).find((r) => r?.id === planId || r?.plan_id === planId) : undefined;
4534
+ const row = Array.isArray(snapshot.plans) ? snapshot.plans.map(asRecord).find((r) => r?.id === planId || r?.plan_id === planId) : undefined;
4034
4535
  if (row === undefined) {
4035
4536
  if (!sdd)
4036
4537
  return [];
4037
- return [leaseViolation("lease.dispatch.plan-not-found", `plan ${planId} is not registered in ${STATUS_FILE} — cannot verify its execution_lease before writable dispatch`, "register the plan row in status.json (first implement dispatch requires a plan row)")];
4538
+ return [leaseViolation("lease.dispatch.plan-not-found", `plan ${planId} is not registered in ${snapshotPath} — cannot verify its execution_lease before writable dispatch`, "register the plan row in the workflow snapshot (first implement dispatch requires a plan row)")];
4038
4539
  }
4039
4540
  if (!sdd && row.status !== "InProgress")
4040
4541
  return [];
@@ -4050,7 +4551,7 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4050
4551
  const wt = worktree === undefined ? undefined : firstToken(worktree);
4051
4552
  if (isNaValue(wt)) {
4052
4553
  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)"));
4053
- } else if (resolve9(wt) !== resolve9(String(lease.worktree_path ?? ""))) {
4554
+ } else if (resolve11(wt) !== resolve11(String(lease.worktree_path ?? ""))) {
4054
4555
  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)"));
4055
4556
  }
4056
4557
  const forms = parseAssignmentBranchForms(header);
@@ -4090,26 +4591,45 @@ function worktreeL2Violations(header) {
4090
4591
  violations.push(...l2PreDispatchCheck({ tracks }).violations);
4091
4592
  return violations;
4092
4593
  }
4594
+ function activeSnapshotRows(harnessDir) {
4595
+ if (!existsSync10(join8(harnessDir, STATUS_FILE)))
4596
+ return { rows: null, controlWorktreePath: undefined, unreadable: false };
4597
+ const selection = resolveActiveWorkflow(harnessDir);
4598
+ if (selection.kind !== "active") {
4599
+ return {
4600
+ rows: null,
4601
+ controlWorktreePath: undefined,
4602
+ unreadable: selection.kind === "error" && selection.code !== "workflow.selection.no-active"
4603
+ };
4604
+ }
4605
+ const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4606
+ if (!existsSync10(snapshotPath))
4607
+ return { rows: null, controlWorktreePath: undefined, unreadable: true };
4608
+ let doc;
4609
+ try {
4610
+ doc = readJson(snapshotPath);
4611
+ } catch {
4612
+ return { rows: null, controlWorktreePath: undefined, unreadable: true };
4613
+ }
4614
+ if (!Array.isArray(doc.plans))
4615
+ return { rows: null, controlWorktreePath: undefined, unreadable: true };
4616
+ const controlWorktreePath = typeof doc.control_worktree_path === "string" ? doc.control_worktree_path : undefined;
4617
+ const rows = doc.plans.map((row) => asRecord(row)).filter((row) => row !== undefined);
4618
+ return { rows, controlWorktreePath, unreadable: false };
4619
+ }
4093
4620
  function worktreeL1Violations(harnessDir, header) {
4094
4621
  if (harnessDir === null)
4095
4622
  return [];
4096
4623
  const planId = planIdOf(header);
4097
4624
  if (planId === undefined || planId === "")
4098
4625
  return [];
4099
- const statusPath = join6(harnessDir, STATUS_FILE);
4100
- if (!existsSync9(statusPath))
4101
- return [];
4102
- let doc;
4103
- try {
4104
- doc = readJson(statusPath);
4105
- } catch {
4626
+ const read = activeSnapshotRows(harnessDir);
4627
+ if (read.rows === null)
4106
4628
  return [];
4107
- }
4108
- const metadata = asRecord(doc.metadata);
4109
- const controlWorktreePath = typeof metadata?.control_worktree_path === "string" ? metadata.control_worktree_path : undefined;
4629
+ const controlWorktreePath = read.controlWorktreePath;
4110
4630
  if (controlWorktreePath === undefined || controlWorktreePath.trim() === "")
4111
4631
  return [];
4112
- const row = Array.isArray(doc.plans) ? doc.plans.map(asRecord).find((r) => r?.id === planId || r?.plan_id === planId) : undefined;
4632
+ const row = read.rows.find((r) => r?.id === planId || r?.plan_id === planId);
4113
4633
  const lease = asRecord(row?.execution_lease);
4114
4634
  const leaseWorktreePath = typeof lease?.worktree_path === "string" ? lease.worktree_path : undefined;
4115
4635
  const leaseWorkingBranch = typeof lease?.working_branch === "string" ? lease.working_branch : undefined;
@@ -4121,19 +4641,10 @@ function worktreeL1Violations(harnessDir, header) {
4121
4641
  function writableFanOutUncovered(harnessDir) {
4122
4642
  if (harnessDir === null)
4123
4643
  return { unreadable: false };
4124
- const statusPath = join6(harnessDir, STATUS_FILE);
4125
- if (!existsSync9(statusPath))
4126
- return { unreadable: false };
4127
- let doc;
4128
- try {
4129
- doc = readJson(statusPath);
4130
- } catch {
4131
- return { unreadable: true };
4132
- }
4133
- if (!Array.isArray(doc.plans))
4134
- return { unreadable: true };
4135
- for (const raw of doc.plans) {
4136
- const row = asRecord(raw);
4644
+ const read = activeSnapshotRows(harnessDir);
4645
+ if (read.rows === null)
4646
+ return { unreadable: read.unreadable };
4647
+ for (const row of read.rows) {
4137
4648
  if (row === undefined || row.status !== "InProgress")
4138
4649
  continue;
4139
4650
  const planId = typeof row.id === "string" && row.id !== "" ? row.id : typeof row.plan_id === "string" && row.plan_id !== "" ? row.plan_id : "in-progress-plan";
@@ -4182,7 +4693,7 @@ function gateWorkflow(ctx, harnessDir, config, adapter, exec) {
4182
4693
  }
4183
4694
  const pb = writableFanOutUncovered(harnessDir);
4184
4695
  if (pb.unreadable) {
4185
- const statusLabel = harnessDir === null ? STATUS_FILE : join6(harnessDir, STATUS_FILE);
4696
+ const statusLabel = harnessDir === null ? STATUS_FILE : join8(harnessDir, STATUS_FILE);
4186
4697
  ctx.logger(DISPATCH_LOGGER).warn(`workflow gate (${toolName}) (fail-open): cannot read ${statusLabel} — P-b lease attribution degraded; call allowed`);
4187
4698
  }
4188
4699
  const verdict = workflowPolicy(config, adapter.workflowAskCache, {
@@ -4301,6 +4812,7 @@ var AGENT_FLOW_FILE = "agent-flow.jsonl";
4301
4812
  var AGENT_FLOW_MAX_EVENTS = 500;
4302
4813
  var AGENT_FLOW_DEFAULT_LIMIT = 50;
4303
4814
  var AGENT_FLOW_SIZE_GATE_BYTES = 64 * 1024;
4815
+ var AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES = 64 * 1024;
4304
4816
  var WORKFLOW_LEDGER_MAX_ID_LENGTH = 512;
4305
4817
  var WORKFLOW_LEDGER_MAX_LABEL_LENGTH = 512;
4306
4818
  var WORKFLOW_LEDGER_MAX_NAME_LENGTH = 1024;
@@ -4319,6 +4831,7 @@ var SETTLE_SEAM = "tools/post-execute";
4319
4831
  var SETTLE_SEAM_PAIRING_NOTE = `settle seam "${SETTLE_SEAM}" IS part of the verified dsh-tools registry surface (runPostExecute dispatches it for every tool call) — foreground dispatch calls settle here, background subagents settle via ctx.jobs.onJobDone pairing; only UNPAIRED payloads (non-dispatch tools, calls outside the apply-scoped pairing window) stay dispatch-only — never a fabricated settle`;
4320
4832
  var logSink;
4321
4833
  var settleNoteLogged = false;
4834
+ var noActiveWarned = false;
4322
4835
  var invalidator;
4323
4836
  function setAgentFlowInvalidator(invalidate) {
4324
4837
  const prior = invalidator;
@@ -4329,6 +4842,7 @@ function setAgentFlowLogger(sink) {
4329
4842
  const prior = logSink;
4330
4843
  logSink = sink;
4331
4844
  settleNoteLogged = false;
4845
+ noActiveWarned = false;
4332
4846
  return prior;
4333
4847
  }
4334
4848
  function log(level, message) {
@@ -4366,25 +4880,92 @@ function callPairingKey(exec) {
4366
4880
  return;
4367
4881
  return `${sessionIdOf(exec) ?? ""}\x00${callId}`;
4368
4882
  }
4369
- function appendEvent(harnessDir, event) {
4370
- const file = join10(harnessDir, AGENT_FLOW_FILE);
4371
- appendFileSync(file, `${JSON.stringify(event)}
4883
+ function resolveAgentFlowWriteDir(harnessDir) {
4884
+ const selection = resolveActiveWorkflow(harnessDir);
4885
+ if (selection.kind === "active")
4886
+ return join10(harnessDir, selection.dir);
4887
+ const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
4888
+ if (!noActiveWarned) {
4889
+ noActiveWarned = true;
4890
+ log("warn", `agent-flow record skipped — ${message}`);
4891
+ }
4892
+ return null;
4893
+ }
4894
+ var WORKFLOW_LEDGER_LOCKDIR = ".ledger-write.lockdir";
4895
+ var WORKFLOW_LOCKER_TIMEOUT_MS = 30000;
4896
+ var WORKFLOW_LOCKER_POLL_MS = 10;
4897
+ var WORKFLOW_LOCKER_HOLDER_PID = "holder.pid";
4898
+ var heldWorkflowLocks = new Set;
4899
+ function sleepSync(ms) {
4900
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
4901
+ }
4902
+ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4903
+ const lockDir = join10(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4904
+ if (heldWorkflowLocks.has(lockDir)) {
4905
+ 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`);
4906
+ }
4907
+ const timeoutMs = opts.timeoutMs ?? WORKFLOW_LOCKER_TIMEOUT_MS;
4908
+ const pollMs = opts.pollMs ?? WORKFLOW_LOCKER_POLL_MS;
4909
+ const deadline = Date.now() + timeoutMs;
4910
+ let acquired = null;
4911
+ for (;; ) {
4912
+ try {
4913
+ mkdirSync3(lockDir);
4914
+ const st = statSync5(lockDir);
4915
+ acquired = { dev: st.dev, ino: st.ino };
4916
+ break;
4917
+ } catch (error) {
4918
+ if (error.code !== "EEXIST")
4919
+ throw error;
4920
+ if (Date.now() >= deadline) {
4921
+ throw new Error(`${lockDir} already exists — another writer holds the workflow write lock; Blocked (same-host exclusive lock; same pattern as the engine's withStatusWriteLock). ` + `Recovery: remove ${lockDir} if no writer is alive (holder.pid inside names the acquiring process)`);
4922
+ }
4923
+ sleepSync(pollMs);
4924
+ }
4925
+ }
4926
+ heldWorkflowLocks.add(lockDir);
4927
+ try {
4928
+ try {
4929
+ writeFileSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4930
+ } catch {}
4931
+ return fn();
4932
+ } finally {
4933
+ heldWorkflowLocks.delete(lockDir);
4934
+ try {
4935
+ const current = statSync5(lockDir);
4936
+ if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
4937
+ try {
4938
+ unlinkSync2(join10(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4939
+ } catch {}
4940
+ rmdirSync(lockDir);
4941
+ }
4942
+ } catch {}
4943
+ }
4944
+ }
4945
+ function appendEvent(workflowDir, event) {
4946
+ const file = join10(workflowDir, AGENT_FLOW_FILE);
4947
+ withWorkflowDirLock(workflowDir, () => {
4948
+ appendFileSync(file, `${JSON.stringify(event)}
4372
4949
  `);
4373
- if (statSync2(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
4374
- return;
4375
- const content = readFileSync6(file, "utf8");
4376
- const lines = content.replace(/\n$/, "").split(`
4950
+ if (statSync5(file).size <= AGENT_FLOW_SIZE_GATE_BYTES)
4951
+ return;
4952
+ const content = readFileSync7(file, "utf8");
4953
+ const lines = content.replace(/\n$/, "").split(`
4377
4954
  `);
4378
- if (lines.length > AGENT_FLOW_MAX_EVENTS) {
4379
- const tmp = `${file}.tmp`;
4380
- writeFileSync2(tmp, `${lines.slice(-AGENT_FLOW_MAX_EVENTS).join(`
4955
+ if (lines.length > AGENT_FLOW_MAX_EVENTS) {
4956
+ const tmp = `${file}.tmp`;
4957
+ writeFileSync2(tmp, `${lines.slice(-AGENT_FLOW_MAX_EVENTS).join(`
4381
4958
  `)}
4382
4959
  `);
4383
- renameSync2(tmp, file);
4384
- }
4960
+ renameSync2(tmp, file);
4961
+ }
4962
+ });
4385
4963
  }
4386
4964
  function recordDispatch(input) {
4387
4965
  try {
4966
+ const workflowDir = resolveAgentFlowWriteDir(input.harnessDir);
4967
+ if (workflowDir === null)
4968
+ return;
4388
4969
  const header = assignmentHeaderRegion(input.prompt);
4389
4970
  const fields = parseAssignmentFields(header);
4390
4971
  const planId = planIdOf(header);
@@ -4403,7 +4984,7 @@ function recordDispatch(input) {
4403
4984
  verdict: verdictOf(input.violations, input.hard),
4404
4985
  hard: input.hard
4405
4986
  };
4406
- appendEvent(input.harnessDir, event);
4987
+ appendEvent(workflowDir, event);
4407
4988
  try {
4408
4989
  invalidator?.(input.harnessDir);
4409
4990
  } catch (error) {
@@ -4415,6 +4996,7 @@ function recordDispatch(input) {
4415
4996
  if (key !== undefined) {
4416
4997
  input.pairing.dispatchByCallId.set(key, {
4417
4998
  harnessDir: input.harnessDir,
4999
+ workflowDir,
4418
5000
  ...agent !== undefined ? { agent } : {},
4419
5001
  role: fields.executeAs ?? "",
4420
5002
  ...planId !== undefined && !isNaValue(planId) ? { planId } : {},
@@ -4431,6 +5013,9 @@ function recordDispatch(input) {
4431
5013
  }
4432
5014
  function recordSettle(input) {
4433
5015
  try {
5016
+ const workflowDir = input.workflowDir ?? resolveAgentFlowWriteDir(input.harnessDir);
5017
+ if (workflowDir === null)
5018
+ return;
4434
5019
  const event = {
4435
5020
  v: 1,
4436
5021
  ts: Date.now(),
@@ -4442,7 +5027,7 @@ function recordSettle(input) {
4442
5027
  ...input.planId !== undefined && input.planId !== "" ? { planId: input.planId } : {},
4443
5028
  ...input.taskId !== undefined && input.taskId !== "" ? { taskId: input.taskId } : {}
4444
5029
  };
4445
- appendEvent(input.harnessDir, event);
5030
+ appendEvent(workflowDir, event);
4446
5031
  try {
4447
5032
  invalidator?.(input.harnessDir);
4448
5033
  } catch (error) {
@@ -4454,7 +5039,10 @@ function recordSettle(input) {
4454
5039
  }
4455
5040
  function recordWorkflowEvent(input) {
4456
5041
  try {
4457
- appendEvent(input.harnessDir, input.event);
5042
+ const workflowDir = input.workflowDir ?? resolveAgentFlowWriteDir(input.harnessDir);
5043
+ if (workflowDir === null)
5044
+ return false;
5045
+ appendEvent(workflowDir, input.event);
4458
5046
  try {
4459
5047
  invalidator?.(input.harnessDir);
4460
5048
  } catch (error) {
@@ -4468,6 +5056,9 @@ function recordWorkflowEvent(input) {
4468
5056
  }
4469
5057
  function recordWorkflowVerdict(input) {
4470
5058
  try {
5059
+ const workflowDir = resolveAgentFlowWriteDir(input.harnessDir);
5060
+ if (workflowDir === null)
5061
+ return;
4471
5062
  const agent = input.exec !== undefined ? agentOfExec(input.exec) : undefined;
4472
5063
  const event = {
4473
5064
  v: 1,
@@ -4481,7 +5072,7 @@ function recordWorkflowVerdict(input) {
4481
5072
  verdict: input.verdict,
4482
5073
  ...input.code !== undefined && input.code !== "" ? { code: input.code } : {}
4483
5074
  };
4484
- appendEvent(input.harnessDir, event);
5075
+ appendEvent(workflowDir, event);
4485
5076
  try {
4486
5077
  invalidator?.(input.harnessDir);
4487
5078
  } catch (error) {
@@ -4706,14 +5297,49 @@ function summaryOf(events) {
4706
5297
  return { role, outcome, count };
4707
5298
  }).sort((a, b) => b.count - a.count || a.role.localeCompare(b.role) || a.outcome.localeCompare(b.outcome));
4708
5299
  }
4709
- function readAgentFlow(harnessDir, limit) {
4710
- const file = join10(harnessDir, AGENT_FLOW_FILE);
4711
- if (!existsSync10(file)) {
5300
+ function ledgerContent(file, n) {
5301
+ const size = statSync5(file).size;
5302
+ if (size <= AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES) {
5303
+ return readFileSync7(file, "utf8");
5304
+ }
5305
+ let windowBytes = AGENT_FLOW_TAIL_READ_THRESHOLD_BYTES;
5306
+ const fd = openSync(file, "r");
5307
+ try {
5308
+ for (;; ) {
5309
+ const start = Math.max(0, size - windowBytes);
5310
+ const buffer = Buffer.alloc(size - start);
5311
+ let read = 0;
5312
+ while (read < buffer.length) {
5313
+ const got = readSync(fd, buffer, read, buffer.length - read, start + read);
5314
+ if (got === 0)
5315
+ break;
5316
+ read += got;
5317
+ }
5318
+ let content = buffer.toString("utf8");
5319
+ if (start > 0) {
5320
+ const firstNewline = content.indexOf(`
5321
+ `);
5322
+ content = firstNewline >= 0 ? content.slice(firstNewline + 1) : "";
5323
+ }
5324
+ const completeLines = content.split(`
5325
+ `).length - 1;
5326
+ if (start === 0 || completeLines >= n)
5327
+ return content;
5328
+ windowBytes *= 2;
5329
+ }
5330
+ } finally {
5331
+ closeSync(fd);
5332
+ }
5333
+ }
5334
+ function readAgentFlow(workflowDir, limit) {
5335
+ const file = join10(workflowDir, AGENT_FLOW_FILE);
5336
+ if (!existsSync11(file)) {
4712
5337
  return { events: [], summary: [] };
4713
5338
  }
5339
+ const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
4714
5340
  let content;
4715
5341
  try {
4716
- content = readFileSync6(file, "utf8");
5342
+ content = ledgerContent(file, n);
4717
5343
  } catch {
4718
5344
  return null;
4719
5345
  }
@@ -4730,7 +5356,6 @@ function readAgentFlow(harnessDir, limit) {
4730
5356
  continue;
4731
5357
  }
4732
5358
  }
4733
- const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
4734
5359
  const latestFirst = events.reverse().slice(0, n);
4735
5360
  return {
4736
5361
  events: latestFirst.map(eventView),
@@ -4740,6 +5365,7 @@ function readAgentFlow(harnessDir, limit) {
4740
5365
  function recordSettleWithRef(ref, outcome, durationMs) {
4741
5366
  recordSettle({
4742
5367
  harnessDir: ref.harnessDir,
5368
+ workflowDir: ref.workflowDir,
4743
5369
  ...ref.agent !== undefined && ref.agent.trim() !== "" ? { agent: ref.agent } : {},
4744
5370
  outcome,
4745
5371
  ...durationMs !== undefined && Number.isFinite(durationMs) ? { durationMs } : {},
@@ -4832,7 +5458,7 @@ function residualSeverityIndex(severity) {
4832
5458
  function pluginVersion() {
4833
5459
  for (const rel of ["../package.json", "../../package.json"]) {
4834
5460
  try {
4835
- const pkg = JSON.parse(readFileSync8(new URL(rel, import.meta.url), "utf8"));
5461
+ const pkg = JSON.parse(readFileSync9(new URL(rel, import.meta.url), "utf8"));
4836
5462
  if (typeof pkg.version === "string" && pkg.version !== "")
4837
5463
  return pkg.version;
4838
5464
  } catch {}
@@ -4846,7 +5472,7 @@ function engineStatusSource(harnessDir) {
4846
5472
  form: "catalog",
4847
5473
  version: pluginVersion(),
4848
5474
  harnessDir,
4849
- enforcement: harnessDir !== null ? resolveCompassEnforcement(harnessDir) : { hard: false, source: "none" },
5475
+ enforcement: harnessDir !== null ? resolveRepoEnforcement(harnessDir) : { hard: false, source: "none" },
4850
5476
  ...iteration !== undefined ? { iteration } : {},
4851
5477
  state: harnessDir !== null ? harnessStateSource(harnessDir) : null
4852
5478
  };
@@ -4904,28 +5530,36 @@ function renderEngineStatusCatalog(source) {
4904
5530
  }
4905
5531
  const state = source.state;
4906
5532
  if (state !== null) {
4907
- lines.push(`plans: ${state.plans.length === 0 ? "none registered" : state.plans.map((p) => `${p.id}(${p.status})`).join(" ")}`);
4908
- lines.push(`residuals: ${state.residuals.length === 0 ? "none open" : state.residuals.map((r) => `${r.severity} ${r.count}`).join(", ")}`);
4909
- if (state.iterationBaseBranch !== null && state.targetBranch !== null) {
4910
- const integration = state.specIntegrationBranch !== null ? ` (spec integration: ${state.specIntegrationBranch})` : "";
4911
- lines.push(`branch: ${state.iterationBaseBranch} ${state.targetBranch}${integration}`);
4912
- }
4913
- const policy = [
4914
- state.pushPolicy !== null ? `push ${state.pushPolicy}` : null,
4915
- state.worktreeMode !== null ? `worktree ${state.worktreeMode}` : null,
4916
- state.controlWorktreePath !== null ? `control ${state.controlWorktreePath}` : null
4917
- ].filter((part) => part !== null).join("; ");
4918
- if (policy !== "")
4919
- lines.push(`policy: ${policy}`);
4920
- lines.push(`leases: ${state.leases.length === 0 ? "none active" : state.leases.map((l) => `${l.planId} → ${l.holder}${l.worktreePath !== null ? ` (${l.worktreePath})` : ""}`).join("; ")}`);
4921
- if (state.knowledge !== null) {
4922
- lines.push(`knowledge: ${state.knowledge.docCount} doc${state.knowledge.docCount === 1 ? "" : "s"} (${state.knowledge.categories.join(", ")})`);
4923
- }
4924
- if (state.direction !== null)
4925
- lines.push(`direction: ${state.direction}`);
4926
- const flow = state.agentFlow;
4927
- if (flow !== null && flow.events.length > 0) {
4928
- lines.push(renderAgentFlowLine(flow));
5533
+ if (state.selection.kind === "error") {
5534
+ lines.push(`workflow selection: ERROR (${state.selection.code}) ${state.selection.message}`);
5535
+ } else {
5536
+ lines.push(`workflow: ${state.selection.workflowId} (${state.selection.kind})`);
5537
+ if (state.selection.kind === "active" && state.selection.warning !== undefined) {
5538
+ lines.push(`workflow warning: ${state.selection.warning.code} ${state.selection.warning.message}`);
5539
+ }
5540
+ lines.push(`plans: ${state.plans.length === 0 ? "none registered" : state.plans.map((p) => `${p.id}(${p.status})`).join(" ")}`);
5541
+ lines.push(`residuals: ${state.residuals.length === 0 ? "none open" : state.residuals.map((r) => `${r.severity} ${r.count}`).join(", ")}`);
5542
+ if (state.iterationBaseBranch !== null && state.targetBranch !== null) {
5543
+ const integration = state.specIntegrationBranch !== null ? ` (spec integration: ${state.specIntegrationBranch})` : "";
5544
+ lines.push(`branch: ${state.iterationBaseBranch} → ${state.targetBranch}${integration}`);
5545
+ }
5546
+ const policy = [
5547
+ state.pushPolicy !== null ? `push ${state.pushPolicy}` : null,
5548
+ state.worktreeMode !== null ? `worktree ${state.worktreeMode}` : null,
5549
+ state.controlWorktreePath !== null ? `control ${state.controlWorktreePath}` : null
5550
+ ].filter((part) => part !== null).join("; ");
5551
+ if (policy !== "")
5552
+ lines.push(`policy: ${policy}`);
5553
+ lines.push(`leases: ${state.leases.length === 0 ? "none active" : state.leases.map((l) => `${l.planId} → ${l.holder}${l.worktreePath !== null ? ` (${l.worktreePath})` : ""}`).join("; ")}`);
5554
+ if (state.knowledge !== null) {
5555
+ lines.push(`knowledge: ${state.knowledge.docCount} doc${state.knowledge.docCount === 1 ? "" : "s"} (${state.knowledge.categories.join(", ")})`);
5556
+ }
5557
+ if (state.direction !== null)
5558
+ lines.push(`direction: ${state.direction}`);
5559
+ const flow = state.agentFlow;
5560
+ if (flow !== null && flow.events.length > 0) {
5561
+ lines.push(renderAgentFlowLine(flow));
5562
+ }
4929
5563
  }
4930
5564
  }
4931
5565
  lines.push("</mstar_engine_status>");
@@ -4953,28 +5587,59 @@ function hhmm(ts) {
4953
5587
  function harnessStateSource(harnessDir) {
4954
5588
  if (harnessDir === null)
4955
5589
  return null;
4956
- const statusPath = join12(harnessDir, STATUS_FILE);
4957
- if (!existsSync11(statusPath))
5590
+ const statusPath = join11(harnessDir, STATUS_FILE);
5591
+ if (!existsSync12(statusPath))
4958
5592
  return null;
4959
5593
  try {
4960
- const doc = readJson(statusPath);
5594
+ const selection = resolveReadWorkflow(harnessDir);
5595
+ const rollup = residualRollup(harnessDir);
4961
5596
  const str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
4962
5597
  const iterationRefsOf = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
5598
+ const compass = steeringCompassPath(harnessDir);
5599
+ let compassFields;
5600
+ if (compass !== undefined) {
5601
+ try {
5602
+ compassFields = parseCompassFrontmatter(compass.compassPath);
5603
+ } catch {
5604
+ compassFields = undefined;
5605
+ }
5606
+ }
5607
+ if (selection.kind === "error") {
5608
+ return selectionErrorState(selection, rollup, harnessDir, compass);
5609
+ }
5610
+ const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5611
+ let snapshot;
5612
+ if (!existsSync12(snapshotPath)) {
5613
+ return selectionErrorState({
5614
+ kind: "error",
5615
+ code: "workflow.selection.snapshot-unreadable",
5616
+ message: `cannot read the selected workflow snapshot ${snapshotPath}`
5617
+ }, rollup, harnessDir, compass);
5618
+ }
5619
+ try {
5620
+ snapshot = readJson(snapshotPath);
5621
+ } catch {
5622
+ return selectionErrorState({
5623
+ kind: "error",
5624
+ code: "workflow.selection.snapshot-unreadable",
5625
+ message: `cannot read the selected workflow snapshot ${snapshotPath}`
5626
+ }, rollup, harnessDir, compass);
5627
+ }
4963
5628
  const plans = [];
4964
5629
  const leases = [];
4965
- if (Array.isArray(doc.plans)) {
4966
- for (const row of doc.plans.map(asRecord)) {
5630
+ if (Array.isArray(snapshot.plans)) {
5631
+ for (const row of snapshot.plans.map(asRecord)) {
4967
5632
  if (row === undefined)
4968
5633
  continue;
4969
5634
  const id = typeof row.plan_id === "string" ? row.plan_id : typeof row.id === "string" ? row.id : undefined;
4970
5635
  if (id === undefined)
4971
5636
  continue;
4972
- const metadata2 = asRecord(row.metadata);
5637
+ const metadata = asRecord(row.metadata);
4973
5638
  plans.push({
4974
5639
  id,
4975
5640
  status: typeof row.status === "string" ? row.status : "",
4976
5641
  doneAt: str(row.done_at),
4977
- iterationRefs: iterationRefsOf(metadata2?.iteration_refs)
5642
+ iterationRefs: iterationRefsOf(metadata?.iteration_refs)
4978
5643
  });
4979
5644
  const lease = asRecord(row.execution_lease);
4980
5645
  if (lease !== undefined && typeof lease.holder === "string") {
@@ -4986,39 +5651,105 @@ function harnessStateSource(harnessDir) {
4986
5651
  }
4987
5652
  }
4988
5653
  }
4989
- const residuals = [];
4990
- const residualMap = asRecord(doc.residual_findings);
4991
- if (residualMap !== undefined) {
4992
- const counts = new Map;
4993
- for (const planId of Object.keys(residualMap)) {
4994
- const findings = residualMap[planId];
4995
- if (!Array.isArray(findings))
4996
- continue;
4997
- for (const finding of findings) {
4998
- const severity = asRecord(finding)?.severity;
4999
- if (typeof severity === "string" && RESIDUAL_SEVERITIES.includes(severity)) {
5000
- counts.set(severity, (counts.get(severity) ?? 0) + 1);
5654
+ const { residuals, residualFindings } = rollup;
5655
+ const branch = asRecord(snapshot.branch);
5656
+ const executionPolicy = asRecord(snapshot.execution_policy);
5657
+ return {
5658
+ selection,
5659
+ plans,
5660
+ residuals,
5661
+ residualFindings,
5662
+ project: projectRollupSource(harnessDir, residuals),
5663
+ iterationBaseBranch: str(branch?.base) ?? str(compassFields?.iteration_base_branch) ?? null,
5664
+ targetBranch: str(branch?.target) ?? str(compassFields?.target_branch) ?? null,
5665
+ specIntegrationBranch: str(branch?.integration),
5666
+ pushPolicy: str(executionPolicy?.push_policy),
5667
+ worktreeMode: str(executionPolicy?.worktree_mode),
5668
+ controlWorktreePath: str(snapshot.control_worktree_path),
5669
+ leases,
5670
+ knowledge: knowledgeDigest(harnessDir),
5671
+ direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
5672
+ agentFlow: readAgentFlow(join11(harnessDir, selection.dir), 50)
5673
+ };
5674
+ } catch {
5675
+ return null;
5676
+ }
5677
+ }
5678
+ function selectionErrorState(selection, rollup, harnessDir, compass) {
5679
+ return {
5680
+ selection,
5681
+ plans: [],
5682
+ residuals: [],
5683
+ residualFindings: null,
5684
+ project: projectRollupSource(harnessDir, rollup.residuals),
5685
+ iterationBaseBranch: null,
5686
+ targetBranch: null,
5687
+ specIntegrationBranch: null,
5688
+ pushPolicy: null,
5689
+ worktreeMode: null,
5690
+ controlWorktreePath: null,
5691
+ leases: [],
5692
+ knowledge: knowledgeDigest(harnessDir),
5693
+ direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
5694
+ agentFlow: null
5695
+ };
5696
+ }
5697
+ function projectRollupSource(harnessDir, residuals) {
5698
+ const milestones = [];
5699
+ const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5700
+ if (existsSync12(projectsDir)) {
5701
+ let entries;
5702
+ try {
5703
+ entries = readdirSync7(projectsDir, { withFileTypes: true });
5704
+ } catch {
5705
+ entries = [];
5706
+ }
5707
+ for (const entry of entries) {
5708
+ if (!entry.isDirectory())
5709
+ continue;
5710
+ const roadmapPath = join11(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5711
+ if (!existsSync12(roadmapPath))
5712
+ continue;
5713
+ try {
5714
+ const doc = parseCompassFrontmatterText(readFileSync9(roadmapPath, "utf8"), roadmapPath);
5715
+ if (Array.isArray(doc.milestones)) {
5716
+ for (const milestone of doc.milestones) {
5717
+ if (typeof milestone === "string" && milestone.trim() !== "")
5718
+ milestones.push(milestone);
5001
5719
  }
5002
5720
  }
5003
- }
5004
- for (const severity of RESIDUAL_SEVERITIES) {
5005
- const count = counts.get(severity);
5006
- if (count !== undefined && count > 0)
5007
- residuals.push({ severity, count });
5721
+ } catch {
5722
+ continue;
5008
5723
  }
5009
5724
  }
5010
- let residualFindings = null;
5011
- if (residualMap !== undefined) {
5012
- residualFindings = [];
5013
- for (const planId of Object.keys(residualMap)) {
5014
- const findings = residualMap[planId];
5725
+ }
5726
+ return { milestones, openResiduals: residuals };
5727
+ }
5728
+ function residualRollup(harnessDir) {
5729
+ const registers = projectRegisters(harnessDir);
5730
+ const residuals = [];
5731
+ const counts = new Map;
5732
+ let residualFindings = null;
5733
+ if (registers.length > 0) {
5734
+ residualFindings = [];
5735
+ for (const register of registers) {
5736
+ const entries = asRecord(register.doc.entries);
5737
+ if (entries === undefined)
5738
+ continue;
5739
+ for (const planId of Object.keys(entries)) {
5740
+ const findings = entries[planId];
5015
5741
  if (!Array.isArray(findings))
5016
5742
  continue;
5017
5743
  for (const finding of findings) {
5018
5744
  const entry = asRecord(finding);
5019
- if (entry === undefined || !isOpenResidualParity(entry))
5745
+ if (entry === undefined)
5746
+ continue;
5747
+ if (!isOpenResidualParity(entry))
5020
5748
  continue;
5021
5749
  const severity = entry.severity;
5750
+ if (typeof severity === "string" && RESIDUAL_SEVERITIES.includes(severity)) {
5751
+ counts.set(severity, (counts.get(severity) ?? 0) + 1);
5752
+ }
5022
5753
  if (typeof severity !== "string" || residualSeverityIndex(severity) === -1)
5023
5754
  continue;
5024
5755
  residualFindings.push({
@@ -5029,46 +5760,50 @@ function harnessStateSource(harnessDir) {
5029
5760
  });
5030
5761
  }
5031
5762
  }
5032
- residualFindings.sort((a, b) => residualSeverityIndex(a.severity) - residualSeverityIndex(b.severity));
5033
- residualFindings = residualFindings.slice(0, 10);
5034
5763
  }
5035
- const metadata = asRecord(doc.metadata);
5036
- const compass = steeringCompassPath(harnessDir);
5037
- let compassFields;
5038
- if (compass !== undefined) {
5039
- try {
5040
- compassFields = parseCompassFrontmatter(compass.compassPath);
5041
- } catch {
5042
- compassFields = undefined;
5043
- }
5764
+ for (const severity of RESIDUAL_SEVERITIES) {
5765
+ const count = counts.get(severity);
5766
+ if (count !== undefined && count > 0)
5767
+ residuals.push({ severity, count });
5044
5768
  }
5045
- return {
5046
- plans,
5047
- residuals,
5048
- residualFindings,
5049
- iterationBaseBranch: str(metadata?.iteration_base_branch) ?? str(compassFields?.iteration_base_branch) ?? null,
5050
- targetBranch: str(metadata?.target_branch) ?? str(compassFields?.target_branch) ?? null,
5051
- specIntegrationBranch: str(metadata?.spec_integration_branch),
5052
- pushPolicy: str(metadata?.push_policy),
5053
- worktreeMode: str(metadata?.worktree_mode),
5054
- controlWorktreePath: str(metadata?.control_worktree_path),
5055
- leases,
5056
- knowledge: knowledgeDigest(harnessDir),
5057
- direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
5058
- agentFlow: readAgentFlow(harnessDir, 50)
5059
- };
5769
+ residualFindings.sort((a, b) => residualSeverityIndex(a.severity) - residualSeverityIndex(b.severity));
5770
+ residualFindings = residualFindings.slice(0, 10);
5771
+ }
5772
+ return { residuals, residualFindings };
5773
+ }
5774
+ function projectRegisters(harnessDir) {
5775
+ const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5776
+ if (!existsSync12(projectsDir))
5777
+ return [];
5778
+ let entries;
5779
+ try {
5780
+ entries = readdirSync7(projectsDir, { withFileTypes: true });
5060
5781
  } catch {
5061
- return null;
5782
+ return [];
5783
+ }
5784
+ const registers = [];
5785
+ for (const entry of entries) {
5786
+ if (!entry.isDirectory())
5787
+ continue;
5788
+ const registerPath = join11(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5789
+ if (!existsSync12(registerPath))
5790
+ continue;
5791
+ try {
5792
+ registers.push({ projectId: entry.name, doc: readJson(registerPath) });
5793
+ } catch {
5794
+ continue;
5795
+ }
5062
5796
  }
5797
+ return registers;
5063
5798
  }
5064
5799
  function knowledgeDigest(harnessDir) {
5065
- const indexPath = join12(harnessDir, "knowledge", "README.md");
5066
- if (!existsSync11(indexPath))
5800
+ const indexPath = join11(harnessDir, "knowledge", "README.md");
5801
+ if (!existsSync12(indexPath))
5067
5802
  return null;
5068
5803
  try {
5069
5804
  const categories = new Set;
5070
5805
  let docCount = 0;
5071
- for (const line of readFileSync8(indexPath, "utf8").split(/\r?\n/)) {
5806
+ for (const line of readFileSync9(indexPath, "utf8").split(/\r?\n/)) {
5072
5807
  const row = line.trim().match(/^\|(.+)\|$/);
5073
5808
  if (row === null)
5074
5809
  continue;
@@ -5091,7 +5826,7 @@ function knowledgeDigest(harnessDir) {
5091
5826
  }
5092
5827
  function compassDirection(compassPath) {
5093
5828
  try {
5094
- const content = readFileSync8(compassPath, "utf8");
5829
+ const content = readFileSync9(compassPath, "utf8");
5095
5830
  const section = content.match(/^## Direction lock[^\n]*\n+([\s\S]*?)(?=\n## |$)/m);
5096
5831
  if (section === null)
5097
5832
  return null;
@@ -5108,23 +5843,23 @@ function compassDirection(compassPath) {
5108
5843
  }
5109
5844
  function steeringCompassPath(harnessDir) {
5110
5845
  const iterationsDir = resolveIterationDir(harnessDir);
5111
- if (!existsSync11(iterationsDir))
5846
+ if (!existsSync12(iterationsDir))
5112
5847
  return;
5113
5848
  let entries;
5114
5849
  try {
5115
- entries = readdirSync4(iterationsDir, { withFileTypes: true });
5850
+ entries = readdirSync7(iterationsDir, { withFileTypes: true });
5116
5851
  } catch {
5117
5852
  return;
5118
5853
  }
5119
5854
  for (const entry of entries) {
5120
5855
  if (!entry.isDirectory())
5121
5856
  continue;
5122
- const compassPath = join12(iterationsDir, entry.name, "delivery-compass.md");
5123
- if (!existsSync11(compassPath))
5857
+ const compassPath = join11(iterationsDir, entry.name, "delivery-compass.md");
5858
+ if (!existsSync12(compassPath))
5124
5859
  continue;
5125
5860
  let content;
5126
5861
  try {
5127
- content = readFileSync8(compassPath, "utf8");
5862
+ content = readFileSync9(compassPath, "utf8");
5128
5863
  } catch {
5129
5864
  continue;
5130
5865
  }
@@ -5138,16 +5873,20 @@ function steeringCompassPath(harnessDir) {
5138
5873
  function iterationGateSource(harnessDir) {
5139
5874
  if (harnessDir === null)
5140
5875
  return;
5141
- const statusPath = join12(harnessDir, STATUS_FILE);
5142
- if (!existsSync11(statusPath))
5876
+ const statusPath = join11(harnessDir, STATUS_FILE);
5877
+ if (!existsSync12(statusPath))
5143
5878
  return;
5144
5879
  const compass = steeringCompassPath(harnessDir);
5145
5880
  if (compass === undefined)
5146
5881
  return;
5882
+ const selection = resolveReadWorkflow(harnessDir);
5883
+ if (selection.kind === "error")
5884
+ return;
5885
+ const snapshotPath = join11(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5147
5886
  try {
5148
- const statusDoc = readJson(statusPath);
5887
+ const snapshotDoc = readJson(snapshotPath);
5149
5888
  const compassDoc = parseCompassFrontmatter(compass.compassPath);
5150
- const result = evaluatePhaseGate(statusDoc, compassDoc);
5889
+ const result = evaluatePhaseGate(snapshotDoc, compassDoc);
5151
5890
  const gate2 = {
5152
5891
  transition: result.transition,
5153
5892
  all_plans_done: result.allPlansDone,
@@ -5159,7 +5898,7 @@ function iterationGateSource(harnessDir) {
5159
5898
  const compassStatus = compassDoc.status === "active" || compassDoc.status === "locked" ? compassDoc.status : undefined;
5160
5899
  return {
5161
5900
  iterationId: compass.iterationId,
5162
- statusPath,
5901
+ statusPath: snapshotPath,
5163
5902
  compassPath: compass.compassPath,
5164
5903
  gate: gate2,
5165
5904
  ...compassStatus !== undefined ? { compassStatus } : {}
@@ -5197,44 +5936,103 @@ function agentDigestKey(agent, cwd) {
5197
5936
  }
5198
5937
 
5199
5938
  // src/gates/status.ts
5200
- import { basename as basename7, dirname as dirname5, join as join13, resolve as resolve10 } from "node:path";
5939
+ import { existsSync as existsSync13, readdirSync as readdirSync9 } from "node:fs";
5940
+ import { basename as basename7, join as join14, relative as relative5, resolve as resolve13 } from "node:path";
5201
5941
  var LOGGER_NAME = "mstar/status-gate";
5202
- function isStatusTarget(harnessDir, target) {
5203
- return basename7(target.displayPath) === STATUS_FILE && resolve10(dirname5(target.displayPath)) === resolve10(harnessDir);
5942
+ function harnessDocKindOfTarget(harnessDir, targetPath) {
5943
+ const resolved = resolve13(targetPath);
5944
+ const name = basename7(resolved);
5945
+ if (name !== STATUS_FILE && name !== WORKFLOW_SNAPSHOT_FILE && name !== PROJECT_REGISTER_FILE)
5946
+ return null;
5947
+ const rel = relative5(harnessDir, resolved);
5948
+ if (name === STATUS_FILE && rel === STATUS_FILE)
5949
+ return "status";
5950
+ let workflowDir;
5951
+ let projectDir;
5952
+ try {
5953
+ workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
5954
+ projectDir = resolveProjectDir(harnessDir, { harnessDir });
5955
+ } catch {
5956
+ workflowDir = join14(harnessDir, "workflows");
5957
+ projectDir = join14(harnessDir, "projects");
5958
+ }
5959
+ if (name === WORKFLOW_SNAPSHOT_FILE && /^[^/]+\/snapshot\.json$/.test(relative5(workflowDir, resolved)))
5960
+ return "snapshot";
5961
+ if (name === PROJECT_REGISTER_FILE && /^[^/]+\/residuals\.json$/.test(relative5(projectDir, resolved)))
5962
+ return "register";
5963
+ return null;
5204
5964
  }
5205
5965
  function resolveHard(harnessDir, config) {
5206
5966
  if (config.enforcement === "hard")
5207
5967
  return true;
5208
5968
  if (config.enforcement === "soft")
5209
5969
  return false;
5210
- return resolveCompassEnforcement(harnessDir).hard;
5970
+ return resolveRepoEnforcement(harnessDir).hard;
5971
+ }
5972
+ function validateStatusValue(doc, kind, harnessDir) {
5973
+ if (kind === "snapshot") {
5974
+ const base = validateWorkflowSnapshot(doc);
5975
+ if (!base.ok)
5976
+ return base;
5977
+ if (harnessDir === null || harnessDir === undefined)
5978
+ return base;
5979
+ const violations = snapshotFindingsCleanupViolations(doc, harnessDir);
5980
+ if (violations.length === 0)
5981
+ return base;
5982
+ return { ok: false, violations };
5983
+ }
5984
+ if (kind === "register")
5985
+ return validateProjectRegister(doc);
5986
+ return validateStatus(doc);
5211
5987
  }
5212
- function validateStatusValue(doc) {
5213
- const base = validateStatus(doc);
5214
- if (!base.ok)
5215
- return base;
5988
+ function snapshotFindingsCleanupViolations(doc, harnessDir) {
5216
5989
  const record = asRecord(doc);
5217
5990
  if (record === undefined)
5218
- return base;
5991
+ return [];
5219
5992
  const violations = [];
5993
+ const registers = projectRegisterDocs(harnessDir);
5220
5994
  for (const row of Array.isArray(record.plans) ? record.plans : []) {
5221
- const metadata = asRecord(row.metadata);
5995
+ const planRow = asRecord(row);
5996
+ const metadata = planRow === undefined ? undefined : asRecord(planRow.metadata);
5222
5997
  const mode = metadata?.["findings_cleanup"];
5223
5998
  if (mode !== "zero-residual" && mode !== "allow-residual")
5224
5999
  continue;
5225
- const planId = typeof row.id === "string" ? row.id : typeof row.plan_id === "string" ? row.plan_id : undefined;
6000
+ const planId = typeof planRow?.id === "string" ? planRow.id : typeof planRow?.plan_id === "string" ? planRow.plan_id : undefined;
5226
6001
  if (planId === undefined)
5227
6002
  continue;
5228
- violations.push(...findingsCleanupGate(record, planId, { mode }).violations);
6003
+ for (const register of registers) {
6004
+ violations.push(...findingsCleanupGate(register.doc, planId, { mode }).violations);
6005
+ }
5229
6006
  }
5230
- if (violations.length === 0)
5231
- return base;
5232
- return { ok: false, violations };
6007
+ return violations;
6008
+ }
6009
+ function projectRegisterDocs(harnessDir) {
6010
+ const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
6011
+ let entries;
6012
+ try {
6013
+ entries = readdirSync9(projectsDir, { withFileTypes: true });
6014
+ } catch {
6015
+ return [];
6016
+ }
6017
+ const registers = [];
6018
+ for (const entry of entries) {
6019
+ if (!entry.isDirectory())
6020
+ continue;
6021
+ const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
6022
+ if (!existsSync13(registerPath))
6023
+ continue;
6024
+ try {
6025
+ registers.push({ projectId: entry.name, doc: readJson(registerPath) });
6026
+ } catch {
6027
+ continue;
6028
+ }
6029
+ }
6030
+ return registers;
5233
6031
  }
5234
- function validateStatusDoc(statusPath) {
6032
+ function validateStatusDoc(path, kind, harnessDir) {
5235
6033
  let doc;
5236
6034
  try {
5237
- doc = readJson(statusPath);
6035
+ doc = readJson(path);
5238
6036
  } catch (error) {
5239
6037
  return {
5240
6038
  ok: false,
@@ -5246,16 +6044,16 @@ function validateStatusDoc(statusPath) {
5246
6044
  }]
5247
6045
  };
5248
6046
  }
5249
- return validateStatusValue(doc);
6047
+ return validateStatusValue(doc, kind, harnessDir);
5250
6048
  }
5251
6049
  function gateStatusIntent(ctx, harnessDir, config, adapter, operation, target) {
5252
6050
  try {
5253
6051
  if (harnessDir === null)
5254
6052
  return;
5255
- if (!isStatusTarget(harnessDir, target))
6053
+ const kind = harnessDocKindOfTarget(harnessDir, target.displayPath);
6054
+ if (kind === null)
5256
6055
  return;
5257
- const statusPath = join13(harnessDir, STATUS_FILE);
5258
- const result = adapter.statusGate(statusPath);
6056
+ const result = adapter.statusGate(target.displayPath, kind, harnessDir);
5259
6057
  const hard = resolveHard(harnessDir, config);
5260
6058
  const verdict = applyEnforcement(result, { hard });
5261
6059
  if (!verdict.ok) {
@@ -5290,8 +6088,8 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
5290
6088
  }
5291
6089
 
5292
6090
  // src/gates/skill-lint.ts
5293
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "node:fs";
5294
- import { basename as basename8, dirname as dirname7, resolve as resolve11, sep as sep2 } from "node:path";
6091
+ import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
6092
+ import { basename as basename8, dirname as dirname7, resolve as resolve14, sep as sep2 } from "node:path";
5295
6093
  var SKILL_LINT_LOGGER = "mstar/skill-lint";
5296
6094
 
5297
6095
  class SkillLintVetoError extends Error {
@@ -5354,8 +6152,8 @@ function skillRootsOf(config) {
5354
6152
  function isSkillTarget(roots, target) {
5355
6153
  if (basename8(target.displayPath) !== "SKILL.md")
5356
6154
  return false;
5357
- const resolvedPath = resolve11(target.displayPath);
5358
- return roots.some((root) => resolvedPath.startsWith(resolve11(root) + sep2));
6155
+ const resolvedPath = resolve14(target.displayPath);
6156
+ return roots.some((root) => resolvedPath.startsWith(resolve14(root) + sep2));
5359
6157
  }
5360
6158
  function skillNameOf(target) {
5361
6159
  return basename8(dirname7(target.displayPath));
@@ -5370,12 +6168,12 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
5370
6168
  return;
5371
6169
  if (!isSkillTarget(roots, target))
5372
6170
  return;
5373
- const skillPath = resolve11(target.displayPath);
5374
- if (!existsSync12(skillPath))
6171
+ const skillPath = resolve14(target.displayPath);
6172
+ if (!existsSync14(skillPath))
5375
6173
  return;
5376
6174
  let doc;
5377
6175
  try {
5378
- doc = readFileSync9(skillPath, "utf8");
6176
+ doc = readFileSync11(skillPath, "utf8");
5379
6177
  } catch (error) {
5380
6178
  ctx.logger(SKILL_LINT_LOGGER).error(`skill lint degraded to allow (cannot read ${skillPath}): ${error.message}`);
5381
6179
  ctx.emit("mstar/skill-lint", {
@@ -5422,8 +6220,8 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
5422
6220
  }
5423
6221
 
5424
6222
  // src/gates/seams.ts
5425
- import { existsSync as existsSync13, readFileSync as readFileSync10, readdirSync as readdirSync6 } from "node:fs";
5426
- import { basename as basename9, dirname as dirname8, join as join14, resolve as resolve12, sep as sep3 } from "node:path";
6223
+ import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
6224
+ import { basename as basename9, dirname as dirname9, join as join16, resolve as resolve15, sep as sep4 } from "node:path";
5427
6225
  var SEAM_LOGGERS = {
5428
6226
  "design-md": "mstar/design-md-lint",
5429
6227
  audit: "mstar/audit-lint",
@@ -5440,7 +6238,7 @@ function isMarkdownDoc(path) {
5440
6238
  function isAuditPlanTarget(path) {
5441
6239
  if (!isMarkdownDoc(path))
5442
6240
  return false;
5443
- const segments = resolve12(path).split(sep3);
6241
+ const segments = resolve15(path).split(sep4);
5444
6242
  for (let i = 0;i < segments.length; i++) {
5445
6243
  if (segments[i] !== "plans")
5446
6244
  continue;
@@ -5451,7 +6249,7 @@ function isAuditPlanTarget(path) {
5451
6249
  return false;
5452
6250
  }
5453
6251
  function isRolesTarget(path) {
5454
- const segments = resolve12(path).split(sep3);
6252
+ const segments = resolve15(path).split(sep4);
5455
6253
  const idx = segments.indexOf("mstar-roles");
5456
6254
  if (idx === -1)
5457
6255
  return false;
@@ -5461,14 +6259,14 @@ function isRolesTarget(path) {
5461
6259
  return rest[0] === "SKILL.md" || rest[0] === "references" && rest[rest.length - 1].endsWith(".md");
5462
6260
  }
5463
6261
  function rolesDirOf(path) {
5464
- const segments = resolve12(path).split(sep3);
6262
+ const segments = resolve15(path).split(sep4);
5465
6263
  const idx = segments.indexOf("mstar-roles");
5466
6264
  if (idx === -1)
5467
- return dirname8(path);
5468
- return idx === 0 ? sep3 : join14(sep3, ...segments.slice(1, idx + 1));
6265
+ return dirname9(path);
6266
+ return idx === 0 ? sep4 : join16(sep4, ...segments.slice(1, idx + 1));
5469
6267
  }
5470
6268
  function isSeamTarget(seam, harnessDir, target) {
5471
- const path = resolve12(target.displayPath);
6269
+ const path = resolve15(target.displayPath);
5472
6270
  switch (seam) {
5473
6271
  case "design-md": {
5474
6272
  const base = basename9(path);
@@ -5479,7 +6277,7 @@ function isSeamTarget(seam, harnessDir, target) {
5479
6277
  case "compound": {
5480
6278
  if (harnessDir === null)
5481
6279
  return false;
5482
- return path.startsWith(resolve12(join14(harnessDir, "knowledge")) + sep3) && isMarkdownDoc(path);
6280
+ return path.startsWith(resolve15(join16(harnessDir, "knowledge")) + sep4) && isMarkdownDoc(path);
5483
6281
  }
5484
6282
  case "roles":
5485
6283
  return isRolesTarget(path);
@@ -5497,10 +6295,10 @@ function auditSecretsViolations(findings, file) {
5497
6295
  function validateDesignDoc(doc, path) {
5498
6296
  const violations = [...validateDesignTokenFrontmatter(doc).violations];
5499
6297
  const isDark = basename9(path) === "DESIGN.dark.md";
5500
- const sibling = join14(dirname8(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
5501
- if (existsSync13(sibling)) {
5502
- const light = isDark ? readFileSync10(sibling, "utf8") : doc;
5503
- const dark = isDark ? doc : readFileSync10(sibling, "utf8");
6298
+ const sibling = join16(dirname9(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
6299
+ if (existsSync15(sibling)) {
6300
+ const light = isDark ? readFileSync13(sibling, "utf8") : doc;
6301
+ const dark = isDark ? doc : readFileSync13(sibling, "utf8");
5504
6302
  violations.push(...assertLightDarkParity(light, dark).violations);
5505
6303
  }
5506
6304
  return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
@@ -5514,21 +6312,21 @@ function validateAuditDoc(doc, path) {
5514
6312
  function validateCompoundDoc(doc, _path, harnessDir) {
5515
6313
  const violations = [...validateSchemaYaml(doc).violations];
5516
6314
  if (harnessDir !== null) {
5517
- violations.push(...referenceExists(dirname8(harnessDir), doc).violations);
6315
+ violations.push(...referenceExists(dirname9(harnessDir), doc).violations);
5518
6316
  }
5519
6317
  return violations.length === 0 ? { ok: true, violations } : { ok: false, violations };
5520
6318
  }
5521
- function validateRolesState(rolesDir, skillsRoot = dirname8(rolesDir)) {
6319
+ function validateRolesState(rolesDir, skillsRoot = dirname9(rolesDir)) {
5522
6320
  const violations = [...validateRoleMapping(rolesDir).violations];
5523
6321
  const skillTexts = {};
5524
- for (const entry of readdirSync6(skillsRoot, { withFileTypes: true })) {
6322
+ for (const entry of readdirSync11(skillsRoot, { withFileTypes: true })) {
5525
6323
  if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
5526
6324
  continue;
5527
- const skillFile = join14(skillsRoot, entry.name, "SKILL.md");
5528
- if (!existsSync13(skillFile))
6325
+ const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
6326
+ if (!existsSync15(skillFile))
5529
6327
  continue;
5530
6328
  try {
5531
- skillTexts[entry.name] = readFileSync10(skillFile, "utf8");
6329
+ skillTexts[entry.name] = readFileSync13(skillFile, "utf8");
5532
6330
  } catch {}
5533
6331
  }
5534
6332
  violations.push(...lintLoadOrder(skillTexts).violations);
@@ -5558,12 +6356,12 @@ function gateSeamIntent(ctx, harnessDir, config, seam, target) {
5558
6356
  try {
5559
6357
  if (!isSeamTarget(seam, harnessDir, target))
5560
6358
  return;
5561
- const path = resolve12(target.displayPath);
5562
- if (!existsSync13(path))
6359
+ const path = resolve15(target.displayPath);
6360
+ if (!existsSync15(path))
5563
6361
  return;
5564
6362
  let doc;
5565
6363
  try {
5566
- doc = readFileSync10(path, "utf8");
6364
+ doc = readFileSync13(path, "utf8");
5567
6365
  } catch (error) {
5568
6366
  logger.error(`seam lint degraded to allow (cannot read ${path}): ${error.message}`);
5569
6367
  emitSeamAdvisory(ctx, seam, target.displayPath, { ok: true, violations: [] }, false, { degraded: true });
@@ -5629,8 +6427,8 @@ function lintRolesWrite(doc, options) {
5629
6427
  }
5630
6428
 
5631
6429
  // src/gates/tools.ts
5632
- import { existsSync as existsSync14, readFileSync as readFileSync11 } from "node:fs";
5633
- import { join as join15, resolve as resolve13 } from "node:path";
6430
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "node:fs";
6431
+ import { join as join17, resolve as resolve16 } from "node:path";
5634
6432
  import { defineTool } from "@deepseek-ai/dsh-tools";
5635
6433
  var ITERATION_VIOLATION_SCHEMA = {
5636
6434
  type: "object",
@@ -5727,12 +6525,12 @@ function registerSddIterationTools(ctx, resolver) {
5727
6525
  }));
5728
6526
  toolsCtx.tools.register(defineTool({
5729
6527
  name: "mstar_iteration_gate",
5730
- description: "Evaluate the iteration phase-transition gate against a status.json and a delivery-compass.md " + "(engine evaluatePhaseGate, mirror of `mstar iteration gate`): returns the transition " + "(phase-2-execute / phase-3-close / phase-4-pr-delivery), the pass/fail verdict, and the " + "§3.1 entry / §3.5 exit checklists with violation codes.",
6528
+ description: "Evaluate the iteration phase-transition gate against a workflow snapshot and a delivery-compass.md " + "(engine evaluatePhaseGate, mirror of `mstar iteration gate --workflow <id>`): returns the transition " + "(phase-2-execute / phase-3-close / phase-4-pr-delivery), the pass/fail verdict, and the " + "§3.1 entry / §3.5 exit checklists with violation codes.",
5731
6529
  parameters: {
5732
- status_path: {
6530
+ snapshot_path: {
5733
6531
  type: "string",
5734
6532
  required: true,
5735
- description: "Path to {HARNESS_DIR}/status.json."
6533
+ description: "Path to {HARNESS_DIR}/workflows/<id>/snapshot.json (the selected workflow snapshot — v3 input, mirrors the CLI `iteration gate --workflow <id>`)."
5736
6534
  },
5737
6535
  compass_path: {
5738
6536
  type: "string",
@@ -5795,13 +6593,13 @@ function registerSddIterationTools(ctx, resolver) {
5795
6593
  presentResult: (_args, _result) => ({ card: "generic", title: "Iteration gate evaluation" }),
5796
6594
  isConcurrencySafe: () => false,
5797
6595
  async execute(args) {
5798
- if (!existsSync14(args.status_path))
5799
- throw new Error(`status file not found: ${args.status_path}`);
5800
- if (!existsSync14(args.compass_path))
6596
+ if (!existsSync16(args.snapshot_path))
6597
+ throw new Error(`workflow snapshot not found: ${args.snapshot_path}`);
6598
+ if (!existsSync16(args.compass_path))
5801
6599
  throw new Error(`compass file not found: ${args.compass_path}`);
5802
- const statusDoc = readJson(args.status_path);
6600
+ const snapshotDoc = readJson(args.snapshot_path);
5803
6601
  const compassDoc = parseCompassFrontmatter(args.compass_path);
5804
- const result = evaluatePhaseGate(statusDoc, compassDoc, {
6602
+ const result = evaluatePhaseGate(snapshotDoc, compassDoc, {
5805
6603
  currentBranch: args.branch,
5806
6604
  specIntegrationBranch: args.integration,
5807
6605
  prBaseBranch: args.target
@@ -5850,11 +6648,11 @@ function registerSeamTools(ctx, resolver) {
5850
6648
  presentCall: (args) => ({ card: "generic", title: "Validate DESIGN.md", kind: "other", rawInput: args.dir }),
5851
6649
  isConcurrencySafe: () => false,
5852
6650
  async execute(args) {
5853
- const abs = resolve13(args.dir);
5854
- const lightPath = join15(abs, "DESIGN.md");
5855
- if (!existsSync14(lightPath))
6651
+ const abs = resolve16(args.dir);
6652
+ const lightPath = join17(abs, "DESIGN.md");
6653
+ if (!existsSync16(lightPath))
5856
6654
  throw new Error(`design file not found: ${lightPath}`);
5857
- const light = readFileSync11(lightPath, "utf8");
6655
+ const light = readFileSync14(lightPath, "utf8");
5858
6656
  const result = validateDesignDoc(light, lightPath);
5859
6657
  const level = completenessLevel(light);
5860
6658
  return {
@@ -5904,10 +6702,10 @@ function registerSeamTools(ctx, resolver) {
5904
6702
  presentCall: (args) => ({ card: "generic", title: "Validate audit plan", kind: "other", rawInput: args.plan_path }),
5905
6703
  isConcurrencySafe: () => false,
5906
6704
  async execute(args) {
5907
- const abs = resolve13(args.plan_path);
5908
- if (!existsSync14(abs))
6705
+ const abs = resolve16(args.plan_path);
6706
+ if (!existsSync16(abs))
5909
6707
  throw new Error(`plan file not found: ${abs}`);
5910
- const text = readFileSync11(abs, "utf8");
6708
+ const text = readFileSync14(abs, "utf8");
5911
6709
  const result = validateAuditDoc(text, abs);
5912
6710
  return { ok: result.ok, violations: result.violations.map(iterationViolationView), secrets: result.findings };
5913
6711
  }
@@ -5944,17 +6742,17 @@ function registerSeamTools(ctx, resolver) {
5944
6742
  presentCall: (args) => ({ card: "generic", title: "Validate knowledge doc", kind: "other", rawInput: args.doc_path }),
5945
6743
  isConcurrencySafe: () => false,
5946
6744
  async execute(args, exec) {
5947
- const abs = resolve13(args.doc_path);
5948
- if (!existsSync14(abs))
6745
+ const abs = resolve16(args.doc_path);
6746
+ if (!existsSync16(abs))
5949
6747
  throw new Error(`knowledge doc not found: ${abs}`);
5950
- const text = readFileSync11(abs, "utf8");
6748
+ const text = readFileSync14(abs, "utf8");
5951
6749
  const base = validateCompoundDoc(text, abs, args.repo_root !== undefined ? null : resolver.forAgent(exec.agent));
5952
6750
  const violations = [...base.violations];
5953
6751
  if (args.repo_root !== undefined) {
5954
- violations.push(...referenceExists(resolve13(args.repo_root), text).violations);
6752
+ violations.push(...referenceExists(resolve16(args.repo_root), text).violations);
5955
6753
  }
5956
6754
  if (args.knowledge_dir !== undefined) {
5957
- const knowledgeDir = resolve13(args.knowledge_dir);
6755
+ const knowledgeDir = resolve16(args.knowledge_dir);
5958
6756
  violations.push(...assertIndexRows(knowledgeDir).violations);
5959
6757
  violations.push(...scopeGuard(abs, [knowledgeDir]).violations);
5960
6758
  }
@@ -5989,10 +6787,10 @@ function registerSeamTools(ctx, resolver) {
5989
6787
  presentCall: (args) => ({ card: "generic", title: "Validate roles mapping", kind: "other", rawInput: args.roles_dir }),
5990
6788
  isConcurrencySafe: () => false,
5991
6789
  async execute(args) {
5992
- const rolesDir = resolve13(args.roles_dir);
5993
- if (!existsSync14(join15(rolesDir, "SKILL.md")))
6790
+ const rolesDir = resolve16(args.roles_dir);
6791
+ if (!existsSync16(join17(rolesDir, "SKILL.md")))
5994
6792
  throw new Error(`roles dir not found: ${rolesDir}`);
5995
- const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve13(args.skills_root) : undefined);
6793
+ const result = validateRolesState(rolesDir, args.skills_root !== undefined ? resolve16(args.skills_root) : undefined);
5996
6794
  return { ok: result.ok, violations: result.violations.map(iterationViolationView) };
5997
6795
  }
5998
6796
  }));
@@ -6000,9 +6798,15 @@ function registerSeamTools(ctx, resolver) {
6000
6798
  }
6001
6799
 
6002
6800
  // src/gates/adapter.ts
6003
- import { existsSync as existsSync15 } from "node:fs";
6801
+ import { existsSync as existsSync17 } from "node:fs";
6802
+ import { join as join18 } from "node:path";
6004
6803
  import { Service as Service2 } from "@deepseek-ai/cordis";
6005
6804
  var HOST_LOGGER = "mstar/host-adapter";
6805
+ function mergeLeasesMatch(left, right) {
6806
+ const a = left ?? {};
6807
+ const b = right ?? {};
6808
+ return ["holder", "claimed_at", "plan_id", "source_branch", "target_branch"].every((key) => a[key] === b[key]);
6809
+ }
6006
6810
 
6007
6811
  class DshHostAdapter extends Service2 {
6008
6812
  host = "dsh";
@@ -6030,10 +6834,10 @@ class DshHostAdapter extends Service2 {
6030
6834
  log(level, msg) {
6031
6835
  this.logSink(level, msg);
6032
6836
  }
6033
- statusGate(statusPath) {
6034
- if (!existsSync15(statusPath))
6837
+ statusGate(path, kind, harnessDir) {
6838
+ if (!existsSync17(path))
6035
6839
  return { ok: true, violations: [] };
6036
- return validateStatusDoc(statusPath);
6840
+ return validateStatusDoc(path, kind, harnessDir);
6037
6841
  }
6038
6842
  dispatchGate(prompt, exec, hard) {
6039
6843
  const harnessDir = this.resolver.forAgent(exec?.agent);
@@ -6061,7 +6865,12 @@ class DshHostAdapter extends Service2 {
6061
6865
  recordWorkflowVerdict(input);
6062
6866
  }
6063
6867
  async beforeStatusWrite(path, doc) {
6064
- const gate2 = doc !== undefined ? validateStatusValue(doc) : this.statusGate(path);
6868
+ const harnessDir = this.resolver.forWorkspace(undefined);
6869
+ const kind = harnessDir === null ? null : harnessDocKindOfTarget(harnessDir, path);
6870
+ if (kind === null) {
6871
+ return { ok: true, severity: "low", code: "host.beforeStatusWrite.ok", message: `status write to ${path} validated` };
6872
+ }
6873
+ const gate2 = doc !== undefined ? validateStatusValue(doc, kind, harnessDir) : this.statusGate(path, kind, harnessDir);
6065
6874
  if (!gate2.ok) {
6066
6875
  const first = gate2.violations[0];
6067
6876
  return { ok: false, severity: first.severity, code: first.code, message: first.message, fix: first.fix, aliases: first.aliases };
@@ -6076,13 +6885,39 @@ class DshHostAdapter extends Service2 {
6076
6885
  return applyEnforcement(gate2, { hard });
6077
6886
  }
6078
6887
  async beforeMerge(lease) {
6079
- return validateIntegrationMergeLease(lease);
6888
+ const violations = [...validateIntegrationMergeLease(lease).violations];
6889
+ const harnessDir = this.resolver.forWorkspace(undefined);
6890
+ if (harnessDir !== null) {
6891
+ const selection = resolveActiveWorkflow(harnessDir);
6892
+ if (selection.kind === "active") {
6893
+ const snapshotPath = join18(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
6894
+ try {
6895
+ const snapshot = readJson(snapshotPath);
6896
+ const stored = snapshot?.integration_merge_lease;
6897
+ if (stored !== undefined) {
6898
+ violations.push(...validateIntegrationMergeLease(stored).violations);
6899
+ if (!mergeLeasesMatch(lease, stored)) {
6900
+ violations.push({
6901
+ ok: false,
6902
+ severity: "high",
6903
+ code: "lease.merge.snapshot-mismatch",
6904
+ message: `snapshot integration_merge_lease (${snapshotPath}) differs from the lease being reserved — the active merge belongs to another holder/plan`,
6905
+ fix: "merge only under the snapshot-recorded integration_merge_lease (or release the lease with user authorization + audit note)"
6906
+ });
6907
+ }
6908
+ }
6909
+ } catch (error) {
6910
+ this.ctx.logger(HOST_LOGGER).warn(`beforeMerge snapshot lease read failed (contained — the passed-lease shape gate stands): ${error.message}`);
6911
+ }
6912
+ }
6913
+ }
6914
+ return { ok: violations.length === 0, violations };
6080
6915
  }
6081
6916
  }
6082
6917
 
6083
6918
  // src/gates/agent-personas.ts
6084
- import { readFileSync as readFileSync12, readdirSync as readdirSync7, statSync as statSync4 } from "node:fs";
6085
- import { join as join16, resolve as resolve14, sep as sep4 } from "node:path";
6919
+ import { readFileSync as readFileSync15, readdirSync as readdirSync12, statSync as statSync6 } from "node:fs";
6920
+ import { join as join19, resolve as resolve17, sep as sep6 } from "node:path";
6086
6921
  var ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
6087
6922
  var defaultCache = new Map;
6088
6923
  function personaFor(roleId, lookup, warn) {
@@ -6099,7 +6934,7 @@ function personaFor(roleId, lookup, warn) {
6099
6934
  function subagentRoleIds(agentsDir) {
6100
6935
  let names;
6101
6936
  try {
6102
- names = readdirSync7(agentsDir);
6937
+ names = readdirSync12(agentsDir);
6103
6938
  } catch {
6104
6939
  return [];
6105
6940
  }
@@ -6110,7 +6945,7 @@ function subagentRoleIds(agentsDir) {
6110
6945
  const roleId = name.slice(0, -3);
6111
6946
  let content;
6112
6947
  try {
6113
- content = readFileSync12(join16(agentsDir, name), "utf8");
6948
+ content = readFileSync15(join19(agentsDir, name), "utf8");
6114
6949
  } catch {
6115
6950
  continue;
6116
6951
  }
@@ -6123,13 +6958,13 @@ function subagentRoleIds(agentsDir) {
6123
6958
  function defaultFromMirror(agentsDir, roleId, warn) {
6124
6959
  if (!ROLE_ID_PATTERN.test(roleId))
6125
6960
  return;
6126
- const root = resolve14(agentsDir);
6127
- const file = resolve14(agentsDir, `${roleId}.md`);
6128
- if (!file.startsWith(root + sep4))
6961
+ const root = resolve17(agentsDir);
6962
+ const file = resolve17(agentsDir, `${roleId}.md`);
6963
+ if (!file.startsWith(root + sep6))
6129
6964
  return;
6130
6965
  let stat;
6131
6966
  try {
6132
- stat = statSync4(file);
6967
+ stat = statSync6(file);
6133
6968
  } catch {
6134
6969
  return;
6135
6970
  }
@@ -6140,7 +6975,7 @@ function defaultFromMirror(agentsDir, roleId, warn) {
6140
6975
  return hit.text;
6141
6976
  let content;
6142
6977
  try {
6143
- content = readFileSync12(file, "utf8");
6978
+ content = readFileSync15(file, "utf8");
6144
6979
  } catch {
6145
6980
  return;
6146
6981
  }
@@ -6328,8 +7163,8 @@ function log2(level, message) {
6328
7163
  }
6329
7164
 
6330
7165
  // src/gates/workflow-ledger.ts
6331
- import { readFileSync as readFileSync13, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
6332
- import { join as join17 } from "node:path";
7166
+ import { readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
7167
+ import { join as join20 } from "node:path";
6333
7168
  var WORKFLOW_LEDGER_LOGGER = "mstar/workflow-ledger";
6334
7169
  var TOOL_WORKFLOW_RUN_START = "tool-workflow/run-start";
6335
7170
  var TOOL_WORKFLOW_AGENT_START = "tool-workflow/agent-start";
@@ -6338,6 +7173,7 @@ var TOOL_WORKFLOW_RUN_END = "tool-workflow/run-end";
6338
7173
  var DEPTH_ADVISORY_THRESHOLD = 2;
6339
7174
  var WORKFLOW_LEDGER_WATERMARK_FILE = "workflow-ledger-cursors.json";
6340
7175
  var WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS = 256;
7176
+ var WORKFLOW_LEDGER_WATERMARK_MAX_DIRS = 64;
6341
7177
  var workflowLedgerLogSink = () => {};
6342
7178
  function setWorkflowLedgerLogger(sink) {
6343
7179
  const prior = workflowLedgerLogSink;
@@ -6357,13 +7193,22 @@ function sessionIdOf2(session) {
6357
7193
  return typeof id === "string" && id !== "" ? id : undefined;
6358
7194
  }
6359
7195
  var watermarkCache = new Map;
6360
- function loadWatermark(harnessDir) {
6361
- const cached = watermarkCache.get(harnessDir);
6362
- if (cached !== undefined)
7196
+ function cacheWatermark(workflowDir, watermark) {
7197
+ watermarkCache.set(workflowDir, watermark);
7198
+ while (watermarkCache.size > WORKFLOW_LEDGER_WATERMARK_MAX_DIRS) {
7199
+ const oldest = watermarkCache.keys().next().value;
7200
+ if (oldest === undefined)
7201
+ break;
7202
+ watermarkCache.delete(oldest);
7203
+ }
7204
+ }
7205
+ function loadWatermark(workflowDir, fresh = false) {
7206
+ const cached = watermarkCache.get(workflowDir);
7207
+ if (cached !== undefined && !fresh)
6363
7208
  return cached;
6364
7209
  const watermark = new Map;
6365
7210
  try {
6366
- const raw = readFileSync13(join17(harnessDir, WORKFLOW_LEDGER_WATERMARK_FILE), "utf8");
7211
+ const raw = readFileSync16(join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE), "utf8");
6367
7212
  const record = asRecord(JSON.parse(raw));
6368
7213
  const cursors = asRecord(record?.cursors);
6369
7214
  if (cursors !== undefined) {
@@ -6376,40 +7221,50 @@ function loadWatermark(harnessDir) {
6376
7221
  } catch (error) {
6377
7222
  const err = error;
6378
7223
  if (err?.code !== "ENOENT") {
6379
- log3("warn", `workflow-ledger watermark unreadable for ${harnessDir} — degrading to in-memory-only (restart re-records): ${errorMessage3(error)}`);
7224
+ log3("warn", `workflow-ledger watermark unreadable for ${workflowDir} — degrading to in-memory-only (restart re-records): ${errorMessage3(error)}`);
6380
7225
  }
7226
+ if (fresh && cached !== undefined)
7227
+ return cached;
6381
7228
  }
6382
- watermarkCache.set(harnessDir, watermark);
7229
+ cacheWatermark(workflowDir, watermark);
6383
7230
  return watermark;
6384
7231
  }
6385
- function saveWatermark(harnessDir, watermark) {
7232
+ function saveWatermark(workflowDir, watermark) {
6386
7233
  try {
6387
- const file = join17(harnessDir, WORKFLOW_LEDGER_WATERMARK_FILE);
7234
+ const file = join20(workflowDir, WORKFLOW_LEDGER_WATERMARK_FILE);
6388
7235
  const tmp = `${file}.tmp`;
6389
7236
  writeFileSync4(tmp, JSON.stringify({ v: 1, cursors: Object.fromEntries(watermark) }));
6390
7237
  renameSync3(tmp, file);
6391
7238
  } catch (error) {
6392
- log3("warn", `workflow-ledger watermark write failed for ${harnessDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
7239
+ log3("warn", `workflow-ledger watermark write failed for ${workflowDir} — in-memory only (restart re-records): ${errorMessage3(error)}`);
6393
7240
  }
6394
7241
  }
6395
- function advanceWatermark(harnessDir, sid, nextSeq, isEvictable) {
6396
- const watermark = loadWatermark(harnessDir);
6397
- if (!watermark.has(sid) && watermark.size >= WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS) {
6398
- let victim;
6399
- for (const key of watermark.keys()) {
6400
- if (isEvictable(key)) {
6401
- victim = key;
6402
- break;
7242
+ function advanceWatermark(workflowDir, sid, nextSeq, isEvictable, lockOpts = {}) {
7243
+ try {
7244
+ withWorkflowDirLock(workflowDir, () => {
7245
+ const watermark = loadWatermark(workflowDir, true);
7246
+ if (!watermark.has(sid) && watermark.size >= WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS) {
7247
+ let victim;
7248
+ for (const key of watermark.keys()) {
7249
+ if (isEvictable(key)) {
7250
+ victim = key;
7251
+ break;
7252
+ }
7253
+ }
7254
+ victim ??= watermark.keys().next().value;
7255
+ if (victim !== undefined) {
7256
+ watermark.delete(victim);
7257
+ log3("warn", `workflow-ledger watermark capped at ${WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS} sessions — evicted ${victim} (a restored evicted session re-records; bounded by the cap)`);
7258
+ }
6403
7259
  }
6404
- }
6405
- victim ??= watermark.keys().next().value;
6406
- if (victim !== undefined) {
6407
- watermark.delete(victim);
6408
- log3("warn", `workflow-ledger watermark capped at ${WORKFLOW_LEDGER_WATERMARK_MAX_SESSIONS} sessions — evicted ${victim} (a restored evicted session re-records; bounded by the cap)`);
6409
- }
7260
+ watermark.set(sid, Math.max(watermark.get(sid) ?? 0, nextSeq));
7261
+ saveWatermark(workflowDir, watermark);
7262
+ }, lockOpts);
7263
+ } catch (error) {
7264
+ const watermark = loadWatermark(workflowDir);
7265
+ watermark.set(sid, Math.max(watermark.get(sid) ?? 0, nextSeq));
7266
+ log3("warn", `workflow-ledger watermark durable advance failed for ${workflowDir} — in-memory cursor advanced, durable cursor NOT (restart reloads the file; outage rows re-record once — bounded duplicate): ${errorMessage3(error)}`);
6410
7267
  }
6411
- watermark.set(sid, nextSeq);
6412
- saveWatermark(harnessDir, watermark);
6413
7268
  }
6414
7269
  function rowOf(session, envelope) {
6415
7270
  const record = asRecord(envelope);
@@ -6517,15 +7372,18 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
6517
7372
  const harnessDir = resolver.forWorkspace(typeof cwd === "string" && cwd.trim() !== "" ? cwd : undefined);
6518
7373
  if (harnessDir === null)
6519
7374
  return;
6520
- const watermark = loadWatermark(harnessDir);
7375
+ const workflowDir = resolveAgentFlowWriteDir(harnessDir);
7376
+ if (workflowDir === null)
7377
+ return;
7378
+ const watermark = loadWatermark(workflowDir);
6521
7379
  let next = watermark.get(sid) ?? 0;
6522
7380
  const events = session.events;
6523
7381
  if (Array.isArray(events) && events.length < next)
6524
7382
  next = 0;
6525
7383
  if (row.seq < next)
6526
7384
  return;
6527
- if (recordWorkflowEvent({ harnessDir, event: row.event })) {
6528
- advanceWatermark(harnessDir, sid, row.seq + 1, (candidate) => sessions.get(candidate) === undefined);
7385
+ if (recordWorkflowEvent({ harnessDir, workflowDir, event: row.event })) {
7386
+ advanceWatermark(workflowDir, sid, row.seq + 1, (candidate) => sessions.get(candidate) === undefined);
6529
7387
  if (workflowAskCache !== undefined && row.event.kind === "workflow-run") {
6530
7388
  try {
6531
7389
  const runName = row.runName ?? row.event.name;
@@ -6589,8 +7447,8 @@ function registerWorkflowLedger(ctx, resolver, workflowAskCache) {
6589
7447
  }
6590
7448
 
6591
7449
  // src/gates/goal-bridge.ts
6592
- import { existsSync as existsSync16, readFileSync as readFileSync14, readdirSync as readdirSync8 } from "node:fs";
6593
- import { join as join18 } from "node:path";
7450
+ import { existsSync as existsSync18, readFileSync as readFileSync17, readdirSync as readdirSync13 } from "node:fs";
7451
+ import { join as join21 } from "node:path";
6594
7452
  var GOAL_BRIDGE_LOGGER = "mstar/goal-bridge";
6595
7453
  var DEFAULT_MAX_GOAL_ROUNDS = 256;
6596
7454
  var FLOW_SEQUENCE = "iteration-start → per-plan cycles → iteration-close → PR delivery → merge-ready";
@@ -6611,6 +7469,19 @@ function log4(level, message) {
6611
7469
  function errorMessage4(error) {
6612
7470
  return error instanceof Error ? error.message : String(error);
6613
7471
  }
7472
+ function projectRegisterPointer(harnessDir) {
7473
+ const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
7474
+ try {
7475
+ for (const entry of readdirSync13(projectsDir, { withFileTypes: true })) {
7476
+ if (!entry.isDirectory())
7477
+ continue;
7478
+ const registerPath = join21(projectsDir, entry.name, PROJECT_REGISTER_FILE);
7479
+ if (existsSync18(registerPath))
7480
+ return registerPath;
7481
+ }
7482
+ } catch {}
7483
+ return join21(projectsDir, _DEFAULT_PROJECT, PROJECT_REGISTER_FILE);
7484
+ }
6614
7485
  function isRootLikeAgent(agent) {
6615
7486
  const header = agent?.session?.header;
6616
7487
  return header?.parentSession === undefined;
@@ -6620,23 +7491,23 @@ function iterationGoalObjective(iterationId) {
6620
7491
  }
6621
7492
  function steeringCompass(harnessDir) {
6622
7493
  const iterationsDir = resolveIterationDir(harnessDir);
6623
- if (!existsSync16(iterationsDir))
7494
+ if (!existsSync18(iterationsDir))
6624
7495
  return;
6625
7496
  let entries;
6626
7497
  try {
6627
- entries = readdirSync8(iterationsDir, { withFileTypes: true });
7498
+ entries = readdirSync13(iterationsDir, { withFileTypes: true });
6628
7499
  } catch {
6629
7500
  return;
6630
7501
  }
6631
7502
  for (const entry of entries) {
6632
7503
  if (!entry.isDirectory())
6633
7504
  continue;
6634
- const compassPath = join18(iterationsDir, entry.name, "delivery-compass.md");
6635
- if (!existsSync16(compassPath))
7505
+ const compassPath = join21(iterationsDir, entry.name, "delivery-compass.md");
7506
+ if (!existsSync18(compassPath))
6636
7507
  continue;
6637
7508
  let content;
6638
7509
  try {
6639
- content = readFileSync14(compassPath, "utf8");
7510
+ content = readFileSync17(compassPath, "utf8");
6640
7511
  } catch {
6641
7512
  continue;
6642
7513
  }
@@ -6745,7 +7616,7 @@ function warnBlockedGoal(harnessDir, advisory) {
6745
7616
  const code = truncateLedgerField(advisory.code, GOAL_ADVISORY_CODE_CAP);
6746
7617
  const reason = truncateLedgerField(normalizeWorkflowName(advisory.message), GOAL_ADVISORY_MESSAGE_CAP);
6747
7618
  const objective = truncateLedgerField(normalizeWorkflowName(advisory.objective), GOAL_ADVISORY_OBJECTIVE_CAP);
6748
- log4("warn", `goal blocked [${code}] — ${reason}; objective: ${objective}; residuals: see ${harnessDir}/${STATUS_FILE} (residual_findings) — advisory only, zero harness writes`);
7619
+ log4("warn", `goal blocked [${code}] — ${reason}; objective: ${objective}; residuals: see ${projectRegisterPointer(harnessDir)} — advisory only, zero harness writes`);
6749
7620
  }
6750
7621
  function rootAgentOf(agent, agents) {
6751
7622
  let current = agent;
@@ -6812,8 +7683,8 @@ function registerGoalBridge(ctx, resolver, config) {
6812
7683
  }
6813
7684
 
6814
7685
  // src/gates/plan-mode-bridge.ts
6815
- import { existsSync as existsSync17 } from "node:fs";
6816
- import { join as join19 } from "node:path";
7686
+ import { existsSync as existsSync19 } from "node:fs";
7687
+ import { join as join22 } from "node:path";
6817
7688
  var PLAN_MODE_BRIDGE_LOGGER = "mstar/plan-mode-bridge";
6818
7689
  var PLAN_STATUS_TODO = "Todo";
6819
7690
  var planModeBridgeLogSink = () => {};
@@ -6829,12 +7700,16 @@ function errorMessage5(error) {
6829
7700
  return error instanceof Error ? error.message : String(error);
6830
7701
  }
6831
7702
  function hasPrepareWindow(harnessDir) {
6832
- const statusPath = join19(harnessDir, STATUS_FILE);
6833
- if (!existsSync17(statusPath))
7703
+ const statusPath = join22(harnessDir, STATUS_FILE);
7704
+ if (!existsSync19(statusPath))
7705
+ return false;
7706
+ const selection = resolveReadWorkflow(harnessDir);
7707
+ if (selection.kind === "error")
6834
7708
  return false;
7709
+ const snapshotPath = join22(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
6835
7710
  let doc;
6836
7711
  try {
6837
- doc = readJson(statusPath);
7712
+ doc = readJson(snapshotPath);
6838
7713
  } catch {
6839
7714
  return false;
6840
7715
  }
@@ -7223,7 +8098,7 @@ function registerHarnessPrompt(ctx, options) {
7223
8098
  return true;
7224
8099
  }
7225
8100
  function sectionEnforcement(harnessDir) {
7226
- return harnessDir === null ? { hard: false, source: "none" } : resolveCompassEnforcement(harnessDir);
8101
+ return harnessDir === null ? { hard: false, source: "none" } : resolveRepoEnforcement(harnessDir);
7227
8102
  }
7228
8103
  function harnessRulesText(harnessDir, enforcement) {
7229
8104
  return [
@@ -7247,6 +8122,14 @@ function engineStatusProvider(ctx, resolver, bootHarnessDir) {
7247
8122
  return engineStatusSummary(entry.source);
7248
8123
  };
7249
8124
  }
8125
+ var DIGEST_PLAN_CAP = 8;
8126
+ var DIGEST_LEASE_CAP = 4;
8127
+ function joinCapped(items, cap, separator, render) {
8128
+ const visible = items.slice(0, cap).map(render);
8129
+ if (items.length > cap)
8130
+ visible.push(`+${items.length - cap} more`);
8131
+ return visible.join(separator);
8132
+ }
7250
8133
  function engineStatusSummary(source) {
7251
8134
  const lines = [
7252
8135
  `mstar engine status: v${source.version} | harness ${stripInterpolationHazard(source.harnessDir ?? "none")} | enforcement ${source.enforcement.hard ? "hard" : "soft"}`
@@ -7259,9 +8142,9 @@ function engineStatusSummary(source) {
7259
8142
  }
7260
8143
  const state = source.state;
7261
8144
  if (state !== null) {
7262
- const plans = state.plans.length === 0 ? "none" : state.plans.map((p) => `${stripInterpolationHazard(p.id)}(${stripInterpolationHazard(p.status)})`).join(" ");
8145
+ const plans = state.plans.length === 0 ? "none" : joinCapped(state.plans, DIGEST_PLAN_CAP, " ", (p) => `${stripInterpolationHazard(p.id)}(${stripInterpolationHazard(p.status)})`);
7263
8146
  const residuals = state.residuals.length === 0 ? "none" : state.residuals.map((r) => `${r.severity} ${r.count}`).join(", ");
7264
- const leases = state.leases.length === 0 ? "none active" : state.leases.map((l) => `${stripInterpolationHazard(l.planId)} → ${stripInterpolationHazard(l.holder)}`).join("; ");
8147
+ const leases = state.leases.length === 0 ? "none active" : joinCapped(state.leases, DIGEST_LEASE_CAP, "; ", (l) => `${stripInterpolationHazard(l.planId)} → ${stripInterpolationHazard(l.holder)}`);
7265
8148
  lines.push(`plans: ${plans} | residuals: ${residuals} | leases: ${leases}`);
7266
8149
  if (state.direction !== null)
7267
8150
  lines.push(`direction: ${stripInterpolationHazard(state.direction)}`);
@@ -7284,7 +8167,7 @@ var inject = ["loader"];
7284
8167
  function packagedCommandsDir() {
7285
8168
  try {
7286
8169
  const dir = fileURLToPath3(new URL("../harness-commands", import.meta.url));
7287
- return existsSync18(dir) ? dir : undefined;
8170
+ return existsSync20(dir) ? dir : undefined;
7288
8171
  } catch {
7289
8172
  return;
7290
8173
  }
@@ -7329,10 +8212,10 @@ function registerMstarCommands(ctx) {
7329
8212
  if (dir === undefined)
7330
8213
  return;
7331
8214
  ctx.inject(["commands"], (commandsCtx) => {
7332
- for (const file of readdirSync9(dir).sort()) {
8215
+ for (const file of readdirSync14(dir).sort()) {
7333
8216
  if (!file.endsWith(".md"))
7334
8217
  continue;
7335
- const parsed = parseCommandMarkdown(readFileSync15(join20(dir, file), "utf8"));
8218
+ const parsed = parseCommandMarkdown(readFileSync18(join23(dir, file), "utf8"));
7336
8219
  if (parsed === undefined)
7337
8220
  continue;
7338
8221
  commandsCtx.commands.register({
@@ -7482,7 +8365,7 @@ function apply(ctx, config) {
7482
8365
  if (skillConfig !== undefined) {
7483
8366
  ctx.plugin({ name: skillLocalName, inject: skillLocalInject, Config: SkillLocalSchema, apply: applySkillLocal }, skillConfig);
7484
8367
  }
7485
- const effectiveHard = config.enforcement === "hard" || bootHarnessDir !== null && resolveCompassEnforcement(bootHarnessDir).hard;
8368
+ const effectiveHard = config.enforcement === "hard" || bootHarnessDir !== null && resolveRepoEnforcement(bootHarnessDir).hard;
7486
8369
  if (effectiveHard && (config.dispatchBinding ?? "").trim() === "") {
7487
8370
  ctx.logger(DISPATCH_LOGGER).warn("Enforcement: hard is active but dispatchBinding is unset — the anti-recursion precheck is skipped (an Assignment whose Execute as equals the dispatching agent cannot be detected)");
7488
8371
  }