@mstar-harness/dsh 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,7 +50,7 @@ The **two-command install is the contract** — folding a `dsh-llm-fallbacks` ro
50
50
  | `harnessDir` | `string` | repo `.mstarc` `[config] harness_dir`, else per-session workspace probe (`.mstar/` → `.agents/` → `.plans/` → `plans/`, from the session workspace root — **never the launch cwd**) | Explicit harness root; wins over engine probing. **Required for repos whose harness root is not a probed name**; the probe starts from the session workspace root (never the launch cwd) and STOPS there — it never walks above the session workspace, so a harness dir above it (e.g. a global `~/.mstar`) is never adopted. |
51
51
  | `enforcement` | `'hard' \| 'soft'` | compass, else warn-only | Per-deployment override. Precedence: Config wins; else the Assignment's own `**Enforcement**: hard` header flag (dispatch gate only); else the repo `.mstarc` `[config] enforcement`; else the iteration compass frontmatter; else warn-only. Config / `.mstarc` `soft` are the ONLY local rollbacks — an Assignment-level `soft` does NOT override a hard compass. |
52
52
  | `dispatchTools` | `string[]` | `['subagent', 'subagent_fork']` | Delegation tool names the dispatch gate matches — the dsh preset's TWO delegation tools, `subagent` and its fork sibling `subagent_fork` (both carry Assignment-shaped `{ description, prompt }` args; a `toolName` config may rename instances). |
53
- | `dispatchBinding` | `string` | unset (precheck skipped) | The dispatching agent's own harness role; an Assignment whose `Execute as` equals it is self-recursion. |
53
+ | `dispatchBinding` | `string` | unset fail-closed `empty-binding` under hard | The dispatching agent's own harness role (the anti-recursion CALLER); an Assignment whose `Execute as` equals it is self-recursion. |
54
54
  | `roleMap` | `Record<string, string>` | unset | mstar role id (`Execute as`) → dsh-llm-fallbacks role id. A taxonomy bridge for logging + future rule-driven interop ONLY — never consulted by the decoration (see LLM fallbacks integration). |
55
55
  | `rolePersonas` | `Record<string, string>` | unset (bundled mirror default) | mstar role id (`Execute as`) → persona text; the role-based subagent decoration's **override** source — a role-matched `subagent/start` registers the persona as the child's `mstar:role-persona` system-prompt section; when unset for a role, the bundled `harness-agents/` mirror default is used (see LLM fallbacks integration). |
56
56
  | `skillRoots` | `string[]` | unset (no custom-root registration) | Additional skill roots registered with the dsh skill-filesystem provider (`customSkillDirs` semantics — scanned before user roots). Dev-time: the mirror `<repo-root>/skills` absolute path. |
package/README.zh.md CHANGED
@@ -50,7 +50,7 @@ dsh plugin --profile web add dsh-llm-fallbacks
50
50
  | `harnessDir` | `string` | 仓库 `.mstarc` `[config] harness_dir`,否则按会话工作区探测(`.mstar/` → `.agents/` → `.plans/` → `plans/`,从会话工作区根目录开始——**绝不从启动 cwd**) | 显式 harness 根目录;优先于 engine 探测。**harness 根不在探测名列表中的仓库必须配置**;探测从会话工作区根开始(绝不从启动 cwd)并在那里**停止**——永不越过会话工作区向上,因此其上方的 harness 目录(如全局 `~/.mstar`)永远不会被采纳。 |
51
51
  | `enforcement` | `'hard' \| 'soft'` | compass,否则仅告警 | 按部署覆盖。优先级:Config 优先;否则取 Assignment 自身的 `**Enforcement**: hard` 头字段(仅派发闸门);否则取仓库 `.mstarc` `[config] enforcement`;否则取迭代 compass frontmatter;否则仅告警。Config / `.mstarc` `soft` 是仅有的本地回滚——Assignment 级 `soft` 不能覆盖 hard compass。 |
52
52
  | `dispatchTools` | `string[]` | `['subagent', 'subagent_fork']` | 派发闸门匹配的委派工具名——dsh preset 的**两个**委派工具:`subagent` 及其 fork 兄弟 `subagent_fork`(两者都携带 Assignment 形态的 `{ description, prompt }` 参数;`toolName` 配置可重命名实例)。 |
53
- | `dispatchBinding` | `string` | 未设置(跳过预检) | 派发方 agent 自身的 harness 角色;Assignment 的 `Execute as` 等于它即自我递归。 |
53
+ | `dispatchBinding` | `string` | 未设置 → hard 下 fail-closed `empty-binding` | 派发方 agent 自身的 harness 角色(反递归 caller);Assignment 的 `Execute as` 等于它即自我递归。 |
54
54
  | `roleMap` | `Record<string, string>` | 未设置 | mstar 角色 id(`Execute as`)→ dsh-llm-fallbacks 角色 id。**仅**作日志与未来规则驱动互操作的分类桥——装饰从不读取它(见 LLM fallbacks integration)。 |
55
55
  | `rolePersonas` | `Record<string, string>` | 未设置(打包镜像默认) | mstar 角色 id(`Execute as`)→ persona 文本;基于角色的 subagent 装饰的**覆盖**来源——角色匹配的 `subagent/start` 会把 persona 注册为子会话的 `mstar:role-persona` system-prompt 段;某角色未设置时使用打包的 `harness-agents/` 镜像默认值(见 LLM fallbacks integration)。 |
56
56
  | `skillRoots` | `string[]` | 未设置(不注册自定义根) | 向 dsh skill-filesystem 提供者注册的额外技能根(`customSkillDirs` 语义——先于用户根扫描)。开发期:镜像 `<repo-root>/skills` 的绝对路径。 |
package/bundle/README.md CHANGED
@@ -79,7 +79,7 @@ The `mstar` row accepts the plugin `Config` (see `src/index.ts`):
79
79
  | `harnessDir` | unset (resolved per session workspace) | explicit `{HARNESS_DIR}` root — **required for repos whose harness root is not a probed name** (`.mstar/` → `.agents/` → `.plans/` → `plans/`); set it to the absolute root in the profile layer (or declare `[config] harness_dir` in a repo `.mstarc` — gitignored local config, honored above probing). Without either, the probe starts from the SESSION workspace root (the session cwd — **never the process/launch cwd**) and **stops there** — it never walks above the session workspace, so a global `~/.mstar` is never adopted |
80
80
  | `enforcement` | **unset — default OFF** | `hard` / `soft` override; absent → the repo `.mstarc` `[config] enforcement`, else the iteration compass decides, warn-only when no compass hardens (never a global always-on hard gate) |
81
81
  | `dispatchTools` | unset (plugin default `['subagent', 'subagent_fork']`) | delegation tool names the dispatch gate matches — the dsh preset's TWO delegation tools (`subagent` + its fork sibling `subagent_fork`, both Assignment-shaped); a custom list overrides the default wholesale, so it must include `subagent_fork` to keep fork dispatches gated |
82
- | `dispatchBinding` | unset | the dispatching agent's role for the anti-recursion precheck |
82
+ | `dispatchBinding` | unset → fail-closed `empty-binding` under hard | the dispatching agent's own role (the anti-recursion CALLER) for the precheck |
83
83
  | `skillRoots` | unset | additional skill roots (custom mirrors) |
84
84
  | `bundledSkillDir` | unset → plugin resolves its OWN packaged `harness-skills/` mirror package-relative | bundled skill mount — the repo-root `skills/` mirror synced by `bundle-assets` at build time (gitignored), resolved package-relative (NOT cwd-anchored). An explicit value wins; a RELATIVE override stays cwd-anchored, so pass an absolute path in the profile layer |
85
85
  | `catalogTtlMs` | unset → `60000` | pre-step catalog cache refresh interval (ms) — how often the per-workspace unified `mstar-engine-status` catalog row (watermark + iteration gate + workspace-state digest) re-reads `status.json` / the compass / the knowledge index; the hot path is a timestamp compare + cache hit between refreshes |
package/dist/index.js CHANGED
@@ -1122,24 +1122,24 @@ import { basename, dirname, join, resolve } from "node:path";
1122
1122
  import { fileURLToPath } from "node:url";
1123
1123
  import { readFileSync as readFileSync2, statSync } from "node:fs";
1124
1124
  import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
1125
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
1125
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync6, realpathSync as realpathSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
1126
1126
  import { execFileSync } from "node:child_process";
1127
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join8, relative as relative2, resolve as resolve6 } from "node:path";
1127
+ import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve7 } from "node:path";
1128
1128
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3 } from "node:fs";
1129
1129
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
1130
1130
  import { AsyncLocalStorage } from "node:async_hooks";
1131
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync } from "node:fs";
1132
- import { dirname as dirname4, join as join6, resolve as resolve4, sep } from "node:path";
1131
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, realpathSync } from "node:fs";
1132
+ import { dirname as dirname4, join as join7, resolve as resolve5, sep } from "node:path";
1133
1133
  import { execFileSync as execFileSync2 } from "node:child_process";
1134
- import { existsSync as existsSync6 } from "node:fs";
1135
- import { isAbsolute as isAbsolute4, resolve as resolve7 } from "node:path";
1134
+ import { existsSync as existsSync7 } from "node:fs";
1135
+ import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
1136
1136
  import { execFileSync as execFileSync3 } from "node:child_process";
