@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,508 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Records or verifies one built-in skill review outcome. */
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import {
|
|
6
|
+
type BuiltinSkillWatchAnalysis,
|
|
7
|
+
buildAnalysis,
|
|
8
|
+
DEFAULT_TARGET_REPO,
|
|
9
|
+
} from "./analysis.ts";
|
|
10
|
+
import { parseReviewEvidence, type ReviewEvidence } from "./evidence.ts";
|
|
11
|
+
import { createIssueComment, editIssueBody, ghJson } from "./github.ts";
|
|
12
|
+
import {
|
|
13
|
+
hasTerminalCandidate,
|
|
14
|
+
parseTrackerState,
|
|
15
|
+
recordOutcome,
|
|
16
|
+
renderTrackerBody,
|
|
17
|
+
type TrackerOutcome,
|
|
18
|
+
} from "./tracker.ts";
|
|
19
|
+
|
|
20
|
+
interface Args {
|
|
21
|
+
repo: string;
|
|
22
|
+
trackerIssue: number | null;
|
|
23
|
+
analysisFile: string | null;
|
|
24
|
+
resultFile: string | null;
|
|
25
|
+
candidateId: string | null;
|
|
26
|
+
outcome: TrackerOutcome | null;
|
|
27
|
+
notes: string;
|
|
28
|
+
expectedGithubLogin: string | null;
|
|
29
|
+
assertTerminal: boolean;
|
|
30
|
+
dryRun: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PullRequestView {
|
|
34
|
+
author: { login: string };
|
|
35
|
+
baseRefName: string;
|
|
36
|
+
body: string;
|
|
37
|
+
files: Array<{ path: string }>;
|
|
38
|
+
headRefOid: string;
|
|
39
|
+
isDraft: boolean;
|
|
40
|
+
state: string;
|
|
41
|
+
url: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TrackerIssueView {
|
|
45
|
+
author: { login: string };
|
|
46
|
+
body: string | null;
|
|
47
|
+
labels: Array<{ name: string }>;
|
|
48
|
+
state: string;
|
|
49
|
+
title: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ReviewResult {
|
|
53
|
+
schema_version: 1;
|
|
54
|
+
candidate_id: string;
|
|
55
|
+
skill: string;
|
|
56
|
+
outcome: "no_drift" | "pr_created" | "needs_human_review";
|
|
57
|
+
notes: string;
|
|
58
|
+
pr_url: string | null;
|
|
59
|
+
evidence: ReviewEvidence;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv: string[]): Args {
|
|
63
|
+
const args: Args = {
|
|
64
|
+
repo: DEFAULT_TARGET_REPO,
|
|
65
|
+
trackerIssue: null,
|
|
66
|
+
analysisFile: null,
|
|
67
|
+
resultFile: null,
|
|
68
|
+
candidateId: null,
|
|
69
|
+
outcome: null,
|
|
70
|
+
notes: "",
|
|
71
|
+
expectedGithubLogin: null,
|
|
72
|
+
assertTerminal: false,
|
|
73
|
+
dryRun: false,
|
|
74
|
+
};
|
|
75
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
76
|
+
const arg = argv[index];
|
|
77
|
+
if (arg === "--repo") args.repo = argv[++index] ?? args.repo;
|
|
78
|
+
else if (arg === "--tracker-issue") {
|
|
79
|
+
args.trackerIssue = Number(argv[++index]);
|
|
80
|
+
} else if (arg === "--analysis-file") {
|
|
81
|
+
args.analysisFile = argv[++index] ?? null;
|
|
82
|
+
} else if (arg === "--result-file") {
|
|
83
|
+
args.resultFile = argv[++index] ?? null;
|
|
84
|
+
} else if (arg === "--candidate-id") {
|
|
85
|
+
args.candidateId = argv[++index] ?? null;
|
|
86
|
+
} else if (arg === "--outcome") {
|
|
87
|
+
args.outcome = parseOutcome(argv[++index]);
|
|
88
|
+
} else if (arg === "--notes") args.notes = argv[++index] ?? "";
|
|
89
|
+
else if (arg === "--expected-github-login") {
|
|
90
|
+
args.expectedGithubLogin = argv[++index] ?? null;
|
|
91
|
+
} else if (arg === "--assert-terminal") args.assertTerminal = true;
|
|
92
|
+
else if (arg === "--dry-run") args.dryRun = true;
|
|
93
|
+
else if (arg === "--help" || arg === "-h") {
|
|
94
|
+
console.log(
|
|
95
|
+
"Usage: bun scripts/builtin-skills-watch/update-tracker.ts --tracker-issue ISSUE (--candidate-id ID --assert-terminal | --analysis-file FILE (--result-file FILE --expected-github-login LOGIN | --outcome error [--notes TEXT])) [--repo OWNER/REPO] [--dry-run]",
|
|
96
|
+
);
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!args.trackerIssue || Number.isNaN(args.trackerIssue)) {
|
|
104
|
+
throw new Error("--tracker-issue is required");
|
|
105
|
+
}
|
|
106
|
+
if (args.assertTerminal) {
|
|
107
|
+
if (!args.candidateId) {
|
|
108
|
+
throw new Error("--candidate-id is required with --assert-terminal");
|
|
109
|
+
}
|
|
110
|
+
return args;
|
|
111
|
+
}
|
|
112
|
+
if (!args.analysisFile) throw new Error("--analysis-file is required");
|
|
113
|
+
if (args.resultFile) {
|
|
114
|
+
if (!args.expectedGithubLogin) {
|
|
115
|
+
throw new Error("--expected-github-login is required with --result-file");
|
|
116
|
+
}
|
|
117
|
+
} else if (args.outcome !== "error") {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"Use --result-file for terminal outcomes or --outcome error",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return args;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function main(): void {
|
|
126
|
+
const args = parseArgs(process.argv.slice(2));
|
|
127
|
+
const tracker = getOpenTrackerIssue(args.repo, args.trackerIssue as number);
|
|
128
|
+
const state = parseTrackerState(tracker.body);
|
|
129
|
+
|
|
130
|
+
if (args.assertTerminal) {
|
|
131
|
+
if (!hasTerminalCandidate(state, args.candidateId as string)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Tracker has no terminal outcome for ${args.candidateId}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
console.log(`Tracker recorded terminal outcome for ${args.candidateId}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const receivedAnalysis = readAnalysis(args.analysisFile as string);
|
|
141
|
+
if (hasTerminalCandidate(state, receivedAnalysis.candidate_id)) {
|
|
142
|
+
console.log(
|
|
143
|
+
`Tracker already recorded terminal outcome for ${receivedAnalysis.candidate_id}`,
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const pending = state.pending[receivedAnalysis.skill];
|
|
148
|
+
if (!pending || pending.candidate_id !== receivedAnalysis.candidate_id) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Candidate ${receivedAnalysis.candidate_id} is not pending for ${receivedAnalysis.skill}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const analysis = buildAnalysis({
|
|
154
|
+
skill: receivedAnalysis.skill,
|
|
155
|
+
currentSha: pending.current_sha,
|
|
156
|
+
auditAt: pending.audit_at,
|
|
157
|
+
previousAudit: previousAudit(state, receivedAnalysis.skill),
|
|
158
|
+
});
|
|
159
|
+
analysis.workflow_run_url = workflowRunUrl(pending.workflow_run_id);
|
|
160
|
+
assertAnalysisIdentity(receivedAnalysis, analysis);
|
|
161
|
+
const result = args.resultFile
|
|
162
|
+
? readReviewResult(args.resultFile, analysis)
|
|
163
|
+
: {
|
|
164
|
+
outcome: "error" as const,
|
|
165
|
+
notes: args.notes || defaultNotes("error"),
|
|
166
|
+
pr_url: null,
|
|
167
|
+
evidence: null,
|
|
168
|
+
};
|
|
169
|
+
if (result.pr_url) {
|
|
170
|
+
verifyPullRequest(
|
|
171
|
+
args.repo,
|
|
172
|
+
result.pr_url,
|
|
173
|
+
analysis,
|
|
174
|
+
args.expectedGithubLogin as string,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const evidenceUrl = result.evidence
|
|
178
|
+
? args.dryRun
|
|
179
|
+
? `https://github.com/letta-ai/letta-code/issues/${args.trackerIssue}#issuecomment-1`
|
|
180
|
+
: createIssueComment(
|
|
181
|
+
args.repo,
|
|
182
|
+
args.trackerIssue as number,
|
|
183
|
+
renderEvidenceComment(analysis, result),
|
|
184
|
+
)
|
|
185
|
+
: null;
|
|
186
|
+
const next = recordOutcome(state, {
|
|
187
|
+
analysis,
|
|
188
|
+
outcome: result.outcome,
|
|
189
|
+
notes: result.notes,
|
|
190
|
+
prUrl: result.pr_url,
|
|
191
|
+
evidence: result.evidence,
|
|
192
|
+
evidenceUrl,
|
|
193
|
+
});
|
|
194
|
+
const inventory = [
|
|
195
|
+
...new Set([
|
|
196
|
+
...analysis.skill_inventory,
|
|
197
|
+
...Object.keys(next.skills),
|
|
198
|
+
...Object.keys(next.pending),
|
|
199
|
+
]),
|
|
200
|
+
].sort();
|
|
201
|
+
const nextBody = renderTrackerBody(next, inventory);
|
|
202
|
+
if (args.dryRun) console.log(nextBody);
|
|
203
|
+
else {
|
|
204
|
+
editIssueBody(args.repo, args.trackerIssue as number, nextBody);
|
|
205
|
+
console.log(
|
|
206
|
+
`Recorded ${analysis.candidate_id} as ${result.outcome} in #${args.trackerIssue}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function previousAudit(
|
|
212
|
+
state: ReturnType<typeof parseTrackerState>,
|
|
213
|
+
skill: string,
|
|
214
|
+
): BuiltinSkillWatchAnalysis["previous_audit"] {
|
|
215
|
+
const audit = state.skills[skill];
|
|
216
|
+
return audit
|
|
217
|
+
? {
|
|
218
|
+
candidate_id: audit.candidate_id,
|
|
219
|
+
audited_sha: audit.audited_sha,
|
|
220
|
+
skill_digest: audit.skill_digest,
|
|
221
|
+
audited_at: audit.audited_at,
|
|
222
|
+
}
|
|
223
|
+
: null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function workflowRunUrl(runId: string): string {
|
|
227
|
+
return `https://github.com/letta-ai/letta-code/actions/runs/${runId}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderEvidenceComment(
|
|
231
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
232
|
+
result: ReviewResult,
|
|
233
|
+
): string {
|
|
234
|
+
return [
|
|
235
|
+
`## Built-in skill audit evidence: ${analysis.skill}`,
|
|
236
|
+
"",
|
|
237
|
+
`Candidate: \`${analysis.candidate_id}\``,
|
|
238
|
+
`Outcome: \`${result.outcome}\``,
|
|
239
|
+
`Notes: ${result.notes}`,
|
|
240
|
+
`PR: ${result.pr_url ?? "-"}`,
|
|
241
|
+
"",
|
|
242
|
+
"```json",
|
|
243
|
+
JSON.stringify(result.evidence, null, 2),
|
|
244
|
+
"```",
|
|
245
|
+
].join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function getOpenTrackerIssue(
|
|
249
|
+
repo: string,
|
|
250
|
+
issueNumber: number,
|
|
251
|
+
): { body: string } {
|
|
252
|
+
const issue = ghJson<TrackerIssueView>([
|
|
253
|
+
"issue",
|
|
254
|
+
"view",
|
|
255
|
+
String(issueNumber),
|
|
256
|
+
"--repo",
|
|
257
|
+
repo,
|
|
258
|
+
"--json",
|
|
259
|
+
"author,body,labels,state,title",
|
|
260
|
+
]);
|
|
261
|
+
validateTrackerIssueView(issue);
|
|
262
|
+
return { body: issue.body ?? "" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function validateTrackerIssueView(issue: TrackerIssueView): void {
|
|
266
|
+
if (
|
|
267
|
+
issue.state !== "OPEN" ||
|
|
268
|
+
issue.title !== "Built-in skill staleness tracker" ||
|
|
269
|
+
!issue.labels.some((label) => label.name === "builtin-skills-watch") ||
|
|
270
|
+
(issue.author.login !== "app/github-actions" &&
|
|
271
|
+
issue.author.login !== "github-actions[bot]")
|
|
272
|
+
) {
|
|
273
|
+
throw new Error("Built-in skills tracker issue is closed or invalid");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function readAnalysis(path: string): BuiltinSkillWatchAnalysis {
|
|
278
|
+
const analysis = JSON.parse(
|
|
279
|
+
readFileSync(path, "utf8"),
|
|
280
|
+
) as BuiltinSkillWatchAnalysis;
|
|
281
|
+
if (
|
|
282
|
+
analysis.schema_version !== 1 ||
|
|
283
|
+
typeof analysis.candidate_id !== "string" ||
|
|
284
|
+
typeof analysis.skill !== "string" ||
|
|
285
|
+
!Array.isArray(analysis.skill_inventory) ||
|
|
286
|
+
!analysis.skill_inventory.includes(analysis.skill)
|
|
287
|
+
) {
|
|
288
|
+
throw new Error("Built-in skill analysis is invalid");
|
|
289
|
+
}
|
|
290
|
+
return analysis;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function assertAnalysisIdentity(
|
|
294
|
+
received: BuiltinSkillWatchAnalysis,
|
|
295
|
+
rebuilt: BuiltinSkillWatchAnalysis,
|
|
296
|
+
): void {
|
|
297
|
+
const fields: Array<keyof BuiltinSkillWatchAnalysis> = [
|
|
298
|
+
"candidate_id",
|
|
299
|
+
"skill",
|
|
300
|
+
"skill_path",
|
|
301
|
+
"skill_files",
|
|
302
|
+
"skill_digest",
|
|
303
|
+
"current_sha",
|
|
304
|
+
"audit_at",
|
|
305
|
+
"previous_audit",
|
|
306
|
+
"repository_changes",
|
|
307
|
+
"skill_inventory",
|
|
308
|
+
];
|
|
309
|
+
for (const field of fields) {
|
|
310
|
+
if (JSON.stringify(received[field]) !== JSON.stringify(rebuilt[field])) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Analysis field ${field} does not match the pending candidate`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function readReviewResult(
|
|
319
|
+
path: string,
|
|
320
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
321
|
+
): ReviewResult {
|
|
322
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
323
|
+
return parseReviewResult(value, analysis);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function parseReviewResult(
|
|
327
|
+
value: unknown,
|
|
328
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
329
|
+
): ReviewResult {
|
|
330
|
+
if (
|
|
331
|
+
!isRecord(value) ||
|
|
332
|
+
!hasExactKeys(value, [
|
|
333
|
+
"schema_version",
|
|
334
|
+
"candidate_id",
|
|
335
|
+
"skill",
|
|
336
|
+
"outcome",
|
|
337
|
+
"notes",
|
|
338
|
+
"pr_url",
|
|
339
|
+
"evidence",
|
|
340
|
+
]) ||
|
|
341
|
+
value.schema_version !== 1 ||
|
|
342
|
+
value.candidate_id !== analysis.candidate_id ||
|
|
343
|
+
value.skill !== analysis.skill ||
|
|
344
|
+
(value.outcome !== "no_drift" &&
|
|
345
|
+
value.outcome !== "pr_created" &&
|
|
346
|
+
value.outcome !== "needs_human_review") ||
|
|
347
|
+
typeof value.notes !== "string" ||
|
|
348
|
+
value.notes.length === 0 ||
|
|
349
|
+
value.notes.length > 120 ||
|
|
350
|
+
(value.pr_url !== null && typeof value.pr_url !== "string")
|
|
351
|
+
) {
|
|
352
|
+
throw new Error("Review result does not match the pending candidate");
|
|
353
|
+
}
|
|
354
|
+
if (
|
|
355
|
+
(value.outcome === "pr_created") !==
|
|
356
|
+
(typeof value.pr_url === "string" && value.pr_url.length > 0)
|
|
357
|
+
) {
|
|
358
|
+
throw new Error("Review result PR URL does not match its outcome");
|
|
359
|
+
}
|
|
360
|
+
const evidence = parseReviewEvidence(value.evidence);
|
|
361
|
+
if (
|
|
362
|
+
evidence.candidate_id !== analysis.candidate_id ||
|
|
363
|
+
evidence.skill !== analysis.skill
|
|
364
|
+
) {
|
|
365
|
+
throw new Error("Review evidence does not match the pending candidate");
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
schema_version: 1,
|
|
369
|
+
candidate_id: value.candidate_id as string,
|
|
370
|
+
skill: value.skill as string,
|
|
371
|
+
outcome: value.outcome,
|
|
372
|
+
notes: value.notes,
|
|
373
|
+
pr_url: value.pr_url,
|
|
374
|
+
evidence,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function verifyPullRequest(
|
|
379
|
+
repo: string,
|
|
380
|
+
prUrl: string,
|
|
381
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
382
|
+
expectedGithubLogin: string,
|
|
383
|
+
): void {
|
|
384
|
+
const authenticatedLogin = ghJson<{ login: string }>(["api", "user"]).login;
|
|
385
|
+
if (authenticatedLogin !== expectedGithubLogin) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Authenticated GitHub login ${authenticatedLogin} does not match ${expectedGithubLogin}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const match = prUrl.match(
|
|
391
|
+
/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)\/?$/,
|
|
392
|
+
);
|
|
393
|
+
if (!match || match[1] !== repo) {
|
|
394
|
+
throw new Error(`PR URL must belong to https://github.com/${repo}`);
|
|
395
|
+
}
|
|
396
|
+
const pullRequest = ghJson<PullRequestView>([
|
|
397
|
+
"pr",
|
|
398
|
+
"view",
|
|
399
|
+
match[2] as string,
|
|
400
|
+
"--repo",
|
|
401
|
+
repo,
|
|
402
|
+
"--json",
|
|
403
|
+
"author,baseRefName,body,files,headRefOid,isDraft,state,url",
|
|
404
|
+
]);
|
|
405
|
+
validatePullRequestView(pullRequest, prUrl, analysis, expectedGithubLogin);
|
|
406
|
+
const comparison = ghJson<{
|
|
407
|
+
status: string;
|
|
408
|
+
merge_base_commit: { sha: string };
|
|
409
|
+
}>([
|
|
410
|
+
"api",
|
|
411
|
+
`repos/${repo}/compare/${analysis.current_sha}...${pullRequest.headRefOid}`,
|
|
412
|
+
]);
|
|
413
|
+
if (
|
|
414
|
+
comparison.status !== "ahead" ||
|
|
415
|
+
comparison.merge_base_commit.sha !== analysis.current_sha
|
|
416
|
+
) {
|
|
417
|
+
throw new Error("Watcher PR head is not based on the audited commit");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function validatePullRequestView(
|
|
422
|
+
pullRequest: PullRequestView,
|
|
423
|
+
prUrl: string,
|
|
424
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
425
|
+
expectedGithubLogin: string,
|
|
426
|
+
): void {
|
|
427
|
+
if (pullRequest.url !== prUrl.replace(/\/$/, "")) {
|
|
428
|
+
throw new Error(`PR URL mismatch: ${pullRequest.url}`);
|
|
429
|
+
}
|
|
430
|
+
if (pullRequest.author.login !== expectedGithubLogin) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`PR author ${pullRequest.author.login} does not match ${expectedGithubLogin}`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (pullRequest.state !== "OPEN" || !pullRequest.isDraft) {
|
|
436
|
+
throw new Error("Watcher PR must be open and draft");
|
|
437
|
+
}
|
|
438
|
+
if (pullRequest.baseRefName !== "main") {
|
|
439
|
+
throw new Error("Watcher PR must target main");
|
|
440
|
+
}
|
|
441
|
+
if (!/^[a-f0-9]{40}$/.test(pullRequest.headRefOid)) {
|
|
442
|
+
throw new Error("Watcher PR head commit is invalid");
|
|
443
|
+
}
|
|
444
|
+
if (
|
|
445
|
+
!pullRequest.body.includes(`Builtin-skill-watch: ${analysis.candidate_id}`)
|
|
446
|
+
) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`PR body is missing Builtin-skill-watch: ${analysis.candidate_id}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
const allowedTestPath = "src/agent/skills-discovery.test.ts";
|
|
452
|
+
if (
|
|
453
|
+
pullRequest.files.length === 0 ||
|
|
454
|
+
!pullRequest.files.some((file) =>
|
|
455
|
+
file.path.startsWith(`${analysis.skill_path}/`),
|
|
456
|
+
) ||
|
|
457
|
+
pullRequest.files.some(
|
|
458
|
+
(file) =>
|
|
459
|
+
!file.path.startsWith(`${analysis.skill_path}/`) &&
|
|
460
|
+
file.path !== allowedTestPath,
|
|
461
|
+
)
|
|
462
|
+
) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
"Watcher PR changed files outside the selected skill scope",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parseOutcome(value: string | undefined): TrackerOutcome {
|
|
470
|
+
if (
|
|
471
|
+
value === "no_drift" ||
|
|
472
|
+
value === "pr_created" ||
|
|
473
|
+
value === "needs_human_review" ||
|
|
474
|
+
value === "error"
|
|
475
|
+
) {
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
throw new Error(`Unknown outcome: ${value}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function defaultNotes(outcome: TrackerOutcome): string {
|
|
482
|
+
switch (outcome) {
|
|
483
|
+
case "no_drift":
|
|
484
|
+
return "reviewed against current sources; no skill change needed";
|
|
485
|
+
case "pr_created":
|
|
486
|
+
return "opened a focused skill update PR";
|
|
487
|
+
case "needs_human_review":
|
|
488
|
+
return "review needs a human decision";
|
|
489
|
+
case "error":
|
|
490
|
+
return "agent review failed before recording a terminal outcome";
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
495
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function hasExactKeys(
|
|
499
|
+
value: Record<string, unknown>,
|
|
500
|
+
expected: string[],
|
|
501
|
+
): boolean {
|
|
502
|
+
return (
|
|
503
|
+
JSON.stringify(Object.keys(value).sort()) ===
|
|
504
|
+
JSON.stringify([...expected].sort())
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (import.meta.main) main();
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ClaudeReleaseSourceDisagreementError,
|
|
3
3
|
parseClaudeGitHubReleases,
|
|
4
4
|
parseClaudeNpmMetadata,
|
|
5
|
+
releaseNotesForRange,
|
|
5
6
|
selectClaudeReleaseCandidate,
|
|
6
7
|
} from "./release-source.ts";
|
|
7
8
|
import type { ClaudeGitHubRelease, ClaudeNpmMetadata } from "./types.ts";
|
|
@@ -109,15 +110,19 @@ describe("selectClaudeReleaseCandidate", () => {
|
|
|
109
110
|
}
|
|
110
111
|
});
|
|
111
112
|
|
|
112
|
-
test("takes the
|
|
113
|
+
test("takes the latest release after the terminal processed version", () => {
|
|
113
114
|
const candidate = selectClaudeReleaseCandidate({
|
|
114
115
|
githubReleases: releases,
|
|
115
116
|
npmMetadata: npm(),
|
|
116
117
|
processedPackageVersions: ["1.0.0"],
|
|
117
118
|
});
|
|
118
119
|
|
|
119
|
-
expect(candidate?.version).toBe("1.
|
|
120
|
-
expect(candidate?.integrity).toBe("sha512-1.
|
|
120
|
+
expect(candidate?.version).toBe("1.2.0");
|
|
121
|
+
expect(candidate?.integrity).toBe("sha512-1.2.0");
|
|
122
|
+
expect(candidate?.release_notes_md).toContain("## [1.1.0]");
|
|
123
|
+
expect(candidate?.release_notes_md).toContain("notes 1.1.0");
|
|
124
|
+
expect(candidate?.release_notes_md).toContain("## [1.2.0]");
|
|
125
|
+
expect(candidate?.release_notes_md).not.toContain("notes 1.0.0");
|
|
121
126
|
});
|
|
122
127
|
|
|
123
128
|
test("bootstrap selects only the current npm latest release", () => {
|
|
@@ -139,7 +144,8 @@ describe("selectClaudeReleaseCandidate", () => {
|
|
|
139
144
|
});
|
|
140
145
|
|
|
141
146
|
expect(candidate?.version).toBe("1.2.0");
|
|
142
|
-
expect(candidate?.release_notes_md).
|
|
147
|
+
expect(candidate?.release_notes_md).toContain("notes 1.1.0");
|
|
148
|
+
expect(candidate?.release_notes_md).toContain("notes 1.2.0");
|
|
143
149
|
});
|
|
144
150
|
|
|
145
151
|
test("validation can replay exact npm versions omitted from the GitHub feed", () => {
|
|
@@ -176,4 +182,10 @@ describe("selectClaudeReleaseCandidate", () => {
|
|
|
176
182
|
}),
|
|
177
183
|
).toBeNull();
|
|
178
184
|
});
|
|
185
|
+
|
|
186
|
+
test("rejects a reversed release-note range", () => {
|
|
187
|
+
expect(() => releaseNotesForRange(releases, "1.2.0", "1.1.0")).toThrow(
|
|
188
|
+
"Previous Claude release 1.2.0 must precede 1.1.0",
|
|
189
|
+
);
|
|
190
|
+
});
|
|
179
191
|
});
|
|
@@ -275,7 +275,10 @@ export function selectClaudeReleaseCandidate(
|
|
|
275
275
|
`Terminal version ${terminalVersion} is not present in GitHub releases.`,
|
|
276
276
|
);
|
|
277
277
|
}
|
|
278
|
-
release =
|
|
278
|
+
release =
|
|
279
|
+
terminalIndex === githubReleases.length - 1
|
|
280
|
+
? undefined
|
|
281
|
+
: githubReleases.at(-1);
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
if (!release) return null;
|
|
@@ -290,12 +293,48 @@ export function selectClaudeReleaseCandidate(
|
|
|
290
293
|
return {
|
|
291
294
|
...npmVersion,
|
|
292
295
|
release_url: release.html_url,
|
|
293
|
-
release_notes_md:
|
|
296
|
+
release_notes_md: releaseNotesForRange(
|
|
297
|
+
githubReleases,
|
|
298
|
+
terminalVersion,
|
|
299
|
+
release.tag_name,
|
|
300
|
+
),
|
|
294
301
|
release_published_at: release.published_at,
|
|
295
302
|
dist_tags: options.npmMetadata.dist_tags,
|
|
296
303
|
};
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
export function releaseNotesForRange(
|
|
307
|
+
releases: ClaudeGitHubRelease[],
|
|
308
|
+
previousVersion: string | null,
|
|
309
|
+
currentVersion: string,
|
|
310
|
+
): string {
|
|
311
|
+
const currentIndex = releases.findIndex(
|
|
312
|
+
(release) => release.tag_name === currentVersion,
|
|
313
|
+
);
|
|
314
|
+
if (currentIndex < 0) {
|
|
315
|
+
throw new Error(`Could not find Claude release ${currentVersion}`);
|
|
316
|
+
}
|
|
317
|
+
const previousIndex = previousVersion
|
|
318
|
+
? releases.findIndex((release) => release.tag_name === previousVersion)
|
|
319
|
+
: -1;
|
|
320
|
+
if (previousIndex >= currentIndex) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Previous Claude release ${previousVersion} must precede ${currentVersion}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const included = releases.slice(
|
|
326
|
+
previousIndex < 0 ? currentIndex : previousIndex + 1,
|
|
327
|
+
currentIndex + 1,
|
|
328
|
+
);
|
|
329
|
+
if (included.length === 1) return included[0]?.body ?? "";
|
|
330
|
+
return included
|
|
331
|
+
.map(
|
|
332
|
+
(release) =>
|
|
333
|
+
`## [${release.tag_name}](${release.html_url})\n\n${release.body?.trim() || "_No release notes._"}`,
|
|
334
|
+
)
|
|
335
|
+
.join("\n\n");
|
|
336
|
+
}
|
|
337
|
+
|
|
299
338
|
async function fetchJson(
|
|
300
339
|
url: string,
|
|
301
340
|
options: ReleaseSourceFetchOptions,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
analyzeCodexRelease,
|
|
19
19
|
DEFAULT_TARGET_REPO,
|
|
20
|
-
|
|
20
|
+
findLatestStableReleaseAfter,
|
|
21
21
|
listStableReleases,
|
|
22
22
|
type Release,
|
|
23
23
|
} from "./release-analysis.ts";
|
|
@@ -104,8 +104,11 @@ async function main() {
|
|
|
104
104
|
const cursorTag = getCodexAuditCursorTag(state);
|
|
105
105
|
if (cursorTag) {
|
|
106
106
|
stableReleases ??= await listStableReleases();
|
|
107
|
-
const
|
|
108
|
-
|
|
107
|
+
const latestRelease = findLatestStableReleaseAfter(
|
|
108
|
+
stableReleases,
|
|
109
|
+
cursorTag,
|
|
110
|
+
);
|
|
111
|
+
if (!latestRelease) {
|
|
109
112
|
console.log(`No stable release after ${cursorTag}; nothing to do.`);
|
|
110
113
|
writeOutput("tracker_issue", String(tracker.number));
|
|
111
114
|
writeOutput("tracker_issue_url", tracker.url);
|
|
@@ -116,7 +119,7 @@ async function main() {
|
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
sinceTag = cursorTag;
|
|
119
|
-
currentTag =
|
|
122
|
+
currentTag = latestRelease.tag_name;
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -142,7 +145,7 @@ async function main() {
|
|
|
142
145
|
state,
|
|
143
146
|
analysis.previous_tag,
|
|
144
147
|
analysis.current_tag,
|
|
145
|
-
analysis.
|
|
148
|
+
analysis.is_latest_release,
|
|
146
149
|
);
|
|
147
150
|
if (next !== state) {
|
|
148
151
|
editIssueBody(args.repo, tracker.number, renderTrackerBody(next));
|