throughline 0.3.23 → 0.3.25

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 (111) hide show
  1. package/.claude/commands/tl-trim.md +42 -0
  2. package/.codex-sidecar.yml +62 -0
  3. package/CHANGELOG.md +583 -0
  4. package/README.ja.md +42 -5
  5. package/README.md +400 -23
  6. package/bin/throughline.mjs +168 -4
  7. package/codex/skills/throughline/SKILL.md +157 -0
  8. package/codex/skills/throughline/agents/openai.yaml +7 -0
  9. package/docs/INHERITANCE_ON_CLEAR_ONLY.md +146 -0
  10. package/docs/L1_L2_L3_REDESIGN.md +415 -0
  11. package/docs/PUBLIC_RELEASE_PLAN.md +184 -0
  12. package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
  13. package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
  14. package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
  15. package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
  16. package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
  17. package/docs/archive/CONCEPT.md +476 -0
  18. package/docs/archive/EXPERIMENT.md +371 -0
  19. package/docs/archive/README.md +22 -0
  20. package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
  21. package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
  22. package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
  23. package/docs/throughline-handoff-context.example.json +57 -0
  24. package/docs/throughline-rollback-context-trim-insight.md +455 -0
  25. package/package.json +6 -2
  26. package/src/cli/codex-capture.mjs +95 -0
  27. package/src/cli/codex-handoff-model-smoke.mjs +292 -0
  28. package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
  29. package/src/cli/codex-handoff-smoke.mjs +163 -0
  30. package/src/cli/codex-handoff-smoke.test.mjs +149 -0
  31. package/src/cli/codex-handoff-start.mjs +291 -0
  32. package/src/cli/codex-handoff-start.test.mjs +194 -0
  33. package/src/cli/codex-hook.mjs +276 -0
  34. package/src/cli/codex-hook.test.mjs +293 -0
  35. package/src/cli/codex-host-primitive-audit.mjs +110 -0
  36. package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
  37. package/src/cli/codex-restore-smoke.mjs +357 -0
  38. package/src/cli/codex-restore-source-audit.mjs +304 -0
  39. package/src/cli/codex-resume.mjs +138 -0
  40. package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
  41. package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
  42. package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
  43. package/src/cli/codex-sidecar-dry-run.mjs +85 -0
  44. package/src/cli/codex-summarize.mjs +224 -0
  45. package/src/cli/codex-threads.mjs +89 -0
  46. package/src/cli/codex-visibility-smoke.mjs +196 -0
  47. package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
  48. package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
  49. package/src/cli/doctor.mjs +503 -1
  50. package/src/cli/doctor.test.mjs +542 -3
  51. package/src/cli/handoff-preview.mjs +78 -0
  52. package/src/cli/help.test.mjs +64 -0
  53. package/src/cli/install.mjs +227 -4
  54. package/src/cli/install.test.mjs +207 -4
  55. package/src/cli/trim.mjs +564 -0
  56. package/src/codex-app-server.mjs +1816 -0
  57. package/src/codex-app-server.test.mjs +512 -0
  58. package/src/codex-auto-refresh.mjs +194 -0
  59. package/src/codex-auto-refresh.test.mjs +182 -0
  60. package/src/codex-capture.mjs +235 -0
  61. package/src/codex-capture.test.mjs +393 -0
  62. package/src/codex-handoff-model-smoke.mjs +114 -0
  63. package/src/codex-handoff-model-smoke.test.mjs +89 -0
  64. package/src/codex-handoff-smoke.mjs +124 -0
  65. package/src/codex-handoff-smoke.test.mjs +103 -0
  66. package/src/codex-handoff.mjs +331 -0
  67. package/src/codex-handoff.test.mjs +220 -0
  68. package/src/codex-host-primitive-audit.mjs +374 -0
  69. package/src/codex-host-primitive-audit.test.mjs +208 -0
  70. package/src/codex-restore-smoke.test.mjs +639 -0
  71. package/src/codex-restore-source-audit.mjs +1348 -0
  72. package/src/codex-restore-source-audit.test.mjs +623 -0
  73. package/src/codex-resume.test.mjs +242 -0
  74. package/src/codex-rollout-memory.mjs +711 -0
  75. package/src/codex-rollout-memory.test.mjs +610 -0
  76. package/src/codex-sidecar-cli.test.mjs +75 -0
  77. package/src/codex-sidecar.mjs +246 -0
  78. package/src/codex-sidecar.test.mjs +172 -0
  79. package/src/codex-summarize.test.mjs +143 -0
  80. package/src/codex-thread-identity.mjs +23 -0
  81. package/src/codex-thread-index.mjs +173 -0
  82. package/src/codex-thread-index.test.mjs +164 -0
  83. package/src/codex-usage.mjs +110 -0
  84. package/src/codex-usage.test.mjs +140 -0
  85. package/src/codex-visibility-smoke.test.mjs +222 -0
  86. package/src/codex-vscode-restore-smoke.mjs +206 -0
  87. package/src/codex-vscode-restore-smoke.test.mjs +325 -0
  88. package/src/codex-vscode-rollback-smoke.mjs +90 -0
  89. package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
  90. package/src/db-schema.test.mjs +97 -0
  91. package/src/haiku-summarizer.mjs +267 -26
  92. package/src/haiku-summarizer.test.mjs +282 -0
  93. package/src/handoff-preview.test.mjs +108 -0
  94. package/src/handoff-record.mjs +294 -0
  95. package/src/handoff-record.test.mjs +226 -0
  96. package/src/hook-entrypoints.test.mjs +326 -0
  97. package/src/package-files.test.mjs +19 -0
  98. package/src/prompt-submit.mjs +9 -6
  99. package/src/resume-context.mjs +44 -140
  100. package/src/resume-context.test.mjs +172 -0
  101. package/src/session-start.mjs +8 -5
  102. package/src/state-file.mjs +50 -6
  103. package/src/state-file.test.mjs +50 -0
  104. package/src/token-monitor.mjs +14 -10
  105. package/src/token-monitor.test.mjs +27 -0
  106. package/src/trim-cli.test.mjs +1584 -0
  107. package/src/trim-model.mjs +584 -0
  108. package/src/trim-model.test.mjs +568 -0
  109. package/src/turn-processor.mjs +17 -10
  110. package/src/vscode-task.mjs +94 -6
  111. package/src/vscode-task.test.mjs +186 -6
