simt-dev 0.5.0 → 0.6.2

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 (85) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +78 -59
  3. package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +13 -0
  4. package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +1 -1
  5. package/docs/decisions/ADR-097-the-channels-publish-by-hand-and-the-version-rides-in-as-an-input.md +71 -0
  6. package/docs/decisions/ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md +149 -0
  7. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +79 -0
  8. package/package.json +4 -4
  9. package/src/agenthooks/render.mjs +87 -21
  10. package/src/argv.mjs +13 -0
  11. package/src/bench/ablate.mjs +1 -3
  12. package/src/bench/grade.mjs +1 -1
  13. package/src/bench/mine.mjs +3 -3
  14. package/src/bench/probes.mjs +1 -3
  15. package/src/bench/report.mjs +1 -3
  16. package/src/bench/run.mjs +1 -3
  17. package/src/bench/tasks.mjs +1 -3
  18. package/src/bench/variants.mjs +1 -3
  19. package/src/brief/assemble.mjs +2 -4
  20. package/src/ci/dependabot.mjs +1 -1
  21. package/src/ci/workflow.mjs +47 -17
  22. package/src/commands/agenthook.mjs +6 -2
  23. package/src/commands/bench.mjs +4 -4
  24. package/src/commands/ci.mjs +1 -1
  25. package/src/commands/compare.mjs +1 -1
  26. package/src/commands/distill.mjs +1 -1
  27. package/src/commands/gate.mjs +2 -2
  28. package/src/commands/graph.mjs +2 -2
  29. package/src/commands/harness.mjs +87 -14
  30. package/src/commands/help.mjs +29 -7
  31. package/src/commands/hooks.mjs +1 -1
  32. package/src/commands/index.mjs +17 -3
  33. package/src/commands/init.mjs +2 -2
  34. package/src/commands/local.mjs +11 -0
  35. package/src/commands/mcp.mjs +1 -1
  36. package/src/commands/pack.mjs +246 -0
  37. package/src/commands/plugin.mjs +61 -0
  38. package/src/commands/rules.mjs +1 -1
  39. package/src/commands/skills.mjs +1 -1
  40. package/src/commands/skillscompile.mjs +1 -3
  41. package/src/commands/skillsinstall.mjs +1 -1
  42. package/src/commands/sync.mjs +8 -8
  43. package/src/commands/tokens.mjs +1 -1
  44. package/src/commands/upgrade.mjs +1 -1
  45. package/src/commands/why.mjs +1 -1
  46. package/src/compare/discover.mjs +1 -1
  47. package/src/compare/hooksstate.mjs +19 -7
  48. package/src/compare/learn.mjs +2 -2
  49. package/src/compare/row.mjs +3 -3
  50. package/src/decisions/index.mjs +1 -1
  51. package/src/distill/distill.mjs +2 -2
  52. package/src/distribute/release.mjs +142 -0
  53. package/src/graph/load.mjs +9 -8
  54. package/src/hooks/dispatcher.mjs +3 -2
  55. package/src/hooks/shim.mjs +11 -6
  56. package/src/index/govern.mjs +1 -1
  57. package/src/index/map.mjs +1 -1
  58. package/src/index/persist.mjs +2 -2
  59. package/src/index/store.mjs +50 -20
  60. package/src/ledger/index.mjs +2 -2
  61. package/src/local/client.mjs +22 -8
  62. package/src/local/config.mjs +17 -0
  63. package/src/locate.mjs +22 -1
  64. package/src/mcp/prompts.mjs +118 -0
  65. package/src/mcp/resources.mjs +96 -22
  66. package/src/mcp/server.mjs +33 -6
  67. package/src/mcp/tools.mjs +12 -12
  68. package/src/measure/session.mjs +1 -1
  69. package/src/memory/decide.mjs +1 -1
  70. package/src/memory/lessons.mjs +14 -14
  71. package/src/name.mjs +11 -1
  72. package/src/package-version.mjs +12 -10
  73. package/src/packs/manage.mjs +563 -0
  74. package/src/packs.mjs +15 -5
  75. package/src/plugins.mjs +164 -0
  76. package/src/projection/loaders.mjs +84 -10
  77. package/src/projection/manifest.mjs +4 -4
  78. package/src/projection/project.mjs +3 -3
  79. package/src/projection/rules.mjs +24 -4
  80. package/src/projection/surfaces.mjs +20 -2
  81. package/src/review/critic.mjs +4 -4
  82. package/src/toolwords.mjs +8 -5
  83. package/src/tsconfig.mjs +3 -3
  84. package/src/upgrade/plan.mjs +1 -1
  85. /package/bin/{simt-dev.mjs → simt.mjs} +0 -0
