Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
02064f0
fix: avoid multi-line stderr in workflow annotations
claude Feb 25, 2026
4d84c1f
fix: sync template setup-api-client with retry-with-backoff and annot…
claude Feb 25, 2026
c36adc1
fix: pre-timeout watchdog, robust parser import, and always-commit sa…
claude Feb 25, 2026
7924d1e
fix: preserve indented checkbox states in PR Meta body sync
claude Feb 26, 2026
6145c5e
chore: sync template scripts
github-actions[bot] Feb 26, 2026
6811187
fix: address review comments on watchdog pre-timeout mechanism
claude Feb 26, 2026
0b9a46c
docs: add watchdog-saved to workflow outputs reference
claude Feb 26, 2026
97056c0
fix: skip non-issue refs like "Run #NNN" in extractIssueNumberFromPull
claude Feb 26, 2026
96e5f6e
feat: add session analysis, completion comment, and error diagnostics…
claude Feb 26, 2026
9280958
fix: address code review feedback from Codex and Copilot
claude Feb 26, 2026
f8737e8
merge: resolve conflicts with main (keep watchdog improvements)
claude Feb 26, 2026
65e89c5
fix: address sync PR review feedback from coding agents
claude Feb 26, 2026
9e89707
fix: install js-yaml locally instead of globally in label sync workflow
claude Feb 26, 2026
7a11836
Fix auto-pilot stall when belt dispatcher run is cancelled
claude Feb 26, 2026
5d4a9ad
Fix keepalive loop stuck in perpetual review after progress stall
claude Feb 26, 2026
26eb35e
Address review feedback on belt dispatch verification
claude Feb 26, 2026
7874710
Merge main: resolve conflict in maint-69-sync-labels.yml
claude Feb 26, 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: 9 additions & 0 deletions .github/scripts/keepalive_loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -2967,6 +2967,9 @@ async function updateKeepaliveLoopSummary({ github: rawGithub, context, core, in
// tasks off. Re-derive the counter here with the authoritative counts.
// When force_retry is active, honour the evaluate-step's reset to 0 and
// do not overwrite it — the human explicitly wants a fresh start.
// After a review action, reset to 0 so the next evaluate triggers a run
// instead of another review (the review already provided course-correction
// feedback — the agent needs a chance to act on it).
if (isForceRetry) {
if (roundsWithoutTaskCompletion !== 0) {
core?.info?.(
Expand All @@ -2975,6 +2978,12 @@ async function updateKeepaliveLoopSummary({ github: rawGithub, context, core, in
);
roundsWithoutTaskCompletion = 0;
}
} else if (action === 'review') {
core?.info?.(
`[summary] review action completed — resetting rounds_without_task_completion ` +
`from ${roundsWithoutTaskCompletion} to 0 so next iteration runs the agent`,
);
roundsWithoutTaskCompletion = 0;
} else {
const prevTasks = previousState?.tasks || {};
const prevUncheckedForCounter = toNumber(prevTasks.unchecked, tasksUnchecked);
Expand Down
160 changes: 143 additions & 17 deletions .github/workflows/agents-auto-pilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1916,23 +1916,90 @@ jobs:
return;
}

// Force-dispatch Codex belt dispatcher to create the branch
try {
await withRetry((client) => client.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
ref: baseBranch,
inputs: {
agent_key: agentKey,
force_issue: issueNumber.toString(),
dry_run: 'false'
// Force-dispatch Codex belt dispatcher to create the branch.
// Retry up to 3 times because GitHub Actions can silently cancel
// queued runs before they receive a runner (observed on issue #34).
const maxDispatchAttempts = 3;
let dispatchSucceeded = false;
for (let attempt = 1; attempt <= maxDispatchAttempts; attempt++) {
const dispatchedAt = new Date();
try {
await withRetry((client) => client.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
ref: baseBranch,
inputs: {
agent_key: agentKey,
force_issue: issueNumber.toString(),
dry_run: 'false'
}
}));
core.info(
`Dispatched belt dispatcher (agent: ${agentKey}) ` +
`for issue #${issueNumber} (attempt ${attempt}/${maxDispatchAttempts})`
);
} catch (dispatchError) {
core.warning(
`Belt dispatch attempt ${attempt} failed: ${dispatchError?.message}`
);
if (attempt < maxDispatchAttempts) {
await new Promise(r => setTimeout(r, attempt * 5000));
}
}));
const prefix = `Dispatched belt dispatcher (agent: ${agentKey}) for issue`;
core.info(`${prefix} #${issueNumber}`);
} catch (dispatchError) {
core.warning(`Could not dispatch belt dispatcher: ${dispatchError?.message}`);
continue;
}

// Wait briefly then verify the dispatched run is queued/in_progress
// (not cancelled before receiving a runner). Only consider runs
// created after the dispatch timestamp to avoid matching stale runs
// for other issues.
await new Promise(r => setTimeout(r, 15000));
try {
const { data: runs } = await withRetry((client) =>
client.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
per_page: 5,
})
);
const recentRuns = runs.workflow_runs.filter(
r => new Date(r.created_at) >= dispatchedAt
);
const alive = recentRuns.find(
r => r.status === 'queued' || r.status === 'in_progress'
);
if (alive) {
core.info(`Belt dispatcher run ${alive.id} is ${alive.status}`);
dispatchSucceeded = true;
break;
}
const succeeded = recentRuns.find(
r => r.conclusion === 'success'
);
if (succeeded) {
core.info(`Belt dispatcher run ${succeeded.id} already succeeded`);
dispatchSucceeded = true;
break;
}
const latest = recentRuns[0] || runs.workflow_runs[0];
core.warning(
`Belt dispatcher run not alive after attempt ${attempt}` +
` (latest: ${latest?.id} ${latest?.conclusion}); ` +
(attempt < maxDispatchAttempts ? 'retrying…' : 'no more attempts.')
);
} catch (checkError) {
core.warning(
`Could not verify dispatcher run after attempt ${attempt}: ` +
`${checkError?.message}; status unknown, will retry.`
);
}
}
if (!dispatchSucceeded) {
core.warning(
`Belt dispatcher could not be confirmed after ${maxDispatchAttempts} attempts ` +
`for issue #${issueNumber}. The branch-check loop will re-dispatch if needed.`
);
}

