@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.
- package/dist/agent-presets.js +17 -17
- package/dist/agent-presets.js.map +1 -1
- package/dist/mcp-client.js +2 -2
- package/dist/mcp-client.js.map +1 -1
- package/dist/types/agent/client-skills.d.ts +3 -0
- package/dist/types/agent/client-skills.d.ts.map +1 -1
- package/dist/types/agent/memory-git.d.ts +2 -0
- package/dist/types/agent/memory-git.d.ts.map +1 -1
- package/dist/types/agent/shared-memory-skills.d.ts +18 -0
- package/dist/types/agent/shared-memory-skills.d.ts.map +1 -0
- package/dist/types/backend/dev/pi-model-factory.d.ts +6 -0
- package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
- package/dist/types/backend/dev/pi-provider-registry.d.ts.map +1 -1
- package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
- package/dist/types/tools/impl/skill.d.ts +10 -5
- package/dist/types/tools/impl/skill.d.ts.map +1 -1
- package/dist/types/tools/secret-substitution.d.ts.map +1 -1
- package/dist/types/types/loop-status-protocol.d.ts +17 -0
- package/dist/types/types/loop-status-protocol.d.ts.map +1 -0
- package/dist/types/types/protocol_v2.d.ts +2 -19
- package/dist/types/types/protocol_v2.d.ts.map +1 -1
- package/dist/types/websocket/listener/inbound-queue.d.ts +5 -0
- package/dist/types/websocket/listener/inbound-queue.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts +9 -0
- package/dist/types/websocket/listener/protocol-outbound-routing.d.ts.map +1 -0
- package/dist/types/websocket/listener/protocol-outbound.d.ts.map +1 -1
- package/dist/types/websocket/listener/runtime.d.ts.map +1 -1
- package/dist/types/websocket/listener/turn-correlation.d.ts +10 -0
- package/dist/types/websocket/listener/turn-correlation.d.ts.map +1 -0
- package/dist/types/websocket/listener/types.d.ts +4 -0
- package/dist/types/websocket/listener/types.d.ts.map +1 -1
- package/image-resize-worker.js +42 -20
- package/letta.js +85237 -138922
- package/package.json +4 -3
- package/scripts/claude-watch/agent-watch.ts +622 -0
- package/scripts/claude-watch/docs-snapshot.test.ts +259 -0
- package/scripts/claude-watch/docs-snapshot.ts +672 -0
- package/scripts/claude-watch/fixtures/historical-replays.json +52 -0
- package/scripts/claude-watch/github.ts +137 -0
- package/scripts/claude-watch/release-analysis.test.ts +235 -0
- package/scripts/claude-watch/release-analysis.ts +297 -0
- package/scripts/claude-watch/release-source.test.ts +179 -0
- package/scripts/claude-watch/release-source.ts +369 -0
- package/scripts/claude-watch/runtime-observations.ts +98 -0
- package/scripts/claude-watch/runtime-probe.test.ts +576 -0
- package/scripts/claude-watch/runtime-probe.ts +911 -0
- package/scripts/claude-watch/runtime-sandbox.ts +170 -0
- package/scripts/claude-watch/state-branch.test.ts +211 -0
- package/scripts/claude-watch/state-branch.ts +316 -0
- package/scripts/claude-watch/tracker.test.ts +148 -0
- package/scripts/claude-watch/tracker.ts +325 -0
- package/scripts/claude-watch/types.ts +186 -0
- package/scripts/claude-watch/update-tracker.ts +201 -0
- package/scripts/codex-watch/agent-watch.ts +65 -10
- package/scripts/codex-watch/release-analysis.test.ts +57 -0
- package/scripts/codex-watch/release-analysis.ts +45 -3
- package/scripts/codex-watch/tracker.test.ts +243 -6
- package/scripts/codex-watch/tracker.ts +222 -23
- package/scripts/pi-ai-watch/agent-watch.ts +253 -0
- package/scripts/pi-ai-watch/github.ts +145 -0
- package/scripts/pi-ai-watch/release-analysis.test.ts +137 -0
- package/scripts/pi-ai-watch/release-analysis.ts +371 -0
- package/scripts/pi-ai-watch/tracker.test.ts +138 -0
- package/scripts/pi-ai-watch/tracker.ts +303 -0
- package/scripts/pi-ai-watch/update-tracker.ts +215 -0
- package/scripts/run-unit-tests.cjs +2 -0
- package/scripts/source-file-size-baseline.json +3 -3
- package/skills/creating-mods/references/commands.md +1 -1
- package/skills/creating-mods/references/ui.md +1 -1
- package/skills/customizing-commands/SKILL.md +1 -1
- package/skills/initializing-memory/SKILL.md +7 -7
- package/skills/self-configuration/SKILL.md +4 -4
- package/scripts/codex-watch/check-release.ts +0 -128
- 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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
82
|
-
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 (
|
|
97
|
-
|
|
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
|
|
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([
|