@kaisers-io/refs 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +222 -1
  2. package/dist/refs.mjs +68 -91
  3. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,225 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0] - 2026-08-31
11
+
12
+ ### Changed
13
+
14
+ - **The skill's version-question flow moved into its own file.** Every question about a
15
+ dependency's source loads `SKILL.md` and `INVESTIGATE.md` in full, and 30 % of
16
+ `INVESTIGATE.md` was a block on resolving versions to tags and diffing between them — read
17
+ on every plain source question, used on almost none of them. It is now `VERSIONS.md`, with a
18
+ route of its own in `SKILL.md` §5 and a one-line pointer at the end of `INVESTIGATE.md`, so
19
+ a mis-route costs one extra read rather than a wrong answer.
20
+
21
+ Alongside it, a compression pass over what remains: the worker output contract was stated
22
+ twice and is now stated once, the five clickable-link rules became one normalization, and a
23
+ handful of sentences that repeated something said a few lines earlier are gone. Nothing
24
+ behavioural was removed — the capability gate, the trust boundary, the five hard rules and
25
+ the worker prompt's own safety rules are untouched, and the measured partial-clone cost model
26
+ (which git commands fetch blobs, and that `git blame` fetches one per visited revision) is
27
+ refs' own measurement rather than something the term "partial clone" implies, so it stays
28
+ verbatim.
29
+
30
+ A plain source question now loads 20 % less. A version question loads about what it did
31
+ before, plus one extra file read.
32
+
33
+ ### Added
34
+
35
+ - **`refs sync` and `refs doctor` now report configuration that has fallen behind its upstream.**
36
+ A configured package path is only a locator, and upstream can delete or move what sits at it.
37
+ Until now only `refs resolve` noticed, for the one package an agent happened to route to, and it
38
+ persisted nothing — so a package deleted upstream could sit wrong in the configuration
39
+ indefinitely while every other package in the same checkout went uninspected.
40
+
41
+ Each successful `refs sync` result now carries a nested `structure: {status, packages}`, probed
42
+ inside the lock the sync already holds, right after the checkout was updated. Nothing is stored:
43
+ the answer is reported and thrown away, so there is no drift state that can itself go stale. A
44
+ removal and a relocation are reported as different findings, because they need opposite repairs —
45
+ telling an agent to "fix the path" of a package upstream deleted sends it looking for something
46
+ that is not there. Human output gains indented lines under the affected ref and stays silent when
47
+ everything resolves; the summary counts and exit code are untouched, since a drifted ref synced
48
+ perfectly well.
49
+
50
+ Only refs that actually sync are probed, which keeps `--stale-only` a genuine no-op — and is why
51
+ `refs doctor` gains a `config-drift` check as the deliberate "check everything now" counterpart.
52
+ It takes each ref's lock with a short timeout and reports the ref as busy rather than waiting,
53
+ writes nothing, and reports `warn` rather than `fail`: the configuration has fallen behind,
54
+ nothing is broken. `refs list` deliberately stays blind — without stored state it would
55
+ turn a cheap inventory command into a locking filesystem sweep.
56
+
57
+ - **`refs resolve` answers in one call what used to take three.** The skill's investigation flow
58
+ began `resolve` → `sync` → `resolve` **again**, and the third call was not ceremony: package
59
+ verification had described the checkout as it was _before_ the sync, so reusing that answer meant
60
+ reporting a path that no longer necessarily held what it claimed. `--sync-if-stale` fetches (or
61
+ clones) only when the ref is stale or its checkout absent, and everything it reports describes the
62
+ checkout afterwards. The rule has left the skill and become code.
63
+
64
+ It refuses, rather than syncing, when the checkout is `unmanaged` or `unverifiable`. `sync`
65
+ hard-resets and cleans; running it against a directory whose identity was never established is
66
+ how a stray clone loses its history. A failing sync fails the command rather than returning a
67
+ success envelope containing a stale path.
68
+
69
+ - **`refs resolve --project <dir>` reports the version a project has installed.** The skill used to
70
+ tell the agent to read the project's lockfile by hand, and nothing in refs touched one — so the
71
+ deterministic half of every "what changed between my version and a newer one" question was done
72
+ by the least deterministic component available, against pnpm's peer-qualified keys, aliases,
73
+ overrides and three vendor-specific formats.
74
+
75
+ The answer is read from `node_modules`, walking up in Node's own lookup order, and stops at the
76
+ first installation slot that exists rather than the first readable manifest — falling through to
77
+ an ancestor would report a shadowed install Node would not have loaded. There is deliberately no
78
+ lockfile fallback: a lockfile says what _should_ be installed, `node_modules` says what _is_, and
79
+ the second is the question. `installed.status` is `found`, `not_materialized`,
80
+ `unsupported_layout` (Yarn PnP, detected but never loaded — `.pnp.cjs` is project code) or
81
+ `unverifiable`.
82
+
83
+ - **`refs resolve --ref <ref>`** scopes a query to one ref's packages. A package name registered by
84
+ several refs used to be answered with "use the full ref key" — advice the command could not
85
+ honour, because a full-key query routes by _ref_ and comes back with `package: null`. The error
86
+ now names a remedy that exists.
87
+
88
+ - `refs doctor` gained a `locks` check. A held lock used to be invisible: acquisition failed with a
89
+ message that named no owner, and `doctor` had no lock check at all — so the one command meant to
90
+ answer "is something stuck?" could not see the thing that was stuck. The check lists every entry
91
+ in the locks directory with its recorded owner, how long it has been held, and against which
92
+ window.
93
+
94
+ A held lock is **not** a warning by itself: that is what a concurrent `refs sync` looks like, so
95
+ it reports `ok` with the holder listed. `warn` is reserved for something that will not resolve on
96
+ its own — a recorded process that is gone, a lock past its window and still there, metadata that
97
+ cannot be read, or something that is not a lock at all occupying a lock name. Like every other
98
+ `doctor` warning, it does not change the exit code.
99
+
100
+ ### Changed
101
+
102
+ - **`refs resolve` establishes that the path it hands back is really this ref's checkout.** It used
103
+ to report presence from a `.git` entry alone, while `add` and `sync` both ran a stronger guard
104
+ before mutating — so the one command whose result is read as "the source is here" was the one
105
+ that did not check what was there. A manual clone at the derived path, a half-finished `remove`,
106
+ a restored backup or a symlinked second home all produced a confident answer about the wrong
107
+ repository, with no error and no warning.
108
+
109
+ Every reply now carries `checkout: {status, reason?}` — `managed`, `missing`, `unmanaged` or
110
+ `unverifiable` — read straight out of `.git/config` without spawning git, so the hot path stays
111
+ subprocess-free. The origin URL is never echoed back in `reason`; it can carry credentials.
112
+
113
+ `managed` requires the `core.hooksPath` marker to be **this home's** hooks directory, not merely
114
+ present — the comparison `add` already makes — so a manual clone that sets it for its own purposes
115
+ does not pass. A config git itself would reject (an unterminated quote, an undefined escape, a
116
+ line that is neither a section nor an assignment) is `unverifiable` rather than partially read: a
117
+ file git would not accept is not evidence of identity.
118
+
119
+ **Package verification is gated on it.** A manifest read inside an unrelated checkout can answer
120
+ `verified` for a package that has nothing to do with the query, so anything other than `managed`
121
+ or `missing` now yields `package.status: "unverifiable"` instead of a confident location.
122
+
123
+ `missing` is unchanged and still means `checkout.status === "missing"`. Callers should branch on
124
+ `checkout.status`, which answers the question `missing` was often assumed to.
125
+
126
+ - **The "lock is held" error now says who holds it and for how long.** It used to read `lock <name>
127
+ is held — another refs process is running`, which left no way to tell a running `sync` from
128
+ something that crashed 90 seconds ago. It now names the recorded pid, whether that pid is still
129
+ present, how long the lock has been held, and when it becomes reclaimable.
130
+
131
+ It deliberately says "recorded pid … is present (identity not verified)" rather than "held by pid
132
+ …": only `ESRCH` establishes that a process is gone, so a pid that answers may equally be an
133
+ unrelated process that reused the number. And it says "reclaimable", never "released
134
+ automatically" — nothing removes a lock in the background; the phrase means the next acquisition
135
+ attempt is entitled to take it.
136
+
137
+ - **An operation that ran without the lock it asked for now fails instead of reporting success.**
138
+ A lock can still be lost while its holder works — a stolen lock is detected by the next renewal,
139
+ or by release finding a foreign token. Previously the callback's result was returned as if
140
+ nothing had happened. It is now reported as a `conflict` (exit code 5), because the work ran
141
+ without the mutual exclusion it requested and its result is not trustworthy. An operation that
142
+ failed on its own keeps precedence: its own error is what the caller sees.
143
+
144
+ ### Fixed
145
+
146
+ - The `rm -rf` commands `refs doctor` and `refs add` suggest are now quoted. Ref keys derive from
147
+ user-supplied urls and permit spaces, `$()`, backticks, semicolons and quotes, and the refs home
148
+ itself routinely sits under a path containing a space — so pasting an unquoted suggestion could
149
+ delete several wrong paths and leave the intended one, or execute a command substitution embedded
150
+ in a repository name. The form is now `rm -rf -- '<path>'`; `--` additionally stops a path
151
+ beginning with `-` from parsing as options.
152
+
153
+ - A pid in a lock's metadata is now required to be a positive integer within the range
154
+ `process.kill` accepts. `0` and negative values are
155
+ process-_group_ selectors for `process.kill`, so metadata carrying either made the liveness probe
156
+ answer for a whole group — reporting a long-gone owner as present, and keeping its lock
157
+ unreclaimable for the rest of its window. A value past that range is worse still: Node rejects it
158
+ with a `TypeError` rather than an errno, which the probe read as "not gone, therefore present".
159
+ Such metadata is now reported as malformed instead of acted on.
160
+
161
+ - A lock is no longer taken away from a holder that is still working, however long the work takes.
162
+ Locks were judged by a fixed ten-minute age: past it, a waiter treated the lock as abandoned and
163
+ stole it even when its owner was demonstrably alive and mid-operation. Since the per-ref lock is held across a whole clone or
164
+ fetch, any repository large enough to take ten minutes could end up with two processes running
165
+ `checkout -B` / `reset --hard` / `clean -fd` against the same directory.
166
+
167
+ A holder now renews a lease while it works, so a lock is abandoned when its process is definitely
168
+ gone **or** its lease has expired — never merely because the work took a long time. "The pid still
169
+ exists" does not override an expired lease, which is what keeps a recycled pid from stranding a
170
+ lock forever.
171
+
172
+ The same constant was also too long at the other end: a crashed `sync` or `add` left its lock
173
+ behind and blocked the ref for the full ten minutes. An abandoned lock is now reclaimable after
174
+ two minutes rather than ten.
175
+
176
+ Two limits are worth knowing. A holder whose event loop is stopped for longer than the lease — a
177
+ suspended process, a sleeping machine — cannot renew, and is stealable after two minutes where it
178
+ used to take ten; in practice a sleeping machine suspends every refs process on it, and the
179
+ pending renewal fires on resume. And a lock written by an older CLI carries no lease and is still
180
+ judged by the ten-minute rule it was written under, so upgrading never dispossesses a running
181
+ older process. The reverse does not hold:
182
+ an older CLI reads no lease, so it can still take a lock from a live current holder once ten
183
+ minutes pass. Holds longer than that during a rolling upgrade are not protected.
184
+
185
+ ## [0.10.0] - 2026-08-13
186
+
187
+ ### Added
188
+
189
+ - `refs resolve` now verifies that the package it routes to is actually where the config says
190
+ it is. A configured `path` is only a locator; the package name is its identity, and upstream
191
+ repos restructure on their own schedule. Previously a package that had moved — or a different
192
+ package that had taken over its directory — was handed back regardless, so an agent read the
193
+ wrong source and answered confidently. That failure produced no error and no warning.
194
+
195
+ `package.status` now reports what was established: `verified`, `relocated` (found at exactly
196
+ one new path, which is returned in place of the stale one), `unmaterialized` (no checkout yet),
197
+ `unverifiable` (verification could not complete — `reason` says why), `ambiguous` (the name
198
+ exists at several paths, listed in `candidates`), or `missing`. All six exit `0`; see
199
+ `docs/commands.md` for the full contract.
200
+
201
+ `relocated` corrects the answer for that call only and never writes to `config.toml`. Persist
202
+ it with `refs edit <ref> --package <name> path <new-path>`.
203
+
204
+ ### Changed
205
+
206
+ - **`resolve`'s `package.local_path` can now be `null`.** It is `null` for `missing` and
207
+ `ambiguous`, where no safe location is known. A caller that treated a zero exit as "here is a
208
+ usable path" must check `package.status` first; previously the field was always a string.
209
+
210
+ - Workspace detection now reports why it found nothing. An unreadable or malformed workspace
211
+ declaration, an unreadable manifest, a candidate resolving outside the repo, an unsupported
212
+ pattern, a package directory reachable only through a symlink — each used to collapse into the
213
+ same empty result, leaving a transient read error indistinguishable from "every package was
214
+ removed". Each is now reported, and a scan carrying any of them is treated as possibly
215
+ incomplete: it can neither conclude that a package is gone nor that a single sighting of one is
216
+ unique. `refs add` is unaffected — it consumes the same best-effort list it always has.
217
+
218
+ A manifest that reads fine but declares no usable `name` is reported too, but does **not** make
219
+ a scan incomplete: there is demonstrably no resolvable package at that path. Nameless manifests
220
+ are common enough (zod's own repository root has none) that treating them as failures would
221
+ permanently suppress detection for those repos.
222
+
223
+ One limit is deliberate and worth knowing: a scan only covers what the repo's workspace
224
+ declaration points at. A package registered by `refs add`'s npm fallback — at `path: "."`, or
225
+ the packument's `directory` — lives outside that coverage, so if it moves, `resolve` reports
226
+ `unverifiable` rather than guessing. It never reports `missing` from a scan that had nowhere
227
+ to look.
228
+
10
229
  ## [0.9.0] - 2026-08-13
11
230
 
12
231
  ### Added
@@ -470,7 +689,9 @@ trusted-publishing pipeline end to end.
470
689
  installed git hooks.
471
690
  - Agent skill (`skills/refs/`) documenting the investigate/add/maintain workflows.
472
691
 
473
- [Unreleased]: https://github.com/kaisers-io/refs/compare/v0.9.0...HEAD
692
+ [Unreleased]: https://github.com/kaisers-io/refs/compare/v0.11.0...HEAD
693
+ [0.11.0]: https://github.com/kaisers-io/refs/compare/v0.10.0...v0.11.0
694
+ [0.10.0]: https://github.com/kaisers-io/refs/compare/v0.9.0...v0.10.0
474
695
  [0.9.0]: https://github.com/kaisers-io/refs/compare/v0.8.3...v0.9.0
475
696
  [0.8.3]: https://github.com/kaisers-io/refs/compare/v0.8.2...v0.8.3
476
697
  [0.8.2]: https://github.com/kaisers-io/refs/compare/v0.8.1...v0.8.2