@@ -0,0 +1,226 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { buildHandoffRecord } from './handoff-record.mjs';
5
+
6
+ function makeDb() {
7
+ const db = new DatabaseSync(':memory:');
8
+ db.exec(`
9
+ CREATE TABLE sessions (
10
+ session_id TEXT PRIMARY KEY,
11
+ project_path TEXT NOT NULL,
12
+ status TEXT NOT NULL DEFAULT 'active',
13
+ created_at INTEGER NOT NULL,
14
+ updated_at INTEGER NOT NULL,
15
+ merged_into TEXT
16
+ );
17
+ CREATE TABLE skeletons (
18
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
19
+ session_id TEXT NOT NULL,
20
+ origin_session_id TEXT,
21
+ turn_number INTEGER NOT NULL,
22
+ role TEXT NOT NULL,
23
+ summary TEXT NOT NULL,
24
+ created_at INTEGER NOT NULL
25
+ );
26
+ CREATE TABLE bodies (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ session_id TEXT NOT NULL,
29
+ origin_session_id TEXT NOT NULL,
30
+ turn_number INTEGER NOT NULL,
31
+ role TEXT NOT NULL,
32
+ text TEXT NOT NULL,
33
+ token_count INTEGER,
34
+ created_at INTEGER NOT NULL
35
+ );
36
+ CREATE TABLE details (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ session_id TEXT NOT NULL,
39
+ origin_session_id TEXT,
40
+ turn_number INTEGER,
41
+ tool_name TEXT NOT NULL,
42
+ input_text TEXT,
43
+ output_text TEXT,
44
+ token_count INTEGER NOT NULL DEFAULT 0,
45
+ created_at INTEGER NOT NULL,
46
+ kind TEXT,
47
+ source_id TEXT
48
+ );
49
+ `);
50
+ return db;
51
+ }
52
+
53
+ function insertSession(db, sessionId = 'new') {
54
+ db.prepare(
55
+ `INSERT INTO sessions (session_id, project_path, status, created_at, updated_at)
56
+ VALUES (?, '/repo', 'active', 1, 2)`,
57
+ ).run(sessionId);
58
+ }
59
+
60
+ function insertSkeleton(db, row) {
61
+ db.prepare(
62
+ `INSERT INTO skeletons (session_id, origin_session_id, turn_number, role, summary, created_at)
63
+ VALUES (?, ?, ?, ?, ?, ?)`,
64
+ ).run(row.session, row.origin, row.turn, row.role, row.summary, row.createdAt);
65
+ }
66
+
67
+ function insertBody(db, row) {
68
+ db.prepare(
69
+ `INSERT INTO bodies (session_id, origin_session_id, turn_number, role, text, token_count, created_at)
70
+ VALUES (?, ?, ?, ?, ?, 1, ?)`,
71
+ ).run(row.session, row.origin, row.turn, row.role, row.text, row.createdAt);
72
+ }
73
+
74
+ function insertDetail(db, row) {
75
+ db.prepare(
76
+ `INSERT INTO details
77
+ (session_id, origin_session_id, turn_number, tool_name, input_text, output_text,
78
+ token_count, created_at, kind, source_id)
79
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`,
80
+ ).run(
81
+ row.session,
82
+ row.origin,
83
+ row.turn,
84
+ row.toolName,
85
+ row.inputText ?? null,
86
+ row.outputText ?? null,
87
+ row.createdAt,
88
+ row.kind,
89
+ row.sourceId ?? null,
90
+ );
91
+ }
92
+
93
+ test('buildHandoffRecord: returns stable projection with memo, L1, L2, thinking, and L3 refs', () => {
94
+ const db = makeDb();
95
+ insertSession(db);
96
+ insertSkeleton(db, {
97
+ session: 'new',
98
+ origin: 'old',
99
+ turn: 1,
100
+ role: 'assistant',
101
+ summary: 'old summary',
102
+ createdAt: 1000,
103
+ });
104
+ insertBody(db, {
105
+ session: 'new',
106
+ origin: 'old',
107
+ turn: 2,
108
+ role: 'user',
109
+ text: 'recent user body',
110
+ createdAt: 2000,
111
+ });
112
+ insertBody(db, {
113
+ session: 'new',
114
+ origin: 'old',
115
+ turn: 2,
116
+ role: 'assistant',
117
+ text: 'recent assistant body',
118
+ createdAt: 2100,
119
+ });
120
+ insertDetail(db, {
121
+ session: 'new',
122
+ origin: 'old',
123
+ turn: 2,
124
+ kind: 'thinking',
125
+ toolName: 'thinking',
126
+ outputText: 'latest thought',
127
+ createdAt: 2200,
128
+ sourceId: 'asst:thinking:0',
129
+ });
130
+ insertDetail(db, {
131
+ session: 'new',
132
+ origin: 'old',
133
+ turn: 2,
134
+ kind: 'tool_input',
135
+ toolName: 'Bash',
136
+ inputText: '{"command":"pwd"}',
137
+ createdAt: 2300,
138
+ sourceId: 'toolu_1',
139
+ });
140
+
141
+ const record = buildHandoffRecord(db, {
142
+ sessionId: 'new',
143
+ isInheritance: true,
144
+ inflightMemo: 'Next: continue',
145
+ });
146
+
147
+ assert.ok(record);
148
+ assert.equal(record.kind, 'handoff_record');
149
+ assert.equal(record.version, 1);
150
+ assert.equal(record.session.id, 'new');
151
+ assert.equal(record.session.projectPath, '/repo');
152
+ assert.equal(record.source.adapter, 'claude');
153
+ assert.equal(record.source.inheritance, true);
154
+ assert.deepEqual(record.source.originSessionIds, ['old']);
155
+ assert.equal(record.intent, 'continue implementation');
156
+ assert.ok(record.constraints.some((c) => c.includes('preserve existing Claude Code')));
157
+ assert.equal(record.memory.inflightMemo, 'Next: continue');
158
+ assert.equal(record.memory.latestThinking[0].text, 'latest thought');
159
+ assert.equal(record.memory.latestThinking[0].sourceId, 'asst:thinking:0');
160
+ assert.equal(record.memory.l1Summaries[0].summary, 'old summary');
161
+ assert.deepEqual(record.memory.recentBodies.map((r) => r.role), ['user', 'assistant']);
162
+ assert.equal(record.references.l3.length, 2);
163
+ assert.deepEqual(record.references.l3.map((r) => r.kind), ['thinking', 'tool_input']);
164
+ assert.match(record.references.l3[0].detailCommand, /^throughline detail \d{2}:\d{2}:\d{2}$/);
165
+ assert.equal(record.stats.preservedContextRows, 3);
166
+ });
167
+
168
+ test('buildHandoffRecord: excludes current origin rows', () => {
169
+ const db = makeDb();
170
+ insertSession(db);
171
+ insertBody(db, {
172
+ session: 'new',
173
+ origin: 'old',
174
+ turn: 1,
175
+ role: 'assistant',
176
+ text: 'old body',
177
+ createdAt: 1000,
178
+ });
179
+ insertBody(db, {
180
+ session: 'new',
181
+ origin: 'new',
182
+ turn: 1,
183
+ role: 'assistant',
184
+ text: 'current body',
185
+ createdAt: 2000,
186
+ });
187
+
188
+ const record = buildHandoffRecord(db, {
189
+ sessionId: 'new',
190
+ excludeOriginId: 'new',
191
+ });
192
+
193
+ assert.ok(record);
194
+ assert.deepEqual(record.source.originSessionIds, ['old']);
195
+ assert.deepEqual(record.memory.recentBodies.map((r) => r.text), ['old body']);
196
+ });
197
+
198
+ test('buildHandoffRecord: keeps L2 bodies from the latest 20 turns, not 40 turns', () => {
199
+ const db = makeDb();
200
+ insertSession(db);
201
+ for (let turn = 1; turn <= 22; turn++) {
202
+ insertBody(db, {
203
+ session: 'new',
204
+ origin: 'new',
205
+ turn,
206
+ role: 'assistant',
207
+ text: `body ${turn}`,
208
+ createdAt: turn * 1000,
209
+ });
210
+ }
211
+
212
+ const record = buildHandoffRecord(db, { sessionId: 'new' });
213
+
214
+ assert.ok(record);
215
+ assert.equal(record.memory.recentBodies.length, 20);
216
+ assert.deepEqual(
217
+ record.memory.recentBodies.map((r) => r.turnNumber),
218
+ Array.from({ length: 20 }, (_, index) => index + 3),
219
+ );
220
+ });
221
+
222
+ test('buildHandoffRecord: returns null when no projected memory exists', () => {
223
+ const db = makeDb();
224
+ insertSession(db);
225
+ assert.equal(buildHandoffRecord(db, { sessionId: 'empty' }), null);
226
+ });
@@ -0,0 +1,326 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import {
5
+ existsSync,
6
+ mkdtempSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { dirname, join } from 'node:path';
12
+ import { DatabaseSync } from 'node:sqlite';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
16
+
17
+ function makeTempHome() {
18
+ return mkdtempSync(join(tmpdir(), 'tl-hooks-home-'));
19
+ }
20
+
21
+ function makeTempProject() {
22
+ return mkdtempSync(join(tmpdir(), 'tl-hooks-project-'));
23
+ }
24
+
25
+ function childEnv(home) {
26
+ return {
27
+ ...process.env,
28
+ HOME: home,
29
+ USERPROFILE: home,
30
+ THROUGHLINE_NO_VSCODE: '1',
31
+ };
32
+ }
33
+
34
+ function runNode(args, { home, cwd = REPO_ROOT, input = '' }) {
35
+ return spawnSync(process.execPath, args, {
36
+ cwd,
37
+ env: childEnv(home),
38
+ input,
39
+ encoding: 'utf8',
40
+ });
41
+ }
42
+
43
+ function openDb(home) {
44
+ return new DatabaseSync(join(home, '.throughline', 'throughline.db'));
45
+ }
46
+
47
+ test('hook modules can be imported without executing their hook body', () => {
48
+ const home = makeTempHome();
49
+ try {
50
+ const result = runNode(
51
+ [
52
+ '--input-type=module',
53
+ '-e',
54
+ [
55
+ "await import('./src/prompt-submit.mjs');",
56
+ "await import('./src/session-start.mjs');",
57
+ "await import('./src/turn-processor.mjs');",
58
+ ].join('\n'),
59
+ ],
60
+ { home },
61
+ );
62
+
63
+ assert.equal(result.status, 0, result.stderr);
64
+ assert.equal(
65
+ existsSync(join(home, '.throughline')),
66
+ false,
67
+ 'importing hook modules should not create the real hook DB or state dir',
68
+ );
69
+ } finally {
70
+ rmSync(home, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test('prompt-submit subprocess writes a /tl baton into an isolated DB', () => {
75
+ const home = makeTempHome();
76
+ const project = makeTempProject();
77
+ try {
78
+ const result = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
79
+ home,
80
+ cwd: project,
81
+ input: JSON.stringify({
82
+ session_id: 'old-session',
83
+ cwd: project,
84
+ prompt: '/tl',
85
+ }),
86
+ });
87
+
88
+ assert.equal(result.status, 0, result.stderr);
89
+
90
+ const db = openDb(home);
91
+ const row = db.prepare('SELECT project_path, session_id FROM handoff_batons').get();
92
+ assert.equal(row.project_path, project);
93
+ assert.equal(row.session_id, 'old-session');
94
+ db.close();
95
+ } finally {
96
+ rmSync(project, { recursive: true, force: true });
97
+ rmSync(home, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ test('save-inflight subprocess stores memo on the current project baton', () => {
102
+ const home = makeTempHome();
103
+ const project = makeTempProject();
104
+ try {
105
+ const baton = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
106
+ home,
107
+ cwd: project,
108
+ input: JSON.stringify({
109
+ session_id: 'old-session',
110
+ cwd: project,
111
+ prompt: '/tl',
112
+ }),
113
+ });
114
+ assert.equal(baton.status, 0, baton.stderr);
115
+
116
+ const memo = 'Next: keep the handoff precise';
117
+ const saved = runNode([join(REPO_ROOT, 'bin/throughline.mjs'), 'save-inflight'], {
118
+ home,
119
+ cwd: project,
120
+ input: memo,
121
+ });
122
+ assert.equal(saved.status, 0, saved.stderr);
123
+ assert.match(saved.stdout, /in-flight memo saved/);
124
+
125
+ const db = openDb(home);
126
+ const row = db.prepare('SELECT memo_text FROM handoff_batons').get();
127
+ assert.equal(row.memo_text, memo);
128
+ db.close();
129
+ } finally {
130
+ rmSync(project, { recursive: true, force: true });
131
+ rmSync(home, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ test('session-start subprocess consumes baton and injects inherited resume context', () => {
136
+ const home = makeTempHome();
137
+ const project = makeTempProject();
138
+ try {
139
+ const baton = runNode([join(REPO_ROOT, 'src/prompt-submit.mjs')], {
140
+ home,
141
+ cwd: project,
142
+ input: JSON.stringify({
143
+ session_id: 'old-session',
144
+ cwd: project,
145
+ prompt: '/tl',
146
+ }),
147
+ });
148
+ assert.equal(baton.status, 0, baton.stderr);
149
+
150
+ const db = openDb(home);
151
+ db.prepare(
152
+ `INSERT INTO sessions (session_id, project_path, status, created_at, updated_at)
153
+ VALUES ('old-session', ?, 'active', 1, 1)`,
154
+ ).run(project);
155
+ db.prepare(
156
+ `INSERT INTO bodies
157
+ (session_id, origin_session_id, turn_number, role, text, token_count, created_at)
158
+ VALUES ('old-session', 'old-session', 1, 'assistant', 'old assistant body', 4, 2)`,
159
+ ).run();
160
+ db.prepare(
161
+ `UPDATE handoff_batons
162
+ SET memo_text = 'handoff memo'
163
+ WHERE project_path = ?`,
164
+ ).run(project);
165
+ db.close();
166
+
167
+ const started = runNode([join(REPO_ROOT, 'src/session-start.mjs')], {
168
+ home,
169
+ cwd: project,
170
+ input: JSON.stringify({
171
+ session_id: 'new-session',
172
+ cwd: project,
173
+ source: 'startup',
174
+ }),
175
+ });
176
+
177
+ assert.equal(started.status, 0, started.stderr);
178
+ assert.match(started.stdout, /handoff memo/);
179
+ assert.match(started.stdout, /old assistant body/);
180
+
181
+ const after = openDb(home);
182
+ const old = after
183
+ .prepare("SELECT merged_into FROM sessions WHERE session_id = 'old-session'")
184
+ .get();
185
+ const batons = after.prepare('SELECT COUNT(*) AS c FROM handoff_batons').get();
186
+ assert.equal(old.merged_into, 'new-session');
187
+ assert.equal(batons.c, 0);
188
+ after.close();
189
+ } finally {
190
+ rmSync(project, { recursive: true, force: true });
191
+ rmSync(home, { recursive: true, force: true });
192
+ }
193
+ });
194
+
195
+ test('session-start subprocess does not inject context when no baton exists', () => {
196
+ const home = makeTempHome();
197
+ const project = makeTempProject();
198
+ try {
199
+ const started = runNode([join(REPO_ROOT, 'src/session-start.mjs')], {
200
+ home,
201
+ cwd: project,
202
+ input: JSON.stringify({
203
+ session_id: 'new-session',
204
+ cwd: project,
205
+ source: 'startup',
206
+ }),
207
+ });
208
+
209
+ assert.equal(started.status, 0, started.stderr);
210
+ assert.equal(started.stdout, '');
211
+
212
+ const db = openDb(home);
213
+ const row = db
214
+ .prepare("SELECT session_id, project_path FROM sessions WHERE session_id = 'new-session'")
215
+ .get();
216
+ assert.equal(row.session_id, 'new-session');
217
+ assert.equal(row.project_path, project);
218
+ db.close();
219
+ } finally {
220
+ rmSync(project, { recursive: true, force: true });
221
+ rmSync(home, { recursive: true, force: true });
222
+ }
223
+ });
224
+
225
+ test('process-turn subprocess stores L2 bodies and L3 details in an isolated DB', () => {
226
+ const home = makeTempHome();
227
+ const project = makeTempProject();
228
+ const transcriptPath = join(project, 'transcript.jsonl');
229
+ try {
230
+ const entries = [
231
+ {
232
+ type: 'user',
233
+ message: {
234
+ role: 'user',
235
+ content: [{ type: 'text', text: 'run the check' }],
236
+ },
237
+ },
238
+ {
239
+ type: 'assistant',
240
+ uuid: 'asst-tool',
241
+ message: {
242
+ role: 'assistant',
243
+ content: [
244
+ { type: 'thinking', thinking: 'I should inspect the output first' },
245
+ {
246
+ type: 'tool_use',
247
+ id: 'toolu_check',
248
+ name: 'Bash',
249
+ input: { command: 'echo ok' },
250
+ },
251
+ ],
252
+ },
253
+ },
254
+ {
255
+ type: 'user',
256
+ message: {
257
+ role: 'user',
258
+ content: [
259
+ {
260
+ type: 'tool_result',
261
+ tool_use_id: 'toolu_check',
262
+ content: 'ok\n',
263
+ },
264
+ ],
265
+ },
266
+ },
267
+ {
268
+ type: 'assistant',
269
+ message: {
270
+ role: 'assistant',
271
+ model: 'claude-opus-4-6',
272
+ usage: {
273
+ input_tokens: 100,
274
+ cache_creation_input_tokens: 20,
275
+ cache_read_input_tokens: 5,
276
+ output_tokens: 10,
277
+ },
278
+ content: [{ type: 'text', text: 'check passed' }],
279
+ },
280
+ },
281
+ ];
282
+ writeFileSync(transcriptPath, entries.map((e) => JSON.stringify(e)).join('\n'));
283
+
284
+ const result = runNode([join(REPO_ROOT, 'src/turn-processor.mjs')], {
285
+ home,
286
+ cwd: project,
287
+ input: JSON.stringify({
288
+ session_id: 'turn-session',
289
+ cwd: project,
290
+ transcript_path: transcriptPath,
291
+ }),
292
+ });
293
+
294
+ assert.equal(result.status, 0, result.stderr);
295
+
296
+ const db = openDb(home);
297
+ const bodies = db
298
+ .prepare('SELECT role, text FROM bodies ORDER BY role')
299
+ .all()
300
+ .map((row) => ({ role: row.role, text: row.text }));
301
+ assert.deepEqual(bodies, [
302
+ { role: 'assistant', text: 'check passed' },
303
+ { role: 'user', text: 'run the check' },
304
+ ]);
305
+
306
+ const details = db
307
+ .prepare('SELECT kind, tool_name, source_id, input_text, output_text FROM details ORDER BY id')
308
+ .all();
309
+ assert.equal(details.length, 3);
310
+ assert.deepEqual(details.map((d) => d.kind), [
311
+ 'thinking',
312
+ 'tool_input',
313
+ 'tool_output',
314
+ ]);
315
+ assert.equal(details[0].source_id, 'asst-tool:thinking:0');
316
+ assert.equal(details[0].output_text, 'I should inspect the output first');
317
+ assert.equal(details[1].tool_name, 'Bash');
318
+ assert.match(details[1].input_text, /echo ok/);
319
+ assert.equal(details[2].tool_name, 'Bash');
320
+ assert.equal(details[2].output_text, 'ok\n');
321
+ db.close();
322
+ } finally {
323
+ rmSync(project, { recursive: true, force: true });
324
+ rmSync(home, { recursive: true, force: true });
325
+ }
326
+ });
@@ -0,0 +1,19 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
6
+
7
+ test('npm package files include Claude and Codex agent surfaces', () => {
8
+ assert.deepEqual(packageJson.files, [
9
+ 'bin/',
10
+ 'src/',
11
+ 'codex/skills/',
12
+ '.claude/commands/',
13
+ '.codex-sidecar.yml',
14
+ 'docs/',
15
+ 'CHANGELOG.md',
16
+ 'README.md',
17
+ 'LICENSE',
18
+ ]);
19
+ });
@@ -18,6 +18,7 @@ import { ensureMonitorTaskFile } from './vscode-task.mjs';
18
18
  import { appendFileSync, mkdirSync } from 'node:fs';
19
19
  import { join, dirname } from 'node:path';
20
20
  import { homedir } from 'node:os';
21
+ import { pathToFileURL } from 'node:url';
21
22
 
22
23
  function logBaton(entry) {
23
24
  const path = join(homedir(), '.throughline', 'logs', 'baton-write.log');
@@ -42,7 +43,7 @@ export function isBatonCommand(prompt) {
42
43
  return false;
43
44
  }
44
45
 
45
- async function main() {
46
+ export async function run() {
46
47
  let raw = '';
47
48
  await new Promise((resolve) => {
48
49
  process.stdin.setEncoding('utf8');
@@ -91,8 +92,10 @@ async function main() {
91
92
  process.exit(0);
92
93
  }
93
94
 
94
- main().catch((err) => {
95
- const msg = err instanceof Error ? err.message : 'unknown';
96
- process.stderr.write(`[prompt-submit] error: ${msg}\n`);
97
- process.exit(1);
98
- });
95
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
96
+ run().catch((err) => {
97
+ const msg = err instanceof Error ? err.message : 'unknown';
98
+ process.stderr.write(`[prompt-submit] error: ${msg}\n`);
99
+ process.exit(1);
100
+ });
101
+ }