@letta-ai/letta-code 0.30.26 → 0.30.28

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 (55) hide show
  1. package/dist/agent-presets.js +17 -17
  2. package/dist/agent-presets.js.map +1 -1
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/turn-recovery-policy.d.ts +33 -0
  6. package/dist/types/agent/turn-recovery-policy.d.ts.map +1 -1
  7. package/dist/types/tools/impl/apply-patch.d.ts.map +1 -1
  8. package/dist/types/tools/secret-substitution.d.ts.map +1 -1
  9. package/dist/types/types/loop-status-protocol.d.ts +17 -0
  10. package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
  11. package/dist/types/types/protocol_v2.d.ts +2 -19
  12. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  13. package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
  14. package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
  15. package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
  16. package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
  17. package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
  18. package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
  19. package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
  20. package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
  21. package/dist/types/websocket/listener/types.d.ts +4 -0
  22. package/dist/types/websocket/listener/types.d.ts.map +1 -1
  23. package/letta.js +545 -120
  24. package/package.json +1 -1
  25. package/scripts/claude-watch/agent-watch.ts +622 -0
  26. package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
  27. package/scripts/claude-watch/docs-snapshot.ts +672 -0
  28. package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
  29. package/scripts/claude-watch/github.ts +137 -0
  30. package/scripts/claude-watch/release-analysis.test.ts +235 -0
  31. package/scripts/claude-watch/release-analysis.ts +297 -0
  32. package/scripts/claude-watch/release-source.test.ts +179 -0
  33. package/scripts/claude-watch/release-source.ts +369 -0
  34. package/scripts/claude-watch/runtime-observations.ts +98 -0
  35. package/scripts/claude-watch/runtime-probe.test.ts +576 -0
  36. package/scripts/claude-watch/runtime-probe.ts +911 -0
  37. package/scripts/claude-watch/runtime-sandbox.ts +170 -0
  38. package/scripts/claude-watch/state-branch.test.ts +211 -0
  39. package/scripts/claude-watch/state-branch.ts +316 -0
  40. package/scripts/claude-watch/tracker.test.ts +148 -0
  41. package/scripts/claude-watch/tracker.ts +325 -0
  42. package/scripts/claude-watch/types.ts +186 -0
  43. package/scripts/claude-watch/update-tracker.ts +201 -0
  44. package/scripts/codex-watch/agent-watch.ts +2 -2
  45. package/scripts/codex-watch/release-analysis.ts +14 -2
  46. package/scripts/codex-watch/tracker.ts +1 -3
  47. package/scripts/run-unit-tests.cjs +2 -0
  48. package/scripts/source-file-size-baseline.json +6 -5
  49. package/skills/creating-mods/references/commands.md +1 -1
  50. package/skills/creating-mods/references/ui.md +1 -1
  51. package/skills/customizing-commands/SKILL.md +1 -1
  52. package/skills/initializing-memory/SKILL.md +7 -7
  53. package/skills/self-configuration/SKILL.md +4 -4
  54. package/scripts/codex-watch/check-release.ts +0 -128
  55. package/scripts/codex-watch/render-issue.ts +0 -273
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letta-ai/letta-code",
3
- "version": "0.30.26",
3
+ "version": "0.30.28",
4
4
  "description": "Letta Code is a CLI tool for interacting with stateful Letta agents from the terminal.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.10",
