@linzumi/cli 1.0.18 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,891 @@
1
+ /*
2
+ - Date: 2026-06-30
3
+ Spec: plans/2026-06-29-gstack-claude-code-support-requirements.md
4
+ Relationship: One-command candidate promotion path for the first real gstack
5
+ Claude Code smoke fixture. It fails fast on missing local gstack setup, then
6
+ records exact raw provider data, normalizes the fixture, writes a candidate
7
+ manifest, and runs replay proofs that can be repeated locally.
8
+ */
9
+ import { spawnSync } from 'node:child_process';
10
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
11
+ import { tmpdir } from 'node:os';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { scanAgentReplayInstallFiles } from '../src/agentReplayInstallSafety';
15
+ import { agentReplayManifestPathForFixture } from '../src/agentReplayManifest';
16
+ import { isJsonObject, type JsonObject } from '../src/protocol';
17
+ import { normalizeAgentReplayText } from './normalize-agent-replay-fixture';
18
+
19
+ type PromoteOptions = {
20
+ readonly outDir: string;
21
+ readonly rawDir: string;
22
+ readonly existingRawFixture: string | undefined;
23
+ readonly fixtureDir: string;
24
+ readonly providerReplayOutDir: string;
25
+ readonly backendReplayOutDir: string;
26
+ readonly browserReplayOutDir: string;
27
+ readonly installFixtureDir: string | undefined;
28
+ readonly speed: string | undefined;
29
+ readonly preflightOnly: boolean;
30
+ readonly keepWorkdirs: boolean;
31
+ readonly claudePermissionMode: string | undefined;
32
+ readonly claudeModel: string | undefined;
33
+ readonly gstackSkillDir: string | undefined;
34
+ readonly gstackCli: string | undefined;
35
+ readonly skipBackend: boolean;
36
+ readonly browserProof: boolean;
37
+ readonly localBrowserProof: boolean;
38
+ readonly backendUrl: string | undefined;
39
+ readonly setupFixture: boolean;
40
+ readonly sessionToken: string | undefined;
41
+ readonly workspace: string | undefined;
42
+ readonly channel: string | undefined;
43
+ readonly threadId: string | undefined;
44
+ };
45
+
46
+ type CommandResult = {
47
+ readonly status: number;
48
+ readonly stdout: string;
49
+ readonly stderr: string;
50
+ };
51
+
52
+ type BackendReplayPromotionResult =
53
+ | { readonly type: 'skipped'; readonly reason: string }
54
+ | {
55
+ readonly type: 'ran';
56
+ readonly proofMode: 'backend' | 'backend-browser';
57
+ readonly result: CommandResult;
58
+ };
59
+
60
+ type PromotionStatus =
61
+ | 'blocked'
62
+ | 'preflight-ok'
63
+ | 'provider-ready'
64
+ | 'backend-ready'
65
+ | 'browser-ready'
66
+ | 'failed';
67
+
68
+ const thisFile = fileURLToPath(import.meta.url);
69
+ const packageRoot = resolve(dirname(thisFile), '..');
70
+ const repoRoot = resolve(packageRoot, '..', '..');
71
+ const webRoot = join(repoRoot, 'kandan', 'server_v2', 'web');
72
+ const tsxLoader = join(repoRoot, 'node_modules', 'tsx', 'dist', 'loader.mjs');
73
+ const provider = 'gstack-claude-smoke';
74
+ const featureId = 'gstack-claude-smoke';
75
+
76
+ async function main(): Promise<void> {
77
+ const args = process.argv.slice(2);
78
+
79
+ if (args.includes('--help')) {
80
+ printHelp();
81
+ return;
82
+ }
83
+
84
+ const options = parseArgs(args);
85
+
86
+ if (options.localBrowserProof && options.backendUrl !== undefined) {
87
+ throw new Error(
88
+ 'use either --local-browser-proof or --backend-url, not both'
89
+ );
90
+ }
91
+
92
+ await mkdir(options.outDir, { recursive: true });
93
+
94
+ const preflight =
95
+ options.existingRawFixture === undefined
96
+ ? runScript('record-agent-replay-fixtures.ts', [
97
+ '--provider',
98
+ provider,
99
+ '--preflight-only',
100
+ '--preflight-json',
101
+ '--out-dir',
102
+ options.rawDir,
103
+ ...(options.gstackSkillDir === undefined
104
+ ? []
105
+ : ['--gstack-skill-dir', options.gstackSkillDir]),
106
+ ...(options.gstackCli === undefined
107
+ ? []
108
+ : ['--gstack-cli', options.gstackCli]),
109
+ ])
110
+ : skippedPreflightForExistingRaw(options.existingRawFixture);
111
+ const preflightJson = parseJsonObjectFromCommandOutput(preflight.stdout);
112
+
113
+ if (preflight.status !== 0) {
114
+ const status = 'blocked';
115
+ await emitSummary(options, {
116
+ status,
117
+ reason: 'gstack preflight failed before fixture capture',
118
+ preflight: preflightJson,
119
+ proofPacket: proofPacket({
120
+ options,
121
+ status,
122
+ preflight,
123
+ }),
124
+ commands: { preflight: commandSummary(preflight) },
125
+ });
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+
130
+ if (options.preflightOnly && options.existingRawFixture === undefined) {
131
+ const status = 'preflight-ok';
132
+ await emitSummary(options, {
133
+ status,
134
+ preflight: preflightJson,
135
+ proofPacket: proofPacket({
136
+ options,
137
+ status,
138
+ preflight,
139
+ }),
140
+ commands: { preflight: commandSummary(preflight) },
141
+ });
142
+ return;
143
+ }
144
+
145
+ const record =
146
+ options.existingRawFixture === undefined
147
+ ? runScript('record-agent-replay-fixtures.ts', [
148
+ '--provider',
149
+ provider,
150
+ '--exact-raw',
151
+ '--out-dir',
152
+ options.rawDir,
153
+ ...(options.keepWorkdirs ? ['--keep-workdirs'] : []),
154
+ ...(options.claudePermissionMode === undefined
155
+ ? []
156
+ : ['--claude-permission-mode', options.claudePermissionMode]),
157
+ ...(options.claudeModel === undefined
158
+ ? []
159
+ : ['--claude-model', options.claudeModel]),
160
+ ...(options.gstackSkillDir === undefined
161
+ ? []
162
+ : ['--gstack-skill-dir', options.gstackSkillDir]),
163
+ ...(options.gstackCli === undefined
164
+ ? []
165
+ : ['--gstack-cli', options.gstackCli]),
166
+ ])
167
+ : existingRawCaptureResult(options.existingRawFixture);
168
+
169
+ if (record.status !== 0) {
170
+ const status = 'failed';
171
+ await emitSummary(options, {
172
+ status,
173
+ reason: 'gstack raw fixture recording failed',
174
+ preflight: preflightJson,
175
+ proofPacket: proofPacket({
176
+ options,
177
+ status,
178
+ preflight,
179
+ record,
180
+ }),
181
+ commands: {
182
+ preflight: commandSummary(preflight),
183
+ record: commandSummary(record),
184
+ },
185
+ });
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+
190
+ const rawFixturePath =
191
+ options.existingRawFixture ?? join(options.rawDir, `${provider}.ndjson`);
192
+ const normalizedFixturePath = join(options.fixtureDir, `${provider}.ndjson`);
193
+ const normalizedText = normalizeAgentReplayText(
194
+ await readFile(rawFixturePath, 'utf8')
195
+ );
196
+ await mkdir(options.fixtureDir, { recursive: true });
197
+ await writeFile(normalizedFixturePath, normalizedText, 'utf8');
198
+ await writeCandidateManifest(
199
+ normalizedFixturePath,
200
+ {
201
+ providerReplay: 'pending',
202
+ backendReplay: 'pending',
203
+ browserAutomation: 'pending',
204
+ oracle: 'pending',
205
+ },
206
+ preflightJson,
207
+ options
208
+ );
209
+
210
+ const providerReplay = runScript('replay-agent-fixture.ts', [
211
+ '--fixture',
212
+ normalizedFixturePath,
213
+ '--coverage',
214
+ 'none',
215
+ '--no-html',
216
+ '--out-dir',
217
+ options.providerReplayOutDir,
218
+ ]);
219
+ const providerReplayJson = parseJsonObjectFromCommandOutput(
220
+ providerReplay.stdout
221
+ );
222
+
223
+ if (providerReplay.status === 0) {
224
+ await writeCandidateManifest(
225
+ normalizedFixturePath,
226
+ {
227
+ providerReplay: 'covered',
228
+ backendReplay: 'pending',
229
+ browserAutomation: 'pending',
230
+ oracle: 'pending',
231
+ },
232
+ preflightJson,
233
+ options
234
+ );
235
+ }
236
+
237
+ const backendReplay = backendReplayCommand(options, normalizedFixturePath);
238
+ const backendReplayJson =
239
+ backendReplay.type === 'skipped' ||
240
+ backendReplay.proofMode === 'backend-browser'
241
+ ? undefined
242
+ : parseJsonObjectFromCommandOutput(backendReplay.result.stdout);
243
+
244
+ if (
245
+ providerReplay.status === 0 &&
246
+ backendReplay.type === 'ran' &&
247
+ backendReplay.result.status === 0
248
+ ) {
249
+ await writeCandidateManifest(
250
+ normalizedFixturePath,
251
+ {
252
+ providerReplay: 'covered',
253
+ backendReplay: 'covered',
254
+ browserAutomation:
255
+ backendReplay.proofMode === 'backend-browser' ? 'covered' : 'pending',
256
+ oracle: 'covered',
257
+ },
258
+ preflightJson,
259
+ options
260
+ );
261
+ }
262
+
263
+ const status = promotionStatus(providerReplay, backendReplay);
264
+ const install = await maybeInstallCandidateFixture({
265
+ options,
266
+ status,
267
+ normalizedFixturePath,
268
+ });
269
+ await emitSummary(options, {
270
+ status,
271
+ rawFixturePath,
272
+ normalizedFixturePath,
273
+ manifestPath: agentReplayManifestPathForFixture(normalizedFixturePath),
274
+ preflight: preflightJson,
275
+ providerReplay: providerReplayJson,
276
+ backendReplay: backendReplayJson,
277
+ install,
278
+ proofPacket: proofPacket({
279
+ options,
280
+ status,
281
+ preflight,
282
+ record,
283
+ rawFixturePath,
284
+ normalizedFixturePath,
285
+ providerReplay,
286
+ backendReplay,
287
+ }),
288
+ commands: {
289
+ preflight: commandSummary(preflight),
290
+ record: commandSummary(record),
291
+ providerReplay: commandSummary(providerReplay),
292
+ ...(backendReplay.type === 'skipped'
293
+ ? { backendReplay: { status: 'skipped', reason: backendReplay.reason } }
294
+ : { backendReplay: commandSummary(backendReplay.result) }),
295
+ },
296
+ });
297
+
298
+ if (status === 'failed' || install.status === 'blocked') {
299
+ process.exitCode = 1;
300
+ }
301
+ }
302
+
303
+ async function maybeInstallCandidateFixture(args: {
304
+ readonly options: PromoteOptions;
305
+ readonly status: PromotionStatus;
306
+ readonly normalizedFixturePath: string;
307
+ }): Promise<JsonObject> {
308
+ const { options, status, normalizedFixturePath } = args;
309
+
310
+ if (options.installFixtureDir === undefined) {
311
+ return { status: 'not-requested' };
312
+ }
313
+
314
+ if (status !== 'browser-ready') {
315
+ return {
316
+ status: 'blocked',
317
+ reason: '--install-fixture-dir requires a browser-ready candidate',
318
+ targetDir: options.installFixtureDir,
319
+ };
320
+ }
321
+
322
+ const sourceManifestPath = agentReplayManifestPathForFixture(
323
+ normalizedFixturePath
324
+ );
325
+ const safetyFindings = await scanAgentReplayInstallFiles([
326
+ normalizedFixturePath,
327
+ sourceManifestPath,
328
+ ]);
329
+
330
+ if (safetyFindings.length > 0) {
331
+ return {
332
+ status: 'blocked',
333
+ reason: 'candidate fixture safety scan failed',
334
+ targetDir: options.installFixtureDir,
335
+ findings: safetyFindings,
336
+ };
337
+ }
338
+
339
+ const targetFixturePath = join(
340
+ options.installFixtureDir,
341
+ `${provider}.ndjson`
342
+ );
343
+ const targetManifestPath =
344
+ agentReplayManifestPathForFixture(targetFixturePath);
345
+ await mkdir(options.installFixtureDir, { recursive: true });
346
+ await copyFile(normalizedFixturePath, targetFixturePath);
347
+ await copyFile(sourceManifestPath, targetManifestPath);
348
+
349
+ return {
350
+ status: 'installed',
351
+ fixturePath: targetFixturePath,
352
+ manifestPath: targetManifestPath,
353
+ };
354
+ }
355
+
356
+ function skippedPreflightForExistingRaw(rawFixturePath: string): CommandResult {
357
+ return {
358
+ status: 0,
359
+ stdout: `${JSON.stringify(
360
+ {
361
+ provider,
362
+ status: 'skipped',
363
+ reason: 'using previously captured real raw fixture',
364
+ rawFixturePath,
365
+ },
366
+ null,
367
+ 2
368
+ )}
369
+ `,
370
+ stderr: '',
371
+ };
372
+ }
373
+
374
+ function existingRawCaptureResult(rawFixturePath: string): CommandResult {
375
+ return {
376
+ status: 0,
377
+ stdout: `using previously captured real raw fixture ${rawFixturePath}
378
+ `,
379
+ stderr: '',
380
+ };
381
+ }
382
+
383
+ function backendReplayCommand(
384
+ options: PromoteOptions,
385
+ fixturePath: string
386
+ ): BackendReplayPromotionResult {
387
+ if (options.skipBackend) {
388
+ return { type: 'skipped', reason: '--skip-backend was provided' };
389
+ }
390
+
391
+ if (options.localBrowserProof) {
392
+ return {
393
+ type: 'ran',
394
+ proofMode: 'backend-browser',
395
+ result: runCommand(
396
+ 'bun',
397
+ [
398
+ 'run',
399
+ 'replay-harness:agent-backend-live',
400
+ '--',
401
+ '--fixture',
402
+ fixturePath,
403
+ '--provider',
404
+ 'claude-code',
405
+ '--setup-fixture',
406
+ '--oracle-feature',
407
+ featureId,
408
+ '--out-dir',
409
+ options.browserReplayOutDir,
410
+ ...(options.speed === undefined ? [] : ['--speed', options.speed]),
411
+ ],
412
+ webRoot
413
+ ),
414
+ };
415
+ }
416
+
417
+ if (options.backendUrl === undefined) {
418
+ return {
419
+ type: 'skipped',
420
+ reason:
421
+ '--backend-url or --local-browser-proof was not provided; backend replay was not attempted',
422
+ };
423
+ }
424
+
425
+ const backendArgs = [
426
+ '--fixture',
427
+ fixturePath,
428
+ '--provider',
429
+ 'claude-code',
430
+ '--backend-url',
431
+ options.backendUrl,
432
+ '--oracle-feature',
433
+ featureId,
434
+ '--out-dir',
435
+ options.browserProof
436
+ ? options.browserReplayOutDir
437
+ : options.backendReplayOutDir,
438
+ ...(options.speed === undefined ? [] : ['--speed', options.speed]),
439
+ ...(options.setupFixture ? ['--setup-fixture'] : []),
440
+ ...(options.sessionToken === undefined
441
+ ? []
442
+ : ['--session-token', options.sessionToken]),
443
+ ...(options.workspace === undefined
444
+ ? []
445
+ : ['--workspace', options.workspace]),
446
+ ...(options.channel === undefined ? [] : ['--channel', options.channel]),
447
+ ...(options.threadId === undefined
448
+ ? []
449
+ : ['--thread-id', options.threadId]),
450
+ ];
451
+
452
+ return {
453
+ type: 'ran',
454
+ proofMode: options.browserProof ? 'backend-browser' : 'backend',
455
+ result: runScript(
456
+ options.browserProof
457
+ ? 'replay-agent-backend-browser.ts'
458
+ : 'replay-agent-backend.ts',
459
+ backendArgs
460
+ ),
461
+ };
462
+ }
463
+
464
+ function promotionStatus(
465
+ providerReplay: CommandResult,
466
+ backendReplay: BackendReplayPromotionResult
467
+ ): PromotionStatus {
468
+ if (providerReplay.status !== 0) {
469
+ return 'failed';
470
+ }
471
+
472
+ switch (backendReplay.type) {
473
+ case 'skipped':
474
+ return 'provider-ready';
475
+ case 'ran':
476
+ if (backendReplay.result.status !== 0) {
477
+ return 'failed';
478
+ }
479
+
480
+ return backendReplay.proofMode === 'backend-browser'
481
+ ? 'browser-ready'
482
+ : 'backend-ready';
483
+ }
484
+ }
485
+
486
+ function proofPacket(args: {
487
+ readonly options: PromoteOptions;
488
+ readonly status: PromotionStatus;
489
+ readonly preflight: CommandResult;
490
+ readonly record?: CommandResult | undefined;
491
+ readonly rawFixturePath?: string | undefined;
492
+ readonly normalizedFixturePath?: string | undefined;
493
+ readonly providerReplay?: CommandResult | undefined;
494
+ readonly backendReplay?: BackendReplayPromotionResult | undefined;
495
+ }): JsonObject {
496
+ const manifestPath =
497
+ args.normalizedFixturePath === undefined
498
+ ? undefined
499
+ : agentReplayManifestPathForFixture(args.normalizedFixturePath);
500
+ const backendSummaryPath =
501
+ args.backendReplay === undefined || args.backendReplay.type === 'skipped'
502
+ ? undefined
503
+ : join(
504
+ args.backendReplay.proofMode === 'backend-browser'
505
+ ? args.options.browserReplayOutDir
506
+ : args.options.backendReplayOutDir,
507
+ 'summary.json'
508
+ );
509
+
510
+ return {
511
+ featureId,
512
+ lane: provider,
513
+ status: args.status,
514
+ artifacts: {
515
+ summaryPath: join(args.options.outDir, 'summary.json'),
516
+ rawDir: args.options.rawDir,
517
+ rawFixturePath: args.rawFixturePath,
518
+ normalizedFixturePath: args.normalizedFixturePath,
519
+ manifestPath,
520
+ providerReplaySummaryPath:
521
+ args.providerReplay === undefined
522
+ ? undefined
523
+ : join(args.options.providerReplayOutDir, 'summary.json'),
524
+ backendReplaySummaryPath: backendSummaryPath,
525
+ browserReplaySummaryPath:
526
+ args.backendReplay?.type === 'ran' &&
527
+ args.backendReplay.proofMode === 'backend-browser'
528
+ ? join(args.options.browserReplayOutDir, 'summary.json')
529
+ : undefined,
530
+ },
531
+ proof: {
532
+ preflight: proofStatusForCommand(args.preflight, 'blocked'),
533
+ rawCapture: proofStatusForOptionalCommand(args.record),
534
+ normalization:
535
+ args.normalizedFixturePath === undefined ? 'pending' : 'covered',
536
+ manifest: manifestPath === undefined ? 'pending' : 'covered',
537
+ providerReplay: proofStatusForOptionalCommand(args.providerReplay),
538
+ backendReplay: proofStatusForBackendReplay(args.backendReplay),
539
+ oracle: proofStatusForBackendReplay(args.backendReplay),
540
+ frontendStorybook:
541
+ args.normalizedFixturePath === undefined ? 'pending' : 'not-applicable',
542
+ browserAutomation: proofStatusForBrowserReplay(args.backendReplay),
543
+ },
544
+ nextCommands: nextPromotionCommands(args.options, args.status),
545
+ };
546
+ }
547
+
548
+ function proofStatusForOptionalCommand(
549
+ result: CommandResult | undefined
550
+ ): string {
551
+ if (result === undefined) {
552
+ return 'pending';
553
+ }
554
+
555
+ return proofStatusForCommand(result, 'failed');
556
+ }
557
+
558
+ function proofStatusForCommand(
559
+ result: CommandResult,
560
+ failedStatus: 'blocked' | 'failed'
561
+ ): string {
562
+ return result.status === 0 ? 'covered' : failedStatus;
563
+ }
564
+
565
+ function proofStatusForBackendReplay(
566
+ result: BackendReplayPromotionResult | undefined
567
+ ): string {
568
+ if (result === undefined || result.type === 'skipped') {
569
+ return 'pending';
570
+ }
571
+
572
+ return result.result.status === 0 ? 'covered' : 'failed';
573
+ }
574
+
575
+ function proofStatusForBrowserReplay(
576
+ result: BackendReplayPromotionResult | undefined
577
+ ): string {
578
+ if (result === undefined || result.type === 'skipped') {
579
+ return 'pending';
580
+ }
581
+
582
+ if (result.proofMode !== 'backend-browser') {
583
+ return 'pending';
584
+ }
585
+
586
+ return result.result.status === 0 ? 'covered' : 'failed';
587
+ }
588
+
589
+ function promotionCommandBase(options: PromoteOptions): string {
590
+ const optionArgs = [
591
+ '--out-dir',
592
+ options.outDir,
593
+ ...(options.existingRawFixture === undefined
594
+ ? []
595
+ : ['--existing-raw-fixture', options.existingRawFixture]),
596
+ ...(options.claudeModel === undefined
597
+ ? []
598
+ : ['--claude-model', options.claudeModel]),
599
+ ...(options.claudePermissionMode === undefined
600
+ ? []
601
+ : ['--claude-permission-mode', options.claudePermissionMode]),
602
+ ...(options.speed === undefined ? [] : ['--speed', options.speed]),
603
+ ...(options.gstackSkillDir === undefined
604
+ ? []
605
+ : ['--gstack-skill-dir', options.gstackSkillDir]),
606
+ ...(options.gstackCli === undefined
607
+ ? []
608
+ : ['--gstack-cli', options.gstackCli]),
609
+ ...(options.keepWorkdirs ? ['--keep-workdirs'] : []),
610
+ ];
611
+
612
+ return `pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- ${optionArgs.map(shellArg).join(' ')}`;
613
+ }
614
+
615
+ function shellArg(value: string): string {
616
+ if (/^[A-Za-z0-9_./:=@+-]+$/u.test(value)) {
617
+ return value;
618
+ }
619
+
620
+ return `'${value.replace(/'/gu, `'"'"'`)}'`;
621
+ }
622
+
623
+ function nextPromotionCommands(
624
+ options: PromoteOptions,
625
+ status: PromotionStatus
626
+ ): readonly string[] {
627
+ const base = promotionCommandBase(options);
628
+ const preflight = `${base} --preflight-only`;
629
+ const providerOnly = `${base} --skip-backend`;
630
+ const backend =
631
+ options.backendUrl === undefined
632
+ ? `${base} --backend-url http://127.0.0.1:4140 --setup-fixture`
633
+ : `${base} --backend-url ${shellArg(options.backendUrl)}${options.setupFixture ? ' --setup-fixture' : ''}`;
634
+ const browser = `${backend} --browser-proof`;
635
+ const localBrowser = `${base} --local-browser-proof`;
636
+
637
+ switch (status) {
638
+ case 'blocked':
639
+ return [preflight, providerOnly];
640
+ case 'preflight-ok':
641
+ return [providerOnly, backend];
642
+ case 'provider-ready':
643
+ return [localBrowser, backend, browser];
644
+ case 'backend-ready':
645
+ return [localBrowser, browser];
646
+ case 'browser-ready':
647
+ return [
648
+ `pnpm --filter @linzumi/cli run replay:agent-fixture -- --fixture ${join(options.fixtureDir, `${provider}.ndjson`)} --coverage none --require-feature ${featureId} --out-dir ${join(options.outDir, 'final-readiness')}`,
649
+ ];
650
+ case 'failed':
651
+ return [preflight, providerOnly, backend];
652
+ }
653
+ }
654
+
655
+ async function writeCandidateManifest(
656
+ fixturePath: string,
657
+ proof: {
658
+ readonly providerReplay: 'covered' | 'pending';
659
+ readonly backendReplay: 'covered' | 'pending';
660
+ readonly browserAutomation: 'covered' | 'pending';
661
+ readonly oracle: 'covered' | 'pending';
662
+ },
663
+ preflight: JsonObject,
664
+ options: PromoteOptions
665
+ ): Promise<void> {
666
+ const manifestPath = agentReplayManifestPathForFixture(fixturePath);
667
+ const manifest = {
668
+ schemaVersion: 1,
669
+ fixtureId: 'gstack-claude-smoke',
670
+ lane: 'gstack-claude-smoke',
671
+ provider: 'claude-code-sdk',
672
+ fixturePath: 'gstack-claude-smoke.ndjson',
673
+ capture: {
674
+ promptId: 'agent-fixture-playground/gstack-claude-smoke-prompt',
675
+ runtimeProfile: 'gstack-claude-code',
676
+ source: 'real Claude Code SDK capture with user-installed gstack',
677
+ ...(options.claudeModel === undefined
678
+ ? {}
679
+ : { model: options.claudeModel }),
680
+ ...candidateManifestPreflightCapture(preflight),
681
+ },
682
+ features: [
683
+ {
684
+ featureId,
685
+ title: 'Gstack skill discovery and local review smoke',
686
+ autonomyStatus:
687
+ proof.backendReplay === 'covered' &&
688
+ proof.oracle === 'covered' &&
689
+ proof.browserAutomation === 'covered'
690
+ ? 'ready-to-implement'
691
+ : 'needs-oracle',
692
+ expectedProof: {
693
+ realFixture: 'covered',
694
+ providerReplay: proof.providerReplay,
695
+ backendReplay: proof.backendReplay,
696
+ frontendStorybook: 'not-applicable',
697
+ browserAutomation: proof.browserAutomation,
698
+ oracle: proof.oracle,
699
+ },
700
+ notes:
701
+ 'Candidate manifest generated by promote:gstack-claude-smoke-fixture. The gstack smoke lane uses real backend/browser proof, so frontendStorybook is not applicable for this candidate. Keep pending replay/oracle rows until the matching proof command has passed for this exact fixture.',
702
+ },
703
+ ],
704
+ };
705
+
706
+ await mkdir(dirname(manifestPath), { recursive: true });
707
+ await writeFile(
708
+ manifestPath,
709
+ `${JSON.stringify(manifest, null, 2)}\n`,
710
+ 'utf8'
711
+ );
712
+ }
713
+
714
+ function candidateManifestPreflightCapture(preflight: JsonObject): JsonObject {
715
+ const setup = preflight.setup;
716
+
717
+ if (!isJsonObject(setup)) {
718
+ return {};
719
+ }
720
+
721
+ return {
722
+ gstackSkillDir:
723
+ typeof setup.skillDir === 'string' ? setup.skillDir : undefined,
724
+ gstackCliPath:
725
+ typeof setup.cliPath === 'string' ? setup.cliPath : undefined,
726
+ checkedSkillDirs: Array.isArray(setup.checkedSkillDirs)
727
+ ? setup.checkedSkillDirs
728
+ : undefined,
729
+ checkedCliNames: Array.isArray(setup.checkedCliNames)
730
+ ? setup.checkedCliNames
731
+ : undefined,
732
+ includeHookEvents: setup.includeHookEvents,
733
+ preservesClaudeTasks: setup.preservesClaudeTasks,
734
+ };
735
+ }
736
+
737
+ async function emitSummary(
738
+ options: PromoteOptions,
739
+ summary: JsonObject
740
+ ): Promise<void> {
741
+ await mkdir(options.outDir, { recursive: true });
742
+ await writeFile(
743
+ join(options.outDir, 'summary.json'),
744
+ `${JSON.stringify(summary, null, 2)}\n`,
745
+ 'utf8'
746
+ );
747
+ process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
748
+ }
749
+
750
+ function runScript(scriptName: string, args: readonly string[]): CommandResult {
751
+ const result = spawnSync(
752
+ process.execPath,
753
+ ['--import', tsxLoader, join('scripts', scriptName), ...args],
754
+ {
755
+ cwd: packageRoot,
756
+ env: process.env,
757
+ encoding: 'utf8',
758
+ maxBuffer: 1024 * 1024 * 20,
759
+ }
760
+ );
761
+
762
+ return {
763
+ status: result.status ?? 1,
764
+ stdout: result.stdout,
765
+ stderr: result.stderr,
766
+ };
767
+ }
768
+
769
+ function runCommand(
770
+ command: string,
771
+ args: readonly string[],
772
+ cwd: string
773
+ ): CommandResult {
774
+ const result = spawnSync(command, args, {
775
+ cwd,
776
+ env: process.env,
777
+ encoding: 'utf8',
778
+ maxBuffer: 1024 * 1024 * 20,
779
+ });
780
+
781
+ return {
782
+ status: result.status ?? 1,
783
+ stdout: result.stdout,
784
+ stderr: result.stderr,
785
+ };
786
+ }
787
+
788
+ function commandSummary(result: CommandResult): JsonObject {
789
+ return {
790
+ status: result.status,
791
+ stdout: result.stdout.slice(0, 20_000),
792
+ stderr: result.stderr.slice(0, 20_000),
793
+ };
794
+ }
795
+
796
+ function parseJsonObjectFromCommandOutput(output: string): JsonObject {
797
+ const start = output.indexOf('{');
798
+ const end = output.lastIndexOf('}');
799
+
800
+ if (start === -1 || end === -1 || end < start) {
801
+ throw new Error(`command output did not contain a JSON object: ${output}`);
802
+ }
803
+
804
+ const parsed: unknown = JSON.parse(output.slice(start, end + 1));
805
+
806
+ if (!isJsonObject(parsed)) {
807
+ throw new Error('command output JSON was not an object');
808
+ }
809
+
810
+ return parsed;
811
+ }
812
+
813
+ function parseArgs(args: readonly string[]): PromoteOptions {
814
+ const outDir = resolve(
815
+ argValue(args, '--out-dir') ??
816
+ join(tmpdir(), `linzumi-gstack-claude-smoke-promotion-${Date.now()}`)
817
+ );
818
+
819
+ return {
820
+ outDir,
821
+ rawDir: resolve(argValue(args, '--raw-dir') ?? join(outDir, 'raw')),
822
+ existingRawFixture:
823
+ argValue(args, '--existing-raw-fixture') === undefined
824
+ ? undefined
825
+ : resolve(argValue(args, '--existing-raw-fixture') ?? ''),
826
+ fixtureDir: resolve(
827
+ argValue(args, '--fixture-dir') ?? join(outDir, 'fixtures')
828
+ ),
829
+ providerReplayOutDir: resolve(
830
+ argValue(args, '--provider-replay-out-dir') ??
831
+ join(outDir, 'provider-replay')
832
+ ),
833
+ backendReplayOutDir: resolve(
834
+ argValue(args, '--backend-replay-out-dir') ??
835
+ join(outDir, 'backend-replay')
836
+ ),
837
+ browserReplayOutDir: resolve(
838
+ argValue(args, '--browser-replay-out-dir') ??
839
+ join(outDir, 'backend-browser')
840
+ ),
841
+ installFixtureDir:
842
+ argValue(args, '--install-fixture-dir') === undefined
843
+ ? undefined
844
+ : resolve(argValue(args, '--install-fixture-dir') ?? ''),
845
+ speed: argValue(args, '--speed'),
846
+ preflightOnly: args.includes('--preflight-only'),
847
+ keepWorkdirs: args.includes('--keep-workdirs'),
848
+ claudePermissionMode: argValue(args, '--claude-permission-mode'),
849
+ claudeModel: argValue(args, '--claude-model'),
850
+ gstackSkillDir: argValue(args, '--gstack-skill-dir'),
851
+ gstackCli: argValue(args, '--gstack-cli'),
852
+ skipBackend: args.includes('--skip-backend'),
853
+ browserProof: args.includes('--browser-proof'),
854
+ localBrowserProof: args.includes('--local-browser-proof'),
855
+ backendUrl: argValue(args, '--backend-url'),
856
+ setupFixture: args.includes('--setup-fixture'),
857
+ sessionToken: argValue(args, '--session-token'),
858
+ workspace: argValue(args, '--workspace'),
859
+ channel: argValue(args, '--channel'),
860
+ threadId: argValue(args, '--thread-id'),
861
+ };
862
+ }
863
+
864
+ function argValue(args: readonly string[], name: string): string | undefined {
865
+ const index = args.indexOf(name);
866
+
867
+ if (index === -1) {
868
+ return undefined;
869
+ }
870
+
871
+ const value = args[index + 1];
872
+
873
+ if (value === undefined || value.startsWith('--')) {
874
+ throw new Error(`${name} requires a value`);
875
+ }
876
+
877
+ return value;
878
+ }
879
+
880
+ function printHelp(): void {
881
+ process.stdout.write(
882
+ `Usage: pnpm run promote:gstack-claude-smoke-fixture -- [options]\n\nOptions:\n --preflight-only Only check local gstack readiness and write summary.json\n --out-dir <path> Promotion artifact root. Default: /tmp/linzumi-gstack-claude-smoke-promotion-*\n --raw-dir <path> Exact raw capture directory. Default: <out-dir>/raw\n --existing-raw-fixture <path> Reuse a previously captured real raw NDJSON fixture without starting Claude\n --fixture-dir <path> Normalized candidate fixture directory. Default: <out-dir>/fixtures\n --provider-replay-out-dir <path> Provider replay artifact directory. Default: <out-dir>/provider-replay\n --backend-replay-out-dir <path> Backend replay artifact directory. Default: <out-dir>/backend-replay\n --browser-replay-out-dir <path> Backend-browser replay artifact directory. Default: <out-dir>/backend-browser\n --speed immediate|realtime|<n>x|<n>ms\n Forwarded to backend replay proof commands. Default: immediate\n --install-fixture-dir <path> Copy browser-ready candidate fixture and manifest into this directory\n --keep-workdirs Forwarded to record:agent-fixtures\n --claude-permission-mode <value> Forwarded to record:agent-fixtures\n --claude-model <model> Forwarded to record:agent-fixtures for cheap-model fixture rerolls\n --gstack-skill-dir <path> Forwarded to record:agent-fixtures for explicit preflight setup\n --gstack-cli <path> Forwarded to record:agent-fixtures for explicit preflight setup\n --skip-backend Do not attempt backend replay after provider replay\n --browser-proof Run backend replay plus browser proof through replay:agent-backend-browser\n --local-browser-proof Boot an isolated local backend, then run backend plus browser proof\n --backend-url <url> If provided, run replay:agent-backend with --oracle-feature gstack-claude-smoke\n --setup-fixture Forwarded to replay:agent-backend\n --session-token <token> Forwarded to replay:agent-backend\n --workspace <slug> Forwarded to replay:agent-backend\n --channel <slug> Forwarded to replay:agent-backend\n --thread-id <id> Forwarded to replay:agent-backend\n\nExamples:\n pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- --preflight-only\n pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- --skip-backend --claude-permission-mode bypassPermissions --claude-model claude-haiku-4-5-20251001\n pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- --existing-raw-fixture /tmp/gstack-raw/gstack-claude-smoke.ndjson --skip-backend\n pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- --backend-url http://127.0.0.1:4140 --setup-fixture\n pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- --backend-url http://127.0.0.1:4140 --setup-fixture --browser-proof\n pnpm --filter @linzumi/cli run promote:gstack-claude-smoke-fixture -- --local-browser-proof --speed 2x --install-fixture-dir packages/linzumi-cli/test/fixtures/agent-raw-replay/gstack\n`
883
+ );
884
+ }
885
+
886
+ main().catch((error) => {
887
+ process.stderr.write(
888
+ `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
889
+ );
890
+ process.exitCode = 1;
891
+ });