simt-dev 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/bin/simt-dev.mjs +21 -2
  3. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  4. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  5. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  6. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  7. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  8. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  9. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  10. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  11. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  12. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  13. package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
  14. package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
  15. package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
  16. package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
  17. package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
  18. package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
  19. package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
  20. package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
  21. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
  22. package/package.json +2 -1
  23. package/rules/core.md +1 -0
  24. package/rules/stacks/android.md +2 -0
  25. package/rules/stacks/cloudflare-workers.md +2 -0
  26. package/rules/stacks/connectiq.md +1 -0
  27. package/rules/stacks/container.md +2 -0
  28. package/rules/stacks/dart.md +2 -0
  29. package/rules/stacks/deno.md +2 -0
  30. package/rules/stacks/dotnet.md +2 -0
  31. package/rules/stacks/elixir.md +2 -0
  32. package/rules/stacks/go.md +2 -0
  33. package/rules/stacks/jvm.md +2 -0
  34. package/rules/stacks/node.md +2 -0
  35. package/rules/stacks/python.md +2 -0
  36. package/rules/stacks/react-native.md +2 -0
  37. package/rules/stacks/react-web.md +2 -0
  38. package/rules/stacks/ruby.md +2 -0
  39. package/rules/stacks/rust.md +2 -0
  40. package/rules/stacks/swift.md +2 -0
  41. package/rules/stacks/typescript.md +2 -0
  42. package/rules/stacks/uv.md +2 -0
  43. package/src/agenthooks/decide.mjs +123 -0
  44. package/src/agenthooks/policy.mjs +78 -0
  45. package/src/agenthooks/render.mjs +240 -0
  46. package/src/argv.mjs +55 -1
  47. package/src/bench/ablate.mjs +630 -0
  48. package/src/bench/adapters.json +149 -0
  49. package/src/bench/grade.mjs +229 -0
  50. package/src/bench/mine.mjs +811 -0
  51. package/src/bench/probes.mjs +1350 -0
  52. package/src/bench/report.mjs +899 -0
  53. package/src/bench/run.mjs +1002 -0
  54. package/src/commands/agenthook.mjs +282 -0
  55. package/src/commands/bench.mjs +359 -0
  56. package/src/commands/compare.mjs +15 -0
  57. package/src/commands/graph.mjs +45 -21
  58. package/src/commands/harness.mjs +301 -0
  59. package/src/commands/help.mjs +73 -2
  60. package/src/commands/index.mjs +67 -25
  61. package/src/commands/init.mjs +12 -8
  62. package/src/commands/mcp.mjs +20 -5
  63. package/src/commands/plan.mjs +99 -0
  64. package/src/commands/rules.mjs +42 -57
  65. package/src/commands/skills.mjs +67 -92
  66. package/src/commands/skillsinstall.mjs +17 -2
  67. package/src/commands/sync.mjs +62 -6
  68. package/src/commands/tokens.mjs +169 -0
  69. package/src/compare/row.mjs +73 -2
  70. package/src/decisions/index.mjs +23 -5
  71. package/src/graph/spec.mjs +227 -0
  72. package/src/graph/speccheck.mjs +68 -0
  73. package/src/hooks/dispatcher.mjs +35 -3
  74. package/src/kernel/card.mjs +254 -0
  75. package/src/kernel/inferability.mjs +188 -0
  76. package/src/mcp/resources.mjs +157 -0
  77. package/src/mcp/schema.mjs +139 -0
  78. package/src/mcp/server.mjs +228 -20
  79. package/src/mcp/tools.mjs +372 -1
  80. package/src/measure/context.mjs +542 -0
  81. package/src/measure/loaderView.mjs +435 -0
  82. package/src/measure/session.mjs +267 -0
  83. package/src/monorepo.mjs +339 -0
  84. package/src/output/index.mjs +78 -2
  85. package/src/packs.mjs +182 -0
  86. package/src/plan/index.mjs +650 -0
  87. package/src/plan/template.mjs +331 -0
  88. package/src/projection/loaders.mjs +689 -0
  89. package/src/projection/manifest.mjs +114 -3
  90. package/src/projection/project.mjs +140 -9
  91. package/src/projection/rules.mjs +365 -22
  92. package/src/projection/surfaces.mjs +155 -23
  93. package/src/projection/targets.mjs +37 -0
  94. package/src/toolwords.mjs +69 -0
  95. package/templates/plan/tracker.json +36 -0
