opencode-swarm 7.135.0 → 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.
- package/dist/cli/{config-doctor-j1x63x87.js → config-doctor-6398t3a6.js} +2 -2
- package/dist/cli/{core-fvqp2gnn.js → core-9876t6cs.js} +1 -1
- package/dist/cli/{curation-policy-xg9zw0ep.js → curation-policy-f7qz4tez.js} +2 -2
- package/dist/cli/{curator-yc06cmnz.js → curator-fjhyezyx.js} +14 -14
- package/dist/cli/{curator-llm-factory-4mat6cq5.js → curator-llm-factory-wr3zmy2c.js} +14 -14
- package/dist/cli/{guardrail-explain-zmbxwh3s.js → guardrail-explain-g1s89b3e.js} +15 -15
- package/dist/cli/{guardrail-log-7egxrxap.js → guardrail-log-bd45kw0x.js} +3 -3
- package/dist/cli/{hive-promoter-7s3gzajy.js → hive-promoter-f2yh22m1.js} +14 -14
- package/dist/cli/{index-a4972t0f.js → index-247b1c1m.js} +3 -3
- package/dist/cli/{index-997d6p42.js → index-2nqsr1dx.js} +2 -2
- package/dist/cli/{index-6kmmdtdp.js → index-4pdrmemy.js} +2 -2
- package/dist/cli/{index-ctvnb76s.js → index-5vp9gka3.js} +1 -1
- package/dist/cli/{index-86wdrskb.js → index-5wx8wdxg.js} +5 -5
- package/dist/cli/{index-crw4n55v.js → index-aky90eqz.js} +36 -36
- package/dist/cli/{index-vf6e8ame.js → index-bjrkg4tc.js} +1 -1
- package/dist/cli/{index-337r9pkr.js → index-dzaarxhg.js} +1 -1
- package/dist/cli/{index-44zgqvqh.js → index-fwkqq9mq.js} +1 -1
- package/dist/cli/{index-x88w0cp5.js → index-g1drmgxe.js} +1 -1
- package/dist/cli/{index-nrvs08pr.js → index-j89zmcpx.js} +4 -4
- package/dist/cli/{index-grxkbbc5.js → index-jtshf1pd.js} +1 -1
- package/dist/cli/{index-e8w62fgs.js → index-jxchddjk.js} +1 -1
- package/dist/cli/{index-4yv2rn8q.js → index-nbnqf4fj.js} +1 -1
- package/dist/cli/{index-96b78s7p.js → index-w80r3axq.js} +16 -16
- package/dist/cli/{index-q9mq5tv3.js → index-wdkkf3xg.js} +2 -2
- package/dist/cli/{index-g0raqavt.js → index-xhpmagdm.js} +1 -1
- package/dist/cli/index.js +14 -14
- package/dist/cli/{knowledge-escalator-d76a090n.js → knowledge-escalator-4tp3yy7e.js} +3 -3
- package/dist/cli/{knowledge-events-3mcg1a0e.js → knowledge-events-7854gb3p.js} +1 -1
- package/dist/cli/{knowledge-store-xtt4sede.js → knowledge-store-cxnr6v01.js} +1 -1
- package/dist/cli/{knowledge-validator-h1k7ymtd.js → knowledge-validator-3n5t55qy.js} +4 -4
- package/dist/cli/{scan-cursor-stztfv7d.js → scan-cursor-0cexjh3b.js} +2 -2
- package/dist/cli/{schema-tbq2srrh.js → schema-rs5a7ywx.js} +1 -1
- package/dist/cli/{scope-persistence-7wydjzx8.js → scope-persistence-1vsnkmm4.js} +1 -1
- package/dist/cli/{skill-generator-02q1qdf0.js → skill-generator-n0h9n0zd.js} +5 -5
- package/dist/hooks/scope-guard.d.ts +11 -4
- package/dist/index.js +10 -10
- package/dist/scope/scope-binding.d.ts +68 -0
- package/dist/session/snapshot-reader.d.ts +6 -0
- package/dist/state.d.ts +118 -0
- package/dist/turbo/lean/lane-scope.d.ts +120 -0
- package/dist/turbo/lean/runner.d.ts +66 -4
- package/package.json +1 -1
- /package/dist/cli/{index-ny93srnd.js → index-fpkb3mw1.js} +0 -0
|
@@ -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;
|
|
@@ -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.
|
|
@@ -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 {
|
|
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
|
-
/**
|
|
199
|
-
|
|
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.135.
|
|
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",
|
|
File without changes
|