@@ -0,0 +1,79 @@
1
+ ---
2
+ simt-affects: [".github/workflows/publish.yml", "scripts/build-sea.mjs", "scripts/verify-release-asset.mjs", "Formula/simt-dev.rb"]
3
+ simt-supersedes: [{ adr: "ADR-098", ruling: 3, kind: "narrowed" }]
4
+ ---
5
+
6
+ # ADR-099 — the release verifies served bytes, and the formula mirrors them
7
+
8
+ Status: **accepted** · 2026-10-27 · narrows ADR-098 ruling 3
9
+
10
+ Affects: `.github/workflows/publish.yml`, `scripts/build-sea.mjs`,
11
+ `scripts/verify-release-asset.mjs`, `Formula/simt-dev.rb`
12
+
13
+ ## Context
14
+
15
+ ADR-098 ruling 3 made determinism the release contract in its strongest form: the
16
+ same tree builds byte-identical assets on any host, so the tag workflow rebuilds
17
+ each asset and fails when its checksum is not the one `Formula/simt-dev.rb` in the
18
+ tagged tree promises. The v0.6.0 tag proved the strong form false. The CI-built
19
+ `linux-x64` asset hashed to `809dcda7…` while the same tree built on a darwin host
20
+ hashed to `b55bffc2…` — and the local rebuild is reproducible, so the difference is
21
+ the host, not the tree. The SEA blob is generated by a node running on the build
22
+ host, and a darwin-generated blob and a linux-generated blob of the same tree
23
+ differ. A check comparing CI bytes against locally-built committed checksums can
24
+ therefore never pass, and keeping it means every release fails at tag time for a
25
+ reason no tree change can fix.
26
+
27
+ The same run surfaced the second defect in the arrangement: all three targets built
28
+ on `ubuntu-latest`, but darwin binaries are finished with `codesign`, which exists
29
+ only on macOS. The darwin jobs never ran anywhere before v0.6.0 — v0.5.0 predates
30
+ the binary channel — so the runner assignment was never exercised either.
31
+
32
+ ## Ruling
33
+
34
+ 1. **Each target builds on its native runner.** `darwin-arm64` and `darwin-x64` on
35
+ `macos-14`, `linux-x64` on `ubuntu-latest`, with `fail-fast: false` so one
36
+ target's failure no longer cancels the others. Determinism is per host family —
37
+ the same tree rebuilt on the same OS family hashes identically — and the matrix
38
+ is what makes that claim checkable instead of aspirational.
39
+
40
+ 2. **The release checks upload integrity, not cross-host equality.** After uploading
41
+ each asset, the workflow downloads what GitHub serves for it
42
+ (`scripts/verify-release-asset.mjs`) and fails when the served sha256 is not the
43
+ one the build just wrote. A torn or clobbered upload graduates into a release
44
+ nobody can detect any other way; the tap installing bytes whose checksum refuses
45
+ them is exactly that failure arriving on somebody else's machine.
46
+
47
+ 3. **The committed formula is a mirror, refreshed after the release moves it — never
48
+ a prediction.** A `sync-formula` job renders `Formula/simt-dev.rb` from the
49
+ served bytes with `scripts/release-formula.mjs` and commits it to `main` when it
50
+ changed. A re-run that finds the mirror current exits quietly, and a formula
51
+ that already names a newer version is left alone, so an older tag can never
52
+ rewrite a newer mirror. The tap push (`publish-channels.yml`) already renders
53
+ from served bytes, so it was correct by construction throughout; this closes the
54
+ loop for the in-repo copy.
55
+
56
+ ## Consequences
57
+
58
+ - **The v0.6.0 equality check is gone, and its premise with it.** Nothing in the
59
+ release compares bytes built on different hosts. The `build-sea.mjs` header no
60
+ longer claims cross-host byte-identicality.
61
+ - **The formula in a tagged tree is the previous release's mirror, and that is
62
+ stated rather than hidden.** Between the tag and `sync-formula`'s commit it names
63
+ the served bytes of the last release, which is what makes it a mirror and not a
64
+ promise.
65
+ - **`verify-release-asset.mjs` is runnable by hand.** It takes `--version` and
66
+ `--target` and answers whether the release serves what a build of this tree
67
+ wrote — the same question the workflow asks, askable from any checkout.
68
+
69
+ ## Rejected
70
+
71
+ - **Generating one blob on one host and sharing it across targets.** It would make
72
+ the three assets share a blob while their node bases still differ per target, so
73
+ cross-host equality would still fail at the base — the part this repository does
74
+ not build. Sharing would add artifact plumbing to preserve a comparison the
75
+ release no longer makes.
76
+ - **Committing the formula from the tag before the assets exist.** That restores the
77
+ prediction in a new place: the committed checksums would again be bytes no
78
+ download has served, and the first thing that reads them (the tap) would read a
79
+ promise instead of a record.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "simt-dev",
3
- "version": "0.5.0",
4
- "description": "Shared agent rules, skills and git hooks for a repository: one command adopts a repo, and every gate it declares is probed before it is written.",
3
+ "version": "0.6.2",
4
+ "description": "An intelligence amplifier for AI coding agents: rules and dual gates adopted by one command, plus local retrieval, task briefs, benches and repo memory — deterministic by default, with local-model adapters that propose and never select.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Said Elimam",
7
7
  "private": false,
@@ -18,7 +18,7 @@
18
18
  "linux"
19
19
  ],
20
20
  "bin": {
21
- "simt": "bin/simt-dev.mjs"
21
+ "simt": "bin/simt.mjs"
22
22
  },
23
23
  "files": [
24
24
  "bin",
@@ -42,6 +42,6 @@
42
42
  ],
