@mstar-harness/engine 3.1.0 → 3.1.2

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.
@@ -173,8 +173,10 @@ export declare function assertTriIdentity(reviewerRoles: readonly string[]): Gat
173
173
  export type ComposeDispatchGateOptions = {
174
174
  /**
175
175
  * Host role-binding field (omp task entry `agent` / opencode `subagent` /
176
- * cursor `subagent_type`); the anti-recursion precheck runs only when
177
- * non-empty.
176
+ * cursor `subagent_type` / dsh `dispatchBinding`). The anti-recursion
177
+ * precheck runs for EVERY Assignment-shaped text: an empty or omitted
178
+ * binding fails closed (`dispatch.anti-recursion.empty-binding`) — the
179
+ * host cannot prove the dispatching agent is not recursing.
178
180
  */
179
181
  agent?: string;
180
182
  /**
@@ -204,7 +206,10 @@ export type ComposeDispatchGateResult = GateResult & {
204
206
  * Assignment-shaped passes silently (`shaped: false`).
205
207
  * 2. `validateAssignmentFields` with `writable: false` when `opts.writable
206
208
  * === false` (read-only roles), else the writable default.
207
- * 3. Anti-recursion precheck when `opts.agent` is non-empty.
209
+ * 3. Anti-recursion precheck runs for every Assignment-shaped text; an
210
+ * empty/omitted host binding fails closed (`dispatch.anti-recursion.
211
+ * empty-binding`), a binding equal to `Execute as` is the existing
212
+ * `dispatch.anti-recursion.self-type` critical.
208
213
  * 4. Default-branch gate for writable text: the branch comes from the
209
214
  * Assignment's own branch forms (create-form name / Working branch /
210
215
  * Branch policy branch), else `$MSTAR_WORKING_BRANCH`; a well-formed
@@ -221,7 +226,11 @@ export declare function composeDispatchGate(text: string, opts?: ComposeDispatch
221
226
  * Anti-recursion precheck (NEVER red line, mstar-dispatch-gates § 承接方反递归
222
227
  * 红线): a leaf executor MUST NOT invoke a Task/subagent whose role-binding
223
228
  * field (`subagent_type` / `agent` / `subagent`) equals its own `Execute as`.
224
- * Comparison is case-insensitive after trim; an empty binding is not a
225
- * self-recursion (its presence is the field gate's job, not this precheck).
229
+ * Comparison is case-insensitive after trim. An EMPTY binding fails closed
230
+ * (`dispatch.anti-recursion.empty-binding`, critical): the host cannot
231
+ * report which agent is calling, so anti-recursion cannot be proven — the
232
+ * dispatch must not proceed as if the NEVER red line held. An empty
233
+ * `executeAs` with a set binding stays ok (field presence is
234
+ * `validateAssignmentFields`' job, not this precheck).
226
235
  */
227
236
  export declare function antiRecursionPrecheck(subagentType: string, executeAs: string): GateResult;
package/dist/engine.js CHANGED
@@ -170,7 +170,7 @@ function isAtOrBelow(dir, root) {
170
170
  return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
171
171
  }
172
172
  // src/path.ts
173
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
173
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
174
174
  import { execFileSync } from "node:child_process";
175
175
  import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
176
176
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
@@ -425,6 +425,23 @@ function assertPlanWritingPath(planPath, harnessDir) {
425
425
  fix: `write the plan under ${planDir}`
426
426
  };
427
427
  }
428
+ if (existsSync2(planAbs)) {
429
+ try {
430
+ const canonicalPlan = realpathSync(planAbs);
431
+ const canonicalPlanDir = existsSync2(planDir) ? realpathSync(planDir) : resolve3(planDir);
432
+ const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
433
+ const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute2(canonicalRel);
434
+ if (!canonicalInside) {
435
+ return {
436
+ ok: false,
437
+ severity: "high",
438
+ code: "plan-path.symlink-escape",
439
+ message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
440
+ fix: `write the plan under ${planDir}`
441
+ };
442
+ }
443
+ } catch {}
444
+ }
428
445
  return {
429
446
  ok: true,
430
447
  severity: "low",
@@ -455,7 +472,7 @@ function hasFiles(dir) {
455
472
  }
456
473
  }
457
474
  // src/status.ts
458
- import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
475
+ import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
459
476
  import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
460
477
 
461
478
  // src/lease.ts
