simt-dev 0.5.0 → 0.6.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 (84) hide show
  1. package/CHANGELOG.md +122 -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/package.json +4 -4
  8. package/src/agenthooks/render.mjs +87 -21
  9. package/src/argv.mjs +13 -0
  10. package/src/bench/ablate.mjs +1 -3
  11. package/src/bench/grade.mjs +1 -1
  12. package/src/bench/mine.mjs +3 -3
  13. package/src/bench/probes.mjs +1 -3
  14. package/src/bench/report.mjs +1 -3
  15. package/src/bench/run.mjs +1 -3
  16. package/src/bench/tasks.mjs +1 -3
  17. package/src/bench/variants.mjs +1 -3
  18. package/src/brief/assemble.mjs +2 -4
  19. package/src/ci/dependabot.mjs +1 -1
  20. package/src/ci/workflow.mjs +40 -12
  21. package/src/commands/agenthook.mjs +6 -2
  22. package/src/commands/bench.mjs +4 -4
  23. package/src/commands/ci.mjs +1 -1
  24. package/src/commands/compare.mjs +1 -1
  25. package/src/commands/distill.mjs +1 -1
  26. package/src/commands/gate.mjs +2 -2
  27. package/src/commands/graph.mjs +2 -2
  28. package/src/commands/harness.mjs +87 -14
  29. package/src/commands/help.mjs +29 -7
  30. package/src/commands/hooks.mjs +1 -1
  31. package/src/commands/index.mjs +17 -3
  32. package/src/commands/init.mjs +2 -2
  33. package/src/commands/local.mjs +11 -0
  34. package/src/commands/mcp.mjs +1 -1
  35. package/src/commands/pack.mjs +246 -0
  36. package/src/commands/plugin.mjs +61 -0
  37. package/src/commands/rules.mjs +1 -1
  38. package/src/commands/skills.mjs +1 -1
  39. package/src/commands/skillscompile.mjs +1 -3
  40. package/src/commands/skillsinstall.mjs +1 -1
  41. package/src/commands/sync.mjs +8 -8
  42. package/src/commands/tokens.mjs +1 -1
  43. package/src/commands/upgrade.mjs +1 -1
  44. package/src/commands/why.mjs +1 -1
  45. package/src/compare/discover.mjs +1 -1
  46. package/src/compare/hooksstate.mjs +19 -7
  47. package/src/compare/learn.mjs +2 -2
  48. package/src/compare/row.mjs +3 -3
  49. package/src/decisions/index.mjs +1 -1
  50. package/src/distill/distill.mjs +2 -2
  51. package/src/distribute/release.mjs +142 -0
  52. package/src/graph/load.mjs +9 -8
  53. package/src/hooks/dispatcher.mjs +3 -2
  54. package/src/hooks/shim.mjs +11 -6
  55. package/src/index/govern.mjs +1 -1
  56. package/src/index/map.mjs +1 -1
  57. package/src/index/persist.mjs +2 -2
  58. package/src/index/store.mjs +50 -20
  59. package/src/ledger/index.mjs +2 -2
  60. package/src/local/client.mjs +22 -8
  61. package/src/local/config.mjs +17 -0
  62. package/src/locate.mjs +22 -1
  63. package/src/mcp/prompts.mjs +118 -0
  64. package/src/mcp/resources.mjs +96 -22
  65. package/src/mcp/server.mjs +33 -6
  66. package/src/mcp/tools.mjs +12 -12
  67. package/src/measure/session.mjs +1 -1
  68. package/src/memory/decide.mjs +1 -1
  69. package/src/memory/lessons.mjs +14 -14
  70. package/src/name.mjs +11 -1
  71. package/src/package-version.mjs +12 -10
  72. package/src/packs/manage.mjs +563 -0
  73. package/src/packs.mjs +15 -5
  74. package/src/plugins.mjs +164 -0
  75. package/src/projection/loaders.mjs +84 -10
  76. package/src/projection/manifest.mjs +4 -4
  77. package/src/projection/project.mjs +3 -3
  78. package/src/projection/rules.mjs +24 -4
  79. package/src/projection/surfaces.mjs +20 -2
  80. package/src/review/critic.mjs +4 -4
  81. package/src/toolwords.mjs +8 -5
  82. package/src/tsconfig.mjs +3 -3
  83. package/src/upgrade/plan.mjs +1 -1
  84. /package/bin/{simt-dev.mjs → simt.mjs} +0 -0
@@ -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';
@@ -264,7 +265,7 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
264
265
  ' - name: tests',
265
266
  ' run: npm test',
266
267
  ' - name: skill registry lint',
267
- ' run: node bin/simt-dev.mjs graph lint',
268
+ ' run: node bin/simt.mjs graph lint',
268
269
  ' - name: the gate copy in this repo is current',
269
270
  // The drift this exception exists to prevent, checked where it would be caught.
270
271
  // Without it a template change lands with the copy still describing the old
