@holmes-lab/holmes-kit 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/cli/agents.d.ts +22 -0
  4. package/dist/holmes/cli/agents.js +76 -1
  5. package/dist/holmes/cli/approve.js +6 -1
  6. package/dist/holmes/cli/doctor.d.ts +36 -1
  7. package/dist/holmes/cli/doctor.js +182 -35
  8. package/dist/holmes/cli/index.js +7 -1
  9. package/dist/holmes/cli/init.js +12 -0
  10. package/dist/holmes/cli/native-deps.d.ts +65 -0
  11. package/dist/holmes/cli/native-deps.js +131 -0
  12. package/dist/holmes/cpg/cycle-observation.d.ts +65 -0
  13. package/dist/holmes/cpg/cycle-observation.js +146 -0
  14. package/dist/holmes/governance/approval-queue.d.ts +23 -4
  15. package/dist/holmes/governance/approval-queue.js +44 -6
  16. package/dist/holmes/hooks/stop.d.ts +15 -0
  17. package/dist/holmes/hooks/stop.js +46 -3
  18. package/dist/holmes/mcp/handlers.d.ts +2 -0
  19. package/dist/holmes/mcp/handlers.js +29 -2
  20. package/dist/holmes/mcp/maintenance-analyze.d.ts +37 -0
  21. package/dist/holmes/mcp/maintenance-analyze.js +73 -1
  22. package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
  23. package/dist/holmes/mcp/maintenance-evidence.js +71 -4
  24. package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
  25. package/dist/holmes/project/install-scripts-policy.js +131 -0
  26. package/dist/holmes/project/npx-bin.d.ts +6 -0
  27. package/dist/holmes/project/npx-bin.js +10 -0
  28. package/dist/holmes/review/failed-test-names.d.ts +19 -0
  29. package/dist/holmes/review/failed-test-names.js +43 -0
  30. package/dist/holmes/review/run-replay.d.ts +23 -0
  31. package/dist/holmes/review/run-replay.js +30 -0
  32. package/dist/holmes/review/test-runner.d.ts +27 -0
  33. package/dist/holmes/review/test-runner.js +59 -3
  34. package/docs/install-guide.md +54 -5
  35. package/package.json +4 -1
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // @implements A-SPEC-582.1
3
+ /**
4
+ * The names behind a red release gate.
5
+ *
6
+ * Measured 2026-09-10: `npm publish` refused with "스위트가 붉습니다" and nothing else, and finding
7
+ * out which test had failed cost two more full-suite runs. `test_run` learned to name its failures
8
+ * in REQ-577; the release gate had not. A gate that says "red" without saying what is a gate that
9
+ * makes the next person guess.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.FAILED_NAME_CAP = void 0;
13
+ exports.failedTestNames = failedTestNames;
14
+ /** How many names a refusal lists before it counts the rest. */
15
+ exports.FAILED_NAME_CAP = 10;
16
+ /** Jest's summary marks each failure with `● <suite> › <test>`. */
17
+ const SUMMARY_LINE = /^\s*●\s+(.+?)\s*$/;
18
+ /**
19
+ * Pull the failing test names out of a jest run's output.
20
+ *
21
+ * PURE, and forgiving: output it cannot read yields an empty list so the caller keeps its existing
22
+ * refusal rather than replacing a working message with an empty one. Jest prints the summary block
23
+ * twice on some configurations, so names are de-duplicated — the same failure listed twice is one
24
+ * failure.
25
+ */
26
+ function failedTestNames(output) {
27
+ const seen = [];
28
+ for (const line of String(output ?? '').split('\n')) {
29
+ const m = SUMMARY_LINE.exec(line);
30
+ if (m === null)
31
+ continue;
32
+ const name = m[1];
33
+ // A `●` line that is not a test name (jest uses the bullet for Console blocks too).
34
+ if (name === '' || name === 'Console' || !name.includes('›'))
35
+ continue;
36
+ if (!seen.includes(name))
37
+ seen.push(name);
38
+ }
39
+ if (seen.length <= exports.FAILED_NAME_CAP)
40
+ return seen;
41
+ const rest = seen.length - exports.FAILED_NAME_CAP;
42
+ return [...seen.slice(0, exports.FAILED_NAME_CAP), `… and ${rest} more`];
43
+ }
@@ -1,6 +1,23 @@
1
1
  import { evaluationMetrics, impactMetrics, ceilingMetrics } from './evaluation-metrics';
