opencode-swarm 7.134.3 → 7.135.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.
Files changed (64) hide show
  1. package/dist/background/pr-monitor-worker.d.ts +14 -13
  2. package/dist/cli/{config-doctor-yqwm2tfh.js → config-doctor-6398t3a6.js} +2 -2
  3. package/dist/cli/{core-d6yd2jzv.js → core-9876t6cs.js} +1 -1
  4. package/dist/cli/{curation-policy-x5m4x34n.js → curation-policy-f7qz4tez.js} +6 -6
  5. package/dist/cli/{curator-thrwgwh1.js → curator-fjhyezyx.js} +23 -23
  6. package/dist/cli/{curator-llm-factory-24mjnh2y.js → curator-llm-factory-wr3zmy2c.js} +23 -23
  7. package/dist/cli/{evidence-summary-service-pay9409b.js → evidence-summary-service-0cczdmjv.js} +6 -6
  8. package/dist/cli/{gate-evidence-b5v1xvxp.js → gate-evidence-8zrwdj69.js} +4 -4
  9. package/dist/cli/{guardrail-explain-nmfq71cm.js → guardrail-explain-g1s89b3e.js} +24 -24
  10. package/dist/cli/{guardrail-log-2a61rz1w.js → guardrail-log-bd45kw0x.js} +3 -3
  11. package/dist/cli/{hive-promoter-2nkjjwgc.js → hive-promoter-f2yh22m1.js} +23 -23
  12. package/dist/cli/{index-5nybajn9.js → index-09b9zncg.js} +1 -1
  13. package/dist/cli/{index-1mq8kmp6.js → index-247b1c1m.js} +3 -3
  14. package/dist/cli/{index-q3m6bpx2.js → index-2nqsr1dx.js} +4 -4
  15. package/dist/cli/{index-yaptf0hg.js → index-4pdrmemy.js} +2 -2
  16. package/dist/cli/{index-2b7sv71f.js → index-5vp9gka3.js} +1 -1
  17. package/dist/cli/{index-mgr951p8.js → index-5wx8wdxg.js} +5 -5
  18. package/dist/cli/{index-q2rrv0cy.js → index-7a2hm51h.js} +57 -2
  19. package/dist/cli/{index-y3v1404y.js → index-7an2qn0s.js} +1 -1
  20. package/dist/cli/{index-6kfqz0jw.js → index-aky90eqz.js} +1289 -1124
  21. package/dist/cli/{index-wk1h5ec2.js → index-b7p0mp2q.js} +2 -2
  22. package/dist/cli/{index-5f403c30.js → index-bjrkg4tc.js} +2 -2
  23. package/dist/cli/{index-tsmjmhnk.js → index-dzaarxhg.js} +2 -2
  24. package/dist/cli/{index-8fhg2nt2.js → index-ens0gt38.js} +5 -5
  25. package/dist/cli/{index-p8hezdtz.js → index-fpkb3mw1.js} +1 -1
  26. package/dist/cli/{index-2t1n9k7b.js → index-fwkqq9mq.js} +1 -1
  27. package/dist/cli/{index-6wya5cqh.js → index-g1drmgxe.js} +3 -3
  28. package/dist/cli/{index-rjngxx7q.js → index-j89zmcpx.js} +6 -6
  29. package/dist/cli/{index-yqvh2s4w.js → index-jtshf1pd.js} +1 -1
  30. package/dist/cli/{index-ddxt327j.js → index-jxchddjk.js} +1 -1
  31. package/dist/cli/{index-8wktghbc.js → index-nbnqf4fj.js} +1 -1
  32. package/dist/cli/{index-txwm0fv4.js → index-w80r3axq.js} +25 -25
  33. package/dist/cli/{index-reesz606.js → index-wdkkf3xg.js} +2 -2
  34. package/dist/cli/{index-4p02dp92.js → index-wxyxf0bd.js} +2 -2
  35. package/dist/cli/{index-dd7ttx8x.js → index-xg9repnx.js} +1 -1
  36. package/dist/cli/{index-y3rj2jgb.js → index-xhpmagdm.js} +2 -2
  37. package/dist/cli/{index-cfkvc5q9.js → index-yhba8swx.js} +1 -1
  38. package/dist/cli/{index-1j3682j8.js → index-z085b4ka.js} +1 -1
  39. package/dist/cli/index.js +23 -23
  40. package/dist/cli/{knowledge-escalator-fqsavryx.js → knowledge-escalator-4tp3yy7e.js} +7 -7
  41. package/dist/cli/{knowledge-events-ykt8vrvj.js → knowledge-events-7854gb3p.js} +5 -5
  42. package/dist/cli/{knowledge-link-nc4y2zkq.js → knowledge-link-etrfntkd.js} +4 -4
  43. package/dist/cli/{knowledge-store-tpd98m39.js → knowledge-store-cxnr6v01.js} +5 -5
  44. package/dist/cli/{knowledge-validator-kc7fnt2r.js → knowledge-validator-3n5t55qy.js} +8 -8
  45. package/dist/cli/{pending-delegations-v9e40hfk.js → pending-delegations-4z3tkkct.js} +3 -3
  46. package/dist/cli/{pr-subscriptions-ztq40hrt.js → pr-subscriptions-29j794kd.js} +3 -3
  47. package/dist/cli/{scan-cursor-121b73j0.js → scan-cursor-0cexjh3b.js} +6 -6
  48. package/dist/cli/{schema-jb4p8ja9.js → schema-rs5a7ywx.js} +1 -1
  49. package/dist/cli/{scope-persistence-hz3zmmsv.js → scope-persistence-1vsnkmm4.js} +4 -4
  50. package/dist/cli/{skill-generator-m2g5mdva.js → skill-generator-n0h9n0zd.js} +9 -9
  51. package/dist/cli/{telemetry-qh78ymq4.js → telemetry-859khp82.js} +9 -1
  52. package/dist/cli/{worktree-collision-ownership-c4bdnjtx.js → worktree-collision-ownership-dtwr0xj4.js} +3 -3
  53. package/dist/commands/context-map-stats.d.ts +5 -0
  54. package/dist/commands/registry.d.ts +14 -0
  55. package/dist/hooks/scope-guard.d.ts +11 -4
  56. package/dist/index.js +189 -187
  57. package/dist/scope/scope-binding.d.ts +68 -0
  58. package/dist/services/status-service.d.ts +12 -2
  59. package/dist/session/snapshot-reader.d.ts +6 -0
  60. package/dist/state.d.ts +118 -0
  61. package/dist/telemetry.d.ts +5 -0
  62. package/dist/turbo/lean/lane-scope.d.ts +120 -0
  63. package/dist/turbo/lean/runner.d.ts +66 -4
  64. package/package.json +1 -1
