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.
- package/.claude/commands/tl-trim.md +42 -0
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +583 -0
- package/README.ja.md +42 -5
- package/README.md +400 -23
- package/bin/throughline.mjs +168 -4
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +146 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +184 -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/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 +227 -4
- package/src/cli/install.test.mjs +207 -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 +97 -0
- 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 +326 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +44 -140
- package/src/resume-context.test.mjs +172 -0
- package/src/session-start.mjs +8 -5
- 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 +94 -6
- package/src/vscode-task.test.mjs +186 -6
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } 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
|
+
const BIN_PATH = join(REPO_ROOT, 'bin/throughline.mjs');
|
|
11
|
+
const EXPERIMENTAL_ENV = 'THROUGHLINE_EXPERIMENTAL_CODEX_ROLLBACK_MODEL_VISIBLE_SMOKE';
|
|
12
|
+
|
|
13
|
+
function makeFakeCodexAppServer(dir, mode) {
|
|
14
|
+
const script = join(dir, `fake-codex-rollback-model-visible-${mode}.mjs`);
|
|
15
|
+
const delta =
|
|
16
|
+
mode === 'visible'
|
|
17
|
+
? 'TL_ROLLBACK_MODEL_VISIBLE_SECRET'
|
|
18
|
+
: 'TL_ROLLBACK_MODEL_VISIBLE_NOT_VISIBLE';
|
|
19
|
+
writeFileSync(
|
|
20
|
+
script,
|
|
21
|
+
`#!/usr/bin/env node
|
|
22
|
+
import { createInterface } from 'node:readline';
|
|
23
|
+
const rl = createInterface({ input: process.stdin });
|
|
24
|
+
let turns = [{ id: 'turn-1' }];
|
|
25
|
+
function send(message) { process.stdout.write(JSON.stringify(message) + '\\n'); }
|
|
26
|
+
rl.on('line', (line) => {
|
|
27
|
+
const msg = JSON.parse(line);
|
|
28
|
+
if (msg.method === 'initialized') return;
|
|
29
|
+
if (msg.method === 'initialize') {
|
|
30
|
+
send({ id: msg.id, result: { userAgent: 'fake-codex' } });
|
|
31
|
+
} else if (msg.method === 'thread/read' || msg.method === 'thread/resume') {
|
|
32
|
+
send({ id: msg.id, result: { thread: { id: msg.params.threadId, turns } } });
|
|
33
|
+
} else if (msg.method === 'thread/rollback') {
|
|
34
|
+
turns = turns.slice(0, Math.max(0, turns.length - msg.params.numTurns));
|
|
35
|
+
send({ id: msg.id, result: { thread: { id: msg.params.threadId, turns } } });
|
|
36
|
+
} else if (msg.method === 'turn/start') {
|
|
37
|
+
const prompt = JSON.stringify(msg.params.input);
|
|
38
|
+
if (prompt.includes('verification') && prompt.includes('TL_ROLLBACK_MODEL_VISIBLE_SECRET')) {
|
|
39
|
+
send({ id: msg.id, error: { code: -32000, message: 'full marker leaked into verify prompt' } });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
turns = [...turns, { id: 'turn-' + (turns.length + 1) }];
|
|
43
|
+
if (prompt.includes('verification')) {
|
|
44
|
+
send({ method: 'item/agentMessage/delta', params: { threadId: msg.params.threadId, turnId: 'turn-verify', itemId: 'item-1', delta: '${delta}' } });
|
|
45
|
+
}
|
|
46
|
+
send({ method: 'turn/completed', params: { threadId: msg.params.threadId, turn: { id: 'turn-current' } } });
|
|
47
|
+
send({ id: msg.id, result: { turn: { id: 'turn-current' } } });
|
|
48
|
+
} else {
|
|
49
|
+
send({ id: msg.id, error: { code: -32601, message: 'unknown method' } });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
`,
|
|
53
|
+
);
|
|
54
|
+
chmodSync(script, 0o755);
|
|
55
|
+
return script;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runSmoke(project, args = [], extraEnv = {}) {
|
|
59
|
+
return spawnSync(
|
|
60
|
+
process.execPath,
|
|
61
|
+
[BIN_PATH, 'codex-rollback-model-visible-smoke', ...args],
|
|
62
|
+
{
|
|
63
|
+
cwd: project,
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
...extraEnv,
|
|
67
|
+
},
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('codex-rollback-model-visible-smoke refuses without explicit experimental env', () => {
|
|
74
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-rollback-visible-cli-'));
|
|
75
|
+
try {
|
|
76
|
+
const result = runSmoke(project, [
|
|
77
|
+
'--verify',
|
|
78
|
+
'--codex-thread-id',
|
|
79
|
+
'thread-rollback-visible',
|
|
80
|
+
'--marker',
|
|
81
|
+
'TL_ROLLBACK_MODEL_VISIBLE_SECRET',
|
|
82
|
+
'--json',
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
assert.equal(result.status, 1);
|
|
86
|
+
const payload = JSON.parse(result.stdout);
|
|
87
|
+
assert.equal(payload.status, 'refused');
|
|
88
|
+
assert.equal(payload.reason, 'experimental_env_required');
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(project, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('codex-rollback-model-visible-smoke prepare starts and rolls back a marker turn', () => {
|
|
95
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-rollback-visible-cli-'));
|
|
96
|
+
try {
|
|
97
|
+
const fake = makeFakeCodexAppServer(project, 'hidden');
|
|
98
|
+
const result = runSmoke(
|
|
99
|
+
project,
|
|
100
|
+
[
|
|
101
|
+
'--prepare',
|
|
102
|
+
'--codex-thread-id',
|
|
103
|
+
'thread-rollback-visible',
|
|
104
|
+
'--marker',
|
|
105
|
+
'TL_ROLLBACK_MODEL_VISIBLE_SECRET',
|
|
106
|
+
'--codex-app-server-bin',
|
|
107
|
+
fake,
|
|
108
|
+
'--json',
|
|
109
|
+
],
|
|
110
|
+
{ [EXPERIMENTAL_ENV]: '1' },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
assert.equal(result.status, 0, result.stderr);
|
|
114
|
+
const payload = JSON.parse(result.stdout);
|
|
115
|
+
assert.equal(payload.mode, 'prepare');
|
|
116
|
+
assert.equal(payload.status, 'prepared');
|
|
117
|
+
assert.equal(payload.rollbackSent, true);
|
|
118
|
+
assert.match(payload.nextCommand, /codex-rollback-model-visible-smoke --verify/);
|
|
119
|
+
assert.match(payload.nextCommand, /TL_ROLLBACK_MODEL_VISIBLE_SECRET/);
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(project, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('codex-rollback-model-visible-smoke marker-file keeps generated marker out of prepare output', () => {
|
|
126
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-rollback-visible-cli-'));
|
|
127
|
+
try {
|
|
128
|
+
const fake = makeFakeCodexAppServer(project, 'hidden');
|
|
129
|
+
const markerFile = join(project, 'rollback-marker.json');
|
|
130
|
+
const result = runSmoke(
|
|
131
|
+
project,
|
|
132
|
+
[
|
|
133
|
+
'--prepare',
|
|
134
|
+
'--codex-thread-id',
|
|
135
|
+
'thread-rollback-visible',
|
|
136
|
+
'--marker-file',
|
|
137
|
+
markerFile,
|
|
138
|
+
'--codex-app-server-bin',
|
|
139
|
+
fake,
|
|
140
|
+
'--json',
|
|
141
|
+
],
|
|
142
|
+
{ [EXPERIMENTAL_ENV]: '1' },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
assert.equal(result.status, 0, result.stderr);
|
|
146
|
+
const payload = JSON.parse(result.stdout);
|
|
147
|
+
const markerPayload = JSON.parse(readFileSync(markerFile, 'utf8'));
|
|
148
|
+
assert.equal(payload.status, 'prepared');
|
|
149
|
+
assert.equal(payload.marker, '[redacted]');
|
|
150
|
+
assert.equal(payload.markerRedacted, true);
|
|
151
|
+
assert.equal(payload.markerFile, markerFile);
|
|
152
|
+
assert.match(markerPayload.marker, /^TL_ROLLBACK_MODEL_VISIBLE_/);
|
|
153
|
+
assert.doesNotMatch(result.stdout, new RegExp(markerPayload.marker));
|
|
154
|
+
assert.match(payload.nextCommand, /--marker-file/);
|
|
155
|
+
assert.doesNotMatch(payload.nextCommand, new RegExp(markerPayload.marker));
|
|
156
|
+
} finally {
|
|
157
|
+
rmSync(project, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('codex-rollback-model-visible-smoke verify exits zero when marker is not reproduced', () => {
|
|
162
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-rollback-visible-cli-'));
|
|
163
|
+
try {
|
|
164
|
+
const fake = makeFakeCodexAppServer(project, 'hidden');
|
|
165
|
+
const result = runSmoke(
|
|
166
|
+
project,
|
|
167
|
+
[
|
|
168
|
+
'--verify',
|
|
169
|
+
'--codex-thread-id',
|
|
170
|
+
'thread-rollback-visible',
|
|
171
|
+
'--marker',
|
|
172
|
+
'TL_ROLLBACK_MODEL_VISIBLE_SECRET',
|
|
173
|
+
'--codex-app-server-bin',
|
|
174
|
+
fake,
|
|
175
|
+
'--json',
|
|
176
|
+
],
|
|
177
|
+
{ [EXPERIMENTAL_ENV]: '1' },
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
assert.equal(result.status, 0, result.stderr);
|
|
181
|
+
const payload = JSON.parse(result.stdout);
|
|
182
|
+
assert.equal(payload.mode, 'verify');
|
|
183
|
+
assert.equal(payload.status, 'not-reproduced');
|
|
184
|
+
assert.equal(payload.promptIncludesMarker, false);
|
|
185
|
+
assert.equal(payload.rolledBackMarkerModelVisible, false);
|
|
186
|
+
assert.equal(payload.modelReportedNotVisible, true);
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(project, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('codex-rollback-model-visible-smoke verify exits nonzero when marker is reproduced', () => {
|
|
193
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-rollback-visible-cli-'));
|
|
194
|
+
try {
|
|
195
|
+
const fake = makeFakeCodexAppServer(project, 'visible');
|
|
196
|
+
const result = runSmoke(
|
|
197
|
+
project,
|
|
198
|
+
[
|
|
199
|
+
'--verify',
|
|
200
|
+
'--codex-thread-id',
|
|
201
|
+
'thread-rollback-visible',
|
|
202
|
+
'--marker',
|
|
203
|
+
'TL_ROLLBACK_MODEL_VISIBLE_SECRET',
|
|
204
|
+
'--codex-app-server-bin',
|
|
205
|
+
fake,
|
|
206
|
+
'--json',
|
|
207
|
+
],
|
|
208
|
+
{ [EXPERIMENTAL_ENV]: '1' },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
assert.equal(result.status, 1);
|
|
212
|
+
const payload = JSON.parse(result.stdout);
|
|
213
|
+
assert.equal(payload.mode, 'verify');
|
|
214
|
+
assert.equal(payload.status, 'reproduced');
|
|
215
|
+
assert.equal(payload.promptIncludesMarker, false);
|
|
216
|
+
assert.equal(payload.rolledBackMarkerModelVisible, true);
|
|
217
|
+
assert.equal(payload.modelReportedNotVisible, false);
|
|
218
|
+
assert.deepEqual(payload.observedMarkers, ['TL_ROLLBACK_MODEL_VISIBLE_SECRET']);
|
|
219
|
+
} finally {
|
|
220
|
+
rmSync(project, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('codex-rollback-model-visible-smoke marker-file redacts reproduced marker from verify output', () => {
|
|
225
|
+
const project = mkdtempSync(join(tmpdir(), 'tl-codex-rollback-visible-cli-'));
|
|
226
|
+
try {
|
|
227
|
+
const fake = makeFakeCodexAppServer(project, 'visible');
|
|
228
|
+
const markerFile = join(project, 'rollback-marker.json');
|
|
229
|
+
writeFileSync(markerFile, JSON.stringify({ marker: 'TL_ROLLBACK_MODEL_VISIBLE_SECRET' }));
|
|
230
|
+
const result = runSmoke(
|
|
231
|
+
project,
|
|
232
|
+
[
|
|
233
|
+
'--verify',
|
|
234
|
+
'--codex-thread-id',
|
|
235
|
+
'thread-rollback-visible',
|
|
236
|
+
'--marker-file',
|
|
237
|
+
markerFile,
|
|
238
|
+
'--codex-app-server-bin',
|
|
239
|
+
fake,
|
|
240
|
+
'--json',
|
|
241
|
+
],
|
|
242
|
+
{ [EXPERIMENTAL_ENV]: '1' },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
assert.equal(result.status, 1);
|
|
246
|
+
const payload = JSON.parse(result.stdout);
|
|
247
|
+
assert.equal(payload.status, 'reproduced');
|
|
248
|
+
assert.equal(payload.marker, '[redacted]');
|
|
249
|
+
assert.equal(payload.markerRedacted, true);
|
|
250
|
+
assert.deepEqual(payload.observedMarkers, ['[redacted]']);
|
|
251
|
+
assert.doesNotMatch(result.stdout, /TL_ROLLBACK_MODEL_VISIBLE_SECRET/);
|
|
252
|
+
} finally {
|
|
253
|
+
rmSync(project, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { diagnoseCodexSidecar } from '../codex-sidecar.mjs';
|
|
2
|
+
|
|
3
|
+
function parseArgs(args) {
|
|
4
|
+
const out = {
|
|
5
|
+
projectPath: process.cwd(),
|
|
6
|
+
preset: 'review',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < args.length; i++) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
if (arg === '--project') {
|
|
12
|
+
const value = args[++i];
|
|
13
|
+
if (!value || value.startsWith('-')) {
|
|
14
|
+
throw new Error('--project requires a path');
|
|
15
|
+
}
|
|
16
|
+
out.projectPath = value;
|
|
17
|
+
} else if (arg === '--preset') {
|
|
18
|
+
const value = args[++i];
|
|
19
|
+
if (!value || value.startsWith('-')) {
|
|
20
|
+
throw new Error('--preset requires a value');
|
|
21
|
+
}
|
|
22
|
+
out.preset = value;
|
|
23
|
+
} else {
|
|
24
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function run(args) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = parseArgs(args);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
37
|
+
process.stderr.write(`[codex-sidecar-diagnostics] ${msg}\n`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = diagnoseCodexSidecar({
|
|
42
|
+
projectPath: parsed.projectPath,
|
|
43
|
+
preset: parsed.preset,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
47
|
+
process.exit(result.status === 'configured' ? 0 : 1);
|
|
48
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { inferWorkflowForPreset, runCodexSidecarDryRun } from '../codex-sidecar.mjs';
|
|
2
|
+
|
|
3
|
+
function parseArgs(args) {
|
|
4
|
+
const out = {
|
|
5
|
+
projectPath: process.cwd(),
|
|
6
|
+
workflow: null,
|
|
7
|
+
preset: 'review',
|
|
8
|
+
contextFile: null,
|
|
9
|
+
turnTimeoutMs: 30_000,
|
|
10
|
+
promptParts: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
if (arg === '--project') {
|
|
16
|
+
const value = args[++i];
|
|
17
|
+
if (!value || value.startsWith('-')) {
|
|
18
|
+
throw new Error('--project requires a path');
|
|
19
|
+
}
|
|
20
|
+
out.projectPath = value;
|
|
21
|
+
} else if (arg === '--workflow') {
|
|
22
|
+
const value = args[++i];
|
|
23
|
+
if (!value || value.startsWith('-')) {
|
|
24
|
+
throw new Error('--workflow requires a value');
|
|
25
|
+
}
|
|
26
|
+
out.workflow = value;
|
|
27
|
+
} else if (arg === '--preset') {
|
|
28
|
+
const value = args[++i];
|
|
29
|
+
if (!value || value.startsWith('-')) {
|
|
30
|
+
throw new Error('--preset requires a value');
|
|
31
|
+
}
|
|
32
|
+
out.preset = value;
|
|
33
|
+
} else if (arg === '--context-file') {
|
|
34
|
+
const value = args[++i];
|
|
35
|
+
if (!value || value.startsWith('-')) {
|
|
36
|
+
throw new Error('--context-file requires a path');
|
|
37
|
+
}
|
|
38
|
+
out.contextFile = value;
|
|
39
|
+
} else if (arg === '--turn-timeout-ms') {
|
|
40
|
+
const value = args[++i];
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
43
|
+
throw new Error('--turn-timeout-ms must be a positive integer');
|
|
44
|
+
}
|
|
45
|
+
out.turnTimeoutMs = parsed;
|
|
46
|
+
} else if (arg.startsWith('-')) {
|
|
47
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
48
|
+
} else {
|
|
49
|
+
out.promptParts.push(arg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
projectPath: out.projectPath,
|
|
55
|
+
workflow: out.workflow ?? inferWorkflowForPreset(out.preset),
|
|
56
|
+
preset: out.preset,
|
|
57
|
+
contextFile: out.contextFile,
|
|
58
|
+
turnTimeoutMs: out.turnTimeoutMs,
|
|
59
|
+
prompt: out.promptParts.length > 0 ? out.promptParts.join(' ') : null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function run(args) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parseArgs(args);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
69
|
+
process.stderr.write(`[codex-sidecar-dry-run] ${msg}\n`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = runCodexSidecarDryRun({
|
|
74
|
+
projectPath: parsed.projectPath,
|
|
75
|
+
workflow: parsed.workflow,
|
|
76
|
+
preset: parsed.preset,
|
|
77
|
+
contextFile: parsed.contextFile,
|
|
78
|
+
prompt: parsed.prompt,
|
|
79
|
+
turnTimeoutMs: parsed.turnTimeoutMs,
|
|
80
|
+
timeoutMs: parsed.turnTimeoutMs + 5_000,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
84
|
+
process.exit(result.status === 'dry-run' ? 0 : 1);
|
|
85
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { getDb } from '../db.mjs';
|
|
2
|
+
import {
|
|
3
|
+
L2_WINDOW,
|
|
4
|
+
countDistinctBodyTurns,
|
|
5
|
+
pickOldestUnsummarizedTurn,
|
|
6
|
+
} from '../turn-processor.mjs';
|
|
7
|
+
import { summarizeToL1 } from '../haiku-summarizer.mjs';
|
|
8
|
+
|
|
9
|
+
function parseArgs(args) {
|
|
10
|
+
const out = {
|
|
11
|
+
sessionId: null,
|
|
12
|
+
codexThreadId: null,
|
|
13
|
+
json: false,
|
|
14
|
+
max: 1,
|
|
15
|
+
projectPath: process.cwd(),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < args.length; i++) {
|
|
19
|
+
const arg = args[i];
|
|
20
|
+
if (arg === '--session') {
|
|
21
|
+
const value = args[++i];
|
|
22
|
+
if (!value || value.startsWith('-')) throw new Error('--session requires a session id');
|
|
23
|
+
out.sessionId = value;
|
|
24
|
+
} else if (arg === '--codex-thread-id') {
|
|
25
|
+
const value = args[++i];
|
|
26
|
+
if (!value || value.startsWith('-')) {
|
|
27
|
+
throw new Error('--codex-thread-id requires a thread id');
|
|
28
|
+
}
|
|
29
|
+
out.codexThreadId = value;
|
|
30
|
+
} else if (arg === '--max') {
|
|
31
|
+
const value = Number(args[++i]);
|
|
32
|
+
if (!Number.isInteger(value) || value < 1) throw new Error('--max must be a positive integer');
|
|
33
|
+
out.max = value;
|
|
34
|
+
} else if (arg === '--project') {
|
|
35
|
+
const value = args[++i];
|
|
36
|
+
if (!value || value.startsWith('-')) throw new Error('--project requires a path');
|
|
37
|
+
out.projectPath = value;
|
|
38
|
+
} else if (arg === '--json') {
|
|
39
|
+
out.json = true;
|
|
40
|
+
} else if (!arg.startsWith('-') && !out.sessionId) {
|
|
41
|
+
out.sessionId = arg;
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!out.sessionId && out.codexThreadId) {
|
|
48
|
+
out.sessionId = `codex:${out.codexThreadId}`;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findLatestCodexSessionId(db, projectPath) {
|
|
54
|
+
const row = db
|
|
55
|
+
.prepare(
|
|
56
|
+
`SELECT session_id
|
|
57
|
+
FROM sessions
|
|
58
|
+
WHERE lower(project_path) = lower(?)
|
|
59
|
+
AND session_id LIKE 'codex:%'
|
|
60
|
+
ORDER BY updated_at DESC
|
|
61
|
+
LIMIT 1`,
|
|
62
|
+
)
|
|
63
|
+
.get(projectPath);
|
|
64
|
+
return row?.session_id ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildL2ForSummary(rows) {
|
|
68
|
+
return rows
|
|
69
|
+
.map((row) => `[${row.role}]: ${row.text}`)
|
|
70
|
+
.join('\n\n')
|
|
71
|
+
.trim();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loadTurnRows(db, { sessionId, originSessionId, turnNumber }) {
|
|
75
|
+
return db
|
|
76
|
+
.prepare(
|
|
77
|
+
`SELECT role, text, created_at
|
|
78
|
+
FROM bodies
|
|
79
|
+
WHERE session_id = ? AND origin_session_id = ? AND turn_number = ?
|
|
80
|
+
ORDER BY created_at ASC, id ASC`,
|
|
81
|
+
)
|
|
82
|
+
.all(sessionId, originSessionId, turnNumber);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function insertSkeleton(db, { sessionId, originSessionId, turnNumber, summary, createdAt }) {
|
|
86
|
+
const result = db
|
|
87
|
+
.prepare(
|
|
88
|
+
`INSERT OR IGNORE INTO skeletons
|
|
89
|
+
(session_id, origin_session_id, turn_number, role, summary, created_at)
|
|
90
|
+
VALUES (?, ?, ?, 'assistant', ?, ?)`,
|
|
91
|
+
)
|
|
92
|
+
.run(sessionId, originSessionId, turnNumber, summary, createdAt);
|
|
93
|
+
return result.changes > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function summarizeCodexSession(db, { sessionId, projectPath, max, env = process.env }) {
|
|
97
|
+
if (!sessionId?.startsWith('codex:')) {
|
|
98
|
+
throw new Error('codex-summarize requires a codex:<thread-id> session');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const totalTurns = countDistinctBodyTurns(db, sessionId);
|
|
102
|
+
if (totalTurns <= L2_WINDOW) {
|
|
103
|
+
return {
|
|
104
|
+
status: 'skipped',
|
|
105
|
+
reason: 'within_l2_window',
|
|
106
|
+
sessionId,
|
|
107
|
+
totalTurns,
|
|
108
|
+
l2Window: L2_WINDOW,
|
|
109
|
+
summarized: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const summarized = [];
|
|
114
|
+
for (let i = 0; i < max; i++) {
|
|
115
|
+
const oldest = pickOldestUnsummarizedTurn(db, sessionId);
|
|
116
|
+
if (!oldest) break;
|
|
117
|
+
const rows = loadTurnRows(db, {
|
|
118
|
+
sessionId,
|
|
119
|
+
originSessionId: oldest.origin_session_id,
|
|
120
|
+
turnNumber: oldest.turn_number,
|
|
121
|
+
});
|
|
122
|
+
const l2Text = buildL2ForSummary(rows);
|
|
123
|
+
const result = summarizeToL1(l2Text, {
|
|
124
|
+
projectPath,
|
|
125
|
+
hostMode: 'codex-primary',
|
|
126
|
+
env,
|
|
127
|
+
});
|
|
128
|
+
const inserted = insertSkeleton(db, {
|
|
129
|
+
sessionId,
|
|
130
|
+
originSessionId: oldest.origin_session_id,
|
|
131
|
+
turnNumber: oldest.turn_number,
|
|
132
|
+
summary: result.summary,
|
|
133
|
+
createdAt: oldest.created_at,
|
|
134
|
+
});
|
|
135
|
+
summarized.push({
|
|
136
|
+
originSessionId: oldest.origin_session_id,
|
|
137
|
+
turnNumber: oldest.turn_number,
|
|
138
|
+
source: result.source ?? 'codex-cli',
|
|
139
|
+
inserted,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
status: summarized.length > 0 ? 'summarized' : 'skipped',
|
|
145
|
+
reason: summarized.length > 0 ? 'codex_cli_l1_written' : 'no_unsummarized_turns',
|
|
146
|
+
sessionId,
|
|
147
|
+
totalTurns,
|
|
148
|
+
l2Window: L2_WINDOW,
|
|
149
|
+
summarized,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderTextResult(result) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push('throughline codex summarize');
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push(` status: ${result.status}`);
|
|
158
|
+
lines.push(` reason: ${result.reason}`);
|
|
159
|
+
lines.push(` session: ${result.sessionId}`);
|
|
160
|
+
lines.push(` totalTurns: ${result.totalTurns}`);
|
|
161
|
+
lines.push(` l2Window: ${result.l2Window}`);
|
|
162
|
+
lines.push(` summarized: ${result.summarized.length}`);
|
|
163
|
+
for (const row of result.summarized) {
|
|
164
|
+
lines.push(` - turn ${row.turnNumber} (${row.source})`);
|
|
165
|
+
}
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function run(args) {
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = parseArgs(args);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
175
|
+
process.stderr.write(`[codex-summarize] ${msg}\n`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const db = getDb();
|
|
180
|
+
const sessionId = parsed.sessionId ?? findLatestCodexSessionId(db, parsed.projectPath);
|
|
181
|
+
if (!sessionId) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
'[codex-summarize] no Codex session found for this project. Pass --session codex:<thread-id> explicitly.\n',
|
|
184
|
+
);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const result = summarizeCodexSession(db, {
|
|
190
|
+
sessionId,
|
|
191
|
+
projectPath: parsed.projectPath,
|
|
192
|
+
max: parsed.max,
|
|
193
|
+
});
|
|
194
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
195
|
+
else process.stdout.write(renderTextResult(result) + '\n');
|
|
196
|
+
process.exit(result.status === 'summarized' || result.status === 'skipped' ? 0 : 1);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
199
|
+
if (parsed.json) {
|
|
200
|
+
process.stdout.write(
|
|
201
|
+
JSON.stringify(
|
|
202
|
+
{
|
|
203
|
+
status: 'error',
|
|
204
|
+
reason: err?.reason ?? 'codex_summarize_failed',
|
|
205
|
+
source: err?.source ?? null,
|
|
206
|
+
message: msg,
|
|
207
|
+
stderr: err?.stderr ?? '',
|
|
208
|
+
exitCode: err?.exitCode ?? null,
|
|
209
|
+
},
|
|
210
|
+
null,
|
|
211
|
+
2,
|
|
212
|
+
) + '\n',
|
|
213
|
+
);
|
|
214
|
+
} else {
|
|
215
|
+
process.stderr.write(`[codex-summarize] ${msg}\n`);
|
|
216
|
+
}
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const _internal = {
|
|
222
|
+
parseArgs,
|
|
223
|
+
summarizeCodexSession,
|
|
224
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { listCodexThreadCandidates } from '../codex-thread-index.mjs';
|
|
2
|
+
|
|
3
|
+
function parseArgs(args) {
|
|
4
|
+
const out = {
|
|
5
|
+
json: false,
|
|
6
|
+
allProjects: false,
|
|
7
|
+
limit: 10,
|
|
8
|
+
codexHome: null,
|
|
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 === '--all-projects') {
|
|
16
|
+
out.allProjects = true;
|
|
17
|
+
} else if (arg === '--limit') {
|
|
18
|
+
const value = Number(args[++i]);
|
|
19
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
20
|
+
throw new Error('--limit must be an integer >= 1');
|
|
21
|
+
}
|
|
22
|
+
out.limit = value;
|
|
23
|
+
} else if (arg === '--codex-home') {
|
|
24
|
+
const value = args[++i];
|
|
25
|
+
if (!value || value.startsWith('-')) {
|
|
26
|
+
throw new Error('--codex-home requires a path');
|
|
27
|
+
}
|
|
28
|
+
out.codexHome = value;
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function run(args) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = parseArgs(args);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
43
|
+
process.stderr.write(`[codex-threads] ${msg}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const candidates = listCodexThreadCandidates({
|
|
48
|
+
codexHome: parsed.codexHome ?? undefined,
|
|
49
|
+
projectPath: process.cwd(),
|
|
50
|
+
allProjects: parsed.allProjects,
|
|
51
|
+
limit: parsed.limit,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (parsed.json) {
|
|
55
|
+
process.stdout.write(JSON.stringify({ candidates }, null, 2) + '\n');
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.stdout.write(renderReport(candidates, { allProjects: parsed.allProjects }) + '\n');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderReport(candidates, { allProjects }) {
|
|
64
|
+
const lines = [];
|
|
65
|
+
lines.push('## Codex Thread Candidates');
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(`Scope: ${allProjects ? 'all projects' : 'current project'}`);
|
|
68
|
+
lines.push('Read-only: yes');
|
|
69
|
+
lines.push('');
|
|
70
|
+
|
|
71
|
+
if (candidates.length === 0) {
|
|
72
|
+
lines.push('No Codex rollout candidates found.');
|
|
73
|
+
lines.push('Pass --all-projects to inspect other projects, or --codex-home <path> for a non-default CODEX_HOME.');
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const candidate of candidates) {
|
|
78
|
+
lines.push(`- ${candidate.id}`);
|
|
79
|
+
if (candidate.threadName) lines.push(` name: ${candidate.threadName}`);
|
|
80
|
+
lines.push(` updated: ${candidate.updatedAt ?? 'unknown'}`);
|
|
81
|
+
lines.push(` cwd: ${candidate.cwd ?? 'unknown'}`);
|
|
82
|
+
lines.push(` rollout: ${candidate.rolloutPath}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('Use one candidate explicitly with throughline trim --host codex --codex-thread-id <id>.');
|
|
87
|
+
lines.push('This command never selects a thread for automatic trim.');
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|