@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
@@ -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,28 +21,30 @@ 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(findNextStableRelease(STABLES, "rust-v0.1.0")?.tag_name).toBe(
27
- "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
28
  );
29
29
  });
30
30
 
31
- test("identifies only adjacent stable release pairs", () => {
32
- expect(
33
- areAdjacentStableReleases(STABLES, "rust-v0.1.0", "rust-v0.2.0"),
34
- ).toBe(true);
35
- expect(
36
- areAdjacentStableReleases(STABLES, "rust-v0.1.0", "rust-v0.3.0"),
37
- ).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");
38
40
  });
39
41
 
40
42
  test("returns null when the durable cursor is current", () => {
41
- expect(findNextStableRelease(STABLES, "rust-v0.3.0")).toBeNull();
43
+ expect(findLatestStableReleaseAfter(STABLES, "rust-v0.3.0")).toBeNull();
42
44
  });
43
45
 
44
46
  test("fails instead of skipping an unavailable cursor", () => {
45
- expect(() => findNextStableRelease(STABLES, "rust-v0.0.9")).toThrow(
47
+ expect(() => findLatestStableReleaseAfter(STABLES, "rust-v0.0.9")).toThrow(
46
48
  "Could not find terminal release rust-v0.0.9",
47
49
  );
48
50
  });
@@ -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,15 +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 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,
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");
216
238
  }
217
239
 
