@letta-ai/letta-code 0.30.27 → 0.30.29

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 (74) hide show
  1. package/dist/agent-presets.js +17 -17
  2. package/dist/agent-presets.js.map +1 -1
  3. package/dist/mcp-client.js +2 -2
  4. package/dist/mcp-client.js.map +1 -1
  5. package/dist/types/agent/client-skills.d.ts +3 -0
  6. package/dist/types/agent/client-skills.d.ts.map +1 -1
  7. package/dist/types/agent/memory-git.d.ts +2 -0
  8. package/dist/types/agent/memory-git.d.ts.map +1 -1
  9. package/dist/types/agent/shared-memory-skills.d.ts +18 -0
  10. package/dist/types/agent/shared-memory-skills.d.ts.map +1 -0
  11. package/dist/types/backend/dev/pi-model-factory.d.ts +6 -0
  12. package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
  13. package/dist/types/backend/dev/pi-provider-registry.d.ts.map +1 -1
  14. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  15. package/dist/types/tools/impl/skill.d.ts +10 -5
  16. package/dist/types/tools/impl/skill.d.ts.map +1 -1
  17. package/dist/types/tools/secret-substitution.d.ts.map +1 -1
  18. package/dist/types/types/loop-status-protocol.d.ts +17 -0
  19. package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
  20. package/dist/types/types/protocol_v2.d.ts +2 -19
  21. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  22. package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
  23. package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
  24. package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
  25. package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
  26. package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
  27. package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
  28. package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
  29. package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
  30. package/dist/types/websocket/listener/types.d.ts +4 -0
  31. package/dist/types/websocket/listener/types.d.ts.map +1 -1
  32. package/image-resize-worker.js +42 -20
  33. package/letta.js +85237 -138922
  34. package/package.json +4 -3
  35. package/scripts/claude-watch/agent-watch.ts +622 -0
  36. package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
  37. package/scripts/claude-watch/docs-snapshot.ts +672 -0
  38. package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
  39. package/scripts/claude-watch/github.ts +137 -0
  40. package/scripts/claude-watch/release-analysis.test.ts +235 -0
  41. package/scripts/claude-watch/release-analysis.ts +297 -0
  42. package/scripts/claude-watch/release-source.test.ts +179 -0
  43. package/scripts/claude-watch/release-source.ts +369 -0
  44. package/scripts/claude-watch/runtime-observations.ts +98 -0
  45. package/scripts/claude-watch/runtime-probe.test.ts +576 -0
  46. package/scripts/claude-watch/runtime-probe.ts +911 -0
  47. package/scripts/claude-watch/runtime-sandbox.ts +170 -0
  48. package/scripts/claude-watch/state-branch.test.ts +211 -0
  49. package/scripts/claude-watch/state-branch.ts +316 -0
  50. package/scripts/claude-watch/tracker.test.ts +148 -0
  51. package/scripts/claude-watch/tracker.ts +325 -0
  52. package/scripts/claude-watch/types.ts +186 -0
  53. package/scripts/claude-watch/update-tracker.ts +201 -0
  54. package/scripts/codex-watch/agent-watch.ts +65 -10
  55. package/scripts/codex-watch/release-analysis.test.ts +57 -0
  56. package/scripts/codex-watch/release-analysis.ts +45 -3
  57. package/scripts/codex-watch/tracker.test.ts +243 -6
  58. package/scripts/codex-watch/tracker.ts +222 -23
  59. package/scripts/pi-ai-watch/agent-watch.ts +253 -0
  60. package/scripts/pi-ai-watch/github.ts +145 -0
  61. package/scripts/pi-ai-watch/release-analysis.test.ts +137 -0
  62. package/scripts/pi-ai-watch/release-analysis.ts +371 -0
  63. package/scripts/pi-ai-watch/tracker.test.ts +138 -0
  64. package/scripts/pi-ai-watch/tracker.ts +303 -0
  65. package/scripts/pi-ai-watch/update-tracker.ts +215 -0
  66. package/scripts/run-unit-tests.cjs +2 -0
  67. package/scripts/source-file-size-baseline.json +3 -3
  68. package/skills/creating-mods/references/commands.md +1 -1
  69. package/skills/creating-mods/references/ui.md +1 -1
  70. package/skills/customizing-commands/SKILL.md +1 -1
  71. package/skills/initializing-memory/SKILL.md +7 -7
  72. package/skills/self-configuration/SKILL.md +4 -4
  73. package/scripts/codex-watch/check-release.ts +0 -128
  74. package/scripts/codex-watch/render-issue.ts +0 -273
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { readFileSync } from "node:fs";
4
+ import { editIssueBody, getIssueBody, ghJson } from "./github.ts";
5
+ import {
6
+ loadStateSnapshotAtCommit,
7
+ resolveStateBranchTip,
8
+ } from "./state-branch.ts";
9
+ import {
10
+ findTrackerEntry,
11
+ hasProcessedCandidate,
12
+ isTerminalOutcome,
13
+ parseTrackerState,
14
+ recordAnalysis,
15
+ renderTrackerBody,
16
+ } from "./tracker.ts";
17
+ import type { ClaudeWatchAnalysis, ClaudeWatchOutcome } from "./types.ts";
18
+
19
+ const DEFAULT_REPO = "letta-ai/letta-code";
20
+
21
+ interface Args {
22
+ repo: string;
23
+ trackerIssue: number | null;
24
+ analysisFile: string | null;
25
+ candidateId: string | null;
26
+ stateCommitSha: string | null;
27
+ outcome: ClaudeWatchOutcome | null;
28
+ notes: string;
29
+ prUrl: string | null;
30
+ assertTerminal: boolean;
31
+ dryRun: boolean;
32
+ }
33
+
34
+ export function parseArgs(argv: string[]): Args {
35
+ const args: Args = {
36
+ repo: DEFAULT_REPO,
37
+ trackerIssue: null,
38
+ analysisFile: null,
39
+ candidateId: null,
40
+ stateCommitSha: null,
41
+ outcome: null,
42
+ notes: "",
43
+ prUrl: null,
44
+ assertTerminal: false,
45
+ dryRun: false,
46
+ };
47
+ for (let index = 0; index < argv.length; index += 1) {
48
+ const argument = argv[index];
49
+ if (argument === "--repo") args.repo = argv[++index] ?? args.repo;
50
+ else if (argument === "--tracker-issue")
51
+ args.trackerIssue = Number(argv[++index]);
52
+ else if (argument === "--analysis-file")
53
+ args.analysisFile = argv[++index] ?? null;
54
+ else if (argument === "--candidate-id")
55
+ args.candidateId = argv[++index] ?? null;
56
+ else if (argument === "--state-commit-sha")
57
+ args.stateCommitSha = argv[++index] ?? null;
58
+ else if (argument === "--outcome")
59
+ args.outcome = parseOutcome(argv[++index]);
60
+ else if (argument === "--notes") args.notes = argv[++index] ?? "";
61
+ else if (argument === "--pr-url") args.prUrl = argv[++index] ?? null;
62
+ else if (argument === "--assert-terminal") args.assertTerminal = true;
63
+ else if (argument === "--dry-run") args.dryRun = true;
64
+ else throw new Error(`Unknown argument: ${argument}`);
65
+ }
66
+ if (!args.trackerIssue || Number.isNaN(args.trackerIssue))
67
+ throw new Error("--tracker-issue is required");
68
+ if (args.assertTerminal) {
69
+ if (!args.candidateId || !args.stateCommitSha)
70
+ throw new Error(
71
+ "--candidate-id and --state-commit-sha are required with --assert-terminal",
72
+ );
73
+ } else {
74
+ if (!args.analysisFile) throw new Error("--analysis-file is required");
75
+ if (!args.outcome) throw new Error("--outcome is required");
76
+ if (isTerminalOutcome(args.outcome) && !args.stateCommitSha)
77
+ throw new Error("terminal outcomes require --state-commit-sha");
78
+ if (args.outcome === "pr_created" && !args.prUrl)
79
+ throw new Error("--pr-url is required for pr_created");
80
+ }
81
+ return args;
82
+ }
83
+
84
+ function parseOutcome(value: string | undefined): ClaudeWatchOutcome {
85
+ if (
86
+ value === "recorded_noop" ||
87
+ value === "no_local_impact" ||
88
+ value === "pr_created" ||
89
+ value === "needs_human_review" ||
90
+ value === "error"
91
+ ) {
92
+ return value;
93
+ }
94
+ throw new Error(`Unknown outcome: ${value}`);
95
+ }
96
+
97
+ export function verifyStateCandidate(
98
+ candidateId: string,
99
+ stateCommitSha: string,
100
+ repoPath = process.cwd(),
101
+ ): void {
102
+ const tip = resolveStateBranchTip(repoPath);
103
+ if (tip !== stateCommitSha) {
104
+ throw new Error(
105
+ `Claude state tip ${tip ?? "missing"} does not match ${stateCommitSha}`,
106
+ );
107
+ }
108
+ const snapshot = loadStateSnapshotAtCommit(repoPath, stateCommitSha);
109
+ if (snapshot?.candidate_id !== candidateId) {
110
+ throw new Error(
111
+ `Claude state candidate ${snapshot?.candidate_id ?? "missing"} does not match ${candidateId}`,
112
+ );
113
+ }
114
+ }
115
+
116
+ function verifyParityPr(
117
+ repo: string,
118
+ prUrl: string,
119
+ candidateId: string,
120
+ ): void {
121
+ const pr = ghJson<{
122
+ isDraft: boolean;
123
+ author: { login: string };
124
+ body: string | null;
125
+ }>(["pr", "view", prUrl, "--repo", repo, "--json", "isDraft,author,body"]);
126
+ if (!pr.isDraft || pr.author.login !== "carenthomas") {
127
+ throw new Error(
128
+ `Parity PR must be a draft authored by carenthomas (got draft=${pr.isDraft}, author=${pr.author.login})`,
129
+ );
130
+ }
131
+ if (!pr.body?.includes(`Claude-watch: ${candidateId}`)) {
132
+ throw new Error("Parity PR body is missing the exact Claude-watch marker");
133
+ }
134
+ }
135
+
136
+ export function main(argv = process.argv.slice(2)): void {
137
+ const args = parseArgs(argv);
138
+ const issue = args.trackerIssue as number;
139
+ const state = parseTrackerState(getIssueBody(args.repo, issue));
140
+
141
+ if (args.assertTerminal) {
142
+ verifyStateCandidate(
143
+ args.candidateId as string,
144
+ args.stateCommitSha as string,
145
+ );
146
+ const entry = findTrackerEntry(state, args.candidateId as string);
147
+ if (
148
+ !entry ||
149
+ !hasProcessedCandidate(state, args.candidateId as string) ||
150
+ entry.state_commit_sha !== args.stateCommitSha
151
+ ) {
152
+ throw new Error(
153
+ `Candidate ${args.candidateId} does not have a matching terminal tracker outcome`,
154
+ );
155
+ }
156
+ console.log(`Verified terminal Claude outcome for ${args.candidateId}`);
157
+ return;
158
+ }
159
+
160
+ const analysis = JSON.parse(
161
+ readFileSync(args.analysisFile as string, "utf8"),
162
+ ) as ClaudeWatchAnalysis;
163
+ if (isTerminalOutcome(args.outcome as ClaudeWatchOutcome)) {
164
+ verifyStateCandidate(analysis.candidate_id, args.stateCommitSha as string);
165
+ }
166
+ if (args.outcome === "pr_created") {
167
+ verifyParityPr(args.repo, args.prUrl as string, analysis.candidate_id);
168
+ }
169
+ const next = recordAnalysis(state, {
170
+ analysis,
171
+ outcome: args.outcome as ClaudeWatchOutcome,
172
+ notes: args.notes || defaultNotes(args.outcome as ClaudeWatchOutcome),
173
+ prUrl: args.prUrl,
174
+ stateCommitSha: args.stateCommitSha,
175
+ error:
176
+ args.outcome === "error" ? (analysis.errors.at(-1) ?? args.notes) : null,
177
+ });
178
+ const nextBody = renderTrackerBody(next);
179
+ if (args.dryRun) console.log(nextBody);
180
+ else editIssueBody(args.repo, issue, nextBody);
181
+ console.log(
182
+ `Recorded ${analysis.candidate_id} as ${args.outcome} in #${issue}`,
183
+ );
184
+ }
185
+
186
+ function defaultNotes(outcome: ClaudeWatchOutcome): string {
187
+ switch (outcome) {
188
+ case "recorded_noop":
189
+ return "no watched Claude surface changed";
190
+ case "no_local_impact":
191
+ return "reviewed; no local Letta Code mirror impact";
192
+ case "pr_created":
193
+ return "opened a focused local mirror PR";
194
+ case "needs_human_review":
195
+ return "public evidence needs human review";
196
+ case "error":
197
+ return "retryable watcher error";
198
+ }
199
+ }
200
+
201
+ if (import.meta.main) main();
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * Amelia-driven Codex release watcher entrypoint.
4
4
  *
