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,164 @@
1
+ // The package generator (plan 0003 R43): one command builds a Claude Code plugin, a
2
+ // Gemini CLI extension, or a Cursor bundle, and all three are generated from the same
3
+ // tables `agent-hooks install` writes into a repository. A package and a repository
4
+ // install therefore cannot drift apart: the hooks document, the MCP registration and
5
+ // (for Cursor) the kernel rules are all read from the one source, and only the entry
6
+ // point differs — a package ships its own copy of the runtime rather than the binary on
7
+ // PATH, so it installs into a clean profile that has never seen this tool.
8
+ //
9
+ // Each package is self-contained in the way its vendor requires:
10
+ //
11
+ // claude a directory with .claude-plugin/plugin.json, hooks/hooks.json and the
12
+ // runtime at its root; Claude names the root ${CLAUDE_PLUGIN_ROOT} and the
13
+ // project ${CLAUDE_PROJECT_DIR}.
14
+ // gemini a directory with gemini-extension.json, hooks/hooks.json and the runtime at
15
+ // its root; the extension names its own directory ${extensionPath} and the
16
+ // workspace ${workspacePath}. The manifest's name must equal the directory's,
17
+ // so it is taken from the destination rather than fixed.
18
+ // cursor a repository drop-in: .cursor/ with rules/kernel.mdc, hooks.json and
19
+ // mcp.json, and the runtime under .cursor/simt/. Cursor names the project
20
+ // ${CURSOR_PROJECT_DIR}.
21
+ //
22
+ // The skill registry is not shipped in any of them: it is workspace data (ADR-061), and
23
+ // a package that carried one snapshot would ship a registry that is stale the moment it
24
+ // leaves the machine it was built on. The command says so rather than pretending.
25
+
26
+ import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
27
+ import { basename, join } from 'node:path';
28
+ import { hookDocument } from './agenthooks/render.mjs';
29
+ import { desiredRegion } from './projection/rules.mjs';
30
+ import { PACKAGE_NAME, BIN_REL } from './name.mjs';
31
+
32
+ /** The kinds `simt plugin` builds, in the order the help line names them. */
33
+ export const PLUGIN_KINDS = ['claude', 'gemini', 'cursor'];
34
+
35
+ /** Where the runtime copy lands inside a package, relative to the package root. */
36
+ function runtimeDir(kind) {
37
+ return kind === 'cursor' ? join('.cursor', 'simt') : '.';
38
+ }
39
+
40
+ /** The entry point a package's config names: its own copy of the runtime, not PATH. */
41
+ function entryPoint(kind) {
42
+ const bin = join(runtimeDir(kind), BIN_REL);
43
+ // The vendor's own variables must survive as literal text in the generated config, so
44
+ // they are escaped here: an unescaped ${...} would interpolate a JS binding that does
45
+ // not exist and ship the string "undefined" into the package.
46
+ if (kind === 'claude') return `node "\${CLAUDE_PLUGIN_ROOT}/${bin}"`;
47
+ if (kind === 'gemini') return `node "\${extensionPath}/${bin}"`;
48
+ // Cursor runs from the repository root, so a relative path is the honest one.
49
+ return `node ${bin}`;
50
+ }
51
+
52
+ /** The project root a package's hook resolves against, from the vendor's env var. */
53
+ function rootFlag(kind) {
54
+ if (kind === 'claude') return ` --root "$CLAUDE_PROJECT_DIR"`;
55
+ if (kind === 'cursor') return ` --root "$CURSOR_PROJECT_DIR"`;
56
+ // Gemini documents no project-dir env var, but its base input carries cwd, which the
57
+ // adapter reads; passing a flag the vendor never sets would only expand to "".
58
+ return '';
59
+ }
60
+
61
+ /** Where the hooks document lands inside a package, relative to the package root. */
62
+ function hooksPath(kind) {
63
+ return kind === 'cursor' ? join('.cursor', 'hooks.json') : join('hooks', 'hooks.json');
64
+ }
65
+
66
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
67
+
68
+ /**
69
+ * The generated files of one package, relative to the package root: the manifest, the
70
+ * hooks document and the MCP registration. The runtime copy is planned separately because
71
+ * it is many files; this returns only the handful that are authored here.
72
+ */
73
+ async function packageFiles(kind, { root = process.cwd(), out } = {}) {
74
+ const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
75
+ const command = `${entryPoint(kind)} agent-hook pre-tool-use --harness ${kind}${rootFlag(kind)}`;
76
+ const files = [];
77
+
78
+ if (kind === 'claude') {
79
+ files.push({
80
+ path: join('.claude-plugin', 'plugin.json'),
81
+ text: json({
82
+ name: PACKAGE_NAME,
83
+ version: pkg.version,
84
+ description: pkg.description,
85
+ mcpServers: { [PACKAGE_NAME]: { command: 'node', args: [`\${CLAUDE_PLUGIN_ROOT}/${BIN_REL}`, 'mcp'] } },
86
+ }),
87
+ });
88
+ }
89
+
90
+ if (kind === 'gemini') {
91
+ // The vendor requires the manifest's name to equal the directory's, so it is the
92
+ // destination's basename rather than a fixed string: a package installed under any
93
+ // name is a valid extension, and the name travels with it.
94
+ files.push({
95
+ path: 'gemini-extension.json',
96
+ text: json({
97
+ name: basename(out),
98
+ version: pkg.version,
99
+ description: pkg.description,
100
+ mcpServers: { [PACKAGE_NAME]: { command: 'node', args: [`\${extensionPath}/${BIN_REL}`, 'mcp'], cwd: '${workspacePath}' } },
101
+ }),
102
+ });
103
+ }
104
+
105
+ if (kind === 'cursor') {
106
+ const kernel = await desiredRegion(root, { mode: 'kernel' });
107
+ files.push({
108
+ path: join('.cursor', 'rules', 'kernel.mdc'),
109
+ text: `---\ndescription: the kernel rules this repository enforces\nalwaysApply: true\n---\n${kernel.body}`,
110
+ });
111
+ files.push({
112
+ path: join('.cursor', 'mcp.json'),
113
+ text: json({ mcpServers: { [PACKAGE_NAME]: { command: 'node', args: [join('.cursor', 'simt', BIN_REL), 'mcp'] } } }),
114
+ });
115
+ }
116
+
117
+ files.push({ path: hooksPath(kind), text: hookDocument(kind, { command }) });
118
+ return files;
119
+ }
120
+
121
+ /**
122
+ * Build one package into `out`. With `apply` false it returns the plan and writes
123
+ * nothing; with true it copies the runtime and writes every generated file. The plan is
124
+ * the same in both cases, so a dry run and an apply agree on what will land.
125
+ */
126
+ export async function buildPackage(kind, { root = process.cwd(), out, apply = false } = {}) {
127
+ if (!PLUGIN_KINDS.includes(kind)) {
128
+ throw new Error(`unknown package kind \`${kind}\`; one of: ${PLUGIN_KINDS.join(', ')}`);
129
+ }
130
+
131
+ const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
132
+ const files = await packageFiles(kind, { root, out });
133
+ const notes = [
134
+ 'the skill registry is workspace data (ADR-061) and is not shipped; run `simt skills index` in the profile to list what a workspace would offer',
135
+ ];
136
+
137
+ if (apply) {
138
+ const runtimeTarget = join(out, runtimeDir(kind));
139
+ await mkdir(runtimeTarget, { recursive: true });
140
+ // The runtime is the same set of paths npm publishes (the `files` field) plus
141
+ // package.json. Copying anything less ships a binary that answers the hook and then
142
+ // dies on the first MCP call: the server reads templates/ and rules/, which a
143
+ // bin+src-only copy leaves behind. The `files` field is the definition of "a working
144
+ // copy of this tool", so it is the set a package ships.
145
+ for (const part of pkg.files) {
146
+ await cp(join(root, part), join(runtimeTarget, part), { recursive: true });
147
+ }
148
+ await cp(join(root, 'package.json'), join(runtimeTarget, 'package.json'));
149
+ for (const f of files) {
150
+ const target = join(out, f.path);
151
+ await mkdir(join(target, '..'), { recursive: true });
152
+ await writeFile(target, f.text);
153
+ }
154
+ }
155
+
156
+ return {
157
+ kind,
158
+ out,
159
+ apply,
160
+ runtime: { target: runtimeDir(kind), parts: [...pkg.files, 'package.json'] },
161
+ files: files.map(({ text, ...rest }) => rest),
162
+ notes,
163
+ };
164
+ }
@@ -38,10 +38,12 @@
38
38
  // required field rather than a comment. A reader that publishes no cap has