- name: Metrics - End capability check timer
Expand Down Expand Up @@ -2380,14 +2447,73 @@ jobs:
const actualBackoffMs = Math.min(backoffMs, maxBackoffMs);
const actualMinutes = Math.round(actualBackoffMs / 60000);

// Re-dispatch the belt if no recent dispatcher run is active
// for this issue. Only consider runs created in the last 30
// minutes to avoid matching stale runs for other issues.
let redispatched = false;
try {
const { data: runs } = await withRetry((client) =>
client.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
per_page: 10,
})
);
const cutoff = new Date(Date.now() - 30 * 60 * 1000);
const recentRuns = runs.workflow_runs.filter(
r => new Date(r.created_at) >= cutoff
);
const alive = recentRuns.find(
r => r.status === 'queued' || r.status === 'in_progress'
);
if (!alive) {
core.info(
`No active belt dispatcher run in last 30m ` +
`(${recentRuns.length} recent runs checked); re-dispatching`
);
const { data: repoInfo } = await withRetry((client) =>
client.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo,
})
);
const dispatchRef = repoInfo.default_branch || 'main';
await withRetry((client) =>
client.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'agents-71-codex-belt-dispatcher.yml',
ref: dispatchRef,
inputs: {
agent_key: agentKey,
force_issue: String(issueNumber),
dry_run: 'false',
},
})
);
redispatched = true;
core.info(`Re-dispatched belt for issue #${issueNumber}`);
} else {
core.info(
`Belt dispatcher run ${alive.id} still ${alive.status}; skipping re-dispatch`
);
}
} catch (redispatchErr) {
core.warning(`Belt re-dispatch check failed: ${redispatchErr?.message}`);
}

const redispatchNote = redispatched
? ' Re-dispatched belt dispatcher.'
: '';
core.info(`Applying branch-creation backoff: waiting ${actualMinutes} minutes`);
await withRetry((client) => client.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `🤖 **Auto-pilot**: Backoff delay (${actualMinutes}m)

Branch not created yet. Attempt ${stallCount + 1}/${maxStallRetries}.
Branch not created yet. Attempt ${stallCount + 1}/${maxStallRetries}.${redispatchNote}
Waiting before retry...`
}));

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/agents-keepalive-loop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ jobs:
- evaluate
- run-codex
- run-claude
- progress-review
# Run if PR exists, handle skipped/failed agent jobs gracefully
# run-codex will be skipped when action != run/fix/conflict, which is expected
if: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/maint-69-sync-labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
github_token: ${{ github.token }}

- name: Install js-yaml
run: npm install js-yaml
run: npm install --no-save --no-package-lock js-yaml

- name: Parse labels-core.yml
id: parse
Expand Down
9 changes: 9 additions & 0 deletions templates/consumer-repo/.github/scripts/keepalive_loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -2967,6 +2967,9 @@ async function updateKeepaliveLoopSummary({ github: rawGithub, context, core, in
// tasks off. Re-derive the counter here with the authoritative counts.
// When force_retry is active, honour the evaluate-step's reset to 0 and
// do not overwrite it — the human explicitly wants a fresh start.
// After a review action, reset to 0 so the next evaluate triggers a run
// instead of another review (the review already provided course-correction
// feedback — the agent needs a chance to act on it).
if (isForceRetry) {
if (roundsWithoutTaskCompletion !== 0) {
core?.info?.(
Expand All @@ -2975,6 +2978,12 @@ async function updateKeepaliveLoopSummary({ github: rawGithub, context, core, in
);
roundsWithoutTaskCompletion = 0;
}
} else if (action === 'review') {
core?.info?.(
`[summary] review action completed — resetting rounds_without_task_completion ` +
`from ${roundsWithoutTaskCompletion} to 0 so next iteration runs the agent`,
);
roundsWithoutTaskCompletion = 0;
} else {
const prevTasks = previousState?.tasks || {};
const prevUncheckedForCounter = toNumber(prevTasks.unchecked, tasksUnchecked);
Expand Down
Loading
Loading