5
- * This runs alongside the legacy per-release issue workflow. It uses a central
6
- * tracker issue for state and only asks Amelia to review non-noop releases.
5
+ * Uses a central tracker issue for state and only asks Amelia to review
6
+ * non-noop releases.
7
7
  */
8
8
 
9
9
  import { appendFileSync, writeFileSync } from "node:fs";
@@ -17,13 +17,19 @@ import {
17
17
  import {
18
18
  analyzeCodexRelease,
19
19
  DEFAULT_TARGET_REPO,
20
+ findNextStableRelease,
21
+ listStableReleases,
22
+ type Release,
20
23
  } from "./release-analysis.ts";
21
24
  import {
25
+ advanceCodexAuditCursor,
22
26
  emptyTrackerState,
23
- hasProcessedTag,
27
+ getCodexAuditCursorTag,
28
+ hasProcessedRange,
24
29
  parseTrackerState,
25
30
  recordAnalysis,
26
31
  renderTrackerBody,
32
+ validateLegacyCodexAuditCursor,
27
33
  } from "./tracker.ts";
28
34
 
29
35
  const DEFAULT_TRACKER_TITLE = "Codex upstream drift tracker";
@@ -77,15 +83,50 @@ function parseArgs(argv: string[]): Args {
77
83
 
78
84
  async function main() {
79
85
  const args = parseArgs(process.argv.slice(2));
86
+ const tracker = ensureTrackerIssue(args);
87
+ let state = parseTrackerState(tracker.body);
88
+ let sinceTag = args.sinceTag;
89
+ let currentTag = args.currentTag;
90
+ let stableReleases: Release[] | undefined;
91
+
92
+ if (!state.audit_cursor_validated) {
93
+ stableReleases = await listStableReleases();
94
+ state = validateLegacyCodexAuditCursor(
95
+ state,
96
+ stableReleases.map((release) => release.tag_name),
97
+ );
98
+ if (!args.dryRun) {
99
+ editIssueBody(args.repo, tracker.number, renderTrackerBody(state));
100
+ }
101
+ }
102
+
103
+ if (!sinceTag && !currentTag) {
104
+ const cursorTag = getCodexAuditCursorTag(state);
105
+ if (cursorTag) {
106
+ stableReleases ??= await listStableReleases();
107
+ const nextRelease = findNextStableRelease(stableReleases, cursorTag);
108
+ if (!nextRelease) {
109
+ console.log(`No stable release after ${cursorTag}; nothing to do.`);
110
+ writeOutput("tracker_issue", String(tracker.number));
111
+ writeOutput("tracker_issue_url", tracker.url);
112
+ writeOutput("current_tag", cursorTag);
113
+ writeOutput("previous_tag", cursorTag);
114
+ writeOutput("verdict", "no-op");
115
+ writeOutput("should_run_agent", "false");
116
+ return;
117
+ }
118
+ sinceTag = cursorTag;
119
+ currentTag = nextRelease.tag_name;
120
+ }
121
+ }
122
+
80
123
  const analysis = await analyzeCodexRelease({
81
- sinceTag: args.sinceTag,
82
- currentTag: args.currentTag,
124
+ sinceTag,
125
+ currentTag,
126
+ stableReleases,
83
127
  });
84
128
  writeFileSync(args.analysisFile, `${JSON.stringify(analysis, null, 2)}\n`);
85
129
 
86
- const tracker = ensureTrackerIssue(args);
87
- const state = parseTrackerState(tracker.body);
88
-
89
130
  writeOutput("tracker_issue", String(tracker.number));
90
131
  writeOutput("tracker_issue_url", tracker.url);
91
132
  writeOutput("analysis_file", args.analysisFile);
@@ -93,8 +134,22 @@ async function main() {
93
134
  writeOutput("previous_tag", analysis.previous_tag);
94
135
  writeOutput("verdict", analysis.verdict);
95
136
 
96
- if (!args.dryRun && hasProcessedTag(state, analysis.current_tag)) {
97
- console.log(`Already processed ${analysis.current_tag}; nothing to do.`);
137
+ if (
138
+ !args.dryRun &&
139
+ hasProcessedRange(state, analysis.previous_tag, analysis.current_tag)
140
+ ) {
141
+ const next = advanceCodexAuditCursor(
142
+ state,
143
+ analysis.previous_tag,
144
+ analysis.current_tag,
145
+ analysis.is_adjacent_release,
146
+ );
147
+ if (next !== state) {
148
+ editIssueBody(args.repo, tracker.number, renderTrackerBody(next));
149
+ }
150
+ console.log(
151
+ `Already processed ${analysis.previous_tag}...${analysis.current_tag}; nothing to do.`,
152
+ );
98
153
  writeOutput("should_run_agent", "false");
99
154
  return;
100
155
  }
@@ -0,0 +1,57 @@
1
+ import {
2
+ areAdjacentStableReleases,
3
+ findNextStableRelease,
4
+ type Release,
5
+ } from "./release-analysis.ts";
6
+
7
+ function release(tag: string, publishedAt: string): Release {
8
+ return {
9
+ tag_name: tag,
10
+ draft: false,
11
+ prerelease: false,
12
+ html_url: `https://github.com/openai/codex/releases/tag/${tag}`,
13
+ body: null,
14
+ published_at: publishedAt,
15
+ };
16
+ }
17
+
18
+ const STABLES = [
19
+ release("rust-v0.1.0", "2026-08-01T00:00:00Z"),
20
+ release("rust-v0.2.0", "2026-08-02T00:00:00Z"),
21
+ release("rust-v0.3.0", "2026-08-03T00:00:00Z"),
22
+ ];
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");
29
+ });
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);
46
+ });
47
+
48
+ test("returns null when the durable cursor is current", () => {
49
+ expect(findNextStableRelease(STABLES, "rust-v0.3.0")).toBeNull();
50
+ });
51
+
52
+ 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");
56
+ });
57
+ });
@@ -9,7 +9,6 @@ import {
9
9
  type ModelsJson,
10
10
  type Verdict,
11
11
  } from "./diff-models-json.ts";
