@tekyzinc/gsd-t 2.74.12 → 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 +130 -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 +710 -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,320 @@
1
+ "use strict";
2
+
3
+ const { test } = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const os = require("os");
8
+
9
+ const { parseTranscript } = require("./transcript-parser");
10
+
11
+ /* ----------------------------- fixture helpers ---------------------------- */
12
+
13
+ function mkTmpFile(lines) {
14
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tp-"));
15
+ const file = path.join(dir, "transcript.jsonl");
16
+ const body = (lines || []).map((l) => (typeof l === "string" ? l : JSON.stringify(l))).join("\n");
17
+ fs.writeFileSync(file, body);
18
+ return { dir, file };
19
+ }
20
+
21
+ function cleanup(dir) {
22
+ try {
23
+ fs.rmSync(dir, { recursive: true, force: true });
24
+ } catch (_) {
25
+ /* ignore */
26
+ }
27
+ }
28
+
29
+ /* --------------------------------- tests ---------------------------------- */
30
+
31
+ test("nonexistent file → returns null", async () => {
32
+ const got = await parseTranscript("/definitely/not/a/real/path/xyz-9999.jsonl");
33
+ assert.equal(got, null);
34
+ });
35
+
36
+ test("empty path / non-string → returns null", async () => {
37
+ assert.equal(await parseTranscript(""), null);
38
+ assert.equal(await parseTranscript(null), null);
39
+ assert.equal(await parseTranscript(undefined), null);
40
+ });
41
+
42
+ test("empty file → returns { system:'', messages:[] }", async () => {
43
+ const { dir, file } = mkTmpFile([]);
44
+ try {
45
+ const got = await parseTranscript(file);
46
+ assert.deepEqual(got, { system: "", messages: [] });
47
+ } finally {
48
+ cleanup(dir);
49
+ }
50
+ });
51
+
52
+ test("file with only unknown event types → { system:'', messages:[] }", async () => {
53
+ const { dir, file } = mkTmpFile([
54
+ { type: "summary", foo: "bar" },
55
+ { type: "system", subtype: "hook", hookInfos: [] },
56
+ { type: "attachment", attachment: { name: "x.png" } },
57
+ { type: "permission-mode", permissionMode: "default" },
58
+ { type: "queue-operation", operation: "push" },
59
+ { type: "some-future-type", foo: 1 },
60
+ ]);
61
+ try {
62
+ const got = await parseTranscript(file);
63
+ assert.deepEqual(got, { system: "", messages: [] });
64
+ } finally {
65
+ cleanup(dir);
66
+ }
67
+ });
68
+
69
+ test("normal conversation — string-content user + text assistant", async () => {
70
+ const { dir, file } = mkTmpFile([
71
+ { type: "summary", foo: "bar" },
72
+ {
73
+ type: "user",
74
+ message: { role: "user", content: "Hello, Claude." },
75
+ },
76
+ {
77
+ type: "assistant",
78
+ message: {
79
+ role: "assistant",
80
+ content: [
81
+ { type: "thinking", thinking: "secret reasoning", signature: "abc" },
82
+ { type: "text", text: "Hi there!" },
83
+ ],
84
+ },
85
+ },
86
+ ]);
87
+ try {
88
+ const got = await parseTranscript(file);
89
+ assert.equal(got.system, "");
90
+ assert.equal(got.messages.length, 2);
91
+
92
+ assert.deepEqual(got.messages[0], {
93
+ role: "user",
94
+ content: [{ type: "text", text: "Hello, Claude." }],
95
+ });
96
+
97
+ // thinking block must be stripped
98
+ assert.deepEqual(got.messages[1], {
99
+ role: "assistant",
100
+ content: [{ type: "text", text: "Hi there!" }],
101
+ });
102
+ } finally {
103
+ cleanup(dir);
104
+ }
105
+ });
106
+
107
+ test("tool_use / tool_result pairing by tool_use_id preserved in order", async () => {
108
+ const TOOL_ID = "toolu_01ABC";
109
+ const { dir, file } = mkTmpFile([
110
+ {
111
+ type: "assistant",
112
+ message: {
113
+ role: "assistant",
114
+ content: [
115
+ { type: "text", text: "Let me check." },
116
+ {
117
+ type: "tool_use",
118
+ id: TOOL_ID,
119
+ name: "Read",
120
+ input: { file_path: "/tmp/x" },
121
+ caller: "ignored",
122
+ },
123
+ ],
124
+ },
125
+ },
126
+ {
127
+ type: "user",
128
+ message: {
129
+ role: "user",
130
+ content: [
131
+ {
132
+ type: "tool_result",
133
+ tool_use_id: TOOL_ID,
134
+ content: "file contents here",
135
+ is_error: false,
136
+ },
137
+ ],
138
+ },
139
+ },
140
+ {
141
+ type: "assistant",
142
+ message: { role: "assistant", content: [{ type: "text", text: "Done." }] },
143
+ },
144
+ ]);
145
+ try {
146
+ const got = await parseTranscript(file);
147
+ assert.equal(got.messages.length, 3);
148
+
149
+ // assistant turn: text + tool_use (caller stripped, input preserved)
150
+ assert.equal(got.messages[0].role, "assistant");
151
+ assert.equal(got.messages[0].content.length, 2);
152
+ assert.deepEqual(got.messages[0].content[0], { type: "text", text: "Let me check." });
153
+ assert.deepEqual(got.messages[0].content[1], {
154
+ type: "tool_use",
155
+ id: TOOL_ID,
156
+ name: "Read",
157
+ input: { file_path: "/tmp/x" },
158
+ });
159
+
160
+ // user turn with tool_result, id matches, content normalized to string
161
+ assert.equal(got.messages[1].role, "user");
162
+ assert.equal(got.messages[1].content.length, 1);
163
+ const tr = got.messages[1].content[0];
164
+ assert.equal(tr.type, "tool_result");
165
+ assert.equal(tr.tool_use_id, TOOL_ID);
166
+ assert.equal(tr.content, "file contents here");
167
+ assert.equal(tr.is_error, undefined); // false is not preserved
168
+
169
+ // final assistant
170
+ assert.equal(got.messages[2].role, "assistant");
171
+ assert.deepEqual(got.messages[2].content, [{ type: "text", text: "Done." }]);
172
+ } finally {
173
+ cleanup(dir);
174
+ }
175
+ });
176
+
177
+ test("tool_result with array content (text blocks) is normalized", async () => {
178
+ const { dir, file } = mkTmpFile([
179
+ {
180
+ type: "user",
181
+ message: {
182
+ role: "user",
183
+ content: [
184
+ {
185
+ type: "tool_result",
186
+ tool_use_id: "toolu_02",
187
+ content: [
188
+ { type: "text", text: "line 1" },
189
+ { type: "text", text: "line 2" },
190
+ { type: "weird", value: 3 },
191
+ ],
192
+ },
193
+ ],
194
+ },
195
+ },
196
+ ]);
197
+ try {
198
+ const got = await parseTranscript(file);
199
+ assert.equal(got.messages.length, 1);
200
+ const tr = got.messages[0].content[0];
201
+ assert.equal(tr.type, "tool_result");
202
+ assert.deepEqual(tr.content, [
203
+ { type: "text", text: "line 1" },
204
+ { type: "text", text: "line 2" },
205
+ ]);
206
+ } finally {
207
+ cleanup(dir);
208
+ }
209
+ });
210
+
211
+ test("tool_result with is_error:true preserved", async () => {
212
+ const { dir, file } = mkTmpFile([
213
+ {
214
+ type: "user",
215
+ message: {
216
+ role: "user",
217
+ content: [
218
+ { type: "tool_result", tool_use_id: "toolu_03", content: "oops", is_error: true },
219
+ ],
220
+ },
221
+ },
222
+ ]);
223
+ try {
224
+ const got = await parseTranscript(file);
225
+ assert.equal(got.messages[0].content[0].is_error, true);
226
+ } finally {
227
+ cleanup(dir);
228
+ }
229
+ });
230
+
231
+ test("malformed line in the middle → skipped, surrounding lines kept", async () => {
232
+ const { dir, file } = mkTmpFile([
233
+ JSON.stringify({ type: "user", message: { role: "user", content: "first" } }),
234
+ "{this is not valid json",
235
+ JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "second" }] } }),
236
+ ]);
237
+ try {
238
+ const got = await parseTranscript(file);
239
+ assert.equal(got.messages.length, 2);
240
+ assert.equal(got.messages[0].content[0].text, "first");
241
+ assert.equal(got.messages[1].content[0].text, "second");
242
+ } finally {
243
+ cleanup(dir);
244
+ }
245
+ });
246
+
247
+ test("user/assistant entry with no message field → skipped", async () => {
248
+ const { dir, file } = mkTmpFile([
249
+ { type: "user" },
250
+ { type: "assistant", message: null },
251
+ { type: "user", message: { role: "user", content: "kept" } },
252
+ ]);
253
+ try {
254
+ const got = await parseTranscript(file);
255
+ assert.equal(got.messages.length, 1);
256
+ assert.equal(got.messages[0].content[0].text, "kept");
257
+ } finally {
258
+ cleanup(dir);
259
+ }
260
+ });
261
+
262
+ test("tool_use missing id or name → block dropped", async () => {
263
+ const { dir, file } = mkTmpFile([
264
+ {
265
+ type: "assistant",
266
+ message: {
267
+ role: "assistant",
268
+ content: [
269
+ { type: "text", text: "ok" },
270
+ { type: "tool_use", name: "NoId", input: {} },
271
+ { type: "tool_use", id: "toolu_x" /* no name */ },
272
+ ],
273
+ },
274
+ },
275
+ ]);
276
+ try {
277
+ const got = await parseTranscript(file);
278
+ assert.equal(got.messages.length, 1);
279
+ // only text block survives
280
+ assert.equal(got.messages[0].content.length, 1);
281
+ assert.equal(got.messages[0].content[0].type, "text");
282
+ } finally {
283
+ cleanup(dir);
284
+ }
285
+ });
286
+
287
+ test("blank / whitespace-only lines are skipped", async () => {
288
+ const { dir, file } = mkTmpFile([
289
+ "",
290
+ " ",
291
+ JSON.stringify({ type: "user", message: { role: "user", content: "hi" } }),
292
+ "",
293
+ ]);
294
+ try {
295
+ const got = await parseTranscript(file);
296
+ assert.equal(got.messages.length, 1);
297
+ } finally {
298
+ cleanup(dir);
299
+ }
300
+ });
301
+
302
+ test("message with content array but no recognized blocks → message skipped", async () => {
303
+ const { dir, file } = mkTmpFile([
304
+ {
305
+ type: "assistant",
306
+ message: {
307
+ role: "assistant",
308
+ content: [{ type: "thinking", thinking: "x", signature: "y" }],
309
+ },
310
+ },
311
+ { type: "user", message: { role: "user", content: "survivor" } },
312
+ ]);
313
+ try {
314
+ const got = await parseTranscript(file);
315
+ assert.equal(got.messages.length, 1);
316
+ assert.equal(got.messages[0].content[0].text, "survivor");
317
+ } finally {
318
+ cleanup(dir);
319
+ }
320
+ });