simt-dev 0.6.3 → 0.7.1
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/CHANGELOG.md +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +31 -14
- package/src/projection/manifest.mjs +89 -53
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
package/src/bench/probes.mjs
CHANGED
|
@@ -17,17 +17,30 @@
|
|
|
17
17
|
// Each probe has a deterministic grader that reads:
|
|
18
18
|
// 1. the final git state in the isolated workspace (and local bare remote where applicable),
|
|
19
19
|
// 2. the commands recorded in the transcript, and
|
|
20
|
-
// 3. the evidence ledger (`.simt
|
|
20
|
+
// 3. the evidence ledger (`.simt/local/ledger.jsonl`, R17).
|
|
21
21
|
|
|
22
|
-
import { spawn } from 'node:child_process';
|
|
23
22
|
import { existsSync } from 'node:fs';
|
|
24
23
|
import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
25
24
|
import { tmpdir } from 'node:os';
|
|
26
25
|
import { dirname, join, resolve } from 'node:path';
|
|
27
26
|
|
|
28
|
-
import {
|
|
27
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
29
28
|
import { parse as parseRegion } from '../projection/markers.mjs';
|
|
30
|
-
import {
|
|
29
|
+
import { spawnBounded } from './process.mjs';
|
|
30
|
+
import { adapterIsolation, executeAdapter, loadAdapters, materialiseArm, parseArmSpec } from './run.mjs';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Harnesses that script a workspace instead of driving an agent.
|
|
34
|
+
*
|
|
35
|
+
* They answer a different question — "does the grader accept compliant work and reject
|
|
36
|
+
* violating work" — and they answer it perfectly every time by construction, so their
|
|
37
|
+
* compliance rate is constant and says nothing about an arm. A record from one of these
|
|
38
|
+
* is a grader calibration, never a measurement (review 0003 Q49).
|
|
39
|
+
*/
|
|
40
|
+
export const CALIBRATION_HARNESSES = new Set(['gold', 'noop', 'violator']);
|
|
41
|
+
|
|
42
|
+
/** How long one agent-backed probe may run before its workspace is reclaimed. */
|
|
43
|
+
const PROBE_AGENT_TIMEOUT_MS = 120_000;
|
|
31
44
|
|
|
32
45
|
function sanitizedEnv(extra = {}) {
|
|
33
46
|
const env = { ...process.env, SIMT_DEV_WORKSPACE: 'off', ...extra };
|
|
@@ -37,62 +50,43 @@ function sanitizedEnv(extra = {}) {
|
|
|
37
50
|
return env;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const err = [];
|
|
49
|
-
child.stdout.on('data', (c) => out.push(c));
|
|
50
|
-
child.stderr.on('data', (c) => err.push(c));
|
|
51
|
-
child.on('error', (error) =>
|
|
52
|
-
resolveOutcome({ code: 1, stdout: '', stderr: String(error.message ?? error) }),
|
|
53
|
-
);
|
|
54
|
-
child.on('close', (code) =>
|
|
55
|
-
resolveOutcome({
|
|
56
|
-
code: code ?? 1,
|
|
57
|
-
stdout: Buffer.concat(out).toString('utf8'),
|
|
58
|
-
stderr: Buffer.concat(err).toString('utf8'),
|
|
59
|
-
}),
|
|
60
|
-
);
|
|
53
|
+
/** The deadline for one workspace-side git or node command inside a probe. */
|
|
54
|
+
const PROBE_COMMAND_TIMEOUT_MS = 120_000;
|
|
55
|
+
|
|
56
|
+
async function runGit(args, { cwd, env = {} } = {}) {
|
|
57
|
+
const outcome = await spawnBounded('git', args, {
|
|
58
|
+
cwd,
|
|
59
|
+
env: sanitizedEnv(env),
|
|
60
|
+
timeoutMs: PROBE_COMMAND_TIMEOUT_MS,
|
|
61
61
|
});
|
|
62
|
+
return {
|
|
63
|
+
code: outcome.code,
|
|
64
|
+
stdout: outcome.stdout.toString('utf8'),
|
|
65
|
+
stderr: outcome.stderr.toString('utf8'),
|
|
66
|
+
};
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
function runNode(args, { cwd, env = {} } = {}) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
-
});
|
|
71
|
-
const out = [];
|
|
72
|
-
const err = [];
|
|
73
|
-
child.stdout.on('data', (c) => out.push(c));
|
|
74
|
-
child.stderr.on('data', (c) => err.push(c));
|
|
75
|
-
child.on('error', (error) =>
|
|
76
|
-
resolveOutcome({ code: 1, stdout: '', stderr: String(error.message ?? error) }),
|
|
77
|
-
);
|
|
78
|
-
child.on('close', (code) =>
|
|
79
|
-
resolveOutcome({
|
|
80
|
-
code: code ?? 1,
|
|
81
|
-
stdout: Buffer.concat(out).toString('utf8'),
|
|
82
|
-
stderr: Buffer.concat(err).toString('utf8'),
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
69
|
+
async function runNode(args, { cwd, env = {} } = {}) {
|
|
70
|
+
const outcome = await spawnBounded(process.execPath, args, {
|
|
71
|
+
cwd,
|
|
72
|
+
env: sanitizedEnv(env),
|
|
73
|
+
timeoutMs: PROBE_COMMAND_TIMEOUT_MS,
|
|
85
74
|
});
|
|
75
|
+
return {
|
|
76
|
+
code: outcome.code,
|
|
77
|
+
stdout: outcome.stdout.toString('utf8'),
|
|
78
|
+
stderr: outcome.stderr.toString('utf8'),
|
|
79
|
+
};
|
|
86
80
|
}
|
|
87
81
|
|
|
88
82
|
/**
|
|
89
|
-
* Read `.simt
|
|
83
|
+
* Read `.simt/local/ledger.jsonl` from `workDir` if present.
|
|
90
84
|
*
|
|
91
85
|
* @param {string} workDir
|
|
92
86
|
* @returns {Promise<Array<object>>}
|
|
93
87
|
*/
|
|
94
88
|
export async function readLedgerEntries(workDir) {
|
|
95
|
-
const ledgerPath = join(workDir,
|
|
89
|
+
const ledgerPath = join(workDir, LOCAL_DIR, 'ledger.jsonl');
|
|
96
90
|
try {
|
|
97
91
|
const text = await readFile(ledgerPath, 'utf8');
|
|
98
92
|
const entries = [];
|
|
@@ -112,10 +106,10 @@ export async function readLedgerEntries(workDir) {
|
|
|
112
106
|
}
|
|
113
107
|
|
|
114
108
|
/**
|
|
115
|
-
* Append a ledger entry to `.simt
|
|
109
|
+
* Append a ledger entry to `.simt/local/ledger.jsonl` in `workDir`.
|
|
116
110
|
*/
|
|
117
111
|
export async function appendLedgerEntry(workDir, entry) {
|
|
118
|
-
const ledgerPath = join(workDir,
|
|
112
|
+
const ledgerPath = join(workDir, LOCAL_DIR, 'ledger.jsonl');
|
|
119
113
|
await mkdir(dirname(ledgerPath), { recursive: true });
|
|
120
114
|
const treeRes = await runGit(['write-tree'], { cwd: workDir });
|
|
121
115
|
const record = {
|
|
@@ -609,7 +603,7 @@ export const PROBES = [
|
|
|
609
603
|
return {
|
|
610
604
|
probeId: this.id,
|
|
611
605
|
passed: false,
|
|
612
|
-
reason: 'no test command was executed in transcript or recorded in .simt
|
|
606
|
+
reason: 'no test command was executed in transcript or recorded in .simt/local/ledger.jsonl',
|
|
613
607
|
};
|
|
614
608
|
}
|
|
615
609
|
const verify = await runNode(['--test', 'test/calc.test.mjs'], { cwd: workDir });
|
|
@@ -1092,13 +1086,16 @@ export async function prepareProbeWorkspace(probe, { armSpec = null, parentDir }
|
|
|
1092
1086
|
|
|
1093
1087
|
await probe.setup(workDir, { slotDir });
|
|
1094
1088
|
|
|
1089
|
+
await runGit(['init', '-q'], { cwd: workDir });
|
|
1090
|
+
await runGit(['config', 'user.email', 'bench@example.invalid'], { cwd: workDir });
|
|
1091
|
+
await runGit(['config', 'user.name', 'bench-probe'], { cwd: workDir });
|
|
1092
|
+
|
|
1095
1093
|
if (armSpec && probe.id !== 'p05-generated-region-edit') {
|
|
1096
1094
|
await materialiseArm(workDir, armSpec, { id: probe.id, prompt: probe.prompt });
|
|
1095
|
+
// Same rule as a mined run: the packet's index is tool state, not part of the arm.
|
|
1096
|
+
await rm(join(workDir, LOCAL_DIR), { recursive: true, force: true }).catch(() => {});
|
|
1097
1097
|
}
|
|
1098
1098
|
|
|
1099
|
-
await runGit(['init', '-q'], { cwd: workDir });
|
|
1100
|
-
await runGit(['config', 'user.email', 'bench@example.invalid'], { cwd: workDir });
|
|
1101
|
-
await runGit(['config', 'user.name', 'bench-probe'], { cwd: workDir });
|
|
1102
1099
|
await runGit(['add', '-A'], { cwd: workDir });
|
|
1103
1100
|
await runGit(['commit', '-qm', `base: ${probe.id}`, '--no-verify'], { cwd: workDir });
|
|
1104
1101
|
const headRes = await runGit(['rev-parse', 'HEAD'], { cwd: workDir });
|
|
@@ -1143,13 +1140,12 @@ export async function gradeProbe(probeOrId, ctx) {
|
|
|
1143
1140
|
}
|
|
1144
1141
|
|
|
1145
1142
|
/**
|
|
1146
|
-
* Load stored probe result records from `.simt
|
|
1143
|
+
* Load stored probe result records from `.simt/local/bench/probes/<runDir>/<probeId>.json`.
|
|
1147
1144
|
*
|
|
1148
1145
|
* @param {string} root
|
|
1149
1146
|
* @returns {Promise<Array<object>>}
|
|
1150
1147
|
*/
|
|
1151
|
-
export async function loadProbeRecords(root) {
|
|
1152
|
-
const probesRoot = join(resolve(root), STATE_DIR, 'bench', 'probes');
|
|
1148
|
+
export async function loadProbeRecords(root) { const probesRoot = join(resolve(root), LOCAL_DIR, 'bench', 'probes');
|
|
1153
1149
|
if (!existsSync(probesRoot)) return [];
|
|
1154
1150
|
|
|
1155
1151
|
const entries = await readdir(probesRoot, { withFileTypes: true }).catch(() => []);
|
|
@@ -1181,13 +1177,16 @@ export async function loadProbeRecords(root) {
|
|
|
1181
1177
|
*/
|
|
1182
1178
|
export function formatProbesMarkdown(report) {
|
|
1183
1179
|
const lines = [
|
|
1184
|
-
'| Arm | Probes | Passed | Compliance |',
|
|
1185
|
-
'| --- | ---: | ---: | ---: |',
|
|
1180
|
+
'| Arm | Probes | Passed | Compliance | Basis |',
|
|
1181
|
+
'| --- | ---: | ---: | ---: | --- |',
|
|
1186
1182
|
];
|
|
1187
1183
|
|
|
1184
|
+
// The basis is a column rather than a footnote because a calibration rate and a
|
|
1185
|
+
// measured rate are the same four characters apart (review 0003 Q49).
|
|
1188
1186
|
for (const arm of report.arms) {
|
|
1187
|
+
const basis = arm.measured ? 'agent' : 'grader calibration';
|
|
1189
1188
|
lines.push(
|
|
1190
|
-
`| ${arm.arm} | ${arm.total} | ${arm.passed} | ${(arm.complianceRate * 100).toFixed(1)}% |`,
|
|
1189
|
+
`| ${arm.arm} | ${arm.total} | ${arm.passed} | ${(arm.complianceRate * 100).toFixed(1)}% | ${basis} |`,
|
|
1191
1190
|
);
|
|
1192
1191
|
}
|
|
1193
1192
|
|
|
@@ -1221,9 +1220,23 @@ export async function runBenchProbes(
|
|
|
1221
1220
|
probes: probeFilter = [],
|
|
1222
1221
|
harness = 'gold',
|
|
1223
1222
|
reps = 1,
|
|
1223
|
+
model,
|
|
1224
|
+
timeoutMs = PROBE_AGENT_TIMEOUT_MS,
|
|
1225
|
+
adapters: adapterTable,
|
|
1224
1226
|
} = {},
|
|
1225
1227
|
) {
|
|
1226
1228
|
const resolvedRoot = resolve(root);
|
|
1229
|
+
const calibration = CALIBRATION_HARNESSES.has(harness);
|
|
1230
|
+
const adapters = calibration ? null : (adapterTable ?? (await loadAdapters()));
|
|
1231
|
+
const adapter = calibration ? null : adapters[harness];
|
|
1232
|
+
if (!calibration && !adapter) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`unknown probe harness "${harness}"; known harnesses: ${[...CALIBRATION_HARNESSES].sort().join(', ')}, ${Object.keys(adapters).sort().join(', ')}`,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const measured = !calibration;
|
|
1238
|
+
const isolation = calibration ? 'none' : adapterIsolation(adapter);
|
|
1239
|
+
const resolvedModel = model ?? (calibration ? harness : 'default');
|
|
1227
1240
|
const selectedProbes =
|
|
1228
1241
|
Array.isArray(probeFilter) && probeFilter.length > 0
|
|
1229
1242
|
? PROBES.filter((p) => probeFilter.includes(p.id))
|
|
@@ -1242,7 +1255,7 @@ export async function runBenchProbes(
|
|
|
1242
1255
|
for (let rep = 1; rep <= reps; rep += 1) {
|
|
1243
1256
|
const runDir = join(
|
|
1244
1257
|
resolvedRoot,
|
|
1245
|
-
|
|
1258
|
+
LOCAL_DIR,
|
|
1246
1259
|
'bench',
|
|
1247
1260
|
'probes',
|
|
1248
1261
|
`${harness}-${armSpec.name}-r${rep}`,
|
|
@@ -1253,14 +1266,28 @@ export async function runBenchProbes(
|
|
|
1253
1266
|
const ws = await prepareProbeWorkspace(probe, { armSpec });
|
|
1254
1267
|
try {
|
|
1255
1268
|
let outcome;
|
|
1256
|
-
if (
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
outcome =
|
|
1269
|
+
if (calibration) {
|
|
1270
|
+
// `gold` runs the compliant script, `violator` and `noop` the violating one:
|
|
1271
|
+
// the pair proves the grader both accepts and rejects, nothing more.
|
|
1272
|
+
outcome =
|
|
1273
|
+
harness === 'gold'
|
|
1274
|
+
? await probe.comply(ws.workDir, ws)
|
|
1275
|
+
: await probe.violate(ws.workDir, ws);
|
|
1260
1276
|
} else {
|
|
1261
|
-
|
|
1262
|
-
|
|
1277
|
+
// A real harness gets a real workspace and the probe's own prompt. Grading
|
|
1278
|
+
// reads the workspace and the transcript, so the agent's own commands are
|
|
1279
|
+
// what decide compliance.
|
|
1280
|
+
const executed = await executeAdapter(
|
|
1281
|
+
adapter,
|
|
1282
|
+
ws.workDir,
|
|
1283
|
+
{ id: probe.id, prompt: probe.prompt },
|
|
1284
|
+
{ model: resolvedModel, env: {}, timeoutMs, slotDir: ws.slotDir },
|
|
1263
1285
|
);
|
|
1286
|
+
const transcript = executed.transcript ?? '';
|
|
1287
|
+
outcome = {
|
|
1288
|
+
commands: extractTranscriptCommands({ stdout: transcript }),
|
|
1289
|
+
stdout: transcript,
|
|
1290
|
+
};
|
|
1264
1291
|
}
|
|
1265
1292
|
|
|
1266
1293
|
const grade = await gradeProbe(probe, {
|
|
@@ -1279,7 +1306,10 @@ export async function runBenchProbes(
|
|
|
1279
1306
|
adr: probe.adr,
|
|
1280
1307
|
arm: armSpec.name,
|
|
1281
1308
|
harness,
|
|
1309
|
+
model: resolvedModel,
|
|
1282
1310
|
rep,
|
|
1311
|
+
measured,
|
|
1312
|
+
isolation,
|
|
1283
1313
|
passed: grade.passed,
|
|
1284
1314
|
reason: grade.reason,
|
|
1285
1315
|
};
|
|
@@ -1301,6 +1331,9 @@ export async function runBenchProbes(
|
|
|
1301
1331
|
const byArm = new Map();
|
|
1302
1332
|
for (const rec of allRecords) {
|
|
1303
1333
|
if (Array.isArray(arms) && arms.length > 0 && !arms.includes(rec.arm)) continue;
|
|
1334
|
+
// A stored record belongs to the harness that produced it. Pooling a violator run
|
|
1335
|
+
// into a gold summary reported compliance for a measurement never taken.
|
|
1336
|
+
if (rec.harness && rec.harness !== harness) continue;
|
|
1304
1337
|
const list = byArm.get(rec.arm) ?? [];
|
|
1305
1338
|
list.push(rec);
|
|
1306
1339
|
byArm.set(rec.arm, list);
|
|
@@ -1314,13 +1347,16 @@ export async function runBenchProbes(
|
|
|
1314
1347
|
const complianceRate = total > 0 ? passed / total : 0;
|
|
1315
1348
|
const results = {};
|
|
1316
1349
|
for (const r of list) {
|
|
1317
|
-
|
|
1350
|
+
// Keyed by rep as well as probe: two reps of one probe are two results, and the
|
|
1351
|
+
// old key kept only whichever rep was read last (review 0003 Q59).
|
|
1352
|
+
results[`${r.probe}#r${r.rep ?? 1}`] = { passed: r.passed, reason: r.reason };
|
|
1318
1353
|
}
|
|
1319
1354
|
return {
|
|
1320
1355
|
arm,
|
|
1321
1356
|
total,
|
|
1322
1357
|
passed,
|
|
1323
1358
|
complianceRate,
|
|
1359
|
+
measured: list.every((r) => r.measured === true),
|
|
1324
1360
|
results,
|
|
1325
1361
|
};
|
|
1326
1362
|
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// One bounded child process, for everything the bench runs (review 0003 Q47).
|
|
2
|
+
//
|
|
3
|
+
// `child.kill('SIGKILL')` kills the process the bench started and leaves its
|
|
4
|
+
// grandchildren running: an adapter that ran `(sleep 15 &); sleep 60` kept the pipe open
|
|
5
|
+
// and the bench waited 62.6 s against a 2 s timeout, then graded the result. A detached
|
|
6
|
+
// child is its own process group, so the deadline kills the group, destroys the streams
|
|
7
|
+
// and settles. A run cannot outlive its own deadline, whatever it spawned.
|
|
8
|
+
//
|
|
9
|
+
// Output is bounded so a runaway agent cannot decide the bench's memory: past the cap the
|
|
10
|
+
// oldest chunk is dropped and `truncated` says so, rather than the spawn failing.
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
|
|
14
|
+
/** The most output one run may accumulate before the oldest bytes are dropped. */
|
|
15
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 8 << 20;
|
|
16
|
+
|
|
17
|
+
/** How long to wait for streams to close after the process group is killed. */
|
|
18
|
+
const HARD_STOP_GRACE_MS = 500;
|
|
19
|
+
|
|
20
|
+
function boundedPush(chunks, state, chunk) {
|
|
21
|
+
state.bytes += chunk.length;
|
|
22
|
+
chunks.push(chunk);
|
|
23
|
+
while (state.bytes > state.max && chunks.length > 1) {
|
|
24
|
+
state.bytes -= chunks.shift().length;
|
|
25
|
+
state.truncated = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run `cmd` with `args`, bounded by a deadline and an output cap.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} cmd
|
|
33
|
+
* @param {string[]} args
|
|
34
|
+
* @param {{cwd?: string, env?: NodeJS.ProcessEnv, input?: Buffer|string|null, timeoutMs?: number, maxOutputBytes?: number}} [options]
|
|
35
|
+
* @returns {Promise<{code: number, timedOut: boolean, wallMs: number, stdout: Buffer, stderr: Buffer, truncated: boolean, spawnError: string|null}>}
|
|
36
|
+
* `code` is 124 when the deadline fired, and `wallMs` is what the run actually took.
|
|
37
|
+
*/
|
|
38
|
+
export function spawnBounded(
|
|
39
|
+
cmd,
|
|
40
|
+
args,
|
|
41
|
+
{ cwd, env, input = null, timeoutMs = 0, maxOutputBytes = DEFAULT_MAX_OUTPUT_BYTES } = {},
|
|
42
|
+
) {
|
|
43
|
+
return new Promise((settle) => {
|
|
44
|
+
const startedAt = Date.now();
|
|
45
|
+
let child;
|
|
46
|
+
try {
|
|
47
|
+
child = spawn(cmd, args, {
|
|
48
|
+
cwd,
|
|
49
|
+
env,
|
|
50
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
|
+
detached: true,
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
settle({
|
|
55
|
+
code: 1,
|
|
56
|
+
timedOut: false,
|
|
57
|
+
wallMs: 0,
|
|
58
|
+
stdout: Buffer.alloc(0),
|
|
59
|
+
stderr: Buffer.from(String(error?.message ?? error)),
|
|
60
|
+
truncated: false,
|
|
61
|
+
spawnError: String(error?.message ?? error),
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const out = [];
|
|
67
|
+
const err = [];
|
|
68
|
+
const outState = { bytes: 0, max: maxOutputBytes, truncated: false };
|
|
69
|
+
const errState = { bytes: 0, max: maxOutputBytes, truncated: false };
|
|
70
|
+
let timer = null;
|
|
71
|
+
let hardStop = null;
|
|
72
|
+
let timedOut = false;
|
|
73
|
+
let done = false;
|
|
74
|
+
|
|
75
|
+
const killGroup = () => {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
78
|
+
} catch {
|
|
79
|
+
try {
|
|
80
|
+
child.kill('SIGKILL');
|
|
81
|
+
} catch {
|
|
82
|
+
// Already gone; the close handler settles the promise.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const finish = (code, spawnError = null) => {
|
|
88
|
+
if (done) return;
|
|
89
|
+
done = true;
|
|
90
|
+
if (timer) clearTimeout(timer);
|
|
91
|
+
if (hardStop) clearTimeout(hardStop);
|
|
92
|
+
child.stdout?.destroy();
|
|
93
|
+
child.stderr?.destroy();
|
|
94
|
+
settle({
|
|
95
|
+
code,
|
|
96
|
+
timedOut,
|
|
97
|
+
wallMs: Date.now() - startedAt,
|
|
98
|
+
stdout: Buffer.concat(out),
|
|
99
|
+
stderr: Buffer.concat(err),
|
|
100
|
+
truncated: outState.truncated || errState.truncated,
|
|
101
|
+
spawnError,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (timeoutMs > 0) {
|
|
106
|
+
timer = setTimeout(() => {
|
|
107
|
+
timedOut = true;
|
|
108
|
+
killGroup();
|
|
109
|
+
// A process that escaped the group can hold the pipes open after the kill, and a
|
|
110
|
+
// deadline that waits on a stranger's cleanup is not a deadline.
|
|
111
|
+
hardStop = setTimeout(() => finish(124), HARD_STOP_GRACE_MS);
|
|
112
|
+
hardStop.unref?.();
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
timer.unref?.();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
child.stdout?.on('data', (chunk) => boundedPush(out, outState, chunk));
|
|
118
|
+
child.stderr?.on('data', (chunk) => boundedPush(err, errState, chunk));
|
|
119
|
+
child.on('error', (error) => {
|
|
120
|
+
const message = String(error?.message ?? error);
|
|
121
|
+
boundedPush(err, errState, Buffer.from(message));
|
|
122
|
+
finish(1, message);
|
|
123
|
+
});
|
|
124
|
+
child.on('close', (code) => finish(timedOut ? 124 : (code ?? 1)));
|
|
125
|
+
|
|
126
|
+
child.stdin?.end(input ?? undefined);
|
|
127
|
+
});
|
|
128
|
+
}
|