@bjoernrochel/sevro 0.1.0-rc.8 → 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 (228) 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 +9 -2
  7. package/docs/codex-host.md +155 -57
  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 +122 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +255 -22
  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 +43 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +42 -11
  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 +9 -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} +77 -87
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
  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} +12 -72
  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/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
  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 +50 -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 +46 -9
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +28 -15
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +121 -81
  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 -183
  158. package/src/hosts/codex-profile.ts +36 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +134 -18
  161. package/src/hosts/codex.ts +210 -183
  162. package/src/hosts/isolation-common.ts +8 -11
  163. package/src/hosts/isolation-roots.ts +2 -2
  164. package/src/hosts/linux-sandbox.ts +15 -2
  165. package/src/hosts/mac-sandbox.ts +87 -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 +216 -48
  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 +26 -11
  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/{runtime-config.ts → runtime/config.ts} +160 -46
  186. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  187. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  188. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  189. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  190. package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
  191. package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
  192. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  193. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  194. package/src/runtime/shared-cache.ts +130 -0
  195. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  196. package/src/schema-types.ts +73 -161
  197. package/src/shell.ts +4 -0
  198. package/src/transcript-retention.ts +146 -0
  199. package/src/value-guards.ts +22 -0
  200. package/docs/assets/sevro-logo.png +0 -0
  201. package/docs/assets/sevro-symbol.svg +0 -1
  202. package/docs/assets/sevro-wordmark.svg +0 -1
  203. package/docs/cache-ownership-validation.md +0 -49
  204. package/docs/claude-continuation-validation.md +0 -47
  205. package/docs/concurrency-validation.md +0 -86
  206. package/docs/configuration-root-validation.md +0 -70
  207. package/docs/engine-slice.md +0 -114
  208. package/docs/evidence/runtime-startup-30/README.md +0 -185
  209. package/docs/evidence/runtime-startup-30/after.json +0 -319
  210. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  211. package/docs/evidence/runtime-startup-30/before.json +0 -403
  212. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  213. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  214. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  216. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  217. package/docs/evidence.md +0 -17
  218. package/docs/fixture-cleanup-validation.md +0 -73
  219. package/docs/guide-verification.json +0 -239
  220. package/src/evaluation-context.ts +0 -259
  221. package/src/evaluation-fixture.ts +0 -302
  222. package/src/evaluation-run.ts +0 -200
  223. package/src/evaluation-trial.ts +0 -1016
  224. package/src/hosts/claude-nested-skills.ts +0 -272
  225. package/src/hosts/codex-command-output.ts +0 -510
  226. package/src/hosts/codex-executor-command.ts +0 -291
  227. package/src/hosts/codex-skill-reads.ts +0 -783
  228. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,16 +1,26 @@
1
1
  import type {
2
2
  InstrumentationCapability,
3
3
  InstrumentationRequest,
4
- } from "./instrumentation";
5
- import type { GeneratedFixture } from "./generated-fixture";
6
- import type { PreparationSources } from "./preparation";
7
- import type { RepositoryFixture } from "./repository-fixture";
8
- import type { EvaluationResult, ExtensionCase } from "./extension-session";
9
- import type { openExtensionSession } from "./extension-session";
10
- import type { Assessment, CheckOutcome } from "./results";
11
- import type { RuntimePolicy } from "./runtime-config";
12
- import type { RuntimeRole } from "./runtime-state";
13
- import type { CandidateArtifactRoots } from "./candidate-artifacts";
4
+ } from "../instrumentation";
5
+ import type { GeneratedFixture } from "../fixtures/generated";
6
+ import type { PreparationSources } from "../fixtures/preparation";
7
+ import type { RepositoryFixture } from "../fixtures/repository";
8
+ import type { ExtensionCase } from "../extension-session";
9
+ import type { openExtensionSession } from "../extension-session";
10
+ import type {
11
+ Assessment,
12
+ CheckDeclaration,
13
+ DomainOutcome,
14
+ ObservationEvidence,
15
+ } from "../contract-types";
16
+ import type {
17
+ CliResultData,
18
+ CliResultDataCase,
19
+ CliResultDataTrial,
20
+ } from "../schema-types";
21
+ import type { RuntimePolicy } from "../runtime/config";
22
+ import type { RuntimeRole } from "../runtime/state";
23
+ import type { CandidateArtifactRoots } from "../candidate-artifacts";
14
24
 
