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
@@ -0,0 +1,110 @@
1
+ # ADR-067 — Which harnesses read AGENTS.md is a column, and a row without a document gets none
2
+
3
+ Status: **accepted** · 2026-10-13 · filed as S42
4
+ **Affects:** `harness detect`, `src/projection/surfaces.mjs`, `src/commands/harness.mjs`, the
5
+ table tests in `test/surfaces.test.mjs`, `test/harness.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Review 0002 filed the finding as "harness coverage stops at five surfaces", and the table
10
+ stops at five: opencode, Cursor, Claude Code, Antigravity and Copilot. The same review then
11
+ listed a dozen more harnesses that read instruction files — Gemini CLI, Windsurf, Zed, Kiro,
12
+ Cline, Roo Code, Junie, Aider, Continue, Amp, Goose — and its own fix had two halves that pull
13
+ against each other.
14
+
15
+ The first half is mechanical: add a column recording whether each harness reads AGENTS.md,
16
+ and add rows only for the ones that do not. Most of the candidates need no row at all,
17
+ because most of them read AGENTS.md natively, and that fact is exactly what the column is for:
18
+ it turns "which harnesses do we support" from an open question into a lookup.
19
+
20
+ The second half is the one the review flagged on itself. It proposes a row per vendor and a
21
+ `verified` field citing that vendor's documentation — while stating in the same paragraph that
22
+ it did not re-verify each vendor's current documentation and that its list should be treated as
23
+ candidates. So the rows would carry a field whose entire purpose is to say "this was checked
24
+ against a source", filled in by a list that was not checked.
25
+
26
+ That combination is the failure this repository exists to prevent, in its exact form. The
27
+ repository's own ruling on manifest comments records the shape of it: a comment claiming each
28
+ value cites its decision record, where none did, is decoration that misdescribes the artefact.
29
+ A surface row is not decoration — it decides whether a reader's agent gets a rule at all. A
30
+ guessed row puts an unverified assumption into every repository that adopts the release, and
31
+ the reader who is wrong about it cannot tell, because the field that would tell them is the
32
+ one that was invented.
33
+
34
+ Detection was the other half and it has no such trap. "Which harnesses is this repository set
35
+ up for" is answerable from directories that exist, provided the command claims exactly that
36
+ much and no more.
37
+
38
+ ## Rulings
39
+
40
+ 1. **Whether a harness reads AGENTS.md is a vendor fact recorded per row, in three values.**
41
+ `all` means every reader the row names loads it; `some` means a subset does and the row
42
+ says which, in its own words; `none` means the row is the only channel those readers have.
43
+ Two values would be wrong in a way that matters: Copilot's CLI reads AGENTS.md and its
44
+ editor, code review and cloud agent do not, and a boolean there would have to be read as
45
+ "this repository is covered for Copilot", which is false for two of its three readers.
46
+ Every row whose value is not `all` carries the sentence that makes it true.
47
+
48
+ 2. **The column is not the existing `readsPrimary` flag, and the two must not be merged.**
49
+ That flag answers whether a per-rule file would repeat text the folded region already
50
+ carries, and it is only meaningful while folding is on. This column answers whether the
51
+ file is read at all, and it decides whether a harness needs a row. Merging them would make
52
+ a duplication question stand in for a coverage question, and the value would then change
53
+ answer a reader is relying on.
54
+
55
+ 3. **A row requires provenance, so no row ships from the candidate list.** Review 0002's
56
+ candidates are recorded as configuration directory names, not as surfaces. Adding a row
57
+ requires a vendor document, and a row whose `verified` cites nothing does not get added —
58
+ which means this ruling lands fewer surfaces than the review asked for, deliberately. The
59
+ cost of waiting is that a harness which does not read AGENTS.md is not served until
60
+ somebody reads its documentation; the cost of guessing is that every adopting repository
61
+ inherits the guess and the reader of the row is the only one who can catch it.
62
+
63
+ 4. **Detection reports presence and calls it presence.** A directory that exists implies a
64
+ harness is in use. It does not imply the harness reads AGENTS.md, is installed, or is
65
+ configured the way this tool would configure it, and the output never says it does. The
66
+ candidate list claims nothing about what those readers load for the same reason ruling 3
67
+ does: the directory name is evidence of a harness, and the fact nobody has verified is
68
+ what it loads.
69
+
70
+ 5. **A configuration directory with no channel is a note, never a problem.** It is the
71
+ expected shape of a repository whose team uses something this release does not serve, and
72
+ the remedy for it is not something the reader can be told to do. The command also exits 0
73
+ whatever it finds, including on a repository with no harness configuration at all: a
74
+ non-zero exit would be read as "this repository is set up wrong", which is the one thing a
75
+ directory scan can never establish.
76
+
77
+ 6. **The home directory is opt-in.** `--user` adds it. A command whose answer changes because
78
+ something was installed elsewhere is one a reader stops trusting, and the repository is
79
+ not the reader's machine.
80
+
81
+ ## Consequences
82
+
83
+ A harness that does not read AGENTS.md and has no row is now visible rather than invisible:
84
+ `harness detect` names its configuration directory as present with no channel, and says in the
85
+ same breath that presence is not a coverage claim. That is the honest answer available without
86
+ vendor documentation, and it is the one a reader can act on — by asking, or by adding a row
87
+ with a citation.
88
+
89
+ The candidate list will be wrong sometimes: a harness may name its directory differently, or
90
+ use a dotless one, or write its config elsewhere. Each of those produces a miss, and a miss is
91
+ silent. That is the shape of the problem as it stands; the alternative was a table of claims.
92
+
93
+ ## Rejected
94
+
95
+ - **Adding a row per review-0002 candidate with `verified` citing the review.** The field would
96
+ name a document that was never read. It is the exact defect the ruling on manifest comments
97
+ records, and it would be harder to spot later because each row would look like a verified
98
+ one.
99
+ - **A two-valued column.** Every row would have to pick the more flattering half, and the
100
+ flattering half for Copilot is the one two of its three readers contradict.
101
+ - **Merging the column into `readsPrimary`.** It would turn "does this harness read the file"
102
+ into "would this duplicate the region", and the answer a reader gets would depend on whether
103
+ they had folded the rules.
104
+ - **Reporting an unclaimed configuration directory as a finding.** It would put a red mark on a
105
+ repository for using a tool this project has not documented, which is a statement about the
106
+ project, not about the repository.
107
+ - **Scanning for configuration files rather than directories.** A file such as a workflow or
108
+ a settings blob is not evidence that a harness is in use, and the one directory deliberately
109
+ excluded here — the one that holds workflows and issues — would have been reported for every
110
+ repository with CI.
@@ -0,0 +1,111 @@
1
+ # ADR-068 — The revision list is a claim about revisions that were read
2
+
3
+ Status: **accepted** · 2026-10-13 · filed as S43
4
+ **Affects:** `mcp`, `src/mcp/server.mjs`, `src/mcp/tools.mjs`, `src/mcp/schema.mjs`, the
5
+ protocol tests in `test/mcp.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Review 0002 found the MCP surface pinned to one protocol revision and two behind the
10
+ published specification, and returning text only: no structured content, no output
11
+ schemas, and no hint that any of the ten tools were read-only. The finding is accurate and
12
+ so is its severity, which is P2 rather than P1 for a reason the row states itself — a
13
+ client that predates a field still reads the text block, so the surface works, it just
14
+ cannot be validated and cannot cache.
15
+
16
+ The tempting fix is to look up what the newest revision says and implement it. ADR-067
17
+ settled a neighbouring question one release ago: a row that cites a document nobody read
18
+ is decoration that misdescribes the artefact. The same trap waits here, and it is worse,
19
+ because a protocol is the one thing in this repository where being confidently wrong is
20
+ not a style defect — it is a client that believes a feature is on.
21
+
22
+ So the revisions this server speaks were read. Each of the three changelogs was fetched
23
+ and read, and what follows is what actually changed for a server on stdio — which is a
24
+ smaller list than the changelogs, because most of each revision's changes are about HTTP
25
+ transports, sessions and authorization that a stdio server never had.
26
+
27
+ ## Rulings
28
+
29
+ 1. **The revision list cites what changed for this transport.** Three revisions are
30
+ supported. The 2025-06-18 revision already had structured tool output, so the gap this
31
+ review found was never the missing feature — it was the missing declaration. The
32
+ 2025-11-25 revision changed nothing a stdio server depends on, and is supported because
33
+ a client on it is a real client. The 2026-07-28 revision removed the handshake, made
34
+ every request carry its own version in `_meta`, added a discovery method that servers
35
+ must implement, required a result-type field on every result, required a cache lifetime
36
+ on list results, removed the liveness method, and moved the resource-not-found code from
37
+ the implementation range to the invalid-params one. Each of those is implemented and
38
+ each has a test.
39
+
40
+ 2. **The version is read from the request, and remembered only where a client needs it.**
41
+ A request that names its version in `_meta` is answered at that version, with no
42
+ memory involved — that is the newest revision's shape, and it is stateless by design. A
43
+ client that negotiated through `initialize` sends bare requests forever after, so the
44
+ stream keeps one remembered revision for it. A client that never says anything gets the
45
+ newest. The remembered value exists for exactly one group of clients and is labelled as
46
+ the residue of a handshake the specification removed.
47
+
48
+ 3. **An unsupported version fails at the one place the client checks.** `initialize`
49
+ answers with the server's own revision, which is the older revision's guidance and the
50
+ only thing that works there. A per-request version we do not speak returns the
51
+ specification's own error for it, naming every revision we do. Silence there would be
52
+ the failure this whole row is about: a client proceeding against a shape we never
53
+ implemented.
54
+
55
+ 4. **Removed methods are refused by name, not dropped.** The liveness method was removed
56
+ in the newest revision, so a client on it gets a method error saying which revision
57
+ removed it, while a client on either older revision still gets its answer. Dropping the
58
+ method entirely would have broken the older clients; answering it to everyone would
59
+ have told the newest one we support something we do not.
60
+
61
+ 5. **Every result carries both blocks, and every tool declares both shapes.** The text
62
+ block is what a client that predates structured content reads; the structured block is
63
+ what a client validates against the declared output schema and what it can index. One
64
+ without the other leaves half the clients guessing. The same holds for a failure, which
65
+ stays a result rather than a protocol error, because a client that renders an exception
66
+ renders no answer at all. The declared shape per tool is what makes the pair checkable,
67
+ and the test calls every tool with the least a client would send and validates what
68
+ comes back.
69
+
70
+ 6. **The byte ceiling bounds the text block only.** A caller who sets a ceiling wants a
71
+ smaller answer, not a malformed one: the structured block stays whole, so it keeps
72
+ matching the schema, and the text block says how much was omitted and how to ask for
73
+ more. A tool that truncated the structured block would make the one representation a
74
+ client can validate fail validation.
75
+
76
+ 7. **The schema checker is in this repository.** The output schemas are here, so the thing
77
+ that reads them is here too: a dependency would be a second copy of rules that could
78
+ drift out of date without anyone noticing, and a tool that has to validate an answer
79
+ has to work on a machine where nothing was installed. The checker understands a small
80
+ set of keywords and throws on one it does not, because a keyword it ignored would turn
81
+ every schema using it into decoration — the check would pass, and the payload would not
82
+ match.
83
+
84
+ ## Consequences
85
+
86
+ A client on any of the three revisions gets an answer in the shape it asked for, and one on
87
+ the newest can cache the tool list for an hour instead of asking on every conversation. A
88
+ client that validates gets a real failure message when a payload grows a field nobody
89
+ wrote down, which is how the schemas here were corrected on the day they were written:
90
+ five of them described a payload that does not exist in that shape.
91
+
92
+ Nothing here adds a resource. Rules and decisions are readable through tools today, and
93
+ publishing them as resources is the next half of the review's fix rather than a detail of
94
+ this one.
95
+
96
+ ## Rejected
97
+
98
+ - **Implementing the newest revision only.** It would have broken every client that
99
+ negotiates, and the specification's own guidance for `initialize` is to answer with what
100
+ the server speaks.
101
+ - **Echoing whatever version a client asks for, as the old code did.** That was the
102
+ original defect: the echo was true for one revision and a guess for the rest.
103
+ - **Answering the removed liveness method to everyone.** A client on the newest revision
104
+ would read a success as support for a method the protocol deleted.
105
+ - **Truncating the structured block with the text.** The one representation a client can
106
+ check would fail the check.
107
+ - **Taking a JSON Schema validator from npm.** Zero runtime dependencies is a hard
108
+ requirement, and the schemas being validated live in this file, so the rules and the
109
+ reader of the rules belong together.
110
+ - **Declaring a resource capability with no resources behind it.** A capability is a
111
+ promise a client will wait on, so it is declared only when a method answers.
@@ -0,0 +1,90 @@
1
+ # ADR-069 — The conformance lint checks the published specification
2
+
3
+ Status: **accepted** · 2026-10-13 · filed as S44
4
+ **Affects:** `graph lint`, `skills install`, `src/graph/spec.mjs`, `src/graph/speccheck.mjs`,
5
+ `src/commands/graph.mjs`, `src/commands/skillsinstall.mjs`, `test/graphspec.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Review 0002 found that the Agent Skills specification sets out limits this package does not
10
+ check, and that `graph lint` — the command whose whole job is to hold skills to their
11
+ declarations — checks bytes and structure instead. Forty extra characters of description
12
+ are not a formatting problem: a client that validates a skill at load time will refuse it,
13
+ and the discoverer of that is whoever runs the client, not whoever wrote the file.
14
+
15
+ So the specification was fetched and read rather than recalled. Two things fell out of
16
+ reading it. The first is that the review's list was incomplete: it named the name, the
17
+ description and the compatibility limits, and missed four rules — a name may not start or
18
+ end with a hyphen, may not contain consecutive hyphens, and must equal the directory it
19
+ lives in, and the main file is recommended to stay under five hundred lines. The second is
20
+ that the specification is explicit about which of its rules are musts and which are
21
+ recommendations, which is a distinction a lint has to keep.
22
+
23
+ It also turns out most real descriptions use a folded block scalar, `description: >-` with
24
+ indented lines under it. A reader that treated the arrow as the value measured a
25
+ two-hundred-character description as two characters, and the first run of this lint produced
26
+ that wrong answer on five real skills before the reader was rewritten.
27
+
28
+ ## Rulings
29
+
30
+ 1. **The lint checks the specification it names, and the flag refuses any other name.**
31
+ `--spec agentskills` is the only value that works, and a different one is a usage error
32
+ rather than an unnamed default. A lint against an unnamed set of rules cannot be argued
33
+ with, and the argument is the whole point of publishing the rules.
34
+
35
+ 2. **A must is an error and a recommendation is a warning.** Name, description and
36
+ compatibility limits, a name that disagrees with its directory, and a file not named
37
+ SKILL.md fail the exit code. Body length and line count warn, because the specification
38
+ words them as recommendations for progressive disclosure, and a lint that fails on a
39
+ recommendation teaches a reader to skip the lint — which costs it every rule it does
40
+ enforce.
41
+
42
+ 3. **The name rules live in one place, and `skills install` asks them too.** The
43
+ specification requires a name to equal its directory, and an install creates the
44
+ directory from the name. So an install is not the place to allow a name the lint would
45
+ reject: it is the last moment anybody can notice before a skill is deployed that no
46
+ client can address by the name inside it. The command refuses with the rule broken.
47
+
48
+ 4. **The frontmatter reader reads block scalars.** A folded or literal block is the ordinary
49
+ spelling of a long description, and a reader that misses one reports a conforming skill
50
+ as broken. This is the one rule in this file that is about the reader rather than the
51
+ skill, and it is here because the first version of this lint was confidently wrong about
52
+ five files.
53
+
54
+ 5. **The conformance pass is additive to the registry pass, not gated by it.** The registry
55
+ answers "does the bookkeeping match the files"; the specification answers "would a
56
+ client accept these files". A reader with no registry can still lint a skill directory,
57
+ so the two passes run side by side, and a path the first cannot read is a warning from
58
+ the second and never a second error for the same thing — one defect, one exit path.
59
+
60
+ 6. **Progressively disclosed fields that are not musts are reported as unknowns, not
61
+ rejected.** An experimental field and a vendor's extra key appear as warnings naming the
62
+ field. The specification says clients may add metadata, and a lint that failed on it
63
+ would fail on a skill built exactly the way the specification invites.
64
+
65
+ ## Consequences
66
+
67
+ Thirty warnings and no errors across the skills this workspace declares, which is the
68
+ outcome the review predicted. The warnings are real: seven bodies over the recommended
69
+ size, one at six times it, and a set of vendor-specific frontmatter keys the specification
70
+ does not define. None of those is a defect in the skills — they are the cost a reader pays
71
+ per activation, which is what the recommendation exists to make visible.
72
+
73
+ The naming rules now hold at install time as well as at lint time, so a skill can no longer
74
+ be written to a directory its own name contradicts.
75
+
76
+ ## Rejected
77
+
78
+ - **Recalling the limits instead of reading them.** The review's summary was missing four
79
+ of them, and the ones it missed are the ones that break an install.
80
+ - **A byte-length check alone.** The specification counts characters and tokens, and the
81
+ body limit is a token recommendation with a line count beside it; a byte check answers a
82
+ different question.
83
+ - **Failing the exit code on the body recommendations.** They are recommendations, and a
84
+ gate that fails on advice is a gate that gets skipped.
85
+ - **A dependency on the upstream reference validator.** It would be a second copy of the
86
+ rules, maintained elsewhere, and this package ships no runtime dependencies.
87
+ - **Rejecting an unknown frontmatter field.** The specification invites extra metadata, and
88
+ a lint that failed on it would refuse skills built to specification.
89
+ - **Two copies of the name rules, one for the lint and one for the install.** The one
90
+ nobody linted would be the one that let an unaddressable skill through.
@@ -0,0 +1,91 @@
1
+ # ADR-070 — The JSON payload carries its own version
2
+
3
+ Status: **accepted** · 2026-10-13 · filed as S51
4
+ **Affects:** `src/output/index.mjs`, `src/commands/index.mjs`, `src/commands/mcp.mjs`,
5
+ `src/commands/graph.mjs`, `src/commands/rules.mjs`, `src/commands/skills.mjs`,
6
+ `src/commands/skillsinstall.mjs`, `src/commands/init.mjs`, `src/argv.mjs`,
7
+ `src/commands/help.mjs`, `test/jsoncontract.test.mjs`
8
+
9
+ ## Context
10
+
11
+ Review 0002 filed that `--json` output had no schema version and no compact form:
12
+ `emitJson` was `JSON.stringify(value, null, 2)` and every payload was four columns of
13
+ whitespace. Two consequences, both invisible until they mattered.
14
+
15
+ The first is that a reader could not detect a shape change. An agent that parses `--json`
16
+ gets a document that parses, and if a field is renamed the parse still succeeds and the
17
+ next lookup returns `undefined`. A version in a README does not help: nobody reads the
18
+ README, and it goes stale without failing anything. The version has to travel inside the
19
+ payload, because the payload is the only thing a reader is guaranteed to see.
20
+
21
+ The second is that indentation is paid for on every call. The reader of `--json` is
22
+ `JSON.parse`, not an eye; the same document is a third larger for nothing. The one case
23
+ where indentation is worth its bytes is a terminal, where a person is the reader.
24
+
25
+ Writing the contract test found the two things the review missed. `doctor` did not use
26
+ `emitJson` at all — it hand-rolled `emit(JSON.stringify(..., null, 2))` — and neither did
27
+ `mcp --list`, `graph lint`, `rules list`, `skills index`, `skills find`, `skills show` or
28
+ `skills install`, so seven surfaces answered `--json` in a shape no reader could rely on.
29
+ And `fail()` wrote only to stderr, which means a command that failed under `--json`
30
+ returned an empty stdout: the caller was told something went wrong and given no way to
31
+ learn what. A failure is a result — the question was asked and the answer is "no, and here
32
+ is why" — and it belongs where results go.
33
+
34
+ One note on how this was found, because it is the argument for the test existing at all:
35
+ the contract test itself passed a `mcp` entry whose argument list was silently dropped by
36
+ an `Array.isArray` mistake, so it ran `mcp --json` with no `--list`, and the command served
37
+ on stdio and waited forever. A contract that cannot fail loudly is not a contract.
38
+
39
+ ## Rulings
40
+
41
+ 1. **The version is in the payload, not in the documentation.**
42
+ `schemaVersion` is written into every object payload that does not already carry one.
43
+ Adding a field is not a bump; removing, renaming or retyping one is. A reader that
44
+ wants to know what changed reads the field, not a changelog.
45
+
46
+ 2. **Compact is the default and `--pretty` asks for indentation, with a terminal as the
47
+ exception.** A pipe gets one document on one line because its reader is a program; a TTY
48
+ gets indentation because its reader is a person. The flag exists to override the
49
+ terminal case, not the pipe.
50
+
51
+ 3. **One place decides the shape for every command.** `dispatch` sets the indent default
52
+ and the failure mode once, from the parsed flags. Threading either through twenty-five
53
+ call sites would mean twenty-five places to forget, and the one that forgot would be the
54
+ one a reader pipes into a script.
55
+
56
+ 4. **A failure under `--json` is a JSON document on stdout.** `ok: false`, the error, the
57
+ remedy and the exit code. Without this, a scripted caller has an empty document and a
58
+ non-zero exit and nothing else, which is the failure mode that makes an agent give up
59
+ rather than retry.
60
+
61
+ 5. **`emitJson` owns the byte ceiling too.** Every `emit(JSON.stringify(...))` call that
62
+ also passed `maxBytes` becomes an `emitJson(payload, { maxBytes })` call, so the
63
+ truncation announcement reads the same everywhere and no surface gets an unbounded
64
+ document by being spelled differently.
65
+
66
+ 6. **The contract test is generated from the command registry.** It iterates the commands
67
+ that emit JSON, runs each through the real binary, and asserts one line, no leading
68
+ whitespace, a parse, and the version. A command added to the registry is not
69
+ automatically in the test — the test asserts the list is still real, and the list is the
70
+ contract.
71
+
72
+ ## Consequences
73
+
74
+ Every `--json` payload now begins with `schemaVersion`, and twelve commands no longer have
75
+ a private idea of what `--json` means. `doctor --json` went from seventeen indented lines
76
+ to one, and the failure of `skills install` in a bare directory now answers in the shape
77
+ its caller asked for.
78
+
79
+ What is not here: the README half of the review's fix — an exit-code table and a per-command
80
+ JSON contract generated from the registry. The exit codes are already named in `EXIT`, and
81
+ generating README prose from them is a documentation change that belongs to whoever owns
82
+ the README, so it is noted in the tracker rather than done silently here.
83
+
84
+ ## Rejected
85
+
86
+ - **A version in a comment or the changelog only.** It is exactly the artefact that goes
87
+ stale without failing anything.
88
+ - **Threading `--pretty` through the call sites.** Twenty-five chances to forget, to fix a
89
+ problem that has one place to live.
90
+ - **Keeping failures on stderr under `--json`.** It leaves a scripted caller with an empty
91
+ document and a number, which is a contract that cannot be acted on.
@@ -0,0 +1,106 @@
1
+ # ADR-071 — A rule that can be narrowed is written narrow
2
+
3
+ Status: **accepted** · 2026-10-14 · filed as S50
4
+ **Affects:** `src/projection/rules.mjs`, `src/projection/targets.mjs`,
5
+ `src/projection/surfaces.mjs`, `src/projection/project.mjs`, `rules/stacks/*.md`,
6
+ `test/scoped.test.mjs`, `test/project.test.mjs`, `test/surfaces.test.mjs`
7
+
8
+ ## Context
9
+
10
+ Every per-rule file this tool ships was always-on, and so was every stack profile, because
11
+ profiles had nowhere else to live: they reached a reader through the generated region in
12
+ AGENTS.md and through nothing else. A repository on Cursor got the profile in both places,
13
+ and a repository on opencode got it in one place, always, whether or not a Python file was
14
+ ever open.
15
+
16
+ Two vendors document how to make that conditional, and both were read rather than recalled.
17
+ Cursor attaches a rule with `alwaysApply: false` and `globs` "when a matching file is in
18
+ context", and separates patterns with commas. Antigravity takes `trigger: glob` with a
19
+ comma-separated `globs`, requires patterns that start with `*` to be **quoted** so YAML does
20
+ not read them as alias anchors, and **discards a rule with invalid frontmatter without a
21
+ message**. It also documents a 24 KB per-file limit and a 20,000-token budget for always_on
22
+ rules, past which rules are demoted to pointers — so an eager profile is not merely
23
+ wasteful, it is a budget the reader spends on rules that did not apply.
24
+
25
+ One row of the surface table is the reason this needed a ruling rather than a patch.
26
+ Decision 3 — an always-on rule emits no scoping frontmatter — was settled on Claude Code's
27
+ `paths:` key silently failing to lazy-load for quoted values and YAML sequences
28
+ (anthropics/claude-code#13905, #17204, #19377), with the documented examples not valid YAML.
29
+ That decision stands. This record says what happens on either side of it, and does not
30
+ rewrite it.
31
+
32
+ ## Rulings
33
+
34
+ 1. **A profile leaves the region only when every selected surface can narrow it.**
35
+ `desiredRegion` asks whether all of them can; when one cannot, the profile stays in the
36
+ region and a note says which surface stopped it. The alternative — writing the profile
37
+ only as a scoped file for the three surfaces that can scope it — leaves a Claude Code
38
+ reader with no rule at all, and a rule nobody loads is worse than a rule that loads
39
+ eagerly.
40
+
41
+ 2. **Three surfaces can narrow, and the answer is per surface rather than per key.**
42
+ Cursor (`globs`), Antigravity (`trigger: glob` plus `globs`) and Copilot (`applyTo`)
43
+ can; Claude Code cannot, for the reason in decision 3; opencode cannot, because it has
44
+ no per-rule channel at all (`rule: null`). `canScope` names the answer, and the shipped
45
+ gate now probes the scoped frontmatter shape as well as the eager one, so a row that
46
+ emitted `globs` without documenting it would fail the gate rather than ship an inert
47
+ key.
48
+
49
+ 3. **Antigravity's patterns are quoted, and a scoped rule always carries a description.**
50
+ Unquoted `**/*.py` is an alias anchor and the whole rule is discarded silently. Cursor
51
+ documents no description plus no globs as manual `@`-mention only, so a scoped rule with
52
+ neither would never attach to anything — a rule that looks installed and is not.
53
+
54
+ 4. **A scope that matches everything is a defect, not a narrowing.** `**`, `**/*` and `*`
55
+ are what a surface applies when it is given no pattern, so a profile carrying one is
56
+ the eager profile wearing a costume. An empty list is the same thing spelled plainly.
57
+ Both are refused by `describeDefect`, where a profile's other frontmatter is checked.
58
+
59
+ 5. **A quoted pattern is read as the pattern.** `parseFrontmatter` did not strip quotes, so
60
+ a profile author quoting `"src/**/*.ts"` would have projected a glob matching nothing.
61
+ The reader now strips them, because the alternative is that the obvious spelling of a
62
+ glob silently does not work.
63
+
64
+ 6. **One construction, and the gate uses it.** `scopedTargets` delegates to `ruleTargets`
65
+ with the profile marked `alwaysApply: false`, and `checkTargets` calls the same function
66
+ `plan` does. A scoped file that was planned and written but never gated would show a
67
+ clean `check` over a stale reader — the asymmetry this module exists to prevent.
68
+
69
+ 7. **The region keeps a pointer section.** A profile that moved is named in AGENTS.md with
70
+ its patterns and the files it moved to. A reader who knows the profile was there and
71
+ cannot find it will otherwise conclude it was deleted, and the region's source list is
72
+ the provenance a review reads.
73
+
74
+ 8. **The shipped profiles carry the scope that selected them.** Eighteen of nineteen: node,
75
+ typescript, react-web, react-native, python, uv, rust, go, ruby, jvm, swift, dotnet,
76
+ dart, deno, elixir, android, cloudflare-workers, container. Each is anchored on the file
77
+ whose presence selected it, so a reader loads the profile exactly when the thing that
78
+ made it apply is in play. `connectiq` has no scope: it is selected by a tag no file
79
+ announces, and inventing an anchor for it would be a claim about a file this tool has
80
+ never seen.
81
+
82
+ ## Consequences
83
+
84
+ A repository on Cursor, Copilot or Antigravity now receives its stack profiles as
85
+ `.cursor/rules/python.mdc`, `.agents/rules/python.md` and
86
+ `.github/instructions/python.instructions.md`, each carrying the patterns and no
87
+ `alwaysApply: true`, with a pointer in the region replacing the profile text. A repository
88
+ whose surfaces include opencode or Claude Code sees no change at all, and says why in a
89
+ note when it asked for narrowing it could not have.
90
+
91
+ A monorepo gets the same treatment per package, because package regions call the same
92
+ `desiredRegion` (ADR-066).
93
+
94
+ ## Rejected
95
+
96
+ - **Emitting `paths:` for Claude Code anyway.** Its parser defect is documented with issue
97
+ numbers and the failure is silent: the reader loads nothing and believes the rule is
98
+ installed.
99
+ - **Narrowing on whichever surfaces can, and leaving the rest to find the profile
100
+ elsewhere.** It trades an eagerly loaded rule for a rule that is never loaded, which is
101
+ the same defect with a worse diagnosis.
102
+ - **A catch-all default scope for a profile with none.** It would look like narrowing in a
103
+ diff and behave exactly like not narrowing.
104
+ - **Reusing the mdc rule-file path for a profile file.** The name comes from `ruleName`,
105
+ which reads `.mdc` targets; a profile has none, and the first version of this wrote
106
+ `null.mdc` and then `python.md.mdc` before either was noticed.
@@ -0,0 +1,104 @@
1
+ # ADR-072 — The budget is a declaration beside the gates, not a number inside the projection
2
+
3
+ Status: **accepted** · 2026-10-14 · filed as S38
4
+ **Affects:** `src/projection/manifest.mjs`, `src/measure/context.mjs`,
5
+ `src/projection/project.mjs`, `src/commands/sync.mjs`, `simt tokens`, `simt-dev check`,
6
+ `test/tokens.test.mjs`, `test/surfaces.test.mjs`
7
+
8
+ ## Context
9
+
10
+ Review 0002 row S38 asks for a per-repository always-on budget: "for these 7 roots,
11
+ generated always-on bytes are at most 30 KB". Plan 0003 spells the mechanism as R08, and
12
+ the plan was read rather than recalled, which is how the two facts below were found.
13
+
14
+ The first is that R08 as written presupposes R07. Its compiler "packs kernel lines by
15
+ priority class" and `check` reports "each cut line" — and there are no kernel lines
16
+ without R07's `projection.mode` and the per-line kernel frontmatter it introduces, whose own default stays
17
+ `full` until R02 proves a narrow mode is not worse. With no kernel there is nothing to cut,
18
+ so the packing half of R08 cannot be built honestly before the R01–R05 evaluation has been
19
+ run. Building it anyway would mean inventing a kernel nobody measured.
20
+
21
+ The second is where the number lives. R08 says `projection.budget.bytes`. In this tool
22
+ `projection` is a generated key: `GENERATED_KEYS` names it, and `withGenerated()` replaces
23
+ it with the tool's own `{ simtDevSha, version }` on every write. A budget written there
24
+ would be deleted by the next `upgrade`, and — because `manifestDrift` compares only the
25
+ generated keys — not reported as deleted. The repo would watch its budget disappear on a
26
+ green gate. That is the same defect this package treats as unrecoverable everywhere else:
27
+ content the repository chose must be preserved by the repair, not overwritten by it.
28
+
29
+ So the part of R08 that is independent of the kernel ships now, and the part that is not
30
+ does not.
31
+
32
+ ## Rulings
33
+
34
+ 1. **The budget is a repository declaration at the top level of the manifest, beside
35
+ `limits` and `gates`.** It is content the repo chose, so it survives `upgrade`, and the
36
+ plan's `projection.budget.bytes` spelling is superseded rather than edited in place. The
37
+ default is not a field either: it is the value whoever measures applies when the key is
38
+ absent, so `simt tokens` and the gate read one number from one place.
39
+
40
+ 2. **A repository that declares nothing is not reported as having declared the default.**
41
+ `buildManifest` omits the key entirely when the repo has none, which keeps
42
+ `budget.declared` an honest fact. Writing the default in would make every adopted repo
43
+ look like it had chosen the review's line — a reader told "declared: true" about a
44
+ number nobody wrote is being lied to about the only thing the number is for.
45
+
46
+ 3. **The default is 30,000 bytes and it is the review's figure, not this tool's
47
+ measurement.** It is written as 30,000 rather than 30 × 1024 because it is a ceiling and
48
+ the conservative reading of "at most 30 KB" is the one that trips first.
49
+
50
+ 4. **Over budget is reported, never enforced.** The finding names the bytes measured, the
51
+ bytes declared and what was not done: nothing was cut, because the projection is still
52
+ in full mode. A reader who sees an over-budget line must not conclude the projection
53
+ shrank, and a gate that failed here would fail on a measurement this tool has not
54
+ validated against a study.
55
+
56
+ 5. **`simt tokens` carries the finding it would raise, produced by the same function.**
57
+ One function words the fact, so the report and the gate cannot describe the same
58
+ overflow two ways.
59
+
60
+ 6. **The rows themselves are unchanged.** Which rules move behind skills is R01–R05's
61
+ finding, not this record's, and a budget that silently dropped rules would be enforcing
62
+ a decision nobody has made.
63
+
64
+ 7. **The meter is the definition of the number, and the gate quotes it.**
65
+ `simt-dev check` calls the same `measureRoot` the meter does rather than summing the
66
+ bytes of the targets it happens to compare. That was the other way round first, and it
67
+ was wrong: the meter also counts managed skills and `.mdc` files, so one repository had
68
+ two budgets and two different numbers for the same declaration. Two definitions of one
69
+ ceiling is the same defect as two projections of one fact, one layer up.
70
+
71
+ 8. **The gate raises the finding and does not block on it.** `budget` is in both
72
+ non-blocking sets, `check` and `sync` alike. Nothing was cut, so the projection is
73
+ exactly what the repository agreed to project; failing the run over a ceiling it wrote
74
+ down itself would train people to raise the ceiling. R08 says non-blocking for the same
75
+ reason, and this record agrees with it now that the finding has a shape to be.
76
+
77
+ 9. **The cost of ruling 7 is accepted rather than avoided.** `check` now reads what the
78
+ meter reads — the projected region, the rule files, the skills — on every run, where it
79
+ previously compared bytes it had already read. A cheaper gate would have to keep its own
80
+ notion of what counts, and this is the one place where the two are not allowed to differ.
81
+
82
+ ## Consequences
83
+
84
+ S38's accept criterion is now checkable rather than asserted: a repository can declare the
85
+ line it intends to carry, and `simt tokens` reports it against that line and says how far
86
+ over it is. Three tests hold the two halves that were easy to get backwards — the budget
87
+ survives an upgrade, a repository that declared nothing keeps `declared: false`, and the
88
+ finding says that nothing was cut.
89
+
90
+ What has not landed: the gate reporting the budget as a finding, and R08's packing. The
91
+ first needs the number the gate is already comparing — the bytes of the region and the rule
92
+ files it checks — so that `check` and `tokens` cannot report different totals for one
93
+ repository; that is a wiring decision, not a measurement. The second needs R07.
94
+
95
+ ## Rejected
96
+
97
+ - **The plan's `projection.budget.bytes`.** The generated block is replaced wholesale on
98
+ every upgrade, and nothing in the repair would notice.
99
+ - **A default written into every manifest.** It would make every adopted repo look like it
100
+ had chosen a number it never chose.
101
+ - **Cutting lines to fit the budget now.** There is no kernel to pack and no measurement
102
+ saying which lines a reader can lose.
103
+ - **Failing the gate on an over-budget projection.** The number is this tool's own
104
+ estimate, and a gate that fails on its own estimate teaches a reader to ignore it.