@tekyzinc/gsd-t 2.74.13 → 3.10.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 (69) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +117 -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-unattended-platform.js +381 -0
  8. package/bin/gsd-t-unattended-safety.js +766 -0
  9. package/bin/gsd-t-unattended.js +1259 -0
  10. package/bin/gsd-t.js +723 -19
  11. package/bin/handoff-lock.js +249 -0
  12. package/bin/headless-auto-spawn.js +328 -0
  13. package/bin/model-selector.js +224 -0
  14. package/bin/runway-estimator.js +242 -0
  15. package/bin/token-budget.js +96 -89
  16. package/bin/token-optimizer.js +471 -0
  17. package/bin/token-telemetry.js +246 -0
  18. package/commands/gsd-t-audit.md +3 -3
  19. package/commands/gsd-t-backlog-list.md +38 -0
  20. package/commands/gsd-t-brainstorm.md +3 -3
  21. package/commands/gsd-t-complete-milestone.md +24 -0
  22. package/commands/gsd-t-debug.md +124 -7
  23. package/commands/gsd-t-discuss.md +10 -3
  24. package/commands/gsd-t-doc-ripple.md +32 -4
  25. package/commands/gsd-t-execute.md +107 -52
  26. package/commands/gsd-t-help.md +22 -0
  27. package/commands/gsd-t-integrate.md +67 -4
  28. package/commands/gsd-t-optimization-apply.md +91 -0
  29. package/commands/gsd-t-optimization-reject.md +94 -0
  30. package/commands/gsd-t-partition.md +7 -0
  31. package/commands/gsd-t-pause.md +3 -0
  32. package/commands/gsd-t-plan.md +10 -3
  33. package/commands/gsd-t-prd.md +3 -3
  34. package/commands/gsd-t-quick.md +71 -9
  35. package/commands/gsd-t-reflect.md +3 -7
  36. package/commands/gsd-t-resume.md +86 -1
  37. package/commands/gsd-t-status.md +31 -0
  38. package/commands/gsd-t-test-sync.md +7 -0
  39. package/commands/gsd-t-unattended-stop.md +83 -0
  40. package/commands/gsd-t-unattended-watch.md +290 -0
  41. package/commands/gsd-t-unattended.md +414 -0
  42. package/commands/gsd-t-verify.md +12 -5
  43. package/commands/gsd-t-visualize.md +3 -7
  44. package/commands/gsd-t-wave.md +82 -18
  45. package/docs/GSD-T-README.md +69 -0
  46. package/docs/architecture.md +176 -4
  47. package/docs/infrastructure.md +221 -0
  48. package/docs/methodology.md +44 -0
  49. package/docs/prd-harness-evolution.md +51 -37
  50. package/docs/requirements.md +95 -0
  51. package/docs/unattended-windows-caveats.md +245 -0
  52. package/package.json +2 -2
  53. package/scripts/context-meter/count-tokens-client.js +221 -0
  54. package/scripts/context-meter/count-tokens-client.test.js +308 -0
  55. package/scripts/context-meter/test-injector.js +55 -0
  56. package/scripts/context-meter/threshold.js +88 -0
  57. package/scripts/context-meter/threshold.test.js +255 -0
  58. package/scripts/context-meter/transcript-parser.js +252 -0
  59. package/scripts/context-meter/transcript-parser.test.js +320 -0
  60. package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
  61. package/scripts/gsd-t-context-meter.js +350 -0
  62. package/scripts/gsd-t-context-meter.test.js +417 -0
  63. package/scripts/gsd-t-heartbeat.js +2 -2
  64. package/scripts/gsd-t-statusline.js +23 -8
  65. package/templates/CLAUDE-global.md +17 -1
  66. package/templates/CLAUDE-project.md +26 -6
  67. package/templates/context-meter-config.json +10 -0
  68. package/templates/prompts/README.md +1 -1
  69. package/bin/task-counter.cjs +0 -161
