@linzumi/cli 1.0.274 → 1.0.276

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.
Files changed (41) hide show
  1. package/README.md +10 -9
  2. package/dist/{impl-ts/cloud-supervisor.mjs → cloud-supervisor.mjs} +85 -85
  3. package/dist/hosted-coding-account-worker.js +25 -0
  4. package/dist/index.js +1609 -3
  5. package/dist/mcp-server.mjs +174 -168
  6. package/dist/{impl-res/native → native}/darwin/manifest.json +5 -2
  7. package/dist/remote-codex-harness-worker.js +757 -0
  8. package/dist/tcp-tunnel-cli.mjs +25 -69
  9. package/dist/topics-cli.mjs +137 -13
  10. package/package.json +9 -16
  11. package/scripts/build.mjs +33 -356
  12. package/scripts/bump-cli-version.mjs +35 -9
  13. package/scripts/check-package-size-budget.mjs +13 -61
  14. package/dist/impl-res/cloud-supervisor.mjs +0 -281
  15. package/dist/impl-res/index.js +0 -1546
  16. package/dist/impl-res/mcp-server.mjs +0 -461
  17. package/dist/impl-ts/assets/linzumi-logo.svg +0 -1
  18. package/dist/impl-ts/crossHarnessFailover.mjs +0 -14
  19. package/dist/impl-ts/index.js +0 -2617
  20. package/dist/impl-ts/mcp-server.mjs +0 -455
  21. package/dist/impl-ts/native/darwin/manifest.json +0 -21
  22. package/dist/impl-ts/native/darwin/preview-process-identity +0 -0
  23. package/dist/impl-ts/tcp-tunnel-cli.mjs +0 -93
  24. package/scripts/build-differential-entry-impl-res.mjs +0 -64
  25. package/scripts/build-differential-entry.mjs +0 -180
  26. package/scripts/build-onboarding-demo-evidence.mjs +0 -186
  27. package/scripts/build-tui-demo-evidence.mjs +0 -107
  28. package/scripts/diagnose-claude-probe.ts +0 -198
  29. package/scripts/differentialMutations.mjs +0 -114
  30. package/scripts/f5_live_cross_harness_failover_e2e.mjs +0 -239
  31. package/scripts/golden-chaos-harness.mjs +0 -1420
  32. package/scripts/normalize-agent-replay-fixture.ts +0 -281
  33. package/scripts/promote-gstack-claude-smoke-fixture.ts +0 -891
  34. package/scripts/record-agent-replay-fixtures.ts +0 -610
  35. package/scripts/replay-agent-backend-browser.ts +0 -198
  36. package/scripts/replay-agent-backend.ts +0 -1961
  37. package/scripts/replay-agent-fixture.ts +0 -326
  38. package/scripts/typecheck.mjs +0 -42
  39. /package/dist/{impl-res/assets → assets}/linzumi-logo.svg +0 -0
  40. /package/dist/{impl-res/crossHarnessFailover.mjs → crossHarnessFailover.mjs} +0 -0
  41. /package/dist/{impl-res/native → native}/darwin/preview-process-identity +0 -0
