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,138 @@
|
|
|
1
|
+
import { getDb } from '../db.mjs';
|
|
2
|
+
import { buildHandoffRecord } from '../handoff-record.mjs';
|
|
3
|
+
import {
|
|
4
|
+
renderCodexActiveWorkContext,
|
|
5
|
+
renderCodexNewThreadHandoff,
|
|
6
|
+
toCodexDeveloperMessageItem,
|
|
7
|
+
} from '../codex-handoff.mjs';
|
|
8
|
+
|
|
9
|
+
const FORMATS = new Set(['text', 'handoff', 'item-json']);
|
|
10
|
+
|
|
11
|
+
async function readStdin() {
|
|
12
|
+
let raw = '';
|
|
13
|
+
await new Promise((resolve) => {
|
|
14
|
+
process.stdin.setEncoding('utf8');
|
|
15
|
+
process.stdin.on('data', (chunk) => {
|
|
16
|
+
raw += chunk;
|
|
17
|
+
});
|
|
18
|
+
process.stdin.on('end', resolve);
|
|
19
|
+
});
|
|
20
|
+
return raw;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseArgs(args) {
|
|
24
|
+
const out = {
|
|
25
|
+
sessionId: null,
|
|
26
|
+
format: 'text',
|
|
27
|
+
memoStdin: false,
|
|
28
|
+
maxDetailRefs: undefined,
|
|
29
|
+
maxRecentBodies: undefined,
|
|
30
|
+
maxBodyChars: undefined,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if (arg === '--session') {
|
|
36
|
+
const value = args[++i];
|
|
37
|
+
if (!value || value.startsWith('-')) {
|
|
38
|
+
throw new Error('--session requires a session id');
|
|
39
|
+
}
|
|
40
|
+
out.sessionId = value;
|
|
41
|
+
} else if (arg === '--format') {
|
|
42
|
+
const value = args[++i];
|
|
43
|
+
if (!FORMATS.has(value)) {
|
|
44
|
+
throw new Error('--format must be text, handoff, or item-json');
|
|
45
|
+
}
|
|
46
|
+
out.format = value;
|
|
47
|
+
} else if (arg === '--max-detail-refs') {
|
|
48
|
+
const value = Number(args[++i]);
|
|
49
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
50
|
+
throw new Error('--max-detail-refs must be a non-negative integer');
|
|
51
|
+
}
|
|
52
|
+
out.maxDetailRefs = value;
|
|
53
|
+
} else if (arg === '--max-recent-bodies') {
|
|
54
|
+
const value = Number(args[++i]);
|
|
55
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
56
|
+
throw new Error('--max-recent-bodies must be a non-negative integer');
|
|
57
|
+
}
|
|
58
|
+
out.maxRecentBodies = value;
|
|
59
|
+
} else if (arg === '--max-body-chars') {
|
|
60
|
+
const value = Number(args[++i]);
|
|
61
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
62
|
+
throw new Error('--max-body-chars must be a non-negative integer');
|
|
63
|
+
}
|
|
64
|
+
out.maxBodyChars = value;
|
|
65
|
+
} else if (arg === '--memo-stdin') {
|
|
66
|
+
out.memoStdin = true;
|
|
67
|
+
} else if (!arg.startsWith('-') && !out.sessionId) {
|
|
68
|
+
out.sessionId = arg;
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findLatestCodexSessionId(db, projectPath) {
|
|
78
|
+
const row = db
|
|
79
|
+
.prepare(
|
|
80
|
+
`SELECT session_id
|
|
81
|
+
FROM sessions
|
|
82
|
+
WHERE lower(project_path) = lower(?)
|
|
83
|
+
AND session_id LIKE 'codex:%'
|
|
84
|
+
ORDER BY updated_at DESC
|
|
85
|
+
LIMIT 1`,
|
|
86
|
+
)
|
|
87
|
+
.get(projectPath);
|
|
88
|
+
return row?.session_id ?? null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function run(args) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = parseArgs(args);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
97
|
+
process.stderr.write(`[codex-resume] ${msg}\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const inflightMemo = parsed.memoStdin ? await readStdin() : null;
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const sessionId = parsed.sessionId ?? findLatestCodexSessionId(db, process.cwd());
|
|
104
|
+
if (!sessionId) {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
'[codex-resume] no Codex session found for this project. Pass --session codex:<thread-id> explicitly.\n',
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const record = buildHandoffRecord(db, {
|
|
112
|
+
sessionId,
|
|
113
|
+
isInheritance: false,
|
|
114
|
+
inflightMemo,
|
|
115
|
+
});
|
|
116
|
+
if (!record) {
|
|
117
|
+
process.stderr.write(`[codex-resume] no handoff memory found for session ${sessionId}\n`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (parsed.format === 'item-json') {
|
|
122
|
+
process.stdout.write(JSON.stringify(toCodexDeveloperMessageItem(record), null, 2) + '\n');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.format === 'handoff') {
|
|
127
|
+
process.stdout.write(
|
|
128
|
+
renderCodexNewThreadHandoff(record, {
|
|
129
|
+
maxDetailRefs: parsed.maxDetailRefs,
|
|
130
|
+
maxRecentBodies: parsed.maxRecentBodies,
|
|
131
|
+
maxBodyChars: parsed.maxBodyChars,
|
|
132
|
+
}) + '\n',
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.stdout.write(renderCodexActiveWorkContext(record) + '\n');
|
|
138
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
runCodexRollbackModelVisiblePrepare,
|
|
7
|
+
runCodexRollbackModelVisibleVerify,
|
|
8
|
+
} from '../codex-app-server.mjs';
|
|
9
|
+
import { resolveCodexThreadIdentity } from '../codex-thread-identity.mjs';
|
|
10
|
+
|
|
11
|
+
const EXPERIMENTAL_ENV = 'THROUGHLINE_EXPERIMENTAL_CODEX_ROLLBACK_MODEL_VISIBLE_SMOKE';
|
|
12
|
+
const MARKER_PREFIX = 'TL_ROLLBACK_MODEL_VISIBLE_';
|
|
13
|
+
const NOT_VISIBLE_TOKEN = 'TL_ROLLBACK_MODEL_VISIBLE_NOT_VISIBLE';
|
|
14
|
+
|
|
15
|
+
function parseArgs(args) {
|
|
16
|
+
const out = {
|
|
17
|
+
mode: null,
|
|
18
|
+
codexThreadId: null,
|
|
19
|
+
marker: null,
|
|
20
|
+
markerFile: null,
|
|
21
|
+
markerPrefix: MARKER_PREFIX,
|
|
22
|
+
markerPrefixExplicit: false,
|
|
23
|
+
json: false,
|
|
24
|
+
afterVsCodeRestart: false,
|
|
25
|
+
codexAppServerBin: null,
|
|
26
|
+
timeoutMs: 180_000,
|
|
27
|
+
requestTimeoutMs: 150_000,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
const arg = args[i];
|
|
32
|
+
if (arg === '--prepare') {
|
|
33
|
+
if (out.mode) throw new Error('pass only one of --prepare or --verify');
|
|
34
|
+
out.mode = 'prepare';
|
|
35
|
+
} else if (arg === '--verify') {
|
|
36
|
+
if (out.mode) throw new Error('pass only one of --prepare or --verify');
|
|
37
|
+
out.mode = 'verify';
|
|
38
|
+
} else if (arg === '--codex-thread-id') {
|
|
39
|
+
const value = args[++i];
|
|
40
|
+
if (!value || value.startsWith('-')) {
|
|
41
|
+
throw new Error('--codex-thread-id requires a thread id');
|
|
42
|
+
}
|
|
43
|
+
out.codexThreadId = value;
|
|
44
|
+
} else if (arg === '--marker') {
|
|
45
|
+
const value = args[++i];
|
|
46
|
+
if (!value || value.startsWith('-')) {
|
|
47
|
+
throw new Error('--marker requires a marker string');
|
|
48
|
+
}
|
|
49
|
+
out.marker = value;
|
|
50
|
+
} else if (arg === '--marker-file') {
|
|
51
|
+
const value = args[++i];
|
|
52
|
+
if (!value || value.startsWith('-')) {
|
|
53
|
+
throw new Error('--marker-file requires a path');
|
|
54
|
+
}
|
|
55
|
+
out.markerFile = value;
|
|
56
|
+
} else if (arg === '--marker-prefix') {
|
|
57
|
+
const value = args[++i];
|
|
58
|
+
if (!value || value.startsWith('-')) {
|
|
59
|
+
throw new Error('--marker-prefix requires a prefix string');
|
|
60
|
+
}
|
|
61
|
+
out.markerPrefix = value;
|
|
62
|
+
out.markerPrefixExplicit = true;
|
|
63
|
+
} else if (arg === '--after-vscode-restart') {
|
|
64
|
+
out.afterVsCodeRestart = true;
|
|
65
|
+
} else if (arg === '--codex-app-server-bin') {
|
|
66
|
+
const value = args[++i];
|
|
67
|
+
if (!value || value.startsWith('-')) {
|
|
68
|
+
throw new Error('--codex-app-server-bin requires a command path');
|
|
69
|
+
}
|
|
70
|
+
out.codexAppServerBin = value;
|
|
71
|
+
} else if (arg === '--timeout-ms') {
|
|
72
|
+
const value = Number(args[++i]);
|
|
73
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
74
|
+
throw new Error('--timeout-ms must be a positive integer');
|
|
75
|
+
}
|
|
76
|
+
out.timeoutMs = value;
|
|
77
|
+
} else if (arg === '--request-timeout-ms') {
|
|
78
|
+
const value = Number(args[++i]);
|
|
79
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
80
|
+
throw new Error('--request-timeout-ms must be a positive integer');
|
|
81
|
+
}
|
|
82
|
+
out.requestTimeoutMs = value;
|
|
83
|
+
} else if (arg === '--json') {
|
|
84
|
+
out.json = true;
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!out.mode) out.mode = 'verify';
|
|
91
|
+
if (out.marker && out.markerFile) {
|
|
92
|
+
throw new Error('pass only one of --marker or --marker-file');
|
|
93
|
+
}
|
|
94
|
+
if (out.mode === 'prepare' && out.markerFile && !out.marker && !out.markerPrefixExplicit) {
|
|
95
|
+
out.markerPrefix = `${MARKER_PREFIX}${randomUUID().replaceAll('-', '').slice(0, 8)}_`;
|
|
96
|
+
}
|
|
97
|
+
if (out.mode === 'prepare' && !out.marker) {
|
|
98
|
+
out.marker = `${out.markerPrefix}${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function markerFilePath(path) {
|
|
105
|
+
return resolve(process.cwd(), path);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readMarkerFile(path) {
|
|
109
|
+
const raw = readFileSync(markerFilePath(path), 'utf8');
|
|
110
|
+
try {
|
|
111
|
+
const payload = JSON.parse(raw);
|
|
112
|
+
if (typeof payload.marker === 'string' && payload.marker) {
|
|
113
|
+
return {
|
|
114
|
+
marker: payload.marker,
|
|
115
|
+
markerPrefix:
|
|
116
|
+
typeof payload.markerPrefix === 'string' && payload.markerPrefix
|
|
117
|
+
? payload.markerPrefix
|
|
118
|
+
: MARKER_PREFIX,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
const marker = raw.trim();
|
|
123
|
+
if (marker) return { marker, markerPrefix: MARKER_PREFIX };
|
|
124
|
+
}
|
|
125
|
+
throw new Error('--marker-file must contain a marker string or JSON with marker');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function writeMarkerFile(path, payload) {
|
|
129
|
+
writeFileSync(markerFilePath(path), `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function renderTextResult(result) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
lines.push('throughline codex rollback model-visible smoke');
|
|
135
|
+
lines.push('');
|
|
136
|
+
lines.push(` mode: ${result.mode ?? 'unknown'}`);
|
|
137
|
+
lines.push(` status: ${result.status}`);
|
|
138
|
+
lines.push(` reason: ${result.reason}`);
|
|
139
|
+
lines.push(` thread: ${result.threadId ?? 'unknown'}`);
|
|
140
|
+
lines.push(` marker: ${result.marker ?? 'unknown'}`);
|
|
141
|
+
lines.push(` proof scope: ${result.proofScope ?? 'none'}`);
|
|
142
|
+
lines.push(` restart safe: ${result.restartSafe ? 'yes' : 'no'}`);
|
|
143
|
+
if (result.afterVsCodeRestart !== undefined) {
|
|
144
|
+
lines.push(` after restart: ${result.afterVsCodeRestart ? 'yes' : 'no'}`);
|
|
145
|
+
}
|
|
146
|
+
if (result.promptIncludesMarker !== undefined) {
|
|
147
|
+
lines.push(` prompt includes marker: ${result.promptIncludesMarker ? 'yes' : 'no'}`);
|
|
148
|
+
}
|
|
149
|
+
if (result.rolledBackMarkerModelVisible !== undefined) {
|
|
150
|
+
const visible =
|
|
151
|
+
result.rolledBackMarkerModelVisible === true
|
|
152
|
+
? 'yes'
|
|
153
|
+
: result.rolledBackMarkerModelVisible === false
|
|
154
|
+
? 'no'
|
|
155
|
+
: 'unknown';
|
|
156
|
+
lines.push(` rolled-back marker visible: ${visible}`);
|
|
157
|
+
}
|
|
158
|
+
if (result.modelReportedNotVisible !== undefined) {
|
|
159
|
+
lines.push(` model reported not visible: ${result.modelReportedNotVisible ? 'yes' : 'no'}`);
|
|
160
|
+
}
|
|
161
|
+
if (result.setupTurnStartSent !== undefined) {
|
|
162
|
+
lines.push(` setup turn: ${result.setupTurnStartSent ? 'started' : 'not-started'}`);
|
|
163
|
+
}
|
|
164
|
+
if (result.rollbackSent !== undefined) {
|
|
165
|
+
lines.push(` rollback: ${result.rollbackSent ? 'sent' : 'not-sent'}`);
|
|
166
|
+
}
|
|
167
|
+
if (result.turnStartSent !== undefined) {
|
|
168
|
+
lines.push(` verify turn: ${result.turnStartSent ? 'started' : 'not-started'}`);
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(result.observedMarkers)) {
|
|
171
|
+
lines.push(` observed markers: ${result.observedMarkers.length}`);
|
|
172
|
+
}
|
|
173
|
+
if (result.agentText) {
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('agent text:');
|
|
176
|
+
lines.push(result.agentText);
|
|
177
|
+
}
|
|
178
|
+
if (result.nextCommand) {
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push('next command:');
|
|
181
|
+
lines.push(result.nextCommand);
|
|
182
|
+
}
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function requireExperimentalEnv(parsed) {
|
|
187
|
+
if (process.env[EXPERIMENTAL_ENV] === '1') return null;
|
|
188
|
+
return {
|
|
189
|
+
mode: parsed.mode,
|
|
190
|
+
status: 'refused',
|
|
191
|
+
reason: 'experimental_env_required',
|
|
192
|
+
requiredEnv: `${EXPERIMENTAL_ENV}=1`,
|
|
193
|
+
proofScope: 'none',
|
|
194
|
+
restartSafe: false,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildNextCommand({ marker, markerFile, threadId, appServerBin }) {
|
|
199
|
+
const parts = [
|
|
200
|
+
`${EXPERIMENTAL_ENV}=1`,
|
|
201
|
+
'throughline',
|
|
202
|
+
'codex-rollback-model-visible-smoke',
|
|
203
|
+
'--verify',
|
|
204
|
+
'--codex-thread-id',
|
|
205
|
+
threadId,
|
|
206
|
+
];
|
|
207
|
+
if (markerFile) {
|
|
208
|
+
parts.push('--marker-file', markerFile);
|
|
209
|
+
} else {
|
|
210
|
+
parts.push('--marker', marker);
|
|
211
|
+
}
|
|
212
|
+
parts.push('--after-vscode-restart');
|
|
213
|
+
if (appServerBin) {
|
|
214
|
+
parts.push('--codex-app-server-bin', appServerBin);
|
|
215
|
+
}
|
|
216
|
+
return parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function escapeRegExp(value) {
|
|
220
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function maybeRedactMarker(result, { markerFile, markerPrefix }) {
|
|
224
|
+
if (!markerFile) return result;
|
|
225
|
+
const redacted = {
|
|
226
|
+
...result,
|
|
227
|
+
marker: '[redacted]',
|
|
228
|
+
markerFile,
|
|
229
|
+
markerRedacted: true,
|
|
230
|
+
};
|
|
231
|
+
if (Array.isArray(redacted.observedMarkers) && redacted.observedMarkers.length > 0) {
|
|
232
|
+
redacted.observedMarkers = redacted.observedMarkers.map(() => '[redacted]');
|
|
233
|
+
}
|
|
234
|
+
if (typeof redacted.agentText === 'string' && typeof result.marker === 'string') {
|
|
235
|
+
redacted.agentText = redacted.agentText.replaceAll(result.marker, '[redacted]');
|
|
236
|
+
if (markerPrefix) {
|
|
237
|
+
redacted.agentText = redacted.agentText.replace(
|
|
238
|
+
new RegExp(`${escapeRegExp(markerPrefix)}[A-Za-z0-9_-]+`, 'g'),
|
|
239
|
+
'[redacted]',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return redacted;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function runPrepare(parsed) {
|
|
247
|
+
const marker = parsed.marker;
|
|
248
|
+
if (parsed.markerFile) {
|
|
249
|
+
writeMarkerFile(parsed.markerFile, {
|
|
250
|
+
marker,
|
|
251
|
+
markerPrefix: parsed.markerPrefix,
|
|
252
|
+
threadId: parsed.codexThreadId,
|
|
253
|
+
preparedAt: new Date().toISOString(),
|
|
254
|
+
mode: 'codex-rollback-model-visible-smoke',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const command = parsed.codexAppServerBin ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex';
|
|
258
|
+
const result = await runCodexRollbackModelVisiblePrepare({
|
|
259
|
+
threadId: parsed.codexThreadId,
|
|
260
|
+
cwd: process.cwd(),
|
|
261
|
+
marker,
|
|
262
|
+
command,
|
|
263
|
+
timeoutMs: parsed.timeoutMs,
|
|
264
|
+
requestTimeoutMs: parsed.requestTimeoutMs,
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
mode: 'prepare',
|
|
268
|
+
...result,
|
|
269
|
+
nextCommand: buildNextCommand({
|
|
270
|
+
marker,
|
|
271
|
+
markerFile: parsed.markerFile,
|
|
272
|
+
threadId: parsed.codexThreadId,
|
|
273
|
+
appServerBin: parsed.codexAppServerBin,
|
|
274
|
+
}),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runVerify(parsed) {
|
|
279
|
+
if (parsed.markerFile) {
|
|
280
|
+
const markerPayload = readMarkerFile(parsed.markerFile);
|
|
281
|
+
parsed.marker = markerPayload.marker;
|
|
282
|
+
parsed.markerPrefix = markerPayload.markerPrefix;
|
|
283
|
+
}
|
|
284
|
+
if (!parsed.marker) {
|
|
285
|
+
return {
|
|
286
|
+
mode: 'verify',
|
|
287
|
+
status: 'refused',
|
|
288
|
+
reason: 'marker_required_for_verify',
|
|
289
|
+
proofScope: 'none',
|
|
290
|
+
restartSafe: false,
|
|
291
|
+
threadId: parsed.codexThreadId,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (!parsed.marker.startsWith(parsed.markerPrefix)) {
|
|
295
|
+
return {
|
|
296
|
+
mode: 'verify',
|
|
297
|
+
status: 'refused',
|
|
298
|
+
reason: 'marker_prefix_mismatch',
|
|
299
|
+
expectedPrefix: parsed.markerPrefix,
|
|
300
|
+
proofScope: 'none',
|
|
301
|
+
restartSafe: false,
|
|
302
|
+
threadId: parsed.codexThreadId,
|
|
303
|
+
marker: parsed.marker,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const command = parsed.codexAppServerBin ?? process.env.THROUGHLINE_CODEX_APP_SERVER_BIN ?? 'codex';
|
|
308
|
+
const result = await runCodexRollbackModelVisibleVerify({
|
|
309
|
+
threadId: parsed.codexThreadId,
|
|
310
|
+
cwd: process.cwd(),
|
|
311
|
+
marker: parsed.marker,
|
|
312
|
+
markerPrefix: parsed.markerPrefix,
|
|
313
|
+
notVisibleToken: NOT_VISIBLE_TOKEN,
|
|
314
|
+
command,
|
|
315
|
+
timeoutMs: parsed.timeoutMs,
|
|
316
|
+
requestTimeoutMs: parsed.requestTimeoutMs,
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
mode: 'verify',
|
|
320
|
+
afterVsCodeRestart: parsed.afterVsCodeRestart,
|
|
321
|
+
...result,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function run(args) {
|
|
326
|
+
let parsed;
|
|
327
|
+
try {
|
|
328
|
+
parsed = parseArgs(args);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const msg = err instanceof Error ? err.message : 'unknown';
|
|
331
|
+
process.stderr.write(`[codex-rollback-model-visible-smoke] ${msg}\n`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
parsed = {
|
|
336
|
+
...parsed,
|
|
337
|
+
...resolveCodexThreadIdentity({ codexThreadId: parsed.codexThreadId }, process.env),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
let result = requireExperimentalEnv(parsed);
|
|
341
|
+
if (!result && !parsed.codexThreadId) {
|
|
342
|
+
result = {
|
|
343
|
+
mode: parsed.mode,
|
|
344
|
+
status: 'refused',
|
|
345
|
+
reason: 'codex_thread_id_required',
|
|
346
|
+
proofScope: 'none',
|
|
347
|
+
restartSafe: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!result) {
|
|
352
|
+
result = parsed.mode === 'prepare' ? await runPrepare(parsed) : await runVerify(parsed);
|
|
353
|
+
}
|
|
354
|
+
result = maybeRedactMarker(result, {
|
|
355
|
+
markerFile: parsed.markerFile,
|
|
356
|
+
markerPrefix: parsed.markerPrefix,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (parsed.json) process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
360
|
+
else {
|
|
361
|
+
const stream = result.status === 'refused' ? process.stderr : process.stdout;
|
|
362
|
+
stream.write(renderTextResult(result) + '\n');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const ok =
|
|
366
|
+
(parsed.mode === 'prepare' && result.status === 'prepared') ||
|
|
367
|
+
(parsed.mode === 'verify' && result.status === 'not-reproduced');
|
|
368
|
+
process.exit(ok ? 0 : 1);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export const _internal = {
|
|
372
|
+
parseArgs,
|
|
373
|
+
};
|