@gh-symphony/cli 0.0.20 → 0.0.22
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/README.md +66 -2
- package/dist/chunk-2TSM3INR.js +1085 -0
- package/dist/chunk-2UW7NQLX.js +684 -0
- package/dist/{chunk-MVRF7BES.js → chunk-36KYEDEO.js} +10 -1
- package/dist/{chunk-TILHWBP6.js → chunk-C67H3OUL.js} +239 -36
- package/dist/{chunk-C7G7RJ4G.js → chunk-DDL4BWSL.js} +1 -1
- package/dist/{chunk-XN5ABWZ6.js → chunk-DFLXHNYQ.js} +26 -30
- package/dist/{chunk-EKKT5USP.js → chunk-E7HYEEZD.js} +487 -133
- package/dist/chunk-EEQQWTXS.js +3257 -0
- package/dist/chunk-GDE6FYN4.js +26 -0
- package/dist/{chunk-Y6TYJMNT.js → chunk-GSX2FV3M.js} +10 -16
- package/dist/{chunk-RN2PACNV.js → chunk-HMLBBZNY.js} +731 -75
- package/dist/{chunk-5NV3LSAJ.js → chunk-IWFX2FMA.js} +5 -1
- package/dist/{chunk-HZVDTAPS.js → chunk-PUDXVBSN.js} +1549 -1458
- package/dist/{chunk-ROGRTUFI.js → chunk-QIRE2VXS.js} +14 -3
- package/dist/{chunk-3AWF54PI.js → chunk-ZHOKYUO3.js} +394 -42
- package/dist/{config-cmd-DNXNL26Z.js → config-cmd-Z3A7V6NC.js} +1 -1
- package/dist/{doctor-IYHCFXOZ.js → doctor-EJUMPBMW.js} +105 -40
- package/dist/index.js +112 -24
- package/dist/{init-KZT6YNOH.js → init-54HMKNYI.js} +8 -3
- package/dist/{logs-6JKKYDGJ.js → logs-GTZ4U5JE.js} +2 -2
- package/dist/project-RMYMZSFV.js +25 -0
- package/dist/{recover-5KQI7WH5.js → recover-LTLKMTRX.js} +7 -5
- package/dist/repo-WI7GF6XQ.js +749 -0
- package/dist/{run-ETC5UTRA.js → run-IHN3ZL35.js} +21 -7
- package/dist/{setup-VWB7RZUQ.js → setup-TZJSM3QV.js} +53 -14
- package/dist/start-RTAHQMR2.js +19 -0
- package/dist/status-F4D52OVK.js +12 -0
- package/dist/stop-MDKMJPVR.js +10 -0
- package/dist/{upgrade-3YNF3VKY.js → upgrade-O33S2SJK.js} +2 -2
- package/dist/{version-NUBTTOG7.js → version-CW54Q7BK.js} +1 -1
- package/dist/worker-entry.js +848 -693
- package/dist/{workflow-TBIFY5MO.js → workflow-L3KT6HB7.js} +177 -11
- package/package.json +4 -2
- package/dist/chunk-M3IFVLQS.js +0 -1155
- package/dist/project-UUVHS3ZR.js +0 -22
- package/dist/repo-HDDE7OUI.js +0 -321
- package/dist/start-ENFLZUI6.js +0 -16
- package/dist/status-QSCFVGRQ.js +0 -11
- package/dist/stop-7MFCBQVW.js +0 -9
|
@@ -0,0 +1,3257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../core/src/workflow/lifecycle.ts
|
|
4
|
+
var DEFAULT_WORKFLOW_LIFECYCLE = {
|
|
5
|
+
stateFieldName: "Status",
|
|
6
|
+
activeStates: ["Todo", "In Progress"],
|
|
7
|
+
terminalStates: ["Done"],
|
|
8
|
+
blockerCheckStates: ["Todo"]
|
|
9
|
+
};
|
|
10
|
+
function isStateActive(state, lifecycle) {
|
|
11
|
+
return matchesWorkflowState(state, lifecycle.activeStates);
|
|
12
|
+
}
|
|
13
|
+
function isStateTerminal(state, lifecycle) {
|
|
14
|
+
return matchesWorkflowState(state, lifecycle.terminalStates);
|
|
15
|
+
}
|
|
16
|
+
function matchesWorkflowState(state, candidates) {
|
|
17
|
+
const normalizedState = normalizeWorkflowState(state);
|
|
18
|
+
return candidates.some((candidate) => normalizeWorkflowState(candidate) === normalizedState);
|
|
19
|
+
}
|
|
20
|
+
function normalizeWorkflowState(state) {
|
|
21
|
+
return state.trim().toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ../core/src/workflow/config.ts
|
|
25
|
+
var DEFAULT_CODEX_COMMAND = "codex app-server";
|
|
26
|
+
var DEFAULT_CLAUDE_COMMAND = "claude";
|
|
27
|
+
var DEFAULT_AGENT_COMMAND = DEFAULT_CODEX_COMMAND;
|
|
28
|
+
var DEFAULT_HOOK_TIMEOUT_MS = 6e4;
|
|
29
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
30
|
+
var DEFAULT_MAX_RETRY_BACKOFF_MS = 3e5;
|
|
31
|
+
var DEFAULT_MAX_DELAY_MS = DEFAULT_MAX_RETRY_BACKOFF_MS;
|
|
32
|
+
var DEFAULT_BASE_DELAY_MS = 1e4;
|
|
33
|
+
var DEFAULT_MAX_TURNS = 20;
|
|
34
|
+
var DEFAULT_MAX_FAILURE_RETRIES = 10;
|
|
35
|
+
var DEFAULT_READ_TIMEOUT_MS = 5e3;
|
|
36
|
+
var DEFAULT_TURN_TIMEOUT_MS = 36e5;
|
|
37
|
+
var DEFAULT_STALL_TIMEOUT_MS = 3e5;
|
|
38
|
+
var DEFAULT_MAX_CONCURRENT_AGENTS = 10;
|
|
39
|
+
var DEFAULT_WORKFLOW_HOOKS = {
|
|
40
|
+
afterCreate: null,
|
|
41
|
+
beforeRun: null,
|
|
42
|
+
afterRun: null,
|
|
43
|
+
beforeRemove: null,
|
|
44
|
+
timeoutMs: DEFAULT_HOOK_TIMEOUT_MS
|
|
45
|
+
};
|
|
46
|
+
var DEFAULT_WORKFLOW_TRACKER = {
|
|
47
|
+
kind: null,
|
|
48
|
+
endpoint: null,
|
|
49
|
+
apiKey: null,
|
|
50
|
+
projectSlug: null,
|
|
51
|
+
activeStates: DEFAULT_WORKFLOW_LIFECYCLE.activeStates,
|
|
52
|
+
terminalStates: DEFAULT_WORKFLOW_LIFECYCLE.terminalStates,
|
|
53
|
+
projectId: null,
|
|
54
|
+
stateFieldName: DEFAULT_WORKFLOW_LIFECYCLE.stateFieldName,
|
|
55
|
+
priorityFieldName: null,
|
|
56
|
+
blockerCheckStates: DEFAULT_WORKFLOW_LIFECYCLE.blockerCheckStates
|
|
57
|
+
};
|
|
58
|
+
var DEFAULT_WORKFLOW_WORKSPACE = {
|
|
59
|
+
root: null
|
|
60
|
+
};
|
|
61
|
+
var DEFAULT_WORKFLOW_AGENT = {
|
|
62
|
+
maxConcurrentAgents: DEFAULT_MAX_CONCURRENT_AGENTS,
|
|
63
|
+
maxRetryBackoffMs: DEFAULT_MAX_RETRY_BACKOFF_MS,
|
|
64
|
+
maxConcurrentAgentsByState: {},
|
|
65
|
+
maxFailureRetries: DEFAULT_MAX_FAILURE_RETRIES,
|
|
66
|
+
maxTurns: DEFAULT_MAX_TURNS,
|
|
67
|
+
retryBaseDelayMs: DEFAULT_BASE_DELAY_MS
|
|
68
|
+
};
|
|
69
|
+
var DEFAULT_WORKFLOW_CODEX = {
|
|
70
|
+
command: DEFAULT_CODEX_COMMAND,
|
|
71
|
+
approvalPolicy: null,
|
|
72
|
+
threadSandbox: null,
|
|
73
|
+
turnSandboxPolicy: null,
|
|
74
|
+
turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
|
|
75
|
+
readTimeoutMs: DEFAULT_READ_TIMEOUT_MS,
|
|
76
|
+
stallTimeoutMs: DEFAULT_STALL_TIMEOUT_MS
|
|
77
|
+
};
|
|
78
|
+
var DEFAULT_WORKFLOW_DEFINITION = {
|
|
79
|
+
promptTemplate: "",
|
|
80
|
+
continuationGuidance: null,
|
|
81
|
+
tracker: DEFAULT_WORKFLOW_TRACKER,
|
|
82
|
+
polling: {
|
|
83
|
+
intervalMs: DEFAULT_POLL_INTERVAL_MS
|
|
84
|
+
},
|
|
85
|
+
workspace: DEFAULT_WORKFLOW_WORKSPACE,
|
|
86
|
+
hooks: DEFAULT_WORKFLOW_HOOKS,
|
|
87
|
+
agent: DEFAULT_WORKFLOW_AGENT,
|
|
88
|
+
runtime: null,
|
|
89
|
+
codex: DEFAULT_WORKFLOW_CODEX,
|
|
90
|
+
lifecycle: DEFAULT_WORKFLOW_LIFECYCLE,
|
|
91
|
+
format: "default",
|
|
92
|
+
githubProjectId: null,
|
|
93
|
+
agentCommand: DEFAULT_CODEX_COMMAND,
|
|
94
|
+
hookPath: null,
|
|
95
|
+
maxConcurrentByState: {}
|
|
96
|
+
};
|
|
97
|
+
function resolveWorkflowRuntimeCommand(workflow) {
|
|
98
|
+
if (!workflow.runtime) {
|
|
99
|
+
return workflow.codex.command;
|
|
100
|
+
}
|
|
101
|
+
if (workflow.runtime.args.length === 0) {
|
|
102
|
+
return workflow.runtime.command;
|
|
103
|
+
}
|
|
104
|
+
return [workflow.runtime.command, ...workflow.runtime.args].join(" ");
|
|
105
|
+
}
|
|
106
|
+
function resolveWorkflowRuntimeTimeouts(workflow) {
|
|
107
|
+
return workflow.runtime?.timeouts ?? workflow.codex;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ../core/src/workflow/parser.ts
|
|
111
|
+
function parseWorkflowMarkdown(markdown, env = process.env, options = {}) {
|
|
112
|
+
const compatibilityMode = options.compatibilityMode ?? "strict";
|
|
113
|
+
const frontMatterMatch = markdown.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
114
|
+
if (!frontMatterMatch) {
|
|
115
|
+
if (compatibilityMode === "legacy") {
|
|
116
|
+
return parseLegacyWorkflowMarkdown(markdown);
|
|
117
|
+
}
|
|
118
|
+
throw new Error("WORKFLOW.md must use YAML front matter.");
|
|
119
|
+
}
|
|
120
|
+
const [, rawFrontMatter, rawPromptTemplate = ""] = frontMatterMatch;
|
|
121
|
+
const frontMatter = parseFrontMatter(rawFrontMatter);
|
|
122
|
+
const promptTemplate = rawPromptTemplate.trim();
|
|
123
|
+
const tracker = readRequiredObject(frontMatter, "tracker");
|
|
124
|
+
const polling = readObject(frontMatter, "polling");
|
|
125
|
+
const workspace = readObject(frontMatter, "workspace");
|
|
126
|
+
const hooks = readObject(frontMatter, "hooks");
|
|
127
|
+
const agent = readObject(frontMatter, "agent");
|
|
128
|
+
const runtimeNode = readOptionalRuntimeObject(frontMatter);
|
|
129
|
+
const hasRuntime = runtimeNode !== null;
|
|
130
|
+
const codex = hasRuntime ? readObject(frontMatter, "codex") : readRequiredObject(frontMatter, "codex");
|
|
131
|
+
const trackerKind = readRequiredString(tracker, "kind", env);
|
|
132
|
+
const activeStates = readStringList(tracker, "active_states") ?? DEFAULT_WORKFLOW_TRACKER.activeStates;
|
|
133
|
+
const terminalStates = readStringList(tracker, "terminal_states") ?? DEFAULT_WORKFLOW_TRACKER.terminalStates;
|
|
134
|
+
const blockerCheckStates = readStringList(tracker, "blocker_check_states") ?? DEFAULT_WORKFLOW_TRACKER.blockerCheckStates;
|
|
135
|
+
const maxConcurrentAgentsByState = readNumberMap(
|
|
136
|
+
agent,
|
|
137
|
+
"max_concurrent_agents_by_state"
|
|
138
|
+
);
|
|
139
|
+
const runtime = hasRuntime ? parseRuntimeConfig(runtimeNode, env) : null;
|
|
140
|
+
const codexConfig = {
|
|
141
|
+
command: readOptionalString(codex, "command", env) ?? DEFAULT_AGENT_COMMAND,
|
|
142
|
+
approvalPolicy: readOptionalString(codex, "approval_policy", env),
|
|
143
|
+
threadSandbox: readOptionalString(codex, "thread_sandbox", env),
|
|
144
|
+
turnSandboxPolicy: readOptionalString(codex, "turn_sandbox_policy", env),
|
|
145
|
+
turnTimeoutMs: readOptionalIntegerLike(codex, "turn_timeout_ms") ?? DEFAULT_TURN_TIMEOUT_MS,
|
|
146
|
+
readTimeoutMs: readOptionalIntegerLike(codex, "read_timeout_ms") ?? DEFAULT_READ_TIMEOUT_MS,
|
|
147
|
+
stallTimeoutMs: readOptionalIntegerLike(codex, "stall_timeout_ms") ?? DEFAULT_STALL_TIMEOUT_MS
|
|
148
|
+
};
|
|
149
|
+
const agentCommand = resolveWorkflowRuntimeCommand({
|
|
150
|
+
runtime,
|
|
151
|
+
codex: codexConfig
|
|
152
|
+
});
|
|
153
|
+
const parsed = {
|
|
154
|
+
promptTemplate,
|
|
155
|
+
continuationGuidance: readOptionalWorkflowString(
|
|
156
|
+
frontMatter,
|
|
157
|
+
"continuationGuidance",
|
|
158
|
+
"continuation_guidance",
|
|
159
|
+
env
|
|
160
|
+
),
|
|
161
|
+
tracker: {
|
|
162
|
+
kind: trackerKind,
|
|
163
|
+
endpoint: readOptionalString(tracker, "endpoint", env),
|
|
164
|
+
apiKey: readOptionalString(tracker, "api_key", env),
|
|
165
|
+
projectSlug: readOptionalString(tracker, "project_slug", env),
|
|
166
|
+
activeStates,
|
|
167
|
+
terminalStates,
|
|
168
|
+
projectId: readOptionalString(tracker, "project_id", env),
|
|
169
|
+
stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
|
|
170
|
+
priorityFieldName: readOptionalString(tracker, "priority_field", env),
|
|
171
|
+
blockerCheckStates
|
|
172
|
+
},
|
|
173
|
+
polling: {
|
|
174
|
+
intervalMs: readOptionalIntegerLike(polling, "interval_ms") ?? DEFAULT_POLL_INTERVAL_MS
|
|
175
|
+
},
|
|
176
|
+
workspace: {
|
|
177
|
+
root: readOptionalString(workspace, "root", env)
|
|
178
|
+
},
|
|
179
|
+
hooks: {
|
|
180
|
+
afterCreate: readOptionalString(hooks, "after_create", env),
|
|
181
|
+
beforeRun: readOptionalString(hooks, "before_run", env),
|
|
182
|
+
afterRun: readOptionalString(hooks, "after_run", env),
|
|
183
|
+
beforeRemove: readOptionalString(hooks, "before_remove", env),
|
|
184
|
+
timeoutMs: readOptionalIntegerLike(hooks, "timeout_ms") ?? DEFAULT_HOOK_TIMEOUT_MS
|
|
185
|
+
},
|
|
186
|
+
agent: {
|
|
187
|
+
maxConcurrentAgents: readOptionalIntegerLike(agent, "max_concurrent_agents") ?? DEFAULT_MAX_CONCURRENT_AGENTS,
|
|
188
|
+
maxRetryBackoffMs: readOptionalIntegerLike(agent, "max_retry_backoff_ms") ?? DEFAULT_MAX_RETRY_BACKOFF_MS,
|
|
189
|
+
maxConcurrentAgentsByState,
|
|
190
|
+
maxFailureRetries: readOptionalIntegerLike(agent, "max_failure_retries") ?? DEFAULT_MAX_FAILURE_RETRIES,
|
|
191
|
+
maxTurns: readOptionalIntegerLike(agent, "max_turns") ?? DEFAULT_MAX_TURNS,
|
|
192
|
+
retryBaseDelayMs: readOptionalIntegerLike(agent, "retry_base_delay_ms") ?? DEFAULT_BASE_DELAY_MS
|
|
193
|
+
},
|
|
194
|
+
runtime,
|
|
195
|
+
codex: codexConfig,
|
|
196
|
+
lifecycle: {
|
|
197
|
+
stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
|
|
198
|
+
activeStates,
|
|
199
|
+
terminalStates,
|
|
200
|
+
blockerCheckStates
|
|
201
|
+
},
|
|
202
|
+
format: "front-matter",
|
|
203
|
+
githubProjectId: readOptionalString(tracker, "project_id", env),
|
|
204
|
+
agentCommand,
|
|
205
|
+
hookPath: readOptionalString(hooks, "after_create", env),
|
|
206
|
+
maxConcurrentByState: maxConcurrentAgentsByState
|
|
207
|
+
};
|
|
208
|
+
return parsed;
|
|
209
|
+
}
|
|
210
|
+
function parseLegacyWorkflowMarkdown(markdown) {
|
|
211
|
+
const promptGuidelines = matchOptionalSection(markdown, "Prompt Guidelines") ?? "";
|
|
212
|
+
return {
|
|
213
|
+
...DEFAULT_WORKFLOW_DEFINITION,
|
|
214
|
+
promptTemplate: promptGuidelines,
|
|
215
|
+
format: "legacy-sectioned"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function parseFrontMatter(frontMatter) {
|
|
219
|
+
const lines = frontMatter.replace(/\r\n/g, "\n").split("\n");
|
|
220
|
+
const [value] = parseBlock(lines, 0, 0);
|
|
221
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
222
|
+
throw new Error("Workflow front matter must be a YAML object.");
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
function parseBlock(lines, startIndex, indent) {
|
|
227
|
+
let index = startIndex;
|
|
228
|
+
let collectionType = null;
|
|
229
|
+
const arrayValues = [];
|
|
230
|
+
const objectValues = {};
|
|
231
|
+
while (index < lines.length) {
|
|
232
|
+
const line = lines[index] ?? "";
|
|
233
|
+
if (!line.trim()) {
|
|
234
|
+
index += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const lineIndent = countIndent(line);
|
|
238
|
+
if (lineIndent < indent) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (lineIndent > indent) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Invalid workflow front matter indentation near "${line.trim()}".`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
const trimmed = line.trim();
|
|
247
|
+
if (trimmed.startsWith("- ")) {
|
|
248
|
+
if (collectionType === "object") {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"Cannot mix array and object values in workflow front matter."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
collectionType = "array";
|
|
254
|
+
const itemText = trimmed.slice(2).trim();
|
|
255
|
+
if (itemText === "|" || itemText === "|-") {
|
|
256
|
+
const [multiline, nextIndex3] = parseMultilineScalar(
|
|
257
|
+
lines,
|
|
258
|
+
index + 1,
|
|
259
|
+
indent + 2
|
|
260
|
+
);
|
|
261
|
+
arrayValues.push(multiline);
|
|
262
|
+
index = nextIndex3;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (itemText) {
|
|
266
|
+
arrayValues.push(parseScalar(itemText));
|
|
267
|
+
index += 1;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const [child2, nextIndex2] = parseBlock(lines, index + 1, indent + 2);
|
|
271
|
+
arrayValues.push(child2);
|
|
272
|
+
index = nextIndex2;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (collectionType === "array") {
|
|
276
|
+
throw new Error(
|
|
277
|
+
"Cannot mix object and array values in workflow front matter."
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
collectionType = "object";
|
|
281
|
+
const separatorIndex = trimmed.indexOf(":");
|
|
282
|
+
if (separatorIndex < 0) {
|
|
283
|
+
throw new Error(`Invalid workflow front matter line "${trimmed}".`);
|
|
284
|
+
}
|
|
285
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
286
|
+
const remainder = trimmed.slice(separatorIndex + 1).trim();
|
|
287
|
+
if (remainder === "|" || remainder === "|-") {
|
|
288
|
+
const [multiline, nextIndex2] = parseMultilineScalar(
|
|
289
|
+
lines,
|
|
290
|
+
index + 1,
|
|
291
|
+
indent + 2
|
|
292
|
+
);
|
|
293
|
+
objectValues[key] = multiline;
|
|
294
|
+
index = nextIndex2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (remainder) {
|
|
298
|
+
objectValues[key] = parseScalar(remainder);
|
|
299
|
+
index += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const [child, nextIndex] = parseBlock(lines, index + 1, indent + 2);
|
|
303
|
+
objectValues[key] = child;
|
|
304
|
+
index = nextIndex;
|
|
305
|
+
}
|
|
306
|
+
return [collectionType === "array" ? arrayValues : objectValues, index];
|
|
307
|
+
}
|
|
308
|
+
function parseMultilineScalar(lines, startIndex, indent) {
|
|
309
|
+
let index = startIndex;
|
|
310
|
+
const collected = [];
|
|
311
|
+
while (index < lines.length) {
|
|
312
|
+
const line = lines[index] ?? "";
|
|
313
|
+
if (!line.trim()) {
|
|
314
|
+
collected.push("");
|
|
315
|
+
index += 1;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const lineIndent = countIndent(line);
|
|
319
|
+
if (lineIndent < indent) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
collected.push(line.slice(indent));
|
|
323
|
+
index += 1;
|
|
324
|
+
}
|
|
325
|
+
return [collected.join("\n").trimEnd(), index];
|
|
326
|
+
}
|
|
327
|
+
function countIndent(line) {
|
|
328
|
+
return line.match(/^ */)?.[0].length ?? 0;
|
|
329
|
+
}
|
|
330
|
+
function parseScalar(value) {
|
|
331
|
+
if (value === "null") return null;
|
|
332
|
+
if (value === "true") return true;
|
|
333
|
+
if (value === "false") return false;
|
|
334
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
335
|
+
return parseInlineArray(value);
|
|
336
|
+
}
|
|
337
|
+
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
|
|
338
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
339
|
+
return value.slice(1, -1);
|
|
340
|
+
}
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
function parseInlineArray(value) {
|
|
344
|
+
const inner = value.slice(1, -1).trim();
|
|
345
|
+
if (!inner) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
return splitInlineArrayEntries(inner).map((entry) => parseScalar(entry));
|
|
349
|
+
}
|
|
350
|
+
function splitInlineArrayEntries(inner) {
|
|
351
|
+
const entries = [];
|
|
352
|
+
let current = "";
|
|
353
|
+
let quote = null;
|
|
354
|
+
for (const char of inner) {
|
|
355
|
+
if (quote) {
|
|
356
|
+
current += char;
|
|
357
|
+
if (char === quote) {
|
|
358
|
+
quote = null;
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (char === '"' || char === "'") {
|
|
363
|
+
quote = char;
|
|
364
|
+
current += char;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (char === ",") {
|
|
368
|
+
pushInlineArrayEntry(entries, current, "middle");
|
|
369
|
+
current = "";
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
current += char;
|
|
373
|
+
}
|
|
374
|
+
if (quote) {
|
|
375
|
+
throw new Error("Workflow front matter inline array has an unterminated string.");
|
|
376
|
+
}
|
|
377
|
+
pushInlineArrayEntry(entries, current, "end");
|
|
378
|
+
return entries;
|
|
379
|
+
}
|
|
380
|
+
function pushInlineArrayEntry(entries, entry, position) {
|
|
381
|
+
const trimmed = entry.trim();
|
|
382
|
+
if (!trimmed) {
|
|
383
|
+
const reason = position === "end" ? "has a trailing comma" : "contains an empty item";
|
|
384
|
+
throw new Error(`Workflow front matter inline array ${reason}.`);
|
|
385
|
+
}
|
|
386
|
+
entries.push(trimmed);
|
|
387
|
+
}
|
|
388
|
+
function parseRuntimeConfig(runtime, env) {
|
|
389
|
+
const kind = readRuntimeKind(runtime, env);
|
|
390
|
+
const isolation = readObject(runtime, "isolation", "runtime.isolation");
|
|
391
|
+
const auth = readObject(runtime, "auth", "runtime.auth");
|
|
392
|
+
const timeouts = readObject(runtime, "timeouts", "runtime.timeouts");
|
|
393
|
+
const configuredCommand = readOptionalString(runtime, "command", env);
|
|
394
|
+
const command = configuredCommand ?? defaultRuntimeCommand(kind);
|
|
395
|
+
if (!command) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
'Workflow front matter field "runtime.command" is required for runtime.kind "custom".'
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
kind,
|
|
402
|
+
command,
|
|
403
|
+
args: readRuntimeArgs(runtime),
|
|
404
|
+
isolation: {
|
|
405
|
+
bare: readOptionalBoolean(isolation, "bare", "runtime.isolation.bare") ?? false,
|
|
406
|
+
strictMcpConfig: readOptionalBoolean(
|
|
407
|
+
isolation,
|
|
408
|
+
"strict_mcp_config",
|
|
409
|
+
"runtime.isolation.strict_mcp_config"
|
|
410
|
+
) ?? false
|
|
411
|
+
},
|
|
412
|
+
auth: {
|
|
413
|
+
env: readOptionalString(auth, "env", env)
|
|
414
|
+
},
|
|
415
|
+
timeouts: {
|
|
416
|
+
turnTimeoutMs: readOptionalIntegerLike(timeouts, "turn_timeout_ms") ?? DEFAULT_TURN_TIMEOUT_MS,
|
|
417
|
+
readTimeoutMs: readOptionalIntegerLike(timeouts, "read_timeout_ms") ?? DEFAULT_READ_TIMEOUT_MS,
|
|
418
|
+
stallTimeoutMs: readOptionalIntegerLike(timeouts, "stall_timeout_ms") ?? DEFAULT_STALL_TIMEOUT_MS
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function readRuntimeKind(runtime, env) {
|
|
423
|
+
const kind = readRequiredString(runtime, "kind", env);
|
|
424
|
+
if (kind === "codex-app-server" || kind === "claude-print" || kind === "custom") {
|
|
425
|
+
return kind;
|
|
426
|
+
}
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Unsupported workflow runtime kind "${kind}". Supported values: codex-app-server, claude-print, custom.`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
function defaultRuntimeCommand(kind) {
|
|
432
|
+
if (kind === "claude-print") {
|
|
433
|
+
return DEFAULT_CLAUDE_COMMAND;
|
|
434
|
+
}
|
|
435
|
+
if (kind === "codex-app-server") {
|
|
436
|
+
return DEFAULT_AGENT_COMMAND;
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
function readObject(input, key, path = key) {
|
|
441
|
+
const value = input[key];
|
|
442
|
+
if (value === void 0 || value === null) {
|
|
443
|
+
return {};
|
|
444
|
+
}
|
|
445
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
446
|
+
throw new Error(`Workflow front matter field "${path}" must be an object.`);
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
function readOptionalRuntimeObject(input) {
|
|
451
|
+
if (input.runtime === void 0 || input.runtime === null) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return readObject(input, "runtime");
|
|
455
|
+
}
|
|
456
|
+
function readRequiredObject(input, key) {
|
|
457
|
+
if (!(key in input)) {
|
|
458
|
+
throw new Error(`Workflow front matter field "${key}" is required.`);
|
|
459
|
+
}
|
|
460
|
+
return readObject(input, key);
|
|
461
|
+
}
|
|
462
|
+
function readOptionalString(input, key, env) {
|
|
463
|
+
const value = input[key];
|
|
464
|
+
if (value === void 0 || value === null) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
if (typeof value !== "string") {
|
|
468
|
+
throw new Error(`Workflow front matter field "${key}" must be a string.`);
|
|
469
|
+
}
|
|
470
|
+
return resolveEnvironmentValue(value, env);
|
|
471
|
+
}
|
|
472
|
+
function readOptionalWorkflowString(input, primaryKey, fallbackKey, env) {
|
|
473
|
+
return readOptionalString(input, primaryKey, env) ?? readOptionalString(input, fallbackKey, env);
|
|
474
|
+
}
|
|
475
|
+
function readRequiredString(input, key, env) {
|
|
476
|
+
const value = readOptionalString(input, key, env);
|
|
477
|
+
if (!value) {
|
|
478
|
+
throw new Error(`Workflow front matter field "${key}" is required.`);
|
|
479
|
+
}
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
function readStringList(input, key) {
|
|
483
|
+
const value = input[key];
|
|
484
|
+
if (value === void 0 || value === null) {
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
if (typeof value === "string") {
|
|
488
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
489
|
+
}
|
|
490
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Workflow front matter field "${key}" must be an array of strings or comma-separated string.`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
function readRuntimeArgs(input) {
|
|
498
|
+
const value = input.args;
|
|
499
|
+
if (value === void 0 || value === null) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
'Workflow front matter field "runtime.args" must be an array of strings.'
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
function readOptionalBoolean(input, key, path = key) {
|
|
510
|
+
const value = input[key];
|
|
511
|
+
if (value === void 0 || value === null) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
if (typeof value !== "boolean") {
|
|
515
|
+
throw new Error(`Workflow front matter field "${path}" must be a boolean.`);
|
|
516
|
+
}
|
|
517
|
+
return value;
|
|
518
|
+
}
|
|
519
|
+
function readOptionalIntegerLike(input, key) {
|
|
520
|
+
const value = input[key];
|
|
521
|
+
if (value === void 0 || value === null) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
if (typeof value === "number") {
|
|
525
|
+
return value;
|
|
526
|
+
}
|
|
527
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
528
|
+
return Number.parseInt(value, 10);
|
|
529
|
+
}
|
|
530
|
+
throw new Error(`Workflow front matter field "${key}" must be an integer.`);
|
|
531
|
+
}
|
|
532
|
+
function readNumberMap(input, key) {
|
|
533
|
+
const value = input[key];
|
|
534
|
+
if (value === void 0 || value === null) {
|
|
535
|
+
return {};
|
|
536
|
+
}
|
|
537
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
538
|
+
throw new Error(`Workflow front matter field "${key}" must be an object.`);
|
|
539
|
+
}
|
|
540
|
+
const result = {};
|
|
541
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
542
|
+
if (typeof entryValue === "number") {
|
|
543
|
+
result[entryKey] = entryValue;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (typeof entryValue === "string" && /^\d+$/.test(entryValue)) {
|
|
547
|
+
result[entryKey] = Number.parseInt(entryValue, 10);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
throw new Error(
|
|
551
|
+
`Workflow front matter field "${key}.${entryKey}" must be an integer.`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
function resolveEnvironmentValue(value, env) {
|
|
557
|
+
const envTokenMatch = value.match(/^(?:env:)?([A-Z0-9_]+)$/);
|
|
558
|
+
if (value.startsWith("env:") && envTokenMatch) {
|
|
559
|
+
const resolved = env[envTokenMatch[1]];
|
|
560
|
+
if (!resolved) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Workflow front matter requires environment variable ${envTokenMatch[1]}.`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return resolved;
|
|
566
|
+
}
|
|
567
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_, name) => {
|
|
568
|
+
const resolved = env[name];
|
|
569
|
+
if (!resolved) {
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Workflow front matter requires environment variable ${name}.`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return resolved;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function matchOptionalSection(markdown, heading) {
|
|
578
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
579
|
+
const pattern = new RegExp(
|
|
580
|
+
`## ${escapedHeading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`
|
|
581
|
+
);
|
|
582
|
+
const match = markdown.match(pattern);
|
|
583
|
+
return match?.[1]?.trim() ?? null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ../core/src/workflow/loader.ts
|
|
587
|
+
import { createHash } from "crypto";
|
|
588
|
+
import { access, readFile, stat } from "fs/promises";
|
|
589
|
+
import { constants } from "fs";
|
|
590
|
+
var WorkflowConfigStore = class {
|
|
591
|
+
cache = /* @__PURE__ */ new Map();
|
|
592
|
+
async load(workflowPath, env = process.env) {
|
|
593
|
+
await access(workflowPath, constants.R_OK);
|
|
594
|
+
const fileStat = await stat(workflowPath);
|
|
595
|
+
const cached = this.cache.get(workflowPath);
|
|
596
|
+
const markdown = await readFile(workflowPath, "utf8");
|
|
597
|
+
const fingerprint = `${fileStat.mtimeMs}:${fileStat.size}:${createHash("sha256").update(markdown).digest("hex")}`;
|
|
598
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
599
|
+
return toWorkflowResolution(workflowPath, cached.workflow, {
|
|
600
|
+
isValid: true,
|
|
601
|
+
usedLastKnownGood: false,
|
|
602
|
+
validationError: null
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const workflow = parseWorkflowMarkdown(markdown, env);
|
|
607
|
+
this.cache.set(workflowPath, {
|
|
608
|
+
fingerprint,
|
|
609
|
+
workflow,
|
|
610
|
+
loadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
611
|
+
});
|
|
612
|
+
return toWorkflowResolution(workflowPath, workflow, {
|
|
613
|
+
isValid: true,
|
|
614
|
+
usedLastKnownGood: false,
|
|
615
|
+
validationError: null
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (cached) {
|
|
619
|
+
return toWorkflowResolution(workflowPath, cached.workflow, {
|
|
620
|
+
isValid: false,
|
|
621
|
+
usedLastKnownGood: true,
|
|
622
|
+
validationError: error instanceof Error ? error.message : "Invalid workflow definition."
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
function createDefaultWorkflowResolution() {
|
|
630
|
+
return createInvalidWorkflowResolution(null, "missing_workflow_file");
|
|
631
|
+
}
|
|
632
|
+
function createInvalidWorkflowResolution(workflowPath, validationError) {
|
|
633
|
+
return toWorkflowResolution(workflowPath, DEFAULT_WORKFLOW_DEFINITION, {
|
|
634
|
+
isValid: false,
|
|
635
|
+
usedLastKnownGood: false,
|
|
636
|
+
validationError
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function toWorkflowResolution(workflowPath, workflow, metadata) {
|
|
640
|
+
return {
|
|
641
|
+
workflowPath,
|
|
642
|
+
workflow,
|
|
643
|
+
lifecycle: workflow.lifecycle,
|
|
644
|
+
promptTemplate: workflow.promptTemplate,
|
|
645
|
+
agentCommand: workflow.agentCommand,
|
|
646
|
+
hookPath: workflow.hookPath ?? "",
|
|
647
|
+
isValid: metadata.isValid,
|
|
648
|
+
usedLastKnownGood: metadata.usedLastKnownGood,
|
|
649
|
+
validationError: metadata.validationError
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ../core/src/workflow/render.ts
|
|
654
|
+
import {
|
|
655
|
+
Liquid,
|
|
656
|
+
ParseError,
|
|
657
|
+
RenderError,
|
|
658
|
+
TokenizationError,
|
|
659
|
+
UndefinedVariableError
|
|
660
|
+
} from "liquidjs";
|
|
661
|
+
function buildPromptVariables(issue, options) {
|
|
662
|
+
return {
|
|
663
|
+
issue: {
|
|
664
|
+
id: issue.id,
|
|
665
|
+
identifier: issue.identifier,
|
|
666
|
+
number: issue.number,
|
|
667
|
+
title: issue.title,
|
|
668
|
+
description: issue.description,
|
|
669
|
+
priority: issue.priority,
|
|
670
|
+
url: issue.url,
|
|
671
|
+
state: issue.state,
|
|
672
|
+
labels: issue.labels,
|
|
673
|
+
blocked_by: issue.blockedBy,
|
|
674
|
+
branch_name: issue.branchName,
|
|
675
|
+
created_at: issue.createdAt,
|
|
676
|
+
updated_at: issue.updatedAt,
|
|
677
|
+
repository: `${issue.repository.owner}/${issue.repository.name}`
|
|
678
|
+
},
|
|
679
|
+
attempt: options.attempt
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
var STRICT_LIQUID_ENGINE = new Liquid({
|
|
683
|
+
strictVariables: true,
|
|
684
|
+
strictFilters: true,
|
|
685
|
+
ownPropertyOnly: true
|
|
686
|
+
});
|
|
687
|
+
function renderPrompt(template, variables, options = {}) {
|
|
688
|
+
const strict = options.strict ?? true;
|
|
689
|
+
if (!strict) {
|
|
690
|
+
return renderLegacyPrompt(template, variables);
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
return STRICT_LIQUID_ENGINE.parseAndRenderSync(template, variables);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
throw normalizeTemplateError(error);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function normalizeTemplateError(error) {
|
|
699
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
700
|
+
if (error instanceof UndefinedVariableError || error instanceof RenderError || error instanceof ParseError && message.startsWith("undefined filter:")) {
|
|
701
|
+
return new Error(`template_render_error: ${message}`, { cause: error });
|
|
702
|
+
}
|
|
703
|
+
if (error instanceof ParseError || error instanceof TokenizationError) {
|
|
704
|
+
return new Error(`template_parse_error: ${message}`, { cause: error });
|
|
705
|
+
}
|
|
706
|
+
return new Error(`template_render_error: ${message}`, { cause: error });
|
|
707
|
+
}
|
|
708
|
+
function flattenVariables(obj, prefix = "") {
|
|
709
|
+
const result = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
711
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
712
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
713
|
+
for (const [nestedKey, nestedValue] of flattenVariables(
|
|
714
|
+
value,
|
|
715
|
+
fullKey
|
|
716
|
+
)) {
|
|
717
|
+
result.set(nestedKey, nestedValue);
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
result.set(fullKey, value);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
function renderLegacyPrompt(template, variables) {
|
|
726
|
+
const flatVars = flattenVariables(variables);
|
|
727
|
+
return template.replace(
|
|
728
|
+
/\{\{([a-zA-Z_][a-zA-Z0-9_.]*)\}\}/g,
|
|
729
|
+
(match, key) => {
|
|
730
|
+
const value = flatVars.get(key);
|
|
731
|
+
if (value === void 0) {
|
|
732
|
+
return match;
|
|
733
|
+
}
|
|
734
|
+
if (value === null) {
|
|
735
|
+
return "";
|
|
736
|
+
}
|
|
737
|
+
return String(value);
|
|
738
|
+
}
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ../core/src/contracts/status-surface.ts
|
|
743
|
+
var WORKFLOW_EXECUTION_PHASES = [
|
|
744
|
+
"planning",
|
|
745
|
+
"human-review",
|
|
746
|
+
"implementation",
|
|
747
|
+
"awaiting-merge",
|
|
748
|
+
"completed"
|
|
749
|
+
];
|
|
750
|
+
function isWorkflowExecutionPhase(value) {
|
|
751
|
+
return typeof value === "string" && WORKFLOW_EXECUTION_PHASES.includes(value);
|
|
752
|
+
}
|
|
753
|
+
var SESSION_EXIT_CLASSIFICATIONS = [
|
|
754
|
+
"completed",
|
|
755
|
+
"budget-exceeded",
|
|
756
|
+
"convergence-detected",
|
|
757
|
+
"max-turns-reached",
|
|
758
|
+
"user-input-required",
|
|
759
|
+
"timeout",
|
|
760
|
+
"error"
|
|
761
|
+
];
|
|
762
|
+
function isSessionExitClassification(value) {
|
|
763
|
+
return typeof value === "string" && SESSION_EXIT_CLASSIFICATIONS.includes(value);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// ../core/src/contracts/run-attempt-phase.ts
|
|
767
|
+
var RUN_ATTEMPT_PHASES = [
|
|
768
|
+
"preparing_workspace",
|
|
769
|
+
"building_prompt",
|
|
770
|
+
"launching_agent",
|
|
771
|
+
"initializing_session",
|
|
772
|
+
"streaming_turn",
|
|
773
|
+
"finishing",
|
|
774
|
+
"succeeded",
|
|
775
|
+
"failed",
|
|
776
|
+
"timed_out",
|
|
777
|
+
"stalled",
|
|
778
|
+
"canceled_by_reconciliation"
|
|
779
|
+
];
|
|
780
|
+
function isRunAttemptPhase(value) {
|
|
781
|
+
return typeof value === "string" && RUN_ATTEMPT_PHASES.includes(value);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ../core/src/contracts/orchestrator-channel.ts
|
|
785
|
+
function isRecord(value) {
|
|
786
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
787
|
+
}
|
|
788
|
+
function isTokenUsage(value) {
|
|
789
|
+
if (!isRecord(value)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
return typeof value.inputTokens === "number" && typeof value.outputTokens === "number" && typeof value.totalTokens === "number";
|
|
793
|
+
}
|
|
794
|
+
function isSessionInfo(value) {
|
|
795
|
+
if (!isRecord(value)) {
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return (typeof value.threadId === "string" || value.threadId === null) && (typeof value.turnId === "string" || value.turnId === null) && typeof value.turnCount === "number" && (typeof value.sessionId === "string" || value.sessionId === null) && (!("exitClassification" in value) || value.exitClassification === void 0 || value.exitClassification === null || isSessionExitClassification(value.exitClassification));
|
|
799
|
+
}
|
|
800
|
+
function isNullableString(value) {
|
|
801
|
+
return typeof value === "string" || value === null;
|
|
802
|
+
}
|
|
803
|
+
function isTurnEventBase(value) {
|
|
804
|
+
return typeof value.startedAt === "string" && isNullableString(value.threadId) && isNullableString(value.turnId) && typeof value.turnCount === "number" && isNullableString(value.sessionId);
|
|
805
|
+
}
|
|
806
|
+
function isOrchestratorChannelEvent(value) {
|
|
807
|
+
if (!isRecord(value)) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
if (typeof value.issueId !== "string") {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (value.type === "codex_update") {
|
|
814
|
+
if (typeof value.lastEventAt !== "string") {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
if ("event" in value && value.event !== void 0 && typeof value.event !== "string") {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
if ("tokenUsage" in value && value.tokenUsage !== void 0 && !isTokenUsage(value.tokenUsage)) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
if ("rateLimits" in value && value.rateLimits !== void 0 && !isRecord(value.rateLimits)) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
if ("sessionInfo" in value && value.sessionInfo !== void 0 && !isSessionInfo(value.sessionInfo)) {
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
if ("executionPhase" in value && value.executionPhase !== void 0 && value.executionPhase !== null && !isWorkflowExecutionPhase(value.executionPhase)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
if ("runPhase" in value && value.runPhase !== void 0 && value.runPhase !== null && !isRunAttemptPhase(value.runPhase)) {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
if ("lastError" in value && value.lastError !== void 0 && value.lastError !== null && typeof value.lastError !== "string") {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
if (value.type === "heartbeat") {
|
|
841
|
+
if (value.lastEventAt !== null && typeof value.lastEventAt !== "string") {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (!isTokenUsage(value.tokenUsage)) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (value.rateLimits !== null && !isRecord(value.rateLimits)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
if (value.sessionInfo !== null && !isSessionInfo(value.sessionInfo)) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
if (value.executionPhase !== null && !isWorkflowExecutionPhase(value.executionPhase)) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
if (value.runPhase !== null && !isRunAttemptPhase(value.runPhase)) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
if (value.lastError !== null && typeof value.lastError !== "string") {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
if (value.type === "turn_started") {
|
|
865
|
+
return isTurnEventBase(value);
|
|
866
|
+
}
|
|
867
|
+
if (value.type === "turn_completed") {
|
|
868
|
+
return isTurnEventBase(value) && typeof value.completedAt === "string" && typeof value.durationMs === "number" && isTokenUsage(value.tokenUsage);
|
|
869
|
+
}
|
|
870
|
+
if (value.type === "turn_failed") {
|
|
871
|
+
return isTurnEventBase(value) && typeof value.failedAt === "string" && typeof value.durationMs === "number" && isTokenUsage(value.tokenUsage) && isNullableString(value.error);
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ../core/src/workflow/exit-classification.ts
|
|
877
|
+
function classifySessionExit(params) {
|
|
878
|
+
if (params.userInputRequired) {
|
|
879
|
+
return "user-input-required";
|
|
880
|
+
}
|
|
881
|
+
if (params.budgetExceeded) {
|
|
882
|
+
return "budget-exceeded";
|
|
883
|
+
}
|
|
884
|
+
if (params.convergenceDetected) {
|
|
885
|
+
return "convergence-detected";
|
|
886
|
+
}
|
|
887
|
+
if (params.runPhase === "timed_out" || params.runPhase === "stalled") {
|
|
888
|
+
return "timeout";
|
|
889
|
+
}
|
|
890
|
+
if (params.maxTurnsReached) {
|
|
891
|
+
return "max-turns-reached";
|
|
892
|
+
}
|
|
893
|
+
if (params.runPhase === "succeeded") {
|
|
894
|
+
return "completed";
|
|
895
|
+
}
|
|
896
|
+
return "error";
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ../core/src/orchestration/retry-policy.ts
|
|
900
|
+
function calculateRetryDelay(attempt, options = {}) {
|
|
901
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
902
|
+
const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
903
|
+
const normalizedAttempt = Math.max(1, attempt);
|
|
904
|
+
const delay = baseDelayMs * 2 ** (normalizedAttempt - 1);
|
|
905
|
+
return Math.min(delay, maxDelayMs);
|
|
906
|
+
}
|
|
907
|
+
function scheduleRetryAt(now, attempt, options = {}) {
|
|
908
|
+
return new Date(now.getTime() + calculateRetryDelay(attempt, options));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ../core/src/runtime/credentials.ts
|
|
912
|
+
import { readFile as readFile2, writeFile } from "fs/promises";
|
|
913
|
+
var TOKEN_REUSE_WINDOW_MS = 60 * 1e3;
|
|
914
|
+
var CODEX_ENV_KEYS = [
|
|
915
|
+
"OPENAI_API_KEY",
|
|
916
|
+
"OPENAI_BASE_URL",
|
|
917
|
+
"OPENAI_ORG_ID",
|
|
918
|
+
"OPENAI_PROJECT"
|
|
919
|
+
];
|
|
920
|
+
var AgentRuntimeCredentialError = class extends Error {
|
|
921
|
+
};
|
|
922
|
+
function extractEnvForCodex(env) {
|
|
923
|
+
return pickRuntimeEnv(env, CODEX_ENV_KEYS);
|
|
924
|
+
}
|
|
925
|
+
function extractEnvForClaude(env, envKey = "ANTHROPIC_API_KEY") {
|
|
926
|
+
const apiKey = env[envKey];
|
|
927
|
+
if (!apiKey) {
|
|
928
|
+
throw new AgentRuntimeCredentialError(
|
|
929
|
+
`${envKey} is required in the credential broker response.`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
[envKey]: apiKey
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function toAgentCredentialCacheEntry(brokerResponse, now = /* @__PURE__ */ new Date()) {
|
|
937
|
+
return {
|
|
938
|
+
env: brokerResponse.env,
|
|
939
|
+
expires_at: brokerResponse.expires_at,
|
|
940
|
+
cachedAt: now.toISOString()
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function shouldReuseAgentCredentialCache(entry, now = /* @__PURE__ */ new Date()) {
|
|
944
|
+
if (Object.keys(entry.env).length === 0) {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
if (!entry.expires_at) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
const expiresAt = Date.parse(entry.expires_at);
|
|
951
|
+
if (Number.isNaN(expiresAt)) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
return expiresAt - now.getTime() > TOKEN_REUSE_WINDOW_MS;
|
|
955
|
+
}
|
|
956
|
+
async function readAgentCredentialCache(path, readFileImpl = readFile2) {
|
|
957
|
+
try {
|
|
958
|
+
return normalizeAgentCredentialCacheEntry(
|
|
959
|
+
JSON.parse(await readFileImpl(path, "utf8"))
|
|
960
|
+
);
|
|
961
|
+
} catch {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async function writeAgentCredentialCache(path, brokerResponse, writeFileImpl = writeFile, now = /* @__PURE__ */ new Date()) {
|
|
966
|
+
const entry = toAgentCredentialCacheEntry(brokerResponse, now);
|
|
967
|
+
await writeFileImpl(path, JSON.stringify(entry), "utf8");
|
|
968
|
+
return entry;
|
|
969
|
+
}
|
|
970
|
+
function pickRuntimeEnv(env, keys) {
|
|
971
|
+
const resolved = {};
|
|
972
|
+
for (const key of keys) {
|
|
973
|
+
const value = env[key];
|
|
974
|
+
if (value) {
|
|
975
|
+
resolved[key] = value;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return resolved;
|
|
979
|
+
}
|
|
980
|
+
function normalizeAgentCredentialCacheEntry(payload) {
|
|
981
|
+
if (!isRecord2(payload)) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
if (!isRecord2(payload.env)) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
const env = Object.fromEntries(
|
|
988
|
+
Object.entries(payload.env).filter(
|
|
989
|
+
(entry) => typeof entry[1] === "string"
|
|
990
|
+
)
|
|
991
|
+
);
|
|
992
|
+
if (Object.keys(env).length === 0) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
env,
|
|
997
|
+
expires_at: typeof payload.expires_at === "string" ? payload.expires_at : void 0,
|
|
998
|
+
cachedAt: typeof payload.cachedAt === "string" ? payload.cachedAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function isRecord2(value) {
|
|
1002
|
+
return value !== null && typeof value === "object";
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ../core/src/runtime/events.ts
|
|
1006
|
+
var DEFAULT_AGENT_INPUT_REQUIRED_REASON = "turn_input_required: agent requires user input";
|
|
1007
|
+
function buildAgentInputRequiredReason(prompt) {
|
|
1008
|
+
if (typeof prompt === "string") {
|
|
1009
|
+
const trimmedPrompt = prompt.trim();
|
|
1010
|
+
if (trimmedPrompt) {
|
|
1011
|
+
return `turn_input_required: ${trimmedPrompt}`;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return DEFAULT_AGENT_INPUT_REQUIRED_REASON;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ../core/src/workspace/env-file.ts
|
|
1018
|
+
import { existsSync, readFileSync } from "fs";
|
|
1019
|
+
function readEnvFile(path) {
|
|
1020
|
+
if (!existsSync(path)) {
|
|
1021
|
+
return {};
|
|
1022
|
+
}
|
|
1023
|
+
return readFileSync(path, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).reduce((result, line) => {
|
|
1024
|
+
const separatorIndex = line.indexOf("=");
|
|
1025
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
1026
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
1027
|
+
if (key) {
|
|
1028
|
+
result[key] = value;
|
|
1029
|
+
}
|
|
1030
|
+
return result;
|
|
1031
|
+
}, {});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ../core/src/workspace/safety.ts
|
|
1035
|
+
import { resolve } from "path";
|
|
1036
|
+
|
|
1037
|
+
// ../core/src/workspace/identity.ts
|
|
1038
|
+
import { resolve as resolve2, join } from "path";
|
|
1039
|
+
import { createHash as createHash2 } from "crypto";
|
|
1040
|
+
var RESERVED_WORKSPACE_KEYS = /* @__PURE__ */ new Set([
|
|
1041
|
+
"cache",
|
|
1042
|
+
"issues.json",
|
|
1043
|
+
"project.json",
|
|
1044
|
+
"runs",
|
|
1045
|
+
"status.json"
|
|
1046
|
+
]);
|
|
1047
|
+
function deriveWorkspaceKey(identifier) {
|
|
1048
|
+
const sanitized = identifier.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1049
|
+
if (!sanitized || /^[.]+$/.test(sanitized)) {
|
|
1050
|
+
return "issue";
|
|
1051
|
+
}
|
|
1052
|
+
return sanitized;
|
|
1053
|
+
}
|
|
1054
|
+
var deriveIssueWorkspaceKeyFromIdentifier = deriveWorkspaceKey;
|
|
1055
|
+
function deriveIssueWorkspaceKey(identityOrIdentifier, issueIdentifier) {
|
|
1056
|
+
if (typeof identityOrIdentifier === "string") {
|
|
1057
|
+
return deriveWorkspaceKey(identityOrIdentifier);
|
|
1058
|
+
}
|
|
1059
|
+
return deriveWorkspaceKey(
|
|
1060
|
+
issueIdentifier ?? identityOrIdentifier.issueSubjectId
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
function deriveLegacyIssueWorkspaceKey(identity, projectId) {
|
|
1064
|
+
const input = [projectId, identity.adapter, identity.issueSubjectId].filter((part) => typeof part === "string").join(":");
|
|
1065
|
+
return createHash2("sha256").update(input).digest("hex").slice(0, 16);
|
|
1066
|
+
}
|
|
1067
|
+
function resolveIssueWorkspaceDirectory(runtimeRoot, workspaceKey) {
|
|
1068
|
+
const normalizedRuntimeRoot = resolve2(runtimeRoot);
|
|
1069
|
+
const candidate = resolve2(normalizedRuntimeRoot, workspaceKey);
|
|
1070
|
+
if (!candidate.startsWith(`${normalizedRuntimeRoot}/`)) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
"Issue workspace path escapes the configured runtime root."
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (isReservedWorkspaceKey(workspaceKey)) {
|
|
1076
|
+
throw new Error("Issue workspace key is reserved by the runtime layout.");
|
|
1077
|
+
}
|
|
1078
|
+
return candidate;
|
|
1079
|
+
}
|
|
1080
|
+
function isReservedWorkspaceKey(workspaceKey) {
|
|
1081
|
+
return workspaceKey.startsWith(".") || RESERVED_WORKSPACE_KEYS.has(workspaceKey);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ../core/src/workspace/hooks.ts
|
|
1085
|
+
import { spawn } from "child_process";
|
|
1086
|
+
var DEFAULT_HOOK_TIMEOUT_MS2 = 6e4;
|
|
1087
|
+
async function executeHook(options) {
|
|
1088
|
+
const { kind, command, cwd, env, timeoutMs } = options;
|
|
1089
|
+
const start = Date.now();
|
|
1090
|
+
const normalizedCommand = normalizeHookCommand(command);
|
|
1091
|
+
return new Promise((resolveResult) => {
|
|
1092
|
+
let timedOut = false;
|
|
1093
|
+
let timer = null;
|
|
1094
|
+
const child = spawn("bash", ["-lc", normalizedCommand], {
|
|
1095
|
+
cwd,
|
|
1096
|
+
env: { ...process.env, ...env },
|
|
1097
|
+
stdio: "pipe"
|
|
1098
|
+
});
|
|
1099
|
+
const stderrChunks = [];
|
|
1100
|
+
child.stderr?.on("data", (chunk) => {
|
|
1101
|
+
stderrChunks.push(chunk);
|
|
1102
|
+
});
|
|
1103
|
+
if (timeoutMs > 0) {
|
|
1104
|
+
timer = setTimeout(() => {
|
|
1105
|
+
timedOut = true;
|
|
1106
|
+
child.kill("SIGTERM");
|
|
1107
|
+
setTimeout(() => {
|
|
1108
|
+
try {
|
|
1109
|
+
child.kill("SIGKILL");
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}, 5e3);
|
|
1113
|
+
}, timeoutMs);
|
|
1114
|
+
}
|
|
1115
|
+
child.on("close", (code) => {
|
|
1116
|
+
if (timer) {
|
|
1117
|
+
clearTimeout(timer);
|
|
1118
|
+
}
|
|
1119
|
+
const durationMs = Date.now() - start;
|
|
1120
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
1121
|
+
if (timedOut) {
|
|
1122
|
+
resolveResult({
|
|
1123
|
+
kind,
|
|
1124
|
+
outcome: "timeout",
|
|
1125
|
+
exitCode: code,
|
|
1126
|
+
durationMs,
|
|
1127
|
+
error: `Hook "${kind}" timed out after ${timeoutMs}ms`
|
|
1128
|
+
});
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (code !== 0) {
|
|
1132
|
+
resolveResult({
|
|
1133
|
+
kind,
|
|
1134
|
+
outcome: "failure",
|
|
1135
|
+
exitCode: code,
|
|
1136
|
+
durationMs,
|
|
1137
|
+
error: stderr || `Hook "${kind}" exited with code ${code}`
|
|
1138
|
+
});
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
resolveResult({
|
|
1142
|
+
kind,
|
|
1143
|
+
outcome: "success",
|
|
1144
|
+
exitCode: 0,
|
|
1145
|
+
durationMs,
|
|
1146
|
+
error: null
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
child.on("error", (err) => {
|
|
1150
|
+
if (timer) {
|
|
1151
|
+
clearTimeout(timer);
|
|
1152
|
+
}
|
|
1153
|
+
resolveResult({
|
|
1154
|
+
kind,
|
|
1155
|
+
outcome: "failure",
|
|
1156
|
+
exitCode: null,
|
|
1157
|
+
durationMs: Date.now() - start,
|
|
1158
|
+
error: err.message
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
function buildHookEnv(context) {
|
|
1164
|
+
const env = {
|
|
1165
|
+
SYMPHONY_PROJECT_ID: context.projectId,
|
|
1166
|
+
SYMPHONY_ISSUE_WORKSPACE_KEY: context.workspaceKey,
|
|
1167
|
+
SYMPHONY_ISSUE_SUBJECT_ID: context.issueSubjectId,
|
|
1168
|
+
SYMPHONY_ISSUE_IDENTIFIER: context.issueIdentifier,
|
|
1169
|
+
SYMPHONY_WORKSPACE_PATH: context.workspacePath,
|
|
1170
|
+
SYMPHONY_REPOSITORY_PATH: context.repositoryPath
|
|
1171
|
+
};
|
|
1172
|
+
if (context.runId) {
|
|
1173
|
+
env.SYMPHONY_RUN_ID = context.runId;
|
|
1174
|
+
}
|
|
1175
|
+
if (context.state) {
|
|
1176
|
+
env.SYMPHONY_ISSUE_STATE = context.state;
|
|
1177
|
+
}
|
|
1178
|
+
return env;
|
|
1179
|
+
}
|
|
1180
|
+
function resolveHookCommand(hooks, kind) {
|
|
1181
|
+
switch (kind) {
|
|
1182
|
+
case "after_create":
|
|
1183
|
+
return hooks.afterCreate;
|
|
1184
|
+
case "before_run":
|
|
1185
|
+
return hooks.beforeRun;
|
|
1186
|
+
case "after_run":
|
|
1187
|
+
return hooks.afterRun;
|
|
1188
|
+
case "before_remove":
|
|
1189
|
+
return hooks.beforeRemove;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async function executeWorkspaceHook(options) {
|
|
1193
|
+
const hookCommand = resolveHookCommand(options.hooks, options.kind);
|
|
1194
|
+
if (!hookCommand) {
|
|
1195
|
+
return {
|
|
1196
|
+
kind: options.kind,
|
|
1197
|
+
outcome: "skipped",
|
|
1198
|
+
exitCode: null,
|
|
1199
|
+
durationMs: 0,
|
|
1200
|
+
error: null
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
return executeHook({
|
|
1204
|
+
kind: options.kind,
|
|
1205
|
+
command: hookCommand,
|
|
1206
|
+
cwd: options.repositoryPath,
|
|
1207
|
+
env: options.env,
|
|
1208
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS2
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
function normalizeHookCommand(command) {
|
|
1212
|
+
const trimmed = command.trim();
|
|
1213
|
+
if (trimmed.includes("/") && !trimmed.startsWith("/") && !trimmed.startsWith("./") && !trimmed.startsWith("../") && !/\s/.test(trimmed)) {
|
|
1214
|
+
return `bash ./${trimmed}`;
|
|
1215
|
+
}
|
|
1216
|
+
return command;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// ../core/src/observability/snapshot-builder.ts
|
|
1220
|
+
function buildProjectSnapshot(input) {
|
|
1221
|
+
const {
|
|
1222
|
+
project,
|
|
1223
|
+
activeRuns,
|
|
1224
|
+
allRuns,
|
|
1225
|
+
summary,
|
|
1226
|
+
lastTickAt,
|
|
1227
|
+
lastError,
|
|
1228
|
+
rateLimits
|
|
1229
|
+
} = input;
|
|
1230
|
+
const cumulativeTokenUsageByIssue = aggregateTokenUsageByIssue(
|
|
1231
|
+
allRuns ?? activeRuns
|
|
1232
|
+
);
|
|
1233
|
+
return {
|
|
1234
|
+
repository: project.repository,
|
|
1235
|
+
tracker: {
|
|
1236
|
+
adapter: project.tracker.adapter,
|
|
1237
|
+
bindingId: project.tracker.bindingId,
|
|
1238
|
+
settings: project.tracker.settings
|
|
1239
|
+
},
|
|
1240
|
+
lastTickAt,
|
|
1241
|
+
health: lastError ? "degraded" : activeRuns.length > 0 ? "running" : "idle",
|
|
1242
|
+
summary: {
|
|
1243
|
+
dispatched: summary.dispatched,
|
|
1244
|
+
suppressed: summary.suppressed,
|
|
1245
|
+
recovered: summary.recovered,
|
|
1246
|
+
activeRuns: activeRuns.length
|
|
1247
|
+
},
|
|
1248
|
+
activeRuns: activeRuns.map((run) => ({
|
|
1249
|
+
runId: run.runId,
|
|
1250
|
+
issueIdentifier: run.issueIdentifier,
|
|
1251
|
+
issueState: run.issueState,
|
|
1252
|
+
status: run.status,
|
|
1253
|
+
retryKind: run.retryKind,
|
|
1254
|
+
port: run.port,
|
|
1255
|
+
runtimeSession: run.runtimeSession ?? null,
|
|
1256
|
+
// New fields from live worker data
|
|
1257
|
+
processId: run.processId ?? null,
|
|
1258
|
+
turnCount: run.turnCount,
|
|
1259
|
+
startedAt: run.startedAt ?? null,
|
|
1260
|
+
lastEvent: run.lastEvent ?? null,
|
|
1261
|
+
lastEventAt: run.lastEventAt ?? null,
|
|
1262
|
+
executionPhase: run.executionPhase ?? null,
|
|
1263
|
+
runPhase: run.runPhase ?? null,
|
|
1264
|
+
tokenUsage: attachCumulativeTokenUsage(
|
|
1265
|
+
run.tokenUsage,
|
|
1266
|
+
cumulativeTokenUsageByIssue.get(run.issueId)
|
|
1267
|
+
)
|
|
1268
|
+
})),
|
|
1269
|
+
retryQueue: activeRuns.filter((run) => run.status === "retrying" && run.retryKind).map((run) => ({
|
|
1270
|
+
runId: run.runId,
|
|
1271
|
+
issueIdentifier: run.issueIdentifier,
|
|
1272
|
+
retryKind: run.retryKind ?? "failure",
|
|
1273
|
+
nextRetryAt: run.nextRetryAt
|
|
1274
|
+
})),
|
|
1275
|
+
lastError,
|
|
1276
|
+
codexTotals: aggregateTokenUsage(allRuns ?? activeRuns, lastTickAt),
|
|
1277
|
+
rateLimits: rateLimits ?? null
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function aggregateTokenUsageByIssue(runs) {
|
|
1281
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1282
|
+
for (const run of runs) {
|
|
1283
|
+
if (!run.tokenUsage) {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
const current = totals.get(run.issueId) ?? {
|
|
1287
|
+
inputTokens: 0,
|
|
1288
|
+
outputTokens: 0,
|
|
1289
|
+
totalTokens: 0
|
|
1290
|
+
};
|
|
1291
|
+
current.inputTokens += run.tokenUsage.inputTokens;
|
|
1292
|
+
current.outputTokens += run.tokenUsage.outputTokens;
|
|
1293
|
+
current.totalTokens += run.tokenUsage.totalTokens;
|
|
1294
|
+
totals.set(run.issueId, current);
|
|
1295
|
+
}
|
|
1296
|
+
return totals;
|
|
1297
|
+
}
|
|
1298
|
+
function attachCumulativeTokenUsage(tokenUsage, cumulative) {
|
|
1299
|
+
if (!tokenUsage) {
|
|
1300
|
+
return void 0;
|
|
1301
|
+
}
|
|
1302
|
+
return {
|
|
1303
|
+
...tokenUsage,
|
|
1304
|
+
cumulativeInputTokens: cumulative?.inputTokens ?? tokenUsage.inputTokens,
|
|
1305
|
+
cumulativeOutputTokens: cumulative?.outputTokens ?? tokenUsage.outputTokens,
|
|
1306
|
+
cumulativeTotalTokens: cumulative?.totalTokens ?? tokenUsage.totalTokens
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function aggregateTokenUsage(runs, lastTickAt) {
|
|
1310
|
+
let inputTokens = 0;
|
|
1311
|
+
let outputTokens = 0;
|
|
1312
|
+
let totalTokens = 0;
|
|
1313
|
+
let earliestStart = null;
|
|
1314
|
+
let latestEnd = null;
|
|
1315
|
+
for (const run of runs) {
|
|
1316
|
+
if (run.tokenUsage) {
|
|
1317
|
+
inputTokens += run.tokenUsage.inputTokens;
|
|
1318
|
+
outputTokens += run.tokenUsage.outputTokens;
|
|
1319
|
+
totalTokens += run.tokenUsage.totalTokens;
|
|
1320
|
+
}
|
|
1321
|
+
if (run.startedAt) {
|
|
1322
|
+
const start = new Date(run.startedAt).getTime();
|
|
1323
|
+
if (earliestStart === null || start < earliestStart) {
|
|
1324
|
+
earliestStart = start;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
const end = run.completedAt ? new Date(run.completedAt).getTime() : new Date(lastTickAt).getTime();
|
|
1328
|
+
if (latestEnd === null || end > latestEnd) {
|
|
1329
|
+
latestEnd = end;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
const secondsRunning = earliestStart !== null && latestEnd !== null ? Math.max(0, Math.round((latestEnd - earliestStart) / 1e3)) : 0;
|
|
1333
|
+
return { inputTokens, outputTokens, totalTokens, secondsRunning };
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// ../core/src/observability/fs-reader.ts
|
|
1337
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1338
|
+
async function readJsonFile(path) {
|
|
1339
|
+
try {
|
|
1340
|
+
const raw = await readFile3(path, "utf8");
|
|
1341
|
+
return JSON.parse(raw);
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
if (isFileMissing(error)) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
throw error;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function safeReadDir(path) {
|
|
1350
|
+
try {
|
|
1351
|
+
return await readdir(path);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if (isFileMissing(error)) {
|
|
1354
|
+
return [];
|
|
1355
|
+
}
|
|
1356
|
+
throw error;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
function isFileMissing(error) {
|
|
1360
|
+
return Boolean(
|
|
1361
|
+
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// ../core/src/observability/event-formatter.ts
|
|
1366
|
+
function formatEventMessage(event) {
|
|
1367
|
+
switch (event.event) {
|
|
1368
|
+
case "run-dispatched":
|
|
1369
|
+
return event.issueState ? `Dispatched from ${event.issueState}` : "Dispatched";
|
|
1370
|
+
case "run-recovered":
|
|
1371
|
+
return "Recovered existing run";
|
|
1372
|
+
case "run-retried":
|
|
1373
|
+
return `Retry ${event.attempt} scheduled (${event.retryKind})`;
|
|
1374
|
+
case "run-failed":
|
|
1375
|
+
return event.lastError;
|
|
1376
|
+
case "run-suppressed":
|
|
1377
|
+
return event.reason;
|
|
1378
|
+
case "hook-executed":
|
|
1379
|
+
return `${event.hook}: ${event.outcome}`;
|
|
1380
|
+
case "hook-failed":
|
|
1381
|
+
return event.error;
|
|
1382
|
+
case "workspace-cleanup":
|
|
1383
|
+
return event.error ? `${event.outcome}: ${event.error}` : event.outcome;
|
|
1384
|
+
case "worker-error":
|
|
1385
|
+
return event.error;
|
|
1386
|
+
case "turn_started":
|
|
1387
|
+
return `Turn ${event.turnCount} started`;
|
|
1388
|
+
case "turn_completed":
|
|
1389
|
+
return `Turn ${event.turnCount} completed in ${event.durationMs}ms`;
|
|
1390
|
+
case "turn_failed":
|
|
1391
|
+
return event.error ?? `Turn ${event.turnCount} failed`;
|
|
1392
|
+
case "session_invalidated":
|
|
1393
|
+
return event.reason;
|
|
1394
|
+
default:
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function parseRecentEvents(raw, limit, options) {
|
|
1399
|
+
const lines = raw.split("\n");
|
|
1400
|
+
if (options.allowPartialFirstLine) {
|
|
1401
|
+
lines.shift();
|
|
1402
|
+
}
|
|
1403
|
+
const events = [];
|
|
1404
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1405
|
+
const line = lines[index]?.trim();
|
|
1406
|
+
if (!line) {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
const event = parseRunEventLine(line);
|
|
1410
|
+
if (!event) {
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
events.push({
|
|
1414
|
+
at: event.at,
|
|
1415
|
+
event: event.event,
|
|
1416
|
+
message: formatEventMessage(event)
|
|
1417
|
+
});
|
|
1418
|
+
if (events.length === limit) {
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return events.reverse();
|
|
1423
|
+
}
|
|
1424
|
+
function parseRunEventLine(line) {
|
|
1425
|
+
try {
|
|
1426
|
+
return JSON.parse(line);
|
|
1427
|
+
} catch {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// ../core/src/observability/status-assembler.ts
|
|
1433
|
+
function isMatchingIssueRun(run, issueId, issueIdentifier) {
|
|
1434
|
+
return Boolean(
|
|
1435
|
+
run && (run.issueId === issueId || run.issueIdentifier === issueIdentifier)
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
function mapIssueOrchestrationStateToStatus(state) {
|
|
1439
|
+
switch (state) {
|
|
1440
|
+
case "claimed":
|
|
1441
|
+
return "starting";
|
|
1442
|
+
case "running":
|
|
1443
|
+
return "running";
|
|
1444
|
+
case "retry_queued":
|
|
1445
|
+
return "retrying";
|
|
1446
|
+
case "released":
|
|
1447
|
+
return "released";
|
|
1448
|
+
case "unclaimed":
|
|
1449
|
+
return "pending";
|
|
1450
|
+
default:
|
|
1451
|
+
return state;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// ../runtime-claude/src/preflight.ts
|
|
1456
|
+
import { execFileSync } from "child_process";
|
|
1457
|
+
import { constants as constants2 } from "fs";
|
|
1458
|
+
import { access as access2, readFile as readFile4 } from "fs/promises";
|
|
1459
|
+
import { isAbsolute, join as join2, resolve as resolve3 } from "path";
|
|
1460
|
+
var DEFAULT_DEPENDENCIES = {
|
|
1461
|
+
execFileSync,
|
|
1462
|
+
readFile: readFile4,
|
|
1463
|
+
access: access2,
|
|
1464
|
+
fetchImpl: fetch,
|
|
1465
|
+
platform: process.platform
|
|
1466
|
+
};
|
|
1467
|
+
var CREDENTIAL_BROKER_TIMEOUT_MS = 5e3;
|
|
1468
|
+
async function runClaudePreflight(options, dependencies = {}) {
|
|
1469
|
+
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
1470
|
+
const env = options.env ?? process.env;
|
|
1471
|
+
const command = resolveRuntimeCommandBinary(options.command) ?? "claude";
|
|
1472
|
+
const checks = [];
|
|
1473
|
+
checks.push(checkClaudeBinary(command, options.cwd, deps));
|
|
1474
|
+
checks.push(await checkAnthropicApiKey(env, options, deps));
|
|
1475
|
+
checks.push(await checkWorkspaceMcpConfig(options.cwd, deps));
|
|
1476
|
+
if (options.includeGhAuth) {
|
|
1477
|
+
checks.push(checkGhAuthentication(options.cwd, deps));
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
ok: checks.every((check) => check.status !== "fail"),
|
|
1481
|
+
checks
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function formatClaudePreflightText(report) {
|
|
1485
|
+
const lines = ["Claude runtime preflight"];
|
|
1486
|
+
for (const check of report.checks) {
|
|
1487
|
+
const label = check.status === "pass" ? "PASS" : check.status === "warn" ? "WARN" : "FAIL";
|
|
1488
|
+
lines.push(`${label} ${check.title}`);
|
|
1489
|
+
lines.push(` ${check.summary}`);
|
|
1490
|
+
if (check.remediation) {
|
|
1491
|
+
lines.push(` Fix: ${check.remediation}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return lines.join("\n");
|
|
1495
|
+
}
|
|
1496
|
+
function pass(id, title, summary, details) {
|
|
1497
|
+
return { id, title, status: "pass", summary, details };
|
|
1498
|
+
}
|
|
1499
|
+
function warn(id, title, summary, remediation, details) {
|
|
1500
|
+
return { id, title, status: "warn", summary, remediation, details };
|
|
1501
|
+
}
|
|
1502
|
+
function fail(id, title, summary, remediation, details) {
|
|
1503
|
+
return { id, title, status: "fail", summary, remediation, details };
|
|
1504
|
+
}
|
|
1505
|
+
function checkClaudeBinary(command, cwd, deps) {
|
|
1506
|
+
const executable = resolveExecutableCommand(command, cwd);
|
|
1507
|
+
try {
|
|
1508
|
+
const locatedPath = locateClaudeBinary(executable, cwd, deps);
|
|
1509
|
+
const version = deps.execFileSync(executable, ["--version"], {
|
|
1510
|
+
encoding: "utf8",
|
|
1511
|
+
cwd,
|
|
1512
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1513
|
+
}).toString().trim();
|
|
1514
|
+
return pass(
|
|
1515
|
+
"claude_binary",
|
|
1516
|
+
"Claude CLI binary",
|
|
1517
|
+
version ? `${executable} is available: ${version}.` : `${executable} is available, but --version returned an empty response.`,
|
|
1518
|
+
{ command: executable, path: locatedPath, version }
|
|
1519
|
+
);
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
return fail(
|
|
1522
|
+
"claude_binary",
|
|
1523
|
+
"Claude CLI binary",
|
|
1524
|
+
`${executable} could not be found or executed from PATH.`,
|
|
1525
|
+
`Install Claude Code and ensure '${executable}' is on PATH, then re-run the command.`,
|
|
1526
|
+
{
|
|
1527
|
+
command: executable,
|
|
1528
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function resolveExecutableCommand(command, cwd) {
|
|
1534
|
+
if ((command.includes("/") || command.includes("\\")) && !isAbsolute(command)) {
|
|
1535
|
+
return resolve3(cwd, command);
|
|
1536
|
+
}
|
|
1537
|
+
return command;
|
|
1538
|
+
}
|
|
1539
|
+
function locateClaudeBinary(command, cwd, deps) {
|
|
1540
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
1541
|
+
return command;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
const locator = deps.platform === "win32" ? "where" : "which";
|
|
1545
|
+
return deps.execFileSync(locator, [command], {
|
|
1546
|
+
encoding: "utf8",
|
|
1547
|
+
cwd,
|
|
1548
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1549
|
+
}).toString().split(/\r?\n/).find((line) => line.trim())?.trim() ?? null;
|
|
1550
|
+
} catch {
|
|
1551
|
+
return null;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function checkGhAuthentication(cwd, deps) {
|
|
1555
|
+
try {
|
|
1556
|
+
deps.execFileSync("gh", ["auth", "status"], {
|
|
1557
|
+
encoding: "utf8",
|
|
1558
|
+
cwd,
|
|
1559
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1560
|
+
});
|
|
1561
|
+
return pass(
|
|
1562
|
+
"gh_authentication",
|
|
1563
|
+
"GitHub CLI authentication",
|
|
1564
|
+
"gh auth status succeeded."
|
|
1565
|
+
);
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
return fail(
|
|
1568
|
+
"gh_authentication",
|
|
1569
|
+
"GitHub CLI authentication",
|
|
1570
|
+
"gh auth status failed or no GitHub login is configured.",
|
|
1571
|
+
"Run 'gh auth login --scopes repo,read:org,project' and re-run the command.",
|
|
1572
|
+
{ error: error instanceof Error ? error.message : String(error) }
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async function checkAnthropicApiKey(env, options, deps) {
|
|
1577
|
+
if (env.ANTHROPIC_API_KEY?.trim()) {
|
|
1578
|
+
return pass(
|
|
1579
|
+
"anthropic_api_key",
|
|
1580
|
+
"Anthropic API key",
|
|
1581
|
+
"ANTHROPIC_API_KEY is configured in the environment.",
|
|
1582
|
+
{ source: "env" }
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
const brokerUrl = env.AGENT_CREDENTIAL_BROKER_URL?.trim();
|
|
1586
|
+
const brokerSecret = env.AGENT_CREDENTIAL_BROKER_SECRET?.trim();
|
|
1587
|
+
if (!brokerUrl || !brokerSecret) {
|
|
1588
|
+
return fail(
|
|
1589
|
+
"anthropic_api_key",
|
|
1590
|
+
"Anthropic API key",
|
|
1591
|
+
"Neither ANTHROPIC_API_KEY nor an agent credential broker is configured.",
|
|
1592
|
+
"Set ANTHROPIC_API_KEY or configure AGENT_CREDENTIAL_BROKER_URL and AGENT_CREDENTIAL_BROKER_SECRET.",
|
|
1593
|
+
{ source: "missing" }
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
if (options.probeCredentialBroker === false) {
|
|
1597
|
+
return pass(
|
|
1598
|
+
"anthropic_api_key",
|
|
1599
|
+
"Anthropic API key",
|
|
1600
|
+
"Agent credential broker configuration is present.",
|
|
1601
|
+
{ source: "broker", brokerUrl }
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
try {
|
|
1605
|
+
const response = await deps.fetchImpl(brokerUrl, {
|
|
1606
|
+
method: "POST",
|
|
1607
|
+
headers: {
|
|
1608
|
+
accept: "application/json",
|
|
1609
|
+
authorization: `Bearer ${brokerSecret}`
|
|
1610
|
+
},
|
|
1611
|
+
signal: AbortSignal.timeout(CREDENTIAL_BROKER_TIMEOUT_MS)
|
|
1612
|
+
});
|
|
1613
|
+
const payload = await response.json();
|
|
1614
|
+
if (response.ok && payload.env?.ANTHROPIC_API_KEY?.trim()) {
|
|
1615
|
+
return pass(
|
|
1616
|
+
"anthropic_api_key",
|
|
1617
|
+
"Anthropic API key",
|
|
1618
|
+
"Agent credential broker is reachable and returned ANTHROPIC_API_KEY.",
|
|
1619
|
+
{ source: "broker", brokerUrl }
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
return fail(
|
|
1623
|
+
"anthropic_api_key",
|
|
1624
|
+
"Anthropic API key",
|
|
1625
|
+
payload.error ? `Agent credential broker did not return ANTHROPIC_API_KEY: ${payload.error}.` : "Agent credential broker did not return ANTHROPIC_API_KEY.",
|
|
1626
|
+
"Set ANTHROPIC_API_KEY or configure the credential broker to return ANTHROPIC_API_KEY.",
|
|
1627
|
+
{ source: "broker", brokerUrl, status: response.status }
|
|
1628
|
+
);
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
return fail(
|
|
1631
|
+
"anthropic_api_key",
|
|
1632
|
+
"Anthropic API key",
|
|
1633
|
+
"Agent credential broker could not be reached.",
|
|
1634
|
+
"Set ANTHROPIC_API_KEY or fix AGENT_CREDENTIAL_BROKER_URL and AGENT_CREDENTIAL_BROKER_SECRET.",
|
|
1635
|
+
{
|
|
1636
|
+
source: "broker",
|
|
1637
|
+
brokerUrl,
|
|
1638
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1639
|
+
}
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
async function checkWorkspaceMcpConfig(cwd, deps) {
|
|
1644
|
+
const path = join2(cwd, ".mcp.json");
|
|
1645
|
+
try {
|
|
1646
|
+
await deps.access(path, constants2.R_OK);
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
const err = error;
|
|
1649
|
+
if (err.code === "ENOENT") {
|
|
1650
|
+
return warn(
|
|
1651
|
+
"claude_mcp_config",
|
|
1652
|
+
"Workspace .mcp.json",
|
|
1653
|
+
".mcp.json was not found in the workspace root. Claude can still run without a workspace MCP config.",
|
|
1654
|
+
void 0,
|
|
1655
|
+
{ path, reason: "missing" }
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
return warn(
|
|
1659
|
+
"claude_mcp_config",
|
|
1660
|
+
"Workspace .mcp.json",
|
|
1661
|
+
`.mcp.json exists but is not readable: ${err.message}.`,
|
|
1662
|
+
"Fix .mcp.json file permissions if this workspace needs Claude MCP servers.",
|
|
1663
|
+
{ path, reason: "unreadable", error: err.message }
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
try {
|
|
1667
|
+
const content = await deps.readFile(path, "utf8");
|
|
1668
|
+
JSON.parse(content);
|
|
1669
|
+
return pass(
|
|
1670
|
+
"claude_mcp_config",
|
|
1671
|
+
"Workspace .mcp.json",
|
|
1672
|
+
".mcp.json is readable and contains valid JSON.",
|
|
1673
|
+
{ path }
|
|
1674
|
+
);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
return fail(
|
|
1677
|
+
"claude_mcp_config",
|
|
1678
|
+
"Workspace .mcp.json",
|
|
1679
|
+
`.mcp.json could not be parsed as JSON: ${error instanceof Error ? error.message : String(error)}.`,
|
|
1680
|
+
"Fix or remove the workspace root .mcp.json file, then re-run the command.",
|
|
1681
|
+
{ path, reason: "invalid_json" }
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function isClaudeRuntimeCommand(command) {
|
|
1686
|
+
return resolveClaudeCommandBinary(command) != null;
|
|
1687
|
+
}
|
|
1688
|
+
function resolveClaudeCommandBinary(command) {
|
|
1689
|
+
const binary = resolveRuntimeCommandBinary(command);
|
|
1690
|
+
return binary != null && isClaudeBinaryName(binary) ? binary : null;
|
|
1691
|
+
}
|
|
1692
|
+
function resolveRuntimeCommandBinary(command) {
|
|
1693
|
+
const normalized = (command ?? "").trim();
|
|
1694
|
+
if (!normalized) {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
const tokens = tokenizeRuntimeCommand(normalized);
|
|
1698
|
+
if (tokens.length === 0) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
const first = stripClaudeCommandQuotes(tokens[0]);
|
|
1702
|
+
if ((first === "bash" || first === "sh" || first === "zsh" || first === "fish") && tokens.length >= 3) {
|
|
1703
|
+
const flagIndex = tokens.findIndex((token) => {
|
|
1704
|
+
const value = stripClaudeCommandQuotes(token);
|
|
1705
|
+
return value === "-c" || value === "-lc";
|
|
1706
|
+
});
|
|
1707
|
+
if (flagIndex >= 0 && flagIndex + 1 < tokens.length) {
|
|
1708
|
+
const shellCommand = stripClaudeCommandQuotes(tokens[flagIndex + 1]);
|
|
1709
|
+
return resolveShellCommandClaudeBinary(shellCommand) ?? resolveRuntimeCommandBinary(shellCommand);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return first;
|
|
1713
|
+
}
|
|
1714
|
+
function stripClaudeCommandQuotes(value) {
|
|
1715
|
+
return value.replace(/^['"]|['"]$/g, "");
|
|
1716
|
+
}
|
|
1717
|
+
function tokenizeRuntimeCommand(command) {
|
|
1718
|
+
return command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
1719
|
+
}
|
|
1720
|
+
function resolveShellCommandClaudeBinary(command) {
|
|
1721
|
+
for (const segment of command.split(/&&|\|\||[;\n]/g)) {
|
|
1722
|
+
const tokens = tokenizeRuntimeCommand(segment);
|
|
1723
|
+
for (const token of tokens) {
|
|
1724
|
+
const value = stripClaudeCommandQuotes(token);
|
|
1725
|
+
if (value.includes("=") && !value.startsWith("/") && !value.startsWith("./")) {
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
if (isClaudeBinaryName(value)) {
|
|
1729
|
+
return value;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
function isClaudeBinaryName(command) {
|
|
1736
|
+
const normalized = (command.split(/[\\/]/).pop() ?? command).toLowerCase().replace(/\.(exe|cmd|bat)$/i, "");
|
|
1737
|
+
return normalized === "claude" || normalized === "claude-code";
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// ../runtime-claude/src/adapter.ts
|
|
1741
|
+
import { randomUUID } from "crypto";
|
|
1742
|
+
import { rm } from "fs/promises";
|
|
1743
|
+
import { join as join5 } from "path";
|
|
1744
|
+
|
|
1745
|
+
// ../runtime-claude/src/argv.ts
|
|
1746
|
+
var DEFAULT_CLAUDE_PRINT_ARGS = [
|
|
1747
|
+
"-p",
|
|
1748
|
+
"--output-format",
|
|
1749
|
+
"stream-json",
|
|
1750
|
+
"--input-format",
|
|
1751
|
+
"stream-json",
|
|
1752
|
+
"--include-partial-messages",
|
|
1753
|
+
// Claude stream-json output requires verbose mode when partial message
|
|
1754
|
+
// events are included; keep this even when callers provide custom args.
|
|
1755
|
+
"--verbose",
|
|
1756
|
+
"--permission-mode",
|
|
1757
|
+
"bypassPermissions"
|
|
1758
|
+
];
|
|
1759
|
+
function buildClaudePrintArgv(options = {}) {
|
|
1760
|
+
const args = options.baseArgs ? withRequiredClaudePrintArgs(options.baseArgs) : [...DEFAULT_CLAUDE_PRINT_ARGS];
|
|
1761
|
+
const { session, isolation, extraArgs } = options;
|
|
1762
|
+
if (session?.mode === "start") {
|
|
1763
|
+
ensureFlagValue(args, "--session-id", session.sessionId);
|
|
1764
|
+
}
|
|
1765
|
+
if (session?.mode === "resume") {
|
|
1766
|
+
ensureFlagValue(args, "--resume", session.sessionId);
|
|
1767
|
+
if (session.forkSession) {
|
|
1768
|
+
ensureFlag(args, "--fork-session");
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (isolation?.bare) {
|
|
1772
|
+
ensureFlag(args, "--bare");
|
|
1773
|
+
}
|
|
1774
|
+
if (isolation?.strictMcpConfig) {
|
|
1775
|
+
ensureFlag(args, "--strict-mcp-config");
|
|
1776
|
+
if (isolation.mcpConfigPath) {
|
|
1777
|
+
ensureFlagValue(args, "--mcp-config", isolation.mcpConfigPath);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (extraArgs?.length) {
|
|
1781
|
+
args.push(...extraArgs);
|
|
1782
|
+
}
|
|
1783
|
+
return args;
|
|
1784
|
+
}
|
|
1785
|
+
function withRequiredClaudePrintArgs(baseArgs) {
|
|
1786
|
+
const args = [...baseArgs];
|
|
1787
|
+
ensureFlag(args, "-p");
|
|
1788
|
+
ensureFlagValue(args, "--output-format", "stream-json");
|
|
1789
|
+
ensureFlagValue(args, "--input-format", "stream-json");
|
|
1790
|
+
ensureFlag(args, "--include-partial-messages");
|
|
1791
|
+
ensureFlag(args, "--verbose");
|
|
1792
|
+
ensureFlagValue(args, "--permission-mode", "bypassPermissions");
|
|
1793
|
+
return args;
|
|
1794
|
+
}
|
|
1795
|
+
function ensureFlag(args, flag) {
|
|
1796
|
+
if (!args.includes(flag)) {
|
|
1797
|
+
args.push(flag);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function ensureFlagValue(args, flag, value) {
|
|
1801
|
+
const index = args.indexOf(flag);
|
|
1802
|
+
if (index === -1) {
|
|
1803
|
+
args.push(flag, value);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const existingValue = args[index + 1];
|
|
1807
|
+
if (existingValue?.startsWith("-")) {
|
|
1808
|
+
args.splice(index + 1, 0, value);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (existingValue !== value) {
|
|
1812
|
+
args.splice(index + 1, existingValue === void 0 ? 0 : 1, value);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// ../runtime-claude/src/mcp-compose.ts
|
|
1817
|
+
import { mkdir, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
1818
|
+
import { basename, dirname, join as join3, resolve as resolve4 } from "path";
|
|
1819
|
+
|
|
1820
|
+
// ../tool-github-graphql/src/tool.ts
|
|
1821
|
+
import { readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
1822
|
+
var DEFAULT_GITHUB_GRAPHQL_API_URL = "https://api.github.com/graphql";
|
|
1823
|
+
var TOKEN_REUSE_WINDOW_MS2 = 60 * 1e3;
|
|
1824
|
+
async function executeGitHubGraphQL(invocation, config, fetchImpl = fetch) {
|
|
1825
|
+
const token = await resolveGitHubGraphQLToken(config, {
|
|
1826
|
+
fetchImpl
|
|
1827
|
+
});
|
|
1828
|
+
const response = await fetchImpl(
|
|
1829
|
+
config.apiUrl ?? DEFAULT_GITHUB_GRAPHQL_API_URL,
|
|
1830
|
+
{
|
|
1831
|
+
method: "POST",
|
|
1832
|
+
headers: {
|
|
1833
|
+
"content-type": "application/json",
|
|
1834
|
+
authorization: `Bearer ${token}`
|
|
1835
|
+
},
|
|
1836
|
+
body: JSON.stringify(invocation)
|
|
1837
|
+
}
|
|
1838
|
+
);
|
|
1839
|
+
const payload = await response.json();
|
|
1840
|
+
if (!response.ok) {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
`GitHub GraphQL request failed with status ${response.status}: ${JSON.stringify(payload)}`
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
if (payload.errors?.length) {
|
|
1846
|
+
throw new Error(payload.errors.map((error) => error.message).join("; "));
|
|
1847
|
+
}
|
|
1848
|
+
return payload;
|
|
1849
|
+
}
|
|
1850
|
+
async function resolveGitHubGraphQLToken(config, dependencies = {}) {
|
|
1851
|
+
if (config.token) {
|
|
1852
|
+
return config.token;
|
|
1853
|
+
}
|
|
1854
|
+
if (!config.tokenBrokerUrl || !config.tokenBrokerSecret) {
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
"Either GITHUB_GRAPHQL_TOKEN or the runtime token broker configuration is required."
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
const now = dependencies.now ?? /* @__PURE__ */ new Date();
|
|
1860
|
+
const readFileImpl = dependencies.readFileImpl ?? readFile5;
|
|
1861
|
+
const writeFileImpl = dependencies.writeFileImpl ?? writeFile2;
|
|
1862
|
+
const cachedToken = config.tokenCachePath ? await readCachedToken(config.tokenCachePath, readFileImpl) : null;
|
|
1863
|
+
if (cachedToken && cachedToken.expiresAt.getTime() - now.getTime() > TOKEN_REUSE_WINDOW_MS2) {
|
|
1864
|
+
return cachedToken.token;
|
|
1865
|
+
}
|
|
1866
|
+
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
1867
|
+
const response = await fetchImpl(config.tokenBrokerUrl, {
|
|
1868
|
+
method: "POST",
|
|
1869
|
+
headers: {
|
|
1870
|
+
accept: "application/json",
|
|
1871
|
+
authorization: `Bearer ${config.tokenBrokerSecret}`
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
const payload = await response.json();
|
|
1875
|
+
if (!response.ok || !payload.token || !payload.expiresAt) {
|
|
1876
|
+
throw new Error(
|
|
1877
|
+
payload.error ?? `Runtime token broker request failed with status ${response.status}.`
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
if (config.tokenCachePath) {
|
|
1881
|
+
await writeFileImpl(config.tokenCachePath, JSON.stringify(payload), "utf8");
|
|
1882
|
+
}
|
|
1883
|
+
return payload.token;
|
|
1884
|
+
}
|
|
1885
|
+
async function readStdin() {
|
|
1886
|
+
const chunks = [];
|
|
1887
|
+
for await (const chunk of process.stdin) {
|
|
1888
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1889
|
+
}
|
|
1890
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1891
|
+
}
|
|
1892
|
+
async function main() {
|
|
1893
|
+
const rawInput = await readStdin();
|
|
1894
|
+
const invocation = JSON.parse(rawInput);
|
|
1895
|
+
const result = await executeGitHubGraphQL(invocation, {
|
|
1896
|
+
token: process.env.GITHUB_GRAPHQL_TOKEN,
|
|
1897
|
+
apiUrl: process.env.GITHUB_GRAPHQL_API_URL,
|
|
1898
|
+
tokenBrokerUrl: process.env.GITHUB_TOKEN_BROKER_URL,
|
|
1899
|
+
tokenBrokerSecret: process.env.GITHUB_TOKEN_BROKER_SECRET,
|
|
1900
|
+
tokenCachePath: process.env.GITHUB_TOKEN_CACHE_PATH
|
|
1901
|
+
});
|
|
1902
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
1903
|
+
`);
|
|
1904
|
+
}
|
|
1905
|
+
if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
|
|
1906
|
+
main().catch((error) => {
|
|
1907
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1908
|
+
process.stderr.write(`${message}
|
|
1909
|
+
`);
|
|
1910
|
+
process.exitCode = 1;
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
async function readCachedToken(path, readFileImpl) {
|
|
1914
|
+
try {
|
|
1915
|
+
const payload = JSON.parse(await readFileImpl(path, "utf8"));
|
|
1916
|
+
if (!payload.token || !payload.expiresAt) {
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
token: payload.token,
|
|
1921
|
+
expiresAt: new Date(payload.expiresAt)
|
|
1922
|
+
};
|
|
1923
|
+
} catch {
|
|
1924
|
+
return null;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// ../tool-github-graphql/src/mcp-server.ts
|
|
1929
|
+
import { fileURLToPath } from "url";
|
|
1930
|
+
var TOOL_SCHEMA = {
|
|
1931
|
+
name: "github_graphql",
|
|
1932
|
+
description: "Execute GitHub GraphQL queries for the active workspace so the agent can mutate project and issue state directly.",
|
|
1933
|
+
inputSchema: {
|
|
1934
|
+
type: "object",
|
|
1935
|
+
properties: {
|
|
1936
|
+
query: {
|
|
1937
|
+
type: "string",
|
|
1938
|
+
description: "GraphQL query or mutation document."
|
|
1939
|
+
},
|
|
1940
|
+
variables: {
|
|
1941
|
+
type: "object",
|
|
1942
|
+
description: "Variables for the GraphQL document."
|
|
1943
|
+
},
|
|
1944
|
+
operationName: {
|
|
1945
|
+
type: "string",
|
|
1946
|
+
description: "Optional GraphQL operation name."
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
required: ["query"],
|
|
1950
|
+
additionalProperties: false
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var lineBuffer = "";
|
|
1954
|
+
function resolveGitHubGraphQLMcpServerEntryPoint() {
|
|
1955
|
+
return fileURLToPath(new URL("./mcp-server.js", import.meta.url));
|
|
1956
|
+
}
|
|
1957
|
+
function sendResponse(id, result) {
|
|
1958
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
1959
|
+
process.stdout.write(msg + "\n");
|
|
1960
|
+
}
|
|
1961
|
+
function sendError(id, code, message) {
|
|
1962
|
+
const msg = JSON.stringify({
|
|
1963
|
+
jsonrpc: "2.0",
|
|
1964
|
+
id,
|
|
1965
|
+
error: { code, message }
|
|
1966
|
+
});
|
|
1967
|
+
process.stdout.write(msg + "\n");
|
|
1968
|
+
}
|
|
1969
|
+
async function handleRequest(msg) {
|
|
1970
|
+
const id = msg.id ?? null;
|
|
1971
|
+
switch (msg.method) {
|
|
1972
|
+
case "initialize": {
|
|
1973
|
+
sendResponse(id, {
|
|
1974
|
+
protocolVersion: "2024-11-05",
|
|
1975
|
+
capabilities: { tools: {} },
|
|
1976
|
+
serverInfo: {
|
|
1977
|
+
name: "github-symphony-graphql",
|
|
1978
|
+
version: "0.1.0"
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
process.stdout.write(
|
|
1982
|
+
JSON.stringify({
|
|
1983
|
+
jsonrpc: "2.0",
|
|
1984
|
+
method: "notifications/initialized"
|
|
1985
|
+
}) + "\n"
|
|
1986
|
+
);
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
case "tools/list": {
|
|
1990
|
+
sendResponse(id, { tools: [TOOL_SCHEMA] });
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
case "tools/call": {
|
|
1994
|
+
const params = msg.params;
|
|
1995
|
+
if (params.name !== "github_graphql") {
|
|
1996
|
+
sendError(id, -32602, `Unknown tool: ${params.name}`);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const args = params.arguments ?? {};
|
|
2000
|
+
const invocation = {
|
|
2001
|
+
query: args.query,
|
|
2002
|
+
variables: args.variables,
|
|
2003
|
+
operationName: args.operationName
|
|
2004
|
+
};
|
|
2005
|
+
try {
|
|
2006
|
+
const result = await executeGitHubGraphQL(invocation, {
|
|
2007
|
+
token: process.env.GITHUB_GRAPHQL_TOKEN,
|
|
2008
|
+
apiUrl: process.env.GITHUB_GRAPHQL_API_URL,
|
|
2009
|
+
tokenBrokerUrl: process.env.GITHUB_TOKEN_BROKER_URL,
|
|
2010
|
+
tokenBrokerSecret: process.env.GITHUB_TOKEN_BROKER_SECRET,
|
|
2011
|
+
tokenCachePath: process.env.GITHUB_TOKEN_CACHE_PATH
|
|
2012
|
+
});
|
|
2013
|
+
sendResponse(id, {
|
|
2014
|
+
content: [
|
|
2015
|
+
{
|
|
2016
|
+
type: "text",
|
|
2017
|
+
text: JSON.stringify(result, null, 2)
|
|
2018
|
+
}
|
|
2019
|
+
]
|
|
2020
|
+
});
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2023
|
+
sendResponse(id, {
|
|
2024
|
+
content: [{ type: "text", text: message }],
|
|
2025
|
+
isError: true
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
case "notifications/initialized":
|
|
2031
|
+
case "ping": {
|
|
2032
|
+
if (id !== null && id !== void 0) {
|
|
2033
|
+
sendResponse(id, {});
|
|
2034
|
+
}
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
default: {
|
|
2038
|
+
if (id !== null && id !== void 0) {
|
|
2039
|
+
sendError(id, -32601, `Method not found: ${msg.method}`);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
async function main2() {
|
|
2045
|
+
process.stdin.setEncoding("utf8");
|
|
2046
|
+
process.stdin.on("data", (chunk) => {
|
|
2047
|
+
lineBuffer += chunk;
|
|
2048
|
+
const lines = lineBuffer.split("\n");
|
|
2049
|
+
lineBuffer = lines.pop() ?? "";
|
|
2050
|
+
for (const line of lines) {
|
|
2051
|
+
const trimmed = line.trim();
|
|
2052
|
+
if (!trimmed) continue;
|
|
2053
|
+
try {
|
|
2054
|
+
const msg = JSON.parse(trimmed);
|
|
2055
|
+
void handleRequest(msg);
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
process.stderr.write(
|
|
2058
|
+
`[github-graphql-mcp] parse error: ${err instanceof Error ? err.message : String(err)}
|
|
2059
|
+
`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
process.stdin.on("end", () => {
|
|
2065
|
+
process.exit(0);
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) {
|
|
2069
|
+
main2().catch((err) => {
|
|
2070
|
+
process.stderr.write(
|
|
2071
|
+
`[github-graphql-mcp] fatal: ${err instanceof Error ? err.message : String(err)}
|
|
2072
|
+
`
|
|
2073
|
+
);
|
|
2074
|
+
process.exitCode = 1;
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// ../tool-github-graphql/src/mcp-entry.ts
|
|
2079
|
+
var DEFAULT_GITHUB_GRAPHQL_API_URL2 = "https://api.github.com/graphql";
|
|
2080
|
+
function createGitHubGraphQLMcpServerEntry(options = {}) {
|
|
2081
|
+
return {
|
|
2082
|
+
command: "node",
|
|
2083
|
+
args: [resolveGitHubGraphQLMcpServerEntryPoint()],
|
|
2084
|
+
env: {
|
|
2085
|
+
GITHUB_GRAPHQL_API_URL: options.githubGraphqlApiUrl ?? DEFAULT_GITHUB_GRAPHQL_API_URL2,
|
|
2086
|
+
...options.githubToken ? {
|
|
2087
|
+
GITHUB_GRAPHQL_TOKEN: options.githubToken
|
|
2088
|
+
} : {},
|
|
2089
|
+
...options.githubTokenBrokerUrl ? {
|
|
2090
|
+
GITHUB_TOKEN_BROKER_URL: options.githubTokenBrokerUrl
|
|
2091
|
+
} : {},
|
|
2092
|
+
...options.githubTokenBrokerSecret ? {
|
|
2093
|
+
GITHUB_TOKEN_BROKER_SECRET: options.githubTokenBrokerSecret
|
|
2094
|
+
} : {},
|
|
2095
|
+
...options.githubTokenCachePath ? {
|
|
2096
|
+
GITHUB_TOKEN_CACHE_PATH: options.githubTokenCachePath
|
|
2097
|
+
} : {},
|
|
2098
|
+
...options.githubProjectId ? {
|
|
2099
|
+
GITHUB_PROJECT_ID: options.githubProjectId
|
|
2100
|
+
} : {}
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// ../runtime-claude/src/mcp-compose.ts
|
|
2106
|
+
async function composeClaudeMcpConfig(workspaceRoot, strictMode, symphonyTokenEnv = {}) {
|
|
2107
|
+
const workspaceMcpPath = join3(workspaceRoot, ".mcp.json");
|
|
2108
|
+
const finalPath = strictMode ? resolveStrictMcpConfigPath(workspaceRoot, symphonyTokenEnv) : workspaceMcpPath;
|
|
2109
|
+
const baseConfig = await readBaseMcpConfig(workspaceMcpPath);
|
|
2110
|
+
const mergedConfig = mergeGitHubGraphQLMcpServer(baseConfig, symphonyTokenEnv);
|
|
2111
|
+
await mkdir(dirname(finalPath), { recursive: true });
|
|
2112
|
+
await writeFile3(finalPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf8");
|
|
2113
|
+
return {
|
|
2114
|
+
finalPath,
|
|
2115
|
+
extraArgv: strictMode ? ["--strict-mcp-config", "--mcp-config", finalPath] : [],
|
|
2116
|
+
...strictMode ? { cleanupPath: finalPath } : {}
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
async function readBaseMcpConfig(workspaceMcpPath) {
|
|
2120
|
+
try {
|
|
2121
|
+
const raw = await readFile6(workspaceMcpPath, "utf8");
|
|
2122
|
+
const parsed = JSON.parse(raw);
|
|
2123
|
+
return isRecord3(parsed) ? parsed : { mcpServers: {} };
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
2126
|
+
return { mcpServers: {} };
|
|
2127
|
+
}
|
|
2128
|
+
throw error;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
function mergeGitHubGraphQLMcpServer(baseConfig, env) {
|
|
2132
|
+
const mcpServers = isRecord3(baseConfig.mcpServers) ? baseConfig.mcpServers : {};
|
|
2133
|
+
return {
|
|
2134
|
+
...baseConfig,
|
|
2135
|
+
mcpServers: {
|
|
2136
|
+
...mcpServers,
|
|
2137
|
+
github_graphql: createGitHubGraphQLMcpServerEntry({
|
|
2138
|
+
githubToken: env.GITHUB_GRAPHQL_TOKEN,
|
|
2139
|
+
githubGraphqlApiUrl: env.GITHUB_GRAPHQL_API_URL,
|
|
2140
|
+
githubTokenBrokerUrl: env.GITHUB_TOKEN_BROKER_URL,
|
|
2141
|
+
githubTokenBrokerSecret: env.GITHUB_TOKEN_BROKER_SECRET,
|
|
2142
|
+
githubTokenCachePath: env.GITHUB_TOKEN_CACHE_PATH,
|
|
2143
|
+
githubProjectId: env.GITHUB_PROJECT_ID
|
|
2144
|
+
})
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function resolveStrictMcpConfigPath(workspaceRoot, env) {
|
|
2149
|
+
const normalizedWorkspaceRoot = resolve4(workspaceRoot);
|
|
2150
|
+
const runtimeDir = env.WORKSPACE_RUNTIME_DIR ?? join3(normalizedWorkspaceRoot, ".runtime", basename(normalizedWorkspaceRoot));
|
|
2151
|
+
return join3(runtimeDir, "mcp.json");
|
|
2152
|
+
}
|
|
2153
|
+
function isRecord3(value) {
|
|
2154
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
2155
|
+
}
|
|
2156
|
+
function isNodeError(error) {
|
|
2157
|
+
return error instanceof Error && "code" in error;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// ../runtime-claude/src/spawn.ts
|
|
2161
|
+
import { spawn as spawn2 } from "child_process";
|
|
2162
|
+
import { finished } from "stream/promises";
|
|
2163
|
+
|
|
2164
|
+
// ../runtime-claude/src/internal.ts
|
|
2165
|
+
function asRecord(value) {
|
|
2166
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2167
|
+
}
|
|
2168
|
+
function getString(value) {
|
|
2169
|
+
if (typeof value === "string") {
|
|
2170
|
+
return value;
|
|
2171
|
+
}
|
|
2172
|
+
if (typeof value === "number") {
|
|
2173
|
+
return String(value);
|
|
2174
|
+
}
|
|
2175
|
+
return void 0;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// ../runtime-claude/src/events.ts
|
|
2179
|
+
var CLAUDE_OBSERVABILITY_PREFIX = "claude-print/";
|
|
2180
|
+
function parseClaudePrintNdjsonLine(line) {
|
|
2181
|
+
const trimmedLine = line.trim();
|
|
2182
|
+
if (!trimmedLine) {
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
try {
|
|
2186
|
+
const parsed = JSON.parse(trimmedLine);
|
|
2187
|
+
if (!asRecord(parsed)) {
|
|
2188
|
+
return {
|
|
2189
|
+
line: trimmedLine,
|
|
2190
|
+
parseError: "Claude stream-json line is not a JSON object."
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
return {
|
|
2194
|
+
line: trimmedLine,
|
|
2195
|
+
message: parsed
|
|
2196
|
+
};
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
return {
|
|
2199
|
+
line: trimmedLine,
|
|
2200
|
+
parseError: error instanceof Error ? error.message : "Unknown JSON parse error."
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
var ClaudePrintEventMapper = class {
|
|
2205
|
+
constructor(options = {}) {
|
|
2206
|
+
this.options = options;
|
|
2207
|
+
}
|
|
2208
|
+
hasStartedTurn = false;
|
|
2209
|
+
latestResultEvent = null;
|
|
2210
|
+
// Claude -p stream-json does not define an ordered collection of error
|
|
2211
|
+
// records for one turn; keep the terminal/latest error for exit classification.
|
|
2212
|
+
latestErrorEvent = null;
|
|
2213
|
+
sawRateLimit = false;
|
|
2214
|
+
mapLine(line) {
|
|
2215
|
+
const record = parseClaudePrintNdjsonLine(line);
|
|
2216
|
+
if (!record?.message) {
|
|
2217
|
+
return [];
|
|
2218
|
+
}
|
|
2219
|
+
return this.mapMessage(record.message);
|
|
2220
|
+
}
|
|
2221
|
+
mapMessage(message) {
|
|
2222
|
+
const type = getEventType(message);
|
|
2223
|
+
const events = [];
|
|
2224
|
+
if (type === "message_start") {
|
|
2225
|
+
events.push(this.buildTurnStartedEvent(message, type));
|
|
2226
|
+
this.hasStartedTurn = true;
|
|
2227
|
+
return events;
|
|
2228
|
+
}
|
|
2229
|
+
if (type === "content_block_start" || type === "tool_use") {
|
|
2230
|
+
if (!this.hasStartedTurn) {
|
|
2231
|
+
events.push(this.buildTurnStartedEvent(message, type));
|
|
2232
|
+
this.hasStartedTurn = true;
|
|
2233
|
+
}
|
|
2234
|
+
const toolUseEvent = mapToolUseEvent(message, this.options);
|
|
2235
|
+
if (toolUseEvent) {
|
|
2236
|
+
events.push(toolUseEvent);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
if (type === "content_block_delta") {
|
|
2240
|
+
if (!this.hasStartedTurn) {
|
|
2241
|
+
events.push(this.buildTurnStartedEvent(message, type));
|
|
2242
|
+
this.hasStartedTurn = true;
|
|
2243
|
+
}
|
|
2244
|
+
events.push({
|
|
2245
|
+
name: "agent.messageDelta",
|
|
2246
|
+
payload: {
|
|
2247
|
+
observabilityEvent: observabilityEventName(type),
|
|
2248
|
+
params: message,
|
|
2249
|
+
delta: extractDeltaText(message),
|
|
2250
|
+
itemId: extractItemId(message)
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
if (type === "result") {
|
|
2255
|
+
this.latestResultEvent = message;
|
|
2256
|
+
const rateLimit = extractRateLimit(message);
|
|
2257
|
+
if (rateLimit) {
|
|
2258
|
+
this.sawRateLimit = true;
|
|
2259
|
+
events.push({
|
|
2260
|
+
name: "agent.rateLimit",
|
|
2261
|
+
payload: {
|
|
2262
|
+
observabilityEvent: observabilityEventName(type),
|
|
2263
|
+
params: {
|
|
2264
|
+
source: "claude",
|
|
2265
|
+
rate_limit: rateLimit,
|
|
2266
|
+
usage: asRecord(message.usage),
|
|
2267
|
+
result: message
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
if (isClaudeResultError(message)) {
|
|
2273
|
+
events.push(buildClaudeErrorEvent(message, type));
|
|
2274
|
+
} else {
|
|
2275
|
+
events.push({
|
|
2276
|
+
name: "agent.turnCompleted",
|
|
2277
|
+
payload: {
|
|
2278
|
+
observabilityEvent: observabilityEventName(type),
|
|
2279
|
+
params: message,
|
|
2280
|
+
inputRequired: false
|
|
2281
|
+
}
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (type === "error") {
|
|
2286
|
+
this.latestErrorEvent = message;
|
|
2287
|
+
events.push(buildClaudeErrorEvent(message, type));
|
|
2288
|
+
}
|
|
2289
|
+
return events;
|
|
2290
|
+
}
|
|
2291
|
+
snapshot() {
|
|
2292
|
+
return {
|
|
2293
|
+
hasStartedTurn: this.hasStartedTurn,
|
|
2294
|
+
latestResultEvent: this.latestResultEvent,
|
|
2295
|
+
latestErrorEvent: this.latestErrorEvent,
|
|
2296
|
+
sawRateLimit: this.sawRateLimit
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
buildTurnStartedEvent(message, type) {
|
|
2300
|
+
return {
|
|
2301
|
+
name: "agent.turnStarted",
|
|
2302
|
+
payload: {
|
|
2303
|
+
observabilityEvent: observabilityEventName(type),
|
|
2304
|
+
params: message
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
function isClaudeResultError(message) {
|
|
2310
|
+
const subtype = getString(message.subtype);
|
|
2311
|
+
const stopReason = getString(message.stop_reason);
|
|
2312
|
+
return message.is_error === true || subtype !== void 0 && subtype.startsWith("error") || stopReason !== void 0 && stopReason.startsWith("error");
|
|
2313
|
+
}
|
|
2314
|
+
function extractRateLimit(message) {
|
|
2315
|
+
const usage = asRecord(message.usage);
|
|
2316
|
+
const rateLimit = usage ? asRecord(usage.rate_limit) : null;
|
|
2317
|
+
if (rateLimit) {
|
|
2318
|
+
return rateLimit;
|
|
2319
|
+
}
|
|
2320
|
+
return asRecord(message.rate_limit);
|
|
2321
|
+
}
|
|
2322
|
+
function getClaudeResultStatus(message) {
|
|
2323
|
+
if (!message) {
|
|
2324
|
+
return void 0;
|
|
2325
|
+
}
|
|
2326
|
+
return getString(message.subtype) ?? getString(message.stop_reason);
|
|
2327
|
+
}
|
|
2328
|
+
function mapToolUseEvent(message, options) {
|
|
2329
|
+
const type = getEventType(message);
|
|
2330
|
+
const contentBlock = asRecord(message.content_block);
|
|
2331
|
+
const toolUse = type === "tool_use" ? message : contentBlock && getString(contentBlock.type) === "tool_use" ? contentBlock : null;
|
|
2332
|
+
if (!toolUse) {
|
|
2333
|
+
return null;
|
|
2334
|
+
}
|
|
2335
|
+
const input = toolUse.input !== void 0 ? toolUse.input : toolUse.arguments;
|
|
2336
|
+
return {
|
|
2337
|
+
name: "agent.toolCallRequested",
|
|
2338
|
+
payload: {
|
|
2339
|
+
observabilityEvent: observabilityEventName(type),
|
|
2340
|
+
params: message,
|
|
2341
|
+
callId: getString(toolUse.id) ?? "",
|
|
2342
|
+
toolName: getString(toolUse.name) ?? "",
|
|
2343
|
+
threadId: options.threadId ?? getString(message.thread_id),
|
|
2344
|
+
turnId: options.turnId ?? getString(message.turn_id),
|
|
2345
|
+
arguments: input
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
function buildClaudeErrorEvent(message, type) {
|
|
2350
|
+
return {
|
|
2351
|
+
name: "agent.error",
|
|
2352
|
+
payload: {
|
|
2353
|
+
observabilityEvent: observabilityEventName(type),
|
|
2354
|
+
params: message,
|
|
2355
|
+
error: describeClaudeError(message)
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function describeClaudeError(message) {
|
|
2360
|
+
const error = asRecord(message.error);
|
|
2361
|
+
return getString(error?.message) ?? getString(error?.type) ?? getString(message.message) ?? getString(message.subtype) ?? getString(message.stop_reason) ?? JSON.stringify(message);
|
|
2362
|
+
}
|
|
2363
|
+
function extractDeltaText(message) {
|
|
2364
|
+
const delta = asRecord(message.delta);
|
|
2365
|
+
return getString(delta?.text) ?? getString(delta?.partial_json) ?? getString(message.text) ?? "";
|
|
2366
|
+
}
|
|
2367
|
+
function extractItemId(message) {
|
|
2368
|
+
return getString(message.item_id) ?? getString(message.content_block_id) ?? getString(message.index) ?? "";
|
|
2369
|
+
}
|
|
2370
|
+
function getEventType(message) {
|
|
2371
|
+
return getString(message.type) ?? "";
|
|
2372
|
+
}
|
|
2373
|
+
function observabilityEventName(type) {
|
|
2374
|
+
return `${CLAUDE_OBSERVABILITY_PREFIX}${type || "unknown"}`;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// ../runtime-claude/src/exit-classifier.ts
|
|
2378
|
+
var TRANSIENT_ERROR_PATTERNS = [
|
|
2379
|
+
/rate.?limit/i,
|
|
2380
|
+
/\b429\b/,
|
|
2381
|
+
/timeout/i,
|
|
2382
|
+
/timed?.?out/i,
|
|
2383
|
+
/temporar/i,
|
|
2384
|
+
/overload/i,
|
|
2385
|
+
/unavailable/i,
|
|
2386
|
+
/ECONNRESET/,
|
|
2387
|
+
/ETIMEDOUT/,
|
|
2388
|
+
/EAI_AGAIN/
|
|
2389
|
+
];
|
|
2390
|
+
function classifyClaudeTurnExit(input) {
|
|
2391
|
+
const resultStatus = getClaudeResultStatus(input.resultEvent);
|
|
2392
|
+
if (input.exitCode === 0 && input.resultEvent && !isClaudeResultError(input.resultEvent)) {
|
|
2393
|
+
return {
|
|
2394
|
+
kind: "success",
|
|
2395
|
+
transient: false,
|
|
2396
|
+
reason: "result_success",
|
|
2397
|
+
resultStatus
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
if (input.exitCode === 0 && !input.resultEvent) {
|
|
2401
|
+
return {
|
|
2402
|
+
kind: "app-error",
|
|
2403
|
+
transient: false,
|
|
2404
|
+
reason: "missing_result",
|
|
2405
|
+
resultStatus
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
if (input.exitCode === 0 && input.resultEvent && isClaudeResultError(input.resultEvent)) {
|
|
2409
|
+
return {
|
|
2410
|
+
kind: "app-error",
|
|
2411
|
+
transient: isTransientClaudeFailure(input),
|
|
2412
|
+
reason: resultStatus ?? "result_error",
|
|
2413
|
+
resultStatus
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
return {
|
|
2417
|
+
kind: "process-error",
|
|
2418
|
+
transient: isTransientClaudeFailure(input),
|
|
2419
|
+
reason: describeProcessFailure(input),
|
|
2420
|
+
resultStatus
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
function isTransientClaudeFailure(input) {
|
|
2424
|
+
if (input.sawRateLimit || input.resultEvent && (extractRateLimit(input.resultEvent) !== null || getClaudeResultStatus(input.resultEvent) === "error_rate_limit")) {
|
|
2425
|
+
return true;
|
|
2426
|
+
}
|
|
2427
|
+
if (input.signal === "SIGTERM") {
|
|
2428
|
+
return true;
|
|
2429
|
+
}
|
|
2430
|
+
const text = [
|
|
2431
|
+
input.spawnErrorMessage,
|
|
2432
|
+
extractFailureMessage(input.errorEvent),
|
|
2433
|
+
extractFailureMessage(input.resultEvent)
|
|
2434
|
+
].join("\n");
|
|
2435
|
+
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(text));
|
|
2436
|
+
}
|
|
2437
|
+
function describeProcessFailure(input) {
|
|
2438
|
+
if (input.signal) {
|
|
2439
|
+
return `signal_${input.signal}`;
|
|
2440
|
+
}
|
|
2441
|
+
if (input.spawnErrorMessage) {
|
|
2442
|
+
return input.spawnErrorMessage;
|
|
2443
|
+
}
|
|
2444
|
+
if (typeof input.exitCode === "number") {
|
|
2445
|
+
return `exit_${input.exitCode}`;
|
|
2446
|
+
}
|
|
2447
|
+
return "process_error";
|
|
2448
|
+
}
|
|
2449
|
+
function extractFailureMessage(event) {
|
|
2450
|
+
if (!event) {
|
|
2451
|
+
return "";
|
|
2452
|
+
}
|
|
2453
|
+
const error = asRecord(event.error);
|
|
2454
|
+
return [
|
|
2455
|
+
getString(error?.message),
|
|
2456
|
+
getString(error?.type),
|
|
2457
|
+
getString(event.message)
|
|
2458
|
+
].filter((value) => value !== void 0).join("\n");
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// ../runtime-claude/src/spawn.ts
|
|
2462
|
+
async function spawnClaudeTurn(input, dependencies = {}) {
|
|
2463
|
+
const command = input.command ?? "claude";
|
|
2464
|
+
const child = (dependencies.spawnImpl ?? spawn2)(command, input.args, {
|
|
2465
|
+
cwd: input.cwd,
|
|
2466
|
+
env: input.env,
|
|
2467
|
+
stdio: "pipe"
|
|
2468
|
+
});
|
|
2469
|
+
dependencies.onSpawned?.(child);
|
|
2470
|
+
const records = [];
|
|
2471
|
+
const eventMapper = new ClaudePrintEventMapper();
|
|
2472
|
+
let emittedErrorEvent = false;
|
|
2473
|
+
const emitEvent = (event) => {
|
|
2474
|
+
if (event.name === "agent.error") {
|
|
2475
|
+
emittedErrorEvent = true;
|
|
2476
|
+
}
|
|
2477
|
+
dependencies.onEvent?.(event);
|
|
2478
|
+
};
|
|
2479
|
+
const stdoutDone = collectNdjsonStream(
|
|
2480
|
+
child.stdout,
|
|
2481
|
+
"stdout",
|
|
2482
|
+
records,
|
|
2483
|
+
eventMapper,
|
|
2484
|
+
emitEvent
|
|
2485
|
+
);
|
|
2486
|
+
const stderrDone = collectNdjsonStream(
|
|
2487
|
+
child.stderr,
|
|
2488
|
+
"stderr",
|
|
2489
|
+
records,
|
|
2490
|
+
null,
|
|
2491
|
+
null
|
|
2492
|
+
);
|
|
2493
|
+
const exitDone = waitForChildExit(child, records);
|
|
2494
|
+
const stdinMessages = Array.isArray(input.stdinMessages) ? input.stdinMessages : [input.stdinMessages];
|
|
2495
|
+
for (const message of stdinMessages) {
|
|
2496
|
+
const didWrite = await writeToStdin(
|
|
2497
|
+
child.stdin,
|
|
2498
|
+
`${JSON.stringify(message)}
|
|
2499
|
+
`
|
|
2500
|
+
);
|
|
2501
|
+
if (!didWrite) {
|
|
2502
|
+
break;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
if (child.stdin && !child.stdin.destroyed && !child.stdin.writableEnded && !child.stdin.writableFinished) {
|
|
2506
|
+
child.stdin.end();
|
|
2507
|
+
}
|
|
2508
|
+
const outcome = await exitDone;
|
|
2509
|
+
await Promise.all([stdoutDone, stderrDone]);
|
|
2510
|
+
const mapperState = eventMapper.snapshot();
|
|
2511
|
+
const classification = classifyClaudeTurnExit({
|
|
2512
|
+
exitCode: outcome.exitCode,
|
|
2513
|
+
signal: outcome.signal,
|
|
2514
|
+
resultEvent: mapperState.latestResultEvent,
|
|
2515
|
+
errorEvent: mapperState.latestErrorEvent,
|
|
2516
|
+
sawRateLimit: mapperState.sawRateLimit,
|
|
2517
|
+
spawnErrorMessage: "errorMessage" in outcome ? outcome.errorMessage : void 0
|
|
2518
|
+
});
|
|
2519
|
+
if ((classification.kind === "app-error" || classification.kind === "process-error") && !emittedErrorEvent) {
|
|
2520
|
+
emitEvent({
|
|
2521
|
+
name: "agent.error",
|
|
2522
|
+
payload: {
|
|
2523
|
+
observabilityEvent: classification.kind === "app-error" ? "claude-print/app-error" : "claude-print/process-exit",
|
|
2524
|
+
params: {
|
|
2525
|
+
exitCode: outcome.exitCode,
|
|
2526
|
+
signal: outcome.signal,
|
|
2527
|
+
classification,
|
|
2528
|
+
errorMessage: "errorMessage" in outcome ? outcome.errorMessage : void 0
|
|
2529
|
+
},
|
|
2530
|
+
error: classification.reason
|
|
2531
|
+
}
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
return {
|
|
2535
|
+
command,
|
|
2536
|
+
args: [...input.args],
|
|
2537
|
+
cwd: input.cwd,
|
|
2538
|
+
records,
|
|
2539
|
+
exitCode: outcome.exitCode,
|
|
2540
|
+
signal: outcome.signal,
|
|
2541
|
+
result: classification.kind,
|
|
2542
|
+
classification,
|
|
2543
|
+
errorMessage: "errorMessage" in outcome ? outcome.errorMessage : void 0
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
async function collectNdjsonStream(stream, channel, records, eventMapper, onEvent) {
|
|
2547
|
+
if (!stream) {
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
let buffer = "";
|
|
2551
|
+
stream.setEncoding("utf8");
|
|
2552
|
+
stream.on("data", (chunk) => {
|
|
2553
|
+
buffer += chunk;
|
|
2554
|
+
while (true) {
|
|
2555
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
2556
|
+
if (newlineIndex === -1) {
|
|
2557
|
+
break;
|
|
2558
|
+
}
|
|
2559
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
2560
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
2561
|
+
if (line.length === 0) {
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2564
|
+
records.push(parseClaudeRecord(channel, line, eventMapper, onEvent));
|
|
2565
|
+
}
|
|
2566
|
+
});
|
|
2567
|
+
try {
|
|
2568
|
+
await finished(stream);
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
records.push({
|
|
2571
|
+
stream: channel,
|
|
2572
|
+
line: "",
|
|
2573
|
+
parseError: error instanceof Error ? error.message : "Unknown stream error."
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
const trailingLine = buffer.trim();
|
|
2577
|
+
if (trailingLine.length > 0) {
|
|
2578
|
+
records.push(
|
|
2579
|
+
parseClaudeRecord(channel, trailingLine, eventMapper, onEvent)
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
function parseClaudeRecord(stream, line, eventMapper, onEvent) {
|
|
2584
|
+
const record = parseClaudePrintNdjsonLine(line);
|
|
2585
|
+
if (record.message) {
|
|
2586
|
+
if (!eventMapper) {
|
|
2587
|
+
return {
|
|
2588
|
+
stream,
|
|
2589
|
+
line: record.line,
|
|
2590
|
+
message: record.message
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
for (const event of eventMapper.mapMessage(record.message)) {
|
|
2594
|
+
onEvent?.(event);
|
|
2595
|
+
}
|
|
2596
|
+
return {
|
|
2597
|
+
stream,
|
|
2598
|
+
line: record.line,
|
|
2599
|
+
message: record.message
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
return {
|
|
2603
|
+
stream,
|
|
2604
|
+
line: record.line,
|
|
2605
|
+
parseError: record.parseError
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
async function writeToStdin(stream, line) {
|
|
2609
|
+
if (!stream || stream.destroyed || stream.writableEnded) {
|
|
2610
|
+
return false;
|
|
2611
|
+
}
|
|
2612
|
+
if (stream.write(line)) {
|
|
2613
|
+
return true;
|
|
2614
|
+
}
|
|
2615
|
+
return waitForDrainOrClosure(stream);
|
|
2616
|
+
}
|
|
2617
|
+
function waitForDrainOrClosure(stream) {
|
|
2618
|
+
return new Promise((resolve5) => {
|
|
2619
|
+
const cleanup = () => {
|
|
2620
|
+
stream.removeListener("drain", handleDrain);
|
|
2621
|
+
stream.removeListener("close", handleClose);
|
|
2622
|
+
stream.removeListener("finish", handleFinish);
|
|
2623
|
+
stream.removeListener("error", handleError);
|
|
2624
|
+
};
|
|
2625
|
+
const handleDrain = () => {
|
|
2626
|
+
cleanup();
|
|
2627
|
+
resolve5(true);
|
|
2628
|
+
};
|
|
2629
|
+
const handleClose = () => {
|
|
2630
|
+
cleanup();
|
|
2631
|
+
resolve5(false);
|
|
2632
|
+
};
|
|
2633
|
+
const handleFinish = () => {
|
|
2634
|
+
cleanup();
|
|
2635
|
+
resolve5(false);
|
|
2636
|
+
};
|
|
2637
|
+
const handleError = () => {
|
|
2638
|
+
cleanup();
|
|
2639
|
+
resolve5(false);
|
|
2640
|
+
};
|
|
2641
|
+
stream.once("drain", handleDrain);
|
|
2642
|
+
stream.once("close", handleClose);
|
|
2643
|
+
stream.once("finish", handleFinish);
|
|
2644
|
+
stream.once("error", handleError);
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
function waitForChildExit(child, records) {
|
|
2648
|
+
return new Promise((resolve5) => {
|
|
2649
|
+
const handleClose = (exitCode, signal) => {
|
|
2650
|
+
cleanup();
|
|
2651
|
+
resolve5({ exitCode, signal });
|
|
2652
|
+
};
|
|
2653
|
+
const handleError = (error) => {
|
|
2654
|
+
cleanup();
|
|
2655
|
+
records.push({
|
|
2656
|
+
stream: "stderr",
|
|
2657
|
+
line: "",
|
|
2658
|
+
parseError: error.message
|
|
2659
|
+
});
|
|
2660
|
+
resolve5({
|
|
2661
|
+
exitCode: null,
|
|
2662
|
+
signal: null,
|
|
2663
|
+
errorMessage: error.message
|
|
2664
|
+
});
|
|
2665
|
+
};
|
|
2666
|
+
const cleanup = () => {
|
|
2667
|
+
child.removeListener("close", handleClose);
|
|
2668
|
+
child.removeListener("error", handleError);
|
|
2669
|
+
};
|
|
2670
|
+
child.on("close", handleClose);
|
|
2671
|
+
child.on("error", handleError);
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// ../runtime-claude/src/session-store.ts
|
|
2676
|
+
import { mkdir as mkdir2, readFile as readFile7, rename, writeFile as writeFile4 } from "fs/promises";
|
|
2677
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
2678
|
+
var CLAUDE_SESSION_PROTOCOL = "claude-print";
|
|
2679
|
+
var CLAUDE_SESSION_FILENAME = "claude-session.json";
|
|
2680
|
+
var ClaudeSessionStore = class {
|
|
2681
|
+
constructor(options) {
|
|
2682
|
+
this.options = options;
|
|
2683
|
+
}
|
|
2684
|
+
sessionFilePath(options) {
|
|
2685
|
+
return join4(
|
|
2686
|
+
options.runDirectory ?? this.runDirectory(options.runId),
|
|
2687
|
+
CLAUDE_SESSION_FILENAME
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
async load(options) {
|
|
2691
|
+
let raw;
|
|
2692
|
+
try {
|
|
2693
|
+
raw = await readFile7(this.sessionFilePath(options), "utf8");
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
if (isFileNotFoundError(error)) {
|
|
2696
|
+
return null;
|
|
2697
|
+
}
|
|
2698
|
+
throw error;
|
|
2699
|
+
}
|
|
2700
|
+
return parseClaudeSessionFile(JSON.parse(raw));
|
|
2701
|
+
}
|
|
2702
|
+
async save(options) {
|
|
2703
|
+
const session = {
|
|
2704
|
+
protocol: CLAUDE_SESSION_PROTOCOL,
|
|
2705
|
+
sessionId: options.sessionId,
|
|
2706
|
+
createdAt: options.createdAt,
|
|
2707
|
+
protocolState: options.protocolState ?? {}
|
|
2708
|
+
};
|
|
2709
|
+
if (options.parentRunId) {
|
|
2710
|
+
session.parentRunId = options.parentRunId;
|
|
2711
|
+
}
|
|
2712
|
+
const path = this.sessionFilePath(options);
|
|
2713
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
2714
|
+
await writeFile4(`${path}.tmp`, `${JSON.stringify(session, null, 2)}
|
|
2715
|
+
`, "utf8");
|
|
2716
|
+
await rename(`${path}.tmp`, path);
|
|
2717
|
+
return session;
|
|
2718
|
+
}
|
|
2719
|
+
runDirectory(runId) {
|
|
2720
|
+
return join4(this.options.runtimeRoot, "runs", runId);
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
function parseClaudeSessionFile(value) {
|
|
2724
|
+
if (!isRecord4(value)) {
|
|
2725
|
+
throw new Error("Claude session file must be a JSON object.");
|
|
2726
|
+
}
|
|
2727
|
+
if (value.protocol !== CLAUDE_SESSION_PROTOCOL) {
|
|
2728
|
+
throw new Error(
|
|
2729
|
+
`Claude session file protocol must be ${CLAUDE_SESSION_PROTOCOL}.`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
if (typeof value.sessionId !== "string" || value.sessionId.length === 0) {
|
|
2733
|
+
throw new Error("Claude session file sessionId must be a non-empty string.");
|
|
2734
|
+
}
|
|
2735
|
+
if (typeof value.createdAt !== "string" || value.createdAt.length === 0) {
|
|
2736
|
+
throw new Error("Claude session file createdAt must be a non-empty string.");
|
|
2737
|
+
}
|
|
2738
|
+
if ("parentRunId" in value && value.parentRunId !== void 0 && typeof value.parentRunId !== "string") {
|
|
2739
|
+
throw new Error("Claude session file parentRunId must be a string.");
|
|
2740
|
+
}
|
|
2741
|
+
if ("protocolState" in value && value.protocolState !== void 0 && !isRecord4(value.protocolState)) {
|
|
2742
|
+
throw new Error("Claude session file protocolState must be an object.");
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
protocol: CLAUDE_SESSION_PROTOCOL,
|
|
2746
|
+
sessionId: value.sessionId,
|
|
2747
|
+
createdAt: value.createdAt,
|
|
2748
|
+
parentRunId: typeof value.parentRunId === "string" ? value.parentRunId : void 0,
|
|
2749
|
+
protocolState: isRecord4(value.protocolState) ? value.protocolState : {}
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
function isRecord4(value) {
|
|
2753
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2754
|
+
}
|
|
2755
|
+
function isFileNotFoundError(error) {
|
|
2756
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// ../runtime-claude/src/adapter.ts
|
|
2760
|
+
var ClaudePrintRuntimeAdapter = class {
|
|
2761
|
+
constructor(config, dependencies = {}) {
|
|
2762
|
+
this.config = config;
|
|
2763
|
+
this.dependencies = dependencies;
|
|
2764
|
+
this.sessionStore = new ClaudeSessionStore({
|
|
2765
|
+
runtimeRoot: config.runtimeRoot ?? join5(config.workingDirectory, ".runtime", "orchestrator")
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
activeChild = null;
|
|
2769
|
+
preparedMcpConfig = null;
|
|
2770
|
+
preparedSession = null;
|
|
2771
|
+
eventHandlers = /* @__PURE__ */ new Set();
|
|
2772
|
+
pendingEvents = [];
|
|
2773
|
+
sessionStore;
|
|
2774
|
+
async prepare(context) {
|
|
2775
|
+
await this.cleanupPreparedMcpConfig();
|
|
2776
|
+
this.pendingEvents.length = 0;
|
|
2777
|
+
this.preparedSession = await this.prepareSession(context);
|
|
2778
|
+
this.preparedMcpConfig = await composeClaudeMcpConfig(
|
|
2779
|
+
this.config.workingDirectory,
|
|
2780
|
+
this.config.isolation?.strictMcpConfig === true,
|
|
2781
|
+
buildClaudeMcpTokenEnvironment({
|
|
2782
|
+
inheritProcessEnv: this.config.inheritProcessEnv === true,
|
|
2783
|
+
configEnv: this.config.env,
|
|
2784
|
+
runtimeDirectory: this.config.runtimeDirectory
|
|
2785
|
+
})
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
async spawnTurn(input) {
|
|
2789
|
+
if (this.activeChild) {
|
|
2790
|
+
throw new Error(
|
|
2791
|
+
"TODO(#8): Claude print runtime adapter supports only one in-flight turn."
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
const session = input.session ?? this.preparedSession?.session;
|
|
2795
|
+
const argv = buildClaudePrintArgv(this.buildArgvOptions(input, session));
|
|
2796
|
+
try {
|
|
2797
|
+
const result = await this.spawnWithArgv(input, argv);
|
|
2798
|
+
if (this.shouldInvalidatePreparedResume(session, result)) {
|
|
2799
|
+
return await this.retryWithFreshSession(input, result);
|
|
2800
|
+
}
|
|
2801
|
+
await this.persistStartedSessionId(result);
|
|
2802
|
+
await this.persistForkedSessionId(result);
|
|
2803
|
+
return result;
|
|
2804
|
+
} finally {
|
|
2805
|
+
this.activeChild = null;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
onEvent(handler) {
|
|
2809
|
+
this.eventHandlers.add(handler);
|
|
2810
|
+
for (const event of this.pendingEvents) {
|
|
2811
|
+
handler(event);
|
|
2812
|
+
}
|
|
2813
|
+
return () => {
|
|
2814
|
+
this.eventHandlers.delete(handler);
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
resolveCredentials(brokerResponse) {
|
|
2818
|
+
return extractEnvForClaude(brokerResponse.env, this.config.authEnvKey);
|
|
2819
|
+
}
|
|
2820
|
+
async shutdown() {
|
|
2821
|
+
this.stopActiveChild();
|
|
2822
|
+
await this.cleanupPreparedMcpConfig();
|
|
2823
|
+
}
|
|
2824
|
+
async cancel(_reason) {
|
|
2825
|
+
this.stopActiveChild();
|
|
2826
|
+
await this.cleanupPreparedMcpConfig();
|
|
2827
|
+
}
|
|
2828
|
+
buildArgvOptions(input, session) {
|
|
2829
|
+
const isolation = {
|
|
2830
|
+
...this.config.isolation,
|
|
2831
|
+
...input.isolation
|
|
2832
|
+
};
|
|
2833
|
+
const configuredExtraArgs = input.extraArgs ?? this.config.extraArgs ?? [];
|
|
2834
|
+
if (this.preparedMcpConfig) {
|
|
2835
|
+
return {
|
|
2836
|
+
baseArgs: this.config.args,
|
|
2837
|
+
session,
|
|
2838
|
+
// prepare() owns MCP argv injection through extraArgv; suppress the
|
|
2839
|
+
// isolation flag here so buildClaudePrintArgv does not add it twice.
|
|
2840
|
+
// Any input mcpConfigPath is intentionally ignored while a prepared
|
|
2841
|
+
// composition result is active.
|
|
2842
|
+
isolation: {
|
|
2843
|
+
...isolation,
|
|
2844
|
+
strictMcpConfig: false,
|
|
2845
|
+
mcpConfigPath: void 0
|
|
2846
|
+
},
|
|
2847
|
+
extraArgs: [
|
|
2848
|
+
...this.preparedMcpConfig.extraArgv,
|
|
2849
|
+
...configuredExtraArgs
|
|
2850
|
+
]
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
if (isolation.strictMcpConfig && !isolation.mcpConfigPath) {
|
|
2854
|
+
throw new Error(
|
|
2855
|
+
"Claude strict MCP config requires prepare() or an explicit mcpConfigPath."
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
return {
|
|
2859
|
+
baseArgs: this.config.args,
|
|
2860
|
+
session,
|
|
2861
|
+
isolation,
|
|
2862
|
+
extraArgs: configuredExtraArgs
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
async prepareSession(context) {
|
|
2866
|
+
const currentOptions = {
|
|
2867
|
+
runId: context.runId,
|
|
2868
|
+
runDirectory: context.runDirectory
|
|
2869
|
+
};
|
|
2870
|
+
const parentRunId = context.previousRunId;
|
|
2871
|
+
try {
|
|
2872
|
+
const current = await this.sessionStore.load(currentOptions);
|
|
2873
|
+
if (current) {
|
|
2874
|
+
return {
|
|
2875
|
+
runId: context.runId,
|
|
2876
|
+
runDirectory: context.runDirectory,
|
|
2877
|
+
sessionFile: current,
|
|
2878
|
+
session: {
|
|
2879
|
+
mode: "resume",
|
|
2880
|
+
sessionId: current.sessionId
|
|
2881
|
+
}
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
return await this.createFreshSession(context, {
|
|
2886
|
+
reason: `session file could not be read or parsed: ${formatErrorMessage(error)}`,
|
|
2887
|
+
invalidatedSessionId: "unknown",
|
|
2888
|
+
parentRunId
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
if (context.previousRunId) {
|
|
2892
|
+
try {
|
|
2893
|
+
const previous = await this.sessionStore.load({
|
|
2894
|
+
runId: context.previousRunId,
|
|
2895
|
+
runDirectory: context.previousRunDirectory
|
|
2896
|
+
});
|
|
2897
|
+
if (previous) {
|
|
2898
|
+
const sessionFile = await this.sessionStore.save({
|
|
2899
|
+
...currentOptions,
|
|
2900
|
+
sessionId: previous.sessionId,
|
|
2901
|
+
createdAt: this.nowIso(),
|
|
2902
|
+
parentRunId: context.previousRunId
|
|
2903
|
+
});
|
|
2904
|
+
return {
|
|
2905
|
+
runId: context.runId,
|
|
2906
|
+
runDirectory: context.runDirectory,
|
|
2907
|
+
sessionFile,
|
|
2908
|
+
session: {
|
|
2909
|
+
mode: "resume",
|
|
2910
|
+
sessionId: previous.sessionId,
|
|
2911
|
+
forkSession: true
|
|
2912
|
+
}
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
} catch (error) {
|
|
2916
|
+
return await this.createFreshSession(context, {
|
|
2917
|
+
reason: `parent session file could not be read or parsed: ${formatErrorMessage(error)}`,
|
|
2918
|
+
invalidatedSessionId: "unknown",
|
|
2919
|
+
parentRunId
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
return await this.createFreshSession(context, { parentRunId });
|
|
2924
|
+
}
|
|
2925
|
+
async createFreshSession(context, options = {}) {
|
|
2926
|
+
const replacementSessionId = this.createSessionId();
|
|
2927
|
+
const sessionFile = await this.sessionStore.save({
|
|
2928
|
+
runId: context.runId,
|
|
2929
|
+
runDirectory: context.runDirectory,
|
|
2930
|
+
sessionId: replacementSessionId,
|
|
2931
|
+
createdAt: this.nowIso(),
|
|
2932
|
+
parentRunId: options.parentRunId
|
|
2933
|
+
});
|
|
2934
|
+
if (options.reason) {
|
|
2935
|
+
this.emitSessionInvalidated({
|
|
2936
|
+
runId: context.runId,
|
|
2937
|
+
sessionId: options.invalidatedSessionId ?? "unknown",
|
|
2938
|
+
replacementSessionId,
|
|
2939
|
+
reason: options.reason
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
return {
|
|
2943
|
+
runId: context.runId,
|
|
2944
|
+
runDirectory: context.runDirectory,
|
|
2945
|
+
sessionFile,
|
|
2946
|
+
session: {
|
|
2947
|
+
mode: "start",
|
|
2948
|
+
sessionId: replacementSessionId
|
|
2949
|
+
}
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
async retryWithFreshSession(input, failedResult) {
|
|
2953
|
+
if (!this.preparedSession) {
|
|
2954
|
+
return failedResult;
|
|
2955
|
+
}
|
|
2956
|
+
const invalidatedSessionId = this.preparedSession.session.sessionId;
|
|
2957
|
+
const replacementSessionId = this.createSessionId();
|
|
2958
|
+
const parentRunId = this.preparedSession.sessionFile.parentRunId;
|
|
2959
|
+
const sessionFile = await this.sessionStore.save({
|
|
2960
|
+
runId: this.preparedSession.runId,
|
|
2961
|
+
runDirectory: this.preparedSession.runDirectory,
|
|
2962
|
+
sessionId: replacementSessionId,
|
|
2963
|
+
createdAt: this.nowIso(),
|
|
2964
|
+
parentRunId
|
|
2965
|
+
});
|
|
2966
|
+
this.preparedSession = {
|
|
2967
|
+
...this.preparedSession,
|
|
2968
|
+
sessionFile,
|
|
2969
|
+
session: {
|
|
2970
|
+
mode: "start",
|
|
2971
|
+
sessionId: replacementSessionId
|
|
2972
|
+
}
|
|
2973
|
+
};
|
|
2974
|
+
this.emitSessionInvalidated({
|
|
2975
|
+
runId: this.preparedSession.runId,
|
|
2976
|
+
sessionId: invalidatedSessionId,
|
|
2977
|
+
replacementSessionId,
|
|
2978
|
+
reason: "claude resume session was rejected with a 4xx response"
|
|
2979
|
+
});
|
|
2980
|
+
const retryArgv = buildClaudePrintArgv(
|
|
2981
|
+
this.buildArgvOptions(input, this.preparedSession.session)
|
|
2982
|
+
);
|
|
2983
|
+
const retryResult = await this.spawnWithArgv(input, retryArgv);
|
|
2984
|
+
await this.persistStartedSessionId(retryResult);
|
|
2985
|
+
return retryResult;
|
|
2986
|
+
}
|
|
2987
|
+
async spawnWithArgv(input, argv) {
|
|
2988
|
+
return await spawnClaudeTurn(
|
|
2989
|
+
{
|
|
2990
|
+
command: input.command ?? this.config.command,
|
|
2991
|
+
args: argv,
|
|
2992
|
+
cwd: input.cwd ?? this.config.workingDirectory,
|
|
2993
|
+
env: buildClaudeSpawnEnv({
|
|
2994
|
+
inheritProcessEnv: this.config.inheritProcessEnv === true,
|
|
2995
|
+
configEnv: this.config.env,
|
|
2996
|
+
inputEnv: input.env
|
|
2997
|
+
}),
|
|
2998
|
+
stdinMessages: input.messages
|
|
2999
|
+
},
|
|
3000
|
+
{
|
|
3001
|
+
...this.dependencies,
|
|
3002
|
+
onSpawned: (child) => {
|
|
3003
|
+
this.activeChild = child;
|
|
3004
|
+
this.dependencies.onSpawned?.(child);
|
|
3005
|
+
},
|
|
3006
|
+
onEvent: (event) => {
|
|
3007
|
+
this.emitEvent(event);
|
|
3008
|
+
try {
|
|
3009
|
+
this.dependencies.onEvent?.(event);
|
|
3010
|
+
} catch {
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
async persistForkedSessionId(result) {
|
|
3017
|
+
if (this.preparedSession?.session.mode !== "resume" || !this.preparedSession.session.forkSession) {
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
const forkedSessionId = findSessionIdInResult(result);
|
|
3021
|
+
const sessionId = forkedSessionId ?? this.preparedSession.session.sessionId;
|
|
3022
|
+
this.preparedSession = {
|
|
3023
|
+
...this.preparedSession,
|
|
3024
|
+
sessionFile: await this.sessionStore.save({
|
|
3025
|
+
runId: this.preparedSession.runId,
|
|
3026
|
+
runDirectory: this.preparedSession.runDirectory,
|
|
3027
|
+
sessionId,
|
|
3028
|
+
createdAt: this.preparedSession.sessionFile.createdAt,
|
|
3029
|
+
parentRunId: this.preparedSession.sessionFile.parentRunId,
|
|
3030
|
+
protocolState: this.preparedSession.sessionFile.protocolState
|
|
3031
|
+
}),
|
|
3032
|
+
session: {
|
|
3033
|
+
mode: "resume",
|
|
3034
|
+
sessionId
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
async persistStartedSessionId(result) {
|
|
3039
|
+
if (this.preparedSession?.session.mode !== "start") {
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (result.result !== "success") {
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
const sessionId = findSessionIdInResult(result) ?? this.preparedSession.session.sessionId;
|
|
3046
|
+
this.preparedSession = {
|
|
3047
|
+
...this.preparedSession,
|
|
3048
|
+
sessionFile: await this.sessionStore.save({
|
|
3049
|
+
runId: this.preparedSession.runId,
|
|
3050
|
+
runDirectory: this.preparedSession.runDirectory,
|
|
3051
|
+
sessionId,
|
|
3052
|
+
createdAt: this.preparedSession.sessionFile.createdAt,
|
|
3053
|
+
parentRunId: this.preparedSession.sessionFile.parentRunId,
|
|
3054
|
+
protocolState: this.preparedSession.sessionFile.protocolState
|
|
3055
|
+
}),
|
|
3056
|
+
session: {
|
|
3057
|
+
mode: "resume",
|
|
3058
|
+
sessionId
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
shouldInvalidatePreparedResume(session, result) {
|
|
3063
|
+
return session === this.preparedSession?.session && session?.mode === "resume" && isResumeRejectedWith4xx(result);
|
|
3064
|
+
}
|
|
3065
|
+
emitSessionInvalidated(payload) {
|
|
3066
|
+
const event = {
|
|
3067
|
+
name: "agent.sessionInvalidated",
|
|
3068
|
+
payload: {
|
|
3069
|
+
params: {},
|
|
3070
|
+
...payload,
|
|
3071
|
+
observabilityEvent: "session_invalidated"
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
if (this.eventHandlers.size === 0) {
|
|
3075
|
+
this.pendingEvents.push(event);
|
|
3076
|
+
} else {
|
|
3077
|
+
for (const handler of this.eventHandlers) {
|
|
3078
|
+
handler(event);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
createSessionId() {
|
|
3083
|
+
return this.dependencies.createSessionId?.() ?? randomUUID();
|
|
3084
|
+
}
|
|
3085
|
+
nowIso() {
|
|
3086
|
+
return (this.dependencies.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
3087
|
+
}
|
|
3088
|
+
stopActiveChild() {
|
|
3089
|
+
if (!this.activeChild || this.activeChild.killed) {
|
|
3090
|
+
this.activeChild = null;
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
this.activeChild.kill("SIGTERM");
|
|
3094
|
+
this.activeChild = null;
|
|
3095
|
+
}
|
|
3096
|
+
async cleanupPreparedMcpConfig() {
|
|
3097
|
+
const cleanupPath = this.preparedMcpConfig?.cleanupPath;
|
|
3098
|
+
this.preparedMcpConfig = null;
|
|
3099
|
+
if (!cleanupPath) {
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
await rm(cleanupPath, { force: true });
|
|
3103
|
+
}
|
|
3104
|
+
emitEvent(event) {
|
|
3105
|
+
for (const handler of this.eventHandlers) {
|
|
3106
|
+
try {
|
|
3107
|
+
handler(event);
|
|
3108
|
+
} catch {
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
function createClaudePrintRuntimeAdapter(config, dependencies = {}) {
|
|
3114
|
+
return new ClaudePrintRuntimeAdapter(config, dependencies);
|
|
3115
|
+
}
|
|
3116
|
+
var DEFAULT_INHERITED_ENV_KEYS = [
|
|
3117
|
+
"HOME",
|
|
3118
|
+
"LANG",
|
|
3119
|
+
"PATH",
|
|
3120
|
+
"SHELL",
|
|
3121
|
+
"SYSTEMROOT",
|
|
3122
|
+
"TEMP",
|
|
3123
|
+
"TERM",
|
|
3124
|
+
"TMP",
|
|
3125
|
+
"TMPDIR",
|
|
3126
|
+
"USER",
|
|
3127
|
+
"USERPROFILE"
|
|
3128
|
+
];
|
|
3129
|
+
function buildClaudeSpawnEnv(options) {
|
|
3130
|
+
if (options.inheritProcessEnv) {
|
|
3131
|
+
return {
|
|
3132
|
+
...process.env,
|
|
3133
|
+
...options.configEnv,
|
|
3134
|
+
...options.inputEnv
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
const env = {};
|
|
3138
|
+
for (const key of DEFAULT_INHERITED_ENV_KEYS) {
|
|
3139
|
+
const value = process.env[key];
|
|
3140
|
+
if (value !== void 0) {
|
|
3141
|
+
env[key] = value;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
Object.assign(env, options.configEnv, options.inputEnv);
|
|
3145
|
+
return env;
|
|
3146
|
+
}
|
|
3147
|
+
function findSessionIdInResult(result) {
|
|
3148
|
+
for (const record of result.records) {
|
|
3149
|
+
const sessionId = findSessionId(record.message);
|
|
3150
|
+
if (sessionId) {
|
|
3151
|
+
return sessionId;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
3156
|
+
function findSessionId(value, depth = 0) {
|
|
3157
|
+
if (depth > 5) {
|
|
3158
|
+
return null;
|
|
3159
|
+
}
|
|
3160
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
const record = value;
|
|
3164
|
+
if (typeof record.sessionId === "string") {
|
|
3165
|
+
return record.sessionId;
|
|
3166
|
+
}
|
|
3167
|
+
if (typeof record.session_id === "string") {
|
|
3168
|
+
return record.session_id;
|
|
3169
|
+
}
|
|
3170
|
+
for (const nested of Object.values(record)) {
|
|
3171
|
+
const sessionId = findSessionId(nested, depth + 1);
|
|
3172
|
+
if (sessionId) {
|
|
3173
|
+
return sessionId;
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
return null;
|
|
3177
|
+
}
|
|
3178
|
+
function isResumeRejectedWith4xx(result) {
|
|
3179
|
+
if (result.result !== "process-error") {
|
|
3180
|
+
return false;
|
|
3181
|
+
}
|
|
3182
|
+
return result.records.some((record) => {
|
|
3183
|
+
const text = record.line.toLowerCase();
|
|
3184
|
+
return text.includes("resume") && /\b4\d\d\b/.test(text);
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
function formatErrorMessage(error) {
|
|
3188
|
+
if (error instanceof Error) {
|
|
3189
|
+
return error.message;
|
|
3190
|
+
}
|
|
3191
|
+
return String(error);
|
|
3192
|
+
}
|
|
3193
|
+
function buildClaudeMcpTokenEnvironment(options) {
|
|
3194
|
+
const source = options.inheritProcessEnv ? {
|
|
3195
|
+
...process.env,
|
|
3196
|
+
...options.configEnv
|
|
3197
|
+
} : {
|
|
3198
|
+
...options.configEnv
|
|
3199
|
+
};
|
|
3200
|
+
return {
|
|
3201
|
+
GITHUB_GRAPHQL_TOKEN: source.GITHUB_GRAPHQL_TOKEN,
|
|
3202
|
+
GITHUB_GRAPHQL_API_URL: source.GITHUB_GRAPHQL_API_URL,
|
|
3203
|
+
GITHUB_TOKEN_BROKER_URL: source.GITHUB_TOKEN_BROKER_URL,
|
|
3204
|
+
GITHUB_TOKEN_BROKER_SECRET: source.GITHUB_TOKEN_BROKER_SECRET,
|
|
3205
|
+
GITHUB_TOKEN_CACHE_PATH: source.GITHUB_TOKEN_CACHE_PATH,
|
|
3206
|
+
GITHUB_PROJECT_ID: source.GITHUB_PROJECT_ID,
|
|
3207
|
+
WORKSPACE_RUNTIME_DIR: options.runtimeDirectory ?? source.WORKSPACE_RUNTIME_DIR
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
export {
|
|
3212
|
+
isOrchestratorChannelEvent,
|
|
3213
|
+
DEFAULT_WORKFLOW_LIFECYCLE,
|
|
3214
|
+
isStateActive,
|
|
3215
|
+
isStateTerminal,
|
|
3216
|
+
matchesWorkflowState,
|
|
3217
|
+
DEFAULT_MAX_FAILURE_RETRIES,
|
|
3218
|
+
resolveWorkflowRuntimeCommand,
|
|
3219
|
+
resolveWorkflowRuntimeTimeouts,
|
|
3220
|
+
parseWorkflowMarkdown,
|
|
3221
|
+
WorkflowConfigStore,
|
|
3222
|
+
createDefaultWorkflowResolution,
|
|
3223
|
+
createInvalidWorkflowResolution,
|
|
3224
|
+
buildPromptVariables,
|
|
3225
|
+
renderPrompt,
|
|
3226
|
+
classifySessionExit,
|
|
3227
|
+
scheduleRetryAt,
|
|
3228
|
+
extractEnvForCodex,
|
|
3229
|
+
extractEnvForClaude,
|
|
3230
|
+
shouldReuseAgentCredentialCache,
|
|
3231
|
+
readAgentCredentialCache,
|
|
3232
|
+
writeAgentCredentialCache,
|
|
3233
|
+
DEFAULT_AGENT_INPUT_REQUIRED_REASON,
|
|
3234
|
+
buildAgentInputRequiredReason,
|
|
3235
|
+
readEnvFile,
|
|
3236
|
+
deriveIssueWorkspaceKeyFromIdentifier,
|
|
3237
|
+
deriveIssueWorkspaceKey,
|
|
3238
|
+
deriveLegacyIssueWorkspaceKey,
|
|
3239
|
+
resolveIssueWorkspaceDirectory,
|
|
3240
|
+
buildHookEnv,
|
|
3241
|
+
executeWorkspaceHook,
|
|
3242
|
+
buildProjectSnapshot,
|
|
3243
|
+
readJsonFile,
|
|
3244
|
+
safeReadDir,
|
|
3245
|
+
isFileMissing,
|
|
3246
|
+
parseRecentEvents,
|
|
3247
|
+
isMatchingIssueRun,
|
|
3248
|
+
mapIssueOrchestrationStateToStatus,
|
|
3249
|
+
resolveGitHubGraphQLToken,
|
|
3250
|
+
createGitHubGraphQLMcpServerEntry,
|
|
3251
|
+
createClaudePrintRuntimeAdapter,
|
|
3252
|
+
runClaudePreflight,
|
|
3253
|
+
formatClaudePreflightText,
|
|
3254
|
+
isClaudeRuntimeCommand,
|
|
3255
|
+
resolveClaudeCommandBinary,
|
|
3256
|
+
resolveRuntimeCommandBinary
|
|
3257
|
+
};
|