@tekyzinc/gsd-t 2.74.13 → 2.76.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +71 -1
  3. package/bin/advisor-integration.js +93 -0
  4. package/bin/check-headless-sessions.js +140 -0
  5. package/bin/context-meter-config.cjs +101 -0
  6. package/bin/context-meter-config.test.cjs +101 -0
  7. package/bin/gsd-t.js +709 -16
  8. package/bin/headless-auto-spawn.js +290 -0
  9. package/bin/model-selector.js +224 -0
  10. package/bin/runway-estimator.js +242 -0
  11. package/bin/token-budget.js +96 -89
  12. package/bin/token-optimizer.js +471 -0
  13. package/bin/token-telemetry.js +246 -0
  14. package/commands/gsd-t-audit.md +3 -3
  15. package/commands/gsd-t-backlog-list.md +38 -0
  16. package/commands/gsd-t-brainstorm.md +3 -3
  17. package/commands/gsd-t-complete-milestone.md +24 -0
  18. package/commands/gsd-t-debug.md +124 -7
  19. package/commands/gsd-t-discuss.md +10 -3
  20. package/commands/gsd-t-doc-ripple.md +32 -4
  21. package/commands/gsd-t-execute.md +107 -52
  22. package/commands/gsd-t-help.md +19 -0
  23. package/commands/gsd-t-integrate.md +67 -4
  24. package/commands/gsd-t-optimization-apply.md +91 -0
  25. package/commands/gsd-t-optimization-reject.md +94 -0
  26. package/commands/gsd-t-partition.md +7 -0
  27. package/commands/gsd-t-pause.md +3 -0
  28. package/commands/gsd-t-plan.md +10 -3
  29. package/commands/gsd-t-prd.md +3 -3
  30. package/commands/gsd-t-quick.md +71 -9
  31. package/commands/gsd-t-reflect.md +3 -7
  32. package/commands/gsd-t-resume.md +36 -0
  33. package/commands/gsd-t-status.md +31 -0
  34. package/commands/gsd-t-test-sync.md +7 -0
  35. package/commands/gsd-t-verify.md +12 -5
  36. package/commands/gsd-t-visualize.md +3 -7
  37. package/commands/gsd-t-wave.md +82 -18
  38. package/docs/GSD-T-README.md +52 -0
  39. package/docs/architecture.md +95 -0
  40. package/docs/infrastructure.md +117 -0
  41. package/docs/methodology.md +36 -0
  42. package/docs/prd-harness-evolution.md +51 -37
  43. package/docs/requirements.md +66 -0
  44. package/package.json +1 -1
  45. package/scripts/context-meter/count-tokens-client.js +221 -0
  46. package/scripts/context-meter/count-tokens-client.test.js +308 -0
  47. package/scripts/context-meter/test-injector.js +55 -0
  48. package/scripts/context-meter/threshold.js +88 -0
  49. package/scripts/context-meter/threshold.test.js +255 -0
  50. package/scripts/context-meter/transcript-parser.js +252 -0
  51. package/scripts/context-meter/transcript-parser.test.js +320 -0
  52. package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
  53. package/scripts/gsd-t-context-meter.js +350 -0
  54. package/scripts/gsd-t-context-meter.test.js +417 -0
  55. package/scripts/gsd-t-heartbeat.js +2 -2
  56. package/scripts/gsd-t-statusline.js +23 -8
  57. package/templates/CLAUDE-global.md +5 -1
  58. package/templates/CLAUDE-project.md +26 -6
  59. package/templates/context-meter-config.json +10 -0
  60. package/templates/prompts/README.md +1 -1
  61. package/bin/task-counter.cjs +0 -161
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gsd-t-context-meter.js
4
+ *
5
+ * PostToolUse hook entry point for GSD-T's Context Meter (M34).
6
+ *
7
+ * Wires together:
8
+ * - bin/context-meter-config.cjs (loadConfig)
9
+ * - scripts/context-meter/transcript-parser.js (parseTranscript)
10
+ * - scripts/context-meter/count-tokens-client.js (countTokens)
11
+ * - scripts/context-meter/threshold.js (computePct/bandFor/buildAdditionalContext)
12
+ *
13
+ * Contract: .gsd-t/contracts/context-meter-contract.md
14
+ *
15
+ * CRITICAL INVARIANT — FAIL OPEN:
16
+ * Every failure path resolves to `{}` on stdout and exits 0. This hook must
17
+ * NEVER throw, NEVER exit non-zero, and NEVER log message content. The
18
+ * entire `runMeter` body is wrapped in a try/catch that swallows anything
19
+ * unexpected and returns `{}` — the same shape the CLI shim emits on parse
20
+ * failure of its own stdin. See contract rule #1.
21
+ *
22
+ * Testability:
23
+ * `runMeter({ payload, projectRoot, env, clock?, baseUrl?, _parseTranscript?,
24
+ * _countTokens?, _loadConfig? })` is the pure async core. Tests
25
+ * fabricate payloads and inject stubs; production code uses only the CLI
26
+ * shim at the bottom of the file (runs when `require.main === module`).
27
+ *
28
+ * @module scripts/gsd-t-context-meter
29
+ */
30
+
31
+ "use strict";
32
+
33
+ const fs = require("fs");
34
+ const path = require("path");
35
+
36
+ const { loadConfig: realLoadConfig } = require("../bin/context-meter-config.cjs");
37
+ const { parseTranscript: realParseTranscript } = require("./context-meter/transcript-parser");
38
+ const { countTokens: realCountTokens } = require("./context-meter/count-tokens-client");
39
+ const { computePct, bandFor, buildAdditionalContext } = require("./context-meter/threshold");
40
+
41
+ const STATE_VERSION = 1;
42
+ const MODEL_ID = "claude-opus-4-6";
43
+
44
+ /* ─────────────────────────── state file helpers ─────────────────────────── */
45
+
46
+ /**
47
+ * Read the current state file. Returns a fresh default on missing file or
48
+ * corruption (never throws).
49
+ */
50
+ function readState(statePath) {
51
+ try {
52
+ const raw = fs.readFileSync(statePath, "utf8");
53
+ const parsed = JSON.parse(raw);
54
+ if (!parsed || typeof parsed !== "object" || parsed.version !== STATE_VERSION) {
55
+ return defaultState();
56
+ }
57
+ return parsed;
58
+ } catch (_) {
59
+ return defaultState();
60
+ }
61
+ }
62
+
63
+ function defaultState() {
64
+ return {
65
+ version: STATE_VERSION,
66
+ timestamp: null,
67
+ inputTokens: 0,
68
+ modelWindowSize: 0,
69
+ pct: 0,
70
+ threshold: "normal",
71
+ checkCount: 0,
72
+ lastError: null,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Atomically write state to disk: write to `{statePath}.tmp` → rename to
78
+ * `{statePath}`. Creates parent directories as needed. Never throws.
79
+ */
80
+ function writeStateAtomic(statePath, state) {
81
+ try {
82
+ const dir = path.dirname(statePath);
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ const tmp = `${statePath}.tmp`;
85
+ fs.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
86
+ fs.renameSync(tmp, statePath);
87
+ } catch (_) {
88
+ /* fail open — logging will also be best-effort */
89
+ }
90
+ }
91
+
92
+ /* ──────────────────────────── logging helper ─────────────────────────── */
93
+
94
+ /**
95
+ * Append a short line-based diagnostic to logPath. Line format:
96
+ * "{ISO-timestamp} {LEVEL} {category} {short-detail}"
97
+ *
98
+ * NEVER logs transcript content, message text, or API request bodies.
99
+ */
100
+ function appendLog(logPath, level, category, detail, clock) {
101
+ try {
102
+ const dir = path.dirname(logPath);
103
+ fs.mkdirSync(dir, { recursive: true });
104
+ const ts = (clock ? clock() : new Date()).toISOString();
105
+ const line = `${ts} ${level} ${category} ${detail || ""}\n`;
106
+ fs.appendFileSync(logPath, line, "utf8");
107
+ } catch (_) {
108
+ /* logging failure must not affect fail-open behavior */
109
+ }
110
+ }
111
+
112
+ /* ──────────────────────── core: runMeter() ──────────────────────── */
113
+
114
+ /**
115
+ * Run the context meter once.
116
+ *
117
+ * @param {object} opts
118
+ * @param {object} opts.payload parsed PostToolUse JSON (with transcript_path)
119
+ * @param {string} opts.projectRoot normally process.cwd()
120
+ * @param {object} opts.env normally process.env
121
+ * @param {Function} [opts.clock] optional () => Date (test seam)
122
+ * @param {string} [opts.baseUrl] optional countTokens _baseUrl override (test seam)
123
+ * @param {Function} [opts._loadConfig] optional loadConfig stub (test seam)
124
+ * @param {Function} [opts._parseTranscript] optional parseTranscript stub (test seam)
125
+ * @param {Function} [opts._countTokens] optional countTokens stub (test seam)
126
+ * @returns {Promise<object>} `{}` or `{ additionalContext: "..." }`
127
+ */
128
+ async function runMeter(opts) {
129
+ // Outer try/catch guarantees we NEVER throw. Any unexpected error → `{}`.
130
+ try {
131
+ const {
132
+ payload,
133
+ projectRoot,
134
+ env,
135
+ clock,
136
+ baseUrl,
137
+ _loadConfig = realLoadConfig,
138
+ _parseTranscript = realParseTranscript,
139
+ _countTokens = realCountTokens,
140
+ } = opts || {};
141
+
142
+ const root = projectRoot || process.cwd();
143
+ const envObj = env || {};
144
+ const now = () => (clock ? clock() : new Date());
145
+
146
+ // 1. Load config (missing file → defaults). Any throw → bail out fail-open.
147
+ let cfg;
148
+ try {
149
+ cfg = _loadConfig(root);
150
+ } catch (_) {
151
+ return {};
152
+ }
153
+
154
+ const statePath = path.isAbsolute(cfg.statePath)
155
+ ? cfg.statePath
156
+ : path.join(root, cfg.statePath);
157
+ const logPath = path.isAbsolute(cfg.logPath)
158
+ ? cfg.logPath
159
+ : path.join(root, cfg.logPath);
160
+
161
+ // 2. Read (possibly corrupt) state, increment checkCount immediately.
162
+ const state = readState(statePath);
163
+ state.version = STATE_VERSION;
164
+ state.modelWindowSize = cfg.modelWindowSize;
165
+ state.checkCount = (Number.isInteger(state.checkCount) ? state.checkCount : 0) + 1;
166
+
167
+ // 3. Check-frequency gate: not our turn → persist counter and bail out `{}`.
168
+ if (state.checkCount % cfg.checkFrequency !== 0) {
169
+ writeStateAtomic(statePath, state);
170
+ return {};
171
+ }
172
+
173
+ // 4. Extract transcript_path from payload (fail open if missing).
174
+ const transcriptPath =
175
+ payload && typeof payload === "object" && typeof payload.transcript_path === "string"
176
+ ? payload.transcript_path
177
+ : null;
178
+
179
+ if (!transcriptPath) {
180
+ state.lastError = {
181
+ code: "no_transcript",
182
+ message: "payload missing transcript_path",
183
+ timestamp: now().toISOString(),
184
+ };
185
+ writeStateAtomic(statePath, state);
186
+ appendLog(logPath, "ERROR", "no_transcript", "payload missing transcript_path", clock);
187
+ return {};
188
+ }
189
+
190
+ // 5. API key env var check.
191
+ const apiKey = envObj[cfg.apiKeyEnvVar];
192
+ if (typeof apiKey !== "string" || apiKey.length === 0) {
193
+ state.lastError = {
194
+ code: "missing_key",
195
+ message: `env var ${cfg.apiKeyEnvVar} not set`,
196
+ timestamp: now().toISOString(),
197
+ };
198
+ writeStateAtomic(statePath, state);
199
+ appendLog(logPath, "ERROR", "missing_key", `env var ${cfg.apiKeyEnvVar} unset`, clock);
200
+ return {};
201
+ }
202
+
203
+ // 6. Parse transcript (streaming, async). null → bail out.
204
+ let parsed;
205
+ try {
206
+ parsed = await _parseTranscript(transcriptPath);
207
+ } catch (_) {
208
+ parsed = null;
209
+ }
210
+ if (!parsed || !Array.isArray(parsed.messages)) {
211
+ state.lastError = {
212
+ code: "parse_failure",
213
+ message: `parseTranscript returned null for ${path.basename(transcriptPath)}`,
214
+ timestamp: now().toISOString(),
215
+ };
216
+ writeStateAtomic(statePath, state);
217
+ appendLog(
218
+ logPath,
219
+ "ERROR",
220
+ "parse_failure",
221
+ `${path.basename(transcriptPath)}`,
222
+ clock
223
+ );
224
+ return {};
225
+ }
226
+
227
+ // 7. Call count_tokens. null → fail open (keep prior inputTokens? reset to 0?)
228
+ // CHOICE: reset inputTokens to 0 on failure to avoid stale-reading-based
229
+ // false-positive threshold trips. lastError still records the failure so
230
+ // consumers can see we didn't get a fresh count.
231
+ let tokenResp;
232
+ try {
233
+ tokenResp = await _countTokens({
234
+ apiKey,
235
+ model: MODEL_ID,
236
+ system: parsed.system || "",
237
+ messages: parsed.messages,
238
+ timeoutMs: cfg.timeoutMs,
239
+ _baseUrl: baseUrl,
240
+ });
241
+ } catch (_) {
242
+ tokenResp = null;
243
+ }
244
+
245
+ if (!tokenResp || !Number.isFinite(tokenResp.inputTokens)) {
246
+ state.inputTokens = 0;
247
+ state.pct = 0;
248
+ state.threshold = "normal";
249
+ state.timestamp = now().toISOString();
250
+ state.lastError = {
251
+ code: "api_error",
252
+ message: "count_tokens returned null",
253
+ timestamp: state.timestamp,
254
+ };
255
+ writeStateAtomic(statePath, state);
256
+ appendLog(logPath, "ERROR", "api_error", "count_tokens null", clock);
257
+ return {};
258
+ }
259
+
260
+ // 8. Success path — compute pct, band, possibly emit additionalContext.
261
+ const pct = computePct({
262
+ inputTokens: tokenResp.inputTokens,
263
+ modelWindowSize: cfg.modelWindowSize,
264
+ });
265
+ const band = bandFor(pct);
266
+
267
+ state.inputTokens = tokenResp.inputTokens;
268
+ state.pct = pct;
269
+ state.threshold = band;
270
+ state.timestamp = now().toISOString();
271
+ state.lastError = null;
272
+ writeStateAtomic(statePath, state);
273
+ appendLog(
274
+ logPath,
275
+ "INFO",
276
+ "measure",
277
+ `tokens=${tokenResp.inputTokens} pct=${pct.toFixed(1)} band=${band}`,
278
+ clock
279
+ );
280
+
281
+ const additionalContext = buildAdditionalContext({
282
+ pct,
283
+ modelWindowSize: cfg.modelWindowSize,
284
+ thresholdPct: cfg.thresholdPct,
285
+ });
286
+ if (additionalContext) {
287
+ return { additionalContext };
288
+ }
289
+ return {};
290
+ } catch (_) {
291
+ // Absolute safety net — fail open no matter what.
292
+ return {};
293
+ }
294
+ }
295
+
296
+ /* ──────────────────────────── CLI shim ──────────────────────────── */
297
+
298
+ function readStdin() {
299
+ return new Promise((resolve) => {
300
+ let data = "";
301
+ try {
302
+ if (process.stdin.isTTY) {
303
+ resolve("");
304
+ return;
305
+ }
306
+ process.stdin.setEncoding("utf8");
307
+ process.stdin.on("data", (chunk) => {
308
+ data += chunk;
309
+ });
310
+ process.stdin.on("end", () => resolve(data));
311
+ process.stdin.on("error", () => resolve(""));
312
+ } catch (_) {
313
+ resolve("");
314
+ }
315
+ });
316
+ }
317
+
318
+ async function main() {
319
+ let payload = null;
320
+ try {
321
+ const raw = await readStdin();
322
+ payload = raw ? JSON.parse(raw) : null;
323
+ } catch (_) {
324
+ payload = null;
325
+ }
326
+
327
+ let out = {};
328
+ try {
329
+ out = await runMeter({
330
+ payload: payload || {},
331
+ projectRoot: process.cwd(),
332
+ env: process.env,
333
+ });
334
+ } catch (_) {
335
+ out = {};
336
+ }
337
+
338
+ try {
339
+ process.stdout.write(JSON.stringify(out || {}));
340
+ } catch (_) {
341
+ process.stdout.write("{}");
342
+ }
343
+ process.exit(0);
344
+ }
345
+
346
+ module.exports = { runMeter, readState, writeStateAtomic, defaultState };
347
+
348
+ if (require.main === module) {
349
+ main();
350
+ }