@astrosheep/keiyaku 0.1.87 → 1.0.1

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.
Files changed (158) hide show
  1. package/README.md +74 -65
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/effective-policy.js +32 -0
  4. package/build/agents/harness/event-channel.js +178 -0
  5. package/build/agents/harness/execution-handle.js +117 -0
  6. package/build/agents/harness/index.js +622 -0
  7. package/build/agents/harness/pump.js +152 -0
  8. package/build/agents/harness/runtime.js +68 -0
  9. package/build/agents/index.js +104 -213
  10. package/build/agents/opencode-sdk.js +12 -18
  11. package/build/agents/providers/claude-agent-sdk.js +529 -0
  12. package/build/agents/providers/codex-app-server.js +656 -0
  13. package/build/agents/providers/codex-sdk.js +193 -0
  14. package/build/agents/providers/opencode-sdk.js +257 -0
  15. package/build/agents/providers/pi.js +345 -0
  16. package/build/agents/selector.js +15 -6
  17. package/build/agents/session.js +29 -0
  18. package/build/agents/types.js +5 -11
  19. package/build/cli/commands/akuma.js +79 -0
  20. package/build/cli/completion.js +71 -0
  21. package/build/cli/draft-artifact.js +50 -0
  22. package/build/cli/flags.js +96 -0
  23. package/build/cli/help.js +119 -0
  24. package/build/cli/index.js +204 -912
  25. package/build/cli/parse.js +280 -0
  26. package/build/cli/render/arc.js +63 -0
  27. package/build/{responses/tool-errors.js → cli/render/errors.js} +15 -16
  28. package/build/cli/render/format.js +199 -0
  29. package/build/cli/render/misc.js +40 -0
  30. package/build/cli/render/petition.js +32 -0
  31. package/build/{responses → cli/render}/response-style.js +4 -4
  32. package/build/cli/render/shared.js +122 -0
  33. package/build/cli/render/status.js +69 -0
  34. package/build/cli/render/summon.js +17 -0
  35. package/build/cli/render/tell.js +11 -0
  36. package/build/cli/skills-install.js +237 -0
  37. package/build/cli/subagent-guard.js +6 -3
  38. package/build/config/akuma-loader.js +268 -0
  39. package/build/config/env-keys.js +0 -10
  40. package/build/config/{schema.js → env.js} +117 -23
  41. package/build/config/settings.js +137 -260
  42. package/build/core/amend.js +120 -0
  43. package/build/core/arc.js +225 -0
  44. package/build/core/atomic-publish.js +64 -0
  45. package/build/core/bind.js +281 -0
  46. package/build/core/claim.js +360 -0
  47. package/build/core/command-io.js +174 -0
  48. package/build/core/context.js +108 -0
  49. package/build/core/contract.js +14 -0
  50. package/build/core/draft.js +82 -0
  51. package/build/core/entry.js +203 -0
  52. package/build/core/forfeit.js +60 -0
  53. package/build/core/hints.js +108 -0
  54. package/build/core/ids.js +70 -0
  55. package/build/core/ledger.js +117 -0
  56. package/build/core/log.js +24 -0
  57. package/build/{protocol → core}/markdown/parser.js +68 -0
  58. package/build/{protocol → core}/markdown/sections.js +2 -28
  59. package/build/core/markdown/titles.js +9 -0
  60. package/build/{tools/petition/claim-gates.js → core/petition-claim-gates.js} +43 -29
  61. package/build/core/petition-claim.js +116 -0
  62. package/build/core/petition-forfeit.js +39 -0
  63. package/build/{tools/round/head-guard.js → core/petition-head-guard.js} +2 -2
  64. package/build/core/petition-run.js +81 -0
  65. package/build/core/petition.js +96 -0
  66. package/build/core/places.js +82 -0
  67. package/build/core/projection-core.js +541 -0
  68. package/build/core/projection-mint.js +79 -0
  69. package/build/core/projection-status.js +179 -0
  70. package/build/core/projection-wake.js +252 -0
  71. package/build/core/queue.js +73 -0
  72. package/build/core/ref-log.js +94 -0
  73. package/build/core/registry.js +96 -0
  74. package/build/core/render.js +326 -0
  75. package/build/core/renew.js +195 -0
  76. package/build/core/scope.js +163 -0
  77. package/build/core/seal.js +195 -0
  78. package/build/core/status.js +476 -0
  79. package/build/{tools/summon/persist.js → core/summon-persist.js} +10 -9
  80. package/build/core/summon.js +377 -0
  81. package/build/{protocol/response-history.js → core/transcripts.js} +83 -39
  82. package/build/core/verdict.js +96 -0
  83. package/build/core/worktree-path.js +134 -0
  84. package/build/flow-error.js +80 -4
  85. package/build/generated/version.js +1 -1
  86. package/build/git/branches.js +8 -9
  87. package/build/git/core.js +1 -1
  88. package/build/git/diff/pathspec.js +2 -3
  89. package/build/git/diff/preview.js +7 -2
  90. package/build/git/diff/read.js +4 -3
  91. package/build/git/staging.js +1 -1
  92. package/build/index.js +12 -6
  93. package/build/keiyaku.js +17 -13
  94. package/build/telemetry/debug-log.js +1 -1
  95. package/package.json +16 -9
  96. package/skills/keiyaku/SKILL.md +16 -13
  97. package/build/agents/claude-agent-sdk-exec.js +0 -352
  98. package/build/agents/codex-cli-exec.js +0 -237
  99. package/build/agents/codex-sdk-exec.js +0 -291
  100. package/build/agents/gemini-cli-core-exec.js +0 -499
  101. package/build/agents/opencode-exec.js +0 -297
  102. package/build/agents/process-runner.js +0 -239
  103. package/build/agents/progress-reporter.js +0 -137
  104. package/build/agents/round-runner.js +0 -53
  105. package/build/config/apply-argument-descriptions.js +0 -16
  106. package/build/config/architect-hints.js +0 -374
  107. package/build/config/base-rules.js +0 -23
  108. package/build/config/config.js +0 -50
  109. package/build/config/dotenv.js +0 -61
  110. package/build/config/keiyaku-home.js +0 -9
  111. package/build/config/path-utils.js +0 -11
  112. package/build/config/render-template.js +0 -8
  113. package/build/git/diff/filter.js +0 -67
  114. package/build/git/keiyaku-state.js +0 -58
  115. package/build/git/snapshot.js +0 -41
  116. package/build/git/stash.js +0 -62
  117. package/build/protocol/bind-draft.js +0 -195
  118. package/build/protocol/draft-artifacts.js +0 -99
  119. package/build/protocol/file-guards.js +0 -24
  120. package/build/protocol/keiyaku-draft.js +0 -168
  121. package/build/protocol/ledger.js +0 -374
  122. package/build/protocol/markdown/index.js +0 -14
  123. package/build/protocol/markdown/normalization.js +0 -71
  124. package/build/protocol/markdown/render.js +0 -68
  125. package/build/protocol/oath.js +0 -36
  126. package/build/protocol/render-keiyaku.js +0 -37
  127. package/build/protocol/review.js +0 -1
  128. package/build/protocol/status-previews.js +0 -68
  129. package/build/responses/responses.js +0 -515
  130. package/build/tools/amend/index.js +0 -18
  131. package/build/tools/amend/run.js +0 -44
  132. package/build/tools/bind/index.js +0 -44
  133. package/build/tools/bind/run.js +0 -237
  134. package/build/tools/contracts.js +0 -1
  135. package/build/tools/create-handler.js +0 -31
  136. package/build/tools/help.js +0 -9
  137. package/build/tools/petition/claim.js +0 -174
  138. package/build/tools/petition/forfeit.js +0 -100
  139. package/build/tools/petition/index.js +0 -83
  140. package/build/tools/petition/run.js +0 -66
  141. package/build/tools/round/guidance.js +0 -7
  142. package/build/tools/round/prompt.js +0 -70
  143. package/build/tools/round/report.js +0 -153
  144. package/build/tools/round/worktree.js +0 -55
  145. package/build/tools/round-close/index.js +0 -19
  146. package/build/tools/round-close/run.js +0 -98
  147. package/build/tools/round-open/index.js +0 -20
  148. package/build/tools/round-open/run.js +0 -77
  149. package/build/tools/schema.js +0 -69
  150. package/build/tools/status/index.js +0 -16
  151. package/build/tools/status/read.js +0 -155
  152. package/build/tools/summon/index.js +0 -27
  153. package/build/tools/summon/run.js +0 -80
  154. /package/build/{config/toml.js → agents/harness/events.js} +0 -0
  155. /package/build/{git/diff/contracts.js → cli/render/types.js} +0 -0
  156. /package/build/{protocol/bind-input.js → cli/types.js} +0 -0
  157. /package/build/{protocol → core}/markdown/lex.js +0 -0
  158. /package/build/{protocol → core}/markdown/types.js +0 -0