43
43
  "scripts": {
44
44
  "test": "node --test --test-concurrency=4 test/*.test.mjs",
45
- "lint": "node bin/simt-dev.mjs doctor"
45
+ "lint": "node bin/simt.mjs doctor"
46
46
  }
47
47
  }
@@ -1,6 +1,6 @@
1
1
  // One renderer per harness: the payload in, the decision out, and the config written.
2
2
  //
3
- // Four harnesses read a hook and no two agree on anything. The config path, the event
3
+ // Five harnesses read a hook and no two agree on anything. The config path, the event
4
4
  // name, the shape of the document, the tool name for a shell command, the key holding
5
5
  // the command, and the JSON a hook answers with are all different, and none of them is
6
6
  // the same fact. So this module owns the differences and `policy.mjs` owns none of
@@ -10,15 +10,17 @@
10
10
  // codex .codex/hooks.json PreToolUse {"hooks":{E:[{matcher,hooks:[h]}]}}
11
11
  // cursor .cursor/hooks.json preToolUse {"version":1,"hooks":{E:[h]}}
12
12
  // antigravity .agents/hooks.json PreToolUse {"<name>":{E:[{matcher,hooks:[h]}]}}
13
+ // gemini .gemini/settings.json BeforeTool {"hooks":{E:[{matcher,hooks:[h]}]}}
13
14
  //
14
15
  // The prompt-submit event (R29) is a second table, and it is shorter because the vendors
15
- // are: two of the four document a prompt event that can put text into the model's context,
16
- // and a delivery is the only thing this tool does there. Neither takes a matcher — both
16
+ // are: three of the five document a prompt event that can put text into the model's context,
17
+ // and a delivery is the only thing this tool does there. None takes a matcher — all
17
18
  // document that field for tool events, and a matcher a harness ignores is a matcher the
18
19
  // next person believes is filtering something.
19
20
  //
20
21
  // claude .claude/settings.json UserPromptSubmit hookSpecificOutput.additionalContext
21
22
  // codex .codex/hooks.json UserPromptSubmit hookSpecificOutput.additionalContext
23
+ // gemini .gemini/settings.json BeforeAgent hookSpecificOutput.additionalContext
22
24
  // cursor .cursor/hooks.json beforeSubmitPrompt CANNOT INJECT: block-only
23
25
  // antigravity .agents/hooks.json (no prompt event documented)
24
26
  //
@@ -26,10 +28,11 @@
26
28
  // can only continue or block, so the entry would carry no packet and a broken one would
27
29
  // stop a person from typing — and its only injection point, `sessionStart.additional_context`,
28
30
  // fires once when the session is created, which is a different lifetime from the brief a
29
- // task needs. Gemini CLI documents `BeforeAgent`, which does inject, but the product was
30
- // replaced by Antigravity CLI on 2026-06-18 and its config is not in the reader table this
31
- // module takes its paths from; a harness this tool did not verify is a harness it does not
32
- // write to.
31
+ // task needs. Gemini CLI was replaced by Antigravity CLI on 2026-06-18, which is the row
32
+ // that covers the successor; the gemini row stays because the extension is still documented
33
+ // and installable and R43 ships a package for it, and its hooks file is in the reader table
34
+ // this module takes its paths from — a harness this tool did not verify is a harness it
35
+ // does not write to.
33
36
  //
34
37
  // The paths are not written here. They come from the reader table (R14) because that is
35
38
  // where each one was verified against its vendor page, and a second copy of a verified
@@ -188,9 +191,43 @@ const ADAPTERS = {
188
191
  prompt: null,
189
192
  post: null,
190
193
  },
194
+ gemini: {
195
+ id: 'gemini',
196
+ event: 'BeforeTool',
197
+ matcher: 'run_shell_command|write_file|replace',
198
+ // The vendor's timeout unit is milliseconds (default 60000), not the seconds every
199
+ // other row uses; a bare 10 here would be a ten-millisecond budget, which is a hook
200
+ // that never finishes. The row carries its own value so the shared constant stays
201
+ // honest for the four vendors that measure in seconds.
202
+ timeout: 10000,
203
+ // The reference documents the decision JSON and an exit-2 system block; the denial
204
+ // travels in the JSON, as antigravity does.
205
+ denyExit: 0,
206
+ verified: 'geminicli.com/docs/hooks/reference/, read 2026-10-26',
207
+ root: (p) => p.cwd,
208
+ extract: (p) => ({
209
+ command: p.tool_name === 'run_shell_command' ? (p.tool_input?.command ?? null) : null,
210
+ writes: ['write_file', 'replace'].includes(p.tool_name) ? [p.tool_input?.file_path].filter(Boolean) : [],
211
+ }),
212
+ deny: (message) => ({ decision: 'deny', reason: message }),
213
+ // BeforeAgent appends text to the prompt for this turn only — the same lifetime as a
214
+ // brief. The documented output has no hookEventName field, unlike Claude's.
215
+ prompt: {
216
+ event: 'BeforeAgent',
217
+ verified: 'geminicli.com/docs/hooks/reference/, read 2026-10-26',
218
+ context: (text) => ({ hookSpecificOutput: { additionalContext: text } }),
219
+ },
220
+ // AfterTool appends to the tool result; its input carries tool_response, which is
221
+ // what the lesson recall reads.
222
+ post: {
223
+ event: 'AfterTool',
224
+ verified: 'geminicli.com/docs/hooks/reference/, read 2026-10-26',
225
+ context: (text) => ({ hookSpecificOutput: { additionalContext: text } }),
226
+ },
227
+ },
191
228
  };
