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
@@ -0,0 +1,61 @@
1
+ // simt plugin <claude|gemini|cursor> --out <dir> [--yes]
2
+ //
3
+ // Build one harness package from the same tables `agent-hooks install` writes into a
4
+ // repository, so a package and a repository install cannot drift apart. A dry run prints
5
+ // the plan and writes nothing; --yes copies the runtime and writes every generated file.
6
+ // The package is self-contained — it carries its own copy of the runtime — so it installs
7
+ // into a clean profile that has never seen this tool (plan 0003 R43).
8
+
9
+ import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.mjs';
10
+ import { CLI } from '../name.mjs';
11
+ import { buildPackage, PLUGIN_KINDS } from '../plugins.mjs';
12
+
13
+ export async function pluginCommand(args, flags) {
14
+ const kind = args[0];
15
+ if (!kind || !PLUGIN_KINDS.includes(kind)) {
16
+ return fail(
17
+ kind ? `unknown package kind \`${kind}\`` : 'plugin needs a kind',
18
+ EXIT.USAGE,
19
+ `one of: ${PLUGIN_KINDS.join(', ')}`,
20
+ );
21
+ }
22
+
23
+ const out = flags.out;
24
+ if (!out) {
25
+ return fail('plugin needs --out <dir>', EXIT.USAGE, 'where the generated package lands');
26
+ }
27
+
28
+ const { apply } = writeMode(flags);
29
+ const root = flags.root ?? process.cwd();
30
+ const plan = await buildPackage(kind, { root, out, apply });
31
+
32
+ if (isJson(flags)) {
33
+ emitJson({
34
+ ok: true,
35
+ command: `plugin ${kind}`,
36
+ out,
37
+ dryRun: !apply,
38
+ runtime: plan.runtime,
39
+ files: plan.files,
40
+ notes: plan.notes,
41
+ });
42
+ return EXIT.OK;
43
+ }
44
+
45
+ const runtimeTarget = plan.runtime.target === '.' ? '(the package root)' : `${plan.runtime.target}/`;
46
+ emit(
47
+ [
48
+ `kind ${kind}`,
49
+ `out ${out}`,
50
+ `runtime copies ${plan.runtime.parts.join(', ')} into ${runtimeTarget}`,
51
+ ...plan.files.map((f) => `${apply ? 'WRITE' : 'PLAN '} ${f.path}`),
52
+ ...plan.notes.map((n) => `note ${n}`),
53
+ '',
54
+ apply
55
+ ? `WROTE ${plan.files.length} generated file(s) plus the runtime`
56
+ : `PLAN nothing written; apply with: ${CLI} plugin ${kind} --out ${out} --yes`,
57
+ ].join('\n'),
58
+ { maxBytes: flags.maxBytes ?? 0 },
59
+ );
60
+ return EXIT.OK;
61
+ }
@@ -1,4 +1,4 @@
1
- // `simt-dev rules` - the rule catalogue and its provenance.
1
+ // `simt rules` - the rule catalogue and its provenance.
2
2
  //
3
3
  // A rule is only meaningful if you can answer "why is this here?". Three
4
4
  // commands, mirroring the skill surface deliberately: `list` is the catalogue,
@@ -1,4 +1,4 @@
1
- // `simt-dev skills` - tier 0 (index), tier 1 (brief), tier 2 (show), plus find/why.
1
+ // `simt skills` - tier 0 (index), tier 1 (brief), tier 2 (show), plus find/why.
2
2
  //
3
3
  // Implementation sequence steps 4 and 5.
4
4
  //
@@ -20,9 +20,7 @@ import { resolveVaultDir } from '../bench/mine.mjs';
20
20
  import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
21
21
  import { nearMatches } from '../resolve/near.mjs';
22
22
  import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
23
- import { CLI, CONFIG_DIR } from '../name.mjs';
24
-
25
- const STATE_DIR = `.${CONFIG_DIR}`;
23
+ import { CLI, STATE_DIR } from '../name.mjs';
26
24
 
