@linzumi/cli 1.0.112 → 1.0.114

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "1.0.112",
3
+ "version": "1.0.114",
4
4
  "description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 = mutation === undefined ? undefined : mutationPlugin(mutation);
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
+ });
@@ -0,0 +1,107 @@
1
+ // Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
2
+ // (M3 cluster 11, the TUI - visual evidence).
3
+ // Relationship: Builds the TS-side VISUAL-EVIDENCE demo driver: a bundle
4
+ // that mounts the REAL TS commander console TUI (reportRunnerConsoleEvent
5
+ // on a live TTY) and replays the SAME dummy-event feed the ReScript demo
6
+ // entrypoint replays
7
+ // (packages/linzumi-cli-rescript/src/tui/commanderTuiDemoFeed.json), so
8
+ // the cluster's before/after terminal captures compare the two
9
+ // implementations on identical input.
10
+ //
11
+ // Run (inside a real terminal / tmux pane):
12
+ // node scripts/build-tui-demo-evidence.mjs
13
+ // LINZUMI_TUI_DEMO_FIXED_NOW=<ms> node .differential/tui-demo.mjs
14
+ //
15
+ // Env knobs (mirroring the ReScript demo):
16
+ // LINZUMI_TUI_DEMO_FIXED_NOW pin Date.now() (and therefore every
17
+ // time-ago cell + the render ticker clock)
18
+ // so captures are deterministic.
19
+ //
20
+ // The output is a local evidence artifact (.differential/ is gitignored,
21
+ // never published).
22
+ import { mkdir } from 'node:fs/promises';
23
+ import { dirname, join } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { build } from 'esbuild';
26
+
27
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
28
+
29
+ const driverSource = `
30
+ import { readFileSync } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ import { reportRunnerConsoleEvent } from './src/runnerConsoleReporter';
33
+
34
+ const fixedNowRaw = process.env.LINZUMI_TUI_DEMO_FIXED_NOW;
35
+ const fixedNow = fixedNowRaw === undefined ? undefined : Number(fixedNowRaw);
36
+ if (fixedNow !== undefined && Number.isFinite(fixedNow)) {
37
+ const RealDate = Date;
38
+ class FixedDate extends RealDate {
39
+ constructor(...args) {
40
+ if (args.length === 0) {
41
+ super(fixedNow);
42
+ } else {
43
+ super(...args);
44
+ }
45
+ }
46
+ static now() {
47
+ return fixedNow;
48
+ }
49
+ }
50
+ globalThis.Date = FixedDate;
51
+ }
52
+
53
+ const now = Date.now();
54
+
55
+ // Resolve *_offset timestamp fields against the demo clock (the ReScript
56
+ // demo performs the same resolution).
57
+ function resolveOffsets(value) {
58
+ if (Array.isArray(value)) {
59
+ return value.map(resolveOffsets);
60
+ }
61
+ if (value !== null && typeof value === 'object') {
62
+ const out = {};
63
+ for (const [key, entry] of Object.entries(value)) {
64
+ if (key.endsWith('_offset') && typeof entry === 'number') {
65
+ out[key.slice(0, -'_offset'.length)] = now + entry;
66
+ } else {
67
+ out[key] = resolveOffsets(entry);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ return value;
73
+ }
74
+
75
+ const feedPath = join(
76
+ process.cwd(),
77
+ '../linzumi-cli-rescript/src/tui/commanderTuiDemoFeed.json'
78
+ );
79
+ const feed = JSON.parse(readFileSync(feedPath, 'utf8'));
80
+ for (const entry of feed.events) {
81
+ reportRunnerConsoleEvent(entry.event, resolveOffsets(entry.payload));
82
+ }
83
+
84
+ // Keep the process (and the TUI) alive for interactive navigation; the
85
+ // TUI's own C-c binding exits.
86
+ setInterval(() => {}, 1000);
87
+ `;
88
+
89
+ await mkdir(join(packageRoot, '.differential'), { recursive: true });
90
+
91
+ await build({
92
+ stdin: {
93
+ contents: driverSource,
94
+ resolveDir: packageRoot,
95
+ sourcefile: 'tui-demo-driver.ts',
96
+ loader: 'ts',
97
+ },
98
+ bundle: true,
99
+ platform: 'node',
100
+ target: 'node20',
101
+ format: 'esm',
102
+ outfile: join(packageRoot, '.differential/tui-demo.mjs'),
103
+ minify: false,
104
+ sourcemap: false,
105
+ legalComments: 'none',
106
+ external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
107
+ });