@bjoernrochel/sevro 0.1.0-rc.7 → 1.0.0

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 (230) hide show
  1. package/CONTRIBUTING.md +22 -6
  2. package/README.md +5 -1
  3. package/docs/README.md +19 -15
  4. package/docs/architecture.md +138 -17
  5. package/docs/assets/README.md +5 -4
  6. package/docs/builtin-graders-v1.md +17 -3
  7. package/docs/codex-host.md +155 -49
  8. package/docs/contracts.md +2 -1
  9. package/docs/creating-cases.md +9 -2
  10. package/docs/development-cli.md +187 -19
  11. package/docs/documentation-quality.md +12 -2
  12. package/docs/evaluation-workflow.md +110 -0
  13. package/docs/extension-protocol-v1.md +26 -3
  14. package/docs/getting-started.md +23 -2
  15. package/docs/guide-evaluation.md +33 -1
  16. package/docs/identity-v1.md +25 -2
  17. package/docs/installing.md +11 -11
  18. package/docs/native-controls.md +76 -5
  19. package/docs/native-hosts.md +196 -7
  20. package/docs/reading-results.md +102 -0
  21. package/docs/recipes.md +489 -0
  22. package/docs/releases.md +31 -12
  23. package/docs/report-v1.md +29 -1
  24. package/docs/results-v1.md +141 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +307 -18
  27. package/docs/troubleshooting.md +12 -12
  28. package/docs/typescript-quality.md +89 -26
  29. package/examples/recipes/candidate.ts +26 -0
  30. package/examples/recipes/cases/coding-change.json +21 -0
  31. package/examples/recipes/cases/host-tools.json +21 -0
  32. package/examples/recipes/cases/mise.json +21 -0
  33. package/examples/recipes/cases/node.json +21 -0
  34. package/examples/recipes/cases/python-uv.json +21 -0
  35. package/examples/recipes/cases/semantic.json +17 -0
  36. package/examples/recipes/coding-change/check.ts +23 -0
  37. package/examples/recipes/extension-artifacts.ts +65 -0
  38. package/examples/recipes/extension-methods.ts +174 -0
  39. package/examples/recipes/extension.ts +37 -0
  40. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  41. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  42. package/examples/recipes/repository-skill/README.md +3 -0
  43. package/examples/recipes/run-inputs.ts +188 -0
  44. package/examples/recipes/run-options.ts +131 -0
  45. package/examples/recipes/run.ts +63 -0
  46. package/examples/recipes/runtime/empty.json +1 -0
  47. package/examples/recipes/runtime/goals.json +1 -0
  48. package/examples/recipes/runtime/mise.json +6 -0
  49. package/examples/recipes/runtime/path.json +1 -0
  50. package/examples/recipes/runtime/plugins.json +4 -0
  51. package/examples/recipes/runtime/python-uv.json +10 -0
  52. package/examples/recipes/semantic-judge.ts +29 -0
  53. package/examples/templates/coding/adapter.ts +17 -0
  54. package/examples/templates/coding/case.json +15 -0
  55. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  56. package/examples/templates/repository-skill/extension-data.ts +64 -0
  57. package/examples/templates/repository-skill/extension.ts +31 -0
  58. package/examples/templates/response/adapter.ts +12 -0
  59. package/examples/templates/response/case.json +13 -0
  60. package/package.json +11 -5
  61. package/schemas/report-v1.schema.json +9 -0
  62. package/schemas/run-evidence-v1.schema.json +2 -1
  63. package/schemas/runtime-v1.schema.json +63 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +206 -0
  66. package/src/candidate-codex-state.ts +185 -0
  67. package/src/case-validation.ts +73 -50
  68. package/src/cli/cancellation.ts +20 -0
  69. package/src/cli/command.ts +87 -0
  70. package/src/cli/completion.ts +15 -0
  71. package/src/cli/demo.ts +60 -0
  72. package/src/cli/evaluation.ts +55 -0
  73. package/src/cli/extension.ts +79 -0
  74. package/src/cli/help.ts +198 -0
  75. package/src/cli/hosts.ts +55 -0
  76. package/src/cli/init-settings.ts +30 -0
  77. package/src/cli/init.ts +111 -0
  78. package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
  79. package/src/cli/loading.ts +151 -0
  80. package/src/cli/preparation.ts +154 -0
  81. package/src/cli/presentation.ts +93 -0
  82. package/src/cli/progress.ts +6 -0
  83. package/src/cli/run-manifest.ts +128 -0
  84. package/src/cli/run-options.ts +403 -0
  85. package/src/cli/run-overrides.ts +69 -0
  86. package/src/cli/settings.ts +154 -0
  87. package/src/cli/summary.ts +43 -0
  88. package/src/cli.ts +13 -591
  89. package/src/contract-types.ts +94 -0
  90. package/src/engine.ts +13 -5
  91. package/src/evaluation/advisory.ts +150 -0
  92. package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
  93. package/src/evaluation/collection.ts +88 -0
  94. package/src/evaluation/context.ts +10 -0
  95. package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
  96. package/src/evaluation/grading.ts +235 -0
  97. package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
  98. package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
  99. package/src/evaluation/progress.ts +19 -0
  100. package/src/evaluation/resources.ts +241 -0
  101. package/src/evaluation/run.ts +206 -0
  102. package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +133 -115
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/evaluation/types.ts +207 -0
  105. package/src/evidence/host-results.ts +85 -0
  106. package/src/evidence/retained-artifacts.ts +18 -0
  107. package/src/evidence/trial-writer.ts +203 -0
  108. package/src/evidence/usage.ts +17 -0
  109. package/src/extension-client.ts +34 -96
  110. package/src/extension-session.ts +24 -48
  111. package/src/{advisory-fixture.ts → fixtures/advisory.ts} +15 -76
  112. package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
  113. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  114. package/src/fixtures/create.ts +183 -0
  115. package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
  116. package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
  117. package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
  118. package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
  119. package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
  120. package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
  121. package/src/fixtures/workspace-state.ts +212 -0
  122. package/src/generated/cli-result.d.cts +2 -1
  123. package/src/generated/extension.d.cts +2 -1
  124. package/src/generated/report.cjs +1 -1
  125. package/src/generated/report.d.cts +2 -1
  126. package/src/generated/run-evidence.cjs +1 -1
  127. package/src/generated/run-evidence.d.cts +2 -1
  128. package/src/generated/runtime.cjs +1 -1
  129. package/src/generated/runtime.d.cts +2 -1
  130. package/src/git.ts +93 -0
  131. package/src/graders/git-head.ts +29 -84
  132. package/src/graders/shell.ts +57 -68
  133. package/src/host-diagnostic-redaction.ts +52 -0
  134. package/src/host-diagnostics.ts +331 -0
  135. package/src/hosts/claude-continuation.ts +57 -40
  136. package/src/hosts/claude-credential.ts +160 -63
  137. package/src/hosts/claude-events.ts +34 -18
  138. package/src/hosts/claude-keychain.ts +78 -21
  139. package/src/hosts/claude-login-recovery.ts +85 -0
  140. package/src/hosts/claude-login-refresh.ts +178 -0
  141. package/src/hosts/claude-login-store.ts +162 -0
  142. package/src/hosts/claude-login.ts +107 -0
  143. package/src/hosts/claude-native-goal.ts +15 -12
  144. package/src/hosts/claude-process.ts +10 -50
  145. package/src/hosts/claude-refresh-lock.ts +81 -0
  146. package/src/hosts/claude-repository-invocation.ts +9 -34
  147. package/src/hosts/claude-settings.ts +91 -14
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +46 -16
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +122 -73
  152. package/src/hosts/codex-app-server.ts +78 -52
  153. package/src/hosts/codex-configuration.ts +5 -10
  154. package/src/hosts/codex-exec-controls.ts +516 -0
  155. package/src/hosts/codex-marketplace.ts +12 -10
  156. package/src/hosts/codex-native-calls.ts +15 -122
  157. package/src/hosts/codex-process.ts +10 -54
  158. package/src/hosts/codex-profile.ts +50 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +141 -19
  161. package/src/hosts/codex.ts +221 -179
  162. package/src/hosts/isolation-common.ts +15 -11
  163. package/src/hosts/isolation-roots.ts +13 -20
  164. package/src/hosts/linux-sandbox.ts +27 -3
  165. package/src/hosts/mac-sandbox.ts +111 -18
  166. package/src/hosts/native-controls.ts +72 -22
  167. package/src/hosts/native-request.ts +55 -0
  168. package/src/hosts/process-lifecycle.ts +221 -45
  169. package/src/hosts/protected-worktrees.ts +1 -1
  170. package/src/hosts/skill-hook-catalog.ts +131 -0
  171. package/src/hosts/skill-hook-handler.ts +374 -0
  172. package/src/hosts/skill-hooks.ts +577 -0
  173. package/src/instrumentation.ts +7 -8
  174. package/src/native-transcript-state.ts +22 -3
  175. package/src/native-transcript-view.ts +52 -13
  176. package/src/native-transcripts.ts +34 -15
  177. package/src/paths.ts +22 -0
  178. package/src/process-lifecycle.ts +150 -0
  179. package/src/provenance.ts +27 -7
  180. package/src/prune.ts +300 -0
  181. package/src/report.ts +63 -36
  182. package/src/resolved-case.ts +1 -1
  183. package/src/results-lock.ts +42 -0
  184. package/src/results.ts +14 -18
  185. package/src/run-owner.ts +4 -4
  186. package/src/{runtime-config.ts → runtime/config.ts} +182 -44
  187. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  188. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  189. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  190. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  191. package/src/{runtime-hooks.ts → runtime/hooks.ts} +33 -22
  192. package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
  193. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  194. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  195. package/src/runtime/shared-cache.ts +130 -0
  196. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  197. package/src/schema-types.ts +73 -161
  198. package/src/shell.ts +4 -0
  199. package/src/transcript-retention.ts +146 -0
  200. package/src/value-guards.ts +22 -0
  201. package/docs/assets/sevro-logo.png +0 -0
  202. package/docs/assets/sevro-symbol.svg +0 -1
  203. package/docs/assets/sevro-wordmark.svg +0 -1
  204. package/docs/cache-ownership-validation.md +0 -49
  205. package/docs/claude-continuation-validation.md +0 -47
  206. package/docs/concurrency-validation.md +0 -86
  207. package/docs/configuration-root-validation.md +0 -70
  208. package/docs/engine-slice.md +0 -114
  209. package/docs/evidence/runtime-startup-30/README.md +0 -185
  210. package/docs/evidence/runtime-startup-30/after.json +0 -319
  211. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  212. package/docs/evidence/runtime-startup-30/before.json +0 -403
  213. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  214. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  216. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  217. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  218. package/docs/evidence.md +0 -17
  219. package/docs/fixture-cleanup-validation.md +0 -70
  220. package/docs/guide-verification.json +0 -239
  221. package/src/evaluation-context.ts +0 -257
  222. package/src/evaluation-fixture.ts +0 -313
  223. package/src/evaluation-run.ts +0 -198
  224. package/src/evaluation-trial.ts +0 -946
  225. package/src/evaluation-types.ts +0 -149
  226. package/src/hosts/claude-nested-skills.ts +0 -272
  227. package/src/hosts/codex-command-output.ts +0 -510
  228. package/src/hosts/codex-executor-command.ts +0 -291
  229. package/src/hosts/codex-skill-reads.ts +0 -609
  230. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,8 +1,11 @@
