@letta-ai/letta-code 0.30.27 → 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/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 +269 -117
- 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 +3 -3
- 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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
emptyTrackerState,
|
|
4
|
+
hasProcessedCandidate,
|
|
5
|
+
parseTrackerState,
|
|
6
|
+
recordAnalysis,
|
|
7
|
+
renderTrackerBody,
|
|
8
|
+
serializeTrackerState,
|
|
9
|
+
TRACKER_ATTEMPT_HISTORY_LIMIT,
|
|
10
|
+
TRACKER_HISTORY_LIMIT,
|
|
11
|
+
} from "./tracker.ts";
|
|
12
|
+
import type { ClaudeWatchAnalysis } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
function analysis(candidateId = "2.1.0"): ClaudeWatchAnalysis {
|
|
15
|
+
return {
|
|
16
|
+
schema_version: 1,
|
|
17
|
+
candidate_id: candidateId,
|
|
18
|
+
previous_candidate_id: "2.0.0",
|
|
19
|
+
previous_version: "2.0.0",
|
|
20
|
+
current_version: candidateId,
|
|
21
|
+
is_bootstrap: false,
|
|
22
|
+
release_url: `https://example.test/releases/${candidateId}`,
|
|
23
|
+
release_notes_md: "notes",
|
|
24
|
+
npm_integrity: `sha512-package-${candidateId}`,
|
|
25
|
+
npm_published_at: "2026-08-01T00:00:00Z",
|
|
26
|
+
release_published_at: "2026-08-01T00:00:00Z",
|
|
27
|
+
dist_tags: { latest: candidateId, stable: candidateId, next: null },
|
|
28
|
+
docs_digest: `docs-${candidateId}`,
|
|
29
|
+
runtime_digest: `runtime-${candidateId}`,
|
|
30
|
+
docs_diff: {
|
|
31
|
+
added_pages: [],
|
|
32
|
+
removed_pages: [],
|
|
33
|
+
changed_pages: [],
|
|
34
|
+
watched_page_diffs: [],
|
|
35
|
+
tools: { added: [], removed: [], changed: [] },
|
|
36
|
+
cli: { added: [], removed: [], changed: [] },
|
|
37
|
+
settings: { added: [], removed: [], changed: [] },
|
|
38
|
+
env_vars: { added: [], removed: [], changed: [] },
|
|
39
|
+
permission_rules: { added: [], removed: [], changed: [] },
|
|
40
|
+
},
|
|
41
|
+
previous_runtime_snapshot: null,
|
|
42
|
+
runtime_snapshot: null,
|
|
43
|
+
runtime_diff: null,
|
|
44
|
+
verdict: "no-op",
|
|
45
|
+
verdict_reasons: [],
|
|
46
|
+
errors: [],
|
|
47
|
+
workflow_run_url: "https://example.test/actions/1",
|
|
48
|
+
state_base_sha: null,
|
|
49
|
+
state_snapshot_candidate_id: "2.0.0",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("Claude tracker", () => {
|
|
54
|
+
test("uses a Claude-only hidden marker and parses malformed state safely", () => {
|
|
55
|
+
expect(parseTrackerState("plain text")).toEqual(emptyTrackerState());
|
|
56
|
+
expect(
|
|
57
|
+
parseTrackerState("<!-- claude-watch-tracker-state\n{nope}\n-->"),
|
|
58
|
+
).toEqual(emptyTrackerState());
|
|
59
|
+
expect(
|
|
60
|
+
parseTrackerState('<!-- codex-agent-watch-state\n{"processed":[]}\n-->'),
|
|
61
|
+
).toEqual(emptyTrackerState());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("round trips digests and terminal state", () => {
|
|
65
|
+
const state = recordAnalysis(emptyTrackerState(), {
|
|
66
|
+
analysis: analysis(),
|
|
67
|
+
outcome: "recorded_noop",
|
|
68
|
+
notes: "nothing watched changed",
|
|
69
|
+
stateCommitSha: "abc123",
|
|
70
|
+
processedAt: "2026-08-01T01:00:00Z",
|
|
71
|
+
});
|
|
72
|
+
expect(parseTrackerState(serializeTrackerState(state))).toEqual(state);
|
|
73
|
+
expect(state.processed[0]).toMatchObject({
|
|
74
|
+
candidate_id: "2.1.0",
|
|
75
|
+
package_digest: "sha512-package-2.1.0",
|
|
76
|
+
docs_digest: "docs-2.1.0",
|
|
77
|
+
runtime_digest: "runtime-2.1.0",
|
|
78
|
+
state_commit_sha: "abc123",
|
|
79
|
+
});
|
|
80
|
+
expect(hasProcessedCandidate(state, "2.1.0")).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("keeps errors nonterminal and accumulates bounded retry history", () => {
|
|
84
|
+
let state = emptyTrackerState();
|
|
85
|
+
for (
|
|
86
|
+
let attempt = 0;
|
|
87
|
+
attempt < TRACKER_ATTEMPT_HISTORY_LIMIT + 4;
|
|
88
|
+
attempt++
|
|
89
|
+
) {
|
|
90
|
+
state = recordAnalysis(state, {
|
|
91
|
+
analysis: analysis(),
|
|
92
|
+
outcome: "error",
|
|
93
|
+
notes: "retryable",
|
|
94
|
+
error: `failure ${attempt}`,
|
|
95
|
+
processedAt: `2026-08-01T01:${String(attempt).padStart(2, "0")}:00Z`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
expect(state.processed).toHaveLength(1);
|
|
99
|
+
expect(state.processed[0]?.attempts).toHaveLength(
|
|
100
|
+
TRACKER_ATTEMPT_HISTORY_LIMIT,
|
|
101
|
+
);
|
|
102
|
+
expect(state.processed[0]?.errors).toHaveLength(
|
|
103
|
+
TRACKER_ATTEMPT_HISTORY_LIMIT,
|
|
104
|
+
);
|
|
105
|
+
expect(hasProcessedCandidate(state, "2.1.0")).toBe(false);
|
|
106
|
+
|
|
107
|
+
state = recordAnalysis(state, {
|
|
108
|
+
analysis: { ...analysis(), verdict: "tool contract review needed" },
|
|
109
|
+
outcome: "pr_created",
|
|
110
|
+
notes: "fixed",
|
|
111
|
+
prUrl: "https://example.test/pull/1",
|
|
112
|
+
stateCommitSha: "def456",
|
|
113
|
+
});
|
|
114
|
+
expect(state.processed).toHaveLength(1);
|
|
115
|
+
expect(state.processed[0]?.attempts).toHaveLength(
|
|
116
|
+
TRACKER_ATTEMPT_HISTORY_LIMIT,
|
|
117
|
+
);
|
|
118
|
+
expect(hasProcessedCandidate(state, "2.1.0")).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("bounds candidate history and shows only actionable non-noops", () => {
|
|
122
|
+
let state = emptyTrackerState();
|
|
123
|
+
for (let index = 0; index < TRACKER_HISTORY_LIMIT + 5; index++) {
|
|
124
|
+
state = recordAnalysis(state, {
|
|
125
|
+
analysis: analysis(`2.${index}.0`),
|
|
126
|
+
outcome: "recorded_noop",
|
|
127
|
+
notes: `noop ${index}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
expect(state.processed).toHaveLength(TRACKER_HISTORY_LIMIT);
|
|
131
|
+
expect(renderTrackerBody(state)).toContain(
|
|
132
|
+
"_No actionable Claude changes recorded yet._",
|
|
133
|
+
);
|
|
134
|
+
expect(renderTrackerBody(state)).not.toContain("| 2.54.0 | no-op |");
|
|
135
|
+
|
|
136
|
+
const actionable = recordAnalysis(state, {
|
|
137
|
+
analysis: {
|
|
138
|
+
...analysis("3.0.0"),
|
|
139
|
+
verdict: "harness behavior review needed",
|
|
140
|
+
},
|
|
141
|
+
outcome: "needs_human_review",
|
|
142
|
+
notes: "inspect hooks | carefully",
|
|
143
|
+
});
|
|
144
|
+
const body = renderTrackerBody(actionable);
|
|
145
|
+
expect(body).toContain("| 3.0.0 | harness behavior review needed |");
|
|
146
|
+
expect(body).toContain("inspect hooks \\| carefully");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClaudeWatchAnalysis,
|
|
3
|
+
ClaudeWatchOutcome,
|
|
4
|
+
ClaudeWatchVerdict,
|
|
5
|
+
} from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** Deliberately different from the Codex watcher marker. */
|
|
8
|
+
export const CLAUDE_TRACKER_STATE_START = "<!-- claude-watch-tracker-state";
|
|
9
|
+
export const CLAUDE_TRACKER_STATE_END = "-->";
|
|
10
|
+
export const TRACKER_HISTORY_LIMIT = 50;
|
|
11
|
+
export const TRACKER_ATTEMPT_HISTORY_LIMIT = 10;
|
|
12
|
+
export const TRACKER_ERROR_HISTORY_LIMIT = 10;
|
|
13
|
+
export const VISIBLE_ACTIONABLE_LIMIT = 20;
|
|
14
|
+
|
|
15
|
+
const TERMINAL_OUTCOMES: ReadonlySet<ClaudeWatchOutcome> = new Set([
|
|
16
|
+
"recorded_noop",
|
|
17
|
+
"no_local_impact",
|
|
18
|
+
"pr_created",
|
|
19
|
+
"needs_human_review",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export interface ClaudeTrackerAttempt {
|
|
23
|
+
attempted_at: string;
|
|
24
|
+
workflow_run_url: string;
|
|
25
|
+
outcome: ClaudeWatchOutcome;
|
|
26
|
+
error: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ClaudeTrackerError {
|
|
30
|
+
occurred_at: string;
|
|
31
|
+
workflow_run_url: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ClaudeTrackerEntry {
|
|
36
|
+
candidate_id: string;
|
|
37
|
+
package_digest: string;
|
|
38
|
+
docs_digest: string;
|
|
39
|
+
runtime_digest: string | null;
|
|
40
|
+
verdict: ClaudeWatchVerdict;
|
|
41
|
+
outcome: ClaudeWatchOutcome;
|
|
42
|
+
pr_url: string | null;
|
|
43
|
+
notes: string;
|
|
44
|
+
processed_at: string;
|
|
45
|
+
workflow_run_url: string;
|
|
46
|
+
attempts: ClaudeTrackerAttempt[];
|
|
47
|
+
errors: ClaudeTrackerError[];
|
|
48
|
+
state_commit_sha: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ClaudeTrackerState {
|
|
52
|
+
last_checked_candidate_id: string | null;
|
|
53
|
+
last_checked_at: string | null;
|
|
54
|
+
processed: ClaudeTrackerEntry[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RecordClaudeAnalysisOptions {
|
|
58
|
+
analysis: ClaudeWatchAnalysis;
|
|
59
|
+
outcome: ClaudeWatchOutcome;
|
|
60
|
+
notes: string;
|
|
61
|
+
prUrl?: string | null;
|
|
62
|
+
stateCommitSha?: string | null;
|
|
63
|
+
error?: string | null;
|
|
64
|
+
processedAt?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function emptyTrackerState(): ClaudeTrackerState {
|
|
68
|
+
return {
|
|
69
|
+
last_checked_candidate_id: null,
|
|
70
|
+
last_checked_at: null,
|
|
71
|
+
processed: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isTerminalOutcome(outcome: ClaudeWatchOutcome): boolean {
|
|
76
|
+
return TERMINAL_OUTCOMES.has(outcome);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function findTrackerEntry(
|
|
80
|
+
state: ClaudeTrackerState,
|
|
81
|
+
candidateId: string,
|
|
82
|
+
): ClaudeTrackerEntry | undefined {
|
|
83
|
+
return state.processed.find((entry) => entry.candidate_id === candidateId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Errors are retryable and therefore do not count as processed. */
|
|
87
|
+
export function hasProcessedCandidate(
|
|
88
|
+
state: ClaudeTrackerState,
|
|
89
|
+
candidateId: string,
|
|
90
|
+
): boolean {
|
|
91
|
+
const entry = findTrackerEntry(state, candidateId);
|
|
92
|
+
return entry !== undefined && isTerminalOutcome(entry.outcome);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseTrackerState(body: string): ClaudeTrackerState {
|
|
96
|
+
const start = body.indexOf(CLAUDE_TRACKER_STATE_START);
|
|
97
|
+
if (start < 0) return emptyTrackerState();
|
|
98
|
+
const jsonStart = start + CLAUDE_TRACKER_STATE_START.length;
|
|
99
|
+
const end = body.indexOf(CLAUDE_TRACKER_STATE_END, jsonStart);
|
|
100
|
+
if (end < 0) return emptyTrackerState();
|
|
101
|
+
try {
|
|
102
|
+
return normalizeState(JSON.parse(body.slice(jsonStart, end).trim()));
|
|
103
|
+
} catch {
|
|
104
|
+
return emptyTrackerState();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function serializeTrackerState(state: ClaudeTrackerState): string {
|
|
109
|
+
return `${CLAUDE_TRACKER_STATE_START}\n${JSON.stringify(normalizeState(state), null, 2)}\n${CLAUDE_TRACKER_STATE_END}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function recordAnalysis(
|
|
113
|
+
state: ClaudeTrackerState,
|
|
114
|
+
options: RecordClaudeAnalysisOptions,
|
|
115
|
+
): ClaudeTrackerState {
|
|
116
|
+
const processedAt = options.processedAt ?? new Date().toISOString();
|
|
117
|
+
const previous = findTrackerEntry(state, options.analysis.candidate_id);
|
|
118
|
+
const error = options.error ?? options.analysis.errors.at(-1) ?? null;
|
|
119
|
+
const attempt: ClaudeTrackerAttempt = {
|
|
120
|
+
attempted_at: processedAt,
|
|
121
|
+
workflow_run_url: options.analysis.workflow_run_url,
|
|
122
|
+
outcome: options.outcome,
|
|
123
|
+
error,
|
|
124
|
+
};
|
|
125
|
+
const errors = error
|
|
126
|
+
? [
|
|
127
|
+
{
|
|
128
|
+
occurred_at: processedAt,
|
|
129
|
+
workflow_run_url: options.analysis.workflow_run_url,
|
|
130
|
+
message: error,
|
|
131
|
+
},
|
|
132
|
+
...(previous?.errors ?? []),
|
|
133
|
+
].slice(0, TRACKER_ERROR_HISTORY_LIMIT)
|
|
134
|
+
: (previous?.errors ?? []);
|
|
135
|
+
|
|
136
|
+
return upsertTrackerEntry(state, {
|
|
137
|
+
candidate_id: options.analysis.candidate_id,
|
|
138
|
+
package_digest: options.analysis.npm_integrity,
|
|
139
|
+
docs_digest: options.analysis.docs_digest,
|
|
140
|
+
runtime_digest: options.analysis.runtime_digest,
|
|
141
|
+
verdict: options.analysis.verdict,
|
|
142
|
+
outcome: options.outcome,
|
|
143
|
+
pr_url: options.prUrl ?? previous?.pr_url ?? null,
|
|
144
|
+
notes: options.notes,
|
|
145
|
+
processed_at: processedAt,
|
|
146
|
+
workflow_run_url: options.analysis.workflow_run_url,
|
|
147
|
+
attempts: [attempt, ...(previous?.attempts ?? [])].slice(
|
|
148
|
+
0,
|
|
149
|
+
TRACKER_ATTEMPT_HISTORY_LIMIT,
|
|
150
|
+
),
|
|
151
|
+
errors,
|
|
152
|
+
state_commit_sha:
|
|
153
|
+
options.stateCommitSha ?? previous?.state_commit_sha ?? null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Upserts by candidate id, so retries never create duplicate history rows. */
|
|
158
|
+
export function upsertTrackerEntry(
|
|
159
|
+
state: ClaudeTrackerState,
|
|
160
|
+
entry: ClaudeTrackerEntry,
|
|
161
|
+
): ClaudeTrackerState {
|
|
162
|
+
const normalizedEntry = normalizeEntry(entry);
|
|
163
|
+
if (!normalizedEntry) return normalizeState(state);
|
|
164
|
+
return {
|
|
165
|
+
last_checked_candidate_id: normalizedEntry.candidate_id,
|
|
166
|
+
last_checked_at: normalizedEntry.processed_at,
|
|
167
|
+
processed: [
|
|
168
|
+
normalizedEntry,
|
|
169
|
+
...normalizeState(state).processed.filter(
|
|
170
|
+
(item) => item.candidate_id !== normalizedEntry.candidate_id,
|
|
171
|
+
),
|
|
172
|
+
].slice(0, TRACKER_HISTORY_LIMIT),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** True only for entries useful to a human; no-op and transient errors stay hidden. */
|
|
177
|
+
export function isActionableEntry(entry: ClaudeTrackerEntry): boolean {
|
|
178
|
+
return (
|
|
179
|
+
entry.outcome === "pr_created" ||
|
|
180
|
+
entry.outcome === "needs_human_review" ||
|
|
181
|
+
(entry.outcome === "no_local_impact" && entry.verdict !== "no-op")
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function renderTrackerBody(state: ClaudeTrackerState): string {
|
|
186
|
+
const normalized = normalizeState(state);
|
|
187
|
+
const rows = normalized.processed
|
|
188
|
+
.filter(isActionableEntry)
|
|
189
|
+
.slice(0, VISIBLE_ACTIONABLE_LIMIT);
|
|
190
|
+
const table =
|
|
191
|
+
rows.length === 0
|
|
192
|
+
? "_No actionable Claude changes recorded yet._"
|
|
193
|
+
: [
|
|
194
|
+
"| Candidate | Verdict | Outcome | PR | Notes |",
|
|
195
|
+
"|---|---|---|---|---|",
|
|
196
|
+
...rows.map(
|
|
197
|
+
(entry) =>
|
|
198
|
+
`| ${escapeTable(entry.candidate_id)} | ${escapeTable(entry.verdict)} | ${escapeTable(entry.outcome)} | ${entry.pr_url ? `[PR](${entry.pr_url})` : "-"} | ${escapeTable(entry.notes)} |`,
|
|
199
|
+
),
|
|
200
|
+
].join("\n");
|
|
201
|
+
const checked =
|
|
202
|
+
normalized.last_checked_candidate_id && normalized.last_checked_at
|
|
203
|
+
? `_Last checked: ${normalized.last_checked_candidate_id} at ${normalized.last_checked_at}._`
|
|
204
|
+
: "_Last checked: never._";
|
|
205
|
+
return [
|
|
206
|
+
"Central tracker for Claude upstream drift monitoring.",
|
|
207
|
+
"",
|
|
208
|
+
checked,
|
|
209
|
+
"",
|
|
210
|
+
"## Recent actionable candidates",
|
|
211
|
+
"",
|
|
212
|
+
table,
|
|
213
|
+
"",
|
|
214
|
+
"## Hidden state",
|
|
215
|
+
"",
|
|
216
|
+
"The workflow uses the Claude-specific hidden JSON block below for dedupe and retries.",
|
|
217
|
+
"",
|
|
218
|
+
serializeTrackerState(normalized),
|
|
219
|
+
"",
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeState(value: unknown): ClaudeTrackerState {
|
|
224
|
+
if (!isRecord(value)) return emptyTrackerState();
|
|
225
|
+
const processed = Array.isArray(value.processed)
|
|
226
|
+
? value.processed
|
|
227
|
+
.map(normalizeEntry)
|
|
228
|
+
.filter((entry): entry is ClaudeTrackerEntry => entry !== null)
|
|
229
|
+
.filter(
|
|
230
|
+
(entry, index, all) =>
|
|
231
|
+
all.findIndex(
|
|
232
|
+
(candidate) => candidate.candidate_id === entry.candidate_id,
|
|
233
|
+
) === index,
|
|
234
|
+
)
|
|
235
|
+
.slice(0, TRACKER_HISTORY_LIMIT)
|
|
236
|
+
: [];
|
|
237
|
+
return {
|
|
238
|
+
last_checked_candidate_id:
|
|
239
|
+
typeof value.last_checked_candidate_id === "string"
|
|
240
|
+
? value.last_checked_candidate_id
|
|
241
|
+
: null,
|
|
242
|
+
last_checked_at:
|
|
243
|
+
typeof value.last_checked_at === "string" ? value.last_checked_at : null,
|
|
244
|
+
processed,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeEntry(value: unknown): ClaudeTrackerEntry | null {
|
|
249
|
+
if (
|
|
250
|
+
!isRecord(value) ||
|
|
251
|
+
typeof value.candidate_id !== "string" ||
|
|
252
|
+
typeof value.package_digest !== "string" ||
|
|
253
|
+
typeof value.docs_digest !== "string" ||
|
|
254
|
+
!(
|
|
255
|
+
typeof value.runtime_digest === "string" || value.runtime_digest === null
|
|
256
|
+
) ||
|
|
257
|
+
!isVerdict(value.verdict) ||
|
|
258
|
+
!isOutcome(value.outcome) ||
|
|
259
|
+
!(typeof value.pr_url === "string" || value.pr_url === null) ||
|
|
260
|
+
typeof value.notes !== "string" ||
|
|
261
|
+
typeof value.processed_at !== "string" ||
|
|
262
|
+
typeof value.workflow_run_url !== "string" ||
|
|
263
|
+
!(
|
|
264
|
+
typeof value.state_commit_sha === "string" ||
|
|
265
|
+
value.state_commit_sha === null
|
|
266
|
+
)
|
|
267
|
+
) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const attempts = Array.isArray(value.attempts)
|
|
271
|
+
? value.attempts.filter(isAttempt).slice(0, TRACKER_ATTEMPT_HISTORY_LIMIT)
|
|
272
|
+
: [];
|
|
273
|
+
const errors = Array.isArray(value.errors)
|
|
274
|
+
? value.errors.filter(isError).slice(0, TRACKER_ERROR_HISTORY_LIMIT)
|
|
275
|
+
: [];
|
|
276
|
+
return { ...value, attempts, errors } as ClaudeTrackerEntry;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isAttempt(value: unknown): value is ClaudeTrackerAttempt {
|
|
280
|
+
return (
|
|
281
|
+
isRecord(value) &&
|
|
282
|
+
typeof value.attempted_at === "string" &&
|
|
283
|
+
typeof value.workflow_run_url === "string" &&
|
|
284
|
+
isOutcome(value.outcome) &&
|
|
285
|
+
(typeof value.error === "string" || value.error === null)
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isError(value: unknown): value is ClaudeTrackerError {
|
|
290
|
+
return (
|
|
291
|
+
isRecord(value) &&
|
|
292
|
+
typeof value.occurred_at === "string" &&
|
|
293
|
+
typeof value.workflow_run_url === "string" &&
|
|
294
|
+
typeof value.message === "string"
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function isVerdict(value: unknown): value is ClaudeWatchVerdict {
|
|
299
|
+
return (
|
|
300
|
+
value === "no-op" ||
|
|
301
|
+
value === "prompt review needed" ||
|
|
302
|
+
value === "tool contract review needed" ||
|
|
303
|
+
value === "tool surface review needed" ||
|
|
304
|
+
value === "harness behavior review needed" ||
|
|
305
|
+
value === "manual review required"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isOutcome(value: unknown): value is ClaudeWatchOutcome {
|
|
310
|
+
return (
|
|
311
|
+
value === "recorded_noop" ||
|
|
312
|
+
value === "no_local_impact" ||
|
|
313
|
+
value === "pr_created" ||
|
|
314
|
+
value === "needs_human_review" ||
|
|
315
|
+
value === "error"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
320
|
+
return typeof value === "object" && value !== null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function escapeTable(value: string): string {
|
|
324
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
325
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export const CLAUDE_WATCH_STATE_SCHEMA_VERSION = 2;
|
|
2
|
+
|
|
3
|
+
export type ClaudeWatchVerdict =
|
|
4
|
+
| "no-op"
|
|
5
|
+
| "prompt review needed"
|
|
6
|
+
| "tool contract review needed"
|
|
7
|
+
| "tool surface review needed"
|
|
8
|
+
| "harness behavior review needed"
|
|
9
|
+
| "manual review required";
|
|
10
|
+
|
|
11
|
+
export type ClaudeWatchOutcome =
|
|
12
|
+
| "recorded_noop"
|
|
13
|
+
| "no_local_impact"
|
|
14
|
+
| "pr_created"
|
|
15
|
+
| "needs_human_review"
|
|
16
|
+
| "error";
|
|
17
|
+
|
|
18
|
+
export interface ClaudeGitHubRelease {
|
|
19
|
+
tag_name: string;
|
|
20
|
+
draft: boolean;
|
|
21
|
+
prerelease: boolean;
|
|
22
|
+
html_url: string;
|
|
23
|
+
body: string | null;
|
|
24
|
+
published_at: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ClaudeNpmVersion {
|
|
28
|
+
version: string;
|
|
29
|
+
published_at: string;
|
|
30
|
+
integrity: string;
|
|
31
|
+
tarball_url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ClaudeNpmMetadata {
|
|
35
|
+
dist_tags: {
|
|
36
|
+
latest: string;
|
|
37
|
+
stable: string | null;
|
|
38
|
+
next: string | null;
|
|
39
|
+
};
|
|
40
|
+
versions: ClaudeNpmVersion[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ClaudeReleaseCandidate extends ClaudeNpmVersion {
|
|
44
|
+
release_url: string;
|
|
45
|
+
release_notes_md: string;
|
|
46
|
+
release_published_at: string;
|
|
47
|
+
dist_tags: ClaudeNpmMetadata["dist_tags"];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ClaudeDocsPageSnapshot {
|
|
51
|
+
url: string;
|
|
52
|
+
hash: string;
|
|
53
|
+
watched: boolean;
|
|
54
|
+
source_path: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ClaudeDocsSnapshot {
|
|
58
|
+
index_url: string;
|
|
59
|
+
index_hash: string;
|
|
60
|
+
digest: string;
|
|
61
|
+
full_scan: boolean;
|
|
62
|
+
scanned_at: string;
|
|
63
|
+
pages: Record<string, ClaudeDocsPageSnapshot>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ClaudeNamedChange {
|
|
67
|
+
added: string[];
|
|
68
|
+
removed: string[];
|
|
69
|
+
changed: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ClaudeDocsPageDiff {
|
|
73
|
+
url: string;
|
|
74
|
+
source_path: string | null;
|
|
75
|
+
preview: string;
|
|
76
|
+
truncated: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ClaudeDocsDiff {
|
|
80
|
+
added_pages: string[];
|
|
81
|
+
removed_pages: string[];
|
|
82
|
+
changed_pages: string[];
|
|
83
|
+
watched_page_diffs: ClaudeDocsPageDiff[];
|
|
84
|
+
tools: ClaudeNamedChange;
|
|
85
|
+
cli: ClaudeNamedChange;
|
|
86
|
+
settings: ClaudeNamedChange;
|
|
87
|
+
env_vars: ClaudeNamedChange;
|
|
88
|
+
permission_rules: ClaudeNamedChange;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ClaudeProbeObservation {
|
|
92
|
+
name: string;
|
|
93
|
+
status: "passed" | "inconclusive" | "failed" | "skipped";
|
|
94
|
+
attempts: number;
|
|
95
|
+
assertions: Record<string, boolean | string | null>;
|
|
96
|
+
tool_calls: Array<{
|
|
97
|
+
name: string;
|
|
98
|
+
input: unknown;
|
|
99
|
+
}>;
|
|
100
|
+
tool_results: string[];
|
|
101
|
+
filesystem_changes: string[];
|
|
102
|
+
error: string | null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ClaudeRuntimeSnapshot {
|
|
106
|
+
probe_contract_version: number;
|
|
107
|
+
version: string;
|
|
108
|
+
version_output: string;
|
|
109
|
+
help_text: string;
|
|
110
|
+
help_hash: string;
|
|
111
|
+
doctor: {
|
|
112
|
+
exit_code: number;
|
|
113
|
+
summary: string;
|
|
114
|
+
};
|
|
115
|
+
auto_mode_defaults: unknown | null;
|
|
116
|
+
init: {
|
|
117
|
+
tools: string[];
|
|
118
|
+
model: string | null;
|
|
119
|
+
capabilities: unknown | null;
|
|
120
|
+
stable_fields: Record<string, unknown>;
|
|
121
|
+
} | null;
|
|
122
|
+
event_inventory: string[];
|
|
123
|
+
probes: ClaudeProbeObservation[];
|
|
124
|
+
digest: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface ClaudeRuntimeDiff {
|
|
128
|
+
tools_added: string[];
|
|
129
|
+
tools_removed: string[];
|
|
130
|
+
help_changed: boolean;
|
|
131
|
+
help_lines_added: string[];
|
|
132
|
+
help_lines_removed: string[];
|
|
133
|
+
doctor_changed: boolean;
|
|
134
|
+
init_changed: boolean;
|
|
135
|
+
auto_mode_defaults_changed: boolean;
|
|
136
|
+
event_types_added: string[];
|
|
137
|
+
event_types_removed: string[];
|
|
138
|
+
changed_probes: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ClaudeWatchStateSnapshot {
|
|
142
|
+
schema_version: number;
|
|
143
|
+
candidate_id: string;
|
|
144
|
+
package_version: string;
|
|
145
|
+
npm_integrity: string;
|
|
146
|
+
npm_published_at: string;
|
|
147
|
+
release_url: string;
|
|
148
|
+
release_notes_md: string;
|
|
149
|
+
release_published_at: string;
|
|
150
|
+
dist_tags: ClaudeNpmMetadata["dist_tags"];
|
|
151
|
+
docs: ClaudeDocsSnapshot;
|
|
152
|
+
runtime: ClaudeRuntimeSnapshot | null;
|
|
153
|
+
fetched_at: string;
|
|
154
|
+
workflow_run_url: string;
|
|
155
|
+
/** Physical parent on the durable state branch. */
|
|
156
|
+
state_commit_parent: string | null;
|
|
157
|
+
/** Snapshot used for semantic diffing; undefined legacy snapshots use the physical parent. */
|
|
158
|
+
analysis_parent_sha?: string | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ClaudeWatchAnalysis {
|
|
162
|
+
schema_version: number;
|
|
163
|
+
candidate_id: string;
|
|
164
|
+
previous_candidate_id: string | null;
|
|
165
|
+
previous_version: string | null;
|
|
166
|
+
current_version: string;
|
|
167
|
+
is_bootstrap: boolean;
|
|
168
|
+
release_url: string;
|
|
169
|
+
release_notes_md: string;
|
|
170
|
+
npm_integrity: string;
|
|
171
|
+
npm_published_at: string;
|
|
172
|
+
release_published_at: string;
|
|
173
|
+
dist_tags: ClaudeNpmMetadata["dist_tags"];
|
|
174
|
+
docs_digest: string;
|
|
175
|
+
runtime_digest: string | null;
|
|
176
|
+
docs_diff: ClaudeDocsDiff;
|
|
177
|
+
previous_runtime_snapshot: ClaudeRuntimeSnapshot | null;
|
|
178
|
+
runtime_snapshot: ClaudeRuntimeSnapshot | null;
|
|
179
|
+
runtime_diff: ClaudeRuntimeDiff | null;
|
|
180
|
+
verdict: ClaudeWatchVerdict;
|
|
181
|
+
verdict_reasons: string[];
|
|
182
|
+
errors: string[];
|
|
183
|
+
workflow_run_url: string;
|
|
184
|
+
state_base_sha: string | null;
|
|
185
|
+
state_snapshot_candidate_id: string | null;
|
|
186
|
+
}
|