@linzumi/cli 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,13 @@
4
4
  Relationship: Automatic raw fixture capture entrypoint. It drives real Codex
5
5
  app-server and Claude Code SDK sessions against the prepared fixture
6
6
  playground and lets the existing live recorder write provider-native NDJSON.
7
+
8
+ - Date: 2026-07-02
9
+ Spec: plans/2026-07-02-claude-plan-mode-observability-v2-implementation-plan.md
10
+ Relationship: Adds the claude-plan-mode capture lane - a real Claude Code
11
+ session started in permissionMode 'plan' with hook events on, auto-approving
12
+ the ExitPlanMode plan review so the fixture records the full observable
13
+ plan-mode lifecycle (status permissionMode frames, ExitPlanMode call/result).
7
14
  */
8
15
  import { mkdtemp, cp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
9
16
  import { tmpdir } from 'node:os';
@@ -21,7 +28,7 @@ import { recordLiveAgentReplay } from '../src/agentReplayLiveRecorder';
21
28
  import { type JsonObject, type JsonValue } from '../src/protocol';
22
29
  import { preflightGstackClaudeSmokeSetup } from '../src/gstackReplayPreflight';
23
30
 
24
- type Provider = 'codex' | 'claude' | 'gstack-claude-smoke';
31
+ type Provider = 'codex' | 'claude' | 'gstack-claude-smoke' | 'claude-plan-mode';
25
32
 
26
33
  type CaptureOptions = {
27
34
  readonly providers: readonly Provider[];
@@ -122,6 +129,9 @@ async function main(): Promise<void> {
122
129
  case 'gstack-claude-smoke':
123
130
  await runClaudeCapture(options, runDir, prompt, 'gstack');
124
131
  break;
132
+ case 'claude-plan-mode':
133
+ await runClaudeCapture(options, runDir, prompt, 'plan-mode');
134
+ break;
125
135
  }
126
136
 
127
137
  recordLiveAgentReplay({
@@ -211,6 +221,8 @@ function providersFromArg(value: string): readonly Provider[] {
211
221
  return ['claude'];
212
222
  case 'gstack-claude-smoke':
213
223
  return ['gstack-claude-smoke'];
224
+ case 'claude-plan-mode':
225
+ return ['claude-plan-mode'];
214
226
  default:
215
227
  throw new Error(`unsupported --provider value: ${value}`);
216
228
  }
@@ -272,6 +284,8 @@ function fixturePromptHeading(provider: Provider): string {
272
284
  return 'Claude Code Prompt';
273
285
  case 'gstack-claude-smoke':
274
286
  return 'Gstack Claude Smoke Prompt';
287
+ case 'claude-plan-mode':
288
+ return 'Claude Plan Mode Prompt';
275
289
  }
276
290
  }
277
291
 
@@ -283,6 +297,7 @@ async function preflightCaptureProvider(
283
297
  switch (provider) {
284
298
  case 'codex':
285
299
  case 'claude':
300
+ case 'claude-plan-mode':
286
301
  return { type: 'none' };
287
302
  case 'gstack-claude-smoke':
288
303
  return {
@@ -520,7 +535,7 @@ async function runClaudeCapture(
520
535
  options: CaptureOptions,
521
536
  cwd: string,
522
537
  prompt: string,
523
- profile: 'generic' | 'gstack'
538
+ profile: 'generic' | 'gstack' | 'plan-mode'
524
539
  ): Promise<void> {
525
540
  const stream = claudeQuery({
526
541
  prompt,
@@ -535,9 +550,24 @@ async function runClaudeCapture(
535
550
  ...(profile === 'gstack'
536
551
  ? { includeHookEvents: true, env: gstackClaudeCaptureEnv(process.env) }
537
552
  : {}),
553
+ // Plan-mode lane: start IN plan mode so the fixture records the status
554
+ // permissionMode frames and the ExitPlanMode call/result lifecycle. The
555
+ // canUseTool bridge auto-approves everything (the playground is a
556
+ // throwaway temp copy and plan mode already blocks writes), so the plan
557
+ // review resolves deterministically without a human.
558
+ ...(profile === 'plan-mode'
559
+ ? {
560
+ permissionMode: 'plan' as PermissionMode,
561
+ includeHookEvents: true,
562
+ canUseTool: async (
563
+ _toolName: string,
564
+ input: Record<string, unknown>
565
+ ) => ({ behavior: 'allow' as const, updatedInput: input }),
566
+ }
567
+ : {}),
538
568
  settingSources: ['user', 'project', 'local'],
539
569
  systemPrompt: { type: 'preset', preset: 'claude_code' },
540
- ...(options.claudePermissionMode === undefined
570
+ ...(options.claudePermissionMode === undefined || profile === 'plan-mode'
541
571
  ? {}
542
572
  : {
543
573
  permissionMode: options.claudePermissionMode,
@@ -490,6 +490,12 @@ async function registerReplayFixtureUser(
490
490
  body: JSON.stringify({
491
491
  display_name: username,
492
492
  e2e_fixture: true,
493
+ // updateViewerFeatureFlag requires a developer user (#2149 gate); the
494
+ // fixture-registration path marks the throwaway user via this param
495
+ // (auth_controller maybe_mark_e2e_fixture_developer), which is only
496
+ // honored when KANDAN_E2E_FIXTURE_REGISTRATION enabled the stub-mode
497
+ // dev backend.
498
+ e2e_fixture_developer: true,
493
499
  email: `${username}@example.test`,
494
500
  password: `pw-${username}`,
495
501
  username,
@@ -892,10 +898,11 @@ async function evaluateBackendReplayOracles(
892
898
  const output =
893
899
  processedState === undefined
894
900
  ? { sourceSeq: undefined, items: [] as BackendReplayThreadOutputItem[] }
895
- : await queryBackendReplayThreadOutput(
901
+ : await queryBackendReplayThreadOutputSettled(
896
902
  options,
897
903
  start.threadId,
898
- processedState.seq
904
+ processedState.seq,
905
+ provider
899
906
  );
900
907
  checks.push({
901
908
  id: 'thread-output-present',
@@ -963,6 +970,7 @@ async function evaluateBackendReplayOracles(
963
970
  featureId: options.oracleFeatureId ?? options.requiredFeatureId,
964
971
  provider,
965
972
  outputItems: output.items,
973
+ requireLane: options.oracleFeatureId !== undefined,
966
974
  })
967
975
  );
968
976
 
@@ -1045,6 +1053,60 @@ async function queryBackendReplayThreadOutput(
1045
1053
  };
1046
1054
  }
1047
1055
 
1056
+ // The terminal message state can land before the async row projection
1057
+ // finishes, so a single post-terminal query races the projection (observed:
1058
+ // the same fixture produced different row subsets run to run). Poll until the
1059
+ // projected output is quiescent - and, when a feature oracle is requested,
1060
+ // until its checks pass - before snapshotting.
1061
+ async function queryBackendReplayThreadOutputSettled(
1062
+ options: BackendReplayOptions,
1063
+ threadId: string,
1064
+ sourceSeq: number,
1065
+ provider: 'codex' | 'claude-code'
1066
+ ): Promise<{
1067
+ readonly sourceSeq: number;
1068
+ readonly items: readonly BackendReplayThreadOutputItem[];
1069
+ }> {
1070
+ const timeoutMs = 30_000;
1071
+ const pollIntervalMs = 250;
1072
+ const startedAtMs = Date.now();
1073
+ const featureId = options.oracleFeatureId ?? options.requiredFeatureId;
1074
+ let last = await queryBackendReplayThreadOutput(options, threadId, sourceSeq);
1075
+ // Content-keyed stability: a row can flip streaming -> completed (or grow
1076
+ // fields) without changing the count, so count-only comparison could
1077
+ // snapshot mid-stream rows as settled.
1078
+ let lastFingerprint = JSON.stringify(last.items);
1079
+ let stableRounds = 0;
1080
+
1081
+ while (Date.now() - startedAtMs < timeoutMs) {
1082
+ const featureSatisfied =
1083
+ featureId === undefined ||
1084
+ featureBackendReplayOracleChecks({
1085
+ featureId,
1086
+ provider,
1087
+ outputItems: last.items,
1088
+ requireLane: options.oracleFeatureId !== undefined,
1089
+ }).every((check) => check.status === 'passed');
1090
+
1091
+ if (featureSatisfied && stableRounds >= 2) {
1092
+ return last;
1093
+ }
1094
+
1095
+ await new Promise((resolvePoll) => setTimeout(resolvePoll, pollIntervalMs));
1096
+ const next = await queryBackendReplayThreadOutput(
1097
+ options,
1098
+ threadId,
1099
+ sourceSeq
1100
+ );
1101
+ const nextFingerprint = JSON.stringify(next.items);
1102
+ stableRounds = nextFingerprint === lastFingerprint ? stableRounds + 1 : 0;
1103
+ lastFingerprint = nextFingerprint;
1104
+ last = next;
1105
+ }
1106
+
1107
+ return last;
1108
+ }
1109
+
1048
1110
  function backendReplayGraphqlVariables(
1049
1111
  options: BackendReplayOptions,
1050
1112
  threadId: string
@@ -115,6 +115,7 @@ function combineDiagnostics(
115
115
  ): ReturnType<typeof collectAgentReplayDiagnostics> {
116
116
  return {
117
117
  droppedEvents: diagnostics.flatMap((entry) => entry.droppedEvents),
118
+ debugOnlyEvents: diagnostics.flatMap((entry) => entry.debugOnlyEvents),
118
119
  };
119
120
  }
120
121