@bjoernrochel/sevro 0.1.0-rc.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/CONTRIBUTING.md +22 -6
  2. package/README.md +5 -1
  3. package/docs/README.md +19 -15
  4. package/docs/architecture.md +138 -17
  5. package/docs/assets/README.md +5 -4
  6. package/docs/builtin-graders-v1.md +9 -2
  7. package/docs/codex-host.md +155 -57
  8. package/docs/contracts.md +2 -1
  9. package/docs/creating-cases.md +9 -2
  10. package/docs/development-cli.md +187 -19
  11. package/docs/documentation-quality.md +12 -2
  12. package/docs/evaluation-workflow.md +110 -0
  13. package/docs/extension-protocol-v1.md +26 -3
  14. package/docs/getting-started.md +23 -2
  15. package/docs/guide-evaluation.md +33 -1
  16. package/docs/identity-v1.md +25 -2
  17. package/docs/installing.md +11 -11
  18. package/docs/native-controls.md +76 -5
  19. package/docs/native-hosts.md +196 -7
  20. package/docs/reading-results.md +102 -0
  21. package/docs/recipes.md +489 -0
  22. package/docs/releases.md +31 -12
  23. package/docs/report-v1.md +29 -1
  24. package/docs/results-v1.md +122 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +255 -22
  27. package/docs/troubleshooting.md +12 -12
  28. package/docs/typescript-quality.md +89 -26
  29. package/examples/recipes/candidate.ts +26 -0
  30. package/examples/recipes/cases/coding-change.json +21 -0
  31. package/examples/recipes/cases/host-tools.json +21 -0
  32. package/examples/recipes/cases/mise.json +21 -0
  33. package/examples/recipes/cases/node.json +21 -0
  34. package/examples/recipes/cases/python-uv.json +21 -0
  35. package/examples/recipes/cases/semantic.json +17 -0
  36. package/examples/recipes/coding-change/check.ts +23 -0
  37. package/examples/recipes/extension-artifacts.ts +65 -0
  38. package/examples/recipes/extension-methods.ts +174 -0
  39. package/examples/recipes/extension.ts +37 -0
  40. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  41. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  42. package/examples/recipes/repository-skill/README.md +3 -0
  43. package/examples/recipes/run-inputs.ts +188 -0
  44. package/examples/recipes/run-options.ts +131 -0
  45. package/examples/recipes/run.ts +63 -0
  46. package/examples/recipes/runtime/empty.json +1 -0
  47. package/examples/recipes/runtime/goals.json +1 -0
  48. package/examples/recipes/runtime/mise.json +6 -0
  49. package/examples/recipes/runtime/path.json +1 -0
  50. package/examples/recipes/runtime/plugins.json +4 -0
  51. package/examples/recipes/runtime/python-uv.json +10 -0
  52. package/examples/recipes/semantic-judge.ts +29 -0
  53. package/examples/templates/coding/adapter.ts +17 -0
  54. package/examples/templates/coding/case.json +15 -0
  55. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  56. package/examples/templates/repository-skill/extension-data.ts +64 -0
  57. package/examples/templates/repository-skill/extension.ts +31 -0
  58. package/examples/templates/response/adapter.ts +12 -0
  59. package/examples/templates/response/case.json +13 -0
  60. package/package.json +11 -5
  61. package/schemas/report-v1.schema.json +9 -0
  62. package/schemas/run-evidence-v1.schema.json +2 -1
  63. package/schemas/runtime-v1.schema.json +43 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +42 -11
  66. package/src/candidate-codex-state.ts +185 -0
  67. package/src/case-validation.ts +73 -50
  68. package/src/cli/cancellation.ts +20 -0
  69. package/src/cli/command.ts +87 -0
  70. package/src/cli/completion.ts +15 -0
  71. package/src/cli/demo.ts +60 -0
  72. package/src/cli/evaluation.ts +55 -0
  73. package/src/cli/extension.ts +79 -0
  74. package/src/cli/help.ts +198 -0
  75. package/src/cli/hosts.ts +55 -0
  76. package/src/cli/init-settings.ts +30 -0
  77. package/src/cli/init.ts +111 -0
  78. package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
  79. package/src/cli/loading.ts +151 -0
  80. package/src/cli/preparation.ts +154 -0
  81. package/src/cli/presentation.ts +93 -0
  82. package/src/cli/progress.ts +6 -0
  83. package/src/cli/run-manifest.ts +128 -0
  84. package/src/cli/run-options.ts +403 -0
  85. package/src/cli/run-overrides.ts +69 -0
  86. package/src/cli/settings.ts +154 -0
  87. package/src/cli/summary.ts +43 -0
  88. package/src/cli.ts +13 -591
  89. package/src/contract-types.ts +94 -0
  90. package/src/engine.ts +9 -5
  91. package/src/evaluation/advisory.ts +150 -0
  92. package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
  93. package/src/evaluation/collection.ts +88 -0
  94. package/src/evaluation/context.ts +10 -0
  95. package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
  96. package/src/evaluation/grading.ts +235 -0
  97. package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
  98. package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
  99. package/src/evaluation/progress.ts +19 -0
  100. package/src/evaluation/resources.ts +241 -0
  101. package/src/evaluation/run.ts +206 -0
  102. package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +77 -87
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
  105. package/src/evidence/host-results.ts +85 -0
  106. package/src/evidence/retained-artifacts.ts +18 -0
  107. package/src/evidence/trial-writer.ts +203 -0
  108. package/src/evidence/usage.ts +17 -0
  109. package/src/extension-client.ts +34 -96
  110. package/src/extension-session.ts +24 -48
  111. package/src/{advisory-fixture.ts → fixtures/advisory.ts} +12 -72
  112. package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
  113. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  114. package/src/fixtures/create.ts +183 -0
  115. package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
  116. package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
  117. package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
  118. package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
  119. package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
  120. package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
  121. package/src/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
  122. package/src/generated/cli-result.d.cts +2 -1
  123. package/src/generated/extension.d.cts +2 -1
  124. package/src/generated/report.cjs +1 -1
  125. package/src/generated/report.d.cts +2 -1
  126. package/src/generated/run-evidence.cjs +1 -1
  127. package/src/generated/run-evidence.d.cts +2 -1
  128. package/src/generated/runtime.cjs +1 -1
  129. package/src/generated/runtime.d.cts +2 -1
  130. package/src/git.ts +93 -0
  131. package/src/graders/git-head.ts +29 -84
  132. package/src/graders/shell.ts +50 -68
  133. package/src/host-diagnostic-redaction.ts +52 -0
  134. package/src/host-diagnostics.ts +331 -0
  135. package/src/hosts/claude-continuation.ts +57 -40
  136. package/src/hosts/claude-credential.ts +160 -63
  137. package/src/hosts/claude-events.ts +34 -18
  138. package/src/hosts/claude-keychain.ts +78 -21
  139. package/src/hosts/claude-login-recovery.ts +85 -0
  140. package/src/hosts/claude-login-refresh.ts +178 -0
  141. package/src/hosts/claude-login-store.ts +162 -0
  142. package/src/hosts/claude-login.ts +107 -0
  143. package/src/hosts/claude-native-goal.ts +15 -12
  144. package/src/hosts/claude-process.ts +10 -50
  145. package/src/hosts/claude-refresh-lock.ts +81 -0
  146. package/src/hosts/claude-repository-invocation.ts +9 -34
  147. package/src/hosts/claude-settings.ts +46 -9
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +28 -15
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +121 -81
  152. package/src/hosts/codex-app-server.ts +78 -52
  153. package/src/hosts/codex-configuration.ts +5 -10
  154. package/src/hosts/codex-exec-controls.ts +516 -0
  155. package/src/hosts/codex-marketplace.ts +12 -10
  156. package/src/hosts/codex-native-calls.ts +15 -122
  157. package/src/hosts/codex-process.ts +10 -183
  158. package/src/hosts/codex-profile.ts +36 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +134 -18
  161. package/src/hosts/codex.ts +210 -183
  162. package/src/hosts/isolation-common.ts +8 -11
  163. package/src/hosts/isolation-roots.ts +2 -2
  164. package/src/hosts/linux-sandbox.ts +15 -2
  165. package/src/hosts/mac-sandbox.ts +87 -18
  166. package/src/hosts/native-controls.ts +72 -22
  167. package/src/hosts/native-request.ts +55 -0
  168. package/src/hosts/process-lifecycle.ts +216 -48
  169. package/src/hosts/protected-worktrees.ts +1 -1
  170. package/src/hosts/skill-hook-catalog.ts +131 -0
  171. package/src/hosts/skill-hook-handler.ts +374 -0
  172. package/src/hosts/skill-hooks.ts +577 -0
  173. package/src/instrumentation.ts +7 -8
  174. package/src/native-transcript-state.ts +22 -3
  175. package/src/native-transcript-view.ts +52 -13
  176. package/src/native-transcripts.ts +26 -11
  177. package/src/paths.ts +22 -0
  178. package/src/process-lifecycle.ts +150 -0
  179. package/src/provenance.ts +27 -7
  180. package/src/prune.ts +300 -0
  181. package/src/report.ts +63 -36
  182. package/src/resolved-case.ts +1 -1
  183. package/src/results-lock.ts +42 -0
  184. package/src/results.ts +14 -18
  185. package/src/{runtime-config.ts → runtime/config.ts} +160 -46
  186. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  187. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  188. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  189. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  190. package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
  191. package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
  192. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  193. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  194. package/src/runtime/shared-cache.ts +130 -0
  195. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  196. package/src/schema-types.ts +73 -161
  197. package/src/shell.ts +4 -0
  198. package/src/transcript-retention.ts +146 -0
  199. package/src/value-guards.ts +22 -0
  200. package/docs/assets/sevro-logo.png +0 -0
  201. package/docs/assets/sevro-symbol.svg +0 -1
  202. package/docs/assets/sevro-wordmark.svg +0 -1
  203. package/docs/cache-ownership-validation.md +0 -49
  204. package/docs/claude-continuation-validation.md +0 -47
  205. package/docs/concurrency-validation.md +0 -86
  206. package/docs/configuration-root-validation.md +0 -70
  207. package/docs/engine-slice.md +0 -114
  208. package/docs/evidence/runtime-startup-30/README.md +0 -185
  209. package/docs/evidence/runtime-startup-30/after.json +0 -319
  210. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  211. package/docs/evidence/runtime-startup-30/before.json +0 -403
  212. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  213. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  214. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  216. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  217. package/docs/evidence.md +0 -17
  218. package/docs/fixture-cleanup-validation.md +0 -73
  219. package/docs/guide-verification.json +0 -239
  220. package/src/evaluation-context.ts +0 -259
  221. package/src/evaluation-fixture.ts +0 -302
  222. package/src/evaluation-run.ts +0 -200
  223. package/src/evaluation-trial.ts +0 -1016
  224. package/src/hosts/claude-nested-skills.ts +0 -272
  225. package/src/hosts/codex-command-output.ts +0 -510
  226. package/src/hosts/codex-executor-command.ts +0 -291
  227. package/src/hosts/codex-skill-reads.ts +0 -783
  228. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,11 +1,20 @@
