@mstar-harness/engine 3.4.0 → 3.4.1

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.
@@ -172,13 +172,24 @@ export declare function assertTriIdentity(reviewerRoles: readonly string[]): Gat
172
172
  */
173
173
  export type ComposeDispatchGateOptions = {
174
174
  /**
175
- * Host role-binding field (omp task entry `agent` / opencode `subagent` /
176
- * cursor `subagent_type` / dsh `dispatchBinding`). The anti-recursion
177
- * precheck runs for EVERY Assignment-shaped text: an empty or omitted
178
- * binding fails closed (`dispatch.anti-recursion.empty-binding`) the
179
- * host cannot prove the dispatching agent is not recursing.
175
+ * Dispatching agent's OWN harness role (dsh `Config.dispatchBinding`).
176
+ * When non-empty, the anti-recursion precheck compares it against the
177
+ * Assignment's `Execute as` equality is self-recursion
178
+ * (`dispatch.anti-recursion.self-type`, critical). Leave unset on hosts
179
+ * whose tool-call event cannot report the dispatching agent's identity
180
+ * (omp/opencode/cursor): the precheck is skipped there and the NEVER red
181
+ * line stays prompt-level (mstar-dispatch-gates).
180
182
  */
181
- agent?: string;
183
+ caller?: string;
184
+ /**
185
+ * True on hosts whose contract declares the caller binding mandatory
186
+ * (dsh): an empty/missing `caller` then fails closed with
187
+ * `dispatch.anti-recursion.empty-binding` (critical) — the host could
188
+ * have declared the binding, so an absent one proves nothing and the
189
+ * dispatch must not proceed as if the NEVER red line held. Default
190
+ * `false`: the precheck is skipped entirely when `caller` is empty.
191
+ */
192
+ callerRequired?: boolean;
182
193
  /**
183
194
  * `false` for read-only roles (scout/explore) — skips the branch-form and
184
195
  * default-branch gates. Default: `true` (writable).
@@ -206,10 +217,14 @@ export type ComposeDispatchGateResult = GateResult & {
206
217
  * Assignment-shaped passes silently (`shaped: false`).
207
218
  * 2. `validateAssignmentFields` with `writable: false` when `opts.writable
208
219
  * === false` (read-only roles), else the writable default.
209
- * 3. Anti-recursion precheck — runs for every Assignment-shaped text; an
210
- * empty/omitted host binding fails closed (`dispatch.anti-recursion.
211
- * empty-binding`), a binding equal to `Execute as` is the existing
212
- * `dispatch.anti-recursion.self-type` critical.
220
+ * 3. Anti-recursion precheck — CALLER semantics (issue #156): runs only
221
+ * when the host supplies its own role binding (`caller`), or fails
222
+ * closed on an empty one when the host contract requires it
223
+ * (`callerRequired`, dsh). A caller equal to `Execute as` is the
224
+ * `dispatch.anti-recursion.self-type` critical; a required-but-empty
225
+ * caller is `dispatch.anti-recursion.empty-binding`. Target-only hosts
226
+ * (omp/opencode/cursor) skip the leg — their binding field carries the
227
+ * spawn TARGET, which equals `Execute as` on every compliant dispatch.
213
228
  * 4. Default-branch gate for writable text: the branch comes from the
214
229
  * Assignment's own branch forms (create-form name / Working branch /
215
230
  * Branch policy branch), else `$MSTAR_WORKING_BRANCH`; a well-formed
@@ -224,13 +239,17 @@ export type ComposeDispatchGateResult = GateResult & {
224
239
  export declare function composeDispatchGate(text: string, opts?: ComposeDispatchGateOptions): ComposeDispatchGateResult;
225
240
  /**
226
241
  * Anti-recursion precheck (NEVER red line, mstar-dispatch-gates § 承接方反递归
227
- * 红线): a leaf executor MUST NOT invoke a Task/subagent whose role-binding
228
- * field (`subagent_type` / `agent` / `subagent`) equals its own `Execute as`.
229
- * Comparison is case-insensitive after trim. An EMPTY binding fails closed
230
- * (`dispatch.anti-recursion.empty-binding`, critical): the host cannot
231
- * report which agent is calling, so anti-recursion cannot be proven the
232
- * dispatch must not proceed as if the NEVER red line held. An empty
233
- * `executeAs` with a set binding stays ok (field presence is
234
- * `validateAssignmentFields`' job, not this precheck).
242
+ * 红线): a leaf executor MUST NOT dispatch a Task/subagent whose target role
243
+ * (the new Assignment's `Execute as`) equals its OWN role. `subagentType`
244
+ * is therefore the DISPATCHING agent's own role binding (dsh
245
+ * `Config.dispatchBinding`) — never the spawn-target field (omp `agent` /
246
+ * opencode `subagent`), which equals `Execute as` on every compliant
247
+ * dispatch (issue #156). Comparison is case-insensitive after trim. An
248
+ * EMPTY binding fails closed (`dispatch.anti-recursion.empty-binding`,
249
+ * critical): the host cannot report which agent is calling, so
250
+ * anti-recursion cannot be proven — the dispatch must not proceed as if
251
+ * the NEVER red line held. An empty `executeAs` with a set binding stays
252
+ * ok (field presence is `validateAssignmentFields`' job, not this
253
+ * precheck).
235
254
  */
236
255
  export declare function antiRecursionPrecheck(subagentType: string, executeAs: string): GateResult;
package/dist/engine.js CHANGED
@@ -965,8 +965,10 @@ function composeDispatchGate(text, opts = {}) {
965
965
  const violations = [];
966
966
  const writable = opts.writable !== false;
967
967
  violations.push(...validateAssignmentFields(text, { writable }).violations);
968
- const agent = opts.agent ?? "";
969
- violations.push(...antiRecursionPrecheck(agent, parseAssignmentFields(text).executeAs ?? "").violations);
968
+ const caller = opts.caller ?? "";
969
+ if (caller.trim() !== "" || opts.callerRequired === true) {
970
+ violations.push(...antiRecursionPrecheck(caller, parseAssignmentFields(text).executeAs ?? "").violations);
971
+ }
970
972
  if (writable) {
971
973
  const forms = parseAssignmentBranchForms(text);
972
974
  const branch = forms.createForm?.name ?? forms.workingBranch ?? forms.directOn?.branch ?? process.env.MSTAR_WORKING_BRANCH;
@@ -989,7 +991,7 @@ function antiRecursionPrecheck(subagentType, executeAs) {
989
991
  return {
990
992
  ok: false,
991
993
  violations: [
992
- 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")
994
+ 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")
993
995
  ]
994
996
  };
995
997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mstar-harness/engine",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
5
5
  "license": "MIT",
6
6
  "repository": {