192
229
 
193
- /** The four adapters, in the order the reader table lists them. */
230
+ /** The five adapters, in the order the reader table lists them. */
194
231
  export function adapters() {
195
232
  return hookRows()
196
233
  .filter((r) => r.path !== null)
@@ -203,9 +240,19 @@ export function adapter(id) {
203
240
  return row && hookPath(id) !== null ? row : null;
204
241
  }
205
242
 
206
- /** The facts a payload carries, and the root it names. An unknown shape yields none. */
207
- export function factsFrom(row, payload) {
208
- return { root: row.root(payload) ?? process.cwd(), ...row.extract(payload ?? {}) };
243
+ /**
244
+ * The facts a payload carries, and the root it names. An unknown shape yields none.
245
+ *
246
+ * `root` is a caller-supplied override that outranks the payload: a packaged hook runs
247
+ * from the plugin directory and the harness's environment variable names the project, so
248
+ * `--root` (from `$CLAUDE_PROJECT_DIR` and friends) must win over a payload cwd that is
249
+ * the plugin's own working directory. An empty override is treated as absent rather than
250
+ * trusted: an env var the harness did not set expands to `""` in the shell, and a root of
251
+ * the empty string would resolve every fact against the filesystem root.
252
+ */
253
+ export function factsFrom(row, payload, { root } = {}) {
254
+ const override = typeof root === 'string' && root.trim() !== '' ? root : null;
255
+ return { root: override ?? row.root(payload) ?? process.cwd(), ...row.extract(payload ?? {}) };
209
256
  }
210
257
 
211
258
  /** The document to print for a decision, or null when there is nothing to say. */
@@ -281,27 +328,33 @@ function eventsOf(row, { prompt = false, post = false } = {}) {
281
328
  return pairs;
282
329
  }
283
330
 
284
- /** The group this tool owns for one event, in that harness's shape. */
285
- function group(row, event = EVENTS[0]) {
286
- const command = hookCommand(row.id, event);
331
+ /**
332
+ * The group this tool owns for one event, in that harness's shape.
333
+ *
334
+ * `command` overrides the entry point for one build: a package ships a self-contained
335
+ * runtime and its config names that copy rather than the binary on PATH, so the shape
336
+ * comes from this table and only the path differs.
337
+ */
338
+ function group(row, event = EVENTS[0], command = hookCommand(row.id, event)) {
339
+ const timeout = row.timeout ?? TIMEOUT_SECONDS;
287
340
  if (event === 'prompt-submit') {
288
341
  // No matcher: both vendors document that field for tool events, and a matcher a
289
342
  // harness ignores is a matcher the next reader believes is filtering something.
290
- return { hooks: [{ type: 'command', command, timeout: TIMEOUT_SECONDS }] };
343
+ return { hooks: [{ type: 'command', command, timeout }] };
291
344
  }
292
345
  if (event === 'post-tool-use') {
293
346
  const matcher = row.post?.matcher;
294
347
  return matcher
295
- ? { matcher, hooks: [{ type: 'command', command, timeout: TIMEOUT_SECONDS }] }
296
- : { hooks: [{ type: 'command', command, timeout: TIMEOUT_SECONDS }] };
348
+ ? { matcher, hooks: [{ type: 'command', command, timeout }] }
349
+ : { hooks: [{ type: 'command', command, timeout }] };
297
350
  }
298
351
  if (row.id === 'cursor') {
299
352
  // failClosed is set because a hooks file this tool wrote should not silently
300
353
  // degrade to fail-open on its own crash: the vendor's default turns a broken hook
301
354
  // into an absent one, and the whole point of writing it is that it runs.
302
- return { command, timeout: TIMEOUT_SECONDS, matcher: row.matcher, failClosed: true };
355
+ return { command, timeout, matcher: row.matcher, failClosed: true };
303
356
  }
304
- return { matcher: row.matcher, hooks: [{ type: 'command', command, timeout: TIMEOUT_SECONDS }] };
357
+ return { matcher: row.matcher, hooks: [{ type: 'command', command, timeout }] };
305
358
  }
306
359
 
307
360
  /** Whether a group is this tool's, judged by the command it runs and not by position. */
@@ -335,11 +388,11 @@ export function installedIn(row, config, event = EVENTS[0]) {
335
388
  * asked for a delivered packet gets the tool hook and nothing extra, and the file it never
336
389
  * asked to change is a file this tool does not touch.
337
390
  */
338
- function withHook(row, config, { prompt = false, post = false } = {}) {
391
+ function withHook(row, config, { prompt = false, post = false, command } = {}) {
339
392
  const box = container(row, config, { create: true });
340
393
  for (const [event, name] of eventsOf(row, { prompt, post })) {
341
394
  const kept = entries(row, config, event).filter((e) => !ours(e));
342
- box[name] = [...kept, group(row, event)];
395
+ box[name] = [...kept, group(row, event, event === EVENTS[0] ? command : undefined)];
343
396
  }
344
397
  return config;
345
398
  }
@@ -359,3 +412,16 @@ export function rendered(row, existing, options = {}) {
359
412
  const text = `${JSON.stringify(withHook(row, config, options), null, 2)}\n`;
360
413
  return { text, action: text === existing ? 'unchanged' : 'updated' };
361
414
  }
415
+
416
+ /**
417
+ * A fresh config document carrying only this tool's hooks, for a package that ships its
418
+ * own runtime: the shape comes from the same table `install` merges into a repository,
419
+ * and only the entry point differs — the package's copy of the binary, named by `command`.
420
+ * Returns null for a harness this tool has no adapter for or that documents no hooks file.
421
+ */
422
+ export function hookDocument(id, { command, prompt = false, post = false } = {}) {
423
+ const row = adapter(id);
424
+ if (!row) return null;
425
+ const config = withHook(row, {}, { prompt, post, command });
426
+ return `${JSON.stringify(config, null, 2)}\n`;
427
+ }
package/src/argv.mjs CHANGED
@@ -84,6 +84,11 @@ export const BOOL_FLAGS = new Set([
84
84
  // the cache is the point - the same source at the same budget is the same answer,
85
85
  // and re-reading it is cheaper than re-deriving it (plan 0003 R38).
86
86
  'refresh',
87
+ // pack lint: reject a pack whose bench summary is missing or unmeasured.
88
+ // Off by default because a scaffold is born unmeasured - `pack new` writes a
89
+ // bench.json marked unmeasured, and a lint that failed it on arrival would make
90
+ // every new pack red before its author wrote the rule (plan 0003 R45).
91
+ 'require-measured',
87
92
  // distill: propose the summary from the configured local chat model instead of
88
93
  // the deterministic outline. Off by default: no model in the default path (P1),
89
94
  // and a model that is not asked for should not be opened against (ADR-090).
@@ -138,6 +143,14 @@ export const VALUE_FLAGS = new Set([
138
143
  'level',
139
144
  // decide: structured supersession spec (ADR-NNN or ADR-NNN:<ruling>[:<kind>], repeatable).
140
145
  'supersedes',
146
+ // pack new: where the pack's bytes come from. Required, because a pack without
147
+ // a source is bytes nobody can obtain again - and a source nobody can act on
148
+ // is refused by lint rather than blessed (plan 0003 R45).
149
+ 'source',
150
+ // plugin: where the generated package lands. A value rather than a boolean because
151
+ // "generate a package" with no destination would have to guess, and a guess about where
152
+ // a directory tree goes is the one failure that leaves files behind.
153
+ 'out',
141
154
  ]);
142
155
 
143
156
  /**
@@ -10,7 +10,7 @@
10
10
  import { mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises';
11
11
  import { dirname, join, resolve } from 'node:path';
12
12
 
13
- import { CONFIG_DIR, MANIFEST_FILE } from '../name.mjs';
13
+ import { MANIFEST_FILE, STATE_DIR } from '../name.mjs';
14
14
  import { readManifest } from '../projection/manifest.mjs';
15
15
  import { inject, parse as parseRegion } from '../projection/markers.mjs';
16
16
  import { loadMdcRules, renderMdc, ruleName } from '../projection/mdc.mjs';
@@ -27,8 +27,6 @@ import {
27
27
  loadRunRecords,
28
28
  } from './report.mjs';
29
29
 
30
- const STATE_DIR = `.${CONFIG_DIR}`;
31
-
32
30
  export const DEFAULT_CANARY_SIZE = 10;
33
31
  export const DEL_R_TOLERANCE = 0.01;
34
32
  export const CANARY_ALPHA = 0.05;
@@ -148,7 +148,7 @@ export async function gradeWorkspace(workDir, task, { vaultRoot, initialHead, ti
148
148
  .toString('utf8')
149
149
  .split('\n')
150
150
  .map((s) => s.trim())
151
- .filter((f) => Boolean(f) && !f.startsWith('.simt-dev/'));
151
+ .filter((f) => Boolean(f) && !f.startsWith('.simt/'));
152
152
 
153
153
  const goldFiles = new Set(extractPatchFiles(goldPatch));
154
154
  const outsideBlastRadius =
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // Hidden test patches live in `~/.cache/simt-dev/vault/<repo-id>/<task-id>/`
10
10
  // outside every worktree (RepoTrials / PatchGym), while deterministic task
11
- // descriptors live in `.simt-dev/bench/tasks/<id>.json`.
11
+ // descriptors live in `.simt/bench/tasks/<id>.json`.
12
12
 
13
13
  import { execFile, spawn } from 'node:child_process';
14
14
  import { createHash } from 'node:crypto';
@@ -661,7 +661,7 @@ async function validateCandidate(root, worktree, candidate, { timeoutMs = 5000 }
661
661
  * Mine verified benchmark tasks from a repository's git history.
662
662
  *
663
663
  * When `apply` is false (the default without `--yes`, or under `--dry-run`),
664
- * candidates are discovered and reported without writing to `.simt-dev/bench/tasks/`
664
+ * candidates are discovered and reported without writing to `.simt/bench/tasks/`
665
665
  * or the vault. When `apply` is true, candidates are validated in isolated
666
666
  * temporary worktrees until `target` valid tasks are collected, and written
667
667
  * deterministically to disk.
@@ -685,7 +685,7 @@ export async function mineTasks(
685
685
  return { ok: false, error: listed.error };
686
686
  }
687
687
 
688
- const tasksDir = join(absRoot, '.simt-dev', 'bench', 'tasks');
688
+ const tasksDir = join(absRoot, '.simt', 'bench', 'tasks');
689
689
  const vaultRoot = resolveVaultDir(absRoot, { env });
690
690
 
691
691
  if (!apply) {
@@ -25,12 +25,10 @@ import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs
25
25
  import { tmpdir } from 'node:os';
26
26
  import { dirname, join, resolve } from 'node:path';
27
27
 
28
- import { CONFIG_DIR } from '../name.mjs';
28
+ import { STATE_DIR } from '../name.mjs';
29
29
  import { parse as parseRegion } from '../projection/markers.mjs';
30
30
  import { materialiseArm, parseArmSpec } from './run.mjs';
31
31
 
32
- const STATE_DIR = `.${CONFIG_DIR}`;
33
-
34
32
  function sanitizedEnv(extra = {}) {
35
33
  const env = { ...process.env, SIMT_DEV_WORKSPACE: 'off', ...extra };
36
34
  for (const key of Object.keys(env)) {
@@ -8,9 +8,7 @@
8
8
  import { readdir, readFile } from 'node:fs/promises';
9
9
  import { join, resolve } from 'node:path';
10
10
 
11
- import { CONFIG_DIR } from '../name.mjs';
12
-
13
- const STATE_DIR = `.${CONFIG_DIR}`;
11
+ import { STATE_DIR } from '../name.mjs';
14
12
 
15
13
  export const DEFAULT_BOOTSTRAP_SAMPLES = 1000;
16
14
  export const DEFAULT_BOOTSTRAP_SEED = 0x51772026;
package/src/bench/run.mjs CHANGED
@@ -9,7 +9,7 @@ import { loadRegistry } from '../graph/load.mjs';
9
9
  import { extractOpenedSkills, matchSkillsForPaths } from '../graph/compile.mjs';
10
10
  import { matchGlob } from '../index/govern.mjs';
11
11
  import { formatLessonParagraph, listPromotedLessons } from '../memory/lessons.mjs';
12
- import { CONFIG_DIR, MANIFEST_FILE } from '../name.mjs';
12
+ import { MANIFEST_FILE, STATE_DIR } from '../name.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';
@@ -21,8 +21,6 @@ import { loadTasks } from './tasks.mjs';
21
21
  import { ablateSectionInWorkspace, parseAblationArmName } from './ablate.mjs';
22
22
  import { applyVariantInWorkspace, parseVariantArmName } from './variants.mjs';
23
23
 
24
- const STATE_DIR = `.${CONFIG_DIR}`;
25
-
26
24
  const BENCH_DIR = dirname(fileURLToPath(import.meta.url));
27
25
  const ADAPTERS_PATH = join(BENCH_DIR, 'adapters.json');
28
26
 
@@ -9,9 +9,7 @@
9
9
  // part of this touches a model, a network or a clock.
10
10
  import { readdir, readFile } from 'node:fs/promises';
11
11
  import { join, resolve } from 'node:path';
12
- import { CONFIG_DIR } from '../name.mjs';
13
-
14
- const STATE_DIR = `.${CONFIG_DIR}`;
12
+ import { STATE_DIR } from '../name.mjs';
15
13
 
16
14
  /**
17
15
  * Load mined benchmark task descriptors.
@@ -10,7 +10,7 @@ import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises';
10
10
  import { dirname, join, resolve } from 'node:path';
11
11
 
12
12
  import { extractVariantsFromRule, readLockedVariants } from '../kernel/variants.mjs';
13
- import { CONFIG_DIR, MANIFEST_FILE } from '../name.mjs';
13
+ import { MANIFEST_FILE, STATE_DIR } from '../name.mjs';
14
14
  import { writeLockVariants } from '../projection/lock.mjs';
15
15
  import { readManifest } from '../projection/manifest.mjs';
16
16
  import { loadMdcRules } from '../projection/mdc.mjs';
@@ -27,8 +27,6 @@ import {
27
27
  loadRunRecords,
28
28
  } from './report.mjs';
29
29
 
30
- const STATE_DIR = `.${CONFIG_DIR}`;
31
-
32
30
  /**
33
31
  * Discover every `simt-variants` slot across applicable rules for `root`.
34
32
  */
@@ -34,14 +34,13 @@
34
34
  // command and the tool leave it on. It is inert either way in a repository that declared no
35
35
  // servers.
36
36
 
37
- import { CLI } from '../name.mjs';
37
+ import { CLI, STATE_DIR } from '../name.mjs';
38
38
  import { DEFAULT_BRIEF_BYTES, buildPacket, collect } from './packet.mjs';
39
39
  import { classifyTask, cardVerbs } from './kind.mjs';
40
40
  import { briefAdvice } from '../advice/advice.mjs';
41
41
  import { commandCard } from '../kernel/card.mjs';
42
42
  import { decisionHits, scopeHits, sourceRoots } from '../index/govern.mjs';
43
43
  import { embedTaskQuery, rankFiles, renderMap } from '../index/map.mjs';
44
- import { SQLITE_FILE } from '../index/persist.mjs';
45
44
  import { searchFtsBm25 } from '../index/store.mjs';
46
45
  import { testEdges, testsFor } from '../index/tests.mjs';
47
46
  import { loadRegistry } from '../graph/load.mjs';
@@ -81,7 +80,6 @@ const MAP_FACTOR = 8;
81
80
  * between two identical commands is not the packet this promises. The tool's own scratch
82
81
  * space is never part of a task's scope.
83
82
  */
84
- const STATE_DIR = `${SQLITE_FILE.split('/')[0]}/`;
85
83
 
86
84
  /** Every file git tracks that carries a generated region, one `git grep` away. */
87
85
  async function generatedRegions(root) {
@@ -128,7 +126,7 @@ export async function assembleBrief({
128
126
  federate = true,
129
127
  } = {}) {
130
128
  const named = paths !== null;
131
- const dirty = (await dirtyPaths(root)).filter((path) => !path.startsWith(STATE_DIR));
129
+ const dirty = (await dirtyPaths(root)).filter((path) => !path.startsWith(`${STATE_DIR}/`));
132
130
  const scope = named ? parsePathList(paths) : dirty;
133
131
 
134
132
  const graph = await loadGraph(root, { build });
@@ -21,7 +21,7 @@
21
21
  // rewritten, their file from the first commit. A repo that wants a different
22
22
  // cadence, a private registry or an `ignore` rule edits it, and the edit
23
23
  // stays. simt-dev's own repository is the ADR-024 exception and is refreshed
24
- // by `simt-dev ci`.
24
+ // by `simt ci`.
25
25
  //
26
26
  // The content is detection-driven, which is the ADR-013 shape: the ecosystems
27
27
  // listed are the ones this tree actually has a manifest for. Writing an `npm`
@@ -1,6 +1,6 @@
1
1
  // The CI gate, as a file a repo owns.
2
2
  //
3
- // `simt-dev check` is the whole gate: projection drift, an unreadable rule, an
3
+ // `simt check` is the whole gate: projection drift, an unreadable rule, an
4
4
  // unrunnable gate, a stale manifest. The pre-commit hook runs it locally, which
5
5
  // stops one person committing a broken region and stops nobody else - a hook is a
6
6
  // developer's machine, and every review is somebody else's. So the same command
@@ -28,7 +28,7 @@
28
28
  // which is why the header says so in the file rather than in this comment.
29
29
  //
30
30
  // The own copy also RUNS something different, and that is not a variation for taste.
31
- // `simt-dev check` is red in this repository by design: AGENTS.md here is written by
31
+ // `simt check` is red in this repository by design: AGENTS.md here is written by
32
32
  // hand and deliberately never self-adopted (see its own "Why this repository is not
33
33
  // self-adopted"), so the check reports it as unmanaged forever. A gate that is red on
34
34
  // the day it lands is the one thing worse than no gate - it teaches the reader to
@@ -51,6 +51,7 @@ import { PACKAGE_NAME, WORKFLOW_FILE_NAME } from '../name.mjs';
51
51
  import { renderLocator } from '../locate.mjs';
52
52
  import { CLI } from '../name.mjs';
53
53
  import { packageVersion } from '../package-version.mjs';
54
+ import { LOCAL_BIN, LOCAL_BIN_DIR, assetNameExpr, localInstall, platformLines, releaseBase } from '../distribute/release.mjs';
54
55
 
55
56
  /** Directory whose presence means this repo runs GitHub Actions. */
56
57
  export const WORKFLOWS_DIR = '.github/workflows';
@@ -127,13 +128,15 @@ function header(own) {
127
128
  * is the release a reader recognises, and Dependabot's `github-actions` ecosystem
128
129
  * rewrites the pair together - which is why the pin needs no maintenance here.
129
130
  *
130
- * The major version is deliberately the one the templates already used rather than the
131
- * newest available. `checkout` v7 and `setup-node` v7 both carry breaking changes, and
132
- * a gate whose behaviour changes because somebody re-ran an updater is precisely what
133
- * pinning exists to prevent. Moving a major is one reviewed line in this table.
131
+ * The major version moves by review, not by updater. `checkout` stayed on v4 while v7
132
+ * carried breaking changes a gate should not absorb by re-running Dependabot; it moved
133
+ * to v7.0.1 when the v7 line had run green in this repository's own workflows (the
134
+ * checkout steps of the v7 Dependabot run all succeeded — only the template-drift
135
+ * tests failed, which is the pin doing its job). `setup-node` stays on v4: no run has
136
+ * yet proven a newer major here, and moving it is the same one reviewed line.
134
137
  */
135
138
  export const ACTIONS = {
136
- checkout: { repo: 'actions/checkout', commit: '11d5960a326750d5838078e36cf38b85af677262', release: 'v4.4.0' },
139
+ checkout: { repo: 'actions/checkout', commit: '3d3c42e5aac5ba805825da76410c181273ba90b1', release: 'v7.0.1' },
137
140
  setupNode: { repo: 'actions/setup-node', commit: '49933ea5288caeca8642d1e84afbd3f7d6820020', release: 'v4.4.0' },
138
141
  };
139
142
 
@@ -264,7 +267,7 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
264
267
  ' - name: tests',
265
268
  ' run: npm test',
266
269
  ' - name: skill registry lint',
267
- ' run: node bin/simt-dev.mjs graph lint',
270
+ ' run: node bin/simt.mjs graph lint',
268
271
  ' - name: the gate copy in this repo is current',
269
272
  // The drift this exception exists to prevent, checked where it would be caught.
270
273
  // Without it a template change lands with the copy still describing the old
@@ -272,7 +275,7 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
272
275
  //
273
276
  // `--check`, and not the bare command: `ci` previews by default and exits 0,
274
277
  // so this step used to print the drift it exists to catch and pass anyway.
275
- ' run: node bin/simt-dev.mjs ci --check',
278
+ ' run: node bin/simt.mjs ci --check',
276
279
  ].join('\n'),
277
280
  // ADR-012 says CI verifies on macOS and Linux. Until this matrix existed that
278
281
  // sentence was false in the one repository that could have made it true, and no
@@ -285,9 +288,19 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
285
288
  const v = version ?? packageVersion();
286
289
  // The install step is the whole of S35's consumer CI: a repository with npm pins
287
290
  // simt-dev as a devDependency and `npm ci` restores it; one without (Gradle,
288
- // Connect IQ) has no node_modules to pin in, so CI installs the same pinned global
289
- // binary the machine remedy names. The two must agree, or the gate passes in one
290
- // place and cannot start in the other.
291
+ // Connect IQ) has no node_modules to pin in, so CI downloads the release binary
292
+ // into the repository - the same pinned bytes the machine remedy names, verified
293
+ // against the checksum published beside it. The two must agree, or the gate
294
+ // passes in one place and cannot start in the other.
295
+ //
296
+ // The binary branch needs no setup-node at all: the SEA runs without a Node on
297
+ // PATH (ADR-098), and installing one to run a file that does not need it would be
298
+ // the toolchain this channel exists to remove, present in the one place it was not.
299
+ //
300
+ // The platform is derived in the shell (platformLines) and not from a
301
+ // `${{ runner.* }}` expression: an expression substituted into `$( … )` is a
302
+ // command substitution trying to execute "Linux", which set -eu refuses - the
303
+ // install would fail on every run in the one repository kind this channel is for.
291
304
  const install = npm
292
305
  ? [
293
306
  ' - name: install dependencies',
@@ -296,19 +309,36 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
296
309
  ]
297
310
  : [
298
311
  ' - name: install the pinned gate binary',
299
- ' # No node_modules to pin in, so a pinned global install: the same bytes',
300
- ' # the machine remedy names, so the gate cannot pass locally and fail here.',
301
- ` run: npm i -g ${PACKAGE_NAME}@${v}`,
312
+ ' # No node_modules to pin in, so the release binary into the repository:',
313
+ ' # the same bytes the machine remedy names, checked against the',
314
+ ' # checksum published beside it before anything runs them.',
315
+ ' run: |',
316
+ indent(
317
+ `set -eu
318
+ ${platformLines().join('\n')}
319
+ asset="${assetNameExpr(v)}"
320
+ base="${releaseBase(v)}"
321
+ curl -fsSL "$base/$asset" -o "$asset"
322
+ expected=$(curl -fsSL "$base/$asset.sha256" | awk '{print $1}')
323
+ actual=$(openssl sha256 "$asset" | grep -oE '[0-9a-f]{64}')
324
+ if [ "$expected" != "$actual" ]; then
325
+ echo "checksum mismatch for $asset: the release asset does not match its published sha256" >&2
326
+ exit 1
327
+ fi
328
+ mkdir -p ${LOCAL_BIN_DIR}
329
+ mv "$asset" ${LOCAL_BIN}
330
+ chmod +x ${LOCAL_BIN}`, 10),
302
331
  ];
303
332
  const remedy = npm
304
333
  ? `REMEDY run: npm i -D ${PACKAGE_NAME} (the gate runs from the pinned dependency)`
305
- : `REMEDY run: npm i -g ${PACKAGE_NAME}@${v} (the gate runs from a pinned global install)`;
334
+ : `REMEDY run: ${localInstall(v)} (the gate runs from the release binary in .simt/bin)`;
306
335
  const locator = renderLocator('check --root "$ROOT"', { remedy });
307
336
  return `${header(false)}${skeleton(
308
337
  `${CLI} check`,
309
338
  [
310
339
  uses('checkout'),
311
- setupNode(nodeFloor()),
340
+ // The binary branch has no setup-node: see the install step above.
341
+ ...(npm ? [setupNode(nodeFloor())] : []),
312
342
  ...install,
313
343
  ` - name: ${CLI} check`,
314
344
  ' run: |',