1
1
  import { lstat, readdir, realpath, rm } from "node:fs/promises";
2
+ import { containsControl } from "./value-guards";
2
3
  import { join, relative, sep } from "node:path";
4
+ import {
5
+ allocateCodexStateLease,
6
+ candidateCodexArtifactBinding,
7
+ candidateCodexArtifactRoots,
8
+ codexStateEnabled,
9
+ releaseCandidateCodexState,
10
+ requireUnreservedArtifactName,
11
+ } from "./candidate-codex-state";
3
12
  import {
4
13
  allocateNativeState,
5
14
  nativeStatePath,
6
15
  } from "./native-transcript-state";
7
- import type { RuntimePolicy, RuntimeRole } from "./runtime-config";
8
- import { clearFixtureContents } from "./fixture-cleanup";
16
+ import type { RuntimePolicy, RuntimeRole } from "./runtime/config";
17
+ import { clearFixtureContents } from "./fixtures/cleanup";
9
18
 
10
19
  export interface RuntimeArtifactDirectory {
11
20
  name: string;
@@ -35,6 +44,7 @@ export function requireArtifactDirectories(
35
44
  function requireArtifactDirectory(directory: RuntimeArtifactDirectory): void {
36
45
  if (!/^[a-z][a-z0-9_]{0,63}$/.test(directory.name))
37
46
  throw new Error("runtime artifact directory name is invalid");
47
+ requireUnreservedArtifactName(directory.name);
38
48
  const path = directory.path;
39
49
  if (!validArtifactPath(path))
40
50
  throw new Error("runtime artifact directory path is invalid");
@@ -47,24 +57,33 @@ function requireArtifactDirectory(directory: RuntimeArtifactDirectory): void {
47
57
  function validArtifactPath(path: string): boolean {
48
58
  if (typeof path !== "string" || path.length > 4096 || path.includes("\\"))
49
59
  return false;
50
- return !Array.from(path).some(
51
- (character) =>
52
- character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f,
53
- );
60
+ return !containsControl(path);
54
61
  }
55
62
 
56
63
  /** Allocate before host execution so exceptions cannot lose cleanup ownership. */
57
64
  export async function allocateCandidateRuntime(
58
65
  policy: RuntimePolicy | undefined,
66
+ retainHome = false,
59
67
  ): Promise<string | undefined> {
60
68
  const directories = policy?.artifactDirectories ?? [];
61
- if (!directories.length) return undefined;
69
+ const codexState = codexStateEnabled(policy);
70
+ if (!candidateRuntimeRequired(directories, codexState, retainHome))
71
+ return undefined;
62
72
  requireArtifactDirectories(directories);
63
73
  const root = await allocateNativeState("candidate-runtime-");
64
74
  leases.set(root, directories);
75
+ allocateCodexStateLease(root, codexState);
65
76
  return root;
66
77
  }
67
78
 
79
+ function candidateRuntimeRequired(
80
+ directories: RuntimeArtifactDirectory[],
81
+ codexState: boolean,
82
+ retainHome: boolean,
83
+ ): boolean {
84
+ return directories.length > 0 || codexState || retainHome;
85
+ }
86
+
68
87
  export async function requireCandidateRuntime(
69
88
  root: string | undefined,
70
89
  role?: RuntimeRole,
@@ -91,6 +110,7 @@ export async function candidateArtifactRoots(
91
110
  const roots = Object.fromEntries(
92
111
  directories.map(({ name, path }) => [name, join(root, "home", path)]),
93
112
  );
113
+ Object.assign(roots, candidateCodexArtifactRoots(root));
94
114
  await requireCandidateArtifactRoots(roots);
95
115
  return roots;
96
116
  }
@@ -100,19 +120,29 @@ export async function requireCandidateArtifactRoots(
100
120
  roots: CandidateArtifactRoots | undefined,
101
121
  ): Promise<void> {
102
122
  for (const [name, path] of Object.entries(roots ?? {})) {
103
- const { root, directory } = artifactRootBinding(name, path);
104
- await requireArtifactAncestors(root, directory.path);
123
+ const binding = artifactRootBinding(name, path);
124
+ await requireArtifactAncestors(binding.root, binding.directoryPath);
125
+ if (binding.file && !(await lstat(path)).isFile())
126
+ throw new Error(
127
+ "candidate Codex configuration must remain a regular file",
128
+ );
105
129
  await requireArtifactTree(path);
106
130
  }
107
131
  }
108
132
 
109
133
  function artifactRootBinding(name: string, path: string) {
134
+ const codex = candidateCodexArtifactBinding(name, path);
135
+ if (codex) return codex;
136
+ return homeArtifactBinding(name, path);
137
+ }
138
+
139
+ function homeArtifactBinding(name: string, path: string) {
110
140
  const parts = relative(nativeStatePath(), path).split(sep);
111
141
  const root = join(nativeStatePath(), parts[0] ?? "");
112
142
  const directory = leases.get(root)?.find((item) => item.name === name);
113
143
  if (!directory || path !== join(root, "home", directory.path))
114
144
  throw new Error("candidate artifact grant is not bound to an active trial");
115
- return { root, directory };
145
+ return { root, directoryPath: join("home", directory.path), file: false };
116
146
  }
117
147
 
118
148
  async function requireArtifactAncestors(
@@ -120,7 +150,7 @@ async function requireArtifactAncestors(
120
150
  path: string,
121
151
  ): Promise<void> {
122
152
  let current = root;
123
- for (const part of ["", "home", ...path.split("/")]) {
153
+ for (const part of ["", ...path.split(sep)]) {
124
154
  current = join(current, part);
125
155
  const info = await lstat(current);
126
156
  if (!info.isDirectory() || (await realpath(current)) !== current)
@@ -169,6 +199,7 @@ export async function releaseCandidateRuntime(
169
199
  root: string | undefined,
170
200
  ): Promise<void> {
171
201
  if (root === undefined) return;
202
+ await releaseCandidateCodexState(root);
172
203
  await clearFixtureContents(root);
173
204
  await rm(root, { recursive: true, force: true });
174
205
  leases.delete(root);
@@ -0,0 +1,185 @@
1
+ import { lstat, realpath, rm } from "node:fs/promises";
2
+ import { basename, dirname, join, relative } from "node:path";
3
+ import { userInfo } from "node:os";
4
+ import { nativeStatePath } from "./native-transcript-state";
5
+ import { clearFixtureContents } from "./fixtures/cleanup";
6
+ import type { RuntimePolicy } from "./runtime/config";
7
+
8
+ interface CodexPaths {
9
+ codexHome: string;
10
+ parentHome: string;
11
+ commandHome: string;
12
+ }
13
+ interface ArtifactBinding {
14
+ root: string;
15
+ path: string;
16
+ directoryPath: string;
17
+ file?: boolean;
18
+ }
19
+ interface CodexLease {
20
+ state?: { stateRoot: string; artifacts: Record<string, ArtifactBinding> };
21
+ }
22
+ const leases = new Map<string, CodexLease>();
23
+ const ARTIFACT_NAMES = [
24
+ "codex_config",
25
+ "codex_skills",
26
+ "agent_skills",
27
+ "command_agent_skills",
28
+ "command_codex_skills",
29
+ ];
30
+
31
+ export function codexStateEnabled(policy: RuntimePolicy | undefined): boolean {
32
+ return policy?.codexState === true;
33
+ }
34
+
35
+ export function requireUnreservedArtifactName(name: string): void {
36
+ if (ARTIFACT_NAMES.includes(name))
37
+ throw new Error(
38
+ "runtime artifact directory name is reserved for Codex state",
39
+ );
40
+ }
41
+
42
+ export function allocateCodexStateLease(root: string, enabled: boolean): void {
43
+ if (enabled) leases.set(root, {});
44
+ }
45
+
46
+ /** Bind the actual host state to one trial before execution; never substitute a grader home. */
47
+ export async function retainCandidateCodexState(
48
+ runtimeRoot: string | undefined,
49
+ stateRoot: string,
50
+ paths: CodexPaths,
51
+ ): Promise<void> {
52
+ if (runtimeRoot === undefined) return;
53
+ const lease = leases.get(runtimeRoot);
54
+ if (!lease) return;
55
+ if (lease.state)
56
+ throw new Error(
57
+ "candidate Codex state is already bound to a native request",
58
+ );
59
+ requireCodexLayout(stateRoot, runtimeRoot, paths);
60
+ await requirePrivateState(stateRoot);
61
+ await requirePrivateState(paths.codexHome);
62
+ lease.state = {
63
+ stateRoot,
64
+ artifacts: codexArtifacts(stateRoot, runtimeRoot, paths),
65
+ };
66
+ }
67
+
68
+ function requireCodexLayout(
69
+ stateRoot: string,
70
+ runtimeRoot: string,
71
+ paths: CodexPaths,
72
+ ): void {
73
+ if (
74
+ dirname(stateRoot) !== nativeStatePath() ||
75
+ !basename(stateRoot).startsWith("codex-")
76
+ )
77
+ throw new Error("candidate Codex state must be a native request directory");
78
+ requireCodexHomes(stateRoot, runtimeRoot, paths);
79
+ }
80
+
81
+ function requireCodexHomes(
82
+ stateRoot: string,
83
+ runtimeRoot: string,
84
+ paths: CodexPaths,
85
+ ): void {
86
+ if (
87
+ paths.codexHome !== join(stateRoot, "codex-home") ||
88
+ paths.parentHome !== join(stateRoot, "home") ||
89
+ paths.commandHome !== join(runtimeRoot, "home")
90
+ )
91
+ throw new Error("candidate Codex homes must belong to the current trial");
92
+ }
93
+
94
+ async function requirePrivateState(root: string): Promise<void> {
95
+ const info = await lstat(root);
96
+ if (
97
+ !info.isDirectory() ||
98
+ info.uid !== userInfo().uid ||
99
+ (info.mode & 0o077) !== 0 ||
100
+ (await realpath(root)) !== root
101
+ )
102
+ throw new Error(
103
+ "candidate Codex state must be owned, private and canonical",
104
+ );
105
+ }
106
+
107
+ function binding(root: string, path: string, file = false): ArtifactBinding {
108
+ return {
109
+ root,
110
+ path,
111
+ directoryPath: relative(root, file ? dirname(path) : path),
112
+ file,
113
+ };
114
+ }
115
+
116
+ function codexArtifacts(
117
+ stateRoot: string,
118
+ runtimeRoot: string,
119
+ paths: CodexPaths,
120
+ ): Record<string, ArtifactBinding> {
121
+ return {
122
+ codex_config: binding(
123
+ stateRoot,
124
+ join(paths.codexHome, "config.toml"),
125
+ true,
126
+ ),
127
+ codex_skills: binding(stateRoot, join(paths.codexHome, "skills")),
128
+ agent_skills: binding(
129
+ stateRoot,
130
+ join(paths.parentHome, ".agents", "skills"),
131
+ ),
132
+ command_agent_skills: binding(
133
+ runtimeRoot,
134
+ join(paths.commandHome, ".agents", "skills"),
135
+ ),
136
+ command_codex_skills: binding(
137
+ runtimeRoot,
138
+ join(paths.commandHome, ".codex", "skills"),
139
+ ),
140
+ };
141
+ }
142
+
143
+ export function candidateCodexArtifactRoots(
144
+ root: string,
145
+ ): Record<string, string> {
146
+ const lease = leases.get(root);
147
+ if (!lease) return {};
148
+ if (!lease.state) throw new Error("candidate Codex state is unavailable");
149
+ return Object.fromEntries(
150
+ Object.entries(lease.state.artifacts).map(([name, item]) => [
151
+ name,
152
+ item.path,
153
+ ]),
154
+ );
155
+ }
156
+
157
+ export function candidateCodexArtifactBinding(
158
+ name: string,
159
+ path: string,
160
+ ): ArtifactBinding | undefined {
161
+ for (const lease of leases.values()) {
162
+ const artifact = lease.state?.artifacts[name];
163
+ if (artifact?.path === path) return artifact;
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ export function candidateCodexStateRetained(
169
+ runtimeRoot: string | undefined,
170
+ stateRoot: string,
171
+ ): boolean {
172
+ return (
173
+ runtimeRoot !== undefined &&
174
+ leases.get(runtimeRoot)?.state?.stateRoot === stateRoot
175
+ );
176
+ }
177
+
178
+ export async function releaseCandidateCodexState(root: string): Promise<void> {
179
+ const state = leases.get(root)?.state;
180
+ if (state) {
181
+ await clearFixtureContents(state.stateRoot);
182
+ await rm(state.stateRoot, { recursive: true, force: true });
183
+ }
184
+ leases.delete(root);
185
+ }
@@ -1,27 +1,19 @@
1
1
  import { type ResolvedCase } from "./engine";
2
- import { prepareGeneratedFixture } from "./generated-fixture";
3
- import { prepareRepositoryFixture } from "./repository-fixture";
4
- import {
5
- isOptionalNonblankString,
6
- isRecord,
7
- isStringArray,
8
- } from "./value-guards";
2
+ import { prepareGeneratedFixture } from "./fixtures/generated";
3
+ import { prepareRepositoryFixture } from "./fixtures/repository";
4
+ import { isOptionalNonblankString, isRecord } from "./value-guards";
9
5
 
10
- function validGeneratedFixture(value: Record<string, unknown>): boolean {
6
+ function fixtureDeclarationIssue(
7
+ value: Record<string, unknown>,
8
+ ): string | undefined {
11
9
  try {
12
- prepareGeneratedFixture(value);
13
- return true;
14
- } catch {
15
- return false;
16
- }
17
- }
18
-
19
- function validRepositoryFixture(value: Record<string, unknown>): boolean {
20
- try {
21
- prepareRepositoryFixture({ kind: "repository", ...value });
22
- return true;
23
- } catch {
24
- return false;
10
+ if (value.kind === "generated") prepareGeneratedFixture(value);
11
+ else prepareRepositoryFixture({ kind: "repository", ...value });
12
+ return undefined;
13
+ } catch (error) {
14
+ const detail =
15
+ error instanceof Error ? error.message : "invalid declaration";
16
+ return `fixture: ${detail}; correct the fixture declaration`;
25
17
  }
26
18
  }
27
19
 
@@ -34,49 +26,80 @@ export function validInlineFixture(value: Record<string, unknown>): boolean {
34
26
  );
35
27
  }
36
28
 
37
- function fixtureFields(value: Record<string, unknown>): boolean {
38
- if (Object.hasOwn(value, "sourceRef")) return validRepositoryFixture(value);
39
- return validInlineFixture(value);
29
+ function inlineFixtureIssue(
30
+ value: Record<string, unknown>,
31
+ ): string | undefined {
32
+ if (!Object.hasOwn(value, "files") || !isRecord(value.files))
33
+ return "fixture.files: expected an object mapping file paths to string contents";
34
+ const entry = Object.entries(value.files).find(
35
+ ([, content]) => typeof content !== "string",
36
+ );
37
+ return entry
38
+ ? `fixture.files[${JSON.stringify(entry[0])}]: expected string contents`
39
+ : undefined;
40
40
  }
41
41
 
42
- function validFixture(value: unknown): boolean {
43
- if (!isRecord(value)) return false;
44
- if (value.kind === "generated") return validGeneratedFixture(value);
45
- if (value.kind !== undefined) return false;
46
- return fixtureFields(value);
42
+ function fixtureIssue(value: unknown): string | undefined {
43
+ if (!isRecord(value)) return "fixture: expected an object";
44
+ if (value.kind === "generated") return fixtureDeclarationIssue(value);
45
+ if (value.kind !== undefined)
46
+ return "fixture.kind: omit for inline/repository fixtures, or use generated";
47
+ if (Object.hasOwn(value, "sourceRef")) return fixtureDeclarationIssue(value);
48
+ return inlineFixtureIssue(value);
47
49
  }
48
50
 
49
51
  function nonemptyString(value: unknown): value is string {
50
52
  return typeof value === "string" && value.length > 0;
51
53
  }
52
54
 
53
- function validCaseDetails(value: Record<string, unknown>): boolean {
54
- return (
55
- nonemptyString(value.id) &&
56
- nonemptyString(value.prompt) &&
57
- isOptionalNonblankString(value.followUpPrompt) &&
58
- validFixture(value.fixture)
59
- );
55
+ function caseDetailsIssue(value: Record<string, unknown>): string | undefined {
56
+ for (const field of ["id", "prompt"])
57
+ if (!nonemptyString(value[field]))
58
+ return `${field}: expected a nonempty string`;
59
+ if (!isOptionalNonblankString(value.followUpPrompt))
60
+ return "followUpPrompt: omit or supply a nonblank string";
61
+ return fixtureIssue(value.fixture);
60
62
  }
61
63
 
62
- function validCheck(value: unknown): boolean {
63
- return (
64
- isRecord(value) &&
65
- typeof value.id === "string" &&
66
- typeof value.grader === "string" &&
67
- isRecord(value.configuration)
68
- );
64
+ function checkIssue(value: unknown, location: string): string | undefined {
65
+ if (!isRecord(value)) return `${location}: expected an object`;
66
+ for (const field of ["id", "grader"])
67
+ if (typeof value[field] !== "string")
68
+ return `${location}.${field}: expected a string`;
69
+ if (!isRecord(value.configuration))
70
+ return `${location}.configuration: expected an object`;
71
+ return undefined;
69
72
  }
70
73
 
71
- function validChecks(value: unknown): boolean {
72
- return Array.isArray(value) && value.every(validCheck);
74
+ function checksIssue(value: unknown): string | undefined {
75
+ if (!Array.isArray(value))
76
+ return "checks: expected an array of check objects";
77
+ for (const [index, check] of value.entries()) {
78
+ const issue = checkIssue(check, `checks[${index}]`);
79
+ if (issue) return issue;
80
+ }
81
+ return undefined;
73
82
  }
74
83
 
75
- export function isResolvedCase(value: unknown): value is ResolvedCase {
84
+ function requiredEvidenceIssue(value: unknown): string | undefined {
85
+ if (!Array.isArray(value))
86
+ return "requiredEvidence: expected an array of strings";
87
+ const index = value.findIndex((entry: unknown) => typeof entry !== "string");
88
+ return index < 0
89
+ ? undefined
90
+ : `requiredEvidence[${index}]: expected a string`;
91
+ }
92
+
93
+ /** Explain the first invalid field using the same checks as the case type guard. */
94
+ export function resolvedCaseIssue(value: unknown): string | undefined {
95
+ if (!isRecord(value)) return "case: expected an object";
76
96
  return (
77
- isRecord(value) &&
78
- validCaseDetails(value) &&
79
- validChecks(value.checks) &&
80
- isStringArray(value.requiredEvidence)
97
+ caseDetailsIssue(value) ??
98
+ checksIssue(value.checks) ??
99
+ requiredEvidenceIssue(value.requiredEvidence)
81
100
  );
82
101
  }
102
+
103
+ export function isResolvedCase(value: unknown): value is ResolvedCase {
104
+ return resolvedCaseIssue(value) === undefined;
105
+ }
@@ -0,0 +1,20 @@
1
+ export class CommandCancellation {
2
+ private readonly controller = new AbortController();
3
+ private readonly interrupt = () => {
4
+ this.controller.abort("SIGINT");
5
+ };
6
+ private readonly terminate = () => {
7
+ this.controller.abort("SIGTERM");
8
+ };
9
+ get signal(): AbortSignal {
10
+ return this.controller.signal;
11
+ }
12
+ constructor() {
13
+ process.on("SIGINT", this.interrupt);
14
+ process.on("SIGTERM", this.terminate);
15
+ }
16
+ dispose(): void {
17
+ process.off("SIGINT", this.interrupt);
18
+ process.off("SIGTERM", this.terminate);
19
+ }
20
+ }
@@ -0,0 +1,87 @@
1
+ import { prepareRunInvocation, type PreparedInvocation } from "./preparation";
2
+ import { evaluateInvocation } from "./evaluation";
3
+ import { CommandCancellation } from "./cancellation";
4
+ import { display, displayFailure, runnerFailureCategory } from "./presentation";
5
+ import { displayEffectiveSettings } from "./settings";
6
+ import { jsonOutputRequested } from "./run-options";
7
+ import { retainCliResult } from "./completion";
8
+
9
+ type CommandRelease = (() => Promise<void>) | undefined;
10
+
11
+ export async function main(
12
+ argv: string[],
13
+ release?: () => Promise<void>,
14
+ ): Promise<void> {
15
+ const json = jsonOutputRequested(argv);
16
+ let prepared: PreparedInvocation;
17
+ try {
18
+ prepared = await prepareRunInvocation(argv);
19
+ } catch (error) {
20
+ await releaseBeforeDisplay(release, 64);
21
+ displayFailure(
22
+ error,
23
+ json,
24
+ 64,
25
+ "invalid invocation",
26
+ argv[0] === "run" ? "run" : "",
27
+ );
28
+ return;
29
+ }
30
+ const cancellation = new CommandCancellation();
31
+ try {
32
+ displayEffectiveSettings(prepared);
33
+ const { result } = await evaluateInvocation(prepared, cancellation.signal);
34
+ const completion = await retainCompletion(
35
+ result,
36
+ prepared.invocation.resultsRoot,
37
+ );
38
+ await releaseBeforeDisplay(release, result.exitCode);
39
+ display(result, prepared.invocation.json, completion);
40
+ process.exitCode = result.exitCode;
41
+ } catch (error) {
42
+ await releaseBeforeDisplay(release, 70);
43
+ displayFailure(
44
+ error,
45
+ prepared.invocation.json,
46
+ runnerFailureCategory(error),
47
+ "runner failure",
48
+ );
49
+ } finally {
50
+ cancellation.dispose();
51
+ }
52
+ }
53
+
54
+ async function retainCompletion(
55
+ result: Parameters<typeof retainCliResult>[0],
56
+ resultsRoot: string,
57
+ ) {
58
+ try {
59
+ return await retainCliResult(result, resultsRoot);
60
+ } catch (cause) {
61
+ if (result.exitCode === 0)
62
+ throw new Error(
63
+ "CLI result/latest could not be retained; check --results-root permissions",
64
+ { cause },
65
+ );
66
+ process.stderr.write(
67
+ "sevro: CLI result/latest could not be retained; original run outcome preserved; inspect evidence directly\n",
68
+ );
69
+ return undefined;
70
+ }
71
+ }
72
+
73
+ async function releaseBeforeDisplay(
74
+ release: CommandRelease,
75
+ exitCode: number,
76
+ ): Promise<void> {
77
+ try {
78
+ await release?.();
79
+ } catch (error) {
80
+ if (exitCode === 0) throw error;
81
+ const detail =
82
+ error instanceof Error
83
+ ? error.message
84
+ : "temporary project could not be removed";
85
+ process.stderr.write(`sevro: demo cleanup failed: ${detail}\n`);
86
+ }
87
+ }
@@ -0,0 +1,15 @@
1
+ import { dirname, join } from "node:path";
2
+ import { atomicWriteJson } from "../storage";
3
+ import type { CliResult } from "../engine";
4
+ import { createReport } from "../report";
5
+ import { withResultsLock } from "../results-lock";
6
+
7
+ /** Latest means the last successfully retained CLI result, including dry/failing runs. */
8
+ export async function retainCliResult(result: CliResult, resultsRoot: string) {
9
+ return withResultsLock(resultsRoot, async () => {
10
+ const resultFile = join(dirname(result.evidencePath), "result.json");
11
+ await atomicWriteJson(resultFile, result);
12
+ await atomicWriteJson(join(resultsRoot, "latest.json"), result);
13
+ return { resultFile, report: await createReport([resultFile]) };
14
+ });
15
+ }
@@ -0,0 +1,60 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { main } from "./command";
5
+ import { InvocationError } from "./invocation";
6
+ import { displayFailure } from "./presentation";
7
+
8
+ /** Run the packaged deterministic case without inheriting a caller's project policy. */
9
+ export async function demoCommand(argv: string[]): Promise<void> {
10
+ const json = argv.includes("--json");
11
+ if (argv.some((arg) => arg !== "--json")) {
12
+ displayFailure(
13
+ new InvocationError("demo accepts only --json; run sevro demo --help"),
14
+ json,
15
+ 64,
16
+ "invalid demo invocation",
17
+ "demo",
18
+ );
19
+ return;
20
+ }
21
+ let projectRoot: string;
22
+ try {
23
+ projectRoot = await mkdtemp(join(tmpdir(), "sevro-demo-"));
24
+ } catch (cause) {
25
+ displayFailure(
26
+ new Error(
27
+ `demo temporary project could not be created under ${tmpdir()}; set TMPDIR to a writable directory`,
28
+ { cause },
29
+ ),
30
+ json,
31
+ 70,
32
+ "demo preparation failed",
33
+ );
34
+ return;
35
+ }
36
+ await main(
37
+ [
38
+ "run",
39
+ ...(json ? ["--json"] : []),
40
+ "--case-file",
41
+ join(import.meta.dir, "..", "..", "examples", "basic", "graded.json"),
42
+ "--adapter-module",
43
+ join(import.meta.dir, "..", "..", "examples", "basic", "host.ts"),
44
+ "--project-root",
45
+ projectRoot,
46
+ "--results-root",
47
+ join(process.cwd(), ".sevro", "results"),
48
+ ],
49
+ demoCleanup(projectRoot),
50
+ );
51
+ }
52
+
53
+ function demoCleanup(projectRoot: string): () => Promise<void> {
54
+ let attempted = false;
55
+ return async () => {
56
+ if (attempted) return;
57
+ attempted = true;
58
+ await rm(projectRoot, { recursive: true, force: true });
59
+ };
60
+ }