From c5ea53bab599d16a6276506e65594a83358b878f Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 15 May 2026 21:03:07 +0200 Subject: [PATCH] Auto-archive plans after grace window --- apps/mcp-server/src/tools/plan.ts | 64 ++++++++++++++++--- apps/mcp-server/test/plan.test.ts | 35 +++++++++- .../.openspec.yaml | 2 + .../proposal.md | 19 ++++++ .../spec.md | 19 ++++++ .../tasks.md | 37 +++++++++++ 6 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/.openspec.yaml create mode 100644 openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/proposal.md create mode 100644 openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/specs/auto-archive-completed-queen-plans-when-remaining-subtasks-0/spec.md create mode 100644 openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/tasks.md diff --git a/apps/mcp-server/src/tools/plan.ts b/apps/mcp-server/src/tools/plan.ts index 5b7f5eb..e141096 100644 --- a/apps/mcp-server/src/tools/plan.ts +++ b/apps/mcp-server/src/tools/plan.ts @@ -582,6 +582,12 @@ export function register(server: McpServer, ctx: ToolContext): void { parent_spec_task_id: located.info.parent_spec_task_id, session_id: args.session_id, }); + scheduleAutoArchiveRetryIfNeeded(store, { + plan_slug: args.plan_slug, + parent_spec_task_id: located.info.parent_spec_task_id, + session_id: args.session_id, + outcome: autoArchive, + }); return { content: [ @@ -1237,6 +1243,7 @@ function completedSpecRowCells(spec: Spec, specRowId: string): string[] { interface AutoArchiveOutcome { status: 'archived' | 'blocked' | 'error' | 'skipped'; reason?: string; + retry_after_ms?: number; archived_path?: string; merged_root_hash?: string; applied?: number; @@ -1251,6 +1258,7 @@ interface AutoArchiveOutcome { * can land first if the lane wants explicit archival. */ const AUTO_ARCHIVE_GRACE_PERIOD_MS = 60_000; +const pendingAutoArchiveRetries = new Set(); function runAutoArchiveIfReady( store: MemoryStore, @@ -1301,6 +1309,7 @@ function runAutoArchiveIfReady( return { status: 'skipped', reason: 'auto_archive grace period pending', + retry_after_ms: AUTO_ARCHIVE_GRACE_PERIOD_MS - (now - latest), }; } } @@ -1429,6 +1438,42 @@ function runAutoArchiveIfReady( } } +function scheduleAutoArchiveRetryIfNeeded( + store: MemoryStore, + args: { + plan_slug: string; + parent_spec_task_id: number | null; + session_id: string; + outcome: AutoArchiveOutcome; + }, +): void { + if (args.parent_spec_task_id == null) return; + if ( + args.outcome.status !== 'skipped' || + args.outcome.reason !== 'auto_archive grace period pending' + ) { + return; + } + const key = `${store.dbPath}:${args.parent_spec_task_id}:${args.plan_slug}`; + if (pendingAutoArchiveRetries.has(key)) return; + pendingAutoArchiveRetries.add(key); + const delay = Math.max(0, args.outcome.retry_after_ms ?? AUTO_ARCHIVE_GRACE_PERIOD_MS); + const timer = setTimeout(() => { + pendingAutoArchiveRetries.delete(key); + try { + ensurePlanAutoArchiveSweepSession(store); + runAutoArchiveIfReady(store, { + plan_slug: args.plan_slug, + parent_spec_task_id: args.parent_spec_task_id, + session_id: PLAN_AUTO_ARCHIVE_SWEEP_SESSION, + }); + } catch { + // Best-effort: explicit task_plan_list / completion paths can retry. + } + }, delay); + timer.unref?.(); +} + /** * Resolve the parent spec task's `repo_root` to a directory that still * exists on disk. When the task was created inside an agent worktree @@ -1486,14 +1531,7 @@ function sweepCompletedPlansForAutoArchive(store: MemoryStore, repo_root?: strin const total = counts.available + counts.claimed + counts.completed + counts.blocked; if (total === 0 || counts.completed !== total) continue; if (!sessionEnsured) { - if (!store.storage.getSession(PLAN_AUTO_ARCHIVE_SWEEP_SESSION)) { - store.startSession({ - id: PLAN_AUTO_ARCHIVE_SWEEP_SESSION, - ide: 'plan-system', - cwd: null, - metadata: { source: 'plan-auto-archive-sweep' }, - }); - } + ensurePlanAutoArchiveSweepSession(store); sessionEnsured = true; } runAutoArchiveIfReady(store, { @@ -1507,6 +1545,16 @@ function sweepCompletedPlansForAutoArchive(store: MemoryStore, repo_root?: strin } } +function ensurePlanAutoArchiveSweepSession(store: MemoryStore): void { + if (store.storage.getSession(PLAN_AUTO_ARCHIVE_SWEEP_SESSION)) return; + store.startSession({ + id: PLAN_AUTO_ARCHIVE_SWEEP_SESSION, + ide: 'plan-system', + cwd: null, + metadata: { source: 'plan-auto-archive-sweep' }, + }); +} + function latestSubtaskCompletedAt( store: MemoryStore, siblingTasks: Array<{ id: number }>, diff --git a/apps/mcp-server/test/plan.test.ts b/apps/mcp-server/test/plan.test.ts index 6b626df..fa34955 100644 --- a/apps/mcp-server/test/plan.test.ts +++ b/apps/mcp-server/test/plan.test.ts @@ -14,7 +14,7 @@ import { MemoryStore, TaskThread, type WorktreeContentionReport } from '@colony/ import { createPlanWorkspace } from '@colony/spec'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { buildServer } from '../src/server.js'; let dataDir: string; @@ -1390,6 +1390,39 @@ describe('task_plan auto-archive', () => { } }); + it('archives automatically after the grace window without a list call', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-05-15T12:00:00Z')); + try { + await call( + 'task_plan_publish', + basicPublishArgs({ slug: 'auto-archive-grace-timer' }), + ); + await claimAndComplete('auto-archive-grace-timer', 0, 'B', 'codex'); + const last = await claimAndComplete('auto-archive-grace-timer', 1, 'C', 'claude'); + expect(last.auto_archive.status).toBe('skipped'); + expect(last.auto_archive.reason).toMatch(/grace/); + expect(existsSync(join(repoRoot, 'openspec/changes/auto-archive-grace-timer/CHANGE.md'))) + .toBe(true); + + await vi.advanceTimersByTimeAsync(60_000); + + expect(existsSync(join(repoRoot, 'openspec/changes/auto-archive-grace-timer/CHANGE.md'))) + .toBe(false); + const parentTask = store.storage + .listTasks(2000) + .find((t) => t.branch === 'spec/auto-archive-grace-timer'); + expect(parentTask).toBeDefined(); + if (parentTask) { + const archived = store.storage.taskObservationsByKind(parentTask.id, 'plan-archived', 10); + expect(archived).toHaveLength(1); + expect(archived[0]?.session_id).toBe('plan-auto-archive-sweep'); + } + } finally { + vi.useRealTimers(); + } + }); + it('delta written then archive throws', async () => { const slug = 'delta-archive-throws'; const published = await call( diff --git a/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/.openspec.yaml b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/.openspec.yaml new file mode 100644 index 0000000..9f70866 --- /dev/null +++ b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-15 diff --git a/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/proposal.md b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/proposal.md new file mode 100644 index 0000000..847812d --- /dev/null +++ b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/proposal.md @@ -0,0 +1,19 @@ +## Why + +Completed Queen plans with `auto_archive=false` defer archival for a short +grace window. The completion path returned `auto_archive grace period pending`, +but without a later `task_plan_list` call the plan could remain completed and +unarchived until health surfaced a recommendation. + +## What Changes + +Schedule a bounded retry when the final sub-task completion hits the grace +window. The retry reuses the existing three-way archive path, records the normal +`plan-archived` / `plan-archive-blocked` / `plan-archive-error` observations, +and dedupes timers per store + plan. + +## Impact + +Affected surface is MCP plan completion. Existing `task_plan_list` sweep remains +as a fallback. Timers are unref'd and best-effort, so explicit list/completion +paths can still retry after process restarts. diff --git a/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/specs/auto-archive-completed-queen-plans-when-remaining-subtasks-0/spec.md b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/specs/auto-archive-completed-queen-plans-when-remaining-subtasks-0/spec.md new file mode 100644 index 0000000..219c430 --- /dev/null +++ b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/specs/auto-archive-completed-queen-plans-when-remaining-subtasks-0/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: auto-archive-completed-queen-plans-when-remaining-subtasks-0 behavior +The MCP plan system SHALL automatically retry archiving a completed Queen plan +after the default grace window when the final sub-task completion deferred +archival. + +#### Scenario: Completion reaches grace window +- **GIVEN** a Queen plan has no remaining incomplete sub-tasks +- **AND** the completion result reports `auto_archive grace period pending` +- **WHEN** the grace window elapses +- **THEN** the system retries the existing three-way archive path automatically +- **AND** records the normal `plan-archived` observation on success. + +#### Scenario: Archive retry cannot merge cleanly +- **GIVEN** the automatic grace-window retry reaches a conflicting spec archive +- **WHEN** the three-way archive path reports conflicts +- **THEN** the system records the normal `plan-archive-blocked` observation +- **AND** later explicit archive/list paths can retry after manual repair. diff --git a/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/tasks.md b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/tasks.md new file mode 100644 index 0000000..ff8d13f --- /dev/null +++ b/openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/tasks.md @@ -0,0 +1,37 @@ +## 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-auto-archive-completed-queen-plans-when-2026-05-15-20-56`; branch=`agent/codex/auto-archive-completed-queen-plans-when-2026-05-15-20-56`; scope=`apps/mcp-server/src/tools/plan.ts`; action=`finish verification and cleanup`. +- Copy prompt: Continue `agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56` on branch `agent/codex/auto-archive-completed-queen-plans-when-2026-05-15-20-56`. Work inside the existing sandbox, review `openspec/changes/agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56/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/auto-archive-completed-queen-plans-when-2026-05-15-20-56 --base main --via-pr --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56`. +- [x] 1.2 Define normative requirements in `specs/auto-archive-completed-queen-plans-when-remaining-subtasks-0/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. + - `pnpm --filter @colony/mcp-server test -- plan.test.ts` + - `pnpm --filter @colony/mcp-server typecheck` + - `git diff --check` +- [x] 3.2 Run `openspec validate agent-codex-auto-archive-completed-queen-plans-when-2026-05-15-20-56 --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// --base dev --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).