@@ -910,10 +927,8 @@ function composeDispatchGate(text, opts = {}) {
910
927
  const violations = [];
911
928
  const writable = opts.writable !== false;
912
929
  violations.push(...validateAssignmentFields(text, { writable }).violations);
913
- const agent = (opts.agent ?? "").trim();
914
- if (agent !== "") {
915
- violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
916
- }
930
+ const agent = opts.agent ?? "";
931
+ violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
917
932
  if (writable) {
918
933
  const forms = parseAssignmentBranchForms(text);
919
934
  const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
@@ -932,7 +947,15 @@ function composeDispatchGate(text, opts = {}) {
932
947
  function antiRecursionPrecheck(subagentType, executeAs) {
933
948
  const binding = subagentType.trim().toLowerCase();
934
949
  const role = executeAs.trim().toLowerCase();
935
- if (binding !== "" && binding === role) {
950
+ if (binding === "") {
951
+ return {
952
+ ok: false,
953
+ violations: [
954
+ violation2("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
955
+ ]
956
+ };
957
+ }
958
+ if (binding === role) {
936
959
  return {
937
960
  ok: false,
938
961
  violations: [
@@ -1294,7 +1317,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1294
1317
  if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
1295
1318
  let realHarnessDir = null;
1296
1319
  try {
1297
- realHarnessDir = realpathSync(harnessDir);
1320
+ realHarnessDir = realpathSync2(harnessDir);
1298
1321
  } catch {}
1299
1322
  for (const entry of doc.workflows) {
1300
1323
  if (!isPlainObject3(entry) || typeof entry.dir !== "string")
@@ -1304,7 +1327,7 @@ function validateStatusV2(docOrPath, opts = {}) {
1304
1327
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
1305
1328
  let physical;
1306
1329
  try {
1307
- physical = realpathSync(snapshotPath);
1330
+ physical = realpathSync2(snapshotPath);
1308
1331
  } catch {
1309
1332
  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`));
1310
1333
  continue;
@@ -1394,7 +1417,7 @@ async function unregisterWorkflow(root, id) {
1394
1417
  }
1395
1418
  function resolveCompassEnforcement(harnessDir) {
1396
1419
  const iterationsDir = resolveIterationDir(harnessDir);
1397
- if (!existsSync2(iterationsDir))
1420
+ if (!existsSync3(iterationsDir))
1398
1421
  return { hard: false, source: "none" };
1399
1422
  let entries;
1400
1423
  try {
@@ -1406,7 +1429,7 @@ function resolveCompassEnforcement(harnessDir) {
1406
1429
  if (!entry.isDirectory())
1407
1430
  continue;
1408
1431
  const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
1409
- if (!existsSync2(compassPath))
1432
+ if (!existsSync3(compassPath))
1410
1433
  continue;
1411
1434
  let content;
1412
1435
  try {
@@ -1442,7 +1465,7 @@ function resolveRepoEnforcement(harnessDir) {
1442
1465
  }
1443
1466
  // src/worktree.ts
1444
1467
  import { execFileSync as execFileSync2 } from "node:child_process";
1445
- import { existsSync as existsSync3 } from "node:fs";
1468
+ import { existsSync as existsSync4 } from "node:fs";
1446
1469
  import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
1447
1470
  var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
1448
1471
  function probeTimeoutMs() {
@@ -1497,7 +1520,7 @@ function l1PreDispatchCheck(input, opts = {}) {
1497
1520
  if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
1498
1521
  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"));
1499
1522
  }
1500
- if (leaseWorktreePath !== "" && !existsSync3(leaseWorktreePath)) {
1523
+ if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
1501
1524
  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>`));
1502
1525
  } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
1503
1526
  const probe = probeBranch(leaseWorktreePath, opts);
@@ -1531,7 +1554,7 @@ function l2PreDispatchCheck(input, opts = {}) {
1531
1554
  return;
1532
1555
  }
1533
1556
  seenPaths.add(normalized);
1534
- if (!existsSync3(track.worktreePath)) {
1557
+ if (!existsSync4(track.worktreePath)) {
1535
1558
  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}`));
1536
1559
  return;
1537
1560
  }
@@ -1594,7 +1617,7 @@ function singleReviewSnapshot(assignments) {
1594
1617
  }
1595
1618
  // src/sdd.ts
1596
1619
  import { execFileSync as execFileSync3 } from "node:child_process";
1597
- import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
1620
+ import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
1598
1621
  import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
1599
1622
  class SddScriptError extends Error {
1600
1623
  exitCode;
@@ -1671,8 +1694,8 @@ function isLinkedWorktree(root) {
1671
1694
  if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
1672
1695
  return true;
1673
1696
  try {
1674
- const gdParent = realpathSync2(dirname6(gitDir));
1675
- const cmAbs = realpathSync2(common);
1697
+ const gdParent = realpathSync3(dirname6(gitDir));
1698
+ const cmAbs = realpathSync3(common);
1676
1699
  return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
1677
1700
  } catch {
1678
1701
  return false;
@@ -1690,10 +1713,10 @@ function sddWorkspace(planId, opts = {}) {
1690
1713
  if (!isDirectory2(controlRoot)) {
1691
1714
  throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
1692
1715
  }
1693
- root = realpathSync2(controlRoot);
1716
+ root = realpathSync3(controlRoot);
1694
1717
  } else {
1695
1718
  const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
1696
- root = realpathSync2(topLevel ?? cwd);
1719
+ root = realpathSync3(topLevel ?? cwd);
1697
1720
  }
1698
1721
  if (!controlRoot && isLinkedWorktree(root)) {
1699
1722
  throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
@@ -1727,7 +1750,7 @@ function sddWorkspace(planId, opts = {}) {
1727
1750
  mkdirSync5(sddDir, { recursive: true });
1728
1751
  writeFileSync3(join7(sddDir, ".gitignore"), `*
1729
1752
  `);
1730
- return realpathSync2(sddDir);
1753
+ return realpathSync3(sddDir);
1731
1754
  }
1732
1755
  function taskBrief(planFile, taskN, outFile, opts = {}) {
1733
1756
  if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
@@ -1879,7 +1902,7 @@ function implementerSessionStickyRules(input) {
1879
1902
  return { resume: true, reason: `sticky resume OK: host_agent_id ${session.host_agent_id}, next task ${nextTask}` };
1880
1903
  }
1881
1904
  // src/iteration.ts
1882
- import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
1905
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
1883
1906
  import { join as join8 } from "node:path";
1884
1907
  var COMPASS_STATUSES = ["active", "locked", "completed"];
1885
1908
  var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
@@ -2108,7 +2131,7 @@ function pushCadenceProbe(ciRunning, reviewWaveActive) {
2108
2131
  return { ok: violations.length === 0, violations };
2109
2132
  }
2110
2133
  function assertIndexRowObligations(iterationsDir) {
2111
- if (!existsSync4(iterationsDir)) {
2134
+ if (!existsSync5(iterationsDir)) {
2112
2135
  return {
2113
2136
  ok: false,
2114
2137
  violations: [
@@ -2116,9 +2139,9 @@ function assertIndexRowObligations(iterationsDir) {
2116
2139
  ]
2117
2140
  };
2118
2141
  }
2119
- const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync4(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2142
+ const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync5(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
2120
2143
  const readmePath = join8(iterationsDir, INDEX_README);
2121
- if (!existsSync4(readmePath)) {
2144
+ if (!existsSync5(readmePath)) {
2122
2145
  return {
2123
2146
  ok: false,
2124
2147
  violations: [
@@ -2209,7 +2232,7 @@ function parseFlowArray(raw, filePath) {
2209
2232
  return items;
2210
2233
  }
2211
2234
  // src/project.ts
2212
- import { existsSync as existsSync5, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
2235
+ import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
2213
2236
  import { join as join9 } from "node:path";
2214
2237
  var PROJECT_ROADMAP_FILE = "roadmap.md";
2215
2238
  var PROJECT_REFERENCES_DIR = "references";
@@ -2393,7 +2416,7 @@ function techDebtRollup(projectDir) {
2393
2416
  if (!project.isDirectory())
2394
2417
  continue;
2395
2418
  const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
2396
- if (!existsSync5(registerPath))
2419
+ if (!existsSync6(registerPath))
2397
2420
  continue;
2398
2421
  let register;
2399
2422
  try {
@@ -3674,7 +3697,7 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
3674
3697
  return { outDir: resolve9(outDir), date, files: written, nextNumber: next };
3675
3698
  }
3676
3699
  // src/compound.ts
3677
- import { existsSync as existsSync6, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
3700
+ import { existsSync as existsSync7, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
3678
3701
  import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
3679
3702
  function violation10(severity, code, message, fix) {
3680
3703
  return { ok: false, severity, code, message, fix };
@@ -3982,7 +4005,7 @@ function referenceExists(repoRoot, docText) {
3982
4005
  for (const { ref, isSymbol, module } of refs) {
3983
4006
  if (!isSymbol || module === undefined) {
3984
4007
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
3985
- if (existsSync6(resolve10(repoRoot, candidate))) {
4008
+ if (existsSync7(resolve10(repoRoot, candidate))) {
3986
4009
  checked++;
3987
4010
  } else {
3988
4011
  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"));
@@ -4030,7 +4053,7 @@ function normalizeIndexRef(cell) {
4030
4053
  function assertIndexRows(knowledgeDir) {
4031
4054
  const violations = [];
4032
4055
  const readmePath = join12(knowledgeDir, "README.md");
4033
- if (!existsSync6(readmePath)) {
4056
+ if (!existsSync7(readmePath)) {
4034
4057
  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"));
4035
4058
  return { ok: false, violations };
4036
4059
  }
@@ -4315,7 +4338,7 @@ function lintStrategySections(docText) {
4315
4338
  return { ok: violations.length === 0, violations };
4316
4339
  }
4317
4340
  // src/roles.ts
4318
- import { existsSync as existsSync7 } from "node:fs";
4341
+ import { existsSync as existsSync8 } from "node:fs";
4319
4342
  import { join as join13 } from "node:path";
4320
4343
  function violation12(severity, code, message, fix) {
4321
4344
  return { ok: false, severity, code, message, fix };
@@ -4372,7 +4395,7 @@ function validateRoleMapping(rolesDir, options = {}) {
4372
4395
  const violations = [];
4373
4396
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
4374
4397
  for (const { agentId, reference } of mapping) {
4375
- if (!existsSync7(join13(rolesDir, reference))) {
4398
+ if (!existsSync8(join13(rolesDir, reference))) {
4376
4399
  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`));
4377
4400
  }
4378
4401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/engine",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
5
5
  "license": "MIT",
6
6
  "repository": {