@letta-ai/letta-code 0.30.31 → 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 (80) 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 +15 -7
  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/backend/api/ephemeral-conversations.d.ts +15 -0
  20. package/dist/types/backend/api/ephemeral-conversations.d.ts.map +1 -0
  21. package/dist/types/channels/gateway-core.d.ts.map +1 -1
  22. package/dist/types/channels/route-thread-key.d.ts +20 -0
  23. package/dist/types/channels/route-thread-key.d.ts.map +1 -0
  24. package/dist/types/channels-public.d.ts +1 -0
  25. package/dist/types/channels-public.d.ts.map +1 -1
  26. package/dist/types/providers/byok-providers.d.ts.map +1 -1
  27. package/dist/types/tools/impl/exec-command.d.ts.map +1 -1
  28. package/dist/types/tools/impl/shell-runner.d.ts +1 -0
  29. package/dist/types/tools/impl/shell-runner.d.ts.map +1 -1
  30. package/dist/types/tools/impl/skill.d.ts.map +1 -1
  31. package/dist/types/tools/toolset.d.ts +1 -1
  32. package/dist/types/tools/toolset.d.ts.map +1 -1
  33. package/dist/types/types/external-tool-protocol.d.ts +2 -2
  34. package/dist/types/types/external-tool-protocol.d.ts.map +1 -1
  35. package/dist/types/types/protocol_v2.d.ts +30 -190
  36. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  37. package/dist/types/types/runtime-scope.d.ts +4 -2
  38. package/dist/types/types/runtime-scope.d.ts.map +1 -1
  39. package/dist/types/types/runtime-start-protocol.d.ts +21 -0
  40. package/dist/types/types/runtime-start-protocol.d.ts.map +1 -0
  41. package/dist/types/types/schedule-protocol.d.ts +151 -1
  42. package/dist/types/types/schedule-protocol.d.ts.map +1 -1
  43. package/dist/types/utils/frontmatter.d.ts.map +1 -1
  44. package/dist/types/websocket/listener/cwd-change.d.ts.map +1 -1
  45. package/dist/types/websocket/listener/protocol-outbound.d.ts +1 -1
  46. package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
  47. package/dist/types/websocket/listener/scope.d.ts +1 -1
  48. package/dist/types/websocket/listener/scope.d.ts.map +1 -1
  49. package/letta.js +1230 -511
  50. package/package.json +5 -3
  51. package/scripts/builtin-skills-watch/agent-watch.test.ts +104 -0
  52. package/scripts/builtin-skills-watch/agent-watch.ts +337 -0
  53. package/scripts/builtin-skills-watch/aggregate-results.test.ts +79 -0
  54. package/scripts/builtin-skills-watch/aggregate-results.ts +326 -0
  55. package/scripts/builtin-skills-watch/analysis.test.ts +70 -0
  56. package/scripts/builtin-skills-watch/analysis.ts +228 -0
  57. package/scripts/builtin-skills-watch/evidence.test.ts +114 -0
  58. package/scripts/builtin-skills-watch/evidence.ts +145 -0
  59. package/scripts/builtin-skills-watch/github.ts +161 -0
  60. package/scripts/builtin-skills-watch/tracker.test.ts +249 -0
  61. package/scripts/builtin-skills-watch/tracker.ts +506 -0
  62. package/scripts/builtin-skills-watch/update-tracker.test.ts +234 -0
  63. package/scripts/builtin-skills-watch/update-tracker.ts +508 -0
  64. package/scripts/claude-watch/release-source.test.ts +16 -4
  65. package/scripts/claude-watch/release-source.ts +41 -2
  66. package/scripts/codex-watch/agent-watch.ts +8 -5
  67. package/scripts/codex-watch/release-analysis.test.ts +21 -27
  68. package/scripts/codex-watch/release-analysis.ts +30 -10
  69. package/scripts/codex-watch/tracker.test.ts +12 -33
  70. package/scripts/codex-watch/tracker.ts +30 -22
  71. package/scripts/pi-ai-watch/agent-watch.ts +9 -14
  72. package/scripts/pi-ai-watch/release-analysis.test.ts +24 -18
  73. package/scripts/pi-ai-watch/release-analysis.ts +85 -45
  74. package/scripts/pi-ai-watch/tracker.test.ts +25 -18
  75. package/scripts/pi-ai-watch/tracker.ts +7 -19
  76. package/scripts/pi-ai-watch/update-tracker.ts +2 -2
  77. package/scripts/source-file-size-baseline.json +3 -3
  78. package/skills/dispatching-coding-agents/SKILL.md +16 -7
  79. package/skills/self-configuration/SKILL.md +20 -1
  80. package/skills/syncing-memory-filesystem/SKILL.md +118 -226
