@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.
- package/dist/agent-presets.js +17 -17
- package/dist/agent-presets.js.map +1 -1
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/turn-recovery-policy.d.ts +33 -0
- package/dist/types/agent/turn-recovery-policy.d.ts.map +1 -1
- package/dist/types/tools/impl/apply-patch.d.ts.map +1 -1
- package/dist/types/tools/secret-substitution.d.ts.map +1 -1
- package/dist/types/types/loop-status-protocol.d.ts +17 -0
- package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2 -19
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
- package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
- package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
- package/dist/types/websocket/listener/types.d.ts +4 -0
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/letta.js +545 -120
- package/package.json +1 -1
- package/scripts/claude-watch/agent-watch.ts +622 -0
- package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
- package/scripts/claude-watch/docs-snapshot.ts +672 -0
- package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
- package/scripts/claude-watch/github.ts +137 -0
- package/scripts/claude-watch/release-analysis.test.ts +235 -0
- package/scripts/claude-watch/release-analysis.ts +297 -0
- package/scripts/claude-watch/release-source.test.ts +179 -0
- package/scripts/claude-watch/release-source.ts +369 -0
- package/scripts/claude-watch/runtime-observations.ts +98 -0
- package/scripts/claude-watch/runtime-probe.test.ts +576 -0
- package/scripts/claude-watch/runtime-probe.ts +911 -0
- package/scripts/claude-watch/runtime-sandbox.ts +170 -0
- package/scripts/claude-watch/state-branch.test.ts +211 -0
- package/scripts/claude-watch/state-branch.ts +316 -0
- package/scripts/claude-watch/tracker.test.ts +148 -0
- package/scripts/claude-watch/tracker.ts +325 -0
- package/scripts/claude-watch/types.ts +186 -0
- package/scripts/claude-watch/update-tracker.ts +201 -0
- package/scripts/codex-watch/agent-watch.ts +2 -2
- package/scripts/codex-watch/release-analysis.ts +14 -2
- package/scripts/codex-watch/tracker.ts +1 -3
- package/scripts/run-unit-tests.cjs +2 -0
- package/scripts/source-file-size-baseline.json +6 -5
- package/skills/creating-mods/references/commands.md +1 -1
- package/skills/creating-mods/references/ui.md +1 -1
- package/skills/customizing-commands/SKILL.md +1 -1
- package/skills/initializing-memory/SKILL.md +7 -7
- package/skills/self-configuration/SKILL.md +4 -4
- package/scripts/codex-watch/check-release.ts +0 -128
- package/scripts/codex-watch/render-issue.ts +0 -273
package/package.json
CHANGED
|
@@ -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
|
+
}
|