@mstar-harness/opencode 3.1.1 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/mstar.js +54 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ The monorepo root [CHANGELOG.md](../../CHANGELOG.md) summarizes cross-surface re
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [3.1.2] - 2026-08-21
|
|
10
|
+
|
|
11
|
+
### Harness
|
|
12
|
+
|
|
13
|
+
- **Anti-recursion fails closed on an empty host binding**: `dispatch.antiRecursionPrecheck` now returns a **critical** `dispatch.anti-recursion.empty-binding` violation when the host role-binding field (`omp task entry agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) is empty, omitted, or whitespace-only — the host cannot prove the dispatching agent is not recursing, so the dispatch must not proceed as if the NEVER red line held. `composeDispatchGate` no longer skips the precheck for empty bindings (the `if (agent !== "")` skip is removed); the precheck is the single decision point and runs for every Assignment-shaped text, including read-only (scout/explore) Assignments (no carve-out). A set binding equal to `Execute as` keeps the existing critical `dispatch.anti-recursion.self-type`; a set binding with an empty `Execute as` stays ok on the anti-recursion leg (field presence remains `validateAssignmentFields`' job).
|
|
14
|
+
- **OpenCode surface**: `validateDispatchAssignment` (via `composeDispatchGate`) now warns `dispatch.anti-recursion.empty-binding` at critical severity when the task tool carries no `subagent` / `subagent_type` key; under the Assignment's own `**Enforcement**: hard` the empty binding hard-blocks (`hardBlocked: true`). No `src` change — the hook already flows the default `""` binding through the engine composition.
|
|
15
|
+
- **dsh surface**: `dispatchGateCore` passes `config.dispatchBinding ?? ''` into the engine composition, so an unset binding now emits `dispatch.anti-recursion.empty-binding` (critical) on every Assignment-shaped dispatch — advisory in warn mode, `PreToolDecision { kind: 'deny' }` under hard. The boot-time warn string no longer claims the precheck is "skipped": an unset `dispatchBinding` under hard enforcement now fails closed until the binding is set. The Zod `dispatchBinding` schema is untouched.
|
|
16
|
+
|
|
17
|
+
- Version alignment with harness **3.1.2** (no OpenCode package API change).
|
|
18
|
+
|
|
19
|
+
See root [CHANGELOG.md](../../CHANGELOG.md) **3.1.2**.
|
|
20
|
+
|
|
9
21
|
## [3.1.1] - 2026-08-20
|
|
10
22
|
|
|
11
23
|
### Bundled harness skills (`harness-skills/` at publish)
|
package/dist/mstar.js
CHANGED
|
@@ -162,10 +162,10 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
162
162
|
import { fileURLToPath } from "node:url";
|
|
163
163
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
164
164
|
import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
165
|
-
import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
165
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, realpathSync, statSync as statSync2 } from "node:fs";
|
|
166
166
|
import { execFileSync } from "node:child_process";
|
|
167
167
|
import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
168
|
-
import { existsSync as
|
|
168
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
169
169
|
import { dirname as dirname5, join as join6, resolve as resolve5, sep } from "node:path";
|
|
170
170
|
import { mkdirSync as mkdirSync3, rmdirSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
171
171
|
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join4, resolve as resolve4 } from "node:path";
|
|
@@ -174,22 +174,22 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
174
174
|
import { mkdirSync as mkdirSync4 } from "node:fs";
|
|
175
175
|
import { join as join5 } from "node:path";
|
|
176
176
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
177
|
-
import { existsSync as
|
|
177
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
178
178
|
import { isAbsolute as isAbsolute4, resolve as resolve6 } from "node:path";
|
|
179
179
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
180
|
-
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as
|
|
180
|
+
import { mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
181
181
|
import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve7 } from "node:path";
|
|
182
|
-
import { existsSync as
|
|
182
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
183
183
|
import { join as join8 } from "node:path";
|
|
184
|
-
import { existsSync as
|
|
184
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
185
185
|
import { join as join9 } from "node:path";
|
|
186
186
|
import { copyFileSync, mkdirSync as mkdirSync6, readFileSync as readFileSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
187
187
|
import { dirname as dirname7, isAbsolute as isAbsolute6, join as join10, relative as relative3, resolve as resolve8, sep as sep2 } from "node:path";
|
|
188
188
|
import { mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
189
189
|
import { join as join11, resolve as resolve9 } from "node:path";
|
|
190
|
-
import { existsSync as
|
|
190
|
+
import { existsSync as existsSync7, readdirSync as readdirSync8, readFileSync as readFileSync10 } from "node:fs";
|
|
191
191
|
import { basename as basename4, isAbsolute as isAbsolute7, join as join12, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
|
|
192
|
-
import { existsSync as
|
|
192
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
193
193
|
import { join as join13 } from "node:path";
|
|
194
194
|
function applyEnforcement(gate, opts) {
|
|
195
195
|
return { ...gate, hardBlocked: opts.hard && gate.violations.length > 0 };
|
|
@@ -552,6 +552,23 @@ function assertPlanWritingPath(planPath, harnessDir) {
|
|
|
552
552
|
fix: `write the plan under ${planDir}`
|
|
553
553
|
};
|
|
554
554
|
}
|
|
555
|
+
if (existsSync2(planAbs)) {
|
|
556
|
+
try {
|
|
557
|
+
const canonicalPlan = realpathSync(planAbs);
|
|
558
|
+
const canonicalPlanDir = existsSync2(planDir) ? realpathSync(planDir) : resolve3(planDir);
|
|
559
|
+
const canonicalRel = relative2(canonicalPlanDir, canonicalPlan);
|
|
560
|
+
const canonicalInside = canonicalRel === "" || !canonicalRel.startsWith("..") && !isAbsolute2(canonicalRel);
|
|
561
|
+
if (!canonicalInside) {
|
|
562
|
+
return {
|
|
563
|
+
ok: false,
|
|
564
|
+
severity: "high",
|
|
565
|
+
code: "plan-path.symlink-escape",
|
|
566
|
+
message: `plan file ${planAbs} resolves to ${canonicalPlan}, outside {PLAN_DIR} (${canonicalPlanDir})`,
|
|
567
|
+
fix: `write the plan under ${planDir}`
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
555
572
|
return {
|
|
556
573
|
ok: true,
|
|
557
574
|
severity: "low",
|
|
@@ -1008,10 +1025,8 @@ function composeDispatchGate(text, opts = {}) {
|
|
|
1008
1025
|
const violations = [];
|
|
1009
1026
|
const writable = opts.writable !== false;
|
|
1010
1027
|
violations.push(...validateAssignmentFields(text, { writable }).violations);
|
|
1011
|
-
const agent =
|
|
1012
|
-
|
|
1013
|
-
violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
|
|
1014
|
-
}
|
|
1028
|
+
const agent = opts.agent ?? "";
|
|
1029
|
+
violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
|
|
1015
1030
|
if (writable) {
|
|
1016
1031
|
const forms = parseAssignmentBranchForms(text);
|
|
1017
1032
|
const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
|
|
@@ -1030,7 +1045,15 @@ function composeDispatchGate(text, opts = {}) {
|
|
|
1030
1045
|
function antiRecursionPrecheck(subagentType, executeAs) {
|
|
1031
1046
|
const binding = subagentType.trim().toLowerCase();
|
|
1032
1047
|
const role = executeAs.trim().toLowerCase();
|
|
1033
|
-
if (binding
|
|
1048
|
+
if (binding === "") {
|
|
1049
|
+
return {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
violations: [
|
|
1052
|
+
violation2("critical", "dispatch.anti-recursion.empty-binding", `empty host role binding — the host cannot report which agent is calling, so anti-recursion cannot be proven (a dispatch could silently recurse)`, "set the host role-binding field (omp task entry `agent` / opencode `subagent` / cursor `subagent_type` / dsh `dispatchBinding`) before dispatching")
|
|
1053
|
+
]
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (binding === role) {
|
|
1034
1057
|
return {
|
|
1035
1058
|
ok: false,
|
|
1036
1059
|
violations: [
|
|
@@ -1378,7 +1401,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1378
1401
|
if (harnessDir !== undefined && Array.isArray(doc.workflows)) {
|
|
1379
1402
|
let realHarnessDir = null;
|
|
1380
1403
|
try {
|
|
1381
|
-
realHarnessDir =
|
|
1404
|
+
realHarnessDir = realpathSync2(harnessDir);
|
|
1382
1405
|
} catch {}
|
|
1383
1406
|
for (const entry of doc.workflows) {
|
|
1384
1407
|
if (!isPlainObject3(entry) || typeof entry.dir !== "string")
|
|
@@ -1388,7 +1411,7 @@ function validateStatusV2(docOrPath, opts = {}) {
|
|
|
1388
1411
|
const label = typeof entry.id === "string" ? entry.id : relSnapshot;
|
|
1389
1412
|
let physical;
|
|
1390
1413
|
try {
|
|
1391
|
-
physical =
|
|
1414
|
+
physical = realpathSync2(snapshotPath);
|
|
1392
1415
|
} catch {
|
|
1393
1416
|
violations.push(violation4("high", "status.workflow.snapshot-missing", `workflows[] lists ${JSON.stringify(label)} but its snapshot does not exist at ${JSON.stringify(relSnapshot)} — the root holds active lifecycles only; unregister the id when its snapshot is removed`));
|
|
1394
1417
|
continue;
|
|
@@ -1477,7 +1500,7 @@ async function unregisterWorkflow(root, id) {
|
|
|
1477
1500
|
}
|
|
1478
1501
|
function resolveCompassEnforcement(harnessDir) {
|
|
1479
1502
|
const iterationsDir = resolveIterationDir(harnessDir);
|
|
1480
|
-
if (!
|
|
1503
|
+
if (!existsSync3(iterationsDir))
|
|
1481
1504
|
return { hard: false, source: "none" };
|
|
1482
1505
|
let entries;
|
|
1483
1506
|
try {
|
|
@@ -1489,7 +1512,7 @@ function resolveCompassEnforcement(harnessDir) {
|
|
|
1489
1512
|
if (!entry.isDirectory())
|
|
1490
1513
|
continue;
|
|
1491
1514
|
const compassPath = join6(iterationsDir, entry.name, "delivery-compass.md");
|
|
1492
|
-
if (!
|
|
1515
|
+
if (!existsSync3(compassPath))
|
|
1493
1516
|
continue;
|
|
1494
1517
|
let content;
|
|
1495
1518
|
try {
|
|
@@ -1575,7 +1598,7 @@ function l1PreDispatchCheck(input, opts = {}) {
|
|
|
1575
1598
|
if (controlWorktreePath !== "" && leaseWorktreePath !== "" && resolve6(controlWorktreePath) === resolve6(leaseWorktreePath)) {
|
|
1576
1599
|
violations.push(violation5("critical", "worktree.l1.lease-equals-control", `execution_lease.worktree_path "${leaseWorktreePath}" equals metadata.control_worktree_path — the feature worktree MUST differ from the control worktree (L1 isolation; product edits never land in the control checkout)`, "use a distinct feature worktree for the plan (git worktree add <path> <branch>) and update the lease"));
|
|
1577
1600
|
}
|
|
1578
|
-
if (leaseWorktreePath !== "" && !
|
|
1601
|
+
if (leaseWorktreePath !== "" && !existsSync4(leaseWorktreePath)) {
|
|
1579
1602
|
violations.push(violation5("high", "worktree.l1.feature-missing", `feature worktree directory "${leaseWorktreePath}" does not exist for plan "${planId}"`, `create it before dispatch: git worktree add ${leaseWorktreePath} <working-branch>`));
|
|
1580
1603
|
} else if (leaseWorktreePath !== "" && leaseWorkingBranch !== "") {
|
|
1581
1604
|
const probe = probeBranch(leaseWorktreePath, opts);
|
|
@@ -1609,7 +1632,7 @@ function l2PreDispatchCheck(input, opts = {}) {
|
|
|
1609
1632
|
return;
|
|
1610
1633
|
}
|
|
1611
1634
|
seenPaths.add(normalized);
|
|
1612
|
-
if (!
|
|
1635
|
+
if (!existsSync4(track.worktreePath)) {
|
|
1613
1636
|
violations.push(violation5("high", "worktree.l2.track-missing", `track worktree directory "${track.worktreePath}" does not exist`, `create it before dispatch: git worktree add ${track.worktreePath} ${track.workingBranch}`));
|
|
1614
1637
|
return;
|
|
1615
1638
|
}
|
|
@@ -1731,8 +1754,8 @@ function isLinkedWorktree(root) {
|
|
|
1731
1754
|
if (gitDir.includes("/.git/worktrees/") || gitDir.includes("/worktrees/"))
|
|
1732
1755
|
return true;
|
|
1733
1756
|
try {
|
|
1734
|
-
const gdParent =
|
|
1735
|
-
const cmAbs =
|
|
1757
|
+
const gdParent = realpathSync3(dirname6(gitDir));
|
|
1758
|
+
const cmAbs = realpathSync3(common);
|
|
1736
1759
|
return join7(gdParent, basename3(gitDir)) !== cmAbs && gitDir !== cmAbs;
|
|
1737
1760
|
} catch {
|
|
1738
1761
|
return false;
|
|
@@ -1750,10 +1773,10 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
1750
1773
|
if (!isDirectory2(controlRoot)) {
|
|
1751
1774
|
throw new SddScriptError(`mstar sdd workspace: CONTROL_ROOT / MSTAR_CONTROL_ROOT is not a directory: ${controlRoot}`, 1);
|
|
1752
1775
|
}
|
|
1753
|
-
root =
|
|
1776
|
+
root = realpathSync3(controlRoot);
|
|
1754
1777
|
} else {
|
|
1755
1778
|
const topLevel = gitOut(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1756
|
-
root =
|
|
1779
|
+
root = realpathSync3(topLevel ?? cwd);
|
|
1757
1780
|
}
|
|
1758
1781
|
if (!controlRoot && isLinkedWorktree(root)) {
|
|
1759
1782
|
throw new SddScriptError(`mstar sdd workspace: linked worktree at ${root} has no {HARNESS_DIR}/status.json (default gitignore).
|
|
@@ -1787,7 +1810,7 @@ function sddWorkspace(planId, opts = {}) {
|
|
|
1787
1810
|
mkdirSync5(sddDir, { recursive: true });
|
|
1788
1811
|
writeFileSync3(join7(sddDir, ".gitignore"), `*
|
|
1789
1812
|
`);
|
|
1790
|
-
return
|
|
1813
|
+
return realpathSync3(sddDir);
|
|
1791
1814
|
}
|
|
1792
1815
|
function taskBrief(planFile, taskN, outFile, opts = {}) {
|
|
1793
1816
|
if (!planFile || !Number.isInteger(taskN) || taskN < 1) {
|
|
@@ -2159,7 +2182,7 @@ function pushCadenceProbe(ciRunning, reviewWaveActive) {
|
|
|
2159
2182
|
return { ok: violations.length === 0, violations };
|
|
2160
2183
|
}
|
|
2161
2184
|
function assertIndexRowObligations(iterationsDir) {
|
|
2162
|
-
if (!
|
|
2185
|
+
if (!existsSync5(iterationsDir)) {
|
|
2163
2186
|
return {
|
|
2164
2187
|
ok: false,
|
|
2165
2188
|
violations: [
|
|
@@ -2167,9 +2190,9 @@ function assertIndexRowObligations(iterationsDir) {
|
|
|
2167
2190
|
]
|
|
2168
2191
|
};
|
|
2169
2192
|
}
|
|
2170
|
-
const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) =>
|
|
2193
|
+
const iterationIds = readdirSync4(iterationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => existsSync5(join8(iterationsDir, entry.name, COMPASS_FILE))).map((entry) => entry.name).sort();
|
|
2171
2194
|
const readmePath = join8(iterationsDir, INDEX_README);
|
|
2172
|
-
if (!
|
|
2195
|
+
if (!existsSync5(readmePath)) {
|
|
2173
2196
|
return {
|
|
2174
2197
|
ok: false,
|
|
2175
2198
|
violations: [
|
|
@@ -2434,7 +2457,7 @@ function techDebtRollup(projectDir) {
|
|
|
2434
2457
|
if (!project.isDirectory())
|
|
2435
2458
|
continue;
|
|
2436
2459
|
const registerPath = join9(projectDir, project.name, PROJECT_REGISTER_FILE);
|
|
2437
|
-
if (!
|
|
2460
|
+
if (!existsSync6(registerPath))
|
|
2438
2461
|
continue;
|
|
2439
2462
|
let register;
|
|
2440
2463
|
try {
|
|
@@ -3852,7 +3875,7 @@ function referenceExists(repoRoot, docText) {
|
|
|
3852
3875
|
for (const { ref, isSymbol, module } of refs) {
|
|
3853
3876
|
if (!isSymbol || module === undefined) {
|
|
3854
3877
|
const candidate = ref.replace(LINE_SUFFIX_RE, "").replace(ANCHOR_RE, "");
|
|
3855
|
-
if (
|
|
3878
|
+
if (existsSync7(resolve10(repoRoot, candidate))) {
|
|
3856
3879
|
checked++;
|
|
3857
3880
|
} else {
|
|
3858
3881
|
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"));
|
|
@@ -3900,7 +3923,7 @@ function normalizeIndexRef(cell) {
|
|
|
3900
3923
|
function assertIndexRows(knowledgeDir) {
|
|
3901
3924
|
const violations = [];
|
|
3902
3925
|
const readmePath = join12(knowledgeDir, "README.md");
|
|
3903
|
-
if (!
|
|
3926
|
+
if (!existsSync7(readmePath)) {
|
|
3904
3927
|
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"));
|
|
3905
3928
|
return { ok: false, violations };
|
|
3906
3929
|
}
|
|
@@ -4163,7 +4186,7 @@ function validateRoleMapping(rolesDir, options = {}) {
|
|
|
4163
4186
|
const violations = [];
|
|
4164
4187
|
const referenceById = new Map(mapping.map((m) => [m.agentId, m.reference]));
|
|
4165
4188
|
for (const { agentId, reference } of mapping) {
|
|
4166
|
-
if (!
|
|
4189
|
+
if (!existsSync8(join13(rolesDir, reference))) {
|
|
4167
4190
|
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`));
|
|
4168
4191
|
}
|
|
4169
4192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mstar-harness/opencode",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Morning Star harness OpenCode plugin — skills bootstrap + engine-backed runtime hooks (status lint, dispatch validation, Enforcement: hard gates).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|