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.
- package/CHANGELOG.md +375 -7
- package/README.md +4 -4
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/README.md +17 -0
- package/package.json +1 -2
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +40 -0
- package/rules/stacks/deno.md +38 -0
- package/rules/stacks/dotnet.md +37 -0
- package/rules/stacks/elixir.md +41 -0
- package/rules/stacks/go.md +36 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +38 -0
- package/rules/stacks/rust.md +36 -0
- package/rules/stacks/swift.md +39 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/argv.mjs +54 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +58 -25
- package/src/commands/harness.mjs +150 -0
- package/src/commands/help.mjs +37 -5
- package/src/commands/hooks.mjs +173 -57
- package/src/commands/index.mjs +95 -69
- package/src/commands/init.mjs +100 -20
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/rules.mjs +96 -60
- package/src/commands/skills.mjs +189 -86
- package/src/commands/skillsinstall.mjs +20 -5
- package/src/commands/sync.mjs +213 -15
- package/src/commands/tokens.mjs +138 -0
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +107 -7
- package/src/decisions/index.mjs +21 -6
- package/src/graph/load.mjs +125 -14
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/capability.mjs +48 -4
- package/src/hooks/dispatcher.mjs +84 -7
- package/src/hooks/gates.mjs +0 -2
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/identity.mjs +0 -11
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +197 -20
- package/src/mcp/tools.mjs +290 -3
- package/src/measure/context.mjs +371 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +86 -9
- package/src/package-version.mjs +74 -0
- package/src/packs.mjs +182 -0
- package/src/projection/conflicts.mjs +56 -5
- package/src/projection/manifest.mjs +335 -25
- package/src/projection/mdc.mjs +4 -13
- package/src/projection/project.mjs +139 -8
- package/src/projection/rules.mjs +308 -27
- package/src/projection/surfaces.mjs +133 -15
- package/src/projection/targets.mjs +90 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +1 -0
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +1 -1
- package/src/workspace.mjs +0 -9
- 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.
|