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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,372 @@ All notable changes to this package are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.2] — 2026-10-14
|
|
10
|
+
|
|
11
|
+
### A repository can ask for a smaller projection
|
|
12
|
+
|
|
13
|
+
`simt tokens` measured the cost and `check` now holds the line the repository draws: `budget.bytes`
|
|
14
|
+
in `simt.json` is how many always-on bytes that repository has agreed to carry, and both the meter and
|
|
15
|
+
the gate report the overflow with the two numbers in them. It sits beside `gates` rather than inside
|
|
16
|
+
`projection`, because that key belongs to this tool and an upgrade replaces it.
|
|
17
|
+
|
|
18
|
+
`mode: kernel` is the other half. It keeps the rules a reader cannot do without — the ones a hard
|
|
19
|
+
constraint or a command depends on — and defers the rest behind a `## Deferred rules` section that
|
|
20
|
+
names each one and the command that shows it. A Python fixture projects 16% smaller. `full` is still
|
|
21
|
+
the default, because nobody has measured yet whether the smaller projection loses compliance, and a
|
|
22
|
+
tool that made that trade silently would be deciding it on the reader's behalf (ADR-073).
|
|
23
|
+
|
|
24
|
+
### A repository can declare how many always-on bytes it is willing to carry
|
|
25
|
+
|
|
26
|
+
`simt tokens` has reported that number since the meter landed, against a constant nobody
|
|
27
|
+
could argue with. A repository can now say what it will carry — `budget: { bytes: N }`
|
|
28
|
+
beside `limits` and `gates` in the manifest — and both `simt tokens` and `simt-dev check`
|
|
29
|
+
measure against that line. One declaration, one meter, one function that words the finding.
|
|
30
|
+
|
|
31
|
+
The declaration is deliberately *not* inside `projection`, which is the one block this tool
|
|
32
|
+
owns and `upgrade` is allowed to replace: a number written there would be deleted silently
|
|
33
|
+
on the next upgrade and never reported as drift. And a repository that declares nothing is
|
|
34
|
+
reported as declaring nothing, rather than being quietly handed the default as though it
|
|
35
|
+
had chosen it.
|
|
36
|
+
|
|
37
|
+
Over budget is reported and never fails the gate. Nothing was cut — the projection is
|
|
38
|
+
still full mode, every rule loaded as written — so the honest thing to say is "you are
|
|
39
|
+
over your line", not to fail a repository over a ceiling it wrote down itself. Which rules
|
|
40
|
+
move behind skills stays with the evaluation that has to decide it (ADR-072).
|
|
41
|
+
|
|
42
|
+
### A repository can declare the always-on bytes it intends to carry
|
|
43
|
+
|
|
44
|
+
`simt tokens` measured the number; it could not be held to one. A manifest now carries
|
|
45
|
+
`budget.bytes` — a declaration beside `limits` and `gates`, not inside `projection`,
|
|
46
|
+
because that block is regenerated on every upgrade and nothing in the repair would notice
|
|
47
|
+
its budget disappearing. A repository that declares nothing is measured against the
|
|
48
|
+
review's 30 KB and reports `declared: false` rather than looking like it chose it.
|
|
49
|
+
|
|
50
|
+
Over budget is reported, never enforced, and the report says what was not done: nothing
|
|
51
|
+
was cut, because the projection is still in full mode. Which rules move behind skills is a
|
|
52
|
+
measurement that has not been taken yet, and a budget that quietly dropped rules would be
|
|
53
|
+
enforcing a decision nobody made (ADR-072).
|
|
54
|
+
|
|
55
|
+
### A stack profile loads only when a matching file is in play
|
|
56
|
+
|
|
57
|
+
A stack profile used to be always-on everywhere, because profiles reached a reader through
|
|
58
|
+
the generated region in AGENTS.md and through nothing else. A profile can now declare
|
|
59
|
+
`simt-scope`, and a repository on Cursor, Copilot or Antigravity gets it as a file the
|
|
60
|
+
surface attaches on patterns: `.cursor/rules/python.mdc` with `globs` and
|
|
61
|
+
`alwaysApply: false`, an Antigravity rule with `trigger: glob`, a Copilot
|
|
62
|
+
`.instructions.md` with `applyTo`. AGENTS.md keeps one line naming the profile, the
|
|
63
|
+
patterns and the file it moved to — a profile that vanishes from the region reads as one
|
|
64
|
+
that was deleted.
|
|
65
|
+
|
|
66
|
+
The narrowing happens only when every selected surface can do it. Claude Code's `paths:`
|
|
67
|
+
key is documented and silently fails to lazy-load for quoted values, and opencode has no
|
|
68
|
+
per-rule channel at all, so a repository carrying either keeps its profiles in the region
|
|
69
|
+
and is told which surface stopped it. A rule that never loads is worse than a rule that
|
|
70
|
+
loads eagerly.
|
|
71
|
+
|
|
72
|
+
Reading the two vendors' docs changed the shape of this: Antigravity requires patterns
|
|
73
|
+
starting with `*` to be quoted or YAML reads them as an alias anchor and the rule is
|
|
74
|
+
discarded with no message, and it documents a 20,000-token budget for always_on rules past
|
|
75
|
+
which they are demoted to pointers — so eager profiles cost the reader a real budget. A
|
|
76
|
+
`simt-scope` that matches everything is now refused, since that is the same behaviour with
|
|
77
|
+
a narrowing-looking spelling (ADR-071).
|
|
78
|
+
|
|
79
|
+
### Every `--json` payload says which version it is
|
|
80
|
+
|
|
81
|
+
An agent that parses `--json` had no way to notice that a field had moved: the parse
|
|
82
|
+
succeeds, the lookup returns `undefined`, and the answer is quietly wrong. Every payload now
|
|
83
|
+
opens with `schemaVersion`, and a version in a comment is not a version, so it travels in
|
|
84
|
+
the document every reader is guaranteed to see.
|
|
85
|
+
|
|
86
|
+
The same document is now one line rather than four columns of whitespace, because the reader
|
|
87
|
+
of `--json` is a program and it pays for every byte. `--pretty` asks for the other form, and
|
|
88
|
+
a terminal gets it without being asked, because there the reader is a person.
|
|
89
|
+
|
|
90
|
+
Writing the contract test found two things the review that asked for this missed. Eight
|
|
91
|
+
commands — `doctor`, `mcp --list`, `graph lint`, `rules list`, and the `skills` ones — had a
|
|
92
|
+
private `emit(JSON.stringify(..., null, 2))` instead of the shared helper, so `--json` meant
|
|
93
|
+
something different depending on which command you asked. And a command that *failed* under
|
|
94
|
+
`--json` wrote only to stderr, so a scripted caller got an empty document and a non-zero
|
|
95
|
+
exit and no way to learn what went wrong. It now answers in the shape it was asked in (ADR-070).
|
|
96
|
+
|
|
97
|
+
### `graph lint` now checks skills against the published specification
|
|
98
|
+
|
|
99
|
+
The Agent Skills specification sets limits nobody here was checking: a name of lowercase
|
|
100
|
+
letters, digits and single hyphens that equals the directory it lives in; a description
|
|
101
|
+
under 1,024 characters; a compatibility note under 500; a body recommended under 5,000
|
|
102
|
+
tokens because an agent loads all of it once the skill activates. `graph lint --spec
|
|
103
|
+
agentskills` checks every skill against those, and the specification was fetched rather
|
|
104
|
+
than recalled — reading it turned up four rules the review that asked for this did not
|
|
105
|
+
list, including the one that breaks installs.
|
|
106
|
+
|
|
107
|
+
What the specification calls a must fails the exit code; what it calls a recommendation
|
|
108
|
+
warns. A lint that fails on advice is a lint that gets skipped. `skills install` now asks
|
|
109
|
+
the same name rules, since the directory is created from the name and an unaddressable
|
|
110
|
+
skill would be the last thing anyone notices.
|
|
111
|
+
|
|
112
|
+
### The MCP surface speaks three protocol revisions, and answers in the shape each asks for
|
|
113
|
+
|
|
114
|
+
The server was pinned to one revision and returned pretty JSON in a text block, so a
|
|
115
|
+
client could read the answer but could not validate it, could not cache it, and could not
|
|
116
|
+
tell a read-only tool from one that writes. All three published revisions were read rather
|
|
117
|
+
than guessed at, and each is answered in its own shape: the newest drops the handshake for
|
|
118
|
+
a version carried on every request, adds a discovery method servers must implement,
|
|
119
|
+
requires a result-type field and a cache lifetime on list results, and deleted the liveness
|
|
120
|
+
method — which older clients still get, and newest clients are told is gone by name.
|
|
121
|
+
|
|
122
|
+
Every tool now declares what it returns and says it is a reader, and every call returns
|
|
123
|
+
the answer in both shapes: the text block for a client that predates structured content,
|
|
124
|
+
the structured block for one that validates against the declared schema. The byte ceiling
|
|
125
|
+
bounds the text only, so the block a client can check still checks. The schema checker
|
|
126
|
+
that makes this testable lives in the package rather than in a dependency.
|
|
127
|
+
|
|
128
|
+
### Which harnesses a repository is set up for is now answerable
|
|
129
|
+
|
|
130
|
+
`simt-dev harness detect` looks at the configuration directories that exist and says
|
|
131
|
+
which harnesses this repository — and, with `--user`, its reader — is set up for. It
|
|
132
|
+
claims presence and nothing else: a directory found is not a claim about what its owner
|
|
133
|
+
reads, because the tool cannot answer that without a vendor document, and a guess here
|
|
134
|
+
would be the reader's problem the first time it was wrong. A harness that reads
|
|
135
|
+
AGENTS.md is served by the standard surface whatever else it reads, and every surface row
|
|
136
|
+
now records that in three values rather than two, because "reads AGENTS.md" and "is
|
|
137
|
+
covered for this harness" are not the same sentence — Copilot's CLI reads it and its
|
|
138
|
+
editor, code review and cloud agent do not.
|
|
139
|
+
|
|
140
|
+
No new harness rows ship with it. The candidates are recorded as directory names, which
|
|
141
|
+
claim presence and not coverage; a row still requires the vendor document it cites, and
|
|
142
|
+
one that reads nothing has to be added on purpose.
|
|
143
|
+
|
|
144
|
+
### A monorepo gets rules for each of its packages
|
|
145
|
+
|
|
146
|
+
One repository, one projection: every package under the root was given the root's rules,
|
|
147
|
+
so a React application inside a node workspace was handed the node profile and nothing
|
|
148
|
+
else, and the profile that should have applied to it was applied by nobody. The rule that
|
|
149
|
+
package needed was not wrong — it was missing.
|
|
150
|
+
|
|
151
|
+
Packages are now discovered from the declarations that already list them (npm and pnpm
|
|
152
|
+
workspaces, turbo, Cargo members, `go.work`, Gradle includes) rather than by walking the
|
|
153
|
+
tree, because a declaration states which directories are members and a marker file only
|
|
154
|
+
says there is a build here. The root keeps what every package shares; each package that
|
|
155
|
+
selects something new gets a generated region carrying exactly its own delta, and a
|
|
156
|
+
package that selects nothing new gets no file at all. `check` reports every package
|
|
157
|
+
region and fails on any one of them, and `compare` counts the drifted regions rather than
|
|
158
|
+
the repositories that hold them.
|
|
159
|
+
|
|
160
|
+
A gate can also name the directory it runs in, which is what a monorepo with no root test
|
|
161
|
+
script needs: `gates.cwd` maps a gate to a directory, a directory that does not exist or
|
|
162
|
+
resolves outside the repository fails the event and is reported by `check` before the
|
|
163
|
+
commit rather than at it, and a directory named for a gate that is not declared is
|
|
164
|
+
reported as the inert entry it is.
|
|
165
|
+
|
|
166
|
+
### One rule set, shared by the repositories that agreed on it
|
|
167
|
+
|
|
168
|
+
Several repositories want the same rules and each kept its own copy, so a policy change
|
|
169
|
+
was several edits that could be several-minus-one, and the repository that missed it
|
|
170
|
+
carried instructions nobody there wrote. A pack is that shared set: `packs` in the manifest
|
|
171
|
+
declares it, the bytes live in the repository, and a digest of them is recorded so nobody
|
|
172
|
+
has to trust that the two agree.
|
|
173
|
+
|
|
174
|
+
A pack's rules are selected by the same matcher as any profile and appear in the region's
|
|
175
|
+
source list under the pack's own directory. Precedence is the repository, then a pack it
|
|
176
|
+
declares, then what the tool ships, and every replacement is reported rather than silent.
|
|
177
|
+
|
|
178
|
+
A pack that disagrees with its pin still projects — the gate blocks and prints both
|
|
179
|
+
digests, because dropping it would let the repair delete the rules it is reporting. One
|
|
180
|
+
that is missing blocks with a remedy that restores it, and one with no digest is reported
|
|
181
|
+
without failing anything, since its rules render correctly. `compare` lists every pack and
|
|
182
|
+
the digest it is pinned to, so a repository behind a shared change is visible before
|
|
183
|
+
anyone follows stale instructions. Nothing is fetched: the bytes are in the repository,
|
|
184
|
+
which is what makes the rendered region reproducible from what the reader has (ADR-065).
|
|
185
|
+
|
|
186
|
+
### `simt tokens` counts what a reader loads
|
|
187
|
+
|
|
188
|
+
Review 0002 filed the estate's always-on context at 90 KB per turn, 72% of it a paragraph
|
|
189
|
+
an earlier repository had already loaded — a number produced by reading seven
|
|
190
|
+
repositories by hand, which ages the moment a rule is edited and cannot be re-derived by
|
|
191
|
+
the reader who has to act on it.
|
|
192
|
+
|
|
193
|
+
`simt tokens` is that reading, as a command. It reports the bytes this tool wrote per
|
|
194
|
+
channel (region, rule files, skill metadata), an approximate token figure, and — with
|
|
195
|
+
`--repos` — the same figure across a workspace, with every repeated paragraph attributed
|
|
196
|
+
to the first root that carries it. A region that ends past a harness's read limit is
|
|
197
|
+
reported with its byte offset, using the same numbers the existing check uses.
|
|
198
|
+
|
|
199
|
+
Three choices keep the figure honest, and all three are recorded in ADR-064: the meter
|
|
200
|
+
counts what the projection wrote and reports hand-written bytes beside it rather than
|
|
201
|
+
folding them into the budget; a region is the span between its two markers, so the blank
|
|
202
|
+
line a person left above the marker is billed to nobody; and an over-budget repository is
|
|
203
|
+
reported rather than failed, because a meter that went red would train people to ignore
|
|
204
|
+
the number.
|
|
205
|
+
|
|
206
|
+
S38 stays open — this is the instrument. Whether the estate gets under 30 KB is R07's
|
|
207
|
+
kernel, R08's budget field and R11's dedupe, measured with this.
|
|
208
|
+
|
|
209
|
+
## [0.2.1] — 2026-10-13
|
|
210
|
+
|
|
211
|
+
### `init` writes in place; the backup tag and adopt branch are opt-in
|
|
212
|
+
|
|
213
|
+
`simt init --yes` used to always tag the pre-adoption commit and branch onto a dedicated
|
|
214
|
+
adopt branch before writing. The safety argument was sound, but consumers kept reporting
|
|
215
|
+
the branch switch as a surprise — a well-announced surprise is still a surprise.
|
|
216
|
+
|
|
217
|
+
The default now writes in place on the branch you already have: nothing is tagged, nothing
|
|
218
|
+
is branched, and the run says so in an `in place` line. The reviewable diff survives —
|
|
219
|
+
`git diff` shows the whole adoption, and discarding it is a checkout and a clean.
|
|
220
|
+
|
|
221
|
+
The old behaviour moves behind `--backup`: `simt init --yes --backup` tags the pre-adoption
|
|
222
|
+
commit and branches as before, for readers who want the named undo path. ADR-063 records
|
|
223
|
+
the decision and the one behaviour it invalidates: consumers who scripted against the old
|
|
224
|
+
default add `--backup`.
|
|
225
|
+
|
|
226
|
+
### A fresh adoption seeds the gates it can run — and only those
|
|
227
|
+
|
|
228
|
+
The eight common ecosystems outside the JavaScript world — Rust, Go, .NET, Swift, Deno,
|
|
229
|
+
Ruby, Dart and Elixir — were recognised by the fingerprint and served by nothing: no
|
|
230
|
+
profile selected them, no gate was proposed for them. A fresh Rust repository received the
|
|
231
|
+
core essay and a manifest that runs nothing, while the ecosystems that emit stack tags got
|
|
232
|
+
the full treatment.
|
|
233
|
+
|
|
234
|
+
Each now has a profile that selects on its own `has-*` fact, so one marker file reaches
|
|
235
|
+
both the rules the repository reads and the gates its commits run. Maven's profile predates
|
|
236
|
+
this; its row is gates-only, wrapper-first, like `gradlew`.
|
|
237
|
+
|
|
238
|
+
`simt init` now seeds the canonical commands for a detected ecosystem — `cargo fmt
|
|
239
|
+
--check`, `go vet ./...`, `dotnet test`, and the rest of the table — into the manifest's
|
|
240
|
+
gates, but only where a probe found the tool: a binary on PATH, or a file in the repository
|
|
241
|
+
for the Maven wrapper. A marker says the ecosystem is here; the probe says its program can
|
|
242
|
+
run. A declared gate that cannot run fails closed on every commit, so the seed fires only
|
|
243
|
+
when both are true — and where the probe fails, the gate is dropped with a remedy that
|
|
244
|
+
names the install instead of blocking the first commit.
|
|
245
|
+
|
|
246
|
+
A seed is a default: a command the repository already declared is never re-seeded, and an
|
|
247
|
+
npm runner keeps its own scripts, because `package.json` is the repository's own answer to
|
|
248
|
+
"how do I test and format". ADR-062 rules the shape of all of it.
|
|
249
|
+
|
|
250
|
+
### The resolver is measured against a gold corpus
|
|
251
|
+
|
|
252
|
+
`simt skills eval` scores the skill resolver against a corpus of real tasks × real
|
|
253
|
+
repositories, each case carrying a gold list of the skills it should select and a list
|
|
254
|
+
of the ones it must not. It reports precision and recall, prints a PASS/FAIL line per
|
|
255
|
+
case naming the skills it missed or selected that it should not have, and exits
|
|
256
|
+
non-zero on a regression — so it can be run from CI the way the gate is run.
|
|
257
|
+
|
|
258
|
+
The scoring lives in `src/resolve/eval.mjs`, which the command and
|
|
259
|
+
`test/eval.test.mjs` both call, so a test cannot drift from what the command measures.
|
|
260
|
+
The corpus lives under `test/` because test data does not ship in the package: an
|
|
261
|
+
installed copy names the path it would have read and says so instead of scoring the
|
|
262
|
+
author's.
|
|
263
|
+
|
|
264
|
+
### The skill registry is workspace data; the package ships none of it
|
|
265
|
+
|
|
266
|
+
The npm tarball and the wheel carried `graph/skills.graph.json` — the directory
|
|
267
|
+
of skill sources for one workspace, naming the repositories whose bodies it
|
|
268
|
+
resolves. Every installed copy served that catalogue to readers who have none
|
|
269
|
+
of those skills, and `skills install` pointed at paths that exist only in the
|
|
270
|
+
workspace that wrote them. ADR-061 rules the registry out of the published set:
|
|
271
|
+
it is data about one workspace, not content of the package.
|
|
272
|
+
|
|
273
|
+
The default registry path now resolves in tiers, and every tier is file
|
|
274
|
+
existence plus configuration — nothing a pull request cannot diff:
|
|
275
|
+
|
|
276
|
+
1. an explicit path, the positional of `simt graph lint`,
|
|
277
|
+
2. the package's own `graph/skills.graph.json` — present in a source checkout,
|
|
278
|
+
absent from an installed copy,
|
|
279
|
+
3. the member of a configured workspace that holds one — naming the workspace
|
|
280
|
+
it came from, and refusing when several members hold one, because picking
|
|
281
|
+
would be a guess.
|
|
282
|
+
|
|
283
|
+
An installed copy in a workspace with no registry says so, and names the three
|
|
284
|
+
ways to configure one (`--workspace`, `SIMT_DEV_WORKSPACE`, the user config
|
|
285
|
+
file) instead of failing on a path it does not have. `simt graph lint`'s
|
|
286
|
+
no-registry remedy names the path explicitly, because its default is the
|
|
287
|
+
registry it lints.
|
|
288
|
+
|
|
289
|
+
The provenance digest stops walking `graph/` for the same reason it no longer
|
|
290
|
+
leaves the package: the digest covers what a published copy ships, and the
|
|
291
|
+
registry is no longer among it.
|
|
292
|
+
|
|
293
|
+
### No published file may name a repository from this estate
|
|
294
|
+
|
|
295
|
+
The decision records ship in all three channels — npm, the wheel and the sdist — so a
|
|
296
|
+
private repository name written into one is published to three registries. That happened:
|
|
297
|
+
the records were pseudonymised once, across 28 files, and nothing stopped the next one. The
|
|
298
|
+
sweep's own commit says why, and it is the right reason: the redaction it used runs on the
|
|
299
|
+
way *out*, at print time, and the bytes on disk are what get published.
|
|
300
|
+
|
|
301
|
+
`test/shipped.test.mjs` now fails on an estate name anywhere in a published file, and
|
|
302
|
+
asserts its own scope against `package.json` in **both** directions — a directory added to
|
|
303
|
+
`files` without being guarded fails, and so does one guarded that nothing publishes, because
|
|
304
|
+
a guard nobody can fail is one somebody eventually deletes. It is scoped to the published
|
|
305
|
+
set rather than a hand-written list for the same reason: the sweep took 28 files to undo by
|
|
306
|
+
hand, and the guard should not need undoing at all.
|
|
307
|
+
|
|
308
|
+
Names are not removed, they are described. The records that use a repository as *evidence* —
|
|
309
|
+
a table of how three repositories answer the same question — carry stable `repo-NN` labels,
|
|
310
|
+
so the comparison survives. Everything else is better off naming what the repository *is*:
|
|
311
|
+
"a Gradle module with no wrapper", "a repository with no package.json". That reads to
|
|
312
|
+
someone outside the estate without a lookup table they do not have.
|
|
313
|
+
|
|
314
|
+
The one place estate names remain is `docs/reviews/`, which is not published by either
|
|
315
|
+
channel. A review whose subject is fourteen repositories cannot pseudonymise them without
|
|
316
|
+
becoming a document about `repo-01` that never existed.
|
|
317
|
+
|
|
318
|
+
### Four shipped strings named a command this package does not install
|
|
319
|
+
|
|
320
|
+
`0.2.0` installs one binary, `simt`, and `package.json`'s `bin` map says so. Four
|
|
321
|
+
places still said `simt-dev`, which a reader types and is met with `unknown command`:
|
|
322
|
+
|
|
323
|
+
- the default remedy on every bad invocation, `run: simt-dev --help` — so the advice
|
|
324
|
+
printed after a bad command could not be followed
|
|
325
|
+
- the usage line, `usage: simt-dev <command>`, sitting above a command list that spelled
|
|
326
|
+
the same tool `simt`
|
|
327
|
+
- `.github/dependabot.yml`'s header, which said `Yours from here: simt-dev` while the
|
|
328
|
+
gate beside it said `Yours from here: simt`
|
|
329
|
+
- the hook shim, whose own resolution list said `simt-dev on PATH` directly above the
|
|
330
|
+
`command -v simt` that does the looking
|
|
331
|
+
|
|
332
|
+
`test/naming.test.mjs` was supposed to prevent this and did not: its rule required a
|
|
333
|
+
verb after the project name, and none of the four is followed by one. It now also
|
|
334
|
+
catches a flag, a `<placeholder>`, the end of a line, and a program name in a
|
|
335
|
+
resolution list — while still accepting the sentences where the project is the
|
|
336
|
+
subject, which is the spelling that is correct. The two `.github` templates get their
|
|
337
|
+
own assertion, because the fourth of these was not a wrong word in a sentence; it was
|
|
338
|
+
two templates disagreeing with each other in the same position.
|
|
339
|
+
|
|
340
|
+
### Fixed
|
|
341
|
+
|
|
342
|
+
- `hooks install` removes a shim whose gate was dropped from the manifest instead of leaving it to run nothing. A repository that removed its last gate was refused outright, with a remedy telling it to put the gate back; only un-routing git was left to the reader. install also no longer chains a hook to itself.
|
|
343
|
+
|
|
344
|
+
- **Dead exports and duplicated frontmatter, with a test that stops them returning (S21).**
|
|
345
|
+
Eight functions were exported and called by nothing (`gateKind`, `gateWhy`, `gateAdr`,
|
|
346
|
+
`resetWorkspace`, `tracksIdentity`, `isTty`, `surfaceFrontmatter`, `workspaceOrNull`),
|
|
347
|
+
three `__internals` testing seams had no importer, and thirteen more bindings were
|
|
348
|
+
exported while being used only inside their own file — `isExecutable` among them, which
|
|
349
|
+
a first pass through this list wrongly recorded as dead and which its own callers
|
|
350
|
+
proved otherwise. `KNOWN_SUBS` was a hand-kept second copy of the dispatch table, and
|
|
351
|
+
had already drifted: it carried `doctor`, a bare command the group lookup can never
|
|
352
|
+
reach. `mdcFrontmatter` was a second construction of frontmatter the writer now derives
|
|
353
|
+
itself, with a test asserting the copy rather than the behaviour.
|
|
354
|
+
|
|
355
|
+
- **"Did you mean" offered the shortest fragment first, and a misspelt skill was
|
|
356
|
+
suggested for a correctly spelled one (S22).** `nearMatches` had no test at all.
|
|
357
|
+
Both directions of substring scored 0 — the same score as an exact match — so the
|
|
358
|
+
alphabetical tiebreak decided the order, and `skills graphs` suggested `graph`
|
|
359
|
+
before `graphs`. Worse, the containment test had no length floor, so `test` matched
|
|
360
|
+
the candidate `e` and the message read `did you mean e, test-writer?`, which an
|
|
361
|
+
agent has every reason to install. A prefix now scores 1 rather than 0, a fragment
|
|
362
|
+
counts only when it is at least half the query, and the edit bound scales with the
|
|
363
|
+
query's length — three edits is a typo in `slice-commmit` and is most of a
|
|
364
|
+
four-character query.
|
|
365
|
+
|
|
366
|
+
- **The conflict scanner skipped two of the five rule surfaces, and one pointer file
|
|
367
|
+
(S22).** `rulePaths` enumerated filenames by hand, so `.claude/rules/` and
|
|
368
|
+
`.agents/rules/` — added to the surface table after the list was written — were
|
|
369
|
+
never scanned, and a hand-written rule contradicting a generated one there was
|
|
370
|
+
invisible. A conflict detector that only looks where it was pointed looks nowhere
|
|
371
|
+
new. The scan list is now derived from the surface table and the pointer table.
|
|
372
|
+
|
|
7
373
|
## [0.2.0] — 2026-10-11
|
|
8
374
|
|
|
9
375
|
### Publishable from PyPI and Homebrew as well as npm
|
|
@@ -48,17 +414,17 @@ recommending. It chained on the event *vocabulary* — anything that was not
|
|
|
48
414
|
`pre-commit` or `pre-push` — when the question worth asking is whether **this
|
|
49
415
|
repository** declares a gate for that event.
|
|
50
416
|
|
|
51
|
-
The difference is
|
|
417
|
+
The difference is a repository whose `pre-push` is `git-lfs`. `pre-push` is an
|
|
52
418
|
event this tool gates in general, so `0.1.0` displaced that hook; and because
|
|
53
|
-
|
|
54
|
-
git would have found no `pre-push` hook at all. LFS objects would have
|
|
55
|
-
being uploaded, with nothing to notice it — the exact silent failure
|
|
56
|
-
exists to prevent, reached by fixing the wrong half of it.
|
|
419
|
+
that repository declares no `pre-push` gate there was no shim for the event
|
|
420
|
+
either, so git would have found no `pre-push` hook at all. LFS objects would have
|
|
421
|
+
stopped being uploaded, with nothing to notice it — the exact silent failure
|
|
422
|
+
chaining exists to prevent, reached by fixing the wrong half of it.
|
|
57
423
|
|
|
58
424
|
`0.1.1` asks the manifest. An event the repo declares a gate for is left to
|
|
59
425
|
`gates.commands`, where the hook runs as a gate and its verdict appears in the
|
|
60
|
-
report; every other foreign hook is chained. That also picks up
|
|
61
|
-
|
|
426
|
+
report; every other foreign hook is chained. That also picks up a static
|
|
427
|
+
site's Lighthouse pre-push, which declares no gates at all.
|
|
62
428
|
*(ADR-018, as amended)*
|
|
63
429
|
|
|
64
430
|
Upgrade with `npm i -D simt-dev@0.1.1`, then `simt hooks install --yes`. Any
|
|
@@ -161,6 +527,8 @@ never ran `simt hooks install` again will find nothing until the dependency is r
|
|
|
161
527
|
and `simt hooks install --yes` is re-run. That is a real upgrade step and it is called out
|
|
162
528
|
here rather than discovered.
|
|
163
529
|
|
|
530
|
+
[0.2.2]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.2
|
|
531
|
+
[0.2.1]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.1
|
|
164
532
|
[0.2.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.0
|
|
165
533
|
[0.1.1]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.1
|
|
166
534
|
[0.1.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -23,13 +23,13 @@ Requires **Node >= 22.20.0** (the PyPI install can supply it, see below).
|
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
25
|
# pinned, per repository - what CI and a teammate both get
|
|
26
|
-
npm i -D simt-dev@0.2.
|
|
26
|
+
npm i -D simt-dev@0.2.1
|
|
27
27
|
|
|
28
28
|
# or globally, if you would rather not add a dependency
|
|
29
|
-
npm i -g simt-dev@0.2.
|
|
29
|
+
npm i -g simt-dev@0.2.1
|
|
30
30
|
|
|
31
31
|
# or with no install at all, for a one-off
|
|
32
|
-
npx -y simt-dev@0.2.
|
|
32
|
+
npx -y simt-dev@0.2.1 check
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
### From PyPI
|
|
@@ -81,7 +81,7 @@ simt compare # what changed across every repository that adopted it
|
|
|
81
81
|
| Path | What |
|
|
82
82
|
| --- | --- |
|
|
83
83
|
| `AGENTS.md` | the projection, between `<!-- simt:begin -->` and `<!-- simt:end -->` (renamed from `simt-dev:`; the old markers are still read, and a region migrates on its next write) |
|
|
84
|
-
| `simt.json` | your manifest: which gates to declare, which substitutions to fill
|
|
84
|
+
| `simt.json` | your manifest: which gates to declare, which substitutions to fill |
|
|
85
85
|
| `.cursor/rules/*.mdc`, `.claude/rules/*.md`, `.agents/rules/*.md`, `.github/instructions/*.instructions.md` | one file per rule, written only for the harnesses that read their own directory |
|
|
86
86
|
| `.editorconfig`, `.gitattributes` | baseline regions |
|
|
87
87
|
| `.simt-hooks/` and `core.hooksPath` | the shared dispatcher, fail-closed |
|
|
@@ -72,6 +72,33 @@ One manifest. Three answers. The one I would have shipped if I had only run `che
|
|
|
72
72
|
every file (`ENOTDIR`) and classified every Gradle repository in the estate as having no
|
|
73
73
|
runner at all — a wrong answer from an evidence reader, which is worse than no answer
|
|
74
74
|
because the caller cannot tell them apart.
|
|
75
|
+
8. **A declared command is a shell command line, not an argv.** *(S17, 2026-10-12)*
|
|
76
|
+
|
|
77
|
+
It was split on whitespace and handed to `execFile` as an argv. That made a declaration
|
|
78
|
+
a repository would plausibly write run the wrong thing rather than fail: a flag whose
|
|
79
|
+
value contains a space became two arguments, and the quotes in a compound line were
|
|
80
|
+
passed to the program as literal characters. There was no way to express a quoted
|
|
81
|
+
argument at all, and nothing said so.
|
|
82
|
+
|
|
83
|
+
So `gates.commands[name]` is one line, run through `sh -c`, and everything the
|
|
84
|
+
repository wrote in it survives — quotes, pipes, `&&`, a path with a space. This is the
|
|
85
|
+
same line the repository would type into its own shell, which is the only definition of
|
|
86
|
+
"what this repository means by that" that does not require a parser here.
|
|
87
|
+
|
|
88
|
+
**The trust boundary is the repository, and it is not a sandbox.** The line comes from
|
|
89
|
+
the repository's own manifest, which it can read and change, and it runs on its author's
|
|
90
|
+
own machine when that author commits. `sh -c` adds no privilege: the same person who
|
|
91
|
+
wrote the line could run it. What `sh -c` must not do is apply to anything this tool
|
|
92
|
+
resolved itself — a formatter path out of `node_modules/.bin` stays an argv, because
|
|
93
|
+
handing it to a shell would change who decided its arguments for no gain. The two
|
|
94
|
+
sources are separate branches in the dispatcher rather than a flag, so the distinction
|
|
95
|
+
cannot be forgotten by a caller that does not know it exists.
|
|
96
|
+
|
|
97
|
+
**Consequence for a repository that relied on the split.** A declaration with no quotes
|
|
98
|
+
behaves identically, because whitespace-splitting a shell word that needed no quoting
|
|
99
|
+
is the same thing the shell does. Every use of `gates.commands` in this estate declares
|
|
100
|
+
a bare path and is unaffected; the change only makes declarations *possible* that were
|
|
101
|
+
previously impossible rather than making any of them wrong.
|
|
75
102
|
|
|
76
103
|
## Consequences
|
|
77
104
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ADR-059 — The version a repository runs is the one it pinned, and the gate refuses otherwise
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-12 · affects all repos
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
ADR-010 ruled that a tool which is genuinely not a project dependency may be invoked directly,
|
|
8
|
+
**provided its version is pinned**. For a repository with a `package.json`, simt-dev is that
|
|
9
|
+
dependency, and npm does the pinning: it installs the version in `package.json` into
|
|
10
|
+
`node_modules`, and a stale copy is a stale lockfile that `npm ci` reproduces.
|
|
11
|
+
|
|
12
|
+
For a repository **without** npm — a Gradle build, a Connect IQ project — there is no
|
|
13
|
+
`node_modules` to pin in. simt-dev is a global install, and nothing pins it. The version on
|
|
14
|
+
`PATH` is whatever the last `npm i -g` left there, and it can be anything. The gate that runs
|
|
15
|
+
on commit is the one in that binary. If it is not the version the repository adopted, the gate
|
|
16
|
+
that runs locally is not the one CI runs: a green tick from a check that ran the wrong version
|
|
17
|
+
is the one outcome worse than a red build, because it is a pass that never happened.
|
|
18
|
+
|
|
19
|
+
The same gap sat in the remedy. The shim and the consumer workflow assumed npm everywhere —
|
|
20
|
+
`npm i -D simt-dev@<v>` and `npm ci`. In a Gradle repository that command installs into a
|
|
21
|
+
`package.json` the repository will never have, so the remedy cannot succeed and the CI step
|
|
22
|
+
cannot start. The install and the remedy each needed to know the kind of repository they were
|
|
23
|
+
in, and neither did.
|
|
24
|
+
|
|
25
|
+
## Ruling
|
|
26
|
+
|
|
27
|
+
1. **The manifest records the version a repository adopted.** `projection` carries
|
|
28
|
+
`version` alongside the existing `simtDevSha`, written at adoption and refreshed by
|
|
29
|
+
`upgrade`. A node_modules copy is pinned by npm itself, so this field matters only where
|
|
30
|
+
npm is not the pin — but it is written everywhere, because a manifest that records the
|
|
31
|
+
version in one shape of repository and not another is two manifests.
|
|
32
|
+
|
|
33
|
+
2. **The gate compares the running binary against that pin, and refuses on a mismatch.** The
|
|
34
|
+
comparison is in the dispatcher (a failed `version` gate) and in `check` (a blocking
|
|
35
|
+
finding), not in the shim's rendered bytes: the dispatcher is where every gate already runs,
|
|
36
|
+
and a comparison duplicated into bytes a formatter might rewrite would drift from the one
|
|
37
|
+
that decides. It is placed after the empty-gate check on purpose — a repository that declares
|
|
38
|
+
no gate has nothing the version could get wrong, and failing it would block commits in repos
|
|
39
|
+
that deliberately declare none.
|
|
40
|
+
|
|
41
|
+
3. **The install and the remedy are per kind.** Where a `package.json` exists, simt-dev is pinned
|
|
42
|
+
as a devDependency and the remedy is `npm i -D simt-dev@<v>`. Where there is no npm, it is a
|
|
43
|
+
pinned global install and the remedy is `npm i -g simt-dev@<v>` (or `export SIMT_DEV_BIN=…`).
|
|
44
|
+
The consumer CI workflow makes the same choice by kind — `npm ci` where the package is a
|
|
45
|
+
declared dependency, the pinned global install otherwise — so the gate that runs in CI is the
|
|
46
|
+
one the machine remedy names, and a check cannot pass in one place and fail to start in the
|
|
47
|
+
other.
|
|
48
|
+
|
|
49
|
+
## Consequences
|
|
50
|
+
|
|
51
|
+
- A stale global binary can no longer produce a green tick from a check that ran a different gate
|
|
52
|
+
than CI does. The mismatch is a refusal with the one command that fixes it, and the remedy and
|
|
53
|
+
the CI step agree on which version to install.
|
|
54
|
+
- A repository adopted before the field existed — a manifest with `simtDevSha` but no
|
|
55
|
+
`projection.version` — is **skipped, not failed**. There is nothing to compare against, and
|
|
56
|
+
failing every pre-0.2 repository the moment this check ships would turn the gate red for a
|
|
57
|
+
reason it cannot fix. `compare` reports that missing field as a stale manifest (ADR-045's
|
|
58
|
+
defect from the far side), so it is visible and repairable rather than silently absent.
|
|
59
|
+
- The version a repository runs is now an invariant the gate enforces, not a comment: the pin in
|
|
60
|
+
the manifest and the binary on `PATH` must agree, or the commit does not happen.
|
|
61
|
+
|
|
62
|
+
## Rejected
|
|
63
|
+
|
|
64
|
+
- **Shell out to `simt --version` in the shim.** The dispatcher runs from the binary that
|
|
65
|
+
resolved it, so reading the running package's version is the same answer without a subprocess,
|
|
66
|
+
and it is correct whether the binary came from `node_modules`, a global install, or a source
|
|
67
|
+
checkout. A subprocess would answer only for the binary that started it.
|
|
68
|
+
- **Fail a repository with no recorded pin.** There is nothing to compare against; it would make
|
|
69
|
+
every pre-0.2 repository red on the day this ships, for a mismatch nobody can resolve until
|
|
70
|
+
they re-adopt. The missing pin is reported as drift, which is the repairable form of the same
|
|
71
|
+
fact.
|
|
72
|
+
- **Put the check only in the shim's rendered bytes.** The dispatcher is where every gate runs and
|
|
73
|
+
where `check` already lives, so the comparison belongs in one place. Rendered bytes are read by
|
|
74
|
+
a formatter and a reviewer; a control that lives in both is two copies of one question, which
|
|
75
|
+
is the shape this record's siblings keep finding drifting.
|