@@ -0,0 +1,656 @@
1
+ import { spawn } from "node:child_process";
2
+ import { z } from "zod";
3
+ import { appendDebugLog } from "../../telemetry/debug-log.js";
4
+ import { SUBAGENT_SESSION_VERSION } from "../session.js";
5
+ import { createProjectionDriver } from "../harness/execution-handle.js";
6
+ import { buildSubagentEnv, createAbortError, emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
7
+ import { coerceString } from "../../agents/index.js";
8
+ /** Session handle for codex-app-server: thread id plus optional CODEX_HOME for cross-process resume. */
9
+ export const codexAppServerSessionSchema = z.object({
10
+ threadId: z.string().trim().min(1),
11
+ codexHome: z.string().trim().min(1).optional(),
12
+ });
13
+ const CODEX_APP_SERVER_PROVIDER = "codex-app-server";
14
+ const MINIMUM_CODEX_APP_SERVER_VERSION = { major: 0, minor: 143, patch: 0 };
15
+ /** Non-interactive defaults for turn/start (faye #316 policy A). */
16
+ export const CODEX_APP_SERVER_TURN_APPROVAL_POLICY = "never";
17
+ export function buildCodexAppServerDefaultSandboxPolicy(cwd) {
18
+ return {
19
+ type: "workspaceWrite",
20
+ writableRoots: [cwd],
21
+ networkAccess: false,
22
+ excludeTmpdirEnvVar: false,
23
+ excludeSlashTmp: false,
24
+ };
25
+ }
26
+ /** True when config.* already owns approval policy — omit turn-level approvalPolicy default. */
27
+ export function configOwnsApprovalPolicy(config) {
28
+ if (!config)
29
+ return false;
30
+ return Object.prototype.hasOwnProperty.call(config, "approval_policy");
31
+ }
32
+ /**
33
+ * True when config.* already owns sandbox settings — omit turn-level sandboxPolicy default.
34
+ * Matches `sandbox_mode` or any `sandbox_workspace_write` / `sandbox_workspace_write.*` key.
35
+ */
36
+ export function configOwnsSandboxPolicy(config) {
37
+ if (!config)
38
+ return false;
39
+ for (const key of Object.keys(config)) {
40
+ if (key === "sandbox_mode" || key === "sandbox_workspace_write" || key.startsWith("sandbox_workspace_write.")) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ function parseSemver(version) {
47
+ const match = version.trim().match(/(\d+)\.(\d+)\.(\d+)/);
48
+ if (!match)
49
+ return undefined;
50
+ return {
51
+ major: Number(match[1]),
52
+ minor: Number(match[2]),
53
+ patch: Number(match[3]),
54
+ };
55
+ }
56
+ export function isCodexAppServerVersionBelowMinimum(version) {
57
+ if (!version)
58
+ return false;
59
+ const parsed = parseSemver(version);
60
+ if (!parsed)
61
+ return false;
62
+ if (parsed.major !== MINIMUM_CODEX_APP_SERVER_VERSION.major) {
63
+ return parsed.major < MINIMUM_CODEX_APP_SERVER_VERSION.major;
64
+ }
65
+ if (parsed.minor !== MINIMUM_CODEX_APP_SERVER_VERSION.minor) {
66
+ return parsed.minor < MINIMUM_CODEX_APP_SERVER_VERSION.minor;
67
+ }
68
+ return parsed.patch < MINIMUM_CODEX_APP_SERVER_VERSION.patch;
69
+ }
70
+ function extractVersionFromInitialize(result) {
71
+ if (!result || typeof result !== "object")
72
+ return undefined;
73
+ const record = result;
74
+ const userAgent = coerceString(record.userAgent);
75
+ if (userAgent) {
76
+ const match = userAgent.match(/(\d+\.\d+\.\d+)/);
77
+ if (match)
78
+ return match[1];
79
+ }
80
+ return coerceString(record.version) ?? coerceString(record.cliVersion);
81
+ }
82
+ class CodexAppServerProcess {
83
+ onEvidence;
84
+ onEvidenceDiagnostic;
85
+ child;
86
+ pending = new Map();
87
+ notificationListeners = new Set();
88
+ serverRequestListeners = new Set();
89
+ exitPromise;
90
+ nextId = 1;
91
+ stdoutBuffer = "";
92
+ closed = false;
93
+ exitSettled = false;
94
+ codexHome;
95
+ constructor(executable, env, codexHome, onEvidence, onEvidenceDiagnostic) {
96
+ this.onEvidence = onEvidence;
97
+ this.onEvidenceDiagnostic = onEvidenceDiagnostic;
98
+ this.codexHome = codexHome;
99
+ const spawnEnv = {
100
+ ...env,
101
+ ...(codexHome ? { CODEX_HOME: codexHome } : {}),
102
+ };
103
+ this.child = spawn(executable, ["app-server", "--listen", "stdio://"], {
104
+ env: spawnEnv,
105
+ stdio: ["pipe", "pipe", "pipe"],
106
+ });
107
+ if (this.onEvidence && this.child.pid)
108
+ emitProviderEvidence(this.onEvidence, { type: "process", provider: CODEX_APP_SERVER_PROVIDER, event: "spawn", pid: this.child.pid }, this.onEvidenceDiagnostic);
109
+ this.child.stdout.setEncoding("utf8");
110
+ this.child.stderr.setEncoding("utf8");
111
+ this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
112
+ // Preserve the pre-evidence diagnostic stream exactly. Process evidence
113
+ // never retains stderr bytes, avoiding credential boundary ambiguity.
114
+ this.child.stderr.on("data", (chunk) => this.onEvidenceDiagnostic?.(chunk));
115
+ this.child.on("error", (error) => this.failAll(error));
116
+ this.exitPromise = new Promise((resolve) => {
117
+ this.child.once("close", (exitCode) => {
118
+ this.exitSettled = true;
119
+ this.closed = true;
120
+ // With no observer, retain the historical error bytes. Evidence adds
121
+ // structured exit facts without changing provider behavior.
122
+ this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
123
+ if (this.onEvidence)
124
+ emitProviderEvidence(this.onEvidence, { type: "process", provider: CODEX_APP_SERVER_PROVIDER, event: "exit", exitCode, stderrTail: "" }, this.onEvidenceDiagnostic);
125
+ resolve();
126
+ });
127
+ });
128
+ }
129
+ onNotification(listener) {
130
+ this.notificationListeners.add(listener);
131
+ return () => this.notificationListeners.delete(listener);
132
+ }
133
+ onServerRequest(listener) {
134
+ this.serverRequestListeners.add(listener);
135
+ return () => this.serverRequestListeners.delete(listener);
136
+ }
137
+ onStdout(chunk) {
138
+ this.stdoutBuffer += chunk;
139
+ let newline = this.stdoutBuffer.indexOf("\n");
140
+ while (newline >= 0) {
141
+ const line = this.stdoutBuffer.slice(0, newline).trim();
142
+ this.stdoutBuffer = this.stdoutBuffer.slice(newline + 1);
143
+ if (line.length > 0) {
144
+ this.handleLine(line);
145
+ }
146
+ newline = this.stdoutBuffer.indexOf("\n");
147
+ }
148
+ }
149
+ handleLine(line) {
150
+ let message;
151
+ try {
152
+ message = JSON.parse(line);
153
+ }
154
+ catch {
155
+ return;
156
+ }
157
+ if (!message || typeof message !== "object")
158
+ return;
159
+ const record = message;
160
+ if ("id" in record && ("result" in record || "error" in record)) {
161
+ const id = record.id;
162
+ const pending = this.pending.get(id);
163
+ if (!pending)
164
+ return;
165
+ this.pending.delete(id);
166
+ if ("error" in record && record.error) {
167
+ const error = record.error;
168
+ pending.reject(new Error(error.message ?? `${CODEX_APP_SERVER_PROVIDER} RPC error`));
169
+ return;
170
+ }
171
+ pending.resolve(record.result);
172
+ return;
173
+ }
174
+ // Server→client request: method + id. Never ignore, auto-approve, or auto-deny (faye #316).
175
+ if (typeof record.method === "string" && "id" in record) {
176
+ const request = {
177
+ id: record.id,
178
+ method: record.method,
179
+ params: record.params ?? undefined,
180
+ };
181
+ for (const listener of this.serverRequestListeners) {
182
+ listener(request);
183
+ }
184
+ return;
185
+ }
186
+ if (typeof record.method === "string" && !("id" in record)) {
187
+ const notification = {
188
+ method: record.method,
189
+ params: record.params ?? undefined,
190
+ };
191
+ for (const listener of this.notificationListeners) {
192
+ listener(notification);
193
+ }
194
+ }
195
+ }
196
+ failAll(error) {
197
+ for (const pending of this.pending.values()) {
198
+ pending.reject(error);
199
+ }
200
+ this.pending.clear();
201
+ }
202
+ request(method, params) {
203
+ if (this.closed) {
204
+ return Promise.reject(new Error(`${CODEX_APP_SERVER_PROVIDER} process is closed`));
205
+ }
206
+ const id = this.nextId++;
207
+ const payload = params === undefined ? { id, method } : { id, method, params };
208
+ const serialized = `${JSON.stringify(payload)}\n`;
209
+ return new Promise((resolve, reject) => {
210
+ this.pending.set(id, { resolve, reject });
211
+ this.child.stdin.write(serialized, (error) => {
212
+ if (error) {
213
+ this.pending.delete(id);
214
+ reject(error);
215
+ }
216
+ });
217
+ // The write call above is the process boundary. Emitting afterward
218
+ // avoids evidence for a synchronous write failure while keeping the
219
+ // outbound fact before any asynchronous RPC acknowledgement.
220
+ this.emitOutbound(method, params);
221
+ });
222
+ }
223
+ notify(method, params) {
224
+ const payload = params === undefined ? { method } : { method, params };
225
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`);
226
+ }
227
+ emitOutbound(method, params) {
228
+ if (!this.onEvidence)
229
+ return;
230
+ const kind = method === "thread/start" ? "thread-start" : method === "thread/resume" ? "thread-resume" : method === "turn/start" ? "turn-start" : method === "turn/steer" ? "steer" : undefined;
231
+ if (!kind)
232
+ return;
233
+ const text = method === "turn/steer"
234
+ ? coerceString((params?.input?.[0]?.text))
235
+ : JSON.stringify(params ?? {});
236
+ if (text === undefined)
237
+ return;
238
+ emitProviderEvidence(this.onEvidence, { type: "outbound", provider: CODEX_APP_SERVER_PROVIDER, direction: "request", kind, ...sha256Utf8(text) }, this.onEvidenceDiagnostic);
239
+ const developerInstructions = coerceString(params?.developerInstructions);
240
+ if ((method === "thread/start" || method === "thread/resume") && developerInstructions !== undefined) {
241
+ emitProviderEvidence(this.onEvidence, { type: "outbound", provider: CODEX_APP_SERVER_PROVIDER, direction: "request", kind: "system-prompt", ...sha256Utf8(developerInstructions) }, this.onEvidenceDiagnostic);
242
+ }
243
+ }
244
+ async close() {
245
+ if (this.exitSettled) {
246
+ this.closed = true;
247
+ return;
248
+ }
249
+ this.closed = true;
250
+ this.failAll(new Error(`${CODEX_APP_SERVER_PROVIDER} process closed`));
251
+ try {
252
+ this.child.stdin.end();
253
+ }
254
+ catch {
255
+ // ignore
256
+ }
257
+ // Destroy stdio so a hung child cannot keep the parent event loop alive via open pipes.
258
+ try {
259
+ this.child.stdout.destroy();
260
+ }
261
+ catch {
262
+ // ignore
263
+ }
264
+ try {
265
+ this.child.stderr.destroy();
266
+ }
267
+ catch {
268
+ // ignore
269
+ }
270
+ if (!this.exitSettled && this.child.exitCode === null && this.child.signalCode === null) {
271
+ try {
272
+ this.child.kill("SIGTERM");
273
+ }
274
+ catch {
275
+ // ignore
276
+ }
277
+ }
278
+ // Escalate to SIGKILL if the child ignores stdin EOF + SIGTERM, then wait for exit
279
+ // (or give up after a hard bound so close() itself cannot pin the event loop).
280
+ const forceKill = new Promise((resolve) => {
281
+ const timer = setTimeout(() => {
282
+ if (!this.exitSettled) {
283
+ try {
284
+ this.child.kill("SIGKILL");
285
+ }
286
+ catch {
287
+ // ignore
288
+ }
289
+ }
290
+ resolve();
291
+ }, 1000);
292
+ timer.unref?.();
293
+ });
294
+ const hardBound = new Promise((resolve) => {
295
+ const timer = setTimeout(resolve, 2000);
296
+ timer.unref?.();
297
+ });
298
+ await Promise.race([this.exitPromise, Promise.all([forceKill, this.exitPromise]).then(() => undefined), hardBound]);
299
+ }
300
+ }
301
+ /** Gate-facing production entry: the harness owns clocks, capture, classification, and outcome construction. */
302
+ export async function startCodexAppServer(prompt, cwd, options = {}) {
303
+ const executable = options.executable?.trim() || "codex";
304
+ const resumeThreadId = options.threadId?.trim();
305
+ let server;
306
+ let activeTurnId;
307
+ let activeTurnSource;
308
+ let activeTurnAcceptingSteer = false;
309
+ let activeThreadId;
310
+ let interruptRequested = false;
311
+ let lastAgentMessage = "";
312
+ let builderDiagnostic;
313
+ const pendingTells = [];
314
+ let drainingTells = false;
315
+ let failActiveTurn;
316
+ const rejectPendingTells = (error) => {
317
+ for (const pending of pendingTells.splice(0))
318
+ pending.reject(error);
319
+ };
320
+ const recordActiveTurnId = (candidate, source) => {
321
+ const turnId = coerceString(candidate);
322
+ if (!turnId)
323
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} ${source} omitted active turn id`);
324
+ if (!activeTurnId) {
325
+ activeTurnId = turnId;
326
+ activeTurnSource = source;
327
+ return turnId;
328
+ }
329
+ if (activeTurnId !== turnId) {
330
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} active turn id mismatch: ${activeTurnSource ?? "first"}=${activeTurnId}, ${source}=${turnId}`);
331
+ }
332
+ return activeTurnId;
333
+ };
334
+ appendDebugLog(`startCodexAppServer: executable=${executable} model=${options.model ?? "default"} mode=${resumeThreadId ? "resume" : "fresh"}`, { cwd, section: CODEX_APP_SERVER_PROVIDER });
335
+ const submitSteer = async (tell, turnId) => {
336
+ // turn/steer response {turnId} = enqueue acceptance (submitted), not consumption.
337
+ // clientUserMessageId = tell.id for later delivered proof via userMessage item clientId.
338
+ // tell.text is already core-rendered; send byte-for-byte — never parse or rewrite.
339
+ const ack = await server.request("turn/steer", {
340
+ threadId: activeThreadId,
341
+ expectedTurnId: turnId,
342
+ clientUserMessageId: tell.id,
343
+ input: [{ type: "text", text: tell.text }],
344
+ });
345
+ const acknowledgedTurnId = coerceString(ack.turnId) ?? turnId;
346
+ if (acknowledgedTurnId !== turnId) {
347
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} turn/steer acknowledgement mismatched active turn: expected=${turnId}, actual=${acknowledgedTurnId}`);
348
+ }
349
+ if (options.onEvidence) {
350
+ emitProviderEvidence(options.onEvidence, {
351
+ type: "ack",
352
+ provider: CODEX_APP_SERVER_PROVIDER,
353
+ fence: { kind: "turn", turnId: acknowledgedTurnId },
354
+ outbound: "steer",
355
+ }, builderDiagnostic);
356
+ }
357
+ return { fence: { kind: "turn", turnId: acknowledgedTurnId } };
358
+ };
359
+ const startTellTurn = async (tell) => {
360
+ const started = await server.request("turn/start", {
361
+ threadId: activeThreadId,
362
+ input: [{ type: "text", text: tell.text }],
363
+ ...(options.model ? { model: options.model } : {}),
364
+ ...(tell.effort ? { effort: tell.effort } : {}),
365
+ ...(!configOwnsApprovalPolicy(options.config) ? { approvalPolicy: CODEX_APP_SERVER_TURN_APPROVAL_POLICY } : {}),
366
+ ...(!configOwnsSandboxPolicy(options.config) ? { sandboxPolicy: buildCodexAppServerDefaultSandboxPolicy(cwd) } : {}),
367
+ });
368
+ const startedTurn = started && typeof started === "object"
369
+ ? (started.turn ?? undefined)
370
+ : undefined;
371
+ const newTurnId = recordActiveTurnId(startedTurn?.id, "response");
372
+ if (options.onEvidence)
373
+ emitProviderEvidence(options.onEvidence, { type: "ack", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId: newTurnId }, outbound: "run" }, builderDiagnostic);
374
+ return { fence: { kind: "turn", turnId: newTurnId } };
375
+ };
376
+ const drainPendingTells = async () => {
377
+ if (drainingTells)
378
+ return;
379
+ drainingTells = true;
380
+ try {
381
+ while (pendingTells.length > 0 && server && activeThreadId) {
382
+ const pending = pendingTells[0];
383
+ if (pending.tell.effort && activeTurnId)
384
+ return;
385
+ if (!pending.tell.effort && activeTurnId && !activeTurnAcceptingSteer)
386
+ return;
387
+ pendingTells.shift();
388
+ try {
389
+ const ack = activeTurnId
390
+ ? await submitSteer(pending.tell, activeTurnId)
391
+ : await startTellTurn(pending.tell);
392
+ pending.resolve(ack);
393
+ }
394
+ catch (error) {
395
+ pending.reject(error);
396
+ failActiveTurn?.(error);
397
+ return;
398
+ }
399
+ }
400
+ }
401
+ finally {
402
+ drainingTells = false;
403
+ }
404
+ };
405
+ const enqueue = async (tell) => {
406
+ if (!server || !activeThreadId) {
407
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} cannot steer: no active turn`);
408
+ }
409
+ return await new Promise((resolve, reject) => {
410
+ pendingTells.push({ tell, resolve, reject });
411
+ void drainPendingTells();
412
+ });
413
+ };
414
+ return createProjectionDriver({
415
+ steerMode: "mid-execution",
416
+ enqueue,
417
+ async abort() {
418
+ interruptRequested = true;
419
+ if (server && activeThreadId && activeTurnId) {
420
+ try {
421
+ // RPC acknowledgement is NOT terminal proof; wait for turn/completed interrupted.
422
+ await server.request("turn/interrupt", {
423
+ threadId: activeThreadId,
424
+ turnId: activeTurnId,
425
+ });
426
+ }
427
+ catch {
428
+ // idle interrupt may error; still rely on process teardown
429
+ }
430
+ }
431
+ await server?.close();
432
+ },
433
+ async run(builder) {
434
+ builderDiagnostic = (text) => builder.emit({ type: "diagnostic", text });
435
+ const env = buildSubagentEnv();
436
+ server = new CodexAppServerProcess(executable, env, options.codexHome, options.onEvidence, (text) => builder.emit({ type: "diagnostic", text }));
437
+ const turnCompleted = new Promise((resolve, reject) => {
438
+ let unsubNotifications = () => { };
439
+ let unsubServerRequest = () => { };
440
+ let settled = false;
441
+ const failTurn = (error) => {
442
+ if (settled)
443
+ return;
444
+ settled = true;
445
+ rejectPendingTells(error);
446
+ const message = error instanceof Error ? error.message : String(error);
447
+ builder.emit({ type: "diagnostic", text: message });
448
+ builder.emit({ type: "fail", message });
449
+ unsubNotifications();
450
+ unsubServerRequest();
451
+ reject(error);
452
+ };
453
+ const failServerRequest = (request) => {
454
+ // Policy A forbids interactive server→client requests. Fail immediately with named diagnostic.
455
+ const message = `${CODEX_APP_SERVER_PROVIDER} unexpected server request '${request.method}' (id=${String(request.id)}); non-interactive policy forbids server→client requests`;
456
+ failTurn(new Error(message));
457
+ };
458
+ unsubServerRequest = server.onServerRequest((request) => {
459
+ failServerRequest(request);
460
+ });
461
+ unsubNotifications = server.onNotification((notification) => {
462
+ try {
463
+ builder.emit({ type: "output", value: notification });
464
+ if (notification.method === "item/agentMessage/delta") {
465
+ const delta = coerceString(notification.params?.delta);
466
+ if (delta) {
467
+ lastAgentMessage = `${lastAgentMessage}${delta}`;
468
+ builder.emit({ type: "message", text: lastAgentMessage });
469
+ }
470
+ }
471
+ if (notification.method === "item/completed") {
472
+ const item = notification.params?.item;
473
+ if (item && (item.type === "agentMessage" || item.type === "agent_message")) {
474
+ const text = coerceString(item.text);
475
+ if (text) {
476
+ lastAgentMessage = text;
477
+ builder.emit({ type: "message", text });
478
+ }
479
+ }
480
+ }
481
+ if (notification.method === "turn/started") {
482
+ const turn = notification.params?.turn;
483
+ const turnId = recordActiveTurnId(turn?.id, "notification");
484
+ activeTurnAcceptingSteer = true;
485
+ const checkpoint = { fence: { kind: "turn", turnId } };
486
+ if (options.onEvidence) {
487
+ emitProviderEvidence(options.onEvidence, {
488
+ type: "checkpoint",
489
+ provider: CODEX_APP_SERVER_PROVIDER,
490
+ ...checkpoint,
491
+ source: "notification",
492
+ }, builderDiagnostic);
493
+ }
494
+ // turn/start may respond before the server has installed its active
495
+ // turn. The native notification is the first steer-safe boundary.
496
+ options.onSyncCheckpoint?.({ enqueue }, checkpoint);
497
+ void drainPendingTells();
498
+ }
499
+ if (notification.method === "turn/completed") {
500
+ const turn = notification.params?.turn ?? {};
501
+ const status = coerceString(turn.status) ?? "unknown";
502
+ // Either response or notification may mint the turn fence first;
503
+ // every later native source must agree with it.
504
+ const turnId = recordActiveTurnId(turn.id, "notification");
505
+ // This must stay in the native notification call stack: the harness
506
+ // checkpoint and its observer evidence describe this exact crossing.
507
+ if (status === "completed") {
508
+ activeTurnAcceptingSteer = false;
509
+ activeTurnId = undefined;
510
+ activeTurnSource = undefined;
511
+ const checkpoint = { fence: { kind: "turn", turnId } };
512
+ if (options.onEvidence) {
513
+ emitProviderEvidence(options.onEvidence, {
514
+ type: "checkpoint",
515
+ provider: CODEX_APP_SERVER_PROVIDER,
516
+ ...checkpoint,
517
+ source: "notification",
518
+ }, builderDiagnostic);
519
+ }
520
+ options.onSyncCheckpoint?.({ enqueue }, checkpoint);
521
+ void drainPendingTells();
522
+ if (pendingTells.length > 0 || drainingTells) {
523
+ return;
524
+ }
525
+ }
526
+ if (settled)
527
+ return;
528
+ settled = true;
529
+ unsubNotifications();
530
+ unsubServerRequest();
531
+ resolve({ status, turn });
532
+ }
533
+ if (notification.method === "error") {
534
+ const message = coerceString(notification.params?.message) ?? `${CODEX_APP_SERVER_PROVIDER} error`;
535
+ builder.emit({ type: "diagnostic", text: message });
536
+ }
537
+ }
538
+ catch (error) {
539
+ // Notification listeners run outside the driver's await chain;
540
+ // convert validation failures into the controlled completion path.
541
+ failTurn(error);
542
+ }
543
+ });
544
+ failActiveTurn = failTurn;
545
+ builder.signal.addEventListener("abort", () => {
546
+ unsubNotifications();
547
+ unsubServerRequest();
548
+ failTurn(createAbortError(`${CODEX_APP_SERVER_PROVIDER} exec cancelled by client`));
549
+ }, { once: true });
550
+ });
551
+ // Abort can settle this before initialization reaches the await below.
552
+ // Attach a handler immediately while preserving the original awaited error.
553
+ turnCompleted.catch(() => undefined);
554
+ try {
555
+ const initResult = await server.request("initialize", {
556
+ clientInfo: {
557
+ name: "keiyaku",
558
+ title: "keiyaku",
559
+ version: "0.0.0",
560
+ },
561
+ capabilities: {
562
+ experimentalApi: false,
563
+ requestAttestation: false,
564
+ },
565
+ });
566
+ const version = extractVersionFromInitialize(initResult);
567
+ if (isCodexAppServerVersionBelowMinimum(version)) {
568
+ const message = `${CODEX_APP_SERVER_PROVIDER} refused: initialize version '${version}' is below minimum supported version 0.143.0`;
569
+ builder.emit({ type: "fail", message });
570
+ throw new Error(message);
571
+ }
572
+ const initRecord = (initResult && typeof initResult === "object" ? initResult : {});
573
+ const serverCodexHome = coerceString(initRecord.codexHome) ?? options.codexHome;
574
+ server.notify("initialized");
575
+ const threadParams = {
576
+ cwd,
577
+ ...(options.model ? { model: options.model } : {}),
578
+ ...(options.config ? { config: options.config } : {}),
579
+ ...(options.developerInstructions ? { developerInstructions: options.developerInstructions } : {}),
580
+ };
581
+ const threadResult = resumeThreadId
582
+ ? await server.request("thread/resume", {
583
+ threadId: resumeThreadId,
584
+ ...threadParams,
585
+ })
586
+ : await server.request("thread/start", threadParams);
587
+ const thread = threadResult && typeof threadResult === "object"
588
+ ? (threadResult.thread ??
589
+ threadResult)
590
+ : undefined;
591
+ activeThreadId = coerceString(thread?.id) ?? resumeThreadId;
592
+ if (!activeThreadId) {
593
+ const message = `${CODEX_APP_SERVER_PROVIDER} exec could not determine thread id`;
594
+ builder.emit({ type: "fail", message });
595
+ throw new Error(message);
596
+ }
597
+ if (options.onEvidence)
598
+ emitProviderEvidence(options.onEvidence, { type: "session", provider: CODEX_APP_SERVER_PROVIDER, threadId: activeThreadId }, builderDiagnostic);
599
+ // Policy A defaults, but omit when config.* already owns that surface (thread/start config wins).
600
+ const turnStartParams = {
601
+ threadId: activeThreadId,
602
+ input: [{ type: "text", text: prompt }],
603
+ ...(options.model ? { model: options.model } : {}),
604
+ ...(options.effort ? { effort: options.effort } : {}),
605
+ };
606
+ if (!configOwnsApprovalPolicy(options.config)) {
607
+ turnStartParams.approvalPolicy = CODEX_APP_SERVER_TURN_APPROVAL_POLICY;
608
+ }
609
+ if (!configOwnsSandboxPolicy(options.config)) {
610
+ turnStartParams.sandboxPolicy = buildCodexAppServerDefaultSandboxPolicy(cwd);
611
+ }
612
+ activeTurnId = undefined;
613
+ activeTurnSource = undefined;
614
+ activeTurnAcceptingSteer = false;
615
+ const turnStart = await server.request("turn/start", turnStartParams);
616
+ const turn = turnStart && typeof turnStart === "object"
617
+ ? (turnStart.turn ?? undefined)
618
+ : undefined;
619
+ recordActiveTurnId(turn?.id, "response");
620
+ if (options.onEvidence)
621
+ emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId: activeTurnId }, source: "response" }, builderDiagnostic);
622
+ const completed = await turnCompleted;
623
+ if (completed.status === "interrupted" || interruptRequested) {
624
+ throw createAbortError(`${CODEX_APP_SERVER_PROVIDER} turn interrupted`);
625
+ }
626
+ if (completed.status === "failed") {
627
+ const message = `${CODEX_APP_SERVER_PROVIDER} turn failed`;
628
+ builder.emit({ type: "fail", message });
629
+ throw new Error(message);
630
+ }
631
+ if (completed.status !== "completed") {
632
+ const message = `${CODEX_APP_SERVER_PROVIDER} turn ended with status '${completed.status}'`;
633
+ builder.emit({ type: "fail", message });
634
+ throw new Error(message);
635
+ }
636
+ // Successful turn/completed is consumption evidence; interrupt ack alone is not terminal proof.
637
+ const session = {
638
+ provider: "codex-app-server",
639
+ version: SUBAGENT_SESSION_VERSION,
640
+ handle: {
641
+ threadId: activeThreadId,
642
+ ...(serverCodexHome ? { codexHome: serverCodexHome } : {}),
643
+ },
644
+ };
645
+ const finalMessage = lastAgentMessage;
646
+ builder.emit({ type: "done", finalMessage, session });
647
+ return { finalMessage, session };
648
+ }
649
+ finally {
650
+ rejectPendingTells(new Error(`${CODEX_APP_SERVER_PROVIDER} execution ended before queued tell acceptance`));
651
+ await server?.close();
652
+ server = undefined;
653
+ }
654
+ },
655
+ });
656
+ }