@@ -41,7 +41,11 @@
41
41
  // #19377), and the documented examples are not valid YAML. Every rule
42
42
  // shipped here is always-on, so the key would buy nothing and cost a
43
43
  // silent-failure surface. Absence of frontmatter is documented to load
44
- // eagerly, which is exactly what is wanted.
44
+ // eagerly, which is exactly what is wanted. (S50 ruled on this: a rule
45
+ // that carries a scope is emitted ONLY for surfaces whose scoping key is
46
+ // documented and parsed correctly, and Claude is not one of them, which
47
+ // is why `canScope` below excludes it rather than trusting `documentedKeys`
48
+ // to carry the reason.)
45
49
  //
46
50
  // 4. `verified` IS THE POINT OF THE TABLE. A row whose `verified` is null was
47
51
  // not checked against a vendor document, and the row says so where a reader
@@ -57,6 +61,22 @@
57
61
  * - `file` a wholly generated file; hand-written body text is retired
58
62
  * - `pointer` a symlink to the primary target; carries no content of its own
59
63
  *
64
+ * `readsAgentsMd` is a vendor fact, in three values rather than two:
65
+ * - `all` every reader this row names loads AGENTS.md
66
+ * - `some` some of them do, and `readsAgentsMdNote` says which
67
+ * - `none` none does, so the row is the only channel its readers have
68
+ *
69
+ * It is not `readsPrimary`, which asks a different question: that one is about
70
+ * whether a per-rule file would duplicate text the region already carries, and it is
71
+ * only meaningful while folding is on. This one is about the FILE, and it is what
72
+ * decides whether a new harness needs a row at all - a harness that reads AGENTS.md
73
+ * is supported by the standard surface, whatever else it reads.
74
+ *
75
+ * `configDirs` are the directories whose presence implies the harness is in use.
76
+ * Detection reads them and claims nothing else: a directory a harness may create for
77
+ * other reasons is a presence, not a proof, and the command says "present", never
78
+ * "installed".
79
+ *
60
80
  * `ruleFrontmatter` returns the keys that editor reads BEFORE any rule text. A
61
81
  * surface that needs none returns '' and the projected file starts at the body.
62
82
  */
@@ -71,17 +91,19 @@ export const SURFACES = [
71
91
  id: 'opencode',
72
92
  label: 'opencode (AGENTS.md + .agents/skills)',
73
93
  readers:
74
- 'opencode, and AGENTS.md is also read natively by Codex, Cursor, Windsurf, Zed, Amp, Jules, Copilot CLI and the DeepSeek harness; Claude Code falls back to it, Antigravity and Gemini CLI accept GEMINI.md as an equal name',
94
+ 'OpenCode v2, and AGENTS.md is also read natively by Codex, Cursor, Windsurf, Zed, Amp, Jules, Copilot CLI and the DeepSeek harness; Claude Code falls back to it, Antigravity and Gemini CLI accept GEMINI.md as an equal name',
75
95
  primary: 'AGENTS.md',
76
96
  kind: 'region',
77
- // opencode documents NO per-rule directory: it reads AGENTS.md (walking up to the
78
- // git worktree), CLAUDE.md only as a fallback, and .opencode/AGENTS.md in
79
- // addition. So `foldRules` — this surface carries every rule inside the one
80
- // generated region, and the other rows are how a rule reaches an editor that
97
+ // OpenCode v2 documents no per-rule directory. It reads AGENTS.md only — every
98
+ // file from the workspace up to the project root, combined — and it does not fall
99
+ // back to CLAUDE.md. So `foldRules`: this surface carries every rule inside the
100
+ // one generated region, and the other rows are how a rule reaches an editor that
81
101
  // cannot read AGENTS.md. Writing .opencode/rules/*.md would be a guess, and a
82
102
  // guessed path is the failure this table exists to prevent.
83
103
  rule: null,
84
104
  foldRules: true,
105
+ readsAgentsMd: 'all',
106
+ configDirs: ['.opencode'],
85
107
  skills: '.agents/skills',
86
108
  documentedSkillsPaths: ['.opencode/skills', '.claude/skills', '.agents/skills'],
87
109
  // Codex concatenates AGENTS.md from the root down and stops at
@@ -96,7 +118,7 @@ export const SURFACES = [
96
118
  },
97
119
  ],
