@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
@@ -0,0 +1,207 @@
1
+ import type {
2
+ InstrumentationCapability,
3
+ InstrumentationRequest,
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";
24
+
25
+ export class EvaluationConfigurationError extends Error {
26
+ constructor(message: string, options?: ErrorOptions) {
27
+ super(message, options);
28
+ this.name = "EvaluationConfigurationError";
29
+ }
30
+ }
31
+
32
+ export interface HostResult {
33
+ finalMessage: string | null;
34
+ complete: boolean;
35
+ executionFailed?: boolean;
36
+ /** Adapter-owned, bounded failure summary suitable for CLI diagnostics. */
37
+ failureMessage?: string;
38
+ processFailure?: {
39
+ phase: "isolation-preflight" | "executable-preflight" | "execution";
40
+ reason: "exit" | "timeout" | "cancelled";
41
+ timeoutMs: number;
42
+ };
43
+ artifacts?: { id: string; bytes: Uint8Array }[];
44
+ observations?: Omit<ObservationEvidence, "source">[];
45
+ actualCondition?: "passive" | "enforced" | "unknown";
46
+ appliedInstrumentation?: InstrumentationRequest[];
47
+ inputTokens?: number | null;
48
+ outputTokens?: number | null;
49
+ costUsd?: number | null;
50
+ usageComplete?: boolean;
51
+ }
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
+
75
+ export interface HostAdapter {
76
+ id: string;
77
+ model: string;
78
+ effort: string;
79
+ instrumentation?: InstrumentationCapability[];
80
+ hostCapabilities?: string[];
81
+ configuration?: Record<string, unknown>;
82
+ run(request: {
83
+ /** Persist private diagnostic bytes before the native host removes its state. */
84
+ retainHostDiagnostics?: (bytes: Uint8Array) => Promise<void>;
85
+ runtimePolicy?: RuntimePolicy;
86
+ runtimeRole?: RuntimeRole;
87
+ candidateTranscriptRoot?: string;
88
+ candidateRuntimeRoot?: string;
89
+ candidateArtifactRoots?: CandidateArtifactRoots;
90
+ prompt: string;
91
+ followUpPrompt?: string;
92
+ workspace: string;
93
+ condition: "passive" | "enforced";
94
+ fixtureBinDir?: string;
95
+ instrumentation?: InstrumentationRequest[];
96
+ codexMarketplace?: {
97
+ artifactRoot: string;
98
+ marketplaceName: string;
99
+ pluginNames: string[];
100
+ artifactPaths: string[];
101
+ };
102
+ claudePluginDirs?: {
103
+ artifactRoots: string[];
104
+ artifactPaths: string[];
105
+ };
106
+ explicitSkillInvocation?:
107
+ | {
108
+ scope?: "plugin";
109
+ pluginName: string;
110
+ skillName: string;
111
+ token: string;
112
+ }
113
+ | {
114
+ scope: "repository";
115
+ pluginName?: never;
116
+ skillName: string;
117
+ token: string;
118
+ };
119
+ signal?: AbortSignal;
120
+ }): Promise<HostResult>;
121
+ }
122
+
123
+ export interface ResolvedCase {
124
+ id: string;
125
+ prompt: string;
126
+ followUpPrompt?: string;
127
+ fixture:
128
+ | { files: Record<string, string>; sourceRef?: never }
129
+ | Omit<RepositoryFixture, "kind">
130
+ | (GeneratedFixture & { sourceRef?: never });
131
+ checks: CheckDeclaration[];
132
+ requiredEvidence: string[];
133
+ }
134
+
135
+ export interface EvaluationProgress {
136
+ stage: "preparation" | "candidate" | "grading" | "evidence";
137
+ status: "started" | "completed" | "skipped";
138
+ trial?: number;
139
+ }
140
+
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>;
148
+ runtimePolicy?: RuntimePolicy;
149
+ projectRoot: string;
150
+ resultsRoot: string;
151
+ runStateRoot?: string;
152
+ case: ResolvedCase;
153
+ host: HostAdapter;
154
+ semanticHost?: HostAdapter;
155
+ advisoryHost?: HostAdapter;
156
+ advisoryExcludedPaths?: string[];
157
+ runnerBuildDigest: string;
158
+ runnerCheckoutRoot?: string;
159
+ projectDigest: string;
160
+ condition: "passive" | "enforced";
161
+ trialCount: number;
162
+ jobs?: number;
163
+ passThreshold: number;
164
+ dry?: boolean;
165
+ signal?: AbortSignal;
166
+ extension?: {
167
+ session: Awaited<ReturnType<typeof openExtensionSession>>;
168
+ resolvedCase: ExtensionCase;
169
+ };
170
+ preparationSources?: PreparationSources;
171
+ shellIsolation?: {
172
+ protectedRoots: string[];
173
+ toolchainBinDir?: string;
174
+ uvRuntimeCache?: boolean;
175
+ };
176
+ }
177
+
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[];
187
+ artifactPath: string;
188
+ }
189
+
190
+ export interface CaseSummary
191
+ extends Assessment, Omit<CliResultDataCase, keyof Assessment | "trials"> {
192
+ trials: TrialSummary[];
193
+ }
194
+
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
+ > {
203
+ runId: string;
204
+ exitCode: number;
205
+ evidencePath: string;
206
+ cases: CaseSummary[];
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
  }