throughline 0.3.24 → 0.4.0

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 (116) hide show
  1. package/.claude/commands/tl.md +6 -21
  2. package/.codex-sidecar.yml +62 -0
  3. package/CHANGELOG.md +632 -0
  4. package/README.ja.md +71 -46
  5. package/README.md +420 -76
  6. package/bin/throughline.mjs +169 -7
  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 +159 -0
  10. package/docs/L1_L2_L3_REDESIGN.md +415 -0
  11. package/docs/PUBLIC_RELEASE_PLAN.md +185 -0
  12. package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +286 -0
  13. package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
  14. package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
  15. package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
  16. package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
  17. package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
  18. package/docs/archive/CONCEPT.md +476 -0
  19. package/docs/archive/EXPERIMENT.md +371 -0
  20. package/docs/archive/README.md +22 -0
  21. package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
  22. package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
  23. package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
  24. package/docs/throughline-handoff-context.example.json +57 -0
  25. package/docs/throughline-rollback-context-trim-insight.md +455 -0
  26. package/package.json +6 -2
  27. package/src/baton.mjs +17 -45
  28. package/src/baton.test.mjs +4 -41
  29. package/src/cli/codex-capture.mjs +95 -0
  30. package/src/cli/codex-handoff-model-smoke.mjs +292 -0
  31. package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
  32. package/src/cli/codex-handoff-smoke.mjs +163 -0
  33. package/src/cli/codex-handoff-smoke.test.mjs +149 -0
  34. package/src/cli/codex-handoff-start.mjs +291 -0
  35. package/src/cli/codex-handoff-start.test.mjs +194 -0
  36. package/src/cli/codex-hook.mjs +276 -0
  37. package/src/cli/codex-hook.test.mjs +293 -0
  38. package/src/cli/codex-host-primitive-audit.mjs +110 -0
  39. package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
  40. package/src/cli/codex-restore-smoke.mjs +357 -0
  41. package/src/cli/codex-restore-source-audit.mjs +304 -0
  42. package/src/cli/codex-resume.mjs +138 -0
  43. package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
  44. package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
  45. package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
  46. package/src/cli/codex-sidecar-dry-run.mjs +85 -0
  47. package/src/cli/codex-summarize.mjs +224 -0
  48. package/src/cli/codex-threads.mjs +89 -0
  49. package/src/cli/codex-visibility-smoke.mjs +196 -0
  50. package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
  51. package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
  52. package/src/cli/doctor.mjs +503 -1
  53. package/src/cli/doctor.test.mjs +542 -3
  54. package/src/cli/handoff-preview.mjs +78 -0
  55. package/src/cli/help.test.mjs +64 -0
  56. package/src/cli/install.mjs +226 -3
  57. package/src/cli/install.test.mjs +205 -4
  58. package/src/cli/trim.mjs +564 -0
  59. package/src/codex-app-server.mjs +1816 -0
  60. package/src/codex-app-server.test.mjs +512 -0
  61. package/src/codex-auto-refresh.mjs +194 -0
  62. package/src/codex-auto-refresh.test.mjs +182 -0
  63. package/src/codex-capture.mjs +235 -0
  64. package/src/codex-capture.test.mjs +393 -0
  65. package/src/codex-handoff-model-smoke.mjs +114 -0
  66. package/src/codex-handoff-model-smoke.test.mjs +89 -0
  67. package/src/codex-handoff-smoke.mjs +124 -0
  68. package/src/codex-handoff-smoke.test.mjs +103 -0
  69. package/src/codex-handoff.mjs +331 -0
  70. package/src/codex-handoff.test.mjs +220 -0
  71. package/src/codex-host-primitive-audit.mjs +374 -0
  72. package/src/codex-host-primitive-audit.test.mjs +208 -0
  73. package/src/codex-restore-smoke.test.mjs +639 -0
  74. package/src/codex-restore-source-audit.mjs +1348 -0
  75. package/src/codex-restore-source-audit.test.mjs +623 -0
  76. package/src/codex-resume.test.mjs +242 -0
  77. package/src/codex-rollout-memory.mjs +711 -0
  78. package/src/codex-rollout-memory.test.mjs +610 -0
  79. package/src/codex-sidecar-cli.test.mjs +75 -0
  80. package/src/codex-sidecar.mjs +246 -0
  81. package/src/codex-sidecar.test.mjs +172 -0
  82. package/src/codex-summarize.test.mjs +143 -0
  83. package/src/codex-thread-identity.mjs +23 -0
  84. package/src/codex-thread-index.mjs +173 -0
  85. package/src/codex-thread-index.test.mjs +164 -0
  86. package/src/codex-usage.mjs +110 -0
  87. package/src/codex-usage.test.mjs +140 -0
  88. package/src/codex-visibility-smoke.test.mjs +222 -0
  89. package/src/codex-vscode-restore-smoke.mjs +206 -0
  90. package/src/codex-vscode-restore-smoke.test.mjs +325 -0
  91. package/src/codex-vscode-rollback-smoke.mjs +90 -0
  92. package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
  93. package/src/db-schema.test.mjs +96 -0
  94. package/src/db.mjs +14 -1
  95. package/src/haiku-summarizer.mjs +267 -26
  96. package/src/haiku-summarizer.test.mjs +282 -0
  97. package/src/handoff-preview.test.mjs +108 -0
  98. package/src/handoff-record.mjs +294 -0
  99. package/src/handoff-record.test.mjs +226 -0
  100. package/src/hook-entrypoints.test.mjs +286 -0
  101. package/src/package-files.test.mjs +19 -0
  102. package/src/prompt-submit.mjs +9 -6
  103. package/src/resume-context.mjs +58 -171
  104. package/src/resume-context.test.mjs +177 -0
  105. package/src/session-start.mjs +85 -26
  106. package/src/state-file.mjs +50 -6
  107. package/src/state-file.test.mjs +50 -0
  108. package/src/token-monitor.mjs +14 -10
  109. package/src/token-monitor.test.mjs +27 -0
  110. package/src/trim-cli.test.mjs +1584 -0
  111. package/src/trim-model.mjs +584 -0
  112. package/src/trim-model.test.mjs +568 -0
  113. package/src/turn-processor.mjs +17 -10
  114. package/src/vscode-task.mjs +33 -10
  115. package/src/vscode-task.test.mjs +19 -9
  116. package/src/cli/save-inflight.mjs +0 -81