1137
- import { mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
1138
- import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join9, resolve as resolve8 } from "node:path";
1139
- import { existsSync as existsSync8, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
1140
- import { basename as basename5, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve11, sep as sep4 } from "node:path";
1141
- import { existsSync as existsSync9 } from "node:fs";
1142
- import { join as join13 } from "node:path";
1137
+ import { mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
1138
+ import { basename as basename4, dirname as dirname6, isAbsolute as isAbsolute6, join as join10, resolve as resolve9 } from "node:path";
1139
+ import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync10 } from "node:fs";
1140
+ import { basename as basename6, isAbsolute as isAbsolute8, join as join13, relative as relative4, resolve as resolve12, sep as sep4 } from "node:path";
1141
+ import { existsSync as existsSync10 } from "node:fs";
1142
+ import { join as join14 } from "node:path";
1143
1143
  var SEVERITY_ORDER = ["critical", "high", "medium", "low", "nit"];
1144
1144
  function applyEnforcement(gate, opts) {
1145
1145
  return { ...gate, hardBlocked: opts.hard && gate.violations.length > 0 };
@@ -1841,8 +1841,10 @@ function composeDispatchGate(text, opts = {}) {
1841
1841
  const violations = [];
1842
1842
  const writable = opts.writable !== false;
1843
1843
  violations.push(...validateAssignmentFields(text, { writable }).violations);
1844
- const agent = opts.agent ?? "";
1845
- violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
1844
+ const caller = opts.caller ?? "";
1845
+ if (caller.trim() !== "" || opts.callerRequired === true) {
1846
+ violations.push(...antiRecursionPrecheck(caller, parseAssignmentFields(text).executeAs ?? "").violations);
1847
+ }
1846
1848
  if (writable) {
1847
1849
  const forms = parseAssignmentBranchForms(text);
1848
1850
  const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
@@ -1865,7 +1867,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
1865
1867
  return {
1866
1868
  ok: false,
1867
1869
  violations: [
1868
- violation3("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
1870
+ violation3("critical", "dispatch.anti-recursion.empty-binding", `empty caller role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "declare the dispatching agent's own role binding (dsh Config `dispatchBinding`) before dispatching")
1869
1871
  ]
1870
1872
  };
1871
1873
  }
@@ -2145,7 +2147,7 @@ function validateStatusV2(docOrPath, opts = {}) {
2145
2147
  if (typeof docOrPath === "string") {
2146
2148
  try {
2147
2149
  doc = readJson(docOrPath);
2148
- harnessDir = dirname4(resolve4(docOrPath));
2150
+ harnessDir = dirname4(resolve5(docOrPath));
2149
2151
  } catch (error) {
2150
2152
  return {
2151
2153
  ok: false,
@@ -2212,8 +2214,8 @@ function validateStatusV2(docOrPath, opts = {}) {
2212
2214
  for (const entry of doc.workflows) {
2213
2215
  if (!isPlainObject4(entry) || typeof entry.dir !== "string")
2214
2216
  continue;
2215
- const relSnapshot = join6(entry.dir, WORKFLOW_SNAPSHOT_FILE);
2216
- const snapshotPath = join6(harnessDir, relSnapshot);
2217
+ const relSnapshot = join7(entry.dir, WORKFLOW_SNAPSHOT_FILE);
2218
+ const snapshotPath = join7(harnessDir, relSnapshot);
2217
2219
  const label = typeof entry.id === "string" ? entry.id : relSnapshot;
2218
2220
  let physical;
2219
2221
  try {
@@ -2249,19 +2251,19 @@ function validateStatusV2(docOrPath, opts = {}) {
2249
2251
  var validateStatus = validateStatusV2;
2250
2252
  function resolveCompassEnforcement(harnessDir) {
2251
2253
  const iterationsDir = resolveIterationDir(harnessDir);
2252
- if (!existsSync3(iterationsDir))
2254
+ if (!existsSync4(iterationsDir))
2253
2255
  return { hard: false, source: "none" };
2254
2256
  let entries;
2255
2257
  try {
2256
- entries = readdirSync2(iterationsDir, { withFileTypes: true });
2258
+ entries = readdirSync3(iterationsDir, { withFileTypes: true });
2257
2259
  } catch {
2258
2260
  return { hard: false, source: "none" };
2259
2261
  }
2260
2262
  for (const entry of entries) {
2261
2263
  if (!entry.isDirectory())
2262
2264
  continue;
2263
- const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
2264
- if (!existsSync3(compassPath))
2265
+ const compassPath = join7(iterationsDir, entry.name, "delivery-compass.md");
2266
+ if (!existsSync4(compassPath))
2265
2267
  continue;
2266
2268
  let content;
2267
2269
  try {
@@ -2280,7 +2282,7 @@ function resolveCompassEnforcement(harnessDir) {
2280
2282
  return { hard: false, source: "none" };
2281
2283
  }
2282
2284
  function resolveMstarcEnforcement(harnessDir) {
2283
- const dir = resolve4(harnessDir);
2285
+ const dir = resolve5(harnessDir);
2284
2286
  const rc = loadMstarc(dir, dirname4(dir));
2285
2287
  const value = rc?.config.enforcement;
2286
2288
  if (value === "hard")
@@ -2392,19 +2394,19 @@ function findingsCleanupGate(register, planId, opts) {
2392
2394
  return { ok: violations.length === 0, violations };
2393
2395
  }
2394
2396
  function resolveHarnessDir(startDir = process.cwd(), opts = {}) {
2395
- const start = resolve6(startDir);
2397
+ const start = resolve7(startDir);
2396
2398
  const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
2397
2399
  if (explicit)
2398
- return resolve6(start, explicit);
2399
- const boundary = resolve6(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
2400
+ return resolve7(start, explicit);
2401
+ const boundary = resolve7(start, opts.workspaceRoot ?? defaultWorkspaceRoot(start));
2400
2402
  const rc = loadMstarc(start, boundary);
2401
2403
  if (rc !== null && rc.config.harnessDir)
2402
- return resolve6(rc.dir, rc.config.harnessDir);
2404
+ return resolve7(rc.dir, rc.config.harnessDir);
2403
2405
  let dir = start;
2404
2406
  for (;; ) {
2405
2407
  if (!isAtOrBelow2(dir, boundary))
2406
2408
  return null;
2407
- for (const candidate of [join8(dir, ".mstar"), join8(dir, ".agents"), join8(dir, ".plans"), join8(dir, "plans")]) {
2409
+ for (const candidate of [join9(dir, ".mstar"), join9(dir, ".agents"), join9(dir, ".plans"), join9(dir, "plans")]) {
2408
2410
  if (isDirectory(candidate))
2409
2411
  return candidate;
2410
2412
  }
@@ -2430,19 +2432,19 @@ function defaultWorkspaceRoot(startDir) {
2430
2432
  if (segment && segment !== ".")
2431
2433
  boundary = dirname5(boundary);
2432
2434
  }
2433
- return resolve6(boundary);
2435
+ return resolve7(boundary);
2434
2436
  } catch {}
2435
2437
  return startDir;
2436
2438
  }
2437
2439
  function isAtOrBelow2(dir, root) {
2438
2440
  const rel = relative2(root, dir);
2439
- return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
2441
+ return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
2440
2442
  }
2441
2443
  function mstarcDirOverride(harnessDir, key) {
2442
- const dir = resolve6(harnessDir);
2444
+ const dir = resolve7(harnessDir);
2443
2445
  const rc = loadMstarc(dir, dirname5(dir));
2444
2446
  const declared = rc?.config[key];
2445
- return declared ? resolve6(rc.dir, declared) : null;
2447
+ return declared ? resolve7(rc.dir, declared) : null;
2446
2448
  }
2447
2449
  function assertSafePathComponent(value, what) {
2448
2450
  if (value === "" || value === "." || value === ".." || !/^[A-Za-z0-9._-]+$/.test(value)) {
@@ -2451,24 +2453,24 @@ function assertSafePathComponent(value, what) {
2451
2453
  }
2452
2454
  function resolveSddDir(harnessDir, planId) {
2453
2455
  assertSafePathComponent(planId, "planId");
2454
- const base = resolve6(harnessDir);
2456
+ const base = resolve7(harnessDir);
2455
2457
  const declared = mstarcDirOverride(base, "sddDir");
2456
- const sddBase = declared !== null ? declared : join8(base, "sdd");
2457
- return join8(sddBase, planId);
2458
+ const sddBase = declared !== null ? declared : join9(base, "sdd");
2459
+ return join9(sddBase, planId);
2458
2460
  }
2459
2461
  function resolveIterationDir(harnessDir) {
2460
2462
  const declared = mstarcDirOverride(harnessDir, "iterationDir");
2461
2463
  if (declared !== null)
2462
2464
  return declared;
2463
- return join8(resolve6(harnessDir), "iterations");
2465
+ return join9(resolve7(harnessDir), "iterations");
2464
2466
  }
2465
2467
  function resolveHarnessSubdir(startDir, opts, key, fallback) {
2466
2468
  const harness = resolveHarnessDir(startDir, opts);
2467
2469
  if (harness === null) {
2468
- throw new Error(`harness dir not found from ${resolve6(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
2470
+ throw new Error(`harness dir not found from ${resolve7(startDir)} — cannot resolve the ${fallback} dir (run \`mstar harness scaffold\`, pass opts.harnessDir, or set MSTAR_HARNESS_DIR)`);
2469
2471
  }
2470
2472
  const declared = mstarcDirOverride(harness, key);
2471
- return declared !== null ? declared : join8(resolve6(harness), fallback);
2473
+ return declared !== null ? declared : join9(resolve7(harness), fallback);
2472
2474
  }
2473
2475
  function resolveWorkflowDir(startDir = process.cwd(), opts = {}) {
2474
2476
  return resolveHarnessSubdir(startDir, opts, "workflowDir", "workflows");
@@ -2558,10 +2560,10 @@ function l1PreDispatchCheck(input, opts = {}) {
2558
2560
  if (leaseWorkingBranch.trim() === "") {
2559
2561
  violations.push(violation7("high", "worktree.l1.lease-branch-missing", `execution_lease.working_branch is empty for plan "${planId}"`, "record the lease working_branch before dispatch"));
2560
2562
  }
2561
- if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve7(controlWorktreePath) === resolve7(leaseWorktreePath)) {
2563
+ if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve8(controlWorktreePath) === resolve8(leaseWorktreePath)) {
2562
2564
  violations.push(violation7("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
2563
2565
  }
2564
- if (leaseWorktreePath !== "" && !existsSync6(leaseWorktreePath)) {
2566
+ if (leaseWorktreePath !== "" && !existsSync7(leaseWorktreePath)) {
2565
2567
  violations.push(violation7("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
2566
2568
  } else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
2567
2569
  const probe = probeBranch(leaseWorktreePath, opts);
@@ -2585,17 +2587,17 @@ function l2PreDispatchCheck(input, opts = {}) {
2585
2587
  violations.push(violation7("high", "worktree.l2.track-invalid", `track ${index + 1} is missing worktreePath and/or workingBranch`, "fill both fields for every track"));
2586
2588
  return;
2587
2589
  }
2588
- if (!isAbsolute4(track.worktreePath)) {
2590
+ if (!isAbsolute5(track.worktreePath)) {
2589
2591
  violations.push(violation7("high", "worktree.l2.track-path-relative", `track ${index + 1} worktreePath "${track.worktreePath}" is not an absolute path — L2 tracks MUST use absolute worktree checkout paths (consistent with the lease validator's absolute worktree_path enforcement)`, `use an absolute path for track ${index + 1} (e.g. /Users/<you>/worktrees/<branch>)`));
2590
2592
  return;
2591
2593
  }
2592
- const normalized = resolve7(track.worktreePath);
2594
+ const normalized = resolve8(track.worktreePath);
2593
2595
  if (seenPaths.has(normalized)) {
2594
2596
  violations.push(violation7("high", "worktree.l2.track-path-collision", `duplicate worktreePath "${track.worktreePath}" across parallel tracks — L2 parallel-writable isolation requires a distinct absolute Worktree path per track (N parallel invokes ≠ isolation)`, "give every parallel track its own git worktree checkout"));
2595
2597
  return;
2596
2598
  }
2597
2599
  seenPaths.add(normalized);
2598
- if (!existsSync6(track.worktreePath)) {
2600
+ if (!existsSync7(track.worktreePath)) {
2599
2601
  violations.push(violation7("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
2600
2602
  return;
2601
2603
  }
@@ -2630,7 +2632,7 @@ function isFile2(file) {
2630
2632
  return false;
2631
2633
  }
2632
2634
  }
2633
- var GIT_CAPTURE_MAX_BYTES = 64 * 1024 * 1024;
2635
+ var GIT_CAPTURE_MAX_BYTES = 67108864;
2634
2636
  function gitOut(cwd, args) {
2635
2637
  try {
2636
2638
  return execFileSync3("git", args, {
@@ -2644,14 +2646,14 @@ function gitOut(cwd, args) {
2644
2646
  }
2645
2647
  }
2646
2648
  function probeHarnessWithStatus(root) {
2647
- if (isFile2(join9(root, ".mstar", "status.json")))
2648
- return join9(root, ".mstar");
2649
- if (isFile2(join9(root, ".agents", "status.json")))
2650
- return join9(root, ".agents");
2651
- if (hasWorkflowSnapshot(join9(root, ".mstar")))
2652
- return join9(root, ".mstar");
2653
- if (hasWorkflowSnapshot(join9(root, ".agents")))
2654
- return join9(root, ".agents");
2649
+ if (isFile2(join10(root, ".mstar", "status.json")))
2650
+ return join10(root, ".mstar");
2651
+ if (isFile2(join10(root, ".agents", "status.json")))
2652
+ return join10(root, ".agents");
2653
+ if (hasWorkflowSnapshot(join10(root, ".mstar")))
2654
+ return join10(root, ".mstar");
2655
+ if (hasWorkflowSnapshot(join10(root, ".agents")))
2656
+ return join10(root, ".agents");
2655
2657
  return null;
2656
2658
  }
2657
2659
  function hasWorkflowSnapshot(harnessDir) {
@@ -2659,13 +2661,13 @@ function hasWorkflowSnapshot(harnessDir) {
2659
2661
  try {
2660
2662
  workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
2661
2663
  } catch {
2662
- workflowsDir = join9(harnessDir, "workflows");
2664
+ workflowsDir = join10(harnessDir, "workflows");
2663
2665
  }
2664
2666
  if (!isDirectory2(workflowsDir))
2665
2667
  return false;
2666
2668
  try {
2667
- for (const entry of readdirSync5(workflowsDir, { withFileTypes: true })) {
2668
- if (entry.isDirectory() && isFile2(join9(workflowsDir, entry.name, "snapshot.json")))
2669
+ for (const entry of readdirSync6(workflowsDir, { withFileTypes: true })) {
2670
+ if (entry.isDirectory() && isFile2(join10(workflowsDir, entry.name, "snapshot.json")))
2669
2671
  return true;
2670
2672
  }
2671
2673
  } catch {
@@ -2678,14 +2680,14 @@ function isLinkedWorktree(root) {
2678
2680
  const commonRaw = gitOut(root, ["rev-parse", "--git-common-dir"]);
2679
2681
  if (gitDirRaw === null || commonRaw === null)
2680
2682
  return false;
2681
- const gitDir = isAbsolute5(gitDirRaw) ? gitDirRaw : join9(root, gitDirRaw);
2682
- const common = isAbsolute5(commonRaw) ? commonRaw : join9(root, commonRaw);
2683
+ const gitDir = isAbsolute6(gitDirRaw) ? gitDirRaw : join10(root, gitDirRaw);
2684
+ const common = isAbsolute6(commonRaw) ? commonRaw : join10(root, commonRaw);
2683
2685
  if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
2684
2686
  return true;
2685
2687
  try {
2686
2688
  const gdParent = realpathSync3(dirname6(gitDir));
2687
2689
  const cmAbs = realpathSync3(common);
2688
- return join9(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
2690
+ return join10(gdParent, basename4(gitDir)) !== cmAbs && gitDir !== cmAbs;
2689
2691
  } catch {
2690
2692
  return false;
2691
2693
  }
@@ -2693,7 +2695,7 @@ function isLinkedWorktree(root) {
2693
2695
  function sddWorkspace(planId, opts = {}) {
2694
2696
  if (!planId) {
2695
2697
  throw new SddScriptError(`usage: mstar sdd workspace PLAN_ID [CONTROL_ROOT]
2696
- ` + " Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.", 2);
2698
+ Set MSTAR_CONTROL_ROOT=<control_worktree_path> when running from a feature worktree.`, 2);
2697
2699
  }
2698
2700
  const cwd = opts.cwd ?? process.cwd();
2699
2701
  const controlRoot = opts.controlRoot ?? (process.env.MSTAR_CONTROL_ROOT || undefined);
@@ -2709,35 +2711,35 @@ function sddWorkspace(planId, opts = {}) {
2709
2711
  }
2710
2712
  if (!controlRoot && isLinkedWorktree(root)) {
2711
2713
  throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
2712
- ` + ` Refusing to create a second SDD tree under the feature checkout.
2713
- ` + ` Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
2714
+ Refusing to create a second SDD tree under the feature checkout.
2715
+ Re-run with MSTAR_CONTROL_ROOT=<control_worktree_path> or: mstar sdd workspace ${planId} <control_worktree_path>
2714
2716
  ` + ` See mstar-branch-worktree «Harness path SSOT under default gitignore».`, 1);
2715
2717
  }
2716
2718
  const harnessOverride = opts.harnessDir ?? (process.env.MSTAR_HARNESS_DIR || undefined);
2717
2719
  let harnessDir;
2718
2720
  if (harnessOverride) {
2719
- harnessDir = resolve8(root, harnessOverride);
2721
+ harnessDir = resolve9(root, harnessOverride);
2720
2722
  } else {
2721
2723
  const rc = findMstarc(root, root);
2722
2724
  const rcHarnessDir = rc !== null ? parseMstarc(readFileSync7(rc, "utf8")).harnessDir : undefined;
2723
2725
  if (rcHarnessDir) {
2724
- harnessDir = resolve8(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2726
+ harnessDir = resolve9(rc !== null ? dirname6(rc) : root, rcHarnessDir);
2725
2727
  } else {
2726
2728
  const probed = probeHarnessWithStatus(root);
2727
2729
  if (probed) {
2728
2730
  harnessDir = probed;
2729
- } else if (isDirectory2(join9(root, ".mstar"))) {
2730
- harnessDir = join9(root, ".mstar");
2731
- } else if (isDirectory2(join9(root, ".agents"))) {
2732
- harnessDir = join9(root, ".agents");
2731
+ } else if (isDirectory2(join10(root, ".mstar"))) {
2732
+ harnessDir = join10(root, ".mstar");
2733
+ } else if (isDirectory2(join10(root, ".agents"))) {
2734
+ harnessDir = join10(root, ".agents");
2733
2735
  } else {
2734
- harnessDir = join9(root, ".mstar");
2736
+ harnessDir = join10(root, ".mstar");
2735
2737
  }
2736
2738
  }
2737
2739
  }
2738
2740
  const sddDir = resolveSddDir(harnessDir, planId);
2739
2741
  mkdirSync6(sddDir, { recursive: true });
2740
- writeFileSync4(join9(sddDir, ".gitignore"), `*
2742
+ writeFileSync4(join10(sddDir, ".gitignore"), `*
2741
2743
  `);
2742
2744
  return realpathSync3(sddDir);
2743
2745
  }
@@ -2760,7 +2762,7 @@ function taskBrief(planFile, taskN, outFile, opts = {}) {
2760
2762
  throw new SddScriptError("mstar sdd task-brief: set SDD_DIR or pass OUTFILE (run mstar sdd workspace PLAN_ID first)", 2);
2761
2763
  }
2762
2764
  mkdirSync6(sddDir, { recursive: true });
2763
- out = join9(sddDir, `task-${taskN}-brief.md`);
2765
+ out = join10(sddDir, `task-${taskN}-brief.md`);
2764
2766
  }
2765
2767
  const records = content.endsWith(`
2766
2768
  `) ? content.split(`
@@ -3501,7 +3503,7 @@ function referenceExists(repoRoot, docText) {
3501
3503
  if (ref === "" || seen.has(ref))
3502
3504
  continue;
3503
3505
  seen.add(ref);
3504
- if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute7(ref)) {
3506
+ if (SCHEME_RE.test(ref) || ref.startsWith("{") || ref.startsWith("#") || ref.includes("*") || ref.includes("?") || ref.startsWith("~") || isAbsolute8(ref)) {
3505
3507
  continue;
3506
3508
  }
3507
3509
  if (ref.includes("/") || REF_EXT_RE.test(ref)) {
@@ -3520,7 +3522,7 @@ function referenceExists(repoRoot, docText) {
3520
3522
  const dir = stack.pop();
3521
3523
  let entries;
3522
3524
  try {
3523
- entries = readdirSync8(dir, { withFileTypes: true });
3525
+ entries = readdirSync9(dir, { withFileTypes: true });
3524
3526
  } catch {
3525
3527
  continue;
3526
3528
  }
@@ -3529,7 +3531,7 @@ function referenceExists(repoRoot, docText) {
3529
3531
  break;
3530
3532
  if (entry.isDirectory()) {
3531
3533
  if (!WALK_SKIP_DIRS.has(entry.name))
3532
- stack.push(join12(dir, entry.name));
3534
+ stack.push(join13(dir, entry.name));
3533
3535
  } else if (!entry.isSymbolicLink()) {
3534
3536
  const base = entry.name.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
3535
3537
  if (moduleNames.has(base))
@@ -3541,7 +3543,7 @@ function referenceExists(repoRoot, docText) {
3541
3543
  for (const { ref, isSymbol, module } of refs) {
3542
3544
  if (!isSymbol || module === undefined) {
3543
3545
  const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
3544
- if (existsSync8(resolve11(repoRoot, candidate))) {
3546
+ if (existsSync9(resolve12(repoRoot, candidate))) {
3545
3547
  checked++;
3546
3548
  } else {
3547
3549
  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"));
@@ -3561,14 +3563,14 @@ function collectKnowledgeDocs(dir) {
3561
3563
  const current = stack.pop();
3562
3564
  let entries;
3563
3565
  try {
3564
- entries = readdirSync8(current, { withFileTypes: true });
3566
+ entries = readdirSync9(current, { withFileTypes: true });
3565
3567
  } catch {
3566
3568
  continue;
3567
3569
  }
3568
3570
  for (const entry of entries) {
3569
3571
  if (entry.isSymbolicLink())
3570
3572
  continue;
3571
- const full = join12(current, entry.name);
3573
+ const full = join13(current, entry.name);
3572
3574
  if (entry.isDirectory()) {
3573
3575
  stack.push(full);
3574
3576
  } else if (entry.name.endsWith(".md") && entry.name !== "README.md" && entry.name !== "index.md") {
@@ -3588,8 +3590,8 @@ function normalizeIndexRef(cell) {
3588
3590
  }
3589
3591
  function assertIndexRows(knowledgeDir) {
3590
3592
  const violations = [];
3591
- const readmePath = join12(knowledgeDir, "README.md");
3592
- if (!existsSync8(readmePath)) {
3593
+ const readmePath = join13(knowledgeDir, "README.md");
3594
+ if (!existsSync9(readmePath)) {
3593
3595
  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"));
3594
3596
  return { ok: false, violations };
3595
3597
  }
@@ -3613,12 +3615,12 @@ function assertIndexRows(knowledgeDir) {
3613
3615
  return { ok: violations.length === 0, violations };
3614
3616
  }
3615
3617
  function isFileLikeRoot(root) {
3616
- return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename5(root));
3618
+ return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename6(root));
3617
3619
  }
3618
3620
  function scopeGuard(path, allowedRoots) {
3619
- const resolved = resolve11(path);
3621
+ const resolved = resolve12(path);
3620
3622
  for (const root of allowedRoots) {
3621
- const r = resolve11(root);
3623
+ const r = resolve12(root);
3622
3624
  if (isFileLikeRoot(r)) {
3623
3625
  if (resolved === r)
3624
3626
  return { ok: true, violations: [] };
@@ -3776,8 +3778,8 @@ function validateRoleMapping(rolesDir, options = {}) {
3776
3778
  const violations = [];
3777
3779
  const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
3778
3780
  for (const { agentId, reference } of mapping) {
3779
- if (!existsSync9(join13(rolesDir, reference))) {
3780
- 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`));
3781
+ if (!existsSync10(join14(rolesDir, reference))) {
3782
+ 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 ${join14(rolesDir, reference)} or fix the mapping row`));
3781
3783
  }
3782
3784
  }
3783
3785
  for (const { family, memberIds } of families) {
@@ -3990,8 +3992,8 @@ class DshMstar extends Service {
3990
3992
 
3991
3993
  // src/gates/_shared.ts
3992
3994
  var import_schemastery = __toESM(require_lib2(), 1);
3993
- import { existsSync as existsSync4 } from "node:fs";
3994
- import { resolve as resolve5 } from "node:path";
3995
+ import { existsSync as existsSync3 } from "node:fs";
3996
+ import { resolve as resolve4 } from "node:path";
3995
3997
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3996
3998
  var STATUS_FILE = "status.json";
3997
3999
  var PERSONA_INTERPOLATION_HAZARD = /\{\{[\s\S]*\}\}/;
@@ -4045,7 +4047,7 @@ function resolvePackagedSkillsDir(fileUrl) {
4045
4047
  for (const rel of ["../harness-skills", "../../harness-skills"]) {
4046
4048
  try {
4047
4049
  const dir = fileURLToPath2(new URL(rel, fileUrl));
4048
- if (existsSync4(dir))
4050
+ if (existsSync3(dir))
4049
4051
  return dir;
4050
4052
  } catch {}
4051
4053
  }
@@ -4064,7 +4066,7 @@ function resolvePackagedAgentsDir(fileUrl) {
4064
4066
  for (const rel of ["../harness-agents", "../../harness-agents"]) {
4065
4067
  try {
4066
4068
  const dir = fileURLToPath2(new URL(rel, fileUrl));
4067
- if (existsSync4(dir))
4069
+ if (existsSync3(dir))
4068
4070
  return dir;
4069
4071
  } catch {}
4070
4072
  }
@@ -4084,14 +4086,14 @@ class HarnessResolver {
4084
4086
  explicit;
4085
4087
  cache = new Map;
4086
4088
  constructor(explicit) {
4087
- this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve5(process.cwd(), explicit) : null;
4089
+ this.explicit = explicit !== undefined && explicit.trim() !== "" ? resolve4(process.cwd(), explicit) : null;
4088
4090
  }
4089
4091
  forWorkspace(cwd) {
4090
4092
  if (this.explicit !== null)
4091
4093
  return this.explicit;
4092
4094
  if (cwd === undefined || cwd.trim() === "")
4093
4095
  return null;
4094
- const abs = resolve5(cwd);
4096
+ const abs = resolve4(cwd);
4095
4097
  const hit = this.cache.get(abs);
4096
4098
  if (hit !== undefined)
4097
4099
  return hit;
@@ -4132,20 +4134,20 @@ function skillLocalConfig(config) {
4132
4134
  }
4133
4135
 
4134
4136
  // src/gates/catalog.ts
4135
- import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync6 } from "node:fs";
4136
- import { join as join10 } from "node:path";
4137
+ import { existsSync as existsSync13, readFileSync as readFileSync8, readdirSync as readdirSync4 } from "node:fs";
4138
+ import { join as join8 } from "node:path";
4137
4139
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
4138
4140
 
4139
4141
  // src/gates/agent-flow.ts
4140
- import { appendFileSync, closeSync, existsSync as existsSync12, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4141
- import { join as join7 } from "node:path";
4142
+ import { appendFileSync, closeSync, existsSync as existsSync11, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, readSync, renameSync as renameSync2, rmdirSync, statSync as statSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
4143
+ import { join as join6 } from "node:path";
4142
4144
 
4143
4145
  // src/gates/dispatch.ts
4144
- import { existsSync as existsSync10 } from "node:fs";
4145
- import { basename as basename4, join as join5, resolve as resolve9 } from "node:path";
4146
+ import { existsSync as existsSync8 } from "node:fs";
4147
+ import { basename as basename2, join as join5, resolve as resolve6 } from "node:path";
4146
4148
 
4147
4149
  // src/gates/workflow-selection.ts
4148
- import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
4150
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
4149
4151
  import { dirname as dirname7, join as join3, relative as relative3 } from "node:path";
4150
4152
  var TERMINAL_STATUS_CACHE_MAX = 64;
4151
4153
  var terminalStatusCache = new Map;
@@ -4180,7 +4182,7 @@ function terminalStatusOf(snapshotPath) {
4180
4182
  }
4181
4183
  function resolveActiveWorkflow(harnessDir) {
4182
4184
  const statusPath = join3(harnessDir, STATUS_FILE);
4183
- if (!existsSync7(statusPath)) {
4185
+ if (!existsSync5(statusPath)) {
4184
4186
  return {
4185
4187
  kind: "error",
4186
4188
  code: "status.missing",
@@ -4238,7 +4240,7 @@ function resolveReadWorkflow(harnessDir) {
4238
4240
  if (active.kind === "error" && active.code !== "workflow.selection.no-active")
4239
4241
  return active;
4240
4242
  const workflowsDir = resolveWorkflowDir(harnessDir, { harnessDir });
4241
- if (!existsSync7(workflowsDir)) {
4243
+ if (!existsSync5(workflowsDir)) {
4242
4244
  return {
4243
4245
  kind: "error",
4244
4246
  code: "workflow.selection.no-snapshot",
@@ -4247,7 +4249,7 @@ function resolveReadWorkflow(harnessDir) {
4247
4249
  }
4248
4250
  let entries;
4249
4251
  try {
4250
- entries = readdirSync3(workflowsDir, { withFileTypes: true });
4252
+ entries = readdirSync2(workflowsDir, { withFileTypes: true });
4251
4253
  } catch {
4252
4254
  return {
4253
4255
  kind: "error",
@@ -4260,7 +4262,7 @@ function resolveReadWorkflow(harnessDir) {
4260
4262
  if (!entry.isDirectory())
4261
4263
  continue;
4262
4264
  const snapshotPath = join3(workflowsDir, entry.name, WORKFLOW_SNAPSHOT_FILE);
4263
- if (!existsSync7(snapshotPath)) {
4265
+ if (!existsSync5(snapshotPath)) {
4264
4266
  terminalStatusCache.delete(snapshotPath);
4265
4267
  continue;
4266
4268
  }
@@ -4272,7 +4274,7 @@ function resolveReadWorkflow(harnessDir) {
4272
4274
  }
4273
4275
  }
4274
4276
  for (const cachedPath of terminalStatusCache.keys()) {
4275
- if (!existsSync7(join3(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
4277
+ if (!existsSync5(join3(dirname7(cachedPath), WORKFLOW_SNAPSHOT_FILE))) {
4276
4278
  terminalStatusCache.delete(cachedPath);
4277
4279
  }
4278
4280
  }
@@ -4435,12 +4437,12 @@ function isNaValue(value) {
4435
4437
  function planIdOf(headerRegion) {
4436
4438
  const planPath = assignmentHeaderValue(headerRegion, "Plan Path");
4437
4439
  if (!isNaValue(planPath)) {
4438
- const id = basename4(firstToken(planPath) ?? "");
4440
+ const id = basename2(firstToken(planPath) ?? "");
4439
4441
  return id.endsWith(".md") ? id.slice(0, -3) : id;
4440
4442
  }
4441
4443
  const sddDir = assignmentHeaderValue(headerRegion, "SDD dir");
4442
4444
  if (!isNaValue(sddDir)) {
4443
- const id = basename4(firstToken(sddDir) ?? "");
4445
+ const id = basename2(firstToken(sddDir) ?? "");
4444
4446
  return id === "" ? undefined : id;
4445
4447
  }
4446
4448
  const planId = assignmentHeaderValue(headerRegion, "plan_id");
@@ -4470,7 +4472,7 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4470
4472
  return [leaseViolation("lease.dispatch.unverifiable", `${join5(harnessDir, STATUS_FILE)}: ${selection.kind === "error" ? selection.message : "no active workflow"} — the plan's execution_lease state is unverifiable; STOP before writable dispatch`, "create a valid v2 status.json registering an active workflow (first implement dispatch requires a plan row + lease)")];
4471
4473
  }
4472
4474
  const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4473
- if (!existsSync10(snapshotPath)) {
4475
+ if (!existsSync8(snapshotPath)) {
4474
4476
  if (!sdd)
4475
4477
  return [];
4476
4478
  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)")];
@@ -4503,7 +4505,7 @@ function leaseGateViolations(harnessDir, exec, writable, prompt) {
4503
4505
  const wt = worktree === undefined ? undefined : firstToken(worktree);
4504
4506
  if (isNaValue(wt)) {
4505
4507
  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)"));
4506
- } else if (resolve9(wt) !== resolve9(String(lease.worktree_path ?? ""))) {
4508
+ } else if (resolve6(wt) !== resolve6(String(lease.worktree_path ?? ""))) {
4507
4509
  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)"));
4508
4510
  }
4509
4511
  const forms = parseAssignmentBranchForms(header);
@@ -4544,7 +4546,7 @@ function worktreeL2Violations(header) {
4544
4546
  return violations;
4545
4547
  }
4546
4548
  function activeSnapshotRows(harnessDir) {
4547
- if (!existsSync10(join5(harnessDir, STATUS_FILE)))
4549
+ if (!existsSync8(join5(harnessDir, STATUS_FILE)))
4548
4550
  return { rows: null, controlWorktreePath: undefined, unreadable: false };
4549
4551
  const selection = resolveActiveWorkflow(harnessDir);
4550
4552
  if (selection.kind !== "active") {
@@ -4555,7 +4557,7 @@ function activeSnapshotRows(harnessDir) {
4555
4557
  };
4556
4558
  }
4557
4559
  const snapshotPath = join5(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
4558
- if (!existsSync10(snapshotPath))
4560
+ if (!existsSync8(snapshotPath))
4559
4561
  return { rows: null, controlWorktreePath: undefined, unreadable: true };
4560
4562
  let doc;
4561
4563
  try {
@@ -4609,7 +4611,7 @@ function dispatchGateCore(config, harnessDir, prompt) {
4609
4611
  const header = assignmentHeaderRegion(prompt);
4610
4612
  const violations = [...worktreeL2Violations(header), ...worktreeL1Violations(harnessDir, header)];
4611
4613
  const writable = isReadOnlyAssignmentRole(parseAssignmentFields(header).executeAs ?? "") ? false : undefined;
4612
- violations.push(...composeDispatchGate(header, { agent: config.dispatchBinding ?? "", writable }).violations);
4614
+ violations.push(...composeDispatchGate(header, { caller: config.dispatchBinding ?? "", callerRequired: true, writable }).violations);
4613
4615
  return { violations, writable };
4614
4616
  }
4615
4617
  function workflowGateInputOf(exec) {
@@ -4835,7 +4837,7 @@ function callPairingKey(exec) {
4835
4837
  function resolveAgentFlowWriteDir(harnessDir) {
4836
4838
  const selection = resolveActiveWorkflow(harnessDir);
4837
4839
  if (selection.kind === "active")
4838
- return join7(harnessDir, selection.dir);
4840
+ return join6(harnessDir, selection.dir);
4839
4841
  const message = selection.kind === "error" ? selection.message : "no active lifecycle (unexpected terminal selection — the active-set resolver has no history view)";
4840
4842
  if (!noActiveWarned) {
4841
4843
  noActiveWarned = true;
@@ -4852,7 +4854,7 @@ function sleepSync(ms) {
4852
4854
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
4853
4855
  }
4854
4856
  function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4855
- const lockDir = join7(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4857
+ const lockDir = join6(workflowDir, WORKFLOW_LEDGER_LOCKDIR);
4856
4858
  if (heldWorkflowLocks.has(lockDir)) {
4857
4859
  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`);
4858
4860
  }
@@ -4878,7 +4880,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4878
4880
  heldWorkflowLocks.add(lockDir);
4879
4881
  try {
4880
4882
  try {
4881
- writeFileSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4883
+ writeFileSync2(join6(lockDir, WORKFLOW_LOCKER_HOLDER_PID), String(process.pid), "utf8");
4882
4884
  } catch {}
4883
4885
  return fn();
4884
4886
  } finally {
@@ -4887,7 +4889,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4887
4889
  const current = statSync5(lockDir);
4888
4890
  if (acquired !== null && current.dev === acquired.dev && current.ino === acquired.ino) {
4889
4891
  try {
4890
- unlinkSync2(join7(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4892
+ unlinkSync2(join6(lockDir, WORKFLOW_LOCKER_HOLDER_PID));
4891
4893
  } catch {}
4892
4894
  rmdirSync(lockDir);
4893
4895
  }
@@ -4895,7 +4897,7 @@ function withWorkflowDirLock(workflowDir, fn, opts = {}) {
4895
4897
  }
4896
4898
  }
4897
4899
  function appendEvent(workflowDir, event) {
4898
- const file = join7(workflowDir, AGENT_FLOW_FILE);
4900
+ const file = join6(workflowDir, AGENT_FLOW_FILE);
4899
4901
  withWorkflowDirLock(workflowDir, () => {
4900
4902
  appendFileSync(file, `${JSON.stringify(event)}
4901
4903
  `);
@@ -5284,8 +5286,8 @@ function ledgerContent(file, n) {
5284
5286
  }
5285
5287
  }
5286
5288
  function readAgentFlow(workflowDir, limit) {
5287
- const file = join7(workflowDir, AGENT_FLOW_FILE);
5288
- if (!existsSync12(file)) {
5289
+ const file = join6(workflowDir, AGENT_FLOW_FILE);
5290
+ if (!existsSync11(file)) {
5289
5291
  return { events: [], summary: [] };
5290
5292
  }
5291
5293
  const n = limit === undefined ? AGENT_FLOW_DEFAULT_LIMIT : Math.max(0, Math.floor(limit));
@@ -5539,7 +5541,7 @@ function hhmm(ts) {
5539
5541
  function harnessStateSource(harnessDir) {
5540
5542
  if (harnessDir === null)
5541
5543
  return null;
5542
- const statusPath = join10(harnessDir, STATUS_FILE);
5544
+ const statusPath = join8(harnessDir, STATUS_FILE);
5543
5545
  if (!existsSync13(statusPath))
5544
5546
  return null;
5545
5547
  try {
@@ -5559,7 +5561,7 @@ function harnessStateSource(harnessDir) {
5559
5561
  if (selection.kind === "error") {
5560
5562
  return selectionErrorState(selection, rollup, harnessDir, compass);
5561
5563
  }
5562
- const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5564
+ const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5563
5565
  let snapshot;
5564
5566
  if (!existsSync13(snapshotPath)) {
5565
5567
  return selectionErrorState({
@@ -5623,7 +5625,7 @@ function harnessStateSource(harnessDir) {
5623
5625
  leases,
5624
5626
  knowledge: knowledgeDigest(harnessDir),
5625
5627
  direction: compass !== undefined ? compassDirection(compass.compassPath) : null,
5626
- agentFlow: readAgentFlow(join10(harnessDir, selection.dir), 50)
5628
+ agentFlow: readAgentFlow(join8(harnessDir, selection.dir), 50)
5627
5629
  };
5628
5630
  } catch {
5629
5631
  return null;
@@ -5656,14 +5658,14 @@ function projectRollupSource(harnessDir, residuals) {
5656
5658
  if (existsSync13(projectsDir)) {
5657
5659
  let entries;
5658
5660
  try {
5659
- entries = readdirSync6(projectsDir, { withFileTypes: true });
5661
+ entries = readdirSync4(projectsDir, { withFileTypes: true });
5660
5662
  } catch {
5661
5663
  entries = [];
5662
5664
  }
5663
5665
  for (const entry of entries) {
5664
5666
  if (!entry.isDirectory())
5665
5667
  continue;
5666
- const roadmapPath = join10(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5668
+ const roadmapPath = join8(projectsDir, entry.name, PROJECT_ROADMAP_FILE);
5667
5669
  if (!existsSync13(roadmapPath))
5668
5670
  continue;
5669
5671
  try {
@@ -5733,7 +5735,7 @@ function projectRegisters(harnessDir) {
5733
5735
  return [];
5734
5736
  let entries;
5735
5737
  try {
5736
- entries = readdirSync6(projectsDir, { withFileTypes: true });
5738
+ entries = readdirSync4(projectsDir, { withFileTypes: true });
5737
5739
  } catch {
5738
5740
  return [];
5739
5741
  }
@@ -5741,7 +5743,7 @@ function projectRegisters(harnessDir) {
5741
5743
  for (const entry of entries) {
5742
5744
  if (!entry.isDirectory())
5743
5745
  continue;
5744
- const registerPath = join10(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5746
+ const registerPath = join8(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5745
5747
  if (!existsSync13(registerPath))
5746
5748
  continue;
5747
5749
  try {
@@ -5753,7 +5755,7 @@ function projectRegisters(harnessDir) {
5753
5755
  return registers;
5754
5756
  }
5755
5757
  function knowledgeDigest(harnessDir) {
5756
- const indexPath = join10(harnessDir, "knowledge", "README.md");
5758
+ const indexPath = join8(harnessDir, "knowledge", "README.md");
5757
5759
  if (!existsSync13(indexPath))
5758
5760
  return null;
5759
5761
  try {
@@ -5803,14 +5805,14 @@ function steeringCompassPath(harnessDir) {
5803
5805
  return;
5804
5806
  let entries;
5805
5807
  try {
5806
- entries = readdirSync6(iterationsDir, { withFileTypes: true });
5808
+ entries = readdirSync4(iterationsDir, { withFileTypes: true });
5807
5809
  } catch {
5808
5810
  return;
5809
5811
  }
5810
5812
  for (const entry of entries) {
5811
5813
  if (!entry.isDirectory())
5812
5814
  continue;
5813
- const compassPath = join10(iterationsDir, entry.name, "delivery-compass.md");
5815
+ const compassPath = join8(iterationsDir, entry.name, "delivery-compass.md");
5814
5816
  if (!existsSync13(compassPath))
5815
5817
  continue;
5816
5818
  let content;
@@ -5829,7 +5831,7 @@ function steeringCompassPath(harnessDir) {
5829
5831
  function iterationGateSource(harnessDir) {
5830
5832
  if (harnessDir === null)
5831
5833
  return;
5832
- const statusPath = join10(harnessDir, STATUS_FILE);
5834
+ const statusPath = join8(harnessDir, STATUS_FILE);
5833
5835
  if (!existsSync13(statusPath))
5834
5836
  return;
5835
5837
  const compass = steeringCompassPath(harnessDir);
@@ -5838,7 +5840,7 @@ function iterationGateSource(harnessDir) {
5838
5840
  const selection = resolveReadWorkflow(harnessDir);
5839
5841
  if (selection.kind === "error")
5840
5842
  return;
5841
- const snapshotPath = join10(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5843
+ const snapshotPath = join8(harnessDir, selection.dir, WORKFLOW_SNAPSHOT_FILE);
5842
5844
  try {
5843
5845
  const snapshotDoc = readJson(snapshotPath);
5844
5846
  const compassDoc = parseCompassFrontmatter(compass.compassPath);
@@ -5892,12 +5894,12 @@ function agentDigestKey(agent, cwd) {
5892
5894
  }
5893
5895
 
5894
5896
  // src/gates/status.ts
5895
- import { existsSync as existsSync14, readdirSync as readdirSync9 } from "node:fs";
5896
- import { basename as basename6, join as join14, relative as relative5, resolve as resolve12 } from "node:path";
5897
+ import { existsSync as existsSync14, readdirSync as readdirSync7 } from "node:fs";
5898
+ import { basename as basename5, join as join11, relative as relative5, resolve as resolve10 } from "node:path";
5897
5899
  var LOGGER_NAME = "mstar/status-gate";
5898
5900
  function harnessDocKindOfTarget(harnessDir, targetPath) {
5899
- const resolved = resolve12(targetPath);
5900
- const name = basename6(resolved);
5901
+ const resolved = resolve10(targetPath);
5902
+ const name = basename5(resolved);
5901
5903
  if (name !== STATUS_FILE && name !== WORKFLOW_SNAPSHOT_FILE && name !== PROJECT_REGISTER_FILE)
5902
5904
  return null;
5903
5905
  const rel = relative5(harnessDir, resolved);
@@ -5909,8 +5911,8 @@ function harnessDocKindOfTarget(harnessDir, targetPath) {
5909
5911
  workflowDir = resolveWorkflowDir(harnessDir, { harnessDir });
5910
5912
  projectDir = resolveProjectDir(harnessDir, { harnessDir });
5911
5913
  } catch {
5912
- workflowDir = join14(harnessDir, "workflows");
5913
- projectDir = join14(harnessDir, "projects");
5914
+ workflowDir = join11(harnessDir, "workflows");
5915
+ projectDir = join11(harnessDir, "projects");
5914
5916
  }
5915
5917
  if (name === WORKFLOW_SNAPSHOT_FILE && /^[^/]+\/snapshot\.json$/.test(relative5(workflowDir, resolved)))
5916
5918
  return "snapshot";
@@ -5966,7 +5968,7 @@ function projectRegisterDocs(harnessDir) {
5966
5968
  const projectsDir = resolveProjectDir(harnessDir, { harnessDir });
5967
5969
  let entries;
5968
5970
  try {
5969
- entries = readdirSync9(projectsDir, { withFileTypes: true });
5971
+ entries = readdirSync7(projectsDir, { withFileTypes: true });
5970
5972
  } catch {
5971
5973
  return [];
5972
5974
  }
@@ -5974,7 +5976,7 @@ function projectRegisterDocs(harnessDir) {
5974
5976
  for (const entry of entries) {
5975
5977
  if (!entry.isDirectory())
5976
5978
  continue;
5977
- const registerPath = join14(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5979
+ const registerPath = join11(projectsDir, entry.name, PROJECT_REGISTER_FILE);
5978
5980
  if (!existsSync14(registerPath))
5979
5981
  continue;
5980
5982
  try {
@@ -6049,7 +6051,7 @@ async function editIntentListener(ctx, resolver, config, adapter, target, actor,
6049
6051
 
6050
6052
  // src/gates/skill-lint.ts
6051
6053
  import { existsSync as existsSync15, readFileSync as readFileSync11 } from "node:fs";
6052
- import { basename as basename7, dirname as dirname9, resolve as resolve14, sep as sep2 } from "node:path";
6054
+ import { basename as basename7, dirname as dirname9, resolve as resolve13, sep as sep2 } from "node:path";
6053
6055
  var SKILL_LINT_LOGGER = "mstar/skill-lint";
6054
6056
 
6055
6057
  class SkillLintVetoError extends Error {
@@ -6112,8 +6114,8 @@ function skillRootsOf(config) {
6112
6114
  function isSkillTarget(roots, target) {
6113
6115
  if (basename7(target.displayPath) !== "SKILL.md")
6114
6116
  return false;
6115
- const resolvedPath = resolve14(target.displayPath);
6116
- return roots.some((root) => resolvedPath.startsWith(resolve14(root) + sep2));
6117
+ const resolvedPath = resolve13(target.displayPath);
6118
+ return roots.some((root) => resolvedPath.startsWith(resolve13(root) + sep2));
6117
6119
  }
6118
6120
  function skillNameOf(target) {
6119
6121
  return basename7(dirname9(target.displayPath));
@@ -6128,7 +6130,7 @@ function gateSkillIntent(ctx, harnessDir, config, target) {
6128
6130
  return;
6129
6131
  if (!isSkillTarget(roots, target))
6130
6132
  return;
6131
- const skillPath = resolve14(target.displayPath);
6133
+ const skillPath = resolve13(target.displayPath);
6132
6134
  if (!existsSync15(skillPath))
6133
6135
  return;
6134
6136
  let doc;
@@ -6180,8 +6182,8 @@ async function skillWriteIntentListener(ctx, resolver, config, target, actor, ne
6180
6182
  }
6181
6183
 
6182
6184
  // src/gates/seams.ts
6183
- import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync11 } from "node:fs";
6184
- import { basename as basename8, dirname as dirname10, join as join16, resolve as resolve15, sep as sep3 } from "node:path";
6185
+ import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync10 } from "node:fs";
6186
+ import { basename as basename8, dirname as dirname10, join as join15, resolve as resolve15, sep as sep3 } from "node:path";
6185
6187
 
6186
6188
  // ../engine/dist/audit.js
6187
6189
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
@@ -6414,7 +6416,7 @@ function rolesDirOf(path) {
6414
6416
  const idx = segments.indexOf("mstar-roles");
6415
6417
  if (idx === -1)
6416
6418
  return dirname10(path);
6417
- return idx === 0 ? sep3 : join16(sep3, ...segments.slice(1, idx + 1));
6419
+ return idx === 0 ? sep3 : join15(sep3, ...segments.slice(1, idx + 1));
6418
6420
  }
6419
6421
  function isSeamTarget(seam, harnessDir, target) {
6420
6422
  const path = resolve15(target.displayPath);
@@ -6428,7 +6430,7 @@ function isSeamTarget(seam, harnessDir, target) {
6428
6430
  case "compound": {
6429
6431
  if (harnessDir === null)
6430
6432
  return false;
6431
- return path.startsWith(resolve15(join16(harnessDir, "knowledge")) + sep3) && isMarkdownDoc(path);
6433
+ return path.startsWith(resolve15(join15(harnessDir, "knowledge")) + sep3) && isMarkdownDoc(path);
6432
6434
  }
6433
6435
  case "roles":
6434
6436
  return isRolesTarget(path);
@@ -6446,7 +6448,7 @@ function auditSecretsViolations(findings, file) {
6446
6448
  function validateDesignDoc(doc, path) {
6447
6449
  const violations = [...validateDesignTokenFrontmatter(doc).violations];
6448
6450
  const isDark = basename8(path) === "DESIGN.dark.md";
6449
- const sibling = join16(dirname10(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
6451
+ const sibling = join15(dirname10(path), isDark ? "DESIGN.md" : "DESIGN.dark.md");
6450
6452
  if (existsSync16(sibling)) {
6451
6453
  const light = isDark ? readFileSync13(sibling, "utf8") : doc;
6452
6454
  const dark = isDark ? doc : readFileSync13(sibling, "utf8");
@@ -6470,10 +6472,10 @@ function validateCompoundDoc(doc, _path, harnessDir) {
6470
6472
  function validateRolesState(rolesDir, skillsRoot = dirname10(rolesDir)) {
6471
6473
  const violations = [...validateRoleMapping(rolesDir).violations];
6472
6474
  const skillTexts = {};
6473
- for (const entry of readdirSync11(skillsRoot, { withFileTypes: true })) {
6475
+ for (const entry of readdirSync10(skillsRoot, { withFileTypes: true })) {
6474
6476
  if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
6475
6477
  continue;
6476
- const skillFile = join16(skillsRoot, entry.name, "SKILL.md");
6478
+ const skillFile = join15(skillsRoot, entry.name, "SKILL.md");
6477
6479
  if (!existsSync16(skillFile))
6478
6480
  continue;
6479
6481
  try {
@@ -52,3 +52,5 @@ Execute **`mstar-audit`** § `pr` variant end to end(SKILL.md common core:re
52
52
  Review findings that need fixing can be turned into self-contained plans for the normal Prepare → Execute flow (reusing `mstar-audit` SKILL.md **`## Plan output (all variants)`** — same contract as the `pr` variant).
53
53
 
54
54
  Output verdict + findings to the user, with the posted GitHub Review URL. Posting procedure (when a PR number exists) → **`references/pr-review.md`** § Comment posting; the main agent saves the Stage 3 report and writes/consolidates the evidence files — every seat returns evidence / findings in its **result payload** (any seat may be write-blocked); writable seats may **best-effort** write files in addition → **`references/pr-review.md`** § Local report archive. Never auto-approve or merge.
55
+
56
+ **Stage 3 envelope (mandatory):** after the tally, the main agent **must** fold the accepted findings into a `mstar.review/v1` envelope (`synthesizeReview` — or the equivalent engine call) and persist it via `mstar-harness persist review --key <plan-or-pr-id> --stdin` (or `getArtifactStore().put`). The Markdown archive is an optional human copy — **not** a substitute for the envelope. Procedure → **`references/pr-review.md`** § Review pipeline (Stage 3).
@@ -8,9 +8,11 @@ Deep PR review is a **three-stage pipeline**: collect → domain review → synt
8
8
 
9
9
  - **Stage 1 — Collect**: PM fans out lightweight read-only agents by **domain** — business domain / change surface / tech stack; use the host's lightest read-only agent (`scout` / `explorer` / `general` — whatever the host offers). Each collect seat reads the changed files in its domain plus related context and returns **evidence in its result payload** (any seat may be **write-blocked** — read-only sandbox / EPERM; the main agent extracts the payload and writes the evidence file — § Local report archive / `references/pr-review-seat-evidence.md`): `file:line` observations, potential issue surfaces, and security-surface observations (the seat carries a security lens per `security-review.md` §2/§3 **research** discipline — trace origin, never invent an attacker, never record secret values — and still records MEDIUM / unverified items as **leads** in its evidence payload; the HIGH-only filter applies to formal findings, not leads). Collect seats produce **no** findings table, compute **no** verdict, and publish **nothing**.
10
10
  - **Stage 2 — Domain review**: mstar built-in roles (`code-reviewer` / `fullstack-dev` / `frontend-dev`) split along the same domain framing, each reviewing code + security in its domain (security via the `security-review.md` lens) and producing findings with **Merge class** (§ Merge class). Each domain seat returns its findings in the **result payload** — any seat may be **write-blocked**; the main agent writes the Stage 2 evidence file (§ Local report archive / `references/pr-review-seat-evidence.md`). A large PR (>~300 changed lines, or spanning multiple change surfaces/domains) or a security-sensitive surface (auth, LLM, supply chain, data — `security-review.md` §9 extended surfaces) adds an **independent cross-domain security seat**.
11
- - **Stage 3 — Synthesis (main agent)**: the main agent (the command's orchestrator) collects all domain findings + evidence files → **dedupe** → **three-way vet** (open each cited file yourself; `file:line` must genuinely support the claim) → **tally** (§ Tally and derived score — formula unchanged) → **verdict** → report + **publish GitHub Review** (§ Comment posting — publishing authority belongs to the main agent). The main agent does not backfill uncollected / unreviewed domains — a missing domain is declared in the report under `- unverified:` / `- notes:`.
11
+ - **Stage 3 — Synthesis (main agent)**: the main agent (the command's orchestrator) collects all domain findings + evidence files → **dedupe** → **three-way vet** (open each cited file yourself; `file:line` must genuinely support the claim) → **tally** (§ Tally and derived score — formula unchanged) → **verdict** → report + **publish GitHub Review** (§ Comment posting — publishing authority belongs to the main agent). The main agent does not backfill uncollected / unreviewed domains — a missing domain is declared in the report under `- unverified:` / `- notes:`. **The envelope is mandatory**: after the tally, the main agent **must** fold the accepted findings into a `mstar.review/v1` envelope (`synthesizeReview` — or the equivalent engine call) and persist it via `mstar-harness persist review --key <plan-or-pr-id> --stdin` (or `getArtifactStore().put`); the Markdown archive is an optional human copy, **not** a substitute for the envelope.
12
12
  - A domain whose seat returned **no evidence** (crashed / Blocked / empty output) is an **uncollected domain**, declared the same way under `- unverified:` / `- notes:`.
13
13
 
14
+ > **Engine check (when available):** run `mstar-harness persist review --key <plan-or-pr-id> --stdin` (or `import { synthesizeReview, validateMstarReviewV1 } from "@mstar-harness/engine"` in a host hook) to fold the accepted findings into the `mstar.review/v1` envelope and persist it — `synthesizeReview` derives verdict/tally from `computePrTally` (pure, no I/O), and `kind: review` runs `validateMstarReviewV1` before put, refusing invalid envelopes (exit 1, nothing written). On `fail` -> do not proceed; fix and re-run. Skill text below remains authoritative when the runtime is absent.
15
+
14
16
  **Scale-driven fan-out** (reuses the existing sizing bands — no new thresholds): Stage 1 collect seats scale with PR size; the extra security seat stays in Stage 2:
15
17
 
16
18
  | Size | Stage 1 collect seats | Extra Stage 2 |
@@ -40,6 +40,8 @@ description: Morning Star 派发与委派门禁 —— 仅 PM 可增派 subagent
40
40
 
41
41
  **Assignment 顶部反模式块**:每个 PM Assignment 开头均有 **`**You are a leaf executor. You MUST NOT:**`** 块(含 IDENTITY + CAPABILITY BOUNDARY + prohibitions),PM 按此 Assignment 的角色+上下文定制反模式清单。leaf executor 收到 Assignment 后须 **首先** 阅读该块;命中任一条 → **停止**(亲自完成或 `Blocked`)。详见 **`mstar-roles/references/project-manager/dispatch-and-assignment.md`**。
42
42
 
43
+ > **Engine 执行范围(caller-scoped,#156)**:engine `antiRecursionPrecheck` 比较的是**派发方自身角色**(caller)与新 Assignment 的 `Execute as`(target)。只有 **dsh**(Config `dispatchBinding`)能观察派发方身份并在 engine 层硬执行(含 `callerRequired` 空绑定 fail-closed);omp / OpenCode / Cursor 的角色绑定字段是**派发目标**——目标 == `Execute as` 正是 C5 合规派发模式——这些宿主上红线保持 prompt 级约束(本节),engine 不做判定。
44
+
43
45
  ## 调度防串扰(强制;leaf executor 已在上方读过反递归红线,此处为完整规则供 PM/对照用)
44
46
 
45
47
  - 只有 **`project-manager`** 可以决定增加/并行 subagent;承接方**默认不得二次分派**。
@@ -58,7 +60,7 @@ description: Morning Star 派发与委派门禁 —— 仅 PM 可增派 subagent
58
60
  - **QC 单席(例外)**:`Execution mode: inline`(hotfix 等),或 Assignment 显式 `QC mode: single` / `QC mode: single — override: <reason>` → `qc-specialist` ×1,`N=1`,写 `{SDD_DIR}/review/qc.md`。
59
61
  - **QC targeted re-review**:Assignment 含 **`QC re-review: targeted — reviewers: …`** 时,**N** = 所列席位数(1–3),同条消息发满 **N**。
60
62
  - **先自检再发送**:发送前核对「Assignment 条数 = 本条消息中的实际 **派发** 调用条数」。
61
- - **先自检字段再发送(与 count 同级门禁)**:核对**每条** invoke 都携带与 **`Execute as`** 匹配的角色绑定字段——omp **`agent`** / Cursor **`subagent_type`** / OpenCode **`subagent`** / Kimi·ZCode **`subagent_type`**。**漏写或取默认通用值**(omp 漏 `agent` ⇒ 自动回退 generic `task`,无报错)= **派发未完成**,与 paste-only(零 invoke)**同等级**:当场补齐重发,不得进入下一 gate。**N=1 顺序链(Review & Edit)不豁免**——count 门在 N=1 恒过,**字段门是唯一保护**。
63
+ - **先自检字段再发送(与 count 同级门禁)**:核对**每条** invoke 都携带与 **`Execute as`** 匹配的角色绑定字段——omp **`agent`** / Cursor **`subagent_type`** / OpenCode **`subagent`** / Kimi·ZCode **`subagent_type`**;宿主列以 **`mstar-host`** §Detect active host 的 tool-shape 检测为准(禁以 config 路径/仓库内容判定)。**漏写或取默认通用值**(omp 漏 `agent` ⇒ 自动回退 generic `task`,无报错)= **派发未完成**,与 paste-only(零 invoke)**同等级**:当场补齐重发,不得进入下一 gate。**N=1 顺序链(Review & Edit)不豁免**——count 门在 N=1 恒过,**字段门是唯一保护**。
62
64
  - **前置步骤与派发回合分离(防串行 rollout)**:为派发准备的 **`bash` / `read` / `glob` / `grep`**(如 `merge-base`、`Review range`、`git rev-parse`)**不计入** `N` 次派发;可在上一条仅含准备的消息完成。准备完成后,**下一条派发消息**须**一次性**含 **`N` 次** Task / subagent invoke。**禁止**先发 `1` 次、等返回再补发其余 `N-1` 次。
63
65
  - **未齐不发(emit zero until batch-ready)**:需并发 `N≥2` 而当前只能发 `1` 条时,本条应发 **`0` 条派发 invoke`**(可继续 read/bash 补齐),**禁止**「先发一个顶一下」;`N` 份 payload 就绪后**单次消息发满 `N`**。见 **`mstar-host`** → `references/parallel-dispatch.md`(具备 invoke / Task / subagent 工具的宿主共用)。
64
66
 
@@ -472,7 +472,9 @@ The dsh web client resolves slash commands against a client-side lexicon driven
472
472
  - The dispatch gate needs the dispatching agent's own role for the
473
473
  anti-recursion precheck: declare it via Config **`dispatchBinding`** (dsh
474
474
  exposes no per-agent role on the tool-execution context). Under hard
475
- enforcement with no binding, the plugin logs the absence.
475
+ enforcement with no binding, the plugin logs the absence AND every
476
+ Assignment-shaped dispatch fails closed (`dispatch.anti-recursion.
477
+ empty-binding` → deny) until the binding is set.
476
478
 
477
479
  ## Files, shell, and approvals
478
480
 
@@ -211,7 +211,8 @@ Cannot emit required **N** → **`Blocked`**.
211
211
  ## In-process engine binding (omp ≥ 17.2.11)
212
212
 
213
213
  - **Surfaces** (repo root = plugin root): `hooks/pre/mstar-gates.ts` — one `tool_call` pre-hook that returns `{ block: true, reason }` (structured refusal the model sees as the tool error) or `undefined` (pass); `tools/mstar_{status_validate,dispatch_validate,lease_verify,path_resolve,iteration_gate,worktree_check}/index.ts` — six model-callable validator tools (engine validators only, Zod params via `pi.zod`).
214
- |- **Enforcement semantics**: block ONLY under `Enforcement: hard`. The status gate reads the repo `.mstarc` `[config] enforcement`, else the harness compass frontmatter (`enforcement: hard`, active/locked iterations only); the dispatch gate reads each Assignment's own header flag (`assignmentHeaderRegion` — a body example never hardens). Soft / no flag silent pass. Rollback = unset the flag (or `.mstarc` `soft`). Never global.
214
+ |- **Enforcement semantics**: block ONLY under `Enforcement: hard`. Both gates read the repo `.mstarc` `[config] enforcement`, else the harness compass frontmatter (`enforcement: hard`, active/locked iterations only); the dispatch gate ALSO honors each Assignment's own header flag (`assignmentHeaderRegion` — a body example never hardens). A hard repo setting therefore hardens flag-less dispatches (Gate 1 / dsh `resolveDispatchHard` parity). Soft-mode dispatch violations are warn-logged through the extension logger (never blocked); soft status-write violations stay a silent pass. Rollback = unset the flag (or `.mstarc` `soft`). Never global.
215
+ - **Anti-recursion scope (issue #156)**: the engine's `antiRecursionPrecheck` is **caller-scoped** — it compares the DISPATCHING agent's own role against the new Assignment's `Execute as`. omp's `tool_call` event carries no caller identity and the task entry `agent` is the spawn TARGET, which equals `Execute as` on every compliant dispatch (C5 above) — so Gate 2 does NOT run the precheck on omp (the pre-#156 wiring hard-blocked every compliant hard-mode dispatch on `self-type`, or on `empty-binding` when `agent` was omitted). The NEVER red line stays prompt-level on this host (`mstar-dispatch-gates`); dsh enforces it in-engine via Config `dispatchBinding`.
215
216
  - **Engine dependency**: the adapters import the published engine package (root `package.json` `dependencies` entry). omp git/npm plugin installs run `bun install <spec>` in the plugins tree → declared deps installed; a bare `-l` / `omp plugin link` symlink install without `node_modules` cannot resolve the modules.
216
217
  - **Graceful degradation (explicit)**: module load failure → `mstar_*` tools skipped, hook absent (no blocking), `commands/*.md` shell-out fallback intact. Caveat: a partial failure is SILENT — no in-band signal that gates are off; verify with `omp -p '/extensions'`.
217
218
  |- **`MSTAR_HARNESS_DIR` override / `.mstarc`**: the hook and tools discover `{HARNESS_DIR}` via `resolveHarnessDir` — a repo `.mstarc` `[config] harness_dir` (gitignored local config) first, then the probe `.mstar/` → `.agents/` → `.plans/`/`plans/`. Repos using a non-standard harness root can declare it in `.mstarc` or MUST export `MSTAR_HARNESS_DIR` (absolute path) in the omp session env — without either the status gate does not cover those roots and tools like `mstar_path_resolve` / `mstar_lease_verify` error out (parity with the opencode binding).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/dsh",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Morning Star harness dsh (DeepSeek Harness) cordis function plugin — in-process engine gates (status/dispatch/lease) with hard refusal channels.",
5
5
  "keywords": [
6
6
  "dsh",