15
25
  export class EvaluationConfigurationError extends Error {
16
26
  constructor(message: string, options?: ErrorOptions) {
@@ -23,17 +33,15 @@ export interface HostResult {
23
33
  finalMessage: string | null;
24
34
  complete: boolean;
25
35
  executionFailed?: boolean;
36
+ /** Adapter-owned, bounded failure summary suitable for CLI diagnostics. */
37
+ failureMessage?: string;
26
38
  processFailure?: {
27
39
  phase: "isolation-preflight" | "executable-preflight" | "execution";
28
40
  reason: "exit" | "timeout" | "cancelled";
29
41
  timeoutMs: number;
30
42
  };
31
43
  artifacts?: { id: string; bytes: Uint8Array }[];
32
- observations?: {
33
- id: string;
34
- completeness: "complete" | "partial" | "unavailable";
35
- data: Record<string, unknown>;
36
- }[];
44
+ observations?: Omit<ObservationEvidence, "source">[];
37
45
  actualCondition?: "passive" | "enforced" | "unknown";
38
46
  appliedInstrumentation?: InstrumentationRequest[];
39
47
  inputTokens?: number | null;
@@ -42,6 +50,28 @@ export interface HostResult {
42
50
  usageComplete?: boolean;
43
51
  }
44
52
 
53
+ /** A failed host call can still provide bounded evidence for retention. */
54
+ export class HostExecutionFailure extends Error {
55
+ readonly result: HostResult;
56
+ constructor(
57
+ message: string | undefined,
58
+ result: HostResult,
59
+ options?: ErrorOptions,
60
+ ) {
61
+ super(message, options);
62
+ this.result = { ...result, executionFailed: true, failureMessage: message };
63
+ }
64
+ }
65
+
66
+ export class TrialPersistenceError extends Error {
67
+ constructor(workspace: string, options?: ErrorOptions) {
68
+ super(
69
+ `trial persistence failed; fixture retained at ${workspace}`,
70
+ options,
71
+ );
72
+ }
73
+ }
74
+
45
75
  export interface HostAdapter {
46
76
  id: string;
47
77
  model: string;
@@ -50,6 +80,8 @@ export interface HostAdapter {
50
80
  hostCapabilities?: string[];
51
81
  configuration?: Record<string, unknown>;
52
82
  run(request: {
83
+ /** Persist private diagnostic bytes before the native host removes its state. */
84
+ retainHostDiagnostics?: (bytes: Uint8Array) => Promise<void>;
53
85
  runtimePolicy?: RuntimePolicy;
54
86
  runtimeRole?: RuntimeRole;
55
87
  candidateTranscriptRoot?: string;
@@ -96,15 +128,23 @@ export interface ResolvedCase {
96
128
  | { files: Record<string, string>; sourceRef?: never }
97
129
  | Omit<RepositoryFixture, "kind">
98
130
  | (GeneratedFixture & { sourceRef?: never });
99
- checks: {
100
- id: string;
101
- grader: string;
102
- configuration: Record<string, unknown>;
103
- }[];
131
+ checks: CheckDeclaration[];
104
132
  requiredEvidence: string[];
105
133
  }
106
134
 
135
+ export interface EvaluationProgress {
136
+ stage: "preparation" | "candidate" | "grading" | "evidence";
137
+ status: "started" | "completed" | "skipped";
138
+ trial?: number;
139
+ }
140
+
107
141
  export interface EvaluationOptions {
142
+ /** Optional best-effort observation; exceptions never change the evaluation. */
143
+ onProgress?: (event: EvaluationProgress) => void;
144
+ /** Redacted operator preview, retained for reproduction outside semantic identity. */
145
+ resolvedRunSettings?: Record<string, unknown>;
146
+ /** Additional non-secret assessment settings included in configuration identity. */
147
+ assessmentSettings?: Record<string, unknown>;
108
148
  runtimePolicy?: RuntimePolicy;
109
149
  projectRoot: string;
110
150
  resultsRoot: string;
@@ -135,23 +175,33 @@ export interface EvaluationOptions {
135
175
  };
136
176
  }
137
177
 
138
- export interface TrialSummary extends Assessment {
139
- trial: number;
140
- checks: CheckOutcome[];
141
- domainOutcomes: NonNullable<EvaluationResult["domainOutcomes"]>;
178
+ /** Persisted evaluations always retain a trial path and a known outcome list. */
179
+ export interface TrialSummary
180
+ extends
181
+ Assessment,
182
+ Omit<
183
+ CliResultDataTrial,
184
+ keyof Assessment | "domainOutcomes" | "artifactPath"
185
+ > {
186
+ domainOutcomes: DomainOutcome[];
142
187
  artifactPath: string;
143
188
  }
144
189
 
145
- export interface CaseSummary extends Assessment {
146
- caseId: string;
190
+ export interface CaseSummary
191
+ extends Assessment, Omit<CliResultDataCase, keyof Assessment | "trials"> {
147
192
  trials: TrialSummary[];
148
193
  }
149
194
 
150
- export interface CliResult extends Assessment {
151
- format: "sevro.cli-result.v1";
195
+ /** Unlike pre-run wire failures, a returned evaluation has run/evidence paths. */
196
+ export interface CliResult
197
+ extends
198
+ Assessment,
199
+ Omit<
200
+ CliResultData,
201
+ keyof Assessment | "runId" | "exitCode" | "evidencePath" | "cases"
202
+ > {
152
203
  runId: string;
153
204
  exitCode: number;
154
205
  evidencePath: string;
155
206
  cases: CaseSummary[];
156
- diagnostic?: { code: string; message: string };
157
207
  }
@@ -0,0 +1,85 @@
1
+ import { isRecord, validQualifiedId as validEvidenceId } from "../value-guards";
2
+ import { canonicalJson } from "../identity";
3
+ import type { HostResult } from "../evaluation/types";
4
+ import type {
5
+ ObservationCompleteness,
6
+ ObservationEvidence,
7
+ } from "../contract-types";
8
+
9
+ export function hostObservations(
10
+ result: HostResult,
11
+ hostId: string,
12
+ existingIds: Set<string>,
13
+ ): ObservationEvidence[] {
14
+ const items = result.observations ?? [];
15
+ if (items.length > 128) throw new Error("too many host observations");
16
+ const ids = new Set([...existingIds, "sevro.observation.final-message"]);
17
+ let totalBytes = 0;
18
+ return items.map((value: unknown) => {
19
+ const item = validatedHostObservation(value, ids);
20
+ ids.add(item.id);
21
+ const data = canonicalJson(item.data);
22
+ totalBytes += Buffer.byteLength(data, "utf8");
23
+ if (totalBytes > 8 * 1024 * 1024)
24
+ throw new Error("host observations exceed 8 MiB");
25
+ return {
26
+ id: item.id,
27
+ source: hostId,
28
+ completeness: item.completeness,
29
+ data: JSON.parse(data) as Record<string, unknown>,
30
+ };
31
+ });
32
+ }
33
+
34
+ export function hostArtifacts(result: HostResult, existingIds: Set<string>) {
35
+ const items = result.artifacts ?? [];
36
+ if (!Array.isArray(items) || items.length > 32)
37
+ throw new Error("too many host artifacts");
38
+ const ids = new Set(existingIds);
39
+ let totalBytes = 0;
40
+ return items.map((value: unknown) => {
41
+ const item = validatedHostArtifact(value, ids);
42
+ ids.add(item.id);
43
+ totalBytes += item.bytes.byteLength;
44
+ if (
45
+ item.bytes.byteLength > 8 * 1024 * 1024 ||
46
+ totalBytes > 32 * 1024 * 1024
47
+ )
48
+ throw new Error("host artifacts exceed the size limit");
49
+ return { id: item.id, bytes: Buffer.from(item.bytes) };
50
+ });
51
+ }
52
+
53
+ function validObservationId(value: unknown, ids: Set<string>): value is string {
54
+ return (
55
+ validEvidenceId(value) &&
56
+ !value.startsWith("sevro.observation.") &&
57
+ !ids.has(value)
58
+ );
59
+ }
60
+
61
+ function validCompleteness(value: unknown): value is ObservationCompleteness {
62
+ return value === "complete" || value === "partial" || value === "unavailable";
63
+ }
64
+
65
+ function validatedHostObservation(value: unknown, ids: Set<string>) {
66
+ if (!isRecord(value)) throw new Error("invalid host observation");
67
+ if (
68
+ !validObservationId(value.id, ids) ||
69
+ !validCompleteness(value.completeness) ||
70
+ !isRecord(value.data)
71
+ )
72
+ throw new Error("invalid host observation");
73
+ return { id: value.id, completeness: value.completeness, data: value.data };
74
+ }
75
+
76
+ function validArtifactId(value: unknown, ids: Set<string>): value is string {
77
+ return validEvidenceId(value) && !ids.has(value);
78
+ }
79
+
80
+ function validatedHostArtifact(value: unknown, ids: Set<string>) {
81
+ if (!isRecord(value)) throw new Error("invalid host artifact");
82
+ if (!validArtifactId(value.id, ids) || !(value.bytes instanceof Uint8Array))
83
+ throw new Error("invalid host artifact");
84
+ return { id: value.id, bytes: value.bytes };
85
+ }
@@ -0,0 +1,18 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { sha256 } from "../identity";
4
+
5
+ export async function verifyRetainedArtifacts(
6
+ artifacts: { path: string; sha256: string }[],
7
+ ): Promise<void> {
8
+ for (const artifact of artifacts) {
9
+ let actual: string;
10
+ try {
11
+ actual = sha256(await readFile(fileURLToPath(artifact.path)));
12
+ } catch {
13
+ throw new Error("retained preparation artifact is unreadable");
14
+ }
15
+ if (actual !== artifact.sha256)
16
+ throw new Error("retained preparation artifact changed");
17
+ }
18
+ }
@@ -0,0 +1,203 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { TrialPersistenceError, type TrialSummary } from "../evaluation/types";
5
+ import { sha256 } from "../identity";
6
+ import type { Assessment, CheckOutcome } from "../results";
7
+ import type {
8
+ ArtifactReference,
9
+ Diagnostic,
10
+ ObservationEvidence,
11
+ RawResultReference,
12
+ } from "../contract-types";
13
+ import type { RunEvidenceDataTrialEvidence } from "../schema-types";
14
+ import { atomicWriteJson } from "../storage";
15
+ import type { hostArtifacts } from "./host-results";
16
+ import { verifyRetainedArtifacts } from "./retained-artifacts";
17
+
18
+ type TrialEvidence = RunEvidenceDataTrialEvidence;
19
+ export type TrialObservation = ObservationEvidence;
20
+
21
+ interface TrialWriterInput {
22
+ readonly runDir: string;
23
+ readonly trial: number;
24
+ readonly workspace: string;
25
+ readonly preparationArtifacts: readonly ArtifactReference[];
26
+ }
27
+
28
+ /** Owns private trial files and their references, independently of run collection. */
29
+ export class TrialEvidenceWriter {
30
+ private readonly references: ArtifactReference[];
31
+ private rawPath: string | null = null;
32
+ private rawDigest: string | null = null;
33
+
34
+ constructor(private readonly input: TrialWriterInput) {
35
+ this.references = [...input.preparationArtifacts];
36
+ }
37
+
38
+ get artifactRefs(): ArtifactReference[] {
39
+ return [...this.references];
40
+ }
41
+
42
+ get hasCandidateMessage(): boolean {
43
+ return this.rawDigest !== null;
44
+ }
45
+
46
+ private async writePrivateFile(
47
+ path: string,
48
+ bytes: string | Uint8Array,
49
+ ): Promise<void> {
50
+ try {
51
+ await writeFile(path, bytes, { flag: "wx", mode: 0o600 });
52
+ } catch (cause) {
53
+ throw new TrialPersistenceError(this.input.workspace, { cause });
54
+ }
55
+ }
56
+
57
+ private async retainReference(
58
+ id: string,
59
+ filename: string,
60
+ bytes: string | Uint8Array,
61
+ ): Promise<ArtifactReference> {
62
+ const path = join(
63
+ this.input.runDir,
64
+ `trial-${this.input.trial}-${filename}`,
65
+ );
66
+ await this.writePrivateFile(path, bytes);
67
+ const reference = {
68
+ id,
69
+ path: pathToFileURL(path).href,
70
+ sha256: sha256(bytes),
71
+ };
72
+ this.references.push(reference);
73
+ return reference;
74
+ }
75
+
76
+ async retainArtifacts(
77
+ artifacts: readonly ReturnType<typeof hostArtifacts>[number][],
78
+ prefix = "",
79
+ ): Promise<void> {
80
+ for (const artifact of artifacts)
81
+ await this.retainReference(
82
+ artifact.id,
83
+ `${prefix}${artifact.id}.bin`,
84
+ artifact.bytes,
85
+ );
86
+ }
87
+
88
+ verifyArtifacts(): Promise<void> {
89
+ return verifyRetainedArtifacts(this.references);
90
+ }
91
+
92
+ observeCandidate(
93
+ source: string,
94
+ result: { finalMessage: string | null; complete: boolean } | null,
95
+ ): TrialObservation {
96
+ if (result === null || result.finalMessage == null) {
97
+ this.rawDigest = null;
98
+ return {
99
+ id: "sevro.observation.final-message",
100
+ source,
101
+ completeness: "unavailable",
102
+ data: {},
103
+ };
104
+ }
105
+ this.rawDigest = sha256(result.finalMessage);
106
+ return {
107
+ id: "sevro.observation.final-message",
108
+ source,
109
+ completeness: result.complete ? "complete" : "partial",
110
+ data: {
111
+ sha256: this.rawDigest,
112
+ byteLength: Buffer.byteLength(result.finalMessage, "utf8"),
113
+ },
114
+ };
115
+ }
116
+
117
+ async retainSemanticVerdicts(
118
+ message: string | null,
119
+ suffix: string,
120
+ ): Promise<void> {
121
+ if (message === null || Buffer.byteLength(message, "utf8") > 1024 * 1024)
122
+ return;
123
+ await this.retainReference(
124
+ `sevro.semantic.verdicts${suffix}`,
125
+ `semantic-verdicts${suffix}.json`,
126
+ Buffer.from(message, "utf8"),
127
+ );
128
+ }
129
+
130
+ async retainAdvisoryResponse(
131
+ source: string,
132
+ message: string | null,
133
+ ): Promise<RawResultReference> {
134
+ if (message === null || Buffer.byteLength(message, "utf8") > 64 * 1024)
135
+ return { source, path: null, sha256: null };
136
+ const reference = await this.retainReference(
137
+ "sevro.advisory.response",
138
+ "advisory.json",
139
+ message,
140
+ );
141
+ return { source, path: reference.path, sha256: reference.sha256 };
142
+ }
143
+
144
+ async retainRawCandidate(message: string | null | undefined): Promise<void> {
145
+ await this.verifyArtifacts();
146
+ if (message == null) {
147
+ this.rawPath = null;
148
+ return;
149
+ }
150
+ this.rawPath = join(this.input.runDir, `trial-${this.input.trial}-raw.txt`);
151
+ await this.writePrivateFile(this.rawPath, message);
152
+ }
153
+
154
+ candidateRawResult(source: string): RawResultReference {
155
+ return {
156
+ source,
157
+ path: this.rawPath ? pathToFileURL(this.rawPath).href : null,
158
+ sha256: this.rawDigest,
159
+ };
160
+ }
161
+
162
+ diagnosticLocation(diagnostic: Diagnostic | undefined) {
163
+ if (!diagnostic) return diagnostic;
164
+ const artifact = this.references.find((ref) =>
165
+ ref.id.endsWith("sevro.host.diagnostics"),
166
+ );
167
+ if (!artifact) return diagnostic;
168
+ const message = `${diagnostic.message}; diagnostics: ${artifact.path}`;
169
+ return message.length <= 4096 ? { ...diagnostic, message } : diagnostic;
170
+ }
171
+
172
+ async persistTrial(
173
+ caseId: string,
174
+ assessment: Assessment,
175
+ checks: CheckOutcome[],
176
+ domainOutcomes: TrialSummary["domainOutcomes"],
177
+ evidence: TrialEvidence,
178
+ ): Promise<TrialSummary> {
179
+ const artifactPath = join(
180
+ this.input.runDir,
181
+ `trial-${this.input.trial}.json`,
182
+ );
183
+ const summary: TrialSummary = {
184
+ trial: this.input.trial,
185
+ ...assessment,
186
+ checks,
187
+ domainOutcomes,
188
+ artifactPath,
189
+ };
190
+ try {
191
+ await atomicWriteJson(artifactPath, {
192
+ format: "sevro.trial-evidence.v1",
193
+ caseId,
194
+ trial: this.input.trial,
195
+ result: summary,
196
+ evidence,
197
+ });
198
+ } catch (cause) {
199
+ throw new TrialPersistenceError(this.input.workspace, { cause });
200
+ }
201
+ return summary;
202
+ }
203
+ }
@@ -0,0 +1,17 @@
1
+ import type { HostResult } from "../evaluation/types";
2
+ import type { UsageEvidence } from "../contract-types";
3
+
4
+ export function usage(result: HostResult | null): UsageEvidence {
5
+ return result === null
6
+ ? { inputTokens: null, outputTokens: null, costUsd: null, complete: false }
7
+ : availableUsage(result);
8
+ }
9
+
10
+ function availableUsage(result: HostResult): UsageEvidence {
11
+ return {
12
+ inputTokens: result.inputTokens ?? null,
13
+ outputTokens: result.outputTokens ?? null,
14
+ costUsd: result.costUsd ?? null,
15
+ complete: result.usageComplete ?? false,
16
+ };
17
+ }
@@ -1,6 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import validExchange from "./generated/extension.cjs";
3
3
  import { isRecord, isStringArray } from "./value-guards";
4
+ import {
5
+ captureBoundedBytes,
6
+ ProcessDeadline,
7
+ terminateOwnedProcessGroup,
8
+ } from "./process-lifecycle";
4
9
 
5
10
  const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
6
11
  const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
@@ -35,61 +40,6 @@ export interface ExchangeOptions {
35
40
  signal?: AbortSignal;
36
41
  }
37
42
 
38
- function boundedChunk(
39
- value: Uint8Array,
40
- size: number,
41
- limit: number,
42
- truncate: boolean,
43
- ): Uint8Array {
44
- if (!truncate && size + value.byteLength > limit)
45
- throw new ExtensionProtocolError("extension message exceeds 8 MiB");
46
- return value.subarray(0, Math.max(0, limit - size));
47
- }
48
-
49
- async function readBounded(
50
- stream: ReadableStream<Uint8Array>,
51
- limit: number,
52
- truncate: boolean,
53
- ): Promise<Uint8Array> {
54
- const reader = stream.getReader();
55
- const chunks: Uint8Array[] = [];
56
- let size = 0;
57
- for (;;) {
58
- const { value, done } = await reader.read();
59
- if (done) break;
60
- const kept = boundedChunk(value, size, limit, truncate);
61
- if (kept.byteLength) chunks.push(kept);
62
- size += kept.byteLength;
63
- }
64
- return concatenateChunks(chunks, size);
65
- }
66
-
67
- function concatenateChunks(chunks: Uint8Array[], size: number): Uint8Array {
68
- const result = new Uint8Array(size);
69
- let offset = 0;
70
- for (const chunk of chunks) {
71
- result.set(chunk, offset);
72
- offset += chunk.byteLength;
73
- }
74
- return result;
75
- }
76
-
77
- function terminate(proc: Bun.Subprocess): void {
78
- if (process.platform !== "win32") {
79
- try {
80
- process.kill(-proc.pid, "SIGKILL");
81
- return;
82
- } catch {
83
- // The group may have exited while the runner was handling the error.
84
- }
85
- }
86
- try {
87
- proc.kill("SIGKILL");
88
- } catch {
89
- // The process already exited.
90
- }
91
- }
92
-
93
43
  interface SuccessfulExtensionResponse extends ExtensionResponse {
94
44
  result: Record<string, unknown>;
95
45
  error?: never;
@@ -217,13 +167,23 @@ function assertSuccessfulResponse(
217
167
  }
218
168
 
219
169
  class ExtensionExchange {
220
- private timer: ReturnType<typeof setTimeout> | undefined;
221
- private cancel: (() => void) | undefined;
170
+ private readonly deadline: ProcessDeadline;
222
171
  constructor(
223
172
  private readonly proc: Bun.Subprocess,
224
- private readonly options: ExchangeOptions,
225
- private readonly timeoutMs: number,
226
- ) {}
173
+ options: ExchangeOptions,
174
+ timeoutMs: number,
175
+ ) {
176
+ this.deadline = new ProcessDeadline({
177
+ timeoutMs,
178
+ signal: options.signal,
179
+ interruption: (reason) =>
180
+ new ExtensionProtocolError(
181
+ reason === "timeout"
182
+ ? "extension timed out"
183
+ : "extension request cancelled",
184
+ ),
185
+ });
186
+ }
227
187
 
228
188
  private async output(
229
189
  request: ExtensionRequest,
@@ -232,9 +192,17 @@ class ExtensionExchange {
232
192
  const stdin = extensionWriter(this.proc);
233
193
  const { stdout, stderr } = extensionReaders(this.proc);
234
194
  const write = writeExtensionRequest(stdin, payload);
235
- const [bytes, , exitCode] = await Promise.all([
236
- readBounded(stdout, MAX_MESSAGE_BYTES, false),
237
- readBounded(stderr, MAX_DIAGNOSTIC_BYTES, true),
195
+ const [{ bytes }, , exitCode] = await Promise.all([
196
+ captureBoundedBytes(stdout, {
197
+ limit: MAX_MESSAGE_BYTES,
198
+ overflow: "reject",
199
+ exceeded: () =>
200
+ new ExtensionProtocolError("extension message exceeds 8 MiB"),
201
+ }),
202
+ captureBoundedBytes(stderr, {
203
+ limit: MAX_DIAGNOSTIC_BYTES,
204
+ overflow: "truncate",
205
+ }),
238
206
  this.proc.exited,
239
207
  write,
240
208
  ]);
@@ -248,48 +216,18 @@ class ExtensionExchange {
248
216
  return response;
249
217
  }
250
218
 
251
- private timeout(): Promise<never> {
252
- return new Promise((_resolve, reject) => {
253
- this.timer = setTimeout(() => {
254
- reject(new ExtensionProtocolError("extension timed out"));
255
- }, this.timeoutMs);
256
- });
257
- }
258
-
259
- private aborted(): Promise<never> {
260
- return new Promise((_resolve, reject) => {
261
- const signal = this.options.signal;
262
- if (!signal) return;
263
- this.cancel = () => {
264
- reject(new ExtensionProtocolError("extension request cancelled"));
265
- };
266
- if (signal.aborted) this.cancel();
267
- else signal.addEventListener("abort", this.cancel, { once: true });
268
- });
269
- }
270
-
271
- private dispose(): void {
272
- if (this.timer) clearTimeout(this.timer);
273
- if (this.cancel)
274
- this.options.signal?.removeEventListener("abort", this.cancel);
275
- }
276
-
277
219
  async run(
278
220
  request: ExtensionRequest,
279
221
  payload: Uint8Array,
280
222
  ): Promise<SuccessfulExtensionResponse> {
281
223
  try {
282
- return await Promise.race([
283
- this.output(request, payload),
284
- this.timeout(),
285
- this.aborted(),
286
- ]);
224
+ return await this.deadline.wait(this.output(request, payload));
287
225
  } catch (cause) {
288
- terminate(this.proc);
226
+ terminateOwnedProcessGroup(this.proc);
289
227
  if (cause instanceof ExtensionProtocolError) throw cause;
290
228
  throw new ExtensionProtocolError("extension transport failed", { cause });
291
229
  } finally {
292
- this.dispose();
230
+ this.deadline.dispose();
293
231
  }
294
232
  }
295
233
  }