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.
Files changed (78) hide show
  1. package/.opencode/skills/execute/SKILL.md +11 -2
  2. package/.opencode/skills/swarm-implement/SKILL.md +1 -1
  3. package/dist/cli/{config-doctor-2mwgdbsn.js → config-doctor-aszp7z1h.js} +2 -2
  4. package/dist/cli/{core-v6bte964.js → core-3s832mxx.js} +1 -1
  5. package/dist/cli/{curation-policy-5dqexw5j.js → curation-policy-a3hs73f7.js} +6 -6
  6. package/dist/cli/{curator-llm-factory-wkjme59b.js → curator-llm-factory-e8ngwyjm.js} +26 -25
  7. package/dist/cli/{curator-jjtp9hn2.js → curator-zxjqcc8r.js} +26 -25
  8. package/dist/cli/{evidence-summary-service-6cttrkzw.js → evidence-summary-service-qfhjfxdz.js} +7 -7
  9. package/dist/cli/{gate-evidence-ecx4djaw.js → gate-evidence-m2grv70v.js} +4 -4
  10. package/dist/cli/{guardrail-explain-xppwfnxr.js → guardrail-explain-rqpmzxyy.js} +27 -26
  11. package/dist/cli/{guardrail-log-swet7rch.js → guardrail-log-vd4a0ng5.js} +4 -3
  12. package/dist/cli/{hive-promoter-xvq8jzbs.js → hive-promoter-jc5vym27.js} +26 -25
  13. package/dist/cli/{index-tc61fybd.js → index-0tsetsxs.js} +4 -4
  14. package/dist/cli/{index-jhb8kjc5.js → index-3nr2sn32.js} +3 -3
  15. package/dist/cli/{index-y06a43sk.js → index-4ft523nb.js} +323 -80
  16. package/dist/cli/index-556ja8an.js +62 -0
  17. package/dist/cli/{index-31vfewmn.js → index-5bkpyvdf.js} +271 -94
  18. package/dist/cli/{index-wqg74mwj.js → index-67yqywan.js} +2 -2
  19. package/dist/cli/{index-gda7cwqz.js → index-6q240xgm.js} +30 -68
  20. package/dist/cli/{index-adde7nm2.js → index-84s6x604.js} +5 -5
  21. package/dist/cli/{index-j0xx9wpj.js → index-85hstja2.js} +1 -1
  22. package/dist/cli/{index-xybtaph7.js → index-a614s16f.js} +1 -1
  23. package/dist/cli/{index-10mz5js6.js → index-chvqee1j.js} +2 -2
  24. package/dist/cli/{index-rrxcfxrd.js → index-ddmnhv4h.js} +1 -1
  25. package/dist/cli/{index-1v89yst3.js → index-gytyqv1d.js} +2 -2
  26. package/dist/cli/{index-93ky909v.js → index-kv36s0x0.js} +3 -3
  27. package/dist/cli/{index-z7jgvm9b.js → index-nbgw5dxa.js} +1 -1
  28. package/dist/cli/{index-tqshdzaw.js → index-nc544sfw.js} +1 -1
  29. package/dist/cli/{index-js7th5my.js → index-nsnfvckc.js} +1 -1
  30. package/dist/cli/{index-4fmztnrq.js → index-p2dz85tv.js} +2 -2
  31. package/dist/cli/{index-y6dvs5tt.js → index-q3qtgtd0.js} +1 -1
  32. package/dist/cli/{index-99af5gvd.js → index-r3pth9a7.js} +230 -118
  33. package/dist/cli/{index-ab57h0yq.js → index-rtetrxxq.js} +1 -1
  34. package/dist/cli/{index-rjratees.js → index-s428dmy6.js} +2 -2
  35. package/dist/cli/{index-q344kj84.js → index-t26nfmcs.js} +1 -1
  36. package/dist/cli/{index-1djv5684.js → index-t31cc7kv.js} +6 -6
  37. package/dist/cli/{index-39k6y018.js → index-tcdcj1g2.js} +2 -2
  38. package/dist/cli/{index-en4fb04r.js → index-tyh0p5e8.js} +1 -1
  39. package/dist/cli/{index-xgmx9had.js → index-y46anh5e.js} +28 -27
  40. package/dist/cli/index-ymszzvnr.js +1725 -0
  41. package/dist/cli/{index-ngqxzkqm.js → index-z1tkxzq4.js} +2 -2
  42. package/dist/cli/{index-btx8j39p.js → index-zd15ya1w.js} +1 -1
  43. package/dist/cli/index.js +26 -25
  44. package/dist/cli/{knowledge-escalator-7n384ys1.js → knowledge-escalator-6473ey4c.js} +7 -7
  45. package/dist/cli/{knowledge-events-yaj0yjd6.js → knowledge-events-wwzpz8m4.js} +5 -5
  46. package/dist/cli/{knowledge-link-xc6yhxmz.js → knowledge-link-trfydkzm.js} +4 -4
  47. package/dist/cli/{knowledge-store-d99k0z2d.js → knowledge-store-99gd42jy.js} +5 -5
  48. package/dist/cli/{knowledge-validator-e5fqy9h4.js → knowledge-validator-v2eevvdg.js} +8 -8
  49. package/dist/cli/{pending-delegations-7dbc0j2a.js → pending-delegations-p6n83x67.js} +3 -3
  50. package/dist/cli/{pr-subscriptions-m943zn6g.js → pr-subscriptions-xbrfr51c.js} +3 -3
  51. package/dist/cli/{runner-x1hy047y.js → runner-9an52tmz.js} +5 -5
  52. package/dist/cli/{scan-cursor-9y9msabt.js → scan-cursor-39zxpw34.js} +6 -6
  53. package/dist/cli/{schema-xg5f11md.js → schema-xhfwr6b2.js} +1 -1
  54. package/dist/cli/scope-persistence-nsa6ra9s.js +61 -0
  55. package/dist/cli/{skill-generator-arvwfdfh.js → skill-generator-5px81476.js} +9 -9
  56. package/dist/cli/{telemetry-0yqed3jv.js → telemetry-7nt7zhyp.js} +1 -1
  57. package/dist/cli/{worktree-collision-ownership-hjrt5hx2.js → worktree-collision-ownership-9x4s4890.js} +3 -3
  58. package/dist/hooks/guardrails/destructive-command.d.ts +35 -0
  59. package/dist/hooks/guardrails/file-authority.d.ts +71 -26
  60. package/dist/hooks/guardrails/index.d.ts +2 -0
  61. package/dist/hooks/guardrails/tool-before.d.ts +5 -0
  62. package/dist/hooks/scope-guard.d.ts +3 -1
  63. package/dist/index.js +533 -522
  64. package/dist/plan/manager.d.ts +2 -0
  65. package/dist/scope/path-identity.d.ts +27 -0
  66. package/dist/scope/scope-binding.d.ts +90 -2
  67. package/dist/scope/scope-lease-renewal.d.ts +28 -0
  68. package/dist/scope/scope-persistence.d.ts +102 -19
  69. package/dist/scope/scope-resolution-diagnostic.d.ts +7 -0
  70. package/dist/services/run-memory.d.ts +53 -5
  71. package/dist/tools/declare-scope.d.ts +3 -1
  72. package/dist/tools/save-plan.d.ts +2 -0
  73. package/dist/tools/update-task-status.d.ts +2 -0
  74. package/dist/turbo/lean/lane-scope.d.ts +1 -1
  75. package/dist/utils/path-security.d.ts +25 -1
  76. package/package.json +1 -1
  77. package/dist/cli/index-2y4jy0de.js +0 -642
  78. package/dist/cli/scope-persistence-n1bryhzf.js +0 -38
@@ -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): void;
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' | 'SCOPE_CONFLICT';
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
- * Persists declared coder scope to `.swarm/scopes/scope-{taskId}.json` so that
5
- * scope survives cross-process delegation in-memory `session.declaredCoderScope`
6
- * is lost when a coder session starts in a separate process (#496 root cause B).
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 scopes return null.
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. Stale lockfile DoS: a crashed writer leaves a lock for up to
41
- * LOCK_STALE_MS (30s). During that window, concurrent `declare_scope`
42
- * calls fail silently and the architect relies on in-memory state.
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 declared scope to `.swarm/scopes/scope-{taskId}.json` atomically.
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: scope persistence is a defense-in-depth layer, not a
69
- * hard requirement. In-memory state remains authoritative for the live process.
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<void>;
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
- ownerSessionId: string;
93
- }): void;
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
- /** Outcome of the task execution */
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
- * Get all failure and retry entries
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
- * @returns Array of fail/retry entries
105
+ * @param input - The attempt to record
58
106
  */
59
- export declare function getFailures(directory: string): Promise<RunMemoryEntry[]>;
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` → register + `writeScopeBindingToDisk`
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 directory path for safety.
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.138.3",
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",