@tekyzinc/gsd-t 5.16.11 → 5.17.10

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
@@ -2,6 +2,88 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.17.10] - 2026-09-01
6
+
7
+ ### Fixed — the code graph indexed one TypeScript project and called it the whole repo
8
+
9
+ Found in a live TimeTracking session: 8 of 245 files indexed, 96 of 50,283 call
10
+ edges resolved (0.2%, all same-file), and not one backend call among them. The
11
+ root `tsconfig.json`'s `include` listed frontend paths only, so `server/src/`
12
+ was never compiled and every backend `who-calls` returned empty.
13
+
14
+ **Three defects, and fixing any one alone changes nothing.**
15
+
16
+ 1. **One indexer run at the repo root.** `--infer-tsconfig` does not cover this —
17
+ it finds *a* tsconfig when the root has none, but still emits one index from
18
+ one root. Now: one run per tsconfig project, merged.
19
+
20
+ 2. **Paths keyed to where the indexer ran.** Indexing `server/` emits
21
+ `src/index.ts` while the graph stores `server/src/index.ts`, so a second run
22
+ *without* re-prefixing resolves nothing while looking like a fix. The prefix
23
+ has to apply to the funcId values too (`relPath#name`), not just the map keys.
24
+
25
+ 3. **`who-calls` claimed `coverage.complete: true` while returning zero.**
26
+ Coverage counted parse failures only; a file that parsed but was never
27
+ compiler-resolved is equally invisible to a call query and never lands in
28
+ `skippedFiles`. "Nothing calls this" is what gets read right before a rename
29
+ or a delete — and it was firing on auth middleware.
30
+
31
+ Why (3) hid so long: the per-file record handed to the query layer was
32
+ `{file, entities, edges}` with no tier field, so the query layer structurally
33
+ could not tell a resolved file from an unresolved one.
34
+
35
+ Import queries deliberately still report complete at floor tier — import edges
36
+ come from the parse, so `who-imports` and `blast-radius` are genuinely complete
37
+ there, and flagging them would train you to ignore the field.
38
+
39
+ Measured on TimeTracking: 8 → 40 indexed files, 0 → 173 resolved call edges into
40
+ `server/`, 96 → 390 overall, `server/` 0 → 13 of 19 files compiler-accurate.
41
+ Affects 13 registered projects with nested tsconfigs (binvoice 1→5 projects,
42
+ newman 1→7, Tekyz-CRM 1→2).
43
+
44
+ - `bin/gsd-t-graph-scip-upgrade.cjs`: `findTsProjectDirs()` + one indexer run per project
45
+ - `bin/gsd-t-scip-reader.cjs`: `pathPrefix` re-roots fileRefs keys AND funcIds
46
+ - `bin/gsd-t-graph-query-cli.cjs`: per-file tier carried onto records + tier-aware coverage
47
+ - `test/m114-nested-tsconfig-graph.test.js`: 12 tests, each mutation-tested
48
+
49
+ **Re-index to pick this up**: `gsd-t graph index` in any project with a nested
50
+ tsconfig. A graph built before this version under-reports call edges.
51
+
52
+ ## [5.16.12] - 2026-08-29
53
+
54
+ ### Fixed — a retired hook fired on every tool call, on every machine, since M61
55
+
56
+ An audit of installed hooks against what the installer registers found all 16
57
+ expected hooks present and correct — plus one that should not have been there.
58
+
59
+ M61 deleted `scripts/gsd-t-context-meter.js` (native `/context` replaced it) and
60
+ unwired the subsystem from `init()` and `doctor()`. `install()` was missed, so it
61
+ kept calling `configureContextMeterHooks()` and re-adding a `PostToolUse` hook
62
+ matching `*` on every install and every update.
63
+
64
+ **It never errored, which is why it survived eight months.** The command is
65
+ guarded `[ -f … ] && node … || true`, so on every single tool call it spawned a
66
+ bash + `npm root -g` subprocess, found nothing, and exited 0 silently — a real
67
+ cost with no signal.
68
+
69
+ The fix is two parts, because dropping the registration alone fixes nobody: a
70
+ machine that installed the hook once keeps running it out of its own
71
+ `settings.json`, and the installer is the only thing that reaches those machines.
72
+
73
+ - `bin/gsd-t.js`: removed the `configureContextMeterHooks()` call from `install()`
74
+ - `bin/gsd-t.js`: added the marker to `removeRetiredHooks()`, so existing machines
75
+ get it stripped on their next install — the same mechanism that retired the
76
+ M105 worktree guard
77
+ - `bin/gsd-t.js`: exported `removeRetiredHooks` — it was not testable from
78
+ outside at all, which is part of why this went unnoticed
79
+ - `test/m61-context-meter-hook-retired.test.js`: 6 regression tests, mutation-tested
80
+ by reverting each half of the fix
81
+ - `.gsd-t/contracts/graph-metrics-contract.md`: stale line citation for `doMetrics`
82
+ (:5486 → :5488), shifted by the edit above and caught by the M99 contract-line test
83
+
84
+ `configureContextMeterHooks` and `removeContextMeterHook` stay defined for the
85
+ uninstall path. No migration needed — the next `gsd-t update` removes the hook.
86
+
5
87
  ## [5.16.11] - 2026-08-27
