simt-dev 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/bin/simt-dev.mjs +21 -2
  3. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  4. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  5. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  6. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  7. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  8. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  9. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  10. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  11. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  12. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  13. package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
  14. package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
  15. package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
  16. package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
  17. package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
  18. package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
  19. package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
  20. package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
  21. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
  22. package/package.json +2 -1
  23. package/rules/core.md +1 -0
  24. package/rules/stacks/android.md +2 -0
  25. package/rules/stacks/cloudflare-workers.md +2 -0
  26. package/rules/stacks/connectiq.md +1 -0
  27. package/rules/stacks/container.md +2 -0
  28. package/rules/stacks/dart.md +2 -0
  29. package/rules/stacks/deno.md +2 -0
  30. package/rules/stacks/dotnet.md +2 -0
  31. package/rules/stacks/elixir.md +2 -0
  32. package/rules/stacks/go.md +2 -0
  33. package/rules/stacks/jvm.md +2 -0
  34. package/rules/stacks/node.md +2 -0
  35. package/rules/stacks/python.md +2 -0
  36. package/rules/stacks/react-native.md +2 -0
  37. package/rules/stacks/react-web.md +2 -0
  38. package/rules/stacks/ruby.md +2 -0
  39. package/rules/stacks/rust.md +2 -0
  40. package/rules/stacks/swift.md +2 -0
  41. package/rules/stacks/typescript.md +2 -0
  42. package/rules/stacks/uv.md +2 -0
  43. package/src/agenthooks/decide.mjs +123 -0
  44. package/src/agenthooks/policy.mjs +78 -0
  45. package/src/agenthooks/render.mjs +240 -0
  46. package/src/argv.mjs +55 -1
  47. package/src/bench/ablate.mjs +630 -0
  48. package/src/bench/adapters.json +149 -0
  49. package/src/bench/grade.mjs +229 -0
  50. package/src/bench/mine.mjs +811 -0
  51. package/src/bench/probes.mjs +1350 -0
  52. package/src/bench/report.mjs +899 -0
  53. package/src/bench/run.mjs +1002 -0
  54. package/src/commands/agenthook.mjs +282 -0
  55. package/src/commands/bench.mjs +359 -0
  56. package/src/commands/compare.mjs +15 -0
  57. package/src/commands/graph.mjs +45 -21
  58. package/src/commands/harness.mjs +301 -0
  59. package/src/commands/help.mjs +73 -2
  60. package/src/commands/index.mjs +67 -25
  61. package/src/commands/init.mjs +12 -8
  62. package/src/commands/mcp.mjs +20 -5
  63. package/src/commands/plan.mjs +99 -0
  64. package/src/commands/rules.mjs +42 -57
  65. package/src/commands/skills.mjs +67 -92
  66. package/src/commands/skillsinstall.mjs +17 -2
  67. package/src/commands/sync.mjs +62 -6
  68. package/src/commands/tokens.mjs +169 -0
  69. package/src/compare/row.mjs +73 -2
  70. package/src/decisions/index.mjs +23 -5
  71. package/src/graph/spec.mjs +227 -0
  72. package/src/graph/speccheck.mjs +68 -0
  73. package/src/hooks/dispatcher.mjs +35 -3
  74. package/src/kernel/card.mjs +254 -0
  75. package/src/kernel/inferability.mjs +188 -0
  76. package/src/mcp/resources.mjs +157 -0
  77. package/src/mcp/schema.mjs +139 -0
  78. package/src/mcp/server.mjs +228 -20
  79. package/src/mcp/tools.mjs +372 -1
  80. package/src/measure/context.mjs +542 -0
  81. package/src/measure/loaderView.mjs +435 -0
  82. package/src/measure/session.mjs +267 -0
  83. package/src/monorepo.mjs +339 -0
  84. package/src/output/index.mjs +78 -2
  85. package/src/packs.mjs +182 -0
  86. package/src/plan/index.mjs +650 -0
  87. package/src/plan/template.mjs +331 -0
  88. package/src/projection/loaders.mjs +689 -0
  89. package/src/projection/manifest.mjs +114 -3
  90. package/src/projection/project.mjs +140 -9
  91. package/src/projection/rules.mjs +365 -22
  92. package/src/projection/surfaces.mjs +155 -23
  93. package/src/projection/targets.mjs +37 -0
  94. package/src/toolwords.mjs +69 -0
  95. package/templates/plan/tracker.json +36 -0
