@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21

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 (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -1,10 +1,23 @@
1
+ import validExchange from "./generated/extension.cjs";
2
+ import { isRecord } from "./value-guards";
1
3
  import { createHash, randomUUID } from "node:crypto";
2
4
  import { createReadStream } from "node:fs";
3
5
  import { isAbsolute } from "node:path";
4
6
  import { canonicalJson, hashJson } from "./identity";
5
- import type { GeneratedFixture } from "./generated-fixture";
6
- import type { RepositoryFixture } from "./repository-fixture";
7
- import type { FixtureSetupDeclaration } from "./fixture-setup";
7
+ import type { GeneratedFixture } from "./fixtures/generated";
8
+ import type { RepositoryFixture } from "./fixtures/repository";
9
+ import type { FixtureSetupDeclaration } from "./fixtures/setup";
10
+ import type {
11
+ ArtifactReference,
12
+ Assessment,
13
+ CheckDeclaration,
14
+ DomainOutcome,
15
+ ExtensionCheckOutcome,
16
+ MetricEvidence,
17
+ ObservationEvidence,
18
+ TaskVerdict,
19
+ } from "./contract-types";
20
+ import type { InstrumentationRequest } from "./instrumentation";
8
21
  import {
9
22
  ExtensionProtocolError,
10
23
  exchangeExtension,
@@ -15,6 +28,13 @@ import {
15
28
 
16
29
  const PROTOCOL = "sevro.extension.v1";
17
30
 
31
+ export class ExtensionSourceChangedError extends ExtensionProtocolError {
32
+ constructor() {
33
+ super("extension source changed during run");
34
+ this.name = "ExtensionSourceChangedError";
35
+ }
36
+ }
37
+
18
38
  export interface ExtensionCase {
19
39
  id: string;
20
40
  prompt: string;
@@ -23,11 +43,7 @@ export interface ExtensionCase {
23
43
  | { kind: "inline"; files: Record<string, string> }
24
44
  | RepositoryFixture
25
45
  | GeneratedFixture;
26
- checks: {
27
- id: string;
28
- grader: string;
29
- configuration: Record<string, unknown>;
30
- }[];
46
+ checks: CheckDeclaration[];
31
47
  requiredEvidence: string[];
32
48
  extensionData: Record<string, unknown>;
33
49
  }
@@ -42,10 +58,7 @@ export interface PreparationResult {
42
58
  gitExclude?: boolean;
43
59
  executable?: boolean;
44
60
  }[];
45
- requestedInstrumentation: {
46
- id: string;
47
- configuration: Record<string, unknown>;
48
- }[];
61
+ requestedInstrumentation: InstrumentationRequest[];
49
62
  fixtureSetup?: FixtureSetupDeclaration;
50
63
  codexMarketplace?: {
51
64
  artifactRoot: string;
@@ -68,48 +81,20 @@ export interface PreparationResult {
68
81
 
69
82
  export interface EvaluationRequest {
70
83
  caseId: string;
71
- execution: {
72
- status: "completed" | "failed" | "cancelled" | "not_run";
84
+ execution: Assessment["execution"] & {
73
85
  errorCode?: string;
74
86
  };
75
- observations: {
76
- id: string;
77
- source: string;
78
- completeness: "complete" | "partial" | "unavailable";
79
- data: Record<string, unknown>;
80
- }[];
81
- builtinChecks: {
82
- id: string;
83
- status: "passed" | "failed" | "unavailable";
84
- detail?: string;
85
- evidenceRefs: string[];
86
- }[];
87
- artifacts: {
88
- id: string;
89
- path: string;
90
- sha256: string;
91
- gitExclude?: boolean;
92
- executable?: boolean;
93
- }[];
87
+ observations: ObservationEvidence[];
88
+ builtinChecks: ExtensionCheckOutcome[];
89
+ artifacts: ArtifactReference[];
94
90
  extensionData: Record<string, unknown>;
95
91
  }
96
92
 
97
93
  export interface EvaluationResult {
98
- checks: {
99
- id: string;
100
- status: "passed" | "failed" | "unavailable";
101
- detail?: string;
102
- evidenceRefs: string[];
103
- }[];
104
- metrics: { id: string; value: number | null; unit: string }[];
105
- domainOutcomes?: {
106
- id: string;
107
- status: "passed" | "failed" | "unavailable";
108
- detail?: string;
109
- evidenceRefs: string[];
110
- data?: Record<string, unknown>;
111
- }[];
112
- taskVerdictRecommendation?: "passed" | "failed" | "not_assessed";
94
+ checks: ExtensionCheckOutcome[];
95
+ metrics: MetricEvidence[];
96
+ domainOutcomes?: DomainOutcome[];
97
+ taskVerdictRecommendation?: TaskVerdict;
113
98
  }
114
99
 
115
100
  export interface ExtensionSessionOptions extends ExchangeOptions {
@@ -130,19 +115,27 @@ export interface ExtensionIdentity extends NegotiatedExtension {
130
115
  replacedBuiltinGraders: string[];
131
116
  }
132
117
 
133
- async function digestSources(paths: string[]): Promise<string> {
134
- const hashes: string[] = [];
135
- for (const path of paths) {
136
- try {
137
- const hash = createHash("sha256");
138
- for await (const chunk of createReadStream(path)) hash.update(chunk);
139
- hashes.push(hash.digest("hex"));
140
- } catch {
141
- throw new ExtensionProtocolError(
142
- "declared extension source is unreadable",
143
- );
118
+ async function digestSource(path: string): Promise<string> {
119
+ try {
120
+ const hash = createHash("sha256");
121
+ for await (const value of createReadStream(path)) {
122
+ const chunk: unknown = value;
123
+ if (!(chunk instanceof Uint8Array))
124
+ throw new Error("invalid extension source stream");
125
+ hash.update(chunk);
144
126
  }
127
+ return hash.digest("hex");
128
+ } catch (cause) {
129
+ throw new ExtensionProtocolError(
130
+ "declared extension source is unreadable",
131
+ { cause },
132
+ );
145
133
  }
134
+ }
135
+
136
+ async function digestSources(paths: string[]): Promise<string> {
137
+ const hashes: string[] = [];
138
+ for (const path of paths) hashes.push(await digestSource(path));
146
139
  return hashJson(hashes);
147
140
  }
148
141
 
@@ -150,11 +143,37 @@ function jsonCopy<T>(value: T): T {
150
143
  return JSON.parse(canonicalJson(value)) as T;
151
144
  }
152
145
 
153
- /** Bind a stateless extension command to a negotiated identity and source snapshot. */
154
- export async function openExtensionSession(options: ExtensionSessionOptions) {
146
+ function absoluteSessionCommand(
147
+ command: string[],
148
+ ): command is [string, ...string[]] {
149
+ const executable = command[0];
150
+ return executable !== undefined && isAbsolute(executable);
151
+ }
152
+
153
+ function uniqueAbsoluteSources(sources: string[]): boolean {
154
+ return (
155
+ sources.length > 0 &&
156
+ sources.every((path) => isAbsolute(path)) &&
157
+ new Set(sources).size === sources.length
158
+ );
159
+ }
160
+
161
+ function sessionSourceClosure(command: string[], sources: string[]): string[] {
162
+ if (!absoluteSessionCommand(command) || !uniqueAbsoluteSources(sources))
163
+ throw new ExtensionProtocolError(
164
+ "extension source files must be unique absolute paths",
165
+ );
166
+ return [...new Set([command[0], ...sources])];
167
+ }
168
+
169
+ function validateGraderReplacements(graders: string[]): void {
170
+ if (new Set(graders).size !== graders.length)
171
+ throw new ExtensionProtocolError("duplicate built-in grader replacement");
172
+ }
173
+
174
+ function sessionConfiguration(options: ExtensionSessionOptions) {
155
175
  const command = [...options.command];
156
176
  const sourceFiles = [...options.sourceFiles];
157
- const sourceClosure = [...new Set([command[0], ...sourceFiles])];
158
177
  const taskVerdictPolicy = options.taskVerdictPolicy;
159
178
  const replacedBuiltinGraders = [
160
179
  ...(options.replaceBuiltinGraders ?? []),
@@ -164,172 +183,284 @@ export async function openExtensionSession(options: ExtensionSessionOptions) {
164
183
  timeoutMs: options.timeoutMs,
165
184
  signal: options.signal,
166
185
  };
167
- if (
168
- !command.length ||
169
- !isAbsolute(command[0] ?? "") ||
170
- !sourceFiles.length ||
171
- sourceFiles.some((path) => !isAbsolute(path)) ||
172
- new Set(sourceFiles).size !== sourceFiles.length
173
- )
174
- throw new ExtensionProtocolError(
175
- "extension source files must be unique absolute paths",
176
- );
177
- if (new Set(replacedBuiltinGraders).size !== replacedBuiltinGraders.length)
178
- throw new ExtensionProtocolError("duplicate built-in grader replacement");
186
+ const sourceClosure = sessionSourceClosure(command, sourceFiles);
187
+ validateGraderReplacements(replacedBuiltinGraders);
179
188
  const configuration = jsonCopy(options.configuration);
180
189
  const redactedConfiguration = jsonCopy(options.redactedConfiguration);
181
- const sourceDigest = await digestSources(sourceClosure);
182
- const negotiated = await negotiateExtension(
190
+ return {
183
191
  command,
192
+ sourceFiles,
193
+ taskVerdictPolicy,
194
+ replacedBuiltinGraders,
195
+ exchangeOptions,
196
+ sourceClosure,
197
+ configuration,
198
+ redactedConfiguration,
199
+ };
200
+ }
201
+
202
+ type SessionConfiguration = ReturnType<typeof sessionConfiguration>;
203
+
204
+ function sessionIdentity(
205
+ context: SessionConfiguration,
206
+ negotiated: NegotiatedExtension,
207
+ sourceDigest: string,
208
+ ): ExtensionIdentity {
209
+ const identity: ExtensionIdentity = {
210
+ ...negotiated,
211
+ sourceDigest,
212
+ configurationDigest: hashJson({
213
+ command: context.command,
214
+ configuration: context.redactedConfiguration,
215
+ taskVerdictPolicy: context.taskVerdictPolicy ?? null,
216
+ replacedBuiltinGraders: context.replacedBuiltinGraders,
217
+ }),
218
+ selectedTaskVerdictPolicy: context.taskVerdictPolicy ?? null,
219
+ replacedBuiltinGraders: context.replacedBuiltinGraders,
220
+ };
221
+ Object.freeze(identity.capabilities);
222
+ Object.freeze(identity.graders);
223
+ Object.freeze(identity.taskVerdictPolicies);
224
+ Object.freeze(identity.replacedBuiltinGraders);
225
+ Object.freeze(identity);
226
+ return identity;
227
+ }
228
+
229
+ async function discoverSession(
230
+ configuration: SessionConfiguration,
231
+ options: ExtensionSessionOptions,
232
+ ) {
233
+ const sourceDigest = await digestSources(configuration.sourceClosure);
234
+ const negotiated = await negotiateExtension(
235
+ configuration.command,
184
236
  {
185
237
  engineCapabilities: options.engineCapabilities,
186
238
  hostCapabilities: options.hostCapabilities,
187
239
  },
188
- exchangeOptions,
240
+ configuration.exchangeOptions,
189
241
  );
190
- if ((await digestSources(sourceClosure)) !== sourceDigest)
242
+ if ((await digestSources(configuration.sourceClosure)) !== sourceDigest)
191
243
  throw new ExtensionProtocolError(
192
244
  "extension source changed during discovery",
193
245
  );
194
246
  if (
195
- taskVerdictPolicy &&
196
- !negotiated.taskVerdictPolicies.includes(taskVerdictPolicy)
247
+ configuration.taskVerdictPolicy &&
248
+ !negotiated.taskVerdictPolicies.includes(configuration.taskVerdictPolicy)
197
249
  )
198
250
  throw new ExtensionProtocolError(
199
251
  "extension did not advertise the selected task policy",
200
252
  );
201
- const identity: ExtensionIdentity = {
202
- ...negotiated,
253
+ return {
254
+ ...configuration,
203
255
  sourceDigest,
204
- configurationDigest: hashJson({
205
- command,
206
- configuration: redactedConfiguration,
207
- taskVerdictPolicy: taskVerdictPolicy ?? null,
208
- replacedBuiltinGraders,
209
- }),
210
- selectedTaskVerdictPolicy: taskVerdictPolicy ?? null,
211
- replacedBuiltinGraders,
256
+ identity: sessionIdentity(configuration, negotiated, sourceDigest),
212
257
  };
213
- Object.freeze(identity.capabilities);
214
- Object.freeze(identity.graders);
215
- Object.freeze(identity.taskVerdictPolicies);
216
- Object.freeze(identity.replacedBuiltinGraders);
217
- Object.freeze(identity);
258
+ }
259
+
260
+ type SessionContext = Awaited<ReturnType<typeof discoverSession>>;
261
+ interface SessionResults {
262
+ resolve: { cases: ExtensionCase[] };
263
+ prepare: PreparationResult;
264
+ evaluate: EvaluationResult;
265
+ }
218
266
 
219
- async function call(
220
- method: "resolve" | "prepare" | "evaluate",
221
- params: object,
222
- ) {
223
- if ((await digestSources(sourceClosure)) !== sourceDigest)
224
- throw new ExtensionProtocolError("extension source changed during run");
225
- const response = await exchangeExtension(
226
- command,
227
- {
228
- protocol: PROTOCOL,
229
- id: randomUUID(),
230
- method,
231
- params: { ...params, configuration },
232
- },
233
- exchangeOptions,
267
+ function sessionResponse<M extends keyof SessionResults>(
268
+ value: unknown,
269
+ method: M,
270
+ ): value is { method: M; result: SessionResults[M] } {
271
+ return (
272
+ validExchange(value) &&
273
+ isRecord(value) &&
274
+ value.method === method &&
275
+ Object.hasOwn(value, "result")
276
+ );
277
+ }
278
+
279
+ async function verifySessionSources(context: SessionContext): Promise<void> {
280
+ if ((await digestSources(context.sourceClosure)) !== context.sourceDigest)
281
+ throw new ExtensionSourceChangedError();
282
+ }
283
+
284
+ async function callSession<M extends keyof SessionResults>(
285
+ context: SessionContext,
286
+ method: M,
287
+ params: object,
288
+ ): Promise<SessionResults[M]> {
289
+ await verifySessionSources(context);
290
+ const response = await exchangeExtension(
291
+ context.command,
292
+ {
293
+ protocol: PROTOCOL,
294
+ id: randomUUID(),
295
+ method,
296
+ params: { ...params, configuration: context.configuration },
297
+ },
298
+ context.exchangeOptions,
299
+ );
300
+ await verifySessionSources(context);
301
+ if (!sessionResponse(response, method))
302
+ throw new ExtensionProtocolError("invalid extension response");
303
+ return response.result;
304
+ }
305
+
306
+ interface ResolvedHostRoute {
307
+ id: string;
308
+ model: string;
309
+ effort: string;
310
+ capabilities: string[];
311
+ }
312
+
313
+ async function resolveSession(
314
+ context: SessionContext,
315
+ projectRoot: string,
316
+ selectors: Record<string, unknown>,
317
+ host: ResolvedHostRoute | undefined,
318
+ ): Promise<ExtensionCase[]> {
319
+ const result = await callSession(context, "resolve", {
320
+ projectRoot,
321
+ selectors,
322
+ ...(host ? { host } : {}),
323
+ });
324
+ const ids = result.cases.map((item) => item.id);
325
+ if (new Set(ids).size !== ids.length)
326
+ throw new ExtensionProtocolError("extension resolved duplicate case IDs");
327
+ return result.cases;
328
+ }
329
+
330
+ async function prepareSession(
331
+ context: SessionContext,
332
+ resolvedCase: ExtensionCase,
333
+ host: { id: string; capabilities: string[] },
334
+ condition: "passive" | "enforced",
335
+ ): Promise<PreparationResult> {
336
+ return callSession(context, "prepare", {
337
+ case: resolvedCase,
338
+ host,
339
+ condition,
340
+ });
341
+ }
342
+
343
+ function validateEvaluationIds(
344
+ result: EvaluationResult,
345
+ outcomes: NonNullable<EvaluationResult["domainOutcomes"]>,
346
+ extensionId: string,
347
+ ): void {
348
+ const ids = [
349
+ ...result.checks.map((check) => check.id),
350
+ ...outcomes.map((outcome) => outcome.id),
351
+ ];
352
+ if (
353
+ new Set(ids).size !== ids.length ||
354
+ ids.some((id) => !id.startsWith(`${extensionId}.`))
355
+ )
356
+ throw new ExtensionProtocolError(
357
+ "extension returned duplicate or foreign result IDs",
234
358
  );
235
- if ((await digestSources(sourceClosure)) !== sourceDigest)
236
- throw new ExtensionProtocolError("extension source changed during run");
237
- return response.result!;
238
- }
359
+ }
239
360
 
361
+ function validateSelectedTaskPolicy(
362
+ result: EvaluationResult,
363
+ policy: string | undefined,
364
+ ): void {
365
+ if (result.taskVerdictRecommendation && !policy)
366
+ throw new ExtensionProtocolError("extension task policy was not selected");
367
+ if (policy && !result.taskVerdictRecommendation)
368
+ throw new ExtensionProtocolError(
369
+ "extension omitted the selected task policy recommendation",
370
+ );
371
+ }
372
+
373
+ function availableEvidence(request: EvaluationRequest): Map<string, string> {
374
+ const available = new Map<string, string>(
375
+ request.observations.map((item) => [item.id, item.completeness]),
376
+ );
377
+ for (const item of request.artifacts) available.set(item.id, "complete");
378
+ for (const item of request.builtinChecks)
379
+ available.set(
380
+ item.id,
381
+ item.status === "unavailable" ? "unavailable" : "complete",
382
+ );
383
+ return available;
384
+ }
385
+
386
+ type EvidenceOutcome = EvaluationResult["checks"][number];
387
+ function passedWithoutEvidence(item: EvidenceOutcome): boolean {
388
+ return item.status === "passed" && item.evidenceRefs.length === 0;
389
+ }
390
+ function passedWithIncompleteEvidence(
391
+ item: EvidenceOutcome,
392
+ available: Map<string, string>,
393
+ ): boolean {
394
+ return (
395
+ item.status === "passed" &&
396
+ item.evidenceRefs.some((id) => available.get(id) !== "complete")
397
+ );
398
+ }
399
+
400
+ function validateResultEvidence(
401
+ item: EvidenceOutcome,
402
+ available: Map<string, string>,
403
+ ): void {
404
+ if (passedWithoutEvidence(item))
405
+ throw new ExtensionProtocolError(
406
+ "extension passed a result without evidence",
407
+ );
408
+ if (item.evidenceRefs.some((id) => !available.has(id)))
409
+ throw new ExtensionProtocolError("extension result cites unknown evidence");
410
+ if (passedWithIncompleteEvidence(item, available))
411
+ throw new ExtensionProtocolError(
412
+ "extension passed a result with incomplete evidence",
413
+ );
414
+ }
415
+
416
+ async function evaluateSession(
417
+ context: SessionContext,
418
+ request: EvaluationRequest,
419
+ ): Promise<EvaluationResult> {
420
+ const result = await callSession(context, "evaluate", {
421
+ ...request,
422
+ ...(context.taskVerdictPolicy
423
+ ? { selectedTaskVerdictPolicy: context.taskVerdictPolicy }
424
+ : {}),
425
+ });
426
+ const outcomes = result.domainOutcomes ?? [];
427
+ validateEvaluationIds(result, outcomes, context.identity.id);
428
+ validateSelectedTaskPolicy(result, context.taskVerdictPolicy);
429
+ const available = availableEvidence(request);
430
+ for (const item of [...result.checks, ...outcomes])
431
+ validateResultEvidence(item, available);
432
+ return result;
433
+ }
434
+
435
+ function extensionSession(context: SessionContext) {
240
436
  return {
241
- identity,
437
+ identity: context.identity,
242
438
  get redactedConfiguration() {
243
- return jsonCopy(redactedConfiguration);
439
+ return jsonCopy(context.redactedConfiguration);
244
440
  },
245
- async resolve(
441
+ resolve(
246
442
  projectRoot: string,
247
443
  selectors: Record<string, unknown>,
248
- host?: {
249
- id: string;
250
- model: string;
251
- effort: string;
252
- capabilities: string[];
253
- },
444
+ host?: ResolvedHostRoute,
254
445
  ): Promise<ExtensionCase[]> {
255
- const result = await call("resolve", {
256
- projectRoot,
257
- selectors,
258
- ...(host ? { host } : {}),
259
- });
260
- const cases = result.cases as ExtensionCase[];
261
- const ids = cases.map((item) => item.id);
262
- if (new Set(ids).size !== ids.length)
263
- throw new ExtensionProtocolError(
264
- "extension resolved duplicate case IDs",
265
- );
266
- return cases;
446
+ return resolveSession(context, projectRoot, selectors, host);
267
447
  },
268
- async prepare(
448
+ prepare(
269
449
  resolvedCase: ExtensionCase,
270
450
  host: { id: string; capabilities: string[] },
271
451
  condition: "passive" | "enforced",
272
452
  ): Promise<PreparationResult> {
273
- return (await call("prepare", {
274
- case: resolvedCase,
275
- host,
276
- condition,
277
- })) as unknown as PreparationResult;
453
+ return prepareSession(context, resolvedCase, host, condition);
278
454
  },
279
- async evaluate(request: EvaluationRequest): Promise<EvaluationResult> {
280
- const result = (await call("evaluate", {
281
- ...request,
282
- ...(taskVerdictPolicy
283
- ? { selectedTaskVerdictPolicy: taskVerdictPolicy }
284
- : {}),
285
- })) as unknown as EvaluationResult;
286
- const outcomes = result.domainOutcomes ?? [];
287
- const ids = [
288
- ...result.checks.map((check) => check.id),
289
- ...outcomes.map((outcome) => outcome.id),
290
- ];
291
- if (
292
- new Set(ids).size !== ids.length ||
293
- ids.some((id) => !id.startsWith(`${identity.id}.`))
294
- )
295
- throw new ExtensionProtocolError(
296
- "extension returned duplicate or foreign result IDs",
297
- );
298
- if (result.taskVerdictRecommendation && !taskVerdictPolicy)
299
- throw new ExtensionProtocolError(
300
- "extension task policy was not selected",
301
- );
302
- if (taskVerdictPolicy && !result.taskVerdictRecommendation)
303
- throw new ExtensionProtocolError(
304
- "extension omitted the selected task policy recommendation",
305
- );
306
- const available = new Map(
307
- request.observations.map((item) => [item.id, item.completeness]),
308
- );
309
- for (const item of request.artifacts) available.set(item.id, "complete");
310
- for (const item of request.builtinChecks)
311
- available.set(
312
- item.id,
313
- item.status === "unavailable" ? "unavailable" : "complete",
314
- );
315
- for (const item of [...result.checks, ...outcomes]) {
316
- if (item.status === "passed" && item.evidenceRefs.length === 0)
317
- throw new ExtensionProtocolError(
318
- "extension passed a result without evidence",
319
- );
320
- if (item.evidenceRefs.some((id) => !available.has(id)))
321
- throw new ExtensionProtocolError(
322
- "extension result cites unknown evidence",
323
- );
324
- if (
325
- item.status === "passed" &&
326
- item.evidenceRefs.some((id) => available.get(id) !== "complete")
327
- )
328
- throw new ExtensionProtocolError(
329
- "extension passed a result with incomplete evidence",
330
- );
331
- }
332
- return result;
455
+ evaluate(request: EvaluationRequest): Promise<EvaluationResult> {
456
+ return evaluateSession(context, request);
333
457
  },
334
458
  };
335
459
  }
460
+
461
+ /** Bind a stateless extension command to a negotiated identity and source snapshot. */
462
+ export async function openExtensionSession(options: ExtensionSessionOptions) {
463
+ return extensionSession(
464
+ await discoverSession(sessionConfiguration(options), options),
465
+ );
466
+ }