@ask-llm/plugin 0.15.0 → 0.16.2
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/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/CHANGELOG.md +966 -0
- package/README.md +2 -0
- package/agents/brainstorm-coordinator.md +1 -1
- package/agents/gemini-reviewer.md +1 -1
- package/dist/antigravity-run.js +0 -0
- package/dist/brainstorm-run.js +0 -0
- package/dist/codex-run.js +0 -0
- package/dist/grok-run.js +0 -0
- package/dist/ollama-run.js +0 -0
- package/dist/run.js +0 -0
- package/package.json +14 -14
- package/pi/extensions/provider-tools.ts +1 -1
- package/scripts/benchmark/README.md +114 -0
- package/scripts/benchmark/fixtures/README.md +29 -0
- package/scripts/codex-pair-debounce-worker.mjs +0 -0
- package/scripts/codex-pair-log.mjs +4 -13
- package/scripts/codex-pair-prompt-drain.mjs +1 -1
- package/scripts/codex-pair-session.mjs +2 -2
- package/scripts/codex-pair-stop-gate.mjs +8 -8
- package/scripts/codex-pair-watch.mjs +20 -39
- package/skills/gemini-review/SKILL.md +1 -1
- package/scripts/lib/broker-lifecycle.mjs +0 -575
- package/scripts/lib/broker-rpc.mjs +0 -203
- package/scripts/lib/broker-transport.mjs +0 -407
- package/scripts/lib/broker.mjs +0 -537
- package/scripts/lib/debounce-state.mjs +0 -208
- package/scripts/lib/parser.d.mts +0 -12
- package/scripts/lib/parser.mjs +0 -229
- package/scripts/lib/process.mjs +0 -56
- package/scripts/lib/prompt.d.mts +0 -8
- package/scripts/lib/prompt.mjs +0 -41
- package/scripts/lib/session-registry.mjs +0 -162
- package/scripts/lib/state.d.mts +0 -58
- package/scripts/lib/state.mjs +0 -733
- package/scripts/lib/stop-gate.mjs +0 -134
package/scripts/lib/broker.mjs
DELETED
|
@@ -1,537 +0,0 @@
|
|
|
1
|
-
// App-server broker interface (ADR-090, refined per ADR-093).
|
|
2
|
-
//
|
|
3
|
-
// Future home of the long-lived codex sidecar that replaces per-edit cold
|
|
4
|
-
// spawns with persistent JSON-RPC requests. Today this module defines the
|
|
5
|
-
// API surface and stable state-file layout — the implementation lands
|
|
6
|
-
// across Tier 3 follow-on milestones 2–4 tracked in docs/ROADMAP.md.
|
|
7
|
-
//
|
|
8
|
-
// **Status:** interface defined; implementation deferred. The `isBrokerEnabled`
|
|
9
|
-
// check returns false until ASK_CODEX_BROKER=1 ships alongside a real
|
|
10
|
-
// implementation. The hook MUST treat broker absence as a no-op and fall
|
|
11
|
-
// back to the existing per-edit codex spawn (ADR-077). This keeps the
|
|
12
|
-
// happy path byte-identical to v0.6.6 until the broker stabilizes.
|
|
13
|
-
//
|
|
14
|
-
// See ADR-090 for original design rationale (transport, lifecycle,
|
|
15
|
-
// failure modes, stale-daemon recovery). See ADR-093 for the protocol
|
|
16
|
-
// discovery findings that refined this interface against the real
|
|
17
|
-
// `codex app-server` JSON-RPC surface (codex-cli 0.130.0+):
|
|
18
|
-
// - Transport: unix:// (POSIX) / ws:// (Windows), via `--listen` flag
|
|
19
|
-
// - Handshake: JSON-RPC `initialize` with clientInfo
|
|
20
|
-
// - Per-review: `thread/start` (ephemeral) + `turn/start` (with
|
|
21
|
-
// outputSchema constraint) + listen for `turn/completed` notification
|
|
22
|
-
// - Cancellation: `turn/interrupt` on the in-flight turn id
|
|
23
|
-
// - Health probe: `model/list` (cheap) or `initialize` with deadline
|
|
24
|
-
|
|
25
|
-
import { join } from "node:path";
|
|
26
|
-
import { connectWebSocket } from "./broker-transport.mjs";
|
|
27
|
-
import { createRpcClient } from "./broker-rpc.mjs";
|
|
28
|
-
|
|
29
|
-
// State file under <markerDir>/.codex-pair/state/ (ADR-092).
|
|
30
|
-
export const BROKER_STATE_FILE = "broker.json";
|
|
31
|
-
export const BROKER_HEALTH_TIMEOUT_MS = 2000;
|
|
32
|
-
export const BROKER_SOCKET_PREFIX = "codex-pair-broker";
|
|
33
|
-
|
|
34
|
-
// Protocol version we target. Pinned so a codex CLI upgrade with breaking
|
|
35
|
-
// protocol changes is detected at handshake time rather than silently
|
|
36
|
-
// producing malformed requests. Verified empirically against codex-cli
|
|
37
|
-
// 0.130.0 via `codex app-server generate-json-schema`; ADR-093 documents
|
|
38
|
-
// the methods + notification stream we depend on.
|
|
39
|
-
export const BROKER_PROTOCOL_VERSION = "v2";
|
|
40
|
-
|
|
41
|
-
// JSON-RPC client → server methods codex-pair USES (subset of the 75
|
|
42
|
-
// available; see ADR-093). Pinning these here documents the contract and
|
|
43
|
-
// lets structural tests catch silent drift.
|
|
44
|
-
export const JSONRPC_METHODS = Object.freeze({
|
|
45
|
-
INITIALIZE: "initialize",
|
|
46
|
-
THREAD_START: "thread/start",
|
|
47
|
-
TURN_START: "turn/start",
|
|
48
|
-
TURN_INTERRUPT: "turn/interrupt",
|
|
49
|
-
MODEL_LIST: "model/list", // used for health probe (cheap, no side effects)
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// JSON-RPC server → client notifications codex-pair LISTENS for. Subset
|
|
53
|
-
// of the full event stream; we ignore the rest. `TURN_COMPLETED` is the
|
|
54
|
-
// terminal event that carries the final agent message (which carries the
|
|
55
|
-
// structured verdict when outputSchema is set).
|
|
56
|
-
export const JSONRPC_NOTIFICATIONS = Object.freeze({
|
|
57
|
-
TURN_COMPLETED: "turn/completed",
|
|
58
|
-
TURN_STARTED: "turn/started",
|
|
59
|
-
ITEM_AGENT_MESSAGE_DELTA: "item/agentMessage/delta", // streaming text
|
|
60
|
-
THREAD_TOKEN_USAGE_UPDATED: "thread/tokenUsage/updated", // cost tracking
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Build the JSON Schema we send as `outputSchema` on `turn/start`. Codex
|
|
64
|
-
// constrains the agent's final message to this shape per ADR-093, which
|
|
65
|
-
// means we get a structured verdict back without prose-parsing (per
|
|
66
|
-
// ADR-083's verdict contract). Centralized here so test fixtures and
|
|
67
|
-
// production code build the same schema.
|
|
68
|
-
// JSON Schema for `turn/start.outputSchema`. Harmonized in M3 to match
|
|
69
|
-
// the `parseConcernsJson` contract in `lib/parser.mjs` so the broker's
|
|
70
|
-
// structured output drops in to the existing per-edit hook flow without
|
|
71
|
-
// translation. Brainstorm-coordinator and codex-pair both flagged the
|
|
72
|
-
// prior mismatch (the original schema used `concerns: { high, med, low }`
|
|
73
|
-
// while the parser expects `findings: [{ severity, ... }]`).
|
|
74
|
-
//
|
|
75
|
-
// Shape matches `parser.mjs::parseConcernsJson`:
|
|
76
|
-
// { verdict: "clean" } → no concerns
|
|
77
|
-
// { verdict: "concerns", findings: [{ severity, body, file?, line?, recommendation? }] }
|
|
78
|
-
//
|
|
79
|
-
// Severity enum mirrors `lib/parser.mjs::SEVERITY_TO_BUCKET`:
|
|
80
|
-
// "high" | "medium" | "low" (parser also accepts "med" but the codex
|
|
81
|
-
// model emits the canonical "medium" — `med` is a legacy alias).
|
|
82
|
-
export function buildVerdictSchema() {
|
|
83
|
-
return {
|
|
84
|
-
type: "object",
|
|
85
|
-
required: ["verdict"],
|
|
86
|
-
additionalProperties: false,
|
|
87
|
-
properties: {
|
|
88
|
-
verdict: {
|
|
89
|
-
type: "string",
|
|
90
|
-
enum: ["clean", "concerns"],
|
|
91
|
-
description: "Closed-set verdict (parser.mjs:parseConcernsJson contract).",
|
|
92
|
-
},
|
|
93
|
-
findings: {
|
|
94
|
-
type: "array",
|
|
95
|
-
description: "Required when verdict == 'concerns'. Empty array also accepted.",
|
|
96
|
-
items: {
|
|
97
|
-
type: "object",
|
|
98
|
-
required: ["severity", "body"],
|
|
99
|
-
additionalProperties: false,
|
|
100
|
-
properties: {
|
|
101
|
-
severity: {
|
|
102
|
-
type: "string",
|
|
103
|
-
enum: ["high", "medium", "low"],
|
|
104
|
-
description: "ADR-077 severity ladder. 'medium' (canonical) — 'med' is a legacy alias.",
|
|
105
|
-
},
|
|
106
|
-
body: {
|
|
107
|
-
type: "string",
|
|
108
|
-
description: "The concern itself — what's wrong + why it matters + how to fix.",
|
|
109
|
-
},
|
|
110
|
-
title: {
|
|
111
|
-
type: "string",
|
|
112
|
-
description: "Optional short title rendered ahead of the file:line line.",
|
|
113
|
-
},
|
|
114
|
-
file: {
|
|
115
|
-
type: "string",
|
|
116
|
-
description: "File path (optional). Prepended to the rendered concern.",
|
|
117
|
-
},
|
|
118
|
-
line_start: {
|
|
119
|
-
type: "integer",
|
|
120
|
-
description: "Line number (optional). Multi-review M3 hotfix: parser.mjs reads `line_start`, not `line`. Rendered as ':<n>' suffix on file.",
|
|
121
|
-
},
|
|
122
|
-
recommendation: {
|
|
123
|
-
type: "string",
|
|
124
|
-
description: "Optional fix suggestion. Appended on a new line after body.",
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Single source of truth for "is the broker active for this project right
|
|
134
|
-
// now". Reads .codex-pair/state/broker.json and returns the broker descriptor
|
|
135
|
-
// (transport URL, pid, started_at, codex version, protocol version) or null
|
|
136
|
-
// if no broker is running. The hook's main flow checks this BEFORE the
|
|
137
|
-
// cache + inflight lock; a live broker bypasses both because the broker
|
|
138
|
-
// itself coordinates concurrent requests.
|
|
139
|
-
//
|
|
140
|
-
// The implementation is intentionally stubbed for v0.7.x Milestone 1.
|
|
141
|
-
// Returning null here causes every hook invocation to fall through to the
|
|
142
|
-
// existing per-edit spawn path — byte-identical behavior to pre-broker.
|
|
143
|
-
// M4: delegate to the lifecycle module's descriptor reader. The function
|
|
144
|
-
// signature is preserved for the stable contract; the body now returns
|
|
145
|
-
// a real descriptor when one exists (vs the M2 stub returning null
|
|
146
|
-
// unconditionally). isBrokerEnabled uses lifecycleReadBrokerDescriptor
|
|
147
|
-
// directly for the same purpose; this exported form is the public API
|
|
148
|
-
// per ADR-090.
|
|
149
|
-
export function readBrokerState(markerDir) {
|
|
150
|
-
return lifecycleReadBrokerDescriptor(markerDir);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Stable predicate the hook can call without knowing the broker mechanics.
|
|
154
|
-
// Returns true iff (a) ASK_CODEX_BROKER=1 in env, (b) readBrokerState
|
|
155
|
-
// returns a non-null descriptor, (c) the broker process is alive AND
|
|
156
|
-
// answered a health probe within BROKER_HEALTH_TIMEOUT_MS. Today (b) is
|
|
157
|
-
// stubbed to null, so this is always false.
|
|
158
|
-
// Tier 3 Milestone 4: implementation flipped from "always false" to a
|
|
159
|
-
// real check. Returns true iff:
|
|
160
|
-
// 1. ASK_CODEX_BROKER=1 in env (master switch — default off)
|
|
161
|
-
// 2. A valid descriptor exists at <markerDir>/.codex-pair/state/broker.json
|
|
162
|
-
// 3. The descriptor's protocolVersion matches BROKER_PROTOCOL_VERSION
|
|
163
|
-
// 4. The recorded pid is still alive (cheap process.kill(pid, 0))
|
|
164
|
-
//
|
|
165
|
-
// Per ADR-077 silent-on-error: any check that fails returns false; caller
|
|
166
|
-
// falls through to the existing per-edit spawn path. clearStaleBrokerState
|
|
167
|
-
// (called by SessionStart per ADR-090) keeps the descriptor honest between
|
|
168
|
-
// sessions; this check is the per-edit-hook's defense for the case where
|
|
169
|
-
// the broker died MID-SESSION.
|
|
170
|
-
export function isBrokerEnabled(markerDir) {
|
|
171
|
-
if (process.env.ASK_CODEX_BROKER !== "1") return false;
|
|
172
|
-
const state = lifecycleReadBrokerDescriptor(markerDir);
|
|
173
|
-
if (!state) return false;
|
|
174
|
-
if (state.protocolVersion !== BROKER_PROTOCOL_VERSION) return false;
|
|
175
|
-
if (!lifecycleIsPidAlive(state.pid)) return false;
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Path resolver for the per-marker-dir broker state file. Used by the
|
|
180
|
-
// SessionStart hook (writer) and the per-edit hook (reader).
|
|
181
|
-
export function brokerStatePath(markerDir, stateDir) {
|
|
182
|
-
return join(markerDir, stateDir, BROKER_STATE_FILE);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Stale-state cleanup helper. SessionStart calls this BEFORE launching a
|
|
186
|
-
// fresh broker; the per-edit hook MAY call it on startup as a belt-and-
|
|
187
|
-
// suspenders defense (but the SessionStart path is the contract per
|
|
188
|
-
// ADR-090). Returns "absent" | "live" | "stale".
|
|
189
|
-
//
|
|
190
|
-
// Implementation lives in `broker-lifecycle.mjs` to keep the descriptor-
|
|
191
|
-
// read + cleanup primitives co-located with the rest of the lifecycle
|
|
192
|
-
// orchestration. Re-exported here so consumers (the per-edit hook in M4,
|
|
193
|
-
// codex-pair-session.mjs in this PR) can import a single contract surface.
|
|
194
|
-
//
|
|
195
|
-
// The implementation needs `BROKER_PROTOCOL_VERSION` (this module's
|
|
196
|
-
// constant) — so the lifecycle module imports it from here, and we
|
|
197
|
-
// re-export the function below. This avoids a circular dep because
|
|
198
|
-
// broker-lifecycle.mjs already imports initializeBroker from this file;
|
|
199
|
-
// adding BROKER_PROTOCOL_VERSION to that import doesn't introduce a new
|
|
200
|
-
// cycle.
|
|
201
|
-
export { clearStaleBrokerState } from "./broker-lifecycle.mjs";
|
|
202
|
-
|
|
203
|
-
// M4: import the descriptor reader + pid-liveness helper into this module
|
|
204
|
-
// for isBrokerEnabled's per-edit-hook gating check. The one-way dep from
|
|
205
|
-
// broker.mjs → broker-lifecycle.mjs is fine: broker-lifecycle imports
|
|
206
|
-
// BROKER_PROTOCOL_VERSION + initializeBroker from this file, but only
|
|
207
|
-
// uses them inside function bodies (called after module init finishes),
|
|
208
|
-
// so the static-evaluation order is acyclic at the value-of-import level.
|
|
209
|
-
import { isPidAlive as lifecycleIsPidAlive, readBrokerDescriptorSync as lifecycleReadBrokerDescriptor } from "./broker-lifecycle.mjs";
|
|
210
|
-
|
|
211
|
-
// Open a transport connection to a running broker, perform the JSON-RPC
|
|
212
|
-
// `initialize` handshake, and return `{ connection, rpc, initializeResult }`.
|
|
213
|
-
// Caller owns connection lifetime — call `connection.close()` and stop
|
|
214
|
-
// using `rpc` when done. On any failure (transport error, handshake
|
|
215
|
-
// timeout, initialize rejection) this rejects; caller falls back to the
|
|
216
|
-
// per-edit spawn path per ADR-077.
|
|
217
|
-
//
|
|
218
|
-
// `clientInfo` is the InitializeParams.clientInfo object — codex-cli
|
|
219
|
-
// 0.130.0 requires `{ name, title, version }` (brainstorm-verified;
|
|
220
|
-
// ADR-093 protocol note). Callers should pass real plugin identity.
|
|
221
|
-
export async function initializeBroker(transportUrl, clientInfo, options = {}) {
|
|
222
|
-
const { handshakeTimeoutMs = 5000, initializeTimeoutMs = 5000 } = options;
|
|
223
|
-
const connection = await connectWebSocket(transportUrl, { handshakeTimeoutMs });
|
|
224
|
-
const rpc = createRpcClient(connection, { defaultTimeoutMs: initializeTimeoutMs });
|
|
225
|
-
try {
|
|
226
|
-
const initializeResult = await rpc.request(
|
|
227
|
-
JSONRPC_METHODS.INITIALIZE,
|
|
228
|
-
{ clientInfo },
|
|
229
|
-
{ timeoutMs: initializeTimeoutMs },
|
|
230
|
-
);
|
|
231
|
-
return { connection, rpc, initializeResult };
|
|
232
|
-
} catch (err) {
|
|
233
|
-
try {
|
|
234
|
-
connection.close(1011, "initialize failed");
|
|
235
|
-
} catch {
|
|
236
|
-
// already torn down
|
|
237
|
-
}
|
|
238
|
-
throw err;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Health probe: open the transport, send `model/list` (idempotent, cheap),
|
|
243
|
-
// wait up to BROKER_HEALTH_TIMEOUT_MS for a response. Returns boolean.
|
|
244
|
-
// Never throws — callers in the hook path treat any failure as "broker
|
|
245
|
-
// unreachable, fall back to per-edit spawn" per ADR-077.
|
|
246
|
-
//
|
|
247
|
-
// `state` is the broker descriptor read from .codex-pair/state/broker.json
|
|
248
|
-
// (shape: `{ transportUrl, pid, codexVersion, protocolVersion, startedAt }`).
|
|
249
|
-
// Health probe uses transportUrl + initializes ad-hoc because the long-
|
|
250
|
-
// lived connection lives in the per-edit hook process, not here.
|
|
251
|
-
//
|
|
252
|
-
// Implementation note: model/list is preferred over `initialize` for the
|
|
253
|
-
// probe because the brainstorm verified that codex's `initialize` is
|
|
254
|
-
// metadata-rich + always-succeeds. `model/list` exercises the actual
|
|
255
|
-
// JSON-RPC plumbing AND validates that the broker can complete a real
|
|
256
|
-
// request — a stricter health signal.
|
|
257
|
-
export async function probeBrokerHealth(state) {
|
|
258
|
-
if (!state || typeof state.transportUrl !== "string") return false;
|
|
259
|
-
let connection;
|
|
260
|
-
let rpc;
|
|
261
|
-
try {
|
|
262
|
-
connection = await connectWebSocket(state.transportUrl, {
|
|
263
|
-
handshakeTimeoutMs: BROKER_HEALTH_TIMEOUT_MS,
|
|
264
|
-
});
|
|
265
|
-
rpc = createRpcClient(connection, { defaultTimeoutMs: BROKER_HEALTH_TIMEOUT_MS });
|
|
266
|
-
// `model/list` requires the connection to have completed `initialize`
|
|
267
|
-
// first per the codex protocol. The PROBE path opens a fresh
|
|
268
|
-
// connection (no broker-side state shared with the long-lived hook
|
|
269
|
-
// connection), so we must initialize here before model/list.
|
|
270
|
-
await rpc.request(
|
|
271
|
-
JSONRPC_METHODS.INITIALIZE,
|
|
272
|
-
{ clientInfo: { name: "codex-pair-health-probe", title: "codex-pair health probe", version: "0.0.0" } },
|
|
273
|
-
{ timeoutMs: BROKER_HEALTH_TIMEOUT_MS },
|
|
274
|
-
);
|
|
275
|
-
await rpc.request(JSONRPC_METHODS.MODEL_LIST, undefined, {
|
|
276
|
-
timeoutMs: BROKER_HEALTH_TIMEOUT_MS,
|
|
277
|
-
});
|
|
278
|
-
return true;
|
|
279
|
-
} catch {
|
|
280
|
-
return false;
|
|
281
|
-
} finally {
|
|
282
|
-
if (connection && !connection.destroyed) {
|
|
283
|
-
try {
|
|
284
|
-
connection.close(1000, "probe done");
|
|
285
|
-
} catch {
|
|
286
|
-
// best-effort
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Submit-review API. The hook calls this when isBrokerEnabled returns
|
|
293
|
-
// true. Today never reached. Real implementation (Milestone 3) performs
|
|
294
|
-
// a 3-step JSON-RPC dance per ADR-093:
|
|
295
|
-
// 1. `thread/start { ephemeral: true, cwd, baseInstructions, model,
|
|
296
|
-
// approvalPolicy: "never", sandbox: <readonly> }`
|
|
297
|
-
// → receives { thread: { id } }
|
|
298
|
-
// 2. `turn/start { threadId, input: [{type:"text", text: prompt}],
|
|
299
|
-
// outputSchema: buildVerdictSchema(), effort: "high" }`
|
|
300
|
-
// → receives { turn: { id } }
|
|
301
|
-
// 3. Listen on the JSON-RPC connection for `turn/completed`
|
|
302
|
-
// notification; extract the final agentMessage; parse its JSON
|
|
303
|
-
// content (constrained by outputSchema) into the verdict shape;
|
|
304
|
-
// return same shape spawnCodex returns today.
|
|
305
|
-
//
|
|
306
|
-
// On abort signal: send `turn/interrupt { turnId }` and reject the
|
|
307
|
-
// promise. On timeout: same. On schema-violating output (rare per ADR-093
|
|
308
|
-
// risk acceptance): reject; caller falls through to per-edit spawn per
|
|
309
|
-
// ADR-077's silent-on-error contract.
|
|
310
|
-
//
|
|
311
|
-
// Args object shape (refined from ADR-090's `(state, prompt, options)`):
|
|
312
|
-
// { state, baseInstructions, prompt, model, threadOptions, abortSignal }
|
|
313
|
-
// Returns: { agentMessage: string, tokenUsage: { ... }, durationMs: number }
|
|
314
|
-
// — mirrors the shape spawnCodex currently produces so the hook's main()
|
|
315
|
-
// integration is a one-line substitution.
|
|
316
|
-
// Tier 3 Milestone 3 implementation. Performs the JSON-RPC dance per
|
|
317
|
-
// ADR-093 + brainstorm-verified protocol facts:
|
|
318
|
-
// 1. `thread/start { ephemeral: true, cwd, baseInstructions, model,
|
|
319
|
-
// approvalPolicy: "never", sandbox: "read-only" }`
|
|
320
|
-
// → receives `{ thread: Thread }`. Pin `thread.id`.
|
|
321
|
-
// 2. Register `turn/completed` waiter BEFORE turn/start (race-safe).
|
|
322
|
-
// 3. `turn/start { threadId, input: [{type:"text", text: prompt}],
|
|
323
|
-
// outputSchema: buildVerdictSchema(), effort: "high",
|
|
324
|
-
// sandboxPolicy: { type: "readOnly", networkAccess: false } }`
|
|
325
|
-
// → receives `{ turn: Turn }`. Pin `turn.id`.
|
|
326
|
-
// 4. Await `turn/completed` notification matching our threadId. Extract
|
|
327
|
-
// the final agentMessage text via `turn.items.findLast(i =>
|
|
328
|
-
// i.type === "agentMessage")?.text`.
|
|
329
|
-
// 5. On abort: send `turn/interrupt { threadId, turnId }` best-effort.
|
|
330
|
-
// 6. Return STRING (matches spawnCodex's return so the hook flow doesn't
|
|
331
|
-
// need a translation layer).
|
|
332
|
-
//
|
|
333
|
-
// `args` shape (refined from ADR-090's `(state, prompt, options)`):
|
|
334
|
-
// { connection, rpc, cwd, baseInstructions, prompt, model, timeoutMs,
|
|
335
|
-
// abortSignal }
|
|
336
|
-
//
|
|
337
|
-
// Caller owns connection + rpc lifetime; submitReview does NOT close them.
|
|
338
|
-
// Failures map to thrown errors with `.code` matching the existing
|
|
339
|
-
// taggedError verdict set in codex-pair-watch.mjs (timeout, error,
|
|
340
|
-
// parse_failed) so the surrounding hook flow handles them uniformly.
|
|
341
|
-
// Wrap an rpc.request for the thread/start + turn/start dance so a
|
|
342
|
-
// transport-layer failure (request timeout, connection closed/errored) on a
|
|
343
|
-
// broker that handshook OK but then hung is tagged brokerFailure=true. Per
|
|
344
|
-
// ADR-077 a post-handshake broker hang must be a silent fallback to the
|
|
345
|
-
// per-edit spawn, but broker-rpc's timeout/close/error rejections are PLAIN
|
|
346
|
-
// Errors lacking that marker, so runCodexWithFallback would otherwise
|
|
347
|
-
// rethrow them as a hard verdict. broker-rpc rejects a transport failure with
|
|
348
|
-
// either a PLAIN Error (timeout / connection-closed → no `.code`) or the RAW
|
|
349
|
-
// socket error (transport "error" event → a STRING `.code` like ECONNRESET).
|
|
350
|
-
// Only a genuine JSON-RPC error RESPONSE carries a NUMERIC `.code` (broker-rpc
|
|
351
|
-
// sets it from env.error.code) — that's a real server-side verdict, not a
|
|
352
|
-
// broker outage. So we tag everything EXCEPT numeric-coded errors.
|
|
353
|
-
async function brokerRequest(rpc, method, params, timeoutMs, brokerPhase) {
|
|
354
|
-
try {
|
|
355
|
-
return await rpc.request(method, params, { timeoutMs });
|
|
356
|
-
} catch (err) {
|
|
357
|
-
if (err && typeof err === "object" && typeof err.code !== "number" && !err.brokerFailure) {
|
|
358
|
-
err.verdict = "error";
|
|
359
|
-
err.brokerFailure = true;
|
|
360
|
-
err.brokerPhase = brokerPhase;
|
|
361
|
-
}
|
|
362
|
-
throw err;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export async function submitReview(args) {
|
|
367
|
-
const { rpc, connection, cwd, baseInstructions, prompt, model, timeoutMs = 60_000, abortSignal } = args;
|
|
368
|
-
if (!rpc) throw new Error("submitReview: rpc client required");
|
|
369
|
-
if (!connection) throw new Error("submitReview: connection required");
|
|
370
|
-
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
371
|
-
throw new Error("submitReview: prompt required");
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const deadline = Date.now() + timeoutMs;
|
|
375
|
-
const remaining = () => Math.max(0, deadline - Date.now());
|
|
376
|
-
|
|
377
|
-
// 1. thread/start (ephemeral — codex auto-discards after the turn).
|
|
378
|
-
// approvalPolicy: "never" is mandatory for the hook context (no user
|
|
379
|
-
// available to approve interactive prompts). sandbox: "read-only" denies
|
|
380
|
-
// file writes from the reviewer.
|
|
381
|
-
const threadResp = await brokerRequest(
|
|
382
|
-
rpc,
|
|
383
|
-
JSONRPC_METHODS.THREAD_START,
|
|
384
|
-
{
|
|
385
|
-
ephemeral: true,
|
|
386
|
-
cwd,
|
|
387
|
-
baseInstructions,
|
|
388
|
-
model,
|
|
389
|
-
approvalPolicy: "never",
|
|
390
|
-
sandbox: "read-only",
|
|
391
|
-
},
|
|
392
|
-
remaining(),
|
|
393
|
-
"thread_start",
|
|
394
|
-
);
|
|
395
|
-
const threadId = threadResp?.thread?.id;
|
|
396
|
-
if (typeof threadId !== "string") {
|
|
397
|
-
// M4 brokerFailure discriminator: thread_start failures indicate the
|
|
398
|
-
// broker is broken at the protocol layer. Hook falls back to spawnCodex.
|
|
399
|
-
const err = new Error("submitReview: thread/start returned no thread.id");
|
|
400
|
-
err.verdict = "error";
|
|
401
|
-
err.brokerFailure = true;
|
|
402
|
-
err.brokerPhase = "thread_start";
|
|
403
|
-
throw err;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// 2. Register the turn/completed waiter BEFORE turn/start. Codex can
|
|
407
|
-
// emit turn/completed between the turn/start dispatch and the listener
|
|
408
|
-
// registration if we order them the other way around (brainstorm
|
|
409
|
-
// Risk #1).
|
|
410
|
-
const completionPromise = rpc.waitFor(
|
|
411
|
-
JSONRPC_NOTIFICATIONS.TURN_COMPLETED,
|
|
412
|
-
(n) => n.params?.threadId === threadId,
|
|
413
|
-
remaining(),
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
// 3. turn/start. outputSchema constrains the agent's final message to
|
|
417
|
-
// the parser-compatible shape (parser.mjs::parseConcernsJson).
|
|
418
|
-
const turnResp = await brokerRequest(
|
|
419
|
-
rpc,
|
|
420
|
-
JSONRPC_METHODS.TURN_START,
|
|
421
|
-
{
|
|
422
|
-
threadId,
|
|
423
|
-
input: [{ type: "text", text: prompt }],
|
|
424
|
-
outputSchema: buildVerdictSchema(),
|
|
425
|
-
effort: "high",
|
|
426
|
-
// Belt-and-suspenders: also pin turn-level sandbox + deny network.
|
|
427
|
-
sandboxPolicy: { type: "readOnly", networkAccess: false },
|
|
428
|
-
},
|
|
429
|
-
remaining(),
|
|
430
|
-
"turn_start",
|
|
431
|
-
);
|
|
432
|
-
const turnId = turnResp?.turn?.id;
|
|
433
|
-
if (typeof turnId !== "string") {
|
|
434
|
-
// M4 brokerFailure discriminator: turn_start failures = broker protocol
|
|
435
|
-
// is broken. Hook falls back to spawnCodex.
|
|
436
|
-
const err = new Error("submitReview: turn/start returned no turn.id");
|
|
437
|
-
err.verdict = "error";
|
|
438
|
-
err.brokerFailure = true;
|
|
439
|
-
err.brokerPhase = "turn_start";
|
|
440
|
-
throw err;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// 4. Wire cancellation. abortSignal abort → turn/interrupt + reject.
|
|
444
|
-
let abortHandler = null;
|
|
445
|
-
let interruptSent = false;
|
|
446
|
-
// Multi-review M3 hotfix: track completion so a late abort (firing
|
|
447
|
-
// AFTER completion resolves but BEFORE finally cleans up) doesn't
|
|
448
|
-
// send a spurious turn/interrupt for an already-done turn.
|
|
449
|
-
let completed = false;
|
|
450
|
-
const abortPromise =
|
|
451
|
-
abortSignal
|
|
452
|
-
? new Promise((_, reject) => {
|
|
453
|
-
abortHandler = () => {
|
|
454
|
-
// Early-return if we already got the completion. Closes the
|
|
455
|
-
// abort-after-completion race window flagged in multi-review.
|
|
456
|
-
if (completed) return;
|
|
457
|
-
interruptSent = true;
|
|
458
|
-
// Best-effort interrupt; don't await it on the abort path —
|
|
459
|
-
// we want to reject the user-facing promise immediately.
|
|
460
|
-
rpc
|
|
461
|
-
.request(JSONRPC_METHODS.TURN_INTERRUPT, { threadId, turnId }, { timeoutMs: 2000 })
|
|
462
|
-
.catch(() => {});
|
|
463
|
-
// Multi-review M3 hotfix: use `verdict` not `code` — the
|
|
464
|
-
// hook's verdictFromError reads err.verdict. "aborted" is
|
|
465
|
-
// not in VERDICT_PREFIXES so map to "error".
|
|
466
|
-
const err = new Error("submitReview: aborted");
|
|
467
|
-
err.verdict = "error";
|
|
468
|
-
err.aborted = true; // structured marker for callers who care
|
|
469
|
-
reject(err);
|
|
470
|
-
};
|
|
471
|
-
if (abortSignal.aborted) abortHandler();
|
|
472
|
-
else abortSignal.addEventListener("abort", abortHandler);
|
|
473
|
-
})
|
|
474
|
-
: null;
|
|
475
|
-
|
|
476
|
-
// 5. Race the completion against the abort.
|
|
477
|
-
let completion;
|
|
478
|
-
try {
|
|
479
|
-
completion = abortPromise
|
|
480
|
-
? await Promise.race([completionPromise, abortPromise])
|
|
481
|
-
: await completionPromise;
|
|
482
|
-
completed = true;
|
|
483
|
-
} catch (err) {
|
|
484
|
-
// On timeout (waitFor rejects), send best-effort interrupt so we
|
|
485
|
-
// don't leak a server-side turn. Multi-review M3 hotfix: use the
|
|
486
|
-
// structured err.timeout marker from broker-rpc, not regex on message.
|
|
487
|
-
if (!interruptSent && err && err.timeout === true) {
|
|
488
|
-
rpc.request(JSONRPC_METHODS.TURN_INTERRUPT, { threadId, turnId }, { timeoutMs: 2000 }).catch(() => {});
|
|
489
|
-
const wrapped = new Error("submitReview: turn timed out");
|
|
490
|
-
wrapped.verdict = "timeout";
|
|
491
|
-
wrapped.timeout = true;
|
|
492
|
-
throw wrapped;
|
|
493
|
-
}
|
|
494
|
-
throw err;
|
|
495
|
-
} finally {
|
|
496
|
-
if (abortHandler && abortSignal) {
|
|
497
|
-
abortSignal.removeEventListener("abort", abortHandler);
|
|
498
|
-
}
|
|
499
|
-
// If the abort handler already fired but we'd completed, it sent a
|
|
500
|
-
// spurious interrupt and rejected an unawaited promise. The flag
|
|
501
|
-
// above prevents that — abortHandler now early-returns if completed.
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// 6. Extract the final agentMessage from `turn.items`. Brainstorm
|
|
505
|
-
// confirmed multiple `agentMessage` items can appear (reasoning summaries
|
|
506
|
-
// vs final answer); `findLast` picks the last/final one.
|
|
507
|
-
// `completed` is read by the abortHandler closure to detect the
|
|
508
|
-
// abort-after-completion race; biome won't strip it.
|
|
509
|
-
const turn = completion?.params?.turn;
|
|
510
|
-
if (!turn || !Array.isArray(turn.items)) {
|
|
511
|
-
// M4: protocol-layer failure → brokerFailure → hook falls back.
|
|
512
|
-
const err = new Error("submitReview: turn/completed missing turn.items");
|
|
513
|
-
err.verdict = "parse_failed";
|
|
514
|
-
err.brokerFailure = true;
|
|
515
|
-
err.brokerPhase = "protocol";
|
|
516
|
-
throw err;
|
|
517
|
-
}
|
|
518
|
-
if (turn.status === "failed" || turn.status === "interrupted") {
|
|
519
|
-
const err = new Error(
|
|
520
|
-
`submitReview: turn ${turn.status}${turn.error?.message ? ` — ${turn.error.message}` : ""}`,
|
|
521
|
-
);
|
|
522
|
-
err.verdict = "error";
|
|
523
|
-
throw err;
|
|
524
|
-
}
|
|
525
|
-
const finalMessage = turn.items.findLast?.((i) => i?.type === "agentMessage");
|
|
526
|
-
if (!finalMessage || typeof finalMessage.text !== "string") {
|
|
527
|
-
// M4: protocol-layer failure (broker spoke turn/completed but with no
|
|
528
|
-
// agentMessage). Mark brokerFailure so hook falls back to spawnCodex —
|
|
529
|
-
// this is a broker-broken state, not a real codex result.
|
|
530
|
-
const err = new Error("submitReview: turn/completed has no agentMessage item");
|
|
531
|
-
err.verdict = "parse_failed";
|
|
532
|
-
err.brokerFailure = true;
|
|
533
|
-
err.brokerPhase = "protocol";
|
|
534
|
-
throw err;
|
|
535
|
-
}
|
|
536
|
-
return finalMessage.text;
|
|
537
|
-
}
|