@@ -1,7 +1,7 @@
1
1
  import {
2
- areAdjacentStableReleases,
3
- findNextStableRelease,
2
+ findLatestStableReleaseAfter,
4
3
  type Release,
4
+ releaseNotesForRange,
5
5
  } from "./release-analysis.ts";
6
6
 
7
7
  function release(tag: string, publishedAt: string): Release {
@@ -10,7 +10,7 @@ function release(tag: string, publishedAt: string): Release {
10
10
  draft: false,
11
11
  prerelease: false,
12
12
  html_url: `https://github.com/openai/codex/releases/tag/${tag}`,
13
- body: null,
13
+ body: `Notes for ${tag}`,
14
14
  published_at: publishedAt,
15
15
  };
16
16
  }
@@ -21,37 +21,31 @@ const STABLES = [
21
21
  release("rust-v0.3.0", "2026-08-03T00:00:00Z"),
22
22
  ];
23
23
 
24
- describe("Codex stable release backlog", () => {
25
- test("selects the first release after the durable cursor", () => {
26
- expect(
27
- findNextStableRelease(STABLES, "rust-v0.1.0")?.tag_name,
28
- ).toBe("rust-v0.2.0");
24
+ describe("Codex stable release range", () => {
25
+ test("selects the latest release after the durable cursor", () => {
26
+ expect(findLatestStableReleaseAfter(STABLES, "rust-v0.1.0")?.tag_name).toBe(
27
+ "rust-v0.3.0",
28
+ );
29
29
  });
30
30
 
31
- test("identifies only adjacent stable release pairs", () => {
32
- expect(
33
- areAdjacentStableReleases(
34
- STABLES,
35
- "rust-v0.1.0",
36
- "rust-v0.2.0",
37
- ),
38
- ).toBe(true);
39
- expect(
40
- areAdjacentStableReleases(
41
- STABLES,
42
- "rust-v0.1.0",
43
- "rust-v0.3.0",
44
- ),
45
- ).toBe(false);
31
+ test("includes release notes for the full cursor-to-latest range", () => {
32
+ const notes = releaseNotesForRange(STABLES, "rust-v0.1.0", "rust-v0.3.0");
33
+ expect(notes).toContain("## [rust-v0.2.0]");
34
+ expect(notes).toContain("Notes for rust-v0.2.0");
35
+ expect(notes).toContain("## [rust-v0.3.0]");
36
+ expect(notes).not.toContain("Notes for rust-v0.1.0");
37
+ expect(() =>
38
+ releaseNotesForRange(STABLES, "rust-v0.3.0", "rust-v0.2.0"),
39
+ ).toThrow("Previous release rust-v0.3.0 must precede rust-v0.2.0");
46
40
  });
47
41
 
48
42
  test("returns null when the durable cursor is current", () => {
49
- expect(findNextStableRelease(STABLES, "rust-v0.3.0")).toBeNull();
43
+ expect(findLatestStableReleaseAfter(STABLES, "rust-v0.3.0")).toBeNull();
50
44
  });
51
45
 
52
46
  test("fails instead of skipping an unavailable cursor", () => {
53
- expect(() =>
54
- findNextStableRelease(STABLES, "rust-v0.0.9"),
55
- ).toThrow("Could not find terminal release rust-v0.0.9");
47
+ expect(() => findLatestStableReleaseAfter(STABLES, "rust-v0.0.9")).toThrow(
48
+ "Could not find terminal release rust-v0.0.9",
49
+ );
56
50
  });
57
51
  });
@@ -45,7 +45,7 @@ export interface PathChangeSummary {
45
45
  export interface CodexWatchAnalysis {
46
46
  previous_tag: string;
47
47
  current_tag: string;
48
- is_adjacent_release: boolean;
48
+ is_latest_release: boolean;
49
49
  release_url: string;
50
50
  release_notes_md: string;
51
51
  verdict: Verdict;
@@ -139,13 +139,13 @@ export async function analyzeCodexRelease(
139
139
  return {
140
140
  previous_tag: previous.tag_name,
141
141
  current_tag: current.tag_name,
142
- is_adjacent_release: areAdjacentStableReleases(
142
+ is_latest_release: current.tag_name === stables.at(-1)?.tag_name,
143
+ release_url: current.html_url,
144
+ release_notes_md: releaseNotesForRange(
143
145
  stables,
144
146
  previous.tag_name,
145
147
  current.tag_name,
146
148
  ),
147
- release_url: current.html_url,
148
- release_notes_md: current.body ?? "",
149
149
  verdict,
150
150
  models_diff: modelsDiff,
151
151
  prompt_md_changed: promptMdChanged,
@@ -207,17 +207,37 @@ function findPreviousStable(
207
207
  return stables[idx - 1] ?? null;
208
208
  }
209
209
 
210
- export function areAdjacentStableReleases(
210
+ export function releaseNotesForRange(
211
211
  stables: Release[],
212
212
  previousTag: string,
213
213
  currentTag: string,
214
- ): boolean {
215
- return (
216
- findPreviousStable(stables, currentTag)?.tag_name === previousTag
214
+ ): string {
215
+ const currentIndex = stables.findIndex(
216
+ (release) => release.tag_name === currentTag,
217
+ );
218
+ if (currentIndex < 0) {
219
+ throw new Error(`Could not find current release ${currentTag}`);
220
+ }
221
+ const previousIndex = stables.findIndex(
222
+ (release) => release.tag_name === previousTag,
217
223
  );
224
+ if (previousIndex >= currentIndex) {
225
+ throw new Error(
226
+ `Previous release ${previousTag} must precede ${currentTag}`,
227
+ );
228
+ }
229
+ const firstIncludedIndex =
230
+ previousIndex < 0 ? currentIndex : previousIndex + 1;
231
+ return stables
232
+ .slice(firstIncludedIndex, currentIndex + 1)
233
+ .map(
234
+ (release) =>
235
+ `## [${release.tag_name}](${release.html_url})\n\n${release.body?.trim() || "_No release notes._"}`,
236
+ )
237
+ .join("\n\n");
218
238
  }
219
239
 
220
- export function findNextStableRelease(
240
+ export function findLatestStableReleaseAfter(
221
241
  stables: Release[],
222
242
  terminalTag: string,
223
243
  ): Release | null {
@@ -227,7 +247,7 @@ export function findNextStableRelease(
227
247
  if (terminalIndex < 0) {
228
248
  throw new Error(`Could not find terminal release ${terminalTag}`);
229
249
  }
230
- return stables[terminalIndex + 1] ?? null;
250
+ return terminalIndex === stables.length - 1 ? null : (stables.at(-1) ?? null);
231
251
  }
232
252
 
233
253
  function cloneCodex(tmp: string): string {
@@ -21,7 +21,7 @@ function analysis(
21
21
  return {
22
22
  previous_tag: "rust-v0.1.0",
23
23
  current_tag: tag,
24
- is_adjacent_release: true,
24
+ is_latest_release: true,
25
25
  release_url: `https://github.com/openai/codex/releases/tag/${tag}`,
26
26
  release_notes_md: "",
27
27
  verdict,
@@ -37,7 +37,7 @@ function analysis(
37
37
 
38
38
  function withoutAuditCursor(body: string): string {
39
39
  return body
40
- .replace(/ "audit_cursor_tag": (?:null|"[^"]+"),\n/, "")
40
+ .replace(/ {2}"audit_cursor_tag": (?:null|"[^"]+"),\n/, "")
41
41
  .replace(' "audit_cursor_validated": true,\n', "");
42
42
  }
43
43
 
@@ -166,7 +166,7 @@ describe("tracker state", () => {
166
166
  test("fails closed on a cumulative legacy replay", () => {
167
167
  const cumulative = {
168
168
  ...analysis("rust-v0.5.0", "tool-surface review needed"),
169
- is_adjacent_release: false,
169
+ is_latest_release: false,
170
170
  };
171
171
  const state = recordAnalysis(emptyTrackerState(), {
172
172
  analysis: cumulative,
@@ -198,11 +198,7 @@ describe("tracker state", () => {
198
198
 
199
199
  const body = renderTrackerBody(state);
200
200
  expect(
201
- hasProcessedRange(
202
- parseTrackerState(body),
203
- "rust-v0.1.0",
204
- "rust-v0.2.0",
205
- ),
201
+ hasProcessedRange(parseTrackerState(body), "rust-v0.1.0", "rust-v0.2.0"),
206
202
  ).toBe(true);
207
203
  expect(body).toContain("_No actionable releases recorded yet._");
208
204
  expect(body).toContain("no watched changes");
@@ -215,23 +211,16 @@ describe("tracker state", () => {
215
211
  );
216
212
  state = upsertTrackerEntry(state, entry(2, "error"));
217
213
 
218
- expect(
219
- hasProcessedRange(state, "rust-v0.1.0", "rust-v0.2.0"),
220
- ).toBe(false);
214
+ expect(hasProcessedRange(state, "rust-v0.1.0", "rust-v0.2.0")).toBe(false);
221
215
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.1.0");
222
216
 
223
217
  state = upsertTrackerEntry(state, entry(2, "no_local_impact"));
224
- expect(
225
- hasProcessedRange(state, "rust-v0.1.0", "rust-v0.2.0"),
226
- ).toBe(true);
218
+ expect(hasProcessedRange(state, "rust-v0.1.0", "rust-v0.2.0")).toBe(true);
227
219
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.2.0");
228
220
  });
229
221
 
230
222
  test("uses an errored release baseline when no terminal entry exists", () => {
231
- const state = upsertTrackerEntry(
232
- emptyTrackerState(),
233
- entry(2, "error"),
234
- );
223
+ const state = upsertTrackerEntry(emptyTrackerState(), entry(2, "error"));
235
224
 
236
225
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.1.0");
237
226
  });
@@ -261,14 +250,14 @@ describe("tracker state", () => {
261
250
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.2.0");
262
251
  });
263
252
 
264
- test("does not advance for an explicit non-adjacent range", () => {
253
+ test("does not advance for an explicit non-latest range", () => {
265
254
  const initial = upsertTrackerEntry(
266
255
  emptyTrackerState(),
267
256
  entry(1, "no_local_impact"),
268
257
  );
269
258
  const cumulative = {
270
259
  ...analysis("rust-v0.5.0", "tool-surface review needed"),
271
- is_adjacent_release: false,
260
+ is_latest_release: false,
272
261
  };
273
262
  const state = recordAnalysis(initial, {
274
263
  analysis: cumulative,
@@ -279,17 +268,12 @@ describe("tracker state", () => {
279
268
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.1.0");
280
269
  expect(
281
270
  getCodexAuditCursorTag(
282
- advanceCodexAuditCursor(
283
- state,
284
- "rust-v0.1.0",
285
- "rust-v0.5.0",
286
- false,
287
- ),
271
+ advanceCodexAuditCursor(state, "rust-v0.1.0", "rust-v0.5.0", false),
288
272
  ),
289
273
  ).toBe("rust-v0.1.0");
290
274
  });
291
275
 
292
- test("advances across an already audited adjacent range", () => {
276
+ test("advances across an already audited latest range", () => {
293
277
  let state = upsertTrackerEntry(
294
278
  emptyTrackerState(),
295
279
  entry(1, "no_local_impact"),
@@ -298,12 +282,7 @@ describe("tracker state", () => {
298
282
  state = upsertTrackerEntry(state, entry(2, "no_local_impact"));
299
283
 
300
284
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.2.0");
301
- state = advanceCodexAuditCursor(
302
- state,
303
- "rust-v0.2.0",
304
- "rust-v0.3.0",
305
- true,
306
- );
285
+ state = advanceCodexAuditCursor(state, "rust-v0.2.0", "rust-v0.3.0", true);
307
286
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.3.0");
308
287
  });
309
288
 
@@ -119,9 +119,9 @@ export function advanceCodexAuditCursor(
119
119
  state: TrackerState,
120
120
  previousTag: string,
121
121
  currentTag: string,
122
- isAdjacentRelease: boolean,
122
+ isLatestRelease: boolean,
123
123
  ): TrackerState {
124
- if (!isAdjacentRelease || state.audit_cursor_tag !== previousTag) return state;
124
+ if (!isLatestRelease || state.audit_cursor_tag !== previousTag) return state;
125
125
  if (!hasProcessedRange(state, previousTag, currentTag)) {
126
126
  throw new Error(
127
127
  `Cannot advance Codex audit cursor without terminal ${previousTag}...${currentTag}`,
@@ -135,35 +135,39 @@ export function recordAnalysis(
135
135
  options: RecordAnalysisOptions,
136
136
  ): TrackerState {
137
137
  const processedAt = options.processedAt ?? new Date().toISOString();
138
- return upsertTrackerEntry(state, {
139
- tag: options.analysis.current_tag,
140
- previous_tag: options.analysis.previous_tag,
141
- verdict: options.analysis.verdict,
142
- outcome: options.outcome,
143
- pr_url: options.prUrl ?? null,
144
- notes: options.notes,
145
- processed_at: processedAt,
146
- compare_url: options.analysis.compare_url,
147
- workflow_run_url: options.analysis.workflow_run_url,
148
- }, options.analysis.is_adjacent_release);
138
+ return upsertTrackerEntry(
139
+ state,
140
+ {
141
+ tag: options.analysis.current_tag,
142
+ previous_tag: options.analysis.previous_tag,
143
+ verdict: options.analysis.verdict,
144
+ outcome: options.outcome,
145
+ pr_url: options.prUrl ?? null,
146
+ notes: options.notes,
147
+ processed_at: processedAt,
148
+ compare_url: options.analysis.compare_url,
149
+ workflow_run_url: options.analysis.workflow_run_url,
150
+ },
151
+ options.analysis.is_latest_release,
152
+ );
149
153
  }
150
154
 
151
155
  export function upsertTrackerEntry(
152
156
  state: TrackerState,
153
157
  entry: TrackerEntry,
154
- advanceAuditCursor = true,
158
+ isLatestRelease = true,
155
159
  ): TrackerState {
156
160
  let auditCursorTag = state.audit_cursor_tag;
157
- if (auditCursorTag === null && !advanceAuditCursor) {
161
+ if (auditCursorTag === null && !isLatestRelease) {
158
162
  auditCursorTag = entry.previous_tag;
159
163
  } else if (
160
- advanceAuditCursor &&
164
+ isLatestRelease &&
161
165
  entry.outcome === "error" &&
162
166
  auditCursorTag === null
163
167
  ) {
164
168
  auditCursorTag = entry.previous_tag;
165
169
  } else if (
166
- advanceAuditCursor &&
170
+ isLatestRelease &&
167
171
  isTerminalOutcome(entry.outcome) &&
168
172
  (auditCursorTag === null || entry.previous_tag === auditCursorTag)
169
173
  ) {
@@ -179,7 +183,10 @@ export function upsertTrackerEntry(
179
183
  ),
180
184
  ];
181
185
  let processed = candidates.slice(0, HIDDEN_STATE_LIMIT);
182
- if (auditCursorTag !== null && !isSupportedAuditCursor(processed, auditCursorTag)) {
186
+ if (
187
+ auditCursorTag !== null &&
188
+ !isSupportedAuditCursor(processed, auditCursorTag)
189
+ ) {
183
190
  const support = candidates.find((candidate) =>
184
191
  isAuditCursorSupportEntry(candidate, auditCursorTag),
185
192
  );
@@ -276,7 +283,10 @@ function escapeTable(value: string): string {
276
283
 
277
284
  function normalizeState(value: unknown): TrackerState {
278
285
  if (!isRecord(value)) throw new TypeError("tracker state must be an object");
279
- if (!Array.isArray(value.processed) || !value.processed.every(isTrackerEntry)) {
286
+ if (
287
+ !Array.isArray(value.processed) ||
288
+ !value.processed.every(isTrackerEntry)
289
+ ) {
280
290
  throw new TypeError("tracker processed entries are invalid");
281
291
  }
282
292
  if (
@@ -346,9 +356,7 @@ function isSupportedAuditCursor(
346
356
  processed: TrackerEntry[],
347
357
  cursorTag: string,
348
358
  ): boolean {
349
- return processed.some((entry) =>
350
- isAuditCursorSupportEntry(entry, cursorTag),
351
- );
359
+ return processed.some((entry) => isAuditCursorSupportEntry(entry, cursorTag));
352
360
  }
353
361
 
354
362
  function isAuditCursorSupportEntry(
@@ -3,7 +3,6 @@
3
3
  import { appendFileSync, writeFileSync } from "node:fs";
4
4
  import {
5
5
  createIssueWithBody,
6
- editIssueBody,
7
6
  ensureLabels,
8
7
  findIssueByExactTitle,
9
8
  getIssueBody,
@@ -13,12 +12,11 @@ import {
13
12
  analyzePiAiRelease,
14
13
  compareStableVersions,
15
14
  DEFAULT_TARGET_REPO,
16
- findNextStableRelease,
15
+ findLatestStableReleaseAfter,
17
16
  listStableReleases,
18
17
  readInstalledVersion,
19
18
  } from "./release-analysis.ts";
20
19
  import {
21
- advanceMergedPr,
22
20
  getPendingPrForCursor,
23
21
  hasCompletedRange,
24
22
  initialTrackerState,
@@ -82,7 +80,7 @@ async function main(): Promise<void> {
82
80
  const args = parseArgs(process.argv.slice(2));
83
81
  const installedVersion = readInstalledVersion();
84
82
  const tracker = ensureTrackerIssue(args, installedVersion);
85
- let state = parseTrackerState(tracker.body);
83
+ const state = parseTrackerState(tracker.body);
86
84
  const releases = await listStableReleases();
87
85
  let previousVersion = args.previousVersion;
88
86
  let currentVersion = args.currentVersion;
@@ -109,19 +107,16 @@ async function main(): Promise<void> {
109
107
  `Merged pi-ai PR ${status.url} targets ${pending.version}, but main still declares ${installedVersion}`,
110
108
  );
111
109
  }
112
- state = advanceMergedPr(state, pending.version);
113
- if (!args.dryRun) {
114
- editIssueBody(args.repo, tracker.number, renderTrackerBody(state));
115
- }
116
110
  } else {
117
- console.log(
118
- `Retrying ${pending.version}; prior PR was closed unmerged.`,
119
- );
111
+ console.log(`Prior pi-ai PR ${status.url} was closed unmerged.`);
120
112
  }
121
113
  }
122
114
 
123
- const next = findNextStableRelease(releases, state.audit_cursor_version);
124
- if (!next) {
115
+ const latest = findLatestStableReleaseAfter(
116
+ releases,
117
+ state.audit_cursor_version,
118
+ );
119
+ if (!latest) {
125
120
  console.log(
126
121
  `No stable pi-ai release after ${state.audit_cursor_version}.`,
127
122
  );
@@ -134,7 +129,7 @@ async function main(): Promise<void> {
134
129
  return;
135
130
  }
136
131
  previousVersion = state.audit_cursor_version;
137
- currentVersion = next.version;
132
+ currentVersion = latest.version;
138
133
  }
139
134
 
140
135
  const analysis = await analyzePiAiRelease({
@@ -3,10 +3,9 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
6
- areAdjacentStableReleases,
7
6
  compareStableVersions,
8
- extractChangelogSection,
9
- findNextStableRelease,
7
+ extractChangelogRange,
8
+ findLatestStableReleaseAfter,
10
9
  type PackageRelease,
11
10
  parseRegistryMetadata,
12
11
  readInstalledVersion,
@@ -59,24 +58,20 @@ describe("pi-ai npm releases", () => {
59
58
  ).toThrow("missing publication time for 0.82.1");
60
59
  });
61
60
 
62
- test("selects exactly the next stable version after the cursor", () => {
61
+ test("selects the latest stable version after the cursor", () => {
63
62
  const releases = releaseList("0.82.1", "0.83.0", "0.84.0");
64
- expect(findNextStableRelease(releases, "0.82.1")?.version).toBe("0.83.0");
65
- expect(findNextStableRelease(releases, "0.84.0")).toBeNull();
66
- expect(() => findNextStableRelease(releases, "0.80.0")).toThrow(
63
+ expect(findLatestStableReleaseAfter(releases, "0.82.1")?.version).toBe(
64
+ "0.84.0",
65
+ );
66
+ expect(findLatestStableReleaseAfter(releases, "0.84.0")).toBeNull();
67
+ expect(() => findLatestStableReleaseAfter(releases, "0.80.0")).toThrow(
67
68
  "Could not find pi-ai cursor release 0.80.0",
68
69
  );
69
70
  });
70
-
71
- test("recognizes only adjacent release pairs", () => {
72
- const releases = releaseList("0.82.1", "0.83.0", "0.84.0");
73
- expect(areAdjacentStableReleases(releases, "0.82.1", "0.83.0")).toBe(true);
74
- expect(areAdjacentStableReleases(releases, "0.82.1", "0.84.0")).toBe(false);
75
- });
76
71
  });
77
72
 
78
73
  describe("pi-ai release evidence", () => {
79
- test("extracts only the requested changelog section", () => {
74
+ test("extracts the cumulative changelog after the cursor", () => {
80
75
  const changelog = `# Changelog
81
76
 
82
77
  ## [0.84.0] - 2026-08-06
@@ -88,16 +83,27 @@ describe("pi-ai release evidence", () => {
88
83
  ## [0.83.0] - 2026-07-29
89
84
 
90
85
  - Previous feature.
86
+
87
+ ## [0.82.1] - 2026-07-20
88
+
89
+ - Cursor release.
91
90
  `;
92
91
  expect(
93
- extractChangelogSection(changelog, "0.84.0"),
92
+ extractChangelogRange(changelog, "0.82.1", "0.84.0"),
94
93
  ).toBe(`## [0.84.0] - 2026-08-06
95
94
 
96
95
  ### Added
97
96
 
98
- - Current feature.`);
99
- expect(() => extractChangelogSection(changelog, "0.82.1")).toThrow(
100
- "Could not find pi-ai changelog section 0.82.1",
97
+ - Current feature.
98
+
99
+ ## [0.83.0] - 2026-07-29
100
+
101
+ - Previous feature.`);
102
+ expect(() => extractChangelogRange(changelog, "0.80.0", "0.84.0")).toThrow(
103
+ "Could not find pi-ai changelog section 0.80.0",
104
+ );
105
+ expect(() => extractChangelogRange(changelog, "0.82.1", "0.85.0")).toThrow(
106
+ "Could not find pi-ai changelog section 0.85.0",
101
107
  );
102
108
  });
103
109