1
- import { isRecord } from "../value-guards";
2
- import { writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
1
+ import { redactDiagnosticText } from "../host-diagnostic-redaction";
2
+ import { isRecord, requireRecord } from "../value-guards";
4
3
  import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
5
4
  import { createInterface } from "node:readline";
5
+ import {
6
+ HostProcessDiagnostics,
7
+ type HostDiagnostics,
8
+ } from "../host-diagnostics";
6
9
  interface HarnessRunRequest {
7
10
  repoDir: string;
8
11
  prompt: string;
@@ -14,9 +17,7 @@ interface HarnessRunRequest {
14
17
 
15
18
  type RecordValue = Record<string, unknown>;
16
19
  function record(value: unknown): RecordValue {
17
- if (!value || typeof value !== "object" || Array.isArray(value))
18
- throw new Error("Malformed app-server object");
19
- return value as RecordValue;
20
+ return requireRecord(value, "Malformed app-server object");
20
21
  }
21
22
  function identifier(value: unknown): string {
22
23
  if (typeof value !== "string" || !value)
@@ -70,15 +71,7 @@ function publicErrorMessage(value: unknown): {
70
71
  messageTruncated?: boolean;
71
72
  } {
72
73
  if (typeof value !== "string") return { message: null };
73
- const redacted = value
74
- .slice(0, 20000)
75
- .replace(/(authorization\s*:\s*)[^\r\n]+/gi, "$1[redacted]")
76
- .replace(/\bBearer\s+\S+/gi, "Bearer [redacted]")
77
- .replace(/\bsk-[A-Za-z0-9_-]+/g, "[redacted]")
78
- .replace(
79
- /((?:api[_-]?key|access[_-]?token|refresh[_-]?token)\s*[=:]\s*)\S+/gi,
80
- "$1[redacted]",
81
- );
74
+ const redacted = redactDiagnosticText(value.slice(0, 20000), []);
82
75
  return {
83
76
  message: redacted.slice(0, 2000),
84
77
  ...(value.length > 20000 || redacted.length > 2000
@@ -145,7 +138,7 @@ class AppServerRpc {
145
138
  {
146
139
  resolve: (value: RecordValue) => void;
147
140
  reject: (error: Error) => void;
148
- timer: ReturnType<typeof setTimeout>;
141
+ method: string;
149
142
  }
150
143
  >();
151
144
  private wake?: () => void;
@@ -155,8 +148,15 @@ class AppServerRpc {
155
148
  private reads: Promise<void>;
156
149
  private errors: Promise<void>;
157
150
  stderr = "";
151
+ readonly capture: HostProcessDiagnostics;
158
152
 
159
- constructor(argv: string[], cwd: string, env: Record<string, string>) {
153
+ constructor(
154
+ argv: string[],
155
+ cwd: string,
156
+ env: Record<string, string>,
157
+ diagnostics: HostDiagnostics,
158
+ ) {
159
+ this.capture = new HostProcessDiagnostics(diagnostics);
160
160
  this.process = spawn(identifier(argv[0]), argv.slice(1), {
161
161
  detached: process.platform !== "win32",
162
162
  cwd,
@@ -166,10 +166,14 @@ class AppServerRpc {
166
166
  this.exited = new Promise((resolve) => {
167
167
  this.process.once("exit", resolve);
168
168
  this.process.once("error", (error) => {
169
+ if (this.process.pid === undefined) this.capture.recordUnstarted();
169
170
  this.fail(error);
170
171
  resolve(null);
171
172
  });
172
173
  });
174
+ this.process.stdout.on("data", (chunk: Buffer) => {
175
+ this.capture.stdout.add(chunk);
176
+ });
173
177
  this.reads = this.readLines().catch((error: unknown) => {
174
178
  this.fail(error);
175
179
  });
@@ -177,7 +181,8 @@ class AppServerRpc {
177
181
  this.fail(error);
178
182
  });
179
183
  this.errors = new Promise((resolve) => {
180
- this.process.stderr.on("data", (chunk) => {
184
+ this.process.stderr.on("data", (chunk: Buffer) => {
185
+ this.capture.stderr.add(chunk);
181
186
  this.stderr = (this.stderr + String(chunk)).slice(-16000);
182
187
  });
183
188
  this.process.stderr.on("end", resolve);
@@ -195,10 +200,7 @@ class AppServerRpc {
195
200
 
196
201
  fail(error: unknown): void {
197
202
  this.failure ??= error instanceof Error ? error : new Error(String(error));
198
- for (const pending of this.pending.values()) {
199
- clearTimeout(pending.timer);
200
- pending.reject(this.failure);
201
- }
203
+ for (const pending of this.pending.values()) pending.reject(this.failure);
202
204
  this.pending.clear();
203
205
  this.wake?.();
204
206
  }
@@ -263,7 +265,6 @@ class AppServerRpc {
263
265
  // Validate before removing the waiter so malformed responses reject it.
264
266
  const value = record(message.error ?? message.result);
265
267
  this.pending.delete(id);
266
- clearTimeout(pending.timer);
267
268
  if (message.error)
268
269
  pending.reject(
269
270
  new Error(`App-server RPC failed (${String(value.code)})`),
@@ -293,14 +294,12 @@ class AppServerRpc {
293
294
  this.wake?.();
294
295
  }
295
296
 
297
+ /** The session's configured time bound is the only deadline, including startup. */
296
298
  async call(method: string, params: RecordValue): Promise<RecordValue> {
297
299
  if (this.failure) throw this.failure;
298
300
  const id = this.nextId++;
299
301
  return new Promise((resolve, reject) => {
300
- const timer = setTimeout(() => {
301
- this.fail(new Error(`App-server RPC timeout: ${method}`));
302
- }, 45000);
303
- this.pending.set(id, { resolve, reject, timer });
302
+ this.pending.set(id, { resolve, reject, method });
304
303
  try {
305
304
  this.send({ id, method, params });
306
305
  } catch (error) {
@@ -309,6 +308,10 @@ class AppServerRpc {
309
308
  });
310
309
  }
311
310
 
311
+ pendingMethods(): string[] {
312
+ return Array.from(this.pending.values(), (pending) => pending.method);
313
+ }
314
+
312
315
  async initialize(): Promise<void> {
313
316
  await this.call("initialize", {
314
317
  clientInfo: { name: "sevro_eval", version: "0.1.0" },
@@ -332,11 +335,13 @@ class AppServerRpc {
332
335
  async close(): Promise<void> {
333
336
  this.closing = true;
334
337
  this.process.stdin.end();
338
+ let shutdownError: NodeJS.ErrnoException | undefined;
335
339
  const terminate = (signal: NodeJS.Signals) => {
336
340
  try {
337
341
  if (this.process.pid) process.kill(-this.process.pid, signal);
338
342
  } catch (error) {
339
- if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
343
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH")
344
+ shutdownError ??= error as NodeJS.ErrnoException;
340
345
  }
341
346
  };
342
347
  const term = setTimeout(() => {
@@ -353,6 +358,7 @@ class AppServerRpc {
353
358
  clearTimeout(term);
354
359
  clearTimeout(kill);
355
360
  }
361
+ if (shutdownError !== undefined) throw shutdownError;
356
362
  }
357
363
  }
358
364
 
@@ -471,6 +477,7 @@ function requireGoalObjective(objective: unknown): void {
471
477
  }
472
478
 
473
479
  export interface AppServerRunOptions {
480
+ diagnostics: HostDiagnostics;
474
481
  vettedHooks?: boolean;
475
482
  request: HarnessRunRequest;
476
483
  argv: string[];
@@ -492,9 +499,9 @@ class AppServerSession {
492
499
  goals: [],
493
500
  };
494
501
  readonly out: string[] = [];
502
+ finalMessage: string | null = null;
495
503
  private threadId = "";
496
504
  private goalSeen = false;
497
- private usage: unknown;
498
505
  private emittedItems = new Set<string>();
499
506
  private followUp?: string;
500
507
 
@@ -621,14 +628,6 @@ class AppServerSession {
621
628
  if (params.willRetry !== true)
622
629
  throw new Error("App-server reported a fatal turn error");
623
630
  break;
624
- case "thread/tokenUsage/updated": {
625
- const total = record(record(params.tokenUsage).total);
626
- this.usage = {
627
- input_tokens: total.inputTokens,
628
- output_tokens: total.outputTokens,
629
- };
630
- break;
631
- }
632
631
  case "item/completed":
633
632
  this.emitItem(params.item);
634
633
  break;
@@ -651,13 +650,10 @@ class AppServerSession {
651
650
  const turnId = this.observeTurn(turn);
652
651
  this.requireCompletedTurn(turn, turnId);
653
652
  const status = await this.completedGoal(turnId);
654
- this.emit({
655
- type: "turn.completed",
656
- ...(this.usage ? { usage: this.usage } : {}),
657
- });
653
+ this.emit({ type: "turn.completed" });
658
654
  if (this.followUp !== undefined)
659
655
  return this.deliverFeedback(turnId, status, this.followUp);
660
- return status !== "active" && (await this.saveFinal(turnId));
656
+ return status !== "active" && (await this.captureFinal(turnId));
661
657
  }
662
658
 
663
659
  private requireCompletedTurn(turn: RecordValue, turnId: string): void {
@@ -686,7 +682,7 @@ class AppServerSession {
686
682
  ): Promise<boolean> {
687
683
  // User input does not require a terminal goal. Preserve the completed
688
684
  // response and actual boundary before delivering the declared follow-up.
689
- if (!(await this.saveFinal(turnId, true))) return false;
685
+ if (!(await this.captureFinal(turnId, true))) return false;
690
686
  const boundary = protocolMessage(
691
687
  await this.options.followUpBoundary(this.threadId, {
692
688
  nativeGoalObserved: this.goalSeen,
@@ -704,7 +700,7 @@ class AppServerSession {
704
700
  return false;
705
701
  }
706
702
 
707
- private async saveFinal(
703
+ private async captureFinal(
708
704
  turnId: string,
709
705
  forFeedback = false,
710
706
  ): Promise<boolean> {
@@ -718,10 +714,7 @@ class AppServerSession {
718
714
  if (!latestTurnReady(latest, turnId)) return false;
719
715
  const final = appServerFinal(thread, turnId);
720
716
  for (const value of turns) this.backfillItems(record(value));
721
- await writeFile(
722
- join(this.options.request.repoDir, ".git", "last-message.md"),
723
- final,
724
- );
717
+ this.finalMessage = final;
725
718
  this.evidence.finalTurnId = turnId;
726
719
  this.emit({
727
720
  type: "item.completed",
@@ -861,10 +854,16 @@ function requireAppServerRun(request: HarnessRunRequest, bound: number): void {
861
854
  throw new Error("Invalid app-server time bound");
862
855
  }
863
856
  function sessionFailure(session: AppServerSession, error: unknown): void {
857
+ session.options.diagnostics.failure = error;
864
858
  session.evidence.failure =
865
859
  error instanceof Error ? error.message : String(error);
866
860
  session.emit({ type: "turn.failed" });
867
861
  }
862
+ /** Name an unanswered request so a stalled startup stays distinguishable. */
863
+ function timeBoundFailure(pending: string[]): string {
864
+ const reached = "App-server evaluation time bound reached";
865
+ return pending.length ? `${reached} awaiting ${pending.join(", ")}` : reached;
866
+ }
868
867
  async function completeSession(
869
868
  session: AppServerSession,
870
869
  rpc: AppServerRpc,
@@ -876,19 +875,32 @@ async function completeSession(
876
875
  };
877
876
  request.signal?.addEventListener("abort", cancel, { once: true });
878
877
  const deadline = setTimeout(() => {
879
- rpc.fail(new Error("App-server evaluation time bound reached"));
878
+ rpc.fail(new Error(timeBoundFailure(rpc.pendingMethods())));
880
879
  }, bound);
880
+ let code = 0;
881
881
  try {
882
882
  await session.run();
883
883
  session.assertSettled();
884
- return 0;
885
884
  } catch (error) {
886
885
  sessionFailure(session, error);
887
- return 1;
886
+ code = 1;
888
887
  } finally {
889
888
  clearTimeout(deadline);
890
889
  request.signal?.removeEventListener("abort", cancel);
890
+ }
891
+ return closeSession(session, rpc, code);
892
+ }
893
+ async function closeSession(
894
+ session: AppServerSession,
895
+ rpc: AppServerRpc,
896
+ code: number,
897
+ ): Promise<number> {
898
+ try {
891
899
  await rpc.close();
900
+ return code;
901
+ } catch (error) {
902
+ if (code === 0) sessionFailure(session, error);
903
+ return 1;
892
904
  }
893
905
  }
894
906
  function settlementCode(session: AppServerSession, code: number): number {
@@ -906,20 +918,34 @@ export async function runCodexAppServer(options: AppServerRunOptions): Promise<{
906
918
  out: string;
907
919
  err: string;
908
920
  code: number;
921
+ finalMessage: string | null;
909
922
  evidence: AppServerEvidence;
910
923
  }> {
911
924
  const { request, argv, env } = options;
912
925
  const bound = request.control?.appServerTimeoutMs ?? 3600000;
913
926
  requireAppServerRun(request, bound);
914
- const rpc = new AppServerRpc(argv, request.repoDir, env);
927
+ const rpc = new AppServerRpc(argv, request.repoDir, env, options.diagnostics);
915
928
  const session = new AppServerSession(rpc, options);
916
929
  const code = await completeSession(session, rpc, request, bound);
917
930
  // Flush stdout before accepting success, including trailing data and terminal errors.
918
931
  const finalCode = settlementCode(session, code);
932
+ if (rpc.process.pid !== undefined)
933
+ await rpc.capture.record(
934
+ { exited: rpc.exited, signalCode: rpc.process.signalCode },
935
+ finalCode === 0 ? "exit" : "error",
936
+ );
919
937
  return {
920
938
  out: session.out.join("\n"),
921
939
  err: rpc.stderr,
922
940
  code: finalCode,
941
+ finalMessage: completedMessage(session, finalCode),
923
942
  evidence: session.evidence,
924
943
  };
925
944
  }
945
+
946
+ function completedMessage(
947
+ session: AppServerSession,
948
+ code: number,
949
+ ): string | null {
950
+ return code === 0 ? session.finalMessage : null;
951
+ }
@@ -1,7 +1,8 @@
1
1
  import type { Stats } from "node:fs";
2
2
  import { isRecord } from "../value-guards";
3
3
  import { lstat, readFile, realpath, stat } from "node:fs/promises";
4
- import { dirname, isAbsolute, join, relative, sep } from "node:path";
4
+ import { dirname, isAbsolute, join } from "node:path";
5
+ import { insideRoot } from "../paths";
5
6
  import { parse } from "smol-toml";
6
7
 
7
8
  export async function configurationRoot(root: string): Promise<string> {
@@ -52,7 +53,7 @@ async function readConfiguration(
52
53
  const info = await lstat(configPath);
53
54
  declared = true;
54
55
  const canonical = await realpath(configPath);
55
- validateConfigurationFile(info, relative(root, canonical));
56
+ validateConfigurationFile(info, insideRoot(root, canonical));
56
57
  source = await readFile(configPath, "utf8");
57
58
  } catch (error) {
58
59
  if (!declared && (error as NodeJS.ErrnoException).code === "ENOENT")
@@ -97,15 +98,9 @@ function concurrencyLimit(limit: unknown, configPath: string): number | null {
97
98
  return Number(limit);
98
99
  }
99
100
 
100
- function validateConfigurationFile(info: Stats, location: string): void {
101
- if (!info.isFile() || info.size > 64 * 1024 || escapedRoot(location))
101
+ function validateConfigurationFile(info: Stats, contained: boolean): void {
102
+ if (!info.isFile() || info.size > 64 * 1024 || !contained)
102
103
  throw new Error(
103
104
  "configuration must be a contained regular file up to 64 KiB",
104
105
  );
105
106
  }
106
-
107
- function escapedRoot(location: string): boolean {
108
- return (
109
- location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)
110
- );
111
- }