@@ -0,0 +1,252 @@
1
+ /**
2
+ * transcript-parser.js
3
+ *
4
+ * Parses a Claude Code transcript JSONL file and reconstructs it into the
5
+ * `{ system, messages }` shape expected by Anthropic's `count_tokens` endpoint.
6
+ *
7
+ * ------------------------------------------------------------------------
8
+ * Claude Code transcript JSONL format (UNDOCUMENTED — observed empirically
9
+ * from real session files under ~/.claude/projects/<slug>/*.jsonl).
10
+ *
11
+ * Each line is a single JSON object. Observed top-level `type` values:
12
+ *
13
+ * - "user" — user turn. `message` = { role: "user", content: STRING|ARRAY }.
14
+ * When `content` is an array, it contains blocks like
15
+ * { type: "tool_result", tool_use_id, content, is_error? }.
16
+ * `content` on the tool_result block can itself be a string
17
+ * or an array of text/image blocks.
18
+ *
19
+ * - "assistant" — assistant turn. `message` = { role, content: ARRAY, model,
20
+ * id, stop_reason, usage, ... }. `content` blocks observed:
21
+ * { type: "text", text }
22
+ * { type: "thinking", thinking, signature }
23
+ * { type: "tool_use", id, name, input, caller? }
24
+ *
25
+ * - "system" — system/tool hook metadata (subtype, hookInfos, etc.).
26
+ * No message payload; SKIPPED.
27
+ *
28
+ * - "summary" — session metadata; SKIPPED.
29
+ * - "attachment" — file/image attachment metadata; SKIPPED for count purposes
30
+ * (count_tokens only needs role/content message blocks).
31
+ * - "file-history-snapshot" — editor file state; SKIPPED.
32
+ * - "permission-mode" — session flag; SKIPPED.
33
+ * - "queue-operation" — internal; SKIPPED.
34
+ * - "last-prompt" — internal; SKIPPED.
35
+ * - (any other / unknown) — SKIPPED with a no-op (forward-compatible).
36
+ *
37
+ * Example scrubbed shapes (content replaced with "..."):
38
+ *
39
+ * {"type":"user","message":{"role":"user","content":"..."},"uuid":"...","sessionId":"..."}
40
+ *
41
+ * {"type":"user","message":{"role":"user","content":[
42
+ * {"type":"tool_result","tool_use_id":"toolu_01...","content":"...","is_error":false}
43
+ * ]},"uuid":"..."}
44
+ *
45
+ * {"type":"assistant","message":{"role":"assistant","content":[
46
+ * {"type":"text","text":"..."},
47
+ * {"type":"tool_use","id":"toolu_01...","name":"Read","input":{"file_path":"..."}}
48
+ * ],"model":"claude-...","usage":{...}}}
49
+ *
50
+ * ------------------------------------------------------------------------
51
+ * count_tokens request body shape:
52
+ *
53
+ * {
54
+ * "system": "",
55
+ * "messages": [
56
+ * { "role": "user"|"assistant", "content": [ {type, ...}, ... ] },
57
+ * ...
58
+ * ]
59
+ * }
60
+ *
61
+ * The parser:
62
+ * - Streams the file line-by-line via readline (transcripts can be large).
63
+ * - Keeps only user/assistant message content in insertion order.
64
+ * - Filters out assistant `thinking` blocks (not accepted by count_tokens).
65
+ * - Preserves the natural pairing of assistant `tool_use` blocks with their
66
+ * later user `tool_result` blocks — they already appear in chronological
67
+ * order in the JSONL, so pairing is implicit (same tool_use_id).
68
+ * - On unreadable file or a JSON.parse throw at the top-level: returns null.
69
+ * Malformed INDIVIDUAL lines inside an otherwise valid file are skipped.
70
+ *
71
+ * Zero external deps — built-in `fs`, `readline` only.
72
+ *
73
+ * @module scripts/context-meter/transcript-parser
74
+ */
75
+
76
+ "use strict";
77
+
78
+ const fs = require("fs");
79
+ const readline = require("readline");
80
+
81
+ /**
82
+ * Parse a Claude Code transcript JSONL file.
83
+ *
84
+ * @param {string} transcriptPath - absolute path to a Claude Code transcript .jsonl
85
+ * @returns {Promise<{system: string, messages: Array<{role: string, content: Array}>} | null>}
86
+ * Resolves to the reconstructed body, or `null` on unreadable file /
87
+ * catastrophic parse failure. Caller treats `null` as "bail out, fail open".
88
+ */
89
+ async function parseTranscript(transcriptPath) {
90
+ if (typeof transcriptPath !== "string" || transcriptPath.length === 0) {
91
+ return null;
92
+ }
93
+
94
+ // Existence/readability check before opening a stream — readline errors on
95
+ // ENOENT are awkward to catch cleanly.
96
+ try {
97
+ fs.accessSync(transcriptPath, fs.constants.R_OK);
98
+ } catch (_) {
99
+ return null;
100
+ }
101
+
102
+ const messages = [];
103
+ let system = "";
104
+
105
+ let stream;
106
+ try {
107
+ stream = fs.createReadStream(transcriptPath, { encoding: "utf8" });
108
+ } catch (_) {
109
+ return null;
110
+ }
111
+
112
+ const rl = readline.createInterface({
113
+ input: stream,
114
+ crlfDelay: Infinity,
115
+ });
116
+
117
+ try {
118
+ for await (const rawLine of rl) {
119
+ const line = rawLine.trim();
120
+ if (line.length === 0) continue;
121
+
122
+ let evt;
123
+ try {
124
+ evt = JSON.parse(line);
125
+ } catch (_) {
126
+ // Malformed line in the middle — skip, keep going.
127
+ continue;
128
+ }
129
+
130
+ if (!evt || typeof evt !== "object") continue;
131
+
132
+ const type = evt.type;
133
+
134
+ if (type === "user" || type === "assistant") {
135
+ const msg = evt.message;
136
+ if (!msg || typeof msg !== "object") continue;
137
+
138
+ const role = msg.role || type;
139
+ const content = normalizeContent(msg.content, role);
140
+ if (content === null) continue;
141
+
142
+ messages.push({ role, content });
143
+ continue;
144
+ }
145
+
146
+ // All other event types are skipped (summary, system, attachment,
147
+ // file-history-snapshot, permission-mode, queue-operation, last-prompt,
148
+ // and any unknown future type).
149
+ }
150
+ } catch (_) {
151
+ // Stream read error mid-flight — bail out fail-open.
152
+ return null;
153
+ }
154
+
155
+ return { system, messages };
156
+ }
157
+
158
+ /**
159
+ * Normalize a message.content value into the array-of-blocks shape expected
160
+ * by count_tokens. Returns null if the content is unusable.
161
+ *
162
+ * - String content → [{ type: "text", text: content }]
163
+ * - Array content → filtered copy retaining only supported block types
164
+ * - Anything else → null
165
+ *
166
+ * Blocks dropped:
167
+ * - assistant "thinking" blocks (not part of count_tokens message schema)
168
+ * - blocks missing their own `type` field
169
+ *
170
+ * Blocks kept:
171
+ * - text → { type, text }
172
+ * - tool_use → { type, id, name, input }
173
+ * - tool_result→ { type, tool_use_id, content, (is_error) }
174
+ * - image → { type, source } (pass-through)
175
+ * - anything else with a type string → passed through untouched (forward-compat)
176
+ */
177
+ function normalizeContent(content, _role) {
178
+ if (typeof content === "string") {
179
+ return [{ type: "text", text: content }];
180
+ }
181
+
182
+ if (!Array.isArray(content)) return null;
183
+
184
+ const out = [];
185
+ for (const block of content) {
186
+ if (!block || typeof block !== "object") continue;
187
+ const t = block.type;
188
+ if (typeof t !== "string") continue;
189
+
190
+ if (t === "thinking") continue;
191
+
192
+ if (t === "text") {
193
+ if (typeof block.text === "string") {
194
+ out.push({ type: "text", text: block.text });
195
+ }
196
+ continue;
197
+ }
198
+
199
+ if (t === "tool_use") {
200
+ // Must have id, name, input to be meaningful.
201
+ if (typeof block.id !== "string" || typeof block.name !== "string") continue;
202
+ out.push({
203
+ type: "tool_use",
204
+ id: block.id,
205
+ name: block.name,
206
+ input: block.input != null ? block.input : {},
207
+ });
208
+ continue;
209
+ }
210
+
211
+ if (t === "tool_result") {
212
+ if (typeof block.tool_use_id !== "string") continue;
213
+ const resultBlock = {
214
+ type: "tool_result",
215
+ tool_use_id: block.tool_use_id,
216
+ content: normalizeToolResultContent(block.content),
217
+ };
218
+ if (block.is_error === true) resultBlock.is_error = true;
219
+ out.push(resultBlock);
220
+ continue;
221
+ }
222
+
223
+ // Unknown but typed block — pass through minimally (forward-compat).
224
+ out.push({ ...block });
225
+ }
226
+
227
+ if (out.length === 0) return null;
228
+ return out;
229
+ }
230
+
231
+ /**
232
+ * Normalize the `content` field of a tool_result block. It may itself be a
233
+ * string or an array of blocks (text / image). count_tokens accepts either
234
+ * form, but we normalize string to array for consistency.
235
+ */
236
+ function normalizeToolResultContent(content) {
237
+ if (typeof content === "string") return content;
238
+ if (!Array.isArray(content)) return "";
239
+ const out = [];
240
+ for (const b of content) {
241
+ if (!b || typeof b !== "object") continue;
242
+ if (b.type === "text" && typeof b.text === "string") {
243
+ out.push({ type: "text", text: b.text });
244
+ } else if (b.type === "image" && b.source) {
245
+ out.push({ type: "image", source: b.source });
246
+ }
247
+ // other inner types dropped
248
+ }
249
+ return out.length > 0 ? out : "";
250
+ }
251
+
252
+ module.exports = { parseTranscript };
@@ -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
+ });