@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.
- package/CONTRIBUTING.md +22 -6
- package/README.md +5 -1
- package/docs/README.md +19 -15
- package/docs/architecture.md +138 -17
- package/docs/assets/README.md +5 -4
- package/docs/builtin-graders-v1.md +9 -2
- package/docs/codex-host.md +155 -57
- package/docs/contracts.md +2 -1
- package/docs/creating-cases.md +9 -2
- package/docs/development-cli.md +187 -19
- package/docs/documentation-quality.md +12 -2
- package/docs/evaluation-workflow.md +110 -0
- package/docs/extension-protocol-v1.md +26 -3
- package/docs/getting-started.md +23 -2
- package/docs/guide-evaluation.md +33 -1
- package/docs/identity-v1.md +25 -2
- package/docs/installing.md +11 -11
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +196 -7
- package/docs/reading-results.md +102 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +31 -12
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +122 -4
- package/docs/run-settings-v1.md +147 -0
- package/docs/runtime-v1.md +255 -22
- package/docs/troubleshooting.md +12 -12
- package/docs/typescript-quality.md +89 -26
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +11 -5
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +43 -7
- package/src/advisory.ts +2 -2
- package/src/candidate-artifacts.ts +42 -11
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +73 -50
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +87 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +60 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +198 -0
- package/src/cli/hosts.ts +55 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +154 -0
- package/src/cli/presentation.ts +93 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +128 -0
- package/src/cli/run-options.ts +403 -0
- package/src/cli/run-overrides.ts +69 -0
- package/src/cli/settings.ts +154 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -591
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +9 -5
- package/src/evaluation/advisory.ts +150 -0
- package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
- package/src/evaluation/grading.ts +235 -0
- package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
- package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +77 -87
- package/src/evaluation/trial.ts +800 -0
- package/src/{evaluation-types.ts → evaluation/types.ts} +79 -29
- package/src/evidence/host-results.ts +85 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +34 -96
- package/src/extension-session.ts +24 -48
- package/src/{advisory-fixture.ts → fixtures/advisory.ts} +12 -72
- package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +183 -0
- package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
- package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
- package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
- package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
- package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
- package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
- package/src/{workspace-state.ts → fixtures/workspace-state.ts} +5 -5
- package/src/generated/cli-result.d.cts +2 -1
- package/src/generated/extension.d.cts +2 -1
- package/src/generated/report.cjs +1 -1
- package/src/generated/report.d.cts +2 -1
- package/src/generated/run-evidence.cjs +1 -1
- package/src/generated/run-evidence.d.cts +2 -1
- package/src/generated/runtime.cjs +1 -1
- package/src/generated/runtime.d.cts +2 -1
- package/src/git.ts +93 -0
- package/src/graders/git-head.ts +29 -84
- package/src/graders/shell.ts +50 -68
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +331 -0
- package/src/hosts/claude-continuation.ts +57 -40
- package/src/hosts/claude-credential.ts +160 -63
- package/src/hosts/claude-events.ts +34 -18
- package/src/hosts/claude-keychain.ts +78 -21
- package/src/hosts/claude-login-recovery.ts +85 -0
- package/src/hosts/claude-login-refresh.ts +178 -0
- package/src/hosts/claude-login-store.ts +162 -0
- package/src/hosts/claude-login.ts +107 -0
- package/src/hosts/claude-native-goal.ts +15 -12
- package/src/hosts/claude-process.ts +10 -50
- package/src/hosts/claude-refresh-lock.ts +81 -0
- package/src/hosts/claude-repository-invocation.ts +9 -34
- package/src/hosts/claude-settings.ts +46 -9
- package/src/hosts/claude-skill-activations.ts +169 -0
- package/src/hosts/claude-state.ts +28 -15
- package/src/hosts/claude-tool-calls.ts +13 -10
- package/src/hosts/claude.ts +121 -81
- package/src/hosts/codex-app-server.ts +78 -52
- package/src/hosts/codex-configuration.ts +5 -10
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-marketplace.ts +12 -10
- package/src/hosts/codex-native-calls.ts +15 -122
- package/src/hosts/codex-process.ts +10 -183
- package/src/hosts/codex-profile.ts +36 -21
- package/src/hosts/codex-session-files.ts +1 -9
- package/src/hosts/codex-state.ts +134 -18
- package/src/hosts/codex.ts +210 -183
- package/src/hosts/isolation-common.ts +8 -11
- package/src/hosts/isolation-roots.ts +2 -2
- package/src/hosts/linux-sandbox.ts +15 -2
- package/src/hosts/mac-sandbox.ts +87 -18
- package/src/hosts/native-controls.ts +72 -22
- package/src/hosts/native-request.ts +55 -0
- package/src/hosts/process-lifecycle.ts +216 -48
- package/src/hosts/protected-worktrees.ts +1 -1
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +577 -0
- package/src/instrumentation.ts +7 -8
- package/src/native-transcript-state.ts +22 -3
- package/src/native-transcript-view.ts +52 -13
- package/src/native-transcripts.ts +26 -11
- package/src/paths.ts +22 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +27 -7
- package/src/prune.ts +300 -0
- package/src/report.ts +63 -36
- package/src/resolved-case.ts +1 -1
- package/src/results-lock.ts +42 -0
- package/src/results.ts +14 -18
- package/src/{runtime-config.ts → runtime/config.ts} +160 -46
- package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
- package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
- package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
- package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
- package/src/{runtime-hooks.ts → runtime/hooks.ts} +32 -22
- package/src/{runtime-paths.ts → runtime/paths.ts} +5 -19
- package/src/{runtime-query.ts → runtime/query.ts} +5 -5
- package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
- package/src/runtime/shared-cache.ts +130 -0
- package/src/{runtime-state.ts → runtime/state.ts} +28 -11
- package/src/schema-types.ts +73 -161
- package/src/shell.ts +4 -0
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +22 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +0 -1
- package/docs/assets/sevro-wordmark.svg +0 -1
- package/docs/cache-ownership-validation.md +0 -49
- package/docs/claude-continuation-validation.md +0 -47
- package/docs/concurrency-validation.md +0 -86
- package/docs/configuration-root-validation.md +0 -70
- package/docs/engine-slice.md +0 -114
- package/docs/evidence/runtime-startup-30/README.md +0 -185
- package/docs/evidence/runtime-startup-30/after.json +0 -319
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
- package/docs/evidence/runtime-startup-30/before.json +0 -403
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
- package/docs/evidence.md +0 -17
- package/docs/fixture-cleanup-validation.md +0 -73
- package/docs/guide-verification.json +0 -239
- package/src/evaluation-context.ts +0 -259
- package/src/evaluation-fixture.ts +0 -302
- package/src/evaluation-run.ts +0 -200
- package/src/evaluation-trial.ts +0 -1016
- package/src/hosts/claude-nested-skills.ts +0 -272
- package/src/hosts/codex-command-output.ts +0 -510
- package/src/hosts/codex-executor-command.ts +0 -291
- package/src/hosts/codex-skill-reads.ts +0 -783
- /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
|
|
8
|
-
import { clearFixtureContents } from "./
|
|
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 !
|
|
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
|
-
|
|
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
|
|
104
|
-
await requireArtifactAncestors(root,
|
|
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 ["",
|
|
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
|
+
}
|
package/src/case-validation.ts
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
import { type ResolvedCase } from "./engine";
|
|
2
|
-
import { prepareGeneratedFixture } from "./generated
|
|
3
|
-
import { prepareRepositoryFixture } from "./repository
|
|
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
|
|
6
|
+
function fixtureDeclarationIssue(
|
|
7
|
+
value: Record<string, unknown>,
|
|
8
|
+
): string | undefined {
|
|
11
9
|
try {
|
|
12
|
-
prepareGeneratedFixture(value);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
if (!isRecord(value)) return
|
|
44
|
-
if (value.kind === "generated") return
|
|
45
|
-
if (value.kind !== undefined)
|
|
46
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
nonemptyString(value
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
typeof value
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
package/src/cli/demo.ts
ADDED
|
@@ -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
|
+
}
|