6
88
 
7
89
  ### Fixed — the voice fix was the wrong fix, and the gate was measuring the wrong thing
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.16.11** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.17.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
4
4
 
5
5
  **Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
6
6
  **Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
@@ -295,16 +295,51 @@ function loadSkippedFiles(storePath) {
295
295
  * @param {Set<string>} skippedFiles — files that failed to parse (D3-T6)
296
296
  * @returns {{ complete: boolean, unparsedContributors?: number, note?: string }}
297
297
  */
298
- function computeCoverage(skippedFiles) {
299
- if (!skippedFiles || skippedFiles.size === 0) {
300
- return { complete: true };
298
+ function computeCoverage(skippedFiles, opts = {}) {
299
+ const n = (skippedFiles && skippedFiles.size) || 0;
300
+
301
+ // M114 — an UNRESOLVED file is as invisible to who-calls as an unparsed one.
302
+ // Parsing a file yields its call SITES; only compiler-accurate resolution says
303
+ // WHICH function each site targets. A floor-tier file parses fine, contributes
304
+ // zero resolved call edges, and never lands in skippedFiles — so coverage used
305
+ // to report complete:true while who-calls returned empty for an entire backend.
306
+ // "Nothing calls this" is what gets read right before a rename or a delete, so
307
+ // a false all-clear here is worse than no answer.
308
+ // Applies to CALL-edge queries only: IMPORT edges come from the parse, so
309
+ // who-imports and blast-radius are genuinely complete at floor tier.
310
+ const unresolved = opts.callEdgesUnresolved ? (opts.unresolvedFiles || 0) : 0;
311
+
312
+ if (n === 0 && !unresolved) return { complete: true };
313
+
314
+ const notes = [];
315
+ if (n) notes.push(`${n} file(s) unparsed`);
316
+ if (unresolved) {
317
+ notes.push(`${unresolved} file(s) parsed but not compiler-resolved — ` +
318
+ `call edges there are unknown, not absent`);
301
319
  }
302
- const n = skippedFiles.size;
303
- return {
320
+ const coverage = {
304
321
  complete: false,
305
- unparsedContributors: n,
306
- note: `result may be incomplete — ${n} file(s) unparsed`,
322
+ note: `result may be incomplete — ${notes.join('; ')}`,
307
323
  };
324
+ if (n) coverage.unparsedContributors = n;
325
+ if (unresolved) coverage.unresolvedContributors = unresolved;
326
+ return coverage;
327
+ }
328
+
329
+ /**
330
+ * Count files whose tier is NOT compiler-accurate — i.e. files that parsed but
331
+ * whose call targets were never resolved. These are invisible to who-calls.
332
+ *
333
+ * @param {{fileTier?: Map<string,string>}} index
334
+ * @returns {number}
335
+ */
336
+ function countUnresolvedFiles(index) {
337
+ if (!index || !index.fileTier) return 0;
338
+ let n = 0;
339
+ for (const tier of index.fileTier.values()) {
340
+ if (tier && tier !== 'compiler-accurate') n++;
341
+ }
342
+ return n;
308
343
  }
309
344
 
310
345
  /**
@@ -332,6 +367,9 @@ function buildIndex(records, skippedFiles) {
332
367
  const funcEntities = new Map();
333
368
  /** @type {Set<string>} */
334
369
  const allFiles = new Set();
370
+ /** @type {Map<string,string>} file → tier (M114: who-calls needs the TARGET
371
+ * file's tier, not just the repo-wide dominant one). */
372
+ const fileTier = new Map();
335
373
 
336
374
  let dominantTier = "compiler-accurate";
337
375
  let hasFloor = false;
@@ -339,6 +377,7 @@ function buildIndex(records, skippedFiles) {
339
377
 
340
378
  for (const rec of records) {
341
379
  allFiles.add(rec.file);
380
+ if (rec.tier) fileTier.set(rec.file, rec.tier);
342
381
 
343
382
  if (rec.tier === "tree-sitter-floor") hasFloor = true;
344
383
  if (rec.tier === "tree-sitter-floor-STALE-SCIP") hasStaleScip = true;
@@ -384,6 +423,7 @@ function buildIndex(records, skippedFiles) {
384
423
  forwardCallEdges,
385
424
  funcEntities,
386
425
  allFiles,
426
+ fileTier,
387
427
  tier: dominantTier,
388
428
  skippedFiles: skippedFiles instanceof Set ? skippedFiles : new Set(),
389
429
  };
@@ -516,6 +556,17 @@ function loadSqliteStore(dbPath) {
516
556
  for (const n of nodes) {
517
557
  if (n.func_id) rec(n.file).entities.push({ funcId: n.func_id, name: n.name, file: n.file, tier: n.tier, endLine: n.end_line });
518
558
  }
559
+ // M114 — carry each file's TIER onto its record. Without this the record is
560
+ // {file, entities, edges} with no tier, so the query layer cannot tell a
561
+ // compiler-resolved file from an unresolved one and coverage cannot report
562
+ // incompleteness. A file is only compiler-accurate if every entity is; a
563
+ // single floor entity means some call target in it is unknown.
564
+ for (const n of nodes) {
565
+ if (!n.file) continue;
566
+ const r = rec(n.file);
567
+ if (n.tier && n.tier !== 'compiler-accurate') r.tier = n.tier;
568
+ else if (!r.tier) r.tier = n.tier || 'compiler-accurate';
569
+ }
519
570
  for (const e of edges) {
520
571
  // src for an IMPORT edge is the source FILE; for a CALL edge it's a funcId
521
572
  // (file#fn@line). The owning file record is the src's file part.
@@ -676,7 +727,7 @@ function queryWhoImports(index, target) {
676
727
  */
677
728
  function queryWhoCalls(index, identity) {
678
729
  const isFuncId = identity.includes("#");
679
- const coverage = computeCoverage(index.skippedFiles);
730
+ const coverage = computeCoverage(index.skippedFiles, { callEdgesUnresolved: true, unresolvedFiles: countUnresolvedFiles(index) });
680
731
 
681
732
  if (isFuncId) {
682
733
  // File-qualified identity — exact funcId lookup (tolerate @line suffix:
@@ -909,7 +960,7 @@ function queryBlastRadius(index, target) {
909
960
  }
910
961
 
911
962
  const results = Array.from(visited).sort();
912
- const coverage = computeCoverage(index.skippedFiles);
963
+ const coverage = computeCoverage(index.skippedFiles, { callEdgesUnresolved: true, unresolvedFiles: countUnresolvedFiles(index) });
913
964
  return { results, tier: index.tier, coverage };
914
965
  }
915
966
 
@@ -1295,6 +1346,7 @@ module.exports = {
1295
1346
  queryDangling,
1296
1347
  queryTestImpl,
1297
1348
  computeCoverage,
1349
+ countUnresolvedFiles,
1298
1350
  loadStore,
1299
1351
  runFreshnessCheck,
1300
1352
  resolveStorePath,
@@ -243,6 +243,59 @@ function isRustCrossCrateEdge(edge, relPath) {
243
243
  return false;
244
244
  }
245
245
 
246
+ // ── TypeScript project discovery (M114) ──────────────────────────────────────
247
+
248
+ /**
249
+ * Find every TypeScript project root in the repo — each directory holding a
250
+ * `tsconfig.json`.
251
+ *
252
+ * Why: scip-typescript indexes ONE tsconfig per run, and a tsconfig's `include`
253
+ * governs what it can see. A repo whose root tsconfig covers only the frontend
254
+ * indexes only the frontend; `server/src/` is never compiled, so not one backend
255
+ * call resolves and `who-calls` answers empty for the entire backend.
256
+ * `--infer-tsconfig` does NOT cover this — it finds *a* tsconfig when the root
257
+ * has none, but still produces a single index from a single root.
258
+ *
259
+ * Returns repo-relative dirs, root first (`''` for the repo root itself), so the
260
+ * root project's symbols win when two projects define the same name.
261
+ *
262
+ * A nested project inside another's `include` gets indexed twice; that is
263
+ * harmless — the second read overwrites identical keys with identical values.
264
+ *
265
+ * [RULE] scip-indexes-every-tsconfig-project
266
+ *
267
+ * @param {string} repoRoot
268
+ * @returns {string[]} repo-relative project dirs, e.g. ['', 'server']
269
+ */
270
+ function findTsProjectDirs(repoRoot) {
271
+ const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'out', '.venv', 'venv',
272
+ 'site-packages', '__pycache__', '.next', 'coverage', 'Pods', '.dart_tool', 'vendor']);
273
+ function isSkip(name) {
274
+ if (SKIP.has(name)) return true;
275
+ return ['dist', 'build', 'out'].some(p => name.length > p.length && name.startsWith(p) &&
276
+ (name[p.length] === '-' || name[p.length] === '.' || name[p.length] === '_'));
277
+ }
278
+ const found = [];
279
+ function walk(dir, depth) {
280
+ if (depth > 4) return; // deep enough for monorepo packages/*/x
281
+ let entries;
282
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
283
+ for (const e of entries) {
284
+ if (e.isFile() && e.name === 'tsconfig.json') {
285
+ const rel = path.relative(repoRoot, dir).split(path.sep).join('/');
286
+ found.push(rel);
287
+ }
288
+ }
289
+ for (const e of entries) {
290
+ if (e.isDirectory() && !isSkip(e.name)) walk(path.join(dir, e.name), depth + 1);
291
+ }
292
+ }
293
+ walk(repoRoot, 0);
294
+ // Root ('') first so its symbols are read before nested ones.
295
+ found.sort((a, b) => (a === '' ? -1 : b === '' ? 1 : a.localeCompare(b)));
296
+ return found;
297
+ }
298
+
246
299
  // ── Repo-level SCIP resolver (M95) ────────────────────────────────────────────
247
300
 
248
301
  /**
@@ -282,11 +335,32 @@ function buildScipResolver(repoRoot, opts = {}) {
282
335
  }
283
336
  }
284
337
 
338
+ // TypeScript: one indexer run PER tsconfig project, each re-prefixed back to
339
+ // repo-root-relative paths before merging. A single root run misses every
340
+ // nested project (see findTsProjectDirs), and a second run without the prefix
341
+ // resolves nothing — both halves are required for either to work.
342
+ const tsProjects = [];
285
343
  if (avail.typescript && langs.typescript) {
286
- try {
287
- const run = runScipTypescript(repoRoot, resolveScipPath('index.scip', repoRoot));
288
- if (run && run.ok) { mergeRead(readScipIndex(run.scipPath)); ranIndexers.push('typescript'); }
289
- } catch { /* degrade to floor for TS */ }
344
+ const dirs = findTsProjectDirs(repoRoot);
345
+ // No tsconfig anywhere → one root run with --infer-tsconfig (prior behavior).
346
+ const targets = dirs.length ? dirs : [''];
347
+ for (const rel of targets) {
348
+ const absRoot = rel ? path.join(repoRoot, rel) : repoRoot;
349
+ // One .scip per project — a shared filename would have each run clobber
350
+ // the last, leaving only the final project's symbols.
351
+ const outName = rel ? `index-ts-${rel.replace(/[^A-Za-z0-9_-]/g, '-')}.scip` : 'index.scip';
352
+ try {
353
+ const run = runScipTypescript(absRoot, resolveScipPath(outName, repoRoot));
354
+ if (run && run.ok) {
355
+ const read = readScipIndex(run.scipPath, rel);
356
+ if (read && read.ok) {
357
+ mergeRead(read);
358
+ tsProjects.push({ dir: rel || '.', files: read.fileRefs.size });
359
+ }
360
+ }
361
+ } catch { /* this project degrades to floor; others still contribute */ }
362
+ }
363
+ if (tsProjects.length) ranIndexers.push('typescript');
290
364
  }
291
365
  if (avail.python && langs.python) {
292
366
  try {
@@ -335,7 +409,17 @@ function buildScipResolver(repoRoot, opts = {}) {
335
409
  return { edges: out, resolved };
336
410
  }
337
411
 
338
- return { ok: true, indexers: ranIndexers, scipPath: resolveScipPath('index.scip', repoRoot), resolveFileEdges };
412
+ // tsProjects reports which tsconfig projects actually contributed refs. A
413
+ // project that ran but yielded 0 files is the signature of this bug class —
414
+ // surfaced rather than swallowed, so a re-index can be inspected.
415
+ return {
416
+ ok: true,
417
+ indexers: ranIndexers,
418
+ tsProjects,
419
+ indexedFiles: fileRefs.size,
420
+ scipPath: resolveScipPath('index.scip', repoRoot),
421
+ resolveFileEdges,
422
+ };
339
423
  }
340
424
 
341
425
  /** No-op resolver used when SCIP is unavailable (floor mode). */
@@ -495,6 +579,7 @@ if (require.main === module) {
495
579
  module.exports = {
496
580
  tryScipUpgrade,
497
581
  buildScipResolver,
582
+ findTsProjectDirs,
498
583
  detectScip,
499
584
  _resetScipCache,
500
585
  isRustCrossCrateEdge,
@@ -118,12 +118,29 @@ function isBuildOutputPath(relPath) {
118
118
  /**
119
119
  * Decode a `.scip` file and build resolution maps.
120
120
  *
121
+ * `pathPrefix` re-roots every path in the index to be repo-root-relative.
122
+ * An indexer reports paths relative to WHERE IT RAN: indexing `server/` emits
123
+ * `src/index.ts`, while the graph stores `server/src/index.ts`. Without the
124
+ * prefix, a nested project's refs key on paths the graph has never heard of and
125
+ * resolve nothing — a second indexer run would LOOK like a fix and change
126
+ * nothing. [RULE] scip-paths-reprefixed-to-repo-root
127
+ *
128
+ * The prefix must apply to BOTH the fileRefs keys AND the funcId values, since
129
+ * a funcId is `relPath#name` — prefixing only the keys leaves every funcId
130
+ * pointing at a path the graph does not have.
131
+ *
121
132
  * @param {string} scipPath absolute path to an index.scip
133
+ * @param {string} [pathPrefix] repo-relative dir the index was produced from
134
+ * (e.g. "server"); "" or "." for the repo root
122
135
  * @returns {{ ok: true, symbolToDef: Map<string,string>,
123
136
  * fileRefs: Map<string, Array<{symbol:string, funcId:string, line:number}>> }
124
137
  * | { ok: false, reason: string }}
125
138
  */
126
- function readScipIndex(scipPath) {
139
+ function readScipIndex(scipPath, pathPrefix) {
140
+ // Normalize: "", ".", "./", "server/" and "server" all mean the same thing.
141
+ const rawPrefix = (pathPrefix || '').replace(/^\.\/+/, '').replace(/\/+$/, '');
142
+ const prefix = (rawPrefix === '' || rawPrefix === '.') ? '' : rawPrefix + '/';
143
+ const reroot = prefix ? (p) => prefix + p : (p) => p;
127
144
  const proto = loadScipProto();
128
145
  if (!proto) {
129
146
  return { ok: false, reason: 'scip-decoder-unavailable' };
@@ -147,22 +164,24 @@ function readScipIndex(scipPath) {
147
164
 
148
165
  // First pass: collect every DEFINITION occurrence → symbol → funcId.
149
166
  for (const doc of docs) {
150
- const relPath = doc.relative_path;
151
- if (!relPath || isBuildOutputPath(relPath)) continue;
167
+ const rawPath = doc.relative_path;
168
+ if (!rawPath || isBuildOutputPath(rawPath)) continue;
169
+ const relPath = reroot(rawPath);
152
170
  for (const occ of doc.occurrences || []) {
153
171
  const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
154
172
  if (!isDef) continue;
155
173
  const name = funcNameFromSymbol(occ.symbol);
156
174
  if (!name) continue;
157
- // funcId = the graph's file#name key
175
+ // funcId = the graph's file#name key (repo-root-relative)
158
176
  symbolToDef.set(occ.symbol, `${relPath}#${name}`);
159
177
  }
160
178
  }
161
179
 
162
180
  // Second pass: collect every REFERENCE occurrence per file, resolved to the def.
163
181
  for (const doc of docs) {
164
- const relPath = doc.relative_path;
165
- if (!relPath || isBuildOutputPath(relPath)) continue;
182
+ const rawPath = doc.relative_path;
183
+ if (!rawPath || isBuildOutputPath(rawPath)) continue;
184
+ const relPath = reroot(rawPath);
166
185
  const refs = [];
167
186
  for (const occ of doc.occurrences || []) {
168
187
  const isDef = (occ.symbol_roles & SYMBOL_ROLE_DEFINITION) !== 0;
package/bin/gsd-t.js CHANGED
@@ -1205,11 +1205,16 @@ function removeInterceptHooks(settingsPath) {
1205
1205
  // session signal, but subagents write those too, so one session with agents
1206
1206
  // looked like several colliding sessions and the guard blocked its own user.
1207
1207
  //
1208
+ // gsd-t-context-meter — M61, script deleted; native /context replaced it.
1209
+ // init() stopped provisioning it then, but install() kept re-registering it,
1210
+ // so every machine still runs it on EVERY tool call: a bash + `npm root -g`
1211
+ // subprocess spawned to look for a file that no longer ships.
1212
+ //
1208
1213
  // Throws on any failure. A retired hook that silently survives keeps blocking
1209
1214
  // edits forever, so "could not remove it" must stop the install loudly rather
1210
1215
  // than report success.
1211
1216
  function removeRetiredHooks(settingsPath) {
1212
- const RETIRED_HOOK_MARKERS = ["gsd-t-worktree-guard"];
1217
+ const RETIRED_HOOK_MARKERS = ["gsd-t-worktree-guard", CONTEXT_METER_HOOK_MARKER];
1213
1218
  const targetPath = settingsPath || SETTINGS_JSON;
1214
1219
  if (!fs.existsSync(targetPath)) return { removed: 0 };
1215
1220
 
@@ -2285,13 +2290,10 @@ async function doInstall(opts = {}) {
2285
2290
  heading("Global Bin Tools (~/.claude/bin/)");
2286
2291
  installGlobalBinTools();
2287
2292
 
2288
- heading("Context Meter (PostToolUse)");
2289
- const cmHook = configureContextMeterHooks(SETTINGS_JSON);
2290
- if (cmHook.installed) {
2291
- if (cmHook.action === "added") success("Context meter PostToolUse hook added");
2292
- else if (cmHook.action === "updated") success("Context meter hook command refreshed");
2293
- else info("Context meter hook already configured");
2294
- }
2293
+ // M61: Context Meter retired (scripts/gsd-t-context-meter.js deleted; native
2294
+ // /context replaces it). The configureContextMeterHooks() call that stood here
2295
+ // re-registered a PostToolUse hook pointing at that deleted script on every
2296
+ // install. removeRetiredHooks() below now strips it instead.
2295
2297
 
2296
2298
  heading("Graph-Intercept (PostToolUse on Grep — M97)");
2297
2299
  const giHook = configureGraphInterceptHook(SETTINGS_JSON);
@@ -2318,7 +2320,7 @@ async function doInstall(opts = {}) {
2318
2320
 
2319
2321
  const retired = removeRetiredHooks(SETTINGS_JSON);
2320
2322
  if (retired.removed > 0) {
2321
- success(`Removed ${retired.removed} retired hook(s) from settings.json (worktree-collision guard — M105)`);
2323
+ success(`Removed ${retired.removed} retired hook(s) from settings.json (worktree-collision guard — M105; context meter — M61)`);
2322
2324
  }
2323
2325
 
2324
2326
  // M105 worktree-collision guard RETIRED (2026-08-08). It detected sessions from
@@ -5610,6 +5612,7 @@ module.exports = {
5610
5612
  installContextMeter,
5611
5613
  configureContextMeterHooks,
5612
5614
  removeContextMeterHook,
5615
+ removeRetiredHooks,
5613
5616
  // M97/M98: intercept hook installers
5614
5617
  configureGraphInterceptHook,
5615
5618
  configureReadInterceptHook,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.16.11",
3
+ "version": "5.17.10",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",