@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
package/src/report.ts CHANGED
@@ -1,86 +1,26 @@
1
+ import type {
2
+ CliResultData,
3
+ ReportData,
4
+ RunEvidenceData,
5
+ } from "./schema-types";
1
6
  import { readFile } from "node:fs/promises";
2
- import { isAbsolute } from "node:path";
7
+ import { isAbsolute, join, resolve } from "node:path";
3
8
  import { parseArgs } from "node:util";
4
9
  import { canonicalJson } from "./identity";
5
10
  import { assertCliResult, assertReport, assertRunEvidence } from "./schema";
11
+ import { canonicalRuntimeRoot } from "./runtime/paths";
12
+ import { nativeTranscriptRetention } from "./transcript-retention";
6
13
 
7
- type State = { status: string };
8
- type Verdict = { verdict: string };
9
- type DomainOutcome = { id: string; status: string };
10
- type Trial = {
11
- trial: number;
12
- execution: State;
13
- grading: State;
14
- task: Verdict;
15
- domainOutcomes?: DomainOutcome[];
16
- };
17
- type Case = {
18
- caseId: string;
19
- execution: State;
20
- grading: State;
21
- task: Verdict;
22
- trials: Trial[];
23
- };
24
- type CliResult = {
25
- format: string;
26
- runId: string | null;
27
- execution: State;
28
- grading: State;
29
- task: Verdict;
30
- exitCode: number;
31
- evidencePath: string | null;
32
- cases: Case[];
33
- };
34
- type Usage = {
35
- inputTokens: number | null;
36
- outputTokens: number | null;
37
- costUsd: number | null;
38
- complete: boolean;
39
- };
40
- type TrialEvidence = {
41
- caseId: string;
42
- trial: number;
43
- executionMode: string;
44
- candidateDurationMs?: number | null;
45
- usage: Usage;
46
- };
47
- type Evidence = {
48
- runId: string;
49
- result: CliResult;
50
- runner: { buildDigest: string };
51
- evaluationIdentity: { digest: string };
52
- condition: { requested: string; actual: string };
53
- routes: Array<{ role: string; host: string; model: string; effort: string }>;
54
- trials: TrialEvidence[];
55
- };
14
+ type CliResult = CliResultData;
15
+ type Evidence = RunEvidenceData;
16
+ type Case = CliResult["cases"][number];
17
+ type Trial = Case["trials"][number];
56
18
 
57
- export type ReportRow = {
58
- resultFile: string;
59
- evidencePath: string | null;
60
- runId: string | null;
61
- caseId: string;
62
- execution: string;
63
- grading: string;
64
- task: string;
65
- exitCode: number;
66
- trialCount: number;
67
- taskPassRate: number | null;
68
- candidateDurationMs: number | null;
69
- inputTokens: number | null;
70
- outputTokens: number | null;
71
- costUsd: number | null;
72
- runnerBuildDigest: string | null;
73
- evaluationDigest: string | null;
74
- requestedCondition: string | null;
75
- actualCondition: string | null;
76
- candidateRoute: { host: string; model: string; effort: string } | null;
77
- routes: Array<{ role: string; host: string; model: string; effort: string }>;
78
- domainOutcomes: Array<{ trial: number; id: string; status: string }>;
79
- };
19
+ export type ReportRow = ReportData["rows"][number];
80
20
 
81
21
  function sumKnown(values: Array<number | null | undefined>): number | null {
82
22
  return values.length && values.every((value) => value != null)
83
- ? values.reduce<number>((total, value) => total + value!, 0)
23
+ ? values.reduce<number>((total, value) => total + value, 0)
84
24
  : null;
85
25
  }
86
26
 