98
120
  verified:
99
- 'opencode docs, "Rules" and "Agent Skills" (read 2026-10-07) — AGENTS.md walked up from cwd to the git worktree; CLAUDE.md used only if no AGENTS.md exists; .opencode/AGENTS.md additionally; skills at .opencode/skills/, .claude/skills/ and .agents/skills/<name>/SKILL.md; skill frontmatter recognises only name, description, license, compatibility, metadata',
121
+ 'OpenCode v2 instructions and skills docs (opencode.ai/v2/docs, read 2026-10-14) — AGENTS.md only, combined from the workspace up to the project root; CLAUDE.md is not a fallback; skills at .opencode/skills/, .claude/skills/ and .agents/skills/<name>/SKILL.md, with the model shown name and description',
100
122
  note:
101
123
  '.agents/skills is one of the three skill locations opencode documents, so the same directory serves the standard surface without a symlink.',
102
124
  },
@@ -107,6 +129,8 @@ export const SURFACES = [
107
129
  primary: null,
108
130
  kind: 'file',
109
131
  rule: '.cursor/rules/{name}.mdc',
132
+ readsAgentsMd: 'all',
133
+ configDirs: ['.cursor'],
110
134
  // `documentedKeys` is the set the vendor's own docs name for this surface, and
111
135
  // `ruleFrontmatter` must emit nothing outside it. A key invented here would be
112
136
  // inert at best; on Antigravity an unrecognised value silently discards the
@@ -129,12 +153,17 @@ export const SURFACES = [
129
153
  // value. One body is written for all of them, so a Cursor name must not land
130
154
  // in a file Claude Code also reads.
131
155
  substitutions: { askTool: 'Use the interactive `AskQuestion` tool' },
132
- ruleFrontmatter: (rule) =>
133
- [
134
- `description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
135
- `alwaysApply: ${rule.alwaysApply ? 'true' : 'false'}`,
136
- ],
137
- verified: 'Cursor project rules (.cursor/rules/*.mdc, description + alwaysApply + globs)',
156
+ ruleFrontmatter: (rule) => [
157
+ `description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
158
+ // `globs` only appears when the rule declares a scope. Cursor attaches a rule
159
+ // with no globs on the strength of `alwaysApply` alone, so emitting either
160
+ // unquoted patterns on an eager rule or no globs at all on a scoped one is
161
+ // asking for the wrong loading behaviour.
162
+ ...(rule.scope ? [`globs: ${rule.scope.map((g) => `"${g}"`).join(', ')}`] : []),
163
+ `alwaysApply: ${rule.alwaysApply ? 'true' : 'false'}`,
164
+ ],
165
+ verified:
166
+ 'Cursor project rules (.cursor/rules/*.mdc, description + alwaysApply + globs); alwaysApply: false with globs is "auto-attached when a matching file is in context", and the docs warn that alwaysApply: false with no description is manual @-mention only',
138
167
  note: 'The only surface that reads `.mdc`. Every other row here is a plain `.md` or a symlink.',
139
168
  },
140
169
  {
@@ -144,6 +173,16 @@ export const SURFACES = [
144
173
  primary: null,
145
174
  kind: 'file',
146
175
  rule: '.claude/rules/{name}.md',
176
+ // `some`, not `none`. It reads AGENTS.md - the same docs say CLAUDE.md is used only
177
+ // when no AGENTS.md exists - but only on that condition, so a repository that has a
178
+ // hand-written CLAUDE.md is read by neither. That is why this row still needs its
179
+ // own channel and a pointer: the fallback is not a channel, it is the absence of
180
+ // one, and a reader who relied on it would be relying on the reader deleting a
181
+ // file they wrote.
182
+ readsAgentsMd: 'some',
183
+ readsAgentsMdNote:
184
+ 'CLAUDE.md is read first; AGENTS.md is read only when there is no CLAUDE.md, which is why the pointer exists',
185
+ configDirs: ['.claude'],
147
186
  // Intentionally no `paths:`. See decision 3 in the header.
148
187
  documentedKeys: ['paths', 'description'],
149
188
  ruleFrontmatter: () => [],
@@ -163,6 +202,8 @@ export const SURFACES = [
163
202
  primary: null,
164
203
  kind: 'file',
165
204
  rule: '.agents/rules/{name}.md',
205
+ readsAgentsMd: 'all',
206
+ configDirs: ['.agents'],
166
207
  // `trigger` is REQUIRED here and an unrecognised value discards the rule with no
167
208
  // message, which is why the value is mapped rather than passed through.
168
209
  documentedKeys: ['trigger', 'description', 'globs', 'glob'],
@@ -173,10 +214,15 @@ export const SURFACES = [
173
214
  // what makes them duplicates rather than a fallback.
174
215
  readsPrimary: true,
175
216
  ruleFrontmatter: (rule) => [
176
- `trigger: ${rule.alwaysApply ? 'always_on' : 'model_decision'}`,
217
+ // `trigger: glob` carries the patterns, and they are quoted because YAML reads a
218
+ // value starting with `*` as an alias anchor — the Antigravity docs require it, and
219
+ // an invalid frontmatter block is discarded with no message at all.
220
+ `trigger: ${rule.scope ? 'glob' : rule.alwaysApply ? 'always_on' : 'model_decision'}`,
221
+ ...(rule.scope ? [`globs: ${rule.scope.map((g) => `"${g}"`).join(', ')}`] : []),
177
222
  `description: ${rule.description ?? `Canonical ${rule.id} rule managed by simt-dev`}`,
178
223
  ],
179
- verified: 'Google Antigravity docs, "Rules" — .agents/rules/*.md, YAML frontmatter with trigger: always_on | model_decision | glob | manual; missing or invalid frontmatter silently discards the rule',
224
+ verified:
225
+ 'Google Antigravity docs, "Rules" — .agents/rules/*.md, YAML frontmatter with trigger: always_on | model_decision | glob | manual; missing or invalid frontmatter silently discards the rule; trigger: glob takes comma-separated globs, which the docs require to be quoted so YAML does not read a leading * as an alias anchor; a 24 KB per-file limit and a 20,000-token budget for always_on rules, with automatic demotion to pointers past it',
180
226
  note:
181
227
  'This is why frontmatter is mandatory here and why the trigger value is not guessed: an unrecognised trigger means the rule is dropped with no message.',
182
228
  },
@@ -187,10 +233,18 @@ export const SURFACES = [
187
233
  primary: null,
188
234
  kind: 'file',
189
235
  rule: '.github/instructions/{name}.instructions.md',
236
+ // Copilot is why the column is three values and not two. Its CLI reads AGENTS.md
237
+ // and its editor and code review do not, so the row both needs its own channel and
238
+ // cannot be described as "reads AGENTS.md" without a reader reading that claim as
239
+ // "this repo is covered for Copilot", which for two of its three readers it is not.
240
+ readsAgentsMd: 'some',
241
+ readsAgentsMdNote:
242
+ 'Copilot CLI reads AGENTS.md; the editor, code review and the cloud agent do not',
243
+ configDirs: ['.github'],
190
244
  documentedKeys: ['applyTo', 'excludeAgent'],
191
- ruleFrontmatter: () => ['applyTo: "**"'],
245
+ ruleFrontmatter: (rule) => [`applyTo: ${rule.scope ? `"${rule.scope.join(',')}"` : '"**"'}`],
192
246
  verified:
193
- 'GitHub docs, "Adding repository custom instructions" — .github/instructions/NAME.instructions.md with applyTo globs; `applyTo: "**"` is the documented example',
247
+ 'GitHub docs, "Adding repository custom instructions" — .github/instructions/NAME.instructions.md with applyTo globs; `applyTo: "**"` is the documented example, and a comma-separated pattern list is how more than one is named',
194
248
  // NOT marked readsPrimary, and this row is the reason the flag is per-surface
195
249
  // rather than global. The reader is split: Copilot CLI reads AGENTS.md, the
196
250
  // editor and code review do not. A per-rule file here is not duplication for two
@@ -232,8 +286,12 @@ export const POINTERS = [
232
286
  to: 'AGENTS.md',
233
287
  readers: 'Gemini CLI, Antigravity',
234
288
  verified:
235
- 'Google Antigravity docs, "Rules": workspace rules are AGENTS.md or GEMINI.md in the root or any subdirectory; Copilot CLI also discovers GEMINI.md',
236
- note: 'Covers Gemini CLI and gives Antigravity a second, equally-native name for the same file.',
289
+ 'Google Antigravity docs, "Rules": workspace rules are AGENTS.md or GEMINI.md in the root or any subdirectory; Copilot CLI also discovers GEMINI.md. ' +
290
+ 'Gemini CLI, read today: its own docs (core memory index, and the configuration reference) say the memory service loads GEMINI.md hierarchically from the working directory up to the project root and the home directory and also into subdirectories, and that `context.fileName` in settings.json "accepts either a single string or an array of strings" naming the context file(s) to load, default undefined',
291
+ note:
292
+ 'Covers Gemini CLI and gives Antigravity a second, equally-native name for the same file. ' +
293
+ 'A repository can also point Gemini CLI at AGENTS.md itself with `context.fileName: ["AGENTS.md"]` and need no pointer at all, ' +
294
+ 'which is the reason there is no separate Gemini CLI row here: this pointer and a native row would be two ways to hand one reader the same rule.',
237
295
  },
238
296
  ];
239
297
 
@@ -277,6 +335,27 @@ export function foldsRules(resolved) {
277
335
  return resolved.surfaces.some((s) => s.foldRules === true);
278
336
  }
279
337
 
338
+ /**
339
+ * Whether this surface can load a rule only when a matching file is in play.
340
+ *
341
+ * Three do, and each for its own documented key: Cursor patterns with `globs` under
342
+ * `alwaysApply: false`, Copilot with `applyTo`, and Antigravity with `trigger: glob`
343
+ * plus `globs`. Claude Code does not — its `paths:` key has a documented parser
344
+ * defect in which quoted values and YAML sequences silently fail to lazy-load — and
345
+ * opencode cannot, because it has no per-rule file channel at all.
346
+ *
347
+ * The question is asked per surface rather than per key because the answer decides
348
+ * whether a scoped profile may leave the region: a profile written only as a scoped
349
+ * file would be absent from a reader that cannot scope it, and a rule nobody loads is
350
+ * worse than a rule that loads eagerly (ADR-071).
351
+ */
352
+ export function canScope(surface) {
353
+ return surface.rule !== null && surface.rule !== undefined && SCOPE_KEYS.has(surface.id);
354
+ }
355
+
356
+ /** Surface ids whose scoping key was read in the vendor's own documentation. */
357
+ const SCOPE_KEYS = new Set(['cursor', 'antigravity', 'copilot']);
358
+
280
359
  /**
281
360
  * Surfaces whose reader already loads the primary document, so a per-rule file for
282
361
  * them repeats text the region already carries.
@@ -295,6 +374,47 @@ export function readsPrimary(resolved) {
295
374
 
296
375
  export const SURFACE_IDS = SURFACES.map((s) => s.id);
297
376
 
377
+ /**
378
+ * Config directories whose presence implies a harness is in use, for the ones this
379
+ * tool has no surface row for.
380
+ *
381
+ * Deliberately a list of DIRECTORY NAMES and nothing else. It carries no claim about
382
+ * what any of these readers loads: the review that proposed the command listed these
383
+ * harnesses as candidates and explicitly did not re-verify each vendor's current
384
+ * documentation, and a row built from that list would ship a `verified` field citing
385
+ * nothing — decoration that misdescribes the artefact, which is the failure this whole
386
+ * repository exists to prevent. A directory here means "a reader may be configured in
387
+ * this repository", never "this harness is installed" and never "its rules are not
388
+ * being loaded".
389
+ *
390
+ * `.github` is absent even though the Copilot row carries it: `.github` holds workflows
391
+ * and issues, so its presence says nothing about whether Copilot is used, and the
392
+ * Copilot row's own detection is not something this command is for.
393
+ */
394
+ export const CANDIDATE_DIRS = [
395
+ { dir: '.gemini', harness: 'Gemini CLI' },
396
+ { dir: '.windsurf', harness: 'Windsurf' },
397
+ { dir: '.zed', harness: 'Zed' },
398
+ { dir: '.kiro', harness: 'Kiro' },
399
+ { dir: '.cline', harness: 'Cline' },
400
+ { dir: '.roo', harness: 'Roo Code' },
401
+ { dir: '.junie', harness: 'Junie' },
402
+ { dir: '.aider', harness: 'Aider' },
403
+ { dir: '.continue', harness: 'Continue' },
404
+ { dir: '.amp', harness: 'Amp' },
405
+ { dir: '.goose', harness: 'Goose' },
406
+ // Home directory, seen only with `harness detect --user`. DeepSeek keeps user data
407
+ // under ~/.dsh and reads the repository's AGENTS.md; it has no project config
408
+ // directory and no per-rule file to project (ADR-067).
409
+ {
410
+ dir: '.dsh',
411
+ harness: 'DeepSeek harness',
412
+ readsAgentsMd: 'all',
413
+ verified:
414
+ 'DeepSeek harness agent-instructions docs (read 2026-10-14): user data lives at $DSH_HOME or ~/.dsh, and the project chain is AGENTS.md',
415
+ },
416
+ ];
417
+
298
418
  /**
299
419
  * Frontmatter keys a surface emits that its own vendor does not document.
300
420
  *
@@ -313,10 +433,22 @@ export function undocumentedKeys(surfaceRows = SURFACES) {
313
433
  for (const s of surfaceRows) {
314
434
  if (!s.ruleFrontmatter) continue;
315
435
  const documented = Array.isArray(s.documentedKeys) ? s.documentedKeys : [];
316
- for (const line of s.ruleFrontmatter({ id: 'probe', description: 'probe', alwaysApply: true })) {
317
- const key = String(line).split(':')[0].trim();
318
- if (!documented.includes(key)) {
319
- bad.push(`${s.id} emits frontmatter key "${key}"; its vendor documents ${documented.join(', ') || '(nothing)'}`);
436
+ // Both shapes, because a scoping key is emitted only on the scoped shape: probing
437
+ // the eager one alone would pass a row that drops `globs` on a scoped rule, and the
438
+ // rule would then load on nothing.
439
+ const probes = [
440
+ { id: 'probe', description: 'probe', alwaysApply: true },
441
+ { id: 'probe', description: 'probe', alwaysApply: false, scope: ['src/**'] },
442
+ ];
443
+ for (const rule of probes) {
444
+ for (const line of s.ruleFrontmatter(rule)) {
445
+ const key = String(line).split(':')[0].trim();
446
+ // One report per key, not per probe: the same invented key appears on both
447
+ // shapes, and a gate that prints it twice reads as two defects.
448
+ if (documented.includes(key) || bad.some((b) => b.includes(`key "${key}"`))) continue;
449
+ bad.push(
450
+ `${s.id} emits frontmatter key "${key}"; its vendor documents ${documented.join(', ') || '(nothing)'}`,
451
+ );
320
452
  }
321
453
  }
322
454
  }
@@ -136,6 +136,43 @@ export async function ruleTargets(root, rule, body, sha, resolved, notes) {
136
136
  return { targets };
137
137
  }
138
138
 
139
+ /**
140
+ * Per-rule files for the profiles that left the region, for the surfaces that can
141
+ * narrow them.
142
+ *
143
+ * A scoped profile is an ordinary rule with a `scope` on it, and that is the whole
144
+ * reason this is a thin wrapper over {@link ruleTargets} rather than a second
145
+ * construction: the byte-level concerns — frontmatter re-derived per surface, the
146
+ * region injected between the markers, the retired-prose note — are already decided
147
+ * there and must not be decided twice. A second implementation would drift from the
148
+ * first and the gate would report drift on a clean tree, which is the failure this
149
+ * module exists to prevent.
150
+ *
151
+ * The one thing it adds is the skip: a surface that folds the region carries the whole
152
+ * region already, so a scoped file is the duplication S12 removed — UNLESS the profile
153
+ * actually left the region, which is `narrowing`, the caller's answer, and which means
154
+ * the reader would otherwise get nothing.
155
+ */
156
+ export async function scopedTargets(root, rules, sha, resolved, notes) {
157
+ const targets = [];
158
+ for (const rule of rules) {
159
+ // `alwaysApply: false` is what asks the surface to attach the file on the patterns
160
+ // rather than always. Cursor's documented reading of no description plus no globs is
161
+ // a manual @-mention, so a scoped rule carries its scope and the description it
162
+ // already had.
163
+ // `target` is what `ruleName` reads, and a stack profile has none: it is a plain
164
+ // `python.md`, not an `.mdc` in the rules directory. Without it the file is named
165
+ // `null.mdc`, which is exactly what the first version of this did.
166
+ const scoped = {
167
+ ...rule,
168
+ target: rule.target ?? `${rule.id}.mdc`,
169
+ alwaysApply: false,
170
+ scope: rule.scope,
171
+ };
172
+ targets.push(...(await ruleTargets(root, scoped, rule.body, sha, resolved, notes)).targets);
173
+ }
174
+ return { targets };
175
+ }
139
176
 
140
177
  /**
141
178
  * Pointer targets: `CLAUDE.md` and `GEMINI.md` as symlinks to `AGENTS.md`.
@@ -0,0 +1,69 @@
1
+ // The `simt-` names this tool owns, and the records that argue about the prefix itself.
2
+ //
3
+ // Two readers need to tell this tool's own vocabulary from a repository in the estate
4
+ // these records were written about: the decision reader, which redacts the second kind
5
+ // and names it, and the guard on shipped text, which refuses to ship it. Both answered
6
+ // the question with a shape — `simt-` followed by anything but `dev` — and the shape is
7
+ // false about the tool's own names, which the corpus prints. `rules why ADR-073` reported
8
+ // that the ruling surveyed `simt-priority` as a repository, and printed the same word as
9
+ // `a repository: hard | command | convention | style` where the record says
10
+ // `simt-priority: hard | command | convention | style`: a reader with the tool and not
11
+ // the estate got a sentence with no subject, from the command whose whole job is to
12
+ // explain a citation.
13
+ //
14
+ // The words are derived from the modules that own them (ADR-022) rather than listed
15
+ // here, because a second list is a list that drifts and then forgives. Both readers read
16
+ // this one, and a name added to a frontmatter vocabulary reaches both without an edit.
17
+ import { KNOWN_KEYS } from './projection/mdc.mjs';
18
+ import { PROFILE_KEYS } from './projection/rules.mjs';
19
+ import { INFERABLE_MARKER } from './kernel/inferability.mjs';
20
+
21
+ /**
22
+ * `simt-` names that belong to this tool and never to a repository.
23
+ *
24
+ * The last three are not frontmatter: they are names this tool writes into files it
25
+ * adopts a repository into — the package, the gate file it puts in `.github`, and the
26
+ * hooks directory it points `core.hooksPath` at. `simt-dev` itself is here for the same
27
+ * reason the estate pattern exempts it, and stating it once here is what stops the two
28
+ * readers from disagreeing about the tool's own name.
29
+ */
30
+ export const TOOL_WORDS = new Set([
31
+ ...KNOWN_KEYS,
32
+ ...PROFILE_KEYS,
33
+ INFERABLE_MARKER,
34
+ 'simt-dev',
35
+ 'simt-dev-check',
36
+ 'simt-hooks',
37
+ ]);
38
+
39
+ /**
40
+ * Records whose subject is the prefix itself, so the prefixed names in them are the
41
+ * argument rather than a citation.
42
+ *
43
+ * Each entry is a file and a reason, never a repository. That distinction is the whole
44
+ * point: an exemption says "this document discusses the namespace", which is a fact
45
+ * about this tool and carries nothing about whoever uses it. A vocabulary cannot settle
46
+ * these two — a hypothetical key in an argument and a repository name are the same shape
47
+ * — so the file is exempted where the word cannot be.
48
+ *
49
+ * The guard fails on a stale entry: each one must still be tripping the rule, so an
50
+ * exemption that stops being needed is removed rather than kept as a precedent.
51
+ */
52
+ export const PREFIX_ARGUING = new Map([
53
+ [
54
+ 'ADR-022-a-key-simt-dev-reads-is-prefixed.md',
55
+ 'is the record arguing for the prefix, using hypothetical keys as examples',
56
+ ],
57
+ [
58
+ 'ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md',
59
+ 'names one prefixed key to record Dependabot rejecting it',
60
+ ],
61
+ [
62
+ 'ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md',
63
+ 'quotes the prefixed names it measured, which are its evidence rather than a citation',
64
+ ],
65
+ [
66
+ 'ADR-081-a-tracker-is-read-through-the-repositorys-template.md',
67
+ 'names the plan overlay file the next record withdraws, and that filename has the shape of a repository',
68
+ ],
69
+ ]);
@@ -0,0 +1,36 @@
1
+ {
2
+ "$comment": "Tracker format delivered with the tool. A repository replaces any section with the plans key in simt.json; a section it omits stays as written here. directories are the only places read, and each word is what a dependency uses to name that place (\"review 0002 S41\"). Headers are tried in the order written. Only a state marked satisfies unblocks a slice that depends on it. ready is the state plan next returns. dependency.group matches a wave of rows; wave names that table's columns.",
3
+ "name": "tracker",
4
+ "directories": [
5
+ { "kind": "plans", "word": "plan", "path": "docs/plans" },
6
+ { "kind": "reviews", "word": "review", "path": "docs/reviews" }
7
+ ],
8
+ "id": {
9
+ "pattern": "^[A-Za-z]{0,4}[- ]?[0-9]{1,4}$",
10
+ "headers": ["id", "#", "step", "item", "slice", "task"]
11
+ },
12
+ "columns": {
13
+ "state": ["state", "status", "done when"],
14
+ "depends": ["depends on", "depends", "dependency", "dependencies", "deps", "needs"],
15
+ "title": ["slice", "deliverable", "title", "summary", "description", "content"],
16
+ "evidence": ["sha / evidence", "sha/evidence", "evidence", "sha", "notes", "note"]
17
+ },
18
+ "states": [
19
+ { "id": "queued", "write": "Queued", "read": ["queued"] },
20
+ { "id": "in-progress", "write": "In progress", "read": ["in progress", "in-progress"] },
21
+ { "id": "complete", "write": "Complete", "read": ["complete", "completed", "done"], "satisfies": true },
22
+ { "id": "accepted", "write": "Accepted", "read": ["accepted"] }
23
+ ],
24
+ "ready": "queued",
25
+ "dependency": {
26
+ "empty": ["none", "null", "n/a", "n.a.", "na", "—", "-", "–"],
27
+ "separators": ",;",
28
+ "notes": true,
29
+ "group": "^waves?\\s+(.+)$"
30
+ },
31
+ "wave": {
32
+ "label": ["wave"],
33
+ "members": ["slices"]
34
+ },
35
+ "section": { "depth": 3 }
36
+ }