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/run.mjs
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { existsSync } from 'node:fs';
|
|
3
2
|
import { lstat, mkdir, mkdtemp, readdir, readFile, rm, stat, unlink, writeFile } from 'node:fs/promises';
|
|
4
3
|
import { tmpdir } from 'node:os';
|
|
5
4
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
6
5
|
import { fileURLToPath } from 'node:url';
|
|
7
6
|
|
|
7
|
+
import { assembleBrief } from '../brief/assemble.mjs';
|
|
8
|
+
import { DEFAULT_BRIEF_BYTES } from '../brief/packet.mjs';
|
|
8
9
|
import { loadRegistry } from '../graph/load.mjs';
|
|
9
10
|
import { extractOpenedSkills, matchSkillsForPaths } from '../graph/compile.mjs';
|
|
10
|
-
import { matchGlob } from '../index/govern.mjs';
|
|
11
|
-
import { formatLessonParagraph, listPromotedLessons } from '../memory/lessons.mjs';
|
|
12
11
|
import { MANIFEST_FILE, STATE_DIR } from '../name.mjs';
|
|
12
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
13
13
|
import { readManifest } from '../projection/manifest.mjs';
|
|
14
14
|
import { plan } from '../projection/project.mjs';
|
|
15
15
|
import { parse as parseRegion } from '../projection/markers.mjs';
|
|
16
|
-
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
17
|
-
import { rankSkills, tokenize } from '../resolve/resolve.mjs';
|
|
18
16
|
import { gradeWorkspace } from './grade.mjs';
|
|
19
17
|
import { resolveVaultDir } from './mine.mjs';
|
|
18
|
+
import { spawnBounded } from './process.mjs';
|
|
20
19
|
import { loadTasks } from './tasks.mjs';
|
|
21
20
|
import { ablateSectionInWorkspace, parseAblationArmName } from './ablate.mjs';
|
|
22
21
|
import { applyVariantInWorkspace, parseVariantArmName } from './variants.mjs';
|
|
@@ -53,48 +52,80 @@ function sanitizedEnv(extra = {}) {
|
|
|
53
52
|
return env;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
timedOut,
|
|
81
|
-
stdout: Buffer.concat(out),
|
|
82
|
-
stderr: Buffer.from(String(error.message ?? error)),
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
child.on('close', (code) => {
|
|
86
|
-
if (timer) clearTimeout(timer);
|
|
87
|
-
resolveOutcome({
|
|
88
|
-
code: timedOut ? 124 : (code ?? 1),
|
|
89
|
-
timedOut,
|
|
90
|
-
stdout: Buffer.concat(out),
|
|
91
|
-
stderr: Buffer.concat(err),
|
|
92
|
-
});
|
|
93
|
-
});
|
|
55
|
+
/**
|
|
56
|
+
* What every benchmarked agent is allowed to inherit from the operator's shell.
|
|
57
|
+
*
|
|
58
|
+
* `PATH` and the locale are what a CLI needs to run at all. Nothing here names a
|
|
59
|
+
* credential or a state directory: those come from the adapter's own `env` list.
|
|
60
|
+
*/
|
|
61
|
+
const ENV_BASELINE = [
|
|
62
|
+
'PATH',
|
|
63
|
+
'LANG',
|
|
64
|
+
'LC_ALL',
|
|
65
|
+
'LC_CTYPE',
|
|
66
|
+
'TZ',
|
|
67
|
+
'TERM',
|
|
68
|
+
'SHELL',
|
|
69
|
+
'TMPDIR',
|
|
70
|
+
'TEMP',
|
|
71
|
+
'TMP',
|
|
72
|
+
'SystemRoot',
|
|
73
|
+
'WINDIR',
|
|
74
|
+
'USER',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/** The isolation levels a run may record; `sandbox` is declared, not implemented. */
|
|
78
|
+
const ISOLATION_LEVELS = new Set(['none', 'env', 'sandbox']);
|
|
94
79
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The environment an arm's agent sees: the baseline, the adapter's declared keys,
|
|
82
|
+
* and nothing else.
|
|
83
|
+
*
|
|
84
|
+
* The agent used to inherit everything the operator had — `SIMT_DEV_VAULT_DIR`,
|
|
85
|
+
* `XDG_CACHE_HOME`, `HOME`, and every credential in the shell. A run that can read
|
|
86
|
+
* the vault can read the hidden tests it is graded against, and a run sharing the
|
|
87
|
+
* operator's HOME writes into their own configuration, so the number it produced
|
|
88
|
+
* described the machine rather than the arm (review 0003 Q49). `home` is the run's
|
|
89
|
+
* own slot, which is removed when the run ends.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} adapter an entry from `adapters.json`
|
|
92
|
+
* @param {{home?: string, extra?: Record<string, string>, base?: NodeJS.ProcessEnv}} [options]
|
|
93
|
+
* @returns {Record<string, string>}
|
|
94
|
+
*/
|
|
95
|
+
export function agentEnv(adapter, { home, extra = {}, base = process.env } = {}) {
|
|
96
|
+
const allowed = new Set([...ENV_BASELINE, ...(adapter?.env ?? [])]);
|
|
97
|
+
const env = {};
|
|
98
|
+
for (const key of allowed) {
|
|
99
|
+
if (typeof base[key] === 'string') env[key] = base[key];
|
|
100
|
+
}
|
|
101
|
+
for (const [key, value] of Object.entries(extra ?? {})) {
|
|
102
|
+
if (typeof value === 'string') env[key] = value;
|
|
103
|
+
}
|
|
104
|
+
if (typeof home === 'string' && home.length > 0) env.HOME = home;
|
|
105
|
+
env.SIMT_DEV_WORKSPACE = 'off';
|
|
106
|
+
for (const key of Object.keys(env)) {
|
|
107
|
+
if (key.startsWith('NODE_TEST')) delete env[key];
|
|
108
|
+
}
|
|
109
|
+
return env;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The isolation an adapter's runs provide, as the record states it.
|
|
114
|
+
*
|
|
115
|
+
* `none` is a built-in that scripts the workspace, `env` is a CLI driven with a
|
|
116
|
+
* scrubbed environment and a slot-local HOME, and `sandbox` is reserved for a
|
|
117
|
+
* real filesystem sandbox — nothing claims it yet, so a record never says it.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} adapter
|
|
120
|
+
* @returns {'none'|'env'|'sandbox'}
|
|
121
|
+
*/
|
|
122
|
+
export function adapterIsolation(adapter) {
|
|
123
|
+
const declared = adapter?.isolation;
|
|
124
|
+
return ISOLATION_LEVELS.has(declared) ? declared : 'none';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runCmd(cmd, args, { cwd, env, input = null, timeoutMs = DEFAULT_AGENT_TIMEOUT_MS } = {}) {
|
|
128
|
+
return spawnBounded(cmd, args, { cwd, env: sanitizedEnv(env), input, timeoutMs });
|
|
98
129
|
}
|
|
99
130
|
|
|
100
131
|
function getByPath(obj, pathStr) {
|
|
@@ -119,7 +150,7 @@ function firstNumeric(obj, paths = []) {
|
|
|
119
150
|
function emptyUsage() {
|
|
120
151
|
return {
|
|
121
152
|
usage: { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, turns: 0, toolCalls: 0 },
|
|
122
|
-
costUsd:
|
|
153
|
+
costUsd: null,
|
|
123
154
|
};
|
|
124
155
|
}
|
|
125
156
|
|
|
@@ -163,7 +194,7 @@ function parseJsonObject(text) {
|
|
|
163
194
|
* @param {string|object} adapterOrId
|
|
164
195
|
* @param {string} stdoutText
|
|
165
196
|
* @param {string} [analyticsText]
|
|
166
|
-
* @returns {{usage: {input: number, cacheWrite: number, cacheRead: number, output: number, turns: number, toolCalls: number}, costUsd: number}}
|
|
197
|
+
* @returns {{usage: {input: number, cacheWrite: number, cacheRead: number, output: number, turns: number, toolCalls: number}, costUsd: number|null}}
|
|
167
198
|
*/
|
|
168
199
|
export function parseAdapterUsage(adapterOrId, stdoutText = '', analyticsText = '') {
|
|
169
200
|
const result = emptyUsage();
|
|
@@ -192,7 +223,7 @@ export function parseAdapterUsage(adapterOrId, stdoutText = '', analyticsText =
|
|
|
192
223
|
result.usage.output = firstNumeric(obj, fields.output) ?? 0;
|
|
193
224
|
result.usage.turns = firstNumeric(obj, fields.turns) ?? 0;
|
|
194
225
|
result.usage.toolCalls = firstNumeric(obj, fields.toolCalls) ?? 0;
|
|
195
|
-
result.costUsd = firstNumeric(obj, fields.costUsd) ??
|
|
226
|
+
result.costUsd = firstNumeric(obj, fields.costUsd) ?? null;
|
|
196
227
|
|
|
197
228
|
// Gemini CLI also nests per-model token totals under `stats.models.<name>.tokens`.
|
|
198
229
|
const models = obj?.stats?.models;
|
|
@@ -265,7 +296,7 @@ export function parseAdapterUsage(adapterOrId, stdoutText = '', analyticsText =
|
|
|
265
296
|
}
|
|
266
297
|
}
|
|
267
298
|
|
|
268
|
-
result.costUsd = Number(result.costUsd.toFixed(6));
|
|
299
|
+
if (result.costUsd !== null) result.costUsd = Number(result.costUsd.toFixed(6));
|
|
269
300
|
return result;
|
|
270
301
|
}
|
|
271
302
|
|
|
@@ -467,55 +498,29 @@ async function applyProjectionMode(workDir, mode) {
|
|
|
467
498
|
}
|
|
468
499
|
}
|
|
469
500
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (lessonGlobs.some((glob) => prompt.includes(glob.replace(/\/\*\*$/, '')))) return true;
|
|
494
|
-
const kw = Array.isArray(lesson.keywords) ? lesson.keywords : [];
|
|
495
|
-
return kw.some((k) => taskTokenSet.has(String(k).toLowerCase()));
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
const lines = [
|
|
499
|
-
`# Task Brief: ${task?.id ?? 'task'}`,
|
|
500
|
-
'',
|
|
501
|
-
`Prompt: ${prompt}`,
|
|
502
|
-
'',
|
|
503
|
-
'## Ranked Skills',
|
|
504
|
-
...(ranked.length > 0
|
|
505
|
-
? ranked.map((r) => `- ${r.skill.name} (score ${r.score}): ${r.skill.description ?? ''}`)
|
|
506
|
-
: ['- (none matched)']),
|
|
507
|
-
...(matchingLessons.length > 0
|
|
508
|
-
? [
|
|
509
|
-
'',
|
|
510
|
-
'## Lessons',
|
|
511
|
-
...matchingLessons.map((l) => `- ${formatLessonParagraph(l)}`),
|
|
512
|
-
]
|
|
513
|
-
: []),
|
|
514
|
-
'',
|
|
515
|
-
];
|
|
516
|
-
const briefPath = join(workDir, STATE_DIR, 'brief.md');
|
|
517
|
-
await mkdir(dirname(briefPath), { recursive: true });
|
|
518
|
-
await writeFile(briefPath, lines.join('\n'), 'utf8');
|
|
501
|
+
/**
|
|
502
|
+
* The `brief` arm's context: the packet `simt brief` delivers, assembled by the same
|
|
503
|
+
* function the command and the prompt-submit hook call.
|
|
504
|
+
*
|
|
505
|
+
* The arm used to re-implement the packet from `registry.skills` — a property the
|
|
506
|
+
* registry does not have, so every run reported "(none matched)" while the real
|
|
507
|
+
* command resolved skills — and wrote it to a file no agent was told to read. An arm
|
|
508
|
+
* meant to measure the packet measured a different artefact (review 0003 Q49).
|
|
509
|
+
*/
|
|
510
|
+
async function buildBriefPrefix(workDir, task) {
|
|
511
|
+
// A packet is built from git's answer; a directory that is not a repository has none,
|
|
512
|
+
// and asking would print a failure into a bench log for no measurement.
|
|
513
|
+
if (!existsSync(join(workDir, '.git'))) return '';
|
|
514
|
+
const assembled = await assembleBrief({
|
|
515
|
+
root: workDir,
|
|
516
|
+
task: String(task?.prompt ?? ''),
|
|
517
|
+
paths: '',
|
|
518
|
+
budget: DEFAULT_BRIEF_BYTES,
|
|
519
|
+
build: true,
|
|
520
|
+
federate: false,
|
|
521
|
+
}).catch(() => null);
|
|
522
|
+
if (!assembled || assembled.error || assembled.cold) return '';
|
|
523
|
+
return assembled.packed?.text ?? '';
|
|
519
524
|
}
|
|
520
525
|
|
|
521
526
|
/**
|
|
@@ -524,9 +529,10 @@ async function writeTaskBrief(workDir, task) {
|
|
|
524
529
|
* @param {string} workDir
|
|
525
530
|
* @param {{name: string, kind?: string, base?: string|null, write?: Record<string, string>, remove?: string[], env?: Record<string, string>}} armSpec
|
|
526
531
|
* @param {object} [task]
|
|
527
|
-
* @returns {Promise<{env: Record<string, string
|
|
532
|
+
* @returns {Promise<{env: Record<string, string>, promptPrefix: string}>}
|
|
528
533
|
*/
|
|
529
534
|
export async function materialiseArm(workDir, armSpec, task = {}) {
|
|
535
|
+
let promptPrefix = '';
|
|
530
536
|
const ablate = armSpec.ablate ?? parseAblationArmName(armSpec.name)?.ablate ?? null;
|
|
531
537
|
const variant = armSpec.variant ?? parseVariantArmName(armSpec.name)?.variant ?? null;
|
|
532
538
|
const base = armSpec.base ?? (BUILTIN_ARMS.has(armSpec.name) ? armSpec.name : null);
|
|
@@ -542,7 +548,7 @@ export async function materialiseArm(workDir, armSpec, task = {}) {
|
|
|
542
548
|
await applyProjectionMode(workDir, 'kernel');
|
|
543
549
|
} else if (base === 'brief') {
|
|
544
550
|
await applyProjectionMode(workDir, 'full');
|
|
545
|
-
await
|
|
551
|
+
promptPrefix = await buildBriefPrefix(workDir, task);
|
|
546
552
|
}
|
|
547
553
|
|
|
548
554
|
for (const relPath of armSpec.remove ?? []) {
|
|
@@ -556,7 +562,7 @@ export async function materialiseArm(workDir, armSpec, task = {}) {
|
|
|
556
562
|
await writeFile(target, String(content), 'utf8');
|
|
557
563
|
}
|
|
558
564
|
|
|
559
|
-
return { env: { ...(armSpec.env ?? {}) } };
|
|
565
|
+
return { env: { ...(armSpec.env ?? {}) }, promptPrefix };
|
|
560
566
|
}
|
|
561
567
|
|
|
562
568
|
/**
|
|
@@ -569,7 +575,7 @@ export async function materialiseArm(workDir, armSpec, task = {}) {
|
|
|
569
575
|
* @param {object} armSpec
|
|
570
576
|
* @param {object} [task]
|
|
571
577
|
* @param {{parentDir?: string}} [options]
|
|
572
|
-
* @returns {Promise<{slotDir: string, workDir: string, initialHead: string, env: Record<string, string
|
|
578
|
+
* @returns {Promise<{slotDir: string, workDir: string, initialHead: string, env: Record<string, string>, promptPrefix: string}>}
|
|
573
579
|
*/
|
|
574
580
|
export async function prepareIsolatedWorkspace(root, baseSha, armSpec, task = {}, { parentDir } = {}) {
|
|
575
581
|
const slotDir = await mkdtemp(join(parentDir ?? tmpdir(), 'bench-run-'));
|
|
@@ -618,12 +624,18 @@ export async function prepareIsolatedWorkspace(root, baseSha, armSpec, task = {}
|
|
|
618
624
|
}
|
|
619
625
|
}
|
|
620
626
|
|
|
621
|
-
const { env } = await materialiseArm(workDir, armSpec, task);
|
|
622
|
-
|
|
623
627
|
await runCmd('git', ['init', '-q'], { cwd: workDir });
|
|
624
628
|
await runCmd('git', ['config', 'user.name', 'bench'], { cwd: workDir });
|
|
625
629
|
await runCmd('git', ['config', 'user.email', 'bench@local'], { cwd: workDir });
|
|
626
630
|
await runCmd('git', ['config', 'commit.gpgsign', 'false'], { cwd: workDir });
|
|
631
|
+
|
|
632
|
+
// The repository exists before an arm materialises: the `brief` arm assembles its
|
|
633
|
+
// packet from git's answer, which a directory that is not yet a repository cannot give.
|
|
634
|
+
const { env, promptPrefix } = await materialiseArm(workDir, armSpec, task);
|
|
635
|
+
// Assembling a packet writes tool state. It belongs to no arm, and leaving it in the
|
|
636
|
+
// base commit would make the `brief` workspace differ by an index as well as by context.
|
|
637
|
+
await rm(join(workDir, LOCAL_DIR), { recursive: true, force: true }).catch(() => {});
|
|
638
|
+
|
|
627
639
|
await runCmd('git', ['add', '-A'], { cwd: workDir });
|
|
628
640
|
const commitRes = await runCmd(
|
|
629
641
|
'git',
|
|
@@ -638,7 +650,7 @@ export async function prepareIsolatedWorkspace(root, baseSha, armSpec, task = {}
|
|
|
638
650
|
const headRes = await runCmd('git', ['rev-parse', 'HEAD'], { cwd: workDir });
|
|
639
651
|
const initialHead = headRes.stdout.toString('utf8').trim();
|
|
640
652
|
|
|
641
|
-
return { slotDir, workDir, initialHead, env };
|
|
653
|
+
return { slotDir, workDir, initialHead, env, promptPrefix };
|
|
642
654
|
}
|
|
643
655
|
|
|
644
656
|
function buildAdapterArgs(adapter, { prompt, turns, model, analyticsLog }) {
|
|
@@ -656,25 +668,75 @@ function buildAdapterArgs(adapter, { prompt, turns, model, analyticsLog }) {
|
|
|
656
668
|
return args;
|
|
657
669
|
}
|
|
658
670
|
|
|
659
|
-
|
|
660
|
-
|
|
671
|
+
/**
|
|
672
|
+
* Apply a patch to a workspace, falling back to a three-way merge.
|
|
673
|
+
*
|
|
674
|
+
* The gold oracle and the regrade path land operator-supplied bytes the same way. They
|
|
675
|
+
* used to disagree (`--binary` on one side only), so a patch that applied under one read
|
|
676
|
+
* as unreproducible under the other (review 0003 Q59).
|
|
677
|
+
*/
|
|
678
|
+
async function applyPatch(workDir, patch, { timeoutMs = DEFAULT_AGENT_TIMEOUT_MS } = {}) {
|
|
679
|
+
if (!patch || patch.length === 0) return { code: 0 };
|
|
680
|
+
const applied = await runCmd('git', ['apply', '--binary', '-'], {
|
|
681
|
+
cwd: workDir,
|
|
682
|
+
input: patch,
|
|
683
|
+
timeoutMs,
|
|
684
|
+
});
|
|
685
|
+
if (applied.code === 0) return applied;
|
|
686
|
+
return runCmd('git', ['apply', '--3way', '-'], { cwd: workDir, input: patch, timeoutMs });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export async function executeAdapter(
|
|
690
|
+
adapter,
|
|
691
|
+
workDir,
|
|
692
|
+
task,
|
|
693
|
+
{ vaultRoot, model, maxTurns = DEFAULT_MAX_TURNS, env, timeoutMs, slotDir, promptPrefix = '' },
|
|
694
|
+
) {
|
|
695
|
+
if (adapter.id === 'noop') {
|
|
696
|
+
return {
|
|
697
|
+
...parseAdapterUsage(adapter, ''),
|
|
698
|
+
transcript: '',
|
|
699
|
+
agent: { exitCode: 0, timedOut: false, wallMs: 0 },
|
|
700
|
+
};
|
|
701
|
+
}
|
|
661
702
|
if (adapter.id === 'gold') {
|
|
703
|
+
const startedAt = Date.now();
|
|
662
704
|
const goldPatch = await readFile(join(vaultRoot, task.id, 'gold.patch'));
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return { ...parseAdapterUsage(adapter, ''), transcript: '' };
|
|
705
|
+
await applyPatch(workDir, goldPatch, { timeoutMs });
|
|
706
|
+
return {
|
|
707
|
+
...parseAdapterUsage(adapter, ''),
|
|
708
|
+
transcript: '',
|
|
709
|
+
agent: { exitCode: 0, timedOut: false, wallMs: Date.now() - startedAt },
|
|
710
|
+
};
|
|
670
711
|
}
|
|
671
712
|
|
|
672
713
|
const analyticsLog = join(slotDir, 'analytics.jsonl');
|
|
673
|
-
|
|
674
|
-
|
|
714
|
+
// The arm's context arrives as a prefix rather than a file the agent must be told to
|
|
715
|
+
// read, so the prompt an adapter receives is the whole thing it was given. A `gold`
|
|
716
|
+
// or `noop` run never reaches this line: it scripts the workspace instead.
|
|
717
|
+
const prompt = [String(promptPrefix ?? ''), String(task.prompt ?? '')]
|
|
718
|
+
.filter((part) => part.length > 0)
|
|
719
|
+
.join('\n\n');
|
|
720
|
+
const args = buildAdapterArgs(adapter, { prompt, turns: maxTurns, model, analyticsLog });
|
|
721
|
+
const outcome = await runCmd(adapter.bin, args, {
|
|
722
|
+
cwd: workDir,
|
|
723
|
+
env: agentEnv(adapter, { home: slotDir, extra: env }),
|
|
724
|
+
timeoutMs,
|
|
725
|
+
});
|
|
726
|
+
// A binary that could not start produced no run. Grading its empty workspace would
|
|
727
|
+
// record an unresolved arm instead of the missing harness it is (review 0003 Q47).
|
|
728
|
+
if (outcome.spawnError) {
|
|
729
|
+
throw new Error(`harness ${adapter.id} could not start ${adapter.bin}: ${outcome.spawnError}`);
|
|
730
|
+
}
|
|
675
731
|
const stdoutText = outcome.stdout.toString('utf8');
|
|
676
732
|
const analyticsText = adapter.format === 'analytics-log' ? await readFile(analyticsLog, 'utf8').catch(() => '') : '';
|
|
677
|
-
return {
|
|
733
|
+
return {
|
|
734
|
+
...parseAdapterUsage(adapter, stdoutText, analyticsText),
|
|
735
|
+
transcript: `${stdoutText}\n${analyticsText}`,
|
|
736
|
+
// A run's own exit code, deadline and wall time belong in its record: the old one
|
|
737
|
+
// stored none, so a timed-out run was indistinguishable from a completed one.
|
|
738
|
+
agent: { exitCode: outcome.code, timedOut: outcome.timedOut, wallMs: outcome.wallMs },
|
|
739
|
+
};
|
|
678
740
|
}
|
|
679
741
|
|
|
680
742
|
// loadTasks lives in tasks.mjs (the advice engine became a second reader, so the loader
|
|
@@ -717,9 +779,10 @@ export async function runBench(
|
|
|
717
779
|
maxTurns = DEFAULT_MAX_TURNS,
|
|
718
780
|
concurrency = DEFAULT_CONCURRENCY,
|
|
719
781
|
cwd = process.cwd(),
|
|
782
|
+
adapters: adapterTable,
|
|
720
783
|
} = {},
|
|
721
784
|
) {
|
|
722
|
-
const adapters = await loadAdapters();
|
|
785
|
+
const adapters = adapterTable ?? (await loadAdapters());
|
|
723
786
|
const adapter = adapters[harness];
|
|
724
787
|
if (!adapter) {
|
|
725
788
|
throw new Error(
|
|
@@ -741,11 +804,15 @@ export async function runBench(
|
|
|
741
804
|
throw new Error(
|
|
742
805
|
filterTaskIds.length > 0
|
|
743
806
|
? `no mined tasks matched: ${filterTaskIds.join(', ')}`
|
|
744
|
-
: `no mined tasks found in
|
|
807
|
+
: `no mined tasks found in .simt/bench/tasks`,
|
|
745
808
|
);
|
|
746
809
|
}
|
|
747
810
|
|
|
748
811
|
const resolvedModel = model ?? (adapter.kind === 'builtin' ? adapter.id : 'default');
|
|
812
|
+
// A built-in harness scripts the workspace instead of driving an agent, so its
|
|
813
|
+
// numbers calibrate the grader and are never an arm's measurement (review 0003 Q49).
|
|
814
|
+
const measured = adapter.kind !== 'builtin';
|
|
815
|
+
const isolation = adapterIsolation(adapter);
|
|
749
816
|
const plannedRuns = [];
|
|
750
817
|
for (const armSpec of armSpecs) {
|
|
751
818
|
for (let rep = 1; rep <= reps; rep += 1) {
|
|
@@ -778,17 +845,19 @@ export async function runBench(
|
|
|
778
845
|
harness,
|
|
779
846
|
model: resolvedModel,
|
|
780
847
|
rep: item.rep,
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
848
|
+
measured,
|
|
849
|
+
isolation, status: 'planned',
|
|
850
|
+
resolved: null,
|
|
851
|
+
gatesPassed: null,
|
|
852
|
+
usage: emptyUsage().usage,
|
|
853
|
+
costUsd: null,
|
|
785
854
|
diff: { files: 0, outsideBlastRadius: 0 },
|
|
786
855
|
probe: null,
|
|
787
856
|
})),
|
|
788
857
|
};
|
|
789
858
|
}
|
|
790
859
|
|
|
791
|
-
const runsBaseDir = join(root,
|
|
860
|
+
const runsBaseDir = join(root, LOCAL_DIR, 'bench', 'runs');
|
|
792
861
|
for (const armSpec of armSpecs) {
|
|
793
862
|
for (let rep = 1; rep <= reps; rep += 1) {
|
|
794
863
|
const runDir = join(runsBaseDir, runIdFor(harness, armSpec.name, rep));
|
|
@@ -803,21 +872,25 @@ export async function runBench(
|
|
|
803
872
|
const parentDir = await mkdtemp(join(tmpdir(), 'bench-batch-'));
|
|
804
873
|
try {
|
|
805
874
|
const records = await mapPool(plannedRuns, concurrency, async (item) => {
|
|
806
|
-
|
|
807
|
-
root,
|
|
808
|
-
item.task.base,
|
|
809
|
-
item.armSpec,
|
|
810
|
-
item.task,
|
|
811
|
-
{ parentDir },
|
|
812
|
-
);
|
|
875
|
+
let slotDir = null;
|
|
813
876
|
try {
|
|
814
|
-
const
|
|
877
|
+
const prepared = await prepareIsolatedWorkspace(
|
|
878
|
+
root,
|
|
879
|
+
item.task.base,
|
|
880
|
+
item.armSpec,
|
|
881
|
+
item.task,
|
|
882
|
+
{ parentDir },
|
|
883
|
+
);
|
|
884
|
+
slotDir = prepared.slotDir;
|
|
885
|
+
const { workDir, initialHead, env, promptPrefix } = prepared;
|
|
886
|
+
const { usage, costUsd, transcript, agent } = await executeAdapter(adapter, workDir, item.task, {
|
|
815
887
|
vaultRoot,
|
|
816
888
|
model: resolvedModel,
|
|
817
889
|
maxTurns,
|
|
818
890
|
env,
|
|
819
891
|
timeoutMs,
|
|
820
892
|
slotDir,
|
|
893
|
+
promptPrefix,
|
|
821
894
|
});
|
|
822
895
|
|
|
823
896
|
const grade = await gradeWorkspace(workDir, item.task, {
|
|
@@ -840,8 +913,13 @@ export async function runBench(
|
|
|
840
913
|
harness,
|
|
841
914
|
model: resolvedModel,
|
|
842
915
|
rep: item.rep,
|
|
916
|
+
measured,
|
|
917
|
+
isolation,
|
|
918
|
+
status: 'ok',
|
|
919
|
+
agent,
|
|
843
920
|
resolved: grade.resolved,
|
|
844
921
|
gatesPassed: grade.gatesPassed,
|
|
922
|
+
f2pPassed: grade.f2pPassed ?? [],
|
|
845
923
|
usage,
|
|
846
924
|
costUsd,
|
|
847
925
|
diff: grade.diff,
|
|
@@ -856,8 +934,33 @@ export async function runBench(
|
|
|
856
934
|
]);
|
|
857
935
|
|
|
858
936
|
return { ...record, runId: item.runId };
|
|
937
|
+
} catch (error) {
|
|
938
|
+
// One failed run is one exclusion, not a lost batch: the old path exited 3 and
|
|
939
|
+
// left a half-written run behind (review 0003 Q47).
|
|
940
|
+
return {
|
|
941
|
+
schemaVersion: 1,
|
|
942
|
+
runId: item.runId,
|
|
943
|
+
task: item.task.id,
|
|
944
|
+
arm: item.armSpec.name,
|
|
945
|
+
harness,
|
|
946
|
+
model: resolvedModel,
|
|
947
|
+
rep: item.rep,
|
|
948
|
+
measured,
|
|
949
|
+
isolation,
|
|
950
|
+
status: 'error',
|
|
951
|
+
error: String(error?.message ?? error),
|
|
952
|
+
resolved: false,
|
|
953
|
+
gatesPassed: false,
|
|
954
|
+
f2pPassed: [],
|
|
955
|
+
usage: emptyUsage().usage,
|
|
956
|
+
costUsd: null,
|
|
957
|
+
diff: { files: 0, outsideBlastRadius: 0 },
|
|
958
|
+
probe: null,
|
|
959
|
+
};
|
|
859
960
|
} finally {
|
|
860
|
-
|
|
961
|
+
if (slotDir !== null) {
|
|
962
|
+
await rm(slotDir, { recursive: true, force: true }).catch(() => {});
|
|
963
|
+
}
|
|
861
964
|
}
|
|
862
965
|
});
|
|
863
966
|
|
|
@@ -876,8 +979,15 @@ export async function runBench(
|
|
|
876
979
|
}
|
|
877
980
|
}
|
|
878
981
|
|
|
982
|
+
/** Two string lists hold the same members, ignoring order and duplicates. */
|
|
983
|
+
function sameStringSet(a, b) {
|
|
984
|
+
const left = [...new Set(a)].sort();
|
|
985
|
+
const right = [...new Set(b)].sort();
|
|
986
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
987
|
+
}
|
|
988
|
+
|
|
879
989
|
async function resolveRegradeTargets(root, target) {
|
|
880
|
-
const runsBaseDir = join(root,
|
|
990
|
+
const runsBaseDir = join(root, LOCAL_DIR, 'bench', 'runs');
|
|
881
991
|
const direct = resolve(root, target);
|
|
882
992
|
const candidate = existsSync(direct) ? direct : join(runsBaseDir, target);
|
|
883
993
|
|
|
@@ -933,7 +1043,7 @@ export async function regradeRun(
|
|
|
933
1043
|
throw new Error(`no stored task runs found under ${target}`);
|
|
934
1044
|
}
|
|
935
1045
|
|
|
936
|
-
const tasksDir = join(root,
|
|
1046
|
+
const tasksDir = join(root, LOCAL_DIR, 'bench', 'runs');
|
|
937
1047
|
const taskDefsDir = join(root, STATE_DIR, 'bench', 'tasks');
|
|
938
1048
|
const parentDir = await mkdtemp(join(tmpdir(), 'bench-regrade-'));
|
|
939
1049
|
|
|
@@ -971,20 +1081,7 @@ export async function regradeRun(
|
|
|
971
1081
|
{ parentDir },
|
|
972
1082
|
);
|
|
973
1083
|
try {
|
|
974
|
-
|
|
975
|
-
const applyRes = await runCmd('git', ['apply', '--binary', '-'], {
|
|
976
|
-
cwd: workDir,
|
|
977
|
-
input: agentPatch,
|
|
978
|
-
timeoutMs,
|
|
979
|
-
});
|
|
980
|
-
if (applyRes.code !== 0) {
|
|
981
|
-
await runCmd('git', ['apply', '--3way', '-'], {
|
|
982
|
-
cwd: workDir,
|
|
983
|
-
input: agentPatch,
|
|
984
|
-
timeoutMs,
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
}
|
|
1084
|
+
await applyPatch(workDir, agentPatch, { timeoutMs });
|
|
988
1085
|
|
|
989
1086
|
const grade = await gradeWorkspace(workDir, task, {
|
|
990
1087
|
vaultRoot,
|
|
@@ -992,9 +1089,12 @@ export async function regradeRun(
|
|
|
992
1089
|
timeoutMs,
|
|
993
1090
|
});
|
|
994
1091
|
|
|
1092
|
+
// The verdicts alone cannot tell "the same tests passed" from "the same number
|
|
1093
|
+
// of tests passed"; the fail-to-pass set is compared too (review 0003 Q59).
|
|
995
1094
|
const reproduced =
|
|
996
1095
|
grade.resolved === stored.resolved &&
|
|
997
1096
|
grade.gatesPassed === stored.gatesPassed &&
|
|
1097
|
+
sameStringSet(grade.f2pPassed ?? [], stored.f2pPassed ?? []) &&
|
|
998
1098
|
grade.diff.files === stored.diff?.files &&
|
|
999
1099
|
grade.diff.outsideBlastRadius === stored.diff?.outsideBlastRadius;
|
|
1000
1100
|
|
|
@@ -1002,6 +1102,7 @@ export async function regradeRun(
|
|
|
1002
1102
|
...stored,
|
|
1003
1103
|
resolved: grade.resolved,
|
|
1004
1104
|
gatesPassed: grade.gatesPassed,
|
|
1105
|
+
f2pPassed: grade.f2pPassed ?? [],
|
|
1005
1106
|
diff: grade.diff,
|
|
1006
1107
|
};
|
|
1007
1108
|
|