@letta-ai/letta-code 0.30.32 → 0.31.0
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-agent-presets.js +11 -0
- package/dist/agent-presets-agent-presets.js.map +9 -0
- package/dist/agent-presets-personality-asset-content.js +42 -0
- package/dist/agent-presets-personality-asset-content.js.map +10 -0
- package/dist/agent-presets.js +17 -2
- package/dist/agent-presets.js.map +3 -3
- package/dist/channels-public.js +16 -1
- package/dist/channels-public.js.map +5 -4
- package/dist/gateway-core.js +5 -4
- package/dist/gateway-core.js.map +3 -3
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/create-agent-request.d.ts +3 -0
- package/dist/types/agent/create-agent-request.d.ts.map +1 -1
- package/dist/types/agent/model.d.ts.map +1 -1
- package/dist/types/agent/personality-asset-content.d.ts +3 -0
- package/dist/types/agent/personality-asset-content.d.ts.map +1 -0
- package/dist/types/agent/skills.d.ts.map +1 -1
- package/dist/types/channels/gateway-core.d.ts.map +1 -1
- package/dist/types/channels/route-thread-key.d.ts +20 -0
- package/dist/types/channels/route-thread-key.d.ts.map +1 -0
- package/dist/types/channels-public.d.ts +1 -0
- package/dist/types/channels-public.d.ts.map +1 -1
- package/dist/types/tools/impl/skill.d.ts.map +1 -1
- package/dist/types/types/protocol_v2.d.ts +3 -144
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/types/schedule-protocol.d.ts +151 -1
- package/dist/types/types/schedule-protocol.d.ts.map +1 -1
- package/dist/types/utils/frontmatter.d.ts.map +1 -1
- package/dist/types/websocket/listener/cwd-change.d.ts.map +1 -1
- package/letta.js +238 -86
- package/package.json +4 -2
- package/scripts/builtin-skills-watch/agent-watch.test.ts +104 -0
- package/scripts/builtin-skills-watch/agent-watch.ts +337 -0
- package/scripts/builtin-skills-watch/aggregate-results.test.ts +79 -0
- package/scripts/builtin-skills-watch/aggregate-results.ts +326 -0
- package/scripts/builtin-skills-watch/analysis.test.ts +70 -0
- package/scripts/builtin-skills-watch/analysis.ts +228 -0
- package/scripts/builtin-skills-watch/evidence.test.ts +114 -0
- package/scripts/builtin-skills-watch/evidence.ts +145 -0
- package/scripts/builtin-skills-watch/github.ts +161 -0
- package/scripts/builtin-skills-watch/tracker.test.ts +249 -0
- package/scripts/builtin-skills-watch/tracker.ts +506 -0
- package/scripts/builtin-skills-watch/update-tracker.test.ts +234 -0
- package/scripts/builtin-skills-watch/update-tracker.ts +508 -0
- package/scripts/claude-watch/release-source.test.ts +16 -4
- package/scripts/claude-watch/release-source.ts +41 -2
- package/scripts/codex-watch/agent-watch.ts +8 -5
- package/scripts/codex-watch/release-analysis.test.ts +18 -16
- package/scripts/codex-watch/release-analysis.ts +31 -9
- package/scripts/codex-watch/tracker.test.ts +5 -5
- package/scripts/codex-watch/tracker.ts +7 -8
- package/scripts/pi-ai-watch/agent-watch.ts +9 -14
- package/scripts/pi-ai-watch/release-analysis.test.ts +24 -18
- package/scripts/pi-ai-watch/release-analysis.ts +85 -45
- package/scripts/pi-ai-watch/tracker.test.ts +25 -18
- package/scripts/pi-ai-watch/tracker.ts +7 -19
- package/scripts/pi-ai-watch/update-tracker.ts +2 -2
- package/scripts/source-file-size-baseline.json +2 -2
- package/skills/dispatching-coding-agents/SKILL.md +16 -7
- package/skills/syncing-memory-filesystem/SKILL.md +118 -226
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import type { BuiltinSkillWatchAnalysis } from "./analysis.ts";
|
|
2
|
+
import { digestReviewEvidence, type ReviewEvidence } from "./evidence.ts";
|
|
3
|
+
|
|
4
|
+
const STATE_START = "<!-- builtin-skills-agent-watch-state";
|
|
5
|
+
const STATE_END = "-->";
|
|
6
|
+
const HISTORY_LIMIT = 10;
|
|
7
|
+
const VISIBLE_HISTORY_LIMIT = 10;
|
|
8
|
+
const MAX_TRACKER_BODY_BYTES = 60_000;
|
|
9
|
+
|
|
10
|
+
export type TerminalOutcome = "no_drift" | "pr_created" | "needs_human_review";
|
|
11
|
+
export type TrackerOutcome = TerminalOutcome | "error";
|
|
12
|
+
|
|
13
|
+
export interface SkillAudit {
|
|
14
|
+
candidate_id: string;
|
|
15
|
+
audited_sha: string;
|
|
16
|
+
skill_digest: string;
|
|
17
|
+
outcome: TerminalOutcome;
|
|
18
|
+
audited_at: string;
|
|
19
|
+
workflow_run_id: string;
|
|
20
|
+
evidence_url: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TrackerEntry {
|
|
24
|
+
candidate_id: string;
|
|
25
|
+
skill: string;
|
|
26
|
+
current_sha: string;
|
|
27
|
+
skill_digest: string;
|
|
28
|
+
outcome: TrackerOutcome;
|
|
29
|
+
pr_url: string | null;
|
|
30
|
+
notes: string;
|
|
31
|
+
processed_at: string;
|
|
32
|
+
workflow_run_url: string;
|
|
33
|
+
evidence_digest: string | null;
|
|
34
|
+
evidence_url: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PendingCandidate {
|
|
38
|
+
candidate_id: string;
|
|
39
|
+
current_sha: string;
|
|
40
|
+
skill_digest: string;
|
|
41
|
+
audit_at: string;
|
|
42
|
+
workflow_run_id: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TrackerState {
|
|
46
|
+
schema_version: 1;
|
|
47
|
+
last_scheduled_at: string | null;
|
|
48
|
+
pending: Record<string, PendingCandidate>;
|
|
49
|
+
skills: Record<string, SkillAudit>;
|
|
50
|
+
history: TrackerEntry[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RecordOutcomeOptions {
|
|
54
|
+
analysis: BuiltinSkillWatchAnalysis;
|
|
55
|
+
outcome: TrackerOutcome;
|
|
56
|
+
notes: string;
|
|
57
|
+
prUrl?: string | null;
|
|
58
|
+
processedAt?: string;
|
|
59
|
+
evidence?: ReviewEvidence | null;
|
|
60
|
+
evidenceUrl?: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function emptyTrackerState(): TrackerState {
|
|
64
|
+
return {
|
|
65
|
+
schema_version: 1,
|
|
66
|
+
last_scheduled_at: null,
|
|
67
|
+
pending: {},
|
|
68
|
+
skills: {},
|
|
69
|
+
history: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function startCandidate(
|
|
74
|
+
state: TrackerState,
|
|
75
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
76
|
+
): TrackerState {
|
|
77
|
+
const pending = state.pending[analysis.skill];
|
|
78
|
+
if (pending) {
|
|
79
|
+
if (pending.candidate_id === analysis.candidate_id) return state;
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Cannot start ${analysis.candidate_id}; ${pending.candidate_id} is pending for ${analysis.skill}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...state,
|
|
86
|
+
last_scheduled_at: analysis.audit_at,
|
|
87
|
+
pending: {
|
|
88
|
+
...state.pending,
|
|
89
|
+
[analysis.skill]: {
|
|
90
|
+
candidate_id: analysis.candidate_id,
|
|
91
|
+
current_sha: analysis.current_sha,
|
|
92
|
+
skill_digest: analysis.skill_digest,
|
|
93
|
+
audit_at: analysis.audit_at,
|
|
94
|
+
workflow_run_id: workflowRunId(analysis.workflow_run_url),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function parseTrackerState(body: string): TrackerState {
|
|
101
|
+
const start = body.indexOf(STATE_START);
|
|
102
|
+
if (start === -1) {
|
|
103
|
+
throw new Error("Built-in skills tracker hidden state is missing");
|
|
104
|
+
}
|
|
105
|
+
const jsonStart = start + STATE_START.length;
|
|
106
|
+
const end = body.indexOf(STATE_END, jsonStart);
|
|
107
|
+
if (end === -1) {
|
|
108
|
+
throw new Error("Built-in skills tracker hidden state is incomplete");
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return normalizeState(JSON.parse(body.slice(jsonStart, end).trim()));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new Error("Built-in skills tracker hidden state is invalid", {
|
|
114
|
+
cause: error,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function recordOutcome(
|
|
120
|
+
state: TrackerState,
|
|
121
|
+
options: RecordOutcomeOptions,
|
|
122
|
+
): TrackerState {
|
|
123
|
+
if (options.notes.length > 120) {
|
|
124
|
+
throw new Error("Tracker notes must be at most 120 characters");
|
|
125
|
+
}
|
|
126
|
+
const existingOutcome = terminalOutcomeForCandidate(
|
|
127
|
+
state,
|
|
128
|
+
options.analysis.candidate_id,
|
|
129
|
+
);
|
|
130
|
+
if (existingOutcome) {
|
|
131
|
+
if (options.outcome === "error" || options.outcome === existingOutcome) {
|
|
132
|
+
return state;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Candidate ${options.analysis.candidate_id} already has terminal outcome ${existingOutcome}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
assertPendingCandidate(
|
|
139
|
+
state.pending[options.analysis.skill],
|
|
140
|
+
options.analysis,
|
|
141
|
+
);
|
|
142
|
+
if (
|
|
143
|
+
isTerminalOutcome(options.outcome) &&
|
|
144
|
+
(!options.evidence || !options.evidenceUrl)
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Terminal skill audits require structured evidence and its comment URL",
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const evidenceDigest = options.evidence
|
|
151
|
+
? digestReviewEvidence(options.evidence)
|
|
152
|
+
: null;
|
|
153
|
+
const processedAt = options.processedAt ?? new Date().toISOString();
|
|
154
|
+
const entry: TrackerEntry = {
|
|
155
|
+
candidate_id: options.analysis.candidate_id,
|
|
156
|
+
skill: options.analysis.skill,
|
|
157
|
+
current_sha: options.analysis.current_sha,
|
|
158
|
+
skill_digest: options.analysis.skill_digest,
|
|
159
|
+
outcome: options.outcome,
|
|
160
|
+
pr_url: options.prUrl ?? null,
|
|
161
|
+
notes: options.notes,
|
|
162
|
+
processed_at: processedAt,
|
|
163
|
+
workflow_run_url: options.analysis.workflow_run_url,
|
|
164
|
+
evidence_digest: evidenceDigest,
|
|
165
|
+
evidence_url: options.evidenceUrl ?? null,
|
|
166
|
+
};
|
|
167
|
+
const history = [
|
|
168
|
+
entry,
|
|
169
|
+
...state.history.filter(
|
|
170
|
+
(candidate) => candidate.candidate_id !== entry.candidate_id,
|
|
171
|
+
),
|
|
172
|
+
].slice(0, HISTORY_LIMIT);
|
|
173
|
+
const skills = { ...state.skills };
|
|
174
|
+
if (isTerminalOutcome(options.outcome)) {
|
|
175
|
+
skills[options.analysis.skill] = {
|
|
176
|
+
candidate_id: options.analysis.candidate_id,
|
|
177
|
+
audited_sha: options.analysis.current_sha,
|
|
178
|
+
skill_digest: options.analysis.skill_digest,
|
|
179
|
+
outcome: options.outcome,
|
|
180
|
+
audited_at: processedAt,
|
|
181
|
+
workflow_run_id: currentWorkflowRunId(options.analysis.workflow_run_url),
|
|
182
|
+
evidence_url: options.evidenceUrl as string,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const pending = { ...state.pending };
|
|
186
|
+
if (isTerminalOutcome(options.outcome)) {
|
|
187
|
+
delete pending[options.analysis.skill];
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
schema_version: 1,
|
|
191
|
+
last_scheduled_at: state.last_scheduled_at,
|
|
192
|
+
pending,
|
|
193
|
+
skills,
|
|
194
|
+
history,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function hasTerminalCandidate(
|
|
199
|
+
state: TrackerState,
|
|
200
|
+
candidateId: string,
|
|
201
|
+
): boolean {
|
|
202
|
+
return terminalOutcomeForCandidate(state, candidateId) !== null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function isTerminalOutcome(
|
|
206
|
+
outcome: TrackerOutcome,
|
|
207
|
+
): outcome is TerminalOutcome {
|
|
208
|
+
return outcome !== "error";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function terminalOutcomeForCandidate(
|
|
212
|
+
state: TrackerState,
|
|
213
|
+
candidateId: string,
|
|
214
|
+
): TerminalOutcome | null {
|
|
215
|
+
const historyEntry = state.history.find(
|
|
216
|
+
(entry) =>
|
|
217
|
+
entry.candidate_id === candidateId && isTerminalOutcome(entry.outcome),
|
|
218
|
+
);
|
|
219
|
+
if (historyEntry && isTerminalOutcome(historyEntry.outcome)) {
|
|
220
|
+
return historyEntry.outcome;
|
|
221
|
+
}
|
|
222
|
+
const skillAudit = Object.values(state.skills).find(
|
|
223
|
+
(audit) => audit.candidate_id === candidateId,
|
|
224
|
+
);
|
|
225
|
+
return skillAudit?.outcome ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function renderTrackerBody(
|
|
229
|
+
state: TrackerState,
|
|
230
|
+
inventory: string[] = Object.keys(state.skills),
|
|
231
|
+
): string {
|
|
232
|
+
const normalized = normalizeState(state);
|
|
233
|
+
const lines = [
|
|
234
|
+
"Central tracker for Amelia-driven built-in skill staleness reviews.",
|
|
235
|
+
"",
|
|
236
|
+
renderScheduleStatus(normalized),
|
|
237
|
+
"",
|
|
238
|
+
"## Skills",
|
|
239
|
+
"",
|
|
240
|
+
renderSkillsTable(normalized, inventory),
|
|
241
|
+
"",
|
|
242
|
+
"## Recent actionable reviews",
|
|
243
|
+
"",
|
|
244
|
+
renderActionableHistory(normalized),
|
|
245
|
+
"",
|
|
246
|
+
"## Hidden state",
|
|
247
|
+
"",
|
|
248
|
+
"The workflow uses the hidden JSON block below for candidate identity, deduplication, and retries.",
|
|
249
|
+
"",
|
|
250
|
+
serializeTrackerState(normalized),
|
|
251
|
+
];
|
|
252
|
+
const body = `${lines.join("\n")}\n`;
|
|
253
|
+
if (Buffer.byteLength(body, "utf8") > MAX_TRACKER_BODY_BYTES) {
|
|
254
|
+
throw new Error("Built-in skills tracker body exceeds 60000 bytes");
|
|
255
|
+
}
|
|
256
|
+
return body;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function serializeTrackerState(state: TrackerState): string {
|
|
260
|
+
return `${STATE_START}\n${JSON.stringify(normalizeState(state), null, 2)}\n${STATE_END}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function renderScheduleStatus(state: TrackerState): string {
|
|
264
|
+
if (!state.last_scheduled_at) {
|
|
265
|
+
return "_Last scheduled audit: never._";
|
|
266
|
+
}
|
|
267
|
+
return `_Last scheduled audit: ${state.last_scheduled_at}. Pending skills: ${Object.keys(state.pending).length}._`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function renderSkillsTable(state: TrackerState, inventory: string[]): string {
|
|
271
|
+
const rows = [
|
|
272
|
+
"| Skill | Last audit | Outcome | Evidence |",
|
|
273
|
+
"|---|---|---|---|",
|
|
274
|
+
];
|
|
275
|
+
for (const skill of [...inventory].sort()) {
|
|
276
|
+
const audit = state.skills[skill];
|
|
277
|
+
if (!audit) {
|
|
278
|
+
rows.push(`| ${escapeTable(skill)} | never | - | - |`);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
rows.push(
|
|
282
|
+
`| ${escapeTable(skill)} | ${escapeTable(audit.audited_at)} | ${escapeTable(audit.outcome)} | ${renderEvidence(audit.evidence_url)} |`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return rows.join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function renderActionableHistory(state: TrackerState): string {
|
|
289
|
+
const entries = state.history
|
|
290
|
+
.filter((entry) => entry.outcome !== "no_drift")
|
|
291
|
+
.slice(0, VISIBLE_HISTORY_LIMIT);
|
|
292
|
+
if (entries.length === 0) return "_No actionable reviews recorded yet._";
|
|
293
|
+
const rows = [
|
|
294
|
+
"| Skill | Candidate | Outcome | PR | Evidence | Notes |",
|
|
295
|
+
"|---|---|---|---|---|---|",
|
|
296
|
+
];
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
rows.push(
|
|
299
|
+
`| ${escapeTable(entry.skill)} | \`${escapeTable(entry.candidate_id)}\` | ${escapeTable(entry.outcome)} | ${renderPr(entry.pr_url)} | ${renderEvidence(entry.evidence_url)} | ${escapeTable(truncate(entry.notes, 80))} |`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return rows.join("\n");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderPr(prUrl: string | null): string {
|
|
306
|
+
return prUrl ? `[PR](${prUrl})` : "-";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderEvidence(evidenceUrl: string | null): string {
|
|
310
|
+
return evidenceUrl ? `[Evidence](${evidenceUrl})` : "-";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function escapeTable(value: string): string {
|
|
314
|
+
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function truncate(value: string, length: number): string {
|
|
318
|
+
return value.length <= length ? value : `${value.slice(0, length - 1)}…`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function normalizeState(value: unknown): TrackerState {
|
|
322
|
+
if (!isRecord(value) || value.schema_version !== 1) {
|
|
323
|
+
throw new TypeError("tracker state must use schema version 1");
|
|
324
|
+
}
|
|
325
|
+
if (!isNullableString(value.last_scheduled_at)) {
|
|
326
|
+
throw new TypeError("tracker last_scheduled_at is invalid");
|
|
327
|
+
}
|
|
328
|
+
if (
|
|
329
|
+
value.last_scheduled_at !== null &&
|
|
330
|
+
!isIsoTimestamp(value.last_scheduled_at)
|
|
331
|
+
) {
|
|
332
|
+
throw new TypeError("tracker last_scheduled_at is not an ISO timestamp");
|
|
333
|
+
}
|
|
334
|
+
if (!isRecord(value.pending)) {
|
|
335
|
+
throw new TypeError("tracker pending candidates are invalid");
|
|
336
|
+
}
|
|
337
|
+
const pending: Record<string, PendingCandidate> = {};
|
|
338
|
+
for (const [skill, candidate] of Object.entries(value.pending)) {
|
|
339
|
+
if (!isPendingCandidate(candidate)) {
|
|
340
|
+
throw new TypeError(`tracker pending candidate for ${skill} is invalid`);
|
|
341
|
+
}
|
|
342
|
+
pending[skill] = candidate;
|
|
343
|
+
}
|
|
344
|
+
if (!isRecord(value.skills)) {
|
|
345
|
+
throw new TypeError("tracker skills are invalid");
|
|
346
|
+
}
|
|
347
|
+
if (!Array.isArray(value.history) || !value.history.every(isTrackerEntry)) {
|
|
348
|
+
throw new TypeError("tracker history is invalid");
|
|
349
|
+
}
|
|
350
|
+
const skills: Record<string, SkillAudit> = {};
|
|
351
|
+
for (const [skill, audit] of Object.entries(value.skills)) {
|
|
352
|
+
if (!isSkillAudit(audit)) {
|
|
353
|
+
throw new TypeError(`tracker audit for ${skill} is invalid`);
|
|
354
|
+
}
|
|
355
|
+
skills[skill] = audit;
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
schema_version: 1,
|
|
359
|
+
last_scheduled_at: value.last_scheduled_at,
|
|
360
|
+
pending,
|
|
361
|
+
skills,
|
|
362
|
+
history: value.history.slice(0, HISTORY_LIMIT),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isSkillAudit(value: unknown): value is SkillAudit {
|
|
367
|
+
return (
|
|
368
|
+
isRecord(value) &&
|
|
369
|
+
isCandidateId(value.candidate_id) &&
|
|
370
|
+
isCommitSha(value.audited_sha) &&
|
|
371
|
+
isDigest(value.skill_digest) &&
|
|
372
|
+
isTerminalOutcomeValue(value.outcome) &&
|
|
373
|
+
isIsoTimestamp(value.audited_at) &&
|
|
374
|
+
isWorkflowRunId(value.workflow_run_id) &&
|
|
375
|
+
isGitHubIssueCommentUrl(value.evidence_url)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isTrackerEntry(value: unknown): value is TrackerEntry {
|
|
380
|
+
return (
|
|
381
|
+
isRecord(value) &&
|
|
382
|
+
isCandidateId(value.candidate_id) &&
|
|
383
|
+
isSkillName(value.skill) &&
|
|
384
|
+
isCommitSha(value.current_sha) &&
|
|
385
|
+
isDigest(value.skill_digest) &&
|
|
386
|
+
isTrackerOutcome(value.outcome) &&
|
|
387
|
+
isNullableString(value.pr_url) &&
|
|
388
|
+
typeof value.notes === "string" &&
|
|
389
|
+
isIsoTimestamp(value.processed_at) &&
|
|
390
|
+
isWorkflowRunUrl(value.workflow_run_url) &&
|
|
391
|
+
(value.evidence_digest === null || isDigest(value.evidence_digest)) &&
|
|
392
|
+
(value.outcome === "error"
|
|
393
|
+
? value.evidence_digest === null && value.evidence_url === null
|
|
394
|
+
: isDigest(value.evidence_digest) &&
|
|
395
|
+
isGitHubIssueCommentUrl(value.evidence_url))
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function isPendingCandidate(value: unknown): value is PendingCandidate {
|
|
400
|
+
return (
|
|
401
|
+
isRecord(value) &&
|
|
402
|
+
isCandidateId(value.candidate_id) &&
|
|
403
|
+
isCommitSha(value.current_sha) &&
|
|
404
|
+
isDigest(value.skill_digest) &&
|
|
405
|
+
isIsoTimestamp(value.audit_at) &&
|
|
406
|
+
isWorkflowRunId(value.workflow_run_id)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function assertPendingCandidate(
|
|
411
|
+
pending: PendingCandidate | null,
|
|
412
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
413
|
+
): void {
|
|
414
|
+
if (!pending || pending.candidate_id !== analysis.candidate_id) {
|
|
415
|
+
throw new Error(`Candidate ${analysis.candidate_id} is not pending`);
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
pending.current_sha !== analysis.current_sha ||
|
|
419
|
+
pending.skill_digest !== analysis.skill_digest ||
|
|
420
|
+
pending.audit_at !== analysis.audit_at
|
|
421
|
+
) {
|
|
422
|
+
throw new Error(
|
|
423
|
+
`Pending candidate ${analysis.candidate_id} does not match analysis`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isCommitSha(value: unknown): value is string {
|
|
429
|
+
return typeof value === "string" && /^[a-f0-9]{40}$/.test(value);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isDigest(value: unknown): value is string {
|
|
433
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isCandidateId(value: unknown): value is string {
|
|
437
|
+
return (
|
|
438
|
+
typeof value === "string" &&
|
|
439
|
+
/^[a-z0-9]+(?:-[a-z0-9]+)*@[a-f0-9]{12}-[a-f0-9]{16}$/.test(value)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isSkillName(value: unknown): value is string {
|
|
444
|
+
return typeof value === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function isIsoTimestamp(value: unknown): value is string {
|
|
448
|
+
if (typeof value !== "string") return false;
|
|
449
|
+
const parsed = new Date(value);
|
|
450
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isWorkflowRunUrl(value: unknown): value is string {
|
|
454
|
+
return (
|
|
455
|
+
typeof value === "string" &&
|
|
456
|
+
/^https:\/\/github\.com\/letta-ai\/letta-code\/actions\/runs\/\d+$/.test(
|
|
457
|
+
value,
|
|
458
|
+
)
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function workflowRunId(url: string): string {
|
|
463
|
+
const match = url.match(/\/actions\/runs\/(\d+)$/);
|
|
464
|
+
if (!match?.[1]) throw new Error(`Invalid workflow run URL: ${url}`);
|
|
465
|
+
return match[1];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function currentWorkflowRunId(candidateRunUrl: string): string {
|
|
469
|
+
const current = process.env.GITHUB_RUN_ID;
|
|
470
|
+
return current && /^\d+$/.test(current)
|
|
471
|
+
? current
|
|
472
|
+
: workflowRunId(candidateRunUrl);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isWorkflowRunId(value: unknown): value is string {
|
|
476
|
+
return typeof value === "string" && /^\d+$/.test(value);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function isGitHubIssueCommentUrl(value: unknown): value is string {
|
|
480
|
+
return (
|
|
481
|
+
typeof value === "string" &&
|
|
482
|
+
/^https:\/\/github\.com\/letta-ai\/letta-code\/issues\/\d+#issuecomment-\d+$/.test(
|
|
483
|
+
value,
|
|
484
|
+
)
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function isTrackerOutcome(value: unknown): value is TrackerOutcome {
|
|
489
|
+
return value === "error" || isTerminalOutcomeValue(value);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function isTerminalOutcomeValue(value: unknown): value is TerminalOutcome {
|
|
493
|
+
return (
|
|
494
|
+
value === "no_drift" ||
|
|
495
|
+
value === "pr_created" ||
|
|
496
|
+
value === "needs_human_review"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function isNullableString(value: unknown): value is string | null {
|
|
501
|
+
return value === null || typeof value === "string";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
505
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
506
|
+
}
|