diff --git a/apps/cli/src/commands/coordination.ts b/apps/cli/src/commands/coordination.ts index 5236297..c32c654 100644 --- a/apps/cli/src/commands/coordination.ts +++ b/apps/cli/src/commands/coordination.ts @@ -175,6 +175,9 @@ function renderCoordinationSweep( lines.push( ` quota-pending claims: ${result.summary.quota_pending_claims} released expired quota-pending: ${result.summary.released_expired_quota_pending_claim_count}`, ); + if (result.released_quota_pending_summary.released_count > 0) { + lines.push(` ${renderReleasedQuotaPendingSummary(result)}`); + } renderSection( lines, @@ -333,6 +336,19 @@ function renderSweepMode(appliedModes: string[]): string { return `${appliedModes.join(', ')}, audit-retaining`; } +function renderReleasedQuotaPendingSummary(result: CoordinationSweepResult): string { + const summary = result.released_quota_pending_summary; + const oldest = + summary.oldest_age_minutes === null ? 'n/a' : `${Math.round(summary.oldest_age_minutes)}m`; + const topTasks = summary.top_tasks + .map( + (task) => + `#${task.task_id} ${task.branch} released=${task.released_count} oldest=${Math.round(task.oldest_age_minutes)}m`, + ) + .join('; '); + return `quota release summary: released=${summary.released_count} oldest=${oldest} top_tasks=${topTasks}`; +} + function staleSignalCount(result: CoordinationSweepResult): number { return ( result.summary.stale_claim_count + diff --git a/apps/cli/test/coordination.test.ts b/apps/cli/test/coordination.test.ts index ddf66d0..6cf7231 100644 --- a/apps/cli/test/coordination.test.ts +++ b/apps/cli/test/coordination.test.ts @@ -596,8 +596,58 @@ describe('colony coordination CLI', () => { } }); }); + + it('prints a compact quota release audit summary', async () => { + let taskId = 0; + const settings = loadSettings(); + await withStore(settings, (store) => { + taskId = seedExpiredQuotaPendingClaims(store); + }); + + await createProgram().parseAsync( + [ + 'node', + 'test', + 'coordination', + 'sweep', + '--repo-root', + repoRoot, + '--release-expired-quota', + ], + { from: 'node' }, + ); + + expect(output).toContain('released expired quota-pending: 2'); + expect(output).toContain( + `quota release summary: released=2 oldest=300m top_tasks=#${taskId} agent/codex/quota-sweep released=2 oldest=300m`, + ); + }); }); +function seedExpiredQuotaPendingClaims(store: MemoryStore): number { + setMinutesAgo(300); + store.startSession({ id: 'codex@quota-sweep', ide: 'codex', cwd: repoRoot }); + const thread = TaskThread.open(store, { + repo_root: repoRoot, + branch: 'agent/codex/quota-sweep', + title: 'quota sweep task', + session_id: 'codex@quota-sweep', + }); + thread.join('codex@quota-sweep', 'codex'); + thread.claimFile({ session_id: 'codex@quota-sweep', file_path: 'src/quota-a.ts' }); + thread.claimFile({ session_id: 'codex@quota-sweep', file_path: 'src/quota-b.ts' }); + thread.relay({ + from_session_id: 'codex@quota-sweep', + from_agent: 'codex', + reason: 'quota', + one_line: 'quota stopped before sweep', + base_branch: 'main', + expires_in_ms: 5 * MINUTE_MS, + }); + vi.setSystemTime(NOW); + return thread.task_id; +} + async function seedSameBranchDuplicateClaims(): Promise { const settings = loadSettings(); await withStore(settings, (store) => { diff --git a/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/.openspec.yaml b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/.openspec.yaml new file mode 100644 index 0000000..9f70866 --- /dev/null +++ b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-15 diff --git a/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/proposal.md b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/proposal.md new file mode 100644 index 0000000..b2fb9ca --- /dev/null +++ b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/proposal.md @@ -0,0 +1,14 @@ +## Why + +`colony coordination sweep --release-expired-quota` can already downgrade expired quota-pending claims, but operators need a compact audit summary that says how much was released, which tasks were affected most, and the oldest released claim age. + +## What Changes + +- Add `released_quota_pending_summary` to coordination sweep results. +- Summarize released quota-pending claims by total count, oldest age, and top affected tasks. +- Render the same compact summary in human `colony coordination sweep` output. +- Add focused core and CLI regression coverage. + +## Impact + +This is additive metadata and display text. Existing JSON consumers keep the existing fields, while operators get clearer proof after applying quota cleanup. diff --git a/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/specs/sweep-expired-quota-pending-claims-via-coordination-sweep/spec.md b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/specs/sweep-expired-quota-pending-claims-via-coordination-sweep/spec.md new file mode 100644 index 0000000..e858c5c --- /dev/null +++ b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/specs/sweep-expired-quota-pending-claims-via-coordination-sweep/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: Expired Quota Sweep Audit Summary +When coordination sweep releases expired or aged quota-pending claims, the system SHALL include compact audit summary metadata for the release. + +#### Scenario: Released quota claims include compact summary +- **GIVEN** coordination sweep releases one or more quota-pending claims +- **WHEN** the sweep result is returned +- **THEN** `released_quota_pending_summary.released_count` equals the number of released quota-pending claims +- **AND** `released_quota_pending_summary.oldest_age_minutes` reports the oldest released claim age +- **AND** `released_quota_pending_summary.top_tasks` reports affected tasks with released count and oldest age +- **AND** human `colony coordination sweep` output includes the same compact quota release summary. diff --git a/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/tasks.md b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/tasks.md new file mode 100644 index 0000000..eece117 --- /dev/null +++ b/openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/tasks.md @@ -0,0 +1,34 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58`; branch=`agent/codex/sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58`; scope=`packages/core/src/coordination-sweep.ts`, `apps/cli/src/commands/coordination.ts`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58` on branch `agent/codex/sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58`. Work inside the existing sandbox, review `openspec/changes/agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58 --base main --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58`. +- [x] 1.2 Define normative requirements in `specs/sweep-expired-quota-pending-claims-via-coordination-sweep/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes. +- [x] 2.2 Add/update focused regression coverage. + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands. +- [x] 3.2 Run `openspec validate agent-codex-sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58 --type change --strict`. +- [x] 3.3 Run `openspec validate --specs`. + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/codex/sweep-expired-quota-pending-claims-via-c-2026-05-15-15-58 --base main --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/packages/core/src/coordination-sweep.ts b/packages/core/src/coordination-sweep.ts index 9b1206d..a1186a0 100644 --- a/packages/core/src/coordination-sweep.ts +++ b/packages/core/src/coordination-sweep.ts @@ -127,6 +127,7 @@ export interface CoordinationSweepResult { released_stale_downstream_blockers: ReleasedStaleDownstreamBlocker[]; released_same_branch_duplicate_claims: ReleasedSameBranchDuplicateClaim[]; released_expired_quota_pending_claims: ReleasedExpiredQuotaPendingClaim[]; + released_quota_pending_summary: ReleasedQuotaPendingSummary; released_stale_claims: ReleasedStaleClaim[]; downgraded_stale_claims: DowngradedStaleClaim[]; skipped_dirty_claims: SkippedDirtyClaim[]; @@ -357,6 +358,17 @@ export interface ReleasedExpiredQuotaPendingClaim { audit_observation_id: number; } +export interface ReleasedQuotaPendingSummary { + released_count: number; + oldest_age_minutes: number | null; + top_tasks: Array<{ + task_id: number; + branch: string; + released_count: number; + oldest_age_minutes: number; + }>; +} + export interface DowngradedStaleClaim { task_id: number; file_path: string; @@ -469,8 +481,14 @@ export function buildCoordinationSweep( typeof opts.release_aged_quota_pending_minutes === 'number' ? releaseAgedQuotaPendingClaims(store, agedQuotaPendingClaims, now) : []; + const releasedQuotaPendingClaims = [ + ...released_expired_quota_pending_claims, + ...released_aged_quota_pending_claims, + ]; const releasedQuotaPendingClaimCount = - released_expired_quota_pending_claims.length + released_aged_quota_pending_claims.length; + releasedQuotaPendingClaims.length; + const released_quota_pending_summary = + summarizeReleasedQuotaPendingClaims(releasedQuotaPendingClaims); const archived_completed_plans = opts.archive_completed_plans === true ? archiveCompletedPlans(store, opts) : []; const remainingStaleClaims = filterRemainingStaleClaims(staleClaims, staleClaimCleanup); @@ -562,10 +580,8 @@ export function buildCoordinationSweep( same_branch_duplicate_claims: remainingSameBranchDuplicateClaims, released_stale_downstream_blockers, released_same_branch_duplicate_claims, - released_expired_quota_pending_claims: [ - ...released_expired_quota_pending_claims, - ...released_aged_quota_pending_claims, - ], + released_expired_quota_pending_claims: releasedQuotaPendingClaims, + released_quota_pending_summary, released_stale_claims: staleClaimCleanup.released_stale_claims, downgraded_stale_claims: staleClaimCleanup.downgraded_stale_claims, skipped_dirty_claims: staleClaimCleanup.skipped_dirty_claims, @@ -1359,6 +1375,52 @@ function releaseAgedQuotaPendingClaims( return released; } +function summarizeReleasedQuotaPendingClaims( + claims: ReleasedExpiredQuotaPendingClaim[], +): ReleasedQuotaPendingSummary { + if (claims.length === 0) { + return { + released_count: 0, + oldest_age_minutes: null, + top_tasks: [], + }; + } + + const byTask = new Map< + number, + { task_id: number; branch: string; released_count: number; oldest_age_minutes: number } + >(); + let oldestAgeMinutes = 0; + for (const claim of claims) { + oldestAgeMinutes = Math.max(oldestAgeMinutes, claim.age_minutes); + const existing = byTask.get(claim.task_id); + if (existing) { + existing.released_count += 1; + existing.oldest_age_minutes = Math.max(existing.oldest_age_minutes, claim.age_minutes); + } else { + byTask.set(claim.task_id, { + task_id: claim.task_id, + branch: claim.branch, + released_count: 1, + oldest_age_minutes: claim.age_minutes, + }); + } + } + + return { + released_count: claims.length, + oldest_age_minutes: oldestAgeMinutes, + top_tasks: [...byTask.values()] + .sort( + (left, right) => + right.released_count - left.released_count || + right.oldest_age_minutes - left.oldest_age_minutes || + left.task_id - right.task_id, + ) + .slice(0, 5), + }; +} + function expireQuotaBatonObservationIfPending( store: MemoryStore, task_id: number, diff --git a/packages/core/test/coordination-sweep.test.ts b/packages/core/test/coordination-sweep.test.ts index 3009f11..9c477fa 100644 --- a/packages/core/test/coordination-sweep.test.ts +++ b/packages/core/test/coordination-sweep.test.ts @@ -276,6 +276,18 @@ describe('buildCoordinationSweep stale claim cleanup', () => { released_quota_pending_claims: 1, }); expect(applied.safe_cleanup.released_quota_pending_claims).toBe(1); + expect(applied.released_quota_pending_summary).toEqual({ + released_count: 1, + oldest_age_minutes: 300, + top_tasks: [ + { + task_id: taskId, + branch: 'agent/quota-expired', + released_count: 1, + oldest_age_minutes: 300, + }, + ], + }); expect(applied.released_expired_quota_pending_claims).toEqual([ expect.objectContaining({ task_id: taskId,