2
2
  import { type ReplayCorpus } from './replay-corpus';
3
3
  import { type PprArmConfig } from '../assoc/assoc-arm';
4
+ /** One case, scored by both arms. Measurement only — never read by a pin. */
5
+ export interface PairedRow {
6
+ commit: string;
7
+ uncited: boolean;
8
+ productRecall10: number;
9
+ baselineRecall10: number;
10
+ truth: number;
11
+ }
12
+ /**
13
+ * @implements A-SPEC-578.7
14
+ * The paired differences on the UNCITED axis, ready for `pairedPower`.
15
+ *
16
+ * Filtering here rather than while measuring is deliberate: observation records every case, and the
17
+ * axis is chosen at analysis time. A harness that only recorded what it currently cares about could
18
+ * never answer a question asked later.
19
+ */
20
+ export declare function uncitedDiffs(rows: readonly PairedRow[]): number[];
4
21
  export interface ReplayResult {
5
22
  corpus: string;
6
23
  cases: number;
@@ -231,6 +248,12 @@ export declare function runReplay(corpus: ReplayCorpus, limit: number, opts?: {
231
248
  /** @implements A-SPEC-567.2 — present only when judgementBundle was asked. */
232
249
  judgementBundle?: import('./judgement-bundle').JudgementBundle;
233
250
  }) => void;
251
+ /**
252
+ * @implements A-SPEC-578.7 — the power arm: one row per case, both arms scored, so the caller
253
+ * can compute an MDE with `pairedPower`. Same idiom as `caseDump` — a callback, never a change
254
+ * to `ReplayResult`, so a pin cannot move because a measurement was asked for.
255
+ */
256
+ pairedDump?: (row: PairedRow) => void;
234
257
  /** @implements A-SPEC-487 — 2-pass semantic injection for the dump only, never the pins. */
235
258
  productSemantic?: {
236
259
  embedBatch: (texts: string[], kind: 'query' | 'doc') => Promise<number[][]>;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.uncitedDiffs = uncitedDiffs;
36
37
  exports.runReplay = runReplay;
37
38
  exports.semanticCaseRanking = semanticCaseRanking;
38
39
  // @implements A-SPEC-346
@@ -41,6 +42,7 @@ exports.semanticCaseRanking = semanticCaseRanking;
41
42
  // @implements A-SPEC-349
42
43
  // @implements A-SPEC-378
43
44
  // @implements A-SPEC-402
45
+ const baseline_arm_1 = require("./baseline-arm");
44
46
  const fs = __importStar(require("node:fs"));
45
47
  const node_child_process_1 = require("node:child_process");
46
48
  const os = __importStar(require("node:os"));
@@ -79,6 +81,17 @@ const dense_retrieval_1 = require("./dense-retrieval");
79
81
  const explore_1 = require("../assoc/explore");
80
82
  const temporal_prior_1 = require("./temporal-prior");
81
83
  const commit_text_1 = require("./commit-text");
84
+ /**
85
+ * @implements A-SPEC-578.7
86
+ * The paired differences on the UNCITED axis, ready for `pairedPower`.
87
+ *
88
+ * Filtering here rather than while measuring is deliberate: observation records every case, and the
89
+ * axis is chosen at analysis time. A harness that only recorded what it currently cares about could
90
+ * never answer a question asked later.
91
+ */
92
+ function uncitedDiffs(rows) {
93
+ return rows.filter((r) => r.uncited).map((r) => r.productRecall10 - r.baselineRecall10);
94
+ }
82
95
  /**
83
96
  * Run the point-in-time replay against ANY corpus.
84
97
  *
@@ -258,6 +271,23 @@ async function runReplay(corpus, limit, opts = {}) {
258
271
  return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
259
272
  })();
260
273
  const ranked = result.candidates.map((x) => x.file);
274
+ // @implements A-SPEC-578.7 — the power arm. Both recalls come from what already exists:
275
+ // the product's from `ranked` (never recomputed — recomputing is how a benchmark ends up
276
+ // scoring a pipeline the product does not ship, measured once already in A-SPEC-573.3),
277
+ // the baseline's from `rankBaseline`, the no-graph floor built in A-SPEC-356 and never
278
+ // called until now. The `uncited` predicate is the PRODUCT'S — `citationsIn` — so the axis
279
+ // is defined the same way here and there.
280
+ if (opts.pairedDump !== undefined && c.files.length > 0) {
281
+ const truth = new Set(c.files);
282
+ const recallOf = (files) => files.slice(0, 10).filter((f) => truth.has(f)).length / truth.size;
283
+ opts.pairedDump({
284
+ commit: c.commit,
285
+ uncited: (0, localize_1.citationsIn)(c.subject, new Set(specs.map((sp) => sp.id))).cited.length === 0,
286
+ productRecall10: recallOf(ranked),
287
+ baselineRecall10: recallOf((0, baseline_arm_1.rankBaseline)(c.subject, scanned, 10).map((h) => h.file)),
288
+ truth: truth.size,
289
+ });
290
+ }
261
291
  // @implements A-SPEC-469 — the union scores what a caller actually RECEIVES as the impact
262
292
  // answer, and that surface is now the graded rankedImpact (the closure stays gate-facing).
263
293
  const impacted = (result.impacts?.rankedImpact ?? []).map((r) => r.file);
@@ -190,6 +190,18 @@ export declare function runGo(files: string[], mode: TestRunPlan['mode'], cwd: s
190
190
  * gate that ran part of its scope has verified less than it reports.
191
191
  */
192
192
  export declare function runTestScope(scope: TestScope, cwd: string): TestRunResult;
193
+ /**
194
+ * The last few lines of a runner's output, bounded by SIZE as well as by line count.
195
+ *
196
+ * A line budget is only a size budget while the lines are short, and jest `--json` breaks that
197
+ * assumption completely: it emits one line. Measured 2026-09-02 — a GREEN `test_run` returned
198
+ * 1,432,092 characters, of which this field was 1,421,327 (99.2%) across "6 lines" whose longest
199
+ * was 1,420,959. The usefulness was inverted: a red run gave six clean lines of stderr summary,
200
+ * and a green run gave the whole document.
201
+ *
202
+ * The END is what survives a cut. A runner's conclusion is always at the bottom.
203
+ */
204
+ export declare const TAIL_MAX_CHARS = 4000;
193
205
  export declare function tailOf(s: string, opts?: number | {
194
206
  lines?: number;
195
207
  maxChars?: number;
@@ -204,4 +216,19 @@ export declare function tailOf(s: string, opts?: number | {
204
216
  * Returns null on anything it cannot read, and the caller falls back to the truncated original:
205
217
  * a failed summary must not turn partial information into none.
206
218
  */
219
+ /**
220
+ * What failed, by name — the red counterpart to `summarizeJestJson`.
221
+ *
222
+ * Measured 2026-09-09: a red `test_run` reported "1 failed" three times running and never said
223
+ * which test. The failing file had to be recovered from the outcome ledger's A-SPEC list, because
224
+ * the red path's tail is the tail of a JSON DOCUMENT — bytes, not a name. Counts tell you a run
225
+ * went red; only a name tells you what to look at.
226
+ *
227
+ * The payload is located by the SAME rule `summarizeJestJson` uses (the first `{`), because a suite
228
+ * that logs anything prints it before the JSON — this repository's does — and two rules for one
229
+ * string is how the two quietly disagree.
230
+ *
231
+ * Only the FIRST line of a failure message: one failure, one look. The rest is in the file.
232
+ */
233
+ export declare function failedTestSummary(stdout: string, limit?: number): string | null;
207
234
  export declare function summarizeJestJson(stdout: string): string | null;
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TAIL_MAX_CHARS = void 0;
36
37
  exports.planTestRun = planTestRun;
37
38
  exports.ecosystemOf = ecosystemOf;
38
39
  exports.parseGoTestJson = parseGoTestJson;
@@ -49,6 +50,7 @@ exports.runDotnet = runDotnet;
49
50
  exports.runGo = runGo;
50
51
  exports.runTestScope = runTestScope;
51
52
  exports.tailOf = tailOf;
53
+ exports.failedTestSummary = failedTestSummary;
52
54
  exports.summarizeJestJson = summarizeJestJson;
53
55
  // @implements A-SPEC-102.1
54
56
  const node_child_process_1 = require("node:child_process");
@@ -304,7 +306,11 @@ function runJest(files, mode, cwd) {
304
306
  const err = e;
305
307
  // A failing suite still emits --json on stdout, so execution evidence — and the red/green outcome
306
308
  // classification (A-SPEC-534.1) — survives a red run.
307
- return { passed: false, tail: tailOf(`${err.stdout ?? ''}\n${err.stderr ?? err.message ?? ''}`), executed: parseExecutedCounts(err.stdout ?? '', cwd), outcomes: classifyJestOutcomes(err.stdout ?? '', cwd) };
309
+ // @implements A-SPEC-577.1 the names first, then jest's own summary from stderr. Without the
310
+ // first half this tail was the tail of a JSON document: it said how many failed and never which.
311
+ const named = failedTestSummary(err.stdout ?? '');
312
+ const tail = tailOf(`${named === null ? (err.stdout ?? '') : named}\n${err.stderr ?? err.message ?? ''}`, { lines: named === null ? 6 : 40 });
313
+ return { passed: false, tail, executed: parseExecutedCounts(err.stdout ?? '', cwd), outcomes: classifyJestOutcomes(err.stdout ?? '', cwd) };
308
314
  }
309
315
  }
310
316
  /**
@@ -703,11 +709,11 @@ function runTestScope(scope, cwd) {
703
709
  *
704
710
  * The END is what survives a cut. A runner's conclusion is always at the bottom.
705
711
  */
706
- const TAIL_MAX_CHARS = 4000;
712
+ exports.TAIL_MAX_CHARS = 4000;
707
713
  function tailOf(s, opts = {}) {
708
714
  // The numeric spelling is the one the go adapter uses (`tailOf(err.stderr, 2)`); keeping it means
709
715
  // this change cannot silently alter a caller that only ever wanted fewer lines.
710
- const { lines = 6, maxChars = TAIL_MAX_CHARS } = typeof opts === 'number' ? { lines: opts } : opts;
716
+ const { lines = 6, maxChars = exports.TAIL_MAX_CHARS } = typeof opts === 'number' ? { lines: opts } : opts;
711
717
  const picked = s.trim().split('\n').slice(-lines).join('\n');
712
718
  if (picked.length <= maxChars)
713
719
  return picked;
@@ -725,6 +731,56 @@ function tailOf(s, opts = {}) {
725
731
  * Returns null on anything it cannot read, and the caller falls back to the truncated original:
726
732
  * a failed summary must not turn partial information into none.
727
733
  */
734
+ // @implements A-SPEC-577.1
735
+ /**
736
+ * What failed, by name — the red counterpart to `summarizeJestJson`.
737
+ *
738
+ * Measured 2026-09-09: a red `test_run` reported "1 failed" three times running and never said
739
+ * which test. The failing file had to be recovered from the outcome ledger's A-SPEC list, because
740
+ * the red path's tail is the tail of a JSON DOCUMENT — bytes, not a name. Counts tell you a run
741
+ * went red; only a name tells you what to look at.
742
+ *
743
+ * The payload is located by the SAME rule `summarizeJestJson` uses (the first `{`), because a suite
744
+ * that logs anything prints it before the JSON — this repository's does — and two rules for one
745
+ * string is how the two quietly disagree.
746
+ *
747
+ * Only the FIRST line of a failure message: one failure, one look. The rest is in the file.
748
+ */
749
+ function failedTestSummary(stdout, limit = 5) {
750
+ const start = stdout.indexOf('{');
751
+ if (start < 0)
752
+ return null;
753
+ let j;
754
+ try {
755
+ j = JSON.parse(stdout.slice(start));
756
+ }
757
+ catch {
758
+ return null;
759
+ }
760
+ if (j === null || typeof j !== 'object' || !Array.isArray(j.testResults))
761
+ return null;
762
+ const failures = [];
763
+ for (const file of j.testResults) {
764
+ const base = typeof file?.name === 'string' ? file.name.split(/[\\/]/).pop() ?? file.name : '(unknown file)';
765
+ for (const a of Array.isArray(file?.assertionResults) ? file.assertionResults : []) {
766
+ if (a?.status !== 'failed')
767
+ continue;
768
+ const msg = Array.isArray(a.failureMessages) ? a.failureMessages.find((m) => typeof m === 'string' && m.trim() !== '') : undefined;
769
+ failures.push({
770
+ where: `${base} \u203a ${String(a.title ?? '(untitled)')}`,
771
+ first: typeof msg === 'string' ? (msg.trim().split('\n')[0] ?? '') : '',
772
+ });
773
+ }
774
+ }
775
+ if (failures.length === 0)
776
+ return null;
777
+ const shown = failures.slice(0, Math.max(0, limit));
778
+ const lines = shown.flatMap((f) => (f.first === '' ? [f.where] : [f.where, ` ${f.first}`]));
779
+ const rest = failures.length - shown.length;
780
+ if (rest > 0)
781
+ lines.push(` \u2026 \uc678 ${rest}\uac74`);
782
+ return lines.join('\n');
783
+ }
728
784
  function summarizeJestJson(stdout) {
729
785
  // The payload is not the whole of stdout. A suite that logs anything prints it BEFORE the JSON,
730
786
  // and this repository's own suite does: measured, the first live run after this function landed
@@ -91,19 +91,63 @@ Your npm metadata cache predates the release — measured minutes after publishi
91
91
  registry already listed the version while a default-cache install still refused it. Add
92
92
  `--prefer-online`, or retry in a few minutes.
93
93
 
94
+ ### npm 12: `better-sqlite3` has no binary and nothing failed
95
+
96
+ npm 12 (and npm ≥ 11.19) **blocks dependency install scripts by default** and skips them
97
+ silently: `npm ci` exits 0, `better-sqlite3` never runs `prebuild-install`, and the first
98
+ `require` dies with "Could not locate the bindings file". Measured 2026-09-09 (Windows 11,
99
+ npm 12.0.1, Node 24.19.0). `npx holmes-kit doctor` names this cause as `scripts-blocked` and prints
100
+ the commands below; do not reinstall — a reinstall reproduces the same state.
101
+
102
+ Only **one** package needs its script: `better-sqlite3`. The 8 tree-sitter packages are also
103
+ listed as blocked, but they load from their shipped `prebuilds/` without the script (measured on
104
+ all 8), so they are deliberately NOT approved. This repository's `package.json` therefore carries
105
+
106
+ ```json
107
+ "allowScripts": { "better-sqlite3@12.11.1": true }
108
+ ```
109
+
110
+ pinned to the lockfile version, so a dependency bump forces a fresh review (a test fails until the
111
+ pin is updated). Recovery, by how you installed — on Windows use the `.cmd` spellings (see below):
112
+
113
+ | Layout | Commands |
114
+ |---|---|
115
+ | A project that depends on holmes-kit | `npm install-scripts approve better-sqlite3@12.11.1` then `npm rebuild better-sqlite3 --foreground-scripts` (from the project root; this writes the pin into YOUR package.json) |
116
+ | This repository checkout | already approved — `npm rebuild better-sqlite3 --foreground-scripts` if the binary is missing |
117
+ | Global (`npm install -g`) | `npm rebuild -g better-sqlite3 --foreground-scripts --allow-scripts=better-sqlite3` — a per-command flag scoped to one package; no npm config is changed. Target the dependency: `rebuild -g @holmes-lab/holmes-kit` re-links the bin and dies `EEXIST` under npm 12 (measured). If this fails `EPERM` under `C:\Program Files\nodejs`, your prefix is protected — see "Before `npm install -g`" above |
118
+ | Bare `npx` | not repairable in place — do a local install and use the first row |
119
+
120
+ Never use `--dangerously-allow-all-scripts` or a global `allow-scripts` config: the point of the
121
+ policy is that only the reviewed package runs code at install time. Older npm 11 (measured 11.6.2)
122
+ ignores the `allowScripts` field and runs scripts as before; npm 11.19 honours it exactly like 12.
123
+
124
+ ### Windows: `npm`/`npx` are blocked by the PowerShell execution policy
125
+
126
+ `npm`, `npx` and `holmes-kit` resolve to `.ps1` shims first, and a `Restricted`/`AllSigned`
127
+ policy refuses them (PSSecurityException). The `.cmd` shims always run: `npm.cmd`, `npx.cmd`.
128
+ doctor emits `.cmd` commands on Windows and appends the policy note when it can observe a
129
+ blocking policy. Do not change the execution policy to fix this.
130
+
94
131
  ### `better-sqlite3` fails to build
95
132
 
96
133
  The one dependency that may need a toolchain. The 8 tree-sitter grammars ship prebuilt binaries
97
134
  (`darwin-arm64`, `darwin-x64`, `linux-x64`, `win32-x64`) and compile nothing; `better-sqlite3`
98
135
  downloads a prebuild at install time and **falls back to compiling** when none matches your
99
- platform and Node ABI. If it compiles, you need:
136
+ platform and Node ABI. doctor reports this as `build-failed` — with the approval in place, the
137
+ script ran and left no binary — and names what it can observe on Windows (Python on PATH, Visual
138
+ Studio C++ Build Tools, a space in the install path). It cannot observe a failed prebuild
139
+ download; rerun with `--foreground-scripts` to see the script's own output. If it compiles, you
140
+ need:
100
141
 
101
142
  | Platform | Toolchain |
102
143
  |---|---|
103
- | Windows | Visual Studio Build Tools (C++ workload) |
144
+ | Windows | Visual Studio Build Tools (C++ workload) + Python 3 on PATH; prefer an install path without spaces |
104
145
  | macOS | Xcode Command Line Tools (`xcode-select --install`) |
105
146
  | Alpine | `apk add --no-cache python3 make g++` |
106
147
 
148
+ A binary that exists but fails with `NODE_MODULE_VERSION` was built for another Node — doctor
149
+ calls that `abi-mismatch`; rebuild against the Node you run.
150
+
107
151
  ### `spawn sh ENOENT` during a git-URL install
108
152
 
109
153
  `npm i -g git+ssh://…` is not a supported path: npm 11 clones the repository into its cache and
@@ -117,15 +161,20 @@ npx holmes-kit doctor # local install
117
161
  holmes-kit doctor # global install
118
162
  ```
119
163
 
120
- Expect `10 pass, 1 warn, 0 fail` on a healthy install. The lines that matter most:
164
+ Expect `0 fail` on a healthy install (a few `warn` lines are normal — measured `12 pass, 4 warn,
165
+ 0 fail` on a Windows source checkout). The lines that matter most:
121
166
 
122
167
  - `global prefix` — whether `-g` would work on this machine, and the remedy if not
123
- - `tree-sitter grammars` / `better-sqlite3` whether the native modules actually load
168
+ - `tree-sitter grammars` every grammar actually PARSES in a fresh process (`8 grammars parse`)
169
+ - `better-sqlite3` — loads and runs a `:memory:` query; on FAIL the detail names the cause
170
+ (`scripts-blocked`, `abi-mismatch`, `build-failed`, or `unknown` with the raw error) and the fix
171
+ is the exact command for your install layout
124
172
 
125
173
  ## What we deliberately do NOT do
126
174
 
127
175
  | Idea | Why not |
128
176
  |---|---|
129
- | A `postinstall` script that prints guidance | Triggers npm 11's `allow-scripts` warning and forfeits this package's current property of running no install scripts at all |
177
+ | A `postinstall` script that prints guidance | Under npm 12 it would be blocked like any other install script, and it forfeits this package's property of running no install scripts of its own |
178
+ | Approving the tree-sitter grammars in `allowScripts` | Measured unnecessary — they load from shipped prebuilds — and every extra approval is code that runs at install time |
130
179
  | Recommending `npx @holmes-lab/holmes-kit init` with no install | `init` writes wiring with absolute paths; under bare `npx` those point into the npx cache and break when it is pruned |
131
180
  | Fixing your npm prefix from inside the package | A package rewriting your npm configuration is exactly the supply-chain behaviour this guide warns about |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.19.0",
4
+ "version": "0.19.2",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",
@@ -85,5 +85,8 @@
85
85
  },
86
86
  "publishConfig": {
87
87
  "access": "public"
88
+ },
89
+ "allowScripts": {
90
+ "better-sqlite3@12.11.1": true
88
91
  }
89
92
  }