39
39
  // `cap: null`, which means "checked, none found", not "not checked".
40
40
  //
41
- // 4. UNDOCUMENTED IS ITS OWN ANSWER. Copilot has no documented hooks file, and Gemini
42
- // CLI's own configuration reference 404s today. Those cells are `null` with a
43
- // `note` saying what was looked for. An empty string would read as "no hooks" and
44
- // an invented path would read as a fact.
41
+ // 4. UNDOCUMENTED IS ITS OWN ANSWER. Copilot has no documented hooks file. Those cells
42
+ // are `null` with a `note` saying what was looked for. An empty string would read
43
+ // as "no hooks" and an invented path would read as a fact. Gemini CLI spent a
44
+ // season here too — its configuration reference 404ed until it was re-checked on
45
+ // 2026-10-26 and found live, which moved it from a thin row to a full one; the rule
46
+ // is unchanged, the example is.
45
47
 
46
48
  /**
47
49
  * One row per reader. Ordered by how widely the reader is deployed, so a truncated
@@ -152,7 +154,7 @@ export const LOADERS = [
152
154
  path: 'opencode.json',
153
155
  key: 'mcp.servers',
154
156
  verified:
155
- 'OpenCode v2 MCP servers docs: servers are named under mcp.servers; V2 does not place server names directly under mcp',
157
+ 'OpenCode v2 MCP servers docs (read 2026-10-14): servers are named under mcp.servers; V2 does not place server names directly under mcp',
156
158
  },
157
159
  verified:
158
160
  'OpenCode v2 instructions, skills, plugins and MCP docs (opencode.ai/v2/docs, read 2026-10-14) — V2 loads AGENTS.md only, combining the global file with every project AGENTS.md from the workspace up to the project root; CLAUDE.md is not a fallback; the instructions array is accepted and not loaded; skills advertise name and description from .opencode/skills, .claude/skills and .agents/skills',
@@ -244,7 +246,7 @@ export const LOADERS = [
244
246
  path: '.mcp.json',
245
247
  key: null,
246
248
  verified:
247
- 'Claude Code MCP docs: project-scoped servers live in .mcp.json in the project root; local and user scope live in ~/.claude.json',
249
+ 'Claude Code MCP docs (read 2026-10-14): project-scoped servers live in .mcp.json in the project root; local and user scope live in ~/.claude.json',
248
250
  },
249
251
  verified:
250
252
  'Claude Code memory docs (docs.claude.com/en/docs/claude-code/memory, read 2026-10-14) — CLAUDE.md and CLAUDE.local.md are concatenated from every directory above the working directory, subdirectory files load on demand, `@path` imports expand to four hops, a CLAUDE.md larger than 4 MiB is skipped, and AGENTS.md is read only when no CLAUDE.md exists above the working directory',
@@ -321,7 +323,7 @@ export const LOADERS = [
321
323
  mcp: {
322
324
  path: '.codex/config.toml',
323
325
  key: 'mcp_servers',
324
- verified: 'Codex MCP docs: MCP configuration is stored in config.toml, ~/.codex/config.toml by default',
326
+ verified: 'Codex MCP docs (read 2026-10-14): MCP configuration is stored in config.toml, ~/.codex/config.toml by default',
325
327
  },
326
328
  verified:
327
329
  'Codex AGENTS.md documentation and configuration reference (developers.openai.com/codex, read 2026-10-14) — the project scope walks from the project root to the working directory, taking at most one file per directory in the order AGENTS.override.md, AGENTS.md, project_doc_fallback_filenames; the merge runs root to cwd and stops adding files at project_doc_max_bytes, 32 KiB by default',
@@ -387,7 +389,7 @@ export const LOADERS = [
387
389
  mcp: {
388
390
  path: '.cursor/mcp.json',
389
391
  key: null,
390
- verified: 'Cursor MCP docs: "Create .cursor/mcp.json in your project"',
392
+ verified: 'Cursor MCP docs (read 2026-10-14): "Create .cursor/mcp.json in your project"',
391
393
  },
392
394
  verified:
393
395
  'Cursor rules documentation (cursor.com/docs/context/rules, read 2026-10-14) — only .cursor/rules/*.mdc is read, a plain .md there is ignored; alwaysApply decides whether a rule costs bytes at start; @file references in a rule body are not inlined; AGENTS.md is read in the project root and in subdirectories, combined with parents',
@@ -471,13 +473,85 @@ export const LOADERS = [
471
473
  mcp: {
472
474
  path: '.agents/mcp_config.json',
473
475
  key: null,
474
- verified: 'Antigravity MCP docs: "Configured in your workspace under .agents/mcp_config.json"',
476
+ verified:
477
+ 'Antigravity MCP docs (read 2026-10-14): "Configured in your workspace under .agents/mcp_config.json"',
475
478
  },
476
479
  verified:
477
480
  'Google Antigravity rules documentation (antigravity.google/docs/rules/, read 2026-10-14) — rules are cumulative across global, workspace and directory scope; .agents/rules/*.md requires frontmatter with trigger, and an invalid block silently discards the rule; the directory is scanned flat and nested files are ignored unless registered in .agents/rules.json; each rule file is truncated at 24,000 bytes after includes are expanded, and global plus always_on rules share a 20,000-token budget past which the largest are demoted to pointers',
478
481
  note:
479
482
  'The flat scan is the fact that made the meter wrong before this table existed: a recursive walk of .agents/rules/ counts bytes for files Antigravity never reads.',
480
483
  },
484
+ {
485
+ id: 'gemini',
486
+ label: 'Gemini CLI',
487
+ readers: 'Gemini CLI (google-gemini/gemini-cli)',
488
+ // No projection surface of its own: the gemini-md pointer already hands the reader
489
+ // the standard region, and `context.fileName` is the only mechanism by which this
490
+ // reader would load AGENTS.md at all — renaming the context file is the vendor's
491
+ // documented way to point at it. A separate rules row would be a second way to hand
492
+ // one reader the same rules (the ruling recorded on the surfaces `gemini-md`
493
+ // pointer). This FACTS row exists because the reader's own facts differ from every
494
+ // other row's: no published cap, depth-5 imports, and the one MCP registration left
495
+ // that a project file can carry.
496
+ surface: null,
497
+ start: [
498
+ {
499
+ scope: 'project',
500
+ path: 'GEMINI.md',
501
+ tree: 'chain',
502
+ atStart: true,
503
+ why: 'Gemini CLI context docs (docs/cli/gemini-md/): GEMINI.md is collected from the working directory upward through parent directories until the first directory holding a boundary marker (.git by default) or home',
504
+ },
505
+ {
506
+ scope: 'user',
507
+ path: '~/.gemini/GEMINI.md',
508
+ tree: 'single',
509
+ atStart: true,
510
+ why: 'Gemini CLI context docs: the user-global ~/.gemini/GEMINI.md opens the concatenation, before any project file',
511
+ },
512
+ ],
513
+ alsoReads: [
514
+ {
515
+ what: 'the context file under another name — settings.json context.fileName, a string or an array',
516
+ atStart: true,
517
+ why: 'Gemini CLI configuration docs: context.fileName renames the context file (a string or an array), so an installation listing AGENTS.md there reads AGENTS.md as its GEMINI.md',
518
+ },
519
+ {
520
+ what: 'GEMINI.md (or the renamed context file) in subdirectories below the working directory',
521
+ atStart: false,
522
+ why: 'Gemini CLI context docs: directories a tool touches are scanned for a context file up to a trusted root, bounded by context.discoveryMaxDirs (200) - not at session start',
523
+ },
524
+ ],
525
+ order:
526
+ 'user-global ~/.gemini/GEMINI.md first, then the project chain general to specific - project-root side first (upward traversal stops at the first directory holding a boundary marker, .git by default) down through each directory to the working directory - then just-in-time files a tool touch discovered; the whole result is one concatenation with origin separators',
527
+ cap: null,
528
+ capNote: 'no byte cap is published for the merged Gemini CLI context (checked 2026-10-26)',
529
+ imports: {
530
+ syntax: '@path',
531
+ depth: 5,
532
+ why: 'Gemini CLI context docs: @path imports resolve relative or absolute to a maximum depth of 5, with cycles prevented by skipping files already on the stack',
533
+ },
534
+ nested:
535
+ 'general to specific, all loaded — unlike Codex, every context file on the walk contributes; a nearer file cannot hide a parent one, it just concatenates after it; subdirectory files wait for a tool touch',
536
+ skills: [],
537
+ skillsNote:
538
+ 'no project skill directory is documented for Gemini CLI; extensions are the distribution mechanism, and they are tooling not context',
539
+ hooks: {
540
+ path: '.gemini/settings.json',
541
+ verified:
542
+ 'Gemini CLI hooks reference (geminicli.com/docs/hooks/reference/, read 2026-10-26): a project .gemini/settings.json carries a top-level hooks object whose events are arrays of {matcher, hooks:[{type:"command",command,timeout? (milliseconds)}]} — the same shape Claude and Codex use; BeforeTool denies with {decision:"deny",reason}, BeforeAgent and AfterTool inject via hookSpecificOutput.additionalContext',
543
+ },
544
+ mcp: {
545
+ path: '.gemini/settings.json',
546
+ key: 'mcpServers',
547
+ verified:
548
+ 'Gemini CLI configuration docs (docs/reference/configuration/, read 2026-10-26): a project .gemini/settings.json (a user ~/.gemini/settings.json merged behind it, objects merged key by key) carries a top-level mcpServers object whose entries name a command, a url or an httpUrl',
549
+ },
550
+ verified:
551
+ 'Gemini CLI context and configuration docs (geminicli.com/docs/cli/gemini-md/ and docs/reference/configuration/, read 2026-10-26) — settings live at project .gemini/settings.json and user ~/.gemini/settings.json, merged for objects; the context is user ~/.gemini/GEMINI.md plus the context file (GEMINI.md unless context.fileName renames it, an array is allowed) from the working directory upward through parents to the first .git or home, with @path imports to depth 5; server registration is the mcpServers object in those same settings files; no byte cap is published',
552
+ note:
553
+ 'The configuration reference this table once recorded as 404ing is live again as of 2026-10-26, which is why the row is full instead of thin. What still shapes a byte count is the pair of absences the docs confirm rather than deny: no cap, and no promise that AGENTS.md is read at all — context.fileName decides that, and it may name anything.',
554
+ },
481
555
  {
482
556
  id: 'copilot',
483
557
  label: 'GitHub Copilot',
@@ -533,7 +607,7 @@ export const LOADERS = [
533
607
  path: '.vscode/mcp.json',
534
608
  key: null,
535
609
  verified:
536
- 'GitHub Copilot MCP docs: the VS Code extension reads .vscode/mcp.json; Copilot CLI reads .mcp.json, .github/mcp.json or ~/.copilot/mcp-config.json; a repository-level configuration entered on github.com is not a file at all',
610
+ 'GitHub Copilot MCP docs (read 2026-10-14): the VS Code extension reads .vscode/mcp.json; Copilot CLI reads .mcp.json, .github/mcp.json or ~/.copilot/mcp-config.json; a repository-level configuration entered on github.com is not a file at all',
537
611
  },
538
612
  verified:
539
613
  'GitHub Copilot documentation (docs.github.com/en/copilot, read 2026-10-14) — repository-wide instructions live at .github/copilot-instructions.md; path-specific instructions live at .github/instructions/**/NAME.instructions.md with applyTo globs and an optional excludeAgent; agent instructions are one or more AGENTS.md anywhere in the repository, nearest wins, or a single root CLAUDE.md or GEMINI.md; no byte cap is published',