@@ -83,6 +83,66 @@ export declare function getAuthorizedScopeBindingByPlanIdentity(input: {
83
83
  taskId: string;
84
84
  activeSessionId: string;
85
85
  }): ScopeBinding | null;
86
+ /**
87
+ * Issue #2002 recurrence guardrail (detection half).
88
+ *
89
+ * The defect class is: *a hook constructed once with the plugin-root
90
+ * `ctx.directory` applies that root to sessions that actually execute in a
91
+ * different root.* Its signature failure is silent — a binding for this exact
92
+ * session exists and is otherwise valid, but `workspaceIdentity` (filter
93
+ * condition #1 in `getAuthorizedScopeBindingByPlanIdentity`) never matches, so
94
+ * the lookup returns null and the caller reports a generic
95
+ * `SCOPE_NOT_DECLARED` that names neither root.
96
+ *
97
+ * This turns that silence into a precise, self-describing diagnostic at the one
98
+ * user-visible failure site (`src/hooks/scope-guard.ts`). It catches the class
99
+ * at ANY call site that resolves a binding with the wrong root — present or
100
+ * future — rather than only at the two gates fixed for this issue.
101
+ *
102
+ * Diagnostic-only: never grants or denies authorization. Callers still fall
103
+ * back to the ordinary `SCOPE_NOT_DECLARED` denial whenever this returns null.
104
+ *
105
+ * The candidate filter mirrors every non-workspace correlation clause
106
+ * `getAuthorizedScopeBindingByPlanIdentity` requires for an otherwise-valid
107
+ * Task-scoped authorization — dispatch correlation (`dispatchCallId` present
108
+ * and equal to `ownerMessageId`/`parentCallId`) plus a genuine parent/child
109
+ * session split — so a stale or malformed "active" leftover that never went
110
+ * through the real claim/derive path cannot be mislabeled as a workspace
111
+ * mismatch just because it happens to share session + task + `activation`.
112
+ * `pr_feedback` bindings are excluded outright: they resolve through an
113
+ * entirely separate authorization path
114
+ * (`getAuthorizedPrFeedbackScopeBinding` / `resolveAuthorizedPrFeedbackScopeBindingFromDisk`)
115
+ * that the caller already attempted before falling into this diagnostic, and
116
+ * a live `pr_feedback` binding otherwise satisfies every correlation clause
117
+ * above.
118
+ *
119
+ * `planId`/`planStructureHash` are deliberately NOT compared here: a
120
+ * plan-drifted binding at the SAME root is already excluded by the
121
+ * `workspaceIdentity` clause below (it would never have reached this
122
+ * diagnostic branch in the first place — the ordinary lookup would have
123
+ * rejected it on plan identity, not returned null for lack of a match at
124
+ * all). Reading the plan at `input.directory` to compare identities would
125
+ * also be unreliable here: `input.directory` is, by construction, the root
126
+ * this diagnostic suspects is wrong, so its `.swarm/plan.json` may not exist
127
+ * or may belong to an unrelated project — silently disabling the guardrail
128
+ * exactly where it matters most.
129
+ *
130
+ * Returns null (no mismatch claim) whenever `input.directory` itself cannot
131
+ * be resolved to a canonical identity (e.g. a deleted lane directory) —
132
+ * otherwise every active binding for the session would trivially satisfy
133
+ * `workspaceIdentity !== null`, misreporting a missing-directory failure as a
134
+ * wrong-root failure.
135
+ *
136
+ * @param input.directory - The root the caller resolved against
137
+ * @param input.activeSessionId - The session attempting the write
138
+ * @param input.taskId - Optional task filter
139
+ * @returns A human-readable mismatch description, or null
140
+ */
141
+ export declare function describeScopeWorkspaceMismatch(input: {
142
+ directory: string;
143
+ activeSessionId: string;
144
+ taskId?: string | null;
145
+ }): string | null;
86
146
  export declare function clearScopeBindings(predicate?: (binding: ScopeBinding) => boolean): ScopeBinding[];