@@ -104,24 +44,17 @@ function passRate(trials: Trial[]): number | null {
104
44
  async function loadResult(path: string) {
105
45
  if (!isAbsolute(path))
106
46
  throw new Error("report result paths must be absolute");
107
- const result = JSON.parse(await readFile(path, "utf8")) as unknown;
47
+ const result: unknown = JSON.parse(await readFile(path, "utf8"));
108
48
  assertCliResult(result);
109
- return result as CliResult;
49
+ return result;
110
50
  }
111
51
 
112
52
  async function loadEvidence(result: CliResult): Promise<Evidence | null> {
113
- if (!result.evidencePath) {
114
- if (result.cases.length || result.exitCode === 0)
115
- throw new Error("completed result has no retained evidence");
116
- return null;
117
- }
118
- if (!isAbsolute(result.evidencePath))
119
- throw new Error("evidence path must be absolute");
120
- const value = JSON.parse(
121
- await readFile(result.evidencePath, "utf8"),
122
- ) as unknown;
53
+ const path = retainedEvidencePath(result);
54
+ if (path === null) return null;
55
+ const value: unknown = JSON.parse(await readFile(path, "utf8"));
123
56
  assertRunEvidence(value);
124
- const evidence = value as Evidence;
57
+ const evidence = value;
125
58
  if (
126
59
  evidence.runId !== result.runId ||
127
60
  canonicalJson(evidence.result) !== canonicalJson(result)
@@ -130,6 +63,17 @@ async function loadEvidence(result: CliResult): Promise<Evidence | null> {
130
63
  return evidence;
131
64
  }
132
65
 
66
+ function retainedEvidencePath(result: CliResult): string | null {
67
+ if (!result.evidencePath) {
68
+ if (result.cases.length || result.exitCode === 0)
69
+ throw new Error("completed result has no retained evidence");
70
+ return null;
71
+ }
72
+ if (!isAbsolute(result.evidencePath))
73
+ throw new Error("evidence path must be absolute");
74
+ return result.evidencePath;
75
+ }
76
+
133
77
  function trialEvidence(
134
78
  caseId: string,
135
79
  trials: Trial[],
@@ -153,15 +97,11 @@ function reportRow(
153
97
  result: CliResult,
154
98
  selected: Case,
155
99
  evidence: Evidence | null,
100
+ transcripts: Awaited<ReturnType<typeof nativeTranscriptRetention>>,
156
101
  ): ReportRow {
157
102
  const retained = trialEvidence(selected.caseId, selected.trials, evidence);
158
- const candidateRoutes =
159
- evidence?.routes.filter((route) => route.role === "candidate") ?? [];
160
- if (evidence && candidateRoutes.length !== 1)
161
- throw new Error("retained candidate route is ambiguous");
162
- const completeUsage =
163
- retained.length > 0 && retained.every((trial) => trial.usage.complete);
164
- const route = candidateRoutes[0];
103
+ const candidateRoute = retainedCandidateRoute(evidence);
104
+ const usage = retainedUsage(retained);
165
105
  return {
166
106
  resultFile: path,
167
107
  evidencePath: result.evidencePath,
@@ -176,23 +116,17 @@ function reportRow(
176
116
  candidateDurationMs: sumKnown(
177
117
  retained.map((trial) => trial.candidateDurationMs),
178
118
  ),
179
- inputTokens: completeUsage
180
- ? sumKnown(retained.map((trial) => trial.usage.inputTokens))
181
- : null,
182
- outputTokens: completeUsage
183
- ? sumKnown(retained.map((trial) => trial.usage.outputTokens))
184
- : null,
185
- costUsd: completeUsage
186
- ? sumKnown(retained.map((trial) => trial.usage.costUsd))
187
- : null,
188
- runnerBuildDigest: evidence?.runner.buildDigest ?? null,
189
- evaluationDigest: evidence?.evaluationIdentity.digest ?? null,
190
- requestedCondition: evidence?.condition.requested ?? null,
191
- actualCondition: evidence?.condition.actual ?? null,
192
- candidateRoute: route
193
- ? { host: route.host, model: route.model, effort: route.effort }
194
- : null,
195
- routes: evidence?.routes ?? [],
119
+ ...usage,
120
+ ...retainedIdentity(evidence),
121
+ candidateRoute,
122
+ nativeTranscripts: {
123
+ retained: transcripts.filter(
124
+ (item) => item.caseId === selected.caseId && !item.pruned,
125
+ ).length,
126
+ pruned: transcripts.filter(
127
+ (item) => item.caseId === selected.caseId && item.pruned,
128
+ ).length,
129
+ },
196
130
  domainOutcomes: selected.trials.flatMap((trial) =>
197
131
  (trial.domainOutcomes ?? []).map((outcome) => ({
198
132
  trial: trial.trial,
@@ -203,21 +137,58 @@ function reportRow(
203
137
  };
204
138
  }
205
139
 
140
+ function retainedCandidateRoute(
141
+ evidence: Evidence | null,
142
+ ): ReportRow["candidateRoute"] {
143
+ if (!evidence) return null;
144
+ const routes = evidence.routes.filter((route) => route.role === "candidate"),
145
+ [route] = routes;
146
+ if (routes.length !== 1 || route === undefined)
147
+ throw new Error("retained candidate route is ambiguous");
148
+ return { host: route.host, model: route.model, effort: route.effort };
149
+ }
150
+ function retainedUsage(retained: Evidence["trials"]) {
151
+ const complete =
152
+ retained.length > 0 && retained.every((trial) => trial.usage.complete);
153
+ return {
154
+ inputTokens: complete
155
+ ? sumKnown(retained.map((trial) => trial.usage.inputTokens))
156
+ : null,
157
+ outputTokens: complete
158
+ ? sumKnown(retained.map((trial) => trial.usage.outputTokens))
159
+ : null,
160
+ costUsd: complete
161
+ ? sumKnown(retained.map((trial) => trial.usage.costUsd))
162
+ : null,
163
+ };
164
+ }
165
+ function retainedIdentity(evidence: Evidence | null) {
166
+ if (!evidence)
167
+ return {
168
+ runnerBuildDigest: null,
169
+ evaluationDigest: null,
170
+ requestedCondition: null,
171
+ actualCondition: null,
172
+ routes: [],
173
+ };
174
+ return {
175
+ runnerBuildDigest: evidence.runner.buildDigest,
176
+ evaluationDigest: evidence.evaluationIdentity.digest,
177
+ requestedCondition: evidence.condition.requested,
178
+ actualCondition: evidence.condition.actual,
179
+ routes: evidence.routes,
180
+ };
181
+ }
182
+
206
183
  export async function createReport(paths: string[]) {
207
184
  if (!paths.length || new Set(paths).size !== paths.length)
208
185
  throw new Error("report needs distinct result files");
209
186
  const rows: ReportRow[] = [];
210
- const inputs: Array<{
211
- resultFile: string;
212
- execution: string;
213
- grading: string;
214
- task: string;
215
- exitCode: number;
216
- caseCount: number;
217
- }> = [];
187
+ const inputs: ReportData["inputs"] = [];
218
188
  for (const path of paths) {
219
189
  const result = await loadResult(path);
220
190
  const evidence = await loadEvidence(result);
191
+ const transcripts = await nativeTranscriptRetention(evidence);
221
192
  inputs.push({
222
193
  resultFile: path,
223
194
  execution: result.execution.status,
@@ -227,7 +198,7 @@ export async function createReport(paths: string[]) {
227
198
  caseCount: result.cases.length,
228
199
  });
229
200
  for (const selected of result.cases)
230
- rows.push(reportRow(path, result, selected, evidence));
201
+ rows.push(reportRow(path, result, selected, evidence, transcripts));
231
202
  }
232
203
  const report = {
233
204
  format: "sevro.report.v1",
@@ -279,15 +250,13 @@ export function renderReport(
279
250
  "",
280
251
  "| Case | Task | Execution | Grading | Pass rate | Candidate time | Candidate tokens | Candidate cost | Condition | Candidate route |",
281
252
  "| --- | --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- |",
282
- ...report.rows.map(
283
- (row) =>
284
- `| ${cell(row.caseId)} | ${row.task} | ${row.execution} | ${row.grading} | ${row.taskPassRate === null ? "unknown" : `${(row.taskPassRate * 100).toFixed(0)}%`} | ${seconds(row.candidateDurationMs)} | ${measured(row.inputTokens)} / ${measured(row.outputTokens)} | ${row.costUsd === null ? "unknown" : `$${row.costUsd.toFixed(4)}`} | ${row.requestedCondition ?? "unknown"} / ${row.actualCondition ?? "unknown"} | ${row.candidateRoute ? cell(`${row.candidateRoute.host}/${row.candidateRoute.model}@${row.candidateRoute.effort}`) : "unknown"} |`,
285
- ),
253
+ ...report.rows.map(caseOutcomeLine),
286
254
  "",
287
255
  "## Retained evidence",
288
256
  "",
289
257
  ...report.rows.map(
290
- (row) => `- ${cell(row.caseId)}: ${row.evidencePath ?? "unavailable"}`,
258
+ (row) =>
259
+ `- ${cell(row.caseId)}: ${row.evidencePath ?? "unavailable"}${transcriptSummary(row)}`,
291
260
  ),
292
261
  "",
293
262
  ];
@@ -324,28 +293,91 @@ export function renderReport(
324
293
  return lines.join("\n");
325
294
  }
326
295
 
296
+ function transcriptSummary(row: ReportRow): string {
297
+ if (!row.nativeTranscripts) return "";
298
+ return `; native transcripts: ${row.nativeTranscripts.retained} retained, ${row.nativeTranscripts.pruned} pruned`;
299
+ }
300
+
301
+ function passRateCell(rate: number | null): string {
302
+ return rate === null ? "unknown" : `${(rate * 100).toFixed(0)}%`;
303
+ }
304
+ function costCell(cost: number | null): string {
305
+ return cost === null ? "unknown" : `$${cost.toFixed(4)}`;
306
+ }
307
+ function routeCell(route: ReportRow["candidateRoute"]): string {
308
+ return route
309
+ ? cell(`${route.host}/${route.model}@${route.effort}`)
310
+ : "unknown";
311
+ }
312
+ function conditionCell(row: ReportRow): string {
313
+ return `${row.requestedCondition ?? "unknown"} / ${row.actualCondition ?? "unknown"}`;
314
+ }
315
+ function caseOutcomeLine(row: ReportRow): string {
316
+ return `| ${cell(row.caseId)} | ${row.task} | ${row.execution} | ${row.grading} | ${passRateCell(row.taskPassRate)} | ${seconds(row.candidateDurationMs)} | ${measured(row.inputTokens)} / ${measured(row.outputTokens)} | ${costCell(row.costUsd)} | ${conditionCell(row)} | ${routeCell(row.candidateRoute)} |`;
317
+ }
318
+
327
319
  export async function reportCommand(argv: string[]): Promise<number> {
328
320
  try {
329
- const { values, positionals } = parseArgs({
330
- args: argv,
331
- allowPositionals: true,
332
- strict: true,
333
- options: {
334
- json: { type: "boolean" },
335
- "result-file": { type: "string", multiple: true },
336
- },
337
- });
338
- if (positionals.length !== 1 || positionals[0] !== "report")
339
- throw new Error("expected the report command");
340
- const report = await createReport(values["result-file"] ?? []);
321
+ const values = reportInvocation(argv);
322
+ const report = await createReport(await reportPaths(values));
341
323
  process.stdout.write(
342
324
  values.json ? `${JSON.stringify(report)}\n` : renderReport(report),
343
325
  );
344
326
  return 0;
345
327
  } catch (error) {
346
- process.stderr.write(
347
- `${error instanceof Error ? error.message : "invalid report input"}\n`,
348
- );
328
+ process.stderr.write(`${reportErrorMessage(error)}\n`);
349
329
  return 64;
350
330
  }
351
331
  }
332
+
333
+ function reportInvocation(argv: string[]) {
334
+ const { values, positionals } = parseArgs({
335
+ args: argv,
336
+ allowPositionals: true,
337
+ strict: true,
338
+ options: {
339
+ json: { type: "boolean" },
340
+ "result-file": { type: "string", multiple: true },
341
+ latest: { type: "boolean" },
342
+ "results-root": { type: "string" },
343
+ },
344
+ });
345
+ if (positionals.length !== 1 || positionals[0] !== "report")
346
+ throw new Error("expected the report command");
347
+ return values;
348
+ }
349
+
350
+ async function reportPaths(
351
+ values: ReturnType<typeof reportInvocation>,
352
+ ): Promise<string[]> {
353
+ validateReportSource(values);
354
+ const paths = selectedReportPaths(values);
355
+ if (paths.some((path) => !path.trim()))
356
+ throw new Error("--result-file must name a readable retained result file");
357
+ return Promise.all(paths.map((path) => canonicalRuntimeRoot(resolve(path))));
358
+ }
359
+
360
+ function validateReportSource(
361
+ values: ReturnType<typeof reportInvocation>,
362
+ ): void {
363
+ if (values.latest && values["result-file"] !== undefined)
364
+ throw new Error("--latest and --result-file are exclusive");
365
+ if (values["results-root"] !== undefined && !values.latest)
366
+ throw new Error("--results-root requires --latest");
367
+ }
368
+
369
+ function selectedReportPaths(
370
+ values: ReturnType<typeof reportInvocation>,
371
+ ): string[] {
372
+ return values.latest
373
+ ? [
374
+ join(
375
+ values["results-root"] ?? join(process.cwd(), ".sevro", "results"),
376
+ "latest.json",
377
+ ),
378
+ ]
379
+ : (values["result-file"] ?? []);
380
+ }
381
+ function reportErrorMessage(error: unknown): string {
382
+ return error instanceof Error ? error.message : "invalid report input";
383
+ }
@@ -0,0 +1,33 @@
1
+ import type { ExtensionCase } from "./extension-session";
2
+ import type { RepositoryFixture } from "./fixtures/repository";
3
+
4
+ function repositoryOverlay(fixture: RepositoryFixture) {
5
+ return {
6
+ ...(fixture.files ? { files: fixture.files } : {}),
7
+ ...(fixture.staged ? { staged: fixture.staged } : {}),
8
+ ...(fixture.commitFiles ? { commitFiles: true } : {}),
9
+ };
10
+ }
11
+
12
+ function repositoryTools(fixture: RepositoryFixture) {
13
+ return {
14
+ ...(fixture.hooks ? { hooks: fixture.hooks } : {}),
15
+ ...(fixture.bin ? { bin: fixture.bin } : {}),
16
+ };
17
+ }
18
+
19
+ /** Translate negotiated fixture declarations into the standalone case representation. */
20
+ export function resolvedFixture(fixture: ExtensionCase["fixture"]) {
21
+ switch (fixture.kind) {
22
+ case "inline":
23
+ return { files: fixture.files };
24
+ case "repository":
25
+ return {
26
+ sourceRef: fixture.sourceRef,
27
+ ...repositoryOverlay(fixture),
28
+ ...repositoryTools(fixture),
29
+ };
30
+ case "generated":
31
+ return fixture;
32
+ }
33
+ }
@@ -0,0 +1,29 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { join } from "node:path";
3
+ import { lstat } from "node:fs/promises";
4
+
5
+ export async function retainedFile(path: string) {
6
+ const info = await lstat(path).catch((cause: unknown) => {
7
+ if ((cause as NodeJS.ErrnoException).code === "ENOENT") return null;
8
+ throw cause;
9
+ });
10
+ if (info && !info.isFile())
11
+ throw new Error(`retained storage must be a regular file: ${path}`);
12
+ return info;
13
+ }
14
+
15
+ /** Serialize result publication and pruning across CLI processes. Closing rolls back the lock transaction. */
16
+ export async function withResultsLock<T>(
17
+ root: string,
18
+ action: () => Promise<T>,
19
+ ): Promise<T> {
20
+ await retainedFile(join(root, ".retention-lock.sqlite"));
21
+ const database = new Database(join(root, ".retention-lock.sqlite"));
22
+ try {
23
+ database.run("PRAGMA busy_timeout = 5000");
24
+ database.run("BEGIN IMMEDIATE");
25
+ return await action();
26
+ } finally {
27
+ database.close();
28
+ }
29
+ }