@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.
Files changed (61) hide show
  1. package/dist/agent-presets-agent-presets.js +11 -0
  2. package/dist/agent-presets-agent-presets.js.map +9 -0
  3. package/dist/agent-presets-personality-asset-content.js +42 -0
  4. package/dist/agent-presets-personality-asset-content.js.map +10 -0
  5. package/dist/agent-presets.js +17 -2
  6. package/dist/agent-presets.js.map +3 -3
  7. package/dist/channels-public.js +16 -1
  8. package/dist/channels-public.js.map +5 -4
  9. package/dist/gateway-core.js +5 -4
  10. package/dist/gateway-core.js.map +3 -3
  11. package/dist/mcp-client.js +2 -2
  12. package/dist/mcp-client.js.map +1 -1
  13. package/dist/types/agent/create-agent-request.d.ts +3 -0
  14. package/dist/types/agent/create-agent-request.d.ts.map +1 -1
  15. package/dist/types/agent/model.d.ts.map +1 -1
  16. package/dist/types/agent/personality-asset-content.d.ts +3 -0
  17. package/dist/types/agent/personality-asset-content.d.ts.map +1 -0
  18. package/dist/types/agent/skills.d.ts.map +1 -1
  19. package/dist/types/channels/gateway-core.d.ts.map +1 -1
  20. package/dist/types/channels/route-thread-key.d.ts +20 -0
  21. package/dist/types/channels/route-thread-key.d.ts.map +1 -0
  22. package/dist/types/channels-public.d.ts +1 -0
  23. package/dist/types/channels-public.d.ts.map +1 -1
  24. package/dist/types/tools/impl/skill.d.ts.map +1 -1
  25. package/dist/types/types/protocol_v2.d.ts +3 -144
  26. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  27. package/dist/types/types/schedule-protocol.d.ts +151 -1
  28. package/dist/types/types/schedule-protocol.d.ts.map +1 -1
  29. package/dist/types/utils/frontmatter.d.ts.map +1 -1
  30. package/dist/types/websocket/listener/cwd-change.d.ts.map +1 -1
  31. package/letta.js +238 -86
  32. package/package.json +4 -2
  33. package/scripts/builtin-skills-watch/agent-watch.test.ts +104 -0
  34. package/scripts/builtin-skills-watch/agent-watch.ts +337 -0
  35. package/scripts/builtin-skills-watch/aggregate-results.test.ts +79 -0
  36. package/scripts/builtin-skills-watch/aggregate-results.ts +326 -0
  37. package/scripts/builtin-skills-watch/analysis.test.ts +70 -0
  38. package/scripts/builtin-skills-watch/analysis.ts +228 -0
  39. package/scripts/builtin-skills-watch/evidence.test.ts +114 -0
  40. package/scripts/builtin-skills-watch/evidence.ts +145 -0
  41. package/scripts/builtin-skills-watch/github.ts +161 -0
  42. package/scripts/builtin-skills-watch/tracker.test.ts +249 -0
  43. package/scripts/builtin-skills-watch/tracker.ts +506 -0
  44. package/scripts/builtin-skills-watch/update-tracker.test.ts +234 -0
  45. package/scripts/builtin-skills-watch/update-tracker.ts +508 -0
  46. package/scripts/claude-watch/release-source.test.ts +16 -4
  47. package/scripts/claude-watch/release-source.ts +41 -2
  48. package/scripts/codex-watch/agent-watch.ts +8 -5
  49. package/scripts/codex-watch/release-analysis.test.ts +18 -16
  50. package/scripts/codex-watch/release-analysis.ts +31 -9
  51. package/scripts/codex-watch/tracker.test.ts +5 -5
  52. package/scripts/codex-watch/tracker.ts +7 -8
  53. package/scripts/pi-ai-watch/agent-watch.ts +9 -14
  54. package/scripts/pi-ai-watch/release-analysis.test.ts +24 -18
  55. package/scripts/pi-ai-watch/release-analysis.ts +85 -45
  56. package/scripts/pi-ai-watch/tracker.test.ts +25 -18
  57. package/scripts/pi-ai-watch/tracker.ts +7 -19
  58. package/scripts/pi-ai-watch/update-tracker.ts +2 -2
  59. package/scripts/source-file-size-baseline.json +2 -2
  60. package/skills/dispatching-coding-agents/SKILL.md +16 -7
  61. package/skills/syncing-memory-filesystem/SKILL.md +118 -226
