@letta-ai/letta-code 0.31.6 → 0.31.8
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/mcp-client.js +8 -8
- package/dist/mcp-client.js.map +3 -3
- package/dist/memory-constraints.js +18 -0
- package/dist/memory-constraints.js.map +10 -0
- package/dist/types/agent/memory-constraints.d.ts +1 -1
- package/dist/types/agent/memory-constraints.d.ts.map +1 -1
- package/dist/types/agent/subagents/index.d.ts +0 -2
- package/dist/types/agent/subagents/index.d.ts.map +1 -1
- package/dist/types/agent/subagents/subagent-model.d.ts +20 -0
- package/dist/types/agent/subagents/subagent-model.d.ts.map +1 -1
- package/dist/types/{utils → agent}/system-prompt-size.d.ts +8 -4
- package/dist/types/agent/system-prompt-size.d.ts.map +1 -0
- package/dist/types/backend/backend.d.ts +7 -0
- package/dist/types/backend/backend.d.ts.map +1 -1
- package/dist/types/cli/helpers/system-prompt-warning.d.ts +2 -1
- package/dist/types/cli/helpers/system-prompt-warning.d.ts.map +1 -1
- package/dist/types/mcp-client.d.ts +6 -0
- package/dist/types/mcp-client.d.ts.map +1 -1
- package/dist/types/memory-constraints.d.ts +15 -0
- package/dist/types/memory-constraints.d.ts.map +1 -0
- package/dist/types/permissions/shell-analysis.d.ts.map +1 -1
- package/dist/types/providers/openai-codex-provider.d.ts +2 -10
- package/dist/types/providers/openai-codex-provider.d.ts.map +1 -1
- package/dist/types/tools/impl/task.d.ts +20 -2
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/types/chatgpt-oauth.d.ts +9 -0
- package/dist/types/types/chatgpt-oauth.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +3 -3
- package/dist/types/types/protocol_v2.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 +29378 -28407
- package/package.json +11 -1
- 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 +2 -1
- package/skills/browser-use/SKILL.md +363 -0
- package/skills/browser-use/references/recording.md +60 -0
- package/skills/letta-guide/SKILL.md +6 -2
- package/skills/self-configuration/SKILL.md +3 -1
- package/dist/types/utils/system-prompt-size.d.ts.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { BuiltinSkillWatchAnalysis } from "./analysis.ts";
|
|
6
|
+
|
|
7
|
+
describe("review result finalization", () => {
|
|
8
|
+
test("validates with the runner parser before encoding the result", () => {
|
|
9
|
+
const directory = mkdtempSync(join(tmpdir(), "skill-result-finalizer-"));
|
|
10
|
+
try {
|
|
11
|
+
const analysis = fakeAnalysis();
|
|
12
|
+
const analysisFile = join(directory, "analysis.json");
|
|
13
|
+
const resultFile = join(directory, "result.json");
|
|
14
|
+
writeFileSync(analysisFile, JSON.stringify(analysis));
|
|
15
|
+
writeFileSync(resultFile, JSON.stringify(fakeResult(analysis)));
|
|
16
|
+
|
|
17
|
+
const finalized = Bun.spawnSync([
|
|
18
|
+
"bun",
|
|
19
|
+
"scripts/builtin-skills-watch/finalize-result.ts",
|
|
20
|
+
"--analysis-file",
|
|
21
|
+
analysisFile,
|
|
22
|
+
"--result-file",
|
|
23
|
+
resultFile,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
expect(finalized.exitCode).toBe(0);
|
|
27
|
+
const line = finalized.stdout.toString().trim();
|
|
28
|
+
expect(line).toStartWith("SKILL_WATCH_RESULT ");
|
|
29
|
+
expect(
|
|
30
|
+
JSON.parse(
|
|
31
|
+
Buffer.from(
|
|
32
|
+
line.slice("SKILL_WATCH_RESULT ".length),
|
|
33
|
+
"base64",
|
|
34
|
+
).toString("utf8"),
|
|
35
|
+
),
|
|
36
|
+
).toEqual(fakeResult(analysis));
|
|
37
|
+
} finally {
|
|
38
|
+
rmSync(directory, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("rejects a result for another candidate", () => {
|
|
43
|
+
const directory = mkdtempSync(join(tmpdir(), "skill-result-finalizer-"));
|
|
44
|
+
try {
|
|
45
|
+
const analysis = fakeAnalysis();
|
|
46
|
+
const analysisFile = join(directory, "analysis.json");
|
|
47
|
+
const resultFile = join(directory, "result.json");
|
|
48
|
+
writeFileSync(analysisFile, JSON.stringify(analysis));
|
|
49
|
+
writeFileSync(
|
|
50
|
+
resultFile,
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
...fakeResult(analysis),
|
|
53
|
+
candidate_id: "creating-skills@bbbbbbbbbbbb-abcdef0123456789",
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const finalized = Bun.spawnSync([
|
|
58
|
+
"bun",
|
|
59
|
+
"scripts/builtin-skills-watch/finalize-result.ts",
|
|
60
|
+
"--analysis-file",
|
|
61
|
+
analysisFile,
|
|
62
|
+
"--result-file",
|
|
63
|
+
resultFile,
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
expect(finalized.exitCode).not.toBe(0);
|
|
67
|
+
expect(finalized.stderr.toString()).toContain(
|
|
68
|
+
"does not match the pending candidate",
|
|
69
|
+
);
|
|
70
|
+
} finally {
|
|
71
|
+
rmSync(directory, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function fakeAnalysis(): BuiltinSkillWatchAnalysis {
|
|
77
|
+
const skill = "creating-skills";
|
|
78
|
+
return {
|
|
79
|
+
schema_version: 1,
|
|
80
|
+
candidate_id: `${skill}@${"a".repeat(12)}-abcdef0123456789`,
|
|
81
|
+
skill,
|
|
82
|
+
skill_path: `src/skills/builtin/${skill}`,
|
|
83
|
+
skill_files: [`src/skills/builtin/${skill}/SKILL.md`],
|
|
84
|
+
skill_digest: "b".repeat(64),
|
|
85
|
+
current_sha: "a".repeat(40),
|
|
86
|
+
audit_at: "2026-08-26T00:00:00.000Z",
|
|
87
|
+
previous_audit: null,
|
|
88
|
+
repository_changes: {
|
|
89
|
+
previous_sha: null,
|
|
90
|
+
changed_files: [],
|
|
91
|
+
commits: [],
|
|
92
|
+
history_available: false,
|
|
93
|
+
truncated: false,
|
|
94
|
+
},
|
|
95
|
+
skill_inventory: [skill],
|
|
96
|
+
workflow_run_url: "https://github.com/letta-ai/letta-code/actions/runs/1",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fakeResult(analysis: BuiltinSkillWatchAnalysis) {
|
|
101
|
+
return {
|
|
102
|
+
schema_version: 1,
|
|
103
|
+
candidate_id: analysis.candidate_id,
|
|
104
|
+
skill: analysis.skill,
|
|
105
|
+
outcome: "no_drift",
|
|
106
|
+
notes: "current source and skill agree",
|
|
107
|
+
pr_url: null,
|
|
108
|
+
evidence: {
|
|
109
|
+
schema_version: 1,
|
|
110
|
+
candidate_id: analysis.candidate_id,
|
|
111
|
+
skill: analysis.skill,
|
|
112
|
+
sources: [
|
|
113
|
+
{
|
|
114
|
+
locator: analysis.skill_path,
|
|
115
|
+
revision: analysis.current_sha,
|
|
116
|
+
content_digest: analysis.skill_digest,
|
|
117
|
+
retrieved_at: analysis.audit_at,
|
|
118
|
+
excerpt: "the current skill matches its owning source",
|
|
119
|
+
claims: ["checked current source"],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
probes: [],
|
|
123
|
+
},
|
|
124
|
+
} as const;
|
|
125
|
+
}
|
|
@@ -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
|
+
});
|