@@ -0,0 +1,194 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const REPO_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
10
+
11
+ function makeTempHome() {
12
+ return mkdtempSync(join(tmpdir(), 'tl-codex-handoff-start-home-'));
13
+ }
14
+
15
+ function makeTempProject() {
16
+ return mkdtempSync(join(tmpdir(), 'tl-codex-handoff-start-project-'));
17
+ }
18
+
19
+ async function seedDb(home, project) {
20
+ const originalHome = process.env.HOME;
21
+ const originalUserProfile = process.env.USERPROFILE;
22
+ process.env.HOME = home;
23
+ process.env.USERPROFILE = home;
24
+ try {
25
+ const mod = await import(`../db.mjs?codexHandoffStart=${Date.now()}-${Math.random()}`);
26
+ const db = mod.getDb();
27
+ db.prepare(
28
+ `INSERT INTO sessions (session_id, project_path, status, created_at, updated_at)
29
+ VALUES ('codex:thread-handoff-start', ?, 'active', 1, 2)`,
30
+ ).run(project);
31
+ db.prepare(
32
+ `INSERT INTO skeletons
33
+ (session_id, origin_session_id, turn_number, role, summary, created_at)
34
+ VALUES ('codex:thread-handoff-start', 'codex:thread-handoff-start', 1, 'assistant',
35
+ 'older handoff start summary', 1000)`,
36
+ ).run();
37
+ db.prepare(
38
+ `INSERT INTO bodies
39
+ (session_id, origin_session_id, turn_number, role, text, token_count, created_at)
40
+ VALUES ('codex:thread-handoff-start', 'codex:thread-handoff-start', 2, 'assistant',
41
+ 'latest handoff start body', 4, 2000)`,
42
+ ).run();
43
+ db.close();
44
+ } finally {
45
+ if (originalHome === undefined) delete process.env.HOME;
46
+ else process.env.HOME = originalHome;
47
+ if (originalUserProfile === undefined) delete process.env.USERPROFILE;
48
+ else process.env.USERPROFILE = originalUserProfile;
49
+ }
50
+ }
51
+
52
+ function runStart(home, project, args = [], input = undefined) {
53
+ return spawnSync(
54
+ process.execPath,
55
+ [join(REPO_ROOT, 'bin/throughline.mjs'), 'codex-handoff-start', ...args],
56
+ {
57
+ cwd: project,
58
+ env: {
59
+ ...process.env,
60
+ HOME: home,
61
+ USERPROFILE: home,
62
+ },
63
+ encoding: 'utf8',
64
+ input,
65
+ },
66
+ );
67
+ }
68
+
69
+ test('codex-handoff-start prints guided ready JSON for latest Codex session', async () => {
70
+ const home = makeTempHome();
71
+ const project = makeTempProject();
72
+ try {
73
+ await seedDb(home, project);
74
+ const result = runStart(home, project, ['--json']);
75
+
76
+ assert.equal(result.status, 0, result.stderr);
77
+ const payload = JSON.parse(result.stdout);
78
+ assert.equal(payload.status, 'ready');
79
+ assert.equal(payload.reason, 'fresh_thread_handoff_start_ready');
80
+ assert.equal(payload.sessionId, 'codex:thread-handoff-start');
81
+ assert.equal(payload.mutatesCurrentThread, false);
82
+ assert.equal(payload.startThreadManually, true);
83
+ assert.equal(payload.memoStdin, false);
84
+ assert.equal(payload.memoReplayNote, null);
85
+ assert.equal(payload.handoffSmoke.status, 'ready');
86
+ assert.match(payload.commands.structuralSmoke, /codex-handoff-smoke --session codex:thread-handoff-start/);
87
+ assert.match(payload.commands.modelSmokeDryRun, /codex-handoff-model-smoke --session codex:thread-handoff-start/);
88
+ assert.match(payload.commands.modelSmokeDryRun, /--dry-run --json/);
89
+ assert.match(payload.commands.renderPrompt, /codex-resume --session codex:thread-handoff-start --format handoff/);
90
+ assert.match(payload.commands.liveModelSmoke, /THROUGHLINE_EXPERIMENTAL_CODEX_HANDOFF_MODEL_SMOKE=1/);
91
+ assert.equal(payload.prompt, undefined);
92
+ } finally {
93
+ rmSync(project, { recursive: true, force: true });
94
+ rmSync(home, { recursive: true, force: true });
95
+ }
96
+ });
97
+
98
+ test('codex-handoff-start can print the exact fresh-thread prompt', async () => {
99
+ const home = makeTempHome();
100
+ const project = makeTempProject();
101
+ try {
102
+ await seedDb(home, project);
103
+ const result = runStart(home, project, [
104
+ '--session',
105
+ 'codex:thread-handoff-start',
106
+ '--print-prompt',
107
+ ]);
108
+
109
+ assert.equal(result.status, 0, result.stderr);
110
+ assert.match(result.stdout, /throughline codex handoff start/);
111
+ assert.match(result.stdout, /status:\s+ready/);
112
+ assert.match(result.stdout, /commands:/);
113
+ assert.match(result.stdout, /## Throughline: New Codex Thread Handoff/);
114
+ assert.match(result.stdout, /latest handoff start body/);
115
+ } finally {
116
+ rmSync(project, { recursive: true, force: true });
117
+ rmSync(home, { recursive: true, force: true });
118
+ }
119
+ });
120
+
121
+ test('codex-handoff-start refuses when structural handoff smoke is not ready', async () => {
122
+ const home = makeTempHome();
123
+ const project = makeTempProject();
124
+ try {
125
+ await seedDb(home, project);
126
+ const result = runStart(home, project, [
127
+ '--session',
128
+ 'codex:thread-handoff-start',
129
+ '--max-prompt-chars',
130
+ '50',
131
+ '--json',
132
+ ]);
133
+
134
+ assert.equal(result.status, 1);
135
+ const payload = JSON.parse(result.stdout);
136
+ assert.equal(payload.status, 'not-ready');
137
+ assert.equal(payload.reason, 'handoff_smoke_not_ready');
138
+ assert.equal(payload.handoffSmoke.status, 'not-ready');
139
+ assert.match(payload.commands.renderPrompt, /codex-resume --session codex:thread-handoff-start/);
140
+ } finally {
141
+ rmSync(project, { recursive: true, force: true });
142
+ rmSync(home, { recursive: true, force: true });
143
+ }
144
+ });
145
+
146
+ test('codex-handoff-start carries memo stdin into the printed prompt', async () => {
147
+ const home = makeTempHome();
148
+ const project = makeTempProject();
149
+ try {
150
+ await seedDb(home, project);
151
+ const result = runStart(
152
+ home,
153
+ project,
154
+ ['--session', 'codex:thread-handoff-start', '--memo-stdin', '--print-prompt'],
155
+ '**Next move**: continue guided start',
156
+ );
157
+
158
+ assert.equal(result.status, 0, result.stderr);
159
+ assert.match(result.stdout, /In-flight Memo/);
160
+ assert.match(result.stdout, /continue guided start/);
161
+ assert.match(result.stdout, /--memo-stdin/);
162
+ } finally {
163
+ rmSync(project, { recursive: true, force: true });
164
+ rmSync(home, { recursive: true, force: true });
165
+ }
166
+ });
167
+
168
+ test('codex-handoff-start propagates memo-stdin to replay commands in JSON guidance', async () => {
169
+ const home = makeTempHome();
170
+ const project = makeTempProject();
171
+ try {
172
+ await seedDb(home, project);
173
+ const result = runStart(
174
+ home,
175
+ project,
176
+ ['--session', 'codex:thread-handoff-start', '--memo-stdin', '--json'],
177
+ '**Next move**: replay memo',
178
+ );
179
+
180
+ assert.equal(result.status, 0, result.stderr);
181
+ const payload = JSON.parse(result.stdout);
182
+ assert.equal(payload.status, 'ready');
183
+ assert.equal(payload.memoStdin, true);
184
+ assert.match(payload.memoReplayNote, /pipe the same memo/);
185
+ assert.match(payload.commands.structuralSmoke, /--memo-stdin --json/);
186
+ assert.match(payload.commands.modelSmokeDryRun, /--memo-stdin --dry-run --json/);
187
+ assert.match(payload.commands.renderPrompt, /--memo-stdin/);
188
+ assert.match(payload.commands.liveModelSmoke, /--memo-stdin --json/);
189
+ assert.equal(payload.prompt, undefined);
190
+ } finally {
191
+ rmSync(project, { recursive: true, force: true });
192
+ rmSync(home, { recursive: true, force: true });
193
+ }
194
+ });
@@ -0,0 +1,276 @@
1
+ function parseArgs(argv) {
2
+ const out = {
3
+ event: null,
4
+ codexThreadId: null,
5
+ codexHome: null,
6
+ projectPath: null,
7
+ json: false,
8
+ };
9
+
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const arg = argv[i];
12
+ if (!arg.startsWith('-') && !out.event) {
13
+ out.event = arg;
14
+ } else if (arg === '--codex-thread-id') {
15
+ const value = argv[++i];
16
+ if (!value || value.startsWith('-')) throw new Error('--codex-thread-id requires an id');
17
+ out.codexThreadId = value;
18
+ } else if (arg === '--codex-home') {
19
+ const value = argv[++i];
20
+ if (!value || value.startsWith('-')) throw new Error('--codex-home requires a path');
21
+ out.codexHome = value;
22
+ } else if (arg === '--project') {
23
+ const value = argv[++i];
24
+ if (!value || value.startsWith('-')) throw new Error('--project requires a path');
25
+ out.projectPath = value;
26
+ } else if (arg === '--json') {
27
+ out.json = true;
28
+ } else {
29
+ throw new Error(`unknown argument: ${arg}`);
30
+ }
31
+ }
32
+
33
+ if (!out.event) out.event = 'stop';
34
+ if (out.event !== 'stop') throw new Error(`unknown Codex hook event: ${out.event}`);
35
+ return out;
36
+ }
37
+
38
+ async function readStdin() {
39
+ const chunks = [];
40
+ for await (const chunk of process.stdin) {
41
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
42
+ }
43
+ return Buffer.concat(chunks).toString('utf8');
44
+ }
45
+
46
+ function parsePayload(raw) {
47
+ if (!raw.trim()) return {};
48
+ try {
49
+ return JSON.parse(raw);
50
+ } catch (err) {
51
+ const msg = err instanceof Error ? err.message : 'unknown';
52
+ throw new Error(`failed to parse Codex hook stdin JSON: ${msg}`);
53
+ }
54
+ }
55
+
56
+ function codexHomeFromTranscriptPath(transcriptPath) {
57
+ if (typeof transcriptPath !== 'string') return null;
58
+ const marker = `${process.platform === 'win32' ? '\\' : '/'}sessions${process.platform === 'win32' ? '\\' : '/'}`;
59
+ const idx = transcriptPath.indexOf(marker);
60
+ if (idx <= 0) return null;
61
+ return transcriptPath.slice(0, idx);
62
+ }
63
+
64
+ function suppressExperimentalWarnings() {
65
+ if (process.env.THROUGHLINE_SHOW_EXPERIMENTAL_WARNINGS === '1') return;
66
+ process.on('warning', (warning) => {
67
+ if (warning?.name === 'ExperimentalWarning') return;
68
+ process.stderr.write(`${warning.name}: ${warning.message}\n`);
69
+ });
70
+ }
71
+
72
+ export async function runCodexStopHook({
73
+ args = {},
74
+ payload = {},
75
+ env = process.env,
76
+ db = null,
77
+ writeMonitorState = null,
78
+ ensureMonitorTask = null,
79
+ buildMonitorUsage = null,
80
+ runAutoRefresh = null,
81
+ } = {}) {
82
+ const [
83
+ { getDb },
84
+ { captureCodexRolloutToDb },
85
+ { resolveCodexThreadIdentity },
86
+ { summarizeCodexSession },
87
+ { writeSessionState },
88
+ { ensureMonitorTaskFile },
89
+ { buildCodexMonitorUsage },
90
+ { runCodexAutoRefresh },
91
+ ] = await Promise.all([
92
+ import('../db.mjs'),
93
+ import('../codex-capture.mjs'),
94
+ import('../codex-thread-identity.mjs'),
95
+ import('./codex-summarize.mjs'),
96
+ import('../state-file.mjs'),
97
+ import('../vscode-task.mjs'),
98
+ import('../codex-usage.mjs'),
99
+ import('../codex-auto-refresh.mjs'),
100
+ ]);
101
+ const actualDb = db ?? getDb();
102
+ const identity = resolveCodexHookThreadIdentity({ args, payload, env, resolveCodexThreadIdentity });
103
+
104
+ if (!identity.codexThreadId) {
105
+ return {
106
+ status: 'skipped',
107
+ reason: 'codex_thread_id_not_available',
108
+ captured: null,
109
+ summarized: null,
110
+ };
111
+ }
112
+
113
+ const projectPath =
114
+ args.projectPath ??
115
+ (typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd());
116
+ const codexHome =
117
+ args.codexHome ??
118
+ codexHomeFromTranscriptPath(payload.transcript_path ?? payload.transcriptPath) ??
119
+ undefined;
120
+
121
+ const ensureTask = ensureMonitorTask ?? ensureMonitorTaskFile;
122
+ try {
123
+ ensureTask({ cwd: projectPath, env });
124
+ } catch (err) {
125
+ const msg = err instanceof Error ? err.message : 'unknown';
126
+ process.stderr.write(`[codex-hook:vscode-task] ${msg}\n`);
127
+ }
128
+
129
+ const captured = captureCodexRolloutToDb(actualDb, {
130
+ threadId: identity.codexThreadId,
131
+ codexHome,
132
+ projectPath,
133
+ });
134
+
135
+ if (captured.status !== 'captured') {
136
+ return {
137
+ status: 'skipped',
138
+ reason: captured.reason ?? 'codex_capture_not_available',
139
+ codexThreadIdSource: identity.codexThreadIdSource,
140
+ captured,
141
+ summarized: null,
142
+ };
143
+ }
144
+
145
+ const usage = (buildMonitorUsage ?? buildCodexMonitorUsage)(captured.rolloutPath);
146
+ let monitorState = null;
147
+ try {
148
+ monitorState = {
149
+ sessionId: captured.sessionId,
150
+ host: 'codex',
151
+ projectPath: captured.projectPath ?? projectPath,
152
+ transcriptPath: null,
153
+ rolloutPath: captured.rolloutPath ?? null,
154
+ pid: process.pid,
155
+ usage,
156
+ };
157
+ (writeMonitorState ?? writeSessionState)(monitorState);
158
+ } catch (err) {
159
+ monitorState = null;
160
+ const msg = err instanceof Error ? err.message : 'unknown';
161
+ process.stderr.write(`[codex-hook:monitor-state] ${msg}\n`);
162
+ }
163
+
164
+ const summarized = summarizeCodexSession(actualDb, {
165
+ sessionId: captured.sessionId,
166
+ projectPath: captured.projectPath ?? projectPath,
167
+ max: 1,
168
+ env,
169
+ });
170
+
171
+ let autoRefresh = null;
172
+ try {
173
+ autoRefresh = await (runAutoRefresh ?? runCodexAutoRefresh)({
174
+ db: actualDb,
175
+ threadId: identity.codexThreadId,
176
+ codexThreadIdSource: identity.codexThreadIdSource,
177
+ codexHome,
178
+ projectPath: captured.projectPath ?? projectPath,
179
+ sessionId: captured.sessionId,
180
+ usage,
181
+ command: env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex',
182
+ });
183
+ } catch (err) {
184
+ const msg = err instanceof Error ? err.message : 'unknown';
185
+ autoRefresh = {
186
+ status: 'error',
187
+ reason: 'auto_refresh_failed',
188
+ message: msg,
189
+ };
190
+ process.stderr.write(`[codex-hook:auto-refresh] ${msg}\n`);
191
+ }
192
+
193
+ return {
194
+ status: 'ok',
195
+ reason: 'codex_rollout_captured',
196
+ codexThreadIdSource: identity.codexThreadIdSource,
197
+ captured,
198
+ summarized,
199
+ monitorState,
200
+ autoRefresh,
201
+ };
202
+ }
203
+
204
+ export async function run(argv = []) {
205
+ suppressExperimentalWarnings();
206
+ let parsed;
207
+ let payload;
208
+ try {
209
+ parsed = parseArgs(argv);
210
+ payload = parsePayload(await readStdin());
211
+ } catch (err) {
212
+ const msg = err instanceof Error ? err.message : 'unknown';
213
+ process.stderr.write(`[codex-hook] ${msg}\n`);
214
+ process.exit(1);
215
+ }
216
+
217
+ try {
218
+ const result = await runCodexStopHook({
219
+ args: parsed,
220
+ payload,
221
+ env: process.env,
222
+ });
223
+ if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
224
+ process.exit(result.status === 'ok' || result.status === 'skipped' ? 0 : 1);
225
+ } catch (err) {
226
+ const msg = err instanceof Error ? err.message : 'unknown';
227
+ if (parsed.json) {
228
+ process.stdout.write(
229
+ JSON.stringify(
230
+ {
231
+ status: 'error',
232
+ reason: err?.reason ?? 'codex_hook_failed',
233
+ source: err?.source ?? null,
234
+ message: msg,
235
+ stderr: err?.stderr ?? '',
236
+ exitCode: err?.exitCode ?? null,
237
+ },
238
+ null,
239
+ 2,
240
+ ) + '\n',
241
+ );
242
+ } else {
243
+ process.stderr.write(`[codex-hook] ${msg}\n`);
244
+ }
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ export const _internal = {
250
+ codexHomeFromTranscriptPath,
251
+ parseArgs,
252
+ parsePayload,
253
+ resolveCodexHookThreadIdentity,
254
+ };
255
+
256
+ function resolveCodexHookThreadIdentity({ args = {}, payload = {}, env, resolveCodexThreadIdentity }) {
257
+ if (args.codexThreadId) {
258
+ return resolveCodexThreadIdentity({ codexThreadId: args.codexThreadId }, env);
259
+ }
260
+
261
+ if (typeof payload.session_id === 'string' && payload.session_id) {
262
+ return {
263
+ codexThreadId: payload.session_id,
264
+ codexThreadIdSource: 'payload:session_id',
265
+ };
266
+ }
267
+
268
+ if (typeof payload.sessionId === 'string' && payload.sessionId) {
269
+ return {
270
+ codexThreadId: payload.sessionId,
271
+ codexThreadIdSource: 'payload:sessionId',
272
+ };
273
+ }
274
+
275
+ return resolveCodexThreadIdentity({ codexThreadId: null }, env);
276
+ }