@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.
- package/README.md +1 -1
- package/dist/crossHarnessFailover.mjs +6 -0
- package/dist/index.js +431 -431
- package/package.json +1 -1
- package/scripts/build-core-fate-ladder-parity-oracle.mjs +66 -0
- package/scripts/build-cross-harness-failover-oracle.mjs +349 -0
- package/scripts/build-differential-entry.mjs +21 -1
- package/scripts/build.mjs +40 -1
- package/scripts/f5_live_cross_harness_failover_e2e.mjs +239 -0
package/package.json
CHANGED
|
@@ -31,8 +31,13 @@ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
31
31
|
const driverSource = `
|
|
32
32
|
import {
|
|
33
33
|
appendFateLadderEntry,
|
|
34
|
+
commitFateLadderFailoverOutcome,
|
|
34
35
|
commitFateLadderHoldDispatch,
|
|
35
36
|
createFateLadderRun,
|
|
37
|
+
crossHarnessFailoverDetail,
|
|
38
|
+
crossHarnessFailoverEnabled,
|
|
39
|
+
crossHarnessFailoverReason,
|
|
40
|
+
crossHarnessFailoverRungSlot,
|
|
36
41
|
decideFateLadderStep,
|
|
37
42
|
defaultFateLadderBudgets,
|
|
38
43
|
fateLadderBudgets,
|
|
@@ -66,6 +71,22 @@ function runJson(run) {
|
|
|
66
71
|
atMs: entry.atMs,
|
|
67
72
|
detail: entry.detail === undefined ? null : entry.detail,
|
|
68
73
|
resetAtMs: entry.resetAtMs === undefined ? null : entry.resetAtMs,
|
|
74
|
+
failover:
|
|
75
|
+
entry.failover === undefined
|
|
76
|
+
? null
|
|
77
|
+
: {
|
|
78
|
+
sourceHarness: entry.failover.sourceHarness,
|
|
79
|
+
targetHarness: entry.failover.targetHarness,
|
|
80
|
+
model: entry.failover.model === undefined ? null : entry.failover.model,
|
|
81
|
+
transpiledItemCount:
|
|
82
|
+
entry.failover.transpiledItemCount === undefined
|
|
83
|
+
? null
|
|
84
|
+
: entry.failover.transpiledItemCount,
|
|
85
|
+
redactedReasoningCount:
|
|
86
|
+
entry.failover.redactedReasoningCount === undefined
|
|
87
|
+
? null
|
|
88
|
+
: entry.failover.redactedReasoningCount,
|
|
89
|
+
},
|
|
69
90
|
})),
|
|
70
91
|
};
|
|
71
92
|
}
|
|
@@ -131,8 +152,17 @@ function runScheduleCase(caseInput) {
|
|
|
131
152
|
inPlaceResume: op.grants?.inPlaceResume === true,
|
|
132
153
|
redispatch: op.grants?.redispatch === true,
|
|
133
154
|
workerRebuild: op.grants?.workerRebuild === true,
|
|
155
|
+
crossHarnessFailover: op.grants?.crossHarnessFailover === true,
|
|
134
156
|
},
|
|
135
157
|
rateLimitResetAtMs: op.resetAtMs ?? undefined,
|
|
158
|
+
failover:
|
|
159
|
+
op.failover === undefined || op.failover === null
|
|
160
|
+
? undefined
|
|
161
|
+
: {
|
|
162
|
+
sourceHarness: op.failover.sourceHarness,
|
|
163
|
+
targetHarness: op.failover.targetHarness,
|
|
164
|
+
model: op.failover.model ?? undefined,
|
|
165
|
+
},
|
|
136
166
|
});
|
|
137
167
|
if (decision.kind !== 'stand') {
|
|
138
168
|
current = decision.run;
|
|
@@ -145,6 +175,24 @@ function runScheduleCase(caseInput) {
|
|
|
145
175
|
current = commitFateLadderHoldDispatch(current, op.nowMs);
|
|
146
176
|
steps.push({ run: runJson(current) });
|
|
147
177
|
}
|
|
178
|
+
} else if (op.op === 'commitFailover') {
|
|
179
|
+
if (current === undefined) {
|
|
180
|
+
steps.push({ skipped: true });
|
|
181
|
+
} else {
|
|
182
|
+
current = commitFateLadderFailoverOutcome(current, {
|
|
183
|
+
outcome: op.outcome,
|
|
184
|
+
atMs: op.nowMs,
|
|
185
|
+
failover: {
|
|
186
|
+
sourceHarness: op.failover.sourceHarness,
|
|
187
|
+
targetHarness: op.failover.targetHarness,
|
|
188
|
+
model: op.failover.model ?? undefined,
|
|
189
|
+
transpiledItemCount: op.failover.transpiledItemCount ?? undefined,
|
|
190
|
+
redactedReasoningCount: op.failover.redactedReasoningCount ?? undefined,
|
|
191
|
+
},
|
|
192
|
+
detail: op.detail ?? undefined,
|
|
193
|
+
});
|
|
194
|
+
steps.push({ run: runJson(current) });
|
|
195
|
+
}
|
|
148
196
|
} else if (op.op === 'append') {
|
|
149
197
|
if (current === undefined) {
|
|
150
198
|
steps.push({ skipped: true });
|
|
@@ -187,7 +235,25 @@ function runCase(input) {
|
|
|
187
235
|
redispatchAfterLocalFailureReason,
|
|
188
236
|
workerRebuildRetryReason,
|
|
189
237
|
rateLimitHoldReasonPrefix,
|
|
238
|
+
crossHarnessFailoverRungSlot,
|
|
239
|
+
crossHarnessFailoverReason,
|
|
240
|
+
crossHarnessFailoverDetailSamples: [
|
|
241
|
+
crossHarnessFailoverDetail({
|
|
242
|
+
sourceHarness: 'codex',
|
|
243
|
+
targetHarness: 'claude_code',
|
|
244
|
+
model: 'parity-model-slug',
|
|
245
|
+
transpiledItemCount: 12,
|
|
246
|
+
redactedReasoningCount: 3,
|
|
247
|
+
}),
|
|
248
|
+
crossHarnessFailoverDetail({
|
|
249
|
+
sourceHarness: 'codex',
|
|
250
|
+
targetHarness: 'claude_code',
|
|
251
|
+
model: undefined,
|
|
252
|
+
}),
|
|
253
|
+
],
|
|
190
254
|
};
|
|
255
|
+
case 'failoverFlag':
|
|
256
|
+
return crossHarnessFailoverEnabled(input.env ?? {});
|
|
191
257
|
case 'budgets':
|
|
192
258
|
return fateLadderBudgets(input.env ?? {});
|
|
193
259
|
case 'holdReason':
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// F5 (compute-nodes program, Workstream F; plan kandan/plans/
|
|
2
|
+
// 2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md).
|
|
3
|
+
// Relationship: Builds the CROSS-HARNESS FAILOVER ORACLE - the in-process
|
|
4
|
+
// driver of the REAL rung path for the F5 differential-harness acceptance
|
|
5
|
+
// (packages/commander-harness/src/conformance/CrossHarnessFailoverConformance.res).
|
|
6
|
+
//
|
|
7
|
+
// The driver exercises the SHIPPED machinery end to end in one process:
|
|
8
|
+
// the REAL channelSession (attachChannelSession: the fate-ladder consult,
|
|
9
|
+
// the backend-dead trigger, the commit-then-advance receipts, the visible
|
|
10
|
+
// I382 rung state) against a scripted dead-able codex client, with the
|
|
11
|
+
// runner-shaped failover executor wired to the REAL transpilation bridge
|
|
12
|
+
// (src/crossHarnessFailover.mjs: T.2 HarnessCodexTranspiler.exportPage ->
|
|
13
|
+
// T.4 HarnessClaudeRehydration -> the claude session-store seeding into an
|
|
14
|
+
// isolated F5_ORACLE_HOME). The conformance suite then completes the
|
|
15
|
+
// resume leg on the seeded store through the REAL HarnessClaudeAdapter
|
|
16
|
+
// against the scripted SDK double (the T.4 Direction-A mechanism) - the
|
|
17
|
+
// spawned-commander environment cannot host a live claude harness, so the
|
|
18
|
+
// in-process oracle is the honest home for the full "the coding job
|
|
19
|
+
// resumes on the other harness" walk (the cluster-8/T.4 posture).
|
|
20
|
+
//
|
|
21
|
+
// Env knobs (set by the conformance suite):
|
|
22
|
+
// F5_ORACLE_HOME - the isolated claude-store root the bridge seeds
|
|
23
|
+
// F5_ORACLE_CWD - the thread's working directory (store keying)
|
|
24
|
+
// F5_ORACLE_EXECUTOR - 'bridge' (the REAL transpile+seed) | 'fail'
|
|
25
|
+
// (a scripted executor failure: the honest-red leg)
|
|
26
|
+
//
|
|
27
|
+
// Output: ONE JSON report line on stdout:
|
|
28
|
+
// { ladderSteps, failoverLogs, visibleReasons, messageStates,
|
|
29
|
+
// seeded: { targetSessionId, sessionFile, transpiledItemCount,
|
|
30
|
+
// redactedReasoningCount } | null,
|
|
31
|
+
// executorRequest: { model, journalTurnIds, journalItemCount } | null }
|
|
32
|
+
//
|
|
33
|
+
// Determinism: scripted data + milestone polls only; no wall-clock rows.
|
|
34
|
+
// The output is a local test artifact (.differential/ is gitignored).
|
|
35
|
+
import { mkdir } from 'node:fs/promises';
|
|
36
|
+
import { dirname, join } from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { build } from 'esbuild';
|
|
39
|
+
|
|
40
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
41
|
+
|
|
42
|
+
const driverSource = `
|
|
43
|
+
import { attachChannelSession } from './src/channelSession';
|
|
44
|
+
import type { HarnessActivityJournal } from './src/channelSession';
|
|
45
|
+
import type { CodexAppServerClient } from './src/codexAppServer';
|
|
46
|
+
import type { JsonObject, JsonRpcResponse, JsonValue } from './src/protocol';
|
|
47
|
+
import type { PhoenixClient } from './src/phoenix';
|
|
48
|
+
// The REAL transpilation bridge (T.2 export -> T.4 rehydration -> store
|
|
49
|
+
// seeding) - the exact module the runner's production executor loads.
|
|
50
|
+
import { transpileAndSeedClaudeSession } from './src/crossHarnessFailover.mjs';
|
|
51
|
+
|
|
52
|
+
const oracleHome = process.env.F5_ORACLE_HOME;
|
|
53
|
+
const oracleCwd = process.env.F5_ORACLE_CWD;
|
|
54
|
+
const executorKind = process.env.F5_ORACLE_EXECUTOR ?? 'bridge';
|
|
55
|
+
if (!oracleHome || !oracleCwd) {
|
|
56
|
+
throw new Error('F5_ORACLE_HOME and F5_ORACLE_CWD are required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The rung ships flag-gated; the oracle exercises the flag-ON path.
|
|
60
|
+
process.env.LINZUMI_CROSS_HARNESS_FAILOVER = '1';
|
|
61
|
+
|
|
62
|
+
const ladderSteps: Array<Record<string, unknown>> = [];
|
|
63
|
+
const failoverLogs: Array<{ event: string; payload: Record<string, unknown> }> = [];
|
|
64
|
+
const visibleReasons: string[] = [];
|
|
65
|
+
const messageStates: Array<Record<string, unknown>> = [];
|
|
66
|
+
let executorRequest: Record<string, unknown> | null = null;
|
|
67
|
+
let seeded: Record<string, unknown> | null = null;
|
|
68
|
+
let executorSettled = false;
|
|
69
|
+
|
|
70
|
+
function okResponse(result: JsonValue): JsonRpcResponse {
|
|
71
|
+
return { jsonrpc: '2.0', id: 1, result };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- The scripted kandan side (minimal PhoenixClient double) ---------------
|
|
75
|
+
const eventCallbacks = new Set<
|
|
76
|
+
(topic: string, event: string, payload: JsonValue) => void
|
|
77
|
+
>();
|
|
78
|
+
const kandan = {
|
|
79
|
+
join: async () => ({ cursor: 0 }),
|
|
80
|
+
unregisterJoin: () => undefined,
|
|
81
|
+
push: async (_topic: string, event: string, payload: JsonObject) => {
|
|
82
|
+
if (event === 'message_state') {
|
|
83
|
+
messageStates.push(payload as Record<string, unknown>);
|
|
84
|
+
if (typeof payload.reason === 'string') {
|
|
85
|
+
visibleReasons.push(payload.reason);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (event === 'session:announce') {
|
|
89
|
+
return { status: 'ok', response: { seq: 10 } };
|
|
90
|
+
}
|
|
91
|
+
return { status: 'ok', response: {} };
|
|
92
|
+
},
|
|
93
|
+
pushIfConnected: async (_topic: string, event: string, payload: JsonObject) =>
|
|
94
|
+
kandan.push(_topic, event, payload),
|
|
95
|
+
onControl: () => undefined,
|
|
96
|
+
onEvent: (
|
|
97
|
+
callback: (topic: string, event: string, payload: JsonValue) => void
|
|
98
|
+
) => {
|
|
99
|
+
eventCallbacks.add(callback);
|
|
100
|
+
},
|
|
101
|
+
onReconnect: () => undefined,
|
|
102
|
+
close: () => undefined,
|
|
103
|
+
} as unknown as PhoenixClient;
|
|
104
|
+
const emit = (topic: string, event: string, payload: JsonValue) => {
|
|
105
|
+
for (const callback of eventCallbacks) callback(topic, event, payload);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// --- The scripted dead-able codex backend ----------------------------------
|
|
109
|
+
let turnStartCount = 0;
|
|
110
|
+
const workerState = { closed: false };
|
|
111
|
+
const codex: CodexAppServerClient = {
|
|
112
|
+
request: async (method) => {
|
|
113
|
+
if (workerState.closed) {
|
|
114
|
+
throw new Error('thread Codex worker IPC client is closed');
|
|
115
|
+
}
|
|
116
|
+
switch (method) {
|
|
117
|
+
case 'thread/start':
|
|
118
|
+
case 'thread/resume':
|
|
119
|
+
return okResponse({ thread: { id: 'oracle-codex-thread-1' } });
|
|
120
|
+
case 'turn/start':
|
|
121
|
+
turnStartCount += 1;
|
|
122
|
+
return okResponse({ turn: { id: 'oracle-turn-1' } });
|
|
123
|
+
case 'thread/read':
|
|
124
|
+
return okResponse({ thread: { turns: [] } });
|
|
125
|
+
default:
|
|
126
|
+
throw new Error('unexpected oracle codex request ' + method);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
notify: () => undefined,
|
|
130
|
+
onNotification: () => undefined,
|
|
131
|
+
onRequest: () => undefined,
|
|
132
|
+
isClosed: () => workerState.closed,
|
|
133
|
+
close: () => {
|
|
134
|
+
workerState.closed = true;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
async function waitFor(predicate: () => boolean, label: string): Promise<void> {
|
|
139
|
+
const deadline = Date.now() + 30_000;
|
|
140
|
+
while (!predicate()) {
|
|
141
|
+
if (Date.now() > deadline) {
|
|
142
|
+
throw new Error('timed out waiting for ' + label);
|
|
143
|
+
}
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function run(): Promise<void> {
|
|
149
|
+
const token =
|
|
150
|
+
'fake.' +
|
|
151
|
+
Buffer.from(
|
|
152
|
+
JSON.stringify({ actor_id: 7, actor_username: 'oracle-owner' })
|
|
153
|
+
).toString('base64url') +
|
|
154
|
+
'.sig';
|
|
155
|
+
|
|
156
|
+
const runtime = await attachChannelSession({
|
|
157
|
+
kandan,
|
|
158
|
+
codex,
|
|
159
|
+
topic: 'local_runner:oracle-f5-failover',
|
|
160
|
+
instanceId: 'instance-oracle-f5',
|
|
161
|
+
options: {
|
|
162
|
+
token,
|
|
163
|
+
runnerId: 'oracle-runner-1',
|
|
164
|
+
cwd: oracleCwd,
|
|
165
|
+
codexBin: 'node',
|
|
166
|
+
launchTui: false,
|
|
167
|
+
channelSession: {
|
|
168
|
+
workspaceSlug: 'oracle-workspace',
|
|
169
|
+
channelSlug: 'oracle-channel',
|
|
170
|
+
listenUser: 'oracle-owner',
|
|
171
|
+
kandanThreadId: 'oracle-kandan-thread-1',
|
|
172
|
+
rootSeq: 60,
|
|
173
|
+
codexThreadId: 'oracle-codex-thread-1',
|
|
174
|
+
// D39: the turn/session spec's model slug, asserted byte-equal on
|
|
175
|
+
// the receipts by the conformance suite.
|
|
176
|
+
model: 'oracle-model-slug',
|
|
177
|
+
streamFlushMs: 0,
|
|
178
|
+
},
|
|
179
|
+
crossHarnessFailover: {
|
|
180
|
+
targetHarness: 'claude_code',
|
|
181
|
+
execute: async (request) => {
|
|
182
|
+
executorRequest = {
|
|
183
|
+
kandanThreadId: request.kandanThreadId,
|
|
184
|
+
codexThreadId: request.codexThreadId ?? null,
|
|
185
|
+
queuedSeq: request.queuedSeq,
|
|
186
|
+
messageBody: request.message.body,
|
|
187
|
+
model: request.model ?? null,
|
|
188
|
+
journalTurnIds: request.activity.turns.map((turn) => turn.turnId),
|
|
189
|
+
journalItemCount: request.activity.itemCount,
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
if (executorKind === 'fail') {
|
|
193
|
+
return { ok: false as const, failure: 'scripted executor failure' };
|
|
194
|
+
}
|
|
195
|
+
const result = await transpileAndSeedClaudeSession({
|
|
196
|
+
activity: request.activity as HarnessActivityJournal,
|
|
197
|
+
codexThreadId: request.codexThreadId ?? 'unknown-codex-session',
|
|
198
|
+
kandanThreadId: request.kandanThreadId,
|
|
199
|
+
cwd: oracleCwd,
|
|
200
|
+
home: oracleHome,
|
|
201
|
+
});
|
|
202
|
+
if (!result.ok) {
|
|
203
|
+
return { ok: false as const, failure: result.failure };
|
|
204
|
+
}
|
|
205
|
+
seeded = {
|
|
206
|
+
targetSessionId: result.targetSessionId,
|
|
207
|
+
sessionFile: result.sessionFile,
|
|
208
|
+
transpiledItemCount: result.transpiledItemCount,
|
|
209
|
+
redactedReasoningCount: result.redactedReasoningCount,
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
ok: true as const,
|
|
213
|
+
transpiledItemCount: result.transpiledItemCount,
|
|
214
|
+
redactedReasoningCount: result.redactedReasoningCount,
|
|
215
|
+
targetSessionId: result.targetSessionId,
|
|
216
|
+
};
|
|
217
|
+
} finally {
|
|
218
|
+
executorSettled = true;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
log: (event: string, payload: Record<string, unknown>) => {
|
|
224
|
+
if (event === 'codex.fate_ladder_step') {
|
|
225
|
+
ladderSteps.push(payload);
|
|
226
|
+
}
|
|
227
|
+
if (event.startsWith('codex.fate_ladder_cross_harness_failover')) {
|
|
228
|
+
failoverLogs.push({ event, payload });
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// The user's coding-job message -> the turn dispatches on codex.
|
|
234
|
+
emit('chat:oracle-workspace:oracle-channel', 'event', {
|
|
235
|
+
seq: 61,
|
|
236
|
+
type: 'thread.message',
|
|
237
|
+
actor: { kind: 'human', slug: 'oracle-owner' },
|
|
238
|
+
actor_user_id: 7,
|
|
239
|
+
body: 'keep working on the migration',
|
|
240
|
+
payload: { thread_id: 'oracle-kandan-thread-1', reply_to_seq: 60 },
|
|
241
|
+
});
|
|
242
|
+
await waitFor(() => turnStartCount >= 1, 'the codex dispatch');
|
|
243
|
+
|
|
244
|
+
// The recorded activity the failover exports: a realistic little session
|
|
245
|
+
// walk INCLUDING a reasoning item (so redaction accounting is exercised).
|
|
246
|
+
runtime.handleCodexNotification('turn/started', {
|
|
247
|
+
threadId: 'oracle-codex-thread-1',
|
|
248
|
+
turn: { id: 'oracle-turn-1' },
|
|
249
|
+
});
|
|
250
|
+
const items: JsonObject[] = [
|
|
251
|
+
{
|
|
252
|
+
id: 'oracle-item-1',
|
|
253
|
+
type: 'userMessage',
|
|
254
|
+
content: [{ type: 'text', text: 'keep working on the migration' }],
|
|
255
|
+
},
|
|
256
|
+
{ id: 'oracle-item-2', type: 'reasoning', summary: [] },
|
|
257
|
+
{
|
|
258
|
+
id: 'oracle-item-3',
|
|
259
|
+
type: 'agentMessage',
|
|
260
|
+
text: 'renamed the column and updated 3 call sites so far',
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
for (const item of items) {
|
|
264
|
+
runtime.handleCodexNotification('item/completed', {
|
|
265
|
+
threadId: 'oracle-codex-thread-1',
|
|
266
|
+
turnId: 'oracle-turn-1',
|
|
267
|
+
item,
|
|
268
|
+
completedAtMs: 1_784_000_000_000,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
272
|
+
|
|
273
|
+
// THE BACKEND DIES (the precise F5 trigger: the worker/IPC client is
|
|
274
|
+
// gone), then the infra-kill terminal lands with fate transient_local.
|
|
275
|
+
workerState.closed = true;
|
|
276
|
+
runtime.handleCodexNotification('turn/failed', {
|
|
277
|
+
threadId: 'oracle-codex-thread-1',
|
|
278
|
+
turnId: 'oracle-turn-1',
|
|
279
|
+
reason: 'thread Codex worker exited',
|
|
280
|
+
retryable: true,
|
|
281
|
+
fate: 'transient_local',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await waitFor(() => executorSettled, 'the failover executor');
|
|
285
|
+
if (executorKind === 'fail') {
|
|
286
|
+
await waitFor(
|
|
287
|
+
() =>
|
|
288
|
+
messageStates.some(
|
|
289
|
+
(state) =>
|
|
290
|
+
state.status === 'failed' &&
|
|
291
|
+
typeof state.reason === 'string' &&
|
|
292
|
+
(state.reason as string).includes('cross-harness failover')
|
|
293
|
+
),
|
|
294
|
+
'the honest failover_failed red'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
// Let the post-executor bookkeeping settle.
|
|
298
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
299
|
+
await runtime.close();
|
|
300
|
+
|
|
301
|
+
process.stdout.write(
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
ladderSteps,
|
|
304
|
+
failoverLogs,
|
|
305
|
+
visibleReasons,
|
|
306
|
+
messageStates,
|
|
307
|
+
seeded,
|
|
308
|
+
executorRequest,
|
|
309
|
+
turnStartCount,
|
|
310
|
+
}) + '\\n'
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
run().then(
|
|
315
|
+
() => process.exit(0),
|
|
316
|
+
(error) => {
|
|
317
|
+
process.stderr.write(
|
|
318
|
+
String(error && error.stack ? error.stack : error) + '\\n'
|
|
319
|
+
);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
const outDir = join(packageRoot, '.differential');
|
|
326
|
+
const outPath = join(outDir, 'cross-harness-failover-oracle.mjs');
|
|
327
|
+
|
|
328
|
+
await mkdir(outDir, { recursive: true });
|
|
329
|
+
|
|
330
|
+
await build({
|
|
331
|
+
stdin: {
|
|
332
|
+
contents: driverSource,
|
|
333
|
+
resolveDir: packageRoot,
|
|
334
|
+
sourcefile: 'cross-harness-failover-driver.ts',
|
|
335
|
+
loader: 'ts',
|
|
336
|
+
},
|
|
337
|
+
bundle: true,
|
|
338
|
+
platform: 'node',
|
|
339
|
+
target: 'node20',
|
|
340
|
+
format: 'esm',
|
|
341
|
+
outfile: outPath,
|
|
342
|
+
minify: false,
|
|
343
|
+
sourcemap: false,
|
|
344
|
+
legalComments: 'none',
|
|
345
|
+
logLevel: 'silent',
|
|
346
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
process.stdout.write(outPath + '\n');
|
|
@@ -133,7 +133,8 @@ const outfile =
|
|
|
133
133
|
? join(packageRoot, '.differential/impl-ts.mjs')
|
|
134
134
|
: join(packageRoot, `.differential/impl-ts.mutant-${mutation.name}.mjs`);
|
|
135
135
|
|
|
136
|
-
const mutationHooks =
|
|
136
|
+
const mutationHooks =
|
|
137
|
+
mutation === undefined ? undefined : mutationPlugin(mutation);
|
|
137
138
|
|
|
138
139
|
await build({
|
|
139
140
|
entryPoints: [join(packageRoot, 'src/differentialCommanderEntry.ts')],
|
|
@@ -158,3 +159,22 @@ await copyFile(
|
|
|
158
159
|
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|
|
159
160
|
join(packageRoot, '.differential/assets/linzumi-logo.svg')
|
|
160
161
|
);
|
|
162
|
+
|
|
163
|
+
// F5: the cross-harness failover bridge rides next to the bundle exactly as
|
|
164
|
+
// it does in dist/ (the runner dynamic-imports ./crossHarnessFailover.mjs
|
|
165
|
+
// relative to import.meta.url), bundled with the compiled ReScript
|
|
166
|
+
// transpiler artifacts resolved at build time. Unlike the dist build there
|
|
167
|
+
// is NO stub fallback here: the differential harness must exercise the REAL
|
|
168
|
+
// transpile leg, so missing artifacts fail this build loudly.
|
|
169
|
+
await build({
|
|
170
|
+
entryPoints: [join(packageRoot, 'src/crossHarnessFailover.mjs')],
|
|
171
|
+
bundle: true,
|
|
172
|
+
platform: 'node',
|
|
173
|
+
target: 'node20',
|
|
174
|
+
format: 'esm',
|
|
175
|
+
outfile: join(packageRoot, '.differential/crossHarnessFailover.mjs'),
|
|
176
|
+
minify: false,
|
|
177
|
+
sourcemap: false,
|
|
178
|
+
legalComments: 'none',
|
|
179
|
+
banner,
|
|
180
|
+
});
|
package/scripts/build.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Spec: plans/2026-05-17-linzumi-cli-node-toolchain-note.md
|
|
2
2
|
// Relationship: Builds the published CLI package with Node-hosted tooling.
|
|
3
|
-
import { copyFile, mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { copyFile, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { build } from 'esbuild';
|
|
@@ -84,6 +84,45 @@ await build({
|
|
|
84
84
|
banner,
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
// F5 cross-harness failover bridge (Workstream F task F5): the runner
|
|
88
|
+
// dynamic-imports ./crossHarnessFailover.mjs relative to its own bundle, so
|
|
89
|
+
// dist ships a self-contained sibling bundle with the compiled ReScript
|
|
90
|
+
// transpiler artifacts (packages/linzumi-cli-rescript, built by `rescript`)
|
|
91
|
+
// resolved at build time. When the artifacts are not built in this checkout
|
|
92
|
+
// the publish must not silently ship a broken rung: emit an HONEST stub
|
|
93
|
+
// whose import succeeds but whose executor refuses loudly - the ladder then
|
|
94
|
+
// terminates red-with-receipts exactly as F2 (the flag's rollback posture) -
|
|
95
|
+
// and warn in the build log.
|
|
96
|
+
try {
|
|
97
|
+
await build({
|
|
98
|
+
entryPoints: [join(packageRoot, 'src/crossHarnessFailover.mjs')],
|
|
99
|
+
bundle: true,
|
|
100
|
+
platform: 'node',
|
|
101
|
+
target: 'node20',
|
|
102
|
+
format: 'esm',
|
|
103
|
+
outfile: join(packageRoot, 'dist/crossHarnessFailover.mjs'),
|
|
104
|
+
minify: true,
|
|
105
|
+
sourcemap: false,
|
|
106
|
+
legalComments: 'none',
|
|
107
|
+
banner,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn(
|
|
111
|
+
'[build] cross-harness failover bridge could not be bundled (are the ' +
|
|
112
|
+
'linzumi-cli-rescript artifacts built?); shipping the honest refusal ' +
|
|
113
|
+
`stub instead: ${error instanceof Error ? error.message : String(error)}`
|
|
114
|
+
);
|
|
115
|
+
await writeFile(
|
|
116
|
+
join(packageRoot, 'dist/crossHarnessFailover.mjs'),
|
|
117
|
+
'export async function transpileAndSeedClaudeSession() {\n' +
|
|
118
|
+
' return {\n' +
|
|
119
|
+
' ok: false,\n' +
|
|
120
|
+
" failure: 'the cross-harness failover transpiler bridge was not bundled into this build',\n" +
|
|
121
|
+
' };\n' +
|
|
122
|
+
'}\n'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
87
126
|
await mkdir(join(packageRoot, 'dist/assets'), { recursive: true });
|
|
88
127
|
await copyFile(
|
|
89
128
|
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|