@@ -1,1961 +0,0 @@
1
- /*
2
- - Date: 2026-06-29
3
- Spec: plans/2026-06-29-agent-backend-replay-orchestrator-spec.md
4
- Relationship: First executable backend replay orchestrator slice. It validates
5
- real provider fixtures, manifests, provider selection, replay timing, and
6
- dropped-event diagnostics before a fixture is allowed to drive the real
7
- Commander-to-Phoenix backend path.
8
-
9
- - Date: 2026-06-30
10
- Spec: plans/2026-06-30-gstack-agent-timeline-event-model-plan.md
11
- Relationship: Queries generic agent status/task timeline rows and includes
12
- them in the backend replay oracle output set.
13
- */
14
- import { randomUUID } from 'node:crypto';
15
- import { mkdir, writeFile } from 'node:fs/promises';
16
- import { join, resolve } from 'node:path';
17
- import { collectAgentReplayDiagnostics } from '../src/agentReplayDiagnostics';
18
- import { featureBackendReplayOracleChecks } from '../src/agentReplayBackendOracles';
19
- import { loadAgentReplayFixture } from '../src/agentReplayFixture';
20
- import {
21
- evaluateAgentReplayFeatureReadiness,
22
- loadAgentReplayManifestForFixture,
23
- summarizeAgentReplayManifest,
24
- type AgentReplayFeatureReadiness,
25
- type AgentReplayManifestSummary,
26
- } from '../src/agentReplayManifest';
27
- import {
28
- agentReplayProviderSelectionFromValue,
29
- createAgentReplayRuntime,
30
- parseAgentReplayClockMode,
31
- type AgentReplayRunnerProviderSelection,
32
- } from '../src/agentReplayRunner';
33
- import { connectPhoenixClient } from '../src/phoenix';
34
- import { type JsonObject } from '../src/protocol';
35
- import { runLocalCodexRunner, type RunnerOptions } from '../src/runner';
36
- import { linzumiCliVersion } from '../src/version';
37
-
38
- const backendReplayOracleTimeoutMs = 30_000;
39
- const backendReplayOraclePollMs = 100;
40
-
41
- type BackendReplayOptions = {
42
- readonly fixturePath: string;
43
- readonly provider: AgentReplayRunnerProviderSelection;
44
- readonly speed: string | undefined;
45
- readonly dryRun: boolean;
46
- readonly probeBackend: boolean;
47
- readonly backendUrl: string | undefined;
48
- readonly workspace: string | undefined;
49
- readonly channel: string | undefined;
50
- readonly threadId: string | undefined;
51
- readonly token: string | undefined;
52
- readonly sessionToken: string | undefined;
53
- readonly runnerId: string;
54
- readonly cwd: string;
55
- readonly workDescription: string;
56
- readonly setupFixture: boolean;
57
- readonly setupLabel: string;
58
- readonly outDir: string;
59
- readonly summary: boolean;
60
- readonly browserAuthFile: string | undefined;
61
- readonly browserAuth: BackendReplayBrowserAuth | undefined;
62
- readonly requiredFeatureId: string | undefined;
63
- readonly oracleFeatureId: string | undefined;
64
- // Wire replay mode (spec/codex-wire-capture-replay.spec.md): serve the
65
- // fixture from the stub codex app-server websocket instead of the
66
- // in-process provider-boundary client, so the runner's REAL socket
67
- // machinery is exercised against the REAL backend.
68
- readonly wire: boolean;
69
- readonly wireStrictness: string | undefined;
70
- // Replay-to-a-point (spec §Replay-to-a-point): --wire-breakpoint specs
71
- // for the wire stub (`ordinal:<n>`, `method:<rpcMethod>[:<n>]`,
72
- // `first-delta`, `turn-started[:<n>]`, `turn-completed[:<n>]`), in hold
73
- // order. While the stub holds, the whole stack (runner -> backend ->
74
- // client projection) is frozen at the recorded prefix; an external
75
- // driver (Playwright DOM assertions, the film rig) observes and releases
76
- // the hold over the stub's HTTP control routes (GET /replay/breakpoint,
77
- // POST /replay/resume against the printed control URL).
78
- readonly wireBreakpoints: readonly string[];
79
- };
80
-
81
- type WireReplayServerHandle = {
82
- readonly url: string;
83
- readonly port: number;
84
- readonly report: () => JsonObject;
85
- readonly waitForCompletion: (timeoutMs: number) => Promise<boolean>;
86
- readonly close: () => Promise<void>;
87
- };
88
-
89
- async function startWireReplayServerForFixture(
90
- options: BackendReplayOptions
91
- ): Promise<WireReplayServerHandle> {
92
- // The implementation is ReScript (zero-TS/zero-raw-JS ratchets); the
93
- // compiled module is loaded through a non-literal specifier so the type
94
- // checker does not require declarations. The CLI test/build pipeline
95
- // compiles the package (`rescript build ../linzumi-cli-rescript`).
96
- const wireModulePath = new URL(
97
- '../../linzumi-cli-rescript/src/harness/HarnessCodexWireReplay.res.mjs',
98
- import.meta.url
99
- ).href;
100
- const wireModule = (await import(wireModulePath)) as {
101
- startWireReplayServerJs: (serverOptions: {
102
- readonly capturePath?: string;
103
- readonly strictness?: string | undefined;
104
- readonly clock?: string | undefined;
105
- readonly breakpoints?: readonly string[];
106
- }) => Promise<WireReplayServerHandle>;
107
- wireReplayClockForBackendSpeed: (
108
- speed: string | undefined
109
- ) => string | undefined;
110
- };
111
-
112
- // Translate the orchestrator's --speed grammar into the wire clock
113
- // grammar (realtime -> recorded, <n>x passes through, <n>ms refuses
114
- // loudly - it has no wire counterpart); property-pinned in
115
- // test/agentReplayFixture.test.ts.
116
- const wireClock = wireModule.wireReplayClockForBackendSpeed(options.speed);
117
-
118
- const server = await wireModule.startWireReplayServerJs({
119
- capturePath: options.fixturePath,
120
- ...(options.wireStrictness === undefined
121
- ? {}
122
- : { strictness: options.wireStrictness }),
123
- ...(wireClock === undefined ? {} : { clock: wireClock }),
124
- ...(options.wireBreakpoints.length === 0
125
- ? {}
126
- : { breakpoints: options.wireBreakpoints }),
127
- });
128
- if (options.wireBreakpoints.length > 0) {
129
- // The external driver's handle on the frozen stack: poll GET
130
- // /replay/breakpoint until held, assert on the frozen DOM/projection,
131
- // then POST /replay/resume.
132
- console.error(
133
- `[wire-breakpoint] control http://127.0.0.1:${server.port} breakpoints=${options.wireBreakpoints.join(
134
- ','
135
- )}`
136
- );
137
- }
138
- return server;
139
- }
140
-
141
- type BackendReplayBrowserAuth = {
142
- readonly accessToken: string;
143
- readonly refreshToken: string | undefined;
144
- readonly userId: string | number | undefined;
145
- readonly username: string;
146
- readonly user: JsonObject;
147
- };
148
-
149
- type BackendReplaySummaryArgs = {
150
- readonly options: BackendReplayOptions;
151
- readonly fixturePath: string;
152
- readonly recordCount: number;
153
- readonly provider: string;
154
- readonly clockMode: ReturnType<typeof parseAgentReplayClockMode>;
155
- readonly manifest: AgentReplayManifestSummary | undefined;
156
- readonly featureReadiness: readonly AgentReplayFeatureReadiness[] | undefined;
157
- readonly diagnostics: ReturnType<typeof collectAgentReplayDiagnostics>;
158
- readonly backendReplay: JsonObject;
159
- };
160
-
161
- async function main(): Promise<void> {
162
- const args = process.argv.slice(2);
163
-
164
- if (args.includes('--help')) {
165
- printHelp();
166
- return;
167
- }
168
-
169
- let options = parseArgs(args);
170
- const clockMode = parseAgentReplayClockMode(options.speed);
171
- const loaded = await loadAgentReplayFixture(options.fixturePath);
172
-
173
- if (loaded.type === 'error') {
174
- throw new Error(JSON.stringify(loaded.error));
175
- }
176
-
177
- const runtime = await createAgentReplayRuntime({
178
- fixturePath: options.fixturePath,
179
- provider: options.provider,
180
- clockMode,
181
- });
182
-
183
- if (runtime.type === 'error') {
184
- throw new Error(runtime.message);
185
- }
186
-
187
- const manifest = await manifestSummary(options.fixturePath);
188
- const featureReadiness = await manifestReadiness(options.fixturePath);
189
- const diagnostics = collectAgentReplayDiagnostics(loaded.fixture);
190
- const requiredFeatureGate = evaluateRequiredFeatureGate(
191
- options.requiredFeatureId,
192
- featureReadiness
193
- );
194
-
195
- if (diagnostics.droppedEvents.length > 0) {
196
- const summary = backendReplaySummary({
197
- options,
198
- fixturePath: loaded.fixture.path,
199
- recordCount: loaded.fixture.records.length,
200
- provider: runtime.runtime.provider,
201
- clockMode,
202
- manifest,
203
- featureReadiness,
204
- diagnostics,
205
- backendReplay: {
206
- status: 'blocked',
207
- reason: 'fixture diagnostics reported dropped provider events',
208
- },
209
- });
210
- await emitBackendReplaySummary(options, summary);
211
- process.exitCode = 1;
212
- return;
213
- }
214
-
215
- if (requiredFeatureGate.status === 'failed') {
216
- const summary = backendReplaySummary({
217
- options,
218
- fixturePath: loaded.fixture.path,
219
- recordCount: loaded.fixture.records.length,
220
- provider: runtime.runtime.provider,
221
- clockMode,
222
- manifest,
223
- featureReadiness,
224
- diagnostics,
225
- backendReplay: {
226
- status: 'blocked',
227
- reason: 'required feature is not ready for backend replay',
228
- requiredFeature: requiredFeatureGate,
229
- },
230
- });
231
- await emitBackendReplaySummary(options, summary);
232
- process.exitCode = 1;
233
- return;
234
- }
235
-
236
- if (!options.dryRun || options.probeBackend) {
237
- options = await maybeSetupBackendReplayFixture(options);
238
- }
239
-
240
- await maybeWriteBackendReplayBrowserAuth(options);
241
-
242
- if (options.probeBackend) {
243
- const probe = await probeBackend(options);
244
- const summary = backendReplaySummary({
245
- options,
246
- fixturePath: loaded.fixture.path,
247
- recordCount: loaded.fixture.records.length,
248
- provider: runtime.runtime.provider,
249
- clockMode,
250
- manifest,
251
- featureReadiness,
252
- diagnostics,
253
- backendReplay: {
254
- status: 'backend-probed',
255
- reason:
256
- 'joined the real Phoenix local_runner channel; replay/oracles are the next slice',
257
- probe,
258
- },
259
- });
260
- await emitBackendReplaySummary(options, summary);
261
- return;
262
- }
263
-
264
- if (options.dryRun) {
265
- const summary = backendReplaySummary({
266
- options,
267
- fixturePath: loaded.fixture.path,
268
- recordCount: loaded.fixture.records.length,
269
- provider: runtime.runtime.provider,
270
- clockMode,
271
- manifest,
272
- featureReadiness,
273
- diagnostics,
274
- backendReplay: {
275
- status: 'not-run',
276
- reason: 'dry-run validates fixture readiness before backend connection',
277
- },
278
- });
279
-
280
- await emitBackendReplaySummary(options, summary);
281
- return;
282
- }
283
-
284
- const replay = await runBackendReplay(options, runtime.runtime.provider);
285
- const summary = backendReplaySummary({
286
- options,
287
- fixturePath: loaded.fixture.path,
288
- recordCount: loaded.fixture.records.length,
289
- provider: runtime.runtime.provider,
290
- clockMode,
291
- manifest,
292
- featureReadiness,
293
- diagnostics,
294
- backendReplay: {
295
- status: 'backend-replay-started',
296
- reason:
297
- 'started the real local runner with provider replay, triggered the normal GraphQL start mutation, and ran backend GraphQL oracles',
298
- replay,
299
- },
300
- });
301
-
302
- await emitBackendReplaySummary(options, summary);
303
-
304
- if (backendReplayOracleStatus(replay) === 'failed') {
305
- process.exitCode = 1;
306
- }
307
- }
308
-
309
- async function emitBackendReplaySummary(
310
- options: BackendReplayOptions,
311
- summary: JsonObject
312
- ): Promise<void> {
313
- if (options.summary) {
314
- await mkdir(options.outDir, { recursive: true });
315
- await writeFile(
316
- join(options.outDir, 'summary.json'),
317
- `${JSON.stringify(summary, null, 2)}\n`,
318
- 'utf8'
319
- );
320
- }
321
-
322
- process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
323
- }
324
-
325
- function frontendReplayTarget(
326
- options: BackendReplayOptions,
327
- threadId: string | undefined
328
- ): JsonObject | undefined {
329
- if (
330
- options.backendUrl === undefined ||
331
- options.workspace === undefined ||
332
- options.channel === undefined ||
333
- threadId === undefined
334
- ) {
335
- return undefined;
336
- }
337
-
338
- const url = new URL(options.backendUrl);
339
-
340
- switch (url.protocol) {
341
- case 'http:':
342
- case 'https:':
343
- break;
344
- case 'ws:':
345
- url.protocol = 'http:';
346
- break;
347
- case 'wss:':
348
- url.protocol = 'https:';
349
- break;
350
- default:
351
- throw new Error(
352
- '--backend-url must use http:, https:, ws:, or wss: protocol'
353
- );
354
- }
355
-
356
- const path = `/w/${encodeURIComponent(options.workspace)}/c/${encodeURIComponent(
357
- options.channel
358
- )}/thread/${encodeURIComponent(threadId)}`;
359
- url.pathname = path;
360
- url.search = '';
361
- url.hash = '';
362
-
363
- return {
364
- path,
365
- url: url.toString(),
366
- };
367
- }
368
-
369
- function backendReplayOracleStatus(replay: JsonObject): string | undefined {
370
- const oracles = replay.oracles;
371
-
372
- if (
373
- typeof oracles === 'object' &&
374
- oracles !== null &&
375
- !Array.isArray(oracles) &&
376
- typeof oracles.status === 'string'
377
- ) {
378
- return oracles.status;
379
- }
380
-
381
- return undefined;
382
- }
383
-
384
- function parseArgs(args: readonly string[]): BackendReplayOptions {
385
- const fixture = argValue(args, '--fixture');
386
-
387
- if (fixture === undefined) {
388
- throw new Error('provide --fixture <path>');
389
- }
390
-
391
- return {
392
- fixturePath: resolve(fixture),
393
- provider: agentReplayProviderSelectionFromValue(
394
- argValue(args, '--provider')
395
- ),
396
- speed: argValue(args, '--speed'),
397
- dryRun: args.includes('--dry-run'),
398
- probeBackend: args.includes('--probe-backend'),
399
- backendUrl: argValue(args, '--backend-url'),
400
- workspace: argValue(args, '--workspace'),
401
- channel: argValue(args, '--channel'),
402
- threadId: argValue(args, '--thread-id'),
403
- token:
404
- argValue(args, '--token') ??
405
- process.env.LINZUMI_AGENT_BACKEND_REPLAY_TOKEN,
406
- sessionToken:
407
- argValue(args, '--session-token') ??
408
- process.env.LINZUMI_AGENT_BACKEND_REPLAY_SESSION_TOKEN,
409
- runnerId:
410
- argValue(args, '--runner-id') ?? `agent-backend-replay-${randomUUID()}`,
411
- cwd: resolve(argValue(args, '--cwd') ?? process.cwd()),
412
- workDescription:
413
- argValue(args, '--work-description') ??
414
- 'Replay recorded agent fixture through the local backend path.',
415
- setupFixture: args.includes('--setup-fixture'),
416
- setupLabel: argValue(args, '--setup-label') ?? 'agent-backend-replay',
417
- outDir: resolve(
418
- argValue(args, '--out-dir') ?? 'agent-backend-replay-output'
419
- ),
420
- summary: !args.includes('--no-summary'),
421
- browserAuthFile:
422
- argValue(args, '--browser-auth-file') === undefined
423
- ? undefined
424
- : resolve(argValue(args, '--browser-auth-file') ?? ''),
425
- browserAuth: undefined,
426
- requiredFeatureId: argValue(args, '--require-feature'),
427
- oracleFeatureId: argValue(args, '--oracle-feature'),
428
- wire: args.includes('--wire'),
429
- wireStrictness: argValue(args, '--wire-strictness'),
430
- wireBreakpoints: argValues(args, '--wire-breakpoint'),
431
- };
432
- }
433
-
434
- function backendReplaySummary(args: BackendReplaySummaryArgs): JsonObject {
435
- return {
436
- mode: args.options.probeBackend
437
- ? 'backend-probe'
438
- : args.options.dryRun
439
- ? 'dry-run'
440
- : 'backend-replay',
441
- fixture: {
442
- path: args.fixturePath,
443
- records: args.recordCount,
444
- },
445
- provider: args.provider,
446
- clockMode: args.clockMode,
447
- backendTarget: {
448
- backendUrl: args.options.backendUrl,
449
- workspace: args.options.workspace,
450
- channel: args.options.channel,
451
- threadId: args.options.threadId,
452
- runnerId: args.options.runnerId,
453
- hasToken: args.options.token !== undefined,
454
- hasSessionToken: args.options.sessionToken !== undefined,
455
- setupFixture: args.options.setupFixture,
456
- setupLabel: args.options.setupFixture
457
- ? args.options.setupLabel
458
- : undefined,
459
- },
460
- artifacts: {
461
- summaryPath: args.options.summary
462
- ? join(args.options.outDir, 'summary.json')
463
- : undefined,
464
- browserAuthPath: args.options.browserAuthFile,
465
- },
466
- requiredFeatureId: args.options.requiredFeatureId,
467
- oracleFeatureId: args.options.oracleFeatureId,
468
- frontendTarget: frontendReplayTarget(args.options, args.options.threadId),
469
- featureCoverage: args.manifest,
470
- featureReadiness: args.featureReadiness,
471
- diagnostics: args.diagnostics,
472
- backendReplay: args.backendReplay,
473
- };
474
- }
475
-
476
- async function maybeSetupBackendReplayFixture(
477
- options: BackendReplayOptions
478
- ): Promise<BackendReplayOptions> {
479
- if (!options.setupFixture) {
480
- return options;
481
- }
482
-
483
- if (options.backendUrl === undefined) {
484
- throw new Error('--setup-fixture requires --backend-url <url>');
485
- }
486
-
487
- const suffix = `${Date.now()}-${randomUUID().slice(0, 8)}`;
488
- const workspace = slugifyReplaySetup(
489
- `${options.setupLabel}-workspace-${suffix}`
490
- );
491
- const channel = slugifyReplaySetup(`${options.setupLabel}-channel-${suffix}`);
492
- const session = await registerReplayFixtureUser(
493
- options.backendUrl,
494
- `${options.setupLabel}-${suffix}`
495
- );
496
- await enableReplayFixtureFeatureFlag(
497
- options.backendUrl,
498
- session.accessToken,
499
- 'CLAUDE_CODE_SUPPORT_PREVIEW'
500
- );
501
- await createReplayWorkspace(options.backendUrl, session.accessToken, {
502
- name: `${options.setupLabel} workspace ${suffix}`,
503
- slug: workspace,
504
- });
505
- await createReplayChannel(options.backendUrl, session.accessToken, {
506
- workspace,
507
- name: `${options.setupLabel} channel ${suffix}`,
508
- slug: channel,
509
- });
510
-
511
- return {
512
- ...options,
513
- workspace,
514
- channel,
515
- sessionToken: session.accessToken,
516
- browserAuth: session,
517
- };
518
- }
519
-
520
- async function maybeWriteBackendReplayBrowserAuth(
521
- options: BackendReplayOptions
522
- ): Promise<void> {
523
- if (options.browserAuthFile === undefined) {
524
- return;
525
- }
526
-
527
- const auth =
528
- options.browserAuth ??
529
- (options.sessionToken === undefined
530
- ? undefined
531
- : {
532
- accessToken: options.sessionToken,
533
- refreshToken: process.env.LINZUMI_AGENT_BACKEND_REPLAY_REFRESH_TOKEN,
534
- userId: 'agent-backend-replay',
535
- username: 'agent-backend-replay',
536
- user: {
537
- id: 'agent-backend-replay',
538
- username: 'agent-backend-replay',
539
- },
540
- });
541
-
542
- if (auth === undefined) {
543
- throw new Error(
544
- '--browser-auth-file requires --setup-fixture, --session-token, or LINZUMI_AGENT_BACKEND_REPLAY_SESSION_TOKEN'
545
- );
546
- }
547
-
548
- await mkdir(resolve(options.browserAuthFile, '..'), { recursive: true });
549
- await writeFile(
550
- options.browserAuthFile,
551
- `${JSON.stringify(auth, null, 2)}\n`,
552
- 'utf8'
553
- );
554
- }
555
-
556
- type ReplayFixtureSession = BackendReplayBrowserAuth;
557
-
558
- async function registerReplayFixtureUser(
559
- backendUrl: string,
560
- label: string
561
- ): Promise<ReplayFixtureSession> {
562
- const username = slugifyReplaySetup(label);
563
- const response = await backendFetch(
564
- backendHttpUrl(backendUrl, '/api/v2/auth/register'),
565
- {
566
- method: 'POST',
567
- headers: { 'content-type': 'application/json' },
568
- body: JSON.stringify({
569
- display_name: username,
570
- e2e_fixture: true,
571
- // updateViewerFeatureFlag requires a developer user (#2149 gate); the
572
- // fixture-registration path marks the throwaway user via this param
573
- // (auth_controller maybe_mark_e2e_fixture_developer), which is only
574
- // honored when KANDAN_E2E_FIXTURE_REGISTRATION enabled the stub-mode
575
- // dev backend.
576
- e2e_fixture_developer: true,
577
- email: `${username}@example.test`,
578
- password: `pw-${username}`,
579
- username,
580
- }),
581
- },
582
- 'fixture user registration'
583
- );
584
- const payload = await parseJsonObjectResponse(
585
- response,
586
- 'fixture user registration'
587
- );
588
- const accessToken = (payload as { access_token?: unknown }).access_token;
589
- const refreshToken = (payload as { refresh_token?: unknown }).refresh_token;
590
- const user = (payload as { user?: unknown }).user;
591
- const payloadUsername = (payload as { username?: unknown }).username;
592
- const userId = (payload as { user_id?: unknown }).user_id;
593
-
594
- if (typeof accessToken !== 'string' || accessToken.trim() === '') {
595
- throw new Error(
596
- `fixture user registration did not return access_token: ${JSON.stringify(payload)}`
597
- );
598
- }
599
-
600
- const userObject =
601
- typeof user === 'object' && user !== null && !Array.isArray(user)
602
- ? (user as JsonObject)
603
- : {
604
- id:
605
- typeof userId === 'string' || typeof userId === 'number'
606
- ? userId
607
- : typeof payloadUsername === 'string'
608
- ? payloadUsername
609
- : label,
610
- username:
611
- typeof payloadUsername === 'string' ? payloadUsername : label,
612
- };
613
-
614
- return {
615
- accessToken,
616
- refreshToken: typeof refreshToken === 'string' ? refreshToken : undefined,
617
- userId:
618
- typeof userId === 'string' || typeof userId === 'number'
619
- ? userId
620
- : typeof userObject.id === 'string' || typeof userObject.id === 'number'
621
- ? userObject.id
622
- : label,
623
- username:
624
- typeof payloadUsername === 'string' && payloadUsername.trim() !== ''
625
- ? payloadUsername
626
- : label,
627
- user: userObject,
628
- };
629
- }
630
-
631
- async function enableReplayFixtureFeatureFlag(
632
- backendUrl: string,
633
- accessToken: string,
634
- flag: 'CLAUDE_CODE_SUPPORT_PREVIEW'
635
- ): Promise<void> {
636
- const response = await backendFetch(
637
- backendHttpUrl(backendUrl, '/api/v2/graphql'),
638
- {
639
- method: 'POST',
640
- headers: authenticatedJsonHeaders(accessToken),
641
- body: JSON.stringify({
642
- query: `
643
- mutation EnableReplayFixtureFeatureFlag(
644
- $flag: ViewerFeatureFlagKey!
645
- $enabled: Boolean!
646
- ) {
647
- updateViewerFeatureFlag(input: {flag: $flag, enabled: $enabled}) {
648
- viewer {
649
- featureFlags {
650
- claudeCodeSupportPreview
651
- }
652
- }
653
- }
654
- }
655
- `,
656
- variables: { enabled: true, flag },
657
- }),
658
- },
659
- 'fixture feature flag enable'
660
- );
661
- const payload = await parseJsonObjectResponse(
662
- response,
663
- 'fixture feature flag enable'
664
- );
665
- const errors = (payload as { errors?: unknown }).errors;
666
-
667
- if (errors !== undefined) {
668
- throw new Error(
669
- `fixture feature flag enable GraphQL errors: ${JSON.stringify(errors)}`
670
- );
671
- }
672
-
673
- const data = objectField(payload, 'data');
674
- const result = objectField(data, 'updateViewerFeatureFlag');
675
- const viewer = objectField(result, 'viewer');
676
- const featureFlags = objectField(viewer, 'featureFlags');
677
-
678
- if (
679
- (featureFlags as { readonly claudeCodeSupportPreview?: unknown })
680
- .claudeCodeSupportPreview !== true
681
- ) {
682
- throw new Error(
683
- `fixture feature flag enable did not enable Claude Code support: ${JSON.stringify(payload)}`
684
- );
685
- }
686
- }
687
-
688
- async function createReplayWorkspace(
689
- backendUrl: string,
690
- accessToken: string,
691
- input: { readonly name: string; readonly slug: string }
692
- ): Promise<void> {
693
- const response = await backendFetch(
694
- backendHttpUrl(backendUrl, '/api/v2/workspaces'),
695
- {
696
- method: 'POST',
697
- headers: authenticatedJsonHeaders(accessToken),
698
- body: JSON.stringify(input),
699
- },
700
- 'fixture workspace create'
701
- );
702
- const payload = await parseJsonObjectResponse(
703
- response,
704
- 'fixture workspace create'
705
- );
706
-
707
- if (typeof (payload as { workspace?: unknown }).workspace !== 'object') {
708
- throw new Error(
709
- `fixture workspace create did not return workspace: ${JSON.stringify(payload)}`
710
- );
711
- }
712
- }
713
-
714
- async function createReplayChannel(
715
- backendUrl: string,
716
- accessToken: string,
717
- input: {
718
- readonly workspace: string;
719
- readonly name: string;
720
- readonly slug: string;
721
- }
722
- ): Promise<void> {
723
- const response = await backendFetch(
724
- backendHttpUrl(
725
- backendUrl,
726
- `/api/v2/workspaces/${encodeURIComponent(input.workspace)}/channels`
727
- ),
728
- {
729
- method: 'POST',
730
- headers: authenticatedJsonHeaders(accessToken),
731
- body: JSON.stringify({
732
- conversation_type: 'public_channel',
733
- name: input.name,
734
- slug: input.slug,
735
- topic: 'Agent backend replay fixture channel',
736
- }),
737
- },
738
- 'fixture channel create'
739
- );
740
- const payload = await parseJsonObjectResponse(
741
- response,
742
- 'fixture channel create'
743
- );
744
-
745
- if (typeof (payload as { channel?: unknown }).channel !== 'object') {
746
- throw new Error(
747
- `fixture channel create did not return channel: ${JSON.stringify(payload)}`
748
- );
749
- }
750
- }
751
-
752
- function authenticatedJsonHeaders(accessToken: string): Record<string, string> {
753
- return {
754
- accept: 'application/json',
755
- authorization: `Bearer ${accessToken}`,
756
- 'content-type': 'application/json',
757
- };
758
- }
759
-
760
- async function backendFetch(
761
- url: string,
762
- init: RequestInit,
763
- label: string
764
- ): Promise<Response> {
765
- try {
766
- return await fetch(url, init);
767
- } catch (error) {
768
- const message = error instanceof Error ? error.message : String(error);
769
- throw new Error(`${label} request failed for ${url}: ${message}`);
770
- }
771
- }
772
-
773
- async function parseJsonObjectResponse(
774
- response: Response,
775
- label: string
776
- ): Promise<object> {
777
- const payload: unknown = await response.json().catch(() => undefined);
778
-
779
- if (
780
- !response.ok ||
781
- typeof payload !== 'object' ||
782
- payload === null ||
783
- Array.isArray(payload) ||
784
- (payload as { ok?: unknown }).ok === false
785
- ) {
786
- throw new Error(
787
- `${label} failed: ${response.status}:${JSON.stringify(payload)}`
788
- );
789
- }
790
-
791
- return payload;
792
- }
793
-
794
- function slugifyReplaySetup(value: string): string {
795
- const slug = value
796
- .toLowerCase()
797
- .replace(/[^a-z0-9]+/gu, '-')
798
- .replace(/^-+|-+$/gu, '')
799
- .slice(0, 58);
800
-
801
- if (slug.trim() === '') {
802
- throw new Error('fixture setup label produced an empty slug');
803
- }
804
-
805
- return slug;
806
- }
807
-
808
- async function runBackendReplay(
809
- options: BackendReplayOptions,
810
- provider: string
811
- ): Promise<JsonObject> {
812
- if (options.backendUrl === undefined) {
813
- throw new Error('backend replay requires --backend-url <url>');
814
- }
815
-
816
- if (options.workspace === undefined || options.workspace.trim() === '') {
817
- throw new Error('backend replay requires --workspace <slug>');
818
- }
819
-
820
- if (options.channel === undefined || options.channel.trim() === '') {
821
- throw new Error('backend replay requires --channel <slug>');
822
- }
823
-
824
- if (
825
- options.sessionToken === undefined ||
826
- options.sessionToken.trim() === ''
827
- ) {
828
- throw new Error(
829
- 'backend replay requires --session-token <browser-session-token> or LINZUMI_AGENT_BACKEND_REPLAY_SESSION_TOKEN so it can call the normal GraphQL start mutation'
830
- );
831
- }
832
-
833
- const runnerToken = await resolveBackendReplayToken(options);
834
- const wireServer = options.wire
835
- ? await startWireReplayServerForFixture(options)
836
- : undefined;
837
-
838
- try {
839
- const runner = await runLocalCodexRunner(
840
- backendReplayRunnerOptions(
841
- options,
842
- provider,
843
- runnerToken,
844
- wireServer?.url
845
- )
846
- );
847
-
848
- try {
849
- const start = await startLocalCodexRunnerInstance(options, provider);
850
- const oracles = await runBackendReplayOracles(options, start, provider);
851
-
852
- return {
853
- runnerId: options.runnerId,
854
- instanceId: runner.instanceId,
855
- codexUrl: runner.codexUrl,
856
- frontendTarget: frontendReplayTarget(options, start.threadId),
857
- start,
858
- oracles,
859
- ...(wireServer === undefined ? {} : { wire: wireServer.report() }),
860
- };
861
- } finally {
862
- await runner.close();
863
- }
864
- } finally {
865
- await wireServer?.close();
866
- }
867
- }
868
-
869
- type BackendReplayStartSuccess = {
870
- readonly typename: 'StartLocalCodexRunnerInstanceSuccess';
871
- readonly runnerKey: string;
872
- readonly threadId: string;
873
- };
874
-
875
- type BackendReplayOracleCheck = {
876
- readonly id: string;
877
- readonly status: 'passed' | 'failed';
878
- readonly details: JsonObject;
879
- };
880
-
881
- async function runBackendReplayOracles(
882
- options: BackendReplayOptions,
883
- start: BackendReplayStartSuccess,
884
- provider: string
885
- ): Promise<JsonObject> {
886
- const startedAtMs = Date.now();
887
- const deadlineMs = startedAtMs + backendReplayOracleTimeoutMs;
888
- let lastResult: JsonObject | undefined;
889
- let attempts = 0;
890
-
891
- while (Date.now() <= deadlineMs) {
892
- attempts += 1;
893
- const result = await evaluateBackendReplayOracles(options, start, provider);
894
- lastResult = result;
895
-
896
- if (result.status === 'passed') {
897
- return {
898
- ...result,
899
- attempts,
900
- elapsedMs: Date.now() - startedAtMs,
901
- timeoutMs: backendReplayOracleTimeoutMs,
902
- };
903
- }
904
-
905
- await waitForBackendReplayOraclePoll();
906
- }
907
-
908
- if (lastResult === undefined) {
909
- return {
910
- status: 'failed',
911
- checks: [],
912
- messageStates: [],
913
- threadOutput: [],
914
- attempts,
915
- elapsedMs: Date.now() - startedAtMs,
916
- timeoutMs: backendReplayOracleTimeoutMs,
917
- timeoutReason: 'backend replay oracle did not run before timeout',
918
- };
919
- }
920
-
921
- return {
922
- ...lastResult,
923
- attempts,
924
- elapsedMs: Date.now() - startedAtMs,
925
- timeoutMs: backendReplayOracleTimeoutMs,
926
- timeoutReason:
927
- 'backend replay oracle conditions did not pass before timeout',
928
- };
929
- }
930
-
931
- function waitForBackendReplayOraclePoll(): Promise<void> {
932
- return new Promise((resolve) => {
933
- const timeout = setTimeout(resolve, backendReplayOraclePollMs);
934
- timeout.unref?.();
935
- });
936
- }
937
-
938
- async function evaluateBackendReplayOracles(
939
- options: BackendReplayOptions,
940
- start: BackendReplayStartSuccess,
941
- provider: string
942
- ): Promise<JsonObject> {
943
- const statesResult = await queryBackendReplayMessageStates(
944
- options,
945
- start.threadId
946
- );
947
- const checks: BackendReplayOracleCheck[] = [];
948
-
949
- checks.push({
950
- id: 'message-states-present',
951
- status: statesResult.states.length > 0 ? 'passed' : 'failed',
952
- details: { count: statesResult.states.length },
953
- });
954
-
955
- const runnerStates = statesResult.states.filter(
956
- (state) => state.runnerKey === options.runnerId
957
- );
958
- checks.push({
959
- id: 'message-states-runner-key',
960
- status: runnerStates.length > 0 ? 'passed' : 'failed',
961
- details: { runnerId: options.runnerId, count: runnerStates.length },
962
- });
963
-
964
- const terminalStates = runnerStates.filter(
965
- (state) => state.status === 'processed' || state.status === 'failed'
966
- );
967
- checks.push({
968
- id: 'message-state-terminal',
969
- status: terminalStates.length > 0 ? 'passed' : 'failed',
970
- details: {
971
- terminalStatuses: terminalStates.map((state) => state.status),
972
- allStatuses: runnerStates.map((state) => state.status),
973
- },
974
- });
975
-
976
- const processedState = terminalStates.find(
977
- (state) => state.status === 'processed'
978
- );
979
- checks.push({
980
- id: 'message-state-processed',
981
- status: processedState === undefined ? 'failed' : 'passed',
982
- details: {
983
- processedSeq: processedState?.seq,
984
- failedReasons: terminalStates
985
- .filter((state) => state.status === 'failed')
986
- .map((state) => state.reason ?? 'unknown'),
987
- },
988
- });
989
-
990
- const output =
991
- processedState === undefined
992
- ? { sourceSeq: undefined, items: [] as BackendReplayThreadOutputItem[] }
993
- : await queryBackendReplayThreadOutputSettled(
994
- options,
995
- start.threadId,
996
- processedState.seq,
997
- provider
998
- );
999
- checks.push({
1000
- id: 'thread-output-present',
1001
- status: output.items.length > 0 ? 'passed' : 'failed',
1002
- details: { sourceSeq: output.sourceSeq, count: output.items.length },
1003
- });
1004
-
1005
- const supportedOutputTypenames = new Set([
1006
- 'ThreadFeedCodexAssistantMessage',
1007
- 'ThreadFeedCodexCommandExecutionMessage',
1008
- 'ThreadFeedCodexFileChangeMessage',
1009
- 'ThreadFeedCodexReasoningMessage',
1010
- 'ThreadFeedAgentStatusEvent',
1011
- 'ThreadFeedAgentTaskUpdateEvent',
1012
- ]);
1013
- const unsupportedOutputItems = output.items.filter(
1014
- (item) => !supportedOutputTypenames.has(item.typename)
1015
- );
1016
- checks.push({
1017
- id: 'thread-output-supported-types',
1018
- status:
1019
- output.items.length > 0 && unsupportedOutputItems.length === 0
1020
- ? 'passed'
1021
- : 'failed',
1022
- details: {
1023
- typenames: output.items.map((item) => item.typename),
1024
- unsupportedTypenames: unsupportedOutputItems.map((item) => item.typename),
1025
- },
1026
- });
1027
-
1028
- const assistantItems = output.items.filter(
1029
- (item) => item.typename === 'ThreadFeedCodexAssistantMessage'
1030
- );
1031
- checks.push({
1032
- id: 'thread-output-assistant-present',
1033
- status: assistantItems.length > 0 ? 'passed' : 'failed',
1034
- details: { count: assistantItems.length },
1035
- });
1036
-
1037
- const providerItems = output.items.filter((item) => {
1038
- switch (provider) {
1039
- case 'claude-code':
1040
- return item.agentProvider === 'claude-code';
1041
- case 'codex':
1042
- return (
1043
- item.agentProvider === 'codex' || item.agentProvider === undefined
1044
- );
1045
- }
1046
- });
1047
- checks.push({
1048
- id: 'thread-output-provider',
1049
- status:
1050
- output.items.length > 0 && providerItems.length === output.items.length
1051
- ? 'passed'
1052
- : 'failed',
1053
- details: {
1054
- expectedProvider: provider,
1055
- acceptedProviders:
1056
- provider === 'codex' ? ['codex', null] : ['claude-code'],
1057
- providers: output.items.map((item) => item.agentProvider ?? null),
1058
- },
1059
- });
1060
- checks.push(
1061
- ...featureBackendReplayOracleChecks({
1062
- featureId: options.oracleFeatureId ?? options.requiredFeatureId,
1063
- provider,
1064
- outputItems: output.items,
1065
- requireLane: options.oracleFeatureId !== undefined,
1066
- })
1067
- );
1068
-
1069
- const failedChecks = checks.filter((check) => check.status === 'failed');
1070
-
1071
- return {
1072
- status: failedChecks.length === 0 ? 'passed' : 'failed',
1073
- checks,
1074
- messageStates: statesResult.states,
1075
- threadOutput: output.items,
1076
- };
1077
- }
1078
-
1079
- type BackendReplayMessageState = {
1080
- readonly seq: number;
1081
- readonly status: string;
1082
- readonly reason: string | undefined;
1083
- readonly runnerKey: string | undefined;
1084
- readonly updatedAt: string | undefined;
1085
- };
1086
-
1087
- type BackendReplayMessageStatesResult = {
1088
- readonly states: readonly BackendReplayMessageState[];
1089
- };
1090
-
1091
- async function queryBackendReplayMessageStates(
1092
- options: BackendReplayOptions,
1093
- threadId: string
1094
- ): Promise<BackendReplayMessageStatesResult> {
1095
- const data = await postGraphql(options, {
1096
- operationName: 'AgentBackendReplayMessageStates',
1097
- query: backendReplayMessageStatesQuery,
1098
- variables: backendReplayGraphqlVariables(options, threadId),
1099
- });
1100
- const connection = objectField(data, 'threadFeedConnection');
1101
- const rawStates = arrayField(connection, 'localCodexMessageStates');
1102
-
1103
- return {
1104
- states: rawStates.map(parseBackendReplayMessageState),
1105
- };
1106
- }
1107
-
1108
- type BackendReplayThreadOutputItem = {
1109
- readonly typename: string;
1110
- readonly seq: number | undefined;
1111
- readonly content: string | undefined;
1112
- readonly sourceMessageSeq: number | undefined;
1113
- readonly streamKey: string | undefined;
1114
- readonly streamState: string | undefined;
1115
- readonly agentProvider: string | undefined;
1116
- readonly kind: string | undefined;
1117
- readonly title: string | undefined;
1118
- readonly body: string | undefined;
1119
- readonly severity: string | undefined;
1120
- readonly state: string | undefined;
1121
- readonly progressLabel: string | undefined;
1122
- };
1123
-
1124
- async function queryBackendReplayThreadOutput(
1125
- options: BackendReplayOptions,
1126
- threadId: string,
1127
- sourceSeq: number
1128
- ): Promise<{
1129
- readonly sourceSeq: number;
1130
- readonly items: readonly BackendReplayThreadOutputItem[];
1131
- }> {
1132
- const data = await postGraphql(options, {
1133
- operationName: 'AgentBackendReplayThreadOutput',
1134
- query: backendReplayThreadOutputQuery,
1135
- variables: {
1136
- ...backendReplayGraphqlVariables(options, threadId),
1137
- sourceSeq,
1138
- },
1139
- });
1140
- const rawItems = arrayField(data, 'localCodexThreadOutput');
1141
-
1142
- return {
1143
- sourceSeq,
1144
- items: rawItems.map(parseBackendReplayThreadOutputItem),
1145
- };
1146
- }
1147
-
1148
- // The terminal message state can land before the async row projection
1149
- // finishes, so a single post-terminal query races the projection (observed:
1150
- // the same fixture produced different row subsets run to run). Poll until the
1151
- // projected output is quiescent - and, when a feature oracle is requested,
1152
- // until its checks pass - before snapshotting.
1153
- async function queryBackendReplayThreadOutputSettled(
1154
- options: BackendReplayOptions,
1155
- threadId: string,
1156
- sourceSeq: number,
1157
- provider: 'codex' | 'claude-code'
1158
- ): Promise<{
1159
- readonly sourceSeq: number;
1160
- readonly items: readonly BackendReplayThreadOutputItem[];
1161
- }> {
1162
- const timeoutMs = 30_000;
1163
- const pollIntervalMs = 250;
1164
- const startedAtMs = Date.now();
1165
- const featureId = options.oracleFeatureId ?? options.requiredFeatureId;
1166
- let last = await queryBackendReplayThreadOutput(options, threadId, sourceSeq);
1167
- // Content-keyed stability: a row can flip streaming -> completed (or grow
1168
- // fields) without changing the count, so count-only comparison could
1169
- // snapshot mid-stream rows as settled.
1170
- let lastFingerprint = JSON.stringify(last.items);
1171
- let stableRounds = 0;
1172
-
1173
- while (Date.now() - startedAtMs < timeoutMs) {
1174
- const featureSatisfied =
1175
- featureId === undefined ||
1176
- featureBackendReplayOracleChecks({
1177
- featureId,
1178
- provider,
1179
- outputItems: last.items,
1180
- requireLane: options.oracleFeatureId !== undefined,
1181
- }).every((check) => check.status === 'passed');
1182
-
1183
- if (featureSatisfied && stableRounds >= 2) {
1184
- return last;
1185
- }
1186
-
1187
- await new Promise((resolvePoll) => setTimeout(resolvePoll, pollIntervalMs));
1188
- const next = await queryBackendReplayThreadOutput(
1189
- options,
1190
- threadId,
1191
- sourceSeq
1192
- );
1193
- const nextFingerprint = JSON.stringify(next.items);
1194
- stableRounds = nextFingerprint === lastFingerprint ? stableRounds + 1 : 0;
1195
- lastFingerprint = nextFingerprint;
1196
- last = next;
1197
- }
1198
-
1199
- return last;
1200
- }
1201
-
1202
- function backendReplayGraphqlVariables(
1203
- options: BackendReplayOptions,
1204
- threadId: string
1205
- ): JsonObject {
1206
- if (options.workspace === undefined || options.channel === undefined) {
1207
- throw new Error('backend oracle requires workspace and channel');
1208
- }
1209
-
1210
- return {
1211
- workspaceSlug: options.workspace,
1212
- channelSlug: options.channel,
1213
- threadId,
1214
- };
1215
- }
1216
-
1217
- function parseBackendReplayMessageState(
1218
- value: unknown
1219
- ): BackendReplayMessageState {
1220
- const state = objectFromUnknown(value, 'localCodexMessageState');
1221
- const seq = numberField(state, 'seq');
1222
- const status = stringField(state, 'status');
1223
-
1224
- return {
1225
- seq,
1226
- status,
1227
- reason: optionalStringField(state, 'reason'),
1228
- runnerKey: optionalStringField(state, 'runnerKey'),
1229
- updatedAt: optionalStringField(state, 'updatedAt'),
1230
- };
1231
- }
1232
-
1233
- function parseBackendReplayThreadOutputItem(
1234
- value: unknown
1235
- ): BackendReplayThreadOutputItem {
1236
- const item = objectFromUnknown(value, 'localCodexThreadOutput item');
1237
-
1238
- return {
1239
- typename: stringField(item, '__typename'),
1240
- seq: optionalNumberField(item, 'seq'),
1241
- content: optionalStringField(item, 'content'),
1242
- sourceMessageSeq: optionalNumberField(item, 'sourceMessageSeq'),
1243
- streamKey: optionalStringField(item, 'streamKey'),
1244
- streamState: optionalStringField(item, 'streamState'),
1245
- agentProvider: optionalStringField(item, 'agentProvider'),
1246
- kind: optionalStringField(item, 'kind'),
1247
- title: optionalStringField(item, 'title'),
1248
- body: optionalStringField(item, 'body'),
1249
- severity: optionalStringField(item, 'severity'),
1250
- state: optionalStringField(item, 'state'),
1251
- progressLabel: optionalStringField(item, 'progressLabel'),
1252
- };
1253
- }
1254
-
1255
- async function postGraphql(
1256
- options: BackendReplayOptions,
1257
- body: JsonObject
1258
- ): Promise<object> {
1259
- if (options.backendUrl === undefined) {
1260
- throw new Error('GraphQL request requires --backend-url <url>');
1261
- }
1262
-
1263
- if (
1264
- options.sessionToken === undefined ||
1265
- options.sessionToken.trim() === ''
1266
- ) {
1267
- throw new Error('GraphQL request requires a non-empty session token');
1268
- }
1269
-
1270
- const response = await backendFetch(
1271
- backendHttpUrl(options.backendUrl, '/api/v2/graphql'),
1272
- {
1273
- method: 'POST',
1274
- headers: {
1275
- accept: 'application/json',
1276
- authorization: `Bearer ${options.sessionToken}`,
1277
- 'content-type': 'application/json',
1278
- },
1279
- body: JSON.stringify(body),
1280
- },
1281
- 'GraphQL request'
1282
- );
1283
- const payload = await parseJsonObjectResponse(response, 'GraphQL request');
1284
- const errors = (payload as { errors?: unknown }).errors;
1285
-
1286
- if (errors !== undefined) {
1287
- throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`);
1288
- }
1289
-
1290
- return objectField(payload, 'data');
1291
- }
1292
-
1293
- function objectField(value: object, field: string): object {
1294
- return objectFromUnknown((value as Record<string, unknown>)[field], field);
1295
- }
1296
-
1297
- function arrayField(value: object, field: string): readonly unknown[] {
1298
- const raw = (value as Record<string, unknown>)[field];
1299
-
1300
- if (!Array.isArray(raw)) {
1301
- throw new Error(`${field} must be an array`);
1302
- }
1303
-
1304
- return raw;
1305
- }
1306
-
1307
- function objectFromUnknown(value: unknown, label: string): object {
1308
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1309
- throw new Error(`${label} must be an object`);
1310
- }
1311
-
1312
- return value;
1313
- }
1314
-
1315
- function stringField(value: object, field: string): string {
1316
- const raw = (value as Record<string, unknown>)[field];
1317
-
1318
- if (typeof raw !== 'string') {
1319
- throw new Error(`${field} must be a string`);
1320
- }
1321
-
1322
- return raw;
1323
- }
1324
-
1325
- function optionalStringField(value: object, field: string): string | undefined {
1326
- const raw = (value as Record<string, unknown>)[field];
1327
-
1328
- if (raw === null || raw === undefined) {
1329
- return undefined;
1330
- }
1331
-
1332
- if (typeof raw !== 'string') {
1333
- throw new Error(`${field} must be a string when present`);
1334
- }
1335
-
1336
- return raw;
1337
- }
1338
-
1339
- function numberField(value: object, field: string): number {
1340
- const raw = (value as Record<string, unknown>)[field];
1341
-
1342
- if (!Number.isInteger(raw)) {
1343
- throw new Error(`${field} must be an integer`);
1344
- }
1345
-
1346
- return raw;
1347
- }
1348
-
1349
- function optionalNumberField(value: object, field: string): number | undefined {
1350
- const raw = (value as Record<string, unknown>)[field];
1351
-
1352
- if (raw === null || raw === undefined) {
1353
- return undefined;
1354
- }
1355
-
1356
- if (!Number.isInteger(raw)) {
1357
- throw new Error(`${field} must be an integer when present`);
1358
- }
1359
-
1360
- return raw;
1361
- }
1362
-
1363
- const backendReplayMessageStatesQuery = `
1364
- query AgentBackendReplayMessageStates(
1365
- $workspaceSlug: String!
1366
- $channelSlug: String!
1367
- $threadId: ID!
1368
- ) {
1369
- threadFeedConnection(
1370
- workspaceSlug: $workspaceSlug
1371
- channelSlug: $channelSlug
1372
- threadId: $threadId
1373
- last: 20
1374
- ) {
1375
- localCodexMessageStates {
1376
- seq
1377
- status
1378
- reason
1379
- runnerKey
1380
- updatedAt
1381
- }
1382
- }
1383
- }
1384
- `;
1385
-
1386
- const backendReplayThreadOutputQuery = `
1387
- query AgentBackendReplayThreadOutput(
1388
- $workspaceSlug: String!
1389
- $channelSlug: String!
1390
- $threadId: ID!
1391
- $sourceSeq: Int!
1392
- ) {
1393
- localCodexThreadOutput(
1394
- workspaceSlug: $workspaceSlug
1395
- channelSlug: $channelSlug
1396
- threadId: $threadId
1397
- sourceSeq: $sourceSeq
1398
- ) {
1399
- __typename
1400
- ... on ThreadFeedEntry {
1401
- seq
1402
- }
1403
- ... on ThreadFeedCodexReasoningMessage {
1404
- sourceMessageSeq
1405
- streamKey
1406
- streamState
1407
- agentProvider
1408
- }
1409
- ... on ThreadFeedCodexAssistantMessage {
1410
- content
1411
- sourceMessageSeq
1412
- streamKey
1413
- streamState
1414
- agentProvider
1415
- }
1416
- ... on ThreadFeedCodexCommandExecutionMessage {
1417
- sourceMessageSeq
1418
- streamKey
1419
- streamState
1420
- agentProvider
1421
- }
1422
- ... on ThreadFeedCodexFileChangeMessage {
1423
- sourceMessageSeq
1424
- streamKey
1425
- streamState
1426
- agentProvider
1427
- }
1428
- ... on ThreadFeedAgentStatusEvent {
1429
- sourceMessageSeq
1430
- agentProvider
1431
- kind
1432
- title
1433
- body
1434
- severity
1435
- }
1436
- ... on ThreadFeedAgentTaskUpdateEvent {
1437
- sourceMessageSeq
1438
- agentProvider
1439
- kind
1440
- title
1441
- body
1442
- state
1443
- progressLabel
1444
- }
1445
- }
1446
- }
1447
- `;
1448
-
1449
- function backendReplayRunnerOptions(
1450
- options: BackendReplayOptions,
1451
- provider: string,
1452
- token: string,
1453
- wireCodexUrl?: string | undefined
1454
- ): RunnerOptions {
1455
- return {
1456
- kandanUrl: options.backendUrl ?? '',
1457
- token,
1458
- runnerId: options.runnerId,
1459
- clientId: 'agent-backend-replay',
1460
- workspaceSlug: options.workspace,
1461
- cwd: options.cwd,
1462
- codexBin: process.env.CODEX_BIN ?? 'codex',
1463
- // Wire mode: the runner connects its REAL websocket client to the stub
1464
- // codex app-server through the same --codex-url seam it uses for a real
1465
- // pre-started codex, instead of substituting an in-process fake client.
1466
- codexUrl: wireCodexUrl,
1467
- launchTui: false,
1468
- allowedCwds: [options.cwd],
1469
- channelSession: undefined,
1470
- agentReplay:
1471
- wireCodexUrl !== undefined
1472
- ? undefined
1473
- : {
1474
- fixturePath: options.fixturePath,
1475
- provider: options.provider,
1476
- clockMode: parseAgentReplayClockMode(options.speed),
1477
- },
1478
- };
1479
- }
1480
-
1481
- async function startLocalCodexRunnerInstance(
1482
- options: BackendReplayOptions,
1483
- provider: string
1484
- ): Promise<BackendReplayStartSuccess> {
1485
- if (options.backendUrl === undefined) {
1486
- throw new Error('start mutation requires --backend-url <url>');
1487
- }
1488
-
1489
- if (options.workspace === undefined || options.channel === undefined) {
1490
- throw new Error('start mutation requires --workspace and --channel');
1491
- }
1492
-
1493
- if (
1494
- options.sessionToken === undefined ||
1495
- options.sessionToken.trim() === ''
1496
- ) {
1497
- throw new Error('start mutation requires a non-empty session token');
1498
- }
1499
-
1500
- const response = await backendFetch(
1501
- backendHttpUrl(options.backendUrl, '/api/v2/graphql'),
1502
- {
1503
- method: 'POST',
1504
- headers: {
1505
- accept: 'application/json',
1506
- authorization: `Bearer ${options.sessionToken}`,
1507
- 'content-type': 'application/json',
1508
- },
1509
- body: JSON.stringify({
1510
- operationName: 'AgentBackendReplayStartLocalCodexRunnerInstance',
1511
- query: startLocalCodexRunnerInstanceMutation,
1512
- variables: {
1513
- workspaceSlug: options.workspace,
1514
- channelSlug: options.channel,
1515
- runnerId: options.runnerId,
1516
- cwd: options.cwd,
1517
- workDescription: options.workDescription,
1518
- agentProvider: provider,
1519
- allowPortForwardingByDefault: true,
1520
- },
1521
- }),
1522
- },
1523
- 'startLocalCodexRunnerInstance GraphQL request'
1524
- );
1525
- const payload: unknown = await response.json();
1526
-
1527
- if (!response.ok) {
1528
- throw new Error(
1529
- `startLocalCodexRunnerInstance HTTP failed: ${response.status}:${JSON.stringify(payload)}`
1530
- );
1531
- }
1532
-
1533
- if (
1534
- typeof payload !== 'object' ||
1535
- payload === null ||
1536
- Array.isArray(payload)
1537
- ) {
1538
- throw new Error(
1539
- 'startLocalCodexRunnerInstance returned a non-object payload'
1540
- );
1541
- }
1542
-
1543
- const errors = (payload as { errors?: unknown }).errors;
1544
- if (errors !== undefined) {
1545
- throw new Error(
1546
- `startLocalCodexRunnerInstance GraphQL errors: ${JSON.stringify(errors)}`
1547
- );
1548
- }
1549
-
1550
- const result = startMutationResult(payload);
1551
-
1552
- switch (result.__typename) {
1553
- case 'StartLocalCodexRunnerInstanceSuccess':
1554
- return {
1555
- typename: result.__typename,
1556
- runnerKey: result.runnerKey,
1557
- threadId: result.threadId,
1558
- };
1559
- case 'StartLocalCodexRunnerInstanceUnavailable':
1560
- throw new Error(
1561
- `startLocalCodexRunnerInstance unavailable: ${result.reason}`
1562
- );
1563
- default:
1564
- throw new Error(
1565
- `startLocalCodexRunnerInstance returned unexpected result: ${JSON.stringify(result)}`
1566
- );
1567
- }
1568
- }
1569
-
1570
- function startMutationResult(payload: object):
1571
- | {
1572
- readonly __typename: 'StartLocalCodexRunnerInstanceSuccess';
1573
- readonly runnerKey: string;
1574
- readonly threadId: string;
1575
- }
1576
- | {
1577
- readonly __typename: 'StartLocalCodexRunnerInstanceUnavailable';
1578
- readonly reason: string;
1579
- }
1580
- | { readonly __typename: string } {
1581
- const data = (payload as { data?: unknown }).data;
1582
-
1583
- if (typeof data !== 'object' || data === null || Array.isArray(data)) {
1584
- throw new Error('startLocalCodexRunnerInstance missing data');
1585
- }
1586
-
1587
- const result = (data as { startLocalCodexRunnerInstance?: unknown })
1588
- .startLocalCodexRunnerInstance;
1589
-
1590
- if (typeof result !== 'object' || result === null || Array.isArray(result)) {
1591
- throw new Error('startLocalCodexRunnerInstance missing result');
1592
- }
1593
-
1594
- const typename = (result as { __typename?: unknown }).__typename;
1595
-
1596
- if (typename === 'StartLocalCodexRunnerInstanceSuccess') {
1597
- const runnerKey = (result as { runnerKey?: unknown }).runnerKey;
1598
- const threadId = (result as { threadId?: unknown }).threadId;
1599
-
1600
- if (typeof runnerKey !== 'string' || typeof threadId !== 'string') {
1601
- throw new Error(
1602
- `startLocalCodexRunnerInstance success had invalid fields: ${JSON.stringify(result)}`
1603
- );
1604
- }
1605
-
1606
- return { __typename: typename, runnerKey, threadId };
1607
- }
1608
-
1609
- if (typename === 'StartLocalCodexRunnerInstanceUnavailable') {
1610
- const reason = (result as { reason?: unknown }).reason;
1611
-
1612
- if (typeof reason !== 'string') {
1613
- throw new Error(
1614
- `startLocalCodexRunnerInstance unavailable had invalid fields: ${JSON.stringify(result)}`
1615
- );
1616
- }
1617
-
1618
- return { __typename: typename, reason };
1619
- }
1620
-
1621
- if (typeof typename === 'string') {
1622
- return { __typename: typename };
1623
- }
1624
-
1625
- throw new Error(
1626
- `startLocalCodexRunnerInstance result missing __typename: ${JSON.stringify(result)}`
1627
- );
1628
- }
1629
-
1630
- const startLocalCodexRunnerInstanceMutation = `
1631
- mutation AgentBackendReplayStartLocalCodexRunnerInstance(
1632
- $workspaceSlug: String!
1633
- $channelSlug: String!
1634
- $runnerId: String!
1635
- $cwd: String!
1636
- $workDescription: String!
1637
- $agentProvider: String
1638
- $allowPortForwardingByDefault: Boolean
1639
- ) {
1640
- startLocalCodexRunnerInstance(
1641
- input: {
1642
- workspaceSlug: $workspaceSlug
1643
- channelSlug: $channelSlug
1644
- runnerKey: $runnerId
1645
- cwd: $cwd
1646
- workDescription: $workDescription
1647
- agentProvider: $agentProvider
1648
- allowPortForwardingByDefault: $allowPortForwardingByDefault
1649
- }
1650
- ) {
1651
- __typename
1652
- ... on StartLocalCodexRunnerInstanceSuccess {
1653
- runnerKey
1654
- threadId
1655
- }
1656
- ... on StartLocalCodexRunnerInstanceUnavailable {
1657
- reason
1658
- }
1659
- }
1660
- }
1661
- `;
1662
-
1663
- async function probeBackend(
1664
- options: BackendReplayOptions
1665
- ): Promise<JsonObject> {
1666
- if (options.backendUrl === undefined) {
1667
- throw new Error('--probe-backend requires --backend-url <url>');
1668
- }
1669
-
1670
- const token = await resolveBackendReplayToken(options);
1671
-
1672
- const client = await connectPhoenixClient(options.backendUrl, token);
1673
-
1674
- try {
1675
- const topic = `local_runner:${options.runnerId}`;
1676
- const reply = await client.join(topic, backendProbeJoinPayload(options), {
1677
- registrationId: 'agent-backend-replay-probe',
1678
- });
1679
-
1680
- return {
1681
- topic,
1682
- ok: reply.ok === true,
1683
- protocol: typeof reply.protocol === 'string' ? reply.protocol : undefined,
1684
- runnerId:
1685
- typeof reply.runner_id === 'string' ? reply.runner_id : undefined,
1686
- serverCapabilities:
1687
- typeof reply.server_capabilities === 'object' &&
1688
- reply.server_capabilities !== null &&
1689
- !Array.isArray(reply.server_capabilities)
1690
- ? reply.server_capabilities
1691
- : undefined,
1692
- };
1693
- } finally {
1694
- client.close();
1695
- }
1696
- }
1697
-
1698
- async function resolveBackendReplayToken(
1699
- options: BackendReplayOptions
1700
- ): Promise<string> {
1701
- if (options.token !== undefined && options.token.trim() !== '') {
1702
- return options.token;
1703
- }
1704
-
1705
- if (
1706
- options.sessionToken !== undefined &&
1707
- options.sessionToken.trim() !== ''
1708
- ) {
1709
- return exchangeSessionTokenForLocalRunnerToken(options);
1710
- }
1711
-
1712
- throw new Error(
1713
- '--probe-backend requires --token <local-runner-token>, LINZUMI_AGENT_BACKEND_REPLAY_TOKEN, --session-token <session-token>, or LINZUMI_AGENT_BACKEND_REPLAY_SESSION_TOKEN'
1714
- );
1715
- }
1716
-
1717
- async function exchangeSessionTokenForLocalRunnerToken(
1718
- options: BackendReplayOptions
1719
- ): Promise<string> {
1720
- if (options.backendUrl === undefined) {
1721
- throw new Error('--session-token exchange requires --backend-url <url>');
1722
- }
1723
-
1724
- if (options.workspace === undefined || options.workspace.trim() === '') {
1725
- throw new Error('--session-token exchange requires --workspace <slug>');
1726
- }
1727
-
1728
- if (
1729
- options.sessionToken === undefined ||
1730
- options.sessionToken.trim() === ''
1731
- ) {
1732
- throw new Error(
1733
- '--session-token exchange requires a non-empty session token'
1734
- );
1735
- }
1736
-
1737
- const endpoint = backendHttpUrl(
1738
- options.backendUrl,
1739
- '/api/v2/local-codex-runner/oauth/session-exchange'
1740
- );
1741
- const body =
1742
- options.channel === undefined || options.channel.trim() === ''
1743
- ? { workspace: options.workspace }
1744
- : { workspace: options.workspace, channel: options.channel };
1745
-
1746
- const response = await backendFetch(
1747
- endpoint,
1748
- {
1749
- method: 'POST',
1750
- headers: {
1751
- accept: 'application/json',
1752
- authorization: `Bearer ${options.sessionToken}`,
1753
- 'content-type': 'application/json',
1754
- },
1755
- body: JSON.stringify(body),
1756
- },
1757
- 'local-runner session token exchange'
1758
- );
1759
- const payload: unknown = await response.json();
1760
-
1761
- if (
1762
- !response.ok ||
1763
- typeof payload !== 'object' ||
1764
- payload === null ||
1765
- Array.isArray(payload) ||
1766
- (payload as { ok?: unknown }).ok !== true ||
1767
- typeof (payload as { access_token?: unknown }).access_token !== 'string'
1768
- ) {
1769
- const error =
1770
- typeof payload === 'object' &&
1771
- payload !== null &&
1772
- !Array.isArray(payload) &&
1773
- typeof (payload as { error?: unknown }).error === 'string'
1774
- ? (payload as { error: string }).error
1775
- : 'unknown';
1776
- throw new Error(
1777
- `local-runner session token exchange failed: ${response.status}:${error}`
1778
- );
1779
- }
1780
-
1781
- return (payload as { access_token: string }).access_token;
1782
- }
1783
-
1784
- function backendHttpUrl(backendUrl: string, pathname: string): string {
1785
- const url = new URL(backendUrl);
1786
-
1787
- switch (url.protocol) {
1788
- case 'http:':
1789
- case 'https:':
1790
- break;
1791
- case 'ws:':
1792
- url.protocol = 'http:';
1793
- break;
1794
- case 'wss:':
1795
- url.protocol = 'https:';
1796
- break;
1797
- default:
1798
- throw new Error(
1799
- '--backend-url must use http:, https:, ws:, or wss: protocol'
1800
- );
1801
- }
1802
-
1803
- url.pathname = pathname;
1804
- url.search = '';
1805
- url.hash = '';
1806
- return url.toString();
1807
- }
1808
-
1809
- function backendProbeJoinPayload(options: BackendReplayOptions): JsonObject {
1810
- return {
1811
- clientName: 'kandan-local-codex-runner',
1812
- clientId: 'agent-backend-replay-probe',
1813
- runnerPid: process.pid,
1814
- version: linzumiCliVersion,
1815
- cwd: options.cwd,
1816
- workspace: options.workspace ?? null,
1817
- channel: options.channel ?? null,
1818
- capabilities: {
1819
- codexAppServer: true,
1820
- codexRemoteTui: true,
1821
- client_message_ids: true,
1822
- commander_pipeline: 'pipeline',
1823
- agentProviders: ['codex', 'claude-code'],
1824
- agentReplay: true,
1825
- },
1826
- };
1827
- }
1828
-
1829
- async function manifestSummary(
1830
- fixturePath: string
1831
- ): Promise<AgentReplayManifestSummary | undefined> {
1832
- const loaded = await loadAgentReplayManifestForFixture(fixturePath);
1833
-
1834
- if (loaded === undefined) {
1835
- return undefined;
1836
- }
1837
-
1838
- if (loaded.type === 'error') {
1839
- throw new Error(JSON.stringify(loaded.error));
1840
- }
1841
-
1842
- return summarizeAgentReplayManifest(loaded.manifest);
1843
- }
1844
-
1845
- async function manifestReadiness(
1846
- fixturePath: string
1847
- ): Promise<readonly AgentReplayFeatureReadiness[] | undefined> {
1848
- const loaded = await loadAgentReplayManifestForFixture(fixturePath);
1849
-
1850
- if (loaded === undefined) {
1851
- return undefined;
1852
- }
1853
-
1854
- if (loaded.type === 'error') {
1855
- throw new Error(JSON.stringify(loaded.error));
1856
- }
1857
-
1858
- return evaluateAgentReplayFeatureReadiness(loaded.manifest);
1859
- }
1860
-
1861
- type RequiredFeatureGate =
1862
- | {
1863
- readonly status: 'passed';
1864
- readonly featureId: string;
1865
- readonly readiness: AgentReplayFeatureReadiness;
1866
- }
1867
- | {
1868
- readonly status: 'failed';
1869
- readonly featureId: string;
1870
- readonly reason: string;
1871
- readonly readiness?: AgentReplayFeatureReadiness | undefined;
1872
- };
1873
-
1874
- function evaluateRequiredFeatureGate(
1875
- featureId: string | undefined,
1876
- readiness: readonly AgentReplayFeatureReadiness[] | undefined
1877
- ): RequiredFeatureGate | { readonly status: 'not-required' } {
1878
- if (featureId === undefined) {
1879
- return { status: 'not-required' };
1880
- }
1881
-
1882
- if (readiness === undefined) {
1883
- return {
1884
- status: 'failed',
1885
- featureId,
1886
- reason: '--require-feature needs a neighboring fixture manifest',
1887
- };
1888
- }
1889
-
1890
- const feature = readiness.find((row) => row.featureId === featureId);
1891
-
1892
- if (feature === undefined) {
1893
- return {
1894
- status: 'failed',
1895
- featureId,
1896
- reason: `feature ${featureId} is not present in the fixture manifest`,
1897
- };
1898
- }
1899
-
1900
- switch (feature.status) {
1901
- case 'implemented':
1902
- case 'ready-to-implement':
1903
- return { status: 'passed', featureId, readiness: feature };
1904
- case 'blocked':
1905
- case 'missing-proof':
1906
- case 'not-started':
1907
- return {
1908
- status: 'failed',
1909
- featureId,
1910
- readiness: feature,
1911
- reason: feature.reason,
1912
- };
1913
- }
1914
- }
1915
-
1916
- // Every value of a repeatable flag, in argv order (--wire-breakpoint may
1917
- // name several ordered holds).
1918
- function argValues(args: readonly string[], name: string): readonly string[] {
1919
- const values: string[] = [];
1920
- for (let index = 0; index < args.length; index += 1) {
1921
- if (args[index] !== name) {
1922
- continue;
1923
- }
1924
- const value = args[index + 1];
1925
- if (value === undefined) {
1926
- throw new Error(`${name} requires a value`);
1927
- }
1928
- values.push(value);
1929
- index += 1;
1930
- }
1931
- return values;
1932
- }
1933
-
1934
- function argValue(args: readonly string[], name: string): string | undefined {
1935
- const index = args.indexOf(name);
1936
-
1937
- if (index === -1) {
1938
- return undefined;
1939
- }
1940
-
1941
- const value = args[index + 1];
1942
-
1943
- if (value === undefined) {
1944
- throw new Error(`${name} requires a value`);
1945
- }
1946
-
1947
- return value;
1948
- }
1949
-
1950
- function printHelp(): void {
1951
- process.stdout.write(
1952
- `Usage: pnpm run replay:agent-backend -- --fixture <path> [options]\n\nOptions:\n --dry-run Validate fixture/manifest/diagnostics without backend connection\n --probe-backend Also join the real Phoenix local_runner channel, then exit\n --provider auto|codex|claude-code Replay provider. Default: auto\n --speed immediate|realtime|<n>x|<n>ms\n Replay speed. Default: immediate\n --backend-url <url> Local Phoenix backend URL; required by --probe-backend/replay\n --setup-fixture Create a throwaway fixture user/workspace/channel first; requires local fixture registration\n --setup-label <label> Prefix for throwaway fixture setup. Default: agent-backend-replay\n --workspace <slug> Target workspace slug for session-token exchange and future replay\n --channel <slug> Target channel slug for token scoping and future replay\n --thread-id <id> Target thread id for the future backend replay slice\n --runner-id <id> Local runner id for backend probing. Default: generated\n --cwd <path> Cwd advertised in backend probe/replay. Default: current directory\n --work-description <text> Work description sent through the normal start mutation\n --token <token> Local-runner token; also reads LINZUMI_AGENT_BACKEND_REPLAY_TOKEN\n --session-token <token> Browser/session token to exchange for a local-runner token; also reads LINZUMI_AGENT_BACKEND_REPLAY_SESSION_TOKEN\n --out-dir <path> Artifact directory for summary.json. Default: agent-backend-replay-output\n --wire Serve the fixture from the stub codex app-server websocket (spec/codex-wire-capture-replay.spec.md) so the runner's real socket machinery is exercised\n --wire-strictness shape|exact Wire mode validation of runner-sent packets against the recording. Default: shape\n --wire-breakpoint <spec> Replay-to-a-point hold (repeatable, ordered): ordinal:<n> | method:<rpcMethod>[:<n>] | first-delta | turn-started[:<n>] | turn-completed[:<n>]. The stub freezes the stack at the hold; observe/release via GET /replay/breakpoint + POST /replay/resume on the printed control URL\n --require-feature <featureId> Fail before backend setup unless manifest readiness says the feature is ready\n --oracle-feature <featureId> Run feature-specific backend oracles without using readiness as a preflight gate\n --no-summary Do not write summary.json; stdout JSON is still printed\n\nExamples:\n pnpm --filter @linzumi/cli run replay:agent-backend -- --fixture test/fixtures/agent-raw-replay/real-probe/claude.ndjson --provider claude-code --dry-run\n pnpm --filter @linzumi/cli run replay:agent-backend -- --fixture test/fixtures/agent-raw-replay/real-probe/claude.ndjson --provider claude-code --probe-backend --backend-url http://127.0.0.1:4140 --workspace default --session-token <browser-session-token>\n pnpm --filter @linzumi/cli run replay:agent-backend -- --fixture test/fixtures/agent-raw-replay/real-probe/claude.ndjson --provider claude-code --backend-url http://127.0.0.1:4140 --setup-fixture\n`
1953
- );
1954
- }
1955
-
1956
- main().catch((error) => {
1957
- process.stderr.write(
1958
- `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
1959
- );
1960
- process.exitCode = 1;
1961
- });