@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 +1 -1
- package/README.zh.md +1 -1
- package/bundle/README.md +1 -1
- package/dist/index.js +148 -146
- package/harness-commands/amazing-pr-review.md +2 -0
- package/harness-skills/mstar-audit/references/pr-review.md +3 -1
- package/harness-skills/mstar-dispatch-gates/SKILL.md +3 -1
- package/harness-skills/mstar-host/references/dsh.md +3 -1
- package/harness-skills/mstar-host/references/omp.md +2 -1
- package/package.json +1 -1
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
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1132
|
-
import { dirname as dirname4, join as
|
|
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
|
|
1135
|
-
import { isAbsolute as
|
|
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
|
|
1138
|
-
import { basename as
|
|
1139
|
-
import { existsSync as
|
|
1140
|
-
import { basename as
|
|
1141
|
-
import { existsSync as
|
|
1142
|
-
import { join as
|
|
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
|
|
1845
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
2216
|
-
const snapshotPath =
|
|
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 (!
|
|
2254
|
+
if (!existsSync4(iterationsDir))
|
|
2253
2255
|
return { hard: false, source: "none" };
|
|
2254
2256
|
let entries;
|
|
2255
2257
|
try {
|
|
2256
|
-
entries =
|
|
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 =
|
|
2264
|
-
if (!
|
|
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 =
|
|
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 =
|
|
2397
|
+
const start = resolve7(startDir);
|
|
2396
2398
|
const explicit = opts.harnessDir ?? process.env.MSTAR_HARNESS_DIR;
|
|
2397
2399
|
if (explicit)
|
|
2398
|
-
return
|
|
2399
|
-
const boundary =
|
|
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
|
|
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 [
|
|
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
|
|
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("..") && !
|
|
2441
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
2440
2442
|
}
|
|
2441
2443
|
function mstarcDirOverride(harnessDir, key) {
|
|
2442
|
-
const dir =
|
|
2444
|
+
const dir = resolve7(harnessDir);
|
|
2443
2445
|
const rc = loadMstarc(dir, dirname5(dir));
|
|
2444
2446
|
const declared = rc?.config[key];
|
|
2445
|
-
return declared ?
|
|
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 =
|
|
2456
|
+
const base = resolve7(harnessDir);
|
|
2455
2457
|
const declared = mstarcDirOverride(base, "sddDir");
|
|
2456
|
-
const sddBase = declared !== null ? declared :
|
|
2457
|
-
return
|
|
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
|
|
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 ${
|
|
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 :
|
|
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 !== "" &&
|
|
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 !== "" && !
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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(
|
|
2648
|
-
return
|
|
2649
|
-
if (isFile2(
|
|
2650
|
-
return
|
|
2651
|
-
if (hasWorkflowSnapshot(
|
|
2652
|
-
return
|
|
2653
|
-
if (hasWorkflowSnapshot(
|
|
2654
|
-
return
|
|
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 =
|
|
2664
|
+
workflowsDir = join10(harnessDir, "workflows");
|
|
2663
2665
|
}
|
|
2664
2666
|
if (!isDirectory2(workflowsDir))
|
|
2665
2667
|
return false;
|
|
2666
2668
|
try {
|
|
2667
|
-
for (const entry of
|
|
2668
|
-
if (entry.isDirectory() && isFile2(
|
|
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 =
|
|
2682
|
-
const common =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2713
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
2730
|
-
harnessDir =
|
|
2731
|
-
} else if (isDirectory2(
|
|
2732
|
-
harnessDir =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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("~") ||
|
|
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 =
|
|
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(
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3592
|
-
if (!
|
|
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(
|
|
3618
|
+
return /^[^.]*\.[A-Za-z0-9]{1,10}$/.test(basename6(root));
|
|
3617
3619
|
}
|
|
3618
3620
|
function scopeGuard(path, allowedRoots) {
|
|
3619
|
-
const resolved =
|
|
3621
|
+
const resolved = resolve12(path);
|
|
3620
3622
|
for (const root of allowedRoots) {
|
|
3621
|
-
const r =
|
|
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 (!
|
|
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 ${
|
|
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
|
|
3994
|
-
import { resolve as
|
|
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 (
|
|
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 (
|
|
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() !== "" ?
|
|
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 =
|
|
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
|
|
4136
|
-
import { join as
|
|
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
|
|
4141
|
-
import { join as
|
|
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
|
|
4145
|
-
import { basename as
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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, {
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
5288
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5896
|
-
import { basename as
|
|
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 =
|
|
5900
|
-
const name =
|
|
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 =
|
|
5913
|
-
projectDir =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
6116
|
-
return roots.some((root) => resolvedPath.startsWith(
|
|
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 =
|
|
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
|
|
6184
|
-
import { basename as basename8, dirname as dirname10, join as
|
|
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 :
|
|
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(
|
|
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 =
|
|
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
|
|
6475
|
+
for (const entry of readdirSync10(skillsRoot, { withFileTypes: true })) {
|
|
6474
6476
|
if (!entry.isDirectory() || !entry.name.startsWith("mstar-"))
|
|
6475
6477
|
continue;
|
|
6476
|
-
const skillFile =
|
|
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
|
|
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`.
|
|
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