@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/results.ts CHANGED
@@ -1,21 +1,17 @@
1
- export type ExecutionStatus = "completed" | "failed" | "cancelled" | "not_run";
2
- export type GradingStatus =
3
- "completed" | "error" | "unavailable" | "not_requested";
4
- export type TaskVerdict = "passed" | "failed" | "not_assessed";
5
-
6
- export interface CheckOutcome {
7
- id: string;
8
- grader: string;
9
- status: "passed" | "failed" | "unavailable";
10
- detail?: string;
11
- evidenceRefs?: string[];
12
- }
13
-
14
- export interface Assessment {
15
- execution: { status: ExecutionStatus };
16
- grading: { status: GradingStatus };
17
- task: { verdict: TaskVerdict };
18
- }
1
+ import type {
2
+ Assessment,
3
+ CheckOutcome,
4
+ ExecutionStatus,
5
+ GradingStatus,
6
+ TaskVerdict,
7
+ } from "./contract-types";
8
+ export type {
9
+ Assessment,
10
+ CheckOutcome,
11
+ ExecutionStatus,
12
+ GradingStatus,
13
+ TaskVerdict,
14
+ } from "./contract-types";
19
15
 
20
16
  export interface TrialInput {
21
17
  execution: ExecutionStatus;
@@ -27,9 +23,19 @@ export interface TrialInput {
27
23
 
28
24
  /** Reduce one trial without treating process completion as task success. */
29
25
  export function assessTrial(input: TrialInput): Assessment {
26
+ const { declared, seen } = validateCheckResults(input);
27
+ const grading = trialGrading(input, declared.size, seen.size);
28
+ const task = trialVerdict(input, grading);
29
+ return {
30
+ execution: { status: input.execution },
31
+ grading: { status: grading },
32
+ task: { verdict: task },
33
+ };
34
+ }
35
+
36
+ function validateCheckResults(input: TrialInput) {
30
37
  const declared = new Set(input.declaredChecks);
31
- if (declared.size !== input.declaredChecks.length || declared.has(""))
32
- throw new Error("check declarations must have unique nonempty IDs");
38
+ validateCheckDeclarations(input.declaredChecks, declared);
33
39
  const seen = new Set<string>();
34
40
  for (const check of input.checks) {
35
41
  if (!declared.has(check.id) || seen.has(check.id))
@@ -37,29 +43,36 @@ export function assessTrial(input: TrialInput): Assessment {
37
43
  seen.add(check.id);
38
44
  }
39
45
 
40
- const grading: GradingStatus = input.graderError
41
- ? "error"
42
- : input.execution !== "completed"
43
- ? "not_requested"
44
- : input.requiredEvidenceUnavailable ||
45
- input.checks.some((check) => check.status === "unavailable") ||
46
- seen.size !== declared.size
47
- ? "unavailable"
48
- : declared.size === 0
49
- ? "not_requested"
50
- : "completed";
51
- const task: TaskVerdict =
52
- input.execution === "completed" &&
53
- input.checks.some((check) => check.status === "failed")
54
- ? "failed"
55
- : input.execution === "completed" && grading === "completed"
56
- ? "passed"
57
- : "not_assessed";
58
- return {
59
- execution: { status: input.execution },
60
- grading: { status: grading },
61
- task: { verdict: task },
62
- };
46
+ return { declared, seen };
47
+ }
48
+
49
+ function trialGrading(
50
+ input: TrialInput,
51
+ declared: number,
52
+ seen: number,
53
+ ): GradingStatus {
54
+ if (input.graderError) return "error";
55
+ if (input.execution !== "completed") return "not_requested";
56
+ if (evidenceUnavailable(input, declared, seen)) return "unavailable";
57
+ return declared === 0 ? "not_requested" : "completed";
58
+ }
59
+
60
+ function evidenceUnavailable(
61
+ input: TrialInput,
62
+ declared: number,
63
+ seen: number,
64
+ ): boolean {
65
+ return (
66
+ Boolean(input.requiredEvidenceUnavailable) ||
67
+ input.checks.some((check) => check.status === "unavailable") ||
68
+ seen !== declared
69
+ );
70
+ }
71
+
72
+ function trialVerdict(input: TrialInput, grading: GradingStatus): TaskVerdict {
73
+ if (input.execution !== "completed") return "not_assessed";
74
+ if (input.checks.some((check) => check.status === "failed")) return "failed";
75
+ return grading === "completed" ? "passed" : "not_assessed";
63
76
  }
64
77
 
65
78
  /** Apply an explicitly selected policy only to a fully assessed trial. */
@@ -83,6 +96,21 @@ export function summarizeAssessments(
83
96
  assessments: Assessment[],
84
97
  passThreshold = 1,
85
98
  ): Assessment {
99
+ validateSummaryInputs(assessments, passThreshold);
100
+ const execution = summaryExecution(assessments);
101
+ const grading = summaryGrading(assessments);
102
+ const task = summaryVerdict(assessments, passThreshold);
103
+ return {
104
+ execution: { status: execution },
105
+ grading: { status: grading },
106
+ task: { verdict: task },
107
+ };
108
+ }
109
+
110
+ function validateSummaryInputs(
111
+ assessments: Assessment[],
112
+ passThreshold: number,
113
+ ): void {
86
114
  if (!assessments.length)
87
115
  throw new Error("at least one assessment is required");
88
116
  if (
@@ -91,38 +119,36 @@ export function summarizeAssessments(
91
119
  passThreshold > 1
92
120
  )
93
121
  throw new Error("pass threshold must be greater than zero and at most one");
94
- const execution: ExecutionStatus = assessments.some(
95
- (item) => item.execution.status === "failed",
96
- )
97
- ? "failed"
98
- : assessments.some((item) => item.execution.status === "cancelled")
99
- ? "cancelled"
100
- : assessments.some((item) => item.execution.status === "not_run")
101
- ? "not_run"
102
- : "completed";
103
- const grading: GradingStatus = assessments.some(
104
- (item) => item.grading.status === "error",
105
- )
106
- ? "error"
107
- : assessments.some((item) => item.grading.status === "unavailable")
108
- ? "unavailable"
109
- : assessments.some((item) => item.grading.status === "completed")
110
- ? "completed"
111
- : "not_requested";
112
- const task: TaskVerdict = assessments.some(
113
- (item) => item.task.verdict === "not_assessed",
114
- )
115
- ? "not_assessed"
116
- : assessments.filter((item) => item.task.verdict === "passed").length /
117
- assessments.length >=
118
- passThreshold
119
- ? "passed"
120
- : "failed";
121
- return {
122
- execution: { status: execution },
123
- grading: { status: grading },
124
- task: { verdict: task },
125
- };
122
+ }
123
+
124
+ function summaryExecution(assessments: Assessment[]): ExecutionStatus {
125
+ const priority: ExecutionStatus[] = ["failed", "cancelled", "not_run"];
126
+ return (
127
+ priority.find((status) =>
128
+ assessments.some((item) => item.execution.status === status),
129
+ ) ?? "completed"
130
+ );
131
+ }
132
+
133
+ function summaryGrading(assessments: Assessment[]): GradingStatus {
134
+ const priority: GradingStatus[] = ["error", "unavailable", "completed"];
135
+ return (
136
+ priority.find((status) =>
137
+ assessments.some((item) => item.grading.status === status),
138
+ ) ?? "not_requested"
139
+ );
140
+ }
141
+
142
+ function summaryVerdict(
143
+ assessments: Assessment[],
144
+ passThreshold: number,
145
+ ): TaskVerdict {
146
+ if (assessments.some((item) => item.task.verdict === "not_assessed"))
147
+ return "not_assessed";
148
+ const passed = assessments.filter(
149
+ (item) => item.task.verdict === "passed",
150
+ ).length;
151
+ return passed / assessments.length >= passThreshold ? "passed" : "failed";
126
152
  }
127
153
 
128
154
  /** Combine case verdicts after each case has applied its own threshold. */
@@ -144,17 +170,38 @@ export function exitCodeFor(
144
170
  assessment: Assessment,
145
171
  context: ExitContext = {},
146
172
  ): number {
173
+ const interruption = interruptionExit(context);
174
+ if (interruption !== null) return interruption;
175
+ if (executionFailed(assessment.execution.status, context)) return 2;
176
+ const gradingCodes: Partial<Record<GradingStatus, number>> = {
177
+ error: 3,
178
+ unavailable: 4,
179
+ };
180
+ return gradingCodes[assessment.grading.status] ?? taskExitCode(assessment);
181
+ }
182
+
183
+ function interruptionExit(context: ExitContext): number | null {
147
184
  if (context.signal === "SIGINT") return 130;
148
185
  if (context.signal === "SIGTERM") return 143;
149
- if (context.internalFailure) return 70;
150
- if (
151
- assessment.execution.status === "failed" ||
152
- assessment.execution.status === "cancelled" ||
153
- (assessment.execution.status === "not_run" && !context.dryRun)
154
- )
155
- return 2;
156
- if (assessment.grading.status === "error") return 3;
157
- if (assessment.grading.status === "unavailable") return 4;
158
- if (assessment.task.verdict === "failed") return 1;
159
- return 0;
186
+ return context.internalFailure ? 70 : null;
187
+ }
188
+
189
+ function executionFailed(
190
+ status: ExecutionStatus,
191
+ context: ExitContext,
192
+ ): boolean {
193
+ return (
194
+ status === "failed" ||
195
+ status === "cancelled" ||
196
+ (status === "not_run" && !context.dryRun)
197
+ );
198
+ }
199
+
200
+ function taskExitCode(assessment: Assessment): number {
201
+ return assessment.task.verdict === "failed" ? 1 : 0;
202
+ }
203
+
204
+ function validateCheckDeclarations(ids: string[], declared: Set<string>): void {
205
+ if (declared.size !== ids.length || declared.has(""))
206
+ throw new Error("check declarations must have unique nonempty IDs");
160
207
  }
package/src/run-owner.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { isRecord } from "./value-guards";
1
2
  import { Database } from "bun:sqlite";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import {
@@ -10,7 +11,7 @@ import {
10
11
  import { hostname } from "node:os";
11
12
  import { basename, dirname, join } from "node:path";
12
13
 
13
- interface ProcessIdentity {
14
+ export interface ProcessIdentity {
14
15
  pid: number;
15
16
  startedAt: string;
16
17
  host: string;
@@ -29,6 +30,60 @@ interface Claim {
29
30
  finalizedAt?: string;
30
31
  }
31
32
 
33
+ interface RetainedClaim {
34
+ format: "sevro.run-owner.v1";
35
+ evaluationDigest: unknown;
36
+ attemptId: unknown;
37
+ status: Claim["status"];
38
+ owner?: unknown;
39
+ activeRunPath: string;
40
+ evidenceDirectory: string;
41
+ artifactPath: string;
42
+ startedAt?: unknown;
43
+ finalizedAt?: unknown;
44
+ }
45
+
46
+ function errorCode(error: unknown): unknown {
47
+ return isRecord(error) ? error.code : undefined;
48
+ }
49
+ function nonemptyText(value: unknown): value is string {
50
+ return typeof value === "string" && value.length > 0;
51
+ }
52
+
53
+ function validClaimStatus(value: unknown): value is Claim["status"] {
54
+ return (
55
+ value === "active" ||
56
+ value === "complete" ||
57
+ value === "diagnostic" ||
58
+ value === "interrupted"
59
+ );
60
+ }
61
+
62
+ function validClaimIdentity(value: Record<string, unknown>): boolean {
63
+ return (
64
+ value.format === "sevro.run-owner.v1" &&
65
+ /^[a-f0-9]{64}$/.test(String(value.evaluationDigest)) &&
66
+ Boolean(value.attemptId) &&
67
+ validClaimStatus(value.status)
68
+ );
69
+ }
70
+
71
+ function validClaimPaths(value: Record<string, unknown>): boolean {
72
+ return (
73
+ nonemptyText(value.activeRunPath) &&
74
+ nonemptyText(value.evidenceDirectory) &&
75
+ nonemptyText(value.artifactPath)
76
+ );
77
+ }
78
+
79
+ function assertRetainedClaim(
80
+ value: unknown,
81
+ path: string,
82
+ ): asserts value is RetainedClaim {
83
+ if (!isRecord(value) || !validClaimIdentity(value) || !validClaimPaths(value))
84
+ throw new Error(`unrecognized Sevro ownership record: ${path}`);
85
+ }
86
+
32
87
  export interface RunOwner {
33
88
  claimPath: string;
34
89
  checkpointPath: string;
@@ -62,41 +117,29 @@ function withClaimLock<T>(claimPath: string, action: () => T): T {
62
117
  mkdirSync(locks, { recursive: true, mode: 0o700 });
63
118
  const database = new Database(join(locks, `${basename(claimPath)}.sqlite`));
64
119
  try {
65
- database.exec("PRAGMA busy_timeout = 5000");
66
- database.exec("CREATE TABLE IF NOT EXISTS mutex (id INTEGER PRIMARY KEY)");
120
+ database.run("PRAGMA busy_timeout = 5000");
121
+ database.run("CREATE TABLE IF NOT EXISTS mutex (id INTEGER PRIMARY KEY)");
67
122
  return database.transaction(action).immediate();
68
123
  } finally {
69
124
  database.close();
70
125
  }
71
126
  }
72
127
 
73
- function readClaim(path: string): Claim | null {
128
+ function readClaim(path: string): RetainedClaim | null {
74
129
  let bytes: string;
75
130
  try {
76
131
  bytes = readFileSync(path, "utf8");
77
132
  } catch (error) {
78
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
133
+ if (errorCode(error) === "ENOENT") return null;
79
134
  throw error;
80
135
  }
81
- const value = JSON.parse(bytes) as Claim;
82
- if (
83
- value?.format !== "sevro.run-owner.v1" ||
84
- !/^[a-f0-9]{64}$/.test(value.evaluationDigest) ||
85
- !value.attemptId ||
86
- !["active", "complete", "diagnostic", "interrupted"].includes(
87
- value.status,
88
- ) ||
89
- !value.activeRunPath ||
90
- !value.evidenceDirectory ||
91
- !value.artifactPath
92
- )
93
- throw new Error(`unrecognized Sevro ownership record: ${path}`);
136
+ const value: unknown = JSON.parse(bytes);
137
+ assertRetainedClaim(value, path);
94
138
  return value;
95
139
  }
96
140
 
97
- function processIdentity(pid: number): ProcessIdentity | null {
98
- const windows = process.platform === "win32";
99
- const argv = windows
141
+ function identityCommand(pid: number, windows: boolean): string[] {
142
+ return windows
100
143
  ? [
101
144
  "powershell.exe",
102
145
  "-NoProfile",
@@ -105,49 +148,79 @@ function processIdentity(pid: number): ProcessIdentity | null {
105
148
  `(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
106
149
  ]
107
150
  : ["/bin/ps", "-p", String(pid), "-o", "lstart="];
151
+ }
152
+
153
+ function identityEnvironment(windows: boolean) {
154
+ return windows
155
+ ? {
156
+ PATH: process.env.PATH ?? "",
157
+ SystemRoot: process.env.SystemRoot ?? "C:\\Windows",
158
+ }
159
+ : { PATH: "/usr/bin:/bin", LC_ALL: "C" };
160
+ }
161
+
162
+ function processStartTime(result: ReturnType<typeof Bun.spawnSync>): string {
163
+ return result.stdout?.toString().trim() ?? "";
164
+ }
165
+
166
+ export function processIdentity(pid: number): ProcessIdentity | null {
167
+ const windows = process.platform === "win32";
168
+ const argv = identityCommand(pid, windows);
108
169
  let result: ReturnType<typeof Bun.spawnSync>;
109
170
  try {
110
171
  result = Bun.spawnSync(argv, {
111
- env: windows
112
- ? {
113
- PATH: process.env.PATH ?? "",
114
- SystemRoot: process.env.SystemRoot ?? "C:\\Windows",
115
- }
116
- : { PATH: "/usr/bin:/bin", LC_ALL: "C" },
172
+ env: identityEnvironment(windows),
117
173
  stdout: "pipe",
118
174
  stderr: "ignore",
119
175
  });
120
176
  } catch {
121
177
  return null;
122
178
  }
123
- const startedAt = result.stdout?.toString().trim() ?? "";
179
+ const startedAt = processStartTime(result);
124
180
  return result.exitCode === 0 && startedAt
125
181
  ? { pid, startedAt, host: hostname() }
126
182
  : null;
127
183
  }
128
184
 
129
- function ownerStatus(owner: ProcessIdentity): "live" | "dead" | "unknown" {
130
- if (
131
- !owner ||
132
- owner.host !== hostname() ||
133
- !Number.isSafeInteger(owner.pid) ||
134
- owner.pid <= 0 ||
135
- !owner.startedAt
136
- )
137
- return "unknown";
185
+ function validProcessId(value: unknown): value is number {
186
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
187
+ }
188
+
189
+ export function validProcessIdentity(value: unknown): value is ProcessIdentity {
190
+ return (
191
+ isRecord(value) &&
192
+ value.host === hostname() &&
193
+ validProcessId(value.pid) &&
194
+ nonemptyText(value.startedAt)
195
+ );
196
+ }
197
+
198
+ function processLiveness(pid: number): "live" | "dead" | "unknown" {
138
199
  try {
139
- process.kill(owner.pid, 0);
200
+ process.kill(pid, 0);
201
+ return "live";
140
202
  } catch (error) {
141
- return (error as NodeJS.ErrnoException).code === "ESRCH"
142
- ? "dead"
143
- : "unknown";
203
+ return errorCode(error) === "ESRCH" ? "dead" : "unknown";
144
204
  }
205
+ }
206
+
207
+ export function ownerStatus(owner: unknown): "live" | "dead" | "unknown" {
208
+ if (!validProcessIdentity(owner)) return "unknown";
209
+ const liveness = processLiveness(owner.pid);
210
+ if (liveness !== "live") return liveness;
145
211
  const current = processIdentity(owner.pid);
146
212
  if (!current) return "unknown";
147
213
  return current.startedAt === owner.startedAt ? "live" : "dead";
148
214
  }
149
215
 
150
- function preserveAbandoned(claim: Claim): void {
216
+ function activeRunFields(value: unknown): Record<string, unknown> {
217
+ if (value == null) return {};
218
+ // Preserve the existing object-spread representation of opaque active records.
219
+ const entries: [string, unknown][] = Object.entries(value);
220
+ return Object.fromEntries(entries);
221
+ }
222
+
223
+ function preserveAbandoned(claim: RetainedClaim): void {
151
224
  const interrupted = {
152
225
  ...claim,
153
226
  status: "interrupted" as const,
@@ -156,9 +229,12 @@ function preserveAbandoned(claim: Claim): void {
156
229
  atomicJson(join(claim.evidenceDirectory, "run-owner.json"), interrupted);
157
230
  let active: Record<string, unknown>;
158
231
  try {
159
- active = JSON.parse(readFileSync(claim.activeRunPath, "utf8"));
232
+ const parsed: unknown = JSON.parse(
233
+ readFileSync(claim.activeRunPath, "utf8"),
234
+ );
235
+ active = activeRunFields(parsed);
160
236
  } catch (error) {
161
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
237
+ if (errorCode(error) !== "ENOENT") throw error;
162
238
  active = {
163
239
  format: "sevro.active-run.v1",
164
240
  runId: claim.attemptId,
@@ -174,6 +250,37 @@ function preserveAbandoned(claim: Claim): void {
174
250
  });
175
251
  }
176
252
 
253
+ function validateClaimIdentityMatch(
254
+ prior: RetainedClaim | null,
255
+ evaluationDigest: string,
256
+ ): void {
257
+ if (prior && prior.evaluationDigest !== evaluationDigest)
258
+ throw new Error("Sevro ownership record has a different identity");
259
+ }
260
+
261
+ function retainPreviousOwner(
262
+ prior: RetainedClaim | null,
263
+ claimPath: string,
264
+ ): void {
265
+ if (prior === null || prior.status !== "active") return;
266
+ const status = ownerStatus(prior.owner);
267
+ if (status === "live")
268
+ throw new Error(`equivalent Sevro run is active: ${claimPath}`);
269
+ if (status === "unknown")
270
+ throw new Error(`Sevro run ownership is unverifiable: ${claimPath}`);
271
+ preserveAbandoned(prior);
272
+ }
273
+
274
+ function validateCheckpointOwner(context: RunOwner): void {
275
+ const current = readClaim(context.claimPath);
276
+ if (!current || current.attemptId !== context.claim.attemptId)
277
+ throw new Error("Sevro run ownership changed before checkpoint");
278
+ if (current.status !== "active")
279
+ throw new Error("Sevro run owner is already finalized");
280
+ if (ownerStatus(current.owner) !== "live")
281
+ throw new Error("Sevro run owner identity is no longer verifiable");
282
+ }
283
+
177
284
  /** Claim one evaluation identity before candidate execution. */
178
285
  export function startRunOwner(options: {
179
286
  stateRoot: string;
@@ -193,16 +300,8 @@ export function startRunOwner(options: {
193
300
  );
194
301
  return withClaimLock(claimPath, () => {
195
302
  const prior = readClaim(claimPath);
196
- if (prior && prior.evaluationDigest !== options.evaluationDigest)
197
- throw new Error("Sevro ownership record has a different identity");
198
- if (prior?.status === "active") {
199
- const status = ownerStatus(prior.owner);
200
- if (status === "live")
201
- throw new Error(`equivalent Sevro run is active: ${claimPath}`);
202
- if (status === "unknown")
203
- throw new Error(`Sevro run ownership is unverifiable: ${claimPath}`);
204
- preserveAbandoned(prior);
205
- }
303
+ validateClaimIdentityMatch(prior, options.evaluationDigest);
304
+ retainPreviousOwner(prior, claimPath);
206
305
  const owner = processIdentity(process.pid);
207
306
  if (!owner) throw new Error("cannot verify Sevro runner process identity");
208
307
  const claim: Claim = {
@@ -234,15 +333,7 @@ export function checkpointRunOwner(
234
333
  alreadyLocked = false,
235
334
  ): void {
236
335
  const write = () => {
237
- if (!alreadyLocked) {
238
- const current = readClaim(context.claimPath);
239
- if (!current || current.attemptId !== context.claim.attemptId)
240
- throw new Error("Sevro run ownership changed before checkpoint");
241
- if (current.status !== "active")
242
- throw new Error("Sevro run owner is already finalized");
243
- if (ownerStatus(current.owner) !== "live")
244
- throw new Error("Sevro run owner identity is no longer verifiable");
245
- }
336
+ if (!alreadyLocked) validateCheckpointOwner(context);
246
337
  const finalizedAt =
247
338
  status === "active" ? undefined : new Date().toISOString();
248
339
  const updatedClaim = {