Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 16 additions & 0 deletions apps/cli/src/commands/coordination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 +
Expand Down
50 changes: 50 additions & 0 deletions apps/cli/test/coordination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const settings = loadSettings();
await withStore(settings, (store) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-15
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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).
72 changes: 67 additions & 5 deletions packages/core/src/coordination-sweep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions packages/core/test/coordination-sweep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading