@holmes-lab/holmes-kit 0.19.2 → 0.19.4
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 +177 -0
- package/README.md +22 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +8 -0
- package/dist/holmes/cli/agents.js +26 -2
- package/dist/holmes/cli/codex-toml.d.ts +10 -0
- package/dist/holmes/cli/codex-toml.js +76 -12
- package/dist/holmes/cli/doctor.d.ts +34 -0
- package/dist/holmes/cli/doctor.js +139 -46
- package/dist/holmes/cli/index.js +13 -0
- package/dist/holmes/cli/init.js +10 -3
- package/dist/holmes/cli/native-deps.js +4 -1
- package/dist/holmes/cli/playbook-skills.js +6 -4
- package/dist/holmes/cli/probe-process.d.ts +8 -0
- package/dist/holmes/cli/probe-process.js +73 -0
- package/dist/holmes/cli/spawn-spec.js +3 -1
- package/dist/holmes/cli/test-platform.d.ts +37 -0
- package/dist/holmes/cli/test-platform.js +126 -1
- package/dist/holmes/governance/approval-grants.js +26 -3
- package/dist/holmes/governance/autonomy.d.ts +17 -1
- package/dist/holmes/governance/autonomy.js +37 -5
- package/dist/holmes/mcp/handlers.d.ts +30 -5
- package/dist/holmes/mcp/handlers.js +111 -13
- package/dist/holmes/mcp/spec-id-guard.d.ts +1 -1
- package/dist/holmes/mcp/spec-id-guard.js +9 -13
- package/dist/holmes/mcp/tool-schemas.js +13 -0
- package/dist/holmes/project/install-scripts-policy.d.ts +16 -2
- package/dist/holmes/project/install-scripts-policy.js +16 -2
- package/dist/holmes/review/point-in-time-replay.js +43 -3
- package/dist/holmes/rtm/graph-store.d.ts +2 -0
- package/dist/holmes/rtm/graph-store.js +14 -0
- package/dist/holmes/rtm/rtm-graph.js +42 -30
- package/dist/holmes/semantic/credentials.js +86 -9
- package/dist/holmes/semantic/embedder.js +6 -39
- package/dist/holmes/semantic/local-model.d.ts +30 -0
- package/dist/holmes/semantic/local-model.js +92 -0
- package/dist/holmes/semantic/model-cache.d.ts +8 -0
- package/dist/holmes/semantic/model-cache.js +67 -0
- package/dist/holmes/semantic/tier.d.ts +7 -0
- package/dist/holmes/semantic/tier.js +9 -3
- package/dist/holmes/spec/renumber.d.ts +72 -0
- package/dist/holmes/spec/renumber.js +341 -0
- package/dist/holmes/spec/spec-id.d.ts +9 -0
- package/dist/holmes/spec/spec-id.js +23 -0
- package/docs/install-guide.md +90 -2
- package/package.json +6 -3
- package/scripts/install.ps1 +30 -27
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,183 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.19.4] - 2026-09-10
|
|
9
|
+
|
|
10
|
+
Three Windows diagnoses that pointed the wrong way, found by running the Windows plan on a real
|
|
11
|
+
non-admin machine (Windows 11, Node 24.19, npm 12.0.1, codex 0.153.4); then two things the new
|
|
12
|
+
required dependency broke on every operating system, found by reviewing that work on macOS.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Preparing the local model is now opt-in** (REQ-594). Installing the package, running
|
|
16
|
+
`holmes-kit init`, and the Windows bootstrap no longer download model assets on their own; each
|
|
17
|
+
names `holmes-kit semantic-setup` instead. Set `HOLMES_AUTO_MODEL_INSTALL=1` to prepare during
|
|
18
|
+
installation. `HOLMES_SKIP_MODEL_INSTALL=1` still refuses and OUTRANKS the opt-in, so an explicit
|
|
19
|
+
refusal is never overridden, and the two skips say different things — telling someone they opted
|
|
20
|
+
out when they did not is a lie. All three operating systems share the default: the bootstrap
|
|
21
|
+
invokes the explicit command, which always downloads, so the gate had to live in the bootstrap too
|
|
22
|
+
or Windows would keep fetching after macOS and Linux stopped.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **The semantic tier reported a capability it did not have** (REQ-594). Availability was decided by
|
|
26
|
+
`require.resolve('@xenova/transformers')`. Once that package became a required dependency the
|
|
27
|
+
predicate answered true for every install and stopped discriminating, so every user without a
|
|
28
|
+
cloud key was reported as `tier: local` — including users who had never downloaded a model, for
|
|
29
|
+
whom no embedding is produced at all. Availability is now a conjunction of runtime AND a prepared
|
|
30
|
+
model cache; an empty cache directory counts as unprepared, because an interrupted download leaves
|
|
31
|
+
exactly that. Until the model is prepared, `doctor` reports tier `none`, names what it costs
|
|
32
|
+
(requests lexical search misses, measured 16.4%, have 0% recall) and the one command that changes
|
|
33
|
+
it — as `PASS`, since a warning no action can resolve teaches people to skip the report.
|
|
34
|
+
- **A new native dependency was approved without the rule that justifies it** (REQ-595).
|
|
35
|
+
`sharp@0.32.6` was added to `allowScripts` while the measured required-script list still held one
|
|
36
|
+
package, so the policy told readers sharp "loads from its shipped prebuilds without an install
|
|
37
|
+
script". Measured 2026-09-10, that is false everywhere: its tarball ships no native binary, its
|
|
38
|
+
hook is `prebuild-install || node-gyp rebuild`, the lockfile records `hasInstallScript: true`.
|
|
39
|
+
Because `allowScripts` is a root-manifest policy that does not transfer to consumers, a rule that
|
|
40
|
+
did not know sharp meant no consumer was ever told to approve it. Both required packages are now
|
|
41
|
+
named. The guard was not loosened: every member of the list must be backed by the lockfile, and an
|
|
42
|
+
empty lock now reports every required package rather than only the first.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- **`spec_renumber`** (REQ-255). Two sessions working from the same base each issued number 596, and
|
|
46
|
+
the repository's own prescription for that was `renumber one (manual until REQ-255) and re-seal` —
|
|
47
|
+
a tool specified after the 2026-08-23 incident and never built. A hand renumber then moved two
|
|
48
|
+
anchors and left seven prose references, one of them inside the shipped `install.ps1`, and the bulk
|
|
49
|
+
regex brought in to finish the job rewrote REQ-253's own `source.ref` into a citation of itself.
|
|
50
|
+
The tool is deliberately asymmetric: it moves what has a definite grammar (filenames, frontmatter
|
|
51
|
+
`id`, every `depends_on`, `slice` tags, `@implements` anchors) and only REPORTS what needs context
|
|
52
|
+
(prose, comments, test titles, CHANGELOG). Re-sealing stays with `spec_approve`; the plan returns
|
|
53
|
+
the two orders instead, which are opposites because unsealing refuses child-first and approving
|
|
54
|
+
refuses parent-first. `dryRun` is the default.
|
|
55
|
+
- **`win32Only`** (A-SPEC-255). The mirror of `posixOnly`, for tests whose subject only exists on
|
|
56
|
+
Windows — NTFS ACLs, PowerShell 5.1 parsing. Without it those tests were written as anonymous
|
|
57
|
+
`platform === 'win32' ? it : it.skip` ternaries: a skip with no stated reason, which is the silence
|
|
58
|
+
`posixOnly` exists to end, in different syntax. A reason is required, as it is there.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- **Windows 0.19.4 release qualification** (REQ-599, merged from the Windows branch; issued there as
|
|
62
|
+
596 and renumbered here — both sessions had forked from the same base and claimed that number).
|
|
63
|
+
Raw Git blob bytes are preserved during replay despite `autocrlf` and explicit EOL attributes;
|
|
64
|
+
native project/store paths are compared canonically, including 8.3 short aliases; npm's JavaScript
|
|
65
|
+
entry is invoked through Node and the Windows prepublish Jest run is held to two workers, because
|
|
66
|
+
seven concurrent workers produced startup failures in six suites; PowerShell 5.1 recovery commands
|
|
67
|
+
are failure-aware and the SQLite plus sharp script approvals are documented. Windows fixtures were
|
|
68
|
+
repaired for filesystem permissions, branch names, paths, environment key case, npm JSON versions
|
|
69
|
+
and line endings **while keeping their negative assertions**. File identities are read as BigInt in
|
|
70
|
+
playbook skill alias detection: actual NTFS ids exceeded Number precision, so distinct ids collided
|
|
71
|
+
numerically and blocked refresh — genuine hard-link refusal is preserved.
|
|
72
|
+
- **The release gate could not find npm and died looking** (REQ-600). `npmCommand` derived its
|
|
73
|
+
candidates from `process.execPath`, and Node resolves that through the symlink: Homebrew's
|
|
74
|
+
`/opt/homebrew/bin/node` becomes `/opt/homebrew/Cellar/node/<ver>/bin/node`, so `../lib` points
|
|
75
|
+
inside the Cellar while npm sits at `/opt/homebrew/lib/node_modules/npm`. It then THREW. Measured
|
|
76
|
+
when the evidence runner went red on nine assertions while the same suite was green under `npx`,
|
|
77
|
+
which sets `npm_execpath`. The search now also looks beside the node that is on PATH — the place
|
|
78
|
+
the symlink came from, which also covers nvm, fnm, volta and distribution packages — and when
|
|
79
|
+
nothing is found it degrades to the platform `npm` instead of dying. Running npm is the goal; the
|
|
80
|
+
JS entry point is only how the Windows shell is avoided, and `execFile` runs a `.cmd` without one.
|
|
81
|
+
- **`doctor` called a protected global prefix writable** (REQ-584). The check asked
|
|
82
|
+
`fs.accessSync(W_OK)`, which on Windows reads POSIX mode bits and never the ACL: on a non-admin
|
|
83
|
+
account with prefix `C:\Program Files\nodejs` it answered "writable" while `mkdir` died EPERM —
|
|
84
|
+
the very error the user then met in `npm install -g`. The probe now TRIES: it creates and removes
|
|
85
|
+
a directory in the deepest existing ancestor (the same syscall npm fails on). Measured on that
|
|
86
|
+
machine: `WARN` with the user-space prefix prescription; `%APPDATA%\npm` still `PASS`. A probe
|
|
87
|
+
entry that cannot be removed is named, never hidden. POSIX verdicts are pinned unchanged.
|
|
88
|
+
- **The antigravity 8.3 conversion had never fired** (REQ-586). `shortPathIfSpaced` handed cmd a
|
|
89
|
+
`for %I in ("<path>") do @echo %~sI` line through Node's default win32 quoting, which rewrites the
|
|
90
|
+
inner quotes as `\"`; cmd's `for` then answered `C:\"C:\Users\…\"` — spaced and quoted — and the
|
|
91
|
+
function fell back to the spaced path. `init --agent antigravity` on an install under
|
|
92
|
+
`C:\Users\<name with a space>\…` therefore wrote an unquoted spaced command agy's launcher can never
|
|
93
|
+
run, and printed "the gate is enforced". The line now goes to cmd verbatim
|
|
94
|
+
(`windowsVerbatimArguments`), the win32 test asserts a REAL conversion on a spaced temp path, and
|
|
95
|
+
when a space still survives (a volume without 8.3 names) init says so instead of claiming
|
|
96
|
+
enforcement. Its sibling: `doctor` then rejected the correct 8.3 wiring as "not this install",
|
|
97
|
+
because `fs.realpathSync` does not expand short names — `realpathSync.native` does, and all three
|
|
98
|
+
wiring checks that share `resolvesToPackage` now use it (A-SPEC-586.2). Measured end to end on
|
|
99
|
+
the spaced install: hook written as `HOLMES~1.JS`, hook runs, `antigravity wiring` PASS.
|
|
100
|
+
- **The Windows `doctor-wiring-handshake` red had a third cause, and doctor hid it** (REQ-585).
|
|
101
|
+
Not the writer (0.19.1's note — `tomlStr` escapes correctly) and not cwd (0.19.2's hypothesis —
|
|
102
|
+
`doctor` now answers the same from `C:\` and the repo root): the test fixture wrote a Windows path
|
|
103
|
+
with raw backslashes into a TOML basic string, and the reader folded every unknown escape to its
|
|
104
|
+
bare letter, so `C:\ai_project\…` became `C:ai_project…` and doctor judged a path nobody wrote. A
|
|
105
|
+
hand-written `config.toml` does the same, and Codex refuses such a file outright. The reader now
|
|
106
|
+
accepts only the escapes TOML defines (`\b \t \n \f \r \" \\ \uXXXX \UXXXXXXXX`), an invalid one
|
|
107
|
+
makes the entry unreadable, and `codex wiring` names the escape and its line with the two valid
|
|
108
|
+
spellings. The fixture goes through `codexMcpBlock`, the same serializer init uses. The suite is
|
|
109
|
+
green on Windows for the first time.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
- **Autonomous approval now covers a low-risk slice's REQ and H-SPEC** (REQ-587). With
|
|
113
|
+
`HOLMES_AUTONOMOUS_APPROVAL` on, upstream documents were graded `hitl` by kind alone, so every
|
|
114
|
+
slice cost the operator two terminal sittings (`holmes-kit approve` for the REQ, again for the
|
|
115
|
+
H-SPEC) — six for the three fixes above. ADR-009's line is "self-approve by default, non-bypassable
|
|
116
|
+
HITL for high risk, risk decided deterministically", and a REQ or H-SPEC carries no code of its
|
|
117
|
+
own: its risk is the risk its A-SPECs declare. `specApprovalAutonomy` now takes the downstream
|
|
118
|
+
A-SPECs and derives the upstream grade — all auto → auto; any hitl (gate behaviour, a
|
|
119
|
+
governance/guardrail/hook/taint path), or no A-SPEC yet, → human. C-SPEC and ADR stay human
|
|
120
|
+
regardless. The ledger line names the A-SPECs the grade was derived from. Autonomy off is
|
|
121
|
+
byte-identical.
|
|
122
|
+
- **`spec_slice_approve` is idempotent** (A-SPEC-587.2). Called again after the REQ was sealed it
|
|
123
|
+
requested a RE-SEAL of that REQ (same request id back in the queue) and stopped there, so the
|
|
124
|
+
H-SPEC was never attempted. Sealed specs are now skipped and reported under `alreadySealed`; the
|
|
125
|
+
chain resumes at the first unsealed spec.
|
|
126
|
+
|
|
127
|
+
### Measured, no change needed
|
|
128
|
+
- **codex hook commands on a spaced path** (the plan's prediction A/B): codex runs a command hook as
|
|
129
|
+
`%COMSPEC% /C "<command_line>"` (codex-rs `hooks/src/engine/command_runner.rs`; the 0.153.4
|
|
130
|
+
binary carries the same runner), a quote-respecting shell. The quoted `node "<path>"` form
|
|
131
|
+
succeeded 3/3 on a spaced install. `kernelCommand` is unchanged.
|
|
132
|
+
- **npm 12 third-party install on Windows**: install scripts blocked → `doctor` diagnosed
|
|
133
|
+
`scripts-blocked` with the `.cmd` recovery line → pasted verbatim, the binding was built and
|
|
134
|
+
`doctor` went to 12 pass / 0 fail. Three-harness wiring from a foreign cwd: 3/3 handshakes.
|
|
135
|
+
|
|
136
|
+
### Windows baseline, classified
|
|
137
|
+
A clean `main` on this machine fails 49 of 416 suites in isolation — none are regressions from this
|
|
138
|
+
change. Sixteen were `Cannot find module 'web-tree-sitter'` (a dependency added by a recent pull and
|
|
139
|
+
not yet installed; `npm install` turns 17 of the 18 green). The rest are environment: spawn
|
|
140
|
+
timeouts in doctor tests, jest-worker "circular structure" crashes (`approve`, `mcp-launcher`,
|
|
141
|
+
`doctor-mcp-wiring-spawn`), `chmod`-based negatives that cannot fail on NTFS, git-history corpora,
|
|
142
|
+
CRLF checkouts compared byte-for-byte, and SQLite "file is not a database" on a corrupt-store
|
|
143
|
+
negative. The list with first-failure lines lives in `docs/goals/GOAL-windows-hardening-2026-09-10.md`.
|
|
144
|
+
|
|
145
|
+
## [0.19.3] - 2026-09-10
|
|
146
|
+
|
|
147
|
+
`doctor` told every third-party install its SQLite binding was missing while it sat on disk. No API change.
|
|
148
|
+
|
|
149
|
+
### Fixed
|
|
150
|
+
- **A false FAIL on the default install layout** (REQ-583). `gatherNativeEvidence` located
|
|
151
|
+
`better-sqlite3` by joining `packageRoot/node_modules/better-sqlite3`, and npm HOISTS
|
|
152
|
+
dependencies to the project root — so for a normal install at
|
|
153
|
+
`<proj>/node_modules/@holmes-lab/holmes-kit` the joined path never existed. Measured against the
|
|
154
|
+
published 0.19.2: the binding was present, `require('better-sqlite3')` executed a `:memory:`
|
|
155
|
+
query, and `doctor` still reported `no binary`. Hoisting is npm's default, which made this the
|
|
156
|
+
default experience of a correct install. The package directory is now RESOLVED
|
|
157
|
+
(`require.resolve('<name>/package.json', { paths: [root] })`) — Node's own rule, so hoisted,
|
|
158
|
+
nested and linked layouts all answer correctly, and a genuinely absent dependency still fails.
|
|
159
|
+
Its sibling — the `node version` check reading `engines.node` — carried the same join and moved
|
|
160
|
+
with it; it had been reporting `engines.node=(unknown)` for the same reason.
|
|
161
|
+
- **The release gate ran the suite in the operator's environment** (REQ-582). `npm publish`
|
|
162
|
+
refused a green tree: `verify-release.js` called `npm test` with the shell's `HOLMES_APPROVAL`,
|
|
163
|
+
which the spawned-hook parity test (A-SPEC-338) legitimately reacts to — the spawned child
|
|
164
|
+
inherits the token while its in-process twin is handed `{approval: undefined}`. The gate now runs
|
|
165
|
+
under `cleanTestEnv`, the same scrub `test_run` uses (borrowed, never copied), and refuses to
|
|
166
|
+
proceed unscrubbed if it cannot load it. It also names the failing tests instead of saying only
|
|
167
|
+
"red" — finding out which test had failed previously cost two extra full-suite runs.
|
|
168
|
+
|
|
169
|
+
- **A live session could block its own publish** (REQ-583). The `APPEND_ONLY_GOVERNANCE` list that
|
|
170
|
+
tells the release gate which dirty files are governance churn rather than unshipped code held
|
|
171
|
+
only the approval queue and provenance — while REQ-564/566/569/578 added `session-context`,
|
|
172
|
+
`impact-advisories`, `anchor-density` and `cycle-observations`, and `test-outcomes` was promoted
|
|
173
|
+
to tracking. Measured: `npm publish` refused because one `session-context` line was appended
|
|
174
|
+
between two `git status` snapshots inside the tree-untouched assertion, and the same list guards
|
|
175
|
+
the dirty-tree check a user meets first. The list now follows the ledgers, and a test pins that
|
|
176
|
+
nothing which ships (`src/`, `dist/`, the manifest, specs) is ever excused.
|
|
177
|
+
|
|
178
|
+
### Verified against the published package
|
|
179
|
+
- Installed `@holmes-lab/holmes-kit@0.19.2` from the registry into a clean project, applied this
|
|
180
|
+
fix, and re-diagnosed from an unrelated working directory: **0 FAIL**, three-harness handshakes
|
|
181
|
+
(claude / codex / antigravity) all PASS on the version-pinned npx wiring, `target wiring — 3
|
|
182
|
+
holmes hook(s) resolve to this install`, and `better-sqlite3 — loads and executes against
|
|
183
|
+
:memory:`.
|
|
184
|
+
|
|
8
185
|
## [0.19.2] - 2026-09-10
|
|
9
186
|
|
|
10
187
|
Wiring diagnosis no longer depends on where you run it. No API change.
|
package/README.md
CHANGED
|
@@ -148,12 +148,27 @@ Out of the box Holmes-Kit runs tier **`none`** — lexical + citation + graph se
|
|
|
148
148
|
egress**. Two opt-in tiers raise recall on requests your vocabulary can't reach (measured on 305
|
|
149
149
|
traceability cases — see the feature list above):
|
|
150
150
|
|
|
151
|
-
**Tier `local` — no egress, no account.**
|
|
152
|
-
|
|
151
|
+
**Tier `local` — no egress, no account.** The embedding runtime ships with holmes-kit as of
|
|
152
|
+
0.19.4, so nothing extra to install. Preparing the model is **opt-in**: neither installation nor
|
|
153
|
+
`init` downloads it, because a large download nobody asked for is not a default. One command:
|
|
153
154
|
```bash
|
|
154
|
-
|
|
155
|
-
npx holmes-kit
|
|
155
|
+
npx holmes-kit semantic-setup # downloads the public bge-m3 assets once, shared across projects
|
|
156
|
+
npx holmes-kit semantic-check # verifies OFFLINE inference — a real 1024-dim normalized vector
|
|
157
|
+
npx holmes-kit doctor # → semantic tier: local (no egress)
|
|
156
158
|
```
|
|
159
|
+
Until it is prepared, `doctor` reports tier **`none`** and names what that costs — requests lexical
|
|
160
|
+
search misses (measured 16.4%) have 0% recall — together with the command that changes it. A runtime
|
|
161
|
+
that resolves is not a tier that answers.
|
|
162
|
+
|
|
163
|
+
To prepare during installation instead, set `HOLMES_AUTO_MODEL_INSTALL=1` before installing.
|
|
164
|
+
`HOLMES_SKIP_MODEL_INSTALL=1` refuses preparation and **outranks** the opt-in, so an explicit refusal
|
|
165
|
+
is never overridden. Models live in `~/.holmes/models` (`%USERPROFILE%\.holmes\models` on Windows);
|
|
166
|
+
`HOLMES_MODEL_CACHE` overrides that, including for a prepopulated offline cache.
|
|
167
|
+
|
|
168
|
+
> On **ARM Linux**, install with Node 20 or 22: `tree-sitter@0.21.1` ships no `linux-arm64` prebuild,
|
|
169
|
+
> so it compiles from source, and that compile fails against Node 24 headers. The failure happens
|
|
170
|
+
> while npm is still installing dependencies — before Holmes-Kit exists — so `doctor` cannot diagnose
|
|
171
|
+
> it. On x64 the prebuild is used and Node 24 is fine.
|
|
157
172
|
|
|
158
173
|
**Tier `cloud` — highest recall, explicit consent (`gemini-embedding-001`).** Setting a key IS
|
|
159
174
|
the consent act: with a key present, repository-derived text is sent to Google's embedding API.
|
|
@@ -213,6 +228,9 @@ flowchart LR
|
|
|
213
228
|
| `holmes-kit autonomy status` | Show the current autonomous-approval posture (project default + session envelope) |
|
|
214
229
|
| `holmes-kit autonomy on --for 2h` | Self-drive low-risk specs for this session only (expiring; needs a TTY or `HOLMES_APPROVAL`) |
|
|
215
230
|
| `holmes-kit autonomy off` | End session autonomy immediately |
|
|
231
|
+
| `holmes-kit semantic-setup` | Download and verify the local `bge-m3` model (opt-in; shared across projects) |
|
|
232
|
+
| `holmes-kit semantic-check` | Verify local inference **offline**, without downloading anything |
|
|
233
|
+
| `holmes-kit semantic-key set` | Store the cloud-tier key outside the tree (stdin only, never argv) |
|
|
216
234
|
| `holmes-kit doctor` | Comprehensive health check of specs, hooks, MCP, and anchors |
|
|
217
235
|
| `holmes-kit doctor --fix` | Automatically self-heal and repair broken hooks or missing skills |
|
|
218
236
|
| `holmes-kit ci` | Run non-interactive headless governance gate for GitHub Actions / GitLab CI |
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
bb0f4497-mtwbj9ia
|
|
@@ -62,6 +62,14 @@ export declare function antigravityHookCommand(scriptPath: string): string;
|
|
|
62
62
|
/** win32-only, best-effort: a spaced path becomes its 8.3 short name (no spaces); anything else — or
|
|
63
63
|
* any lookup failure — returns the input unchanged. */
|
|
64
64
|
export declare function shortPathIfSpaced(p: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* @implements A-SPEC-586
|
|
67
|
+
* The agy launcher splits its single command string on whitespace, so a hook path that still carries
|
|
68
|
+
* a space after the 8.3 attempt is a hook that never runs. One line per such hook — init prints
|
|
69
|
+
* these INSTEAD of "the gate is enforced". Empty when every path is space-free (posix without a
|
|
70
|
+
* space, or win32 where 8.3 resolved it).
|
|
71
|
+
*/
|
|
72
|
+
export declare function antigravityHookWarnings(packageRoot: string): string[];
|
|
65
73
|
export declare const MANAGED_BEGIN = "<!-- holmes-kit:managed:begin -->";
|
|
66
74
|
export declare const MANAGED_END = "<!-- holmes-kit:managed:end -->";
|
|
67
75
|
/**
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.MANAGED_END = exports.MANAGED_BEGIN = exports.CODEX_MARKETPLACE = exports.CODEX_PLUGIN_DIR = exports.HARNESS_ENFORCES = exports.AGENTS = void 0;
|
|
37
37
|
exports.antigravityHookCommand = antigravityHookCommand;
|
|
38
38
|
exports.shortPathIfSpaced = shortPathIfSpaced;
|
|
39
|
+
exports.antigravityHookWarnings = antigravityHookWarnings;
|
|
39
40
|
exports.mergeAgentsMd = mergeAgentsMd;
|
|
40
41
|
exports.agentFiles = agentFiles;
|
|
41
42
|
exports.agentLinks = agentLinks;
|
|
@@ -132,13 +133,36 @@ function shortPathIfSpaced(p) {
|
|
|
132
133
|
if (process.platform !== 'win32' || !p.includes(' '))
|
|
133
134
|
return p;
|
|
134
135
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
// @implements A-SPEC-586 — VERBATIM. Node's default win32 quoting rewrites the inner `"` as `\"`
|
|
137
|
+
// (CRT rules), but cmd.exe is not a CRT program: its `for` then sees `(\"C:\…\")` and `%~sI`
|
|
138
|
+
// answers `C:\"C:\Users\…\"` — spaced and quoted, so the check below rejected it and the
|
|
139
|
+
// conversion had NEVER fired (measured 2026-09-10). Handing the line to cmd untouched yields
|
|
140
|
+
// the 8.3 form on the same path.
|
|
141
|
+
const r = (0, node_child_process_1.spawnSync)('cmd', ['/d', '/c', `for %I in ("${p}") do @echo %~sI`], { encoding: 'utf8', windowsVerbatimArguments: true });
|
|
142
|
+
const out = r.status === 0 && typeof r.stdout === 'string' ? r.stdout.trim() : '';
|
|
143
|
+
return out !== '' && !out.includes(' ') && !out.includes('"') ? out : p;
|
|
137
144
|
}
|
|
138
145
|
catch {
|
|
139
146
|
return p;
|
|
140
147
|
}
|
|
141
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* @implements A-SPEC-586
|
|
151
|
+
* The agy launcher splits its single command string on whitespace, so a hook path that still carries
|
|
152
|
+
* a space after the 8.3 attempt is a hook that never runs. One line per such hook — init prints
|
|
153
|
+
* these INSTEAD of "the gate is enforced". Empty when every path is space-free (posix without a
|
|
154
|
+
* space, or win32 where 8.3 resolved it).
|
|
155
|
+
*/
|
|
156
|
+
function antigravityHookWarnings(packageRoot) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const script of ['holmes-hook-antigravity.js', 'holmes-stop-antigravity.js']) {
|
|
159
|
+
const final = shortPathIfSpaced(path.join(packageRoot, 'bin', script));
|
|
160
|
+
if (!/\s/.test(final))
|
|
161
|
+
continue;
|
|
162
|
+
out.push(`antigravity: the hook path still contains a space (${final.replace(/\\/g, '/')}) — agy's launcher splits the command on whitespace, so this hook never runs and the gate is NOT enforced. Install holmes-kit in a path without spaces${process.platform === 'win32' ? ' (or enable NTFS 8.3 names on this volume)' : ''}.`);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
142
166
|
const antigravityHookEntry = (packageRoot, script) => ({
|
|
143
167
|
type: 'command',
|
|
144
168
|
command: antigravityHookCommand(shortPathIfSpaced(path.join(packageRoot, 'bin', script))),
|
|
@@ -24,3 +24,13 @@ export declare function readCodexHolmesEntry(raw: string): {
|
|
|
24
24
|
command: string;
|
|
25
25
|
args: string[];
|
|
26
26
|
} | null;
|
|
27
|
+
/**
|
|
28
|
+
* @implements A-SPEC-585
|
|
29
|
+
* The first invalid basic-string escape inside OUR table's `command =` / `args =` lines, with its
|
|
30
|
+
* 1-based line — or null when every escape there is one TOML defines. Doctor turns this into the
|
|
31
|
+
* diagnosis Codex itself would give (it refuses the file), instead of judging a mangled path.
|
|
32
|
+
*/
|
|
33
|
+
export declare function invalidTomlEscape(raw: string): {
|
|
34
|
+
escape: string;
|
|
35
|
+
line: number;
|
|
36
|
+
} | null;
|
|
@@ -5,6 +5,7 @@ exports.codexMcpBlock = codexMcpBlock;
|
|
|
5
5
|
exports.mergeCodexToml = mergeCodexToml;
|
|
6
6
|
exports.removeCodexToml = removeCodexToml;
|
|
7
7
|
exports.readCodexHolmesEntry = readCodexHolmesEntry;
|
|
8
|
+
exports.invalidTomlEscape = invalidTomlEscape;
|
|
8
9
|
/** The one table holmes-kit owns in a Codex config.toml. */
|
|
9
10
|
exports.CODEX_TABLE = 'mcp_servers.holmes-kit';
|
|
10
11
|
const HEADER = `[${exports.CODEX_TABLE}]`;
|
|
@@ -251,24 +252,87 @@ function readCodexHolmesEntry(raw) {
|
|
|
251
252
|
return null;
|
|
252
253
|
let command = null;
|
|
253
254
|
let args = null;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
try {
|
|
256
|
+
for (let i = region.start + 1; i < region.end; i++) {
|
|
257
|
+
const cmd = lines[i].match(/^\s*command\s*=\s*"((?:[^"\\]|\\.)*)"\s*(#.*)?$/);
|
|
258
|
+
if (cmd)
|
|
259
|
+
command = unescapeToml(cmd[1]);
|
|
260
|
+
const arr = lines[i].match(/^\s*args\s*=\s*\[(.*)\]\s*(#.*)?$/);
|
|
261
|
+
if (arr)
|
|
262
|
+
args = parseTomlStringArray(arr[1]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
if (e instanceof InvalidTomlEscape)
|
|
267
|
+
return null; // @implements A-SPEC-585 — unreadable, not mangled
|
|
268
|
+
throw e;
|
|
261
269
|
}
|
|
262
270
|
if (command === null || args === null)
|
|
263
271
|
return null;
|
|
264
272
|
return { command, args };
|
|
265
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* @implements A-SPEC-585
|
|
276
|
+
* The first invalid basic-string escape inside OUR table's `command =` / `args =` lines, with its
|
|
277
|
+
* 1-based line — or null when every escape there is one TOML defines. Doctor turns this into the
|
|
278
|
+
* diagnosis Codex itself would give (it refuses the file), instead of judging a mangled path.
|
|
279
|
+
*/
|
|
280
|
+
function invalidTomlEscape(raw) {
|
|
281
|
+
const lines = raw.split('\n');
|
|
282
|
+
const region = ourRegion(lines);
|
|
283
|
+
if (!region)
|
|
284
|
+
return null;
|
|
285
|
+
for (let i = region.start + 1; i < region.end; i++) {
|
|
286
|
+
if (!/^\s*(command|args)\s*=/.test(lines[i]))
|
|
287
|
+
continue;
|
|
288
|
+
const re = /"((?:[^"\\]|\\.)*)"/g;
|
|
289
|
+
let mm;
|
|
290
|
+
while ((mm = re.exec(lines[i])) !== null) {
|
|
291
|
+
const bad = firstInvalidEscape(mm[1]);
|
|
292
|
+
if (bad !== null)
|
|
293
|
+
return { escape: bad, line: i + 1 };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
class InvalidTomlEscape extends Error {
|
|
299
|
+
escape;
|
|
300
|
+
constructor(escape) {
|
|
301
|
+
super(`invalid TOML escape ${escape}`);
|
|
302
|
+
this.escape = escape;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const SIMPLE_ESCAPES = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' };
|
|
306
|
+
/** The offending escape (`\a`, or a `\u`/`\U` with too few hex digits), or null when all are valid. */
|
|
307
|
+
function firstInvalidEscape(s) {
|
|
308
|
+
for (let i = 0; i < s.length; i++) {
|
|
309
|
+
if (s[i] !== '\\')
|
|
310
|
+
continue;
|
|
311
|
+
const e = s[i + 1] ?? '';
|
|
312
|
+
if (e in SIMPLE_ESCAPES) {
|
|
313
|
+
i += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const width = e === 'u' ? 4 : e === 'U' ? 8 : 0;
|
|
317
|
+
if (width > 0 && new RegExp(`^[0-9a-fA-F]{${width}}$`).test(s.slice(i + 2, i + 2 + width))) {
|
|
318
|
+
i += 1 + width;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
return `\\${e}`;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
// @implements A-SPEC-585 — strict: only the escapes TOML defines. Anything else THROWS, so the
|
|
326
|
+
// caller answers "unreadable" — an unknown escape used to fold to its bare letter and turned
|
|
327
|
+
// `C:\ai_project\…` into `C:ai_project…` (measured on every Windows checkout).
|
|
266
328
|
function unescapeToml(s) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
329
|
+
const bad = firstInvalidEscape(s);
|
|
330
|
+
if (bad !== null)
|
|
331
|
+
throw new InvalidTomlEscape(bad);
|
|
332
|
+
return s.replace(/\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|.)/g, (_, e) => {
|
|
333
|
+
if (e[0] === 'u' || e[0] === 'U')
|
|
334
|
+
return String.fromCodePoint(parseInt(e.slice(1), 16));
|
|
335
|
+
return SIMPLE_ESCAPES[e];
|
|
272
336
|
});
|
|
273
337
|
}
|
|
274
338
|
/** Parse a TOML inline array of basic strings: `"a", "b"` → ['a','b']. Non-conforming → []. */
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { NativeEvidence } from './native-deps';
|
|
2
2
|
import { type SemanticTier } from '../semantic/tier';
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a script path belongs to the packageRoot, supporting symlinked global installs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolvesToPackage(filePath: string, packageRoot: string): boolean;
|
|
3
7
|
/**
|
|
4
8
|
* `holmes-kit doctor` — install diagnostics.
|
|
5
9
|
*
|
|
@@ -62,7 +66,22 @@ export interface PrefixProbe {
|
|
|
62
66
|
writable?: boolean;
|
|
63
67
|
platform?: string;
|
|
64
68
|
unreadable?: string;
|
|
69
|
+
/** @implements A-SPEC-584 — a probe entry the caller created but could not remove. */
|
|
70
|
+
leftover?: string;
|
|
65
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* @implements A-SPEC-584
|
|
74
|
+
* Is `dir` writable by THIS account? Asked by TRYING, not by `fs.accessSync(W_OK)`: on Windows the
|
|
75
|
+
* mode bits accessSync consults do not carry the ACL, so it answered "writable" for
|
|
76
|
+
* `C:\Program Files\nodejs` on a non-admin account while `mkdir` died EPERM (measured 2026-09-10).
|
|
77
|
+
* A mkdir is the very syscall npm's own EPERM comes from (creating the scope directory), so the probe
|
|
78
|
+
* makes one and removes it. A probe entry that could not be removed is reported, never hidden.
|
|
79
|
+
* Nothing thrown; a missing or non-directory `dir` is simply not writable.
|
|
80
|
+
*/
|
|
81
|
+
export declare function probeWritable(dir: string): {
|
|
82
|
+
writable: boolean;
|
|
83
|
+
leftover?: string;
|
|
84
|
+
};
|
|
66
85
|
/**
|
|
67
86
|
* Judge the global prefix. Pure — the probing lives with the caller.
|
|
68
87
|
*
|
|
@@ -104,6 +123,21 @@ export declare function grammarProbe(packageRoot: string): {
|
|
|
104
123
|
* local dependency. A global or npx install has no project package.json that npm consults, so its
|
|
105
124
|
* coverage is `uncovered` by construction and the remedy is the per-command `--allow-scripts`.
|
|
106
125
|
*/
|
|
126
|
+
/**
|
|
127
|
+
* Where an installed dependency actually lives, asked of NODE rather than guessed from a path.
|
|
128
|
+
*
|
|
129
|
+
* 0.19.2 shipped a false FAIL to every third-party consumer: it built the directory by joining
|
|
130
|
+
* `packageRoot/node_modules/<name>`, and npm HOISTS dependencies to the project root, so for an
|
|
131
|
+
* install at `<proj>/node_modules/@holmes-lab/holmes-kit` the joined path never existed — while the
|
|
132
|
+
* binding sat at `<proj>/node_modules/better-sqlite3` and loaded fine. Hoisting is npm's default,
|
|
133
|
+
* which made the false verdict the default path of a normal install.
|
|
134
|
+
*
|
|
135
|
+
* `package.json` is the resolution target because we want the DIRECTORY, not the entry point: a
|
|
136
|
+
* package whose `exports` hides its main still resolves its own manifest by convention. A failure
|
|
137
|
+
* to resolve returns null, and the caller reports "absent" exactly as before — the repair changes
|
|
138
|
+
* how we look, never whether a real miss is reported.
|
|
139
|
+
*/
|
|
140
|
+
export declare function resolveDepDir(fromRoot: string, name: string): string | null;
|
|
107
141
|
export declare function gatherNativeEvidence(packageRoot: string): NativeEvidence;
|
|
108
142
|
/** The parent environment minus the variables that legitimately change a gate decision. Pure. */
|
|
109
143
|
export declare function probeEnv(parent: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|