opencode-swarm 7.136.2 → 7.136.4

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 (87) hide show
  1. package/.opencode/skills/brainstorm/SKILL.md +1 -1
  2. package/.opencode/skills/clarify/SKILL.md +3 -3
  3. package/.opencode/skills/clarify-spec/SKILL.md +2 -2
  4. package/.opencode/skills/consult/SKILL.md +1 -1
  5. package/.opencode/skills/council/SKILL.md +1 -1
  6. package/.opencode/skills/critic-gate/SKILL.md +8 -3
  7. package/.opencode/skills/discover/SKILL.md +1 -1
  8. package/.opencode/skills/execute/SKILL.md +1 -1
  9. package/.opencode/skills/gate-attribution/SKILL.md +1 -1
  10. package/.opencode/skills/issue-ingest/SKILL.md +3 -3
  11. package/.opencode/skills/phase-wrap/SKILL.md +1 -1
  12. package/.opencode/skills/plan/SKILL.md +3 -3
  13. package/.opencode/skills/pre-phase-briefing/SKILL.md +1 -1
  14. package/.opencode/skills/resume/SKILL.md +1 -1
  15. package/.opencode/skills/specify/SKILL.md +1 -1
  16. package/dist/cli/{config-doctor-b67nb84b.js → config-doctor-qwp5y9dk.js} +2 -2
  17. package/dist/cli/{core-4z1s2ak1.js → core-jpjk2qvt.js} +1 -1
  18. package/dist/cli/{curation-policy-kvzhfaj1.js → curation-policy-8tsbaa4q.js} +6 -6
  19. package/dist/cli/{curator-nrmj8bds.js → curator-763smew7.js} +25 -25
  20. package/dist/cli/{curator-llm-factory-x7kvry9x.js → curator-llm-factory-dg8mmzy7.js} +25 -25
  21. package/dist/cli/{evidence-summary-service-cf8sz2bq.js → evidence-summary-service-nse06dqr.js} +7 -7
  22. package/dist/cli/{gate-evidence-aenyz6vt.js → gate-evidence-kdygjr56.js} +4 -4
  23. package/dist/cli/{guardrail-explain-5kd44rcj.js → guardrail-explain-6jc012g3.js} +26 -26
  24. package/dist/cli/{guardrail-log-aksrzqdx.js → guardrail-log-86aw7fne.js} +3 -3
  25. package/dist/cli/{hive-promoter-cy21rhnd.js → hive-promoter-en786ja4.js} +25 -25
  26. package/dist/cli/{index-84nz7bhe.js → index-06htxvzq.js} +3 -3
  27. package/dist/cli/{index-s0vahtdm.js → index-1hef020t.js} +1 -1
  28. package/dist/cli/{index-kwwxevne.js → index-45y0y3xh.js} +5 -5
  29. package/dist/cli/{index-ad6j0m7k.js → index-4cv991ra.js} +3 -3
  30. package/dist/cli/{index-45t7w06b.js → index-7t3vjw5e.js} +1 -1
  31. package/dist/cli/{index-m5fw4mer.js → index-89fpahtg.js} +1 -1
  32. package/dist/cli/{index-rw3f73aq.js → index-94pvh7hc.js} +2 -2
  33. package/dist/cli/{index-ey29aap6.js → index-b3jfcptk.js} +1 -1
  34. package/dist/cli/{index-0755132s.js → index-bb3ks0v3.js} +6 -6
  35. package/dist/cli/{index-7a2hm51h.js → index-cze4bq1x.js} +41 -0
  36. package/dist/cli/{index-mrtms113.js → index-d2sf9an1.js} +2 -2
  37. package/dist/cli/{index-wxyxf0bd.js → index-d61h3f3h.js} +2 -2
  38. package/dist/cli/{index-ckybsn6p.js → index-et8b70c8.js} +27 -27
  39. package/dist/cli/{index-1kn24ja0.js → index-fhnbzz75.js} +2 -2
  40. package/dist/cli/{index-s0rbdp61.js → index-fkdxfdbx.js} +4 -4
  41. package/dist/cli/{index-37v2wxqe.js → index-g0sdhyqk.js} +1 -1
  42. package/dist/cli/{index-7ayaq27q.js → index-gjs5g97v.js} +1 -1
  43. package/dist/cli/{index-c9ddxv4k.js → index-hh34tyv7.js} +1 -1
  44. package/dist/cli/{index-tcn457d5.js → index-kym0cctr.js} +1 -1
  45. package/dist/cli/{index-z1tm47nj.js → index-ne28wyyc.js} +2 -2
  46. package/dist/cli/{index-wjm896ey.js → index-nfm9f10v.js} +1 -1
  47. package/dist/cli/{index-y0xaq4f3.js → index-pq8ge6fe.js} +8 -2
  48. package/dist/cli/{index-azghvnja.js → index-qv1xc6rd.js} +2 -2
  49. package/dist/cli/{index-wnz282j3.js → index-rkp6jvc3.js} +2 -2
  50. package/dist/cli/{index-sr2cynyw.js → index-t1s4b5ha.js} +108 -51
  51. package/dist/cli/{index-09b9zncg.js → index-tncy55bp.js} +1 -1
  52. package/dist/cli/{index-vg3yx648.js → index-w6j1n5az.js} +1 -1
  53. package/dist/cli/{index-21115szq.js → index-wy2f83j5.js} +5 -5
  54. package/dist/cli/{index-dzyjb33e.js → index-zzhyws9g.js} +1 -1
  55. package/dist/cli/index.js +25 -25
  56. package/dist/cli/{knowledge-escalator-ta3xjrdj.js → knowledge-escalator-e815bveb.js} +7 -7
  57. package/dist/cli/{knowledge-events-dk6banmz.js → knowledge-events-dz2tyhpw.js} +5 -5
  58. package/dist/cli/{knowledge-link-mm1w967j.js → knowledge-link-zr40rnwr.js} +4 -4
  59. package/dist/cli/{knowledge-store-h3jz10bv.js → knowledge-store-97qr7m9k.js} +5 -5
  60. package/dist/cli/{knowledge-validator-jsz1dxkr.js → knowledge-validator-xsetvy4v.js} +8 -8
  61. package/dist/cli/{pending-delegations-qajsxct0.js → pending-delegations-0h5b18p7.js} +3 -3
  62. package/dist/cli/{pr-subscriptions-qhr41epq.js → pr-subscriptions-jn0h047q.js} +3 -3
  63. package/dist/cli/{runner-2v413r74.js → runner-deeswadt.js} +5 -5
  64. package/dist/cli/{scan-cursor-48gwzh9c.js → scan-cursor-129fwf7e.js} +6 -6
  65. package/dist/cli/{schema-f937b9cs.js → schema-3xdza5gg.js} +1 -1
  66. package/dist/cli/{scope-persistence-h2fpgxww.js → scope-persistence-5xc9ntdh.js} +4 -4
  67. package/dist/cli/{skill-generator-4p10ktw1.js → skill-generator-8zhprasg.js} +9 -9
  68. package/dist/cli/{telemetry-859khp82.js → telemetry-6678gya0.js} +1 -1
  69. package/dist/cli/{worktree-collision-ownership-13btcj9g.js → worktree-collision-ownership-wt7cc850.js} +3 -3
  70. package/dist/config/schema.d.ts +10 -0
  71. package/dist/hooks/context-budget.d.ts +8 -0
  72. package/dist/hooks/delegation-gate.d.ts +12 -1
  73. package/dist/hooks/gate-denial-tracker.d.ts +175 -0
  74. package/dist/hooks/guardrails/execution-episode.d.ts +41 -0
  75. package/dist/hooks/guardrails/execution-stall.d.ts +285 -0
  76. package/dist/hooks/guardrails/internals-guard.d.ts +117 -0
  77. package/dist/hooks/guardrails/messages-transform.d.ts +85 -0
  78. package/dist/hooks/index.d.ts +1 -1
  79. package/dist/hooks/message-priority.d.ts +62 -4
  80. package/dist/hooks/trajectory-logger.d.ts +76 -0
  81. package/dist/index.js +477 -463
  82. package/dist/memory/schema.d.ts +3 -3
  83. package/dist/prm/index.d.ts +2 -0
  84. package/dist/state.d.ts +32 -0
  85. package/dist/telemetry.d.ts +66 -1
  86. package/dist/types/events.d.ts +14 -1
  87. package/package.json +2 -1