27
25
  async function readSkillRawText(skill, workspaceRoot, root) {
28
26
  const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
@@ -1,4 +1,4 @@
1
- // `simt-dev skills install` - pin a skill into a repo as a local copy.
1
+ // `simt skills install` - pin a skill into a repo as a local copy.
2
2
  //
3
3
  // Plan 0002 section 8.3: installed skills are recorded in `simt-dev.json`
4
4
  // `localSkills` and are excluded from central-drift accounting. The point is a
@@ -1,5 +1,5 @@
1
- // simt-dev sync --yes — write the generated regions.
2
- // simt-dev check — the gate.
1
+ // simt sync --yes — write the generated regions.
2
+ // simt check — the gate.
3
3
  //
4
4
  // They share plan/project.mjs so they can never disagree about what the regions
5
5
  // should contain. sync decides whether to act; check decides severity and exit
@@ -36,7 +36,7 @@ import { lintDecisions } from '../memory/decide.mjs';
36
36
  /** Findings `sync` reports and does not fail on.
37
37
  *
38
38
  * `pointer-unread` is a misaimed pointer whose reader this repository serves. `sync`
39
- * must not refuse on it: the remedy is `simt-dev sync --yes`, so a `sync` that blocked
39
+ * must not refuse on it: the remedy is `simt sync --yes`, so a `sync` that blocked
40
40
  * would be a gate whose remedy is the command being run — ADR-025 ruling 6's hazard, and
41
41
  * worse than a noisy gate. `check` disagrees, and the reason is the whole of ADR-051.
42
42
  *
@@ -194,9 +194,9 @@ export async function syncCommand(positional, flags) {
194
194
  if (st.state === 'current') continue;
195
195
  // A *mispointed* pointer is one this tool wrote and this tool may repair: it is a
196
196
  // symlink, so it has no content, and the remedy printed by `check` for this exact
197
- // finding is `simt-dev sync --yes`. Reporting a plan and then calling `symlink()` at
197
+ // finding is `simt sync --yes`. Reporting a plan and then calling `symlink()` at
198
198
  // an existing path is `EEXIST` — which is what this did, and the remedy it offered
199
- // was `simt-dev doctor`, which reports nothing about it. A command whose own remedy
199
+ // was `simt doctor`, which reports nothing about it. A command whose own remedy
200
200
  // crashes on the state it names has no remedy at all.
201
201
  //
202
202
  // Only a symlink is replaced. A regular file or a directory is somebody's work and
@@ -364,7 +364,7 @@ export async function syncCommand(positional, flags) {
364
364
 
365
365
  const idle = changed.length === 0 && pointerPlan.length === 0 && !manifestChanged;
366
366
  if (checkOnly) return idle ? EXIT.OK : EXIT.FAIL;
367
- // A preview that found work is not a success: `simt-dev sync --yes` with no flags is a
367
+ // A preview that found work is not a success: `simt sync --yes` with no flags is a
368
368
  // check that also prints the plan, and a check that exits 0 while reporting drift
369
369
  // is the one-question-two-answers shape ADR-035 rules out. This is what lets a
370
370
  // caller run `sync` in place of `sync --check` and get the same verdict.
@@ -500,8 +500,8 @@ export async function checkCommand(positional, flags) { const root = flags.root
500
500
  const { manifest } = await readManifest(root);
501
501
  if (manifest) {
502
502
  // The gate runs from whatever binary resolved it. For a repository without npm that
503
- // is a global install, and the only thing that knows which version it should be is
504
- // the pin the repo recorded at adoption. A stale binary would run a different gate
503
+ // is the release binary in `.simt/bin` (ADR-098), and the only thing that knows
504
+ // which version it should be is the pin the repo recorded at adoption. A stale binary would run a different gate
505
505
  // than CI does, so the mismatch fails the gate rather than warning.
506
506
  const mismatch = assertVersionPin(manifest);
507
507
  if (mismatch) {
@@ -1,4 +1,4 @@
1
- // simt-dev tokens - what a reader loads, counted.
1
+ // simt tokens - what a reader loads, counted.
2
2
  //
3
3
  // Review 0002 S38 put the estate's always-on context at 90 KB per turn, 72% of it a
4
4
  // paragraph an earlier root had already loaded, and could only say so by reading seven
@@ -1,4 +1,4 @@
1
- // simt-dev upgrade - move every consumer to this simt-dev, as one reviewable set.
1
+ // simt upgrade - move every consumer to this simt-dev, as one reviewable set.
2
2
  //
3
3
  // Why this command exists, and why it is shaped the way it is:
4
4
  //
@@ -26,7 +26,7 @@ import { listPromotedLessons } from '../memory/lessons.mjs';
26
26
 
27
27
  const exec = promisify(execFile);
28
28
 
29
- const RULE_DIRS = ['rules', 'rules/stacks', 'rules/mdc', '.simt-dev/profiles'];
29
+ const RULE_DIRS = ['rules', 'rules/stacks', 'rules/mdc', '.simt/profiles'];
30
30
 
31
31
  export async function whyCommand(args, flags, commands = []) {
32
32
  if (args.length > 1) return fail(`unknown why argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} why [path]`);
@@ -1,4 +1,4 @@
1
- // Find the sibling repositories a `simt-dev compare` should report on.
1
+ // Find the sibling repositories a `simt compare` should report on.
2
2
  //
3
3
  // Three things here are load-bearing, and each exists because the alternative is
4
4
  // a wrong answer rather than a missing one:
@@ -1,7 +1,7 @@
1
1
  // Read a repo's hook routing and shim state without changing anything.
2
2
  //
3
- // This is the read half of what `simt-dev hooks status` does inline. It is a separate
4
- // module because two callers need it - `hooks status` and `simt-dev compare` - and a
3
+ // This is the read half of what `simt hooks status` does inline. It is a separate
4
+ // module because two callers need it - `hooks status` and `simt compare` - and a
5
5
  // second implementation of "is this shim current?" would be exactly the
6
6
  // duplicated-source-of-truth problem the tool exists to remove. `compare` reads it
7
7
  // for N repos, so it must not shell out per repo per event.
@@ -11,7 +11,7 @@ import { readFile, stat } from 'node:fs/promises';
11
11
  import { basename, join } from 'node:path';
12
12
  import { promisify } from 'node:util';
13
13
 
14
- import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderShim } from '../hooks/shim.mjs';
14
+ import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderShim } from '../hooks/shim.mjs';
15
15
  import { foreignHooks, resolveHooksDir } from '../hooks/where.mjs';
16
16
  import { packageVersion, hasNpm } from '../package-version.mjs';
17
17
 
@@ -123,8 +123,8 @@ export async function readHooksPath(root) {
123
123
  * already records for the same reason.
124
124
  *
125
125
  * `root` is the repository the shim belongs to: the remedy a shim prints depends on
126
- * the repo's kind (a devDependency where package.json exists, a pinned global install
127
- * where there is none), so the comparison must render the shim the way THIS repo got it,
126
+ * the repo's kind (a devDependency where package.json exists, the release binary into
127
+ * the repository where there is none), so the comparison must render the shim the way THIS repo got it,
128
128
  * or a non-npm repository would read as stale the moment this tool ships.
129
129
  */
130
130
  async function describeShim(path, root) {
@@ -142,7 +142,15 @@ async function describeShim(path, root) {
142
142
  }
143
143
  let stale = true;
144
144
  try {
145
- stale = content !== renderShim(basename(path), { npm: await hasNpm(root), version: packageVersion() });
145
+ // A chained shim is current against the repository hook it chains to, not against
146
+ // the dispatcher template: comparing it to renderShim read every chained ungated
147
+ // event as stale forever, and no reinstall could clear it.
148
+ if (content.includes(CHAIN_MARKER)) {
149
+ const chain = await readChainedShim(path);
150
+ stale = chain === null || content !== renderChainedShim(chain.event, chain.target);
151
+ } else {
152
+ stale = content !== renderShim(basename(path), { npm: await hasNpm(root), version: packageVersion() });
153
+ }
146
154
  } catch {
147
155
  stale = true;
148
156
  }
@@ -177,17 +185,21 @@ export async function readHooksState(root, options = {}) {
177
185
  const staleOurs = [];
178
186
  for (const hook of live) {
179
187
  const shim = EVENTS.includes(hook.event) ? await describeShim(hook.path, root) : { stale: true };
180
- if (!shim.stale) continue;
181
188
  // A chained shim is ours too, and its name says nothing: `post-commit` is what the
182
189
  // repository called its own hook, and the shim exists under that name precisely so
183
190
  // git finds it. Reading it as foreign would make every re-install refuse with "this
184
191
  // repo already runs hooks simt did not write" - a shim this tool wrote, refusing
185
192
  // itself. Identity is content, the same rule as the dispatcher shim above.
193
+ //
194
+ // Classified before the currency skip, because a current chain shim is not stale -
195
+ // and install still has to maintain it from `chained`, or a second run would forget
196
+ // the very shim that is already correct.
186
197
  const chain = await readChainedShim(hook.path);
187
198
  if (chain) {
188
199
  chained.push({ ...chain, path: hook.path });
189
200
  continue;
190
201
  }
202
+ if (!shim.stale) continue;
191
203
  // And so is a dispatcher shim from an EARLIER version of this tool, which is the
192
204
  // ordinary state of every repository adopted before the last release.
193
205
  //
@@ -1,6 +1,6 @@
1
1
  // Estate learning through `compare` (plan 0003 R36).
2
2
  //
3
- // Aggregates `.simt-dev/ledger.jsonl`, `.simt-dev/lessons.jsonl`, and ablation/bench
3
+ // Aggregates `.simt/ledger.jsonl`, `.simt/lessons.jsonl`, and ablation/bench
4
4
  // records across workspace repositories to surface:
5
5
  // 1. the most-violated rules (from hook denials and `review-block` findings);
6
6
  // 2. the most-failing and slowest gates (from evidence ledgers);
@@ -189,7 +189,7 @@ async function collectRepoAblationRecords(repoPath) {
189
189
  }
190
190
 
191
191
  async function readExplicitNoValueSections(repoPath) {
192
- const file = join(repoPath, '.simt-dev', 'ablation.json');
192
+ const file = join(repoPath, '.simt', 'ablation.json');
193
193
  try {
194
194
  const raw = JSON.parse(await readFile(file, 'utf8'));
195
195
  const list = Array.isArray(raw?.sections) ? raw.sections : Array.isArray(raw) ? raw : [];
@@ -1,4 +1,4 @@
1
- // The matrix a `simt-dev compare` row is built from: one repo's adoption state.
1
+ // The matrix a `simt compare` row is built from: one repo's adoption state.
2
2
  //
3
3
  // The shape of a row answers one question - "would this repo behave correctly if
4
4
  // an agent followed it today?" - from the four things that can make it not:
@@ -255,7 +255,7 @@ export async function buildRow(path, context) { const name = path.split('/').fi
255
255
  if (region.status === 'drift') {
256
256
  // A drift has two causes with different fixes, and the SHA says which. When
257
257
  // the recorded SHA matches this simt-dev, the region was hand-edited and
258
- // `simt-dev sync --yes` is right; when it does not, the repo is simply behind and the
258
+ // `simt sync --yes` is right; when it does not, the repo is simply behind and the
259
259
  // same command is also right - but the message must not claim a version
260
260
  // mismatch when the versions are identical, because that sends the reader
261
261
  // looking for a pin that is already correct.
@@ -274,7 +274,7 @@ export async function buildRow(path, context) { const name = path.split('/').fi
274
274
  } else if (region.status === 'current' && row.pinnedSha && row.pinnedSha !== context.expectedSha) {
275
275
  // The region matches this simt-dev but the manifest pins an older one. The
276
276
  // next sync would stamp the newer SHA silently, so the pin is the thing
277
- // that is stale - and it is the value `simt-dev upgrade` reads.
277
+ // that is stale - and it is the value `simt upgrade` reads.
278
278
  problems.push(
279
279
  `manifest pins simt-dev ${row.pinnedSha}, this is ${context.expectedSha}; run: ${CLI} upgrade`,
280
280
  );
@@ -1,4 +1,4 @@
1
- // The decision index: what `simt-dev rules why` traces back to.
1
+ // The decision index: what `simt rules why` traces back to.
2
2
  //
3
3
  // Rule bodies cite the decision that settled them inline, as `(ADR-001)`. That
4
4
  // citation is the only thing standing between "a rule someone wrote" and "a rule
@@ -6,7 +6,7 @@
6
6
  // needs the guardrails here, because its output is the kind a reader would paste
7
7
  // into a pack and forget to check.
8
8
  //
9
- // Nothing here writes outside `.simt-dev/distilled/`, and nothing here edits a
9
+ // Nothing here writes outside `.simt/distilled/`, and nothing here edits a
10
10
  // source file (ADR-090, ADR-075). A proposal that a human did not merge is a dead
11
11
  // file, which is the intended failure mode: proposals expire, projections do not.
12
12
 
@@ -34,7 +34,7 @@ import {
34
34
  } from './engine.mjs';
35
35
 
36
36
  export const DEFAULT_BUDGET = 2048;
37
- const DISTILL_DIR = '.simt-dev/distilled';
37
+ const DISTILL_DIR = '.simt/distilled';
38
38
 
39
39
  /**
40
40
  * The row's acceptance metric, stated where a caller reads results (ADR-076):
@@ -0,0 +1,142 @@
1
+ // The binary channel: what a release publishes, where a repository keeps its copy,
2
+ // and the one command that gets it there.
3
+ //
4
+ // This module is the single place that knows those facts, because three generated
5
+ // surfaces must agree on them and they are rendered from different call sites: the
6
+ // locator block (src/locate.mjs) checks a path, the hook shim's remedy prints a
7
+ // download command, and the CI workflow renders the same download as a step. A
8
+ // second copy of the asset name or the URL is a copy that drifts, and a drifted
9
+ // remedy tells a reader to fetch bytes that do not exist.
10
+ //
11
+ // The channel is a fourth beside npm, PyPI and Homebrew (ADR-058), and unlike the
12
+ // other three it is the one that needs no toolchain at all: the npm channel needs
13
+ // npm, the wheel needs a Python, and the formula - until this release - needed
14
+ // node. The binary is a file. That is the whole design (ADR-098), and it is why
15
+ // the locator checks a path in the repository before it checks PATH: the bytes a
16
+ // repository pinned are the bytes its gate runs, on a machine with nothing else.
17
+
18
+ import { CLI, PACKAGE_NAME } from '../name.mjs';
19
+
20
+ // The GitHub repository the release is published from. A fact about where the
21
+ // bytes come from, like the npm registry host: a reader outside this estate
22
+ // reads it as a URL, not as an organisation. Not exported: the only surface a
23
+ // caller needs is the URL, and a second copy of the slug is a copy that drifts.
24
+ const REPO_SLUG = 'QuazarMedia/simt-dev';
25
+
26
+ /** The platforms the release builds. The set is small on purpose: it is the set
27
+ * the locator's remedy and the CI step can name, and a platform in this list that
28
+ * the release does not build is a remedy that downloads a 404. */
29
+ export const TARGETS = ['darwin-arm64', 'darwin-x64', 'linux-x64'];
30
+
31
+ // Where a repository keeps the binary it pinned, relative to its top level.
32
+ // Beneath `.simt`, like every other per-repo state this tool writes, so a reader
33
+ // who has seen one knows where to look for all of them. The name is the command,
34
+ // not the package: the reader types one program and this is it, the same way
35
+ // `bin/simt.mjs` is it on npm.
36
+ const LOCAL_BIN_NAME = CLI;
37
+
38
+ /** The directory a repository keeps its pinned binary in, relative to its top
39
+ * level. Exported because a caller that installs the binary must create it and
40
+ * has no other source for the fact. */
41
+ export const LOCAL_BIN_DIR = '.simt/bin';
42
+
43
+ /** The path the locator checks, relative to the repository's top level. */
44
+ export const LOCAL_BIN = `${LOCAL_BIN_DIR}/${LOCAL_BIN_NAME}`;
45
+
46
+ /**
47
+ * The release asset name for a version and a target.
48
+ *
49
+ * The package name, not the command: it sits beside its siblings in the release
50
+ * and next to the npm tarball `simt-dev-0.5.0.tgz`, where one name for the project
51
+ * is what a reader matching names expects.
52
+ *
53
+ * @param {string} version x.y.z
54
+ * @param {string} osArch one of TARGETS
55
+ * @returns {string} e.g. `simt-dev-0.5.0-darwin-arm64`
56
+ */
57
+ export function assetName(version, osArch) {
58
+ if (!TARGETS.includes(osArch)) throw new Error(`unknown target ${osArch}; known: ${TARGETS.join(', ')}`);
59
+ return `${PACKAGE_NAME}-${version}-${osArch}`;
60
+ }
61
+
62
+ /**
63
+ * The asset name with the platform left for the shell to fill in at runtime.
64
+ *
65
+ * The CI step and the remedy both run on a machine whose platform is not known
66
+ * where the text was written, so the name is completed from `uname` or the
67
+ * runner there. One template shared with {@link assetName} keeps the two from
68
+ * drifting: a name rendered here that the release does not publish is a 404.
69
+ *
70
+ * @param {string} version x.y.z
71
+ * @returns {string} e.g. `simt-dev-0.5.0-$os-$arch`, for a shell with $os and $arch set
72
+ */
73
+ export function assetNameExpr(version) {
74
+ return `${PACKAGE_NAME}-${version}-$os-$arch`;
75
+ }
76
+
77
+ /**
78
+ * The URL the release serves a version's assets from.
79
+ *
80
+ * @param {string} version x.y.z
81
+ * @returns {string}
82
+ */
83
+ export function releaseBase(version) {
84
+ return `https://github.com/${REPO_SLUG}/releases/download/v${version}`;
85
+ }
86
+
87
+ /**
88
+ * The URL the release serves one asset from.
89
+ *
90
+ * @param {string} version x.y.z
91
+ * @param {string} osArch one of TARGETS
92
+ * @returns {string}
93
+ */
94
+ export function releaseUrl(version, osArch) {
95
+ return `${releaseBase(version)}/${assetName(version, osArch)}`;
96
+ }
97
+
98
+ /**
99
+ * The download a reader without npm runs from their repository's top level: fetch
100
+ * the asset this machine needs and put it where the locator looks.
101
+ *
102
+ * One line on purpose - it is rendered into a remedy that a reader copies from an
103
+ * error message, and a multi-line script in an error message is not copied. The
104
+ * platform is derived rather than baked in, because the remedy lives in a file
105
+ * that is committed and shared: a URL rendered for the machine that wrote the
106
+ * shim would be a 404 for the colleague who clones it.
107
+ *
108
+ * The transport is TLS from the release, and the version the bytes report is
109
+ * checked against the repository's pin by the dispatcher itself (ADR-098), so
110
+ * the remedy does not also carry a checksum: it is the shortest command that
111
+ * reaches a working state, and the gate re-verifies what it runs.
112
+ *
113
+ * @param {string} version x.y.z
114
+ * @returns {string} POSIX sh, run from the repository's top level
115
+ */
116
+ export function localInstall(version) {
117
+ return [
118
+ `mkdir -p ${LOCAL_BIN_DIR}`,
119
+ ...platformLines(),
120
+ `curl -fsSL "${releaseBase(version)}/${assetNameExpr(version)}" -o ${LOCAL_BIN}`,
121
+ `chmod +x ${LOCAL_BIN}`,
122
+ ].join(' && ');
123
+ }
124
+
125
+ /**
126
+ * The two shell lines that derive this machine's release target at run time.
127
+ *
128
+ * `uname -m` answers `x86_64` on both platforms' Intel builds and `arm64` on
129
+ * Apple Silicon, so one substitution covers the naming gap. Both the remedy and
130
+ * the generated CI step name their asset from these lines: the text is committed
131
+ * and shared, so a URL baked for the machine that wrote it is a 404 for the
132
+ * colleague who clones it, and a workflow expression interpolated into a command
133
+ * substitution is worse - the CI step once rendered `os=$('${{ runner.os }}' …)`,
134
+ * which the runner substitutes to `os=$('Linux' …)` and `set -eu` refuses, so the
135
+ * install failed on every run in the one repository kind this channel exists for.
136
+ * The step is executed, not pattern-matched, in test/binary.test.mjs.
137
+ *
138
+ * @returns {string[]} POSIX sh lines assigning `$os` and `$arch`
139
+ */
140
+ export function platformLines() {
141
+ return [`os=$(uname -s | tr A-Z a-z)`, `arch=$(uname -m | sed "s/x86_64/x64/")`];
142
+ }
@@ -35,14 +35,15 @@ function defaultRegistryPath() {
35
35
  /**
36
36
  * The registry to read when the reader names none, in order (ADR-061 ruling 3):
37
37
  *
38
- * 1. The package root, when it holds one. In this checkout that is how the tool finds its
39
- * own registry; in an installed package `graph` is not in the published `files`, so it
40
- * never does, and the reader falls through to 2 instead of being handed this workspace's
41
- * catalogue.
42
- * 2. A member of the resolved workspace, when exactly one holds one: a tool installed from
43
- * node_modules or a formula resolves its workspace by configuration, and the member that
44
- * holds the registry is usually the tool's own checkout. More than one holding it is
45
- * ambiguity and the reader must pick; none is an honest absence, reported with the
38
+ * 1. The package root, when it holds one. In a source checkout that is how the tool
39
+ * finds its own registry, sitting next to the code that reads it. An installed
40
+ * package holds no registry (ADR-061 ruling 2): the catalogue is workspace data,
41
+ * and an install serving another estate's skills to an adopting repository would
42
+ * be a confident wrong answer. This tier is a source-checkout convenience; when it
43
+ * is empty, resolution falls through to the workspace member or an honest absence.
44
+ * 2. A member of the resolved workspace, when exactly one holds one: reachable in a checkout
45
+ * whose package root holds no registry. More than one holding it is ambiguity and the
46
+ * reader must pick; none is an honest absence, reported with the
46
47
  * workspace that was looked in.
47
48
  *
48
49
  * Every step is file existence plus the configured workspace, so the answer can be
@@ -68,8 +68,9 @@ export async function runGates(event, { root, manifest }) {
68
68
  }
69
69
 
70
70
  // The gates below run from the binary that resolved this shim. For a repository
71
- // without npm that is a global install, whose version nothing else pins: if it is not
72
- // the one this repo adopted, the gates that run here are not the ones CI runs.
71
+ // without npm that is the release binary in `.simt/bin` (ADR-098), whose version
72
+ // nothing else pins: if it is not the one this repo adopted, the gates that run here
73
+ // are not the ones CI runs.
73
74
  // Placed after the empty-list check on purpose - with no gates there is nothing the
74
75
  // version could get wrong, and failing that case would block commits in repos that
75
76
  // deliberately declare none.
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { renderLocator } from '../locate.mjs';
20
20
  import { BIN_ENV, CLI, MANIFEST_FILE, PACKAGE_NAME } from '../name.mjs';
21
+ import { localInstall } from '../distribute/release.mjs';
21
22
  import { packageVersion } from '../package-version.mjs';
22
23
 
23
24
  /**
@@ -49,8 +50,8 @@ export function fileFor(event) {
49
50
  * @param {string} event one of EVENTS
50
51
  * @param {{npm?: boolean, version?: string}} [opts]
51
52
  * `npm` is whether the repository has a package.json: it decides the remedy's install
52
- * (a devDependency where one exists, a pinned global install where there is no
53
- * node_modules to pin in). `version` defaults to the running package's own version,
53
+ * (a devDependency where one exists, the release binary into the repository where
54
+ * there is no node_modules to pin in). `version` defaults to the running package's own version,
54
55
  * so a shim generated by 0.2.0 names 0.2.0 in its remedy and CI install.
55
56
  * @returns {string} POSIX sh source, LF-terminated, executable
56
57
  */
@@ -62,8 +63,9 @@ export function renderShim(event, { npm = true, version } = {}) {
62
63
  // The install that finds the binary when node_modules and PATH both come up empty.
63
64
  // A repository with a package.json pins simt-dev as a devDependency, so the remedy
64
65
  // installs it there; one without (Gradle, Connect IQ) has no node_modules to pin in,
65
- // so the remedy is a pinned global install - the same pinned bytes CI gets.
66
- const install = npm ? `npm i -D ${PACKAGE_NAME}@${v}` : `npm i -g ${PACKAGE_NAME}@${v}`;
66
+ // so the remedy downloads the release binary into the repository - the same pinned
67
+ // bytes CI gets, and one that needs no npm and no node on the machine (ADR-098).
68
+ const install = npm ? `npm i -D ${PACKAGE_NAME}@${v}` : localInstall(v);
67
69
  const D = '$';
68
70
  return `#!/bin/sh
69
71
  ${SHIM_MARKER}
@@ -75,7 +77,8 @@ ${SHIM_MARKER}
75
77
  # This file only locates the dispatcher and hands over. It resolves in order:
76
78
  # 1. ${D}SIMT_DEV_BIN explicit override, for CI images and odd checkouts
77
79
  # 2. node_modules/ the pinned project dependency
78
- # 3. ${CLI} on PATH a global install
80
+ # 3. .simt/bin/ the release binary this repository downloaded
81
+ # 4. ${CLI} on PATH a global install
79
82
  # If none resolves it REFUSES rather than exiting 0. A hook that cannot verify
80
83
  # anything must never report success - that is what teaches authors to reach for
81
84
  # --no-verify.
@@ -102,7 +105,9 @@ ${renderLocator(`hooks run ${event} --root "${D}ROOT"`, {
102
105
  */
103
106
  export function renderEvidenceShim({ npm = true, version } = {}) {
104
107
  const v = version ?? packageVersion();
105
- const install = npm ? `npm i -D ${PACKAGE_NAME}@${v}` : `npm i -g ${PACKAGE_NAME}@${v}`;
108
+ // The same split as renderShim: a devDependency where one exists, the release
109
+ // binary into the repository where there is no node_modules to pin in.
110
+ const install = npm ? `npm i -D ${PACKAGE_NAME}@${v}` : localInstall(v);
106
111
  const D = '$';
107
112
  return `#!/bin/sh
108
113
  ${SHIM_MARKER}
@@ -56,7 +56,7 @@ export function sourceRoots(files) {
56
56
  /**
57
57
  * One Affects token.
58
58
  *
59
- * A prefixed command (`simt-dev init`, `simt hooks run`) is a command only when
59
+ * A prefixed command (`simt init`, `simt hooks run`) is a command only when
60
60
  * that command exists. The tool name on its own is not a command and not a path.
61
61
  * Anything else is a path or a glob, at the repo root and then under each source
62
62
  * root. A token that is none of those is unresolved.
package/src/index/map.mjs CHANGED
@@ -285,7 +285,7 @@ export async function embedTaskQuery(root, task, { embed = false, allowRemote =
285
285
  * }} [opts]
286
286
  */
287
287
  export async function measureRecall(root, opts = {}) {
288
- const tasksDir = join(root, '.simt-dev', 'bench', 'tasks');
288
+ const tasksDir = join(root, '.simt', 'bench', 'tasks');
289
289
  let names;
290
290
  try {
291
291
  names = (await readdir(tasksDir)).filter((name) => name.endsWith('.json')).sort();
@@ -7,8 +7,8 @@
7
7
  import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
8
8
  import { dirname, join } from 'node:path';
9
9
 
10
- export const SQLITE_FILE = '.simt-dev/index.sqlite';
11
- export const JSONL_FILE = '.simt-dev/index.jsonl';
10
+ export const SQLITE_FILE = '.simt/index.sqlite';
11
+ export const JSONL_FILE = '.simt/index.jsonl';
12
12
  export const SCHEMA_V = '3';
13
13
 
14
14
  const SCHEMA = `