218
- export function findNextStableRelease(
240
+ export function findLatestStableReleaseAfter(
219
241
  stables: Release[],
220
242
  terminalTag: string,
221
243
  ): Release | null {
@@ -225,7 +247,7 @@ export function findNextStableRelease(
225
247
  if (terminalIndex < 0) {
226
248
  throw new Error(`Could not find terminal release ${terminalTag}`);
227
249
  }
228
- return stables[terminalIndex + 1] ?? null;
250
+ return terminalIndex === stables.length - 1 ? null : (stables.at(-1) ?? null);
229
251
  }
230
252
 
231
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,
@@ -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,
@@ -250,14 +250,14 @@ describe("tracker state", () => {
250
250
  expect(getCodexAuditCursorTag(state)).toBe("rust-v0.2.0");
251
251
  });
252
252
 
253
- test("does not advance for an explicit non-adjacent range", () => {
253
+ test("does not advance for an explicit non-latest range", () => {
254
254
  const initial = upsertTrackerEntry(
255
255
  emptyTrackerState(),
256
256
  entry(1, "no_local_impact"),
257
257
  );
258
258
  const cumulative = {
259
259
  ...analysis("rust-v0.5.0", "tool-surface review needed"),
260
- is_adjacent_release: false,
260
+ is_latest_release: false,
261
261
  };
262
262
  const state = recordAnalysis(initial, {
263
263
  analysis: cumulative,
@@ -273,7 +273,7 @@ describe("tracker state", () => {
273
273
  ).toBe("rust-v0.1.0");
274
274
  });
275
275
 
276
- test("advances across an already audited adjacent range", () => {
276
+ test("advances across an already audited latest range", () => {
277
277
  let state = upsertTrackerEntry(
278
278
  emptyTrackerState(),
279
279
  entry(1, "no_local_impact"),
@@ -119,10 +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)
125
- return state;
124
+ if (!isLatestRelease || state.audit_cursor_tag !== previousTag) return state;
126
125
  if (!hasProcessedRange(state, previousTag, currentTag)) {
127
126
  throw new Error(
128
127
  `Cannot advance Codex audit cursor without terminal ${previousTag}...${currentTag}`,
@@ -149,26 +148,26 @@ export function recordAnalysis(
149
148
  compare_url: options.analysis.compare_url,
150
149
  workflow_run_url: options.analysis.workflow_run_url,
151
150
  },
152
- options.analysis.is_adjacent_release,
151
+ options.analysis.is_latest_release,
153
152
  );
154
153
  }
155
154
 
156
155
  export function upsertTrackerEntry(
157
156
  state: TrackerState,
158
157
  entry: TrackerEntry,
159
- advanceAuditCursor = true,
158
+ isLatestRelease = true,
160
159
  ): TrackerState {
161
160
  let auditCursorTag = state.audit_cursor_tag;
162
- if (auditCursorTag === null && !advanceAuditCursor) {
161
+ if (auditCursorTag === null && !isLatestRelease) {
163
162
  auditCursorTag = entry.previous_tag;
164
163
  } else if (
165
- advanceAuditCursor &&
164
+ isLatestRelease &&
166
165
  entry.outcome === "error" &&
167
166
  auditCursorTag === null
168
167
  ) {
169
168
  auditCursorTag = entry.previous_tag;
170
169
  } else if (
171
- advanceAuditCursor &&
170
+ isLatestRelease &&
172
171
  isTerminalOutcome(entry.outcome) &&
173
172
  (auditCursorTag === null || entry.previous_tag === auditCursorTag)
174
173
  ) {
@@ -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
 
@@ -42,11 +42,14 @@ export interface PiAiWatchAnalysis {
42
42
  installed_version: string;
43
43
  previous_version: string;
44
44
  current_version: string;
45
- is_adjacent_release: boolean;
45
+ is_latest_release: boolean;
46
46
  published_at: string;
47
47
  integrity: string | null;
48
48
  tarball_url: string | null;
49
+ previous_git_head: string | null;
49
50
  git_head: string | null;
51
+ previous_tag_commit: string;
52
+ current_tag_commit: string;
50
53
  release_url: string;
51
54
  compare_url: string;
52
55
  changelog_md: string;
@@ -79,6 +82,11 @@ export async function analyzePiAiRelease(
79
82
  `Could not find previous pi-ai release before ${current.version}`,
80
83
  );
81
84
  }
85
+ if (compareStableVersions(previous.version, current.version) >= 0) {
86
+ throw new Error(
87
+ `Previous pi-ai release ${previous.version} must precede ${current.version}`,
88
+ );
89
+ }
82
90
 
83
91
  const installedVersion = options.installedVersion ?? readInstalledVersion();
84
92
  const previousTag = `v${previous.version}`;
@@ -86,11 +94,13 @@ export async function analyzePiAiRelease(
86
94
  const temp = mkdtempSync(join(tmpdir(), "pi-ai-watch-"));
87
95
  try {
88
96
  const repoDir = clonePi(temp);
89
- fetchTag(repoDir, previousTag);
90
- fetchTag(repoDir, currentTag);
91
- verifyTagMatchesPackage(repoDir, previousTag, previous);
92
- verifyTagMatchesPackage(repoDir, currentTag, current);
93
- const changelog = showFile(repoDir, currentTag, "packages/ai/CHANGELOG.md");
97
+ const previousSource = resolveReleaseSource(repoDir, previousTag, previous);
98
+ const currentSource = resolveReleaseSource(repoDir, currentTag, current);
99
+ const changelog = showFile(
100
+ repoDir,
101
+ currentSource.sourceCommit,
102
+ "packages/ai/CHANGELOG.md",
103
+ );
94
104
  if (changelog === null) {
95
105
  throw new Error(`packages/ai/CHANGELOG.md is missing at ${currentTag}`);
96
106
  }
@@ -100,24 +110,35 @@ export async function analyzePiAiRelease(
100
110
  installed_version: installedVersion,
101
111
  previous_version: previous.version,
102
112
  current_version: current.version,
103
- is_adjacent_release: areAdjacentStableReleases(
104
- releases,
105
- previous.version,
106
- current.version,
107
- ),
113
+ is_latest_release: current.version === releases.at(-1)?.version,
108
114
  published_at: current.published_at,
109
115
  integrity: current.integrity,
110
116
  tarball_url: current.tarball_url,
117
+ previous_git_head: previous.git_head,
111
118
  git_head: current.git_head,
119
+ previous_tag_commit: previousSource.tagCommit,
120
+ current_tag_commit: currentSource.tagCommit,
112
121
  release_url: `https://github.com/${PI_AI_REPO}/releases/tag/${currentTag}`,
113
- compare_url: `https://github.com/${PI_AI_REPO}/compare/${previousTag}...${currentTag}`,
114
- changelog_md: extractChangelogSection(changelog, current.version),
115
- changed_files: changedFiles(repoDir, previousTag, currentTag),
116
- diff_stat: diffStat(repoDir, previousTag, currentTag),
122
+ compare_url: `https://github.com/${PI_AI_REPO}/compare/${previousSource.sourceCommit}...${currentSource.sourceCommit}`,
123
+ changelog_md: extractChangelogRange(
124
+ changelog,
125
+ previous.version,
126
+ current.version,
127
+ ),
128
+ changed_files: changedFiles(
129
+ repoDir,
130
+ previousSource.sourceCommit,
131
+ currentSource.sourceCommit,
132
+ ),
133
+ diff_stat: diffStat(
134
+ repoDir,
135
+ previousSource.sourceCommit,
136
+ currentSource.sourceCommit,
137
+ ),
117
138
  package_json_diff: diffPreview(
118
139
  repoDir,
119
- previousTag,
120
- currentTag,
140
+ previousSource.sourceCommit,
141
+ currentSource.sourceCommit,
121
142
  "packages/ai/package.json",
122
143
  ),
123
144
  workflow_run_url: workflowRunUrl(),
@@ -182,7 +203,7 @@ export function readInstalledVersion(lockfilePath = "bun.lock"): string {
182
203
  return match[1];
183
204
  }
184
205
 
185
- export function findNextStableRelease(
206
+ export function findLatestStableReleaseAfter(
186
207
  releases: PackageRelease[],
187
208
  cursorVersion: string,
188
209
  ): PackageRelease | null {
@@ -192,33 +213,33 @@ export function findNextStableRelease(
192
213
  if (index < 0) {
193
214
  throw new Error(`Could not find pi-ai cursor release ${cursorVersion}`);
194
215
  }
195
- return releases[index + 1] ?? null;
216
+ return index === releases.length - 1 ? null : (releases.at(-1) ?? null);
196
217
  }
197
218
 
198
- export function areAdjacentStableReleases(
199
- releases: PackageRelease[],
219
+ export function extractChangelogRange(
220
+ changelog: string,
200
221
  previousVersion: string,
201
222
  currentVersion: string,
202
- ): boolean {
203
- return (
204
- findPreviousStableRelease(releases, currentVersion)?.version ===
205
- previousVersion
206
- );
207
- }
208
-
209
- export function extractChangelogSection(
210
- changelog: string,
211
- version: string,
212
223
  ): string {
213
- const heading = new RegExp(`^## \\[${escapeRegExp(version)}\\].*$`, "m");
214
- const match = heading.exec(changelog);
215
- if (!match)
216
- throw new Error(`Could not find pi-ai changelog section ${version}`);
217
- const start = match.index;
218
- const remaining = changelog.slice(start + match[0].length);
219
- const next = /^## \[/m.exec(remaining);
220
- const end = next ? start + match[0].length + next.index : changelog.length;
221
- return changelog.slice(start, end).trim();
224
+ const currentHeading = new RegExp(
225
+ `^## \\[${escapeRegExp(currentVersion)}\\].*$`,
226
+ "m",
227
+ ).exec(changelog);
228
+ if (!currentHeading) {
229
+ throw new Error(`Could not find pi-ai changelog section ${currentVersion}`);
230
+ }
231
+ const previousHeading = new RegExp(
232
+ `^## \\[${escapeRegExp(previousVersion)}\\].*$`,
233
+ "m",
234
+ ).exec(changelog.slice(currentHeading.index + currentHeading[0].length));
235
+ if (!previousHeading) {
236
+ throw new Error(
237
+ `Could not find pi-ai changelog section ${previousVersion}`,
238
+ );
239
+ }
240
+ const end =
241
+ currentHeading.index + currentHeading[0].length + previousHeading.index;
242
+ return changelog.slice(currentHeading.index, end).trim();
222
243
  }
223
244
 
224
245
  function findPreviousStableRelease(
@@ -276,18 +297,37 @@ function fetchTag(repoDir: string, tag: string): void {
276
297
  );
277
298
  }
278
299
 
279
- function verifyTagMatchesPackage(
300
+ function resolveReleaseSource(
280
301
  repoDir: string,
281
302
  tag: string,
282
303
  release: PackageRelease,
283
- ): void {
284
- if (!release.git_head) return;
304
+ ): { sourceCommit: string; tagCommit: string } {
305
+ fetchTag(repoDir, tag);
285
306
  const tagCommit = git(["rev-list", "-n", "1", tag], repoDir).trim();
286
- if (tagCommit !== release.git_head) {
307
+ const sourceCommit = release.git_head ?? tagCommit;
308
+ if (sourceCommit !== tagCommit) {
309
+ fetchCommit(repoDir, sourceCommit);
310
+ }
311
+ const packageJson = showFile(
312
+ repoDir,
313
+ sourceCommit,
314
+ "packages/ai/package.json",
315
+ );
316
+ if (packageJson === null) {
317
+ throw new Error(`packages/ai/package.json is missing at ${sourceCommit}`);
318
+ }
319
+ const sourceVersion = (JSON.parse(packageJson) as { version?: unknown })
320
+ .version;
321
+ if (sourceVersion !== release.version) {
287
322
  throw new Error(
288
- `pi-ai npm ${release.version} gitHead ${release.git_head} does not match ${tag} commit ${tagCommit}`,
323
+ `pi-ai npm ${release.version} source ${sourceCommit} declares version ${String(sourceVersion)}`,
289
324
  );
290
325
  }
326
+ return { sourceCommit, tagCommit };
327
+ }
328
+
329
+ function fetchCommit(repoDir: string, commit: string): void {
330
+ git(["fetch", "--filter=blob:none", "origin", commit], repoDir);
291
331
  }
292
332
 
293
333
  function showFile(repoDir: string, tag: string, path: string): string | null {