@@ -0,0 +1,149 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "adapters": {
4
+ "claude": {
5
+ "id": "claude",
6
+ "label": "Claude Code",
7
+ "kind": "cli",
8
+ "bin": "claude",
9
+ "template": "claude -p \"{prompt}\" --output-format json --max-turns {turns}",
10
+ "args": ["-p", "{prompt}", "--output-format", "json", "--max-turns", "{turns}"],
11
+ "modelArgs": ["--model", "{model}"],
12
+ "format": "json",
13
+ "usageSource": "usage.*, total_cost_usd, num_turns",
14
+ "fields": {
15
+ "input": ["usage.input_tokens", "usage.input"],
16
+ "cacheWrite": ["usage.cache_creation_input_tokens", "usage.cacheWrite"],
17
+ "cacheRead": ["usage.cache_read_input_tokens", "usage.cacheRead"],
18
+ "output": ["usage.output_tokens", "usage.output"],
19
+ "turns": ["num_turns", "usage.turns"],
20
+ "toolCalls": ["num_tool_calls", "tool_calls", "usage.toolCalls"],
21
+ "costUsd": ["total_cost_usd", "cost_usd", "costUsd"]
22
+ }
23
+ },
24
+ "codex": {
25
+ "id": "codex",
26
+ "label": "Codex",
27
+ "kind": "cli",
28
+ "bin": "codex",
29
+ "template": "codex exec --json \"{prompt}\"",
30
+ "args": ["exec", "--json", "{prompt}"],
31
+ "modelArgs": ["--model", "{model}"],
32
+ "format": "jsonl",
33
+ "usageSource": "token events in JSONL",
34
+ "fields": {
35
+ "input": ["usage.input_tokens", "input_tokens", "usage.input"],
36
+ "cacheWrite": ["usage.cache_creation_input_tokens", "usage.cache_write_input_tokens", "cache_write_input_tokens"],
37
+ "cacheRead": ["usage.cached_input_tokens", "usage.cache_read_input_tokens", "cached_input_tokens"],
38
+ "output": ["usage.output_tokens", "output_tokens", "usage.output"],
39
+ "turns": ["turns", "num_turns"],
40
+ "toolCalls": ["tool_calls", "num_tool_calls"],
41
+ "costUsd": ["total_cost_usd", "cost_usd", "cost"]
42
+ }
43
+ },
44
+ "gemini": {
45
+ "id": "gemini",
46
+ "label": "Gemini CLI",
47
+ "kind": "cli",
48
+ "bin": "gemini",
49
+ "template": "gemini -p \"{prompt}\" --output-format json",
50
+ "args": ["-p", "{prompt}", "--output-format", "json"],
51
+ "modelArgs": ["--model", "{model}"],
52
+ "format": "json",
53
+ "usageSource": "stats block",
54
+ "fields": {
55
+ "input": ["stats.inputTokens", "stats.tokens.prompt", "stats.prompt_tokens", "usage.input_tokens"],
56
+ "cacheWrite": ["stats.cacheWriteTokens", "stats.tokens.cacheWrite", "usage.cache_creation_input_tokens"],
57
+ "cacheRead": ["stats.cachedTokens", "stats.tokens.cached", "usage.cache_read_input_tokens"],
58
+ "output": ["stats.outputTokens", "stats.tokens.candidates", "stats.completion_tokens", "usage.output_tokens"],
59
+ "turns": ["stats.turns", "stats.totalTurns", "num_turns"],
60
+ "toolCalls": ["stats.toolCalls", "stats.tools.totalCalls", "num_tool_calls"],
61
+ "costUsd": ["stats.totalCostUsd", "stats.costUsd", "total_cost_usd"]
62
+ }
63
+ },
64
+ "opencode": {
65
+ "id": "opencode",
66
+ "label": "opencode",
67
+ "kind": "cli",
68
+ "bin": "opencode",
69
+ "template": "opencode run \"{prompt}\"",
70
+ "args": ["run", "--format", "json", "{prompt}"],
71
+ "modelArgs": ["--model", "{model}"],
72
+ "format": "jsonl",
73
+ "usageSource": "session export",
74
+ "fields": {
75
+ "input": ["tokens.input", "usage.input_tokens", "part.tokens.input"],
76
+ "cacheWrite": ["tokens.cache.write", "usage.cache_creation_input_tokens", "part.tokens.cache.write"],
77
+ "cacheRead": ["tokens.cache.read", "usage.cache_read_input_tokens", "part.tokens.cache.read"],
78
+ "output": ["tokens.output", "usage.output_tokens", "part.tokens.output"],
79
+ "turns": ["turns", "num_turns"],
80
+ "toolCalls": ["toolCalls", "num_tool_calls"],
81
+ "costUsd": ["cost", "part.cost", "total_cost_usd"]
82
+ }
83
+ },
84
+ "cursor": {
85
+ "id": "cursor",
86
+ "label": "Cursor CLI",
87
+ "kind": "cli",
88
+ "bin": "cursor-agent",
89
+ "template": "cursor-agent -p \"{prompt}\" --output-format json",
90
+ "args": ["-p", "{prompt}", "--output-format", "json"],
91
+ "modelArgs": ["--model", "{model}"],
92
+ "format": "json",
93
+ "usageSource": "result JSON",
94
+ "fields": {
95
+ "input": ["usage.input_tokens", "usage.input", "stats.inputTokens"],
96
+ "cacheWrite": ["usage.cache_creation_input_tokens", "usage.cacheWrite", "stats.cacheWriteTokens"],
97
+ "cacheRead": ["usage.cache_read_input_tokens", "usage.cacheRead", "stats.cachedTokens"],
98
+ "output": ["usage.output_tokens", "usage.output", "stats.outputTokens"],
99
+ "turns": ["num_turns", "turns", "usage.turns"],
100
+ "toolCalls": ["num_tool_calls", "tool_calls", "usage.toolCalls", "toolCalls"],
101
+ "costUsd": ["total_cost_usd", "costUsd", "cost_usd"]
102
+ }
103
+ },
104
+ "aider": {
105
+ "id": "aider",
106
+ "label": "Aider",
107
+ "kind": "cli",
108
+ "bin": "aider",
109
+ "template": "aider --message \"{prompt}\" --yes-always",
110
+ "args": ["--message", "{prompt}", "--yes-always", "--analytics-log", "{analyticsLog}"],
111
+ "modelArgs": ["--model", "{model}"],
112
+ "format": "analytics-log",
113
+ "usageSource": "--analytics-log file",
114
+ "fields": {
115
+ "input": ["prompt_tokens", "properties.prompt_tokens", "usage.input_tokens"],
116
+ "cacheWrite": ["cache_creation_input_tokens", "properties.cache_write_tokens"],
117
+ "cacheRead": ["cache_read_input_tokens", "properties.cache_read_tokens"],
118
+ "output": ["completion_tokens", "properties.completion_tokens", "usage.output_tokens"],
119
+ "turns": ["turns", "properties.turns"],
120
+ "toolCalls": ["tool_calls", "properties.tool_calls"],
121
+ "costUsd": ["cost", "total_cost", "properties.cost", "properties.total_cost"]
122
+ }
123
+ },
124
+ "gold": {
125
+ "id": "gold",
126
+ "label": "Gold Oracle",
127
+ "kind": "builtin",
128
+ "bin": null,
129
+ "template": null,
130
+ "args": [],
131
+ "modelArgs": [],
132
+ "format": "builtin",
133
+ "usageSource": "vault gold.patch",
134
+ "fields": {}
135
+ },
136
+ "noop": {
137
+ "id": "noop",
138
+ "label": "No-op Baseline",
139
+ "kind": "builtin",
140
+ "bin": null,
141
+ "template": null,
142
+ "args": [],
143
+ "modelArgs": [],
144
+ "format": "builtin",
145
+ "usageSource": "none",
146
+ "fields": {}
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,229 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFile, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ import { runGates } from '../hooks/dispatcher.mjs';
6
+ import { packageVersion } from '../package-version.mjs';
7
+ import { readManifest } from '../projection/manifest.mjs';
8
+ import { detectTestRunner } from './mine.mjs';
9
+
10
+ const DEFAULT_GRADE_TIMEOUT_MS = 45_000;
11
+
12
+ function sanitizedEnv(extra = {}) {
13
+ const env = { ...process.env, SIMT_DEV_WORKSPACE: 'off', ...extra };
14
+ for (const key of Object.keys(env)) {
15
+ if (key.startsWith('NODE_TEST')) delete env[key];
16
+ }
17
+ return env;
18
+ }
19
+
20
+ function runCmd(cmd, args, { cwd, input = null, timeoutMs = DEFAULT_GRADE_TIMEOUT_MS } = {}) {
21
+ return new Promise((resolveOutcome) => {
22
+ const child = spawn(cmd, args, {
23
+ cwd,
24
+ env: sanitizedEnv(),
25
+ stdio: ['pipe', 'pipe', 'pipe'],
26
+ });
27
+ const out = [];
28
+ const err = [];
29
+ let timedOut = false;
30
+ const timer =
31
+ timeoutMs > 0
32
+ ? setTimeout(() => {
33
+ timedOut = true;
34
+ child.kill('SIGKILL');
35
+ }, timeoutMs)
36
+ : null;
37
+
38
+ child.stdout.on('data', (chunk) => out.push(chunk));
39
+ child.stderr.on('data', (chunk) => err.push(chunk));
40
+ child.on('error', (error) => {
41
+ if (timer) clearTimeout(timer);
42
+ resolveOutcome({
43
+ code: 1,
44
+ timedOut,
45
+ stdout: Buffer.concat(out),
46
+ stderr: Buffer.from(String(error.message ?? error)),
47
+ });
48
+ });
49
+ child.on('close', (code) => {
50
+ if (timer) clearTimeout(timer);
51
+ resolveOutcome({
52
+ code: timedOut ? 124 : (code ?? 1),
53
+ timedOut,
54
+ stdout: Buffer.concat(out),
55
+ stderr: Buffer.concat(err),
56
+ });
57
+ });
58
+
59
+ if (input !== null) child.stdin.end(input);
60
+ else child.stdin.end();
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Extract repository-relative file paths touched by a unified git diff.
66
+ *
67
+ * @param {Buffer|string} patch
68
+ * @returns {string[]}
69
+ */
70
+ export function extractPatchFiles(patch) {
71
+ const text = Buffer.isBuffer(patch) ? patch.toString('utf8') : String(patch ?? '');
72
+ const files = new Set();
73
+ for (const line of text.split('\n')) {
74
+ const match = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
75
+ if (match) files.add(match[2]);
76
+ }
77
+ return [...files].sort();
78
+ }
79
+
80
+ async function resetFilesToHead(workDir, initialHead, files) {
81
+ for (const file of files) {
82
+ const existsAtHead = await runCmd('git', ['cat-file', '-e', `${initialHead}:${file}`], { cwd: workDir });
83
+ if (existsAtHead.code === 0) {
84
+ await runCmd('git', ['checkout', '-f', initialHead, '--', file], { cwd: workDir });
85
+ } else {
86
+ await runCmd('git', ['rm', '-f', '--ignore-unmatch', '--', file], { cwd: workDir });
87
+ await rm(join(workDir, file), { force: true }).catch(() => {});
88
+ }
89
+ }
90
+ }
91
+
92
+ async function evaluateDeclaredGates(workDir, testPassed) {
93
+ const { exists, data: manifest, error } = await readManifest(workDir);
94
+ if (!exists) return testPassed;
95
+ if (error || !manifest) return false;
96
+
97
+ const hasCustomTestCmd = Boolean(manifest?.gates?.commands?.test);
98
+ const filterGates = (list) =>
99
+ Array.isArray(list) ? list.filter((g) => g !== 'test' || hasCustomTestCmd) : [];
100
+
101
+ const alignedManifest = {
102
+ ...manifest,
103
+ projection: {
104
+ ...(manifest.projection ?? {}),
105
+ version: packageVersion(),
106
+ },
107
+ gates: {
108
+ ...(manifest.gates ?? {}),
109
+ preCommit: filterGates(manifest?.gates?.preCommit),
110
+ prePush: filterGates(manifest?.gates?.prePush),
111
+ },
112
+ };
113
+
114
+ await runCmd('git', ['add', '-A'], { cwd: workDir });
115
+ const preCommit = await runGates('pre-commit', { root: workDir, manifest: alignedManifest });
116
+ const prePush = await runGates('pre-push', { root: workDir, manifest: alignedManifest });
117
+ return testPassed && !preCommit.failed && !prePush.failed;
118
+ }
119
+
120
+ /**
121
+ * Grade an isolated agent workspace after the agent finishes.
122
+ *
123
+ * Measures the agent's diff before applying the hidden verifier test patch, then resets
124
+ * any test files touched by `test.patch` to `initialHead` so an agent cannot pass by
125
+ * weakening or deleting the test suite.
126
+ *
127
+ * @param {string} workDir
128
+ * @param {object} task
129
+ * @param {{vaultRoot: string, initialHead: string, timeoutMs?: number}} options
130
+ * @returns {Promise<{resolved: boolean, gatesPassed: boolean, diff: {files: number, outsideBlastRadius: number}, agentPatch: Buffer}>}
131
+ */
132
+ export async function gradeWorkspace(workDir, task, { vaultRoot, initialHead, timeoutMs = DEFAULT_GRADE_TIMEOUT_MS }) {
133
+ const taskVaultDir = join(vaultRoot, task.id);
134
+ const [testPatch, goldPatch] = await Promise.all([
135
+ readFile(join(taskVaultDir, 'test.patch')),
136
+ readFile(join(taskVaultDir, 'gold.patch')).catch(() => Buffer.alloc(0)),
137
+ ]);
138
+
139
+ // Remove transient brief file if the `brief` arm or agent left one untracked, so it
140
+ // is not counted as an implementation edit outside the task's blast radius.
141
+ await runCmd('git', ['add', '-N', '.'], { cwd: workDir });
142
+ const [diffNamesRes, diffPatchRes] = await Promise.all([
143
+ runCmd('git', ['diff', '--name-only', initialHead], { cwd: workDir }),
144
+ runCmd('git', ['diff', '--binary', initialHead], { cwd: workDir }),
145
+ ]);
146
+
147
+ const changedFiles = diffNamesRes.stdout
148
+ .toString('utf8')
149
+ .split('\n')
150
+ .map((s) => s.trim())
151
+ .filter((f) => Boolean(f) && !f.startsWith('.simt-dev/'));
152
+
153
+ const goldFiles = new Set(extractPatchFiles(goldPatch));
154
+ const outsideBlastRadius =
155
+ goldFiles.size > 0
156
+ ? changedFiles.filter((file) => !goldFiles.has(file)).length
157
+ : 0;
158
+ const diff = {
159
+ files: changedFiles.length,
160
+ outsideBlastRadius,
161
+ };
162
+
163
+ const testPatchFiles = extractPatchFiles(testPatch);
164
+ await resetFilesToHead(workDir, initialHead, testPatchFiles);
165
+
166
+ const applyTest = await runCmd('git', ['apply', '-'], {
167
+ cwd: workDir,
168
+ input: testPatch,
169
+ timeoutMs,
170
+ });
171
+ if (applyTest.code !== 0) {
172
+ return {
173
+ resolved: false,
174
+ gatesPassed: false,
175
+ diff,
176
+ agentPatch: diffPatchRes.stdout,
177
+ };
178
+ }
179
+
180
+ const runner = await detectTestRunner(workDir, testPatchFiles);
181
+ if (!runner) {
182
+ return {
183
+ resolved: false,
184
+ gatesPassed: false,
185
+ diff,
186
+ agentPatch: diffPatchRes.stdout,
187
+ };
188
+ }
189
+
190
+ const execSpec = runner.buildExec(workDir);
191
+ const runRes = await runCmd(execSpec.cmd, execSpec.args, { cwd: workDir, timeoutMs });
192
+ const reportText = execSpec.reportPath
193
+ ? await readFile(execSpec.reportPath, 'utf8').catch(() => '')
194
+ : '';
195
+ const parsed = runner.parse(runRes.stdout.toString('utf8'), reportText);
196
+ const testRun = {
197
+ code: runRes.code,
198
+ timedOut: runRes.timedOut,
199
+ passed: parsed.passed,
200
+ failed: parsed.failed,
201
+ };
202
+ const passedSet = new Set(testRun.passed);
203
+ const f2pResolved =
204
+ Array.isArray(task.f2p) &&
205
+ task.f2p.length > 0 &&
206
+ task.f2p.every((id) => passedSet.has(id));
207
+ const p2pIntact =
208
+ Array.isArray(task.p2p) &&
209
+ task.p2p.every((id) => passedSet.has(id));
210
+
211
+ const resolved =
212
+ !testRun.timedOut &&
213
+ testRun.code === 0 &&
214
+ testRun.failed.length === 0 &&
215
+ f2pResolved &&
216
+ p2pIntact;
217
+
218
+ const gatesPassed = await evaluateDeclaredGates(
219
+ workDir,
220
+ !testRun.timedOut && testRun.code === 0 && testRun.failed.length === 0,
221
+ );
222
+
223
+ return {
224
+ resolved,
225
+ gatesPassed,
226
+ diff,
227
+ agentPatch: diffPatchRes.stdout,
228
+ };
229
+ }