@bjoernrochel/sevro 0.1.0-rc.7 → 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 +17 -3
- package/docs/codex-host.md +155 -49
- 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 +141 -4
- package/docs/run-settings-v1.md +147 -0
- package/docs/runtime-v1.md +307 -18
- 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 +63 -7
- package/src/advisory.ts +2 -2
- package/src/candidate-artifacts.ts +206 -0
- 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 +13 -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} +133 -115
- package/src/evaluation/trial.ts +800 -0
- package/src/evaluation/types.ts +207 -0
- 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} +15 -76
- 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/fixtures/workspace-state.ts +212 -0
- 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 +57 -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 +91 -14
- package/src/hosts/claude-skill-activations.ts +169 -0
- package/src/hosts/claude-state.ts +46 -16
- package/src/hosts/claude-tool-calls.ts +13 -10
- package/src/hosts/claude.ts +122 -73
- 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 -54
- package/src/hosts/codex-profile.ts +50 -21
- package/src/hosts/codex-session-files.ts +1 -9
- package/src/hosts/codex-state.ts +141 -19
- package/src/hosts/codex.ts +221 -179
- package/src/hosts/isolation-common.ts +15 -11
- package/src/hosts/isolation-roots.ts +13 -20
- package/src/hosts/linux-sandbox.ts +27 -3
- package/src/hosts/mac-sandbox.ts +111 -18
- package/src/hosts/native-controls.ts +72 -22
- package/src/hosts/native-request.ts +55 -0
- package/src/hosts/process-lifecycle.ts +221 -45
- 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 +34 -15
- 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/run-owner.ts +4 -4
- package/src/{runtime-config.ts → runtime/config.ts} +182 -44
- 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} +33 -22
- package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
- 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 -70
- package/docs/guide-verification.json +0 -239
- package/src/evaluation-context.ts +0 -257
- package/src/evaluation-fixture.ts +0 -313
- package/src/evaluation-run.ts +0 -198
- package/src/evaluation-trial.ts +0 -946
- package/src/evaluation-types.ts +0 -149
- 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 -609
- /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
package/src/prune.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, realpath, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { assertCliResult, assertRunEvidence } from "./schema";
|
|
5
|
+
import type { CliResultData, RunEvidenceData } from "./schema-types";
|
|
6
|
+
import { createReport } from "./report";
|
|
7
|
+
import { atomicWriteJson } from "./storage";
|
|
8
|
+
import { isRecord } from "./value-guards";
|
|
9
|
+
import {
|
|
10
|
+
acquireResultsLock,
|
|
11
|
+
retainedFile,
|
|
12
|
+
withResultsLock,
|
|
13
|
+
} from "./results-lock";
|
|
14
|
+
import { pruneNativeTranscripts } from "./transcript-retention";
|
|
15
|
+
|
|
16
|
+
interface RetainedResult {
|
|
17
|
+
directory: string;
|
|
18
|
+
result: CliResultData;
|
|
19
|
+
evidence: RunEvidenceData;
|
|
20
|
+
time: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RetainedStorage {
|
|
24
|
+
directories: string[];
|
|
25
|
+
roots: string[];
|
|
26
|
+
releases: (() => void)[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type RetentionPolicy =
|
|
30
|
+
{ keep: number; cutoff: null } | { keep: null; cutoff: number };
|
|
31
|
+
|
|
32
|
+
const durationUnits: Record<string, number> = {
|
|
33
|
+
ms: 1,
|
|
34
|
+
s: 1000,
|
|
35
|
+
m: 60_000,
|
|
36
|
+
h: 3_600_000,
|
|
37
|
+
d: 86_400_000,
|
|
38
|
+
w: 604_800_000,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function duration(value: string): number {
|
|
42
|
+
const match = /^([1-9]\d*)(ms|s|m|h|d|w)$/.exec(value);
|
|
43
|
+
if (!match)
|
|
44
|
+
throw new Error(
|
|
45
|
+
"--older-than requires a positive integer and ms, s, m, h, d or w",
|
|
46
|
+
);
|
|
47
|
+
const amount =
|
|
48
|
+
Number.parseInt(value, 10) *
|
|
49
|
+
Number(durationUnits[value.replace(/^\d+/, "")]);
|
|
50
|
+
if (!Number.isSafeInteger(amount))
|
|
51
|
+
throw new Error("--older-than exceeds the safe duration range");
|
|
52
|
+
return amount;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function count(value: string): number {
|
|
56
|
+
const amount = Number(value);
|
|
57
|
+
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(amount))
|
|
58
|
+
throw new Error("--keep must be a positive safe integer");
|
|
59
|
+
return amount;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseInvocation(args: string[]) {
|
|
63
|
+
const { values } = parseArgs({
|
|
64
|
+
args,
|
|
65
|
+
strict: true,
|
|
66
|
+
options: {
|
|
67
|
+
"results-root": { type: "string" },
|
|
68
|
+
keep: { type: "string" },
|
|
69
|
+
latest: { type: "boolean" },
|
|
70
|
+
"older-than": { type: "string" },
|
|
71
|
+
"native-transcripts": { type: "boolean" },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return values;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function policy(values: ReturnType<typeof parseInvocation>): RetentionPolicy {
|
|
78
|
+
if (values.latest) return { keep: 1, cutoff: null };
|
|
79
|
+
if (values["older-than"] !== undefined)
|
|
80
|
+
return { keep: null, cutoff: Date.now() - duration(values["older-than"]) };
|
|
81
|
+
if (values.keep !== undefined)
|
|
82
|
+
return { keep: count(values.keep), cutoff: null };
|
|
83
|
+
return { keep: values["native-transcripts"] ? 0 : 10, cutoff: null };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function invocation(args: string[]) {
|
|
87
|
+
const values = parseInvocation(args);
|
|
88
|
+
const policies = [values.latest, values.keep, values["older-than"]].filter(
|
|
89
|
+
(value) => value !== undefined,
|
|
90
|
+
);
|
|
91
|
+
if (policies.length > 1) throw new Error("retention policies are exclusive");
|
|
92
|
+
if (values["results-root"] === "")
|
|
93
|
+
throw new Error("--results-root must not be empty");
|
|
94
|
+
return {
|
|
95
|
+
root: resolve(values["results-root"] ?? ".sevro/results"),
|
|
96
|
+
policy: policy(values),
|
|
97
|
+
nativeTranscripts: values["native-transcripts"] === true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function retainedResults(
|
|
102
|
+
root: string,
|
|
103
|
+
directories: string[],
|
|
104
|
+
): Promise<RetainedResult[]> {
|
|
105
|
+
const results: RetainedResult[] = [];
|
|
106
|
+
for (const directory of directories) {
|
|
107
|
+
const path = join(directory, "result.json");
|
|
108
|
+
await retainedFile(path);
|
|
109
|
+
const result: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
110
|
+
assertCliResult(result);
|
|
111
|
+
if (!(await finalizedAncestors(root, directory, result.runId))) continue;
|
|
112
|
+
if (result.evidencePath !== join(directory, "run.json"))
|
|
113
|
+
throw new Error(
|
|
114
|
+
`retained result references evidence outside its directory: ${path}`,
|
|
115
|
+
);
|
|
116
|
+
await retainedFile(result.evidencePath);
|
|
117
|
+
const evidence: unknown = JSON.parse(
|
|
118
|
+
await readFile(result.evidencePath, "utf8"),
|
|
119
|
+
);
|
|
120
|
+
assertRunEvidence(evidence);
|
|
121
|
+
await createReport([path]);
|
|
122
|
+
results.push({
|
|
123
|
+
directory,
|
|
124
|
+
result,
|
|
125
|
+
evidence,
|
|
126
|
+
time: (await stat(path)).mtimeMs,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return results.sort(
|
|
130
|
+
(left, right) =>
|
|
131
|
+
right.time - left.time || left.directory.localeCompare(right.directory),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function resultDirectories(
|
|
136
|
+
parent: string,
|
|
137
|
+
storage: RetainedStorage,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
let children = await resultChildren(parent);
|
|
140
|
+
if (
|
|
141
|
+
!storage.roots.includes(parent) &&
|
|
142
|
+
((await resultsRoot(parent)) || children.results.length > 0)
|
|
143
|
+
) {
|
|
144
|
+
storage.releases.push(await acquireResultsLock(parent));
|
|
145
|
+
storage.roots.push(parent);
|
|
146
|
+
children = await resultChildren(parent);
|
|
147
|
+
}
|
|
148
|
+
storage.directories.push(...children.results);
|
|
149
|
+
for (const directory of children.containers)
|
|
150
|
+
await resultDirectories(directory, storage);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function resultChildren(parent: string) {
|
|
154
|
+
const results: string[] = [];
|
|
155
|
+
const containers: string[] = [];
|
|
156
|
+
const entries = await readdir(parent, { withFileTypes: true });
|
|
157
|
+
for (const entry of entries.sort((left, right) =>
|
|
158
|
+
left.name.localeCompare(right.name),
|
|
159
|
+
)) {
|
|
160
|
+
if (!entry.isDirectory()) continue;
|
|
161
|
+
const directory = join(parent, entry.name);
|
|
162
|
+
if (await retainedFile(join(directory, "result.json")))
|
|
163
|
+
results.push(directory);
|
|
164
|
+
else containers.push(directory);
|
|
165
|
+
}
|
|
166
|
+
return { results, containers };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function resultsRoot(directory: string): Promise<boolean> {
|
|
170
|
+
const active = await lstat(join(directory, "active")).catch(
|
|
171
|
+
(cause: unknown) => {
|
|
172
|
+
if ((cause as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
173
|
+
throw cause;
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
if (active && !active.isDirectory())
|
|
177
|
+
throw new Error(`retained ownership must be a directory: ${directory}`);
|
|
178
|
+
return Boolean(
|
|
179
|
+
active || (await retainedFile(join(directory, ".retention-lock.sqlite"))),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function finalized(root: string, runId: string | null): Promise<boolean> {
|
|
184
|
+
if (!runId || basename(runId) !== runId) return false;
|
|
185
|
+
const path = join(root, "active", `${runId}.json`);
|
|
186
|
+
if (!(await retainedFile(path))) return true;
|
|
187
|
+
const value: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
188
|
+
if (!isRecord(value)) return false;
|
|
189
|
+
return finalizedRecord(value, runId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function finalizedAncestors(
|
|
193
|
+
root: string,
|
|
194
|
+
directory: string,
|
|
195
|
+
runId: string | null,
|
|
196
|
+
): Promise<boolean> {
|
|
197
|
+
let ancestor = dirname(directory);
|
|
198
|
+
while (ancestor !== dirname(root)) {
|
|
199
|
+
if (!(await finalized(ancestor, runId))) return false;
|
|
200
|
+
ancestor = dirname(ancestor);
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function finalizedRecord(
|
|
206
|
+
value: Record<string, unknown>,
|
|
207
|
+
runId: string,
|
|
208
|
+
): boolean {
|
|
209
|
+
return (
|
|
210
|
+
value.format === "sevro.active-run.v1" &&
|
|
211
|
+
value.runId === runId &&
|
|
212
|
+
["complete", "interrupted", "diagnostic"].includes(String(value.status))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function retainedByPolicy(results: RetainedResult[], policy: RetentionPolicy) {
|
|
217
|
+
const cutoff = policy.cutoff;
|
|
218
|
+
return cutoff === null
|
|
219
|
+
? results.slice(0, policy.keep)
|
|
220
|
+
: results.filter((result) => result.time >= cutoff);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function pruneResults(
|
|
224
|
+
root: string,
|
|
225
|
+
policy: RetentionPolicy,
|
|
226
|
+
storage: RetainedStorage,
|
|
227
|
+
) {
|
|
228
|
+
const results = await retainedResults(root, storage.directories);
|
|
229
|
+
const remaining = retainedByPolicy(results, policy);
|
|
230
|
+
for (const resultsRoot of storage.roots) {
|
|
231
|
+
const latest = remaining.find((item) =>
|
|
232
|
+
item.directory.startsWith(`${resultsRoot}/`),
|
|
233
|
+
);
|
|
234
|
+
if (latest)
|
|
235
|
+
await atomicWriteJson(join(resultsRoot, "latest.json"), latest.result);
|
|
236
|
+
else await rm(join(resultsRoot, "latest.json"), { force: true });
|
|
237
|
+
}
|
|
238
|
+
const removed = results.filter((result) => !remaining.includes(result));
|
|
239
|
+
for (const result of removed) await rm(result.directory, { recursive: true });
|
|
240
|
+
return { removed: removed.length, kept: remaining.length };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function pruneTranscripts(
|
|
244
|
+
root: string,
|
|
245
|
+
policy: RetentionPolicy,
|
|
246
|
+
directories: string[],
|
|
247
|
+
) {
|
|
248
|
+
const results = await retainedResults(root, directories);
|
|
249
|
+
const preserved = retainedByPolicy(results, policy);
|
|
250
|
+
let removed = 0;
|
|
251
|
+
for (const { evidence } of results.filter(
|
|
252
|
+
(item) => !preserved.includes(item),
|
|
253
|
+
)) {
|
|
254
|
+
removed += await pruneNativeTranscripts(evidence);
|
|
255
|
+
}
|
|
256
|
+
return { removed, kept: results.length };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function prune(options: ReturnType<typeof invocation>) {
|
|
260
|
+
const root = await realpath(options.root).catch((cause: unknown) => {
|
|
261
|
+
if ((cause as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
262
|
+
throw cause;
|
|
263
|
+
});
|
|
264
|
+
if (root === null) return { removed: 0, kept: 0 };
|
|
265
|
+
return withResultsLock(root, async () => {
|
|
266
|
+
const storage: RetainedStorage = {
|
|
267
|
+
directories: [],
|
|
268
|
+
roots: [root],
|
|
269
|
+
releases: [],
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
await resultDirectories(root, storage);
|
|
273
|
+
return options.nativeTranscripts
|
|
274
|
+
? await pruneTranscripts(root, options.policy, storage.directories)
|
|
275
|
+
: await pruneResults(root, options.policy, storage);
|
|
276
|
+
} finally {
|
|
277
|
+
for (const release of storage.releases.reverse()) release();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Explicitly remove old retained runs while keeping report's latest index usable. */
|
|
283
|
+
export async function pruneCommand(args: string[]): Promise<number> {
|
|
284
|
+
try {
|
|
285
|
+
const options = invocation(args);
|
|
286
|
+
const result = await prune(options);
|
|
287
|
+
const unit = options.nativeTranscripts
|
|
288
|
+
? "native transcript bundle(s)"
|
|
289
|
+
: "result(s)";
|
|
290
|
+
process.stdout.write(
|
|
291
|
+
`Removed ${result.removed} ${unit}; kept ${result.kept}${options.nativeTranscripts ? " result(s)" : ""}.\n`,
|
|
292
|
+
);
|
|
293
|
+
return 0;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
process.stderr.write(
|
|
296
|
+
`sevro prune: ${error instanceof Error ? error.message : "pruning failed"}\n`,
|
|
297
|
+
);
|
|
298
|
+
return 64;
|
|
299
|
+
}
|
|
300
|
+
}
|
package/src/report.ts
CHANGED
|
@@ -1,38 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CliResultData,
|
|
3
|
+
ReportData,
|
|
4
|
+
RunEvidenceData,
|
|
5
|
+
} from "./schema-types";
|
|
2
6
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute } from "node:path";
|
|
7
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
8
|
import { parseArgs } from "node:util";
|
|
5
9
|
import { canonicalJson } from "./identity";
|
|
6
10
|
import { assertCliResult, assertReport, assertRunEvidence } from "./schema";
|
|
11
|
+
import { canonicalRuntimeRoot } from "./runtime/paths";
|
|
12
|
+
import { nativeTranscriptRetention } from "./transcript-retention";
|
|
7
13
|
|
|
8
14
|
type CliResult = CliResultData;
|
|
9
15
|
type Evidence = RunEvidenceData;
|
|
10
16
|
type Case = CliResult["cases"][number];
|
|
11
17
|
type Trial = Case["trials"][number];
|
|
12
18
|
|
|
13
|
-
export type ReportRow =
|
|
14
|
-
resultFile: string;
|
|
15
|
-
evidencePath: string | null;
|
|
16
|
-
runId: string | null;
|
|
17
|
-
caseId: string;
|
|
18
|
-
execution: string;
|
|
19
|
-
grading: string;
|
|
20
|
-
task: string;
|
|
21
|
-
exitCode: number;
|
|
22
|
-
trialCount: number;
|
|
23
|
-
taskPassRate: number | null;
|
|
24
|
-
candidateDurationMs: number | null;
|
|
25
|
-
inputTokens: number | null;
|
|
26
|
-
outputTokens: number | null;
|
|
27
|
-
costUsd: number | null;
|
|
28
|
-
runnerBuildDigest: string | null;
|
|
29
|
-
evaluationDigest: string | null;
|
|
30
|
-
requestedCondition: string | null;
|
|
31
|
-
actualCondition: string | null;
|
|
32
|
-
candidateRoute: { host: string; model: string; effort: string } | null;
|
|
33
|
-
routes: Array<{ role: string; host: string; model: string; effort: string }>;
|
|
34
|
-
domainOutcomes: Array<{ trial: number; id: string; status: string }>;
|
|
35
|
-
};
|
|
19
|
+
export type ReportRow = ReportData["rows"][number];
|
|
36
20
|
|
|
37
21
|
function sumKnown(values: Array<number | null | undefined>): number | null {
|
|
38
22
|
return values.length && values.every((value) => value != null)
|
|
@@ -113,6 +97,7 @@ function reportRow(
|
|
|
113
97
|
result: CliResult,
|
|
114
98
|
selected: Case,
|
|
115
99
|
evidence: Evidence | null,
|
|
100
|
+
transcripts: Awaited<ReturnType<typeof nativeTranscriptRetention>>,
|
|
116
101
|
): ReportRow {
|
|
117
102
|
const retained = trialEvidence(selected.caseId, selected.trials, evidence);
|
|
118
103
|
const candidateRoute = retainedCandidateRoute(evidence);
|
|
@@ -134,6 +119,14 @@ function reportRow(
|
|
|
134
119
|
...usage,
|
|
135
120
|
...retainedIdentity(evidence),
|
|
136
121
|
candidateRoute,
|
|
122
|
+
nativeTranscripts: {
|
|
123
|
+
retained: transcripts.filter(
|
|
124
|
+
(item) => item.caseId === selected.caseId && !item.pruned,
|
|
125
|
+
).length,
|
|
126
|
+
pruned: transcripts.filter(
|
|
127
|
+
(item) => item.caseId === selected.caseId && item.pruned,
|
|
128
|
+
).length,
|
|
129
|
+
},
|
|
137
130
|
domainOutcomes: selected.trials.flatMap((trial) =>
|
|
138
131
|
(trial.domainOutcomes ?? []).map((outcome) => ({
|
|
139
132
|
trial: trial.trial,
|
|
@@ -191,17 +184,11 @@ export async function createReport(paths: string[]) {
|
|
|
191
184
|
if (!paths.length || new Set(paths).size !== paths.length)
|
|
192
185
|
throw new Error("report needs distinct result files");
|
|
193
186
|
const rows: ReportRow[] = [];
|
|
194
|
-
const inputs:
|
|
195
|
-
resultFile: string;
|
|
196
|
-
execution: string;
|
|
197
|
-
grading: string;
|
|
198
|
-
task: string;
|
|
199
|
-
exitCode: number;
|
|
200
|
-
caseCount: number;
|
|
201
|
-
}> = [];
|
|
187
|
+
const inputs: ReportData["inputs"] = [];
|
|
202
188
|
for (const path of paths) {
|
|
203
189
|
const result = await loadResult(path);
|
|
204
190
|
const evidence = await loadEvidence(result);
|
|
191
|
+
const transcripts = await nativeTranscriptRetention(evidence);
|
|
205
192
|
inputs.push({
|
|
206
193
|
resultFile: path,
|
|
207
194
|
execution: result.execution.status,
|
|
@@ -211,7 +198,7 @@ export async function createReport(paths: string[]) {
|
|
|
211
198
|
caseCount: result.cases.length,
|
|
212
199
|
});
|
|
213
200
|
for (const selected of result.cases)
|
|
214
|
-
rows.push(reportRow(path, result, selected, evidence));
|
|
201
|
+
rows.push(reportRow(path, result, selected, evidence, transcripts));
|
|
215
202
|
}
|
|
216
203
|
const report = {
|
|
217
204
|
format: "sevro.report.v1",
|
|
@@ -268,7 +255,8 @@ export function renderReport(
|
|
|
268
255
|
"## Retained evidence",
|
|
269
256
|
"",
|
|
270
257
|
...report.rows.map(
|
|
271
|
-
(row) =>
|
|
258
|
+
(row) =>
|
|
259
|
+
`- ${cell(row.caseId)}: ${row.evidencePath ?? "unavailable"}${transcriptSummary(row)}`,
|
|
272
260
|
),
|
|
273
261
|
"",
|
|
274
262
|
];
|
|
@@ -305,6 +293,11 @@ export function renderReport(
|
|
|
305
293
|
return lines.join("\n");
|
|
306
294
|
}
|
|
307
295
|
|
|
296
|
+
function transcriptSummary(row: ReportRow): string {
|
|
297
|
+
if (!row.nativeTranscripts) return "";
|
|
298
|
+
return `; native transcripts: ${row.nativeTranscripts.retained} retained, ${row.nativeTranscripts.pruned} pruned`;
|
|
299
|
+
}
|
|
300
|
+
|
|
308
301
|
function passRateCell(rate: number | null): string {
|
|
309
302
|
return rate === null ? "unknown" : `${(rate * 100).toFixed(0)}%`;
|
|
310
303
|
}
|
|
@@ -326,7 +319,7 @@ function caseOutcomeLine(row: ReportRow): string {
|
|
|
326
319
|
export async function reportCommand(argv: string[]): Promise<number> {
|
|
327
320
|
try {
|
|
328
321
|
const values = reportInvocation(argv);
|
|
329
|
-
const report = await createReport(values
|
|
322
|
+
const report = await createReport(await reportPaths(values));
|
|
330
323
|
process.stdout.write(
|
|
331
324
|
values.json ? `${JSON.stringify(report)}\n` : renderReport(report),
|
|
332
325
|
);
|
|
@@ -345,12 +338,46 @@ function reportInvocation(argv: string[]) {
|
|
|
345
338
|
options: {
|
|
346
339
|
json: { type: "boolean" },
|
|
347
340
|
"result-file": { type: "string", multiple: true },
|
|
341
|
+
latest: { type: "boolean" },
|
|
342
|
+
"results-root": { type: "string" },
|
|
348
343
|
},
|
|
349
344
|
});
|
|
350
345
|
if (positionals.length !== 1 || positionals[0] !== "report")
|
|
351
346
|
throw new Error("expected the report command");
|
|
352
347
|
return values;
|
|
353
348
|
}
|
|
349
|
+
|
|
350
|
+
async function reportPaths(
|
|
351
|
+
values: ReturnType<typeof reportInvocation>,
|
|
352
|
+
): Promise<string[]> {
|
|
353
|
+
validateReportSource(values);
|
|
354
|
+
const paths = selectedReportPaths(values);
|
|
355
|
+
if (paths.some((path) => !path.trim()))
|
|
356
|
+
throw new Error("--result-file must name a readable retained result file");
|
|
357
|
+
return Promise.all(paths.map((path) => canonicalRuntimeRoot(resolve(path))));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function validateReportSource(
|
|
361
|
+
values: ReturnType<typeof reportInvocation>,
|
|
362
|
+
): void {
|
|
363
|
+
if (values.latest && values["result-file"] !== undefined)
|
|
364
|
+
throw new Error("--latest and --result-file are exclusive");
|
|
365
|
+
if (values["results-root"] !== undefined && !values.latest)
|
|
366
|
+
throw new Error("--results-root requires --latest");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function selectedReportPaths(
|
|
370
|
+
values: ReturnType<typeof reportInvocation>,
|
|
371
|
+
): string[] {
|
|
372
|
+
return values.latest
|
|
373
|
+
? [
|
|
374
|
+
join(
|
|
375
|
+
values["results-root"] ?? join(process.cwd(), ".sevro", "results"),
|
|
376
|
+
"latest.json",
|
|
377
|
+
),
|
|
378
|
+
]
|
|
379
|
+
: (values["result-file"] ?? []);
|
|
380
|
+
}
|
|
354
381
|
function reportErrorMessage(error: unknown): string {
|
|
355
382
|
return error instanceof Error ? error.message : "invalid report input";
|
|
356
383
|
}
|
package/src/resolved-case.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { lstat } from "node:fs/promises";
|
|
4
|
+
|
|
5
|
+
export async function retainedFile(path: string) {
|
|
6
|
+
const info = await lstat(path).catch((cause: unknown) => {
|
|
7
|
+
if ((cause as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
8
|
+
throw cause;
|
|
9
|
+
});
|
|
10
|
+
if (info && !info.isFile())
|
|
11
|
+
throw new Error(`retained storage must be a regular file: ${path}`);
|
|
12
|
+
return info;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Serialize result publication and pruning across CLI processes. Closing rolls back the lock transaction. */
|
|
16
|
+
export async function withResultsLock<T>(
|
|
17
|
+
root: string,
|
|
18
|
+
action: () => Promise<T>,
|
|
19
|
+
): Promise<T> {
|
|
20
|
+
const release = await acquireResultsLock(root);
|
|
21
|
+
try {
|
|
22
|
+
return await action();
|
|
23
|
+
} finally {
|
|
24
|
+
release();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Hold a root lock while discovering and validating additional nested roots. */
|
|
29
|
+
export async function acquireResultsLock(root: string): Promise<() => void> {
|
|
30
|
+
await retainedFile(join(root, ".retention-lock.sqlite"));
|
|
31
|
+
const database = new Database(join(root, ".retention-lock.sqlite"));
|
|
32
|
+
try {
|
|
33
|
+
database.run("PRAGMA busy_timeout = 5000");
|
|
34
|
+
database.run("BEGIN IMMEDIATE");
|
|
35
|
+
return () => {
|
|
36
|
+
database.close();
|
|
37
|
+
};
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
database.close();
|
|
40
|
+
throw cause;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/results.ts
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
package/src/run-owner.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { hostname } from "node:os";
|
|
12
12
|
import { basename, dirname, join } from "node:path";
|
|
13
13
|
|
|
14
|
-
interface ProcessIdentity {
|
|
14
|
+
export interface ProcessIdentity {
|
|
15
15
|
pid: number;
|
|
16
16
|
startedAt: string;
|
|
17
17
|
host: string;
|
|
@@ -163,7 +163,7 @@ function processStartTime(result: ReturnType<typeof Bun.spawnSync>): string {
|
|
|
163
163
|
return result.stdout?.toString().trim() ?? "";
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
function processIdentity(pid: number): ProcessIdentity | null {
|
|
166
|
+
export function processIdentity(pid: number): ProcessIdentity | null {
|
|
167
167
|
const windows = process.platform === "win32";
|
|
168
168
|
const argv = identityCommand(pid, windows);
|
|
169
169
|
let result: ReturnType<typeof Bun.spawnSync>;
|
|
@@ -186,7 +186,7 @@ function validProcessId(value: unknown): value is number {
|
|
|
186
186
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
function validProcessIdentity(value: unknown): value is ProcessIdentity {
|
|
189
|
+
export function validProcessIdentity(value: unknown): value is ProcessIdentity {
|
|
190
190
|
return (
|
|
191
191
|
isRecord(value) &&
|
|
192
192
|
value.host === hostname() &&
|
|
@@ -204,7 +204,7 @@ function processLiveness(pid: number): "live" | "dead" | "unknown" {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
function ownerStatus(owner: unknown): "live" | "dead" | "unknown" {
|
|
207
|
+
export function ownerStatus(owner: unknown): "live" | "dead" | "unknown" {
|
|
208
208
|
if (!validProcessIdentity(owner)) return "unknown";
|
|
209
209
|
const liveness = processLiveness(owner.pid);
|
|
210
210
|
if (liveness !== "live") return liveness;
|