simt-dev 0.2.0 → 0.2.2

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 (90) hide show
  1. package/CHANGELOG.md +375 -7
  2. package/README.md +4 -4
  3. package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
  4. package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
  5. package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
  6. package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
  7. package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
  8. package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
  9. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  10. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  11. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  12. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  13. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  14. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  15. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  16. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  17. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  18. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  19. package/docs/decisions/README.md +17 -0
  20. package/package.json +1 -2
  21. package/rules/core.md +1 -0
  22. package/rules/stacks/android.md +2 -0
  23. package/rules/stacks/cloudflare-workers.md +2 -0
  24. package/rules/stacks/connectiq.md +1 -0
  25. package/rules/stacks/container.md +2 -0
  26. package/rules/stacks/dart.md +40 -0
  27. package/rules/stacks/deno.md +38 -0
  28. package/rules/stacks/dotnet.md +37 -0
  29. package/rules/stacks/elixir.md +41 -0
  30. package/rules/stacks/go.md +36 -0
  31. package/rules/stacks/jvm.md +2 -0
  32. package/rules/stacks/node.md +2 -0
  33. package/rules/stacks/python.md +2 -0
  34. package/rules/stacks/react-native.md +2 -0
  35. package/rules/stacks/react-web.md +2 -0
  36. package/rules/stacks/ruby.md +38 -0
  37. package/rules/stacks/rust.md +36 -0
  38. package/rules/stacks/swift.md +39 -0
  39. package/rules/stacks/typescript.md +2 -0
  40. package/rules/stacks/uv.md +2 -0
  41. package/src/argv.mjs +54 -6
  42. package/src/ci/dependabot.mjs +1 -1
  43. package/src/ci/workflow.mjs +33 -11
  44. package/src/commands/compare.mjs +15 -0
  45. package/src/commands/graph.mjs +58 -25
  46. package/src/commands/harness.mjs +150 -0
  47. package/src/commands/help.mjs +37 -5
  48. package/src/commands/hooks.mjs +173 -57
  49. package/src/commands/index.mjs +95 -69
  50. package/src/commands/init.mjs +100 -20
  51. package/src/commands/mcp.mjs +20 -5
  52. package/src/commands/rules.mjs +96 -60
  53. package/src/commands/skills.mjs +189 -86
  54. package/src/commands/skillsinstall.mjs +20 -5
  55. package/src/commands/sync.mjs +213 -15
  56. package/src/commands/tokens.mjs +138 -0
  57. package/src/compare/hooksstate.mjs +96 -9
  58. package/src/compare/row.mjs +107 -7
  59. package/src/decisions/index.mjs +21 -6
  60. package/src/graph/load.mjs +125 -14
  61. package/src/graph/spec.mjs +227 -0
  62. package/src/graph/speccheck.mjs +68 -0
  63. package/src/hooks/capability.mjs +48 -4
  64. package/src/hooks/dispatcher.mjs +84 -7
  65. package/src/hooks/gates.mjs +0 -2
  66. package/src/hooks/resolve.mjs +24 -25
  67. package/src/hooks/shim.mjs +29 -5
  68. package/src/identity.mjs +0 -11
  69. package/src/mcp/schema.mjs +139 -0
  70. package/src/mcp/server.mjs +197 -20
  71. package/src/mcp/tools.mjs +290 -3
  72. package/src/measure/context.mjs +371 -0
  73. package/src/monorepo.mjs +339 -0
  74. package/src/output/index.mjs +86 -9
  75. package/src/package-version.mjs +74 -0
  76. package/src/packs.mjs +182 -0
  77. package/src/projection/conflicts.mjs +56 -5
  78. package/src/projection/manifest.mjs +335 -25
  79. package/src/projection/mdc.mjs +4 -13
  80. package/src/projection/project.mjs +139 -8
  81. package/src/projection/rules.mjs +308 -27
  82. package/src/projection/surfaces.mjs +133 -15
  83. package/src/projection/targets.mjs +90 -7
  84. package/src/resolve/eval.mjs +64 -0
  85. package/src/resolve/fingerprint.mjs +1 -0
  86. package/src/resolve/near.mjs +26 -7
  87. package/src/resolve/resolve.mjs +1 -1
  88. package/src/resolve/source.mjs +1 -1
  89. package/src/workspace.mjs +0 -9
  90. package/graph/skills.graph.json +0 -1622