@@ -0,0 +1,114 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { digestReviewEvidence, parseReviewEvidence } from "./evidence.ts";
3
+
4
+ describe("review evidence", () => {
5
+ test("accepts bounded source and probe evidence", () => {
6
+ const evidence = parseReviewEvidence({
7
+ schema_version: 1,
8
+ candidate_id: "creating-skills@abc-candidate",
9
+ skill: "creating-skills",
10
+ sources: [
11
+ {
12
+ locator: "src/skills/builtin/creating-skills/SKILL.md",
13
+ revision: "a".repeat(40),
14
+ content_digest: "b".repeat(64),
15
+ retrieved_at: "2026-08-26T00:00:00.000Z",
16
+ excerpt: "name must match the skill directory",
17
+ claims: ["frontmatter rules match the validator"],
18
+ },
19
+ ],
20
+ probes: [
21
+ {
22
+ command: "bun test src/skills/builtin/creating-skills",
23
+ result_digest: "c".repeat(64),
24
+ summary: "validator tests passed",
25
+ },
26
+ ],
27
+ });
28
+
29
+ expect(digestReviewEvidence(evidence)).toMatch(/^[a-f0-9]{64}$/);
30
+ });
31
+
32
+ test("requires at least one source", () => {
33
+ expect(() =>
34
+ parseReviewEvidence({
35
+ schema_version: 1,
36
+ candidate_id: "candidate",
37
+ skill: "creating-skills",
38
+ sources: [],
39
+ probes: [],
40
+ }),
41
+ ).toThrow("sources are invalid");
42
+ });
43
+
44
+ test("rejects malformed digests and timestamps", () => {
45
+ expect(() =>
46
+ parseReviewEvidence({
47
+ schema_version: 1,
48
+ candidate_id: "candidate",
49
+ skill: "creating-skills",
50
+ sources: [
51
+ {
52
+ locator: "https://docs.letta.com/",
53
+ revision: null,
54
+ content_digest: "not-a-digest",
55
+ retrieved_at: "yesterday",
56
+ excerpt: "current docs text",
57
+ claims: ["checked docs"],
58
+ },
59
+ ],
60
+ probes: [],
61
+ }),
62
+ ).toThrow("sources are invalid");
63
+ });
64
+
65
+ test("rejects evidence too large for the tracker", () => {
66
+ expect(() =>
67
+ parseReviewEvidence({
68
+ schema_version: 1,
69
+ candidate_id: "candidate",
70
+ skill: "creating-skills",
71
+ sources: [
72
+ {
73
+ locator: "x".repeat(500),
74
+ revision: "y".repeat(300),
75
+ content_digest: null,
76
+ retrieved_at: "2026-08-26T00:00:00.000Z",
77
+ excerpt: "q".repeat(300),
78
+ claims: ["z".repeat(500)],
79
+ },
80
+ ],
81
+ probes: [],
82
+ }),
83
+ ).toThrow("exceeds 650 bytes");
84
+ });
85
+
86
+ test("requires a revision or digest and rejects unknown fields", () => {
87
+ const source = {
88
+ locator: "https://docs.letta.com/",
89
+ revision: null,
90
+ content_digest: null,
91
+ retrieved_at: "2026-08-26T00:00:00.000Z",
92
+ excerpt: "current docs text",
93
+ claims: ["checked docs"],
94
+ };
95
+ expect(() =>
96
+ parseReviewEvidence({
97
+ schema_version: 1,
98
+ candidate_id: "candidate",
99
+ skill: "creating-skills",
100
+ sources: [source],
101
+ probes: [],
102
+ }),
103
+ ).toThrow("sources are invalid");
104
+ expect(() =>
105
+ parseReviewEvidence({
106
+ schema_version: 1,
107
+ candidate_id: "candidate",
108
+ skill: "creating-skills",
109
+ sources: [{ ...source, content_digest: "a".repeat(64), secret: "no" }],
110
+ probes: [],
111
+ }),
112
+ ).toThrow("sources are invalid");
113
+ });
114
+ });
@@ -0,0 +1,145 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const MAX_SOURCES = 5;
4
+ const MAX_PROBES = 5;
5
+ const MAX_CLAIMS_PER_SOURCE = 5;
6
+ const MAX_SERIALIZED_BYTES = 650;
7
+
8
+ export interface EvidenceSource {
9
+ locator: string;
10
+ revision: string | null;
11
+ content_digest: string | null;
12
+ retrieved_at: string;
13
+ excerpt: string;
14
+ claims: string[];
15
+ }
16
+
17
+ export interface EvidenceProbe {
18
+ command: string;
19
+ result_digest: string;
20
+ summary: string;
21
+ }
22
+
23
+ export interface ReviewEvidence {
24
+ schema_version: 1;
25
+ candidate_id: string;
26
+ skill: string;
27
+ sources: EvidenceSource[];
28
+ probes: EvidenceProbe[];
29
+ }
30
+
31
+ export function parseReviewEvidence(value: unknown): ReviewEvidence {
32
+ if (
33
+ !isRecord(value) ||
34
+ !hasExactKeys(value, [
35
+ "schema_version",
36
+ "candidate_id",
37
+ "skill",
38
+ "sources",
39
+ "probes",
40
+ ]) ||
41
+ value.schema_version !== 1
42
+ ) {
43
+ throw new TypeError("review evidence must use schema version 1");
44
+ }
45
+ if (!isBoundedString(value.candidate_id, 300)) {
46
+ throw new TypeError("review evidence candidate_id is invalid");
47
+ }
48
+ if (!isBoundedString(value.skill, 100)) {
49
+ throw new TypeError("review evidence skill is invalid");
50
+ }
51
+ if (
52
+ !Array.isArray(value.sources) ||
53
+ value.sources.length === 0 ||
54
+ value.sources.length > MAX_SOURCES ||
55
+ !value.sources.every(isEvidenceSource)
56
+ ) {
57
+ throw new TypeError("review evidence sources are invalid");
58
+ }
59
+ if (
60
+ !Array.isArray(value.probes) ||
61
+ value.probes.length > MAX_PROBES ||
62
+ !value.probes.every(isEvidenceProbe)
63
+ ) {
64
+ throw new TypeError("review evidence probes are invalid");
65
+ }
66
+ const evidence: ReviewEvidence = {
67
+ schema_version: 1,
68
+ candidate_id: value.candidate_id,
69
+ skill: value.skill,
70
+ sources: value.sources,
71
+ probes: value.probes,
72
+ };
73
+ if (
74
+ Buffer.byteLength(JSON.stringify(evidence), "utf8") > MAX_SERIALIZED_BYTES
75
+ ) {
76
+ throw new TypeError("review evidence exceeds 650 bytes");
77
+ }
78
+ return evidence;
79
+ }
80
+
81
+ export function digestReviewEvidence(evidence: ReviewEvidence): string {
82
+ return createHash("sha256").update(JSON.stringify(evidence)).digest("hex");
83
+ }
84
+
85
+ function isEvidenceSource(value: unknown): value is EvidenceSource {
86
+ return (
87
+ isRecord(value) &&
88
+ hasExactKeys(value, [
89
+ "locator",
90
+ "revision",
91
+ "content_digest",
92
+ "retrieved_at",
93
+ "excerpt",
94
+ "claims",
95
+ ]) &&
96
+ isBoundedString(value.locator, 500) &&
97
+ (value.revision === null || isBoundedString(value.revision, 300)) &&
98
+ (value.content_digest === null || isDigest(value.content_digest)) &&
99
+ (value.revision !== null || value.content_digest !== null) &&
100
+ isIsoTimestamp(value.retrieved_at) &&
101
+ isBoundedString(value.excerpt, 300) &&
102
+ Array.isArray(value.claims) &&
103
+ value.claims.length > 0 &&
104
+ value.claims.length <= MAX_CLAIMS_PER_SOURCE &&
105
+ value.claims.every((claim) => isBoundedString(claim, 500))
106
+ );
107
+ }
108
+
109
+ function isEvidenceProbe(value: unknown): value is EvidenceProbe {
110
+ return (
111
+ isRecord(value) &&
112
+ hasExactKeys(value, ["command", "result_digest", "summary"]) &&
113
+ isBoundedString(value.command, 500) &&
114
+ isDigest(value.result_digest) &&
115
+ isBoundedString(value.summary, 500)
116
+ );
117
+ }
118
+
119
+ function isIsoTimestamp(value: unknown): value is string {
120
+ if (typeof value !== "string") return false;
121
+ const parsed = new Date(value);
122
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
123
+ }
124
+
125
+ function isDigest(value: unknown): value is string {
126
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
127
+ }
128
+
129
+ function isBoundedString(value: unknown, maxLength: number): value is string {
130
+ return (
131
+ typeof value === "string" && value.length > 0 && value.length <= maxLength
132
+ );
133
+ }
134
+
135
+ function isRecord(value: unknown): value is Record<string, unknown> {
136
+ return typeof value === "object" && value !== null && !Array.isArray(value);
137
+ }
138
+
139
+ function hasExactKeys(
140
+ value: Record<string, unknown>,
141
+ expected: string[],
142
+ ): boolean {
143
+ const keys = Object.keys(value).sort();
144
+ return JSON.stringify(keys) === JSON.stringify([...expected].sort());
145
+ }
@@ -0,0 +1,161 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export function runGh(args: string[], input?: string): string {
7
+ const result = spawnSync("gh", args, {
8
+ encoding: "utf8",
9
+ input,
10
+ maxBuffer: 50 * 1024 * 1024,
11
+ stdio: input ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
12
+ });
13
+ if (result.status !== 0) {
14
+ throw new Error(`gh ${args.join(" ")} failed:\n${result.stderr}`);
15
+ }
16
+ return result.stdout;
17
+ }
18
+
19
+ export function ghJson<T>(args: string[], input?: string): T {
20
+ return JSON.parse(runGh(args, input)) as T;
21
+ }
22
+
23
+ export function ensureLabels(repo: string, labels: string[]): void {
24
+ for (const label of labels) {
25
+ const result = spawnSync("gh", ["label", "create", label, "--repo", repo], {
26
+ encoding: "utf8",
27
+ stdio: ["ignore", "pipe", "pipe"],
28
+ });
29
+ if (result.status !== 0 && !result.stderr.includes("already exists")) {
30
+ throw new Error(`gh label create ${label} failed:\n${result.stderr}`);
31
+ }
32
+ }
33
+ }
34
+
35
+ export function createIssueWithBody(
36
+ repo: string,
37
+ title: string,
38
+ body: string,
39
+ labels: string[] = [],
40
+ ): string {
41
+ const bodyFile = writeTempMarkdown(body, "builtin-skills-watch-issue");
42
+ try {
43
+ const args = [
44
+ "issue",
45
+ "create",
46
+ "--repo",
47
+ repo,
48
+ "--title",
49
+ title,
50
+ "--body-file",
51
+ bodyFile,
52
+ ];
53
+ for (const label of labels) args.push("--label", label);
54
+ return runGh(args).trim();
55
+ } finally {
56
+ rmSync(bodyFile, { force: true });
57
+ }
58
+ }
59
+
60
+ export function editIssueBody(
61
+ repo: string,
62
+ issueNumber: number,
63
+ body: string,
64
+ ): void {
65
+ const bodyFile = writeTempMarkdown(body, "builtin-skills-watch-tracker");
66
+ try {
67
+ runGh([
68
+ "issue",
69
+ "edit",
70
+ String(issueNumber),
71
+ "--repo",
72
+ repo,
73
+ "--body-file",
74
+ bodyFile,
75
+ ]);
76
+ } finally {
77
+ rmSync(bodyFile, { force: true });
78
+ }
79
+ }
80
+
81
+ export function createIssueComment(
82
+ repo: string,
83
+ issueNumber: number,
84
+ body: string,
85
+ ): string {
86
+ const bodyFile = writeTempMarkdown(body, "builtin-skills-watch-evidence");
87
+ try {
88
+ return runGh([
89
+ "issue",
90
+ "comment",
91
+ String(issueNumber),
92
+ "--repo",
93
+ repo,
94
+ "--body-file",
95
+ bodyFile,
96
+ ]).trim();
97
+ } finally {
98
+ rmSync(bodyFile, { force: true });
99
+ }
100
+ }
101
+
102
+ export function getIssueBody(repo: string, issueNumber: number): string {
103
+ const issue = ghJson<{ body: string | null }>([
104
+ "issue",
105
+ "view",
106
+ String(issueNumber),
107
+ "--repo",
108
+ repo,
109
+ "--json",
110
+ "body",
111
+ ]);
112
+ return issue.body ?? "";
113
+ }
114
+
115
+ export function findIssuesByExactTitle(
116
+ repo: string,
117
+ title: string,
118
+ label: string,
119
+ ): Array<{
120
+ number: number;
121
+ title: string;
122
+ state: string;
123
+ author: { login: string };
124
+ labels: Array<{ name: string }>;
125
+ }> {
126
+ const issues = ghJson<
127
+ Array<{
128
+ number: number;
129
+ title: string;
130
+ state: string;
131
+ author: { login: string };
132
+ labels: Array<{ name: string }>;
133
+ }>
134
+ >([
135
+ "issue",
136
+ "list",
137
+ "--repo",
138
+ repo,
139
+ "--state",
140
+ "all",
141
+ "--label",
142
+ label,
143
+ "--search",
144
+ `${title} in:title`,
145
+ "--limit",
146
+ "20",
147
+ "--json",
148
+ "number,title,state,author,labels",
149
+ ]);
150
+ return issues.filter(
151
+ (issue) =>
152
+ issue.title === title &&
153
+ issue.labels.some((candidate) => candidate.name === label),
154
+ );
155
+ }
156
+
157
+ function writeTempMarkdown(body: string, prefix: string): string {
158
+ const path = join(tmpdir(), `${prefix}-${Date.now()}.md`);
159
+ writeFileSync(path, body);
160
+ return path;
161
+ }
@@ -0,0 +1,249 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { BuiltinSkillWatchAnalysis } from "./analysis.ts";
3
+ import type { ReviewEvidence } from "./evidence.ts";
4
+ import {
5
+ emptyTrackerState,
6
+ hasTerminalCandidate,
7
+ parseTrackerState,
8
+ recordOutcome,
9
+ renderTrackerBody,
10
+ startCandidate,
11
+ } from "./tracker.ts";
12
+
13
+ const INVENTORY = ["creating-skills", "syncing-memory-filesystem"];
14
+ const EVIDENCE_URL =
15
+ "https://github.com/letta-ai/letta-code/issues/1#issuecomment-1";
16
+
17
+ describe("built-in skills tracker", () => {
18
+ test("round trips hidden state and shows never-audited skills", () => {
19
+ const state = emptyTrackerState();
20
+ const body = renderTrackerBody(state, INVENTORY);
21
+
22
+ expect(parseTrackerState(body)).toEqual(state);
23
+ expect(body).toContain("| creating-skills | never | - | - |");
24
+ });
25
+
26
+ test("terminal outcomes advance only the selected skill", () => {
27
+ const analysis = fakeAnalysis("creating-skills", 1);
28
+ const next = recordOutcome(startCandidate(emptyTrackerState(), analysis), {
29
+ analysis,
30
+ outcome: "no_drift",
31
+ notes: "current",
32
+ processedAt: "2026-08-26T01:00:00.000Z",
33
+ evidence: fakeEvidence(analysis),
34
+ evidenceUrl: EVIDENCE_URL,
35
+ });
36
+
37
+ expect(next.skills["creating-skills"]).toMatchObject({
38
+ candidate_id: analysis.candidate_id,
39
+ audited_sha: "a".repeat(40),
40
+ outcome: "no_drift",
41
+ });
42
+ expect(next.skills["syncing-memory-filesystem"]).toBeUndefined();
43
+ expect(hasTerminalCandidate(next, analysis.candidate_id)).toBe(true);
44
+ });
45
+
46
+ test("errors remain retryable and do not advance the skill audit", () => {
47
+ const analysis = fakeAnalysis("creating-skills", 2);
48
+ const next = recordOutcome(startCandidate(emptyTrackerState(), analysis), {
49
+ analysis,
50
+ outcome: "error",
51
+ notes: "action failed",
52
+ processedAt: "2026-08-26T01:00:00.000Z",
53
+ });
54
+
55
+ expect(next.skills["creating-skills"]).toBeUndefined();
56
+ expect(next.history[0]?.outcome).toBe("error");
57
+ expect(hasTerminalCandidate(next, analysis.candidate_id)).toBe(false);
58
+ expect(next.pending["creating-skills"]?.candidate_id).toBe(
59
+ analysis.candidate_id,
60
+ );
61
+ expect(startCandidate(next, analysis)).toEqual(next);
62
+ expect(parseTrackerState(renderTrackerBody(next, INVENTORY))).toEqual(next);
63
+ });
64
+
65
+ test("a terminal retry replaces the same candidate history entry", () => {
66
+ const analysis = fakeAnalysis("creating-skills", 3);
67
+ const failed = recordOutcome(
68
+ startCandidate(emptyTrackerState(), analysis),
69
+ {
70
+ analysis,
71
+ outcome: "error",
72
+ notes: "failed",
73
+ processedAt: "2026-08-26T01:00:00.000Z",
74
+ },
75
+ );
76
+ const recovered = recordOutcome(failed, {
77
+ analysis,
78
+ outcome: "pr_created",
79
+ notes: "fixed",
80
+ prUrl: "https://github.com/letta-ai/letta-code/pull/1",
81
+ processedAt: "2026-08-26T02:00:00.000Z",
82
+ evidence: fakeEvidence(analysis),
83
+ evidenceUrl: EVIDENCE_URL,
84
+ });
85
+
86
+ expect(recovered.history).toHaveLength(1);
87
+ expect(recovered.history[0]?.outcome).toBe("pr_created");
88
+ expect(recovered.history[0]?.pr_url).toBe(
89
+ "https://github.com/letta-ai/letta-code/pull/1",
90
+ );
91
+ });
92
+
93
+ test("keeps history bounded", () => {
94
+ let state = emptyTrackerState();
95
+ for (let index = 0; index < 60; index += 1) {
96
+ const analysis = fakeAnalysis("creating-skills", index);
97
+ state = startCandidate(state, analysis);
98
+ state = recordOutcome(state, {
99
+ analysis,
100
+ outcome: "no_drift",
101
+ notes: "current",
102
+ processedAt: `2026-08-${String((index % 26) + 1).padStart(2, "0")}T00:00:00.000Z`,
103
+ evidence: fakeEvidence(analysis),
104
+ evidenceUrl: EVIDENCE_URL,
105
+ });
106
+ }
107
+
108
+ expect(state.history).toHaveLength(10);
109
+ expect(state.history[0]?.candidate_id).toBe(
110
+ fakeAnalysis("creating-skills", 59).candidate_id,
111
+ );
112
+ });
113
+
114
+ test("rejects malformed hidden state", () => {
115
+ expect(() =>
116
+ parseTrackerState("<!-- builtin-skills-agent-watch-state\n{}\n-->"),
117
+ ).toThrow("hidden state is invalid");
118
+ });
119
+
120
+ test("does not let a later error replace a terminal outcome", () => {
121
+ const analysis = fakeAnalysis("creating-skills", 4);
122
+ const terminal = recordOutcome(
123
+ startCandidate(emptyTrackerState(), analysis),
124
+ {
125
+ analysis,
126
+ outcome: "no_drift",
127
+ notes: "current",
128
+ evidence: fakeEvidence(analysis),
129
+ evidenceUrl: EVIDENCE_URL,
130
+ processedAt: "2026-08-26T01:00:00.000Z",
131
+ },
132
+ );
133
+ const afterError = recordOutcome(terminal, {
134
+ analysis,
135
+ outcome: "error",
136
+ notes: "late failure",
137
+ processedAt: "2026-08-26T02:00:00.000Z",
138
+ });
139
+
140
+ expect(afterError).toEqual(terminal);
141
+ expect(afterError.history[0]?.outcome).toBe("no_drift");
142
+ });
143
+
144
+ test("tracks and completes pending candidates independently", () => {
145
+ const first = fakeAnalysis("creating-skills", 5);
146
+ const second = fakeAnalysis("syncing-memory-filesystem", 6);
147
+ const pending = startCandidate(
148
+ startCandidate(emptyTrackerState(), first),
149
+ second,
150
+ );
151
+
152
+ expect(Object.keys(pending.pending).sort()).toEqual(INVENTORY);
153
+ const completed = recordOutcome(pending, {
154
+ analysis: first,
155
+ outcome: "no_drift",
156
+ notes: "current",
157
+ evidence: fakeEvidence(first),
158
+ evidenceUrl: EVIDENCE_URL,
159
+ });
160
+ expect(completed.pending["creating-skills"]).toBeUndefined();
161
+ expect(completed.pending["syncing-memory-filesystem"]?.candidate_id).toBe(
162
+ second.candidate_id,
163
+ );
164
+ });
165
+
166
+ test("keeps a full daily skill batch below the issue body limit", () => {
167
+ let state = emptyTrackerState();
168
+ const inventory: string[] = [];
169
+ const skillCount = 50;
170
+ for (let index = 0; index < skillCount; index += 1) {
171
+ const skill = `skill-${index}`;
172
+ inventory.push(skill);
173
+ const analysis = fakeAnalysis(skill, index);
174
+ state = startCandidate(state, analysis);
175
+ }
176
+ expect(Object.keys(state.pending)).toHaveLength(skillCount);
177
+ for (let index = 0; index < skillCount; index += 1) {
178
+ const skill = `skill-${index}`;
179
+ const analysis = fakeAnalysis(skill, index);
180
+ state = recordOutcome(state, {
181
+ analysis,
182
+ outcome: "no_drift",
183
+ notes: "n".repeat(120),
184
+ evidence: fakeEvidence(analysis),
185
+ evidenceUrl: EVIDENCE_URL,
186
+ processedAt: `2026-08-26T${String(Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}:00.000Z`,
187
+ });
188
+ }
189
+
190
+ expect(
191
+ Buffer.byteLength(renderTrackerBody(state, inventory), "utf8"),
192
+ ).toBeLessThan(60_000);
193
+ for (let index = 0; index < skillCount; index += 1) {
194
+ state = startCandidate(
195
+ state,
196
+ fakeAnalysis(`skill-${index}`, index + skillCount),
197
+ );
198
+ }
199
+ expect(
200
+ Buffer.byteLength(renderTrackerBody(state, inventory), "utf8"),
201
+ ).toBeLessThan(60_000);
202
+ });
203
+ });
204
+
205
+ function fakeAnalysis(
206
+ skill: string,
207
+ candidateNumber: number,
208
+ ): BuiltinSkillWatchAnalysis {
209
+ const candidateId = `${skill}@aaaaaaaaaaaa-${candidateNumber.toString(16).padStart(16, "0")}`;
210
+ return {
211
+ schema_version: 1,
212
+ candidate_id: candidateId,
213
+ skill,
214
+ skill_path: `src/skills/builtin/${skill}`,
215
+ skill_files: [`src/skills/builtin/${skill}/SKILL.md`],
216
+ skill_digest: "b".repeat(64),
217
+ current_sha: "a".repeat(40),
218
+ audit_at: "2026-08-26T00:00:00.000Z",
219
+ previous_audit: null,
220
+ repository_changes: {
221
+ previous_sha: null,
222
+ changed_files: [],
223
+ commits: [],
224
+ history_available: false,
225
+ truncated: false,
226
+ },
227
+ skill_inventory: INVENTORY,
228
+ workflow_run_url: "https://github.com/letta-ai/letta-code/actions/runs/1",
229
+ };
230
+ }
231
+
232
+ function fakeEvidence(analysis: BuiltinSkillWatchAnalysis): ReviewEvidence {
233
+ return {
234
+ schema_version: 1,
235
+ candidate_id: analysis.candidate_id,
236
+ skill: analysis.skill,
237
+ sources: [
238
+ {
239
+ locator: analysis.skill_path,
240
+ revision: analysis.current_sha,
241
+ content_digest: analysis.skill_digest,
242
+ retrieved_at: analysis.audit_at,
243
+ excerpt: "the selected skill matches its current owning source",
244
+ claims: ["skill files reviewed against current source"],
245
+ },
246
+ ],
247
+ probes: [],
248
+ };
249
+ }