@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.
- package/CHANGELOG.md +130 -0
- package/README.md +71 -1
- package/bin/advisor-integration.js +93 -0
- package/bin/check-headless-sessions.js +140 -0
- package/bin/context-meter-config.cjs +101 -0
- package/bin/context-meter-config.test.cjs +101 -0
- package/bin/gsd-t.js +710 -16
- package/bin/headless-auto-spawn.js +290 -0
- package/bin/model-selector.js +224 -0
- package/bin/runway-estimator.js +242 -0
- package/bin/token-budget.js +96 -89
- package/bin/token-optimizer.js +471 -0
- package/bin/token-telemetry.js +246 -0
- package/commands/gsd-t-audit.md +3 -3
- package/commands/gsd-t-backlog-list.md +38 -0
- package/commands/gsd-t-brainstorm.md +3 -3
- package/commands/gsd-t-complete-milestone.md +24 -0
- package/commands/gsd-t-debug.md +124 -7
- package/commands/gsd-t-discuss.md +10 -3
- package/commands/gsd-t-doc-ripple.md +32 -4
- package/commands/gsd-t-execute.md +107 -52
- package/commands/gsd-t-help.md +19 -0
- package/commands/gsd-t-integrate.md +67 -4
- package/commands/gsd-t-optimization-apply.md +91 -0
- package/commands/gsd-t-optimization-reject.md +94 -0
- package/commands/gsd-t-partition.md +7 -0
- package/commands/gsd-t-pause.md +3 -0
- package/commands/gsd-t-plan.md +10 -3
- package/commands/gsd-t-prd.md +3 -3
- package/commands/gsd-t-quick.md +71 -9
- package/commands/gsd-t-reflect.md +3 -7
- package/commands/gsd-t-resume.md +36 -0
- package/commands/gsd-t-status.md +31 -0
- package/commands/gsd-t-test-sync.md +7 -0
- package/commands/gsd-t-verify.md +12 -5
- package/commands/gsd-t-visualize.md +3 -7
- package/commands/gsd-t-wave.md +82 -18
- package/docs/GSD-T-README.md +52 -0
- package/docs/architecture.md +95 -0
- package/docs/infrastructure.md +117 -0
- package/docs/methodology.md +36 -0
- package/docs/prd-harness-evolution.md +51 -37
- package/docs/requirements.md +66 -0
- package/package.json +1 -1
- package/scripts/context-meter/count-tokens-client.js +221 -0
- package/scripts/context-meter/count-tokens-client.test.js +308 -0
- package/scripts/context-meter/test-injector.js +55 -0
- package/scripts/context-meter/threshold.js +88 -0
- package/scripts/context-meter/threshold.test.js +255 -0
- package/scripts/context-meter/transcript-parser.js +252 -0
- package/scripts/context-meter/transcript-parser.test.js +320 -0
- package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
- package/scripts/gsd-t-context-meter.js +350 -0
- package/scripts/gsd-t-context-meter.test.js +417 -0
- package/scripts/gsd-t-heartbeat.js +2 -2
- package/scripts/gsd-t-statusline.js +23 -8
- package/templates/CLAUDE-global.md +5 -1
- package/templates/CLAUDE-project.md +26 -6
- package/templates/context-meter-config.json +10 -0
- package/templates/prompts/README.md +1 -1
- 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
|
+
});
|