@letta-ai/letta-code 0.30.28 → 0.30.30

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 (82) hide show
  1. package/dist/agent-presets.js +55 -2490
  2. package/dist/agent-presets.js.map +4 -4
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/acting-user.d.ts +21 -0
  6. package/dist/types/agent/acting-user.d.ts.map +1 -0
  7. package/dist/types/agent/available-models.d.ts +2 -0
  8. package/dist/types/agent/available-models.d.ts.map +1 -1
  9. package/dist/types/agent/client-skills.d.ts +3 -0
  10. package/dist/types/agent/client-skills.d.ts.map +1 -1
  11. package/dist/types/agent/memory-git.d.ts +3 -11
  12. package/dist/types/agent/memory-git.d.ts.map +1 -1
  13. package/dist/types/agent/message.d.ts +1 -0
  14. package/dist/types/agent/message.d.ts.map +1 -1
  15. package/dist/types/agent/model-catalog.d.ts +14 -52
  16. package/dist/types/agent/model-catalog.d.ts.map +1 -1
  17. package/dist/types/agent/model.d.ts.map +1 -1
  18. package/dist/types/agent/personality-presets.d.ts +1 -1
  19. package/dist/types/agent/personality-presets.d.ts.map +1 -1
  20. package/dist/types/agent/shared-memory-skills.d.ts +18 -0
  21. package/dist/types/agent/shared-memory-skills.d.ts.map +1 -0
  22. package/dist/types/agent/subagents/index.d.ts +1 -1
  23. package/dist/types/agent/subagents/index.d.ts.map +1 -1
  24. package/dist/types/agent/turn-recovery-policy.d.ts +10 -7
  25. package/dist/types/agent/turn-recovery-policy.d.ts.map +1 -1
  26. package/dist/types/agent-presets.d.ts +0 -1
  27. package/dist/types/agent-presets.d.ts.map +1 -1
  28. package/dist/types/backend/api/conversations.d.ts +1 -0
  29. package/dist/types/backend/api/conversations.d.ts.map +1 -1
  30. package/dist/types/backend/backend.d.ts +4 -2
  31. package/dist/types/backend/backend.d.ts.map +1 -1
  32. package/dist/types/backend/dev/pi-model-factory.d.ts +6 -0
  33. package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
  34. package/dist/types/backend/dev/pi-provider-registry.d.ts.map +1 -1
  35. package/dist/types/backend/local/local-model-config.d.ts +3 -0
  36. package/dist/types/backend/local/local-model-config.d.ts.map +1 -1
  37. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  38. package/dist/types/backend/local/system-prompt-compilation.d.ts.map +1 -1
  39. package/dist/types/cli/helpers/stream-processor.d.ts +1 -0
  40. package/dist/types/cli/helpers/stream-processor.d.ts.map +1 -1
  41. package/dist/types/cli/helpers/stream-stall-reconciler.d.ts +2 -1
  42. package/dist/types/cli/helpers/stream-stall-reconciler.d.ts.map +1 -1
  43. package/dist/types/cli/helpers/stream.d.ts +1 -1
  44. package/dist/types/cli/helpers/stream.d.ts.map +1 -1
  45. package/dist/types/reminders/state.d.ts +6 -1
  46. package/dist/types/reminders/state.d.ts.map +1 -1
  47. package/dist/types/tools/impl/bash-output.d.ts +1 -0
  48. package/dist/types/tools/impl/bash-output.d.ts.map +1 -1
  49. package/dist/types/tools/impl/github-pull-request-tracker.d.ts +7 -3
  50. package/dist/types/tools/impl/github-pull-request-tracker.d.ts.map +1 -1
  51. package/dist/types/tools/impl/glob.d.ts +1 -0
  52. package/dist/types/tools/impl/glob.d.ts.map +1 -1
  53. package/dist/types/tools/impl/grep.d.ts +1 -0
  54. package/dist/types/tools/impl/grep.d.ts.map +1 -1
  55. package/dist/types/tools/impl/skill.d.ts +10 -5
  56. package/dist/types/tools/impl/skill.d.ts.map +1 -1
  57. package/dist/types/tools/impl/task-output.d.ts +1 -0
  58. package/dist/types/tools/impl/task-output.d.ts.map +1 -1
  59. package/dist/types/tools/impl/task.d.ts.map +1 -1
  60. package/dist/types/tools/manager.d.ts.map +1 -1
  61. package/dist/types/tools/toolset.d.ts.map +1 -1
  62. package/dist/types/types/approval-classification-protocol.d.ts +13 -0
  63. package/dist/types/types/approval-classification-protocol.d.ts.map +1 -0
  64. package/dist/types/types/protocol_v2.d.ts +4 -8
  65. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  66. package/image-resize-worker.js +42 -20
  67. package/letta.js +94957 -151018
  68. package/package.json +4 -3
  69. package/scripts/codex-watch/agent-watch.ts +63 -8
  70. package/scripts/codex-watch/release-analysis.test.ts +57 -0
  71. package/scripts/codex-watch/release-analysis.ts +31 -1
  72. package/scripts/codex-watch/tracker.test.ts +243 -6
  73. package/scripts/codex-watch/tracker.ts +221 -20
  74. package/scripts/isolated-unit-tests.json +15 -0
  75. package/scripts/pi-ai-watch/agent-watch.ts +253 -0
  76. package/scripts/pi-ai-watch/github.ts +145 -0
  77. package/scripts/pi-ai-watch/release-analysis.test.ts +137 -0
  78. package/scripts/pi-ai-watch/release-analysis.ts +371 -0
  79. package/scripts/pi-ai-watch/tracker.test.ts +138 -0
  80. package/scripts/pi-ai-watch/tracker.ts +303 -0
  81. package/scripts/pi-ai-watch/update-tracker.ts +215 -0
  82. package/scripts/source-file-size-baseline.json +6 -6
