@holmes-lab/holmes-kit 0.3.6 → 0.3.7

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 CHANGED
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
4
4
 
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
+ <!-- @implements A-SPEC-209 -->
8
+ ## [0.3.7] - 2026-09-01
9
+
10
+ Field-incident hardening: consistency warning lints over the code graph, and an approval queue
11
+ that stops burying the one approval that matters.
12
+
13
+ ### Added
14
+
15
+ - **Consistency lints, opt-in** (A-SPEC-506.1): `cpg_scan` accepts `lints: true` and adds a
16
+ warning-signal field (Python, with an honest `limits` envelope): `dynamicRef` — a literal
17
+ `getattr`/`hasattr` name absent from the scanned symbol census (field incident: 49
18
+ owner-approved deletions silently no-op'ed against a nonexistent method) — and
19
+ `asyncBlocking` — a known-blocking call inside an `async def`, unwrapped on its line (field
20
+ incident: scheduler starvation while the same file's other call sites used `to_thread`). No
21
+ gate consumes these; the default scan result is unchanged.
22
+ - **Approval-queue staleness** (A-SPEC-507.1): the `approve` list sorts by latest activity and
23
+ folds entries whose last activity exceeded `--stale-hours` (default 24; `--all` unfolds,
24
+ `--stale-hours 0` disables) — measured: 51 pending rows, mostly week-old residue, buried the
25
+ one live approval at [32]. The ledger deletes nothing; a re-filed request revives itself; and
26
+ gate-path folds take no clock at all (value-identical without the view options).
27
+
7
28
  <!-- @implements A-SPEC-209 -->
8
29
  ## [0.3.6] - 2026-09-01
9
30
 
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- c3e5866-mthi2ccn
1
+ eb3164c-mthu5jwj
@@ -21,7 +21,19 @@ export declare function holdRequest(root: string, id: string, question: string,
21
21
  * when, and any standing question. Deliberately NO scope grammar anywhere — the moment the screen
22
22
  * teaches syntax, humans start typing it.
23
23
  */
24
- export declare function renderPending(state: QueueState): string;
24
+ /**
25
+ * The list-view stale flags, parsed once: `--stale-hours <h>` (default 24; 0 disables folding —
26
+ * `--ttl` was already taken by the grant-lifetime flag) and `--all` to unfold. Returning null
27
+ * means "no clock": readQueue is then called without opts and the fold stays value-identical.
28
+ */
29
+ export declare function staleView(flags: Record<string, unknown>): {
30
+ ttlMs: number;
31
+ showAll: boolean;
32
+ } | null;
33
+ export declare function renderPending(state: QueueState, view?: {
34
+ staleHours: number;
35
+ showAll: boolean;
36
+ }): string;
25
37
  /**
26
38
  * @implements A-SPEC-262.1
27
39
  * What a decision line must say. Round-1: `✓ 승인 — <expires> 까지 유효` named NOTHING, so an index
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.grantRequest = grantRequest;
37
37
  exports.denyRequest = denyRequest;
38
38
  exports.holdRequest = holdRequest;
39
+ exports.staleView = staleView;
39
40
  exports.renderPending = renderPending;
40
41
  exports.subjectCells = subjectCells;
41
42
  exports.subjectRoom = subjectRoom;
@@ -236,7 +237,20 @@ function holdRequest(root, id, question, actor) {
236
237
  * when, and any standing question. Deliberately NO scope grammar anywhere — the moment the screen
237
238
  * teaches syntax, humans start typing it.
238
239
  */
239
- function renderPending(state) {
240
+ // @implements A-SPEC-507.1
241
+ /**
242
+ * The list-view stale flags, parsed once: `--stale-hours <h>` (default 24; 0 disables folding —
243
+ * `--ttl` was already taken by the grant-lifetime flag) and `--all` to unfold. Returning null
244
+ * means "no clock": readQueue is then called without opts and the fold stays value-identical.
245
+ */
246
+ function staleView(flags) {
247
+ const raw = flags['stale-hours'];
248
+ const hours = typeof raw === 'string' ? Number(raw) : 24;
249
+ if (!Number.isFinite(hours) || hours <= 0)
250
+ return null;
251
+ return { ttlMs: hours * 3_600_000, showAll: flags.all === true };
252
+ }
253
+ function renderPending(state, view) {
240
254
  if (state.pending.length === 0) {
241
255
  // Round-7: this early return made the malformed-line row below UNREACHABLE in exactly the case
242
256
  // it exists for — a wholly corrupted queue printed "no requests waiting" and exited 0, which is
@@ -249,7 +263,13 @@ function renderPending(state) {
249
263
  return '승인 대기 중인 요청이 없습니다.';
250
264
  }
251
265
  const lines = [`◆ 승인 대기 ${state.pending.length}건`, ''];
252
- state.pending.forEach((p, i) => {
266
+ // @implements A-SPEC-507.1 the LIST VIEW sorts newest-activity-first and folds stale entries
267
+ // (C4: 51 rows, mostly week-old residue, buried the one live approval at [32]). Only when a view
268
+ // is passed: callers without one keep the legacy insertion order byte-for-byte.
269
+ const shown = view
270
+ ? [...state.pending].sort((a, b) => (Date.parse(b.lastTs) || 0) - (Date.parse(a.lastTs) || 0))
271
+ : state.pending;
272
+ shown.forEach((p, i) => {
253
273
  // @implements A-SPEC-262.1 — every field here is AGENT-CONTROLLED (the target is the command it
254
274
  // was blocked on). Round-1 forged two extra rows and hid the real ones behind an ANSI conceal,
255
275
  // and the operator granted a `curl … | sh` they never saw. The template owns the line structure.
@@ -266,6 +286,19 @@ function renderPending(state) {
266
286
  lines.push(` id: ${(0, screen_safe_1.rowField)(p.id, 40)}`);
267
287
  lines.push('');
268
288
  });
289
+ if (view && state.expired.length > 0) {
290
+ if (view.showAll) {
291
+ // Folding is not concealment: --all shows every expired entry under a template-owned prefix.
292
+ for (const p of state.expired) {
293
+ lines.push(`[만료] ${subjectCells('[만료] ', p)}`);
294
+ lines.push(` 마지막 활동: ${(0, screen_safe_1.rowField)(p.lastTs, 28)} · id: ${(0, screen_safe_1.rowField)(p.id, 40)}`);
295
+ lines.push('');
296
+ }
297
+ }
298
+ else {
299
+ lines.push(`⏳ 만료로 접힘 ${state.expired.length}건 (마지막 활동 ${view.staleHours}h 초과) — --all 로 표시`);
300
+ }
301
+ }
269
302
  if (state.malformedLines > 0)
270
303
  lines.push(`(큐에 읽을 수 없는 줄 ${state.malformedLines}건 — 손상 여부를 확인하십시오)`);
271
304
  return lines.join('\n');
@@ -787,8 +787,14 @@ async function main(argv) {
787
787
  process.stdout.write(r.ok ? `✓ 보류 — ${ref.subject('✓ 보류 — ')}\n${ref.detail('질문은 다음 거부 문면에')}\n` : `✗ ${r.reason}\n`);
788
788
  return r.ok ? 0 : 1;
789
789
  }
790
+ // @implements A-SPEC-507.1 — stale folding is a LIST-VIEW judgment: the clock enters only
791
+ // here, never on gate-path folds. `--stale-hours 0` disables it; `--all` unfolds.
792
+ const { staleView } = require('./approve');
793
+ const stale = staleView(flags);
794
+ const queueOpts = stale ? { now: Date.now(), ttlMs: stale.ttlMs } : undefined;
795
+ const listView = stale ? { staleHours: stale.ttlMs / 3_600_000, showAll: stale.showAll } : undefined;
790
796
  if (flags.list) {
791
- process.stdout.write(renderPending(readQueue(root)) + '\n');
797
+ process.stdout.write(renderPending(readQueue(root, queueOpts), listView) + '\n');
792
798
  return 0;
793
799
  }
794
800
  // @implements A-SPEC-262.2 — the RESIDENT surface. Validate --poll-ms first (so a bad value is
@@ -833,9 +839,9 @@ async function main(argv) {
833
839
  // @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
834
840
  // with the real pending id filled in; the explicit --list above stays script-clean.
835
841
  if (!process.stdin.isTTY) {
836
- const state = readQueue(root);
842
+ const state = readQueue(root, queueOpts);
837
843
  const hint = renderNonTtyHint(state);
838
- process.stdout.write(renderPending(state) + (hint ? '\n\n' + hint : '') + '\n');
844
+ process.stdout.write(renderPending(state, listView) + (hint ? '\n\n' + hint : '') + '\n');
839
845
  return 0;
840
846
  }
841
847
  // @implements A-SPEC-262.1
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Consistency lints over what the repo already KNOWS — two field incidents, one gap:
3
+ *
4
+ * P-G `getattr(mem, "delete_fact", None)` referenced a symbol no class defines; the optional
5
+ * default hid the absence and 49 owner-approved deletions silently no-op'ed. The symbol
6
+ * census (CPG scan) had the answer all along — nobody asked it.
7
+ * P-J an `async def` body called sqlite directly and starved the scheduler, while the SAME
8
+ * file's other call sites wrapped the call in `to_thread` — the correct convention was one
9
+ * screen away.
10
+ *
11
+ * WARNING SIGNALS ONLY: no gate consumes these (association proposes, determinism judges — the
12
+ * standing principle). Text heuristics with honest limits (LINT_LIMITS): literal names only,
13
+ * same-line wrapping exemption only, Python only — both incidents were Python; extensions need
14
+ * their own field evidence, like every pattern list in this repository.
15
+ */
16
+ export declare const LINT_LIMITS: readonly string[];
17
+ /** P-G: literal getattr/hasattr names absent from the repo's symbol census. */
18
+ export declare function dynamicRefFindings(code: string, defined: ReadonlySet<string>): Array<{
19
+ name: string;
20
+ line: number;
21
+ }>;
22
+ /** P-J: known-blocking calls inside an async def's indentation block, unwrapped on their line. */
23
+ export declare function asyncBlockingFindings(code: string): Array<{
24
+ pattern: string;
25
+ line: number;
26
+ }>;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // @implements A-SPEC-506.1
3
+ /**
4
+ * Consistency lints over what the repo already KNOWS — two field incidents, one gap:
5
+ *
6
+ * P-G `getattr(mem, "delete_fact", None)` referenced a symbol no class defines; the optional
7
+ * default hid the absence and 49 owner-approved deletions silently no-op'ed. The symbol
8
+ * census (CPG scan) had the answer all along — nobody asked it.
9
+ * P-J an `async def` body called sqlite directly and starved the scheduler, while the SAME
10
+ * file's other call sites wrapped the call in `to_thread` — the correct convention was one
11
+ * screen away.
12
+ *
13
+ * WARNING SIGNALS ONLY: no gate consumes these (association proposes, determinism judges — the
14
+ * standing principle). Text heuristics with honest limits (LINT_LIMITS): literal names only,
15
+ * same-line wrapping exemption only, Python only — both incidents were Python; extensions need
16
+ * their own field evidence, like every pattern list in this repository.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.LINT_LIMITS = void 0;
20
+ exports.dynamicRefFindings = dynamicRefFindings;
21
+ exports.asyncBlockingFindings = asyncBlockingFindings;
22
+ exports.LINT_LIMITS = [
23
+ '리터럴 이름 인자만 판정한다 — getattr(obj, 변수)는 판정 불가로 건너뛴다',
24
+ '차단 패턴 목록은 실사고·명백성 기반 최소 집합이다(time.sleep·requests.동사·sqlite3.·subprocess.run·urllib.request.urlopen) — 확장은 새 실측으로만',
25
+ 'to_thread/run_in_executor 면제는 같은 행에서만 인식한다 — 여러 줄 래핑은 오탐될 수 있다',
26
+ '.py 텍스트 휴리스틱이다 — 비파이썬 언어와 문자열 속 코드는 보지 않는다',
27
+ ];
28
+ const DYNAMIC_REF_RE = /\b(?:getattr|hasattr)\(\s*[^,()]+,\s*['"](\w+)['"]/g;
29
+ /** P-G: literal getattr/hasattr names absent from the repo's symbol census. */
30
+ function dynamicRefFindings(code, defined) {
31
+ const out = [];
32
+ for (const m of code.matchAll(DYNAMIC_REF_RE)) {
33
+ const name = m[1];
34
+ if (defined.has(name))
35
+ continue;
36
+ out.push({ name, line: code.slice(0, m.index).split('\n').length });
37
+ }
38
+ return out;
39
+ }
40
+ const BLOCKING = ['time.sleep(', 'requests.get(', 'requests.post(', 'requests.put(',
41
+ 'requests.delete(', 'requests.request(', 'sqlite3.', 'subprocess.run(', 'urllib.request.urlopen'];
42
+ const EXEMPT_RE = /to_thread|run_in_executor/;
43
+ /** P-J: known-blocking calls inside an async def's indentation block, unwrapped on their line. */
44
+ function asyncBlockingFindings(code) {
45
+ const out = [];
46
+ const lines = code.split('\n');
47
+ let asyncIndent = null; // indentation of the enclosing `async def`, when inside one
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const line = lines[i];
50
+ const indent = line.length - line.trimStart().length;
51
+ const isBlank = line.trim() === '';
52
+ if (asyncIndent !== null && !isBlank && indent <= asyncIndent)
53
+ asyncIndent = null; // dedent ends the block
54
+ const def = /^(\s*)async\s+def\b/.exec(line);
55
+ if (def) {
56
+ asyncIndent = def[1].length;
57
+ continue;
58
+ }
59
+ if (asyncIndent === null || isBlank || EXEMPT_RE.test(line))
60
+ continue;
61
+ for (const pattern of BLOCKING) {
62
+ if (line.includes(pattern))
63
+ out.push({ pattern, line: i + 1 });
64
+ }
65
+ }
66
+ return out;
67
+ }
@@ -31,6 +31,14 @@ export interface PendingRequest {
31
31
  }
32
32
  export interface QueueState {
33
33
  pending: PendingRequest[];
34
+ /**
35
+ * @implements A-SPEC-507.1
36
+ * Pending entries whose last activity exceeded the caller's TTL — populated ONLY when the fold
37
+ * was given a clock ({now, ttlMs}), so gate-path folds never depend on when you look. C4
38
+ * measured the harm of agelessness: 51 entries, mostly week-old residue, buried the one live
39
+ * approval at [32]. Expiry is a display-layer judgment; the ledger keeps every event.
40
+ */
41
+ expired: PendingRequest[];
34
42
  /** Broken lines are counted, never swallowed: an empty-looking queue must be distinguishable from a corrupted one. */
35
43
  malformedLines: number;
36
44
  /**
@@ -64,7 +72,10 @@ export declare function approvalRequestId(kind: string, target: string): string;
64
72
  * entry's pending state: an old CLI reading a new queue must not silently mis-report what is
65
73
  * waiting.
66
74
  */
67
- export declare function foldQueue(lines: string[]): QueueState;
75
+ export declare function foldQueue(lines: string[], opts?: {
76
+ now: number;
77
+ ttlMs: number;
78
+ }): QueueState;
68
79
  /**
69
80
  * Append a request event. Fire-and-forget.
70
81
  *
@@ -79,7 +90,10 @@ export declare function enqueueApprovalRequest(root: string, req: {
79
90
  why: string;
80
91
  }): boolean;
81
92
  /** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
82
- export declare function readQueue(root: string): QueueState;
93
+ export declare function readQueue(root: string, opts?: {
94
+ now: number;
95
+ ttlMs: number;
96
+ }): QueueState;
83
97
  /**
84
98
  * The refusal-message suffix pointing the operator at the review CLI.
85
99
  *
@@ -83,7 +83,7 @@ function approvalRequestId(kind, target) {
83
83
  * entry's pending state: an old CLI reading a new queue must not silently mis-report what is
84
84
  * waiting.
85
85
  */
86
- function foldQueue(lines) {
86
+ function foldQueue(lines, opts) {
87
87
  const pending = new Map();
88
88
  const decisions = {};
89
89
  let malformedLines = 0;
@@ -155,7 +155,18 @@ function foldQueue(lines) {
155
155
  break;
156
156
  }
157
157
  }
158
- return { pending: [...pending.values()], malformedLines, decisions };
158
+ const all = [...pending.values()];
159
+ if (!opts)
160
+ return { pending: all, expired: [], malformedLines, decisions };
161
+ // @implements A-SPEC-507.1 — strict excess only, and an unparseable lastTs stays ACTIVE: a
162
+ // clockless entry must never be silently hidden by a clock it does not carry.
163
+ const expired = [];
164
+ const active = [];
165
+ for (const p of all) {
166
+ const last = Date.parse(p.lastTs);
167
+ (Number.isFinite(last) && last + opts.ttlMs < opts.now ? expired : active).push(p);
168
+ }
169
+ return { pending: active, expired, malformedLines, decisions };
159
170
  }
160
171
  /**
161
172
  * Append a request event. Fire-and-forget.
@@ -223,7 +234,7 @@ function isPlainFile(file) {
223
234
  }
224
235
  }
225
236
  /** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
226
- function readQueue(root) {
237
+ function readQueue(root, opts) {
227
238
  const file = path.join(root, exports.QUEUE_RELPATH);
228
239
  try {
229
240
  // TYPE BEFORE READ. `readFileSync` on a FIFO blocks inside open(2) forever — no throw, no
@@ -232,12 +243,12 @@ function readQueue(root) {
232
243
  // hook, the stop hook and the MCP handlers, not just the CLI. `approve-context.ts` has guarded
233
244
  // this since the 2026-08-24 hang; the queue reader, which far more code depends on, did not.
234
245
  if (!fs.lstatSync(file).isFile())
235
- return { pending: [], malformedLines: 1, decisions: {} };
246
+ return { pending: [], expired: [], malformedLines: 1, decisions: {} };
236
247
  const raw = fs.readFileSync(file, 'utf8');
237
- return foldQueue(raw.split('\n'));
248
+ return foldQueue(raw.split('\n'), opts);
238
249
  }
239
250
  catch {
240
- return { pending: [], malformedLines: 0, decisions: {} };
251
+ return { pending: [], expired: [], malformedLines: 0, decisions: {} };
241
252
  }
242
253
  }
243
254
  /**
@@ -272,6 +272,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
272
272
  }>;
273
273
  cpg_scan(a: {
274
274
  root: string;
275
+ lints?: boolean;
275
276
  }): Promise<{
276
277
  files: number;
277
278
  symbols: number;
@@ -280,6 +281,27 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
280
281
  reason: string;
281
282
  }[];
282
283
  skippedCount: number;
284
+ } | {
285
+ lints: {
286
+ dynamicRef: {
287
+ file: string;
288
+ name: string;
289
+ line: number;
290
+ }[];
291
+ asyncBlocking: {
292
+ file: string;
293
+ pattern: string;
294
+ line: number;
295
+ }[];
296
+ limits: string[];
297
+ };
298
+ files: number;
299
+ symbols: number;
300
+ skipped: {
301
+ file: string;
302
+ reason: string;
303
+ }[];
304
+ skippedCount: number;
283
305
  }>;
284
306
  /**
285
307
  * @implements A-SPEC-138
@@ -131,6 +131,7 @@ const risk_classifier_1 = require("../guardrail/risk-classifier");
131
131
  const risk_gate_1 = require("../guardrail/risk-gate");
132
132
  const elicit_approval_1 = require("./elicit-approval");
133
133
  const anchor_comment_1 = require("../rtm/anchor-comment");
134
+ const consistency_lints_1 = require("../cpg/consistency-lints");
134
135
  const approval_queue_1 = require("../governance/approval-queue");
135
136
  const approval_grants_1 = require("../governance/approval-grants");
136
137
  const spec_digest_1 = require("../spec/spec-digest");
@@ -1633,7 +1634,37 @@ function makeRawHandlers(store, opts) {
1633
1634
  // with casualties" by counts alone. Bounded listing (50), complete count.
1634
1635
  const { scanned, skipped } = cachedScanWithReport(root);
1635
1636
  const symbols = scanned.reduce((n, f) => n + f.symbols.length, 0);
1636
- return { files: scanned.length, symbols, skipped: skipped.slice(0, 50), skippedCount: skipped.length };
1637
+ const base = { files: scanned.length, symbols, skipped: skipped.slice(0, 50), skippedCount: skipped.length };
1638
+ if (!a.lints)
1639
+ return base;
1640
+ // @implements A-SPEC-506.1 — opt-in consistency lints (P-G/P-J field incidents): WARNING
1641
+ // signals only, additive field only, Python only; the default path above is byte-identical.
1642
+ const defined = new Set();
1643
+ for (const f of scanned)
1644
+ for (const s of f.symbols) {
1645
+ defined.add(s.name);
1646
+ const last = s.name.split('.').pop();
1647
+ if (last)
1648
+ defined.add(last);
1649
+ }
1650
+ const dynamicRef = [];
1651
+ const asyncBlocking = [];
1652
+ for (const f of scanned) {
1653
+ if (!f.sourcePath.endsWith('.py'))
1654
+ continue;
1655
+ let code;
1656
+ try {
1657
+ code = fs.readFileSync(path.join(root, f.sourcePath), 'utf8');
1658
+ }
1659
+ catch {
1660
+ continue;
1661
+ }
1662
+ for (const hit of (0, consistency_lints_1.dynamicRefFindings)(code, defined))
1663
+ dynamicRef.push({ file: f.sourcePath, ...hit });
1664
+ for (const hit of (0, consistency_lints_1.asyncBlockingFindings)(code))
1665
+ asyncBlocking.push({ file: f.sourcePath, ...hit });
1666
+ }
1667
+ return { ...base, lints: { dynamicRef, asyncBlocking, limits: [...consistency_lints_1.LINT_LIMITS] } };
1637
1668
  },
1638
1669
  /**
1639
1670
  * @implements A-SPEC-138
@@ -171,10 +171,10 @@ exports.TOOL_SCHEMAS = {
171
171
  },
172
172
  },
173
173
  cpg_scan: {
174
- description: 'Scan the repository tree with tree-sitter across the 8 supported languages; returns { files, symbols } counts plus the skip report ({ skipped, skippedCount }) naming any claimed file the scan could not ingest — an empty report distinguishes a clean tree from one with casualties.',
174
+ description: 'Scan the repository tree with tree-sitter across the 8 supported languages; returns { files, symbols } counts plus the skip report ({ skipped, skippedCount }) naming any claimed file the scan could not ingest — an empty report distinguishes a clean tree from one with casualties. Pass lints:true to ALSO receive opt-in consistency WARNING signals (Python only, additive `lints` field with its own `limits`): dynamicRef (a literal getattr/hasattr name absent from the scanned symbol census) and asyncBlocking (a known-blocking call inside an async def, unwrapped on its line). Signals route a review; no gate consumes them.',
175
175
  inputSchema: {
176
176
  type: 'object',
177
- properties: { root: ROOT },
177
+ properties: { root: ROOT, lints: { type: 'boolean', description: 'Include the consistency-lint warning signals (default false — the base result is unchanged without it).' } },
178
178
  required: ['root'],
179
179
  },
180
180
  },
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.3.6",
4
+ "version": "0.3.7",
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",