@akagilnc/pi-workflow-roles 0.1.3999 → 0.1.4021
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-host/production-host.js +10 -0
- package/dist/headless-host/description.js +319 -21
- package/dist/headless-host/production-host.js +462 -52
- package/dist/host-descriptions.js +19 -6
- package/dist/public-cli/main.js +10 -0
- package/package.json +1 -1
- package/src/headless-host/description.ts +381 -20
- package/src/headless-host/production-host.ts +5 -5
- package/src/headless-host/role-turn-host.ts +290 -27
- package/src/host-descriptions.ts +20 -7
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Headless CLI last hop (#645/#646/#820): spawn/parse/bind. Shared retry/resume
|
|
3
|
+
* loop = external-host-turn-loop. Claude print-mode and codex exec share this
|
|
4
|
+
* lifecycle; argv/parse are protocol-specific.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
3
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
4
9
|
import { writeFile } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
10
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
11
|
|
|
7
12
|
import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
|
|
8
13
|
import {
|
|
@@ -19,8 +24,13 @@ import {
|
|
|
19
24
|
|
|
20
25
|
import { reportHostSessionEvent } from "../host-session-record.ts";
|
|
21
26
|
import {
|
|
27
|
+
closeJsonSchemaForCodex,
|
|
28
|
+
codexTurnArgs,
|
|
22
29
|
headlessMcpConfigDocument,
|
|
23
30
|
headlessTurnArgs,
|
|
31
|
+
isClaudePrintDescription,
|
|
32
|
+
isCodexExecDescription,
|
|
33
|
+
isPlainObject,
|
|
24
34
|
type HeadlessHostDescription,
|
|
25
35
|
} from "./description.ts";
|
|
26
36
|
|
|
@@ -71,7 +81,7 @@ export type HeadlessCliResult = Readonly<{
|
|
|
71
81
|
* envelope without stream-json `type`). Intermediate stream-json events are not.
|
|
72
82
|
*/
|
|
73
83
|
function isHeadlessResultCandidate(value: unknown): value is HeadlessCliResult {
|
|
74
|
-
if (
|
|
84
|
+
if (!isPlainObject(value)) return false;
|
|
75
85
|
const record = value as HeadlessCliResult & { type?: unknown };
|
|
76
86
|
return record.type === undefined
|
|
77
87
|
|| record.type === "result"
|
|
@@ -79,7 +89,7 @@ function isHeadlessResultCandidate(value: unknown): value is HeadlessCliResult {
|
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/**
|
|
82
|
-
* Parse host stdout into the result envelope.
|
|
92
|
+
* Parse Claude host stdout into the result envelope.
|
|
83
93
|
* Production uses `--output-format stream-json` (#811 live records); last-result
|
|
84
94
|
* line is the typed receipt. A single-document `json` body still parses so a
|
|
85
95
|
* misconfigured description yields a typed miss rather than a silent empty parse.
|
|
@@ -101,7 +111,7 @@ export function parseHeadlessCliStdout(stdout: string): HeadlessCliResult | unde
|
|
|
101
111
|
if (text === "") continue;
|
|
102
112
|
try {
|
|
103
113
|
const value = JSON.parse(text) as unknown;
|
|
104
|
-
if (
|
|
114
|
+
if (!isPlainObject(value)) continue;
|
|
105
115
|
const record = value as HeadlessCliResult & { type?: unknown };
|
|
106
116
|
// Multi-line stream: only explicit result / structured_output lines (not bare objects).
|
|
107
117
|
if (record.type === "result" || record.structured_output !== undefined) {
|
|
@@ -129,6 +139,116 @@ function resultCandidateText(line: string): string | undefined {
|
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Minimal consumer-driven parse of `codex exec --json` JSONL (ADR 0043).
|
|
144
|
+
* Only takes thread_id, final agent_message text, and terminal turn.failed.
|
|
145
|
+
* Top-level `error` events are non-terminal (reconnect notices, skill budget
|
|
146
|
+
* warnings); they must not poison a later turn.completed receipt.
|
|
147
|
+
*/
|
|
148
|
+
export type CodexExecTurnObservation = Readonly<{
|
|
149
|
+
threadId?: string;
|
|
150
|
+
/** Last `item.completed` agent_message text (final message / structured receipt). */
|
|
151
|
+
finalMessage?: string;
|
|
152
|
+
/** Present only for terminal `turn.failed` (not recoverable `error` events). */
|
|
153
|
+
failureDiagnostic?: string;
|
|
154
|
+
turnCompleted: boolean;
|
|
155
|
+
}>;
|
|
156
|
+
|
|
157
|
+
function createCodexExecTurnObserver(): {
|
|
158
|
+
readonly observe: (event: unknown) => void;
|
|
159
|
+
readonly result: () => CodexExecTurnObservation;
|
|
160
|
+
} {
|
|
161
|
+
let threadId: string | undefined;
|
|
162
|
+
let finalMessage: string | undefined;
|
|
163
|
+
let failureDiagnostic: string | undefined;
|
|
164
|
+
let turnCompleted = false;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
observe(value) {
|
|
168
|
+
if (!isPlainObject(value)) return;
|
|
169
|
+
const type = typeof value.type === "string" ? value.type : undefined;
|
|
170
|
+
if (type === "thread.started" && typeof value.thread_id === "string" && value.thread_id !== "") {
|
|
171
|
+
threadId = value.thread_id;
|
|
172
|
+
} else if (type === "item.completed" && isPlainObject(value.item)) {
|
|
173
|
+
if (value.item.type === "agent_message" && typeof value.item.text === "string") {
|
|
174
|
+
finalMessage = value.item.text;
|
|
175
|
+
}
|
|
176
|
+
} else if (type === "turn.completed") {
|
|
177
|
+
turnCompleted = true;
|
|
178
|
+
failureDiagnostic = undefined;
|
|
179
|
+
} else if (type === "turn.failed") {
|
|
180
|
+
turnCompleted = false;
|
|
181
|
+
failureDiagnostic = formatCodexFailurePayload(value.error ?? value);
|
|
182
|
+
}
|
|
183
|
+
// Top-level `error` is non-terminal; exit/receipt handling remains downstream.
|
|
184
|
+
},
|
|
185
|
+
result() {
|
|
186
|
+
return {
|
|
187
|
+
...(threadId === undefined ? {} : { threadId }),
|
|
188
|
+
...(finalMessage === undefined ? {} : { finalMessage }),
|
|
189
|
+
...(failureDiagnostic === undefined ? {} : { failureDiagnostic }),
|
|
190
|
+
turnCompleted,
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatCodexFailurePayload(payload: unknown): string {
|
|
197
|
+
if (typeof payload === "string" && payload.trim() !== "") return payload;
|
|
198
|
+
if (isPlainObject(payload)) {
|
|
199
|
+
if (typeof payload.message === "string" && payload.message.trim() !== "") return payload.message;
|
|
200
|
+
try {
|
|
201
|
+
return JSON.stringify(payload);
|
|
202
|
+
} catch {
|
|
203
|
+
return "codex turn failed";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return String(payload);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** cwd or an ancestor has a `.git` entry (file or directory). */
|
|
210
|
+
function cwdIsGitWorkTree(cwd: string): boolean {
|
|
211
|
+
let dir = cwd;
|
|
212
|
+
for (;;) {
|
|
213
|
+
if (existsSync(join(dir, ".git"))) return true;
|
|
214
|
+
const parent = dirname(dir);
|
|
215
|
+
if (parent === dir) return false;
|
|
216
|
+
dir = parent;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Absolute git common dir for workspace-write extra roots (worktree index.lock).
|
|
222
|
+
* When cwd is not a git work tree → undefined (caller skips extra roots).
|
|
223
|
+
* When cwd is a git work tree, git non-zero / empty stdout / spawn failure
|
|
224
|
+
* must fail loud with the real cause — never wash into "no common dir".
|
|
225
|
+
*/
|
|
226
|
+
function resolveGitCommonDir(cwd: string): string | undefined {
|
|
227
|
+
if (!cwdIsGitWorkTree(cwd)) return undefined;
|
|
228
|
+
let result: { status: number | null; stdout: string; stderr: string; error?: Error };
|
|
229
|
+
try {
|
|
230
|
+
result = spawnSync("git", ["rev-parse", "--git-common-dir"], {
|
|
231
|
+
cwd,
|
|
232
|
+
encoding: "utf8",
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
236
|
+
throw new Error(`git rev-parse --git-common-dir failed: ${message}`);
|
|
237
|
+
}
|
|
238
|
+
if (result.error !== undefined) {
|
|
239
|
+
throw new Error(`git rev-parse --git-common-dir failed: ${result.error.message}`);
|
|
240
|
+
}
|
|
241
|
+
if (result.status !== 0) {
|
|
242
|
+
const detail = result.stderr.trim() || `exit ${String(result.status)}`;
|
|
243
|
+
throw new Error(`git rev-parse --git-common-dir failed: ${detail}`);
|
|
244
|
+
}
|
|
245
|
+
const raw = result.stdout.trim();
|
|
246
|
+
if (raw === "") {
|
|
247
|
+
throw new Error("git rev-parse --git-common-dir returned empty stdout");
|
|
248
|
+
}
|
|
249
|
+
return isAbsolute(raw) ? raw : resolve(cwd, raw);
|
|
250
|
+
}
|
|
251
|
+
|
|
132
252
|
function spawnHeadlessTurn(options: {
|
|
133
253
|
readonly binary: string;
|
|
134
254
|
readonly args: readonly string[];
|
|
@@ -259,19 +379,71 @@ function terminalFromSpawned(
|
|
|
259
379
|
};
|
|
260
380
|
}
|
|
261
381
|
|
|
262
|
-
|
|
382
|
+
function buildTurnArgs(options: {
|
|
383
|
+
readonly description: HeadlessHostDescription;
|
|
384
|
+
readonly prompt: string;
|
|
385
|
+
readonly systemPromptPath: string;
|
|
386
|
+
readonly jsonSchema: Readonly<Record<string, unknown>>;
|
|
387
|
+
readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
388
|
+
readonly mcpConfigPath?: string;
|
|
389
|
+
readonly outputSchemaPath?: string;
|
|
390
|
+
readonly model?: string;
|
|
391
|
+
readonly effort?: string;
|
|
392
|
+
readonly sessionId: string | undefined;
|
|
393
|
+
readonly sessionKind: "new" | "resume";
|
|
394
|
+
readonly cwd: string;
|
|
395
|
+
readonly writableRoots?: readonly string[];
|
|
396
|
+
}): readonly string[] {
|
|
397
|
+
if (isCodexExecDescription(options.description)) {
|
|
398
|
+
if (options.sessionKind === "resume" && !options.sessionId) {
|
|
399
|
+
throw new Error("codex resume requires a bound thread_id");
|
|
400
|
+
}
|
|
401
|
+
if (options.outputSchemaPath === undefined) throw new Error("codex requires an output schema path");
|
|
402
|
+
return codexTurnArgs({
|
|
403
|
+
prompt: options.prompt,
|
|
404
|
+
systemPromptPath: options.systemPromptPath,
|
|
405
|
+
outputSchemaPath: options.outputSchemaPath,
|
|
406
|
+
mcpServers: options.mcpServers,
|
|
407
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
408
|
+
...(options.effort === undefined ? {} : { effort: options.effort }),
|
|
409
|
+
session: options.sessionKind === "resume"
|
|
410
|
+
? { kind: "resume", id: options.sessionId! }
|
|
411
|
+
: { kind: "new" },
|
|
412
|
+
skipGitRepoCheck: !cwdIsGitWorkTree(options.cwd),
|
|
413
|
+
...(!options.writableRoots?.length ? {} : { writableRoots: options.writableRoots }),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!isClaudePrintDescription(options.description)) throw new Error("unsupported headless host protocol");
|
|
418
|
+
if (!options.sessionId) throw new Error("claude print-mode requires a session id");
|
|
419
|
+
if (options.mcpConfigPath === undefined) throw new Error("claude print-mode requires an MCP config path");
|
|
420
|
+
return headlessTurnArgs({
|
|
421
|
+
description: options.description,
|
|
422
|
+
prompt: options.prompt,
|
|
423
|
+
systemPromptPath: options.systemPromptPath,
|
|
424
|
+
jsonSchema: options.jsonSchema,
|
|
425
|
+
mcpConfigPath: options.mcpConfigPath,
|
|
426
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
427
|
+
...(options.effort === undefined ? {} : { effort: options.effort }),
|
|
428
|
+
session: { kind: options.sessionKind, id: options.sessionId },
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Headless last hop (#820): session bind/resume, CLI spawn turn, MCP/json-schema/output-schema mount. */
|
|
263
433
|
export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig): RoleTurnHost {
|
|
264
434
|
return createSerializedRoleTurnHost(async (request): Promise<RoleTurnResult> => {
|
|
265
435
|
const prepared = await config.prepare(request);
|
|
266
436
|
const systemPrompt = renderSystemPromptOverride(prepared.systemPrompt);
|
|
437
|
+
const codex = isCodexExecDescription(config.description);
|
|
267
438
|
let outcome: RoleTurnResult = failure("session", "HeadlessNoOutcome", "no-outcome");
|
|
268
439
|
try {
|
|
440
|
+
// Claude mints a package UUID for --session-id; codex waits for thread.started.
|
|
269
441
|
let sessionId = await config.sessionIdentity.load(request.principal);
|
|
270
442
|
let sessionKind: "new" | "resume" =
|
|
271
443
|
request.continuation.kind === "resume" && sessionId !== undefined && sessionId !== ""
|
|
272
444
|
? "resume"
|
|
273
445
|
: "new";
|
|
274
|
-
if (sessionKind === "new") {
|
|
446
|
+
if (sessionKind === "new" && !codex) {
|
|
275
447
|
sessionId = randomUUID();
|
|
276
448
|
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
277
449
|
}
|
|
@@ -280,12 +452,21 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
280
452
|
const env: NodeJS.ProcessEnv = { ...process.env, ...(config.env ?? {}) };
|
|
281
453
|
const systemPromptPath = join(request.runDirectory, "headless-system-prompt.txt");
|
|
282
454
|
await writeFile(systemPromptPath, systemPrompt, "utf8");
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"
|
|
288
|
-
|
|
455
|
+
let mcpConfigPath: string | undefined;
|
|
456
|
+
let outputSchemaPath: string | undefined;
|
|
457
|
+
if (codex) {
|
|
458
|
+
// Codex --output-schema needs a closed transport projection on disk.
|
|
459
|
+
outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
|
|
460
|
+
const closed = closeJsonSchemaForCodex(prepared.jsonSchema);
|
|
461
|
+
await writeFile(outputSchemaPath, `${JSON.stringify(closed, null, 2)}\n`, "utf8");
|
|
462
|
+
} else {
|
|
463
|
+
mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
|
|
464
|
+
await writeFile(
|
|
465
|
+
mcpConfigPath,
|
|
466
|
+
`${JSON.stringify(headlessMcpConfigDocument(prepared.mcpServers), null, 2)}\n`,
|
|
467
|
+
"utf8",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
289
470
|
|
|
290
471
|
const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
|
|
291
472
|
outcome = await driveExternalRoleTurnRounds(prepared, request, {
|
|
@@ -293,19 +474,33 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
293
474
|
currentSessionId: () => sessionId,
|
|
294
475
|
afterRetry() { sessionKind = "resume"; },
|
|
295
476
|
async runRound({ prompt, abortSignal }) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
? {
|
|
306
|
-
|
|
307
|
-
|
|
477
|
+
let args: readonly string[];
|
|
478
|
+
try {
|
|
479
|
+
const gitCommonDir = codex ? resolveGitCommonDir(request.cwd) : undefined;
|
|
480
|
+
args = buildTurnArgs({
|
|
481
|
+
description: config.description,
|
|
482
|
+
prompt,
|
|
483
|
+
systemPromptPath,
|
|
484
|
+
jsonSchema: prepared.jsonSchema,
|
|
485
|
+
mcpServers: prepared.mcpServers,
|
|
486
|
+
...(mcpConfigPath === undefined ? {} : { mcpConfigPath }),
|
|
487
|
+
...(outputSchemaPath === undefined ? {} : { outputSchemaPath }),
|
|
488
|
+
...(request.model?.model !== undefined ? { model: request.model.model } : {}),
|
|
489
|
+
...(request.model?.thinking !== undefined ? { effort: request.model.thinking } : {}),
|
|
490
|
+
sessionId,
|
|
491
|
+
sessionKind,
|
|
492
|
+
cwd: request.cwd,
|
|
493
|
+
...(gitCommonDir === undefined ? {} : { writableRoots: [gitCommonDir] }),
|
|
494
|
+
});
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
497
|
+
return {
|
|
498
|
+
status: "terminal",
|
|
499
|
+
result: failure("session", "HeadlessArgvFailure", "argv-failed", { diagnostic: message }, message),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
308
502
|
|
|
503
|
+
const codexObserver = codex ? createCodexExecTurnObserver() : undefined;
|
|
309
504
|
let spawned: { code: number | null; stdout: string; stderr: string; timedOut: boolean };
|
|
310
505
|
try {
|
|
311
506
|
spawned = await spawnHeadlessTurn({
|
|
@@ -325,7 +520,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
325
520
|
// Non-JSON noise on stdout is not a host structured event.
|
|
326
521
|
return;
|
|
327
522
|
}
|
|
328
|
-
//
|
|
523
|
+
// One bounded live seam owns both recording and host-specific reduction.
|
|
329
524
|
reportHostSessionEvent({
|
|
330
525
|
host: config.hostName,
|
|
331
526
|
cwd: request.cwd,
|
|
@@ -333,6 +528,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
333
528
|
source: "headless-host",
|
|
334
529
|
event,
|
|
335
530
|
});
|
|
531
|
+
codexObserver?.observe(event);
|
|
336
532
|
},
|
|
337
533
|
});
|
|
338
534
|
} catch (error) {
|
|
@@ -373,6 +569,72 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
373
569
|
});
|
|
374
570
|
}
|
|
375
571
|
|
|
572
|
+
if (codex) {
|
|
573
|
+
const observation = codexObserver!.result();
|
|
574
|
+
if (observation.threadId === undefined || observation.threadId === "") {
|
|
575
|
+
return terminalFromSpawned(spawned, {
|
|
576
|
+
cause: "session",
|
|
577
|
+
identity: { name: "HeadlessMissingThreadId", code: "missing-thread-id" },
|
|
578
|
+
diagnostic: "codex exec emitted no thread.started thread_id",
|
|
579
|
+
details: { sessionId, exitCode: spawned.code },
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
sessionId = observation.threadId;
|
|
583
|
+
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
584
|
+
|
|
585
|
+
if (observation.failureDiagnostic !== undefined) {
|
|
586
|
+
return terminalFromSpawned(spawned, {
|
|
587
|
+
cause: "output",
|
|
588
|
+
identity: { name: "HeadlessCliError", code: "codex-turn-failed" },
|
|
589
|
+
diagnostic: observation.failureDiagnostic,
|
|
590
|
+
details: { sessionId, exitCode: spawned.code },
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Non-zero exit without a parseable failure event still fails loud.
|
|
595
|
+
if (spawned.code !== 0 && spawned.code !== null) {
|
|
596
|
+
return terminalFromSpawned(spawned, {
|
|
597
|
+
cause: "output",
|
|
598
|
+
identity: { name: "HeadlessCliError", code: "codex-nonzero-exit" },
|
|
599
|
+
diagnostic: spawned.stderr.trim() || `codex exec exited ${String(spawned.code)}`,
|
|
600
|
+
details: { sessionId, exitCode: spawned.code },
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!observation.turnCompleted) {
|
|
605
|
+
return terminalFromSpawned(spawned, {
|
|
606
|
+
cause: "output",
|
|
607
|
+
identity: { name: "HeadlessCliError", code: "codex-missing-terminal-event" },
|
|
608
|
+
diagnostic: "codex exec exited without turn.completed",
|
|
609
|
+
details: { sessionId, exitCode: spawned.code },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (observation.finalMessage === undefined) {
|
|
614
|
+
return terminalFromSpawned(spawned, {
|
|
615
|
+
cause: "output",
|
|
616
|
+
identity: { name: "HeadlessEmptyOutput", code: "empty-stdout" },
|
|
617
|
+
diagnostic: spawned.stderr.trim() || "codex exec produced no agent_message",
|
|
618
|
+
details: { sessionId, exitCode: spawned.code },
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
let receipt: unknown;
|
|
623
|
+
try {
|
|
624
|
+
receipt = JSON.parse(observation.finalMessage);
|
|
625
|
+
} catch {
|
|
626
|
+
return terminalFromSpawned(spawned, {
|
|
627
|
+
cause: "output",
|
|
628
|
+
identity: { name: "HeadlessEmptyOutput", code: "unparseable-final-message" },
|
|
629
|
+
diagnostic: "codex final agent_message was not JSON",
|
|
630
|
+
details: { sessionId, exitCode: spawned.code },
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
await prepared.ingestStructuredOutput(receipt);
|
|
634
|
+
return { status: "delivered", stderr: spawned.stderr };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Claude print-mode path.
|
|
376
638
|
const envelope = parseHeadlessCliStdout(spawned.stdout);
|
|
377
639
|
if (envelope === undefined) {
|
|
378
640
|
return terminalFromSpawned(spawned, {
|
|
@@ -383,6 +645,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
383
645
|
});
|
|
384
646
|
}
|
|
385
647
|
|
|
648
|
+
// Bind the host-reported session id (authoritative for --resume).
|
|
386
649
|
if (typeof envelope.session_id === "string" && envelope.session_id !== "") {
|
|
387
650
|
sessionId = envelope.session_id;
|
|
388
651
|
await config.sessionIdentity.bind(request.principal, sessionId);
|
package/src/host-descriptions.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Packaged host description tables (#729 / #731 / #645).
|
|
3
3
|
* Key = seat-table `host` value.
|
|
4
4
|
* - ACP family rows feed the generic ACP factory.
|
|
5
|
-
* - Headless CLI family rows feed the generic headless factory (#645
|
|
5
|
+
* - Headless CLI family rows feed the generic headless factory (#645 claude / #646 codex).
|
|
6
6
|
* pi is the in-process default, not a row.
|
|
7
7
|
* Unregistered names fail closed (#510); these tables do not fallback.
|
|
8
8
|
*/
|
|
@@ -66,15 +66,19 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Headless CLI family (#645). Claude is the first row;
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
69
|
+
* Headless CLI family (#645 / #646). Claude print-mode is the first row;
|
|
70
|
+
* codex exec (#646) adds another. Protocol-specific argv/parse live in
|
|
71
|
+
* headless-host helpers (#752 per-host impl).
|
|
72
|
+
* Claude fixedArgs: print mode, isolation without `--bare` (OAuth stays), full
|
|
73
|
+
* permissions. stream-json + verbose: live host events for sitian records
|
|
74
|
+
* (#811); result is last line. `--setting-sources` empty = load no
|
|
75
|
+
* user/project/local CLAUDE.md/hooks/skills (role envelope is delivered via
|
|
76
|
+
* `--system-prompt` wholesale replace). `--strict-mcp-config` with no
|
|
77
|
+
* `--mcp-config` drops operator MCP + claude.ai connectors.
|
|
75
78
|
*/
|
|
76
79
|
export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDescription>> = Object.freeze({
|
|
77
80
|
"claude": Object.freeze({
|
|
81
|
+
protocol: "claude-print",
|
|
78
82
|
binaryFromHome: Object.freeze([".local", "bin", "claude"]),
|
|
79
83
|
sessionBindingFile: "claude-headless-session.json",
|
|
80
84
|
fixedArgs: Object.freeze([
|
|
@@ -98,6 +102,15 @@ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDes
|
|
|
98
102
|
sessionIdFlag: "--session-id",
|
|
99
103
|
resumeFlag: "--resume",
|
|
100
104
|
}),
|
|
105
|
+
/**
|
|
106
|
+
* Codex headless (#646). Binary under operator home; auth stays in CODEX_HOME.
|
|
107
|
+
* Argv/parse/schema-close are codex-exec helpers — not Claude flag mapping.
|
|
108
|
+
*/
|
|
109
|
+
"codex": Object.freeze({
|
|
110
|
+
protocol: "codex-exec",
|
|
111
|
+
binaryFromHome: Object.freeze([".local", "bin", "codex"]),
|
|
112
|
+
sessionBindingFile: "codex-headless-session.json",
|
|
113
|
+
}),
|
|
101
114
|
});
|
|
102
115
|
|
|
103
116
|
export function lookupHostDescription(host: string): AcpHostDescription | undefined {
|