@@ -26,6 +26,8 @@ export interface TrackerEntry {
26
26
  }
27
27
 
28
28
  export interface TrackerState {
29
+ audit_cursor_tag: string | null;
30
+ audit_cursor_validated: boolean;
29
31
  last_checked_tag: string | null;
30
32
  last_checked_at: string | null;
31
33
  processed: TrackerEntry[];
@@ -41,6 +43,8 @@ export interface RecordAnalysisOptions {
41
43
 
42
44
  export function emptyTrackerState(): TrackerState {
43
45
  return {
46
+ audit_cursor_tag: null,
47
+ audit_cursor_validated: true,
44
48
  last_checked_tag: null,
45
49
  last_checked_at: null,
46
50
  processed: [],
@@ -49,21 +53,81 @@ export function emptyTrackerState(): TrackerState {
49
53
 
50
54
  export function parseTrackerState(body: string): TrackerState {
51
55
  const start = body.indexOf(STATE_START);
52
- if (start === -1) return emptyTrackerState();
56
+ if (start === -1) throw new Error("Codex tracker hidden state is missing");
53
57
 
54
58
  const jsonStart = start + STATE_START.length;
55
59
  const end = body.indexOf(STATE_END, jsonStart);
56
- if (end === -1) return emptyTrackerState();
60
+ if (end === -1) throw new Error("Codex tracker hidden state is incomplete");
57
61
 
58
62
  try {
59
63
  return normalizeState(JSON.parse(body.slice(jsonStart, end).trim()));
60
- } catch {
61
- return emptyTrackerState();
64
+ } catch (error) {
65
+ throw new Error("Codex tracker hidden state is invalid", { cause: error });
62
66
  }
63
67
  }
64
68
 
65
- export function hasProcessedTag(state: TrackerState, tag: string): boolean {
66
- return state.processed.some((entry) => entry.tag === tag);
69
+ export function isTerminalOutcome(outcome: TrackerOutcome): boolean {
70
+ return outcome !== "error";
71
+ }
72
+
73
+ export function hasProcessedRange(
74
+ state: TrackerState,
75
+ previousTag: string,
76
+ currentTag: string,
77
+ ): boolean {
78
+ return state.processed.some(
79
+ (entry) =>
80
+ entry.previous_tag === previousTag &&
81
+ entry.tag === currentTag &&
82
+ isTerminalOutcome(entry.outcome),
83
+ );
84
+ }
85
+
86
+ export function getCodexAuditCursorTag(state: TrackerState): string | null {
87
+ return state.audit_cursor_tag;
88
+ }
89
+
90
+ export function validateLegacyCodexAuditCursor(
91
+ state: TrackerState,
92
+ stableTags: string[],
93
+ ): TrackerState {
94
+ if (state.audit_cursor_validated) return state;
95
+
96
+ let tag = state.audit_cursor_tag;
97
+ while (tag !== null) {
98
+ const entry = state.processed.find(
99
+ (candidate) =>
100
+ candidate.tag === tag && isTerminalOutcome(candidate.outcome),
101
+ );
102
+ if (!entry) break;
103
+ const currentIndex = stableTags.indexOf(entry.tag);
104
+ if (
105
+ currentIndex < 1 ||
106
+ stableTags[currentIndex - 1] !== entry.previous_tag
107
+ ) {
108
+ throw new Error(
109
+ `Legacy Codex tracker range is not adjacent: ${entry.previous_tag}...${entry.tag}`,
110
+ );
111
+ }
112
+ tag = entry.previous_tag;
113
+ }
114
+
115
+ return { ...state, audit_cursor_validated: true };
116
+ }
117
+
118
+ export function advanceCodexAuditCursor(
119
+ state: TrackerState,
120
+ previousTag: string,
121
+ currentTag: string,
122
+ isAdjacentRelease: boolean,
123
+ ): TrackerState {
124
+ if (!isAdjacentRelease || state.audit_cursor_tag !== previousTag) return state;
125
+ if (!hasProcessedRange(state, previousTag, currentTag)) {
126
+ throw new Error(
127
+ `Cannot advance Codex audit cursor without terminal ${previousTag}...${currentTag}`,
128
+ );
129
+ }
130
+ return { ...state, audit_cursor_tag: currentTag };
67
131
  }
68
132
 
69
133
  export function recordAnalysis(
@@ -81,19 +145,51 @@ export function recordAnalysis(
81
145
  processed_at: processedAt,
82
146
  compare_url: options.analysis.compare_url,
83
147
  workflow_run_url: options.analysis.workflow_run_url,
84
- });
148
+ }, options.analysis.is_adjacent_release);
85
149
  }
86
150
 
87
151
  export function upsertTrackerEntry(
88
152
  state: TrackerState,
89
153
  entry: TrackerEntry,
154
+ advanceAuditCursor = true,
90
155
  ): TrackerState {
91
- const processed = [
156
+ let auditCursorTag = state.audit_cursor_tag;
157
+ if (auditCursorTag === null && !advanceAuditCursor) {
158
+ auditCursorTag = entry.previous_tag;
159
+ } else if (
160
+ advanceAuditCursor &&
161
+ entry.outcome === "error" &&
162
+ auditCursorTag === null
163
+ ) {
164
+ auditCursorTag = entry.previous_tag;
165
+ } else if (
166
+ advanceAuditCursor &&
167
+ isTerminalOutcome(entry.outcome) &&
168
+ (auditCursorTag === null || entry.previous_tag === auditCursorTag)
169
+ ) {
170
+ auditCursorTag = entry.tag;
171
+ }
172
+
173
+ const candidates = [
92
174
  entry,
93
- ...state.processed.filter((existing) => existing.tag !== entry.tag),
94
- ].slice(0, HIDDEN_STATE_LIMIT);
175
+ ...state.processed.filter(
176
+ (existing) =>
177
+ existing.tag !== entry.tag ||
178
+ existing.previous_tag !== entry.previous_tag,
179
+ ),
180
+ ];
181
+ let processed = candidates.slice(0, HIDDEN_STATE_LIMIT);
182
+ if (auditCursorTag !== null && !isSupportedAuditCursor(processed, auditCursorTag)) {
183
+ const support = candidates.find((candidate) =>
184
+ isAuditCursorSupportEntry(candidate, auditCursorTag),
185
+ );
186
+ if (!support) throw new Error("Codex audit cursor has no supporting entry");
187
+ processed = [...processed.slice(0, HIDDEN_STATE_LIMIT - 1), support];
188
+ }
95
189
 
96
190
  return {
191
+ audit_cursor_tag: auditCursorTag,
192
+ audit_cursor_validated: state.audit_cursor_validated,
97
193
  last_checked_tag: entry.tag,
98
194
  last_checked_at: entry.processed_at,
99
195
  processed,
@@ -179,28 +275,129 @@ function escapeTable(value: string): string {
179
275
  }
180
276
 
181
277
  function normalizeState(value: unknown): TrackerState {
182
- if (!isRecord(value)) return emptyTrackerState();
278
+ if (!isRecord(value)) throw new TypeError("tracker state must be an object");
279
+ if (!Array.isArray(value.processed) || !value.processed.every(isTrackerEntry)) {
280
+ throw new TypeError("tracker processed entries are invalid");
281
+ }
282
+ if (
283
+ value.last_checked_tag !== null &&
284
+ typeof value.last_checked_tag !== "string"
285
+ ) {
286
+ throw new TypeError("tracker last_checked_tag is invalid");
287
+ }
288
+ if (
289
+ value.last_checked_at !== null &&
290
+ typeof value.last_checked_at !== "string"
291
+ ) {
292
+ throw new TypeError("tracker last_checked_at is invalid");
293
+ }
183
294
 
184
- const processed = Array.isArray(value.processed)
185
- ? value.processed.filter(isTrackerEntry).slice(0, HIDDEN_STATE_LIMIT)
186
- : [];
295
+ const processed = value.processed.slice(
296
+ 0,
297
+ HIDDEN_STATE_LIMIT,
298
+ ) as TrackerEntry[];
299
+ const lastCheckedTag = value.last_checked_tag as string | null;
300
+ const lastChecked =
301
+ lastCheckedTag === null
302
+ ? null
303
+ : processed.find((entry) => entry.tag === lastCheckedTag);
304
+ if (lastCheckedTag === null ? processed.length > 0 : !lastChecked) {
305
+ throw new TypeError("tracker last_checked_tag is inconsistent");
306
+ }
307
+
308
+ let auditCursorTag: string | null;
309
+ let auditCursorValidated: boolean;
310
+ if (value.audit_cursor_tag === undefined) {
311
+ auditCursorTag = deriveLegacyAuditCursor(processed);
312
+ auditCursorValidated = false;
313
+ } else if (
314
+ (value.audit_cursor_tag === null ||
315
+ typeof value.audit_cursor_tag === "string") &&
316
+ value.audit_cursor_validated === true
317
+ ) {
318
+ auditCursorTag = value.audit_cursor_tag;
319
+ auditCursorValidated = true;
320
+ } else {
321
+ throw new TypeError("tracker audit cursor is invalid");
322
+ }
323
+ if (auditCursorTag !== null && !isStableTag(auditCursorTag)) {
324
+ throw new TypeError("tracker audit_cursor_tag is invalid");
325
+ }
326
+ if (auditCursorTag === null && processed.length > 0) {
327
+ throw new TypeError("tracker audit_cursor_tag is missing");
328
+ }
329
+ if (
330
+ auditCursorTag !== null &&
331
+ !isSupportedAuditCursor(processed, auditCursorTag)
332
+ ) {
333
+ throw new TypeError("tracker audit_cursor_tag is inconsistent");
334
+ }
187
335
 
188
336
  return {
189
- last_checked_tag:
190
- typeof value.last_checked_tag === "string"
191
- ? value.last_checked_tag
192
- : null,
193
- last_checked_at:
194
- typeof value.last_checked_at === "string" ? value.last_checked_at : null,
337
+ audit_cursor_tag: auditCursorTag,
338
+ audit_cursor_validated: auditCursorValidated,
339
+ last_checked_tag: lastCheckedTag,
340
+ last_checked_at: value.last_checked_at as string | null,
195
341
  processed,
196
342
  };
197
343
  }
198
344
 
345
+ function isSupportedAuditCursor(
346
+ processed: TrackerEntry[],
347
+ cursorTag: string,
348
+ ): boolean {
349
+ return processed.some((entry) =>
350
+ isAuditCursorSupportEntry(entry, cursorTag),
351
+ );
352
+ }
353
+
354
+ function isAuditCursorSupportEntry(
355
+ entry: TrackerEntry,
356
+ cursorTag: string,
357
+ ): boolean {
358
+ return (
359
+ (entry.tag === cursorTag && isTerminalOutcome(entry.outcome)) ||
360
+ entry.previous_tag === cursorTag
361
+ );
362
+ }
363
+
364
+ function deriveLegacyAuditCursor(processed: TrackerEntry[]): string | null {
365
+ const terminal = processed.filter((entry) =>
366
+ isTerminalOutcome(entry.outcome),
367
+ );
368
+ if (terminal.length === 0) {
369
+ const errorBaselines = [
370
+ ...new Set(
371
+ processed
372
+ .filter((entry) => entry.outcome === "error")
373
+ .map((entry) => entry.previous_tag),
374
+ ),
375
+ ];
376
+ if (errorBaselines.length > 1) {
377
+ throw new TypeError("legacy tracker error baselines are ambiguous");
378
+ }
379
+ return errorBaselines[0] ?? null;
380
+ }
381
+
382
+ const tags = terminal.map((entry) => entry.tag);
383
+ if (new Set(tags).size !== tags.length) {
384
+ throw new TypeError("legacy tracker terminal tags are duplicated");
385
+ }
386
+ const previousTags = new Set(terminal.map((entry) => entry.previous_tag));
387
+ const endpoints = tags.filter((tag) => !previousTags.has(tag));
388
+ if (endpoints.length !== 1) {
389
+ throw new TypeError("legacy tracker audit chain is ambiguous");
390
+ }
391
+ return endpoints[0] ?? null;
392
+ }
393
+
199
394
  function isTrackerEntry(value: unknown): value is TrackerEntry {
200
395
  if (!isRecord(value)) return false;
201
396
  return (
202
397
  typeof value.tag === "string" &&
398
+ isStableTag(value.tag) &&
203
399
  typeof value.previous_tag === "string" &&
400
+ isStableTag(value.previous_tag) &&
204
401
  isVerdict(value.verdict) &&
205
402
  isOutcome(value.outcome) &&
206
403
  (typeof value.pr_url === "string" || value.pr_url === null) &&
@@ -211,6 +408,10 @@ function isTrackerEntry(value: unknown): value is TrackerEntry {
211
408
  );
212
409
  }
213
410
 
411
+ function isStableTag(value: string): boolean {
412
+ return /^(?:rust-v|v)?\d+\.\d+\.\d+$/.test(value);
413
+ }
414
+
214
415
  function isVerdict(value: unknown): value is Verdict {
215
416
  return (
216
417
  value === "no-op" ||
@@ -145,6 +145,16 @@
145
145
  "timeoutMs": 15000,
146
146
  "reason": "Uses a top-level Bun module mock for the backend client."
147
147
  },
148
+ {
149
+ "path": "src/agent/available-models.test.ts",
150
+ "timeoutMs": 15000,
151
+ "reason": "Mutates the backend singleton, shared model catalog cache, and Date.now while testing cache freshness."
152
+ },
153
+ {
154
+ "path": "src/tools/toolset-provider-resolution.test.ts",
155
+ "timeoutMs": 15000,
156
+ "reason": "Mutates the backend singleton and shared model catalog cache while exercising provider fallback behavior."
157
+ },
148
158
  {
149
159
  "path": "src/tools/toolset-client-tool-rule-cleanup.test.ts",
150
160
  "timeoutMs": 15000,
@@ -175,6 +185,11 @@
175
185
  "timeoutMs": 15000,
176
186
  "reason": "Mutates HOME and the remote-settings singleton while exercising cwd recovery persistence."
177
187
  },
188
+ {
189
+ "path": "src/websocket/listener/commands/model-toolset.test.ts",
190
+ "timeoutMs": 15000,
191
+ "reason": "Mutates the backend singleton and shared model catalog cache while asserting backend-native catalog metadata."
192
+ },
178
193
  {
179
194
  "path": "src/websocket/listener/protocol-ergonomics.test.ts",
180
195
  "timeoutMs": 15000,
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { appendFileSync, writeFileSync } from "node:fs";
4
+ import {
5
+ createIssueWithBody,
6
+ editIssueBody,
7
+ ensureLabels,
8
+ findIssueByExactTitle,
9
+ getIssueBody,
10
+ getPullRequestStatus,
11
+ } from "./github.ts";
12
+ import {
13
+ analyzePiAiRelease,
14
+ compareStableVersions,
15
+ DEFAULT_TARGET_REPO,
16
+ findNextStableRelease,
17
+ listStableReleases,
18
+ readInstalledVersion,
19
+ } from "./release-analysis.ts";
20
+ import {
21
+ advanceMergedPr,
22
+ getPendingPrForCursor,
23
+ hasCompletedRange,
24
+ initialTrackerState,
25
+ parseTrackerState,
26
+ renderTrackerBody,
27
+ } from "./tracker.ts";
28
+
29
+ const DEFAULT_TRACKER_TITLE = "pi-ai dependency upgrade tracker";
30
+ const DEFAULT_ANALYSIS_FILE = "pi-ai-watch-analysis.json";
31
+
32
+ interface Args {
33
+ dryRun: boolean;
34
+ previousVersion: string | null;
35
+ currentVersion: string | null;
36
+ repo: string;
37
+ trackerTitle: string;
38
+ analysisFile: string;
39
+ }
40
+
41
+ interface TrackerIssue {
42
+ number: number;
43
+ url: string;
44
+ body: string;
45
+ }
46
+
47
+ function parseArgs(argv: string[]): Args {
48
+ const args: Args = {
49
+ dryRun: false,
50
+ previousVersion: null,
51
+ currentVersion: null,
52
+ repo: DEFAULT_TARGET_REPO,
53
+ trackerTitle: DEFAULT_TRACKER_TITLE,
54
+ analysisFile: DEFAULT_ANALYSIS_FILE,
55
+ };
56
+ for (let index = 0; index < argv.length; index += 1) {
57
+ const argument = argv[index];
58
+ if (argument === "--dry-run") args.dryRun = true;
59
+ else if (argument === "--previous-version") {
60
+ args.previousVersion = argv[++index] ?? null;
61
+ } else if (argument === "--current-version") {
62
+ args.currentVersion = argv[++index] ?? null;
63
+ } else if (argument === "--repo") {
64
+ args.repo = argv[++index] ?? args.repo;
65
+ } else if (argument === "--tracker-title") {
66
+ args.trackerTitle = argv[++index] ?? args.trackerTitle;
67
+ } else if (argument === "--analysis-file") {
68
+ args.analysisFile = argv[++index] ?? args.analysisFile;
69
+ } else if (argument === "--help" || argument === "-h") {
70
+ console.log(
71
+ "Usage: bun scripts/pi-ai-watch/agent-watch.ts [--dry-run] [--previous-version VERSION] [--current-version VERSION] [--repo OWNER/REPO] [--tracker-title TITLE] [--analysis-file FILE]",
72
+ );
73
+ process.exit(0);
74
+ } else {
75
+ throw new Error(`Unknown argument: ${argument}`);
76
+ }
77
+ }
78
+ return args;
79
+ }
80
+
81
+ async function main(): Promise<void> {
82
+ const args = parseArgs(process.argv.slice(2));
83
+ const installedVersion = readInstalledVersion();
84
+ const tracker = ensureTrackerIssue(args, installedVersion);
85
+ let state = parseTrackerState(tracker.body);
86
+ const releases = await listStableReleases();
87
+ let previousVersion = args.previousVersion;
88
+ let currentVersion = args.currentVersion;
89
+ const isScheduledSelection = !previousVersion && !currentVersion;
90
+
91
+ if (isScheduledSelection) {
92
+ const pending = getPendingPrForCursor(state);
93
+ if (pending?.pr_url) {
94
+ const status = getPullRequestStatus(pending.pr_url);
95
+ if (status.state === "OPEN") {
96
+ console.log(`Waiting for pending pi-ai upgrade PR: ${status.url}`);
97
+ writeCommonOutputs(tracker, {
98
+ installedVersion,
99
+ previousVersion: pending.previous_version,
100
+ currentVersion: pending.version,
101
+ });
102
+ writeOutput("pending_pr_url", status.url);
103
+ writeOutput("should_run_agent", "false");
104
+ return;
105
+ }
106
+ if (status.state === "MERGED") {
107
+ if (compareStableVersions(installedVersion, pending.version) < 0) {
108
+ throw new Error(
109
+ `Merged pi-ai PR ${status.url} targets ${pending.version}, but main still declares ${installedVersion}`,
110
+ );
111
+ }
112
+ state = advanceMergedPr(state, pending.version);
113
+ if (!args.dryRun) {
114
+ editIssueBody(args.repo, tracker.number, renderTrackerBody(state));
115
+ }
116
+ } else {
117
+ console.log(
118
+ `Retrying ${pending.version}; prior PR was closed unmerged.`,
119
+ );
120
+ }
121
+ }
122
+
123
+ const next = findNextStableRelease(releases, state.audit_cursor_version);
124
+ if (!next) {
125
+ console.log(
126
+ `No stable pi-ai release after ${state.audit_cursor_version}.`,
127
+ );
128
+ writeCommonOutputs(tracker, {
129
+ installedVersion,
130
+ previousVersion: state.audit_cursor_version,
131
+ currentVersion: state.audit_cursor_version,
132
+ });
133
+ writeOutput("should_run_agent", "false");
134
+ return;
135
+ }
136
+ previousVersion = state.audit_cursor_version;
137
+ currentVersion = next.version;
138
+ }
139
+
140
+ const analysis = await analyzePiAiRelease({
141
+ previousVersion,
142
+ currentVersion,
143
+ installedVersion,
144
+ stableReleases: releases,
145
+ });
146
+ writeFileSync(args.analysisFile, `${JSON.stringify(analysis, null, 2)}\n`);
147
+ writeCommonOutputs(tracker, {
148
+ installedVersion,
149
+ previousVersion: analysis.previous_version,
150
+ currentVersion: analysis.current_version,
151
+ });
152
+ writeOutput("analysis_file", args.analysisFile);
153
+
154
+ if (
155
+ !args.dryRun &&
156
+ hasCompletedRange(
157
+ state,
158
+ analysis.previous_version,
159
+ analysis.current_version,
160
+ )
161
+ ) {
162
+ console.log(
163
+ `Already completed pi-ai review ${analysis.previous_version}...${analysis.current_version}.`,
164
+ );
165
+ writeOutput("should_run_agent", "false");
166
+ return;
167
+ }
168
+
169
+ if (args.dryRun) {
170
+ console.log(JSON.stringify(analysis, null, 2));
171
+ writeOutput("should_run_agent", "false");
172
+ return;
173
+ }
174
+
175
+ console.log(
176
+ `pi-ai ${analysis.previous_version}...${analysis.current_version} needs Amelia review.`,
177
+ );
178
+ writeOutput("should_run_agent", "true");
179
+ }
180
+
181
+ function ensureTrackerIssue(
182
+ args: Args,
183
+ installedVersion: string,
184
+ ): TrackerIssue {
185
+ const existing = args.dryRun
186
+ ? null
187
+ : findIssueByExactTitle(args.repo, args.trackerTitle);
188
+ if (existing) {
189
+ return {
190
+ number: existing.number,
191
+ url: `https://github.com/${args.repo}/issues/${existing.number}`,
192
+ body: getIssueBody(args.repo, existing.number),
193
+ };
194
+ }
195
+
196
+ const body = renderTrackerBody(initialTrackerState(installedVersion));
197
+ if (args.dryRun) {
198
+ return {
199
+ number: 0,
200
+ url: `https://github.com/${args.repo}/issues/0`,
201
+ body,
202
+ };
203
+ }
204
+
205
+ const labels = ["pi-ai-watch", "automation"];
206
+ ensureLabels(args.repo, labels);
207
+ const issueUrl = createIssueWithBody(
208
+ args.repo,
209
+ args.trackerTitle,
210
+ body,
211
+ labels,
212
+ );
213
+ return {
214
+ number: issueNumberFromUrl(issueUrl),
215
+ url: issueUrl,
216
+ body,
217
+ };
218
+ }
219
+
220
+ function issueNumberFromUrl(issueUrl: string): number {
221
+ const issueNumber = Number(issueUrl.trim().split("/").at(-1));
222
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
223
+ throw new Error(`Could not parse issue number from ${issueUrl}`);
224
+ }
225
+ return issueNumber;
226
+ }
227
+
228
+ function writeCommonOutputs(
229
+ tracker: TrackerIssue,
230
+ versions: {
231
+ installedVersion: string;
232
+ previousVersion: string;
233
+ currentVersion: string;
234
+ },
235
+ ): void {
236
+ writeOutput("tracker_issue", String(tracker.number));
237
+ writeOutput("tracker_issue_url", tracker.url);
238
+ writeOutput("installed_version", versions.installedVersion);
239
+ writeOutput("previous_version", versions.previousVersion);
240
+ writeOutput("current_version", versions.currentVersion);
241
+ }
242
+
243
+ function writeOutput(name: string, value: string): void {
244
+ const outputPath = process.env.GITHUB_OUTPUT;
245
+ if (!outputPath) return;
246
+ appendFileSync(outputPath, `${name}=${value}\n`);
247
+ }
248
+
249
+ main().catch((error) => {
250
+ console.error(error);
251
+ writeOutput("should_run_agent", "false");
252
+ process.exit(1);
253
+ });