@linzumi/cli 1.0.112 → 1.0.113

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.
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ // F5 LIVE real-harness e2e (compute-nodes program, Workstream F): the
3
+ // cross-harness failover RUNG PATH against the REAL claude-code harness -
4
+ // no scripted double anywhere on the target side.
5
+ //
6
+ // 1. The failover ORACLE (the same bundle the conformance suite runs:
7
+ // scripts/build-cross-harness-failover-oracle.mjs) drives the REAL
8
+ // channelSession: a codex thread runs, its backend DIES mid-thread
9
+ // (the worker/IPC client closes - the precise F5 trigger), the fate
10
+ // ladder decides the cross_harness_failover rung, and the REAL
11
+ // transpilation bridge (T.2 exportPage -> T.4 rehydrateSession)
12
+ // seeds the REAL claude session store (~/.claude/projects/...).
13
+ // 2. The REAL @anthropic-ai/claude-agent-sdk validates the seeded
14
+ // session (getSessionInfo), reads the transpiled history back
15
+ // (getSessionMessages), and RESUMES it (query with options.resume),
16
+ // completing a REAL follow-up turn whose answer must reference
17
+ // content that exists ONLY in the failed codex thread's transpiled
18
+ // activity (the coherence probe).
19
+ //
20
+ // This is a LOCAL, manual, credentialed run (it spends one real model
21
+ // turn); it is NOT in any CI test list. Evidence lands in
22
+ // /tmp/linzumi-evidence-f5/ and is mirrored into the committed evidence
23
+ // doc (kandan/server_v2/web/docs/qa-evidence/2026-07-14-f5-failover/).
24
+ //
25
+ // Scope honesty (the T.4 precedent, extended): there is deliberately NO
26
+ // deployed-preview leg - the failover's kandan/codex sides ride the
27
+ // oracle's scripted doubles (killing a real production backend mid-thread
28
+ // on the preview is not an exercisable CI act), while the LADDER, the
29
+ // TRANSPILERS, the SESSION STORE, and the CLAUDE HARNESS are all real.
30
+
31
+ import { execFileSync } from 'node:child_process';
32
+ import { randomUUID } from 'node:crypto';
33
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
34
+ import { homedir } from 'node:os';
35
+ import { dirname, join, resolve } from 'node:path';
36
+ import { fileURLToPath } from 'node:url';
37
+
38
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
39
+ const evidenceDir = '/tmp/linzumi-evidence-f5';
40
+ mkdirSync(evidenceDir, { recursive: true });
41
+
42
+ function fail(message) {
43
+ console.error(`F5-LIVE-E2E FAIL: ${message}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ // --- 1. The REAL rung path: oracle -> ladder -> bridge -> REAL store ---------
48
+
49
+ execFileSync(
50
+ process.execPath,
51
+ [join(packageRoot, 'scripts', 'build-cross-harness-failover-oracle.mjs')],
52
+ { stdio: 'inherit' }
53
+ );
54
+
55
+ const cwdRaw = join('/tmp', `linzumi-f5-live-${Date.now()}`);
56
+ mkdirSync(cwdRaw, { recursive: true });
57
+ const cwd = realpathSync(cwdRaw);
58
+
59
+ const reportRaw = execFileSync(
60
+ process.execPath,
61
+ [join(packageRoot, '.differential', 'cross-harness-failover-oracle.mjs')],
62
+ {
63
+ env: {
64
+ ...process.env,
65
+ // The REAL store: the bridge seeds ~/.claude/projects/<munged-cwd>/.
66
+ F5_ORACLE_HOME: homedir(),
67
+ F5_ORACLE_CWD: cwd,
68
+ F5_ORACLE_EXECUTOR: 'bridge',
69
+ },
70
+ encoding: 'utf8',
71
+ }
72
+ );
73
+ const report = JSON.parse(reportRaw.trim());
74
+
75
+ const failoverStep = (report.ladderSteps ?? []).find(
76
+ (step) => step.rung === 'cross_harness_failover'
77
+ );
78
+ if (!failoverStep) fail('the ladder never decided the failover rung');
79
+ if (failoverStep.fate !== 'transient_local') {
80
+ fail(`unexpected fate: ${failoverStep.fate}`);
81
+ }
82
+ const dispatchedLog = (report.failoverLogs ?? []).find(
83
+ (log) => log.event === 'codex.fate_ladder_cross_harness_failover_dispatched'
84
+ );
85
+ if (!dispatchedLog) fail('no failover_dispatched receipt log');
86
+ if (dispatchedLog.payload.model !== 'oracle-model-slug') {
87
+ fail('the receipts did not carry the explicitly-passed model (D39)');
88
+ }
89
+ if (!report.seeded) fail('the bridge seeded no session store');
90
+ const { targetSessionId, sessionFile, transpiledItemCount } = report.seeded;
91
+ console.log('[f5-live] rung decided + REAL store seeded:', sessionFile);
92
+ console.log(
93
+ '[f5-live] transpiled items:',
94
+ transpiledItemCount,
95
+ '(redacted reasoning:',
96
+ report.seeded.redactedReasoningCount + ')'
97
+ );
98
+
99
+ writeFileSync(
100
+ join(evidenceDir, 'f5-live-oracle-report.json'),
101
+ JSON.stringify(report, null, 2) + '\n'
102
+ );
103
+
104
+ // The seeded store must contain the recap marker + the journal content.
105
+ const storeLines = readFileSync(sessionFile, 'utf8')
106
+ .trim()
107
+ .split('\n')
108
+ .map((line) => JSON.parse(line));
109
+ const storeText = JSON.stringify(storeLines);
110
+ if (!storeText.includes('[linzumi cross-harness resume]')) {
111
+ fail('the seeded store carries no cross-harness resume marker');
112
+ }
113
+ if (!storeText.includes('renamed the column and updated 3 call sites')) {
114
+ fail('the seeded store lost the transpiled codex activity');
115
+ }
116
+
117
+ // --- 2. The REAL SDK: validate, read back, RESUME, follow-up turn ------------
118
+
119
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
120
+
121
+ // Never run the harness as a nested claude-code session.
122
+ const env = { ...process.env };
123
+ delete env.CLAUDECODE;
124
+ delete env.CLAUDE_CODE_ENTRYPOINT;
125
+ delete env.CLAUDE_CODE_SSE_PORT;
126
+
127
+ const info = await sdk.getSessionInfo(targetSessionId, { dir: cwd });
128
+ if (!info) fail('the real SDK store did not find the seeded session');
129
+ console.log('[f5-live] getSessionInfo found the failover-seeded session');
130
+
131
+ const storedMessages = await sdk.getSessionMessages(targetSessionId, {
132
+ dir: cwd,
133
+ });
134
+ if (storedMessages.length !== storeLines.length) {
135
+ fail(
136
+ `store read-back count ${storedMessages.length} != seeded ${storeLines.length}`
137
+ );
138
+ }
139
+ console.log(
140
+ '[f5-live] getSessionMessages returned the seeded history:',
141
+ storedMessages.length,
142
+ 'messages'
143
+ );
144
+
145
+ // The coherence probe: answerable ONLY from the transpiled codex activity
146
+ // that crossed with the failover.
147
+ const probe =
148
+ 'Answer from this conversation only, in one short sentence: before the ' +
149
+ 'harness failover, how many call sites did the agent say it had already ' +
150
+ 'updated after renaming the column?';
151
+
152
+ const transcript = [];
153
+ const query = sdk.query({
154
+ prompt: probe,
155
+ options: {
156
+ cwd,
157
+ resume: targetSessionId,
158
+ model: 'haiku',
159
+ permissionMode: 'default',
160
+ allowedTools: [],
161
+ maxTurns: 1,
162
+ persistSession: true,
163
+ env,
164
+ },
165
+ });
166
+
167
+ let announcedSessionId = null;
168
+ let resultMessage = null;
169
+ let assistantText = '';
170
+ for await (const message of query) {
171
+ transcript.push(message);
172
+ if (message.type === 'system' && message.subtype === 'init') {
173
+ announcedSessionId = message.session_id;
174
+ console.log('[f5-live] system:init announced session:', announcedSessionId);
175
+ }
176
+ if (message.type === 'assistant') {
177
+ const blocks = message.message?.content ?? [];
178
+ for (const block of blocks) {
179
+ if (block.type === 'text') assistantText += block.text + '\n';
180
+ }
181
+ }
182
+ if (message.type === 'result') {
183
+ resultMessage = message;
184
+ break;
185
+ }
186
+ }
187
+
188
+ writeFileSync(
189
+ join(evidenceDir, 'f5-live-resume-transcript.ndjson'),
190
+ transcript.map((message) => JSON.stringify(message)).join('\n') + '\n'
191
+ );
192
+
193
+ if (!resultMessage) fail('the follow-up turn produced no result terminal');
194
+ if (resultMessage.is_error) {
195
+ fail(`the follow-up turn errored: ${JSON.stringify(resultMessage)}`);
196
+ }
197
+ console.log('[f5-live] follow-up turn completed:', resultMessage.subtype);
198
+ console.log('[f5-live] follow-up answer:', assistantText.trim());
199
+
200
+ const coherent = /\b(3|three)\b/i.test(assistantText);
201
+ if (!coherent) {
202
+ fail(
203
+ 'the follow-up answer does not reference the transpiled codex activity ' +
204
+ '(expected the 3 updated call sites) - the resume did not carry the ' +
205
+ 'failed thread across the harness boundary'
206
+ );
207
+ }
208
+ console.log(
209
+ '[f5-live] COHERENCE PROVED: the answer references the 3 updated call ' +
210
+ 'sites, which exist only in the transpiled codex activity'
211
+ );
212
+
213
+ const summary = {
214
+ ran_at: new Date().toISOString(),
215
+ trigger:
216
+ 'transient_local infra kill on a DEAD codex worker (the oracle-scripted backend), redispatch/worker_rebuild unavailable',
217
+ flag: 'LINZUMI_CROSS_HARNESS_FAILOVER=1 (default OFF)',
218
+ ladder_decision: failoverStep,
219
+ failover_receipt: dispatchedLog.payload,
220
+ source_harness: 'codex',
221
+ target_harness: 'claude_code',
222
+ target_session_id: targetSessionId,
223
+ announced_session_id: announcedSessionId,
224
+ session_file: sessionFile,
225
+ transpiled_item_count: transpiledItemCount,
226
+ redacted_reasoning_count: report.seeded.redactedReasoningCount,
227
+ follow_up_result_subtype: resultMessage.subtype,
228
+ follow_up_answer: assistantText.trim(),
229
+ coherence_probe: probe,
230
+ coherence_proved: true,
231
+ scope_honesty:
232
+ 'kandan/codex sides scripted by the oracle doubles; the ladder, the T transpilers, the session store, and the claude harness (SDK resume + real model turn) are all real. No deployed-preview leg (killing a real backend mid-thread on the preview is not an exercisable CI act).',
233
+ };
234
+ writeFileSync(
235
+ join(evidenceDir, 'f5-live-failover-summary.json'),
236
+ JSON.stringify(summary, null, 2) + '\n'
237
+ );
238
+ console.log('[f5-live] PASS - evidence in', evidenceDir);
239
+ process.exit(0);