87
147
  /**
88
148
  * Atomically claim the pending scope for the exact Task call whose upstream
@@ -103,6 +163,14 @@ export declare function deriveChildScopeBinding(parent: ScopeBinding, input: {
103
163
  childSessionId: string;
104
164
  parentCallId: string;
105
165
  }): ScopeBinding;
166
+ /**
167
+ * Containment predicate shared by every scope consumer: a candidate path is
168
+ * covered by a scope entry when it is that entry or lives beneath it. Exported
169
+ * so authority-minting callers (e.g. the Lean Turbo lane publisher) narrow a
170
+ * candidate file set against plan authority using the *same* semantics the
171
+ * write gates later enforce, instead of re-implementing containment.
172
+ */
173
+ export declare function scopeContains(scope: readonly string[], candidate: string): boolean;
106
174
  export declare function resolveCoderScopeSources(input: {
107
175
  explicitFiles: readonly string[] | null | undefined;
108
176
  planFiles: readonly string[] | null | undefined;
@@ -118,6 +118,16 @@ export interface StatusData {
118
118
  sharedRoot?: string;
119
119
  generation?: number;
120
120
  };
121
+ /** FR-010: last heartbeat activity for the session */
122
+ lastActivity?: {
123
+ sessionId: string;
124
+ /** epoch ms from getLastHeartbeat */
125
+ timestamp: number;
126
+ /** null when no heartbeat recorded */
127
+ agoMs: number | null;
128
+ /** human-readable: "5s ago", "12m ago", "2h ago", "never" */
129
+ agoLabel: string;
130
+ };
121
131
  /**
122
132
  * #1850: memory cohort link status. Distinct from `cohort` (knowledge link)
123
133
  * so status can distinguish knowledge-linked from memory-linked (acceptance
@@ -139,7 +149,7 @@ export interface StatusData {
139
149
  * Get status data from the swarm directory.
140
150
  * Returns structured data that can be used by GUI, background flows, or commands.
141
151
  */
142
- export declare function getStatusData(directory: string, agents: Record<string, AgentDefinition>): Promise<StatusData>;
152
+ export declare function getStatusData(directory: string, agents: Record<string, AgentDefinition>, sessionId?: string): Promise<StatusData>;
143
153
  /**
144
154
  * Format status data as markdown for command output.
145
155
  * This is the thin adapter that delegates to the service.
@@ -149,4 +159,4 @@ export declare function formatStatusMarkdown(status: StatusData): string;
149
159
  * Handle status command - delegates to service and formats output.
150
160
  * Kept for backward compatibility - thin adapter.
151
161
  */
152
- export declare function handleStatusCommand(directory: string, agents: Record<string, AgentDefinition>): Promise<string>;
162
+ export declare function handleStatusCommand(directory: string, agents: Record<string, AgentDefinition>, sessionId?: string): Promise<string>;
@@ -7,6 +7,12 @@ import type { SerializedAgentSession, SnapshotData } from './snapshot-writer';
7
7
  /**
8
8
  * Transient session fields that must be reset on rehydration.
9
9
  * Centralised here to keep the reset logic DRY and auditable.
10
+ *
11
+ * `workspaceDirectory` (issue #2002) deliberately does NOT belong in this
12
+ * list: it is never part of `SerializedAgentSession` in the first place (see
13
+ * the TRUST BOUNDARY / DELIBERATELY NOT SNAPSHOTTED comment at its field
14
+ * declaration in `src/state.ts`), so `deserializeAgentSession` never restores
15
+ * it and there is nothing here to reset.
10
16
  */
11
17
  export declare const TRANSIENT_SESSION_FIELDS: ReadonlyArray<{
12
18
  name: string;
package/dist/state.d.ts CHANGED
@@ -226,6 +226,72 @@ export interface AgentSessionState {
226
226
  } | null;
227
227
  /** Declared file scope for current coder task (null = no scope declared) */
228
228
  declaredCoderScope: string[] | null;
229
+ /**
230
+ * Issue #2002: the root this session actually EXECUTES in, when it differs
231
+ * from the plugin-root `ctx.directory`.
232
+ *
233
+ * Worktree-isolated coder children are created with
234
+ * `session.create({ query: { directory: <lane> } })`, and their scope binding
235
+ * is derived and published against that lane. The write gates are constructed
236
+ * once at plugin init with `ctx.directory` and would otherwise resolve every
237
+ * session's binding, containment, and `.swarm/` reads against the project
238
+ * root — which can never match a lane-rooted binding.
239
+ *
240
+ * TRUST BOUNDARY: written ONLY by `recordSessionWorkspaceRoot`, whose single
241
+ * closed allowlist of production callers each pass `provisionWorktree`'s own
242
+ * output. `ensureAgentSession`'s `directory` argument (which can carry an
243
+ * agent-supplied `working_directory`, e.g. via `declare_scope`) must NEVER
244
+ * write this field.
245
+ *
246
+ * DELIBERATELY NOT SNAPSHOTTED (issue #2002 follow-up). `serializeAgentSession`
247
+ * (`src/session/snapshot-writer.ts`) intentionally omits this field, and it
248
+ * MUST stay omitted. The reason is trust, not oversight: `.swarm/session/state.json`
249
+ * lives under the project-root `.swarm/` directory, which the architect agent
250
+ * can write, so a restored string in that file is not a trusted resolution
251
+ * root — a tampered or stale snapshot could install an arbitrary root, or
252
+ * re-point a session at a *different* lane it never owned (cross-lane
253
+ * containment re-rooting). Persisting it would only be safe if rehydrate could
254
+ * revalidate the restored value against a durable, plugin-owned record of
255
+ * provisioned lanes, and no such record exists:
256
+ * - `worktree-provisioning-owner.ts`'s durable marker
257
+ * (`recordWorktreeProvisioningOwner`) stores no `worktreePath` at all, and
258
+ * its `worktreeSessionId` field is always the *parent* session id
259
+ * (`worktree-isolation.ts` passes `args.parentSessionID`, confirmed by
260
+ * every production and test call site) — it cannot answer "does child
261
+ * session X own lane path Y".
262
+ * - `background-delegations.jsonl` (`src/background/pending-delegations.ts`)
263
+ * does carry a `worktree.worktreePath` keyed by the child session id, but
264
+ * it is written only inside the `isBackgroundTrue(...)` branch of
265
+ * `delegation-gate.ts` (~line 3157) — i.e. only for `background: true` Task
266
+ * dispatches. It is never written for the ordinary synchronous worktree
267
+ * dispatch in `worktree-isolation.ts`, nor for the Lean Turbo dispatch in
268
+ * `src/turbo/lean/lane-scope.ts`, which are the two actual
269
+ * `recordSessionWorkspaceRoot` callers. It does not cover the field it
270
+ * would need to validate.
271
+ * - Re-deriving the expected path from `provisionWorktree`'s own naming
272
+ * scheme (`resolveWorktreeBaseDir(directory, worktreeDir)/parentSessionId/taskId`,
273
+ * or the `os.tmpdir()/swwt/...` Windows-shortened form) does not close the
274
+ * gap either: it requires the child's *parent* session id, which is only
275
+ * available from `delegationChains` in the very same untrusted snapshot —
276
+ * validating one untrusted field with another untrusted field is circular,
277
+ * not a validation. It would also require spawning `git worktree list` per
278
+ * restored session inside `loadSnapshot`, which runs on the plugin-init
279
+ * path under a bounded timeout (AGENTS.md invariant 1 / repro-704).
280
+ * Net effect: a plugin restart mid-lane loses the recorded root, and
281
+ * `resolveSessionWorkspaceDirectory` falls back to the plugin-root directory —
282
+ * fail-closed, byte-identical to pre-#2002 behaviour, never a widened
283
+ * authority (see its own doc comment). The lane coder is blocked with
284
+ * `SCOPE_NOT_DECLARED` (`src/hooks/scope-guard.ts`,
285
+ * `src/hooks/guardrails/tool-before.ts`) until a human/architect re-dispatches
286
+ * it, exactly like the pre-fix bug — not worse. `rehydrateState` (which would
287
+ * apply a restored value) only runs from `loadSnapshot`, itself only called at
288
+ * plugin init (`src/index.ts`), so this window is a real process restart, not
289
+ * something that can strip a live in-session lane coder of its root.
290
+ * Do NOT add this field to `TRANSIENT_SESSION_FIELDS`
291
+ * (`src/session/snapshot-reader.ts`) either — it is never restored in the
292
+ * first place, so there is nothing to reset.
293
+ */
294
+ workspaceDirectory?: string;
229
295
  /** Last scope violation message (null = no violation) */
230
296
  lastScopeViolation: string | null;
231
297
  /** Flag for one-shot scope violation warning injection in messagesTransform */
@@ -720,6 +786,58 @@ export declare function getAgentSession(sessionId: string): AgentSessionState |
720
786
  * @returns The AgentSessionState
721
787
  */
722
788
  export declare function ensureAgentSession(sessionId: string, agentName?: string, directory?: string): AgentSessionState;
789
+ /**
790
+ * Issue #2002 — record the root a session actually EXECUTES in, when it
791
+ * differs from the plugin-root `ctx.directory`.
792
+ *
793
+ * TRUST BOUNDARY (security-critical): this function performs NO path
794
+ * validation. `laneRoot` is trusted purely because of WHO calls it, not
795
+ * because of its shape — `provisionWorktree` can legitimately relocate a lane
796
+ * outside the swarm worktree base (e.g. a `os.tmpdir()`-shortened path on
797
+ * Windows when the path-budget check trips), so a shape predicate would
798
+ * refuse a legitimately provisioned lane and silently reintroduce the bug
799
+ * this closes.
800
+ *
801
+ * The only thing standing between this setter and a privilege-escalation bug
802
+ * is a closed allowlist of production callers, each of which passes
803
+ * `provisionWorktree`'s own return value — never a tool argument, never
804
+ * `ensureAgentSession`'s `directory` parameter (which CAN carry an
805
+ * agent-supplied `working_directory`, e.g. via `declare_scope`). That
806
+ * allowlist is mechanically enforced by
807
+ * `tests/unit/state/session-workspace-root-trusted-callers.test.ts`. Adding a
808
+ * new call site is a privilege-escalation review, not a formality: the
809
+ * reviewer must confirm the new caller passes `provisionWorktree` output
810
+ * before adding it to that test's allowlist.
811
+ *
812
+ * A blank or whitespace-only `laneRoot` is ignored (treated as unset) rather
813
+ * than being recorded and later resolving to `path.resolve('')` (cwd).
814
+ *
815
+ * ORDERING IS PART OF THE CONTRACT. This never creates the session — callers
816
+ * MUST register it with its real agent name (`ensureAgentSession(sessionId,
817
+ * 'coder', laneRoot)`) FIRST. Calling this on an unregistered session is a
818
+ * deliberate no-op, so resolution falls back to the plugin root and the write
819
+ * is blocked. Creating the session here instead would register
820
+ * `swarmState.activeAgent` as 'unknown', which fails OPEN rather than closed —
821
+ * see the inline comment in the body for the full chain.
822
+ *
823
+ * @param sessionId - The session identifier
824
+ * @param laneRoot - `provisionWorktree`'s own output; never agent-supplied
825
+ */
826
+ export declare function recordSessionWorkspaceRoot(sessionId: string, laneRoot: string): void;
827
+ /**
828
+ * Issue #2002 — resolve the root a write gate should evaluate path
829
+ * containment and scope-binding lookups against for a given session.
830
+ *
831
+ * Fail-closed: returns `fallbackDirectory` (the plugin-root `ctx.directory`)
832
+ * whenever no root was recorded for this session, or the recorded value is
833
+ * blank/unset — i.e. byte-identical to pre-#2002 behaviour for every session
834
+ * that was never lane-rooted.
835
+ *
836
+ * @param sessionId - The session identifier
837
+ * @param fallbackDirectory - The plugin-root directory to fall back to
838
+ * @returns the session's recorded workspace root, or `fallbackDirectory`
839
+ */
840
+ export declare function resolveSessionWorkspaceDirectory(sessionId: string, fallbackDirectory: string): string;
723
841
  /**
724
842
  * Update only the agent event timestamp (for stale detection).
725
843
  * Does NOT change agent name or reset guardrail state.
@@ -10,6 +10,10 @@ export type ReviewerGateEvidenceKind = 'genuine' | 'fallback' | 'data_quality' |
10
10
  */
11
11
  export type ReviewerGateReasonCode = 'lean_turbo_completed_lane' | 'standard_turbo_non_tier3' | 'durable_evidence_complete' | 'workflow_state_complete' | 'stage_b_parallel_complete' | 'no_active_sessions' | 'zero_valid_sessions' | 'restart_recovery_complete' | 'scoped_delegation_complete' | 'unscoped_delegation_complete' | 'corrupt_evidence' | 'required_gates_missing' | 'inspection_error';
12
12
  export type TelemetryListener = (event: TelemetryEvent, data: Record<string, unknown>) => void;
13
+ export declare function startHeartbeatTracking(): void;
14
+ export declare function getLastHeartbeat(sessionId: string): number | undefined;
15
+ export declare function stopHeartbeatTracking(): void;
16
+ export declare function resetHeartbeatTrackingForTesting(): void;
13
17
  /**
14
18
  * Number of emits between rotation checks. 50 keeps the overhead at one
15
19
  * `statSync` per 50 telemetry writes (~once per few seconds in a busy
@@ -87,4 +91,5 @@ export declare const _internals: {
87
91
  telemetry: typeof telemetry;
88
92
  emit: typeof emit;
89
93
  rotateTelemetryIfNeeded: typeof rotateTelemetryIfNeeded;
94
+ heartbeatListenerCount: () => number;
90
95
  };
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Lean Turbo lane write authority (issue #2002 — Lean Turbo half).
3
+ *
4
+ * ## The defect this closes
5
+ *
6
+ * A Lean Turbo lane creates a coder session rooted at the lane worktree and
7
+ * dispatches it with `tools: { write: true, edit: true, patch: true }`
8
+ * (`src/turbo/lean/runner.ts`), but nothing ever published a v2 scope binding
9
+ * for that session and nothing ever materialized the authoritative plan into
10
+ * the lane. `resolveAuthorizedScopeBinding` starts from
11
+ * `readCurrentPlan(directory)` (`src/scope/scope-persistence.ts`), so with no
12
+ * `.swarm/plan.json` in the lane it returned null before it even looked at a
13
+ * binding — and every lane coder write failed `SCOPE_NOT_DECLARED` regardless
14
+ * of which directory the gate resolved.
15
+ *
16
+ * This module publishes that authority using the same primitives as the
17
+ * standard worktree path (`src/hooks/delegation-gate.ts`, the
18
+ * `savePlan` → `deriveChildScopeBinding` → register + `writeScopeBindingToDisk`
19
+ * sequence), so every identity condition in
20
+ * `getAuthorizedScopeBindingByPlanIdentity` is satisfied without weakening or
21
+ * bypassing any of them.
22
+ *
23
+ * ## Authority boundary
24
+ *
25
+ * A lane binding authorizes exactly the intersection of
26
+ * 1. `lane.files` — the file set the lane already holds exclusive locks on
27
+ * (`acquireLaneLocks(directory, laneId, lane.files, agent, lane.taskIds[0], …)`
28
+ * in `src/turbo/lean/runner.ts`), and
29
+ * 2. the authoritative plan's `files_touched` for the lane's own tasks.
30
+ *
31
+ * It therefore grants nothing the lane's pre-existing file locks did not
32
+ * already reserve for this dispatch, and nothing the plan does not attribute to
33
+ * this lane's tasks. It is rooted at the lane worktree, owned by the lane's
34
+ * child session id, and correlated to a single synthetic dispatch id — so it
35
+ * can never authorize a write in another lane or in the project root.
36
+ *
37
+ * ## Known gap (deliberate, not a bypass)
38
+ *
39
+ * A lane may carry several task ids (`src/turbo/lean/planner.ts` assigns every
40
+ * non-conflicting ready task to the first non-conflicting lane). The scope
41
+ * gates resolve a binding by a single `session.currentTaskId`, so a lane gets
42
+ * ONE binding labelled with the lane's representative task id
43
+ * (`lane.taskIds[0]`, the same representative the lock layer already uses) and
44
+ * carrying the lane's whole plan-backed authority. This mirrors the lock
45
+ * record 1:1; it does not widen the lane past what the lock already reserved.
46
+ *
47
+ * ## Fail-closed contract
48
+ *
49
+ * When a lane has no representative task id in strict `N.M[.P]` form, no
50
+ * plan-backed files, or an unusable child identity, this module publishes
51
+ * NOTHING and returns null. The lane coder then hits the ordinary
52
+ * `SCOPE_NOT_DECLARED` block — byte-identical to pre-fix behaviour. There is no
53
+ * path here that grants a lane coder write authority without a validated,
54
+ * plan-correlated, lane-rooted binding.
55
+ */
56
+ import type { Plan } from '../../config/plan-schema';
57
+ import { type ScopeBinding } from '../../scope/scope-binding';
58
+ /**
59
+ * Namespace for the synthetic dispatch-call id that correlates a lane's child
60
+ * binding to its parent Lean Turbo run. Lean Turbo dispatches through the
61
+ * OpenCode session API rather than the Task tool, so there is no upstream
62
+ * `callID`. The prefix keeps these ids disjoint from real Task call ids so a
63
+ * lane binding can never be claimed by a Task-dispatch lookup.
64
+ */
65
+ export declare const LEAN_TURBO_LANE_DISPATCH_PREFIX = "lean-turbo-lane";
66
+ /** Minimal lane shape this module needs. Matches `LeanTurboLane`. */
67
+ export interface LeanTurboLaneScopeLane {
68
+ laneId: string;
69
+ taskIds: readonly string[];
70
+ files: readonly string[];
71
+ }
72
+ export interface PublishLeanTurboLaneScopeInput {
73
+ /** Project root the Lean Turbo run was started in. */
74
+ primaryDirectory: string;
75
+ /** Root the lane's coder session actually executes in. */
76
+ laneRoot: string;
77
+ /**
78
+ * True only when `laneRoot` is `provisionWorktree`'s own output. Never
79
+ * derive this from a path comparison and never from an agent-supplied
80
+ * value: it gates both the lane plan materialization and the workspace-root
81
+ * recording, which are the two writes that must follow provisioning
82
+ * authority only.
83
+ */
84
+ isolated: boolean;
85
+ /** Authoritative plan for this run (already loaded from the project root). */
86
+ plan: Plan;
87
+ lane: LeanTurboLaneScopeLane;
88
+ /** Session running the Lean Turbo phase (the architect). */
89
+ parentSessionId: string;
90
+ /** Session id returned by `session.create` for this lane. */
91
+ childSessionId: string;
92
+ }
93
+ /**
94
+ * Resolve the file set a lane may be authorized for: lane-locked files that the
95
+ * authoritative plan also attributes to one of the lane's own tasks.
96
+ *
97
+ * Returns null when the plan attributes no usable file to this lane's tasks or
98
+ * when no locked lane file falls inside that plan authority — both are
99
+ * fail-closed outcomes, never a widening fallback.
100
+ */
101
+ export declare function resolveLeanTurboLaneAuthorityFiles(plan: Plan, lane: LeanTurboLaneScopeLane): string[] | null;
102
+ /**
103
+ * Publish one Lean Turbo lane's write authority.
104
+ *
105
+ * Ordering is deliberate:
106
+ * 1. validate identity and resolve plan-backed authority (cheap, no writes) —
107
+ * a failure here publishes nothing at all;
108
+ * 2. materialize the authoritative plan into the lane (isolated lanes only,
109
+ * so concurrent lanes never re-write the project's own plan ledger);
110
+ * 3. derive the active child binding, register it, and persist it under the
111
+ * lane root;
112
+ * 4. bind the child session to the lane root so the write gates resolve
113
+ * against the lane instead of the plugin-root `ctx.directory`.
114
+ *
115
+ * @returns the published child binding, or null when the lane cannot be
116
+ * authorized (fail closed — the caller must NOT grant any substitute).
117
+ * @throws only on unexpected I/O failure (e.g. `savePlan` rejecting). The
118
+ * caller treats a throw as a hard dispatch failure.
119
+ */
120
+ export declare function publishLeanTurboLaneScopeBinding(input: PublishLeanTurboLaneScopeInput): Promise<ScopeBinding | null>;
@@ -16,13 +16,15 @@
16
16
  * - If dispatch fails, locks for that lane are released and lane is marked 'failed'
17
17
  */
18
18
  import type { OpencodeClient } from '@opencode-ai/sdk';
19
+ import type { Plan } from '../../config/plan-schema';
19
20
  import type { LeanTurboConfig } from '../../config/schema';
20
21
  import { loadFullAutoRunState } from '../../full-auto/state';
21
22
  import { acquireLaneLocks, releaseLaneLocks } from '../../parallel/file-locks';
22
23
  import { loadPlanJsonOnly } from '../../plan/manager';
23
- import { ensureAgentSession, hasActiveFullAuto } from '../../state';
24
+ import { hasActiveFullAuto } from '../../state';
24
25
  import { type ModelOverride } from '../../utils/model-dispatch-fallback';
25
26
  import { writeLaneEvidence, writePhaseEvidence } from './evidence';
27
+ import { publishLeanTurboLaneScopeBinding } from './lane-scope';
26
28
  import { attemptMergeBackFromDirty, getMergeStrategy, mergeLaneBranch, postMergeCleanup, startupOrphanRecovery } from './merge-back';
27
29
  import { planLeanTurboLanes } from './planner';
28
30
  import type { LeanTurboDegradedTask, LeanTurboLane } from './state';
@@ -167,6 +169,32 @@ export interface LeanTurboPhaseResult {
167
169
  /** Lanes whose coder completed but merge-back to primary branch failed */
168
170
  mergeBackFailures?: MergeBackFailureInfo[];
169
171
  }
172
+ /**
173
+ * Orchestrates Lean Turbo lane execution.
174
+ *
175
+ * ## Usage
176
+ *
177
+ * ```ts
178
+ * const runner = new LeanTurboRunner({
179
+ * directory: projectRoot,
180
+ * sessionID: 'sess-abc123',
181
+ * opencodeClient: swarmState.opencodeClient,
182
+ * generatedAgentNames: swarmState.generatedAgentNames,
183
+ * });
184
+ *
185
+ * const result = await runner.runPhase(1);
186
+ * // ... monitor lanes ...
187
+ * await runner.cleanup();
188
+ * ```
189
+ */
190
+ /**
191
+ * Error code returned when a lane's write authority could not be published
192
+ * (issue #2002). Deliberately free of any token matched by
193
+ * `TRANSIENT_MODEL_ERROR_PATTERN` / `QUOTA_ERROR_PATTERN`, and classified
194
+ * `permanent` explicitly in `_processLane` so a broken authority handshake can
195
+ * never be mistaken for a provider blip and retried across fallback models.
196
+ */
197
+ export declare const LANE_SCOPE_DENIED_CODE = "LEAN_TURBO_LANE_SCOPE_DENIED";
170
198
  export declare class LeanTurboRunner {
171
199
  /**
172
200
  * Test-only dependency-injection seam.
@@ -195,8 +223,11 @@ export declare class LeanTurboRunner {
195
223
  startupOrphanRecovery: typeof startupOrphanRecovery;
196
224
  getMergeStrategy: typeof getMergeStrategy;
197
225
  assertCleanWorkingTree: typeof assertCleanWorkingTree;
198
- /** DI seam for ensureAgentSession — allows tests to intercept advisory queue writes. */
199
- ensureAgentSession: typeof ensureAgentSession;
226
+ /**
227
+ * Issue #2002: publishes a lane's plan-correlated, lane-rooted write
228
+ * authority before the lane's coder prompt is sent.
229
+ */
230
+ publishLeanTurboLaneScopeBinding: typeof publishLeanTurboLaneScopeBinding;
200
231
  };
201
232
  /**
202
233
  * Test-only dependency-injection seam for session operations.
@@ -269,12 +300,43 @@ export declare class LeanTurboRunner {
269
300
  *
270
301
  * @param lane - Lane to dispatch
271
302
  * @param agentName - Agent name to dispatch to
303
+ * @param worktreeDirectory - Provisioned lane worktree, when isolation is on
304
+ * @param model - Per-call model override for a fallback attempt (#1896)
305
+ * @param plan - Authoritative plan for this run. Required to publish the
306
+ * lane's write authority (issue #2002). When omitted, no binding is
307
+ * minted AND the lane's `write`/`edit`/`patch` tools are force-disabled
308
+ * (see `_doDispatch`) — an unscoped lane can never be dispatched
309
+ * writable, regardless of caller. Only `_processLane` supplies a plan in
310
+ * production (always, per `runPhase`'s own `NO_PLAN` guard); direct
311
+ * callers that omit it get a read-only lane rather than a silent
312
+ * unscoped-but-writable one.
272
313
  */
273
- dispatchLane(lane: LeanTurboLane, agentName: string, worktreeDirectory?: string, model?: ModelOverride): Promise<LaneDispatchResult>;
314
+ dispatchLane(lane: LeanTurboLane, agentName: string, worktreeDirectory?: string, model?: ModelOverride, plan?: Plan): Promise<LaneDispatchResult>;
274
315
  /**
275
316
  * Internal dispatch implementation (separated for timeout wrapping).
276
317
  */
277
318
  private _doDispatch;
319
+ /**
320
+ * Publish the lane's write authority for a freshly created lane session
321
+ * (issue #2002 — Lean Turbo half).
322
+ *
323
+ * Two distinct fail-closed outcomes, deliberately kept apart:
324
+ *
325
+ * - **No authorizable lane** (`publishLeanTurboLaneScopeBinding` returns
326
+ * null: representative task id not in strict `N.M[.P]` form, no
327
+ * plan-backed files, unusable child identity). Nothing is published and
328
+ * the lane still runs — the coder is blocked by the ordinary
329
+ * `SCOPE_NOT_DECLARED` gate exactly as it was before this fix. An
330
+ * architect advisory makes the unscoped lane visible instead of silent.
331
+ * - **Publication failed unexpectedly** (I/O error, plan materialization
332
+ * rejected). That is not a pre-existing state, so the lane session is
333
+ * torn down and the dispatch fails permanently rather than running a
334
+ * coder whose authority is in an unknown state.
335
+ *
336
+ * @returns a failing `LaneDispatchResult` to abort the dispatch, or null to
337
+ * continue.
338
+ */
339
+ private _publishLaneScope;
278
340
  /**
279
341
  * Get current status of all lanes tracked by this runner.
280
342
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.134.3",
3
+ "version": "7.135.1",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",