Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
892674f
ci(release): resolve develop-into-next sync conflicts with Claude
avallete Oct 9, 2026
625cd6d
ci(release): repair the develop-into-next sync PR after CI and review
avallete Oct 9, 2026
3cd302c
Merge remote-tracking branch 'origin/develop' into avallete/ia-workfl…
avallete Oct 10, 2026
816393f
ci(release): address the AI review of the sync repair loop
avallete Oct 10, 2026
5dce049
ci(release): fix CI and the second AI review of the sync repair loop
avallete Oct 10, 2026
3a0bb0f
ci(release): redact agent text and harden sync resolution checks
avallete Oct 10, 2026
dbd4f7a
ci(release): keep secret-shaped agent output off the sync branch
avallete Oct 10, 2026
37efb50
ci(release): scan every line the sync agent adds for secrets
avallete Oct 10, 2026
44295d2
ci(release): address the remaining sync review findings
avallete Oct 12, 2026
d027b7f
ci(release): keep approval jobs and check outages out of sync repair
avallete Oct 12, 2026
d63ccad
ci(release): list unreported resolver edits and flag lockfile failures
avallete Oct 12, 2026
314ce36
ci(release): keep the Anthropic key out of the sync agent's container
avallete Oct 12, 2026
c67bfbf
ci(release): slim the Claude develop-into-next sync down to resolve a…
avallete Oct 12, 2026
99fceb9
Merge branch 'develop' into avallete/ia-workflow-next-sync-5c9aa1
avallete Oct 12, 2026
6e9786c
ci(release): let the dispatched AI review run Codex on sync PRs
avallete Oct 12, 2026
2a7c810
ci(release): deny the sync agent's searches under /proc
avallete Oct 12, 2026
f07f22e
ci(release): address the AI review of the slim sync agent
avallete Oct 12, 2026
36d0739
ci(release): check the resolved sync merge and let Claude fix it befo…
avallete Oct 12, 2026
de1fd97
ci(release): keep only quick checks and one fix pass in the sync reso…
avallete Oct 12, 2026
49b1ccc
ci(release): split the sync resolve into resolve, check, and fix jobs
avallete Oct 12, 2026
7018c1a
ci(release): propose the sync agent's workflow edits as review sugges…
avallete Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,13 @@ Full procedures live in the
**Resolving a sync PR.** When `Sync branches` cannot merge cleanly it opens
`sync/<source>-into-<target>` (for example `sync/develop-into-next`); CODEOWNERS
requests the CLI team, and for `main-into-develop` the release Slack channel is
also told. Further syncs for that pair skip while it is open. Merge the target into the sync
branch, resolve, and push; then **approve** the PR. Approval fast-forwards the
also told. Further syncs for that pair skip while it is open. For
`develop-into-next`, Claude resolves the merge on the branch and repairs CI
failures and AI review findings; check its comments, answer any decision it
lists, and approve
([runbook](../apps/cli/docs/release-process.md#claude-resolved-develop-into-next)).
Otherwise merge the target into the sync branch, resolve, and push; then
**approve** the PR. Approval fast-forwards the
target and deletes the branch. Never use the merge button. If the target or the
source moved, the bot merges the latest target and source into the approved head and lands
it directly if the merge is clean, as for a clean sync (the merge commit is
Expand Down
183 changes: 183 additions & 0 deletions .github/scripts/sync-agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { describe, expect, test } from "bun:test";

import { type CheckRun, REQUIRED_CHECKS } from "./fast-forward.ts";
import { buildSuggestions, planRepair, type ReviewThread } from "./sync-agent.ts";

const HEAD = "a".repeat(40);
const RESOLVED = "<!-- sync-agent resolve -->\nClaude resolved the merge.";
const repaired = (head: string) => `<!-- sync-agent repair head=${head} -->`;

function run(id: number, name: string, conclusion: string | null = "success"): CheckRun {
return { id, name, status: conclusion === null ? "in_progress" : "completed", conclusion };
}

const green = REQUIRED_CHECKS.map((name, index) => run(index + 1, name));
const finding: ReviewThread = {
threadId: "T_1",
commentId: 7,
path: "apps/cli/src/index.ts",
line: 3,
body: "This drops develop's fix.",
};

describe("planRepair", () => {
test("repairs a failed job once the head's checks settle, ignoring the branch-policy check", () => {
const failed = run(99, "Check code quality", "failure");
const plan = planRepair({
head: HEAD,
reviewRunning: false,
botComments: [RESOLVED],
runs: [...green, failed, run(100, "Require fast-forward", "failure")],
threads: [],
});

expect(plan).toEqual({ action: "repair", failures: [failed], threads: [] });
});

test("waits while a check runs or a required check has not started", () => {
const running = planRepair({
head: HEAD,
reviewRunning: false,
botComments: [RESOLVED],
runs: [...green, run(99, "CodeQL", null)],
threads: [finding],
});
const missing = planRepair({
head: HEAD,
reviewRunning: false,
botComments: [RESOLVED],
runs: green.slice(1),
threads: [finding],
});

expect(running).toEqual({ action: "skip", reason: "Checks are still running." });
expect(missing).toEqual({ action: "skip", reason: "Checks are still running." });
});

test("leaves a hand-resolved pull request to the maintainer", () => {
const plan = planRepair({
head: HEAD,
botComments: [],
runs: green,
threads: [finding],
reviewRunning: false,
});

expect(plan.action).toBe("skip");
});

test("uses the latest attempt of a re-run job", () => {
const plan = planRepair({
head: HEAD,
reviewRunning: false,
botComments: [RESOLVED],
runs: [...green, run(-1, "Check code quality", "failure")],
threads: [],
});

expect(plan).toEqual({ action: "skip", reason: "Nothing to repair." });
});

test("hands a head a round left failing to a maintainer unless new findings arrive", () => {
const state = {
head: HEAD,
reviewRunning: false,
botComments: [RESOLVED, repaired(HEAD)],
runs: [...green, run(99, "Check code quality", "failure")],
};

expect(planRepair({ ...state, threads: [] }).action).toBe("exhausted");
expect(planRepair({ ...state, threads: [finding] }).action).toBe("repair");
});

test("hands the pull request to a maintainer once after the round limit", () => {
const state = {
head: HEAD,
reviewRunning: false,
runs: [...green, run(99, "Check code quality", "failure")],
threads: [],
};
const rounds = [RESOLVED, repaired("b".repeat(40)), repaired("c".repeat(40))];

expect(planRepair({ ...state, botComments: rounds }).action).toBe("exhausted");
});

test("stops repairing after the handoff, even on a maintainer's new head", () => {
const plan = planRepair({
head: "d".repeat(40),
reviewRunning: false,
botComments: [RESOLVED, repaired(HEAD), "<!-- sync-agent exhausted -->"],
runs: [...green, run(99, "Check code quality", "failure")],
threads: [finding],
});

expect(plan).toEqual({ action: "skip", reason: "Already handed to a maintainer." });
});

test("waits for this pull request's AI review to finish", () => {
const plan = planRepair({
head: HEAD,
reviewRunning: true,
botComments: [RESOLVED],
runs: [...green, run(99, "Check code quality", "failure")],
threads: [],
});

expect(plan).toEqual({ action: "skip", reason: "The AI review is still running." });
});

test("waits while workflow suggestions on the head are not committed", () => {
const plan = planRepair({
head: HEAD,
reviewRunning: false,
botComments: [RESOLVED, `<!-- sync-agent workflow-suggestions head=${HEAD} -->`],
runs: [...green, run(99, "Check code quality", "failure")],
threads: [],
});

expect(plan.action).toBe("skip");
});
});

describe("buildSuggestions", () => {
const diff = [
"diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml",
"--- a/.github/workflows/test.yml",
"+++ b/.github/workflows/test.yml",
"@@ -10 +9,0 @@",
'- go-cache: "false"',
"@@ -20,0 +20 @@",
"+ shell: bash",
"@@ -90 +89 @@",
"- old: true",
"+ new: true",
"diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml",
"deleted file mode 100644",
"--- a/.github/workflows/go.yml",
"+++ /dev/null",
"@@ -1 +0,0 @@",
"-name: Go",
"",
].join("\n");

test("suggests edits inside the pull request's diff and leaves the rest to apply by hand", () => {
const { comments, leftover } = buildSuggestions(
diff,
new Map([[".github/workflows/test.yml", [[5, 25]]]]),
(_path, line) => `line ${line}`,
);

expect(comments).toEqual([
{ path: ".github/workflows/test.yml", line: 10, side: "RIGHT", body: "```suggestion\n```" },
{
path: ".github/workflows/test.yml",
line: 20,
side: "RIGHT",
body: "```suggestion\nline 20\n shell: bash\n```",
},
]);
expect(leftover).toContain("@@ -90 +89 @@");
expect(leftover).toContain("deleted file mode 100644");
expect(leftover).not.toContain("go-cache");
});
});
Loading
Loading