@letta-ai/letta-code 0.31.5 → 0.31.7
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 +5 -1
- package/dist/agent-presets.js.map +2 -2
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/attached-repositories.d.ts +7 -0
- package/dist/types/agent/attached-repositories.d.ts.map +1 -0
- package/dist/types/agent/client-skills.d.ts +1 -1
- package/dist/types/agent/client-skills.d.ts.map +1 -1
- package/dist/types/agent/memory-constraints.d.ts +22 -0
- package/dist/types/agent/memory-constraints.d.ts.map +1 -0
- package/dist/types/agent/memory-git-hooks.d.ts +5 -5
- package/dist/types/agent/memory-git-hooks.d.ts.map +1 -1
- package/dist/types/agent/memory-git.d.ts +36 -5
- package/dist/types/agent/memory-git.d.ts.map +1 -1
- package/dist/types/agent/shared-memory-skills.d.ts +1 -1
- package/dist/types/agent/shared-memory-skills.d.ts.map +1 -1
- package/dist/types/backend/backend.d.ts +7 -0
- package/dist/types/backend/backend.d.ts.map +1 -1
- package/dist/types/backend/dev/pi-model-factory.d.ts +0 -1
- package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
- package/dist/types/backend/local/local-model-config.d.ts.map +1 -1
- package/dist/types/tools/impl/skill.d.ts +1 -1
- package/dist/types/tools/impl/skill.d.ts.map +1 -1
- package/dist/types/utils/secrets-store.d.ts +4 -0
- package/dist/types/utils/secrets-store.d.ts.map +1 -1
- package/letta.js +20198 -20036
- package/package.json +2 -2
- package/scripts/agent-watch/verify-pr-identity.test.ts +97 -0
- package/scripts/agent-watch/verify-pr-identity.ts +119 -0
- package/scripts/builtin-skills-watch/aggregate-results.ts +13 -8
- package/scripts/builtin-skills-watch/evidence.test.ts +61 -17
- package/scripts/builtin-skills-watch/evidence.ts +66 -38
- package/scripts/builtin-skills-watch/finalize-result.test.ts +125 -0
- package/scripts/builtin-skills-watch/finalize-result.ts +44 -0
- package/scripts/builtin-skills-watch/reconcile-results.test.ts +58 -0
- package/scripts/builtin-skills-watch/reconcile-results.ts +311 -0
- package/scripts/builtin-skills-watch/result-artifacts.test.ts +83 -0
- package/scripts/builtin-skills-watch/result-artifacts.ts +160 -0
- package/scripts/builtin-skills-watch/update-tracker.test.ts +17 -1
- package/scripts/builtin-skills-watch/update-tracker.ts +89 -15
- package/scripts/claude-watch/update-tracker.test.ts +30 -0
- package/scripts/claude-watch/update-tracker.ts +19 -5
- package/scripts/codex-watch/update-tracker.test.ts +28 -0
- package/scripts/codex-watch/update-tracker.ts +59 -7
- package/scripts/source-file-size-baseline.json +1 -1
- package/skills/letta-guide/SKILL.md +6 -2
- package/skills/managing-shared-memory/SKILL.md +4 -5
- package/skills/submitting-feedback/SKILL.md +21 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Validates and encodes one agent-authored review result for the runner. */
|
|
3
|
+
|
|
4
|
+
import { readAnalysis, readReviewResult } from "./update-tracker.ts";
|
|
5
|
+
|
|
6
|
+
interface Args {
|
|
7
|
+
analysisFile: string | null;
|
|
8
|
+
resultFile: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv: string[]): Args {
|
|
12
|
+
const args: Args = { analysisFile: null, resultFile: null };
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const arg = argv[index];
|
|
15
|
+
if (arg === "--analysis-file") {
|
|
16
|
+
args.analysisFile = argv[++index] ?? null;
|
|
17
|
+
} else if (arg === "--result-file") {
|
|
18
|
+
args.resultFile = argv[++index] ?? null;
|
|
19
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
20
|
+
console.log(
|
|
21
|
+
"Usage: bun scripts/builtin-skills-watch/finalize-result.ts --analysis-file FILE --result-file FILE",
|
|
22
|
+
);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!args.analysisFile || !args.resultFile) {
|
|
29
|
+
throw new Error("--analysis-file and --result-file are required");
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function main(): void {
|
|
35
|
+
const args = parseArgs(process.argv.slice(2));
|
|
36
|
+
const analysis = readAnalysis(args.analysisFile as string);
|
|
37
|
+
const result = readReviewResult(args.resultFile as string, analysis);
|
|
38
|
+
const encoded = Buffer.from(JSON.stringify(result), "utf8").toString(
|
|
39
|
+
"base64",
|
|
40
|
+
);
|
|
41
|
+
console.log(`SKILL_WATCH_RESULT ${encoded}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (import.meta.main) main();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type CandidatePullRequest,
|
|
4
|
+
selectCanonicalPullRequest,
|
|
5
|
+
} from "./reconcile-results.ts";
|
|
6
|
+
|
|
7
|
+
describe("pending watcher PR reconciliation", () => {
|
|
8
|
+
test("prefers a merged PR and closes every open duplicate", () => {
|
|
9
|
+
const merged = pullRequest(4067, "MERGED");
|
|
10
|
+
const firstDuplicate = pullRequest(4077, "OPEN");
|
|
11
|
+
const secondDuplicate = pullRequest(4099, "OPEN");
|
|
12
|
+
|
|
13
|
+
expect(
|
|
14
|
+
selectCanonicalPullRequest([firstDuplicate, secondDuplicate, merged]),
|
|
15
|
+
).toEqual({
|
|
16
|
+
canonical: merged,
|
|
17
|
+
duplicateOpen: [firstDuplicate, secondDuplicate],
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("uses the oldest open PR when no candidate was merged", () => {
|
|
22
|
+
const older = pullRequest(4078, "OPEN");
|
|
23
|
+
const newer = pullRequest(4098, "OPEN");
|
|
24
|
+
|
|
25
|
+
expect(selectCanonicalPullRequest([newer, older])).toEqual({
|
|
26
|
+
canonical: older,
|
|
27
|
+
duplicateOpen: [newer],
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("ignores closed unmerged PRs and rejects multiple merged PRs", () => {
|
|
32
|
+
expect(selectCanonicalPullRequest([pullRequest(1, "CLOSED")])).toEqual({
|
|
33
|
+
canonical: null,
|
|
34
|
+
duplicateOpen: [],
|
|
35
|
+
});
|
|
36
|
+
expect(() =>
|
|
37
|
+
selectCanonicalPullRequest([
|
|
38
|
+
pullRequest(1, "MERGED"),
|
|
39
|
+
pullRequest(2, "MERGED"),
|
|
40
|
+
]),
|
|
41
|
+
).toThrow("Multiple merged watcher PRs");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function pullRequest(number: number, state: string): CandidatePullRequest {
|
|
46
|
+
return {
|
|
47
|
+
number,
|
|
48
|
+
author: { login: "amelia-letta" },
|
|
49
|
+
baseRefName: "main",
|
|
50
|
+
body: "Builtin-skill-watch: creating-skills@aaaaaaaaaaaa-0000000000000001",
|
|
51
|
+
files: [{ path: "src/skills/builtin/creating-skills/SKILL.md" }],
|
|
52
|
+
headRefOid: number.toString(16).padStart(40, "0"),
|
|
53
|
+
isDraft: state === "OPEN",
|
|
54
|
+
mergedAt: state === "MERGED" ? "2026-08-27T00:00:00Z" : null,
|
|
55
|
+
state,
|
|
56
|
+
url: `https://github.com/letta-ai/letta-code/pull/${number}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/** Reconciles pending tracker candidates against PR side effects before retries. */
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type BuiltinSkillWatchAnalysis,
|
|
6
|
+
buildAnalysis,
|
|
7
|
+
DEFAULT_TARGET_REPO,
|
|
8
|
+
listBuiltinSkillsAtCommit,
|
|
9
|
+
} from "./analysis.ts";
|
|
10
|
+
import type { ReviewEvidence } from "./evidence.ts";
|
|
11
|
+
import { createIssueComment, editIssueBody, ghJson, runGh } from "./github.ts";
|
|
12
|
+
import {
|
|
13
|
+
parseTrackerState,
|
|
14
|
+
recordOutcome,
|
|
15
|
+
renderTrackerBody,
|
|
16
|
+
} from "./tracker.ts";
|
|
17
|
+
import {
|
|
18
|
+
getOpenTrackerIssue,
|
|
19
|
+
type PullRequestView,
|
|
20
|
+
verifyReconciledPullRequest,
|
|
21
|
+
} from "./update-tracker.ts";
|
|
22
|
+
|
|
23
|
+
interface Args {
|
|
24
|
+
repo: string;
|
|
25
|
+
trackerIssue: number | null;
|
|
26
|
+
expectedGithubLogin: string | null;
|
|
27
|
+
dryRun: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CandidatePullRequest extends PullRequestView {
|
|
31
|
+
mergedAt: string | null;
|
|
32
|
+
number: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PullRequestReconciliation {
|
|
36
|
+
canonical: CandidatePullRequest | null;
|
|
37
|
+
duplicateOpen: CandidatePullRequest[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv: string[]): Args {
|
|
41
|
+
const args: Args = {
|
|
42
|
+
repo: DEFAULT_TARGET_REPO,
|
|
43
|
+
trackerIssue: null,
|
|
44
|
+
expectedGithubLogin: null,
|
|
45
|
+
dryRun: false,
|
|
46
|
+
};
|
|
47
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
48
|
+
const arg = argv[index];
|
|
49
|
+
if (arg === "--repo") args.repo = argv[++index] ?? args.repo;
|
|
50
|
+
else if (arg === "--tracker-issue") {
|
|
51
|
+
args.trackerIssue = Number(argv[++index]);
|
|
52
|
+
} else if (arg === "--expected-github-login") {
|
|
53
|
+
args.expectedGithubLogin = argv[++index] ?? null;
|
|
54
|
+
} else if (arg === "--dry-run") args.dryRun = true;
|
|
55
|
+
else if (arg === "--help" || arg === "-h") {
|
|
56
|
+
console.log(
|
|
57
|
+
"Usage: bun scripts/builtin-skills-watch/reconcile-results.ts --tracker-issue ISSUE --expected-github-login LOGIN [--repo OWNER/REPO] [--dry-run]",
|
|
58
|
+
);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
} else {
|
|
61
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!args.trackerIssue || Number.isNaN(args.trackerIssue)) {
|
|
65
|
+
throw new Error("--tracker-issue is required");
|
|
66
|
+
}
|
|
67
|
+
if (!args.expectedGithubLogin) {
|
|
68
|
+
throw new Error("--expected-github-login is required");
|
|
69
|
+
}
|
|
70
|
+
return args;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main(): void {
|
|
74
|
+
const args = parseArgs(process.argv.slice(2));
|
|
75
|
+
assertAuthenticatedLogin(args.expectedGithubLogin as string);
|
|
76
|
+
const tracker = getOpenTrackerIssue(args.repo, args.trackerIssue as number);
|
|
77
|
+
let state = parseTrackerState(tracker.body);
|
|
78
|
+
const inventory = listBuiltinSkillsAtCommit("HEAD");
|
|
79
|
+
let reconciled = 0;
|
|
80
|
+
|
|
81
|
+
for (const skill of Object.keys(state.pending).sort()) {
|
|
82
|
+
const pending = state.pending[skill];
|
|
83
|
+
if (!pending) continue;
|
|
84
|
+
const analysis = buildAnalysis({
|
|
85
|
+
skill,
|
|
86
|
+
currentSha: pending.current_sha,
|
|
87
|
+
auditAt: pending.audit_at,
|
|
88
|
+
previousAudit: previousAudit(state, skill),
|
|
89
|
+
});
|
|
90
|
+
analysis.workflow_run_url = workflowRunUrl(pending.workflow_run_id);
|
|
91
|
+
const pullRequests = findCandidatePullRequests(
|
|
92
|
+
args.repo,
|
|
93
|
+
analysis.candidate_id,
|
|
94
|
+
);
|
|
95
|
+
const selected = selectCanonicalPullRequest(pullRequests);
|
|
96
|
+
if (!selected.canonical) continue;
|
|
97
|
+
|
|
98
|
+
verifyReconciledPullRequest(
|
|
99
|
+
args.repo,
|
|
100
|
+
selected.canonical.url,
|
|
101
|
+
analysis,
|
|
102
|
+
args.expectedGithubLogin as string,
|
|
103
|
+
);
|
|
104
|
+
for (const duplicate of selected.duplicateOpen) {
|
|
105
|
+
verifyReconciledPullRequest(
|
|
106
|
+
args.repo,
|
|
107
|
+
duplicate.url,
|
|
108
|
+
analysis,
|
|
109
|
+
args.expectedGithubLogin as string,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (args.dryRun) {
|
|
114
|
+
console.log(
|
|
115
|
+
`${analysis.skill}: would reconcile ${selected.canonical.url}${renderDuplicatePlan(selected.duplicateOpen)}`,
|
|
116
|
+
);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const duplicate of selected.duplicateOpen) {
|
|
121
|
+
closeDuplicatePullRequest(args.repo, duplicate, selected.canonical);
|
|
122
|
+
}
|
|
123
|
+
const evidence = reconciliationEvidence(analysis, selected.canonical);
|
|
124
|
+
const evidenceUrl = createIssueComment(
|
|
125
|
+
args.repo,
|
|
126
|
+
args.trackerIssue as number,
|
|
127
|
+
renderEvidenceComment(analysis, selected.canonical, evidence),
|
|
128
|
+
);
|
|
129
|
+
state = recordOutcome(state, {
|
|
130
|
+
analysis,
|
|
131
|
+
outcome: "pr_created",
|
|
132
|
+
notes: reconciliationNotes(selected.canonical),
|
|
133
|
+
prUrl: selected.canonical.url,
|
|
134
|
+
evidence,
|
|
135
|
+
evidenceUrl,
|
|
136
|
+
});
|
|
137
|
+
reconciled += 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (args.dryRun) return;
|
|
141
|
+
const latest = getOpenTrackerIssue(args.repo, args.trackerIssue as number);
|
|
142
|
+
if (latest.body !== tracker.body) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"Tracker body changed while PR side effects were reconciled",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (reconciled > 0) {
|
|
148
|
+
editIssueBody(
|
|
149
|
+
args.repo,
|
|
150
|
+
args.trackerIssue as number,
|
|
151
|
+
renderTrackerBody(state, inventory),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
console.log(`Reconciled ${reconciled} pending built-in skill candidates`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function selectCanonicalPullRequest(
|
|
158
|
+
pullRequests: CandidatePullRequest[],
|
|
159
|
+
): PullRequestReconciliation {
|
|
160
|
+
const merged = pullRequests
|
|
161
|
+
.filter((pullRequest) => pullRequest.state === "MERGED")
|
|
162
|
+
.sort(byPullRequestNumber);
|
|
163
|
+
if (merged.length > 1) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Multiple merged watcher PRs found for one candidate: ${merged.map((pullRequest) => `#${pullRequest.number}`).join(", ")}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const open = pullRequests
|
|
169
|
+
.filter((pullRequest) => pullRequest.state === "OPEN")
|
|
170
|
+
.sort(byPullRequestNumber);
|
|
171
|
+
const canonical = merged[0] ?? open[0] ?? null;
|
|
172
|
+
return {
|
|
173
|
+
canonical,
|
|
174
|
+
duplicateOpen: canonical
|
|
175
|
+
? open.filter((pullRequest) => pullRequest.number !== canonical.number)
|
|
176
|
+
: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function findCandidatePullRequests(
|
|
181
|
+
repo: string,
|
|
182
|
+
candidateId: string,
|
|
183
|
+
): CandidatePullRequest[] {
|
|
184
|
+
const marker = `Builtin-skill-watch: ${candidateId}`;
|
|
185
|
+
return ghJson<CandidatePullRequest[]>([
|
|
186
|
+
"pr",
|
|
187
|
+
"list",
|
|
188
|
+
"--repo",
|
|
189
|
+
repo,
|
|
190
|
+
"--state",
|
|
191
|
+
"all",
|
|
192
|
+
"--search",
|
|
193
|
+
`${marker} in:body`,
|
|
194
|
+
"--limit",
|
|
195
|
+
"20",
|
|
196
|
+
"--json",
|
|
197
|
+
"number,state,isDraft,mergedAt,author,baseRefName,headRefOid,body,files,url",
|
|
198
|
+
]).filter((pullRequest) => hasExactMarker(pullRequest.body, marker));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function assertAuthenticatedLogin(expectedGithubLogin: string): void {
|
|
202
|
+
const authenticated = ghJson<{ login: string }>(["api", "user"]).login;
|
|
203
|
+
if (authenticated !== expectedGithubLogin) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Authenticated GitHub login ${authenticated} does not match ${expectedGithubLogin}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function closeDuplicatePullRequest(
|
|
211
|
+
repo: string,
|
|
212
|
+
duplicate: CandidatePullRequest,
|
|
213
|
+
canonical: CandidatePullRequest,
|
|
214
|
+
): void {
|
|
215
|
+
runGh([
|
|
216
|
+
"pr",
|
|
217
|
+
"close",
|
|
218
|
+
String(duplicate.number),
|
|
219
|
+
"--repo",
|
|
220
|
+
repo,
|
|
221
|
+
"--comment",
|
|
222
|
+
`Closing as a duplicate of ${canonical.url}, which has the same exact built-in skill watcher candidate.`,
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function reconciliationEvidence(
|
|
227
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
228
|
+
pullRequest: CandidatePullRequest,
|
|
229
|
+
): ReviewEvidence {
|
|
230
|
+
const status = pullRequest.state === "MERGED" ? "merged" : "open draft";
|
|
231
|
+
return {
|
|
232
|
+
schema_version: 1,
|
|
233
|
+
candidate_id: analysis.candidate_id,
|
|
234
|
+
skill: analysis.skill,
|
|
235
|
+
sources: [
|
|
236
|
+
{
|
|
237
|
+
locator: pullRequest.url,
|
|
238
|
+
revision: pullRequest.headRefOid,
|
|
239
|
+
content_digest: null,
|
|
240
|
+
retrieved_at: new Date().toISOString(),
|
|
241
|
+
excerpt: `The ${status} watcher PR has the exact candidate marker and a validated skill-only diff.`,
|
|
242
|
+
claims: [
|
|
243
|
+
"the watcher already created and validated a PR for this exact candidate",
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
probes: [],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderEvidenceComment(
|
|
252
|
+
analysis: BuiltinSkillWatchAnalysis,
|
|
253
|
+
pullRequest: CandidatePullRequest,
|
|
254
|
+
evidence: ReviewEvidence,
|
|
255
|
+
): string {
|
|
256
|
+
return [
|
|
257
|
+
`## Reconciled built-in skill audit: ${analysis.skill}`,
|
|
258
|
+
"",
|
|
259
|
+
`Candidate: \`${analysis.candidate_id}\``,
|
|
260
|
+
`PR: ${pullRequest.url}`,
|
|
261
|
+
`PR state: \`${pullRequest.state.toLowerCase()}\``,
|
|
262
|
+
"",
|
|
263
|
+
"```json",
|
|
264
|
+
JSON.stringify(evidence, null, 2),
|
|
265
|
+
"```",
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function reconciliationNotes(pullRequest: CandidatePullRequest): string {
|
|
270
|
+
return pullRequest.state === "MERGED"
|
|
271
|
+
? `reconciled merged watcher PR #${pullRequest.number}`
|
|
272
|
+
: `reconciled open draft watcher PR #${pullRequest.number}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderDuplicatePlan(duplicates: CandidatePullRequest[]): string {
|
|
276
|
+
return duplicates.length === 0
|
|
277
|
+
? ""
|
|
278
|
+
: ` and close duplicate ${duplicates.map((pullRequest) => `#${pullRequest.number}`).join(", ")}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function previousAudit(
|
|
282
|
+
state: ReturnType<typeof parseTrackerState>,
|
|
283
|
+
skill: string,
|
|
284
|
+
): BuiltinSkillWatchAnalysis["previous_audit"] {
|
|
285
|
+
const audit = state.skills[skill];
|
|
286
|
+
return audit
|
|
287
|
+
? {
|
|
288
|
+
candidate_id: audit.candidate_id,
|
|
289
|
+
audited_sha: audit.audited_sha,
|
|
290
|
+
skill_digest: audit.skill_digest,
|
|
291
|
+
audited_at: audit.audited_at,
|
|
292
|
+
}
|
|
293
|
+
: null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function workflowRunUrl(runId: string): string {
|
|
297
|
+
return `https://github.com/letta-ai/letta-code/actions/runs/${runId}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function hasExactMarker(body: string, marker: string): boolean {
|
|
301
|
+
return body.split(/\r?\n/).some((line) => line.trim() === marker);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function byPullRequestNumber(
|
|
305
|
+
left: CandidatePullRequest,
|
|
306
|
+
right: CandidatePullRequest,
|
|
307
|
+
): number {
|
|
308
|
+
return left.number - right.number;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (import.meta.main) main();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
discoverReviewArtifacts,
|
|
7
|
+
formatFailureReceipt,
|
|
8
|
+
} from "./result-artifacts.ts";
|
|
9
|
+
|
|
10
|
+
const CANDIDATE = "creating-skills@aaaaaaaaaaaa-abcdef0123456789";
|
|
11
|
+
|
|
12
|
+
describe("review artifact discovery", () => {
|
|
13
|
+
test("finds both nested and root-level result artifacts", () => {
|
|
14
|
+
const directory = mkdtempSync(join(tmpdir(), "skill-artifacts-"));
|
|
15
|
+
try {
|
|
16
|
+
const nested = join(directory, "builtin-skill-result-creating-skills");
|
|
17
|
+
mkdirSync(nested);
|
|
18
|
+
writeFileSync(
|
|
19
|
+
join(nested, "result.json"),
|
|
20
|
+
JSON.stringify({ candidate_id: CANDIDATE }),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const discovered = discoverReviewArtifacts(directory);
|
|
24
|
+
expect(discovered.results.get(CANDIDATE)).toBe(
|
|
25
|
+
join(nested, "result.json"),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
rmSync(nested, { recursive: true });
|
|
29
|
+
writeFileSync(
|
|
30
|
+
join(directory, "result.json"),
|
|
31
|
+
JSON.stringify({ candidate_id: CANDIDATE }),
|
|
32
|
+
);
|
|
33
|
+
expect(discoverReviewArtifacts(directory).results.get(CANDIDATE)).toBe(
|
|
34
|
+
join(directory, "result.json"),
|
|
35
|
+
);
|
|
36
|
+
} finally {
|
|
37
|
+
rmSync(directory, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("preserves a runner-authored failure receipt", () => {
|
|
42
|
+
const directory = mkdtempSync(join(tmpdir(), "skill-artifacts-"));
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(
|
|
45
|
+
join(directory, "failure.json"),
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
schema_version: 1,
|
|
48
|
+
candidate_id: CANDIDATE,
|
|
49
|
+
skill: "creating-skills",
|
|
50
|
+
kind: "action_failed",
|
|
51
|
+
message: "Letta Code Action failed before returning a result",
|
|
52
|
+
conversation_id: "conv-123",
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const receipt =
|
|
57
|
+
discoverReviewArtifacts(directory).failures.get(CANDIDATE);
|
|
58
|
+
expect(receipt).toBeDefined();
|
|
59
|
+
expect(formatFailureReceipt(receipt!)).toContain("conversation conv-123");
|
|
60
|
+
} finally {
|
|
61
|
+
rmSync(directory, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("rejects duplicate results for one candidate", () => {
|
|
66
|
+
const directory = mkdtempSync(join(tmpdir(), "skill-artifacts-"));
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(join(directory, "first"));
|
|
69
|
+
mkdirSync(join(directory, "second"));
|
|
70
|
+
for (const subdirectory of ["first", "second"]) {
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(directory, subdirectory, "result.json"),
|
|
73
|
+
JSON.stringify({ candidate_id: CANDIDATE }),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
expect(() => discoverReviewArtifacts(directory)).toThrow(
|
|
77
|
+
`duplicate review result for ${CANDIDATE}`,
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(directory, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface ReviewFailureReceipt {
|
|
5
|
+
schema_version: 1;
|
|
6
|
+
candidate_id: string;
|
|
7
|
+
skill: string;
|
|
8
|
+
kind:
|
|
9
|
+
| "action_failed"
|
|
10
|
+
| "execution_file_missing"
|
|
11
|
+
| "result_marker_missing"
|
|
12
|
+
| "result_decode_failed";
|
|
13
|
+
message: string;
|
|
14
|
+
conversation_id: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReviewArtifacts {
|
|
18
|
+
results: Map<string, string>;
|
|
19
|
+
failures: Map<string, ReviewFailureReceipt>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function discoverReviewArtifacts(root: string): ReviewArtifacts {
|
|
23
|
+
const artifacts: ReviewArtifacts = {
|
|
24
|
+
results: new Map(),
|
|
25
|
+
failures: new Map(),
|
|
26
|
+
};
|
|
27
|
+
for (const path of walkFiles(root)) {
|
|
28
|
+
const name = basename(path);
|
|
29
|
+
if (name === "result.json") {
|
|
30
|
+
const candidateId = readCandidateId(path, "review result");
|
|
31
|
+
addUnique(artifacts.results, candidateId, path, "review result");
|
|
32
|
+
} else if (name === "failure.json") {
|
|
33
|
+
const receipt = parseFailureReceipt(
|
|
34
|
+
JSON.parse(readFileSync(path, "utf8")) as unknown,
|
|
35
|
+
);
|
|
36
|
+
addUnique(
|
|
37
|
+
artifacts.failures,
|
|
38
|
+
receipt.candidate_id,
|
|
39
|
+
receipt,
|
|
40
|
+
"failure receipt",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return artifacts;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatFailureReceipt(receipt: ReviewFailureReceipt): string {
|
|
48
|
+
const conversation = receipt.conversation_id
|
|
49
|
+
? ` (conversation ${receipt.conversation_id})`
|
|
50
|
+
: "";
|
|
51
|
+
return `${receipt.kind}: ${receipt.message}${conversation}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walkFiles(root: string): string[] {
|
|
55
|
+
const files: string[] = [];
|
|
56
|
+
let entries: ReturnType<typeof readdirSync>;
|
|
57
|
+
try {
|
|
58
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (isMissingPath(error)) return files;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const path = join(root, entry.name);
|
|
65
|
+
if (entry.isDirectory()) files.push(...walkFiles(path));
|
|
66
|
+
else if (entry.isFile()) files.push(path);
|
|
67
|
+
}
|
|
68
|
+
return files.sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readCandidateId(path: string, description: string): string {
|
|
72
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
73
|
+
if (!isRecord(value) || !isCandidateId(value.candidate_id)) {
|
|
74
|
+
throw new Error(`${description} ${path} has no valid candidate_id`);
|
|
75
|
+
}
|
|
76
|
+
return value.candidate_id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseFailureReceipt(value: unknown): ReviewFailureReceipt {
|
|
80
|
+
if (
|
|
81
|
+
!isRecord(value) ||
|
|
82
|
+
!hasExactKeys(value, [
|
|
83
|
+
"schema_version",
|
|
84
|
+
"candidate_id",
|
|
85
|
+
"skill",
|
|
86
|
+
"kind",
|
|
87
|
+
"message",
|
|
88
|
+
"conversation_id",
|
|
89
|
+
]) ||
|
|
90
|
+
value.schema_version !== 1 ||
|
|
91
|
+
!isCandidateId(value.candidate_id) ||
|
|
92
|
+
!isSkillName(value.skill) ||
|
|
93
|
+
!isFailureKind(value.kind) ||
|
|
94
|
+
typeof value.message !== "string" ||
|
|
95
|
+
value.message.length === 0 ||
|
|
96
|
+
value.message.length > 500 ||
|
|
97
|
+
(value.conversation_id !== null && !isConversationId(value.conversation_id))
|
|
98
|
+
) {
|
|
99
|
+
throw new Error("Review failure receipt is invalid");
|
|
100
|
+
}
|
|
101
|
+
return value as unknown as ReviewFailureReceipt;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function addUnique<T>(
|
|
105
|
+
values: Map<string, T>,
|
|
106
|
+
candidateId: string,
|
|
107
|
+
value: T,
|
|
108
|
+
description: string,
|
|
109
|
+
): void {
|
|
110
|
+
if (values.has(candidateId)) {
|
|
111
|
+
throw new Error(`Found duplicate ${description} for ${candidateId}`);
|
|
112
|
+
}
|
|
113
|
+
values.set(candidateId, value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isFailureKind(value: unknown): value is ReviewFailureReceipt["kind"] {
|
|
117
|
+
return (
|
|
118
|
+
value === "action_failed" ||
|
|
119
|
+
value === "execution_file_missing" ||
|
|
120
|
+
value === "result_marker_missing" ||
|
|
121
|
+
value === "result_decode_failed"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isCandidateId(value: unknown): value is string {
|
|
126
|
+
return (
|
|
127
|
+
typeof value === "string" &&
|
|
128
|
+
/^[a-z0-9]+(?:-[a-z0-9]+)*@[a-f0-9]{12}-[a-f0-9]{16}$/.test(value)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isSkillName(value: unknown): value is string {
|
|
133
|
+
return typeof value === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isConversationId(value: unknown): value is string {
|
|
137
|
+
return typeof value === "string" && /^conv-[a-zA-Z0-9-]+$/.test(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isMissingPath(error: unknown): boolean {
|
|
141
|
+
return (
|
|
142
|
+
error instanceof Error &&
|
|
143
|
+
"code" in error &&
|
|
144
|
+
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasExactKeys(
|
|
153
|
+
value: Record<string, unknown>,
|
|
154
|
+
expected: string[],
|
|
155
|
+
): boolean {
|
|
156
|
+
return (
|
|
157
|
+
JSON.stringify(Object.keys(value).sort()) ===
|
|
158
|
+
JSON.stringify([...expected].sort())
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
parseReviewResult,
|
|
7
7
|
type TrackerIssueView,
|
|
8
8
|
validatePullRequestView,
|
|
9
|
+
validateReconciledPullRequestView,
|
|
9
10
|
validateTrackerIssueView,
|
|
10
11
|
} from "./update-tracker.ts";
|
|
11
12
|
|
|
@@ -79,6 +80,21 @@ describe("watcher PR validation", () => {
|
|
|
79
80
|
),
|
|
80
81
|
).toThrow("outside the selected skill scope");
|
|
81
82
|
});
|
|
83
|
+
|
|
84
|
+
test("accepts a merged exact-candidate PR only during reconciliation", () => {
|
|
85
|
+
const merged = validPullRequest({
|
|
86
|
+
isDraft: false,
|
|
87
|
+
mergedAt: "2026-08-27T00:00:00Z",
|
|
88
|
+
state: "MERGED",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(() =>
|
|
92
|
+
validateReconciledPullRequestView(merged, URL, analysis(), LOGIN),
|
|
93
|
+
).not.toThrow();
|
|
94
|
+
expect(() =>
|
|
95
|
+
validatePullRequestView(merged, URL, analysis(), LOGIN),
|
|
96
|
+
).toThrow("open and draft");
|
|
97
|
+
});
|
|
82
98
|
});
|
|
83
99
|
|
|
84
100
|
describe("watcher analysis validation", () => {
|
|
@@ -135,7 +151,7 @@ describe("watcher result validation", () => {
|
|
|
135
151
|
evidence: evidence(current),
|
|
136
152
|
};
|
|
137
153
|
expect(() => parseReviewResult({ ...base, secret: "no" }, current)).toThrow(
|
|
138
|
-
"
|
|
154
|
+
"unknown or missing fields",
|
|
139
155
|
);
|
|
140
156
|
expect(() =>
|
|
141
157
|
parseReviewResult(
|