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.
- package/.claude/commands/tl.md +6 -21
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +632 -0
- package/README.ja.md +71 -46
- package/README.md +420 -76
- package/bin/throughline.mjs +169 -7
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +159 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +185 -0
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +286 -0
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
- package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
- package/docs/archive/CONCEPT.md +476 -0
- package/docs/archive/EXPERIMENT.md +371 -0
- package/docs/archive/README.md +22 -0
- package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
- package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
- package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
- package/docs/throughline-handoff-context.example.json +57 -0
- package/docs/throughline-rollback-context-trim-insight.md +455 -0
- package/package.json +6 -2
- package/src/baton.mjs +17 -45
- package/src/baton.test.mjs +4 -41
- package/src/cli/codex-capture.mjs +95 -0
- package/src/cli/codex-handoff-model-smoke.mjs +292 -0
- package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
- package/src/cli/codex-handoff-smoke.mjs +163 -0
- package/src/cli/codex-handoff-smoke.test.mjs +149 -0
- package/src/cli/codex-handoff-start.mjs +291 -0
- package/src/cli/codex-handoff-start.test.mjs +194 -0
- package/src/cli/codex-hook.mjs +276 -0
- package/src/cli/codex-hook.test.mjs +293 -0
- package/src/cli/codex-host-primitive-audit.mjs +110 -0
- package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
- package/src/cli/codex-restore-smoke.mjs +357 -0
- package/src/cli/codex-restore-source-audit.mjs +304 -0
- package/src/cli/codex-resume.mjs +138 -0
- package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
- package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
- package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
- package/src/cli/codex-sidecar-dry-run.mjs +85 -0
- package/src/cli/codex-summarize.mjs +224 -0
- package/src/cli/codex-threads.mjs +89 -0
- package/src/cli/codex-visibility-smoke.mjs +196 -0
- package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
- package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
- package/src/cli/doctor.mjs +503 -1
- package/src/cli/doctor.test.mjs +542 -3
- package/src/cli/handoff-preview.mjs +78 -0
- package/src/cli/help.test.mjs +64 -0
- package/src/cli/install.mjs +226 -3
- package/src/cli/install.test.mjs +205 -4
- package/src/cli/trim.mjs +564 -0
- package/src/codex-app-server.mjs +1816 -0
- package/src/codex-app-server.test.mjs +512 -0
- package/src/codex-auto-refresh.mjs +194 -0
- package/src/codex-auto-refresh.test.mjs +182 -0
- package/src/codex-capture.mjs +235 -0
- package/src/codex-capture.test.mjs +393 -0
- package/src/codex-handoff-model-smoke.mjs +114 -0
- package/src/codex-handoff-model-smoke.test.mjs +89 -0
- package/src/codex-handoff-smoke.mjs +124 -0
- package/src/codex-handoff-smoke.test.mjs +103 -0
- package/src/codex-handoff.mjs +331 -0
- package/src/codex-handoff.test.mjs +220 -0
- package/src/codex-host-primitive-audit.mjs +374 -0
- package/src/codex-host-primitive-audit.test.mjs +208 -0
- package/src/codex-restore-smoke.test.mjs +639 -0
- package/src/codex-restore-source-audit.mjs +1348 -0
- package/src/codex-restore-source-audit.test.mjs +623 -0
- package/src/codex-resume.test.mjs +242 -0
- package/src/codex-rollout-memory.mjs +711 -0
- package/src/codex-rollout-memory.test.mjs +610 -0
- package/src/codex-sidecar-cli.test.mjs +75 -0
- package/src/codex-sidecar.mjs +246 -0
- package/src/codex-sidecar.test.mjs +172 -0
- package/src/codex-summarize.test.mjs +143 -0
- package/src/codex-thread-identity.mjs +23 -0
- package/src/codex-thread-index.mjs +173 -0
- package/src/codex-thread-index.test.mjs +164 -0
- package/src/codex-usage.mjs +110 -0
- package/src/codex-usage.test.mjs +140 -0
- package/src/codex-visibility-smoke.test.mjs +222 -0
- package/src/codex-vscode-restore-smoke.mjs +206 -0
- package/src/codex-vscode-restore-smoke.test.mjs +325 -0
- package/src/codex-vscode-rollback-smoke.mjs +90 -0
- package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
- package/src/db-schema.test.mjs +96 -0
- package/src/db.mjs +14 -1
- package/src/haiku-summarizer.mjs +267 -26
- package/src/haiku-summarizer.test.mjs +282 -0
- package/src/handoff-preview.test.mjs +108 -0
- package/src/handoff-record.mjs +294 -0
- package/src/handoff-record.test.mjs +226 -0
- package/src/hook-entrypoints.test.mjs +286 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +58 -171
- package/src/resume-context.test.mjs +177 -0
- package/src/session-start.mjs +85 -26
- package/src/state-file.mjs +50 -6
- package/src/state-file.test.mjs +50 -0
- package/src/token-monitor.mjs +14 -10
- package/src/token-monitor.test.mjs +27 -0
- package/src/trim-cli.test.mjs +1584 -0
- package/src/trim-model.mjs +584 -0
- package/src/trim-model.test.mjs +568 -0
- package/src/turn-processor.mjs +17 -10
- package/src/vscode-task.mjs +33 -10
- package/src/vscode-task.test.mjs +19 -9
- package/src/cli/save-inflight.mjs +0 -81
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
7
|
+
|
|
8
|
+
import { runCodexStopHook, _internal } from './codex-hook.mjs';
|
|
9
|
+
|
|
10
|
+
function makeDb() {
|
|
11
|
+
const db = new DatabaseSync(':memory:');
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE sessions (
|
|
14
|
+
session_id TEXT PRIMARY KEY,
|
|
15
|
+
project_path TEXT NOT NULL,
|
|
16
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
17
|
+
created_at INTEGER NOT NULL,
|
|
18
|
+
updated_at INTEGER NOT NULL,
|
|
19
|
+
merged_into TEXT
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE skeletons (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
session_id TEXT NOT NULL,
|
|
24
|
+
origin_session_id TEXT,
|
|
25
|
+
turn_number INTEGER NOT NULL,
|
|
26
|
+
role TEXT NOT NULL,
|
|
27
|
+
summary TEXT NOT NULL,
|
|
28
|
+
created_at INTEGER NOT NULL
|
|
29
|
+
);
|
|
30
|
+
CREATE TABLE bodies (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
session_id TEXT NOT NULL,
|
|
33
|
+
origin_session_id TEXT NOT NULL,
|
|
34
|
+
turn_number INTEGER NOT NULL,
|
|
35
|
+
role TEXT NOT NULL,
|
|
36
|
+
text TEXT NOT NULL,
|
|
37
|
+
token_count INTEGER,
|
|
38
|
+
created_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE details (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
session_id TEXT NOT NULL,
|
|
43
|
+
origin_session_id TEXT,
|
|
44
|
+
turn_number INTEGER,
|
|
45
|
+
tool_name TEXT NOT NULL,
|
|
46
|
+
input_text TEXT,
|
|
47
|
+
output_text TEXT,
|
|
48
|
+
token_count INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
created_at INTEGER NOT NULL,
|
|
50
|
+
kind TEXT,
|
|
51
|
+
source_id TEXT
|
|
52
|
+
);
|
|
53
|
+
`);
|
|
54
|
+
return db;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('codex-hook stop captures rollout using Codex stdin payload fields', async () => {
|
|
58
|
+
const codexHome = mkdtempSync(join(tmpdir(), 'tl-codex-hook-home-'));
|
|
59
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-hook-project-'));
|
|
60
|
+
const threadId = '019dfaba-f87e-7f41-a144-d5ca7c6dd7f9';
|
|
61
|
+
const db = makeDb();
|
|
62
|
+
let monitorState = null;
|
|
63
|
+
let monitorTaskCwd = null;
|
|
64
|
+
try {
|
|
65
|
+
const rolloutPath = writeRollout(codexHome, {
|
|
66
|
+
id: threadId,
|
|
67
|
+
cwd: project,
|
|
68
|
+
events: [
|
|
69
|
+
event('user_message', { message: 'hook request' }),
|
|
70
|
+
event('task_started'),
|
|
71
|
+
turnContext({ model: 'gpt-5.5', cwd: project }),
|
|
72
|
+
event('agent_message', { message: 'hook answer' }),
|
|
73
|
+
event('task_complete'),
|
|
74
|
+
tokenCountEvent({
|
|
75
|
+
inputTokens: 12345,
|
|
76
|
+
outputTokens: 67,
|
|
77
|
+
contextWindow: 258400,
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const result = await runCodexStopHook({
|
|
83
|
+
payload: {
|
|
84
|
+
session_id: threadId,
|
|
85
|
+
transcript_path: rolloutPath,
|
|
86
|
+
cwd: project,
|
|
87
|
+
},
|
|
88
|
+
env: {},
|
|
89
|
+
db,
|
|
90
|
+
ensureMonitorTask: ({ cwd }) => {
|
|
91
|
+
monitorTaskCwd = cwd;
|
|
92
|
+
},
|
|
93
|
+
writeMonitorState: (state) => {
|
|
94
|
+
monitorState = state;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert.equal(result.status, 'ok');
|
|
99
|
+
assert.equal(result.codexThreadIdSource, 'payload:session_id');
|
|
100
|
+
assert.equal(result.captured.sessionId, `codex:${threadId}`);
|
|
101
|
+
assert.equal(result.captured.capturedTurns, 1);
|
|
102
|
+
assert.equal(result.captured.capturedRows, 2);
|
|
103
|
+
assert.equal(result.summarized.status, 'skipped');
|
|
104
|
+
assert.equal(result.summarized.reason, 'within_l2_window');
|
|
105
|
+
assert.equal(monitorState.sessionId, `codex:${threadId}`);
|
|
106
|
+
assert.equal(monitorState.host, 'codex');
|
|
107
|
+
assert.equal(monitorState.projectPath, project);
|
|
108
|
+
assert.equal(monitorState.transcriptPath, null);
|
|
109
|
+
assert.equal(monitorState.rolloutPath, rolloutPath);
|
|
110
|
+
assert.equal(monitorState.usage.tokens, 12345);
|
|
111
|
+
assert.equal(monitorState.usage.model, 'gpt-5.5');
|
|
112
|
+
assert.equal(monitorState.usage.source, 'codex-rollout-token-count');
|
|
113
|
+
assert.equal(result.monitorState.sessionId, `codex:${threadId}`);
|
|
114
|
+
assert.equal(result.autoRefresh.status, 'skipped');
|
|
115
|
+
assert.equal(result.autoRefresh.reason, 'below_threshold');
|
|
116
|
+
assert.equal(monitorTaskCwd, project);
|
|
117
|
+
} finally {
|
|
118
|
+
db.close();
|
|
119
|
+
rmSync(codexHome, { recursive: true, force: true });
|
|
120
|
+
rmSync(project, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('codex-hook stop runs auto refresh when verified usage reaches 90%', async () => {
|
|
125
|
+
const codexHome = mkdtempSync(join(tmpdir(), 'tl-codex-hook-home-'));
|
|
126
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-hook-project-'));
|
|
127
|
+
const threadId = '019dfaba-f87e-7f41-a144-d5ca7c6dd7f9';
|
|
128
|
+
const db = makeDb();
|
|
129
|
+
let autoRefreshArgs = null;
|
|
130
|
+
try {
|
|
131
|
+
const rolloutPath = writeRollout(codexHome, {
|
|
132
|
+
id: threadId,
|
|
133
|
+
cwd: project,
|
|
134
|
+
events: [
|
|
135
|
+
event('user_message', { message: 'hook request' }),
|
|
136
|
+
event('task_started'),
|
|
137
|
+
turnContext({ model: 'gpt-5.5', cwd: project }),
|
|
138
|
+
event('agent_message', { message: 'hook answer' }),
|
|
139
|
+
event('task_complete'),
|
|
140
|
+
tokenCountEvent({
|
|
141
|
+
inputTokens: 240_000,
|
|
142
|
+
outputTokens: 67,
|
|
143
|
+
contextWindow: 258400,
|
|
144
|
+
}),
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await runCodexStopHook({
|
|
149
|
+
payload: {
|
|
150
|
+
session_id: threadId,
|
|
151
|
+
transcript_path: rolloutPath,
|
|
152
|
+
cwd: project,
|
|
153
|
+
},
|
|
154
|
+
env: {},
|
|
155
|
+
db,
|
|
156
|
+
ensureMonitorTask: () => {},
|
|
157
|
+
writeMonitorState: () => {},
|
|
158
|
+
runAutoRefresh: async (args) => {
|
|
159
|
+
autoRefreshArgs = args;
|
|
160
|
+
return {
|
|
161
|
+
status: 'refreshed-live',
|
|
162
|
+
reason: 'rollback_and_inject_sent_live',
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
assert.equal(result.status, 'ok');
|
|
168
|
+
assert.equal(result.autoRefresh.status, 'refreshed-live');
|
|
169
|
+
assert.equal(autoRefreshArgs.threadId, threadId);
|
|
170
|
+
assert.equal(autoRefreshArgs.codexThreadIdSource, 'payload:session_id');
|
|
171
|
+
assert.equal(autoRefreshArgs.codexHome, codexHome);
|
|
172
|
+
assert.equal(autoRefreshArgs.projectPath, project);
|
|
173
|
+
assert.equal(autoRefreshArgs.sessionId, `codex:${threadId}`);
|
|
174
|
+
assert.equal(autoRefreshArgs.usage.tokens, 240_000);
|
|
175
|
+
assert.equal(autoRefreshArgs.usage.contextWindowSize, 258400);
|
|
176
|
+
assert.equal(autoRefreshArgs.usage.estimated, false);
|
|
177
|
+
assert.equal(autoRefreshArgs.command, 'codex');
|
|
178
|
+
} finally {
|
|
179
|
+
db.close();
|
|
180
|
+
rmSync(codexHome, { recursive: true, force: true });
|
|
181
|
+
rmSync(project, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('codex-hook stop reports camelCase payload thread id source', async () => {
|
|
186
|
+
const codexHome = mkdtempSync(join(tmpdir(), 'tl-codex-hook-home-'));
|
|
187
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-hook-project-'));
|
|
188
|
+
const threadId = '019dfaba-f87e-7f41-a144-d5ca7c6dd7f9';
|
|
189
|
+
const db = makeDb();
|
|
190
|
+
try {
|
|
191
|
+
const rolloutPath = writeRollout(codexHome, {
|
|
192
|
+
id: threadId,
|
|
193
|
+
cwd: project,
|
|
194
|
+
events: [
|
|
195
|
+
event('user_message', { message: 'hook request' }),
|
|
196
|
+
event('task_started'),
|
|
197
|
+
event('agent_message', { message: 'hook answer' }),
|
|
198
|
+
event('task_complete'),
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const result = await runCodexStopHook({
|
|
203
|
+
payload: {
|
|
204
|
+
sessionId: threadId,
|
|
205
|
+
transcriptPath: rolloutPath,
|
|
206
|
+
cwd: project,
|
|
207
|
+
},
|
|
208
|
+
env: {},
|
|
209
|
+
db,
|
|
210
|
+
ensureMonitorTask: () => {},
|
|
211
|
+
writeMonitorState: () => {},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
assert.equal(result.status, 'ok');
|
|
215
|
+
assert.equal(result.codexThreadIdSource, 'payload:sessionId');
|
|
216
|
+
assert.equal(result.captured.sessionId, `codex:${threadId}`);
|
|
217
|
+
} finally {
|
|
218
|
+
db.close();
|
|
219
|
+
rmSync(codexHome, { recursive: true, force: true });
|
|
220
|
+
rmSync(project, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('codex-hook stop skips cleanly when Codex thread id is unavailable', async () => {
|
|
225
|
+
const db = makeDb();
|
|
226
|
+
try {
|
|
227
|
+
const result = await runCodexStopHook({
|
|
228
|
+
payload: { cwd: process.cwd() },
|
|
229
|
+
env: {},
|
|
230
|
+
db,
|
|
231
|
+
});
|
|
232
|
+
assert.equal(result.status, 'skipped');
|
|
233
|
+
assert.equal(result.reason, 'codex_thread_id_not_available');
|
|
234
|
+
} finally {
|
|
235
|
+
db.close();
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('codexHomeFromTranscriptPath infers CODEX_HOME from rollout path', () => {
|
|
240
|
+
const path = '/tmp/codex-home/sessions/2026/05/06/rollout-2026-05-06T09-40-50-id.jsonl';
|
|
241
|
+
assert.equal(_internal.codexHomeFromTranscriptPath(path), '/tmp/codex-home');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
function writeRollout(home, { id, cwd, events }) {
|
|
245
|
+
const dir = join(home, 'sessions', '2026', '05', '06');
|
|
246
|
+
mkdirSync(dir, { recursive: true });
|
|
247
|
+
const path = join(dir, `rollout-2026-05-06T09-40-50-${id}.jsonl`);
|
|
248
|
+
writeFileSync(path, [sessionMeta(id, cwd), ...events].map((row) => JSON.stringify(row)).join('\n') + '\n');
|
|
249
|
+
return path;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function sessionMeta(id, cwd) {
|
|
253
|
+
return {
|
|
254
|
+
timestamp: '2026-05-06T00:40:50.000Z',
|
|
255
|
+
type: 'session_meta',
|
|
256
|
+
payload: {
|
|
257
|
+
id,
|
|
258
|
+
timestamp: '2026-05-06T00:40:50.000Z',
|
|
259
|
+
cwd,
|
|
260
|
+
source: 'vscode',
|
|
261
|
+
cli_version: '0.128.0-alpha.1',
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function event(type, payload = {}) {
|
|
267
|
+
return {
|
|
268
|
+
timestamp: '2026-05-06T00:41:00.000Z',
|
|
269
|
+
type: 'event_msg',
|
|
270
|
+
payload: { type, ...payload },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function turnContext(payload = {}) {
|
|
275
|
+
return {
|
|
276
|
+
timestamp: '2026-05-06T00:41:00.000Z',
|
|
277
|
+
type: 'turn_context',
|
|
278
|
+
payload,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function tokenCountEvent({ inputTokens, outputTokens, contextWindow }) {
|
|
283
|
+
return event('token_count', {
|
|
284
|
+
info: {
|
|
285
|
+
last_token_usage: {
|
|
286
|
+
input_tokens: inputTokens,
|
|
287
|
+
output_tokens: outputTokens,
|
|
288
|
+
total_tokens: inputTokens + outputTokens,
|
|
289
|
+
},
|
|
290
|
+
model_context_window: contextWindow,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { runCodexHostPrimitiveAudit } from '../codex-host-primitive-audit.mjs';
|
|
2
|
+
|
|
3
|
+
function parseArgs(args) {
|
|
4
|
+
const out = {
|
|
5
|
+
json: false,
|
|
6
|
+
command: process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex',
|
|
7
|
+
schemaDir: null,
|
|
8
|
+
keepGeneratedSchema: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg === '--json') {
|
|
14
|
+
out.json = true;
|
|
15
|
+
} else if (arg === '--codex-app-server-bin') {
|
|
16
|
+
const value = args[++i];
|
|
17
|
+
if (!value || value.startsWith('-')) throw new Error('--codex-app-server-bin requires a command path');
|
|
18
|
+
out.command = value;
|
|
19
|
+
} else if (arg === '--schema-dir') {
|
|
20
|
+
const value = args[++i];
|
|
21
|
+
if (!value || value.startsWith('-')) throw new Error('--schema-dir requires a path');
|
|
22
|
+
out.schemaDir = value;
|
|
23
|
+
} else if (arg === '--keep-generated-schema') {
|
|
24
|
+
out.keepGeneratedSchema = true;
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderTextResult(result) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
lines.push('throughline codex host primitive audit');
|
|
36
|
+
lines.push('');
|
|
37
|
+
lines.push(` status: ${result.status}`);
|
|
38
|
+
lines.push(` reason: ${result.reason}`);
|
|
39
|
+
lines.push(` proof scope: ${result.proofScope ?? 'none'}`);
|
|
40
|
+
lines.push(` restart safe: ${result.restartSafePrimitive ? 'yes' : 'no'}`);
|
|
41
|
+
if (result.generation?.outDir) {
|
|
42
|
+
lines.push(
|
|
43
|
+
` schema: ${result.generation.outDir}${result.schemaRetained ? '' : ' (temporary, removed)'}`,
|
|
44
|
+
);
|
|
45
|
+
} else if (result.schemaDir) {
|
|
46
|
+
lines.push(` schema: ${result.schemaDir}`);
|
|
47
|
+
}
|
|
48
|
+
if (result.facts) {
|
|
49
|
+
const hasNonResurrection =
|
|
50
|
+
result.facts.hasCurrentThreadNonResurrectionPrimitive ??
|
|
51
|
+
result.facts.hasCurrentThreadRemediationPrimitive;
|
|
52
|
+
lines.push('');
|
|
53
|
+
lines.push(` thread/rollback: ${result.facts.threadRollback ? 'present' : 'absent'}`);
|
|
54
|
+
lines.push(` thread/inject_items: ${result.facts.threadInjectItems ? 'present' : 'absent'}`);
|
|
55
|
+
lines.push(` thread/compact/start: ${result.facts.threadCompactStart ? 'present' : 'absent'}`);
|
|
56
|
+
lines.push(` thread/read: ${result.facts.threadRead ? 'present' : 'absent'}`);
|
|
57
|
+
lines.push(` thread/turns/list: ${result.facts.threadTurnsList ? 'present' : 'absent'}`);
|
|
58
|
+
lines.push(` thread/resume(history): ${result.facts.threadResumeHistory?.reason ?? 'unknown'}`);
|
|
59
|
+
lines.push(
|
|
60
|
+
` current-thread non-resurrection: ${hasNonResurrection ? 'candidate' : 'absent'}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (result.repairContract) {
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(` repair contract: ${result.repairContract.status}`);
|
|
66
|
+
lines.push(` repair scope: ${result.repairContract.scope}`);
|
|
67
|
+
for (const criterion of result.repairContract.criteria ?? []) {
|
|
68
|
+
const evidence =
|
|
69
|
+
Array.isArray(criterion.evidence) && criterion.evidence.length > 0
|
|
70
|
+
? ` (${criterion.evidence.join(', ')})`
|
|
71
|
+
: '';
|
|
72
|
+
lines.push(` repair criterion: ${criterion.id} = ${criterion.status}${evidence}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const decision of result.decisions ?? []) {
|
|
76
|
+
lines.push(` decision: ${decision.id} = ${decision.status}`);
|
|
77
|
+
}
|
|
78
|
+
if (result.recommendation) {
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push(` recommendation: ${result.recommendation.status}`);
|
|
81
|
+
lines.push(` next: ${result.recommendation.nextAction}`);
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function run(args) {
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = parseArgs(args);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
92
|
+
process.stderr.write(`[codex-host-primitive-audit] ${msg}\n`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = runCodexHostPrimitiveAudit(parsed);
|
|
97
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
98
|
+
else process.stdout.write(renderTextResult(result) + '\n');
|
|
99
|
+
process.exit(
|
|
100
|
+
result.status === 'host-primitive-audit-blocked' ||
|
|
101
|
+
result.status === 'host-primitive-audit-needs-live-validation'
|
|
102
|
+
? 0
|
|
103
|
+
: 1,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const _internal = {
|
|
108
|
+
parseArgs,
|
|
109
|
+
renderTextResult,
|
|
110
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { _internal } from './codex-host-primitive-audit.mjs';
|
|
5
|
+
|
|
6
|
+
test('codex-host-primitive-audit CLI parses schema and output options', () => {
|
|
7
|
+
assert.deepEqual(
|
|
8
|
+
_internal.parseArgs([
|
|
9
|
+
'--json',
|
|
10
|
+
'--codex-app-server-bin',
|
|
11
|
+
'/tmp/codex',
|
|
12
|
+
'--schema-dir',
|
|
13
|
+
'/tmp/schema',
|
|
14
|
+
'--keep-generated-schema',
|
|
15
|
+
]),
|
|
16
|
+
{
|
|
17
|
+
json: true,
|
|
18
|
+
command: '/tmp/codex',
|
|
19
|
+
schemaDir: '/tmp/schema',
|
|
20
|
+
keepGeneratedSchema: true,
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('codex-host-primitive-audit text output shows diagnostic recommendation', () => {
|
|
26
|
+
const text = _internal.renderTextResult({
|
|
27
|
+
status: 'host-primitive-audit-blocked',
|
|
28
|
+
reason: 'no_current_thread_restore_non_resurrection_primitive',
|
|
29
|
+
proofScope: 'codex_app_server_protocol_schema_only',
|
|
30
|
+
restartSafePrimitive: false,
|
|
31
|
+
schemaRetained: true,
|
|
32
|
+
schemaDir: '/tmp/schema',
|
|
33
|
+
facts: {
|
|
34
|
+
threadRollback: true,
|
|
35
|
+
threadInjectItems: true,
|
|
36
|
+
threadCompactStart: true,
|
|
37
|
+
threadRead: false,
|
|
38
|
+
threadTurnsList: true,
|
|
39
|
+
threadResumeHistory: { reason: 'thread_resume_history_is_marked_do_not_use' },
|
|
40
|
+
hasCurrentThreadRemediationPrimitive: false,
|
|
41
|
+
hasCurrentThreadNonResurrectionPrimitive: false,
|
|
42
|
+
},
|
|
43
|
+
repairContract: {
|
|
44
|
+
status: 'blocked-missing-current-thread-non-resurrection-guarantee',
|
|
45
|
+
scope: 'host_agnostic_same_thread_repair_contract',
|
|
46
|
+
criteria: [
|
|
47
|
+
{
|
|
48
|
+
id: 'same_current_thread_repair_primitive',
|
|
49
|
+
status: 'missing',
|
|
50
|
+
evidence: [],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'restore_source_non_resurrection_guarantee',
|
|
54
|
+
status: 'missing',
|
|
55
|
+
evidence: [],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
decisions: [{ id: 'non_resurrection_guarantee_absent', status: 'blocking' }],
|
|
60
|
+
recommendation: {
|
|
61
|
+
status: 'diagnostic-only',
|
|
62
|
+
nextAction: 'do not enable',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.match(text, /status:\s+host-primitive-audit-blocked/);
|
|
67
|
+
assert.match(text, /current-thread non-resurrection:\s+absent/);
|
|
68
|
+
assert.match(text, /repair contract: blocked-missing-current-thread-non-resurrection-guarantee/);
|
|
69
|
+
assert.match(text, /repair criterion: same_current_thread_repair_primitive = missing/);
|
|
70
|
+
assert.match(
|
|
71
|
+
text,
|
|
72
|
+
/repair criterion: restore_source_non_resurrection_guarantee = missing/,
|
|
73
|
+
);
|
|
74
|
+
assert.match(text, /decision: non_resurrection_guarantee_absent = blocking/);
|
|
75
|
+
});
|