@@ -12,7 +12,7 @@
12
12
  // Inventing a number here would be worse than declaring none.
13
13
  //
14
14
  // Every default this module produces is explained by the ADR that requires it. The
15
- // rationale is not duplicated into the JSON (data must stay data); `simt-dev init`
15
+ // rationale is not duplicated into the JSON (data must stay data); `simt init`
16
16
  // prints the reason for each value it writes.
17
17
 
18
18
  import { readFile, rename, writeFile } from 'node:fs/promises';
@@ -430,7 +430,7 @@ export function buildManifest({ root, stack = [], sha, version, existing = null,
430
430
  gates: gatesFinal,
431
431
  localSkills: existing?.localSkills ?? [],
432
432
  // ADR-013: profile ids the repo wants, whether they ship centrally or live
433
- // in `.simt-dev/profiles/`. Detection already selects the right profiles; this
433
+ // in `.simt/profiles/`. Detection already selects the right profiles; this
434
434
  // is the declaration a reviewer reads to see the repo's intent stated rather
435
435
  // than inferred.
436
436
  profiles: existing?.profiles ?? [],
@@ -482,8 +482,8 @@ export function buildManifest({ root, stack = [], sha, version, existing = null,
482
482
  // simtDevSha is the digest of the rule bytes this projection was rendered from;
483
483
  // version is the simt-dev release that produced it. The gate compares the running
484
484
  // binary to version and refuses on a mismatch, which is how a repository without
485
- // npm (a global install, pinned only by this field) is held to the release it
486
- // adopted. See src/package-version.mjs.
485
+ // npm (the release binary in `.simt/bin`, pinned only by this field) is held to
486
+ // the release it adopted. See src/package-version.mjs.
487
487
  projection: { simtDevSha: sha ?? null, version: version ?? null },
488
488
  };
489
489
  return withRepoKeys(existing, owned);
@@ -11,7 +11,7 @@
11
11
  // which are interchangeable in a profile (ADR-013) - plus one fallback profile
12
12
  // so a JS/TS repo is never left with core rules and nothing else.
13
13
  //
14
- // Profiles may live in the repo, not only here. `.simt-dev/profiles/*.md` uses
14
+ // Profiles may live in the repo, not only here. `.simt/profiles/*.md` uses
15
15
  // the same frontmatter contract as `rules/stacks/*.md` and is selected by the
16
16
  // same matcher, so supporting an ecosystem nobody here has heard of requires no
17
17
  // code change and no release.
@@ -412,7 +412,7 @@ export async function checkTargets(root, flags = {}) {
412
412
  //
413
413
  // Reported as `orphaned`, never `drift`. ADR-021 ruling 6: `sync` does not delete a
414
414
  // file it once generated, so there is nothing for the reader to repair and a
415
- // `run: simt-dev sync --yes` remedy would be a lie. Removal is a human decision.
415
+ // `run: simt sync --yes` remedy would be a lie. Removal is a human decision.
416
416
  findings.push(...(await orphanedRuleFiles(root, resolved)));
417
417
 
418
418
  // Pointers are checked last, and whether one blocks depends on who reads it.
@@ -604,7 +604,7 @@ async function regionOpts(root) {
604
604
  * A reader that stops at a byte offset never sees a region that ends past it.
605
605
  * The finding names the reader, because "the file is long" does not say who stops.
606
606
  */
607
- // Exported for the context meter (`simt-dev tokens`), which answers the same question
607
+ // Exported for the context meter (`simt tokens`), which answers the same question
608
608
  // about a region rather than about a diff. It reads the structured fields instead of
609
609
  // re-deriving the limit, because a second implementation of "which reader stops, and
610
610
  // where" is a second thing to keep right - and a second thing to quote in a message.
@@ -15,7 +15,7 @@
15
15
  // which are interchangeable in a profile (ADR-013) - plus one fallback profile so
16
16
  // a JS/TS repo is never left with core rules and nothing else.
17
17
  //
18
- // Profiles may live in the repo, not only here. `.simt-dev/profiles/*.md` uses the
18
+ // Profiles may live in the repo, not only here. `.simt/profiles/*.md` uses the
19
19
  // same frontmatter contract as `rules/stacks/*.md` and is selected by the same
20
20
  // matcher, so supporting an ecosystem nobody here has heard of requires no code
21
21
  // change and no release.
@@ -46,6 +46,17 @@ const HERE = dirname(fileURLToPath(import.meta.url));
46
46
  export const ROOT = resolve(HERE, '..', '..');
47
47
  const RULES_DIR = join(ROOT, 'rules');
48
48
 
49
+ // The orientation pointer that follows the command card in every projected region.
50
+ //
51
+ // The agent playbook — `brief` on a new task, `why` before an edit — lives in this
52
+ // package's README and never reaches the agent working in an adopted repository: the
53
+ // projection carries the rules, not the tool that serves them. Without this line the
54
+ // agent has no way to know `simt brief` exists, and "answers over exploration" has no
55
+ // delivery path in the one file every harness reads. Static on purpose: a byte that
56
+ // varied per repository would break the stable prefix this region exists to protect.
57
+ export const ORIENTATION_POINTER =
58
+ `New task? Run \`${CLI} brief "<task>"\` before exploring; before editing a file, \`${CLI} why <file>\`.`;
59
+
49
60
  /**
50
61
  * The identity recorded in generated regions and in the manifest pin.
51
62
  *
@@ -131,7 +142,7 @@ async function walk(dir, prefix = '') {
131
142
  }
132
143
 
133
144
  /** Repo-local profile directory, relative to a repo root (ADR-013 section 2). */
134
- export const LOCAL_PROFILE_DIR = '.simt-dev/profiles';
145
+ export const LOCAL_PROFILE_DIR = '.simt/profiles';
135
146
 
136
147
  /**
137
148
  * Which origin outranks which when two profiles claim one `simt-id`.
@@ -501,7 +512,12 @@ export async function desiredRegion(root, flags) {
501
512
  // symmetric: packing a line that should have been deferred spends the budget the mode
502
513
  // exists to save, while deferring a line that should have been packed costs a reader
503
514
  // one `rules show`. The mode is a promise about bytes, so it is the bytes that win.
504
- const mode = await projectionMode(root);
515
+ // An explicit `mode` flag overrides the declared one. The caller is the MCP
516
+ // `simt://kernel` resource, which by name promises the kernel's bytes for any
517
+ // repository: the declared mode governs what `sync` writes to disk, not what a read
518
+ // path may look at, so the read renders in the mode the URI names instead of
519
+ // re-reading a manifest it must not obey (ADR-002).
520
+ const mode = typeof flags?.mode === 'string' ? flags.mode : await projectionMode(root);
505
521
  const packed = [];
506
522
  const deferred = [];
507
523
  for (const r of inRegion) {
@@ -683,6 +699,10 @@ export async function desiredRegion(root, flags) {
683
699
  const repoRules = kept.filter((r) => !toolRule(r)).sort((a, b) => String(a.id).localeCompare(String(b.id)));
684
700
  let text = render(stable);
685
701
  if (card.text) text += `${text === '' ? '' : '\n\n'}${card.text}`;
702
+ // The pointer follows the card: the card says what this repository runs, and the
703
+ // pointer says how the tool serves a task. Adjacent, they read as one block of
704
+ // repo-facing advice instead of two facts the reader has to connect.
705
+ text += `${text === '' ? '' : '\n\n'}${ORIENTATION_POINTER}`;
686
706
  const repoText = render(repoRules);
687
707
  if (repoText) text += `\n\n${repoText}`;
688
708
  const sources = [...stable, ...repoRules].map((r) => r.source);
@@ -962,7 +982,7 @@ function scopedPaths(rule, resolved) {
962
982
 
963
983
  /**
964
984
  * Read every rule file once, with its frontmatter parsed, plus any repo-local
965
- * profile under `.simt-dev/profiles/`.
985
+ * profile under `.simt/profiles/`.
966
986
  *
967
987
  * A rule without a `simt-id` is a defect, not a default: ids are what `rules why`
968
988
  * and the generated region's source list refer to.
@@ -30,7 +30,7 @@
30
30
  // `.agents/rules/` or `.github/instructions/`, and a symlink into a
31
31
  // directory whose reader's symlink behaviour is unverified is a guess with a
32
32
  // mode bit. So those get machine-written copies — and because every byte is
33
- // derived from `rules/mdc/*.mdc` and `simt-dev check` compares each against
33
+ // derived from `rules/mdc/*.mdc` and `simt check` compares each against
34
34
  // that same derivation, they are not duplication, they are a build output.
35
35
  // The difference from that duplicate is that nobody can hand-edit one
36
36
  // and have it survive a sync.
@@ -387,12 +387,30 @@ export const SURFACE_IDS = SURFACES.map((s) => s.id);
387
387
  * this repository", never "this harness is installed" and never "its rules are not
388
388
  * being loaded".
389
389
  *
390
+ * The exception that proves the rule is `.gemini`: R42 read Gemini CLI's
391
+ * configuration docs, so its entry carries readsAgentsMd and a `verified` the same way
392
+ * a surface row does. Everyone else stays a bare name until somebody reads their
393
+ * vendor pages — the bar for facts here is a document, not an ML search snippet, and
394
+ * that bar is what keeps this list honest as harnesses come and go.
395
+ *
390
396
  * `.github` is absent even though the Copilot row carries it: `.github` holds workflows
391
397
  * and issues, so its presence says nothing about whether Copilot is used, and the
392
398
  * Copilot row's own detection is not something this command is for.
393
399
  */
394
400
  export const CANDIDATE_DIRS = [
395
- { dir: '.gemini', harness: 'Gemini CLI' },
401
+ {
402
+ // The one candidate with verified facts (R42): Gemini CLI's configuration reference
403
+ // was live and read on 2026-10-26, so presence now carries provenance instead of
404
+ // the default "a reader may be configured here". AGENTS.md is loaded only when
405
+ // settings.json `context.fileName` names it - the context file is GEMINI.md
406
+ // otherwise - so the fact is "some", which here means one reader under
407
+ // configuration, and the note says exactly that.
408
+ dir: '.gemini',
409
+ harness: 'Gemini CLI',
410
+ readsAgentsMd: 'some',
411
+ readsAgentsMdNote: 'AGENTS.md is loaded only when settings.json context.fileName names it; the context file is GEMINI.md otherwise',
412
+ verified: 'Gemini CLI configuration docs (geminicli.com/docs/reference/configuration/, read 2026-10-26) — a .gemini directory holds the project settings.json, and the reader loads GEMINI.md unless context.fileName renames it',
413
+ },
396
414
  { dir: '.windsurf', harness: 'Windsurf' },
397
415
  { dir: '.zed', harness: 'Zed' },
398
416
  { dir: '.kiro', harness: 'Kiro' },
@@ -5,10 +5,10 @@
5
5
  // consumer: it sends the brief to the endpoint named in the manifest (R37) and asks
6
6
  // for an ADR-008 verdict skeleton, then verifies the verdict vocabulary and every
7
7
  // cited id against that brief before the reply becomes a proposal under
8
- // `.simt-dev/reviews/`. A proposal a human never reads is a dead file, which is the
8
+ // `.simt/reviews/`. A proposal a human never reads is a dead file, which is the
9
9
  // intended failure mode (ADR-075, ADR-090).
10
10
  //
11
- // Nothing here writes outside `.simt-dev/reviews/`, and nothing here edits a source
11
+ // Nothing here writes outside `.simt/reviews/`, and nothing here edits a source
12
12
  // file — the model is a critic, not an author. The verdict is advisory by
13
13
  // construction; no gate reads it. The row's own promotion rule names the test:
14
14
  // precision at least 0.8 on a labelled set of historical diffs. Until
@@ -32,9 +32,9 @@ import { digestOf, promptHash, proposalFrontmatter } from '../distill/engine.mjs
32
32
 
33
33
  const exec = promisify(execFile);
34
34
 
35
- // Locked on-disk beneath `.simt-dev`, like `.simt-dev/distilled` (R38) — a cache of
35
+ // Locked on-disk beneath `.simt`, like `.simt/distilled` (R38) — a cache of
36
36
  // proposals keyed by content digest, never projected and never committed.
37
- const REVIEW_DIR = '.simt-dev/reviews';
37
+ const REVIEW_DIR = '.simt/reviews';
38
38
  const ENGINE_REVIEW_LOCAL = 'review-local';
39
39
 
40
40
  /** The diff brief's byte cap; `--budget` overrides it, `0` means unlimited. */
package/src/toolwords.mjs CHANGED
@@ -21,21 +21,24 @@ import { CHECK_MARKER } from './kernel/checks.mjs';
21
21
  import { VARIANTS_MARKER } from './kernel/variants.mjs';
22
22
  import { LESSON_KEYS } from './memory/lessons.mjs';
23
23
  import { DECISION_KEYS } from './memory/decide.mjs';
24
+ import { PROMPT_WORDS } from './mcp/prompts.mjs';
24
25
 
25
26
  /**
26
27
  * `simt-` names that belong to this tool and never to a repository.
27
28
  *
28
- * The last three are not frontmatter: they are names this tool writes into files it
29
- * adopts a repository into — the package, the gate file it puts in `.github`, and the
30
- * hooks directory it points `core.hooksPath` at. `simt-dev` itself is here for the same
31
- * reason the estate pattern exempts it, and stating it once here is what stops the two
32
- * readers from disagreeing about the tool's own name.
29
+ * The three markers and the four suffixed names are not frontmatter: they are names this
30
+ * tool writes into files it adopts a repository into — the package, the gate file it puts
31
+ * in `.github`, the hooks directory it points `core.hooksPath` at, and the two prompts it
32
+ * serves over MCP, derived from the module that owns them. `simt-dev` itself is here for
33
+ * the same reason the estate pattern exempts it, and stating it once here is what stops
34
+ * the two readers from disagreeing about the tool's own name.
33
35
  */
34
36
  export const TOOL_WORDS = new Set([
35
37
  ...KNOWN_KEYS,
36
38
  ...PROFILE_KEYS,
37
39
  ...LESSON_KEYS,
38
40
  ...DECISION_KEYS,
41
+ ...PROMPT_WORDS,
39
42
  INFERABLE_MARKER,
40
43
  CHECK_MARKER,
41
44
  VARIANTS_MARKER,
package/src/tsconfig.mjs CHANGED
@@ -16,8 +16,8 @@
16
16
  // tool's output rather than the file's.
17
17
  //
18
18
  // The whole file is owned by `init` alone. It is not in `src/ci/` and not in
19
- // `src/projection/baselines.mjs` because it is neither a `.github` file that `simt-dev ci`
20
- // refreshes nor a marker region: `simt-dev check` reports its absence and never its contents.
19
+ // `src/projection/baselines.mjs` because it is neither a `.github` file that `simt ci`
20
+ // refreshes nor a marker region: `simt check` reports its absence and never its contents.
21
21
 
22
22
  import { readFile } from 'node:fs/promises';
23
23
  import { join } from 'node:path';
@@ -174,7 +174,7 @@ export function readJsonc(text) {
174
174
  *
175
175
  * There is no `refresh`. ADR-026 gave simt-dev's own `.github` files a refresh path because
176
176
  * this repository is the template's only author; a base file has the same argument, and it
177
- * is deliberately not extended to it. `simt-dev check` in this repository reports the
177
+ * is deliberately not extended to it. `simt check` in this repository reports the
178
178
  * baseline files as unmanaged by design, so a refresh path here would be a way to make this
179
179
  * repository's own gate green by rewriting the file it is judging — which is the same
180
180
  * mistake as a gate that edits its own expectations.
@@ -29,7 +29,7 @@ export function planUpgrade({ name, path, manifest, manifestExists, manifestErro
29
29
  const primary = list[0] ?? null;
30
30
  const base = { name, path, sha, pinnedSha: manifest?.projection?.simtDevSha ?? null };
31
31
 
32
- // A repo that never adopted is not behind; it is absent. `simt-dev init` is the command
32
+ // A repo that never adopted is not behind; it is absent. `simt init` is the command
33
33
  // for it, and calling this a failure would make the exit code describe the estate's
34
34
  // migration progress rather than this run's correctness.
35
35
  if (!manifestExists) {
File without changes