opencode-swarm 7.138.3 → 7.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/skills/execute/SKILL.md +11 -2
- package/.opencode/skills/swarm-implement/SKILL.md +1 -1
- package/dist/cli/{config-doctor-2mwgdbsn.js → config-doctor-aszp7z1h.js} +2 -2
- package/dist/cli/{core-v6bte964.js → core-3s832mxx.js} +1 -1
- package/dist/cli/{curation-policy-5dqexw5j.js → curation-policy-a3hs73f7.js} +6 -6
- package/dist/cli/{curator-llm-factory-wkjme59b.js → curator-llm-factory-e8ngwyjm.js} +26 -25
- package/dist/cli/{curator-jjtp9hn2.js → curator-zxjqcc8r.js} +26 -25
- package/dist/cli/{evidence-summary-service-6cttrkzw.js → evidence-summary-service-qfhjfxdz.js} +7 -7
- package/dist/cli/{gate-evidence-ecx4djaw.js → gate-evidence-m2grv70v.js} +4 -4
- package/dist/cli/{guardrail-explain-xppwfnxr.js → guardrail-explain-rqpmzxyy.js} +27 -26
- package/dist/cli/{guardrail-log-swet7rch.js → guardrail-log-vd4a0ng5.js} +4 -3
- package/dist/cli/{hive-promoter-xvq8jzbs.js → hive-promoter-jc5vym27.js} +26 -25
- package/dist/cli/{index-tc61fybd.js → index-0tsetsxs.js} +4 -4
- package/dist/cli/{index-jhb8kjc5.js → index-3nr2sn32.js} +3 -3
- package/dist/cli/{index-y06a43sk.js → index-4ft523nb.js} +323 -80
- package/dist/cli/index-556ja8an.js +62 -0
- package/dist/cli/{index-31vfewmn.js → index-5bkpyvdf.js} +271 -94
- package/dist/cli/{index-wqg74mwj.js → index-67yqywan.js} +2 -2
- package/dist/cli/{index-gda7cwqz.js → index-6q240xgm.js} +30 -68
- package/dist/cli/{index-adde7nm2.js → index-84s6x604.js} +5 -5
- package/dist/cli/{index-j0xx9wpj.js → index-85hstja2.js} +1 -1
- package/dist/cli/{index-xybtaph7.js → index-a614s16f.js} +1 -1
- package/dist/cli/{index-10mz5js6.js → index-chvqee1j.js} +2 -2
- package/dist/cli/{index-rrxcfxrd.js → index-ddmnhv4h.js} +1 -1
- package/dist/cli/{index-1v89yst3.js → index-gytyqv1d.js} +2 -2
- package/dist/cli/{index-93ky909v.js → index-kv36s0x0.js} +3 -3
- package/dist/cli/{index-z7jgvm9b.js → index-nbgw5dxa.js} +1 -1
- package/dist/cli/{index-tqshdzaw.js → index-nc544sfw.js} +1 -1
- package/dist/cli/{index-js7th5my.js → index-nsnfvckc.js} +1 -1
- package/dist/cli/{index-4fmztnrq.js → index-p2dz85tv.js} +2 -2
- package/dist/cli/{index-y6dvs5tt.js → index-q3qtgtd0.js} +1 -1
- package/dist/cli/{index-99af5gvd.js → index-r3pth9a7.js} +230 -118
- package/dist/cli/{index-ab57h0yq.js → index-rtetrxxq.js} +1 -1
- package/dist/cli/{index-rjratees.js → index-s428dmy6.js} +2 -2
- package/dist/cli/{index-q344kj84.js → index-t26nfmcs.js} +1 -1
- package/dist/cli/{index-1djv5684.js → index-t31cc7kv.js} +6 -6
- package/dist/cli/{index-39k6y018.js → index-tcdcj1g2.js} +2 -2
- package/dist/cli/{index-en4fb04r.js → index-tyh0p5e8.js} +1 -1
- package/dist/cli/{index-xgmx9had.js → index-y46anh5e.js} +28 -27
- package/dist/cli/index-ymszzvnr.js +1725 -0
- package/dist/cli/{index-ngqxzkqm.js → index-z1tkxzq4.js} +2 -2
- package/dist/cli/{index-btx8j39p.js → index-zd15ya1w.js} +1 -1
- package/dist/cli/index.js +26 -25
- package/dist/cli/{knowledge-escalator-7n384ys1.js → knowledge-escalator-6473ey4c.js} +7 -7
- package/dist/cli/{knowledge-events-yaj0yjd6.js → knowledge-events-wwzpz8m4.js} +5 -5
- package/dist/cli/{knowledge-link-xc6yhxmz.js → knowledge-link-trfydkzm.js} +4 -4
- package/dist/cli/{knowledge-store-d99k0z2d.js → knowledge-store-99gd42jy.js} +5 -5
- package/dist/cli/{knowledge-validator-e5fqy9h4.js → knowledge-validator-v2eevvdg.js} +8 -8
- package/dist/cli/{pending-delegations-7dbc0j2a.js → pending-delegations-p6n83x67.js} +3 -3
- package/dist/cli/{pr-subscriptions-m943zn6g.js → pr-subscriptions-xbrfr51c.js} +3 -3
- package/dist/cli/{runner-x1hy047y.js → runner-9an52tmz.js} +5 -5
- package/dist/cli/{scan-cursor-9y9msabt.js → scan-cursor-39zxpw34.js} +6 -6
- package/dist/cli/{schema-xg5f11md.js → schema-xhfwr6b2.js} +1 -1
- package/dist/cli/scope-persistence-nsa6ra9s.js +61 -0
- package/dist/cli/{skill-generator-arvwfdfh.js → skill-generator-5px81476.js} +9 -9
- package/dist/cli/{telemetry-0yqed3jv.js → telemetry-7nt7zhyp.js} +1 -1
- package/dist/cli/{worktree-collision-ownership-hjrt5hx2.js → worktree-collision-ownership-9x4s4890.js} +3 -3
- package/dist/hooks/guardrails/destructive-command.d.ts +35 -0
- package/dist/hooks/guardrails/file-authority.d.ts +71 -26
- package/dist/hooks/guardrails/index.d.ts +2 -0
- package/dist/hooks/guardrails/tool-before.d.ts +5 -0
- package/dist/hooks/scope-guard.d.ts +3 -1
- package/dist/index.js +533 -522
- package/dist/plan/manager.d.ts +2 -0
- package/dist/scope/path-identity.d.ts +27 -0
- package/dist/scope/scope-binding.d.ts +90 -2
- package/dist/scope/scope-lease-renewal.d.ts +28 -0
- package/dist/scope/scope-persistence.d.ts +102 -19
- package/dist/scope/scope-resolution-diagnostic.d.ts +7 -0
- package/dist/services/run-memory.d.ts +53 -5
- package/dist/tools/declare-scope.d.ts +3 -1
- package/dist/tools/save-plan.d.ts +2 -0
- package/dist/tools/update-task-status.d.ts +2 -0
- package/dist/turbo/lean/lane-scope.d.ts +1 -1
- package/dist/utils/path-security.d.ts +25 -1
- package/package.json +1 -1
- package/dist/cli/index-2y4jy0de.js +0 -642
- package/dist/cli/scope-persistence-n1bryhzf.js +0 -38
package/dist/plan/manager.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface AcknowledgedRemovals {
|
|
|
41
41
|
import { type Plan, type RuntimePlan, type TaskStatus } from '../config/plan-schema';
|
|
42
42
|
import { isGitRepo } from '../git/branch';
|
|
43
43
|
import { getWorktreeMergeFailure } from '../hooks/delegation-gate/worktree-merge-status';
|
|
44
|
+
import { recordTaskAttempt } from '../services/run-memory.js';
|
|
44
45
|
import { isEpicModeActiveForProject } from '../turbo/epic/state.js';
|
|
45
46
|
import { commitTaskCompletion } from '../turbo/epic/task-commit.js';
|
|
46
47
|
import { readTaskScopes } from '../turbo/lean/conflicts.js';
|
|
@@ -64,6 +65,7 @@ export declare const _internals: {
|
|
|
64
65
|
readTaskScopes: typeof readTaskScopes;
|
|
65
66
|
commitTaskCompletion: typeof commitTaskCompletion;
|
|
66
67
|
getWorktreeMergeFailure: typeof getWorktreeMergeFailure;
|
|
68
|
+
recordTaskAttempt: typeof recordTaskAttempt;
|
|
67
69
|
};
|
|
68
70
|
/** @internal Test seam for snapshot retry helper */
|
|
69
71
|
export declare const _snapshot_test_exports: {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
/** Filesystem identity semantics used for comparisons, never for I/O. */
|
|
3
|
+
export type PathFlavor = 'win32' | 'posix';
|
|
4
|
+
export declare function getPathFlavor(pathLib?: Pick<typeof path, 'sep'>): PathFlavor;
|
|
5
|
+
/**
|
|
6
|
+
* Produces a comparison-only path identity. Windows identities case-fold and
|
|
7
|
+
* normalize both slash forms; POSIX identities preserve case.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizePathIdentity(value: string, flavor?: PathFlavor): string;
|
|
10
|
+
export declare function pathIdentitiesEqual(a: string, b: string, flavor?: PathFlavor): boolean;
|
|
11
|
+
/** True when target is the container itself or a descendant of it. */
|
|
12
|
+
export declare function isPathIdentityWithin(target: string, container: string, flavor?: PathFlavor): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Resolves a candidate and declared entries with one filesystem flavor, then
|
|
15
|
+
* applies the canonical identity containment rule. This is the shared scope
|
|
16
|
+
* predicate for guardrails, completion observers, and authority evaluation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isPathWithinDeclaredScope(filePath: string, scopeEntries: readonly string[], cwd?: string, flavor?: PathFlavor): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Compares drive/UNC/extended-device roots with Windows case-insensitive
|
|
21
|
+
* identity while retaining POSIX case-sensitive path semantics.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isOnDifferentPathRoot(target: string, cwd: string, flavor?: PathFlavor): boolean;
|
|
24
|
+
/** Returns a safe, caller-facing reason when path text is deceptive. */
|
|
25
|
+
export declare function unsafePathTextReason(value: string): string | null;
|
|
26
|
+
/** Single-line, bounded rendering for untrusted diagnostic fields. */
|
|
27
|
+
export declare function sanitizeDiagnosticText(value: unknown, maxLength?: number): string;
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { Plan } from '../config/plan-schema';
|
|
2
2
|
export declare const MAX_PENDING_SCOPE_BINDINGS = 256;
|
|
3
|
+
export declare const DEFAULT_SCOPE_BINDING_TTL_MS: number;
|
|
4
|
+
export declare const MAX_SCOPE_BINDING_TOMBSTONES = 256;
|
|
5
|
+
export type ScopeBindingLifecycleState = 'live' | 'expired' | 'revoked' | 'superseded';
|
|
3
6
|
export type ScopeBindingSource = 'declare_scope' | 'plan' | 'file_directive' | 'pr_feedback' | 'worktree_derived';
|
|
4
7
|
export interface ScopeBinding {
|
|
5
8
|
version: 2;
|
|
9
|
+
/** Stable logical authority identity across lifecycle generations. */
|
|
10
|
+
bindingId: string;
|
|
11
|
+
/** Exact immutable generation identity used by persistence and cleanup. */
|
|
12
|
+
generationId: string;
|
|
13
|
+
/** Monotonic CAS revision within one generation. */
|
|
14
|
+
revision: number;
|
|
15
|
+
lifecycleState: ScopeBindingLifecycleState;
|
|
6
16
|
workspaceIdentity: string;
|
|
7
17
|
planId: string;
|
|
8
18
|
planStructureHash: string;
|
|
@@ -14,14 +24,43 @@ export interface ScopeBinding {
|
|
|
14
24
|
activation: 'declaration' | 'pending_child' | 'active';
|
|
15
25
|
parentOwnerSessionId?: string;
|
|
16
26
|
parentCallId?: string;
|
|
27
|
+
predecessorGenerationId?: string;
|
|
28
|
+
childSessionId?: string;
|
|
17
29
|
/** PR_FEEDBACK bindings carry their parent workflow identity and revision. */
|
|
18
30
|
workflowSessionId?: string;
|
|
19
31
|
workflowRevisionDigest?: string;
|
|
20
32
|
source: ScopeBindingSource;
|
|
21
33
|
files: string[];
|
|
22
34
|
declaredAt: number;
|
|
35
|
+
updatedAt: number;
|
|
36
|
+
leaseStartedAt: number;
|
|
23
37
|
expiresAt: number;
|
|
24
38
|
}
|
|
39
|
+
export declare const MAX_SCOPE_PATH_BYTES = 4096;
|
|
40
|
+
export declare const MAX_SCOPE_FILES_BYTES: number;
|
|
41
|
+
export type ScopeBindingAdmissionResult = {
|
|
42
|
+
ok: true;
|
|
43
|
+
binding: ScopeBinding;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
code: 'SCOPE_BINDING_CAPACITY' | 'SCOPE_BINDING_STALE';
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
export type ScopeBindingResolution = {
|
|
50
|
+
status: 'found';
|
|
51
|
+
binding: ScopeBinding;
|
|
52
|
+
} | {
|
|
53
|
+
status: 'expired';
|
|
54
|
+
candidates: ScopeBinding[];
|
|
55
|
+
totalCandidates: number;
|
|
56
|
+
} | {
|
|
57
|
+
status: 'ambiguous';
|
|
58
|
+
candidates: ScopeBinding[];
|
|
59
|
+
totalCandidates: number;
|
|
60
|
+
} | {
|
|
61
|
+
status: 'not_declared';
|
|
62
|
+
};
|
|
63
|
+
export declare function isScopeBindingIdentity(value: unknown): value is string;
|
|
25
64
|
export declare function canonicalWorkspaceIdentity(directory: string): string | null;
|
|
26
65
|
export declare function normalizeScopeFiles(files: readonly string[]): string[] | null;
|
|
27
66
|
export declare function createScopeBinding(input: {
|
|
@@ -36,7 +75,7 @@ export declare function createScopeBinding(input: {
|
|
|
36
75
|
activation?: ScopeBinding['activation'];
|
|
37
76
|
ttlMs?: number;
|
|
38
77
|
}): ScopeBinding | null;
|
|
39
|
-
export declare function registerScopeBinding(binding: ScopeBinding):
|
|
78
|
+
export declare function registerScopeBinding(binding: ScopeBinding): ScopeBindingAdmissionResult;
|
|
40
79
|
/**
|
|
41
80
|
* Resolve the binding created for one fully-approved parent Task dispatch.
|
|
42
81
|
* An already-claimed child binding wins over its pending parent precursor;
|
|
@@ -83,6 +122,14 @@ export declare function getAuthorizedScopeBindingByPlanIdentity(input: {
|
|
|
83
122
|
taskId: string;
|
|
84
123
|
activeSessionId: string;
|
|
85
124
|
}): ScopeBinding | null;
|
|
125
|
+
/** Resolve authorization without collapsing expired and ambiguous states. */
|
|
126
|
+
export declare function resolveAuthorizedScopeBindingByPlanIdentity(input: {
|
|
127
|
+
directory: string;
|
|
128
|
+
planId: string;
|
|
129
|
+
planStructureHash: string;
|
|
130
|
+
taskId: string;
|
|
131
|
+
activeSessionId: string;
|
|
132
|
+
}): ScopeBindingResolution;
|
|
86
133
|
/**
|
|
87
134
|
* Issue #2002 recurrence guardrail (detection half).
|
|
88
135
|
*
|
|
@@ -144,6 +191,22 @@ export declare function describeScopeWorkspaceMismatch(input: {
|
|
|
144
191
|
taskId?: string | null;
|
|
145
192
|
}): string | null;
|
|
146
193
|
export declare function clearScopeBindings(predicate?: (binding: ScopeBinding) => boolean): ScopeBinding[];
|
|
194
|
+
/** Remove one exact generation; sibling generations are never affected. */
|
|
195
|
+
export declare function clearExactScopeBinding(input: {
|
|
196
|
+
bindingId: string;
|
|
197
|
+
generationId: string;
|
|
198
|
+
}): ScopeBinding | null;
|
|
199
|
+
export declare function installScopeBindingTombstone(binding: ScopeBinding): void;
|
|
200
|
+
export declare function hasScopeBindingDenyOverlay(binding: ScopeBinding): boolean;
|
|
201
|
+
/** Fail-closed overlay used only when durable revocation cannot be verified. */
|
|
202
|
+
export declare function installFailedRevocationOverlay(binding: ScopeBinding, reason: Exclude<ScopeBindingLifecycleState, 'live'>): boolean;
|
|
203
|
+
/**
|
|
204
|
+
* Installs an immediate generation-wide deny intent before durable retirement
|
|
205
|
+
* contends with refresh. Unlike the failure-only overlay, cleanup owns the
|
|
206
|
+
* whole exact generation and therefore deliberately follows a newer revision.
|
|
207
|
+
*/
|
|
208
|
+
export declare function installScopeBindingRetirementIntent(binding: Pick<ScopeBinding, 'bindingId' | 'generationId'> & Partial<ScopeBinding>): ScopeBinding | null;
|
|
209
|
+
export declare function updateExactScopeBinding(binding: ScopeBinding): ScopeBindingAdmissionResult;
|
|
147
210
|
/**
|
|
148
211
|
* Atomically claim the pending scope for the exact Task call whose upstream
|
|
149
212
|
* metadata identified the actual OpenCode child session. The call id is part of
|
|
@@ -158,6 +221,11 @@ export declare function claimScopeBindingForChild(input: {
|
|
|
158
221
|
previous: ScopeBinding;
|
|
159
222
|
claimed: ScopeBinding;
|
|
160
223
|
} | null;
|
|
224
|
+
export declare function createClaimedScopeBinding(previous: ScopeBinding, input: {
|
|
225
|
+
parentSessionId: string;
|
|
226
|
+
childSessionId: string;
|
|
227
|
+
dispatchCallId: string;
|
|
228
|
+
}): ScopeBinding;
|
|
161
229
|
export declare function deriveChildScopeBinding(parent: ScopeBinding, input: {
|
|
162
230
|
childDirectory: string;
|
|
163
231
|
childSessionId: string;
|
|
@@ -181,5 +249,25 @@ export declare function resolveCoderScopeSources(input: {
|
|
|
181
249
|
source: 'declare_scope' | 'plan' | 'file_directive';
|
|
182
250
|
} | {
|
|
183
251
|
ok: false;
|
|
184
|
-
code: 'SCOPE_NOT_DECLARED'
|
|
252
|
+
code: 'SCOPE_NOT_DECLARED';
|
|
253
|
+
sources?: ScopeSourceSets;
|
|
254
|
+
} | {
|
|
255
|
+
ok: false;
|
|
256
|
+
code: 'SCOPE_CONFLICT';
|
|
257
|
+
authoritativeSource: 'declare_scope' | 'plan';
|
|
258
|
+
authoritativeFiles: string[];
|
|
259
|
+
conflictingSource: 'plan' | 'file_directive';
|
|
260
|
+
conflictingFiles: string[];
|
|
261
|
+
disagreement: string[];
|
|
262
|
+
sources: ScopeSourceSets;
|
|
185
263
|
};
|
|
264
|
+
interface ScopeSourceSets {
|
|
265
|
+
explicit: string[] | null;
|
|
266
|
+
plan: string[] | null;
|
|
267
|
+
directives: string[] | null;
|
|
268
|
+
}
|
|
269
|
+
/** One bounded renderer shared by normal and PR-feedback preflight paths. */
|
|
270
|
+
export declare function formatCoderScopeConflict(decision: Extract<ReturnType<typeof resolveCoderScopeSources>, {
|
|
271
|
+
ok: false;
|
|
272
|
+
}>): string;
|
|
273
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ScopeBinding } from './scope-binding.js';
|
|
2
|
+
import { refreshScopeBindingLease, type ScopePersistenceResult } from './scope-persistence.js';
|
|
3
|
+
export interface ScopeLeaseCandidateInput {
|
|
4
|
+
callID: string;
|
|
5
|
+
sessionID: string;
|
|
6
|
+
tool: string;
|
|
7
|
+
directory: string;
|
|
8
|
+
binding: ScopeBinding;
|
|
9
|
+
targets: string[];
|
|
10
|
+
args?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface ScopeLeaseAfterOutput {
|
|
13
|
+
title?: unknown;
|
|
14
|
+
output?: unknown;
|
|
15
|
+
metadata?: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface ScopeLeaseRenewalTracker {
|
|
18
|
+
remember(input: ScopeLeaseCandidateInput): void;
|
|
19
|
+
consume(input: {
|
|
20
|
+
callID: string;
|
|
21
|
+
sessionID: string;
|
|
22
|
+
tool: string;
|
|
23
|
+
output: ScopeLeaseAfterOutput | null | undefined;
|
|
24
|
+
}): Promise<ScopePersistenceResult | null>;
|
|
25
|
+
}
|
|
26
|
+
type RefreshLease = typeof refreshScopeBindingLease;
|
|
27
|
+
export declare function createScopeLeaseRenewalTracker(refresh?: RefreshLease, now?: () => number): ScopeLeaseRenewalTracker;
|
|
28
|
+
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scope persistence for #519 (v6.71.1 hotfix).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Retains the legacy v1 `.swarm/scopes/scope-{taskId}.json` projection used by
|
|
5
|
+
* worktree materialization, and persists authorization-capable v2 bindings as
|
|
6
|
+
* exact identity/generation records. Durable v2 state is authoritative across
|
|
7
|
+
* processes; memory is only a cache and can never revive a retired generation.
|
|
7
8
|
*
|
|
8
9
|
* Also exposes a fallback reader that reads `plan.json:phases[].tasks[].files_touched`
|
|
9
10
|
* for the active task, so architect-authored plans become a durable scope source
|
|
@@ -13,7 +14,8 @@
|
|
|
13
14
|
* - Atomic write via temp + rename (POSIX atomic on same filesystem).
|
|
14
15
|
* - File lock via proper-lockfile while writing.
|
|
15
16
|
* - Schema versioning: readers fail closed on unknown version.
|
|
16
|
-
* - TTL: default 24h from declaredAt; expired
|
|
17
|
+
* - Legacy v1 TTL: default 24h from declaredAt; expired projections return null.
|
|
18
|
+
* - V2 lifecycle: typed live/expired/revoked/ambiguous/overloaded decisions.
|
|
17
19
|
* - Symlink guards (defence in depth):
|
|
18
20
|
* * realpath containment check on `.swarm/scopes/` (closes parent-dir attack)
|
|
19
21
|
* * O_NOFOLLOW on both write-create and read-fd (closes leaf-file TOCTOU)
|
|
@@ -31,20 +33,15 @@
|
|
|
31
33
|
* - This is command-level containment, not an operating-system syscall sandbox.
|
|
32
34
|
*
|
|
33
35
|
* RESIDUAL RISKS:
|
|
34
|
-
* 1. Platform-portability of symlink guards:
|
|
36
|
+
* 1. Platform-portability of legacy v1 symlink guards:
|
|
35
37
|
* - realpath resolves POSIX symlinks and Windows junctions, but the
|
|
36
38
|
* Windows behaviour is not covered by CI (Linux-only test matrix).
|
|
37
39
|
* - O_NOFOLLOW is a no-op on Windows (falls back to 0). The realpath
|
|
38
40
|
* containment check on `.swarm/scopes/` remains the primary guard
|
|
39
41
|
* on that platform; leaf-file TOCTOU on Windows is not closed.
|
|
40
|
-
* 2.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Acceptable because in-memory state remains authoritative inside the
|
|
44
|
-
* live process; disk is a fallback.
|
|
45
|
-
* 3. Temp-file leak: a crash between `Bun.write(tmp)` and `renameSync`
|
|
46
|
-
* leaves `scope-{id}.json.tmp.{ts}.{rand}` files. No sweeper runs today;
|
|
47
|
-
* accumulation is bounded by `/swarm close` (which rm -rf's .swarm/scopes/).
|
|
42
|
+
* 2. Legacy v1 temp-file leak: a crash between atomic-write steps can leave
|
|
43
|
+
* temporary projection files. V2 lifecycle maintenance is bounded and
|
|
44
|
+
* prunes settled receipts, tombstones, sidecars, and retirement intents.
|
|
48
45
|
*
|
|
49
46
|
* NOT a standalone syscall security boundary. Shell-write detection and the
|
|
50
47
|
* central write-target resolver provide command-level interception; this module
|
|
@@ -53,6 +50,31 @@
|
|
|
53
50
|
import { type Plan } from '../config/plan-schema';
|
|
54
51
|
import { type ScopeBinding } from './scope-binding';
|
|
55
52
|
declare const SCOPE_SCHEMA_VERSION: 1;
|
|
53
|
+
export type ScopePersistenceCode = 'SCOPE_NOT_DECLARED' | 'SCOPE_BINDING_PERSISTENCE_FAILED' | 'SCOPE_BINDING_CAPACITY' | 'SCOPE_BINDING_ALREADY_CLAIMED' | 'SCOPE_BINDING_AMBIGUOUS' | 'SCOPE_BINDING_EXPIRED' | 'SCOPE_BINDING_STORE_OVERLOADED' | 'SCOPE_BINDING_STALE';
|
|
54
|
+
export type ScopePersistenceResult<T = ScopeBinding> = {
|
|
55
|
+
ok: true;
|
|
56
|
+
value: T;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
code: ScopePersistenceCode;
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
export type DurableScopeBindingResolution = {
|
|
63
|
+
status: 'found';
|
|
64
|
+
binding: ScopeBinding;
|
|
65
|
+
} | {
|
|
66
|
+
status: 'expired';
|
|
67
|
+
candidates: ScopeBinding[];
|
|
68
|
+
totalCandidates: number;
|
|
69
|
+
} | {
|
|
70
|
+
status: 'ambiguous';
|
|
71
|
+
candidates: ScopeBinding[];
|
|
72
|
+
totalCandidates: number;
|
|
73
|
+
} | {
|
|
74
|
+
status: 'overloaded';
|
|
75
|
+
} | {
|
|
76
|
+
status: 'not_declared';
|
|
77
|
+
};
|
|
56
78
|
export interface PersistedScope {
|
|
57
79
|
version: typeof SCOPE_SCHEMA_VERSION;
|
|
58
80
|
taskId: string;
|
|
@@ -62,11 +84,11 @@ export interface PersistedScope {
|
|
|
62
84
|
}
|
|
63
85
|
export type PersistedScopeBinding = ScopeBinding;
|
|
64
86
|
/**
|
|
65
|
-
* Write
|
|
87
|
+
* Write the legacy v1 `.swarm/scopes/scope-{taskId}.json` projection atomically.
|
|
66
88
|
* Safe to call concurrently — proper-lockfile serialises writers per-file.
|
|
67
89
|
*
|
|
68
|
-
* Silent on I/O failure
|
|
69
|
-
*
|
|
90
|
+
* Silent on I/O failure for backward compatibility. This projection is not an
|
|
91
|
+
* authorization source; v2 callers use the fail-closed exact-generation APIs.
|
|
70
92
|
*/
|
|
71
93
|
export declare function writeScopeToDisk(directory: string, taskId: string, files: string[], ttlMs?: number): Promise<void>;
|
|
72
94
|
/**
|
|
@@ -74,7 +96,9 @@ export declare function writeScopeToDisk(directory: string, taskId: string, file
|
|
|
74
96
|
* compatibility with older non-authorization consumers, but strict Task
|
|
75
97
|
* preflight and declare_scope use only this schema.
|
|
76
98
|
*/
|
|
77
|
-
export declare function writeScopeBindingToDisk(directory: string, binding: ScopeBinding): Promise<
|
|
99
|
+
export declare function writeScopeBindingToDisk(directory: string, binding: ScopeBinding): Promise<ScopePersistenceResult>;
|
|
100
|
+
/** Mandatory transaction: durable verification precedes memory authority. */
|
|
101
|
+
export declare function persistAndRegisterScopeBinding(directory: string, binding: ScopeBinding): Promise<ScopePersistenceResult>;
|
|
78
102
|
/** Read and fully validate a v2 binding; v1 records never authorize here. */
|
|
79
103
|
export declare function readScopeBindingFromDisk(input: {
|
|
80
104
|
directory: string;
|
|
@@ -85,17 +109,67 @@ export declare function readScopeBindingFromDisk(input: {
|
|
|
85
109
|
requireDispatchCorrelation?: boolean;
|
|
86
110
|
requireDeclaration?: boolean;
|
|
87
111
|
}): ScopeBinding | null;
|
|
112
|
+
export declare function resolveScopeBindingFromDisk(input: {
|
|
113
|
+
directory: string;
|
|
114
|
+
taskId: string;
|
|
115
|
+
plan: Plan;
|
|
116
|
+
ownerSessionId: string;
|
|
117
|
+
parentCallId?: string;
|
|
118
|
+
requireDispatchCorrelation?: boolean;
|
|
119
|
+
requireDeclaration?: boolean;
|
|
120
|
+
includeExpired?: boolean;
|
|
121
|
+
}): DurableScopeBindingResolution;
|
|
88
122
|
/** Remove one identity-bound authorization without disturbing sibling sessions. */
|
|
89
123
|
export declare function clearScopeBindingFromDisk(input: {
|
|
90
124
|
directory: string;
|
|
125
|
+
binding: ScopeBinding;
|
|
126
|
+
}): ScopePersistenceResult;
|
|
127
|
+
export declare function tombstoneScopeBinding(directory: string, binding: ScopeBinding, reason: 'expired' | 'revoked' | 'superseded'): Promise<ScopePersistenceResult>;
|
|
128
|
+
/** CAS renewal for one exact active generation. */
|
|
129
|
+
export declare function refreshScopeBindingLease(input: {
|
|
130
|
+
directory: string;
|
|
131
|
+
bindingId: string;
|
|
132
|
+
generationId: string;
|
|
133
|
+
expectedRevision: number;
|
|
134
|
+
activeSessionId: string;
|
|
91
135
|
taskId: string;
|
|
92
|
-
|
|
93
|
-
}):
|
|
136
|
+
ttlMs?: number;
|
|
137
|
+
}): Promise<ScopePersistenceResult>;
|
|
138
|
+
/**
|
|
139
|
+
* Cross-process, successor-first claim transaction. All contenders for one
|
|
140
|
+
* pending generation lock the same stable receipt path.
|
|
141
|
+
*/
|
|
142
|
+
export declare function claimScopeBindingForChildDurably(input: {
|
|
143
|
+
directory: string;
|
|
144
|
+
parentSessionId: string;
|
|
145
|
+
childSessionId: string;
|
|
146
|
+
dispatchCallId: string;
|
|
147
|
+
}): Promise<ScopePersistenceResult<{
|
|
148
|
+
previous: ScopeBinding;
|
|
149
|
+
claimed: ScopeBinding;
|
|
150
|
+
}>>;
|
|
151
|
+
/**
|
|
152
|
+
* Declaration transaction used by declare_scope. Replacement is restricted to
|
|
153
|
+
* the same canonical workspace, task, and owning architect session.
|
|
154
|
+
*/
|
|
155
|
+
export declare function replaceExistingScopeDeclaration(input: {
|
|
156
|
+
directory: string;
|
|
157
|
+
binding: ScopeBinding;
|
|
158
|
+
replaceExisting: boolean;
|
|
159
|
+
}): Promise<ScopePersistenceResult>;
|
|
160
|
+
export declare function flushScopeBindingMaintenance(directory?: string): Promise<void>;
|
|
161
|
+
/** Prune old non-authorizing generations and settled receipts under one lock. */
|
|
162
|
+
export declare function pruneScopeBindingTombstones(directory: string): Promise<ScopePersistenceResult<number>>;
|
|
94
163
|
export declare function resolveAuthorizedScopeBinding(input: {
|
|
95
164
|
directory: string;
|
|
96
165
|
taskId: string;
|
|
97
166
|
activeSessionId: string;
|
|
98
167
|
}): ScopeBinding | null;
|
|
168
|
+
export declare function resolveAuthorizedScopeBindingDetailed(input: {
|
|
169
|
+
directory: string;
|
|
170
|
+
taskId: string;
|
|
171
|
+
activeSessionId: string;
|
|
172
|
+
}): DurableScopeBindingResolution;
|
|
99
173
|
/**
|
|
100
174
|
* Recover one unambiguous child binding after an in-memory session restart.
|
|
101
175
|
* Every candidate is still checked against current plan identity, exact child
|
|
@@ -105,12 +179,21 @@ export declare function resolveAuthorizedScopeBindingForSession(input: {
|
|
|
105
179
|
directory: string;
|
|
106
180
|
activeSessionId: string;
|
|
107
181
|
}): ScopeBinding | null;
|
|
182
|
+
export declare function resolveAuthorizedScopeBindingForSessionDetailed(input: {
|
|
183
|
+
directory: string;
|
|
184
|
+
activeSessionId: string;
|
|
185
|
+
}): DurableScopeBindingResolution;
|
|
108
186
|
/** Recover one exact planless PR_FEEDBACK child binding after plugin restart. */
|
|
109
187
|
export declare function resolveAuthorizedPrFeedbackScopeBindingFromDisk(input: {
|
|
110
188
|
directory: string;
|
|
111
189
|
activeSessionId: string;
|
|
112
190
|
taskId?: string;
|
|
113
191
|
}): ScopeBinding | null;
|
|
192
|
+
export declare const _scopePersistenceInternals: {
|
|
193
|
+
liveBindingCapacity: number;
|
|
194
|
+
bindingFileScanCapacity: number;
|
|
195
|
+
maintenanceFileScanCapacity: number;
|
|
196
|
+
};
|
|
114
197
|
/**
|
|
115
198
|
* Read persisted scope for a task. Returns null on:
|
|
116
199
|
* - file missing
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DurableScopeBindingResolution } from './scope-persistence.js';
|
|
2
|
+
/** One bounded, single-line recovery contract for durable binding lookup failures. */
|
|
3
|
+
export declare function formatScopeResolutionDiagnostic(input: {
|
|
4
|
+
resolution: DurableScopeBindingResolution;
|
|
5
|
+
taskId: string | null;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
}): string | null;
|
|
@@ -16,7 +16,28 @@ export interface RunMemoryEntry {
|
|
|
16
16
|
taskFingerprint: string;
|
|
17
17
|
/** Which agent executed the task (e.g. "coder") */
|
|
18
18
|
agent: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Outcome of the task execution.
|
|
21
|
+
*
|
|
22
|
+
* PRODUCER COVERAGE (keep this note accurate — directive 2, no unwired code):
|
|
23
|
+
* - `pass` / `fail` are produced today by TWO call sites, deliberately split:
|
|
24
|
+
* 1. `plan/manager.updateTaskStatus` records the TERMINAL outcome after
|
|
25
|
+
* savePlan succeeds — `completed` -> pass, `blocked` -> fail. It lives
|
|
26
|
+
* there, not in the `update_task_status` tool, because the tool is only
|
|
27
|
+
* one of two writers: the council APPROVE fast-path completes tasks via
|
|
28
|
+
* `advanceTaskStateAndPersist` (src/state.ts) with no tool call.
|
|
29
|
+
* 2. `update_task_status` records a `fail` when a council or QA gate
|
|
30
|
+
* BLOCKS a completion. That is a refusal, not a status transition, so
|
|
31
|
+
* it never reaches updateTaskStatus and must be recorded at the tool.
|
|
32
|
+
* - `retry` has a CONSUMER (`summarizeTask` below treats it like `fail`) but
|
|
33
|
+
* no producer. It is deliberately retained for the guardrails
|
|
34
|
+
* transient-retry path (AGENTS.md invariant 9), which owns retry
|
|
35
|
+
* accounting and is out of scope here. Do not add a `retry` producer from
|
|
36
|
+
* the task-status path — a gate block is a real failure, not a retry.
|
|
37
|
+
* - `skip` has neither producer nor consumer (`summarizeTask` ignores it).
|
|
38
|
+
* It is retained only so pre-existing `.swarm/run-memory.jsonl` lines
|
|
39
|
+
* written by hand or by older builds still parse.
|
|
40
|
+
*/
|
|
20
41
|
outcome: 'pass' | 'fail' | 'retry' | 'skip';
|
|
21
42
|
/** 1-indexed attempt number */
|
|
22
43
|
attemptNumber: number;
|
|
@@ -51,12 +72,39 @@ export declare function recordOutcome(directory: string, entry: RunMemoryEntry):
|
|
|
51
72
|
*/
|
|
52
73
|
export declare function getTaskHistory(directory: string, taskId: string): Promise<RunMemoryEntry[]>;
|
|
53
74
|
/**
|
|
54
|
-
*
|
|
75
|
+
* Input for {@link recordTaskAttempt}. `attemptNumber` and `taskFingerprint`
|
|
76
|
+
* are derived, not supplied — callers know the task, not its history.
|
|
77
|
+
*/
|
|
78
|
+
export interface TaskAttemptInput {
|
|
79
|
+
/** Plan.json task ID (e.g. "3.2") */
|
|
80
|
+
taskId: string;
|
|
81
|
+
/** Which agent executed the task (e.g. "coder") */
|
|
82
|
+
agent: string;
|
|
83
|
+
/** Outcome of this attempt */
|
|
84
|
+
outcome: RunMemoryEntry['outcome'];
|
|
85
|
+
/** One-line failure reason — required in practice for fail/retry to be useful */
|
|
86
|
+
failureReason?: string;
|
|
87
|
+
/** Plan `files_touched` for the task; feeds the fingerprint */
|
|
88
|
+
fileTargets?: string[];
|
|
89
|
+
/** Wall-clock time in milliseconds */
|
|
90
|
+
durationMs?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Record one task attempt, deriving the attempt number from prior history.
|
|
94
|
+
*
|
|
95
|
+
* This is the single production producer for `.swarm/run-memory.jsonl`, which
|
|
96
|
+
* `getRunMemorySummary` reads back for cross-turn failure-pattern avoidance.
|
|
97
|
+
*
|
|
98
|
+
* Advisory-only and fail-open by contract: run memory is context enrichment,
|
|
99
|
+
* never a correctness gate, so a write failure must never surface to — or
|
|
100
|
+
* abort — the caller's primary operation (a task status update). The work is
|
|
101
|
+
* still awaited rather than fire-and-forget, so the entry is durable before the
|
|
102
|
+
* caller returns and a test can observe it.
|
|
55
103
|
*
|
|
56
104
|
* @param directory - The swarm workspace directory
|
|
57
|
-
* @
|
|
105
|
+
* @param input - The attempt to record
|
|
58
106
|
*/
|
|
59
|
-
export declare function
|
|
107
|
+
export declare function recordTaskAttempt(directory: string, input: TaskAttemptInput): Promise<void>;
|
|
60
108
|
/**
|
|
61
109
|
* Group entries by taskId
|
|
62
110
|
*/
|
|
@@ -79,8 +127,8 @@ export declare function getRunMemorySummary(directory: string): Promise<string |
|
|
|
79
127
|
export declare const _internals: {
|
|
80
128
|
generateTaskFingerprint: typeof generateTaskFingerprint;
|
|
81
129
|
recordOutcome: typeof recordOutcome;
|
|
130
|
+
recordTaskAttempt: typeof recordTaskAttempt;
|
|
82
131
|
getTaskHistory: typeof getTaskHistory;
|
|
83
|
-
getFailures: typeof getFailures;
|
|
84
132
|
getRunMemorySummary: typeof getRunMemorySummary;
|
|
85
133
|
groupByTaskId: typeof groupByTaskId;
|
|
86
134
|
summarizeTask: typeof summarizeTask;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This tool must be called before delegating to coder to enable scope containment checking.
|
|
5
5
|
*/
|
|
6
6
|
import type { ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
7
|
+
import type { AuthorityConfig } from '../config/schema';
|
|
7
8
|
/**
|
|
8
9
|
* Arguments for the declare_scope tool
|
|
9
10
|
*/
|
|
@@ -11,6 +12,7 @@ export interface DeclareScopeArgs {
|
|
|
11
12
|
taskId: string;
|
|
12
13
|
files: string[];
|
|
13
14
|
whitelist?: string[];
|
|
15
|
+
replace_existing?: boolean;
|
|
14
16
|
working_directory?: string;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
@@ -47,7 +49,7 @@ export declare function executeDeclareScope(args: DeclareScopeArgs, fallbackDir?
|
|
|
47
49
|
sessionID: string;
|
|
48
50
|
messageID: string;
|
|
49
51
|
agentName?: string;
|
|
50
|
-
}): Promise<DeclareScopeResult>;
|
|
52
|
+
}, authorityConfig?: AuthorityConfig): Promise<DeclareScopeResult>;
|
|
51
53
|
/**
|
|
52
54
|
* Tool definition for declare_scope
|
|
53
55
|
*/
|
|
@@ -23,6 +23,8 @@ export interface SavePlanArgs {
|
|
|
23
23
|
size?: 'small' | 'medium' | 'large';
|
|
24
24
|
depends?: string[];
|
|
25
25
|
acceptance?: string;
|
|
26
|
+
/** Exact project-relative paths this task is expected to modify. */
|
|
27
|
+
files_touched?: string[];
|
|
26
28
|
/** Spec FR-###/SC-### IDs this task maps to (issue #1687, FR-000). */
|
|
27
29
|
fr_refs?: string[];
|
|
28
30
|
}>;
|
|
@@ -6,6 +6,7 @@ import type { ToolContext, ToolDefinition } from '@opencode-ai/plugin/tool';
|
|
|
6
6
|
import { readTaskEvidenceRaw } from '../gate-evidence.js';
|
|
7
7
|
import { tryAcquireLock } from '../parallel/file-locks.js';
|
|
8
8
|
import { loadPlan, updateTaskStatus } from '../plan/manager';
|
|
9
|
+
import { recordTaskAttempt } from '../services/run-memory.js';
|
|
9
10
|
import { hasActiveLeanTurbo, hasActiveTurboMode } from '../state';
|
|
10
11
|
import { type ReviewerGateEvidenceKind, type ReviewerGateReasonCode } from '../telemetry.js';
|
|
11
12
|
import { verifyLeanTurboTaskCompletion } from '../turbo/lean/task-completion';
|
|
@@ -26,6 +27,7 @@ export declare const _internals: {
|
|
|
26
27
|
verifyLeanTurboTaskCompletion: typeof verifyLeanTurboTaskCompletion;
|
|
27
28
|
hasPassedDurableGateEvidence: typeof hasPassedDurableGateEvidence;
|
|
28
29
|
emitReviewerGateDecision: (sessionId: string, taskId: string, blocked: boolean, reasonCode: ReviewerGateReasonCode, evidenceKind: ReviewerGateEvidenceKind) => void;
|
|
30
|
+
recordTaskAttempt: typeof recordTaskAttempt;
|
|
29
31
|
};
|
|
30
32
|
/**
|
|
31
33
|
* Arguments for the update_task_status tool
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* This module publishes that authority using the same primitives as the
|
|
17
17
|
* standard worktree path (`src/hooks/delegation-gate.ts`, the
|
|
18
|
-
* `savePlan` → `deriveChildScopeBinding` →
|
|
18
|
+
* `savePlan` → `deriveChildScopeBinding` → `persistAndRegisterScopeBinding`
|
|
19
19
|
* sequence), so every identity condition in
|
|
20
20
|
* `getAuthorizedScopeBindingByPlanIdentity` is satisfied without weakening or
|
|
21
21
|
* bypassing any of them.
|
|
@@ -17,10 +17,34 @@ export declare function containsPathTraversal(str: string): boolean;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function containsControlChars(str: string): boolean;
|
|
19
19
|
/**
|
|
20
|
-
* Validate a
|
|
20
|
+
* Validate a **project/workspace root** for safety.
|
|
21
|
+
*
|
|
22
|
+
* Distinct from {@link validateDirectory}, which additionally rejects absolute
|
|
23
|
+
* paths and is therefore only correct for a *relative sub-path*. A workspace
|
|
24
|
+
* root is absolute by contract — `ctx.directory` injected by `createSwarmTool`
|
|
25
|
+
* is always an absolute project root (AGENTS.md invariant 4) — so applying
|
|
26
|
+
* `validateDirectory` to one rejects 100% of real inputs and silently kills
|
|
27
|
+
* whatever feature depends on it. That is exactly how `.swarm/run-memory.jsonl`
|
|
28
|
+
* came to have no producer and a consumer that threw on every call.
|
|
29
|
+
*
|
|
30
|
+
* This validator keeps the checks that are meaningful for a root (empty,
|
|
31
|
+
* traversal segments, control/bidi characters) and drops the absolute-path
|
|
32
|
+
* rejection. Containment of the actual file is NOT this function's job and is
|
|
33
|
+
* unchanged: `validateSwarmPath(directory, filename)` resolves the target under
|
|
34
|
+
* `<directory>/.swarm`, rejects any path that escapes it, and rejects a
|
|
35
|
+
* symlinked `.swarm` base.
|
|
36
|
+
*
|
|
37
|
+
* @param directory - The workspace root to validate
|
|
38
|
+
* @throws Error if the root is invalid
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateWorkspaceRoot(directory: string): void;
|
|
41
|
+
/**
|
|
42
|
+
* Validate a relative directory path for safety.
|
|
21
43
|
* Rejects empty paths, paths with traversal, control characters, and absolute paths.
|
|
22
44
|
* Throws an Error if the directory is invalid.
|
|
23
45
|
*
|
|
46
|
+
* Do NOT use this on a project/workspace root — see {@link validateWorkspaceRoot}.
|
|
47
|
+
*
|
|
24
48
|
* @param directory - The directory string to validate
|
|
25
49
|
* @throws Error if directory is invalid
|
|
26
50
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.139.0",
|
|
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",
|