@@ -0,0 +1,285 @@
1
+ /**
2
+ * ARCHITECT EXECUTION-STALL DETECTOR (issue #2063, workstream B5)
3
+ *
4
+ * Shape (ii) of the reported loop is an architect that keeps making tool calls
5
+ * — reads, greps, bash probes — while nothing in the world changes: no
6
+ * delegation completes, no file is written, no task status moves. The no-op
7
+ * detector (B2) observes this but is advisory by design, because read-only
8
+ * modes legitimately make hundreds of non-write calls. This module is the hard
9
+ * lever, and the thing that makes it safe to be hard is the EPISODE.
10
+ *
11
+ * ── Episode ───────────────────────────────────────────────────────────────
12
+ * An episode ARMS when the session actually attempts execution work:
13
+ * (a) a `Task` dispatch to a mutating/verifying role is ATTEMPTED — counted
14
+ * on the ATTEMPT, in `tool.execute.before`, so a dispatch that a LATER
15
+ * gate denies (the motivating `ACCEPTANCE_FIELD_REQUIRED` loop) still
16
+ * arms the episode. Guardrails' toolBefore runs at `src/index.ts:2710`,
17
+ * the delegation gate at `:2719`, so ordering guarantees this; or
18
+ * (b) `update_task_status(..., in_progress)` succeeds.
19
+ *
20
+ * An episode DISARMS on either of two conditions:
21
+ *
22
+ * (1) IDLENESS, not elapsed time since arming: `execution_stall_episode_minutes`
23
+ * with NO TOOL CALLS AT ALL. This distinction is the whole point (critic
24
+ * round-3 fix 1). Keying the lapse to arming time would let an hour-long
25
+ * slow stall time out and reset before it ever reached the hard rung;
26
+ * keying it to idleness means continuous non-progress activity KEEPS the
27
+ * episode armed and the counter climbing, while a genuinely abandoned
28
+ * episode ages out.
29
+ *
30
+ * (2) NO OPEN TASK: the plan carries no task with status `in_progress`
31
+ * (reviewer round-4 fix, REQUIRED 1). Idleness alone was not enough: a
32
+ * CONTINUOUSLY-ACTIVE architect that finishes its execution phase with a
33
+ * final `update_task_status(..., completed)` and then flows into commit /
34
+ * CI / reporting work never goes idle, so it kept accumulating
35
+ * non-progress `bash`/`read` calls with no reachable progress event and
36
+ * was hard-denied at 60 while doing exactly the right thing. This
37
+ * condition is the symmetric counterpart of arming path (b): an episode
38
+ * that arms when a task opens must end when no task is open.
39
+ *
40
+ * Lapse (1) is evaluated LAZILY on the next observed tool call. There are no
41
+ * timers — invariant 1 forbids background work on the plugin, and a timer
42
+ * would keep the process alive. The observable consequence: an episode that
43
+ * went idle stays nominally armed until one more tool call arrives, at which
44
+ * point it disarms BEFORE that call is counted.
45
+ *
46
+ * Disarm (2) is evaluated at two places, both in `tool.execute.after`:
47
+ * - immediately after a successful `update_task_status` to a NON-`in_progress`
48
+ * status (the in-band signal — `plan.json` is current there, because
49
+ * `plan/manager.savePlan` writes the projection as its LAST step
50
+ * (`src/plan/manager.ts:215` documents this) and `updateTaskStatus` awaits
51
+ * it (`:2144`) before `executeUpdateTaskStatus` builds the `success: true`
52
+ * payload (`src/tools/update-task-status.ts:1329`) this hook reads); and
53
+ * - inside the periodic workspace probe, so an OUT-OF-BAND plan change (the
54
+ * plan file edited directly, a sub-agent path that settles the last task)
55
+ * eventually disarms too. The probe already does I/O once per
56
+ * WORKSPACE_PROBE_EVERY_CALLS calls, so the extra read is bounded.
57
+ *
58
+ * DEVIATION (deliberate): a MISSING, unreadable, or malformed `plan.json`
59
+ * answers `'unknown'`, and `'unknown'` does NOT disarm. Read literally, "no
60
+ * task with status in_progress" is also true of a plan that does not exist —
61
+ * but a session with no plan cannot be "finishing its execution phase", and
62
+ * treating an unreadable plan as a disarm would silently delete the lever for
63
+ * every plan-less architect loop. Only POSITIVE evidence (a parsed plan whose
64
+ * tasks are all non-`in_progress`) disarms.
65
+ *
66
+ * ── Progress ──────────────────────────────────────────────────────────────
67
+ * A progress event resets the counter and clears any active denial rung, but
68
+ * KEEPS the episode armed:
69
+ * - successful completion of a `Task` dispatch to a mutating/verifying role.
70
+ * Read-only roles (`explorer`, `sme`) deliberately do NOT count: otherwise
71
+ * "delegate the spelunking" is a trivial escape from the whole lever.
72
+ * - any file-write tool success (same `WRITE_TOOL_NAMES` set as B2).
73
+ * - `update_task_status` success, ANY status.
74
+ * - a periodic workspace-diff probe showing new changes.
75
+ *
76
+ * ── Ladder ────────────────────────────────────────────────────────────────
77
+ * `execution_stall_warn_calls` (default 30) → strong advisory, once/streak.
78
+ * `execution_stall_stop_calls` (default 60) → HARD DENY of non-productive
79
+ * tools ONLY (read/glob/grep/bash/shell). `Task`, `update_task_status`,
80
+ * and every plan/status/query/swarm tool stay open BY CONSTRUCTION: the
81
+ * deny set is a closed allowlist of denied names, not an exclusion list.
82
+ *
83
+ * ── Scope ─────────────────────────────────────────────────────────────────
84
+ * Architect sessions only. A subagent has a real budget window and the
85
+ * existing circuit breaker; this lever exists for the session that is exempt
86
+ * from both. Determined the same way every other guardrail does it
87
+ * (`swarmState.activeAgent` → `stripKnownSwarmPrefix` → `ORCHESTRATOR_NAME`),
88
+ * falling back to the session's own `agentName`.
89
+ *
90
+ * Denials are thrown from the fail-closed chain, so they flow through the B1
91
+ * gate-denial tracker: an architect that keeps retrying the denied read
92
+ * escalates to the STOP directive.
93
+ */
94
+ import type { BackgroundWorkspaceSnapshot } from '../../background/pending-delegations.js';
95
+ import { captureWorkspaceSnapshot, changedFilesSinceSnapshot } from '../../background/workspace-snapshot.js';
96
+ /** Default advisory rung (mirrors `guardrails.execution_stall_warn_calls`). */
97
+ export declare const DEFAULT_EXECUTION_STALL_WARN_CALLS = 30;
98
+ /** Default hard rung (mirrors `guardrails.execution_stall_stop_calls`). */
99
+ export declare const DEFAULT_EXECUTION_STALL_STOP_CALLS = 60;
100
+ /** Default idleness lapse (mirrors `guardrails.execution_stall_episode_minutes`). */
101
+ export declare const DEFAULT_EXECUTION_STALL_EPISODE_MINUTES = 30;
102
+ /**
103
+ * Canonical roles whose dispatch arms an episode and whose COMPLETION counts as
104
+ * progress.
105
+ *
106
+ * `security_reviewer` is the `qa-gate-pipeline.ts` id for the security review
107
+ * lane. It is not itself a member of `ALL_AGENT_NAMES`, so the canonicalizer
108
+ * resolves `security_reviewer` / `security-reviewer` to `reviewer` via its
109
+ * longest-suffix scan — which is already in this set. The explicit entry is
110
+ * kept so the set reads as the spec states it and so a future promotion of
111
+ * `security_reviewer` to a canonical role needs no change here.
112
+ */
113
+ export declare const MUTATING_DELEGATION_ROLES: ReadonlySet<string>;
114
+ /**
115
+ * The ONLY tools the hard rung denies.
116
+ *
117
+ * A closed allowlist of DENIED names — never an exclusion list — so a tool that
118
+ * did not exist when this was written can never accidentally become deniable.
119
+ * `shell` accompanies `bash` for the same reason as in `internals-guard.ts`:
120
+ * `tool-before.ts` treats the pair as one tool everywhere else.
121
+ */
122
+ export declare const EXECUTION_STALL_DENIED_TOOLS: ReadonlySet<string>;
123
+ /**
124
+ * Bound on tracked sessions (invariant 8). Same order as the no-op detector's
125
+ * `MAX_TRACKED_NO_OP_SESSIONS`.
126
+ */
127
+ export declare const MAX_TRACKED_STALL_SESSIONS = 200;
128
+ interface ExecutionStallState {
129
+ armed: boolean;
130
+ /** Non-progress tool calls since the last progress event. Armed only. */
131
+ nonProgressCalls: number;
132
+ /** Wall clock of the last observed tool call; drives the idleness lapse. */
133
+ lastToolCallAt: number;
134
+ /** Advisory rung latch — one advisory per non-progress streak. */
135
+ warnIssued: boolean;
136
+ /** Hard-rung telemetry latch — one event per non-progress streak. */
137
+ denyTelemetryIssued: boolean;
138
+ /** Baseline for the workspace-diff probe; null until first captured. */
139
+ workspaceBaseline: BackgroundWorkspaceSnapshot | null;
140
+ /** Set on arm; the baseline is captured on the next `toolAfter`. */
141
+ needsWorkspaceBaseline: boolean;
142
+ /** Calls counted since the last workspace probe. */
143
+ callsSinceWorkspaceProbe: number;
144
+ }
145
+ /**
146
+ * True when `sessionID` is the architect session.
147
+ *
148
+ * Matches the convention in `messages-transform.ts:302-307` and
149
+ * `tool-before.ts:1838-1850`: `activeAgent` wins, the session's own `agentName`
150
+ * is the fallback, and an unknown session is NOT the architect (fail open — a
151
+ * lever that cannot identify its subject stays silent).
152
+ */
153
+ export declare function isArchitectStallSession(sessionID: string): boolean;
154
+ /**
155
+ * Materialize the architect's `agentSessions` entry.
156
+ *
157
+ * DEVIATION, recorded deliberately: `execution-episode.ts` documents that its
158
+ * setter no-ops for an unknown session so a containment lever cannot
159
+ * materialize session state as a side effect. But the architect legitimately
160
+ * has NO `agentSessions` entry when guardrails' `toolBefore` runs —
161
+ * `resolveSessionAndWindow` returns `null` for it before ever calling
162
+ * `ensureAgentSession` (`tool-before.ts:1842`), and `src/index.ts` only ensures
163
+ * one later in the chain (`:2804`, `:2877`) and conditionally. Without this,
164
+ * arming would silently write nothing, B3's consumer would never fire, and
165
+ * `pushAdvisory` would have no queue — i.e. the producer would be unwired.
166
+ *
167
+ * The agent name is passed ONLY when creating a new entry. Passing it to an
168
+ * existing session triggers `ensureAgentSession`'s rename path
169
+ * (`telemetry.agentActivated`, `delegationActive = false`, circuit reset),
170
+ * which this lever must never cause.
171
+ */
172
+ declare function ensureArchitectSession(sessionID: string): void;
173
+ /**
174
+ * Canonical role of a `Task` dispatch, or `null` when the call is not a
175
+ * delegation. Uses the canonical resolver rather than string literals so
176
+ * prefixed names (`mega_coder`) and hyphenated aliases resolve correctly.
177
+ */
178
+ export declare function canonicalDispatchRole(tool: string, args: unknown): string | null;
179
+ /**
180
+ * Tri-state answer to "does the plan still carry an OPEN (`in_progress`) task?".
181
+ *
182
+ * `'unknown'` is NOT a synonym for `'none'` — see the DEVIATION note in the
183
+ * module header. Only `'none'` disarms an episode.
184
+ */
185
+ export type PlanOpenTaskState = 'open' | 'none' | 'unknown';
186
+ /**
187
+ * Read `.swarm/plan.json` and report whether any task is still `in_progress`.
188
+ *
189
+ * Reads the PROJECTION rather than replaying the ledger, matching the existing
190
+ * cheap in-hook predicates (`delegation-gate.ts:2046`,
191
+ * `context-capsule-inject.ts:86`): this is a containment predicate, not a plan
192
+ * mutation, and invariant 5 only forbids *writing* outside the ledger path.
193
+ * Synchronous and never throws — the callers are inside a `tool.execute.after`
194
+ * hook that must not become a failure surface.
195
+ */
196
+ export declare function readPlanOpenTaskState(directory: string): PlanOpenTaskState;
197
+ export interface ExecutionStallOptions {
198
+ /**
199
+ * `guardrails.enabled`. Mirrors `GateDenialOptions.enabled` (B1): when a
200
+ * user turns guardrails off, this lever must be fully inert — no arming, no
201
+ * counting, no advisory, no denial.
202
+ */
203
+ enabled?: boolean;
204
+ /** `guardrails.execution_stall_warn_calls` */
205
+ warnCalls?: number;
206
+ /** `guardrails.execution_stall_stop_calls` */
207
+ stopCalls?: number;
208
+ /** `guardrails.execution_stall_episode_minutes` */
209
+ episodeMinutes?: number;
210
+ }
211
+ /**
212
+ * The advisory text at the warn rung. Exported so tests pin the wording.
213
+ */
214
+ export declare function executionStallAdvisoryText(count: number): string;
215
+ /**
216
+ * The denial text at the hard rung. Leading token is the `EXECUTION_STALL`
217
+ * code that B1's `deriveGateDenialCode` keys its streak on.
218
+ */
219
+ export declare function executionStallDenialText(count: number, normalizedTool: string): string;
220
+ /**
221
+ * `tool.execute.before`, EARLY (before any guardrails gate can throw).
222
+ *
223
+ * Pure bookkeeping: lapse evaluation, episode arming, non-progress counting,
224
+ * and the advisory rung. NEVER throws — the denial is a separate call
225
+ * ({@link enforceExecutionStallDenial}) placed in the handler tail so the
226
+ * circuit-breaker accounting in `tool-before.ts` still runs for a denied call,
227
+ * exactly as C3 required for the PRM hard stop.
228
+ */
229
+ export declare function observeExecutionStallToolCall(params: {
230
+ sessionID: string;
231
+ tool: string;
232
+ args: unknown;
233
+ callID: string;
234
+ options?: ExecutionStallOptions;
235
+ }): void;
236
+ /**
237
+ * `tool.execute.before`, TAIL. Throws `EXECUTION_STALL: …` when the hard rung
238
+ * is active and the tool is one of the non-productive five.
239
+ *
240
+ * Placed above `if (!resolved) return;` in `tool-before.ts` for the same reason
241
+ * the PRM hard stop is: the architect IS the `resolved === null` case, so a
242
+ * denial below that early return would not exist for the only session this
243
+ * lever targets.
244
+ */
245
+ export declare function enforceExecutionStallDenial(params: {
246
+ sessionID: string;
247
+ tool: string;
248
+ options?: ExecutionStallOptions;
249
+ }): void;
250
+ /**
251
+ * `tool.execute.after`. Records progress events, arms on a successful
252
+ * `update_task_status(in_progress)`, and runs the periodic workspace probe.
253
+ * NEVER throws.
254
+ */
255
+ export declare function recordExecutionStallToolAfter(params: {
256
+ sessionID: string;
257
+ tool: string;
258
+ callID: string;
259
+ args?: Record<string, unknown>;
260
+ output: unknown;
261
+ directory: string;
262
+ options?: ExecutionStallOptions;
263
+ }): void;
264
+ /**
265
+ * Test/DI seam (AGENTS.md invariant 7). `now` is the fake-clock seam the
266
+ * idleness-lapse tests drive; the two workspace functions are indirected so a
267
+ * test never spawns git, and `readPlanOpenTaskState` so a test can drive the
268
+ * disarm edge without a plan fixture (the disarm tests exercise the REAL reader
269
+ * against a real `.swarm/plan.json` as well).
270
+ */
271
+ export declare const _internals: {
272
+ now: () => number;
273
+ captureWorkspaceSnapshot: typeof captureWorkspaceSnapshot;
274
+ changedFilesSinceSnapshot: typeof changedFilesSinceSnapshot;
275
+ ensureArchitectSession: typeof ensureArchitectSession;
276
+ readPlanOpenTaskState: typeof readPlanOpenTaskState;
277
+ };
278
+ export declare const _test_exports: {
279
+ readonly WORKSPACE_PROBE_EVERY_CALLS: 10;
280
+ readonly stateCount: () => number;
281
+ readonly peekState: (sessionID: string) => Readonly<ExecutionStallState> | undefined;
282
+ readonly pendingDispatchRoleCount: () => number;
283
+ readonly reset: () => void;
284
+ };
285
+ export {};
@@ -0,0 +1,117 @@
1
+ /**
2
+ * PLUGIN-INTERNALS READ GUARD (issue #2063, workstream B4)
3
+ *
4
+ * When a fail-closed gate denies a dispatch, the observed failure mode is an
5
+ * agent that goes looking for the answer inside the *plugin's own installed
6
+ * files* — `node_modules/opencode-swarm/**`, `~/.cache/opencode/packages/...`,
7
+ * `dist/index.js`. That search can never succeed: the installed bundle is not
8
+ * the state the error names, it is not editable from the user's workspace, and
9
+ * every minute spent in it is a minute the blocker is not surfaced to the user.
10
+ *
11
+ * This guard is the mechanical brake for that specific behaviour. It denies
12
+ * `read` / `glob` / `grep` / `bash` calls whose RESOLVED target lands inside the
13
+ * installed package directory.
14
+ *
15
+ * DESIGN CONSTRAINTS (all three are load-bearing):
16
+ *
17
+ * 1. **Explicitly best-effort.** `cd` chains, shell variables, wrapper CLIs and
18
+ * any other indirection evade it. The durable coverage for evasive
19
+ * spelunking is B5 (execution-stall) plus the A4 prompt rule; this guard
20
+ * exists to catch the overwhelmingly common direct form cheaply. It must
21
+ * therefore FAIL OPEN on every kind of resolution uncertainty — a false
22
+ * denial of a legitimate read is far worse than a missed evasion.
23
+ *
24
+ * 2. **Self-development exemption.** When the workspace root IS the
25
+ * opencode-swarm repository, the "installed package" and "the code the user
26
+ * is working on" are the same tree, and the guard would block the plugin's
27
+ * own maintainers from reading their own source. The exemption is keyed on
28
+ * `package.json#name === 'opencode-swarm'` at the workspace root.
29
+ *
30
+ * 3. **Runtime-derived package root.** The root is derived from this module's
31
+ * own location (`import.meta.url`), walking up to the nearest directory
32
+ * whose `package.json` is named `opencode-swarm`. It is NOT hardcoded and
33
+ * NOT derived from the workspace, so it is correct under every cache layout
34
+ * in AGENTS.md invariant 12 and under a plain `node_modules` install.
35
+ *
36
+ * The denial is thrown from the guardrails `tool.execute.before` chain, so it
37
+ * flows through the B1 gate-denial tracker automatically: an agent that keeps
38
+ * retrying the same spelunking read escalates to the STOP directive.
39
+ */
40
+ /**
41
+ * The exact denial text. Exported so tests pin the wording rather than a
42
+ * paraphrase, and so the leading `SWARM_INTERNALS_OFF_LIMITS:` token — which
43
+ * `deriveGateDenialCode` (B1) uses as the streak key — cannot drift.
44
+ */
45
+ export declare const SWARM_INTERNALS_DENIAL_MESSAGE = "SWARM_INTERNALS_OFF_LIMITS: the swarm plugin's installed files are never the fix for a gate error. Fix the dispatch/state the error names, or report the blocker to the user.";
46
+ /**
47
+ * Locate the installed opencode-swarm package root from this module's own
48
+ * location. Computed once (including the `null` "could not determine" result)
49
+ * and cached for the process lifetime — the plugin's own location cannot change
50
+ * while it is loaded.
51
+ */
52
+ declare function resolvePackageRoot(): string | null;
53
+ /**
54
+ * True when `directory` is an opencode-swarm checkout, i.e. the guard must be
55
+ * inert. Cached per workspace root with a hard size cap (invariant 8).
56
+ */
57
+ declare function isSelfDevelopmentWorkspace(directory: string): boolean;
58
+ /**
59
+ * True when `target` is `root` or lies beneath it. Pure string containment on
60
+ * already-resolved paths — no `realpath`, because a symlink probe is filesystem
61
+ * I/O on a hot hook path and this guard is explicitly best-effort.
62
+ */
63
+ export declare function isInsidePackageRoot(root: string, target: string): boolean;
64
+ /**
65
+ * Extract the path candidates this guard is willing to reason about.
66
+ *
67
+ * Returns ONLY candidates that are unambiguously absolute (or `~`-anchored).
68
+ * A relative path is deliberately dropped: resolving it would require guessing
69
+ * a base directory, and a wrong guess produces a false denial.
70
+ */
71
+ export declare function extractGuardedPathCandidates(normalizedTool: string, args: unknown): string[];
72
+ export interface InternalsGuardOptions {
73
+ /**
74
+ * `guardrails.enabled`. Mirrors `GateDenialOptions.enabled` (B1): when the
75
+ * user turns guardrails off, every guardrails behaviour must be inert or the
76
+ * config surface lies. Defaults to enabled.
77
+ */
78
+ enabled?: boolean;
79
+ }
80
+ /**
81
+ * Deny a read/glob/grep/bash call that targets the installed plugin package.
82
+ *
83
+ * Throws `SWARM_INTERNALS_OFF_LIMITS: …` on a positive match; returns normally
84
+ * (fail open) in every other case, including:
85
+ * - guardrails disabled,
86
+ * - the tool is not one of the guarded five,
87
+ * - the workspace is an opencode-swarm checkout,
88
+ * - the package root could not be derived,
89
+ * - no unambiguously-absolute candidate could be extracted.
90
+ */
91
+ export declare function enforceInternalsGuard(params: {
92
+ sessionID: string;
93
+ tool: string;
94
+ args: unknown;
95
+ directory: string;
96
+ options?: InternalsGuardOptions;
97
+ }): void;
98
+ /**
99
+ * Test/DI seam (AGENTS.md invariant 7). `moduleUrl`, `existsSync`,
100
+ * `readFileSync` and `homedir` are indirected so a test can stand up a FAKE
101
+ * installed-package tree without `mock.module` on `node:fs`; the two resolvers
102
+ * are indirected so a test can pin a package root without a real install.
103
+ *
104
+ * `resetCaches` exists because both resolutions are memoized for the process
105
+ * lifetime — without it a test that swaps `moduleUrl` would read a value
106
+ * computed by an earlier test.
107
+ */
108
+ export declare const _internals: {
109
+ moduleUrl: () => string;
110
+ existsSync: (p: string) => boolean;
111
+ readFileSync: (p: string, enc: "utf-8") => string;
112
+ homedir: () => string;
113
+ resolvePackageRoot: typeof resolvePackageRoot;
114
+ isSelfDevelopmentWorkspace: typeof isSelfDevelopmentWorkspace;
115
+ resetCaches: () => void;
116
+ };
117
+ export {};
@@ -21,11 +21,29 @@ export interface MessagesTransformContext {
21
21
  requireReviewerAndTestEngineer: boolean;
22
22
  /** Shared consecutiveNoToolTurns Map (also used by toolBefore) */
23
23
  consecutiveNoToolTurns: Map<string, number>;
24
+ /**
25
+ * Issue #2063 B3 — sessionID → id of the most recent assistant message that
26
+ * was counted by the MEDIUM band of the runaway detector.
27
+ *
28
+ * Keyed on the host message id and NEVER on an array index: compaction
29
+ * rewrites the window, so an index recorded on one turn points at a
30
+ * different message on the next. The marker exists so the counter can be
31
+ * reset when the USER speaks after the counted turn — a user reply means the
32
+ * "model is narrating instead of acting" hypothesis is no longer the right
33
+ * explanation for the silence.
34
+ */
35
+ lastCountedAssistantMsgId: Map<string, string>;
24
36
  }