@@ -0,0 +1,622 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { mkdirSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import {
7
+ type CapturedDocs,
8
+ captureDocsSnapshot,
9
+ sha256,
10
+ } from "./docs-snapshot.ts";
11
+ import {
12
+ createIssue,
13
+ editIssueBody,
14
+ ensureLabel,
15
+ findIssueByExactTitle,
16
+ getIssueBody,
17
+ } from "./github.ts";
18
+ import {
19
+ analyzeClaudeCandidate,
20
+ buildClaudeStateSnapshot,
21
+ rebuildClaudeAnalysisFromState,
22
+ } from "./release-analysis.ts";
23
+ import {
24
+ fetchClaudeGitHubReleases,
25
+ fetchClaudeNpmMetadata,
26
+ selectClaudeReleaseCandidate,
27
+ } from "./release-source.ts";
28
+ import {
29
+ captureClaudeRuntime,
30
+ createClaudeRuntimeCommandPlan,
31
+ isClaudeProbeContractCurrent,
32
+ } from "./runtime-probe.ts";
33
+ import {
34
+ fetchStateBranchTip,
35
+ finalizeStateBranch,
36
+ loadStateFilesAtCommit,
37
+ loadStateSnapshotAtCommit,
38
+ } from "./state-branch.ts";
39
+ import {
40
+ type ClaudeTrackerState,
41
+ findTrackerEntry,
42
+ hasProcessedCandidate,
43
+ isTerminalOutcome,
44
+ parseTrackerState,
45
+ recordAnalysis,
46
+ renderTrackerBody,
47
+ } from "./tracker.ts";
48
+ import {
49
+ CLAUDE_WATCH_STATE_SCHEMA_VERSION,
50
+ type ClaudeReleaseCandidate,
51
+ type ClaudeRuntimeSnapshot,
52
+ type ClaudeWatchAnalysis,
53
+ type ClaudeWatchOutcome,
54
+ type ClaudeWatchStateSnapshot,
55
+ } from "./types.ts";
56
+
57
+ const DEFAULT_REPO = "letta-ai/letta-code";
58
+ const TRACKER_TITLE = "Claude upstream drift tracker";
59
+ const TRACKER_LABEL = "claude-watch";
60
+
61
+ interface Args {
62
+ repo: string;
63
+ analysisFile: string;
64
+ previousVersion: string | null;
65
+ currentVersion: string | null;
66
+ forceDocsScan: boolean;
67
+ dryRun: boolean;
68
+ validationOnly: boolean;
69
+ rebuildStateCommit: string | null;
70
+ }
71
+
72
+ interface TrackerContext {
73
+ number: number;
74
+ url: string;
75
+ state: ClaudeTrackerState;
76
+ }
77
+
78
+ export function parseArgs(argv: string[]): Args {
79
+ const args: Args = {
80
+ repo: DEFAULT_REPO,
81
+ analysisFile: join(tmpdir(), "claude-watch-analysis.json"),
82
+ previousVersion: null,
83
+ currentVersion: null,
84
+ forceDocsScan: false,
85
+ dryRun: false,
86
+ validationOnly: false,
87
+ rebuildStateCommit: null,
88
+ };
89
+ for (let index = 0; index < argv.length; index += 1) {
90
+ const argument = argv[index];
91
+ if (argument === "--repo") args.repo = argv[++index] ?? args.repo;
92
+ else if (argument === "--analysis-file")
93
+ args.analysisFile = argv[++index] ?? args.analysisFile;
94
+ else if (argument === "--previous-version")
95
+ args.previousVersion = argv[++index] ?? null;
96
+ else if (argument === "--current-version")
97
+ args.currentVersion = argv[++index] ?? null;
98
+ else if (argument === "--force-docs-scan") args.forceDocsScan = true;
99
+ else if (argument === "--dry-run") args.dryRun = true;
100
+ else if (argument === "--validation-only") args.validationOnly = true;
101
+ else if (argument === "--rebuild-state-commit")
102
+ args.rebuildStateCommit = argv[++index] ?? null;
103
+ else throw new Error(`Unknown argument: ${argument}`);
104
+ }
105
+ return args;
106
+ }
107
+
108
+ function workflowRunUrl(): string {
109
+ const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
110
+ const repo = process.env.GITHUB_REPOSITORY ?? DEFAULT_REPO;
111
+ const run = process.env.GITHUB_RUN_ID;
112
+ return run ? `${server}/${repo}/actions/runs/${run}` : "local-dry-run";
113
+ }
114
+
115
+ function setOutput(name: string, value: string | number | boolean): void {
116
+ const output = process.env.GITHUB_OUTPUT;
117
+ if (output)
118
+ writeFileSync(output, `${name}=${String(value)}\n`, { flag: "a" });
119
+ else console.log(`${name}=${String(value)}`);
120
+ }
121
+
122
+ function writeAnalysis(path: string, analysis: ClaudeWatchAnalysis): void {
123
+ mkdirSync(resolve(path, ".."), { recursive: true });
124
+ writeFileSync(path, `${JSON.stringify(analysis, null, 2)}\n`);
125
+ }
126
+
127
+ function ensureTracker(repo: string, dryRun: boolean): TrackerContext {
128
+ const existing = findIssueByExactTitle(repo, TRACKER_TITLE);
129
+ if (existing) {
130
+ return {
131
+ number: existing.number,
132
+ url: existing.url,
133
+ state: parseTrackerState(existing.body),
134
+ };
135
+ }
136
+ if (dryRun)
137
+ return { number: 0, url: "dry-run", state: parseTrackerState("") };
138
+ ensureLabel(repo, TRACKER_LABEL);
139
+ const emptyBody = renderTrackerBody(parseTrackerState(""));
140
+ const created = createIssue(repo, TRACKER_TITLE, emptyBody, TRACKER_LABEL);
141
+ return {
142
+ number: created.number,
143
+ url: created.url,
144
+ state: parseTrackerState(created.body),
145
+ };
146
+ }
147
+
148
+ function recordTrackerOutcome(
149
+ repo: string,
150
+ tracker: TrackerContext,
151
+ analysis: ClaudeWatchAnalysis,
152
+ outcome: ClaudeWatchOutcome,
153
+ notes: string,
154
+ stateCommitSha: string | null,
155
+ error: string | null = null,
156
+ ): TrackerContext {
157
+ const current = parseTrackerState(getIssueBody(repo, tracker.number));
158
+ const next = recordAnalysis(current, {
159
+ analysis,
160
+ outcome,
161
+ notes,
162
+ stateCommitSha,
163
+ error,
164
+ });
165
+ editIssueBody(repo, tracker.number, renderTrackerBody(next));
166
+ return { ...tracker, state: next };
167
+ }
168
+
169
+ function trackerOutputs(tracker: TrackerContext): void {
170
+ setOutput("tracker_issue", tracker.number);
171
+ setOutput("tracker_issue_url", tracker.url);
172
+ }
173
+
174
+ function analysisOutputs(
175
+ analysis: ClaudeWatchAnalysis,
176
+ shouldRunAgent: boolean,
177
+ stateCommitSha: string | null,
178
+ ): void {
179
+ setOutput("should_run_agent", shouldRunAgent);
180
+ setOutput("candidate_id", analysis.candidate_id);
181
+ setOutput("previous_version", analysis.previous_version ?? "");
182
+ setOutput("current_version", analysis.current_version);
183
+ setOutput("verdict", analysis.verdict);
184
+ setOutput("state_commit_sha", stateCommitSha ?? "");
185
+ }
186
+
187
+ function stateSources(repoPath: string, commitSha: string | null) {
188
+ if (!commitSha) return {};
189
+ return Object.fromEntries(
190
+ Object.entries(loadStateFilesAtCommit(repoPath, commitSha)).filter(
191
+ ([path]) => path.startsWith("sources/"),
192
+ ),
193
+ );
194
+ }
195
+
196
+ function releaseForState(
197
+ snapshot: ClaudeWatchStateSnapshot,
198
+ ): ClaudeReleaseCandidate {
199
+ return {
200
+ version: snapshot.package_version,
201
+ published_at: snapshot.npm_published_at,
202
+ integrity: snapshot.npm_integrity,
203
+ tarball_url: "",
204
+ release_url: snapshot.release_url,
205
+ release_notes_md: snapshot.release_notes_md,
206
+ release_published_at: snapshot.release_published_at,
207
+ dist_tags: snapshot.dist_tags,
208
+ };
209
+ }
210
+
211
+ async function selectCandidate(
212
+ state: ClaudeWatchStateSnapshot | null,
213
+ args: Args,
214
+ ): Promise<ClaudeReleaseCandidate> {
215
+ const [githubReleases, npmMetadata] = await Promise.all([
216
+ fetchClaudeGitHubReleases(),
217
+ fetchClaudeNpmMetadata(),
218
+ ]);
219
+ const selected = selectClaudeReleaseCandidate({
220
+ githubReleases,
221
+ npmMetadata,
222
+ previousVersion: args.previousVersion ?? state?.package_version ?? null,
223
+ currentVersion: args.currentVersion,
224
+ allowNpmOnlyExactVersions: args.validationOnly,
225
+ });
226
+ if (selected) return selected;
227
+ const latest = selectClaudeReleaseCandidate({
228
+ githubReleases,
229
+ npmMetadata,
230
+ currentVersion: npmMetadata.dist_tags.latest,
231
+ });
232
+ if (!latest) throw new Error("Claude latest release could not be selected");
233
+ return latest;
234
+ }
235
+
236
+ async function captureRuntimePair(
237
+ candidate: ClaudeReleaseCandidate,
238
+ previous: ClaudeWatchStateSnapshot | null,
239
+ args: Args,
240
+ runBehaviorProbes: boolean,
241
+ ): Promise<{
242
+ previousRuntime: ClaudeRuntimeSnapshot | null;
243
+ currentRuntime: ClaudeRuntimeSnapshot | null;
244
+ }> {
245
+ const previousVersion =
246
+ args.previousVersion ?? previous?.package_version ?? null;
247
+ const packageChanged = previousVersion !== candidate.version;
248
+ if (args.dryRun) {
249
+ const root = join(tmpdir(), "claude-watch-dry-run");
250
+ const plans = [
251
+ ...(packageChanged && previousVersion
252
+ ? [
253
+ createClaudeRuntimeCommandPlan(
254
+ previousVersion,
255
+ join(root, "previous"),
256
+ ),
257
+ ]
258
+ : []),
259
+ ...(packageChanged || !previous?.runtime
260
+ ? [
261
+ createClaudeRuntimeCommandPlan(
262
+ candidate.version,
263
+ join(root, "current"),
264
+ ),
265
+ ]
266
+ : []),
267
+ ];
268
+ console.log(
269
+ JSON.stringify(
270
+ {
271
+ dry_run_runtime_commands: plans.map((plan) => ({
272
+ package: plan.packageSpec,
273
+ install: [plan.install.command, ...plan.install.args],
274
+ public: [
275
+ [plan.version.command, ...plan.version.args],
276
+ [plan.help.command, ...plan.help.args],
277
+ [plan.doctor.command, ...plan.doctor.args],
278
+ [plan.autoModeDefaults.command, ...plan.autoModeDefaults.args],
279
+ ],
280
+ authenticated: [
281
+ [plan.init.command, ...plan.init.args],
282
+ ...(runBehaviorProbes
283
+ ? plan.probes.map(({ command }) => [
284
+ command.command,
285
+ ...command.args,
286
+ ])
287
+ : []),
288
+ ],
289
+ })),
290
+ },
291
+ null,
292
+ 2,
293
+ ),
294
+ );
295
+ return {
296
+ previousRuntime: previous?.runtime ?? null,
297
+ currentRuntime: previous?.runtime ?? null,
298
+ };
299
+ }
300
+ if (!packageChanged) {
301
+ const currentRuntime = await captureClaudeRuntime({
302
+ version: candidate.version,
303
+ tempDir: tmpdir(),
304
+ requireAuth: true,
305
+ reuseProbes: runBehaviorProbes ? undefined : previous?.runtime?.probes,
306
+ });
307
+ return { previousRuntime: previous?.runtime ?? null, currentRuntime };
308
+ }
309
+ const [previousRuntime, currentRuntime] = await Promise.all([
310
+ previousVersion
311
+ ? captureClaudeRuntime({
312
+ version: previousVersion,
313
+ tempDir: tmpdir(),
314
+ requireAuth: true,
315
+ })
316
+ : Promise.resolve(null),
317
+ captureClaudeRuntime({
318
+ version: candidate.version,
319
+ tempDir: tmpdir(),
320
+ requireAuth: true,
321
+ }),
322
+ ]);
323
+ return { previousRuntime, currentRuntime };
324
+ }
325
+
326
+ function buildErrorAnalysis(
327
+ error: unknown,
328
+ state: ClaudeWatchStateSnapshot | null,
329
+ ): ClaudeWatchAnalysis {
330
+ const message = error instanceof Error ? error.message : String(error);
331
+ const candidate = state
332
+ ? releaseForState(state)
333
+ : {
334
+ version: "unknown",
335
+ published_at: new Date(0).toISOString(),
336
+ integrity: "unknown",
337
+ tarball_url: "",
338
+ release_url: "https://github.com/anthropics/claude-code/releases",
339
+ release_notes_md: "",
340
+ release_published_at: new Date(0).toISOString(),
341
+ dist_tags: { latest: "unknown", stable: null, next: null },
342
+ };
343
+ const docs = state?.docs ?? {
344
+ index_url: "https://code.claude.com/docs/llms.txt",
345
+ index_hash: "unknown",
346
+ digest: "unknown",
347
+ full_scan: false,
348
+ scanned_at: new Date(0).toISOString(),
349
+ pages: {},
350
+ };
351
+ const analysis = analyzeClaudeCandidate({
352
+ candidate,
353
+ previous: state,
354
+ currentDocs: docs,
355
+ currentRuntime: state?.runtime ?? null,
356
+ workflowRunUrl: workflowRunUrl(),
357
+ stateBaseSha: state?.state_commit_parent ?? null,
358
+ errors: [message],
359
+ });
360
+ return {
361
+ ...analysis,
362
+ candidate_id: `${state?.candidate_id ?? "claude@unknown"}:error:${sha256(message).slice(0, 12)}`,
363
+ };
364
+ }
365
+
366
+ export async function main(argv = process.argv.slice(2)): Promise<void> {
367
+ const args = parseArgs(argv);
368
+ if (
369
+ (args.previousVersion || args.currentVersion) &&
370
+ !args.dryRun &&
371
+ !args.validationOnly
372
+ ) {
373
+ throw new Error(
374
+ "Manual version overrides require --dry-run or --validation-only so historical replays cannot advance durable watcher state.",
375
+ );
376
+ }
377
+ const repoPath = process.cwd();
378
+ if (args.rebuildStateCommit) {
379
+ const rebuilt = rebuildClaudeAnalysisFromState(
380
+ repoPath,
381
+ args.rebuildStateCommit,
382
+ );
383
+ writeAnalysis(args.analysisFile, rebuilt);
384
+ console.log(JSON.stringify(rebuilt, null, 2));
385
+ return;
386
+ }
387
+
388
+ const tracker = ensureTracker(args.repo, args.dryRun || args.validationOnly);
389
+ trackerOutputs(tracker);
390
+ const stateTip = args.validationOnly ? null : fetchStateBranchTip(repoPath);
391
+ const state = stateTip ? loadStateSnapshotAtCommit(repoPath, stateTip) : null;
392
+ if (stateTip && !state) {
393
+ throw new Error(`Invalid Claude state snapshot at ${stateTip}`);
394
+ }
395
+ const probeContractStale = state?.runtime
396
+ ? !isClaudeProbeContractCurrent(state.runtime)
397
+ : false;
398
+ const stateContractStale =
399
+ state !== null &&
400
+ state.schema_version !== CLAUDE_WATCH_STATE_SCHEMA_VERSION;
401
+ const comparisonBaselineStale = probeContractStale || stateContractStale;
402
+
403
+ if (
404
+ !args.validationOnly &&
405
+ stateTip &&
406
+ state &&
407
+ !comparisonBaselineStale &&
408
+ !hasProcessedCandidate(tracker.state, state.candidate_id)
409
+ ) {
410
+ const analysis = rebuildClaudeAnalysisFromState(repoPath, stateTip);
411
+ writeAnalysis(args.analysisFile, analysis);
412
+ if (analysis.verdict === "no-op" && !args.dryRun) {
413
+ const updated = recordTrackerOutcome(
414
+ args.repo,
415
+ tracker,
416
+ analysis,
417
+ "recorded_noop",
418
+ "reconciled finalized no-op state after a partial run",
419
+ stateTip,
420
+ );
421
+ trackerOutputs(updated);
422
+ analysisOutputs(analysis, false, stateTip);
423
+ } else {
424
+ analysisOutputs(analysis, !args.dryRun, stateTip);
425
+ }
426
+ return;
427
+ }
428
+
429
+ if (!args.validationOnly && state && stateTip) {
430
+ const entry = findTrackerEntry(tracker.state, state.candidate_id);
431
+ if (
432
+ entry &&
433
+ isTerminalOutcome(entry.outcome) &&
434
+ entry.state_commit_sha !== stateTip
435
+ ) {
436
+ if (!args.dryRun) {
437
+ recordTrackerOutcome(
438
+ args.repo,
439
+ tracker,
440
+ rebuildClaudeAnalysisFromState(repoPath, stateTip),
441
+ entry.outcome,
442
+ entry.notes,
443
+ stateTip,
444
+ );
445
+ }
446
+ }
447
+ }
448
+
449
+ let analysis: ClaudeWatchAnalysis;
450
+ let docsCapture: CapturedDocs;
451
+ let currentRuntime: ClaudeRuntimeSnapshot | null;
452
+ let previousForAnalysis = state;
453
+ try {
454
+ const candidate = await selectCandidate(state, args);
455
+ const packageChanged = state?.package_version !== candidate.version;
456
+ docsCapture = await captureDocsSnapshot({
457
+ previous: state?.docs ?? null,
458
+ forceFullScan: args.forceDocsScan,
459
+ packageRelease: packageChanged,
460
+ });
461
+ const docsChanged = docsCapture.snapshot.digest !== state?.docs.digest;
462
+ const runtime = await captureRuntimePair(
463
+ candidate,
464
+ state,
465
+ args,
466
+ packageChanged ||
467
+ docsChanged ||
468
+ !state?.runtime ||
469
+ comparisonBaselineStale,
470
+ );
471
+ currentRuntime = runtime.currentRuntime;
472
+ if (comparisonBaselineStale) {
473
+ previousForAnalysis = null;
474
+ } else if (args.previousVersion && runtime.previousRuntime) {
475
+ previousForAnalysis = {
476
+ schema_version: 1,
477
+ candidate_id: `manual-replay@${args.previousVersion}`,
478
+ package_version: args.previousVersion,
479
+ npm_integrity: "manual-replay",
480
+ npm_published_at: candidate.published_at,
481
+ release_url: candidate.release_url,
482
+ release_notes_md: "manual replay baseline",
483
+ release_published_at: candidate.release_published_at,
484
+ dist_tags: candidate.dist_tags,
485
+ docs: docsCapture.snapshot,
486
+ runtime: runtime.previousRuntime,
487
+ fetched_at: new Date().toISOString(),
488
+ workflow_run_url: workflowRunUrl(),
489
+ state_commit_parent: null,
490
+ };
491
+ } else if (state && runtime.previousRuntime) {
492
+ previousForAnalysis = { ...state, runtime: runtime.previousRuntime };
493
+ }
494
+ analysis = analyzeClaudeCandidate({
495
+ candidate,
496
+ previous: previousForAnalysis,
497
+ currentDocs: docsCapture.snapshot,
498
+ previousSources: comparisonBaselineStale
499
+ ? {}
500
+ : stateSources(repoPath, stateTip),
501
+ currentSources: docsCapture.sources,
502
+ currentRuntime,
503
+ workflowRunUrl: workflowRunUrl(),
504
+ stateBaseSha: comparisonBaselineStale ? null : stateTip,
505
+ });
506
+ } catch (error) {
507
+ analysis = buildErrorAnalysis(error, state);
508
+ writeAnalysis(args.analysisFile, analysis);
509
+ analysisOutputs(analysis, false, null);
510
+ if (!args.dryRun && !args.validationOnly) {
511
+ recordTrackerOutcome(
512
+ args.repo,
513
+ tracker,
514
+ analysis,
515
+ "error",
516
+ "retryable detector error; state was not advanced",
517
+ null,
518
+ analysis.errors[0] ?? "unknown detector error",
519
+ );
520
+ }
521
+ throw error;
522
+ }
523
+
524
+ writeAnalysis(args.analysisFile, analysis);
525
+ if (args.validationOnly) {
526
+ analysisOutputs(analysis, false, null);
527
+ console.log(JSON.stringify(analysis, null, 2));
528
+ return;
529
+ }
530
+ if (state?.candidate_id === analysis.candidate_id) {
531
+ analysisOutputs(analysis, false, stateTip);
532
+ return;
533
+ }
534
+ if (hasProcessedCandidate(tracker.state, analysis.candidate_id)) {
535
+ const entry = findTrackerEntry(tracker.state, analysis.candidate_id);
536
+ if (args.dryRun || !entry) {
537
+ analysisOutputs(analysis, false, entry?.state_commit_sha ?? stateTip);
538
+ return;
539
+ }
540
+ const snapshot = buildClaudeStateSnapshot(
541
+ analysis,
542
+ docsCapture.snapshot,
543
+ currentRuntime,
544
+ );
545
+ const finalized = finalizeStateBranch({
546
+ repoPath,
547
+ snapshot,
548
+ files: docsCapture.sources,
549
+ expectedBaseSha: stateTip,
550
+ });
551
+ recordTrackerOutcome(
552
+ args.repo,
553
+ tracker,
554
+ analysis,
555
+ entry.outcome,
556
+ entry.notes,
557
+ finalized.commitSha,
558
+ );
559
+ analysisOutputs(analysis, false, finalized.commitSha);
560
+ return;
561
+ }
562
+ if (args.dryRun) {
563
+ analysisOutputs(analysis, false, null);
564
+ console.log(JSON.stringify(analysis, null, 2));
565
+ return;
566
+ }
567
+
568
+ const snapshot = buildClaudeStateSnapshot(
569
+ analysis,
570
+ docsCapture.snapshot,
571
+ currentRuntime,
572
+ );
573
+ let finalized: ReturnType<typeof finalizeStateBranch>;
574
+ try {
575
+ finalized = finalizeStateBranch({
576
+ repoPath,
577
+ snapshot,
578
+ files: docsCapture.sources,
579
+ expectedBaseSha: stateTip,
580
+ });
581
+ } catch (error) {
582
+ const message = error instanceof Error ? error.message : String(error);
583
+ const failedAnalysis: ClaudeWatchAnalysis = {
584
+ ...analysis,
585
+ verdict: "manual review required",
586
+ verdict_reasons: [`state finalization failed: ${message}`],
587
+ errors: [...analysis.errors, message],
588
+ };
589
+ writeAnalysis(args.analysisFile, failedAnalysis);
590
+ recordTrackerOutcome(
591
+ args.repo,
592
+ tracker,
593
+ failedAnalysis,
594
+ "error",
595
+ "retryable state finalization error; durable state was not advanced",
596
+ null,
597
+ message,
598
+ );
599
+ throw error;
600
+ }
601
+ if (analysis.verdict === "no-op") {
602
+ const updated = recordTrackerOutcome(
603
+ args.repo,
604
+ tracker,
605
+ analysis,
606
+ "recorded_noop",
607
+ analysis.verdict_reasons.join("; "),
608
+ finalized.commitSha,
609
+ );
610
+ trackerOutputs(updated);
611
+ analysisOutputs(analysis, false, finalized.commitSha);
612
+ return;
613
+ }
614
+ analysisOutputs(analysis, true, finalized.commitSha);
615
+ }
616
+
617
+ if (import.meta.main) {
618
+ main().catch((error) => {
619
+ console.error(error instanceof Error ? error.message : error);
620
+ process.exit(1);
621
+ });
622
+ }