12
- import type { PathChangeSummary, RenderInput } from "./render-issue.ts";
13
12
 
14
13
  export const CODEX_REPO = "openai/codex";
15
14
  export const DEFAULT_TARGET_REPO =
@@ -35,11 +34,26 @@ export interface Release {
35
34
  export interface AnalyzeCodexReleaseOptions {
36
35
  sinceTag: string | null;
37
36
  currentTag: string | null;
37
+ stableReleases?: Release[];
38
38
  }
39
39
 
40
- export interface CodexWatchAnalysis extends RenderInput {
40
+ export interface PathChangeSummary {
41
+ path: string;
42
+ commits: string[];
43
+ }
44
+
45
+ export interface CodexWatchAnalysis {
46
+ previous_tag: string;
47
+ current_tag: string;
48
+ is_adjacent_release: boolean;
49
+ release_url: string;
50
+ release_notes_md: string;
41
51
  verdict: Verdict;
42
52
  models_diff: ModelsDiff | null;
53
+ prompt_md_changed: boolean;
54
+ prompt_md_diff_preview: string | null;
55
+ path_changes: PathChangeSummary[];
56
+ workflow_run_url: string;
43
57
  compare_url: string;
44
58
  changed_files: string[];
45
59
  }
@@ -47,7 +61,7 @@ export interface CodexWatchAnalysis extends RenderInput {
47
61
  export async function analyzeCodexRelease(
48
62
  options: AnalyzeCodexReleaseOptions,
49
63
  ): Promise<CodexWatchAnalysis> {
50
- const stables = await listStableReleases();
64
+ const stables = options.stableReleases ?? (await listStableReleases());
51
65
  if (stables.length === 0) throw new Error("No stable Codex releases found");
52
66
 
53
67
  const current = options.currentTag
@@ -125,6 +139,11 @@ export async function analyzeCodexRelease(
125
139
  return {
126
140
  previous_tag: previous.tag_name,
127
141
  current_tag: current.tag_name,
142
+ is_adjacent_release: areAdjacentStableReleases(
143
+ stables,
144
+ previous.tag_name,
145
+ current.tag_name,
146
+ ),
128
147
  release_url: current.html_url,
129
148
  release_notes_md: current.body ?? "",
130
149
  verdict,
@@ -188,6 +207,29 @@ function findPreviousStable(
188
207
  return stables[idx - 1] ?? null;
189
208
  }
190
209
 
210
+ export function areAdjacentStableReleases(
211
+ stables: Release[],
212
+ previousTag: string,
213
+ currentTag: string,
214
+ ): boolean {
215
+ return (
216
+ findPreviousStable(stables, currentTag)?.tag_name === previousTag
217
+ );
218
+ }
219
+
220
+ export function findNextStableRelease(
221
+ stables: Release[],
222
+ terminalTag: string,
223
+ ): Release | null {
224
+ const terminalIndex = stables.findIndex(
225
+ (release) => release.tag_name === terminalTag,
226
+ );
227
+ if (terminalIndex < 0) {
228
+ throw new Error(`Could not find terminal release ${terminalTag}`);
229
+ }
230
+ return stables[terminalIndex + 1] ?? null;
231
+ }
232
+
191
233
  function cloneCodex(tmp: string): string {
192
234
  const dir = join(tmp, "codex");
193
235
  git([