@linzumi/cli 1.0.22 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +319 -317
- package/package.json +1 -1
- package/scripts/record-agent-replay-fixtures.ts +33 -3
- package/scripts/replay-agent-backend.ts +64 -2
- package/scripts/replay-agent-fixture.ts +1 -0
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|