@@ -272,7 +273,7 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
272
273
  //
273
274
  // `--check`, and not the bare command: `ci` previews by default and exits 0,
274
275
  // so this step used to print the drift it exists to catch and pass anyway.
275
- ' run: node bin/simt-dev.mjs ci --check',
276
+ ' run: node bin/simt.mjs ci --check',
276
277
  ].join('\n'),
277
278
  // ADR-012 says CI verifies on macOS and Linux. Until this matrix existed that
278
279
  // sentence was false in the one repository that could have made it true, and no
@@ -285,9 +286,19 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
285
286
  const v = version ?? packageVersion();
286
287
  // The install step is the whole of S35's consumer CI: a repository with npm pins
287
288
  // 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.
289
+ // Connect IQ) has no node_modules to pin in, so CI downloads the release binary
290
+ // into the repository - the same pinned bytes the machine remedy names, verified
291
+ // against the checksum published beside it. The two must agree, or the gate
292
+ // passes in one place and cannot start in the other.
293
+ //
294
+ // The binary branch needs no setup-node at all: the SEA runs without a Node on
295
+ // PATH (ADR-098), and installing one to run a file that does not need it would be
296
+ // the toolchain this channel exists to remove, present in the one place it was not.
297
+ //
298
+ // The platform is derived in the shell (platformLines) and not from a
299
+ // `${{ runner.* }}` expression: an expression substituted into `$( … )` is a
300
+ // command substitution trying to execute "Linux", which set -eu refuses - the
301
+ // install would fail on every run in the one repository kind this channel is for.
291
302
  const install = npm
292
303
  ? [
293
304
  ' - name: install dependencies',
@@ -296,19 +307,36 @@ export function renderWorkflow({ own = false, npm = true, version } = {}) {
296
307
  ]
297
308
  : [
298
309
  ' - 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}`,
310
+ ' # No node_modules to pin in, so the release binary into the repository:',
311
+ ' # the same bytes the machine remedy names, checked against the',
312
+ ' # checksum published beside it before anything runs them.',
313
+ ' run: |',
314
+ indent(
315
+ `set -eu
316
+ ${platformLines().join('\n')}
317
+ asset="${assetNameExpr(v)}"
318
+ base="${releaseBase(v)}"
319
+ curl -fsSL "$base/$asset" -o "$asset"
320
+ expected=$(curl -fsSL "$base/$asset.sha256" | awk '{print $1}')
321
+ actual=$(openssl sha256 "$asset" | grep -oE '[0-9a-f]{64}')
322
+ if [ "$expected" != "$actual" ]; then
323
+ echo "checksum mismatch for $asset: the release asset does not match its published sha256" >&2
324
+ exit 1
325
+ fi
326
+ mkdir -p ${LOCAL_BIN_DIR}
327
+ mv "$asset" ${LOCAL_BIN}
328
+ chmod +x ${LOCAL_BIN}`, 10),
302
329
  ];
303
330
  const remedy = npm
304
331
  ? `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)`;
332
+ : `REMEDY run: ${localInstall(v)} (the gate runs from the release binary in .simt/bin)`;
306
333
  const locator = renderLocator('check --root "$ROOT"', { remedy });
307
334
  return `${header(false)}${skeleton(
308
335
  `${CLI} check`,
309
336
  [
310
337
  uses('checkout'),
311
- setupNode(nodeFloor()),
338
+ // The binary branch has no setup-node: see the install step above.
339
+ ...(npm ? [setupNode(nodeFloor())] : []),
312
340
  ...install,
313
341
  ` - name: ${CLI} check`,
314
342
  ' run: |',
@@ -1,4 +1,4 @@
1
- // simt-dev agent-hook <event> | agent-hooks install | agent-hooks status
1
+ // simt agent-hook <event> | agent-hooks install | agent-hooks status
2
2
  //
3
3
  // Three commands, one subject: the rules the tool projects, enforced while the agent is
4
4
  // working rather than when git is committing. `agent-hook` is the entry point a harness
@@ -92,7 +92,11 @@ export async function agentHookCommand(args, flags) {
92
92
  return EXIT.OK;
93
93
  }
94
94
 
95
- const { root, command, writes } = factsFrom(row, payload);
95
+ // `--root` outranks the payload: a packaged hook runs from the plugin directory and the
96
+ // harness's environment variable names the project, so a payload cwd that is the plugin's
97
+ // own working directory must not win. The repository-install path passes no flag, and the
98
+ // payload's cwd is what the harness reports.
99
+ const { root, command, writes } = factsFrom(row, payload, { root: flags.root });
96
100
  const facts = await factsFor(root, { command, writes });
97
101
  const decision = decide({ event, facts, env: process.env });
98
102
 
@@ -2,7 +2,7 @@
2
2
  // and statistical report (`bench report`, R03).
3
3
  //
4
4
  // Like every writer in this CLI, `bench mine` and `bench run` are previews by
5
- // default and write artifacts to `.simt-dev/bench/` only when authorised with
5
+ // default and write artifacts to `.simt/bench/` only when authorised with
6
6
  // `--yes` (and not `--dry-run`). `bench report` is read-only.
7
7
 
8
8
  import { resolve } from 'node:path';
@@ -91,7 +91,7 @@ export async function benchRunCommand(positional, flags) {
91
91
  return fail(
92
92
  `${root}: ${error.message ?? error}`,
93
93
  EXIT.CONFIG,
94
- `pass a stored run name under .simt-dev/bench/runs/: ${CLI} bench run --regrade <run>`,
94
+ `pass a stored run name under .simt/bench/runs/: ${CLI} bench run --regrade <run>`,
95
95
  );
96
96
  }
97
97
 
@@ -222,7 +222,7 @@ export async function benchReportCommand(positional, flags) {
222
222
  return fail(
223
223
  `${root}: ${error.message ?? error}`,
224
224
  EXIT.CONFIG,
225
- `run benchmark arms first with \`${CLI} bench run --yes\` or fix .simt-dev/bench/prices.json`,
225
+ `run benchmark arms first with \`${CLI} bench run --yes\` or fix .simt/bench/prices.json`,
226
226
  );
