@akagilnc/pi-workflow-roles 0.1.4528 → 0.1.4620
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 +5 -4
- package/README.zh-CN.md +6 -5
- package/dist/acp-host/production-host.js +1834 -637
- package/dist/archivist-record-topology.js +27 -35
- package/dist/headless-host/description.js +7 -4
- package/dist/headless-host/production-host.js +1652 -519
- package/dist/migrate-book-topology.js +8 -11
- package/dist/navigator-attendance.js +146 -241
- package/dist/navigator-public-session.js +81 -15
- package/dist/package-contracts/navigator-output.js +30 -12
- package/dist/public-cli/invocation.js +10 -4
- package/dist/public-cli/judge-run.js +2 -2
- package/dist/public-cli/main.js +879 -410
- package/dist/public-cli/option-definitions.js +6 -6
- package/dist/public-cli/post-admission.js +25 -10
- package/dist/public-cli/reviewer-run.js +339 -0
- package/dist/public-cli/run-lifecycle.js +29 -7
- package/dist/public-cli/settlement.js +46 -32
- package/dist/public-cli/terminal.js +17 -27
- package/dist/public-cli/turn-request.js +1 -1
- package/dist/public-role-summons.js +370 -20
- package/dist/submission-ledger.js +56 -23
- package/package.json +1 -1
- package/resources/836-deleted-machine-instruction-inventory.md +2 -0
- package/resources/navigator-route-playbook.md +9 -0
- package/scripts/build-package.mjs +6 -1
- package/src/acp-host/role-turn-host.ts +129 -13
- package/src/headless-host/description.ts +17 -7
- package/src/headless-host/role-turn-host.ts +42 -11
- package/src/host-contracts.ts +0 -1
- package/src/navigator-attendance.ts +290 -425
- package/src/navigator-public-session.ts +126 -25
- package/src/navigator-role.ts +2 -2
- package/src/package-contracts/navigator-output.ts +41 -25
- package/src/public-cli/coder-run.ts +2 -2
- package/src/public-cli/fixer-run.ts +2 -2
- package/src/public-cli/invocation.ts +19 -9
- package/src/public-cli/judge-run.ts +2 -2
- package/src/public-cli/merger-run.ts +2 -2
- package/src/public-cli/option-definitions.ts +6 -6
- package/src/public-cli/post-admission.ts +41 -10
- package/src/public-cli/reviewer-run.ts +238 -53
- package/src/public-cli/run-lifecycle.ts +37 -7
- package/src/public-cli/settlement.ts +42 -34
- package/src/public-cli/terminal.ts +39 -37
- package/src/public-cli/turn-request.ts +7 -1
- package/src/public-role-summons.ts +544 -22
- package/src/reviewer-role.ts +1 -2
- package/src/role-envelope.ts +89 -4
- package/src/role-runtime.ts +76 -16
- package/src/submission-ledger.ts +63 -21
- package/dist/public-cli/command-renderer.js +0 -5
- package/dist/public-command-renderer.js +0 -29
- package/src/public-cli/command-renderer.ts +0 -9
- package/src/public-command-renderer.ts +0 -55
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## 到场阶段(#959 / ADR 0073)
|
|
2
|
+
|
|
3
|
+
代码只传中立阶段指针句与 typed 材料;阶段行为说明住本手册,随案附卷。
|
|
4
|
+
|
|
5
|
+
- **待命轮**:父衙门进行中。请在本宿主会话熟悉下列材料并待命;结算结果送达后,再给出下一步建议。本轮不要提交最终路线建议。
|
|
6
|
+
- **结算投喂轮**:父衙门结算已送达。请根据下列材料给出下一步建议。
|
|
7
|
+
|
|
8
|
+
Early 待命轮的输出由执行机制丢弃;最终路线建议只在结算投喂轮给出。
|
|
9
|
+
|
|
1
10
|
## 宿主轴(与角色命令面)
|
|
2
11
|
|
|
3
12
|
游奕使自动出席,亦可 `ak-role navigator` 直调(自动出席不变)。主会话宿主由公开角色的 host 轴决定:调用 `--host` → 席位 `config set-host` → 默认 `pi`。配置默认 host 后,调用者仍用与 Pi 相同的 `ak-role <role> …` 命令面;游奕使随该宿主下的角色跑次出席,席位模型走 run 目录 `institutional-resolution.json` 的 navigator 座(显式 `config set navigator` 优先,否则继承父席有效模型),不再依赖父宿主 ExtensionContext。
|
|
@@ -5,7 +5,6 @@ import { build } from "esbuild";
|
|
|
5
5
|
|
|
6
6
|
const entries = [
|
|
7
7
|
"packaged-role-registry",
|
|
8
|
-
"public-command-renderer",
|
|
9
8
|
"work-subject-identity",
|
|
10
9
|
"navigator-invocation-identity",
|
|
11
10
|
// Static import of navigator-invocation-identity (#603: non-bundle graph closure).
|
|
@@ -22,6 +21,10 @@ const entries = [
|
|
|
22
21
|
"activation-ledger-topology",
|
|
23
22
|
"activation-reconciliation",
|
|
24
23
|
"archivist-record-entry",
|
|
24
|
+
// Pure subject nest topology — static import of archivist-record-entry;
|
|
25
|
+
// cold discovery without SessionManager (#636). Keep listed while that
|
|
26
|
+
// relative edge remains (#857 removal left the import graph open).
|
|
27
|
+
"archivist-record-topology",
|
|
25
28
|
// Session material loaders used by published non-bundle roots.
|
|
26
29
|
"session-opening-materials",
|
|
27
30
|
// Value-import closure of published non-bundle roots (build-package-only loadable).
|
|
@@ -205,6 +208,8 @@ export async function buildPackageArtifacts() {
|
|
|
205
208
|
await mkdir(pluginDir, { recursive: true });
|
|
206
209
|
await cp("resources/method-host-plugin/.claude-plugin", join(pluginDir, ".claude-plugin"), { recursive: true });
|
|
207
210
|
await cp("resources/methods", join(pluginDir, "skills"), { recursive: true });
|
|
211
|
+
// Packaged handbook/playbook paths resolve via injected packageRoot (#962) —
|
|
212
|
+
// do not mirror resources/ into dist/resources (one authority, no parallel copy).
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
const isMain =
|
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from "../external-host-turn-loop.ts";
|
|
10
10
|
|
|
11
11
|
import { reportHostSessionEvent } from "../host-session-record.ts";
|
|
12
|
+
import {
|
|
13
|
+
NAVIGATOR_OUTPUT_TOOL_NAME,
|
|
14
|
+
navigatorProseFromUnknown,
|
|
15
|
+
} from "../package-contracts/navigator-output.ts";
|
|
12
16
|
import {
|
|
13
17
|
renderSystemPromptOverride,
|
|
14
18
|
type PreparedRoleTurn,
|
|
@@ -16,6 +20,53 @@ import {
|
|
|
16
20
|
} from "../prepared-role-turn.ts";
|
|
17
21
|
import { acpModelId, type AcpHostDescription } from "./description.ts";
|
|
18
22
|
|
|
23
|
+
/**
|
|
24
|
+
* #959: collect free-form agent text from ACP session/update stream.
|
|
25
|
+
* Used only when the navigator seat spoke prose without calling the output tool.
|
|
26
|
+
* ACP agent speech is only agent_message / agent_message_chunk — never user,
|
|
27
|
+
* thought, or other *_message kinds (load replay must not poison the bucket).
|
|
28
|
+
*
|
|
29
|
+
* Standard ACP nests under params.update: { sessionUpdate, content }.
|
|
30
|
+
* Flat params.sessionUpdate / string params.update remain accepted for host variants.
|
|
31
|
+
*/
|
|
32
|
+
function acpAgentTextChunk(params: Readonly<Record<string, unknown>>): string | undefined {
|
|
33
|
+
let kind: unknown;
|
|
34
|
+
let content: unknown;
|
|
35
|
+
let textFallback: unknown;
|
|
36
|
+
|
|
37
|
+
const nested = params.update;
|
|
38
|
+
if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) {
|
|
39
|
+
const record = nested as Record<string, unknown>;
|
|
40
|
+
kind = record.sessionUpdate;
|
|
41
|
+
content = record.content;
|
|
42
|
+
textFallback = record.text;
|
|
43
|
+
} else {
|
|
44
|
+
kind = params.sessionUpdate ?? nested;
|
|
45
|
+
content = params.content;
|
|
46
|
+
textFallback = params.text;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (kind !== "agent_message_chunk" && kind !== "agent_message") return undefined;
|
|
50
|
+
if (typeof content === "string" && content.length > 0) return content;
|
|
51
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) {
|
|
52
|
+
const record = content as Record<string, unknown>;
|
|
53
|
+
if (typeof record.text === "string" && record.text.length > 0) return record.text;
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(content)) {
|
|
56
|
+
const parts: string[] = [];
|
|
57
|
+
for (const part of content) {
|
|
58
|
+
if (typeof part === "string" && part.length > 0) parts.push(part);
|
|
59
|
+
else if (typeof part === "object" && part !== null) {
|
|
60
|
+
const record = part as Record<string, unknown>;
|
|
61
|
+
if (typeof record.text === "string" && record.text.length > 0) parts.push(record.text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (parts.length > 0) return parts.join("");
|
|
65
|
+
}
|
|
66
|
+
if (typeof textFallback === "string" && textFallback.length > 0) return textFallback;
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
19
70
|
/** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
|
|
20
71
|
export interface AcpConnection {
|
|
21
72
|
request(method: string, params: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>>>;
|
|
@@ -43,7 +94,13 @@ export type AcpRoleTurnHostConfig = Readonly<{
|
|
|
43
94
|
prepare(request: RoleTurnRequest): Promise<PreparedRoleTurn>;
|
|
44
95
|
}>;
|
|
45
96
|
|
|
46
|
-
function failure(
|
|
97
|
+
function failure(
|
|
98
|
+
cause: "activation" | "session" | "output",
|
|
99
|
+
name: string,
|
|
100
|
+
code: string,
|
|
101
|
+
details?: Readonly<Record<string, unknown>>,
|
|
102
|
+
diagnostic?: string,
|
|
103
|
+
): RoleTurnResult {
|
|
47
104
|
return {
|
|
48
105
|
code: null,
|
|
49
106
|
stderr: "",
|
|
@@ -51,11 +108,27 @@ function failure(cause: "activation" | "session" | "output", name: string, code:
|
|
|
51
108
|
knownFailure: {
|
|
52
109
|
cause,
|
|
53
110
|
identity: { name, code },
|
|
111
|
+
...(diagnostic === undefined ? {} : { diagnostic }),
|
|
54
112
|
...(details === undefined ? {} : { details }),
|
|
55
113
|
},
|
|
56
114
|
};
|
|
57
115
|
}
|
|
58
116
|
|
|
117
|
+
/** Success→dispose failure; existing failure keeps primary cause + cleanup detail. */
|
|
118
|
+
function withCleanupFailure(outcome: RoleTurnResult, cleanupError: unknown): RoleTurnResult {
|
|
119
|
+
const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
120
|
+
if (outcome.knownFailure === undefined) {
|
|
121
|
+
return failure("session", "AcpDisposeFailure", "dispose-failed", { cleanupError: message }, message);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...outcome,
|
|
125
|
+
knownFailure: {
|
|
126
|
+
...outcome.knownFailure,
|
|
127
|
+
details: { ...(outcome.knownFailure.details ?? {}), cleanupError: message },
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
59
132
|
type RpcReply = { readonly id?: unknown; readonly method?: unknown; readonly params?: unknown; readonly result?: unknown; readonly error?: unknown };
|
|
60
133
|
|
|
61
134
|
function acpError(code: string, message: string, cause?: unknown): Error & { readonly code: string } {
|
|
@@ -182,10 +255,12 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
182
255
|
let connection: AcpConnection | undefined;
|
|
183
256
|
let sessionId: string | undefined;
|
|
184
257
|
let accepted = false;
|
|
258
|
+
// Mutable so dispose failure can outrank a clean turn (headless withCleanupFailure face).
|
|
259
|
+
let outcome: RoleTurnResult = failure("session", "AcpNoOutcome", "no-outcome");
|
|
185
260
|
try {
|
|
186
261
|
if (prepared.mcpServers.length === 0) {
|
|
187
|
-
|
|
188
|
-
}
|
|
262
|
+
outcome = failure("activation", "UncontrolledAcpSession", "ak-config-missing");
|
|
263
|
+
} else {
|
|
189
264
|
connection = await config.connect(request);
|
|
190
265
|
// Live host-session records: ACP session/update → sitian sole entry (#811).
|
|
191
266
|
// One abort + one race helper + one outer projection — write failure ends
|
|
@@ -214,8 +289,17 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
214
289
|
): Promise<Readonly<Record<string, unknown>>> =>
|
|
215
290
|
raceAgainstHostAbort(connection!.request(method, params), recordAbort.signal, "host-session-record-failed");
|
|
216
291
|
|
|
292
|
+
// #959: navigator free-form agent text — only while session/prompt is in flight.
|
|
293
|
+
// session/load replays history via session/update; those must not enter the bucket
|
|
294
|
+
// (resume / set_model load would otherwise prepend prior turns as "this turn" prose).
|
|
295
|
+
const agentProseChunks: string[] = [];
|
|
296
|
+
let collectAgentProse = false;
|
|
217
297
|
connection.onNotification?.((method, params) => {
|
|
218
298
|
if (method !== "session/update" || hostSessionRecordFailure !== undefined) return;
|
|
299
|
+
if (collectAgentProse) {
|
|
300
|
+
const chunk = acpAgentTextChunk(params);
|
|
301
|
+
if (chunk !== undefined) agentProseChunks.push(chunk);
|
|
302
|
+
}
|
|
219
303
|
try {
|
|
220
304
|
reportHostSessionEvent({
|
|
221
305
|
host: config.hostName,
|
|
@@ -243,11 +327,11 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
243
327
|
if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
|
|
244
328
|
typeof entry === "object" && entry !== null
|
|
245
329
|
&& (entry as { modelId?: unknown }).modelId === acpModelId(config.modelPassing, request.model))) {
|
|
246
|
-
|
|
330
|
+
outcome = failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
|
|
247
331
|
provider: request.model.provider,
|
|
248
332
|
model: request.model.model,
|
|
249
333
|
});
|
|
250
|
-
}
|
|
334
|
+
} else {
|
|
251
335
|
|
|
252
336
|
const sessionBindParams = {
|
|
253
337
|
cwd: request.cwd,
|
|
@@ -269,15 +353,19 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
269
353
|
sessionId = await loadSession(boundSessionId);
|
|
270
354
|
}
|
|
271
355
|
}
|
|
356
|
+
let sessionReady = true;
|
|
272
357
|
if (sessionId === undefined) {
|
|
273
358
|
const session = await rpc("session/new", sessionBindParams);
|
|
274
359
|
sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
|
|
275
360
|
if (sessionId === undefined || sessionId === "") {
|
|
276
|
-
|
|
361
|
+
outcome = failure("session", "AcpSessionFailure", "session-id-missing");
|
|
362
|
+
sessionReady = false;
|
|
363
|
+
} else {
|
|
364
|
+
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
277
365
|
}
|
|
278
|
-
await config.sessionIdentity.bind(request.principal, sessionId);
|
|
279
366
|
}
|
|
280
367
|
|
|
368
|
+
if (sessionReady) {
|
|
281
369
|
// set_model seat provider:model (#778); may rebuild agent — re-bind via loadSession.
|
|
282
370
|
if (config.modelPassing === "set_model" && request.model !== undefined && sessionId !== undefined) {
|
|
283
371
|
await rpc("session/set_model", {
|
|
@@ -300,6 +388,9 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
300
388
|
if (abortSignal !== undefined) abortParts.push(abortSignal);
|
|
301
389
|
const combinedAbort = AbortSignal.any(abortParts);
|
|
302
390
|
let result: Readonly<Record<string, unknown>>;
|
|
391
|
+
// Open the prose gate only for this prompt round; clear any stale chunks first.
|
|
392
|
+
agentProseChunks.length = 0;
|
|
393
|
+
collectAgentProse = prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME;
|
|
303
394
|
try {
|
|
304
395
|
result = await raceAgainstHostAbort(
|
|
305
396
|
activeConnection.request("session/prompt", {
|
|
@@ -310,17 +401,33 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
310
401
|
"ACP host aborted",
|
|
311
402
|
);
|
|
312
403
|
} catch (error) {
|
|
404
|
+
collectAgentProse = false;
|
|
405
|
+
agentProseChunks.length = 0;
|
|
313
406
|
if (hostSessionRecordFailure !== undefined) {
|
|
314
407
|
return { status: "terminal", result: hostSessionRecordResult() };
|
|
315
408
|
}
|
|
316
409
|
throw error;
|
|
317
410
|
}
|
|
411
|
+
collectAgentProse = false;
|
|
318
412
|
if (result.stopReason === "refusal") {
|
|
413
|
+
agentProseChunks.length = 0;
|
|
319
414
|
return {
|
|
320
415
|
status: "terminal",
|
|
321
416
|
result: failure("output", "AcpRefusal", "refusal", { sessionId }),
|
|
322
417
|
};
|
|
323
418
|
}
|
|
419
|
+
// #959: navigator prose exit when the model spoke without the output tool.
|
|
420
|
+
// Tool path still wins via MCP; ingest is a no-op once the tool already sealed.
|
|
421
|
+
// Emptiness via shared projector; payload keeps original bytes (LLM 原话过手).
|
|
422
|
+
if (prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
423
|
+
const prose = agentProseChunks.join("");
|
|
424
|
+
agentProseChunks.length = 0;
|
|
425
|
+
if (navigatorProseFromUnknown(prose) !== undefined) {
|
|
426
|
+
await prepared.ingestStructuredOutput({ prose });
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
agentProseChunks.length = 0;
|
|
430
|
+
}
|
|
324
431
|
return { status: "delivered", stderr: activeConnection.stderr?.() ?? "" };
|
|
325
432
|
},
|
|
326
433
|
async afterAccepted() {
|
|
@@ -328,14 +435,19 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
328
435
|
accepted = true;
|
|
329
436
|
},
|
|
330
437
|
});
|
|
331
|
-
|
|
332
|
-
|
|
438
|
+
outcome = hostSessionRecordFailure !== undefined ? hostSessionRecordResult() : turnResult;
|
|
439
|
+
} // sessionReady
|
|
440
|
+
} // model match else
|
|
333
441
|
} catch (error) {
|
|
334
442
|
// recordAbort races setup RPCs via raceAgainstHostAbort (host-aborted code);
|
|
335
443
|
// noteHostSessionRecordFailure always sets the typed failure before aborting.
|
|
336
|
-
if (hostSessionRecordFailure !== undefined)
|
|
337
|
-
|
|
444
|
+
if (hostSessionRecordFailure !== undefined) {
|
|
445
|
+
outcome = hostSessionRecordResult();
|
|
446
|
+
} else {
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
338
449
|
}
|
|
450
|
+
} // mcpServers else
|
|
339
451
|
} finally {
|
|
340
452
|
if (connection !== undefined) {
|
|
341
453
|
if (sessionId !== undefined && !accepted) {
|
|
@@ -345,8 +457,12 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
|
|
|
345
457
|
try { await connection.close(); }
|
|
346
458
|
catch { /* keep turn result */ }
|
|
347
459
|
}
|
|
348
|
-
try {
|
|
349
|
-
|
|
460
|
+
try {
|
|
461
|
+
await prepared.dispose?.();
|
|
462
|
+
} catch (cleanupError) {
|
|
463
|
+
outcome = withCleanupFailure(outcome, cleanupError);
|
|
464
|
+
}
|
|
350
465
|
}
|
|
466
|
+
return outcome;
|
|
351
467
|
});
|
|
352
468
|
}
|
|
@@ -83,7 +83,11 @@ export function headlessTurnArgs(options: {
|
|
|
83
83
|
readonly description: ClaudePrintHostDescription;
|
|
84
84
|
/** Absolute path written by the adapter; paired with `systemPromptFlag`. */
|
|
85
85
|
readonly systemPromptPath: string;
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Closed JSON Schema for structured_output seats.
|
|
88
|
+
* Optional: omit for prose-exit seats (#959 navigator) so the model may speak free text.
|
|
89
|
+
*/
|
|
90
|
+
readonly jsonSchema?: Readonly<Record<string, unknown>>;
|
|
87
91
|
/** Absolute path to host-native MCP config JSON; omitted when no AK MCP servers. */
|
|
88
92
|
readonly mcpConfigPath?: string;
|
|
89
93
|
readonly model?: string;
|
|
@@ -99,9 +103,10 @@ export function headlessTurnArgs(options: {
|
|
|
99
103
|
...description.fixedArgs,
|
|
100
104
|
description.systemPromptFlag,
|
|
101
105
|
options.systemPromptPath,
|
|
102
|
-
description.jsonSchemaFlag,
|
|
103
|
-
JSON.stringify(options.jsonSchema),
|
|
104
106
|
];
|
|
107
|
+
if (options.jsonSchema !== undefined) {
|
|
108
|
+
args.push(description.jsonSchemaFlag, JSON.stringify(options.jsonSchema));
|
|
109
|
+
}
|
|
105
110
|
if (options.pluginDir) args.push("--plugin-dir", options.pluginDir);
|
|
106
111
|
if (options.mcpConfigPath !== undefined && options.mcpConfigPath !== "") {
|
|
107
112
|
args.push(description.mcpConfigFlag, options.mcpConfigPath);
|
|
@@ -145,7 +150,7 @@ function codexTomlStringTable(entries: Readonly<Record<string, string>>): string
|
|
|
145
150
|
* Free-form JSON leaf for Type.Unknown under Codex strict transport.
|
|
146
151
|
* Strict rejects bare untyped nodes and root-level additionalProperties:true;
|
|
147
152
|
* a $defs anyOf of JSON values (with nested additionalProperties as $ref)
|
|
148
|
-
* is accepted and keeps array/object receipts expressible
|
|
153
|
+
* is accepted and keeps array/object receipts expressible.
|
|
149
154
|
* Package code still does not validate or reject the receipt against schema.
|
|
150
155
|
*/
|
|
151
156
|
const CODEX_JSON_VALUE_DEF = "codexJsonValue";
|
|
@@ -398,8 +403,11 @@ function codexMcpConfigArgs(
|
|
|
398
403
|
export function codexTurnArgs(options: {
|
|
399
404
|
/** Absolute path for `-c model_instructions_file=…`. */
|
|
400
405
|
readonly systemPromptPath: string;
|
|
401
|
-
/**
|
|
402
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Absolute path for `--output-schema` (closed transport schema).
|
|
408
|
+
* Optional: omit for prose-exit seats (#959 navigator) so agent_message stays free text.
|
|
409
|
+
*/
|
|
410
|
+
readonly outputSchemaPath?: string;
|
|
403
411
|
readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
404
412
|
readonly model?: string;
|
|
405
413
|
readonly effort?: string;
|
|
@@ -441,7 +449,9 @@ export function codexTurnArgs(options: {
|
|
|
441
449
|
}
|
|
442
450
|
|
|
443
451
|
args.push("-c", `model_instructions_file=${codexTomlString(options.systemPromptPath)}`);
|
|
444
|
-
|
|
452
|
+
if (options.outputSchemaPath !== undefined) {
|
|
453
|
+
args.push("--output-schema", options.outputSchemaPath);
|
|
454
|
+
}
|
|
445
455
|
args.push(...codexMcpConfigArgs(options.mcpServers));
|
|
446
456
|
|
|
447
457
|
if (options.model !== undefined && options.model !== "") {
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
isPlainObject,
|
|
40
40
|
type HeadlessHostDescription,
|
|
41
41
|
} from "./description.ts";
|
|
42
|
+
import { NAVIGATOR_OUTPUT_TOOL_NAME } from "../package-contracts/navigator-output.ts";
|
|
42
43
|
|
|
43
44
|
export type HeadlessRoleTurnHostConfig = Readonly<{
|
|
44
45
|
description: HeadlessHostDescription;
|
|
@@ -406,7 +407,8 @@ function terminalFromSpawned(
|
|
|
406
407
|
function buildTurnArgs(options: {
|
|
407
408
|
readonly description: HeadlessHostDescription;
|
|
408
409
|
readonly systemPromptPath: string;
|
|
409
|
-
|
|
410
|
+
/** Closed schema; omit for #959 navigator prose-exit seats. */
|
|
411
|
+
readonly jsonSchema?: Readonly<Record<string, unknown>>;
|
|
410
412
|
readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
411
413
|
readonly mcpConfigPath?: string;
|
|
412
414
|
readonly outputSchemaPath?: string;
|
|
@@ -422,10 +424,10 @@ function buildTurnArgs(options: {
|
|
|
422
424
|
if (options.sessionKind === "resume" && !options.sessionId) {
|
|
423
425
|
throw new Error("codex resume requires a bound thread_id");
|
|
424
426
|
}
|
|
425
|
-
|
|
427
|
+
// #959: prose-exit seats (navigator) omit --output-schema; other seats still require it.
|
|
426
428
|
return codexTurnArgs({
|
|
427
429
|
systemPromptPath: options.systemPromptPath,
|
|
428
|
-
outputSchemaPath: options.outputSchemaPath,
|
|
430
|
+
...(options.outputSchemaPath === undefined ? {} : { outputSchemaPath: options.outputSchemaPath }),
|
|
429
431
|
mcpServers: options.mcpServers,
|
|
430
432
|
...(options.model === undefined ? {} : { model: options.model }),
|
|
431
433
|
...(options.effort === undefined ? {} : { effort: options.effort }),
|
|
@@ -443,7 +445,8 @@ function buildTurnArgs(options: {
|
|
|
443
445
|
return headlessTurnArgs({
|
|
444
446
|
description: options.description,
|
|
445
447
|
systemPromptPath: options.systemPromptPath,
|
|
446
|
-
|
|
448
|
+
// #959: navigator omits --json-schema so free-form prose is a lawful exit.
|
|
449
|
+
...(options.jsonSchema === undefined ? {} : { jsonSchema: options.jsonSchema }),
|
|
447
450
|
mcpConfigPath: options.mcpConfigPath,
|
|
448
451
|
...(options.model === undefined ? {} : { model: options.model }),
|
|
449
452
|
...(options.effort === undefined ? {} : { effort: options.effort }),
|
|
@@ -482,12 +485,15 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
482
485
|
? (prompt) => applyCodexSkillInvocation(request.methods, prompt)
|
|
483
486
|
: (prompt) => prompt;
|
|
484
487
|
if (codex) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
outputSchemaPath,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
488
|
+
// #959: navigator is a prose-exit seat — no closed output-schema.
|
|
489
|
+
if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
490
|
+
outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
|
|
491
|
+
await writeFile(
|
|
492
|
+
outputSchemaPath,
|
|
493
|
+
`${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}\n`,
|
|
494
|
+
"utf8",
|
|
495
|
+
);
|
|
496
|
+
}
|
|
491
497
|
} else {
|
|
492
498
|
mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
|
|
493
499
|
await writeFile(
|
|
@@ -517,7 +523,10 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
517
523
|
args = buildTurnArgs({
|
|
518
524
|
description: config.description,
|
|
519
525
|
systemPromptPath,
|
|
520
|
-
|
|
526
|
+
// #959: navigator prose exit — no closed JSON schema on claude either.
|
|
527
|
+
...(prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME
|
|
528
|
+
? {}
|
|
529
|
+
: { jsonSchema: prepared.jsonSchema }),
|
|
521
530
|
mcpServers: prepared.mcpServers,
|
|
522
531
|
...(mcpConfigPath === undefined ? {} : { mcpConfigPath }),
|
|
523
532
|
...(outputSchemaPath === undefined ? {} : { outputSchemaPath }),
|
|
@@ -657,6 +666,20 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
657
666
|
});
|
|
658
667
|
}
|
|
659
668
|
|
|
669
|
+
// #959: navigator prose exit — final agent_message is presented as-is.
|
|
670
|
+
// Empty text is not a lawful accepted receipt (align pi no_receipt / loud empty).
|
|
671
|
+
if (prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME) {
|
|
672
|
+
if (observation.finalMessage.trim() === "") {
|
|
673
|
+
return terminalFromSpawned(spawned, {
|
|
674
|
+
cause: "output",
|
|
675
|
+
identity: { name: "HeadlessEmptyOutput", code: "empty-stdout" },
|
|
676
|
+
diagnostic: spawned.stderr.trim() || "codex exec produced empty agent_message",
|
|
677
|
+
details: { sessionId, exitCode: spawned.code },
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
await prepared.ingestStructuredOutput({ prose: observation.finalMessage });
|
|
681
|
+
return { status: "delivered", stderr: spawned.stderr };
|
|
682
|
+
}
|
|
660
683
|
let receipt: unknown;
|
|
661
684
|
try {
|
|
662
685
|
receipt = JSON.parse(observation.finalMessage);
|
|
@@ -716,6 +739,14 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
716
739
|
|
|
717
740
|
if (envelope.structured_output !== undefined) {
|
|
718
741
|
await prepared.ingestStructuredOutput(envelope.structured_output);
|
|
742
|
+
} else if (
|
|
743
|
+
prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME
|
|
744
|
+
&& typeof envelope.result === "string"
|
|
745
|
+
&& envelope.result.trim() !== ""
|
|
746
|
+
) {
|
|
747
|
+
// #959: claude prose exit — free-form result text is the receipt body
|
|
748
|
+
// when structured_output is absent (json-schema omitted for navigator).
|
|
749
|
+
await prepared.ingestStructuredOutput({ prose: envelope.result });
|
|
719
750
|
}
|
|
720
751
|
return { status: "delivered", stderr: spawned.stderr };
|
|
721
752
|
},
|
package/src/host-contracts.ts
CHANGED
|
@@ -100,7 +100,6 @@ export type RoleTurnActivation =
|
|
|
100
100
|
| {
|
|
101
101
|
readonly role: "reviewer";
|
|
102
102
|
readonly baseRevision: string;
|
|
103
|
-
/** Caller-selected single lens; no default (ADR 0082 reviewer-method-lens). */
|
|
104
103
|
readonly lens: "completeness" | "correctness";
|
|
105
104
|
readonly authorityRefs: readonly string[];
|
|
106
105
|
readonly ticketNumber?: number;
|