@@ -0,0 +1,115 @@
1
+ # ADR-066 — A monorepo projects once per package
2
+
3
+ Status: **accepted** · 2026-10-13 · filed as S41
4
+ **Affects:** `init`, `sync`, `check`, `compare`, `hooks run`, `src/monorepo.mjs`,
5
+ `src/projection/rules.mjs`, `src/projection/project.mjs`, `src/hooks/dispatcher.mjs`,
6
+ the `gates.cwd` manifest field
7
+
8
+ ## Context
9
+
10
+ A monorepo received exactly one projection: the root's `AGENTS.md`, fingerprinted from
11
+ the root. Everything below the root was invisible to the detector, so a React
12
+ application inside a node workspace was handed the node profile and nothing else, while
13
+ a Workers service beside it was handed the same. Neither the repository's rules nor the
14
+ tool's output said another package existed with different rules.
15
+
16
+ The estate's own monorepos made this expensive rather than merely wrong. Fourteen
17
+ repositories hold between them hundreds of packages, and one rule set per repository
18
+ means every package is told the union of what the root can see and nothing about what it
19
+ is. The profile that should have applied to a package is applied by nobody, so the rule
20
+ that package needed was simply missing — not wrong, missing.
21
+
22
+ Three shapes of answer were available, and the cheapest is the one that was not taken.
23
+ Writing every package's full profile set into the root duplicates text the reader loads
24
+ once per turn (review 0002 S38 measured that duplication at 72% of the workspace's
25
+ always-on bytes). Writing the root's set into each package does the same and worse.
26
+ Projecting the root once and each package once, with each package carrying only what the
27
+ root does not, adds bytes proportional to the number of packages that actually differ,
28
+ and nothing at all to the ones that do not.
29
+
30
+ The harder half is that `check` is the gate. A projection the tool writes but does not
31
+ check is a projection nobody is held to, and a monorepo that drifts in one package would
32
+ report `current` while CI is green and the reader is wrong.
33
+
34
+ ## Rulings
35
+
36
+ 1. **A workspace is read from its declarations, never from a directory scan.** npm
37
+ workspaces, pnpm's package list, turbo's workspace list, Cargo members, a go.work use
38
+ list and Gradle includes are all read, because each one is a file that already states
39
+ which directories are members and why. Finding a marker file by walking the tree is
40
+ the alternative, and it is the one this repository's rules forbid: a heuristic that
41
+ cannot be explained by a declaration is a heuristic a reader has to reverse-engineer
42
+ from its output, and the only way to find out why a directory was or was not given
43
+ rules is to read this tool's source. A declaration this tool cannot read is a
44
+ directory it does not discover, which is an honest absence rather than a guess.
45
+
46
+ 2. **The root keeps the shared rules; a package carries its delta.** The root is
47
+ fingerprinted as before and renders the core plus every profile the root's own signals
48
+ select. A package is fingerprinted the same way, and renders only the profiles the
49
+ root did not already select. A reader opening the package file is therefore never
50
+ shown a rule the root already gave them, and a reader of the root file is never shown
51
+ a rule that belongs to one package. The duplication the workspace reader pays for is
52
+ one sentence of orientation per package, and the bytes it pays for scale with the
53
+ packages that differ rather than with the repository count.
54
+
55
+ 3. **An empty delta writes nothing.** Two node packages under one workspace select the
56
+ same profiles, so the delta is empty and there is no file. A file whose entire content
57
+ is a claim about rules it does not contain is always-on bytes spent on nothing, which
58
+ is the cost S38 exists to measure, and the reader is left looking for rules that are
59
+ not there.
60
+
61
+ 4. **A gate names its directory in a map, not in a new shape for the command.** The
62
+ manifest's gate commands stay name-to-command-line strings, and an added map gives
63
+ each gate a directory to run in. Re-reading a command string to discover where it runs
64
+ would mean the answer to "where does anything run" lives inside a value whose purpose
65
+ is something else, and a manifest written by an earlier release would have to be
66
+ rewritten to gain the capability. A directory that does not exist, or that resolves
67
+ outside the repository, fails the event with a message naming which of the two things
68
+ is wrong, because they are repaired by different edits. Containment is checked on the
69
+ resolved path rather than on the text: a relative traversal and a symlink both pass a
70
+ prefix test applied to a string.
71
+
72
+ 5. **`check` reports a bad directory before a commit, not at one.** The dispatcher fails
73
+ closed on the same condition, so a reader would otherwise find out by being unable to
74
+ commit — after writing a commit, which is the wrong order for the news. The same
75
+ applies to a directory entry naming a gate that is not declared at all: the entry is
76
+ inert, the reader believes the gate runs where it says, and nothing anywhere says
77
+ otherwise.
78
+
79
+ 6. **`compare` counts findings, not repositories.** One monorepo with three drifted
80
+ packages is one repository with three findings. A summary that said one problem would
81
+ understate the work by a factor of three, and a reader comparing two runs would see a
82
+ single count that moves by three when the underlying state changed by three. The
83
+ repository still counts once among repositories with problems, because that count is
84
+ about how much work a person has.
85
+
86
+ ## Consequences
87
+
88
+ - A workspace whose members are declared in a form this tool does not read gets the root
89
+ projection only, exactly as it did before. That is a gap to close by adding a row to
90
+ the declaration table, not a behaviour to document.
91
+ - Nested regions are checked, and their findings are printed with their paths. A reader
92
+ who edits a package file by hand is told which file, which is the same information the
93
+ root's own drift finding carries.
94
+ - The package file is a generated region like any other: the markers, the source list and
95
+ the repair command all behave identically, so there is no second kind of generated file
96
+ for a reader to learn.
97
+
98
+ ## Rejected
99
+
100
+ - **Scanning for marker files instead of reading declarations.** Rejected for ruling 1.
101
+ A marker file answers "is there a build here", which is not the question; a declaration
102
+ answers "is this a member of the workspace", which is.
103
+ - **Folding every package's rules into the root region.** Rejected for ruling 2. It is
104
+ the smallest change to make and it makes every reader in the workspace carry every
105
+ package's rules, which is the cost S38 measured and S38's kernel exists to remove.
106
+ - **A directory field on each declared command.** Rejected for ruling 4, for the reason
107
+ recorded there: it puts where a gate runs inside the value that says what it runs, and
108
+ it makes an old manifest unreadable rather than merely older.
109
+ - **Skipping the directory check because the dispatcher already fails closed.**
110
+ Rejected for ruling 5. The dispatcher's refusal is correct but late; the reader who
111
+ learns about it from a failed commit has already done the work the commit would have
112
+ saved them.
113
+ - **Refusing to project a monorepo at all until every package is understood.** Rejected:
114
+ a root-only projection is a strictly smaller claim than it was, and refusing to improve
115
+ it because the improvement is incomplete makes the reader worse off than the tool.
@@ -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.