25
37
  type ChatMessageLike = {
38
+ /**
39
+ * `id` is a real host-message field (see `src/hooks/pr-workflow-auto-wake.ts`,
40
+ * which reads `info.id` off host session/message envelopes). It is optional
41
+ * here because synthetic messages this plugin itself unshifts carry no id.
42
+ */
26
43
  info?: {
27
44
  role?: string;
28
45
  sessionID?: string;
46
+ id?: string;
29
47
  };
30
48
  parts?: Array<{
31
49
  type?: string;
@@ -39,6 +57,72 @@ type ChatMessageLike = {
39
57
  * never contained, making the guard permanently inert.
40
58
  */
41
59
  export declare const RUNAWAY_OUTPUT_ADVISORY_MARKER = "Model is generating analysis without taking action";
60
+ /**
61
+ * Lower bound of the runaway detector's MEDIUM band (issue #2063 B3), and the
62
+ * same threshold below which a tool-less assistant turn is treated as a short
63
+ * acknowledgement and RESETS the counter.
64
+ *
65
+ * One constant, two call sites, deliberately: the reset boundary and the
66
+ * counting boundary must be the same number or the band between them either
67
+ * double-counts or silently swallows turns. Exported so tests pin the real
68
+ * value instead of a hand-copied literal.
69
+ *
70
+ * The medium band only counts while an execution episode is armed
71
+ * ({@link isExecutionEpisodeArmed}). Ordinary conversation produces plenty of
72
+ * 200–4000 char replies with no tool calls, and treating those as a runaway is
73
+ * the false-positive class this gate exists to prevent. Above 4000 chars the
74
+ * behaviour is unchanged and NOT episode-gated.
75
+ */
76
+ export declare const RUNAWAY_MEDIUM_MIN = 200;
77
+ /**
78
+ * Issue #2063 B3 — bound on {@link MessagesTransformContext.lastCountedAssistantMsgId}.
79
+ *
80
+ * AGENTS.md invariant 8: session-keyed state needs an explicit eviction
81
+ * strategy. Least-recently-written wins (delete-before-set), matching the no-op
82
+ * detector's LRU in `guardrails/index.ts` and for the same reason — plain
83
+ * insertion order would evict the long-lived architect session first.
84
+ */
85
+ export declare const MAX_TRACKED_COUNTED_ASSISTANT_MSGS = 200;
86
+ declare function rememberCountedAssistantMsg(map: Map<string, string>, sessionId: string, messageId: string): void;
87
+ /**
88
+ * Prefix identifying a PRM course correction in the advisory queue
89
+ * (issue #2063 C1).
90
+ *
91
+ * Shared with the producer's test so the forward filter and the rendered
92
+ * dedupe key (`[prm:<pattern>:<level>]`, built in `src/prm/index.ts`) cannot
93
+ * drift apart. This file already carries the scar of exactly that drift:
94
+ * {@link RUNAWAY_OUTPUT_ADVISORY_MARKER} exists because a predicate once tested
95
+ * for a string no producer emitted, leaving the guard permanently inert. A
96
+ * hand-copied literal in a fixture would re-arm the same failure.
97
+ */
98
+ export declare const PRM_ADVISORY_FORWARD_PREFIX = "[prm:";
99
+ /**
100
+ * Tier-0 test seam (zero mocks) for the pure LRU helper above. Production code
101
+ * calls the local binding; this exists so the eviction bound required by
102
+ * AGENTS.md invariant 8 can be asserted directly instead of by driving 200+
103
+ * sessions through the whole handler.
104
+ */
105
+ export declare const _test_exports: {
106
+ rememberCountedAssistantMsg: typeof rememberCountedAssistantMsg;
107
+ };
108
+ /**
109
+ * Drain-level byte budget for the architect [ADVISORIES] block (issue #1976).
110
+ *
111
+ * Advisories are prepended AFTER token accounting and therefore escape the
112
+ * context-budget handler; without a bound, a single turn with many producers
113
+ * (or a handful of large ones) can flood the architect prompt — the same
114
+ * failure mode that produced the PR_REVIEW banner flood (55.3% of non-blank
115
+ * lines in a real transcript).
116
+ *
117
+ * This is a defense-in-depth backstop behind the per-producer
118
+ * `pushAdvisory` helper (dedupe + length cap). When the joined block exceeds
119
+ * the budget, the OLDEST entries are dropped (keep-latest) because high-value
120
+ * advisories tend to arrive LATE in a turn — "keep earliest" is a priority
121
+ * inversion called out in the issue. Truncation is disclosed in the block
122
+ * header so the architect knows recent items were retained over earlier ones.
123
+ */
124
+ export declare const MAX_ADVISORY_BLOCK_BYTES = 6000;
125
+ export declare const ADVISORY_TRUNCATION_NOTE = "[advisory block truncated to keep highest-value recent items]";
42
126
  /**
43
127
  * Bound a set of advisory strings to a total byte budget, keeping the latest
44
128
  * (dropping oldest from the front). Returns the kept entries and whether any
@@ -64,6 +148,7 @@ export declare function createMessagesTransformHandler(ctx: MessagesTransformCon
64
148
  role: string;
65
149
  agent?: string;
66
150
  sessionID?: string;
151
+ id?: string;
67
152
  };
68
153
  parts: Array<{
69
154
  type: string;
@@ -10,7 +10,7 @@ export { createDelegationTrackerHook } from './delegation-tracker';
10
10
  export { extractCurrentPhase, extractCurrentPhaseFromPlan, extractCurrentTask, extractCurrentTaskFromPlan, extractDecisions, extractIncompleteTasks, extractIncompleteTasksFromPlan, extractPatterns, } from './extractors';
11
11
  export { createFullAutoInterceptHook } from './full-auto-intercept';
12
12
  export { checkFileAuthority, createGuardrailsHooks, DEFAULT_AGENT_AUTHORITY_RULES, } from './guardrails';
13
- export { classifyMessage, classifyMessages, containsPlanContent, isDuplicateToolRead, isStaleError, isToolResult, MessagePriority, type MessagePriorityType, type MessageWithParts, } from './message-priority';
13
+ export { classifyMessage, classifyMessages, containsPlanContent, getCompletedToolOutputs, getToolNames, getToolParts, isDuplicateToolRead, isStaleError, isToolResult, MessagePriority, type MessagePriorityType, type MessageWithParts, } from './message-priority';
14
14
  export { consolidateSystemMessages } from './messages-transform';
15
15
  export { extractModelInfo, NATIVE_MODEL_LIMITS, PROVIDER_CAPS, resolveModelLimit, } from './model-limits';
16
16
  export { type CuratorDelegateFactory, createPhaseMonitorHook, } from './phase-monitor';
@@ -36,13 +36,27 @@ interface MessageInfo {
36
36
  sessionID?: string;
37
37
  modelID?: string;
38
38
  providerID?: string;
39
- toolName?: string;
40
- toolArgs?: unknown;
41
39
  [key: string]: unknown;
42
40
  }
43
41
  interface MessagePart {
44
42
  type?: string;
45
43
  text?: string;
44
+ tool?: string;
45
+ state?: ToolStateLike;
46
+ [key: string]: unknown;
47
+ }
48
+ /**
49
+ * Minimal shape of a `ToolPart.state` value. The OpenCode SDK `ToolState` is a
50
+ * discriminated union on `status`; only `completed` carries `output` and only
51
+ * `error` carries `error`. We read defensively and never mutate `state` in
52
+ * place (see context-budget.ts mask/prune logic — those replace the whole
53
+ * ToolPart with a synthetic text part rather than corrupting the union).
54
+ */
55
+ interface ToolStateLike {
56
+ status?: string;
57
+ output?: string;
58
+ error?: string;
59
+ input?: Record<string, unknown>;
46
60
  [key: string]: unknown;
47
61
  }
48
62
  export interface MessageWithParts {
@@ -58,16 +72,60 @@ export interface MessageWithParts {
58
72
  */
59
73
  export declare function containsPlanContent(text: string): boolean;
60
74
  /**
61
- * Checks if a message is a tool result (assistant message with tool call).
75
+ * Returns all `ToolPart` objects in a message's `parts[]`.
76
+ *
77
+ * Per the OpenCode SDK contract (`@opencode-ai/sdk` v1+v2), tool results are
78
+ * delivered as `ToolPart` objects (`part.type === 'tool'`, with `part.tool`
79
+ * and `part.state`) inside a message's `parts[]` array — they are NOT separate
80
+ * `role:'tool'` messages and do not carry an `info.toolName` field. A message
81
+ * may contain multiple tool parts (parallel tool calls).
82
+ *
83
+ * @param message - The message to inspect
84
+ * @returns Array of tool parts (empty if none / malformed)
85
+ */
86
+ export declare function getToolParts(message: MessageWithParts): MessagePart[];
87
+ /**
88
+ * Returns the completed tool outputs in a message — the countable, maskable
89
+ * payloads (`part.state.output` where `state.status === 'completed'`).
90
+ *
91
+ * Pending/running tools have no output; error tools expose `state.error`
92
+ * (diagnostic signal — never masked, only routed via the stale-error →
93
+ * DISPOSABLE pruning path). Completed outputs are the heavy payloads the
94
+ * context budget must count and may mask/prune.
95
+ *
96
+ * @param message - The message to inspect
97
+ * @returns Array of `{ part, output }` for each completed tool part
98
+ */
99
+ export declare function getCompletedToolOutputs(message: MessageWithParts): Array<{
100
+ part: MessagePart;
101
+ output: string;
102
+ }>;
103
+ /**
104
+ * Returns the tool names (`part.tool`) for every tool part in a message.
105
+ *
106
+ * @param message - The message to inspect
107
+ * @returns Array of tool name strings (empty if none)
108
+ */
109
+ export declare function getToolNames(message: MessageWithParts): string[];
110
+ /**
111
+ * Checks if a message carries at least one tool result (a `ToolPart` in its
112
+ * `parts[]`).
113
+ *
114
+ * This detects the real OpenCode SDK shape. The legacy `info.toolName` field
115
+ * never existed on production payloads and was removed (issue #2068).
62
116
  *
63
117
  * @param message - The message to check
64
- * @returns true if the message appears to be a tool result
118
+ * @returns true if the message contains at least one tool part
65
119
  */
66
120
  export declare function isToolResult(message: MessageWithParts): boolean;
67
121
  /**
68
122
  * Checks if two consecutive tool read calls are duplicates
69
123
  * (same tool with same first argument).
70
124
  *
125
+ * Compares the first tool part in each message (`part.tool` name and the first
126
+ * value of `part.state.input`). Two messages are duplicate reads when both
127
+ * call a read tool whose name contains "read" with the same first input value.
128
+ *
71
129
  * @param current - The current message
72
130
  * @param previous - The previous message
73
131
  * @returns true if this is a duplicate tool read