@rallycry/conveyor-agent 11.0.19 → 11.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{boot-4ZNOCKFZ.js → boot-Z3EBJ7GX.js} +2 -2
- package/dist/{chunk-SR66HQKB.js → chunk-37J5MMQT.js} +1 -1
- package/dist/chunk-4ZDIBDQC.js +4926 -0
- package/dist/{chunk-GZRZGBIK.js → chunk-5WQHDGIW.js} +16 -2
- package/dist/{chunk-GL2DIQEQ.js → chunk-Q4FQOJ7D.js} +23 -4
- package/dist/{chunk-N5LEVAGA.js → chunk-WPXSZKMS.js} +912 -3170
- package/dist/cli.js +9 -14
- package/dist/index.d.ts +2 -15
- package/dist/index.js +4 -4
- package/dist/{serve-boot-RU5BYIK4.js → serve-boot-IRRHYA7W.js} +3 -3
- package/package.json +1 -1
- package/skills/conveyor-build/SKILL.md +3 -4
- package/skills/conveyor-build/references/pack-path.md +13 -7
- package/skills/conveyor-review/SKILL.md +15 -2
- package/dist/chunk-WS7QRB37.js +0 -2590
|
@@ -0,0 +1,4926 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLONE_TIMEOUT_MS,
|
|
3
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
4
|
+
FETCH_TIMEOUT_MS,
|
|
5
|
+
GIT_PREP_MAX_RETRIES
|
|
6
|
+
} from "./chunk-Q4FQOJ7D.js";
|
|
7
|
+
import {
|
|
8
|
+
gitCredentialHelper,
|
|
9
|
+
sleep,
|
|
10
|
+
syncGithubTokenFiles,
|
|
11
|
+
writeGitCredential
|
|
12
|
+
} from "./chunk-W4LZ7R6Z.js";
|
|
13
|
+
import {
|
|
14
|
+
buildConveyorSocketOptions,
|
|
15
|
+
callWithAck,
|
|
16
|
+
heartbeatStatusFor,
|
|
17
|
+
loopStatusForRunnerStatus,
|
|
18
|
+
waitForConnected
|
|
19
|
+
} from "./chunk-IA45XHOA.js";
|
|
20
|
+
import {
|
|
21
|
+
WorkbenchError,
|
|
22
|
+
getWorkbenchClient
|
|
23
|
+
} from "./chunk-SQM2BQ7H.js";
|
|
24
|
+
import {
|
|
25
|
+
workbenchEnabled
|
|
26
|
+
} from "./chunk-KMB3BU4S.js";
|
|
27
|
+
|
|
28
|
+
// src/setup/bootstrap.ts
|
|
29
|
+
var BOOTSTRAP_TIMEOUT_MS = 3e4;
|
|
30
|
+
var RETRY_DELAYS_MS = [5e3, 1e4, 2e4];
|
|
31
|
+
function emitFailureEvent(payload) {
|
|
32
|
+
process.stderr.write(JSON.stringify(payload) + "\n");
|
|
33
|
+
}
|
|
34
|
+
async function singleBootstrapAttempt(apiUrl, instanceName, bootstrapToken, timeoutMs) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
const headers = {};
|
|
39
|
+
if (bootstrapToken) headers["x-codespace-token"] = bootstrapToken;
|
|
40
|
+
const response = await fetch(`${apiUrl}/api/codespace/bootstrap/${instanceName}`, {
|
|
41
|
+
headers,
|
|
42
|
+
signal: controller.signal
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const errorText = await response.text().catch(() => "");
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
status: response.status,
|
|
49
|
+
errorText: errorText.slice(0, 500),
|
|
50
|
+
reason: response.status === 401 || response.status === 403 ? "auth_rejected" : "http_error"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const body = await response.json();
|
|
54
|
+
return { ok: true, body };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
const reason = controller.signal.aborted ? "timeout" : "network_error";
|
|
58
|
+
return { ok: false, errorText: message.slice(0, 500), reason };
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function buildFailure(reason, attempts, status, detail) {
|
|
64
|
+
const out = { ok: false, reason, attempts };
|
|
65
|
+
if (status === void 0) {
|
|
66
|
+
} else {
|
|
67
|
+
out.status = status;
|
|
68
|
+
}
|
|
69
|
+
if (detail) out.detail = detail;
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function isRetryable(reason, retryOnHttpError) {
|
|
73
|
+
if (reason === "timeout" || reason === "network_error") return true;
|
|
74
|
+
return retryOnHttpError === true && reason === "http_error";
|
|
75
|
+
}
|
|
76
|
+
async function fetchBootstrap(opts) {
|
|
77
|
+
const timeoutMs = opts.timeoutMs ?? BOOTSTRAP_TIMEOUT_MS;
|
|
78
|
+
const delays = opts.retryDelaysMs ?? RETRY_DELAYS_MS;
|
|
79
|
+
const maxAttempts = delays.length + 1;
|
|
80
|
+
const hasBootstrapToken = Boolean(opts.bootstrapToken);
|
|
81
|
+
const hasTaskToken = Boolean(process.env.CONVEYOR_TASK_TOKEN);
|
|
82
|
+
let lastReason = "unknown";
|
|
83
|
+
let lastStatus;
|
|
84
|
+
let lastDetail;
|
|
85
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
86
|
+
const result = await singleBootstrapAttempt(
|
|
87
|
+
opts.apiUrl,
|
|
88
|
+
opts.instanceName,
|
|
89
|
+
opts.bootstrapToken,
|
|
90
|
+
timeoutMs
|
|
91
|
+
);
|
|
92
|
+
if (result.ok && result.body) {
|
|
93
|
+
return { ok: true, config: result.body, attempts: attempt };
|
|
94
|
+
}
|
|
95
|
+
lastReason = result.reason ?? "unknown";
|
|
96
|
+
lastStatus = result.status;
|
|
97
|
+
lastDetail = result.errorText;
|
|
98
|
+
const failurePayload = {
|
|
99
|
+
event: "bootstrap_failed",
|
|
100
|
+
reason: lastReason,
|
|
101
|
+
apiUrl: opts.apiUrl,
|
|
102
|
+
instanceName: opts.instanceName,
|
|
103
|
+
hasBootstrapToken,
|
|
104
|
+
hasTaskToken,
|
|
105
|
+
attempt
|
|
106
|
+
};
|
|
107
|
+
if (lastStatus === void 0) {
|
|
108
|
+
} else {
|
|
109
|
+
failurePayload.status = lastStatus;
|
|
110
|
+
}
|
|
111
|
+
if (lastDetail) failurePayload.detail = lastDetail;
|
|
112
|
+
emitFailureEvent(failurePayload);
|
|
113
|
+
if (!isRetryable(lastReason, opts.retryOnHttpError) || attempt >= maxAttempts) {
|
|
114
|
+
return buildFailure(lastReason, attempt, lastStatus, lastDetail);
|
|
115
|
+
}
|
|
116
|
+
await sleep(delays[attempt - 1]);
|
|
117
|
+
}
|
|
118
|
+
return buildFailure(lastReason, maxAttempts, lastStatus, lastDetail);
|
|
119
|
+
}
|
|
120
|
+
function applyBootstrapToEnv(config) {
|
|
121
|
+
for (const [key, value] of Object.entries(config.envVars ?? {})) {
|
|
122
|
+
process.env[key] = value;
|
|
123
|
+
}
|
|
124
|
+
if (config.mode === "project") {
|
|
125
|
+
if (config.projectToken) process.env.CONVEYOR_PROJECT_TOKEN = config.projectToken;
|
|
126
|
+
if (config.projectId) process.env.CONVEYOR_PROJECT_ID = config.projectId;
|
|
127
|
+
if (config.workspaceBranch) process.env.CONVEYOR_WORKSPACE_BRANCH = config.workspaceBranch;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (config.taskId) process.env.CONVEYOR_TASK_ID = config.taskId;
|
|
131
|
+
if (config.sessionId) process.env.CONVEYOR_SESSION_ID = config.sessionId;
|
|
132
|
+
if (config.taskToken) process.env.CONVEYOR_TASK_TOKEN = config.taskToken;
|
|
133
|
+
if (config.agentMode !== void 0) process.env.CONVEYOR_AGENT_MODE = config.agentMode;
|
|
134
|
+
if (config.isAuto !== void 0) process.env.CONVEYOR_IS_AUTO = config.isAuto;
|
|
135
|
+
if (config.runnerMode) process.env.CONVEYOR_MODE = config.runnerMode;
|
|
136
|
+
if (config.taskBranch) process.env.CONVEYOR_TASK_BRANCH = config.taskBranch;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/utils/logger.ts
|
|
140
|
+
function createServiceLogger(service) {
|
|
141
|
+
const prefix = `[conveyor-agent:${service}]`;
|
|
142
|
+
return {
|
|
143
|
+
info(message, data) {
|
|
144
|
+
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
145
|
+
process.stderr.write(`${prefix} ${message}${extra}
|
|
146
|
+
`);
|
|
147
|
+
},
|
|
148
|
+
warn(message, data) {
|
|
149
|
+
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
150
|
+
process.stderr.write(`${prefix} WARN ${message}${extra}
|
|
151
|
+
`);
|
|
152
|
+
},
|
|
153
|
+
error(message, data) {
|
|
154
|
+
const extra = data ? ` ${JSON.stringify(data)}` : "";
|
|
155
|
+
process.stderr.write(`${prefix} ERROR ${message}${extra}
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/connection/agent-connection.ts
|
|
162
|
+
import { existsSync } from "fs";
|
|
163
|
+
import { fileURLToPath } from "url";
|
|
164
|
+
import { Worker } from "worker_threads";
|
|
165
|
+
import { io } from "socket.io-client";
|
|
166
|
+
|
|
167
|
+
// src/setup/bootstrap-poll.ts
|
|
168
|
+
var PollUntilBoundHttpError = class extends Error {
|
|
169
|
+
constructor(status) {
|
|
170
|
+
super(`pollUntilBound got unexpected status ${status}`);
|
|
171
|
+
this.status = status;
|
|
172
|
+
this.name = "PollUntilBoundHttpError";
|
|
173
|
+
}
|
|
174
|
+
status;
|
|
175
|
+
};
|
|
176
|
+
async function pollUntilBound(opts) {
|
|
177
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 2e3;
|
|
178
|
+
const maxWaitMs = opts.maxWaitMs ?? 30 * 60 * 1e3;
|
|
179
|
+
const deadline = Date.now() + maxWaitMs;
|
|
180
|
+
while (true) {
|
|
181
|
+
const response = await fetch(`${opts.apiUrl}/api/v3/pods/bootstrap`, {
|
|
182
|
+
headers: { Authorization: `Bearer ${opts.bootstrapToken}` }
|
|
183
|
+
});
|
|
184
|
+
if (response.status === 200) {
|
|
185
|
+
return await response.json();
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 204) {
|
|
188
|
+
if (Date.now() >= deadline) {
|
|
189
|
+
throw new Error(`pollUntilBound timed out after ${maxWaitMs}ms waiting for pod bind`);
|
|
190
|
+
}
|
|
191
|
+
await sleep(pollIntervalMs);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw new PollUntilBoundHttpError(response.status);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/connection/bundle-credentials.ts
|
|
199
|
+
function readBundleIdentity(sessionJwt) {
|
|
200
|
+
if (!sessionJwt) return {};
|
|
201
|
+
const segments = sessionJwt.split(".");
|
|
202
|
+
if (segments.length !== 3) return {};
|
|
203
|
+
try {
|
|
204
|
+
const json = Buffer.from(segments[1], "base64url").toString("utf8");
|
|
205
|
+
const claims = JSON.parse(json);
|
|
206
|
+
return {
|
|
207
|
+
...typeof claims.sessionId === "string" ? { sessionId: claims.sessionId } : {},
|
|
208
|
+
...typeof claims.role === "string" ? { role: claims.role } : {}
|
|
209
|
+
};
|
|
210
|
+
} catch {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function bundleMayWriteGithubFiles(bundle, self) {
|
|
215
|
+
const identity = readBundleIdentity(bundle.sessionJwt);
|
|
216
|
+
if (identity.sessionId && self.sessionId && identity.sessionId !== self.sessionId) {
|
|
217
|
+
return {
|
|
218
|
+
allowed: false,
|
|
219
|
+
reason: `bundle resolved to session ${identity.sessionId}, not ours (${self.sessionId})`
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (identity.role === "reader" && self.role && self.role !== "reader") {
|
|
223
|
+
return {
|
|
224
|
+
allowed: false,
|
|
225
|
+
reason: `bundle carries a reader-scoped token but this session is a ${self.role}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { allowed: true };
|
|
229
|
+
}
|
|
230
|
+
function applyBundleGithubToken(bundle, self) {
|
|
231
|
+
if (!bundle.githubToken) return { written: false, reason: "bundle carried no GitHub token" };
|
|
232
|
+
const permitted = bundleMayWriteGithubFiles(bundle, self);
|
|
233
|
+
if (!permitted.allowed) {
|
|
234
|
+
return { written: false, ...permitted.reason ? { reason: permitted.reason } : {} };
|
|
235
|
+
}
|
|
236
|
+
syncGithubTokenFiles(bundle.githubToken);
|
|
237
|
+
return { written: true };
|
|
238
|
+
}
|
|
239
|
+
function syncBundleGithubToken(token) {
|
|
240
|
+
syncGithubTokenFiles(token);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/connection/agent-connection.ts
|
|
244
|
+
var logger = createServiceLogger("agent-connection");
|
|
245
|
+
var EVENT_BATCH_MS = 500;
|
|
246
|
+
var MAX_EVENT_BUFFER = 5e3;
|
|
247
|
+
var TOKEN_REFRESH_INTERVAL_MS = 45 * 60 * 1e3;
|
|
248
|
+
var AgentConnection = class _AgentConnection {
|
|
249
|
+
socket = null;
|
|
250
|
+
config;
|
|
251
|
+
eventBuffer = [];
|
|
252
|
+
flushTimer = null;
|
|
253
|
+
tokenRefreshTimer = null;
|
|
254
|
+
lastEmittedStatus = null;
|
|
255
|
+
lastReportedStatus = null;
|
|
256
|
+
droppedEventCount = 0;
|
|
257
|
+
// Pending answer resolvers for askUserQuestion room-event fallback
|
|
258
|
+
pendingAnswerResolvers = /* @__PURE__ */ new Map();
|
|
259
|
+
// Dedup: suppress near-identical messages within a short window
|
|
260
|
+
recentMessages = [];
|
|
261
|
+
static DEDUP_WINDOW_MS = 3e4;
|
|
262
|
+
static DEDUP_SIMILARITY_THRESHOLD = 0.7;
|
|
263
|
+
static DEDUP_PREVIEW_LIMIT = 120;
|
|
264
|
+
// Early-buffering: events that arrive before callbacks are registered
|
|
265
|
+
earlyMessages = [];
|
|
266
|
+
earlyStop = false;
|
|
267
|
+
earlySoftStop = false;
|
|
268
|
+
earlyModeChanges = [];
|
|
269
|
+
// Registered callbacks
|
|
270
|
+
messageCallback = null;
|
|
271
|
+
stopCallback = null;
|
|
272
|
+
softStopCallback = null;
|
|
273
|
+
modeChangeCallback = null;
|
|
274
|
+
apiKeyUpdateCallback = null;
|
|
275
|
+
pullBranchCallback = null;
|
|
276
|
+
runStartCommandCallback = null;
|
|
277
|
+
earlyRunStartCommand = false;
|
|
278
|
+
earlyPullBranches = [];
|
|
279
|
+
spawnReviewCallback = null;
|
|
280
|
+
earlySpawnReviews = [];
|
|
281
|
+
spawnBuilderCallback = null;
|
|
282
|
+
earlySpawnBuilders = [];
|
|
283
|
+
spawnTuiCallback = null;
|
|
284
|
+
earlySpawnTuis = [];
|
|
285
|
+
probeUsageCallback = null;
|
|
286
|
+
earlyProbeUsage = false;
|
|
287
|
+
// PTY relay (S5 terminal). Single-slot callbacks, set per PtySession run.
|
|
288
|
+
ptyInputCallback = null;
|
|
289
|
+
ptyResizeCallback = null;
|
|
290
|
+
/** Epoch ms of the last relayed keystroke, or null before any input. The
|
|
291
|
+
* question-grace expiry reads it to defer un-parking while a human is
|
|
292
|
+
* actively typing into the Connected-TUI (they may be mid-answer). */
|
|
293
|
+
_lastPtyInputAt = null;
|
|
294
|
+
constructor(config) {
|
|
295
|
+
this.config = config;
|
|
296
|
+
}
|
|
297
|
+
get sessionId() {
|
|
298
|
+
return this.config.sessionId;
|
|
299
|
+
}
|
|
300
|
+
get connected() {
|
|
301
|
+
return this.socket?.connected ?? false;
|
|
302
|
+
}
|
|
303
|
+
// ── Typed service method call ──────────────────────────────────────────
|
|
304
|
+
// Socket.IO keeps the SAME Socket instance across transport-level
|
|
305
|
+
// reconnects (it only goes null on an explicit disconnect() teardown), so a
|
|
306
|
+
// brief flap leaves `this.socket` non-null but `.connected === false`. Rather
|
|
307
|
+
// than failing a tool call instantly (which the spawned `claude` surfaces as
|
|
308
|
+
// "Conveyor MCP disconnected" and an excuse to go idle), we wait out a short
|
|
309
|
+
// reconnect window, then emit with an ack timeout so a buffered packet whose
|
|
310
|
+
// ack never returns can't hang the call forever. We do NOT auto-retry the
|
|
311
|
+
// emit — re-sending a write could double-apply it; the agent prompt instructs
|
|
312
|
+
// the model to retry the tool, which is the safe place to decide idempotency.
|
|
313
|
+
static CALL_CONNECT_WAIT_MS = 2e4;
|
|
314
|
+
static CALL_ACK_TIMEOUT_MS = 3e4;
|
|
315
|
+
// ── Proactive socket recycle ───────────────────────────────────────────
|
|
316
|
+
// Cloud Run severs every WebSocket at its request timeout (3600s is the
|
|
317
|
+
// platform ceiling), so a socket that lives past ~60 minutes is killed at a
|
|
318
|
+
// random moment — historically mid-tool-call, which let the spawned CLI
|
|
319
|
+
// abandon its MCP session. Recycle the transport at a QUIET moment (no
|
|
320
|
+
// in-flight RPC) before the platform deadline instead: an engine-level close
|
|
321
|
+
// looks like a transport drop, so Socket.IO's auto-reconnect and the
|
|
322
|
+
// io "reconnect" → reconnectToSession() recovery path run unchanged. Jitter
|
|
323
|
+
// keeps a fleet of pods from recycling in one thundering herd.
|
|
324
|
+
static SOCKET_RECYCLE_BASE_MS = 52 * 60 * 1e3;
|
|
325
|
+
static SOCKET_RECYCLE_JITTER_MS = 4 * 60 * 1e3;
|
|
326
|
+
static SOCKET_RECYCLE_BUSY_POLL_MS = 15e3;
|
|
327
|
+
recycleTimer = null;
|
|
328
|
+
pendingCalls = 0;
|
|
329
|
+
async call(method, payload) {
|
|
330
|
+
const socket = this.socket;
|
|
331
|
+
if (!socket) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Not connected (method: ${String(method)}, session: ${this.config.sessionId})`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
this.pendingCalls++;
|
|
337
|
+
try {
|
|
338
|
+
if (!socket.connected) {
|
|
339
|
+
await this.waitForConnected(socket, _AgentConnection.CALL_CONNECT_WAIT_MS, String(method));
|
|
340
|
+
}
|
|
341
|
+
return await this.emitWithAck(socket, method, payload);
|
|
342
|
+
} finally {
|
|
343
|
+
this.pendingCalls--;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** (Re)arm the recycle timer — called on every successful (re)connect. */
|
|
347
|
+
scheduleSocketRecycle() {
|
|
348
|
+
this.clearSocketRecycle();
|
|
349
|
+
const delay2 = _AgentConnection.SOCKET_RECYCLE_BASE_MS + Math.random() * _AgentConnection.SOCKET_RECYCLE_JITTER_MS;
|
|
350
|
+
this.armRecycleTimer(delay2);
|
|
351
|
+
}
|
|
352
|
+
clearSocketRecycle() {
|
|
353
|
+
if (this.recycleTimer) {
|
|
354
|
+
clearTimeout(this.recycleTimer);
|
|
355
|
+
this.recycleTimer = null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
armRecycleTimer(delay2) {
|
|
359
|
+
this.recycleTimer = setTimeout(() => {
|
|
360
|
+
this.recycleTimer = null;
|
|
361
|
+
this.attemptSocketRecycle();
|
|
362
|
+
}, delay2);
|
|
363
|
+
this.recycleTimer.unref?.();
|
|
364
|
+
}
|
|
365
|
+
attemptSocketRecycle() {
|
|
366
|
+
const socket = this.socket;
|
|
367
|
+
if (!socket?.connected) return;
|
|
368
|
+
if (this.pendingCalls > 0) {
|
|
369
|
+
this.armRecycleTimer(_AgentConnection.SOCKET_RECYCLE_BUSY_POLL_MS);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
process.stderr.write(
|
|
373
|
+
"[conveyor-agent] Recycling socket ahead of the platform request timeout\n"
|
|
374
|
+
);
|
|
375
|
+
socket.io.engine?.close?.();
|
|
376
|
+
}
|
|
377
|
+
/** Resolve once `socket` reports connected, or reject after `timeoutMs`. */
|
|
378
|
+
waitForConnected(socket, timeoutMs, method) {
|
|
379
|
+
return waitForConnected(socket, timeoutMs, () => {
|
|
380
|
+
return new Error(
|
|
381
|
+
`Not connected \u2014 socket did not reconnect within ${timeoutMs / 1e3}s (method: ${method}, session: ${this.config.sessionId}). Transient; retry.`
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/** Emit an RPC and resolve on ack, rejecting if no ack arrives in time. */
|
|
386
|
+
emitWithAck(socket, method, payload) {
|
|
387
|
+
return callWithAck(
|
|
388
|
+
socket,
|
|
389
|
+
`agentSessionService:${String(method)}`,
|
|
390
|
+
payload,
|
|
391
|
+
{
|
|
392
|
+
timeoutMs: _AgentConnection.CALL_ACK_TIMEOUT_MS,
|
|
393
|
+
requireData: true,
|
|
394
|
+
makeTimeoutError: () => new Error(
|
|
395
|
+
`Service call timed out after ${_AgentConnection.CALL_ACK_TIMEOUT_MS / 1e3}s (method: ${String(method)}, session: ${this.config.sessionId}). Usually a transient reconnect; retry.`
|
|
396
|
+
),
|
|
397
|
+
makeFailureError: (error) => new Error(error ?? `Service call failed: ${String(method)}`)
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
// ── Connection lifecycle ───────────────────────────────────────────────
|
|
402
|
+
// oxlint-disable-next-line max-lines-per-function -- socket setup requires registering many co-located event handlers
|
|
403
|
+
connect() {
|
|
404
|
+
if (!this.config.apiUrl) {
|
|
405
|
+
return Promise.reject(new Error("Cannot connect: apiUrl is empty"));
|
|
406
|
+
}
|
|
407
|
+
this.startProactiveTokenRefresh();
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
let settled = false;
|
|
410
|
+
let attempts = 0;
|
|
411
|
+
const maxInitialAttempts = 30;
|
|
412
|
+
process.stderr.write(
|
|
413
|
+
`[conveyor-agent] Connecting to ${this.config.apiUrl} (mode: ${this.config.runnerMode ?? "task"}, session: ${this.config.sessionId})
|
|
414
|
+
`
|
|
415
|
+
);
|
|
416
|
+
this.socket = io(
|
|
417
|
+
this.config.apiUrl,
|
|
418
|
+
buildConveyorSocketOptions({
|
|
419
|
+
taskToken: this.config.taskToken,
|
|
420
|
+
runnerMode: this.config.runnerMode ?? "task"
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
this.socket.on("session:message", (msg) => {
|
|
424
|
+
const incoming = {
|
|
425
|
+
content: msg.content,
|
|
426
|
+
userId: msg.userId,
|
|
427
|
+
...msg.source && { source: msg.source },
|
|
428
|
+
...msg.files && { files: msg.files },
|
|
429
|
+
...msg.delivery === "prefill" && { delivery: msg.delivery }
|
|
430
|
+
};
|
|
431
|
+
if (this.messageCallback) this.messageCallback(incoming);
|
|
432
|
+
else this.earlyMessages.push(incoming);
|
|
433
|
+
});
|
|
434
|
+
this.socket.on("session:stop", () => {
|
|
435
|
+
if (this.stopCallback) this.stopCallback();
|
|
436
|
+
else this.earlyStop = true;
|
|
437
|
+
});
|
|
438
|
+
this.socket.on("session:softStop", () => {
|
|
439
|
+
if (this.softStopCallback) this.softStopCallback();
|
|
440
|
+
else this.earlySoftStop = true;
|
|
441
|
+
});
|
|
442
|
+
this.socket.on("session:modeChange", (data) => {
|
|
443
|
+
if (this.modeChangeCallback) this.modeChangeCallback(data);
|
|
444
|
+
else this.earlyModeChanges.push(data);
|
|
445
|
+
});
|
|
446
|
+
this.socket.on(
|
|
447
|
+
"session:answerQuestion",
|
|
448
|
+
(data) => {
|
|
449
|
+
const resolver = this.pendingAnswerResolvers.get(data.requestId);
|
|
450
|
+
if (resolver) resolver(data.answers);
|
|
451
|
+
}
|
|
452
|
+
);
|
|
453
|
+
this.socket.on("agentRunner:updateApiKey", (data) => {
|
|
454
|
+
if (this.apiKeyUpdateCallback) this.apiKeyUpdateCallback(data);
|
|
455
|
+
});
|
|
456
|
+
this.socket.on("session:pullBranch", (data) => {
|
|
457
|
+
if (this.pullBranchCallback) this.pullBranchCallback(data);
|
|
458
|
+
else this.earlyPullBranches.push(data);
|
|
459
|
+
});
|
|
460
|
+
this.socket.on("session:spawnReview", (data) => {
|
|
461
|
+
if (this.spawnReviewCallback) this.spawnReviewCallback(data);
|
|
462
|
+
else this.earlySpawnReviews.push(data);
|
|
463
|
+
});
|
|
464
|
+
this.socket.on("session:spawnBuilder", (data) => {
|
|
465
|
+
if (this.spawnBuilderCallback) this.spawnBuilderCallback(data);
|
|
466
|
+
else this.earlySpawnBuilders.push(data);
|
|
467
|
+
});
|
|
468
|
+
this.socket.on("session:spawnTui", (data) => {
|
|
469
|
+
if (this.spawnTuiCallback) this.spawnTuiCallback(data);
|
|
470
|
+
else this.earlySpawnTuis.push(data);
|
|
471
|
+
});
|
|
472
|
+
this.socket.on("session:probeUsage", () => {
|
|
473
|
+
if (this.probeUsageCallback) this.probeUsageCallback();
|
|
474
|
+
else this.earlyProbeUsage = true;
|
|
475
|
+
});
|
|
476
|
+
this.socket.on("session:runStartCommand", () => {
|
|
477
|
+
if (this.runStartCommandCallback) this.runStartCommandCallback();
|
|
478
|
+
else this.earlyRunStartCommand = true;
|
|
479
|
+
});
|
|
480
|
+
this.socket.on("pty:input", (data) => {
|
|
481
|
+
if (data.sessionId && data.sessionId !== this.config.sessionId) return;
|
|
482
|
+
this._lastPtyInputAt = Date.now();
|
|
483
|
+
this.ptyInputCallback?.(data.data);
|
|
484
|
+
});
|
|
485
|
+
this.socket.on("pty:resize", (data) => {
|
|
486
|
+
if (data.sessionId && data.sessionId !== this.config.sessionId) return;
|
|
487
|
+
this.ptyResizeCallback?.(data.cols, data.rows);
|
|
488
|
+
});
|
|
489
|
+
this.socket.on("connect", () => {
|
|
490
|
+
process.stderr.write("[conveyor-agent] Socket connected\n");
|
|
491
|
+
this.scheduleSocketRecycle();
|
|
492
|
+
if (!settled) {
|
|
493
|
+
settled = true;
|
|
494
|
+
resolve();
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
this.socket.on("connect_error", (err) => {
|
|
498
|
+
attempts++;
|
|
499
|
+
process.stderr.write(
|
|
500
|
+
`[conveyor-agent] Connection error (attempt ${attempts}/${maxInitialAttempts}): ${err.message}
|
|
501
|
+
`
|
|
502
|
+
);
|
|
503
|
+
if (!settled && attempts >= maxInitialAttempts) {
|
|
504
|
+
settled = true;
|
|
505
|
+
reject(
|
|
506
|
+
new Error(
|
|
507
|
+
`Failed to connect to ${this.config.apiUrl} after ${maxInitialAttempts} attempts: ${err.message}`
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
this.socket.on("disconnect", (reason) => {
|
|
513
|
+
process.stderr.write(`[conveyor-agent] Disconnected: ${reason}
|
|
514
|
+
`);
|
|
515
|
+
if (reason === "io server disconnect" || reason === "server namespace disconnect") {
|
|
516
|
+
this.scheduleReconnectAfterServerDisconnect();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
this.socket.on("auth:rejected", () => {
|
|
520
|
+
process.stderr.write("[conveyor-agent] Auth rejected by server, refreshing taskToken\n");
|
|
521
|
+
void this.refreshTaskTokenFromBootstrap().catch(() => {
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
this.socket.io.on("reconnect", (reconnectAttempts) => {
|
|
525
|
+
process.stderr.write(
|
|
526
|
+
`[conveyor-agent] Reconnected (attempts: ${reconnectAttempts}, ${(/* @__PURE__ */ new Date()).toISOString()})
|
|
527
|
+
`
|
|
528
|
+
);
|
|
529
|
+
this.sendHeartbeat();
|
|
530
|
+
void this.reconnectToSession();
|
|
531
|
+
});
|
|
532
|
+
this.socket.io.on("reconnect_attempt", () => {
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
disconnect() {
|
|
537
|
+
this.stopProactiveTokenRefresh();
|
|
538
|
+
this.clearSocketRecycle();
|
|
539
|
+
this.stopHeartbeatWorker();
|
|
540
|
+
void this.flushEvents();
|
|
541
|
+
if (this.socket) {
|
|
542
|
+
this.socket.io.reconnection(false);
|
|
543
|
+
this.socket.removeAllListeners();
|
|
544
|
+
this.socket.disconnect();
|
|
545
|
+
this.socket = null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// ── Reconnect with retry ────────────────────────────────────────────
|
|
549
|
+
//
|
|
550
|
+
// Socket.IO already retries the transport forever. This higher-level helper
|
|
551
|
+
// re-issues the `connectAgent` RPC after a successful reconnect to re-join
|
|
552
|
+
// the session room and drain pending messages. We retry indefinitely with a
|
|
553
|
+
// capped exponential backoff — a stranded codespace with a missing agent is
|
|
554
|
+
// worse than a long-running reconnect loop, and a transient API outage
|
|
555
|
+
// shouldn't kill the agent process.
|
|
556
|
+
static RECONNECT_BASE_DELAY_MS = 2e3;
|
|
557
|
+
static RECONNECT_MAX_DELAY_MS = 6e4;
|
|
558
|
+
static RECONNECT_STATUS_EVERY_N = 3;
|
|
559
|
+
isReconnecting = false;
|
|
560
|
+
reconnectingAfterServerDisconnect = false;
|
|
561
|
+
/** Capped exponential backoff (2s, 4s, 8s, 16s, 32s, then 60s steady) shared
|
|
562
|
+
* by both reconnect loops (connectAgent-RPC and server-disconnect). */
|
|
563
|
+
static backoffDelayMs(attempt) {
|
|
564
|
+
return Math.min(
|
|
565
|
+
_AgentConnection.RECONNECT_BASE_DELAY_MS * 2 ** Math.min(attempt - 1, 5),
|
|
566
|
+
_AgentConnection.RECONNECT_MAX_DELAY_MS
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
/** Sleep `ms`, unref'd so it never holds the process open on its own. */
|
|
570
|
+
static delay(ms) {
|
|
571
|
+
return new Promise((resolve) => {
|
|
572
|
+
const timer = setTimeout(resolve, ms);
|
|
573
|
+
timer.unref?.();
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Invoked after every successful session reconnect (the `connectAgent` RPC
|
|
578
|
+
* re-established the session room). The runner uses this to force a TUI
|
|
579
|
+
* repaint: the reconnect may have landed on a different/restarted API
|
|
580
|
+
* process whose PTY scrollback ring is empty, and a quiet terminal would
|
|
581
|
+
* otherwise never re-seed it.
|
|
582
|
+
*/
|
|
583
|
+
onReconnected;
|
|
584
|
+
async reconnectToSession() {
|
|
585
|
+
if (this.isReconnecting) return;
|
|
586
|
+
this.isReconnecting = true;
|
|
587
|
+
try {
|
|
588
|
+
let attempt = 0;
|
|
589
|
+
while (this.socket) {
|
|
590
|
+
attempt++;
|
|
591
|
+
try {
|
|
592
|
+
const { pendingMessages } = await this.call("connectAgent", {
|
|
593
|
+
sessionId: this.config.sessionId
|
|
594
|
+
});
|
|
595
|
+
this.drainPendingMessages(pendingMessages);
|
|
596
|
+
process.stderr.write(
|
|
597
|
+
`[conveyor-agent] Reconnected to session successfully (attempts: ${attempt})
|
|
598
|
+
`
|
|
599
|
+
);
|
|
600
|
+
if (this.lastEmittedStatus && this.lastEmittedStatus !== this.lastReportedStatus) {
|
|
601
|
+
const status = this.lastEmittedStatus;
|
|
602
|
+
void this.call("reportAgentStatus", {
|
|
603
|
+
sessionId: this.config.sessionId,
|
|
604
|
+
status
|
|
605
|
+
}).then(() => {
|
|
606
|
+
this.lastReportedStatus = status;
|
|
607
|
+
}).catch(() => {
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
this.sendEvent({
|
|
611
|
+
type: "agent_runner_status",
|
|
612
|
+
reason: "reconnected",
|
|
613
|
+
attempts: attempt
|
|
614
|
+
});
|
|
615
|
+
try {
|
|
616
|
+
this.onReconnected?.();
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
} catch (err) {
|
|
621
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
622
|
+
const delayMs = _AgentConnection.backoffDelayMs(attempt);
|
|
623
|
+
process.stderr.write(
|
|
624
|
+
`[conveyor-agent] connectAgent failed (attempt ${attempt}): ${errMsg} \u2014 retrying in ${delayMs / 1e3}s
|
|
625
|
+
`
|
|
626
|
+
);
|
|
627
|
+
if (this.looksLikeAuthError(errMsg)) {
|
|
628
|
+
void this.refreshTaskTokenFromBootstrap().catch(() => {
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (attempt % _AgentConnection.RECONNECT_STATUS_EVERY_N === 0) {
|
|
632
|
+
this.sendEvent({
|
|
633
|
+
type: "agent_runner_status",
|
|
634
|
+
reason: "reconnecting",
|
|
635
|
+
attempt
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
await _AgentConnection.delay(delayMs);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} finally {
|
|
642
|
+
this.isReconnecting = false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Drive a bounded reconnect after a server-initiated disconnect. Loops until
|
|
647
|
+
* the socket reconnects or is torn down, nudging socket.connect() on each
|
|
648
|
+
* pass with a capped exponential backoff. A token refresh is attempted every
|
|
649
|
+
* pass (rate-limited to once/60s inside refreshTaskTokenFromBootstrap) but
|
|
650
|
+
* its result NEVER gates the reconnect — the socket must recover even when
|
|
651
|
+
* there is no fresh token to apply.
|
|
652
|
+
*/
|
|
653
|
+
scheduleReconnectAfterServerDisconnect() {
|
|
654
|
+
if (this.reconnectingAfterServerDisconnect) return;
|
|
655
|
+
this.reconnectingAfterServerDisconnect = true;
|
|
656
|
+
void this.reconnectAfterServerDisconnect().finally(() => {
|
|
657
|
+
this.reconnectingAfterServerDisconnect = false;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async reconnectAfterServerDisconnect() {
|
|
661
|
+
let attempt = 0;
|
|
662
|
+
while (this.socket && !this.socket.connected) {
|
|
663
|
+
attempt++;
|
|
664
|
+
try {
|
|
665
|
+
await this.refreshTaskTokenFromBootstrap();
|
|
666
|
+
} catch {
|
|
667
|
+
}
|
|
668
|
+
const socket = this.socket;
|
|
669
|
+
if (!socket || socket.connected) return;
|
|
670
|
+
socket.connect();
|
|
671
|
+
try {
|
|
672
|
+
await this.waitForConnected(
|
|
673
|
+
socket,
|
|
674
|
+
_AgentConnection.CALL_CONNECT_WAIT_MS,
|
|
675
|
+
"server-disconnect-reconnect"
|
|
676
|
+
);
|
|
677
|
+
this.sendHeartbeat();
|
|
678
|
+
void this.reconnectToSession();
|
|
679
|
+
return;
|
|
680
|
+
} catch {
|
|
681
|
+
const delayMs = _AgentConnection.backoffDelayMs(attempt);
|
|
682
|
+
process.stderr.write(
|
|
683
|
+
`[conveyor-agent] server-disconnect reconnect attempt ${attempt} did not connect within ${_AgentConnection.CALL_CONNECT_WAIT_MS / 1e3}s \u2014 retrying in ${delayMs / 1e3}s
|
|
684
|
+
`
|
|
685
|
+
);
|
|
686
|
+
await _AgentConnection.delay(delayMs);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
looksLikeAuthError(message) {
|
|
691
|
+
return /unauthor|forbid|auth|token|session (?:not found|expired|invalid)|invalid session/i.test(
|
|
692
|
+
message
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
// ── Proactive task-token refresh ────────────────────────────────────────
|
|
696
|
+
//
|
|
697
|
+
// Socket.IO only re-presents the taskToken on a (re)connect handshake, and
|
|
698
|
+
// the server only re-validates the JWT then. So a token that expires while
|
|
699
|
+
// the socket stays connected goes unnoticed until the next RPC fails. Re-mint
|
|
700
|
+
// periodically from the bootstrap endpoint — refreshFromBootstrap() updates
|
|
701
|
+
// both this.config.taskToken and socket.auth.taskToken, so any later
|
|
702
|
+
// reconnect carries a fresh token. No-ops for project mode / missing
|
|
703
|
+
// codespace env, and is rate-limited to once/60s inside refreshFromBootstrap.
|
|
704
|
+
startProactiveTokenRefresh() {
|
|
705
|
+
if (this.tokenRefreshTimer) return;
|
|
706
|
+
this.tokenRefreshTimer = setInterval(() => {
|
|
707
|
+
void this.refreshTaskTokenFromBootstrap().catch(() => {
|
|
708
|
+
});
|
|
709
|
+
}, TOKEN_REFRESH_INTERVAL_MS);
|
|
710
|
+
this.tokenRefreshTimer.unref?.();
|
|
711
|
+
}
|
|
712
|
+
stopProactiveTokenRefresh() {
|
|
713
|
+
if (this.tokenRefreshTimer) {
|
|
714
|
+
clearInterval(this.tokenRefreshTimer);
|
|
715
|
+
this.tokenRefreshTimer = null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
drainPendingMessages(messages) {
|
|
719
|
+
for (const msg of messages) {
|
|
720
|
+
if (!msg.content) continue;
|
|
721
|
+
if (this.messageCallback) {
|
|
722
|
+
this.messageCallback({ content: msg.content, userId: msg.userId });
|
|
723
|
+
} else {
|
|
724
|
+
this.earlyMessages.push({ content: msg.content, userId: msg.userId });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// ── Callback registration with early-buffer draining ───────────────
|
|
729
|
+
onMessage(callback) {
|
|
730
|
+
this.messageCallback = callback;
|
|
731
|
+
for (const msg of this.earlyMessages) callback(msg);
|
|
732
|
+
this.earlyMessages = [];
|
|
733
|
+
}
|
|
734
|
+
onStop(callback) {
|
|
735
|
+
this.stopCallback = callback;
|
|
736
|
+
if (this.earlyStop) {
|
|
737
|
+
callback();
|
|
738
|
+
this.earlyStop = false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
onSoftStop(callback) {
|
|
742
|
+
this.softStopCallback = callback;
|
|
743
|
+
if (this.earlySoftStop) {
|
|
744
|
+
callback();
|
|
745
|
+
this.earlySoftStop = false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
onModeChange(callback) {
|
|
749
|
+
this.modeChangeCallback = callback;
|
|
750
|
+
for (const data of this.earlyModeChanges) callback(data);
|
|
751
|
+
this.earlyModeChanges = [];
|
|
752
|
+
}
|
|
753
|
+
onApiKeyUpdate(callback) {
|
|
754
|
+
this.apiKeyUpdateCallback = callback;
|
|
755
|
+
}
|
|
756
|
+
onPullBranch(callback) {
|
|
757
|
+
this.pullBranchCallback = callback;
|
|
758
|
+
for (const data of this.earlyPullBranches) callback(data);
|
|
759
|
+
this.earlyPullBranches = [];
|
|
760
|
+
}
|
|
761
|
+
onSpawnReview(callback) {
|
|
762
|
+
this.spawnReviewCallback = callback;
|
|
763
|
+
for (const data of this.earlySpawnReviews) callback(data);
|
|
764
|
+
this.earlySpawnReviews = [];
|
|
765
|
+
}
|
|
766
|
+
/** Mirror of onSpawnReview for the Builder handoff. Drains the early buffer
|
|
767
|
+
* so a Build pressed during boot is not lost. */
|
|
768
|
+
onSpawnBuilder(callback) {
|
|
769
|
+
this.spawnBuilderCallback = callback;
|
|
770
|
+
for (const data of this.earlySpawnBuilders) callback(data);
|
|
771
|
+
this.earlySpawnBuilders = [];
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Report that a same-pod review child failed to spawn (fire-and-forget).
|
|
775
|
+
* The server Ends the orphaned review session and falls back to a dedicated
|
|
776
|
+
* review pod. sessionId is OUR (builder) session — the task-identity guard runs on
|
|
777
|
+
* it; the review session is identified separately.
|
|
778
|
+
*/
|
|
779
|
+
reportReviewSpawnFailure(reviewSessionId, error) {
|
|
780
|
+
if (!this.socket) return;
|
|
781
|
+
void this.call("reportReviewSpawnFailure", {
|
|
782
|
+
sessionId: this.config.sessionId,
|
|
783
|
+
reviewSessionId,
|
|
784
|
+
...error ? { error: error.slice(0, 2e3) } : {}
|
|
785
|
+
}).catch(() => {
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Report that this (planner) pod could not spawn the Builder child, so the
|
|
790
|
+
* server can End the orphaned build session and reopen the card instead of
|
|
791
|
+
* leaving it InProgress with a Builder tab that never appears.
|
|
792
|
+
*
|
|
793
|
+
* sessionId is OUR (planner) session — the task-identity guard runs on it;
|
|
794
|
+
* the build session is identified separately.
|
|
795
|
+
*/
|
|
796
|
+
reportBuilderSpawnFailure(buildSessionId, error) {
|
|
797
|
+
if (!this.socket) return;
|
|
798
|
+
void this.call("reportBuilderSpawnFailure", {
|
|
799
|
+
sessionId: this.config.sessionId,
|
|
800
|
+
buildSessionId,
|
|
801
|
+
...error ? { error: error.slice(0, 2e3) } : {}
|
|
802
|
+
}).catch(() => {
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Report that this pod's git credential is dead and refreshing did not fix
|
|
807
|
+
* it (fire-and-forget).
|
|
808
|
+
*
|
|
809
|
+
* Purely diagnostic. Until this existed a pod could lose git entirely and
|
|
810
|
+
* leave no server-side trace at all — the refresh RPC succeeded every time,
|
|
811
|
+
* so the failure was visible only in pod stderr, which is why it took two
|
|
812
|
+
* investigations to attribute. `tokenShape` describes the served credential
|
|
813
|
+
* (length, prefix class, mtime) and NEVER carries its value.
|
|
814
|
+
*/
|
|
815
|
+
reportCredentialFailure(details) {
|
|
816
|
+
if (!this.socket) return;
|
|
817
|
+
void this.call("reportCredentialFailure", {
|
|
818
|
+
sessionId: this.config.sessionId,
|
|
819
|
+
...details.error ? { error: details.error.slice(0, 2e3) } : {},
|
|
820
|
+
...details.tokenShape ? { tokenShape: details.tokenShape.slice(0, 500) } : {},
|
|
821
|
+
...details.healed === void 0 ? {} : { healed: details.healed }
|
|
822
|
+
}).catch(() => {
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Ask the server to destroy and recreate this pod (fire-and-forget). The
|
|
827
|
+
* agent calls this only when it has proven it cannot recover in place — the
|
|
828
|
+
* shared `~/.claude` GCS FUSE mount is dead and no in-container action can
|
|
829
|
+
* remount it. The server rate-limits the recycle and posts `reason` to the
|
|
830
|
+
* card; old servers that don't know the method reject harmlessly, leaving
|
|
831
|
+
* today's behavior (a failed turn with a chat warning).
|
|
832
|
+
*/
|
|
833
|
+
requestWorkspaceRecycle(reason) {
|
|
834
|
+
if (!this.socket) return;
|
|
835
|
+
void this.call("requestWorkspaceRecycle", {
|
|
836
|
+
sessionId: this.config.sessionId,
|
|
837
|
+
reason: reason.slice(0, 2e3)
|
|
838
|
+
}).catch(() => {
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Fire-and-forget: every Claude API retry for the turn failed, so hand the
|
|
843
|
+
* card back. The server returns a PR-less InProgress card to Open, posts the
|
|
844
|
+
* cause, notifies stakeholders and sleeps this workspace. Best-effort — an
|
|
845
|
+
* old server rejects the unknown method harmlessly and today's behaviour
|
|
846
|
+
* (chat line + idle agent) remains. `detail` is the last API error text.
|
|
847
|
+
*/
|
|
848
|
+
reportApiOutage(detail, attempts) {
|
|
849
|
+
if (!this.socket) return;
|
|
850
|
+
void this.call("reportApiOutage", {
|
|
851
|
+
sessionId: this.config.sessionId,
|
|
852
|
+
detail: detail.slice(0, 2e3),
|
|
853
|
+
attempts
|
|
854
|
+
}).catch(() => {
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
onSpawnTui(callback) {
|
|
858
|
+
this.spawnTuiCallback = callback;
|
|
859
|
+
for (const data of this.earlySpawnTuis) callback(data);
|
|
860
|
+
this.earlySpawnTuis = [];
|
|
861
|
+
}
|
|
862
|
+
/** Register the on-demand usage-refresh handler; drains an early-buffered
|
|
863
|
+
* `session:probeUsage` that arrived before the runner was ready. */
|
|
864
|
+
onProbeUsage(callback) {
|
|
865
|
+
this.probeUsageCallback = callback;
|
|
866
|
+
if (this.earlyProbeUsage) {
|
|
867
|
+
this.earlyProbeUsage = false;
|
|
868
|
+
callback();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Report that a same-pod TUI/shell child failed to spawn (fire-and-forget).
|
|
873
|
+
* The server Ends the orphaned session — no fallback pod (unlike review).
|
|
874
|
+
* sessionId is OUR (builder) session — the task-identity guard runs on it.
|
|
875
|
+
*/
|
|
876
|
+
reportSessionSpawnFailure(spawnedSessionId, error) {
|
|
877
|
+
if (!this.socket) return;
|
|
878
|
+
void this.call("reportSessionSpawnFailure", {
|
|
879
|
+
sessionId: this.config.sessionId,
|
|
880
|
+
spawnedSessionId,
|
|
881
|
+
...error ? { error: error.slice(0, 2e3) } : {}
|
|
882
|
+
}).catch(() => {
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
/** Register the restart handler, draining a `session:runStartCommand` that
|
|
886
|
+
* arrived before the supervisor was ready. Collapsed to one drain: two
|
|
887
|
+
* clicks during boot should produce one restart, not two competing ones. */
|
|
888
|
+
onRunStartCommand(callback) {
|
|
889
|
+
this.runStartCommandCallback = callback;
|
|
890
|
+
if (this.earlyRunStartCommand) {
|
|
891
|
+
this.earlyRunStartCommand = false;
|
|
892
|
+
callback();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// ── PTY relay (S5 Connected-TUI terminal) ──────────────────────────
|
|
896
|
+
/**
|
|
897
|
+
* Forward a raw chunk of terminal output to the S2 relay (fire-and-forget).
|
|
898
|
+
* The first chunk creates the server-side scrollback ring, which is what
|
|
899
|
+
* surfaces the terminal in the UI. `dims` seed/refresh the ring geometry.
|
|
900
|
+
*/
|
|
901
|
+
sendPtyOutput(data, dims) {
|
|
902
|
+
if (!this.socket) return;
|
|
903
|
+
void this.call("ptyOutput", {
|
|
904
|
+
sessionId: this.config.sessionId,
|
|
905
|
+
data,
|
|
906
|
+
...dims ? { cols: dims.cols, rows: dims.rows } : {}
|
|
907
|
+
}).catch(() => {
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Forward one compact chat-proxy event derived from the transcript JSONL to
|
|
912
|
+
* the relay (fire-and-forget). Feeds the experimental chat PTY proxy ring.
|
|
913
|
+
* Old servers that don't know the method reject harmlessly.
|
|
914
|
+
*/
|
|
915
|
+
sendPtyChatEvent(event) {
|
|
916
|
+
if (!this.socket) return;
|
|
917
|
+
void this.call("ptyChatEvent", {
|
|
918
|
+
sessionId: this.config.sessionId,
|
|
919
|
+
event
|
|
920
|
+
}).catch(() => {
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Report that the interactive CLI process for this session has died and no
|
|
925
|
+
* respawn is imminent (fire-and-forget). The server clears the scrollback
|
|
926
|
+
* ring and broadcasts pty:ended so clients hide the Connected-TUI tab. Old
|
|
927
|
+
* servers that don't know the method reject harmlessly.
|
|
928
|
+
*/
|
|
929
|
+
sendPtyEnded() {
|
|
930
|
+
if (!this.socket) return;
|
|
931
|
+
void this.call("ptyEnded", { sessionId: this.config.sessionId }).catch(() => {
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Report the port this pod's in-pod PTY stream server bound to, or null when
|
|
936
|
+
* it stopped (fire-and-forget). The server persists it so a viewer can be
|
|
937
|
+
* handed a port-scoped tunnel URL and stream the TUI straight from the pod.
|
|
938
|
+
* Old servers that don't know the method reject harmlessly — the session then
|
|
939
|
+
* just stays on the relay transport.
|
|
940
|
+
*/
|
|
941
|
+
reportPtyStream(port) {
|
|
942
|
+
if (!this.socket) return;
|
|
943
|
+
void this.call("reportPtyStream", {
|
|
944
|
+
sessionId: this.config.sessionId,
|
|
945
|
+
port
|
|
946
|
+
}).catch(() => {
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/** Epoch ms of the last relayed TUI keystroke, or null before any. */
|
|
950
|
+
get lastPtyInputAt() {
|
|
951
|
+
return this._lastPtyInputAt;
|
|
952
|
+
}
|
|
953
|
+
/** Subscribe to relayed keystrokes. Returns an unsubscribe fn. */
|
|
954
|
+
onPtyInput(handler) {
|
|
955
|
+
this.ptyInputCallback = handler;
|
|
956
|
+
return () => {
|
|
957
|
+
if (this.ptyInputCallback === handler) this.ptyInputCallback = null;
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
/** Subscribe to relayed (reconciled) terminal resizes. Returns an unsubscribe fn. */
|
|
961
|
+
onPtyResize(handler) {
|
|
962
|
+
this.ptyResizeCallback = handler;
|
|
963
|
+
return () => {
|
|
964
|
+
if (this.ptyResizeCallback === handler) this.ptyResizeCallback = null;
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
// ── Convenience methods (thin wrappers around call / emit) ─────────
|
|
968
|
+
async emitStatus(status, reason, questionText) {
|
|
969
|
+
this.lastEmittedStatus = status;
|
|
970
|
+
await this.flushEvents();
|
|
971
|
+
const payload = {
|
|
972
|
+
sessionId: this.config.sessionId,
|
|
973
|
+
status,
|
|
974
|
+
...reason ? { reason } : {},
|
|
975
|
+
// Only sent with a pending TUI questionnaire (reason "user_question") so
|
|
976
|
+
// the server can surface the real question text in the notification.
|
|
977
|
+
...questionText ? { questionText } : {}
|
|
978
|
+
};
|
|
979
|
+
const AWAIT_STATUSES = ["idle", "waiting_for_input", "connected"];
|
|
980
|
+
if (AWAIT_STATUSES.includes(status)) {
|
|
981
|
+
try {
|
|
982
|
+
await this.call("reportAgentStatus", payload);
|
|
983
|
+
this.lastReportedStatus = status;
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
987
|
+
void this.call("reportAgentStatus", payload).then(() => {
|
|
988
|
+
this.lastReportedStatus = status;
|
|
989
|
+
}).catch(() => {
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
postChatMessage(content, milestone) {
|
|
994
|
+
if (!this.socket) return;
|
|
995
|
+
if (this.suppressIfDuplicate(content)) return;
|
|
996
|
+
void this.call("postAgentMessage", {
|
|
997
|
+
sessionId: this.config.sessionId,
|
|
998
|
+
content,
|
|
999
|
+
milestone
|
|
1000
|
+
}).catch(() => {
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
// Awaitable variant of postChatMessage for callers that need to guarantee
|
|
1004
|
+
// the message is acknowledged by the server before proceeding (e.g. before
|
|
1005
|
+
// aborting the session). Dedup still applies; a suppressed message resolves
|
|
1006
|
+
// immediately without hitting the wire.
|
|
1007
|
+
async postChatMessageAwait(content, milestone) {
|
|
1008
|
+
if (!this.socket) return;
|
|
1009
|
+
if (this.suppressIfDuplicate(content)) return;
|
|
1010
|
+
try {
|
|
1011
|
+
await this.call("postAgentMessage", {
|
|
1012
|
+
sessionId: this.config.sessionId,
|
|
1013
|
+
content,
|
|
1014
|
+
milestone
|
|
1015
|
+
});
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
process.stderr.write(
|
|
1018
|
+
`[conveyor-agent] postChatMessageAwait failed: ${err instanceof Error ? err.message : String(err)}
|
|
1019
|
+
`
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
suppressIfDuplicate(content) {
|
|
1024
|
+
const d = this.checkAndTrackDuplicate(content);
|
|
1025
|
+
if (!d.duplicate) return false;
|
|
1026
|
+
process.stderr.write(
|
|
1027
|
+
`[dedup] Suppressed near-duplicate (matched: "${d.matchedMessagePreview}")
|
|
1028
|
+
`
|
|
1029
|
+
);
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
// Exposed so `post_to_chat` can surface suppression back to the agent.
|
|
1033
|
+
checkAndTrackDuplicate(content) {
|
|
1034
|
+
const now = Date.now();
|
|
1035
|
+
this.recentMessages = this.recentMessages.filter(
|
|
1036
|
+
(m) => now - m.timestamp < _AgentConnection.DEDUP_WINDOW_MS
|
|
1037
|
+
);
|
|
1038
|
+
const words = new Set(
|
|
1039
|
+
content.toLowerCase().replace(/[^\w\s]/g, "").split(/\s+/).filter((w) => w.length >= 3)
|
|
1040
|
+
);
|
|
1041
|
+
if (words.size === 0) return { duplicate: false };
|
|
1042
|
+
for (const recent of this.recentMessages) {
|
|
1043
|
+
let intersection = 0;
|
|
1044
|
+
for (const w of words) if (recent.words.has(w)) intersection++;
|
|
1045
|
+
const union = (/* @__PURE__ */ new Set([...words, ...recent.words])).size;
|
|
1046
|
+
if (union > 0 && intersection / union > _AgentConnection.DEDUP_SIMILARITY_THRESHOLD) {
|
|
1047
|
+
return { duplicate: true, matchedMessagePreview: recent.preview };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const max = _AgentConnection.DEDUP_PREVIEW_LIMIT;
|
|
1051
|
+
const preview = content.length > max ? content.slice(0, max) + "\u2026" : content;
|
|
1052
|
+
this.recentMessages.push({ words, timestamp: now, preview });
|
|
1053
|
+
if (this.recentMessages.length > 3) this.recentMessages.shift();
|
|
1054
|
+
return { duplicate: false };
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* @param loopStatus overrides the status derived from the last emitted
|
|
1058
|
+
* runner status. SessionRunner passes it so an idle runner that still has
|
|
1059
|
+
* background work outstanding in the pod beats as `waiting` (→ `active` on
|
|
1060
|
+
* the wire) rather than `idle`, which would let the workspace activity
|
|
1061
|
+
* clock expire mid-gate. See connection/loop-lag.ts `heartbeatStatusFor`.
|
|
1062
|
+
*
|
|
1063
|
+
* Without an override the status comes from `loopStatusForRunnerStatus`, the
|
|
1064
|
+
* same total classifier SessionRunner uses, so both paths agree. This used to
|
|
1065
|
+
* be a partial map covering 5 of the 11 `AgentRunnerStatus` values with a
|
|
1066
|
+
* `?? "active"` fallback, which meant a parked runner (`waiting_for_input`,
|
|
1067
|
+
* `finished`, `error`, `stopping`, `disconnected`) beat as ACTIVE on every
|
|
1068
|
+
* no-arg call site — the reconnect paths below, and the shell/project/adhoc
|
|
1069
|
+
* runners, which never pass a loop status at all. That renewed the workspace
|
|
1070
|
+
* activity clock for an agent doing nothing, so the card stayed "active" and
|
|
1071
|
+
* its pod stayed up long past the project's inactivity window.
|
|
1072
|
+
*/
|
|
1073
|
+
sendHeartbeat(loopLagMs, loopStatus) {
|
|
1074
|
+
if (!this.socket) return;
|
|
1075
|
+
const heartbeatStatus = heartbeatStatusFor(
|
|
1076
|
+
loopStatus ?? loopStatusForRunnerStatus(this.lastEmittedStatus)
|
|
1077
|
+
);
|
|
1078
|
+
void this.call("heartbeat", {
|
|
1079
|
+
sessionId: this.config.sessionId,
|
|
1080
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1081
|
+
status: heartbeatStatus,
|
|
1082
|
+
...loopLagMs !== void 0 && loopLagMs > 0 ? { loopLagMs: Math.round(loopLagMs) } : {}
|
|
1083
|
+
}).catch(() => {
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
// ── Starvation-proof heartbeat worker ────────────────────────────────
|
|
1087
|
+
//
|
|
1088
|
+
// A worker thread with its own event loop + Socket.IO connection keeps the
|
|
1089
|
+
// v3 session lease renewed even when the MAIN loop is stalled (the failure
|
|
1090
|
+
// mode where a heavy gate got the session declared stranded and restarted
|
|
1091
|
+
// mid-run). Best-effort by design: any spawn/runtime failure just degrades
|
|
1092
|
+
// heartbeats to main-loop-only. See heartbeat-worker.ts for the policy.
|
|
1093
|
+
heartbeatWorker = null;
|
|
1094
|
+
startHeartbeatWorker(sharedBuffer, intervalMs = 3e4) {
|
|
1095
|
+
if (this.heartbeatWorker) return;
|
|
1096
|
+
try {
|
|
1097
|
+
const workerUrl = new URL("./heartbeat-worker.js", import.meta.url);
|
|
1098
|
+
if (!existsSync(fileURLToPath(workerUrl))) {
|
|
1099
|
+
process.stderr.write(
|
|
1100
|
+
"[conveyor-agent] heartbeat worker bundle not found \u2014 main-loop heartbeat only\n"
|
|
1101
|
+
);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const worker = new Worker(workerUrl, {
|
|
1105
|
+
workerData: {
|
|
1106
|
+
apiUrl: this.config.apiUrl,
|
|
1107
|
+
taskToken: this.config.taskToken,
|
|
1108
|
+
sessionId: this.config.sessionId,
|
|
1109
|
+
runnerMode: this.config.runnerMode ?? "task",
|
|
1110
|
+
sharedBuffer,
|
|
1111
|
+
intervalMs
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
worker.unref();
|
|
1115
|
+
worker.on("error", (err) => {
|
|
1116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1117
|
+
process.stderr.write(`[conveyor-agent] heartbeat worker error: ${message}
|
|
1118
|
+
`);
|
|
1119
|
+
this.heartbeatWorker = null;
|
|
1120
|
+
});
|
|
1121
|
+
worker.on("exit", (code) => {
|
|
1122
|
+
if (code !== 0) {
|
|
1123
|
+
process.stderr.write(`[conveyor-agent] heartbeat worker exited (code ${code})
|
|
1124
|
+
`);
|
|
1125
|
+
}
|
|
1126
|
+
this.heartbeatWorker = null;
|
|
1127
|
+
});
|
|
1128
|
+
this.heartbeatWorker = worker;
|
|
1129
|
+
process.stderr.write("[conveyor-agent] heartbeat worker started\n");
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
process.stderr.write(
|
|
1132
|
+
`[conveyor-agent] heartbeat worker failed to start: ${err instanceof Error ? err.message : String(err)}
|
|
1133
|
+
`
|
|
1134
|
+
);
|
|
1135
|
+
this.heartbeatWorker = null;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
stopHeartbeatWorker() {
|
|
1139
|
+
const worker = this.heartbeatWorker;
|
|
1140
|
+
this.heartbeatWorker = null;
|
|
1141
|
+
if (worker) void worker.terminate();
|
|
1142
|
+
}
|
|
1143
|
+
emitModeChanged(agentMode) {
|
|
1144
|
+
this.sendEvent({ type: "mode_changed", agentMode });
|
|
1145
|
+
}
|
|
1146
|
+
async updateTaskFields(fields) {
|
|
1147
|
+
if (!this.socket) return { ok: false, error: "socket not connected" };
|
|
1148
|
+
try {
|
|
1149
|
+
await this.call("updateTaskFields", { sessionId: this.config.sessionId, ...fields });
|
|
1150
|
+
return { ok: true };
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
storeSessionId(sdkSessionId) {
|
|
1156
|
+
void this.call("storeSessionId", { sessionId: this.config.sessionId, sdkSessionId }).catch(
|
|
1157
|
+
() => {
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
/** Report the full current set of runtime-discovered listening ports.
|
|
1162
|
+
* Throws on failure so the PortDiscovery poller can retry on its next
|
|
1163
|
+
* tick (a swallowed error here would silently drop the delta). */
|
|
1164
|
+
async reportDiscoveredPorts(ports) {
|
|
1165
|
+
await this.call("reportDiscoveredPorts", { sessionId: this.config.sessionId, ports });
|
|
1166
|
+
}
|
|
1167
|
+
/** Boot-milestone report over the socket — the codespace-parity fallback
|
|
1168
|
+
* for the GKE pod bootstrap-token route. Fire-and-forget: a failed report
|
|
1169
|
+
* must never delay or fail the boot path. */
|
|
1170
|
+
reportBootMilestone(key) {
|
|
1171
|
+
void this.call("reportBootMilestone", { sessionId: this.config.sessionId, key }).catch(
|
|
1172
|
+
() => {
|
|
1173
|
+
}
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
// ── Typing indicators ───────────────────────────────────────────────
|
|
1177
|
+
sendTypingStart() {
|
|
1178
|
+
this.sendEvent({ type: "agent_typing_start" });
|
|
1179
|
+
}
|
|
1180
|
+
sendTypingStop() {
|
|
1181
|
+
this.sendEvent({ type: "agent_typing_stop" });
|
|
1182
|
+
}
|
|
1183
|
+
// ── RPC convenience wrappers (v6 compat, will migrate to call()) ───
|
|
1184
|
+
emitRateLimitPause(resetsAt) {
|
|
1185
|
+
this.sendEvent({ type: "rate_limit_update", resetsAt });
|
|
1186
|
+
}
|
|
1187
|
+
updateStatus(status) {
|
|
1188
|
+
this.emitStatus(status);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* The session's key hit a hard usage cap — ask the server to stamp it
|
|
1192
|
+
* limited and hand back the best remaining key's credential env (or a
|
|
1193
|
+
* requeue confirmation when none is left). Awaited: the caller swaps
|
|
1194
|
+
* credentials and resumes on success, so it needs the real response.
|
|
1195
|
+
*/
|
|
1196
|
+
async cycleCodingAgentKey(rateLimitType, resetsAt) {
|
|
1197
|
+
return await this.call("cycleCodingAgentKey", {
|
|
1198
|
+
sessionId: this.config.sessionId,
|
|
1199
|
+
rateLimitType,
|
|
1200
|
+
...resetsAt ? { resetsAt } : {}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
// ── Question handling ──────────────────────────────────────────────
|
|
1204
|
+
async askUserQuestion(questions) {
|
|
1205
|
+
const questionText = questions.map(
|
|
1206
|
+
(q) => `**${q.header}**
|
|
1207
|
+
${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o.description}`).join("\n") : ""}`
|
|
1208
|
+
).join("\n\n");
|
|
1209
|
+
const requestId = crypto.randomUUID();
|
|
1210
|
+
const roomEventPromise = new Promise((resolve) => {
|
|
1211
|
+
this.pendingAnswerResolvers.set(requestId, resolve);
|
|
1212
|
+
});
|
|
1213
|
+
const rpcPromise = this.call("askUserQuestion", {
|
|
1214
|
+
sessionId: this.config.sessionId,
|
|
1215
|
+
question: questionText,
|
|
1216
|
+
requestId,
|
|
1217
|
+
questions
|
|
1218
|
+
}).then((res) => res.answers);
|
|
1219
|
+
try {
|
|
1220
|
+
return await Promise.race([rpcPromise, roomEventPromise]);
|
|
1221
|
+
} finally {
|
|
1222
|
+
this.pendingAnswerResolvers.delete(requestId);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// ── Typed service method wrappers ───────────────────────────────────
|
|
1226
|
+
getTaskProperties() {
|
|
1227
|
+
return this.call("getTaskProperties", { sessionId: this.config.sessionId });
|
|
1228
|
+
}
|
|
1229
|
+
triggerIdentification() {
|
|
1230
|
+
return this.call("triggerIdentification", { sessionId: this.config.sessionId });
|
|
1231
|
+
}
|
|
1232
|
+
handoffToImplementer(payload) {
|
|
1233
|
+
return this.call("handoffToImplementer", {
|
|
1234
|
+
sessionId: this.config.sessionId,
|
|
1235
|
+
...payload
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
async refreshAuthToken() {
|
|
1239
|
+
const result = await this.refreshFromBootstrap();
|
|
1240
|
+
return result.refreshedClaude;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Refresh the in-process `CONVEYOR_TASK_TOKEN` from the bootstrap endpoint.
|
|
1244
|
+
* Returns true if a new token was applied. Rate-limited locally to once per
|
|
1245
|
+
* 60s so a tight auth-rejected loop can't hammer the bootstrap endpoint —
|
|
1246
|
+
* the server enforces the same window via `lastBootstrapAt`.
|
|
1247
|
+
*/
|
|
1248
|
+
lastTaskTokenRefreshAt = 0;
|
|
1249
|
+
async refreshTaskTokenFromBootstrap() {
|
|
1250
|
+
const result = await this.refreshFromBootstrap();
|
|
1251
|
+
return result.refreshedTaskToken;
|
|
1252
|
+
}
|
|
1253
|
+
refreshFromBootstrap() {
|
|
1254
|
+
const none = Promise.resolve({ refreshedClaude: false, refreshedTaskToken: false });
|
|
1255
|
+
const podBootstrapToken = process.env.POD_BOOTSTRAP_TOKEN;
|
|
1256
|
+
const codespaceName = process.env.CODESPACE_NAME;
|
|
1257
|
+
const apiUrl = this.config.apiUrl;
|
|
1258
|
+
if (!apiUrl || !podBootstrapToken && !codespaceName) {
|
|
1259
|
+
return none;
|
|
1260
|
+
}
|
|
1261
|
+
const now = Date.now();
|
|
1262
|
+
if (now - this.lastTaskTokenRefreshAt < 6e4) {
|
|
1263
|
+
return none;
|
|
1264
|
+
}
|
|
1265
|
+
this.lastTaskTokenRefreshAt = now;
|
|
1266
|
+
if (podBootstrapToken) {
|
|
1267
|
+
return this.refreshFromV3Bootstrap(apiUrl, podBootstrapToken);
|
|
1268
|
+
}
|
|
1269
|
+
if (!codespaceName) return none;
|
|
1270
|
+
return this.refreshFromCodespaceBootstrap(apiUrl, codespaceName);
|
|
1271
|
+
}
|
|
1272
|
+
/** Legacy GitHub Codespaces refresh path — keys on instance name. */
|
|
1273
|
+
async refreshFromCodespaceBootstrap(apiUrl, codespaceName) {
|
|
1274
|
+
const bootstrapToken = process.env.CONVEYOR_BOOTSTRAP_TOKEN;
|
|
1275
|
+
const result = await fetchBootstrap({
|
|
1276
|
+
apiUrl,
|
|
1277
|
+
instanceName: codespaceName,
|
|
1278
|
+
bootstrapToken
|
|
1279
|
+
// Do not retry on http errors during a runtime refresh — a 401/403
|
|
1280
|
+
// means the token is consumed / session terminal and retrying won't
|
|
1281
|
+
// help. Network/timeout still retry inside fetchBootstrap.
|
|
1282
|
+
});
|
|
1283
|
+
if (!result.ok) {
|
|
1284
|
+
logger.warn("bootstrap refresh abandoned \u2014 pod credentials will go stale", {
|
|
1285
|
+
path: "codespace",
|
|
1286
|
+
reason: result.reason,
|
|
1287
|
+
status: result.status,
|
|
1288
|
+
attempts: result.attempts,
|
|
1289
|
+
detail: result.detail
|
|
1290
|
+
});
|
|
1291
|
+
return { refreshedClaude: false, refreshedTaskToken: false };
|
|
1292
|
+
}
|
|
1293
|
+
const previousTaskToken = process.env.CONVEYOR_TASK_TOKEN;
|
|
1294
|
+
applyBootstrapToEnv(result.config);
|
|
1295
|
+
const env = result.config.envVars ?? {};
|
|
1296
|
+
syncBundleGithubToken(env.CONVEYOR_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN);
|
|
1297
|
+
const refreshedTaskToken = result.config.mode !== "project" && Boolean(result.config.taskToken) && result.config.taskToken !== previousTaskToken;
|
|
1298
|
+
if (refreshedTaskToken && result.config.taskToken) {
|
|
1299
|
+
this.config.taskToken = result.config.taskToken;
|
|
1300
|
+
if (this.socket) {
|
|
1301
|
+
const auth = this.socket.auth;
|
|
1302
|
+
if (auth && typeof auth === "object") {
|
|
1303
|
+
auth.taskToken = result.config.taskToken;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
this.heartbeatWorker?.postMessage({ taskToken: result.config.taskToken });
|
|
1307
|
+
}
|
|
1308
|
+
const refreshedClaude = Boolean(result.config.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
|
|
1309
|
+
return { refreshedClaude, refreshedTaskToken };
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* v3 refresh: re-fetch the full bootstrap bundle from the pod's bound v3
|
|
1313
|
+
* route and swap the credentials in place. The GitHub installation token
|
|
1314
|
+
* dies at ~1h and the sessionJwt at 24h; re-polling the bootstrap GET with
|
|
1315
|
+
* the same pod token is the designed refresh mechanism.
|
|
1316
|
+
*/
|
|
1317
|
+
async refreshFromV3Bootstrap(apiUrl, bootstrapToken) {
|
|
1318
|
+
const bundle = await this.pollBundleWithRateLimitRetry(apiUrl, bootstrapToken);
|
|
1319
|
+
if (!bundle) {
|
|
1320
|
+
return { refreshedClaude: false, refreshedTaskToken: false };
|
|
1321
|
+
}
|
|
1322
|
+
const previousTaskToken = process.env.CONVEYOR_TASK_TOKEN;
|
|
1323
|
+
for (const [key, value] of Object.entries(bundle.envVars ?? {})) {
|
|
1324
|
+
process.env[key] = value;
|
|
1325
|
+
}
|
|
1326
|
+
if (bundle.githubToken) {
|
|
1327
|
+
process.env.CONVEYOR_GITHUB_TOKEN = bundle.githubToken;
|
|
1328
|
+
this.applyBundleCredentialFiles(bundle, previousTaskToken);
|
|
1329
|
+
}
|
|
1330
|
+
if (bundle.anthropicKey) process.env.ANTHROPIC_API_KEY = bundle.anthropicKey;
|
|
1331
|
+
if (bundle.gcpToken) process.env.CLOUDSDK_AUTH_ACCESS_TOKEN = bundle.gcpToken;
|
|
1332
|
+
const refreshedTaskToken = Boolean(bundle.sessionJwt) && bundle.sessionJwt !== previousTaskToken;
|
|
1333
|
+
if (refreshedTaskToken) {
|
|
1334
|
+
process.env.CONVEYOR_TASK_TOKEN = bundle.sessionJwt;
|
|
1335
|
+
this.config.taskToken = bundle.sessionJwt;
|
|
1336
|
+
if (this.socket) {
|
|
1337
|
+
const auth = this.socket.auth;
|
|
1338
|
+
if (auth && typeof auth === "object") {
|
|
1339
|
+
auth.taskToken = bundle.sessionJwt;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
this.heartbeatWorker?.postMessage({ taskToken: bundle.sessionJwt });
|
|
1343
|
+
}
|
|
1344
|
+
const refreshedClaude = Boolean(bundle.envVars?.CLAUDE_CODE_OAUTH_TOKEN);
|
|
1345
|
+
return { refreshedClaude, refreshedTaskToken };
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Write the bundle's GitHub token to the shared credential files — unless the
|
|
1349
|
+
* bundle belongs to another session.
|
|
1350
|
+
*
|
|
1351
|
+
* The bootstrap GET is keyed by the POD, so on a pod also hosting a same-pod
|
|
1352
|
+
* review it can resolve to the reader session, whose token is read-only.
|
|
1353
|
+
* Writing that over the shared files silently downgrades the builder's push
|
|
1354
|
+
* credential. Our own taskToken carries the same claims, so it is what we
|
|
1355
|
+
* compare against.
|
|
1356
|
+
*
|
|
1357
|
+
* The legitimate case this path exists for — our own session's bundle
|
|
1358
|
+
* refreshing the token when the RPC is failing — is unaffected.
|
|
1359
|
+
*/
|
|
1360
|
+
applyBundleCredentialFiles(bundle, previousTaskToken) {
|
|
1361
|
+
const self = readBundleIdentity(previousTaskToken);
|
|
1362
|
+
const result = applyBundleGithubToken(bundle, {
|
|
1363
|
+
sessionId: this.config.sessionId || self.sessionId,
|
|
1364
|
+
...self.role ? { role: self.role } : {}
|
|
1365
|
+
});
|
|
1366
|
+
if (!result.written && result.reason) {
|
|
1367
|
+
process.stderr.write(
|
|
1368
|
+
`[conveyor-agent] Skipped writing GitHub credential files from the bootstrap bundle: ${result.reason}
|
|
1369
|
+
`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Poll the bootstrap bundle once (maxWaitMs 0), retrying only on a transient
|
|
1375
|
+
* 429 (standby-pool pods share one Cloud NAT IP against podBootstrapLimiter)
|
|
1376
|
+
* with a short backoff. Returns null when the refresh should be abandoned —
|
|
1377
|
+
* a non-429 error, or 429s past the retry budget — so the caller no-ops
|
|
1378
|
+
* instead of parking a RUNNING pod in a poll loop.
|
|
1379
|
+
*/
|
|
1380
|
+
async pollBundleWithRateLimitRetry(apiUrl, bootstrapToken) {
|
|
1381
|
+
const retryDelaysMs = [1e3, 3e3];
|
|
1382
|
+
for (let attempt = 0; ; attempt++) {
|
|
1383
|
+
try {
|
|
1384
|
+
return await pollUntilBound({ apiUrl, bootstrapToken, maxWaitMs: 0 });
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
const isRateLimited = err instanceof PollUntilBoundHttpError && err.status === 429;
|
|
1387
|
+
if (!isRateLimited || attempt >= retryDelaysMs.length) {
|
|
1388
|
+
logger.warn("bootstrap refresh abandoned \u2014 pod credentials will go stale", {
|
|
1389
|
+
path: "v3",
|
|
1390
|
+
attempt,
|
|
1391
|
+
rateLimited: isRateLimited,
|
|
1392
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1393
|
+
});
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
await new Promise((resolve) => {
|
|
1397
|
+
setTimeout(resolve, retryDelaysMs[attempt]);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
// ── Event buffering ────────────────────────────────────────────────
|
|
1403
|
+
sendEvent(event) {
|
|
1404
|
+
if (!this.socket) return;
|
|
1405
|
+
this.enqueueEvents([{ event }], false);
|
|
1406
|
+
}
|
|
1407
|
+
/** Append (or, on `toFront`, prepend for a failed-flush re-queue) events to
|
|
1408
|
+
* the buffer, then cap + arm the flush timer. Single owner of the overflow
|
|
1409
|
+
* policy so append and re-queue can't diverge on the drop accounting. */
|
|
1410
|
+
enqueueEvents(entries, toFront) {
|
|
1411
|
+
if (toFront) this.eventBuffer.unshift(...entries);
|
|
1412
|
+
else this.eventBuffer.push(...entries);
|
|
1413
|
+
while (this.eventBuffer.length > MAX_EVENT_BUFFER) {
|
|
1414
|
+
this.eventBuffer.shift();
|
|
1415
|
+
this.droppedEventCount++;
|
|
1416
|
+
if (this.droppedEventCount === 1 || this.droppedEventCount % 500 === 0) {
|
|
1417
|
+
process.stderr.write(
|
|
1418
|
+
`[conveyor-agent] eventBuffer overflow \u2014 dropped ${this.droppedEventCount} event(s) (cap: ${MAX_EVENT_BUFFER})
|
|
1419
|
+
`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (this.socket && !this.flushTimer) {
|
|
1424
|
+
this.flushTimer = setTimeout(() => void this.flushEvents(), EVENT_BATCH_MS);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
async flushEvents() {
|
|
1428
|
+
if (this.flushTimer) {
|
|
1429
|
+
clearTimeout(this.flushTimer);
|
|
1430
|
+
this.flushTimer = null;
|
|
1431
|
+
}
|
|
1432
|
+
if (!this.socket || this.eventBuffer.length === 0) return;
|
|
1433
|
+
const entries = this.eventBuffer;
|
|
1434
|
+
this.eventBuffer = [];
|
|
1435
|
+
const events = entries.map((entry) => entry.event);
|
|
1436
|
+
try {
|
|
1437
|
+
await this.call("emitAgentEvent", { sessionId: this.config.sessionId, events });
|
|
1438
|
+
} catch {
|
|
1439
|
+
this.requeueFailedEvents(entries);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/** Put a failed flush's events back at the FRONT of the buffer, preserving
|
|
1443
|
+
* order, via the shared cap-and-arm path. */
|
|
1444
|
+
requeueFailedEvents(entries) {
|
|
1445
|
+
this.enqueueEvents(entries, true);
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
// ../shared/dist/chunk-42BS7Y35.js
|
|
1450
|
+
import { z } from "zod";
|
|
1451
|
+
var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
|
|
1452
|
+
var DEFAULT_OPUS_MODEL = "claude-opus-5";
|
|
1453
|
+
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
|
|
1454
|
+
var FABLE_MODEL = "claude-fable-5-1";
|
|
1455
|
+
var PREVIOUS_SONNET_MODEL = "claude-sonnet-4-6";
|
|
1456
|
+
var PREVIOUS_OPUS_MODEL = "claude-opus-4-8";
|
|
1457
|
+
var PTY_STREAM_PORT_BASE = 7420;
|
|
1458
|
+
var PTY_STREAM_PORT_ATTEMPTS = 8;
|
|
1459
|
+
function encodePtyStreamFrame(frame) {
|
|
1460
|
+
return `${JSON.stringify(frame)}
|
|
1461
|
+
`;
|
|
1462
|
+
}
|
|
1463
|
+
function isRecord(value) {
|
|
1464
|
+
return typeof value === "object" && value !== null;
|
|
1465
|
+
}
|
|
1466
|
+
function parsePtyStreamFrame(line) {
|
|
1467
|
+
if (!line) return null;
|
|
1468
|
+
let parsed;
|
|
1469
|
+
try {
|
|
1470
|
+
parsed = JSON.parse(line);
|
|
1471
|
+
} catch {
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
if (!isRecord(parsed) || typeof parsed.t !== "string") return null;
|
|
1475
|
+
switch (parsed.t) {
|
|
1476
|
+
case "hello":
|
|
1477
|
+
case "data":
|
|
1478
|
+
case "ended":
|
|
1479
|
+
case "input":
|
|
1480
|
+
case "resize":
|
|
1481
|
+
return parsed;
|
|
1482
|
+
default:
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
var PtyStreamFrameReader = class {
|
|
1487
|
+
constructor(onFrame) {
|
|
1488
|
+
this.onFrame = onFrame;
|
|
1489
|
+
}
|
|
1490
|
+
onFrame;
|
|
1491
|
+
buffer = "";
|
|
1492
|
+
push(chunk) {
|
|
1493
|
+
this.buffer += chunk;
|
|
1494
|
+
let index = this.buffer.indexOf("\n");
|
|
1495
|
+
while (index >= 0) {
|
|
1496
|
+
const line = this.buffer.slice(0, index);
|
|
1497
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
1498
|
+
const frame = parsePtyStreamFrame(line);
|
|
1499
|
+
if (frame) this.onFrame(frame);
|
|
1500
|
+
index = this.buffer.indexOf("\n");
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
var PREVIEW_PORT_DENY_LIST = [
|
|
1505
|
+
5432,
|
|
1506
|
+
6379,
|
|
1507
|
+
9200,
|
|
1508
|
+
...Array.from({ length: PTY_STREAM_PORT_ATTEMPTS }, (_, i) => PTY_STREAM_PORT_BASE + i)
|
|
1509
|
+
];
|
|
1510
|
+
function normalizeCheckpointPath(value) {
|
|
1511
|
+
let normalized = value.trim().replace(/\/{2,}/g, "/");
|
|
1512
|
+
normalized = normalized.split("/").filter((segment) => segment !== ".").join("/");
|
|
1513
|
+
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
1514
|
+
return normalized;
|
|
1515
|
+
}
|
|
1516
|
+
var checkpointPathSchema = z.string().transform(normalizeCheckpointPath).pipe(
|
|
1517
|
+
z.string().min(1).refine((value) => value !== ".", "Checkpoint paths must name a repository entry").refine((value) => !value.startsWith("/"), "Checkpoint paths must be repository-relative").refine(
|
|
1518
|
+
(value) => !/^[A-Za-z]:[\\/]/.test(value) && !value.includes("\\"),
|
|
1519
|
+
"Checkpoint paths must use repository-relative POSIX syntax"
|
|
1520
|
+
).refine(
|
|
1521
|
+
(value) => !value.split("/").includes(".."),
|
|
1522
|
+
"Checkpoint paths must not traverse a parent directory"
|
|
1523
|
+
)
|
|
1524
|
+
);
|
|
1525
|
+
var secretNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/);
|
|
1526
|
+
var checkpointKeySchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
1527
|
+
var checkpointDigestRefSchema = z.string().regex(/^[^\s@]+@sha256:[0-9a-f]{64}$/);
|
|
1528
|
+
var ACTIONS_PREBAKE_REGISTRY_PATTERN = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::(?:[1-9][0-9]{0,4}))?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
|
1529
|
+
var actionsPrebakeRegistrySchema = z.string().trim().min(1).regex(
|
|
1530
|
+
ACTIONS_PREBAKE_REGISTRY_PATTERN,
|
|
1531
|
+
"Actions prebake registry must be a lowercase host[:port] with an optional path prefix"
|
|
1532
|
+
).refine((value) => {
|
|
1533
|
+
const port = /:([0-9]+)(?:\/|$)/.exec(value)?.[1];
|
|
1534
|
+
return !port || Number(port) <= 65535;
|
|
1535
|
+
}, "Actions prebake registry port must be between 1 and 65535");
|
|
1536
|
+
function uniqueSortedArray(item, minimum = 0) {
|
|
1537
|
+
return z.array(item).min(minimum).superRefine((values, ctx) => {
|
|
1538
|
+
if (new Set(values).size !== values.length) {
|
|
1539
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Duplicate values are not allowed" });
|
|
1540
|
+
}
|
|
1541
|
+
}).transform((values) => [...values].sort());
|
|
1542
|
+
}
|
|
1543
|
+
var projectCheckpointSettingsSchema = z.object({
|
|
1544
|
+
enabled: z.literal(true),
|
|
1545
|
+
cacheCommand: z.string().trim().min(1),
|
|
1546
|
+
cacheInputPaths: uniqueSortedArray(checkpointPathSchema, 1),
|
|
1547
|
+
reusableArtifactPaths: uniqueSortedArray(checkpointPathSchema, 1),
|
|
1548
|
+
finalizeCommand: z.string().trim().min(1),
|
|
1549
|
+
credentialEpoch: z.string().trim().min(1),
|
|
1550
|
+
requiredSecretNames: uniqueSortedArray(secretNameSchema).optional(),
|
|
1551
|
+
optionalSecretNames: uniqueSortedArray(secretNameSchema).optional(),
|
|
1552
|
+
bakeWebAppBuild: z.boolean().optional()
|
|
1553
|
+
}).superRefine((checkpoint, ctx) => {
|
|
1554
|
+
const required = new Set(checkpoint.requiredSecretNames ?? []);
|
|
1555
|
+
for (const name of checkpoint.optionalSecretNames ?? []) {
|
|
1556
|
+
if (required.has(name)) {
|
|
1557
|
+
ctx.addIssue({
|
|
1558
|
+
code: z.ZodIssueCode.custom,
|
|
1559
|
+
path: ["optionalSecretNames"],
|
|
1560
|
+
message: "A secret cannot be both required and optional"
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
var CARD_DESCRIPTION_MAX = 255;
|
|
1566
|
+
var CARD_DESCRIPTION_LIMIT_MESSAGE = `Card descriptions are capped at ${CARD_DESCRIPTION_MAX} characters \u2014 write 1-2 plain sentences a non-engineer can read; put technical detail in the plan or card chat.`;
|
|
1567
|
+
var CARD_DESCRIPTION_FIELD_HINT = `max ${CARD_DESCRIPTION_MAX} chars, 1-2 plain sentences a non-engineer can read \u2014 put technical detail in the plan`;
|
|
1568
|
+
var DEFAULT_CI_WAIT_TIMEOUT_MINUTES = 45;
|
|
1569
|
+
var MAX_CI_WAIT_TIMEOUT_MINUTES = 180;
|
|
1570
|
+
var SEVERITY_ENUM = [
|
|
1571
|
+
"DEBUG",
|
|
1572
|
+
"INFO",
|
|
1573
|
+
"NOTICE",
|
|
1574
|
+
"WARNING",
|
|
1575
|
+
"ERROR",
|
|
1576
|
+
"CRITICAL",
|
|
1577
|
+
"ALERT",
|
|
1578
|
+
"EMERGENCY"
|
|
1579
|
+
];
|
|
1580
|
+
var MAX_LINE_CHARS = 400;
|
|
1581
|
+
var DEFAULT_SINCE_MINUTES = 60;
|
|
1582
|
+
function truncateLine(text) {
|
|
1583
|
+
const oneLine = text.replace(/\s*\n\s*/g, " \u23CE ");
|
|
1584
|
+
if (oneLine.length <= MAX_LINE_CHARS) return oneLine;
|
|
1585
|
+
const overflow = oneLine.length - MAX_LINE_CHARS;
|
|
1586
|
+
return `${oneLine.slice(0, MAX_LINE_CHARS)}\u2026[+${overflow}c]`;
|
|
1587
|
+
}
|
|
1588
|
+
function entrySource(entry) {
|
|
1589
|
+
return entry.resource.service_name ?? entry.resource.pod_name ?? entry.resource.database_id ?? entry.resourceType ?? "-";
|
|
1590
|
+
}
|
|
1591
|
+
var PAYLOAD_SKIP_KEYS = /* @__PURE__ */ new Set(["message", "severity", "timestamp", "level", "stack"]);
|
|
1592
|
+
var PAYLOAD_PRIORITY = [
|
|
1593
|
+
"error",
|
|
1594
|
+
"outcome",
|
|
1595
|
+
"serviceName",
|
|
1596
|
+
"methodName",
|
|
1597
|
+
"userId",
|
|
1598
|
+
"taskId",
|
|
1599
|
+
"sessionId",
|
|
1600
|
+
"workspaceId",
|
|
1601
|
+
"projectId",
|
|
1602
|
+
"durationMs"
|
|
1603
|
+
];
|
|
1604
|
+
var PAYLOAD_VALUE_MAX_CHARS = 160;
|
|
1605
|
+
function compactPayloadValue(value) {
|
|
1606
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
1607
|
+
const flat = (raw ?? "undefined").replace(/\s+/g, " ");
|
|
1608
|
+
const quoted = typeof value === "string" && /[\s"]/.test(flat) ? JSON.stringify(flat) : flat;
|
|
1609
|
+
return quoted.length > PAYLOAD_VALUE_MAX_CHARS ? `${quoted.slice(0, PAYLOAD_VALUE_MAX_CHARS)}\u2026` : quoted;
|
|
1610
|
+
}
|
|
1611
|
+
function formatPayloadSuffix(payloadJson) {
|
|
1612
|
+
if (!payloadJson) return "";
|
|
1613
|
+
let parsed;
|
|
1614
|
+
try {
|
|
1615
|
+
parsed = JSON.parse(payloadJson);
|
|
1616
|
+
} catch {
|
|
1617
|
+
return "";
|
|
1618
|
+
}
|
|
1619
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
|
|
1620
|
+
const obj = parsed;
|
|
1621
|
+
const rank = (key) => {
|
|
1622
|
+
const i = PAYLOAD_PRIORITY.indexOf(key);
|
|
1623
|
+
return i === -1 ? PAYLOAD_PRIORITY.length : i;
|
|
1624
|
+
};
|
|
1625
|
+
const parts = Object.keys(obj).filter((k) => !PAYLOAD_SKIP_KEYS.has(k) && obj[k] !== void 0 && obj[k] !== null).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).map((k) => `${k}=${compactPayloadValue(obj[k])}`);
|
|
1626
|
+
return parts.length > 0 ? ` | ${parts.join(" ")}` : "";
|
|
1627
|
+
}
|
|
1628
|
+
function formatLogEntryLine(entry) {
|
|
1629
|
+
const httpPrefix = entry.httpRequest?.status ? `http ${entry.httpRequest.status} ${entry.httpRequest.method ?? ""} ${entry.httpRequest.url ?? ""}`.trim() + " \u2014 " : "";
|
|
1630
|
+
return `${entry.timestamp} ${entry.severity.padEnd(7)} [${entrySource(entry)}] ${truncateLine(
|
|
1631
|
+
`${httpPrefix}${entry.message}${formatPayloadSuffix(entry.payload)}`
|
|
1632
|
+
)}`;
|
|
1633
|
+
}
|
|
1634
|
+
async function runQueryGcpLogs(port, params, now = Date.now) {
|
|
1635
|
+
const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
|
|
1636
|
+
const result = await port.queryGcpLogs({
|
|
1637
|
+
projectId: params.projectId,
|
|
1638
|
+
env: params.env,
|
|
1639
|
+
severity: params.severity,
|
|
1640
|
+
services: params.services,
|
|
1641
|
+
sqlInstances: params.sqlInstances,
|
|
1642
|
+
allServices: params.allServices,
|
|
1643
|
+
search: params.search,
|
|
1644
|
+
filter: params.filter,
|
|
1645
|
+
startTime,
|
|
1646
|
+
endTime: params.endTime,
|
|
1647
|
+
limit: params.limit,
|
|
1648
|
+
pageToken: params.pageToken
|
|
1649
|
+
});
|
|
1650
|
+
if (result.error) return result.error;
|
|
1651
|
+
const header = [
|
|
1652
|
+
`env=${params.env ?? "prod"}`,
|
|
1653
|
+
`window=${startTime}\u2192${params.endTime ?? "now"}`,
|
|
1654
|
+
...params.severity ? [`minSeverity=${params.severity}`] : [],
|
|
1655
|
+
`scope=${result.scopedServices ? `[${result.scopedServices.join(", ")}]` : "all"}`,
|
|
1656
|
+
`entries=${result.entries.length}`
|
|
1657
|
+
].join(" ");
|
|
1658
|
+
const lines = result.entries.map(formatLogEntryLine);
|
|
1659
|
+
const footer = result.nextPageToken ? [`-- more available: pass pageToken="${result.nextPageToken}" to continue`] : [];
|
|
1660
|
+
if (lines.length === 0) {
|
|
1661
|
+
return [
|
|
1662
|
+
header,
|
|
1663
|
+
"(no matching log entries \u2014 widen the window, lower minSeverity, or drop filters)"
|
|
1664
|
+
].join("\n");
|
|
1665
|
+
}
|
|
1666
|
+
return [header, ...lines, ...footer].join("\n");
|
|
1667
|
+
}
|
|
1668
|
+
async function runQueryGrafanaLogs(port, params, now = Date.now) {
|
|
1669
|
+
const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
|
|
1670
|
+
const result = await port.queryGrafanaLogs({
|
|
1671
|
+
projectId: params.projectId,
|
|
1672
|
+
env: params.env,
|
|
1673
|
+
level: params.level,
|
|
1674
|
+
services: params.services,
|
|
1675
|
+
search: params.search,
|
|
1676
|
+
logql: params.logql,
|
|
1677
|
+
startTime,
|
|
1678
|
+
endTime: params.endTime,
|
|
1679
|
+
limit: params.limit
|
|
1680
|
+
});
|
|
1681
|
+
if (result.error) return result.error;
|
|
1682
|
+
const header = [
|
|
1683
|
+
`env=${params.env ?? "prod"}`,
|
|
1684
|
+
`window=${startTime}\u2192${params.endTime ?? "now"}`,
|
|
1685
|
+
...params.level ? [`minLevel=${params.level}`] : [],
|
|
1686
|
+
...result.logql ? [`logql=${truncateLine(result.logql)}`] : [],
|
|
1687
|
+
`entries=${result.entries.length}`
|
|
1688
|
+
].join(" ");
|
|
1689
|
+
const lines = result.entries.map(formatLogEntryLine);
|
|
1690
|
+
const oldest = result.entries.map((e) => e.timestamp).sort()[0];
|
|
1691
|
+
const footer = result.hasMore ? [`-- hit the limit: older lines exist \u2014 pass endTime="${oldest}" to page further back`] : [];
|
|
1692
|
+
if (lines.length === 0) {
|
|
1693
|
+
return [
|
|
1694
|
+
header,
|
|
1695
|
+
"(no matching log entries \u2014 widen the window, lower minLevel, or drop filters)"
|
|
1696
|
+
].join("\n");
|
|
1697
|
+
}
|
|
1698
|
+
return [header, ...lines, ...footer].join("\n");
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// ../shared/dist/index.js
|
|
1702
|
+
import { z as z2 } from "zod";
|
|
1703
|
+
import { z as z22 } from "zod";
|
|
1704
|
+
import { z as z3 } from "zod";
|
|
1705
|
+
import { z as z4 } from "zod";
|
|
1706
|
+
import { z as z5 } from "zod";
|
|
1707
|
+
import { z as z6 } from "zod";
|
|
1708
|
+
import { z as z7 } from "zod";
|
|
1709
|
+
import { z as z8 } from "zod";
|
|
1710
|
+
import { z as z9 } from "zod";
|
|
1711
|
+
import { z as z10 } from "zod";
|
|
1712
|
+
var EXTERNAL_AGENT_MESSAGE_SOURCE = "external_agent";
|
|
1713
|
+
var TUI_KINDS = ["claude-code", "opencode", "codex"];
|
|
1714
|
+
var ACHIEVEMENT_RARITIES = [
|
|
1715
|
+
{
|
|
1716
|
+
key: "common",
|
|
1717
|
+
name: "Common",
|
|
1718
|
+
color: "#22c55e",
|
|
1719
|
+
iconPath: "/storypoints/square-solid-full.svg"
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
key: "magic",
|
|
1723
|
+
name: "Magic",
|
|
1724
|
+
color: "#3b82f6",
|
|
1725
|
+
iconPath: "/storypoints/diamond-solid-full.svg"
|
|
1726
|
+
},
|
|
1727
|
+
{ key: "rare", name: "Rare", color: "#eab308", iconPath: "/storypoints/gem-solid-full.svg" },
|
|
1728
|
+
{
|
|
1729
|
+
key: "unique",
|
|
1730
|
+
name: "Unique",
|
|
1731
|
+
color: "#f97316",
|
|
1732
|
+
iconPath: "/storypoints/scroll-sharp-solid-full.svg"
|
|
1733
|
+
},
|
|
1734
|
+
{ key: "pack", name: "Pack", color: "#9c27b0", iconPath: "/storypoints/pack.svg" }
|
|
1735
|
+
];
|
|
1736
|
+
var RISK_LEVELS = ["critical", "high", "medium", "low"];
|
|
1737
|
+
var riskLevelSchema = z2.enum(RISK_LEVELS);
|
|
1738
|
+
var DEFAULT_RISK_LEVELS = [
|
|
1739
|
+
{
|
|
1740
|
+
level: "critical",
|
|
1741
|
+
value: 4,
|
|
1742
|
+
label: "Critical",
|
|
1743
|
+
description: "Touches critical surface area; give it the closest review.",
|
|
1744
|
+
color: "#dc2626",
|
|
1745
|
+
ordinal: 0
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
level: "high",
|
|
1749
|
+
value: 3,
|
|
1750
|
+
label: "Elevated",
|
|
1751
|
+
description: "Touches important surface area; review carefully.",
|
|
1752
|
+
color: "#ea580c",
|
|
1753
|
+
ordinal: 1
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
level: "medium",
|
|
1757
|
+
value: 2,
|
|
1758
|
+
label: "Moderate",
|
|
1759
|
+
description: "Moderate surface area; normal review.",
|
|
1760
|
+
color: "#d97706",
|
|
1761
|
+
ordinal: 2
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
level: "low",
|
|
1765
|
+
value: 1,
|
|
1766
|
+
label: "Minimal",
|
|
1767
|
+
description: "Small or isolated surface area.",
|
|
1768
|
+
color: "#64748b",
|
|
1769
|
+
ordinal: 3
|
|
1770
|
+
}
|
|
1771
|
+
];
|
|
1772
|
+
var LEVEL_BY_VALUE = new Map(
|
|
1773
|
+
DEFAULT_RISK_LEVELS.map((m) => [m.value, m.level])
|
|
1774
|
+
);
|
|
1775
|
+
var ACTIVE_WORK_STATUSES = [
|
|
1776
|
+
"InProgress",
|
|
1777
|
+
"ReviewPR",
|
|
1778
|
+
"ReviewDev",
|
|
1779
|
+
"ReviewLive",
|
|
1780
|
+
"Complete"
|
|
1781
|
+
];
|
|
1782
|
+
var IDENTIFIED_WORK_STATUSES = ["Open", ...ACTIVE_WORK_STATUSES];
|
|
1783
|
+
var DEFAULT_TASK_STATUS_COLOR = "#a8a29e";
|
|
1784
|
+
var DEFAULT_TASK_STATUS_COLOR_INT = Number.parseInt(DEFAULT_TASK_STATUS_COLOR.slice(1), 16);
|
|
1785
|
+
var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
|
|
1786
|
+
var MAX_FILE_TAGS = 5;
|
|
1787
|
+
var MAX_FILE_TAG_LENGTH = 100;
|
|
1788
|
+
var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
|
|
1789
|
+
var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
|
|
1790
|
+
var IDLE_HEARTBEAT_MS = 90 * 1e3;
|
|
1791
|
+
var RUNNER_MODES = [
|
|
1792
|
+
"task",
|
|
1793
|
+
"plan",
|
|
1794
|
+
"pm",
|
|
1795
|
+
"code-review",
|
|
1796
|
+
"adhoc",
|
|
1797
|
+
"pack",
|
|
1798
|
+
"shell",
|
|
1799
|
+
"serving"
|
|
1800
|
+
];
|
|
1801
|
+
var TurnEndToolCallSchema = z22.object({
|
|
1802
|
+
tool: z22.string(),
|
|
1803
|
+
input: z22.string().optional(),
|
|
1804
|
+
output: z22.string().optional(),
|
|
1805
|
+
timestamp: z22.string().optional()
|
|
1806
|
+
}).passthrough();
|
|
1807
|
+
var KnownAgentEventSchema = z22.discriminatedUnion("type", [
|
|
1808
|
+
// ── Lifecycle / connection ────────────────────────────────────────────
|
|
1809
|
+
z22.object({
|
|
1810
|
+
type: z22.literal("connected"),
|
|
1811
|
+
sessionId: z22.string(),
|
|
1812
|
+
projectId: z22.string().optional()
|
|
1813
|
+
}).passthrough(),
|
|
1814
|
+
// Open-ended context snapshot spread from buildInitializationContext().
|
|
1815
|
+
z22.object({ type: z22.literal("session_manifest") }).passthrough(),
|
|
1816
|
+
z22.object({
|
|
1817
|
+
type: z22.literal("agent_runner_status"),
|
|
1818
|
+
reason: z22.string(),
|
|
1819
|
+
attempt: z22.number().optional(),
|
|
1820
|
+
attempts: z22.number().optional()
|
|
1821
|
+
}).passthrough(),
|
|
1822
|
+
z22.object({ type: z22.literal("shutdown"), reason: z22.string().optional() }).passthrough(),
|
|
1823
|
+
z22.object({ type: z22.literal("mode_changed"), agentMode: z22.string() }).passthrough(),
|
|
1824
|
+
z22.object({ type: z22.literal("mode_transition"), from: z22.string(), to: z22.string() }).passthrough(),
|
|
1825
|
+
// ── Turn stream ───────────────────────────────────────────────────────
|
|
1826
|
+
z22.object({ type: z22.literal("message"), content: z22.string() }).passthrough(),
|
|
1827
|
+
z22.object({ type: z22.literal("thinking"), message: z22.string() }).passthrough(),
|
|
1828
|
+
z22.object({
|
|
1829
|
+
type: z22.literal("tool_use"),
|
|
1830
|
+
tool: z22.string(),
|
|
1831
|
+
// Producers send JSON.stringify(input); consumers defend against
|
|
1832
|
+
// object inputs from older agents, so the wire stays permissive here.
|
|
1833
|
+
input: z22.unknown().optional()
|
|
1834
|
+
}).passthrough(),
|
|
1835
|
+
z22.object({
|
|
1836
|
+
type: z22.literal("tool_result"),
|
|
1837
|
+
tool: z22.string(),
|
|
1838
|
+
output: z22.unknown().optional(),
|
|
1839
|
+
isError: z22.boolean().optional(),
|
|
1840
|
+
redactedCount: z22.number().optional()
|
|
1841
|
+
}).passthrough(),
|
|
1842
|
+
z22.object({ type: z22.literal("turn_end"), toolCalls: z22.array(TurnEndToolCallSchema) }).passthrough(),
|
|
1843
|
+
z22.object({
|
|
1844
|
+
type: z22.literal("completed"),
|
|
1845
|
+
summary: z22.string().optional(),
|
|
1846
|
+
durationMs: z22.number().optional()
|
|
1847
|
+
}).passthrough(),
|
|
1848
|
+
z22.object({ type: z22.literal("error"), message: z22.string() }).passthrough(),
|
|
1849
|
+
z22.object({ type: z22.literal("agent_typing_start") }).passthrough(),
|
|
1850
|
+
z22.object({ type: z22.literal("agent_typing_stop") }).passthrough(),
|
|
1851
|
+
// ── Telemetry ─────────────────────────────────────────────────────────
|
|
1852
|
+
// heartbeat/typing: legacy telemetry the server still classifies as
|
|
1853
|
+
// transient (TRANSIENT_EVENT_TYPES) — kept in the vocabulary.
|
|
1854
|
+
z22.object({ type: z22.literal("heartbeat") }).passthrough(),
|
|
1855
|
+
z22.object({ type: z22.literal("typing") }).passthrough(),
|
|
1856
|
+
z22.object({
|
|
1857
|
+
type: z22.literal("context_update"),
|
|
1858
|
+
contextTokens: z22.number(),
|
|
1859
|
+
contextWindow: z22.number(),
|
|
1860
|
+
inputTokens: z22.number().optional(),
|
|
1861
|
+
cacheReadInputTokens: z22.number().optional(),
|
|
1862
|
+
cacheCreationInputTokens: z22.number().optional(),
|
|
1863
|
+
totalTokensUsed: z22.number().optional()
|
|
1864
|
+
}).passthrough(),
|
|
1865
|
+
// Four producer shapes share this type: {rateLimitType, utilization, status}
|
|
1866
|
+
// (SDK rate_limit_event), {resetsAt} (agent-connection resume notice), the
|
|
1867
|
+
// usage-sampler ({rateLimitType, utilization, status, resetsAt, gauges}
|
|
1868
|
+
// — resetsAt matches rateLimitType; gauges survives via .passthrough()), and
|
|
1869
|
+
// {unmeasurable, reason} (the sampler reporting it cannot read this key).
|
|
1870
|
+
z22.object({
|
|
1871
|
+
type: z22.literal("rate_limit_update"),
|
|
1872
|
+
rateLimitType: z22.string().optional(),
|
|
1873
|
+
utilization: z22.number().optional(),
|
|
1874
|
+
status: z22.string().optional(),
|
|
1875
|
+
resetsAt: z22.string().optional(),
|
|
1876
|
+
unmeasurable: z22.boolean().optional(),
|
|
1877
|
+
reason: z22.string().optional()
|
|
1878
|
+
}).passthrough(),
|
|
1879
|
+
z22.object({
|
|
1880
|
+
type: z22.literal("context_compacted"),
|
|
1881
|
+
trigger: z22.string().optional(),
|
|
1882
|
+
preTokens: z22.number().optional()
|
|
1883
|
+
}).passthrough(),
|
|
1884
|
+
z22.object({
|
|
1885
|
+
type: z22.literal("tool_progress"),
|
|
1886
|
+
toolName: z22.string().optional(),
|
|
1887
|
+
elapsedSeconds: z22.number().optional()
|
|
1888
|
+
}).passthrough(),
|
|
1889
|
+
z22.object({
|
|
1890
|
+
type: z22.literal("subagent_started"),
|
|
1891
|
+
sdkTaskId: z22.string().optional(),
|
|
1892
|
+
description: z22.string().optional()
|
|
1893
|
+
}).passthrough(),
|
|
1894
|
+
z22.object({
|
|
1895
|
+
type: z22.literal("subagent_progress"),
|
|
1896
|
+
sdkTaskId: z22.string().optional(),
|
|
1897
|
+
description: z22.string().optional(),
|
|
1898
|
+
toolUses: z22.number().optional(),
|
|
1899
|
+
durationMs: z22.number().optional()
|
|
1900
|
+
}).passthrough(),
|
|
1901
|
+
// ── Work products ─────────────────────────────────────────────────────
|
|
1902
|
+
z22.object({ type: z22.literal("pr_created"), url: z22.string(), number: z22.number() }).passthrough(),
|
|
1903
|
+
z22.object({
|
|
1904
|
+
type: z22.literal("code_review_complete"),
|
|
1905
|
+
result: z22.enum(["approved", "changes_requested"]),
|
|
1906
|
+
summary: z22.string().optional(),
|
|
1907
|
+
issues: z22.array(
|
|
1908
|
+
z22.object({
|
|
1909
|
+
file: z22.string(),
|
|
1910
|
+
line: z22.number().optional(),
|
|
1911
|
+
severity: z22.string().optional(),
|
|
1912
|
+
description: z22.string().optional()
|
|
1913
|
+
}).passthrough()
|
|
1914
|
+
).optional()
|
|
1915
|
+
}).passthrough(),
|
|
1916
|
+
// ── Environment setup / start command ─────────────────────────────────
|
|
1917
|
+
z22.object({ type: z22.literal("setup_output"), stream: z22.string(), data: z22.string() }).passthrough(),
|
|
1918
|
+
z22.object({
|
|
1919
|
+
type: z22.literal("setup_complete"),
|
|
1920
|
+
startCommandRunning: z22.boolean().optional(),
|
|
1921
|
+
startCommandConfigured: z22.boolean().optional(),
|
|
1922
|
+
// Sanitized server-side by sanitizeSessionPreviewPorts — stays unknown.
|
|
1923
|
+
previewPorts: z22.unknown().optional()
|
|
1924
|
+
}).passthrough(),
|
|
1925
|
+
z22.object({ type: z22.literal("setup_error"), message: z22.string() }).passthrough(),
|
|
1926
|
+
z22.object({ type: z22.literal("start_command_started") }).passthrough(),
|
|
1927
|
+
z22.object({ type: z22.literal("start_command_output"), stream: z22.string(), data: z22.string() }).passthrough(),
|
|
1928
|
+
z22.object({
|
|
1929
|
+
type: z22.literal("start_command_exited"),
|
|
1930
|
+
code: z22.number().nullable().optional(),
|
|
1931
|
+
signal: z22.string().nullable().optional(),
|
|
1932
|
+
message: z22.string().optional()
|
|
1933
|
+
}).passthrough(),
|
|
1934
|
+
z22.object({ type: z22.literal("start_command_error"), message: z22.string() }).passthrough()
|
|
1935
|
+
]);
|
|
1936
|
+
var AgentEventSchema = z22.union([
|
|
1937
|
+
KnownAgentEventSchema,
|
|
1938
|
+
z22.object({ type: z22.string().min(1) }).catchall(z22.unknown())
|
|
1939
|
+
]);
|
|
1940
|
+
var cardDescription = z3.string().max(CARD_DESCRIPTION_MAX, CARD_DESCRIPTION_LIMIT_MESSAGE).optional();
|
|
1941
|
+
var AgentHeartbeatSchema = z3.object({
|
|
1942
|
+
sessionId: z3.string().optional(),
|
|
1943
|
+
timestamp: z3.string(),
|
|
1944
|
+
status: z3.enum(["active", "idle", "building"]),
|
|
1945
|
+
currentAction: z3.string().optional(),
|
|
1946
|
+
/** Sender-observed main event-loop lag (ms) — see AgentHeartbeat.loopLagMs. */
|
|
1947
|
+
loopLagMs: z3.number().nonnegative().optional()
|
|
1948
|
+
});
|
|
1949
|
+
var CreatePRInputSchema = z3.object({
|
|
1950
|
+
title: z3.string().min(1),
|
|
1951
|
+
body: z3.string(),
|
|
1952
|
+
head: z3.string().optional(),
|
|
1953
|
+
base: z3.string().optional()
|
|
1954
|
+
});
|
|
1955
|
+
var PostToChatInputSchema = z3.object({
|
|
1956
|
+
message: z3.string().min(1),
|
|
1957
|
+
type: z3.enum(["message", "question", "update"]).optional().default("message"),
|
|
1958
|
+
milestone: z3.enum(["plan_ready", "implementation_complete", "blocked"]).optional()
|
|
1959
|
+
});
|
|
1960
|
+
var GetTaskContextRequestSchema = z3.object({
|
|
1961
|
+
sessionId: z3.string(),
|
|
1962
|
+
includeHistory: z3.boolean().optional().default(false),
|
|
1963
|
+
/**
|
|
1964
|
+
* Read the plan-revised marker WITHOUT consuming it. Bookkeeping fetches
|
|
1965
|
+
* (the session-identity check, the branch refresh) pass true so they cannot
|
|
1966
|
+
* race the boot fetch and swallow the notice before it reaches the prompt.
|
|
1967
|
+
* Defaults to false — consuming — so a pod running an older agent build still
|
|
1968
|
+
* clears the marker instead of showing the notice on every boot forever.
|
|
1969
|
+
*/
|
|
1970
|
+
peekPlanRevision: z3.boolean().optional().default(false)
|
|
1971
|
+
});
|
|
1972
|
+
var GetChatMessagesRequestSchema = z3.object({
|
|
1973
|
+
sessionId: z3.string(),
|
|
1974
|
+
limit: z3.number().int().positive().optional().default(50),
|
|
1975
|
+
offset: z3.number().int().nonnegative().optional().default(0),
|
|
1976
|
+
/** Task id or slug to read chat from. Omit for the session's own task. Only
|
|
1977
|
+
* the session's own task or one of its children resolves — anything else is
|
|
1978
|
+
* an error, never a silent fallback to the caller's own chat. */
|
|
1979
|
+
taskId: z3.string().optional()
|
|
1980
|
+
});
|
|
1981
|
+
var GetTaskFilesRequestSchema = z3.object({
|
|
1982
|
+
sessionId: z3.string()
|
|
1983
|
+
});
|
|
1984
|
+
var GetTaskFileRequestSchema = z3.object({
|
|
1985
|
+
sessionId: z3.string(),
|
|
1986
|
+
fileId: z3.string()
|
|
1987
|
+
});
|
|
1988
|
+
var GetTaskRequestSchema = z3.object({
|
|
1989
|
+
sessionId: z3.string(),
|
|
1990
|
+
taskSlugOrId: z3.string()
|
|
1991
|
+
});
|
|
1992
|
+
var GetCliHistoryRequestSchema = z3.object({
|
|
1993
|
+
sessionId: z3.string(),
|
|
1994
|
+
limit: z3.number().int().positive().optional().default(100),
|
|
1995
|
+
source: z3.enum(["agent", "application"]).optional(),
|
|
1996
|
+
/** Task id or slug to read logs from. Omit for the session's own task. Only
|
|
1997
|
+
* the session's own task or one of its children resolves — anything else is
|
|
1998
|
+
* an error, never a silent fallback to the caller's own logs. */
|
|
1999
|
+
taskId: z3.string().optional()
|
|
2000
|
+
});
|
|
2001
|
+
var ListSubtasksRequestSchema = z3.object({
|
|
2002
|
+
sessionId: z3.string(),
|
|
2003
|
+
/** "compact" returns the slim orchestration view (ListSubtasksCompactResponse:
|
|
2004
|
+
* per-child status, agent, story points, PR state, dependencies); "full" (default — wire-compat with older
|
|
2005
|
+
* agents) returns the verbose SubtaskSummaryDTO[] including description/plan. */
|
|
2006
|
+
view: z3.enum(["compact", "full"]).optional()
|
|
2007
|
+
});
|
|
2008
|
+
var GetDependenciesRequestSchema = z3.object({
|
|
2009
|
+
sessionId: z3.string()
|
|
2010
|
+
});
|
|
2011
|
+
var GetSuggestionsRequestSchema = z3.object({
|
|
2012
|
+
sessionId: z3.string(),
|
|
2013
|
+
status: z3.string().optional(),
|
|
2014
|
+
limit: z3.number().int().min(1).max(100).optional()
|
|
2015
|
+
});
|
|
2016
|
+
var ListManualTestsRequestSchema = z3.object({
|
|
2017
|
+
sessionId: z3.string()
|
|
2018
|
+
});
|
|
2019
|
+
var QueryManualTestsRequestSchema = z3.object({
|
|
2020
|
+
sessionId: z3.string(),
|
|
2021
|
+
cardStatuses: z3.array(z3.string()).optional(),
|
|
2022
|
+
testStatuses: z3.array(z3.enum(["open", "approved", "rejected"])).optional()
|
|
2023
|
+
});
|
|
2024
|
+
var CreatePullRequestRequestSchema = CreatePRInputSchema.extend({ sessionId: z3.string() });
|
|
2025
|
+
var RequestFileUploadRequestSchema = z3.object({
|
|
2026
|
+
sessionId: z3.string(),
|
|
2027
|
+
fileName: z3.string().min(1).max(255),
|
|
2028
|
+
mimeType: z3.string().min(1).max(128),
|
|
2029
|
+
fileSize: z3.number().int().positive().max(MAX_FILE_SIZE_BYTES)
|
|
2030
|
+
});
|
|
2031
|
+
var ConfirmFileUploadRequestSchema = z3.object({
|
|
2032
|
+
sessionId: z3.string(),
|
|
2033
|
+
fileId: z3.string(),
|
|
2034
|
+
title: z3.string().max(500).optional(),
|
|
2035
|
+
/** Glossary tag names (or ids) this file is an example of. */
|
|
2036
|
+
tags: z3.array(z3.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional()
|
|
2037
|
+
});
|
|
2038
|
+
var UpdateTaskStatusRequestSchema = z3.object({
|
|
2039
|
+
sessionId: z3.string(),
|
|
2040
|
+
status: z3.string(),
|
|
2041
|
+
force: z3.boolean().optional().default(false)
|
|
2042
|
+
});
|
|
2043
|
+
var StoreSessionIdRequestSchema = z3.object({
|
|
2044
|
+
sessionId: z3.string(),
|
|
2045
|
+
sdkSessionId: z3.string()
|
|
2046
|
+
});
|
|
2047
|
+
var SetManualTestsRequestSchema = z3.object({
|
|
2048
|
+
sessionId: z3.string(),
|
|
2049
|
+
items: z3.array(z3.object({ title: z3.string().min(1) })).min(1)
|
|
2050
|
+
});
|
|
2051
|
+
var EditManualTestRequestSchema = z3.object({
|
|
2052
|
+
sessionId: z3.string(),
|
|
2053
|
+
title: z3.string().min(1),
|
|
2054
|
+
newTitle: z3.string().min(1)
|
|
2055
|
+
});
|
|
2056
|
+
var RemoveManualTestRequestSchema = z3.object({
|
|
2057
|
+
sessionId: z3.string(),
|
|
2058
|
+
title: z3.string().min(1)
|
|
2059
|
+
});
|
|
2060
|
+
var ApproveManualTestRequestSchema = z3.object({
|
|
2061
|
+
sessionId: z3.string(),
|
|
2062
|
+
title: z3.string().min(1)
|
|
2063
|
+
});
|
|
2064
|
+
var RejectManualTestRequestSchema = z3.object({
|
|
2065
|
+
sessionId: z3.string(),
|
|
2066
|
+
title: z3.string().min(1),
|
|
2067
|
+
reason: z3.string().min(1).max(2e3)
|
|
2068
|
+
});
|
|
2069
|
+
var SessionStartRequestSchema = z3.object({
|
|
2070
|
+
sessionId: z3.string(),
|
|
2071
|
+
agentVersion: z3.string(),
|
|
2072
|
+
capabilities: z3.array(z3.string())
|
|
2073
|
+
});
|
|
2074
|
+
var SessionStopRequestSchema = z3.object({
|
|
2075
|
+
sessionId: z3.string(),
|
|
2076
|
+
reason: z3.string().optional()
|
|
2077
|
+
});
|
|
2078
|
+
var EndReviewSessionRequestSchema = z3.object({
|
|
2079
|
+
sessionId: z3.string(),
|
|
2080
|
+
reason: z3.enum(["approved", "changes_requested", "finished"]).optional()
|
|
2081
|
+
});
|
|
2082
|
+
var ConnectAgentRequestSchema = z3.object({
|
|
2083
|
+
sessionId: z3.string()
|
|
2084
|
+
});
|
|
2085
|
+
var ReportAgentStatusRequestSchema = z3.object({
|
|
2086
|
+
sessionId: z3.string(),
|
|
2087
|
+
status: z3.string(),
|
|
2088
|
+
/** Why the agent reports this status (e.g. "user_question" while an AskUserQuestion questionnaire is pending in the TUI). */
|
|
2089
|
+
reason: z3.string().optional(),
|
|
2090
|
+
/**
|
|
2091
|
+
* The pending question text, sent only alongside `reason: "user_question"`
|
|
2092
|
+
* so the server can surface it in the user-question notification body (and
|
|
2093
|
+
* thus the Attention feed) instead of a generic string. Optional: older
|
|
2094
|
+
* agents omit it and the server falls back to the generic wording.
|
|
2095
|
+
*/
|
|
2096
|
+
questionText: z3.string().optional()
|
|
2097
|
+
});
|
|
2098
|
+
var NotifyAgentVersionRequestSchema = z3.object({
|
|
2099
|
+
sessionId: z3.string(),
|
|
2100
|
+
agentVersion: z3.string()
|
|
2101
|
+
});
|
|
2102
|
+
var DiscoveredPortSchema = z3.object({
|
|
2103
|
+
port: z3.number().int().min(1).max(65535),
|
|
2104
|
+
label: z3.string().min(1).max(64).optional(),
|
|
2105
|
+
protocol: z3.enum(["http", "tcp"]).optional(),
|
|
2106
|
+
detectedAt: z3.string()
|
|
2107
|
+
});
|
|
2108
|
+
var ReportDiscoveredPortsRequestSchema = z3.object({
|
|
2109
|
+
sessionId: z3.string(),
|
|
2110
|
+
ports: z3.array(DiscoveredPortSchema).max(64)
|
|
2111
|
+
});
|
|
2112
|
+
var ReportBootMilestoneRequestSchema = z3.object({
|
|
2113
|
+
sessionId: z3.string(),
|
|
2114
|
+
key: z3.string().max(64)
|
|
2115
|
+
});
|
|
2116
|
+
var CreateSubtaskRequestSchema = z3.object({
|
|
2117
|
+
sessionId: z3.string(),
|
|
2118
|
+
title: z3.string().min(1),
|
|
2119
|
+
description: cardDescription,
|
|
2120
|
+
plan: z3.string().optional(),
|
|
2121
|
+
storyPointValue: z3.number().int().positive().optional(),
|
|
2122
|
+
ordinal: z3.number().int().nonnegative().optional(),
|
|
2123
|
+
followParentStatus: z3.boolean().optional(),
|
|
2124
|
+
/** Sibling subtask ids or slugs this subtask blocks on (explicit dependency
|
|
2125
|
+
* metadata — preferred over encoding order in plan text / ordinal). */
|
|
2126
|
+
dependsOn: z3.array(z3.string().min(1)).max(32).optional(),
|
|
2127
|
+
/** Glossary tag names to assign to the child. Unmatched names come back in
|
|
2128
|
+
* the response rather than failing the create. */
|
|
2129
|
+
tags: z3.array(z3.string().min(1)).max(10).optional()
|
|
2130
|
+
});
|
|
2131
|
+
var UpdateSubtaskRequestSchema = z3.object({
|
|
2132
|
+
sessionId: z3.string(),
|
|
2133
|
+
subtaskId: z3.string(),
|
|
2134
|
+
title: z3.string().min(1).optional(),
|
|
2135
|
+
description: cardDescription,
|
|
2136
|
+
plan: z3.string().optional(),
|
|
2137
|
+
/** Orchestration statuses only ("Planning" | "Open") — the pack parent's
|
|
2138
|
+
* sanctioned promotion path. Execution statuses stay with the build
|
|
2139
|
+
* pipeline / force_update_task_status. Enforced server-side. */
|
|
2140
|
+
status: z3.string().optional(),
|
|
2141
|
+
/** Assign a project agent to the child — accepts the agent's id or exact
|
|
2142
|
+
* name; resolved against the parent task's project server-side. */
|
|
2143
|
+
agentIdOrName: z3.string().min(1).optional(),
|
|
2144
|
+
storyPointValue: z3.number().int().positive().optional(),
|
|
2145
|
+
followParentStatus: z3.boolean().optional(),
|
|
2146
|
+
/** Replace this subtask's dependency edges with these sibling ids/slugs.
|
|
2147
|
+
* Empty array clears all. Omit to leave dependencies unchanged. */
|
|
2148
|
+
dependsOn: z3.array(z3.string().min(1)).max(32).optional()
|
|
2149
|
+
});
|
|
2150
|
+
var DeleteSubtaskRequestSchema = z3.object({
|
|
2151
|
+
sessionId: z3.string(),
|
|
2152
|
+
subtaskId: z3.string()
|
|
2153
|
+
});
|
|
2154
|
+
var SetSubtaskParentRequestSchema = z3.object({
|
|
2155
|
+
sessionId: z3.string(),
|
|
2156
|
+
taskId: z3.string().min(1),
|
|
2157
|
+
detach: z3.boolean().optional(),
|
|
2158
|
+
ordinal: z3.number().int().nonnegative().optional(),
|
|
2159
|
+
followParentStatus: z3.boolean().optional()
|
|
2160
|
+
});
|
|
2161
|
+
var GetTaskPropertiesRequestSchema = z3.object({
|
|
2162
|
+
sessionId: z3.string()
|
|
2163
|
+
});
|
|
2164
|
+
var UpdateTaskFieldsRequestSchema = z3.object({
|
|
2165
|
+
sessionId: z3.string(),
|
|
2166
|
+
plan: z3.string().optional(),
|
|
2167
|
+
description: cardDescription
|
|
2168
|
+
});
|
|
2169
|
+
var UpdateTaskPropertiesRequestSchema = z3.object({
|
|
2170
|
+
sessionId: z3.string(),
|
|
2171
|
+
title: z3.string().optional(),
|
|
2172
|
+
storyPointValue: z3.number().int().positive().optional(),
|
|
2173
|
+
tagIds: z3.array(z3.string()).optional(),
|
|
2174
|
+
tagNames: z3.array(z3.string()).optional(),
|
|
2175
|
+
githubPRUrl: z3.string().url().optional(),
|
|
2176
|
+
githubBranch: z3.string().optional(),
|
|
2177
|
+
// Canonical risk level, or null to clear — same semantics as the headless
|
|
2178
|
+
// update_task boundary (resolved to the project's Risk row in the handler).
|
|
2179
|
+
risk: riskLevelSchema.nullable().optional()
|
|
2180
|
+
});
|
|
2181
|
+
var ListIconsRequestSchema = z3.object({
|
|
2182
|
+
sessionId: z3.string()
|
|
2183
|
+
});
|
|
2184
|
+
var GenerateTaskIconRequestSchema = z3.object({
|
|
2185
|
+
sessionId: z3.string(),
|
|
2186
|
+
prompt: z3.string().min(1),
|
|
2187
|
+
aspectRatio: z3.string().optional()
|
|
2188
|
+
});
|
|
2189
|
+
var SearchFaIconsRequestSchema = z3.object({
|
|
2190
|
+
sessionId: z3.string(),
|
|
2191
|
+
query: z3.string().min(1),
|
|
2192
|
+
first: z3.number().int().positive().optional()
|
|
2193
|
+
});
|
|
2194
|
+
var PickFaIconRequestSchema = z3.object({
|
|
2195
|
+
sessionId: z3.string(),
|
|
2196
|
+
fontAwesomeId: z3.string().min(1),
|
|
2197
|
+
fontAwesomeStyle: z3.string().optional()
|
|
2198
|
+
});
|
|
2199
|
+
var CreateFollowUpTaskRequestSchema = z3.object({
|
|
2200
|
+
sessionId: z3.string(),
|
|
2201
|
+
title: z3.string().min(1),
|
|
2202
|
+
description: cardDescription,
|
|
2203
|
+
plan: z3.string().optional(),
|
|
2204
|
+
storyPointValue: z3.number().int().positive().optional()
|
|
2205
|
+
});
|
|
2206
|
+
var AddDependencyRequestSchema = z3.object({
|
|
2207
|
+
sessionId: z3.string(),
|
|
2208
|
+
dependsOnSlugOrId: z3.string()
|
|
2209
|
+
});
|
|
2210
|
+
var RemoveDependencyRequestSchema = z3.object({
|
|
2211
|
+
sessionId: z3.string(),
|
|
2212
|
+
dependsOnSlugOrId: z3.string()
|
|
2213
|
+
});
|
|
2214
|
+
var CreateSuggestionRequestSchema = z3.object({
|
|
2215
|
+
sessionId: z3.string(),
|
|
2216
|
+
title: z3.string().min(1),
|
|
2217
|
+
description: cardDescription,
|
|
2218
|
+
tagNames: z3.array(z3.string()).optional()
|
|
2219
|
+
});
|
|
2220
|
+
var VoteSuggestionRequestSchema = z3.object({
|
|
2221
|
+
sessionId: z3.string(),
|
|
2222
|
+
suggestionId: z3.string(),
|
|
2223
|
+
value: z3.union([z3.literal(1), z3.literal(-1)])
|
|
2224
|
+
});
|
|
2225
|
+
var TriggerIdentificationRequestSchema = z3.object({
|
|
2226
|
+
sessionId: z3.string()
|
|
2227
|
+
});
|
|
2228
|
+
var HandoffToImplementerRequestSchema = z3.object({
|
|
2229
|
+
sessionId: z3.string(),
|
|
2230
|
+
// Optional difficulty sizing — sets the task's story points before resolving
|
|
2231
|
+
// the matched implementer agent. Omit to hand off using the task's current
|
|
2232
|
+
// story points (or the project's default task agent when unsized).
|
|
2233
|
+
storyPoints: z3.number().int().positive().optional(),
|
|
2234
|
+
// Optional kickoff note posted to the task chat alongside the handoff notice.
|
|
2235
|
+
message: z3.string().optional()
|
|
2236
|
+
});
|
|
2237
|
+
var SubmitCodeReviewResultRequestSchema = z3.object({
|
|
2238
|
+
sessionId: z3.string(),
|
|
2239
|
+
approved: z3.boolean(),
|
|
2240
|
+
content: z3.string(),
|
|
2241
|
+
// Canonical risk level the reviewer assigned to this change. Required on every
|
|
2242
|
+
// verdict — the reviewer must judge it. Applied authoritatively server-side
|
|
2243
|
+
// (may raise OR lower an already-set value; the reviewer has that authority).
|
|
2244
|
+
risk: riskLevelSchema,
|
|
2245
|
+
// The commit SHA the reviewer actually reviewed. When present, the verdict is
|
|
2246
|
+
// rejected unless the task is still at this SHA (guards against a late
|
|
2247
|
+
// old-SHA verdict overwriting a newer review cycle).
|
|
2248
|
+
reviewedSha: z3.string().optional()
|
|
2249
|
+
});
|
|
2250
|
+
var CycleCodingAgentKeyRequestSchema = z3.object({
|
|
2251
|
+
sessionId: z3.string(),
|
|
2252
|
+
rateLimitType: z3.string(),
|
|
2253
|
+
resetsAt: z3.string().optional()
|
|
2254
|
+
});
|
|
2255
|
+
var PostChildChatMessageRequestSchema = z3.object({
|
|
2256
|
+
sessionId: z3.string(),
|
|
2257
|
+
childTaskId: z3.string(),
|
|
2258
|
+
message: z3.string().min(1)
|
|
2259
|
+
});
|
|
2260
|
+
var UpdateChildStatusRequestSchema = z3.object({
|
|
2261
|
+
sessionId: z3.string(),
|
|
2262
|
+
childTaskId: z3.string(),
|
|
2263
|
+
status: z3.string()
|
|
2264
|
+
});
|
|
2265
|
+
var GetAgentStatusRequestSchema = z3.object({
|
|
2266
|
+
taskId: z3.string()
|
|
2267
|
+
});
|
|
2268
|
+
var GetUiCliHistoryRequestSchema = z3.object({
|
|
2269
|
+
taskId: z3.string()
|
|
2270
|
+
});
|
|
2271
|
+
var GetActivePtySessionRequestSchema = z3.object({
|
|
2272
|
+
taskId: z3.string()
|
|
2273
|
+
});
|
|
2274
|
+
var ListActivePtySessionsRequestSchema = z3.object({
|
|
2275
|
+
taskId: z3.string()
|
|
2276
|
+
});
|
|
2277
|
+
var SendSoftStopRequestSchema = z3.object({
|
|
2278
|
+
taskId: z3.string()
|
|
2279
|
+
});
|
|
2280
|
+
var StopTaskSessionRequestSchema = z3.object({
|
|
2281
|
+
taskId: z3.string(),
|
|
2282
|
+
sessionId: z3.string()
|
|
2283
|
+
});
|
|
2284
|
+
var FlushTaskQueueRequestSchema = z3.object({
|
|
2285
|
+
taskId: z3.string(),
|
|
2286
|
+
softStop: z3.boolean().optional()
|
|
2287
|
+
});
|
|
2288
|
+
var CancelTaskQueuedMessageRequestSchema = z3.object({
|
|
2289
|
+
taskId: z3.string(),
|
|
2290
|
+
messageId: z3.string()
|
|
2291
|
+
});
|
|
2292
|
+
var FlushSingleQueuedMessageRequestSchema = z3.object({
|
|
2293
|
+
taskId: z3.string(),
|
|
2294
|
+
messageId: z3.string(),
|
|
2295
|
+
softStop: z3.boolean().optional()
|
|
2296
|
+
});
|
|
2297
|
+
var AnswerAgentQuestionRequestSchema = z3.object({
|
|
2298
|
+
taskId: z3.string(),
|
|
2299
|
+
requestId: z3.string(),
|
|
2300
|
+
answers: z3.record(z3.string(), z3.string())
|
|
2301
|
+
});
|
|
2302
|
+
var ClearAgentTodosRequestSchema = z3.object({
|
|
2303
|
+
taskId: z3.string()
|
|
2304
|
+
});
|
|
2305
|
+
var AgentQuestionOptionSchema = z3.object({
|
|
2306
|
+
label: z3.string(),
|
|
2307
|
+
description: z3.string(),
|
|
2308
|
+
preview: z3.string().optional()
|
|
2309
|
+
});
|
|
2310
|
+
var AgentQuestionSchema = z3.object({
|
|
2311
|
+
question: z3.string(),
|
|
2312
|
+
header: z3.string(),
|
|
2313
|
+
options: z3.array(AgentQuestionOptionSchema),
|
|
2314
|
+
multiSelect: z3.boolean().optional()
|
|
2315
|
+
});
|
|
2316
|
+
var AskUserQuestionRequestSchema = z3.object({
|
|
2317
|
+
sessionId: z3.string(),
|
|
2318
|
+
question: z3.string().min(1),
|
|
2319
|
+
requestId: z3.string().min(1),
|
|
2320
|
+
questions: z3.array(AgentQuestionSchema).min(1)
|
|
2321
|
+
});
|
|
2322
|
+
var PostAgentMessageRequestSchema = z3.object({
|
|
2323
|
+
sessionId: z3.string().min(1),
|
|
2324
|
+
content: z3.string(),
|
|
2325
|
+
milestone: z3.enum(["plan_ready", "implementation_complete", "blocked"]).optional()
|
|
2326
|
+
});
|
|
2327
|
+
var EmitAgentEventRequestSchema = z3.object({
|
|
2328
|
+
sessionId: z3.string(),
|
|
2329
|
+
events: z3.array(AgentEventSchema).max(500)
|
|
2330
|
+
});
|
|
2331
|
+
var RefreshGithubTokenRequestSchema = z3.object({
|
|
2332
|
+
sessionId: z3.string(),
|
|
2333
|
+
forceFresh: z3.boolean().optional()
|
|
2334
|
+
});
|
|
2335
|
+
var ReportCredentialFailureRequestSchema = z3.object({
|
|
2336
|
+
sessionId: z3.string(),
|
|
2337
|
+
error: z3.string().max(2e3).optional(),
|
|
2338
|
+
tokenShape: z3.string().max(500).optional(),
|
|
2339
|
+
healed: z3.boolean().optional()
|
|
2340
|
+
});
|
|
2341
|
+
var ReportReviewSpawnFailureRequestSchema = z3.object({
|
|
2342
|
+
sessionId: z3.string(),
|
|
2343
|
+
reviewSessionId: z3.string(),
|
|
2344
|
+
error: z3.string().max(2e3).optional()
|
|
2345
|
+
});
|
|
2346
|
+
var ReportBuilderSpawnFailureRequestSchema = ReportReviewSpawnFailureRequestSchema.omit({
|
|
2347
|
+
reviewSessionId: true
|
|
2348
|
+
}).extend({ buildSessionId: z3.string() });
|
|
2349
|
+
var SpawnTaskSessionRequestSchema = z3.object({
|
|
2350
|
+
taskId: z3.string(),
|
|
2351
|
+
kind: z3.enum(["tui", "shell"])
|
|
2352
|
+
});
|
|
2353
|
+
var StartCodeReviewRequestSchema = z3.object({
|
|
2354
|
+
taskId: z3.string(),
|
|
2355
|
+
force: z3.boolean().optional()
|
|
2356
|
+
});
|
|
2357
|
+
var StopCodeReviewRequestSchema = z3.object({
|
|
2358
|
+
taskId: z3.string()
|
|
2359
|
+
});
|
|
2360
|
+
var ReportSessionSpawnFailureRequestSchema = z3.object({
|
|
2361
|
+
sessionId: z3.string(),
|
|
2362
|
+
spawnedSessionId: z3.string(),
|
|
2363
|
+
error: z3.string().max(2e3).optional()
|
|
2364
|
+
});
|
|
2365
|
+
var RefreshGithubTokenResponseSchema = z3.object({
|
|
2366
|
+
token: z3.string()
|
|
2367
|
+
});
|
|
2368
|
+
var PTY_FRAME_MAX_CHARS = 256 * 1024;
|
|
2369
|
+
var PTY_MAX_DIMENSION = 1e3;
|
|
2370
|
+
var PtyOutputRequestSchema = z3.object({
|
|
2371
|
+
sessionId: z3.string(),
|
|
2372
|
+
data: z3.string().max(PTY_FRAME_MAX_CHARS),
|
|
2373
|
+
cols: z3.number().int().positive().max(PTY_MAX_DIMENSION).optional(),
|
|
2374
|
+
rows: z3.number().int().positive().max(PTY_MAX_DIMENSION).optional()
|
|
2375
|
+
});
|
|
2376
|
+
var PtyEndedRequestSchema = z3.object({
|
|
2377
|
+
sessionId: z3.string()
|
|
2378
|
+
});
|
|
2379
|
+
var PtyInputRequestSchema = z3.object({
|
|
2380
|
+
sessionId: z3.string(),
|
|
2381
|
+
data: z3.string().max(PTY_FRAME_MAX_CHARS)
|
|
2382
|
+
});
|
|
2383
|
+
var PtyResizeRequestSchema = z3.object({
|
|
2384
|
+
sessionId: z3.string(),
|
|
2385
|
+
cols: z3.number().int().positive().max(PTY_MAX_DIMENSION),
|
|
2386
|
+
rows: z3.number().int().positive().max(PTY_MAX_DIMENSION)
|
|
2387
|
+
});
|
|
2388
|
+
var PtyAttachRequestSchema = z3.object({
|
|
2389
|
+
sessionId: z3.string()
|
|
2390
|
+
});
|
|
2391
|
+
var ReportPtyStreamRequestSchema = z3.object({
|
|
2392
|
+
sessionId: z3.string(),
|
|
2393
|
+
port: z3.number().int().positive().max(65535).nullable()
|
|
2394
|
+
});
|
|
2395
|
+
var GetPtyStreamEndpointRequestSchema = z3.object({
|
|
2396
|
+
sessionId: z3.string()
|
|
2397
|
+
});
|
|
2398
|
+
var PtyChatEventPayloadSchema = z3.discriminatedUnion("kind", [
|
|
2399
|
+
z3.object({
|
|
2400
|
+
kind: z3.literal("init"),
|
|
2401
|
+
model: z3.string().max(200),
|
|
2402
|
+
claudeSessionId: z3.string().max(100).optional()
|
|
2403
|
+
}),
|
|
2404
|
+
z3.object({
|
|
2405
|
+
kind: z3.literal("user_text"),
|
|
2406
|
+
text: z3.string().max(16384),
|
|
2407
|
+
// Set by the SERVER (never the agent) when this prompt was injected by
|
|
2408
|
+
// Conveyor rather than typed by a human — the routed message's `source`
|
|
2409
|
+
// (`ci_success`, `review_trigger`, `automated_feedback`, …). The agent
|
|
2410
|
+
// pastes automated messages into the TUI exactly like human prompts, so the
|
|
2411
|
+
// CLI records both as plain transcript `user` records; without this the
|
|
2412
|
+
// builder chat renders "All CI checks passed on your PR." as the human's
|
|
2413
|
+
// own bubble. Absent ⇒ a genuine human prompt.
|
|
2414
|
+
source: z3.string().max(60).optional()
|
|
2415
|
+
}),
|
|
2416
|
+
z3.object({ kind: z3.literal("assistant_text"), text: z3.string().max(16384) }),
|
|
2417
|
+
z3.object({
|
|
2418
|
+
kind: z3.literal("tool_use"),
|
|
2419
|
+
name: z3.string().max(200),
|
|
2420
|
+
// Compact preview: JSON.stringify(input) truncated agent-side. The cap
|
|
2421
|
+
// matches the text events because AskUserQuestion payloads ride this field
|
|
2422
|
+
// and the web lifts them into an interactive card — a tight cap forced
|
|
2423
|
+
// option descriptions down to 80 chars, making them unreadable. Every
|
|
2424
|
+
// other tool keeps a far smaller agent-side budget (`TOOL_INPUT_MAX` in
|
|
2425
|
+
// `chat-record-mapper.ts`), so the ring does not grow for normal calls.
|
|
2426
|
+
input: z3.string().max(16384),
|
|
2427
|
+
// Transcript tool_use block id — lets the client pair the tool_result.
|
|
2428
|
+
id: z3.string().max(100).optional()
|
|
2429
|
+
}),
|
|
2430
|
+
z3.object({
|
|
2431
|
+
kind: z3.literal("tool_result"),
|
|
2432
|
+
// tool_use block id this result answers (absent on malformed records).
|
|
2433
|
+
toolUseId: z3.string().max(100).optional(),
|
|
2434
|
+
// Compact output preview, truncated agent-side.
|
|
2435
|
+
output: z3.string().max(2e3),
|
|
2436
|
+
isError: z3.boolean().optional()
|
|
2437
|
+
}),
|
|
2438
|
+
z3.object({ kind: z3.literal("turn_end") })
|
|
2439
|
+
]);
|
|
2440
|
+
var PtyChatEventRequestSchema = z3.object({
|
|
2441
|
+
sessionId: z3.string(),
|
|
2442
|
+
event: PtyChatEventPayloadSchema
|
|
2443
|
+
});
|
|
2444
|
+
var PtyChatAttachRequestSchema = z3.object({
|
|
2445
|
+
sessionId: z3.string()
|
|
2446
|
+
});
|
|
2447
|
+
var CreatePRResponseSchema = z3.object({
|
|
2448
|
+
prNumber: z3.number().int().positive(),
|
|
2449
|
+
prUrl: z3.string().url(),
|
|
2450
|
+
/** Advisory glossary-upkeep note derived from the PR's changed files matched
|
|
2451
|
+
* against tag contextPaths — rendered into the tool result, never stored. */
|
|
2452
|
+
glossaryNote: z3.string().optional()
|
|
2453
|
+
});
|
|
2454
|
+
var PostToChatResponseSchema = z3.object({
|
|
2455
|
+
messageId: z3.string()
|
|
2456
|
+
});
|
|
2457
|
+
var UpdateTaskStatusResponseSchema = z3.object({
|
|
2458
|
+
taskId: z3.string(),
|
|
2459
|
+
status: z3.string()
|
|
2460
|
+
});
|
|
2461
|
+
var StoreSessionIdResponseSchema = z3.object({
|
|
2462
|
+
success: z3.boolean()
|
|
2463
|
+
});
|
|
2464
|
+
var HeartbeatResponseSchema = z3.object({
|
|
2465
|
+
acknowledged: z3.boolean()
|
|
2466
|
+
});
|
|
2467
|
+
var SessionStartResponseSchema = z3.object({
|
|
2468
|
+
sessionId: z3.string(),
|
|
2469
|
+
startedAt: z3.string()
|
|
2470
|
+
});
|
|
2471
|
+
var SessionStopResponseSchema = z3.object({
|
|
2472
|
+
sessionId: z3.string(),
|
|
2473
|
+
stoppedAt: z3.string()
|
|
2474
|
+
});
|
|
2475
|
+
var DeleteSubtaskResponseSchema = z3.object({
|
|
2476
|
+
deleted: z3.boolean()
|
|
2477
|
+
});
|
|
2478
|
+
var ParkOnCheckResultRequestSchema = z4.object({
|
|
2479
|
+
sessionId: z4.string(),
|
|
2480
|
+
sha: z4.string().regex(/^[0-9a-f]{7,40}$/i).optional(),
|
|
2481
|
+
prNumber: z4.number().int().positive().optional(),
|
|
2482
|
+
timeoutMinutes: z4.number().int().min(1).max(MAX_CI_WAIT_TIMEOUT_MINUTES).optional()
|
|
2483
|
+
});
|
|
2484
|
+
var GIT_BRANCH_NAME_MAX = 255;
|
|
2485
|
+
var GIT_BRANCH_NAME_MESSAGE = "Invalid git branch name \u2014 use only letters, numbers, '.', '_', '/' and '-', starting with a letter or number, with no '..', '@{' or '//', and no trailing '/', '-', '.' or '.lock'";
|
|
2486
|
+
var ALLOWED_REF = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
2487
|
+
function isValidGitBranchName(name) {
|
|
2488
|
+
if (typeof name !== "string") return false;
|
|
2489
|
+
if (name.length === 0 || name.length > GIT_BRANCH_NAME_MAX) return false;
|
|
2490
|
+
if (!ALLOWED_REF.test(name)) return false;
|
|
2491
|
+
if (name.includes("..") || name.includes("@{") || name.includes("//")) return false;
|
|
2492
|
+
if (name.endsWith("/") || name.endsWith("-")) return false;
|
|
2493
|
+
if (name.endsWith(".") || name.endsWith(".lock")) return false;
|
|
2494
|
+
return true;
|
|
2495
|
+
}
|
|
2496
|
+
var cardDescription2 = z5.string().max(CARD_DESCRIPTION_MAX, CARD_DESCRIPTION_LIMIT_MESSAGE).optional();
|
|
2497
|
+
var ListAccessibleProjectsRequestSchema = z5.object({
|
|
2498
|
+
pageSize: z5.number().int().positive().max(100).optional().default(100)
|
|
2499
|
+
});
|
|
2500
|
+
var ListProjectTasksRequestSchema = z5.object({
|
|
2501
|
+
projectId: z5.string(),
|
|
2502
|
+
status: z5.string().optional(),
|
|
2503
|
+
// Card types to include. Omitted/empty → defaults to ["task"] in the handler
|
|
2504
|
+
// (mirrors searchProjectTasks) so listing doesn't surface incidents/suggestions
|
|
2505
|
+
// unless asked. Enum validation lives at the MCP tool layer.
|
|
2506
|
+
typeFilters: z5.array(z5.string()).optional(),
|
|
2507
|
+
assigneeId: z5.string().optional(),
|
|
2508
|
+
unassigned: z5.boolean().optional(),
|
|
2509
|
+
// Scope to a sub-project board when provided. Unlike the board layer's `?? null`
|
|
2510
|
+
// semantics, agents default to seeing the whole project when omitted.
|
|
2511
|
+
subProjectId: z5.string().nullable().optional(),
|
|
2512
|
+
limit: z5.number().int().positive().optional().default(50)
|
|
2513
|
+
}).refine((p) => !(p.unassigned && p.assigneeId), {
|
|
2514
|
+
message: "Pass either assigneeId or unassigned, not both"
|
|
2515
|
+
});
|
|
2516
|
+
var GetProjectTaskRequestSchema = z5.object({
|
|
2517
|
+
projectId: z5.string(),
|
|
2518
|
+
taskId: z5.string()
|
|
2519
|
+
});
|
|
2520
|
+
var SearchProjectTasksRequestSchema = z5.object({
|
|
2521
|
+
projectId: z5.string(),
|
|
2522
|
+
// Tag names, matched case-insensitively against the project glossary.
|
|
2523
|
+
tagNames: z5.array(z5.string()).optional(),
|
|
2524
|
+
// How to combine tagNames: "any" (default) = carries at least one,
|
|
2525
|
+
// "all" = carries every one.
|
|
2526
|
+
tagMatch: z5.enum(["any", "all"]).optional(),
|
|
2527
|
+
// Expand each named tag to its descendants in the tag DAG before matching,
|
|
2528
|
+
// so a parent tag sweeps its whole area. Default false.
|
|
2529
|
+
includeChildTags: z5.boolean().optional(),
|
|
2530
|
+
searchQuery: z5.string().optional(),
|
|
2531
|
+
statusFilters: z5.array(z5.string()).optional(),
|
|
2532
|
+
// Card types to include. Omitted/empty → defaults to ["task"] in the handler so
|
|
2533
|
+
// search doesn't surface incidents/suggestions unless asked. Enum validation lives
|
|
2534
|
+
// at the MCP tool layer (mirrors statusFilters).
|
|
2535
|
+
typeFilters: z5.array(z5.string()).optional(),
|
|
2536
|
+
assigneeId: z5.string().optional(),
|
|
2537
|
+
unassigned: z5.boolean().optional(),
|
|
2538
|
+
// Scope to a sub-project board when provided. Unlike the board layer's `?? null`
|
|
2539
|
+
// semantics, agents default to seeing the whole project when omitted.
|
|
2540
|
+
subProjectId: z5.string().nullable().optional(),
|
|
2541
|
+
limit: z5.number().int().positive().optional().default(20)
|
|
2542
|
+
}).refine((p) => !(p.unassigned && p.assigneeId), {
|
|
2543
|
+
message: "Pass either assigneeId or unassigned, not both"
|
|
2544
|
+
});
|
|
2545
|
+
var ListProjectTagsRequestSchema = z5.object({
|
|
2546
|
+
projectId: z5.string()
|
|
2547
|
+
});
|
|
2548
|
+
var GetProjectTagRequestSchema = z5.object({
|
|
2549
|
+
projectId: z5.string(),
|
|
2550
|
+
/** Tag id or exact (case-insensitive) tag name. */
|
|
2551
|
+
tag: z5.string().min(1).max(100)
|
|
2552
|
+
});
|
|
2553
|
+
var ListProjectTagAttachmentsRequestSchema = z5.object({
|
|
2554
|
+
projectId: z5.string(),
|
|
2555
|
+
/** Tag id or exact (case-insensitive) tag name. */
|
|
2556
|
+
tag: z5.string().min(1).max(100),
|
|
2557
|
+
limit: z5.number().int().min(1).max(60).optional(),
|
|
2558
|
+
offset: z5.number().int().min(0).optional()
|
|
2559
|
+
});
|
|
2560
|
+
var SetProjectFileTagsRequestSchema = z5.object({
|
|
2561
|
+
projectId: z5.string(),
|
|
2562
|
+
taskId: z5.string(),
|
|
2563
|
+
fileId: z5.string(),
|
|
2564
|
+
tags: z5.array(z5.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS),
|
|
2565
|
+
requestingUserId: z5.string().optional()
|
|
2566
|
+
});
|
|
2567
|
+
var GetProjectSummaryRequestSchema = z5.object({
|
|
2568
|
+
projectId: z5.string()
|
|
2569
|
+
});
|
|
2570
|
+
var GetProjectOnboardingStatusRequestSchema = z5.object({
|
|
2571
|
+
projectId: z5.string()
|
|
2572
|
+
});
|
|
2573
|
+
var GetProjectOnboardingStepRequestSchema = z5.object({
|
|
2574
|
+
projectId: z5.string()
|
|
2575
|
+
});
|
|
2576
|
+
var GetProjectConnectUrlsRequestSchema = z5.object({
|
|
2577
|
+
projectId: z5.string()
|
|
2578
|
+
});
|
|
2579
|
+
var conveyorCapabilitySchema = z5.enum([
|
|
2580
|
+
"read",
|
|
2581
|
+
"create",
|
|
2582
|
+
"update",
|
|
2583
|
+
"chat",
|
|
2584
|
+
"files",
|
|
2585
|
+
"build"
|
|
2586
|
+
]);
|
|
2587
|
+
var GetConnectionContextRequestSchema = z5.object({
|
|
2588
|
+
projectId: z5.string(),
|
|
2589
|
+
// Optional board scope (CONVEYOR_SUBPROJECT_ID). Validated to belong to the
|
|
2590
|
+
// project in the handler; an invalid/foreign id is reported, not silently
|
|
2591
|
+
// dropped, so a mis-scoped connection is never presented as board-specific.
|
|
2592
|
+
subProjectId: z5.string().nullable().optional()
|
|
2593
|
+
});
|
|
2594
|
+
var VerifyConnectionRequestSchema = z5.object({
|
|
2595
|
+
projectId: z5.string(),
|
|
2596
|
+
subProjectId: z5.string().nullable().optional(),
|
|
2597
|
+
intendedActions: z5.array(conveyorCapabilitySchema).optional()
|
|
2598
|
+
});
|
|
2599
|
+
var ListAccessibleSubprojectsRequestSchema = z5.object({
|
|
2600
|
+
projectId: z5.string()
|
|
2601
|
+
});
|
|
2602
|
+
var CreateProjectTaskRequestSchema = z5.object({
|
|
2603
|
+
projectId: z5.string(),
|
|
2604
|
+
title: z5.string().min(1),
|
|
2605
|
+
description: cardDescription2,
|
|
2606
|
+
plan: z5.string().optional(),
|
|
2607
|
+
status: z5.string().optional(),
|
|
2608
|
+
// Assign to a sub-project board. Validated to belong to `projectId` in the handler.
|
|
2609
|
+
subProjectId: z5.string().nullable().optional(),
|
|
2610
|
+
requestingUserId: z5.string().optional()
|
|
2611
|
+
});
|
|
2612
|
+
var SetProjectTaskParentRequestSchema = z5.object({
|
|
2613
|
+
projectId: z5.string(),
|
|
2614
|
+
/** Card to move — id or slug. */
|
|
2615
|
+
taskId: z5.string().min(1),
|
|
2616
|
+
/** New parent (id or slug), or null to detach. */
|
|
2617
|
+
parentTaskId: z5.string().min(1).nullable(),
|
|
2618
|
+
ordinal: z5.number().int().nonnegative().optional(),
|
|
2619
|
+
followParentStatus: z5.boolean().optional(),
|
|
2620
|
+
requestingUserId: z5.string().optional()
|
|
2621
|
+
}).strict();
|
|
2622
|
+
var UpdateProjectTaskRequestSchema = z5.object({
|
|
2623
|
+
projectId: z5.string(),
|
|
2624
|
+
taskId: z5.string(),
|
|
2625
|
+
title: z5.string().optional(),
|
|
2626
|
+
description: cardDescription2,
|
|
2627
|
+
plan: z5.string().optional(),
|
|
2628
|
+
// Enum validation lives at the MCP tool layer (mirrors createProjectTask);
|
|
2629
|
+
// the handler routes through the shared updateStatus core (InProgress
|
|
2630
|
+
// dependency check + cleanup/board/Slack side effects), not the stricter
|
|
2631
|
+
// card-type-validating path the Socket.IO updateTaskStatus mutation uses.
|
|
2632
|
+
status: z5.string().optional(),
|
|
2633
|
+
// Canonical risk level, or null to clear. Resolved to the project's
|
|
2634
|
+
// configured Risk row (by rank) in the handler.
|
|
2635
|
+
risk: riskLevelSchema.nullable().optional(),
|
|
2636
|
+
// Story-point value, or null to clear. Resolved to the project's configured
|
|
2637
|
+
// StoryPoint row in the handler, which rejects an unconfigured value.
|
|
2638
|
+
storyPointValue: z5.number().int().positive().nullable().optional(),
|
|
2639
|
+
assignedUserId: z5.string().nullish(),
|
|
2640
|
+
// Move to a different sub-project board, or null to move to the parent board.
|
|
2641
|
+
// Validated to belong to `projectId` in the handler.
|
|
2642
|
+
subProjectId: z5.string().nullable().optional(),
|
|
2643
|
+
// Record the task's ACTUAL working branch (e.g. a locally-driven pack's
|
|
2644
|
+
// branch, so identification never mints a competing name and pack-child
|
|
2645
|
+
// merge handling matches reality), or null to detach. Guarded in the
|
|
2646
|
+
// handler: the ref must exist on origin and no live workspace may be bound
|
|
2647
|
+
// to a different branch.
|
|
2648
|
+
githubBranch: z5.string().min(1).max(GIT_BRANCH_NAME_MAX).refine(isValidGitBranchName, { message: GIT_BRANCH_NAME_MESSAGE }).nullable().optional(),
|
|
2649
|
+
requestingUserId: z5.string().optional()
|
|
2650
|
+
}).strict().refine(
|
|
2651
|
+
(v) => v.title !== void 0 || v.description !== void 0 || v.plan !== void 0 || v.status !== void 0 || v.risk !== void 0 || v.storyPointValue !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0 || v.githubBranch !== void 0,
|
|
2652
|
+
{
|
|
2653
|
+
message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, assignedUserId, subProjectId, or githubBranch)"
|
|
2654
|
+
}
|
|
2655
|
+
);
|
|
2656
|
+
var TransitionProjectTaskStatusRequestSchema = z5.object({
|
|
2657
|
+
projectId: z5.string(),
|
|
2658
|
+
taskId: z5.string(),
|
|
2659
|
+
toStatus: z5.string(),
|
|
2660
|
+
expectedFromStatus: z5.string().optional(),
|
|
2661
|
+
// Optional raise-only risk to attempt alongside the transition
|
|
2662
|
+
// (approve → low, request_changes → medium by default).
|
|
2663
|
+
risk: riskLevelSchema.optional(),
|
|
2664
|
+
requestingUserId: z5.string().optional()
|
|
2665
|
+
});
|
|
2666
|
+
var MoveProjectCardRequestSchema = z5.object({
|
|
2667
|
+
projectId: z5.string(),
|
|
2668
|
+
taskId: z5.string(),
|
|
2669
|
+
destinationProjectId: z5.string(),
|
|
2670
|
+
requestingUserId: z5.string().optional()
|
|
2671
|
+
});
|
|
2672
|
+
var PostToProjectTaskChatRequestSchema = z5.object({
|
|
2673
|
+
projectId: z5.string(),
|
|
2674
|
+
taskId: z5.string(),
|
|
2675
|
+
content: z5.string(),
|
|
2676
|
+
requestingUserId: z5.string().optional()
|
|
2677
|
+
});
|
|
2678
|
+
var GetProjectTaskCliRequestSchema = z5.object({
|
|
2679
|
+
projectId: z5.string(),
|
|
2680
|
+
taskId: z5.string(),
|
|
2681
|
+
limit: z5.number().int().positive().optional().default(50),
|
|
2682
|
+
source: z5.string().optional()
|
|
2683
|
+
});
|
|
2684
|
+
var GetProjectTaskSessionsRequestSchema = z5.object({
|
|
2685
|
+
projectId: z5.string(),
|
|
2686
|
+
taskId: z5.string()
|
|
2687
|
+
});
|
|
2688
|
+
var QueryProjectGcpLogsRequestSchema = z5.object({
|
|
2689
|
+
projectId: z5.string(),
|
|
2690
|
+
env: z5.enum(["prod", "dev", "claudespace"]).optional(),
|
|
2691
|
+
severity: z5.enum(["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR", "CRITICAL", "ALERT", "EMERGENCY"]).optional(),
|
|
2692
|
+
services: z5.array(z5.string().min(1).max(200)).max(25).optional(),
|
|
2693
|
+
sqlInstances: z5.array(z5.string().min(1).max(200)).max(25).optional(),
|
|
2694
|
+
allServices: z5.boolean().optional(),
|
|
2695
|
+
search: z5.string().max(256).optional(),
|
|
2696
|
+
filter: z5.string().max(1e3).optional(),
|
|
2697
|
+
startTime: z5.string().optional(),
|
|
2698
|
+
endTime: z5.string().optional(),
|
|
2699
|
+
limit: z5.number().int().min(1).max(200).optional().default(50),
|
|
2700
|
+
pageToken: z5.string().max(4096).optional()
|
|
2701
|
+
});
|
|
2702
|
+
var QueryProjectGrafanaLogsRequestSchema = z5.object({
|
|
2703
|
+
projectId: z5.string(),
|
|
2704
|
+
env: z5.enum(["prod", "dev"]).optional(),
|
|
2705
|
+
services: z5.array(z5.string().min(1).max(200)).max(25).optional(),
|
|
2706
|
+
level: z5.enum(["debug", "info", "warn", "error", "fatal"]).optional(),
|
|
2707
|
+
search: z5.string().max(256).optional(),
|
|
2708
|
+
logql: z5.string().max(2e3).optional(),
|
|
2709
|
+
startTime: z5.string().optional(),
|
|
2710
|
+
endTime: z5.string().optional(),
|
|
2711
|
+
limit: z5.number().int().min(1).max(200).optional().default(50)
|
|
2712
|
+
});
|
|
2713
|
+
var driveFileNameSchema = z5.string().min(1).max(255).regex(/^[^/\\\r\n]+$/, "File names cannot contain slashes or line breaks");
|
|
2714
|
+
var DRIVE_MAX_CONTENT_CHARS = 1e6;
|
|
2715
|
+
var ListProjectDriveFilesRequestSchema = z5.object({
|
|
2716
|
+
projectId: z5.string(),
|
|
2717
|
+
folderId: z5.string().max(200).optional(),
|
|
2718
|
+
search: z5.string().max(200).optional(),
|
|
2719
|
+
limit: z5.number().int().min(1).max(200).optional()
|
|
2720
|
+
});
|
|
2721
|
+
var ReadProjectDriveFileRequestSchema = z5.object({
|
|
2722
|
+
projectId: z5.string(),
|
|
2723
|
+
fileId: z5.string().min(1).max(200)
|
|
2724
|
+
});
|
|
2725
|
+
var CreateProjectDriveFileRequestSchema = z5.object({
|
|
2726
|
+
projectId: z5.string(),
|
|
2727
|
+
name: driveFileNameSchema,
|
|
2728
|
+
content: z5.string().max(DRIVE_MAX_CONTENT_CHARS),
|
|
2729
|
+
mimeType: z5.string().max(200).optional(),
|
|
2730
|
+
folderId: z5.string().max(200).optional()
|
|
2731
|
+
});
|
|
2732
|
+
var UpdateProjectDriveFileRequestSchema = z5.object({
|
|
2733
|
+
projectId: z5.string(),
|
|
2734
|
+
fileId: z5.string().min(1).max(200),
|
|
2735
|
+
content: z5.string().max(DRIVE_MAX_CONTENT_CHARS),
|
|
2736
|
+
mimeType: z5.string().max(200).optional()
|
|
2737
|
+
});
|
|
2738
|
+
var DeleteProjectDriveFileRequestSchema = z5.object({
|
|
2739
|
+
projectId: z5.string(),
|
|
2740
|
+
fileId: z5.string().min(1).max(200)
|
|
2741
|
+
});
|
|
2742
|
+
var CreateProjectDriveFolderRequestSchema = z5.object({
|
|
2743
|
+
projectId: z5.string(),
|
|
2744
|
+
name: driveFileNameSchema,
|
|
2745
|
+
folderId: z5.string().max(200).optional()
|
|
2746
|
+
});
|
|
2747
|
+
var StartProjectBuildRequestSchema = z5.object({
|
|
2748
|
+
projectId: z5.string(),
|
|
2749
|
+
taskId: z5.string(),
|
|
2750
|
+
requestingUserId: z5.string().optional()
|
|
2751
|
+
});
|
|
2752
|
+
var StopProjectBuildRequestSchema = z5.object({
|
|
2753
|
+
projectId: z5.string(),
|
|
2754
|
+
taskId: z5.string(),
|
|
2755
|
+
requestingUserId: z5.string().optional()
|
|
2756
|
+
});
|
|
2757
|
+
var StartProjectWorkspaceRequestSchema = z5.object({
|
|
2758
|
+
projectId: z5.string(),
|
|
2759
|
+
requestingUserId: z5.string().optional()
|
|
2760
|
+
});
|
|
2761
|
+
var StopProjectWorkspaceRequestSchema = z5.object({
|
|
2762
|
+
projectId: z5.string(),
|
|
2763
|
+
destroy: z5.boolean().optional(),
|
|
2764
|
+
requestingUserId: z5.string().optional()
|
|
2765
|
+
});
|
|
2766
|
+
var ListMyLiveSessionsRequestSchema = z5.object({
|
|
2767
|
+
projectId: z5.string(),
|
|
2768
|
+
/** Admin-only: list another member's sessions instead of the caller's. */
|
|
2769
|
+
targetUserId: z5.string().optional()
|
|
2770
|
+
});
|
|
2771
|
+
var ListProjectSessionGroupsRequestSchema = z5.object({
|
|
2772
|
+
projectId: z5.string()
|
|
2773
|
+
});
|
|
2774
|
+
var ListMyLiveSessionsAcrossProjectsRequestSchema = z5.object({});
|
|
2775
|
+
var ListSessionGroupsAcrossProjectsRequestSchema = z5.object({});
|
|
2776
|
+
var GetProjectAvailableTuisRequestSchema = z5.object({
|
|
2777
|
+
projectId: z5.string()
|
|
2778
|
+
});
|
|
2779
|
+
var StartAdhocSessionRequestSchema = z5.object({
|
|
2780
|
+
projectId: z5.string(),
|
|
2781
|
+
label: z5.string().max(200).optional(),
|
|
2782
|
+
/** Coding-agent key to launch under — validated pick-time (ownership + TUI availability) in the handler. */
|
|
2783
|
+
codingAgentKeyId: z5.string().optional(),
|
|
2784
|
+
/** Model override (Claude model id) — overrides the launch key's own model. */
|
|
2785
|
+
model: z5.string().max(200).optional(),
|
|
2786
|
+
/**
|
|
2787
|
+
* Session role. Constrained: other task-less modes fall through to the pm
|
|
2788
|
+
* runner in the pod entrypoint, and "review" would crash without a task.
|
|
2789
|
+
*/
|
|
2790
|
+
mode: z5.enum(["adhoc", "pm"]).optional(),
|
|
2791
|
+
/** Base branch to check out (defaults to the project's dev branch). */
|
|
2792
|
+
branch: z5.string().max(300).optional(),
|
|
2793
|
+
/**
|
|
2794
|
+
* Server-assembled instructions the pod's TUI auto-submits once on first boot
|
|
2795
|
+
* (headless kickoff). Used by the onboarding "Set it up for me" flow to seed a
|
|
2796
|
+
* setup-driver prompt; the session stays watchable/interactive in the Sessions
|
|
2797
|
+
* view. `ensureAdhocWorkspace` persists it and clears it after first submit.
|
|
2798
|
+
*/
|
|
2799
|
+
initialPrompt: z5.string().max(2e4).optional(),
|
|
2800
|
+
requestingUserId: z5.string().optional()
|
|
2801
|
+
});
|
|
2802
|
+
var StopAdhocSessionRequestSchema = z5.object({
|
|
2803
|
+
projectId: z5.string(),
|
|
2804
|
+
workspaceId: z5.string(),
|
|
2805
|
+
destroy: z5.boolean().optional(),
|
|
2806
|
+
requestingUserId: z5.string().optional()
|
|
2807
|
+
});
|
|
2808
|
+
var ResumeAdhocSessionRequestSchema = z5.object({
|
|
2809
|
+
projectId: z5.string(),
|
|
2810
|
+
workspaceId: z5.string(),
|
|
2811
|
+
requestingUserId: z5.string().optional()
|
|
2812
|
+
});
|
|
2813
|
+
var RefreshCodingAgentKeyUsageRequestSchema = z5.object({
|
|
2814
|
+
projectId: z5.string(),
|
|
2815
|
+
keyId: z5.string().optional(),
|
|
2816
|
+
requestingUserId: z5.string().optional()
|
|
2817
|
+
});
|
|
2818
|
+
var ListKeysToProbeRequestSchema = z5.object({
|
|
2819
|
+
sessionId: z5.string()
|
|
2820
|
+
});
|
|
2821
|
+
var CreateProjectReleaseRequestSchema = z5.object({
|
|
2822
|
+
projectId: z5.string(),
|
|
2823
|
+
taskIds: z5.array(z5.string()).optional(),
|
|
2824
|
+
requestingUserId: z5.string().optional()
|
|
2825
|
+
});
|
|
2826
|
+
var AddTasksToProjectReleaseRequestSchema = z5.object({
|
|
2827
|
+
projectId: z5.string(),
|
|
2828
|
+
taskIds: z5.array(z5.string()).min(1),
|
|
2829
|
+
requestingUserId: z5.string().optional()
|
|
2830
|
+
});
|
|
2831
|
+
var ApproveProjectMergePRRequestSchema = z5.object({
|
|
2832
|
+
projectId: z5.string(),
|
|
2833
|
+
childTaskId: z5.string(),
|
|
2834
|
+
requestingUserId: z5.string().optional()
|
|
2835
|
+
});
|
|
2836
|
+
var ListProjectSubtasksRequestSchema = z5.object({
|
|
2837
|
+
projectId: z5.string(),
|
|
2838
|
+
taskId: z5.string()
|
|
2839
|
+
});
|
|
2840
|
+
var CreateProjectSubtaskRequestSchema = z5.object({
|
|
2841
|
+
projectId: z5.string(),
|
|
2842
|
+
parentTaskId: z5.string(),
|
|
2843
|
+
title: z5.string().min(1),
|
|
2844
|
+
description: cardDescription2,
|
|
2845
|
+
plan: z5.string().optional(),
|
|
2846
|
+
ordinal: z5.number().int().nonnegative().optional(),
|
|
2847
|
+
storyPointValue: z5.number().int().positive().optional(),
|
|
2848
|
+
followParentStatus: z5.boolean().optional(),
|
|
2849
|
+
/** Sibling subtask ids or slugs this subtask blocks on (explicit dependency
|
|
2850
|
+
* metadata — preferred over encoding order in plan text / ordinal). */
|
|
2851
|
+
dependsOn: z5.array(z5.string().min(1)).max(32).optional(),
|
|
2852
|
+
requestingUserId: z5.string().optional()
|
|
2853
|
+
});
|
|
2854
|
+
var UpdateProjectSubtaskRequestSchema = z5.object({
|
|
2855
|
+
projectId: z5.string(),
|
|
2856
|
+
subtaskId: z5.string(),
|
|
2857
|
+
title: z5.string().optional(),
|
|
2858
|
+
description: cardDescription2,
|
|
2859
|
+
plan: z5.string().optional(),
|
|
2860
|
+
status: z5.string().optional(),
|
|
2861
|
+
ordinal: z5.number().int().nonnegative().optional(),
|
|
2862
|
+
storyPointValue: z5.number().int().positive().optional(),
|
|
2863
|
+
followParentStatus: z5.boolean().optional(),
|
|
2864
|
+
/** Replace-set of sibling subtask ids/slugs this subtask blocks on ([] clears).
|
|
2865
|
+
* Mirrors the in-pod updateSubtask semantics. */
|
|
2866
|
+
dependsOn: z5.array(z5.string().min(1)).max(32).optional(),
|
|
2867
|
+
requestingUserId: z5.string().optional()
|
|
2868
|
+
});
|
|
2869
|
+
var DeleteProjectSubtaskRequestSchema = z5.object({
|
|
2870
|
+
projectId: z5.string(),
|
|
2871
|
+
subtaskId: z5.string(),
|
|
2872
|
+
requestingUserId: z5.string().optional()
|
|
2873
|
+
});
|
|
2874
|
+
var GetProjectTaskChatRequestSchema = z5.object({
|
|
2875
|
+
projectId: z5.string(),
|
|
2876
|
+
taskId: z5.string(),
|
|
2877
|
+
limit: z5.number().int().positive().optional().default(20)
|
|
2878
|
+
});
|
|
2879
|
+
var AddProjectTaskDependencyRequestSchema = z5.object({
|
|
2880
|
+
projectId: z5.string(),
|
|
2881
|
+
taskId: z5.string(),
|
|
2882
|
+
dependsOnSlugOrId: z5.string(),
|
|
2883
|
+
requestingUserId: z5.string().optional()
|
|
2884
|
+
});
|
|
2885
|
+
var RemoveProjectTaskDependencyRequestSchema = z5.object({
|
|
2886
|
+
projectId: z5.string(),
|
|
2887
|
+
taskId: z5.string(),
|
|
2888
|
+
dependsOnSlugOrId: z5.string(),
|
|
2889
|
+
requestingUserId: z5.string().optional()
|
|
2890
|
+
});
|
|
2891
|
+
var VoteProjectSuggestionRequestSchema = z5.object({
|
|
2892
|
+
projectId: z5.string(),
|
|
2893
|
+
suggestionId: z5.string(),
|
|
2894
|
+
value: z5.union([z5.literal(1), z5.literal(-1)]),
|
|
2895
|
+
requestingUserId: z5.string().optional()
|
|
2896
|
+
});
|
|
2897
|
+
var GetProjectTaskDependenciesRequestSchema = z5.object({
|
|
2898
|
+
projectId: z5.string(),
|
|
2899
|
+
taskId: z5.string()
|
|
2900
|
+
});
|
|
2901
|
+
var ListProjectTaskFilesRequestSchema = z5.object({
|
|
2902
|
+
projectId: z5.string(),
|
|
2903
|
+
taskId: z5.string()
|
|
2904
|
+
});
|
|
2905
|
+
var GetProjectAttachmentRequestSchema = z5.object({
|
|
2906
|
+
projectId: z5.string(),
|
|
2907
|
+
taskId: z5.string(),
|
|
2908
|
+
fileId: z5.string(),
|
|
2909
|
+
/** Byte offset into text content (paging large logs/JSON). Default 0. */
|
|
2910
|
+
offset: z5.number().int().nonnegative().optional(),
|
|
2911
|
+
/** Max bytes of text content to return from `offset`. Server default applies. */
|
|
2912
|
+
maxBytes: z5.number().int().positive().optional()
|
|
2913
|
+
});
|
|
2914
|
+
var RequestProjectFileUploadRequestSchema = z5.object({
|
|
2915
|
+
projectId: z5.string(),
|
|
2916
|
+
taskId: z5.string(),
|
|
2917
|
+
fileName: z5.string().min(1).max(255),
|
|
2918
|
+
mimeType: z5.string().min(1).max(128),
|
|
2919
|
+
fileSize: z5.number().int().positive().max(MAX_FILE_SIZE_BYTES),
|
|
2920
|
+
requestingUserId: z5.string().optional()
|
|
2921
|
+
});
|
|
2922
|
+
var ConfirmProjectFileUploadRequestSchema = z5.object({
|
|
2923
|
+
projectId: z5.string(),
|
|
2924
|
+
taskId: z5.string(),
|
|
2925
|
+
fileId: z5.string(),
|
|
2926
|
+
/** When set, the attachment is also posted to the task chat with this text. */
|
|
2927
|
+
comment: z5.string().max(2e3).optional(),
|
|
2928
|
+
/** Glossary tag names (or ids) this file is an example of. */
|
|
2929
|
+
tags: z5.array(z5.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional(),
|
|
2930
|
+
requestingUserId: z5.string().optional()
|
|
2931
|
+
});
|
|
2932
|
+
var CreateProjectPullRequestRequestSchema = z5.object({
|
|
2933
|
+
projectId: z5.string(),
|
|
2934
|
+
taskId: z5.string(),
|
|
2935
|
+
title: z5.string().min(1),
|
|
2936
|
+
body: z5.string(),
|
|
2937
|
+
head: z5.string().optional(),
|
|
2938
|
+
base: z5.string().optional(),
|
|
2939
|
+
requestingUserId: z5.string().optional()
|
|
2940
|
+
});
|
|
2941
|
+
var ListProjectMembersRequestSchema = z5.object({
|
|
2942
|
+
projectId: z5.string()
|
|
2943
|
+
});
|
|
2944
|
+
var AddProjectTaskReviewerRequestSchema = z5.object({
|
|
2945
|
+
projectId: z5.string(),
|
|
2946
|
+
taskId: z5.string(),
|
|
2947
|
+
userId: z5.string(),
|
|
2948
|
+
requestingUserId: z5.string().optional()
|
|
2949
|
+
});
|
|
2950
|
+
var RemoveProjectTaskReviewerRequestSchema = z5.object({
|
|
2951
|
+
projectId: z5.string(),
|
|
2952
|
+
taskId: z5.string(),
|
|
2953
|
+
userId: z5.string(),
|
|
2954
|
+
requestingUserId: z5.string().optional()
|
|
2955
|
+
});
|
|
2956
|
+
var ListProjectManualTestsRequestSchema = z5.object({
|
|
2957
|
+
projectId: z5.string(),
|
|
2958
|
+
taskId: z5.string()
|
|
2959
|
+
});
|
|
2960
|
+
var QueryProjectManualTestsRequestSchema = z5.object({
|
|
2961
|
+
projectId: z5.string(),
|
|
2962
|
+
cardStatuses: z5.array(z5.string()).optional(),
|
|
2963
|
+
testStatuses: z5.array(z5.enum(["open", "approved", "rejected"])).optional()
|
|
2964
|
+
});
|
|
2965
|
+
var SetProjectManualTestsRequestSchema = z5.object({
|
|
2966
|
+
projectId: z5.string(),
|
|
2967
|
+
taskId: z5.string(),
|
|
2968
|
+
items: z5.array(z5.object({ title: z5.string().min(1) })).min(1),
|
|
2969
|
+
requestingUserId: z5.string().optional()
|
|
2970
|
+
});
|
|
2971
|
+
var EditProjectManualTestRequestSchema = z5.object({
|
|
2972
|
+
projectId: z5.string(),
|
|
2973
|
+
taskId: z5.string(),
|
|
2974
|
+
title: z5.string().min(1),
|
|
2975
|
+
newTitle: z5.string().min(1),
|
|
2976
|
+
requestingUserId: z5.string().optional()
|
|
2977
|
+
});
|
|
2978
|
+
var RemoveProjectManualTestRequestSchema = z5.object({
|
|
2979
|
+
projectId: z5.string(),
|
|
2980
|
+
taskId: z5.string(),
|
|
2981
|
+
title: z5.string().min(1),
|
|
2982
|
+
requestingUserId: z5.string().optional()
|
|
2983
|
+
});
|
|
2984
|
+
var ApproveProjectManualTestRequestSchema = z5.object({
|
|
2985
|
+
projectId: z5.string(),
|
|
2986
|
+
taskId: z5.string(),
|
|
2987
|
+
title: z5.string().min(1),
|
|
2988
|
+
requestingUserId: z5.string().optional()
|
|
2989
|
+
});
|
|
2990
|
+
var RejectProjectManualTestRequestSchema = z5.object({
|
|
2991
|
+
projectId: z5.string(),
|
|
2992
|
+
taskId: z5.string(),
|
|
2993
|
+
title: z5.string().min(1),
|
|
2994
|
+
reason: z5.string().min(1).max(2e3),
|
|
2995
|
+
requestingUserId: z5.string().optional()
|
|
2996
|
+
});
|
|
2997
|
+
var CreateProjectSuggestionRequestSchema = z5.object({
|
|
2998
|
+
projectId: z5.string(),
|
|
2999
|
+
title: z5.string().min(1),
|
|
3000
|
+
description: cardDescription2,
|
|
3001
|
+
tagNames: z5.array(z5.string()).optional(),
|
|
3002
|
+
requestingUserId: z5.string().optional()
|
|
3003
|
+
});
|
|
3004
|
+
var ListProjectChannelsRequestSchema = z6.object({
|
|
3005
|
+
projectId: z6.string()
|
|
3006
|
+
});
|
|
3007
|
+
var READ_CHANNEL_MESSAGES_MAX_LIMIT = 50;
|
|
3008
|
+
var ReadChannelMessagesRequestSchema = z6.object({
|
|
3009
|
+
projectId: z6.string(),
|
|
3010
|
+
channelId: z6.string().min(1).max(200),
|
|
3011
|
+
limit: z6.number().int().min(1).max(READ_CHANNEL_MESSAGES_MAX_LIMIT).optional(),
|
|
3012
|
+
/** Provider-native cursor: return messages OLDER than this one. */
|
|
3013
|
+
before: z6.string().max(100).optional(),
|
|
3014
|
+
/** Provider-native cursor: return messages NEWER than this one. */
|
|
3015
|
+
after: z6.string().max(100).optional(),
|
|
3016
|
+
/**
|
|
3017
|
+
* Read one thread instead of the channel surface. Slack calls this a
|
|
3018
|
+
* `thread_ts`; on Discord it is the thread channel's id. One field for both,
|
|
3019
|
+
* because a caller holding a `threadTs` from a previous read should not have
|
|
3020
|
+
* to know which provider produced it.
|
|
3021
|
+
*/
|
|
3022
|
+
threadTs: z6.string().max(100).optional()
|
|
3023
|
+
});
|
|
3024
|
+
var POST_CHANNEL_MESSAGE_MAX_CHARS = 1800;
|
|
3025
|
+
var PostChannelMessageRequestSchema = z6.object({
|
|
3026
|
+
projectId: z6.string(),
|
|
3027
|
+
channelId: z6.string().min(1).max(200),
|
|
3028
|
+
text: z6.string().min(1).max(POST_CHANNEL_MESSAGE_MAX_CHARS),
|
|
3029
|
+
/** Reply inside a thread rather than to the channel. */
|
|
3030
|
+
threadTs: z6.string().max(100).optional()
|
|
3031
|
+
});
|
|
3032
|
+
var GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS = 90;
|
|
3033
|
+
var GetProjectAnalyticsSummaryRequestSchema = z6.object({
|
|
3034
|
+
projectId: z6.string(),
|
|
3035
|
+
rangeDays: z6.number().int().min(1).max(GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS).optional(),
|
|
3036
|
+
campaign: z6.string().max(200).optional()
|
|
3037
|
+
});
|
|
3038
|
+
var RequestWorkspaceRecycleRequestSchema = z7.object({
|
|
3039
|
+
sessionId: z7.string(),
|
|
3040
|
+
reason: z7.string().max(2e3)
|
|
3041
|
+
});
|
|
3042
|
+
var ReportApiOutageRequestSchema = z7.object({
|
|
3043
|
+
sessionId: z7.string(),
|
|
3044
|
+
detail: z7.string().max(2e3),
|
|
3045
|
+
attempts: z7.number().int().min(0).max(100)
|
|
3046
|
+
});
|
|
3047
|
+
var SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
3048
|
+
var ReviewGuideFileReferenceSchema = z8.object({
|
|
3049
|
+
path: z8.string().min(1).max(500),
|
|
3050
|
+
startLine: z8.number().int().positive().max(1e6).optional(),
|
|
3051
|
+
endLine: z8.number().int().positive().max(1e6).optional(),
|
|
3052
|
+
hunkHeader: z8.string().min(1).max(300).optional()
|
|
3053
|
+
}).strict().superRefine((value, ctx) => {
|
|
3054
|
+
if (value.endLine !== void 0 && value.startLine === void 0) {
|
|
3055
|
+
ctx.addIssue({
|
|
3056
|
+
code: "custom",
|
|
3057
|
+
path: ["startLine"],
|
|
3058
|
+
message: "startLine is required when endLine is set"
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
if (value.startLine !== void 0 && value.endLine !== void 0 && value.endLine < value.startLine) {
|
|
3062
|
+
ctx.addIssue({
|
|
3063
|
+
code: "custom",
|
|
3064
|
+
path: ["endLine"],
|
|
3065
|
+
message: "endLine must be greater than or equal to startLine"
|
|
3066
|
+
});
|
|
3067
|
+
}
|
|
3068
|
+
});
|
|
3069
|
+
var ReviewGuideSectionSchema = z8.object({
|
|
3070
|
+
title: z8.string().min(1).max(160),
|
|
3071
|
+
explanation: z8.string().min(1).max(2e3),
|
|
3072
|
+
classification: z8.enum(["core", "supporting"]).optional(),
|
|
3073
|
+
files: z8.array(ReviewGuideFileReferenceSchema).min(1).max(20)
|
|
3074
|
+
}).strict();
|
|
3075
|
+
var ReviewGuideContentSchema = z8.object({
|
|
3076
|
+
overview: z8.string().min(1).max(3e3),
|
|
3077
|
+
sections: z8.array(ReviewGuideSectionSchema).min(1).max(12)
|
|
3078
|
+
}).strict();
|
|
3079
|
+
var PublishReviewGuideRequestSchema = ReviewGuideContentSchema.extend({
|
|
3080
|
+
sessionId: z8.string().min(1),
|
|
3081
|
+
reviewedSha: z8.string().regex(SHA_PATTERN, "reviewedSha must be a 40-character commit SHA")
|
|
3082
|
+
}).strict();
|
|
3083
|
+
var CONTEXT_LINK_LOCATOR_MAX = 300;
|
|
3084
|
+
var TEST_TITLE = /\b(?:it|test|describe)(?:\.\w+)?\s*\(\s*(['"`])((?:(?!\1)[\s\S])*)\1/g;
|
|
3085
|
+
function extractTestTitles(content) {
|
|
3086
|
+
return [...content.matchAll(TEST_TITLE)].map((m) => m[2]);
|
|
3087
|
+
}
|
|
3088
|
+
function isPlaceholderLocator(locator) {
|
|
3089
|
+
return locator.includes("<") || locator.includes(">");
|
|
3090
|
+
}
|
|
3091
|
+
function locatorMatchesContent(content, locatorType, locator) {
|
|
3092
|
+
if (locatorType === "code") return content.includes(locator);
|
|
3093
|
+
return extractTestTitles(content).some((title) => title.includes(locator));
|
|
3094
|
+
}
|
|
3095
|
+
var TAG_DESCRIPTION_MAX = CARD_DESCRIPTION_MAX;
|
|
3096
|
+
var TAG_OVERVIEW_MAX = 32e3;
|
|
3097
|
+
var TAG_REASON_MAX = 500;
|
|
3098
|
+
var ProjectTagContextPathSchema = z9.object({
|
|
3099
|
+
type: z9.enum(["rule", "doc", "file", "folder"]),
|
|
3100
|
+
path: z9.string().min(1).max(500),
|
|
3101
|
+
label: z9.string().max(100).optional(),
|
|
3102
|
+
/** Verified-link tether — text that must keep existing in the file. */
|
|
3103
|
+
locator: z9.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional(),
|
|
3104
|
+
/** test = must appear in a real test/describe title; code = any substring. */
|
|
3105
|
+
locatorType: z9.enum(["test", "code"]).optional()
|
|
3106
|
+
}).refine((link) => link.locator === void 0 === (link.locatorType === void 0), {
|
|
3107
|
+
message: "locator and locatorType must be provided together"
|
|
3108
|
+
}).refine((link) => link.locator === void 0 || link.type !== "folder", {
|
|
3109
|
+
message: "folder links cannot carry a locator"
|
|
3110
|
+
});
|
|
3111
|
+
var hexColor = z9.string().regex(/^#[0-9a-fA-F]{6}$/, "Expected #RRGGBB hex color");
|
|
3112
|
+
var overviewPathSchema = z9.string().min(1).max(500).regex(/^[^\r\n]*$/, "Overview path cannot contain line breaks");
|
|
3113
|
+
var CreateProjectTagRequestSchema = z9.object({
|
|
3114
|
+
projectId: z9.string(),
|
|
3115
|
+
name: z9.string().min(1).max(50),
|
|
3116
|
+
color: hexColor.optional(),
|
|
3117
|
+
description: z9.string().max(TAG_DESCRIPTION_MAX).optional(),
|
|
3118
|
+
overview: z9.string().max(TAG_OVERVIEW_MAX).optional(),
|
|
3119
|
+
/** Source the overview from this repo file (stored overview stays as the pending fallback). */
|
|
3120
|
+
overviewPath: overviewPathSchema.optional(),
|
|
3121
|
+
contextPaths: z9.array(ProjectTagContextPathSchema).max(20).optional(),
|
|
3122
|
+
/** Parents to link at create time (multi-parent DAG). */
|
|
3123
|
+
parentTagIds: z9.array(z9.string()).max(25).optional(),
|
|
3124
|
+
requestingUserId: z9.string().optional()
|
|
3125
|
+
});
|
|
3126
|
+
var UpdateProjectTagRequestSchema = z9.object({
|
|
3127
|
+
projectId: z9.string(),
|
|
3128
|
+
tagId: z9.string(),
|
|
3129
|
+
name: z9.string().min(1).max(50).optional(),
|
|
3130
|
+
color: hexColor.optional(),
|
|
3131
|
+
description: z9.string().max(TAG_DESCRIPTION_MAX).optional(),
|
|
3132
|
+
/** Full markdown glossary body; null clears it. Rejected while overviewPath is set. */
|
|
3133
|
+
overview: z9.string().max(TAG_OVERVIEW_MAX).nullable().optional(),
|
|
3134
|
+
/** Repo file to source the overview from; null clears back to the stored overview. */
|
|
3135
|
+
overviewPath: overviewPathSchema.nullable().optional(),
|
|
3136
|
+
/** Full replacement of the tag's context links when provided. */
|
|
3137
|
+
contextPaths: z9.array(ProjectTagContextPathSchema).max(20).optional(),
|
|
3138
|
+
/** Full-set replacement of the tag's parent tags (multi-parent DAG). */
|
|
3139
|
+
parentTagIds: z9.array(z9.string()).max(25).optional(),
|
|
3140
|
+
/** One-line revision provenance, recorded in the tag's history. */
|
|
3141
|
+
reason: z9.string().max(TAG_REASON_MAX).optional(),
|
|
3142
|
+
/** Card the caller was working in — stamped into the revision history. */
|
|
3143
|
+
taskId: z9.string().optional(),
|
|
3144
|
+
requestingUserId: z9.string().optional()
|
|
3145
|
+
});
|
|
3146
|
+
var PostToProjectChatRequestSchema = z9.object({
|
|
3147
|
+
projectId: z9.string(),
|
|
3148
|
+
content: z9.string().min(1).max(2e4),
|
|
3149
|
+
requestingUserId: z9.string().optional(),
|
|
3150
|
+
/** Marks the post so the server can persist it beyond chat (tag-audit summaries land in tag history). */
|
|
3151
|
+
kind: z9.enum(["tag_audit_summary"]).optional()
|
|
3152
|
+
});
|
|
3153
|
+
var StartTagAuditRequestSchema = z9.object({
|
|
3154
|
+
projectId: z9.string(),
|
|
3155
|
+
requestingUserId: z9.string().optional()
|
|
3156
|
+
});
|
|
3157
|
+
var StartTaskAuditRequestSchema = z9.object({
|
|
3158
|
+
projectId: z9.string(),
|
|
3159
|
+
taskIds: z9.array(z9.string()).min(1).max(20),
|
|
3160
|
+
requestingUserId: z9.string().optional()
|
|
3161
|
+
});
|
|
3162
|
+
var GetActiveAuditSessionsRequestSchema = z9.object({
|
|
3163
|
+
projectId: z9.string()
|
|
3164
|
+
});
|
|
3165
|
+
var ReportTaskAuditResultRequestSchema = z9.object({
|
|
3166
|
+
projectId: z9.string(),
|
|
3167
|
+
taskId: z9.string(),
|
|
3168
|
+
summary: z9.string(),
|
|
3169
|
+
turnGrades: z9.array(
|
|
3170
|
+
z9.object({
|
|
3171
|
+
turnIndex: z9.number(),
|
|
3172
|
+
phase: z9.enum(["planning", "building", "human"]),
|
|
3173
|
+
grade: z9.enum(["correct", "neutral", "blunder"]),
|
|
3174
|
+
reasoning: z9.string(),
|
|
3175
|
+
eventType: z9.string(),
|
|
3176
|
+
eventSummary: z9.string()
|
|
3177
|
+
})
|
|
3178
|
+
),
|
|
3179
|
+
planningAccuracy: z9.number().nullable(),
|
|
3180
|
+
buildingAccuracy: z9.number().nullable(),
|
|
3181
|
+
humanAccuracy: z9.number().nullable(),
|
|
3182
|
+
planningCorrect: z9.number(),
|
|
3183
|
+
planningNeutral: z9.number(),
|
|
3184
|
+
planningBlunder: z9.number(),
|
|
3185
|
+
buildingCorrect: z9.number(),
|
|
3186
|
+
buildingNeutral: z9.number(),
|
|
3187
|
+
buildingBlunder: z9.number(),
|
|
3188
|
+
humanCorrect: z9.number(),
|
|
3189
|
+
humanNeutral: z9.number(),
|
|
3190
|
+
humanBlunder: z9.number(),
|
|
3191
|
+
humanEvaluations: z9.array(
|
|
3192
|
+
z9.object({
|
|
3193
|
+
messageIndex: z9.number(),
|
|
3194
|
+
rating: z9.union([z9.literal(-1), z9.literal(0), z9.literal(1)]),
|
|
3195
|
+
reasoning: z9.string()
|
|
3196
|
+
})
|
|
3197
|
+
).optional(),
|
|
3198
|
+
suggestionIds: z9.array(z9.string()),
|
|
3199
|
+
auditCostUsd: z9.number().nullable(),
|
|
3200
|
+
model: z9.string().nullable(),
|
|
3201
|
+
/** When set, the audit is marked failed with this message instead. */
|
|
3202
|
+
error: z9.string().optional()
|
|
3203
|
+
});
|
|
3204
|
+
var GetTaskAuditsRequestSchema = z9.object({
|
|
3205
|
+
projectId: z9.string(),
|
|
3206
|
+
limit: z9.number().int().positive().max(200).optional().default(50)
|
|
3207
|
+
});
|
|
3208
|
+
var GetTaskAuditRequestSchema = z9.object({
|
|
3209
|
+
projectId: z9.string(),
|
|
3210
|
+
auditId: z9.string()
|
|
3211
|
+
});
|
|
3212
|
+
var GetTaskAuditAggregatesRequestSchema = z9.object({
|
|
3213
|
+
projectId: z9.string()
|
|
3214
|
+
});
|
|
3215
|
+
var DeleteTaskAuditRequestSchema = z9.object({
|
|
3216
|
+
projectId: z9.string(),
|
|
3217
|
+
auditId: z9.string(),
|
|
3218
|
+
requestingUserId: z9.string().optional()
|
|
3219
|
+
});
|
|
3220
|
+
var MarkInitialPromptSubmittedRequestSchema = z9.object({
|
|
3221
|
+
sessionId: z9.string()
|
|
3222
|
+
});
|
|
3223
|
+
var CRITICAL_AUTOMATED_SOURCES = /* @__PURE__ */ new Set([
|
|
3224
|
+
"ci_failure",
|
|
3225
|
+
"review_trigger",
|
|
3226
|
+
"merge_conflict",
|
|
3227
|
+
"merge_failed",
|
|
3228
|
+
"pull_branch",
|
|
3229
|
+
// Child-task events for pack parents: the orchestrator must act (merge the
|
|
3230
|
+
// child's PR, start unblocked siblings, finish the pack) even after it
|
|
3231
|
+
// reported completed for a prior turn. Only ever sent to parent tasks.
|
|
3232
|
+
"parent",
|
|
3233
|
+
// The result of a CI wait the agent parked with `wait_for_checks`. The agent
|
|
3234
|
+
// ended its turn BECAUSE it was told to, so the wake that answers it must
|
|
3235
|
+
// clear the completion guard.
|
|
3236
|
+
"ci_result"
|
|
3237
|
+
]);
|
|
3238
|
+
var MEETING_CHECKLIST_TITLE_MAX = 300;
|
|
3239
|
+
var MEETING_TRANSCRIPT_MAX_CHARS = 2e6;
|
|
3240
|
+
var MEETING_TITLE_MAX = 200;
|
|
3241
|
+
var MEETING_OCCURRED_AT_MIN_YEAR = 2e3;
|
|
3242
|
+
var MEETING_OCCURRED_AT_MAX_FUTURE_MS = 48 * 60 * 60 * 1e3;
|
|
3243
|
+
var OCCURRED_AT_RANGE_MESSAGE = `occurredAt must be a real date: no earlier than ${MEETING_OCCURRED_AT_MIN_YEAR}, and no more than 48 hours in the future.`;
|
|
3244
|
+
var MeetingOccurredAtSchema = z10.string().datetime().refine((value) => {
|
|
3245
|
+
const ms = Date.parse(value);
|
|
3246
|
+
if (Number.isNaN(ms)) return false;
|
|
3247
|
+
if (ms > Date.now() + MEETING_OCCURRED_AT_MAX_FUTURE_MS) return false;
|
|
3248
|
+
return new Date(ms).getUTCFullYear() >= MEETING_OCCURRED_AT_MIN_YEAR;
|
|
3249
|
+
}, OCCURRED_AT_RANGE_MESSAGE);
|
|
3250
|
+
var CreateMeetingFromTranscriptRequestSchema = z10.object({
|
|
3251
|
+
projectId: z10.string().cuid(),
|
|
3252
|
+
rawText: z10.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
|
|
3253
|
+
title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
|
|
3254
|
+
/** ISO 8601. Defaults to now when the source carries no date. */
|
|
3255
|
+
occurredAt: MeetingOccurredAtSchema.optional(),
|
|
3256
|
+
/** Override auto-detection. Rarely needed; detection handles the three formats. */
|
|
3257
|
+
format: z10.enum(["text", "vtt", "srt"]).optional(),
|
|
3258
|
+
source: z10.enum(["manual", "slack"]).optional()
|
|
3259
|
+
});
|
|
3260
|
+
var GetMeetingRequestSchema = z10.object({
|
|
3261
|
+
projectId: z10.string().cuid(),
|
|
3262
|
+
meetingId: z10.string().cuid()
|
|
3263
|
+
});
|
|
3264
|
+
var UpdateMeetingRequestSchema = z10.object({
|
|
3265
|
+
projectId: z10.string().cuid(),
|
|
3266
|
+
meetingId: z10.string().cuid(),
|
|
3267
|
+
title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
|
|
3268
|
+
occurredAt: MeetingOccurredAtSchema.optional()
|
|
3269
|
+
});
|
|
3270
|
+
var RegenerateMeetingSummaryRequestSchema = z10.object({
|
|
3271
|
+
projectId: z10.string().cuid(),
|
|
3272
|
+
meetingId: z10.string().cuid()
|
|
3273
|
+
});
|
|
3274
|
+
var DeleteMeetingRequestSchema = z10.object({
|
|
3275
|
+
projectId: z10.string().cuid(),
|
|
3276
|
+
meetingId: z10.string().cuid()
|
|
3277
|
+
});
|
|
3278
|
+
var checklistTitle = z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX);
|
|
3279
|
+
var ListMeetingChecklistRequestSchema = z10.object({
|
|
3280
|
+
projectId: z10.string().cuid(),
|
|
3281
|
+
meetingId: z10.string().cuid()
|
|
3282
|
+
});
|
|
3283
|
+
var AddMeetingChecklistItemsRequestSchema = z10.object({
|
|
3284
|
+
projectId: z10.string().cuid(),
|
|
3285
|
+
meetingId: z10.string().cuid(),
|
|
3286
|
+
items: z10.array(z10.object({ title: checklistTitle })).min(1).max(50)
|
|
3287
|
+
});
|
|
3288
|
+
var UpdateMeetingChecklistItemRequestSchema = z10.object({
|
|
3289
|
+
projectId: z10.string().cuid(),
|
|
3290
|
+
meetingId: z10.string().cuid(),
|
|
3291
|
+
itemId: z10.string().cuid(),
|
|
3292
|
+
title: checklistTitle.optional(),
|
|
3293
|
+
ordinal: z10.number().int().min(0).optional(),
|
|
3294
|
+
/** Explicit null clears the link; undefined leaves it alone. */
|
|
3295
|
+
linkedTaskId: z10.string().cuid().nullable().optional()
|
|
3296
|
+
}).refine(
|
|
3297
|
+
(v) => v.title !== void 0 || v.ordinal !== void 0 || v.linkedTaskId !== void 0,
|
|
3298
|
+
"Pass at least one of title, ordinal, or linkedTaskId."
|
|
3299
|
+
);
|
|
3300
|
+
var DeleteMeetingChecklistItemRequestSchema = z10.object({
|
|
3301
|
+
projectId: z10.string().cuid(),
|
|
3302
|
+
meetingId: z10.string().cuid(),
|
|
3303
|
+
itemId: z10.string().cuid()
|
|
3304
|
+
});
|
|
3305
|
+
var SetMeetingChecklistItemCheckedRequestSchema = z10.object({
|
|
3306
|
+
projectId: z10.string().cuid(),
|
|
3307
|
+
meetingId: z10.string().cuid(),
|
|
3308
|
+
itemId: z10.string().cuid(),
|
|
3309
|
+
checked: z10.boolean(),
|
|
3310
|
+
/** Attach the card in the same call that ticks the item. */
|
|
3311
|
+
linkedTaskId: z10.string().cuid().nullable().optional()
|
|
3312
|
+
});
|
|
3313
|
+
var ListMeetingsRequestSchema = z10.object({
|
|
3314
|
+
projectId: z10.string().cuid(),
|
|
3315
|
+
limit: z10.number().int().min(1).max(50).optional(),
|
|
3316
|
+
search: z10.string().max(200).optional()
|
|
3317
|
+
});
|
|
3318
|
+
var ReadMeetingTranscriptRequestSchema = z10.object({
|
|
3319
|
+
projectId: z10.string().cuid(),
|
|
3320
|
+
meetingId: z10.string().cuid(),
|
|
3321
|
+
offset: z10.number().int().min(0).optional(),
|
|
3322
|
+
limit: z10.number().int().min(1).max(500).optional()
|
|
3323
|
+
});
|
|
3324
|
+
var MEETING_SUMMARY_MAX_CHARS = 5e4;
|
|
3325
|
+
var AddProjectMeetingChecklistItemsRequestSchema = z10.object({
|
|
3326
|
+
projectId: z10.string().cuid(),
|
|
3327
|
+
meetingId: z10.string().cuid(),
|
|
3328
|
+
items: z10.array(z10.object({ title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX) })).min(1).max(50),
|
|
3329
|
+
requestingUserId: z10.string().optional()
|
|
3330
|
+
});
|
|
3331
|
+
var CheckProjectMeetingChecklistItemRequestSchema = z10.object({
|
|
3332
|
+
projectId: z10.string().cuid(),
|
|
3333
|
+
meetingId: z10.string().cuid(),
|
|
3334
|
+
title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
|
|
3335
|
+
checked: z10.boolean(),
|
|
3336
|
+
/** Card id or slug. Resolved server-side and required to be in the project. */
|
|
3337
|
+
linkedTask: z10.string().min(1).optional(),
|
|
3338
|
+
requestingUserId: z10.string().optional()
|
|
3339
|
+
});
|
|
3340
|
+
var EditProjectMeetingChecklistItemRequestSchema = z10.object({
|
|
3341
|
+
projectId: z10.string().cuid(),
|
|
3342
|
+
meetingId: z10.string().cuid(),
|
|
3343
|
+
title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
|
|
3344
|
+
newTitle: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
|
|
3345
|
+
requestingUserId: z10.string().optional()
|
|
3346
|
+
});
|
|
3347
|
+
var RemoveProjectMeetingChecklistItemRequestSchema = z10.object({
|
|
3348
|
+
projectId: z10.string().cuid(),
|
|
3349
|
+
meetingId: z10.string().cuid(),
|
|
3350
|
+
title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
|
|
3351
|
+
requestingUserId: z10.string().optional()
|
|
3352
|
+
});
|
|
3353
|
+
var CreateProjectMeetingRequestSchema = z10.object({
|
|
3354
|
+
projectId: z10.string().cuid(),
|
|
3355
|
+
rawText: z10.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
|
|
3356
|
+
title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
|
|
3357
|
+
occurredAt: MeetingOccurredAtSchema.optional(),
|
|
3358
|
+
requestingUserId: z10.string().optional()
|
|
3359
|
+
});
|
|
3360
|
+
var UpdateProjectMeetingRequestSchema = z10.object({
|
|
3361
|
+
projectId: z10.string().cuid(),
|
|
3362
|
+
meetingId: z10.string().cuid(),
|
|
3363
|
+
title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
|
|
3364
|
+
occurredAt: MeetingOccurredAtSchema.optional(),
|
|
3365
|
+
summary: z10.string().min(1).max(MEETING_SUMMARY_MAX_CHARS).optional(),
|
|
3366
|
+
requestingUserId: z10.string().optional()
|
|
3367
|
+
}).refine(
|
|
3368
|
+
(v) => v.title !== void 0 || v.occurredAt !== void 0 || v.summary !== void 0,
|
|
3369
|
+
"Pass at least one of title, occurredAt, or summary."
|
|
3370
|
+
);
|
|
3371
|
+
var AGENT_STATUS_REASON_USER_QUESTION = "user_question";
|
|
3372
|
+
var TASK_CHAT_HISTORY_LIMIT = 20;
|
|
3373
|
+
var PM_CHAT_HISTORY_LIMIT = 40;
|
|
3374
|
+
var AGENT_CHAT_HISTORY_FETCH_LIMIT = Math.max(TASK_CHAT_HISTORY_LIMIT, PM_CHAT_HISTORY_LIMIT) + 10;
|
|
3375
|
+
function formatModelId(provider, model) {
|
|
3376
|
+
return `${provider}/${model}`;
|
|
3377
|
+
}
|
|
3378
|
+
function anthropicEntry(model, label, inputPerMillion, outputPerMillion, opts = {}) {
|
|
3379
|
+
return {
|
|
3380
|
+
provider: "anthropic",
|
|
3381
|
+
model,
|
|
3382
|
+
id: formatModelId("anthropic", model),
|
|
3383
|
+
label,
|
|
3384
|
+
format: "anthropic-messages",
|
|
3385
|
+
inputPrice: inputPerMillion / 1e6,
|
|
3386
|
+
outputPrice: outputPerMillion / 1e6,
|
|
3387
|
+
supportsTools: true,
|
|
3388
|
+
supportsEffort: opts.supportsEffort ?? true,
|
|
3389
|
+
...opts.experimental ? { experimental: true } : {}
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
var ANTHROPIC_CATALOG = [
|
|
3393
|
+
anthropicEntry(DEFAULT_OPUS_MODEL, "Opus 5 Latest", 5, 25),
|
|
3394
|
+
anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 4.8", 5, 25),
|
|
3395
|
+
anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5 Latest", 3, 15),
|
|
3396
|
+
anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet 4.6", 3, 15),
|
|
3397
|
+
// The Haiku line (4.5 and older) predates the tuning surface and 400s on it.
|
|
3398
|
+
anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
|
|
3399
|
+
anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
|
|
3400
|
+
];
|
|
3401
|
+
var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
3402
|
+
var DEFAULT_CODEX_CODING_MODEL = "gpt-5.6-terra";
|
|
3403
|
+
function isCodexReasoningEffort(value) {
|
|
3404
|
+
return typeof value === "string" && CODEX_REASONING_EFFORTS.includes(value);
|
|
3405
|
+
}
|
|
3406
|
+
var HUMAN_PROSE_WRITING_STYLE = `## Writing style for humans
|
|
3407
|
+
When you write prose a person will read \u2014 chat messages, plan updates, PR titles and bodies, PR review guides (the \`publish_review_guide\` overview and section explanations), review comments \u2014 follow these rules (based on ASD-STE100 Simplified Technical English):
|
|
3408
|
+
- Use active voice. Say who does what ("The API rejects the request", not "the request is rejected").
|
|
3409
|
+
- Use simple tenses ("we received", not "we have received").
|
|
3410
|
+
- One instruction or fact per sentence. Keep sentences under ~20 words.
|
|
3411
|
+
- Pick one word for one thing and reuse it. Do not rotate synonyms (check/verify/confirm) for the same action.
|
|
3412
|
+
- Prefer the plain, common word ("use", not "utilize"; "start", not "initiate").
|
|
3413
|
+
- Do not stack more than 3 nouns in a row ("task queue handler" is the limit).
|
|
3414
|
+
- Use a numbered or bulleted list for 3+ steps or conditions instead of burying them in one sentence.
|
|
3415
|
+
- Define a technical term on first use when a non-engineer will read the message.
|
|
3416
|
+
- Never drop a condition, number, or scope qualifier to shorten a sentence. Precision beats brevity.
|
|
3417
|
+
These rules do NOT apply to code, code comments, commit messages, or quoted output.`;
|
|
3418
|
+
var CLAUDESPACE_WORKLOAD_LABEL = "rc-workload";
|
|
3419
|
+
var CLAUDESPACE_WORKLOAD_VALUE = "claudespace";
|
|
3420
|
+
var CONVEYOR_POD_SELECTOR = `${CLAUDESPACE_WORKLOAD_LABEL}=${CLAUDESPACE_WORKLOAD_VALUE}`;
|
|
3421
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
3422
|
+
var MENTION_TOKEN_REGEX = /@\[(\w+):([^\]]+)\]/g;
|
|
3423
|
+
function parseMentions(content) {
|
|
3424
|
+
const tokens = [];
|
|
3425
|
+
for (const match of content.matchAll(MENTION_TOKEN_REGEX)) {
|
|
3426
|
+
tokens.push({ type: match[1], id: match[2] });
|
|
3427
|
+
}
|
|
3428
|
+
return tokens;
|
|
3429
|
+
}
|
|
3430
|
+
var POSTGRES_ENV = {
|
|
3431
|
+
POSTGRES_HOST_AUTH_METHOD: "trust",
|
|
3432
|
+
POSTGRES_DB: "conveyor"
|
|
3433
|
+
};
|
|
3434
|
+
var FIREBASE_EMULATOR_COMMAND = [
|
|
3435
|
+
"sh",
|
|
3436
|
+
"-c",
|
|
3437
|
+
`set -e; mkdir -p /home/node && cd /home/node && cat > firebase.json <<'EOF'
|
|
3438
|
+
{"emulators":{"auth":{"host":"0.0.0.0","port":9099},"hub":{"host":"0.0.0.0","port":4400},"ui":{"enabled":false}}}
|
|
3439
|
+
EOF
|
|
3440
|
+
exec firebase emulators:start --only=auth --project=rally-cry-dev`
|
|
3441
|
+
];
|
|
3442
|
+
var FIREBASE_EMULATOR_ENV = {
|
|
3443
|
+
METADATA_SERVER_DETECTION: "none",
|
|
3444
|
+
GOOGLE_APPLICATION_CREDENTIALS: "/dev/null"
|
|
3445
|
+
};
|
|
3446
|
+
var CATALOG = {
|
|
3447
|
+
postgresql: {
|
|
3448
|
+
name: "postgresql",
|
|
3449
|
+
image: "postgres:16-alpine",
|
|
3450
|
+
command: [
|
|
3451
|
+
"sh",
|
|
3452
|
+
"-c",
|
|
3453
|
+
// Start postgres, wait for readiness, then create the test database.
|
|
3454
|
+
// Durability flags: service state is ephemeral by design (overlayfs, no
|
|
3455
|
+
// PVC; a crash re-seeds from pod-data), so commits must never wait on a
|
|
3456
|
+
// WAL flush. fsync=off + synchronous_commit=off + full_page_writes=off
|
|
3457
|
+
// remove all blocking storage I/O — without them, per-commit flushes on
|
|
3458
|
+
// the node's network boot disk dominated int-test wall time (~100ms/commit).
|
|
3459
|
+
"if [ ! -s /var/lib/postgresql/data/PG_VERSION ] && [ -d /var/lib/postgresql/pod-data ]; then cp -a /var/lib/postgresql/pod-data/. /var/lib/postgresql/data/; fi; chown -R postgres:postgres /var/lib/postgresql/data; docker-entrypoint.sh postgres -c fsync=off -c synchronous_commit=off -c full_page_writes=off & PID=$!; for i in $(seq 1 30); do pg_isready -U postgres && break; sleep 1; done; createdb -U postgres conveyor_test 2>/dev/null || true; wait $PID"
|
|
3460
|
+
],
|
|
3461
|
+
ports: [5432],
|
|
3462
|
+
livenessProbe: {
|
|
3463
|
+
exec: ["pg_isready", "-U", "postgres"],
|
|
3464
|
+
periodSeconds: 30,
|
|
3465
|
+
failureThreshold: 3,
|
|
3466
|
+
timeoutSeconds: 5,
|
|
3467
|
+
initialDelaySeconds: 60
|
|
3468
|
+
},
|
|
3469
|
+
env: { ...POSTGRES_ENV },
|
|
3470
|
+
resources: {
|
|
3471
|
+
// postgres runs from the baked seed at /var/lib/postgresql/pod-data via
|
|
3472
|
+
// overlayfs CoW (no emptyDir — see k8s-pod-spec.ts), so WAL + catalog +
|
|
3473
|
+
// re-seed churn charges the container's ephemeral-storage. It must exceed
|
|
3474
|
+
// the ~1Gi ceiling where pods evicted, but is bounded by TWO Autopilot rules:
|
|
3475
|
+
// 1) limit == request — Autopilot caps the limit DOWN to the request at
|
|
3476
|
+
// admission, so headroom must live on the REQUEST, not just the limit;
|
|
3477
|
+
// 2) the SUM of all container ephemeral requests in a pod must be ≤ 10Gi
|
|
3478
|
+
// (an emptyDir doesn't escape this: its usage still evicts against the
|
|
3479
|
+
// container limit, and raising the limit re-hits rule 1 → the cap).
|
|
3480
|
+
// The agent is trimmed to 4Gi (resource-tiers.ts) to free room: with
|
|
3481
|
+
// agent 4 + gcsfuse 1 + lgtm 1 + es/redis/firebase 0.75, postgres gets 3Gi
|
|
3482
|
+
// and the heaviest (URC) pod sits at 9.75Gi + 10Mi for GCS Fuse metadata
|
|
3483
|
+
// prefetch — under the 10Gi cap — while giving postgres 3x the ~1Gi
|
|
3484
|
+
// ceiling that evicted. Keep request == limit;
|
|
3485
|
+
// see the per-pod ephemeral budget guard test.
|
|
3486
|
+
// CPU: the request is the CFS floor; the old 50m starved postgres to 5ms
|
|
3487
|
+
// of CPU per 100ms period — every query burst hit throttle stalls, which
|
|
3488
|
+
// showed up as ~100ms floors on trivial statements and dominated the API
|
|
3489
|
+
// int suite even after the fsync flags above. 250m is paid for out of the
|
|
3490
|
+
// workbench's derived share (resource-tiers.ts); measured across the fleet
|
|
3491
|
+
// postgres peaks at 0.66 cores and idles far below 250m, and the limit
|
|
3492
|
+
// (unchanged at 2) is what serves the peaks: the first-boot pod-data seed
|
|
3493
|
+
// copy and int-suite query storms borrow idle node CPU without moving the
|
|
3494
|
+
// request.
|
|
3495
|
+
requests: { cpuMillicores: 250, memoryMi: 512, ephemeralMi: 3 * 1024 },
|
|
3496
|
+
limits: { cpuMillicores: 2e3, memoryMi: 512, ephemeralMi: 3 * 1024 }
|
|
3497
|
+
},
|
|
3498
|
+
connectionEnv: {
|
|
3499
|
+
DATABASE_URL: "postgresql://postgres@postgresql:5432/conveyor",
|
|
3500
|
+
TEST_DATABASE_URL: "postgresql://postgres@postgresql:5432/conveyor_test"
|
|
3501
|
+
},
|
|
3502
|
+
statefulBake: true,
|
|
3503
|
+
bake: {
|
|
3504
|
+
// Start postgres, wait for readiness, then create the test database so
|
|
3505
|
+
// it's baked into the committed image. No pod-data restore (nothing is
|
|
3506
|
+
// seeded yet) and no durability flags (the bake's writes must land).
|
|
3507
|
+
// NOTE: docker-compose interpolates `$VAR`/`$(...)`, so every `$` that
|
|
3508
|
+
// must reach the shell is doubled.
|
|
3509
|
+
command: [
|
|
3510
|
+
"sh",
|
|
3511
|
+
"-c",
|
|
3512
|
+
"docker-entrypoint.sh postgres & PID=$$!; for i in $$(seq 1 30); do pg_isready -U postgres && break; sleep 1; done; createdb -U postgres conveyor_test 2>/dev/null || true; wait $$PID"
|
|
3513
|
+
],
|
|
3514
|
+
environment: { ...POSTGRES_ENV },
|
|
3515
|
+
healthcheck: {
|
|
3516
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"],
|
|
3517
|
+
intervalSec: 2,
|
|
3518
|
+
timeoutSec: 5,
|
|
3519
|
+
retries: 15
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
},
|
|
3523
|
+
redis: {
|
|
3524
|
+
name: "redis",
|
|
3525
|
+
image: "redis:7-alpine",
|
|
3526
|
+
command: ["redis-server", "--appendonly", "yes", "--dir", "/data"],
|
|
3527
|
+
ports: [6379],
|
|
3528
|
+
env: {},
|
|
3529
|
+
resources: {
|
|
3530
|
+
requests: { cpuMillicores: 25, memoryMi: 32, ephemeralMi: 256 },
|
|
3531
|
+
limits: { cpuMillicores: 50, memoryMi: 64, ephemeralMi: 256 }
|
|
3532
|
+
},
|
|
3533
|
+
connectionEnv: {
|
|
3534
|
+
REDIS_URL: "redis://redis:6379",
|
|
3535
|
+
AUTH_REDIS_URL: "redis://redis:6379"
|
|
3536
|
+
},
|
|
3537
|
+
statefulBake: false,
|
|
3538
|
+
// Redis holds no baked state, so the bake runs the stock image CMD.
|
|
3539
|
+
bake: {},
|
|
3540
|
+
mirror: { src: "redis:7-alpine", dest: "mirror-redis:7-alpine" }
|
|
3541
|
+
},
|
|
3542
|
+
elasticsearch: {
|
|
3543
|
+
name: "elasticsearch",
|
|
3544
|
+
// 9.4.0 matches universal-rally-cry's local compose + its v9 ES client —
|
|
3545
|
+
// the v9 client sends Accept: compatible-with=9, which an 8.x server
|
|
3546
|
+
// rejects (media_type_header_exception), breaking search/audit indexing
|
|
3547
|
+
// in pods. 512m heap matches the project compose sizing.
|
|
3548
|
+
image: "docker.elastic.co/elasticsearch/elasticsearch:9.4.0",
|
|
3549
|
+
mirror: {
|
|
3550
|
+
src: "docker.elastic.co/elasticsearch/elasticsearch:9.4.0",
|
|
3551
|
+
dest: "mirror-elasticsearch:9.4.0"
|
|
3552
|
+
},
|
|
3553
|
+
ports: [9200],
|
|
3554
|
+
// Baked service images are `docker commit`s of a recently-running ES, so
|
|
3555
|
+
// they carry a stale data-dir node.lock; ES 9 hard-fails on it at boot
|
|
3556
|
+
// ("Underlying file changed by an external force" → AlreadyClosedException).
|
|
3557
|
+
// Clear it before handing off to the stock entrypoint.
|
|
3558
|
+
command: [
|
|
3559
|
+
"sh",
|
|
3560
|
+
"-c",
|
|
3561
|
+
// ALL Lucene lock files, not just node.lock — the baked image also
|
|
3562
|
+
// carries per-index write.lock + snapshot_cache/write.lock, and ES 9
|
|
3563
|
+
// fail-fasts on any of them ("changed by an external force").
|
|
3564
|
+
"find /usr/share/elasticsearch/data -name '*.lock' -type f -delete 2>/dev/null; exec /usr/local/bin/docker-entrypoint.sh eswrapper"
|
|
3565
|
+
],
|
|
3566
|
+
env: {
|
|
3567
|
+
"discovery.type": "single-node",
|
|
3568
|
+
"xpack.security.enabled": "false",
|
|
3569
|
+
"xpack.ml.enabled": "false",
|
|
3570
|
+
"xpack.watcher.enabled": "false",
|
|
3571
|
+
"xpack.profiling.enabled": "false",
|
|
3572
|
+
"ingest.geoip.downloader.enabled": "false",
|
|
3573
|
+
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
|
3574
|
+
},
|
|
3575
|
+
resources: {
|
|
3576
|
+
// CPU limit 8x the request: ES cold-start is a CPU-bound JVM boot
|
|
3577
|
+
// (class loading + JIT + recovery of the docker-commit'ed data dir),
|
|
3578
|
+
// and a 500m hard cap put it at ~135s to yellow, past the sidecar
|
|
3579
|
+
// wait script's original 90s budget. Bursting to 2 cut it to ~40s on
|
|
3580
|
+
// the real cluster (A/B on identical nodes, 2 rounds). The burst only
|
|
3581
|
+
// borrows idle node CPU at boot; under contention CFS still floors ES
|
|
3582
|
+
// at its request. That request is 250m: the fleet-wide peak is 1.6
|
|
3583
|
+
// cores (served by the limit) and steady state is far below 250m, so
|
|
3584
|
+
// the old 500m only inflated the billed pod total.
|
|
3585
|
+
requests: { cpuMillicores: 250, memoryMi: 1024, ephemeralMi: 256 },
|
|
3586
|
+
limits: { cpuMillicores: 2e3, memoryMi: 1024, ephemeralMi: 256 }
|
|
3587
|
+
},
|
|
3588
|
+
connectionEnv: {
|
|
3589
|
+
ELASTICSEARCH_URL: "http://elasticsearch:9200"
|
|
3590
|
+
},
|
|
3591
|
+
statefulBake: true,
|
|
3592
|
+
bake: {
|
|
3593
|
+
// No lock-clearing command at bake time: the bake starts from the stock
|
|
3594
|
+
// image, which has no committed data dir to unlock yet.
|
|
3595
|
+
environment: {
|
|
3596
|
+
"discovery.type": "single-node",
|
|
3597
|
+
"xpack.security.enabled": "false",
|
|
3598
|
+
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
},
|
|
3602
|
+
// All-in-one image bundling Grafana + Loki + Tempo + Mimir + an OTEL Collector.
|
|
3603
|
+
// Pinned tag (not :latest) so the image-builder content hash stays
|
|
3604
|
+
// deterministic across upstream releases — see image-builder.ts content-hash
|
|
3605
|
+
// dedup keyed on `deps.sorted()`.
|
|
3606
|
+
lgtm: {
|
|
3607
|
+
name: "lgtm",
|
|
3608
|
+
image: "grafana/otel-lgtm:0.11.6",
|
|
3609
|
+
// The otel-lgtm image's own CMD is ["/otel-lgtm/run-all.sh"] (WORKDIR
|
|
3610
|
+
// /otel-lgtm). Declaring it explicitly makes lgtm a command-based service so
|
|
3611
|
+
// the lazy start-file gate wraps it like the others — otherwise it launches
|
|
3612
|
+
// via image CMD at boot and can't be parked (which is why warm-booting it at
|
|
3613
|
+
// a minimal request OOMKilled it). getSidecarSpecs strips this command for a
|
|
3614
|
+
// baked lgtm image, so it only applies to the stock image whose launcher is
|
|
3615
|
+
// exactly this path.
|
|
3616
|
+
command: ["/otel-lgtm/run-all.sh"],
|
|
3617
|
+
ports: [
|
|
3618
|
+
// OTLP gRPC + HTTP — agents and user code emit telemetry here.
|
|
3619
|
+
4317,
|
|
3620
|
+
4318,
|
|
3621
|
+
// Grafana UI — reachable through the existing preview proxy on
|
|
3622
|
+
// https://3000-{sessionId}.preview.<PREVIEW_DOMAIN>/.
|
|
3623
|
+
3e3
|
|
3624
|
+
],
|
|
3625
|
+
env: {
|
|
3626
|
+
// The pod is per-task; the preview proxy authenticates the session
|
|
3627
|
+
// upstream, so anonymous in-pod admin is acceptable here.
|
|
3628
|
+
GF_AUTH_ANONYMOUS_ENABLED: "true",
|
|
3629
|
+
GF_AUTH_ANONYMOUS_ORG_ROLE: "Admin",
|
|
3630
|
+
GF_SECURITY_ALLOW_EMBEDDING: "true",
|
|
3631
|
+
ENABLE_LOGS_GRAFANA: "true",
|
|
3632
|
+
ENABLE_LOGS_OTELCOL: "true"
|
|
3633
|
+
},
|
|
3634
|
+
resources: {
|
|
3635
|
+
// Autopilot caps the ephemeral-storage limit to the request (see the
|
|
3636
|
+
// postgresql note), so the 1Gi headroom must be on the request too.
|
|
3637
|
+
// CPU: the collector draws ~0.3 cores at idle fleet-wide, so the request
|
|
3638
|
+
// stays at 250m to cover that draw under node contention (a request
|
|
3639
|
+
// below usage is a container CFS throttles continuously); the 1-core
|
|
3640
|
+
// limit covers ingest bursts.
|
|
3641
|
+
requests: { cpuMillicores: 250, memoryMi: 1024, ephemeralMi: 1024 },
|
|
3642
|
+
limits: { cpuMillicores: 1e3, memoryMi: 2048, ephemeralMi: 1024 }
|
|
3643
|
+
},
|
|
3644
|
+
statefulBake: false,
|
|
3645
|
+
// No `bake` block: a BAKE produces traces nobody reads, so starting the
|
|
3646
|
+
// (heavy) collector image would only add a pull + boot to every build.
|
|
3647
|
+
// lgtm still runs for live pods, and is never committed regardless.
|
|
3648
|
+
mirror: { src: "grafana/otel-lgtm:0.11.6", dest: "mirror-otel-lgtm:0.11.6" }
|
|
3649
|
+
},
|
|
3650
|
+
"firebase-auth-emulator": {
|
|
3651
|
+
name: "firebase-auth-emulator",
|
|
3652
|
+
image: "andreysenov/firebase-tools:latest",
|
|
3653
|
+
command: FIREBASE_EMULATOR_COMMAND,
|
|
3654
|
+
ports: [9099, 4400],
|
|
3655
|
+
env: {
|
|
3656
|
+
// The emulator container inherits the pod's Workload Identity, so
|
|
3657
|
+
// firebase-tools finds GCP credentials and its `emulators:start` does an
|
|
3658
|
+
// online "auto auth" + project validation that stalls ~47s on the pod's
|
|
3659
|
+
// locked-down egress (the dominant service boot cost — measured live).
|
|
3660
|
+
// The emulator needs NO real credentials, so cut off credential discovery
|
|
3661
|
+
// for this container only: google-auth-library skips metadata detection
|
|
3662
|
+
// and finds no key file, firebase-tools logs "not authenticated" and
|
|
3663
|
+
// starts the emulator immediately. The agent container keeps its WI.
|
|
3664
|
+
...FIREBASE_EMULATOR_ENV
|
|
3665
|
+
},
|
|
3666
|
+
resources: {
|
|
3667
|
+
requests: { cpuMillicores: 100, memoryMi: 256, ephemeralMi: 256 },
|
|
3668
|
+
limits: { cpuMillicores: 500, memoryMi: 512, ephemeralMi: 256 }
|
|
3669
|
+
},
|
|
3670
|
+
connectionEnv: {
|
|
3671
|
+
// In docker-compose, services reach each other by service name — the
|
|
3672
|
+
// runtime pod equivalents use `localhost` because co-located services
|
|
3673
|
+
// share the pod's network namespace.
|
|
3674
|
+
FIREBASE_AUTH_EMULATOR_HOST: "firebase-auth-emulator:9099",
|
|
3675
|
+
NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_HOST: "firebase-auth-emulator:9099"
|
|
3676
|
+
},
|
|
3677
|
+
statefulBake: false,
|
|
3678
|
+
mirror: {
|
|
3679
|
+
src: "andreysenov/firebase-tools:latest",
|
|
3680
|
+
dest: "mirror-firebase-tools:latest"
|
|
3681
|
+
},
|
|
3682
|
+
bake: {
|
|
3683
|
+
// The emulator writes its config at startup, so the bake needs the same
|
|
3684
|
+
// inline command the runtime uses.
|
|
3685
|
+
command: FIREBASE_EMULATOR_COMMAND,
|
|
3686
|
+
environment: FIREBASE_EMULATOR_ENV,
|
|
3687
|
+
healthcheck: {
|
|
3688
|
+
// Probe with node, the one runtime this image guarantees. The image
|
|
3689
|
+
// (`andreysenov/firebase-tools`) is a slim Node image that ships
|
|
3690
|
+
// NEITHER `wget` NOR `curl`, so the `wget -qO- …` probe this replaced
|
|
3691
|
+
// exited 127 on every attempt: a healthy emulator burned all 20 retries
|
|
3692
|
+
// and every bake reported it unhealthy. See the catalog invariant in
|
|
3693
|
+
// `service-definitions.test.ts`, which also executes this probe.
|
|
3694
|
+
test: [
|
|
3695
|
+
"CMD-SHELL",
|
|
3696
|
+
`node -e "fetch('http://localhost:9099/').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"`
|
|
3697
|
+
],
|
|
3698
|
+
intervalSec: 3,
|
|
3699
|
+
timeoutSec: 5,
|
|
3700
|
+
retries: 20
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
};
|
|
3705
|
+
var SERVICE_DEFINITIONS = CATALOG;
|
|
3706
|
+
var SUPPORTED_CLAUDESPACE_DEPS = Object.keys(CATALOG);
|
|
3707
|
+
var STATEFUL_BAKE_DEPS = new Set(
|
|
3708
|
+
SUPPORTED_CLAUDESPACE_DEPS.filter((dep) => SERVICE_DEFINITIONS[dep].statefulBake)
|
|
3709
|
+
);
|
|
3710
|
+
var MIRRORABLE_SIDECARS = Object.fromEntries(
|
|
3711
|
+
SUPPORTED_CLAUDESPACE_DEPS.flatMap((dep) => {
|
|
3712
|
+
const mirror = SERVICE_DEFINITIONS[dep].mirror;
|
|
3713
|
+
return mirror ? [[dep, mirror]] : [];
|
|
3714
|
+
})
|
|
3715
|
+
);
|
|
3716
|
+
var LEVELS_PER_BAND = 100;
|
|
3717
|
+
var PRESTIGE_TIERS = ACHIEVEMENT_RARITIES.map((rarity, index) => ({
|
|
3718
|
+
prestige: index + 1,
|
|
3719
|
+
name: rarity.name,
|
|
3720
|
+
color: rarity.color,
|
|
3721
|
+
iconPath: rarity.iconPath,
|
|
3722
|
+
minLevel: (index + 1) * LEVELS_PER_BAND
|
|
3723
|
+
}));
|
|
3724
|
+
var TOP_PRESTIGE_BAND = PRESTIGE_TIERS.length;
|
|
3725
|
+
var POD_PROFILES = ["full", "reader"];
|
|
3726
|
+
var POD_PROFILE_ENV = "CONVEYOR_POD_PROFILE";
|
|
3727
|
+
function podProfileRunsWorkload(profile) {
|
|
3728
|
+
return profile === "full";
|
|
3729
|
+
}
|
|
3730
|
+
function isPodProfile(value) {
|
|
3731
|
+
return typeof value === "string" && POD_PROFILES.includes(value);
|
|
3732
|
+
}
|
|
3733
|
+
var PRE_BUILD_TASK_STATUSES = /* @__PURE__ */ new Set(["Planning", "Open"]);
|
|
3734
|
+
function hasTaskPlan(plan) {
|
|
3735
|
+
return !!plan?.trim();
|
|
3736
|
+
}
|
|
3737
|
+
var CARD_TYPE_SURFACE = {
|
|
3738
|
+
task: "board",
|
|
3739
|
+
chat: "board",
|
|
3740
|
+
incident: "report",
|
|
3741
|
+
suggestion: "report"
|
|
3742
|
+
};
|
|
3743
|
+
var surfaceTypes = (surface) => Object.keys(CARD_TYPE_SURFACE).filter(
|
|
3744
|
+
(type) => CARD_TYPE_SURFACE[type] === surface
|
|
3745
|
+
);
|
|
3746
|
+
var BOARD_CARD_TYPES = surfaceTypes("board");
|
|
3747
|
+
var REPORT_CARD_TYPES = surfaceTypes("report");
|
|
3748
|
+
|
|
3749
|
+
// src/runner/git-run.ts
|
|
3750
|
+
import { execFile } from "child_process";
|
|
3751
|
+
import { promisify } from "util";
|
|
3752
|
+
var execFileAsync = promisify(execFile);
|
|
3753
|
+
var GIT_TIMEOUT_MS = 6e4;
|
|
3754
|
+
var GIT_SLOW_TIMEOUT_MS = 12e4;
|
|
3755
|
+
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
3756
|
+
async function git(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
|
|
3757
|
+
if (workbenchEnabled()) {
|
|
3758
|
+
try {
|
|
3759
|
+
const { stdout: stdout2 } = await getWorkbenchClient().execFile("git", args, {
|
|
3760
|
+
cwd,
|
|
3761
|
+
timeout: timeoutMs,
|
|
3762
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
3763
|
+
});
|
|
3764
|
+
return stdout2.trim();
|
|
3765
|
+
} catch (err) {
|
|
3766
|
+
if (err instanceof Error && err.message.startsWith("Command timed out:")) {
|
|
3767
|
+
err.killed = true;
|
|
3768
|
+
}
|
|
3769
|
+
throw err;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
3773
|
+
cwd,
|
|
3774
|
+
timeout: timeoutMs,
|
|
3775
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
3776
|
+
});
|
|
3777
|
+
return stdout.toString().trim();
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// src/runner/git-credential-ops.ts
|
|
3781
|
+
function credentialErrorText(err) {
|
|
3782
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
3783
|
+
return raw.replace(/\/\/[^@\s]+@/g, "//***@");
|
|
3784
|
+
}
|
|
3785
|
+
async function updateRemoteCredential(cwd, credential) {
|
|
3786
|
+
const result = { ok: false, storeWritten: false, helperConfigured: false };
|
|
3787
|
+
try {
|
|
3788
|
+
const currentUrl = await git(cwd, ["remote", "get-url", "origin"]);
|
|
3789
|
+
const cloneUrl = credential.cloneUrl ?? currentUrl;
|
|
3790
|
+
const normalizedUrl = writeGitCredential(cwd, cloneUrl, credential);
|
|
3791
|
+
result.storeWritten = true;
|
|
3792
|
+
if (currentUrl !== normalizedUrl) {
|
|
3793
|
+
await git(cwd, ["remote", "set-url", "origin", normalizedUrl]);
|
|
3794
|
+
}
|
|
3795
|
+
await git(cwd, ["config", "--local", "credential.helper", gitCredentialHelper(cwd)]);
|
|
3796
|
+
result.helperConfigured = true;
|
|
3797
|
+
result.ok = true;
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
result.error = credentialErrorText(err);
|
|
3800
|
+
}
|
|
3801
|
+
return result;
|
|
3802
|
+
}
|
|
3803
|
+
async function updateRemoteToken(cwd, token) {
|
|
3804
|
+
const username = process.env.CONVEYOR_GIT_USERNAME || "x-access-token";
|
|
3805
|
+
const cloneUrl = process.env.CONVEYOR_GIT_CLONE_URL || void 0;
|
|
3806
|
+
const credential = await updateRemoteCredential(cwd, { username, secret: token, cloneUrl });
|
|
3807
|
+
process.env.CONVEYOR_GIT_SECRET = token;
|
|
3808
|
+
const files = syncGithubTokenFiles(token);
|
|
3809
|
+
return { credential, files };
|
|
3810
|
+
}
|
|
3811
|
+
async function verifyGitCredential(cwd) {
|
|
3812
|
+
try {
|
|
3813
|
+
await git(cwd, ["ls-remote", "--heads", "origin"], 3e4);
|
|
3814
|
+
return { ok: true, outcome: "ok" };
|
|
3815
|
+
} catch (err) {
|
|
3816
|
+
const killed = err.killed === true;
|
|
3817
|
+
return {
|
|
3818
|
+
ok: false,
|
|
3819
|
+
outcome: killed ? "timeout" : "denied",
|
|
3820
|
+
error: credentialErrorText(err)
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
// src/runner/git-utils.ts
|
|
3826
|
+
import { realpathSync } from "fs";
|
|
3827
|
+
|
|
3828
|
+
// src/runner/force-fresh-cooldown.ts
|
|
3829
|
+
var FORCE_FRESH_COOLDOWN_MS = 30 * 60 * 1e3;
|
|
3830
|
+
var blockedUntil = 0;
|
|
3831
|
+
function forceFreshCooldownRemainingMs() {
|
|
3832
|
+
return Math.max(0, blockedUntil - Date.now());
|
|
3833
|
+
}
|
|
3834
|
+
function forceFreshMintBlocked() {
|
|
3835
|
+
return forceFreshCooldownRemainingMs() > 0;
|
|
3836
|
+
}
|
|
3837
|
+
function recordForceFreshFailure() {
|
|
3838
|
+
blockedUntil = Date.now() + FORCE_FRESH_COOLDOWN_MS;
|
|
3839
|
+
}
|
|
3840
|
+
function clearForceFreshCooldown() {
|
|
3841
|
+
blockedUntil = 0;
|
|
3842
|
+
}
|
|
3843
|
+
function forceFreshCooldownNotice() {
|
|
3844
|
+
const minutes = Math.ceil(forceFreshCooldownRemainingMs() / 6e4);
|
|
3845
|
+
return `- the force-fresh retry was SKIPPED: one already failed against this pod, so it is on a ${Math.round(FORCE_FRESH_COOLDOWN_MS / 6e4)}-minute cooldown (${minutes} min left). Re-minting cannot fix a credential the pod cannot serve.`;
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// src/runner/git-utils.ts
|
|
3849
|
+
async function ensureOnTaskBranch(cwd, taskBranch, baseBranch) {
|
|
3850
|
+
if (!taskBranch) return true;
|
|
3851
|
+
try {
|
|
3852
|
+
if (await getCurrentBranch(cwd) === taskBranch) return true;
|
|
3853
|
+
let existsOnOrigin = true;
|
|
3854
|
+
try {
|
|
3855
|
+
await git(cwd, [
|
|
3856
|
+
"fetch",
|
|
3857
|
+
"origin",
|
|
3858
|
+
`+refs/heads/${taskBranch}:refs/remotes/origin/${taskBranch}`
|
|
3859
|
+
]);
|
|
3860
|
+
} catch (err) {
|
|
3861
|
+
if (String(err).includes("couldn't find remote ref")) existsOnOrigin = false;
|
|
3862
|
+
else throw err;
|
|
3863
|
+
}
|
|
3864
|
+
if (existsOnOrigin) {
|
|
3865
|
+
await git(cwd, ["checkout", "-B", taskBranch, `origin/${taskBranch}`], 3e4);
|
|
3866
|
+
process.stderr.write(`[conveyor-agent] Checked out task branch ${taskBranch}
|
|
3867
|
+
`);
|
|
3868
|
+
return true;
|
|
3869
|
+
}
|
|
3870
|
+
if (!baseBranch) {
|
|
3871
|
+
process.stderr.write(
|
|
3872
|
+
`[conveyor-agent] Warning: task branch ${taskBranch} missing on origin and no base branch given
|
|
3873
|
+
`
|
|
3874
|
+
);
|
|
3875
|
+
return false;
|
|
3876
|
+
}
|
|
3877
|
+
await git(cwd, [
|
|
3878
|
+
"fetch",
|
|
3879
|
+
"origin",
|
|
3880
|
+
`+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`
|
|
3881
|
+
]);
|
|
3882
|
+
await git(cwd, ["checkout", "-B", taskBranch, `origin/${baseBranch}`], 3e4);
|
|
3883
|
+
await git(cwd, ["push", "-u", "origin", taskBranch], 3e4);
|
|
3884
|
+
process.stderr.write(
|
|
3885
|
+
`[conveyor-agent] Created task branch ${taskBranch} from origin/${baseBranch} and pushed
|
|
3886
|
+
`
|
|
3887
|
+
);
|
|
3888
|
+
return true;
|
|
3889
|
+
} catch {
|
|
3890
|
+
process.stderr.write(`[conveyor-agent] Warning: ensureOnTaskBranch(${taskBranch}) failed
|
|
3891
|
+
`);
|
|
3892
|
+
return false;
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
async function hasUncommittedChanges(cwd) {
|
|
3896
|
+
const status = await git(cwd, ["status", "--porcelain"], GIT_SLOW_TIMEOUT_MS);
|
|
3897
|
+
return status.length > 0;
|
|
3898
|
+
}
|
|
3899
|
+
async function getCurrentBranch(cwd) {
|
|
3900
|
+
try {
|
|
3901
|
+
const branch = await git(cwd, ["branch", "--show-current"]);
|
|
3902
|
+
return branch || null;
|
|
3903
|
+
} catch {
|
|
3904
|
+
return null;
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
async function hasUnpushedCommits(cwd) {
|
|
3908
|
+
try {
|
|
3909
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
3910
|
+
if (!currentBranch) return false;
|
|
3911
|
+
try {
|
|
3912
|
+
await git(cwd, ["rev-parse", `origin/${currentBranch}`]);
|
|
3913
|
+
} catch {
|
|
3914
|
+
try {
|
|
3915
|
+
await git(cwd, ["rev-parse", "HEAD"]);
|
|
3916
|
+
return true;
|
|
3917
|
+
} catch {
|
|
3918
|
+
return false;
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
const ahead = await git(cwd, [
|
|
3922
|
+
"rev-list",
|
|
3923
|
+
"--count",
|
|
3924
|
+
"HEAD",
|
|
3925
|
+
"--not",
|
|
3926
|
+
`origin/${currentBranch}`
|
|
3927
|
+
]);
|
|
3928
|
+
return parseInt(ahead, 10) > 0;
|
|
3929
|
+
} catch {
|
|
3930
|
+
return false;
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
async function remoteMatchesLocalHead(cwd, branch) {
|
|
3934
|
+
try {
|
|
3935
|
+
const [remote, local] = await Promise.all([
|
|
3936
|
+
git(cwd, ["ls-remote", "origin", `refs/heads/${branch}`], GIT_SLOW_TIMEOUT_MS),
|
|
3937
|
+
git(cwd, ["rev-parse", "HEAD"])
|
|
3938
|
+
]);
|
|
3939
|
+
const remoteSha = remote.split(/\s+/)[0] ?? "";
|
|
3940
|
+
return /^[0-9a-f]{40}$/i.test(remoteSha) && remoteSha === local.trim();
|
|
3941
|
+
} catch {
|
|
3942
|
+
return false;
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
async function stageAndCommit(cwd, message) {
|
|
3946
|
+
try {
|
|
3947
|
+
await git(cwd, ["add", "-A"], GIT_SLOW_TIMEOUT_MS);
|
|
3948
|
+
if (!await hasUncommittedChanges(cwd)) return null;
|
|
3949
|
+
await git(cwd, ["commit", "-m", message], GIT_SLOW_TIMEOUT_MS);
|
|
3950
|
+
return await git(cwd, ["rev-parse", "HEAD"]);
|
|
3951
|
+
} catch {
|
|
3952
|
+
return null;
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
function errLooksLikeAuth(err) {
|
|
3956
|
+
if (err.killed) return true;
|
|
3957
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
3958
|
+
const stdout = err.stdout?.toString() ?? "";
|
|
3959
|
+
const msg = stderr || stdout || (err instanceof Error ? err.message : "");
|
|
3960
|
+
return /authentication|authorization|403|401|token/i.test(msg);
|
|
3961
|
+
}
|
|
3962
|
+
async function tryPush(cwd, branch, skipVerify = false) {
|
|
3963
|
+
const noVerify = skipVerify ? ["--no-verify"] : [];
|
|
3964
|
+
try {
|
|
3965
|
+
await git(cwd, ["push", ...noVerify, "origin", branch], 3e4);
|
|
3966
|
+
return true;
|
|
3967
|
+
} catch (err) {
|
|
3968
|
+
if (errLooksLikeAuth(err)) return false;
|
|
3969
|
+
process.stderr.write(
|
|
3970
|
+
`[conveyor-agent] Plain push of ${branch} failed \u2014 retrying with --force-with-lease
|
|
3971
|
+
`
|
|
3972
|
+
);
|
|
3973
|
+
try {
|
|
3974
|
+
await git(cwd, ["push", ...noVerify, "--force-with-lease", "origin", branch], 3e4);
|
|
3975
|
+
return true;
|
|
3976
|
+
} catch {
|
|
3977
|
+
return false;
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
async function isAuthError(cwd) {
|
|
3982
|
+
try {
|
|
3983
|
+
await git(cwd, ["push", "--dry-run"], 3e4);
|
|
3984
|
+
return false;
|
|
3985
|
+
} catch (err) {
|
|
3986
|
+
return errLooksLikeAuth(err);
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
function wipRefForBranch(branch) {
|
|
3990
|
+
return `conveyor-wip/${branch}`;
|
|
3991
|
+
}
|
|
3992
|
+
var wipRefPushed = /* @__PURE__ */ new Set();
|
|
3993
|
+
var wipRefPreserved = /* @__PURE__ */ new Set();
|
|
3994
|
+
async function createWipSnapshot(cwd, message) {
|
|
3995
|
+
let savedIndexTree;
|
|
3996
|
+
try {
|
|
3997
|
+
savedIndexTree = await git(cwd, ["write-tree"], GIT_SLOW_TIMEOUT_MS);
|
|
3998
|
+
} catch {
|
|
3999
|
+
return null;
|
|
4000
|
+
}
|
|
4001
|
+
try {
|
|
4002
|
+
await git(cwd, ["add", "-A"], GIT_SLOW_TIMEOUT_MS);
|
|
4003
|
+
const sha = await git(cwd, ["stash", "create", message], GIT_SLOW_TIMEOUT_MS);
|
|
4004
|
+
return sha || null;
|
|
4005
|
+
} catch {
|
|
4006
|
+
return null;
|
|
4007
|
+
} finally {
|
|
4008
|
+
try {
|
|
4009
|
+
await git(cwd, ["read-tree", savedIndexTree], GIT_SLOW_TIMEOUT_MS);
|
|
4010
|
+
} catch {
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
async function tryPushRefspec(cwd, refspec, force = false) {
|
|
4015
|
+
try {
|
|
4016
|
+
const forceArgs = force ? ["--force"] : [];
|
|
4017
|
+
await git(cwd, ["push", "--no-verify", ...forceArgs, "origin", refspec], 3e4);
|
|
4018
|
+
return true;
|
|
4019
|
+
} catch {
|
|
4020
|
+
return false;
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
async function refreshRemoteToken(cwd, refreshToken) {
|
|
4024
|
+
if (!refreshToken) return;
|
|
4025
|
+
try {
|
|
4026
|
+
const token = await refreshToken();
|
|
4027
|
+
if (token) {
|
|
4028
|
+
await updateRemoteToken(cwd, token);
|
|
4029
|
+
process.env.GITHUB_TOKEN = token;
|
|
4030
|
+
process.env.GH_TOKEN = token;
|
|
4031
|
+
}
|
|
4032
|
+
} catch {
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
async function restoreWipSnapshot(cwd, branch) {
|
|
4036
|
+
if (!branch) return "none";
|
|
4037
|
+
const ref = wipRefForBranch(branch);
|
|
4038
|
+
try {
|
|
4039
|
+
await git(cwd, ["fetch", "origin", `+refs/heads/${ref}:refs/remotes/origin/${ref}`]);
|
|
4040
|
+
} catch (err) {
|
|
4041
|
+
if (isMissingRefError(err)) return "none";
|
|
4042
|
+
wipRefPreserved.add(cwd);
|
|
4043
|
+
return "failed";
|
|
4044
|
+
}
|
|
4045
|
+
try {
|
|
4046
|
+
const sha = await git(cwd, ["rev-parse", `refs/remotes/origin/${ref}`]);
|
|
4047
|
+
const parent = await git(cwd, ["rev-parse", `${sha}^`]);
|
|
4048
|
+
const head = await git(cwd, ["rev-parse", "HEAD"]);
|
|
4049
|
+
if (parent !== head) {
|
|
4050
|
+
try {
|
|
4051
|
+
await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
|
|
4052
|
+
wipRefPushed.add(cwd);
|
|
4053
|
+
wipRefPreserved.delete(cwd);
|
|
4054
|
+
return "applied";
|
|
4055
|
+
} catch {
|
|
4056
|
+
try {
|
|
4057
|
+
await git(cwd, ["reset", "--merge"], GIT_SLOW_TIMEOUT_MS);
|
|
4058
|
+
} catch {
|
|
4059
|
+
}
|
|
4060
|
+
wipRefPreserved.add(cwd);
|
|
4061
|
+
return "stale";
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
await git(cwd, ["stash", "apply", sha], GIT_SLOW_TIMEOUT_MS);
|
|
4065
|
+
wipRefPushed.add(cwd);
|
|
4066
|
+
wipRefPreserved.delete(cwd);
|
|
4067
|
+
return "applied";
|
|
4068
|
+
} catch {
|
|
4069
|
+
wipRefPreserved.add(cwd);
|
|
4070
|
+
return "failed";
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
function isMissingRefError(err) {
|
|
4074
|
+
const stderr = err.stderr?.toString() ?? "";
|
|
4075
|
+
const msg = stderr || (err instanceof Error ? err.message : String(err));
|
|
4076
|
+
return /couldn't find remote ref|couldn't find remote|no such ref|not our ref/i.test(msg);
|
|
4077
|
+
}
|
|
4078
|
+
async function flushPendingChanges(cwd, opts) {
|
|
4079
|
+
let committed = false;
|
|
4080
|
+
let pushed = false;
|
|
4081
|
+
let hadWork = false;
|
|
4082
|
+
try {
|
|
4083
|
+
const branch = await getCurrentBranch(cwd);
|
|
4084
|
+
if (!branch) return { committed, pushed, hadWork };
|
|
4085
|
+
const dirty = await hasUncommittedChanges(cwd);
|
|
4086
|
+
const unpushed = await hasUnpushedCommits(cwd);
|
|
4087
|
+
if (!dirty && !unpushed) {
|
|
4088
|
+
await dropStaleWipRef(cwd, branch, opts?.refreshToken);
|
|
4089
|
+
return { committed, pushed, hadWork };
|
|
4090
|
+
}
|
|
4091
|
+
hadWork = true;
|
|
4092
|
+
await refreshRemoteToken(cwd, opts?.refreshToken);
|
|
4093
|
+
if (unpushed) {
|
|
4094
|
+
pushed = await pushToOrigin(cwd, opts?.refreshToken);
|
|
4095
|
+
}
|
|
4096
|
+
if (dirty && !wipRefPreserved.has(cwd)) {
|
|
4097
|
+
const message = opts?.wipMessage ?? "WIP: conveyor-agent snapshot";
|
|
4098
|
+
const sha = await createWipSnapshot(cwd, message);
|
|
4099
|
+
if (sha) {
|
|
4100
|
+
committed = await tryPushRefspec(cwd, `${sha}:refs/heads/${wipRefForBranch(branch)}`, true);
|
|
4101
|
+
if (committed) wipRefPushed.add(cwd);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
} catch {
|
|
4105
|
+
}
|
|
4106
|
+
return { committed, pushed, hadWork };
|
|
4107
|
+
}
|
|
4108
|
+
async function dropStaleWipRef(cwd, branch, refreshToken) {
|
|
4109
|
+
if (wipRefPreserved.has(cwd) || !wipRefPushed.has(cwd)) return;
|
|
4110
|
+
await refreshRemoteToken(cwd, refreshToken);
|
|
4111
|
+
if (await tryPushRefspec(cwd, `:refs/heads/${wipRefForBranch(branch)}`)) {
|
|
4112
|
+
wipRefPushed.delete(cwd);
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
async function pushToOrigin(cwd, refreshToken, skipVerify = false) {
|
|
4116
|
+
try {
|
|
4117
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
4118
|
+
if (!currentBranch) return false;
|
|
4119
|
+
if (refreshToken) {
|
|
4120
|
+
try {
|
|
4121
|
+
const token = await refreshToken();
|
|
4122
|
+
if (token) {
|
|
4123
|
+
await updateRemoteToken(cwd, token);
|
|
4124
|
+
process.env.GITHUB_TOKEN = token;
|
|
4125
|
+
process.env.GH_TOKEN = token;
|
|
4126
|
+
}
|
|
4127
|
+
} catch {
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
if (await tryPush(cwd, currentBranch, skipVerify)) {
|
|
4131
|
+
clearForceFreshCooldown();
|
|
4132
|
+
return true;
|
|
4133
|
+
}
|
|
4134
|
+
if (refreshToken && !forceFreshMintBlocked() && await isAuthError(cwd)) {
|
|
4135
|
+
const token = await refreshToken({ forceFresh: true });
|
|
4136
|
+
if (token) {
|
|
4137
|
+
await updateRemoteToken(cwd, token);
|
|
4138
|
+
process.env.GITHUB_TOKEN = token;
|
|
4139
|
+
process.env.GH_TOKEN = token;
|
|
4140
|
+
const pushed = await tryPush(cwd, currentBranch, skipVerify);
|
|
4141
|
+
if (pushed) clearForceFreshCooldown();
|
|
4142
|
+
else recordForceFreshFailure();
|
|
4143
|
+
return pushed;
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
return false;
|
|
4147
|
+
} catch {
|
|
4148
|
+
return false;
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
function branchBackupRef(branch) {
|
|
4152
|
+
return `conveyor-wip/branches/${branch}`;
|
|
4153
|
+
}
|
|
4154
|
+
async function listWorktrees(cwd) {
|
|
4155
|
+
try {
|
|
4156
|
+
const out = await git(cwd, ["worktree", "list", "--porcelain"]);
|
|
4157
|
+
const result = [];
|
|
4158
|
+
for (const entry of out.split("\n\n")) {
|
|
4159
|
+
const lines = entry.trim().split("\n");
|
|
4160
|
+
const wl = lines.find((l) => l.startsWith("worktree "));
|
|
4161
|
+
if (!wl) continue;
|
|
4162
|
+
const bl = lines.find((l) => l.startsWith("branch "));
|
|
4163
|
+
result.push({
|
|
4164
|
+
path: wl.slice("worktree ".length),
|
|
4165
|
+
branch: bl ? bl.slice("branch refs/heads/".length) : null
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
return result;
|
|
4169
|
+
} catch {
|
|
4170
|
+
return [];
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
async function listLocalBranches(cwd) {
|
|
4174
|
+
try {
|
|
4175
|
+
const out = await git(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]);
|
|
4176
|
+
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
4177
|
+
} catch {
|
|
4178
|
+
return [];
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
async function branchUnpushedCount(cwd, branch) {
|
|
4182
|
+
try {
|
|
4183
|
+
const n = await git(cwd, ["rev-list", "--count", branch, "--not", "--remotes=origin"]);
|
|
4184
|
+
return Number.parseInt(n, 10) || 0;
|
|
4185
|
+
} catch {
|
|
4186
|
+
return 0;
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
function samePath(a, b) {
|
|
4190
|
+
try {
|
|
4191
|
+
return realpathSync(a) === realpathSync(b);
|
|
4192
|
+
} catch {
|
|
4193
|
+
return a === b;
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
async function flushAllPendingWork(cwd, opts) {
|
|
4197
|
+
try {
|
|
4198
|
+
const primary = await flushPendingChanges(cwd, opts);
|
|
4199
|
+
await refreshRemoteToken(cwd, opts?.refreshToken);
|
|
4200
|
+
const currentBranch = await getCurrentBranch(cwd);
|
|
4201
|
+
const worktreesSnapshotted = await snapshotOtherWorktrees(cwd, opts?.wipMessage);
|
|
4202
|
+
const branchesBackedUp = await backupOtherBranches(cwd, currentBranch);
|
|
4203
|
+
return {
|
|
4204
|
+
hadWork: primary.hadWork || worktreesSnapshotted > 0 || branchesBackedUp > 0,
|
|
4205
|
+
branchesBackedUp,
|
|
4206
|
+
worktreesSnapshotted
|
|
4207
|
+
};
|
|
4208
|
+
} catch {
|
|
4209
|
+
return { hadWork: false, branchesBackedUp: 0, worktreesSnapshotted: 0 };
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
async function snapshotOtherWorktrees(cwd, wipMessage) {
|
|
4213
|
+
let count = 0;
|
|
4214
|
+
for (const wt of await listWorktrees(cwd)) {
|
|
4215
|
+
if (samePath(wt.path, cwd) || !wt.branch) continue;
|
|
4216
|
+
try {
|
|
4217
|
+
if (!await hasUncommittedChanges(wt.path)) continue;
|
|
4218
|
+
const sha = await createWipSnapshot(wt.path, wipMessage ?? "WIP: conveyor-agent snapshot");
|
|
4219
|
+
if (sha && await tryPushRefspec(wt.path, `${sha}:refs/heads/${wipRefForBranch(wt.branch)}`, true)) {
|
|
4220
|
+
wipRefPushed.add(wt.path);
|
|
4221
|
+
count++;
|
|
4222
|
+
}
|
|
4223
|
+
} catch {
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
return count;
|
|
4227
|
+
}
|
|
4228
|
+
async function backupOtherBranches(cwd, currentBranch) {
|
|
4229
|
+
let count = 0;
|
|
4230
|
+
for (const branch of await listLocalBranches(cwd)) {
|
|
4231
|
+
if (branch === currentBranch || branch.startsWith("conveyor-wip/")) continue;
|
|
4232
|
+
try {
|
|
4233
|
+
if (await branchUnpushedCount(cwd, branch) === 0) continue;
|
|
4234
|
+
if (await tryPushRefspec(
|
|
4235
|
+
cwd,
|
|
4236
|
+
`refs/heads/${branch}:refs/heads/${branchBackupRef(branch)}`,
|
|
4237
|
+
true
|
|
4238
|
+
)) {
|
|
4239
|
+
count++;
|
|
4240
|
+
}
|
|
4241
|
+
} catch {
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
return count;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
// src/setup/config.ts
|
|
4248
|
+
import { join } from "path";
|
|
4249
|
+
|
|
4250
|
+
// src/workbench/fs.ts
|
|
4251
|
+
import {
|
|
4252
|
+
readFile as localReadFile,
|
|
4253
|
+
readdir as localReaddir,
|
|
4254
|
+
stat as localStat
|
|
4255
|
+
} from "fs/promises";
|
|
4256
|
+
async function readWorkspaceFile(path) {
|
|
4257
|
+
if (workbenchEnabled()) {
|
|
4258
|
+
return (await getWorkbenchClient().readFile(path)).toString("utf8");
|
|
4259
|
+
}
|
|
4260
|
+
return localReadFile(path, "utf-8");
|
|
4261
|
+
}
|
|
4262
|
+
function readWorkspaceBytes(path) {
|
|
4263
|
+
if (workbenchEnabled()) return getWorkbenchClient().readFile(path);
|
|
4264
|
+
return localReadFile(path);
|
|
4265
|
+
}
|
|
4266
|
+
function readWorkspaceDir(path) {
|
|
4267
|
+
if (workbenchEnabled()) return getWorkbenchClient().readdir(path);
|
|
4268
|
+
return localReaddir(path);
|
|
4269
|
+
}
|
|
4270
|
+
async function statWorkspacePath(path) {
|
|
4271
|
+
if (workbenchEnabled()) return getWorkbenchClient().stat(path);
|
|
4272
|
+
try {
|
|
4273
|
+
const s = await localStat(path);
|
|
4274
|
+
return {
|
|
4275
|
+
exists: true,
|
|
4276
|
+
isFile: s.isFile(),
|
|
4277
|
+
isDirectory: s.isDirectory(),
|
|
4278
|
+
size: s.size,
|
|
4279
|
+
mtimeMs: s.mtimeMs
|
|
4280
|
+
};
|
|
4281
|
+
} catch {
|
|
4282
|
+
return { exists: false, isFile: false, isDirectory: false, size: 0, mtimeMs: 0 };
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
async function workspacePathExists(path) {
|
|
4286
|
+
return (await statWorkspacePath(path)).exists;
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
// src/setup/config.ts
|
|
4290
|
+
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
4291
|
+
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
4292
|
+
async function loadForwardPorts(workspaceDir) {
|
|
4293
|
+
try {
|
|
4294
|
+
const raw = await readWorkspaceFile(join(workspaceDir, DEVCONTAINER_PATH));
|
|
4295
|
+
const parsed = JSON.parse(raw);
|
|
4296
|
+
const ports = (parsed.forwardPorts ?? []).filter(
|
|
4297
|
+
(p) => typeof p === "number" && !DEVCONTAINER_PORT_DENY_LIST.has(p)
|
|
4298
|
+
);
|
|
4299
|
+
const attributes = {};
|
|
4300
|
+
for (const [key, value] of Object.entries(parsed.portsAttributes ?? {})) {
|
|
4301
|
+
if (!value || typeof value !== "object") continue;
|
|
4302
|
+
const entry = {};
|
|
4303
|
+
if (typeof value.label === "string") entry.label = value.label;
|
|
4304
|
+
if (value.visibility === "public" || value.visibility === "private") {
|
|
4305
|
+
entry.visibility = value.visibility;
|
|
4306
|
+
}
|
|
4307
|
+
attributes[key] = entry;
|
|
4308
|
+
}
|
|
4309
|
+
return { ports, attributes };
|
|
4310
|
+
} catch {
|
|
4311
|
+
return { ports: [], attributes: {} };
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function buildSessionPreviewPorts(result) {
|
|
4315
|
+
return result.ports.filter((port) => !DEVCONTAINER_PORT_DENY_LIST.has(port)).map((port) => {
|
|
4316
|
+
const attr = result.attributes[String(port)];
|
|
4317
|
+
const entry = { port };
|
|
4318
|
+
if (attr?.label) entry.label = attr.label;
|
|
4319
|
+
if (attr?.visibility) entry.visibility = attr.visibility;
|
|
4320
|
+
return entry;
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4323
|
+
function loadConveyorConfig() {
|
|
4324
|
+
const envStart = process.env.CONVEYOR_START_COMMAND;
|
|
4325
|
+
if (envStart) {
|
|
4326
|
+
return { startCommand: envStart };
|
|
4327
|
+
}
|
|
4328
|
+
return null;
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
// src/runner/lifecycle.ts
|
|
4332
|
+
var DEFAULT_LIFECYCLE_CONFIG = {
|
|
4333
|
+
idleTimeoutMs: 30 * 60 * 1e3,
|
|
4334
|
+
dormantTimeoutMs: 60 * 60 * 1e3,
|
|
4335
|
+
heartbeatIntervalMs: 3e4,
|
|
4336
|
+
tokenRefreshIntervalMs: 45 * 60 * 1e3,
|
|
4337
|
+
gitFlushIntervalMs: 2 * 60 * 1e3,
|
|
4338
|
+
usageSampleIntervalMs: 5 * 60 * 1e3,
|
|
4339
|
+
usageSampleInitialDelayMs: 3e4
|
|
4340
|
+
};
|
|
4341
|
+
var Lifecycle = class {
|
|
4342
|
+
config;
|
|
4343
|
+
callbacks;
|
|
4344
|
+
heartbeatTimer = null;
|
|
4345
|
+
tokenRefreshTimer = null;
|
|
4346
|
+
idleTimer = null;
|
|
4347
|
+
idleCheckInterval = null;
|
|
4348
|
+
dormantTimer = null;
|
|
4349
|
+
gitFlushTimer = null;
|
|
4350
|
+
usageSampleTimer = null;
|
|
4351
|
+
constructor(config, callbacks) {
|
|
4352
|
+
this.config = config;
|
|
4353
|
+
this.callbacks = callbacks;
|
|
4354
|
+
}
|
|
4355
|
+
// ── Heartbeat ──────────────────────────────────────────────────────
|
|
4356
|
+
startHeartbeat() {
|
|
4357
|
+
this.stopHeartbeat();
|
|
4358
|
+
this.heartbeatTimer = setInterval(() => {
|
|
4359
|
+
this.callbacks.onHeartbeat();
|
|
4360
|
+
}, this.config.heartbeatIntervalMs);
|
|
4361
|
+
}
|
|
4362
|
+
stopHeartbeat() {
|
|
4363
|
+
if (this.heartbeatTimer) {
|
|
4364
|
+
clearInterval(this.heartbeatTimer);
|
|
4365
|
+
this.heartbeatTimer = null;
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
// ── Token refresh ─────────────────────────────────────────────────
|
|
4369
|
+
startTokenRefresh() {
|
|
4370
|
+
this.stopTokenRefresh();
|
|
4371
|
+
this.callbacks.onTokenRefresh();
|
|
4372
|
+
this.tokenRefreshTimer = setInterval(() => {
|
|
4373
|
+
this.callbacks.onTokenRefresh();
|
|
4374
|
+
}, this.config.tokenRefreshIntervalMs);
|
|
4375
|
+
}
|
|
4376
|
+
stopTokenRefresh() {
|
|
4377
|
+
if (this.tokenRefreshTimer) {
|
|
4378
|
+
clearInterval(this.tokenRefreshTimer);
|
|
4379
|
+
this.tokenRefreshTimer = null;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
// ── Periodic git flush ────────────────────────────────────────────
|
|
4383
|
+
startGitFlush() {
|
|
4384
|
+
this.stopGitFlush();
|
|
4385
|
+
if (this.config.gitFlushIntervalMs <= 0) return;
|
|
4386
|
+
this.gitFlushTimer = setInterval(() => {
|
|
4387
|
+
this.callbacks.onGitFlush();
|
|
4388
|
+
}, this.config.gitFlushIntervalMs);
|
|
4389
|
+
}
|
|
4390
|
+
stopGitFlush() {
|
|
4391
|
+
if (this.gitFlushTimer) {
|
|
4392
|
+
clearInterval(this.gitFlushTimer);
|
|
4393
|
+
this.gitFlushTimer = null;
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
// ── Claude key usage sampling ─────────────────────────────────────
|
|
4397
|
+
startUsageSample() {
|
|
4398
|
+
this.stopUsageSample();
|
|
4399
|
+
if (this.config.usageSampleIntervalMs <= 0) return;
|
|
4400
|
+
this.usageSampleTimer = setTimeout(() => {
|
|
4401
|
+
this.callbacks.onUsageSample();
|
|
4402
|
+
this.usageSampleTimer = setInterval(() => {
|
|
4403
|
+
this.callbacks.onUsageSample();
|
|
4404
|
+
}, this.config.usageSampleIntervalMs);
|
|
4405
|
+
}, this.config.usageSampleInitialDelayMs);
|
|
4406
|
+
}
|
|
4407
|
+
stopUsageSample() {
|
|
4408
|
+
if (this.usageSampleTimer) {
|
|
4409
|
+
clearInterval(this.usageSampleTimer);
|
|
4410
|
+
this.usageSampleTimer = null;
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
// ── Idle timer ─────────────────────────────────────────────────────
|
|
4414
|
+
/** Start (or restart) the idle timer.
|
|
4415
|
+
* @param overrideMs Optional custom delay in ms, mirroring
|
|
4416
|
+
* `startDormantTimer`. SessionRunner passes a short delay when it DEFERS a
|
|
4417
|
+
* shutdown because a spawned child is still working: the pod must re-check
|
|
4418
|
+
* soon after that child exits, rather than granting itself a fresh full idle
|
|
4419
|
+
* window every time it defers. */
|
|
4420
|
+
startIdleTimer(overrideMs) {
|
|
4421
|
+
this.clearIdleTimers();
|
|
4422
|
+
const delay2 = Math.max(0, overrideMs ?? this.config.idleTimeoutMs);
|
|
4423
|
+
this.idleTimer = setTimeout(() => {
|
|
4424
|
+
this.callbacks.onIdleTimeout();
|
|
4425
|
+
}, delay2);
|
|
4426
|
+
}
|
|
4427
|
+
cancelIdleTimer() {
|
|
4428
|
+
this.clearIdleTimers();
|
|
4429
|
+
}
|
|
4430
|
+
// ── Dormant timer ──────────────────────────────────────────────────
|
|
4431
|
+
/** Start (or restart) the dormant timer.
|
|
4432
|
+
* @param overrideMs Optional custom delay in ms. When provided, the timer
|
|
4433
|
+
* fires after exactly that delay instead of `dormantTimeoutMs`. SessionRunner
|
|
4434
|
+
* uses this to enforce an *absolute* deadline across cycles: even if the
|
|
4435
|
+
* dormant wait is interrupted by an inbound message, the next iteration
|
|
4436
|
+
* passes the remaining time, so the agent shuts down at the original
|
|
4437
|
+
* deadline regardless of message volume. */
|
|
4438
|
+
startDormantTimer(overrideMs) {
|
|
4439
|
+
this.cancelDormantTimer();
|
|
4440
|
+
const delay2 = Math.max(0, overrideMs ?? this.config.dormantTimeoutMs);
|
|
4441
|
+
this.dormantTimer = setTimeout(() => {
|
|
4442
|
+
this.callbacks.onDormantTimeout();
|
|
4443
|
+
}, delay2);
|
|
4444
|
+
}
|
|
4445
|
+
cancelDormantTimer() {
|
|
4446
|
+
if (this.dormantTimer) {
|
|
4447
|
+
clearTimeout(this.dormantTimer);
|
|
4448
|
+
this.dormantTimer = null;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
// ── Cleanup ────────────────────────────────────────────────────────
|
|
4452
|
+
destroy() {
|
|
4453
|
+
this.stopHeartbeat();
|
|
4454
|
+
this.stopTokenRefresh();
|
|
4455
|
+
this.stopGitFlush();
|
|
4456
|
+
this.stopUsageSample();
|
|
4457
|
+
this.clearIdleTimers();
|
|
4458
|
+
this.cancelDormantTimer();
|
|
4459
|
+
}
|
|
4460
|
+
// ── Private ────────────────────────────────────────────────────────
|
|
4461
|
+
clearIdleTimers() {
|
|
4462
|
+
if (this.idleTimer) {
|
|
4463
|
+
clearTimeout(this.idleTimer);
|
|
4464
|
+
this.idleTimer = null;
|
|
4465
|
+
}
|
|
4466
|
+
if (this.idleCheckInterval) {
|
|
4467
|
+
clearInterval(this.idleCheckInterval);
|
|
4468
|
+
this.idleCheckInterval = null;
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
};
|
|
4472
|
+
|
|
4473
|
+
// src/setup/git-ready.ts
|
|
4474
|
+
var GATE_MARGIN_MS = 5 * 6e4;
|
|
4475
|
+
var DEFAULT_TIMEOUT_MS = GIT_PREP_MAX_RETRIES * (CLONE_TIMEOUT_MS + FETCH_TIMEOUT_MS) + (GIT_PREP_MAX_RETRIES - 1) * DEFAULT_RETRY_DELAY_MS + GATE_MARGIN_MS;
|
|
4476
|
+
var DEFAULT_POLL_MS = 200;
|
|
4477
|
+
function awaitGitReady(opts = {}) {
|
|
4478
|
+
if (!workbenchEnabled()) {
|
|
4479
|
+
return Promise.resolve("not-gated");
|
|
4480
|
+
}
|
|
4481
|
+
const clientFn = opts.clientFn ?? getWorkbenchClient;
|
|
4482
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4483
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
4484
|
+
opts.onLog?.("waiting for workspace git (workbench daemon)");
|
|
4485
|
+
return pollDaemon(clientFn, timeoutMs, pollMs, opts.onLog, opts.signal);
|
|
4486
|
+
}
|
|
4487
|
+
function delay(ms, signal) {
|
|
4488
|
+
if (!signal) {
|
|
4489
|
+
return new Promise((resolve) => {
|
|
4490
|
+
setTimeout(resolve, ms);
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
if (signal.aborted) return Promise.resolve();
|
|
4494
|
+
return new Promise((resolve) => {
|
|
4495
|
+
const timer = setTimeout(() => {
|
|
4496
|
+
signal.removeEventListener("abort", onAbort);
|
|
4497
|
+
resolve();
|
|
4498
|
+
}, ms);
|
|
4499
|
+
const onAbort = () => {
|
|
4500
|
+
clearTimeout(timer);
|
|
4501
|
+
resolve();
|
|
4502
|
+
};
|
|
4503
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
async function pollOnce(clientFn, reportError) {
|
|
4507
|
+
try {
|
|
4508
|
+
const frame = await clientFn().gitStatus();
|
|
4509
|
+
if (frame.state === "ready") return { state: "ready", log: "workspace git ready" };
|
|
4510
|
+
if (frame.state === "failed") {
|
|
4511
|
+
return {
|
|
4512
|
+
state: "failed",
|
|
4513
|
+
log: `workspace git preparation failed: ${frame.reason ?? "unknown reason"}`
|
|
4514
|
+
};
|
|
4515
|
+
}
|
|
4516
|
+
return { state: null };
|
|
4517
|
+
} catch (err) {
|
|
4518
|
+
if (err instanceof WorkbenchError && err.code === "unauthorized") {
|
|
4519
|
+
return {
|
|
4520
|
+
state: "failed",
|
|
4521
|
+
log: "workspace git gate unauthorized \u2014 workbench token missing or invalid, giving up"
|
|
4522
|
+
};
|
|
4523
|
+
}
|
|
4524
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4525
|
+
return reportError ? { state: null, log: `workspace git poll error (retrying): ${message}` } : { state: null };
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
async function pollDaemon(clientFn, timeoutMs, pollMs, onLog, signal) {
|
|
4529
|
+
const deadline = Date.now() + timeoutMs;
|
|
4530
|
+
let loggedError = false;
|
|
4531
|
+
for (; ; ) {
|
|
4532
|
+
if (signal?.aborted) {
|
|
4533
|
+
onLog?.("workspace git wait aborted \u2014 giving up");
|
|
4534
|
+
return "timeout";
|
|
4535
|
+
}
|
|
4536
|
+
const outcome = await pollOnce(clientFn, !loggedError);
|
|
4537
|
+
if (outcome.log !== void 0) {
|
|
4538
|
+
loggedError = true;
|
|
4539
|
+
onLog?.(outcome.log);
|
|
4540
|
+
}
|
|
4541
|
+
if (outcome.state !== null) return outcome.state;
|
|
4542
|
+
if (Date.now() >= deadline) {
|
|
4543
|
+
onLog?.(`workspace git not ready after ${timeoutMs}ms \u2014 giving up`);
|
|
4544
|
+
return "timeout";
|
|
4545
|
+
}
|
|
4546
|
+
await delay(pollMs, signal);
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
// src/runner/port-discovery.ts
|
|
4551
|
+
import { readFile } from "fs/promises";
|
|
4552
|
+
import { execFile as execFile2 } from "child_process";
|
|
4553
|
+
var PROC_TCP_LISTEN_STATE = "0A";
|
|
4554
|
+
function isLoopbackHexAddress(hex) {
|
|
4555
|
+
const addr = hex.toUpperCase();
|
|
4556
|
+
if (addr.length === 8) {
|
|
4557
|
+
return addr.slice(6, 8) === "7F";
|
|
4558
|
+
}
|
|
4559
|
+
if (addr.length === 32) {
|
|
4560
|
+
if (addr === "00000000000000000000000001000000") return true;
|
|
4561
|
+
if (addr.slice(0, 16) === "0000000000000000" && addr.slice(16, 24) === "FFFF0000") {
|
|
4562
|
+
return addr.slice(30, 32) === "7F";
|
|
4563
|
+
}
|
|
4564
|
+
return false;
|
|
4565
|
+
}
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
function parseProcNetTcpListeners(content) {
|
|
4569
|
+
const sockets = [];
|
|
4570
|
+
const lines = content.split("\n");
|
|
4571
|
+
for (let i = 1; i < lines.length; i++) {
|
|
4572
|
+
const line = lines[i];
|
|
4573
|
+
if (!line) continue;
|
|
4574
|
+
const cols = line.trim().split(/\s+/);
|
|
4575
|
+
if (cols.length < 4 || cols[3] !== PROC_TCP_LISTEN_STATE) continue;
|
|
4576
|
+
const local = cols[1];
|
|
4577
|
+
if (!local) continue;
|
|
4578
|
+
const [addrHex, portHex] = local.split(":");
|
|
4579
|
+
if (!addrHex || !portHex) continue;
|
|
4580
|
+
const port = Number.parseInt(portHex, 16);
|
|
4581
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
|
|
4582
|
+
sockets.push({ port, loopback: isLoopbackHexAddress(addrHex) });
|
|
4583
|
+
}
|
|
4584
|
+
return sockets;
|
|
4585
|
+
}
|
|
4586
|
+
function collectScan(sockets) {
|
|
4587
|
+
const ports = /* @__PURE__ */ new Set();
|
|
4588
|
+
const hasExternal = /* @__PURE__ */ new Set();
|
|
4589
|
+
for (const { port, loopback } of sockets) {
|
|
4590
|
+
ports.add(port);
|
|
4591
|
+
if (!loopback) hasExternal.add(port);
|
|
4592
|
+
}
|
|
4593
|
+
const loopbackOnly = /* @__PURE__ */ new Set();
|
|
4594
|
+
for (const port of ports) {
|
|
4595
|
+
if (!hasExternal.has(port)) loopbackOnly.add(port);
|
|
4596
|
+
}
|
|
4597
|
+
return { ports, loopbackOnly };
|
|
4598
|
+
}
|
|
4599
|
+
var DEFAULT_PROC_PATHS = ["/proc/net/tcp", "/proc/net/tcp6"];
|
|
4600
|
+
async function readProcListeningPorts(procPaths = DEFAULT_PROC_PATHS) {
|
|
4601
|
+
const sockets = [];
|
|
4602
|
+
let readable = false;
|
|
4603
|
+
for (const path of procPaths) {
|
|
4604
|
+
try {
|
|
4605
|
+
const content = await readFile(path, "utf8");
|
|
4606
|
+
readable = true;
|
|
4607
|
+
sockets.push(...parseProcNetTcpListeners(content));
|
|
4608
|
+
} catch {
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
return readable ? collectScan(sockets) : null;
|
|
4612
|
+
}
|
|
4613
|
+
async function readNetstatListeningPorts() {
|
|
4614
|
+
const output = await new Promise((resolve) => {
|
|
4615
|
+
execFile2("netstat", ["-an", "-p", "tcp"], { timeout: 5e3 }, (err, stdout) => {
|
|
4616
|
+
resolve(err ? null : stdout);
|
|
4617
|
+
});
|
|
4618
|
+
});
|
|
4619
|
+
if (output === null) return null;
|
|
4620
|
+
const sockets = [];
|
|
4621
|
+
for (const line of output.split("\n")) {
|
|
4622
|
+
if (!line.includes("LISTEN")) continue;
|
|
4623
|
+
const cols = line.trim().split(/\s+/);
|
|
4624
|
+
const local = cols[3];
|
|
4625
|
+
if (!local) continue;
|
|
4626
|
+
const lastDot = local.lastIndexOf(".");
|
|
4627
|
+
if (lastDot < 0) continue;
|
|
4628
|
+
const host = local.slice(0, lastDot);
|
|
4629
|
+
const port = Number(local.slice(lastDot + 1));
|
|
4630
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
|
|
4631
|
+
const loopback = host.startsWith("127.") || host === "::1" || host === "localhost";
|
|
4632
|
+
sockets.push({ port, loopback });
|
|
4633
|
+
}
|
|
4634
|
+
return collectScan(sockets);
|
|
4635
|
+
}
|
|
4636
|
+
async function readListeningPorts() {
|
|
4637
|
+
const proc = await readProcListeningPorts();
|
|
4638
|
+
if (proc !== null) return proc;
|
|
4639
|
+
if (process.platform !== "linux") return readNetstatListeningPorts();
|
|
4640
|
+
return null;
|
|
4641
|
+
}
|
|
4642
|
+
var DEFAULT_EXCLUDED_PORTS = [2222, 5432, 6379, 9200];
|
|
4643
|
+
var DEFAULT_EPHEMERAL_PORT_MIN = 32768;
|
|
4644
|
+
var DEFAULT_DISCOVERY_INTERVAL_MS = 15e3;
|
|
4645
|
+
var DEFAULT_MAX_PORTS = 16;
|
|
4646
|
+
var CONFIRM_SCANS = 2;
|
|
4647
|
+
var PortDiscovery = class {
|
|
4648
|
+
opts;
|
|
4649
|
+
intervalMs;
|
|
4650
|
+
maxPorts;
|
|
4651
|
+
excluded;
|
|
4652
|
+
ephemeralPortMin;
|
|
4653
|
+
scan;
|
|
4654
|
+
now;
|
|
4655
|
+
log;
|
|
4656
|
+
baseline = null;
|
|
4657
|
+
tracked = /* @__PURE__ */ new Map();
|
|
4658
|
+
/** Loopback-only candidates already warned about (once per port). */
|
|
4659
|
+
warnedLoopback = /* @__PURE__ */ new Set();
|
|
4660
|
+
timer = null;
|
|
4661
|
+
ticking = false;
|
|
4662
|
+
disabled = false;
|
|
4663
|
+
stopped = false;
|
|
4664
|
+
/** Set when the confirmed set changed (or a report failed) — cleared only
|
|
4665
|
+
* after a successful report, so transient RPC failures retry next tick. */
|
|
4666
|
+
reportPending = false;
|
|
4667
|
+
lastReportedKey = "";
|
|
4668
|
+
constructor(options) {
|
|
4669
|
+
this.opts = options;
|
|
4670
|
+
this.intervalMs = options.intervalMs ?? DEFAULT_DISCOVERY_INTERVAL_MS;
|
|
4671
|
+
this.maxPorts = options.maxPorts ?? DEFAULT_MAX_PORTS;
|
|
4672
|
+
this.excluded = new Set(options.excludedPorts ?? DEFAULT_EXCLUDED_PORTS);
|
|
4673
|
+
this.ephemeralPortMin = options.ephemeralPortMin ?? DEFAULT_EPHEMERAL_PORT_MIN;
|
|
4674
|
+
this.scan = options.scan ?? readListeningPorts;
|
|
4675
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4676
|
+
this.log = options.log ?? ((m) => process.stderr.write(`[conveyor-agent] ${m}
|
|
4677
|
+
`));
|
|
4678
|
+
}
|
|
4679
|
+
/** Take the baseline scan and start polling. Safe to call once. */
|
|
4680
|
+
async start() {
|
|
4681
|
+
if (this.timer || this.disabled || this.stopped) return;
|
|
4682
|
+
const baseline = await this.scanSafe();
|
|
4683
|
+
if (this.stopped) return;
|
|
4684
|
+
if (baseline === null) {
|
|
4685
|
+
this.disabled = true;
|
|
4686
|
+
this.log("Port discovery disabled: no listening-socket source available");
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
this.baseline = baseline.ports;
|
|
4690
|
+
this.timer = setInterval(() => void this.tick(), this.intervalMs);
|
|
4691
|
+
this.timer.unref?.();
|
|
4692
|
+
}
|
|
4693
|
+
stop() {
|
|
4694
|
+
this.stopped = true;
|
|
4695
|
+
if (this.timer) {
|
|
4696
|
+
clearInterval(this.timer);
|
|
4697
|
+
this.timer = null;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
/** One poll cycle. Exposed for tests (deterministic, no timers needed). */
|
|
4701
|
+
async tick() {
|
|
4702
|
+
if (this.ticking || this.disabled || !this.baseline) return;
|
|
4703
|
+
this.ticking = true;
|
|
4704
|
+
try {
|
|
4705
|
+
const current = await this.scanSafe();
|
|
4706
|
+
if (current === null) return;
|
|
4707
|
+
this.updateTracking(current);
|
|
4708
|
+
if (this.reportPending) await this.flushReport();
|
|
4709
|
+
} finally {
|
|
4710
|
+
this.ticking = false;
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
async scanSafe() {
|
|
4714
|
+
try {
|
|
4715
|
+
return await this.scan();
|
|
4716
|
+
} catch {
|
|
4717
|
+
return null;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
isCandidate(port) {
|
|
4721
|
+
if (this.baseline?.has(port)) return false;
|
|
4722
|
+
if (this.excluded.has(port)) return false;
|
|
4723
|
+
if (port >= this.ephemeralPortMin) return false;
|
|
4724
|
+
return true;
|
|
4725
|
+
}
|
|
4726
|
+
updateTracking(current) {
|
|
4727
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
4728
|
+
for (const port of current.ports) {
|
|
4729
|
+
if (!this.isCandidate(port)) continue;
|
|
4730
|
+
if (current.loopbackOnly.has(port)) {
|
|
4731
|
+
if (!this.warnedLoopback.has(port)) {
|
|
4732
|
+
this.warnedLoopback.add(port);
|
|
4733
|
+
this.log(
|
|
4734
|
+
`Port ${port} is listening on loopback only and cannot be previewed \u2014 bind 0.0.0.0 (or the pod IP) to make it reachable through the preview proxy`
|
|
4735
|
+
);
|
|
4736
|
+
}
|
|
4737
|
+
continue;
|
|
4738
|
+
}
|
|
4739
|
+
reachable.add(port);
|
|
4740
|
+
}
|
|
4741
|
+
for (const port of reachable) {
|
|
4742
|
+
const entry = this.tracked.get(port);
|
|
4743
|
+
if (!entry) {
|
|
4744
|
+
this.tracked.set(port, { seen: 1, missed: 0, confirmed: false, detectedAt: "" });
|
|
4745
|
+
continue;
|
|
4746
|
+
}
|
|
4747
|
+
entry.seen += 1;
|
|
4748
|
+
entry.missed = 0;
|
|
4749
|
+
if (!entry.confirmed && entry.seen >= CONFIRM_SCANS) {
|
|
4750
|
+
entry.confirmed = true;
|
|
4751
|
+
entry.detectedAt = this.now().toISOString();
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
for (const [port, entry] of this.tracked) {
|
|
4755
|
+
if (reachable.has(port)) continue;
|
|
4756
|
+
entry.missed += 1;
|
|
4757
|
+
entry.seen = 0;
|
|
4758
|
+
if (entry.missed >= CONFIRM_SCANS || !entry.confirmed) this.tracked.delete(port);
|
|
4759
|
+
}
|
|
4760
|
+
const key = this.confirmedKey();
|
|
4761
|
+
if (key !== this.lastReportedKey) this.reportPending = true;
|
|
4762
|
+
}
|
|
4763
|
+
confirmedPorts() {
|
|
4764
|
+
const confirmed = [...this.tracked.entries()].filter(([, entry]) => entry.confirmed).sort(([a], [b]) => a - b).slice(0, this.maxPorts);
|
|
4765
|
+
return confirmed.map(([port, entry]) => ({
|
|
4766
|
+
port,
|
|
4767
|
+
protocol: "tcp",
|
|
4768
|
+
detectedAt: entry.detectedAt
|
|
4769
|
+
}));
|
|
4770
|
+
}
|
|
4771
|
+
confirmedKey() {
|
|
4772
|
+
return this.confirmedPorts().map(({ port }) => port).join(",");
|
|
4773
|
+
}
|
|
4774
|
+
async flushReport() {
|
|
4775
|
+
const ports = this.confirmedPorts();
|
|
4776
|
+
const key = ports.map(({ port }) => port).join(",");
|
|
4777
|
+
try {
|
|
4778
|
+
await this.opts.report(ports);
|
|
4779
|
+
this.lastReportedKey = key;
|
|
4780
|
+
this.reportPending = false;
|
|
4781
|
+
this.log(`Discovered preview ports: [${key || "none"}]`);
|
|
4782
|
+
} catch {
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4785
|
+
try {
|
|
4786
|
+
await this.opts.onReported?.(ports);
|
|
4787
|
+
} catch {
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
};
|
|
4791
|
+
|
|
4792
|
+
// src/runner/codespace-port-visibility.ts
|
|
4793
|
+
import { execFile as execFile3 } from "child_process";
|
|
4794
|
+
var GH_TIMEOUT_MS = 15e3;
|
|
4795
|
+
var VISIBILITIES = ["org", "public"];
|
|
4796
|
+
function runGh(args) {
|
|
4797
|
+
return new Promise((resolve) => {
|
|
4798
|
+
execFile3("gh", [...args], { timeout: GH_TIMEOUT_MS }, (error, _stdout, stderr) => {
|
|
4799
|
+
resolve({ ok: !error, stderr: (stderr || (error ? String(error.message) : "")).trim() });
|
|
4800
|
+
});
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4803
|
+
function isCodespaceEnvironment(env = process.env) {
|
|
4804
|
+
return env.CODESPACES === "true" && !!env.CODESPACE_NAME;
|
|
4805
|
+
}
|
|
4806
|
+
var CodespacePortVisibility = class {
|
|
4807
|
+
env;
|
|
4808
|
+
run;
|
|
4809
|
+
log;
|
|
4810
|
+
/** Ports already attempted (success or failure) — one try per process. */
|
|
4811
|
+
attempted = /* @__PURE__ */ new Set();
|
|
4812
|
+
constructor(options = {}) {
|
|
4813
|
+
this.env = options.env ?? process.env;
|
|
4814
|
+
this.run = options.run ?? runGh;
|
|
4815
|
+
this.log = options.log ?? ((m) => process.stderr.write(`[conveyor-agent] ${m}
|
|
4816
|
+
`));
|
|
4817
|
+
}
|
|
4818
|
+
/** Flip every not-yet-attempted port. Resolves even when everything fails. */
|
|
4819
|
+
async ensureVisible(ports) {
|
|
4820
|
+
if (!isCodespaceEnvironment(this.env)) return;
|
|
4821
|
+
const codespaceName = this.env.CODESPACE_NAME;
|
|
4822
|
+
for (const port of ports) {
|
|
4823
|
+
if (this.attempted.has(port)) continue;
|
|
4824
|
+
this.attempted.add(port);
|
|
4825
|
+
await this.flip(port, codespaceName);
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
async flip(port, codespaceName) {
|
|
4829
|
+
let lastError = "";
|
|
4830
|
+
for (const visibility of VISIBILITIES) {
|
|
4831
|
+
const result = await this.run([
|
|
4832
|
+
"codespace",
|
|
4833
|
+
"ports",
|
|
4834
|
+
"visibility",
|
|
4835
|
+
`${port}:${visibility}`,
|
|
4836
|
+
"-c",
|
|
4837
|
+
codespaceName
|
|
4838
|
+
]).catch((error) => ({
|
|
4839
|
+
ok: false,
|
|
4840
|
+
stderr: error instanceof Error ? error.message : String(error)
|
|
4841
|
+
}));
|
|
4842
|
+
if (result.ok) {
|
|
4843
|
+
this.log(`Forwarded port ${port} set to ${visibility} visibility`);
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4846
|
+
lastError = result.stderr;
|
|
4847
|
+
}
|
|
4848
|
+
this.log(
|
|
4849
|
+
`Could not change visibility of forwarded port ${port} \u2014 the preview URL may 404 for other users${lastError ? `: ${lastError}` : ""}`
|
|
4850
|
+
);
|
|
4851
|
+
}
|
|
4852
|
+
};
|
|
4853
|
+
|
|
4854
|
+
export {
|
|
4855
|
+
fetchBootstrap,
|
|
4856
|
+
applyBootstrapToEnv,
|
|
4857
|
+
createServiceLogger,
|
|
4858
|
+
AgentConnection,
|
|
4859
|
+
DEFAULT_SONNET_MODEL,
|
|
4860
|
+
FABLE_MODEL,
|
|
4861
|
+
PTY_STREAM_PORT_BASE,
|
|
4862
|
+
PTY_STREAM_PORT_ATTEMPTS,
|
|
4863
|
+
encodePtyStreamFrame,
|
|
4864
|
+
PtyStreamFrameReader,
|
|
4865
|
+
CARD_DESCRIPTION_FIELD_HINT,
|
|
4866
|
+
DEFAULT_CI_WAIT_TIMEOUT_MINUTES,
|
|
4867
|
+
MAX_CI_WAIT_TIMEOUT_MINUTES,
|
|
4868
|
+
SEVERITY_ENUM,
|
|
4869
|
+
runQueryGcpLogs,
|
|
4870
|
+
runQueryGrafanaLogs,
|
|
4871
|
+
EXTERNAL_AGENT_MESSAGE_SOURCE,
|
|
4872
|
+
TUI_KINDS,
|
|
4873
|
+
MAX_FILE_SIZE_BYTES,
|
|
4874
|
+
RUNNER_MODES,
|
|
4875
|
+
ReviewGuideContentSchema,
|
|
4876
|
+
CONTEXT_LINK_LOCATOR_MAX,
|
|
4877
|
+
isPlaceholderLocator,
|
|
4878
|
+
locatorMatchesContent,
|
|
4879
|
+
TAG_DESCRIPTION_MAX,
|
|
4880
|
+
TAG_OVERVIEW_MAX,
|
|
4881
|
+
TAG_REASON_MAX,
|
|
4882
|
+
CRITICAL_AUTOMATED_SOURCES,
|
|
4883
|
+
AGENT_STATUS_REASON_USER_QUESTION,
|
|
4884
|
+
TASK_CHAT_HISTORY_LIMIT,
|
|
4885
|
+
PM_CHAT_HISTORY_LIMIT,
|
|
4886
|
+
DEFAULT_CODEX_CODING_MODEL,
|
|
4887
|
+
isCodexReasoningEffort,
|
|
4888
|
+
HUMAN_PROSE_WRITING_STYLE,
|
|
4889
|
+
parseMentions,
|
|
4890
|
+
POD_PROFILE_ENV,
|
|
4891
|
+
podProfileRunsWorkload,
|
|
4892
|
+
isPodProfile,
|
|
4893
|
+
PRE_BUILD_TASK_STATUSES,
|
|
4894
|
+
hasTaskPlan,
|
|
4895
|
+
DEFAULT_LIFECYCLE_CONFIG,
|
|
4896
|
+
Lifecycle,
|
|
4897
|
+
readWorkspaceFile,
|
|
4898
|
+
readWorkspaceBytes,
|
|
4899
|
+
readWorkspaceDir,
|
|
4900
|
+
statWorkspacePath,
|
|
4901
|
+
workspacePathExists,
|
|
4902
|
+
GIT_TIMEOUT_MS,
|
|
4903
|
+
git,
|
|
4904
|
+
updateRemoteToken,
|
|
4905
|
+
verifyGitCredential,
|
|
4906
|
+
forceFreshMintBlocked,
|
|
4907
|
+
recordForceFreshFailure,
|
|
4908
|
+
clearForceFreshCooldown,
|
|
4909
|
+
forceFreshCooldownNotice,
|
|
4910
|
+
ensureOnTaskBranch,
|
|
4911
|
+
hasUncommittedChanges,
|
|
4912
|
+
getCurrentBranch,
|
|
4913
|
+
hasUnpushedCommits,
|
|
4914
|
+
remoteMatchesLocalHead,
|
|
4915
|
+
stageAndCommit,
|
|
4916
|
+
restoreWipSnapshot,
|
|
4917
|
+
flushPendingChanges,
|
|
4918
|
+
pushToOrigin,
|
|
4919
|
+
flushAllPendingWork,
|
|
4920
|
+
awaitGitReady,
|
|
4921
|
+
PortDiscovery,
|
|
4922
|
+
CodespacePortVisibility,
|
|
4923
|
+
loadForwardPorts,
|
|
4924
|
+
buildSessionPreviewPorts,
|
|
4925
|
+
loadConveyorConfig
|
|
4926
|
+
};
|