227
227
  }
228
228
 
@@ -405,7 +405,7 @@ export async function benchVariantsCommand(positional, flags) {
405
405
  // and until a bench here shows that, every report calls the critic advisory.
406
406
  export async function benchCriticCommand(positional, flags, commands = []) {
407
407
  const root = resolve(flags.root ?? positional[0] ?? process.cwd());
408
- const labelsPath = flags.labels ?? '.simt-dev/critic-labels.jsonl';
408
+ const labelsPath = flags.labels ?? '.simt/critic-labels.jsonl';
409
409
  const result = await criticBench(root, { labelsPath, flags, commands });
410
410
  if (typeof result === 'number') return result;
411
411
 
@@ -1,4 +1,4 @@
1
- // simt-dev ci — the .github files, in the one repository whose copies are refreshed.
1
+ // simt ci — the .github files, in the one repository whose copies are refreshed.
2
2
  //
3
3
  // Why this exists rather than a flag on `init`: the write-once rule is about a
4
4
  // repository's own file, and simt-dev's own repository has no file of its own here —
@@ -1,4 +1,4 @@
1
- // simt-dev compare - the org-wide matrix.
1
+ // simt compare - the org-wide matrix.
2
2
  //
3
3
  // This command exists because the 2026-10-05 audit had to be reconstructed by
4
4
  // hand, from 16 repositories, into 40 KB of prose. Everything it found is a fact
@@ -1,7 +1,7 @@
1
1
  // `simt distill <skill|doc>` (plan 0003 R38).
2
2
  //
3
3
  // A budgeted summary with citations to source line ranges, written to
4
- // `.simt-dev/distilled/` as a proposal a human merges. The deterministic engine
4
+ // `.simt/distilled/` as a proposal a human merges. The deterministic engine
5
5
  // is the default; a local model proposes only when the caller asks for it and the
6
6
  // manifest names one (ADR-090, ADR-075). Model output never lands anywhere
7
7
  // projected or committed text does — that is the direction this command cannot
@@ -117,11 +117,11 @@ async function runSelected(root, flags, selected) {
117
117
 
118
118
  async function writeRaw(root, body) {
119
119
  try {
120
- const dir = join(root, '.simt-dev', 'gate');
120
+ const dir = join(root, '.simt', 'gate');
121
121
  await mkdir(dir, { recursive: true });
122
122
  const name = `gate-${Date.now()}.log`;
123
123
  await writeFile(join(dir, name), body);
124
- return `.simt-dev/gate/${name}`;
124
+ return `.simt/gate/${name}`;
125
125
  } catch {
126
126
  return '';
127
127
  }
@@ -1,4 +1,4 @@
1
- // `simt-dev graph lint` - verify the hand-authored registry against reality.
1
+ // `simt graph lint` - verify the hand-authored registry against reality.
2
2
  //
3
3
  // Implementation sequence step 2. This command is the reason hand-authoring the
4
4
  // registry is safe: 38 nodes across 6 repos is bookkeeping that rots silently.
@@ -64,7 +64,7 @@ export async function lintCommand(positional, flags) {
64
64
  }
65
65
 
66
66
  if (!registry) {
67
- // The default remedy for this exit code is `run: simt-dev graph lint`, which from
67
+ // The default remedy for this exit code is `run: simt graph lint`, which from
68
68
  // inside `graph lint` is circular: the reader just ran it and got this. Name the
69
69
  // argument the command takes, because an explicit path is the way out (ADR-061).
70
70
  return fail(errors[0] ?? `${path}: unreadable`, EXIT.REGISTRY, `run: ${CLI} graph lint <path-to-registry>`);