From 29b44fffd0bce61e9bdeff9e6c99fe60390141fc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 11 May 2026 13:40:48 +0900 Subject: [PATCH] feat(hooks/atlas): call completeBoulder when progress.isComplete --- .../atlas/idle-event-complete-boulder.test.ts | 78 +++++++++++++++++++ src/hooks/atlas/idle-event.ts | 8 ++ 2 files changed, 86 insertions(+) create mode 100644 src/hooks/atlas/idle-event-complete-boulder.test.ts diff --git a/src/hooks/atlas/idle-event-complete-boulder.test.ts b/src/hooks/atlas/idle-event-complete-boulder.test.ts new file mode 100644 index 000000000..a03b27fe7 --- /dev/null +++ b/src/hooks/atlas/idle-event-complete-boulder.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { randomUUID } from "node:crypto" +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { clearBoulderState, readBoulderState, writeBoulderState } from "../../features/boulder-state" + +const { createAtlasHook } = await import("./index") + +describe("atlas hook idle-event complete boulder", () => { + let testDirectory = "" + + beforeEach(() => { + testDirectory = join(tmpdir(), `atlas-idle-complete-${randomUUID()}`) + if (!existsSync(testDirectory)) { + mkdirSync(testDirectory, { recursive: true }) + } + clearBoulderState(testDirectory) + }) + + afterEach(() => { + clearBoulderState(testDirectory) + if (existsSync(testDirectory)) { + rmSync(testDirectory, { recursive: true, force: true }) + } + }) + + it("marks work completed with ended_at and elapsed_ms when progress is complete", async () => { + // given + const sessionID = "ses_complete" + const planPath = join(testDirectory, "complete-plan.md") + writeFileSync(planPath, "# Plan\n\n## TODOs\n- [x] 1. Done\n", "utf-8") + writeBoulderState(testDirectory, { + schema_version: 2, + active_work_id: "work-complete", + active_plan: planPath, + started_at: "2026-01-02T10:00:00.000Z", + session_ids: [sessionID], + plan_name: "complete-plan", + works: { + "work-complete": { + work_id: "work-complete", + active_plan: planPath, + plan_name: "complete-plan", + started_at: "2026-01-02T10:00:00.000Z", + session_ids: [sessionID], + status: "active", + }, + }, + }) + + const hook = createAtlasHook({ + directory: testDirectory, + client: { + session: { + get: async () => ({ data: { id: sessionID } }), + messages: async () => ({ data: [] }), + prompt: async () => ({ data: {} }), + promptAsync: async () => ({ data: {} }), + }, + }, + } as unknown as Parameters[0]) + + // when + await hook.handler({ + event: { + type: "session.idle", + properties: { sessionID }, + }, + }) + + // then + const work = readBoulderState(testDirectory)?.works?.["work-complete"] + expect(work?.status).toBe("completed") + expect(work?.ended_at).toBeString() + expect((work?.elapsed_ms ?? 0) > 0).toBe(true) + }) +}) diff --git a/src/hooks/atlas/idle-event.ts b/src/hooks/atlas/idle-event.ts index 22a755468..417a1f5b9 100644 --- a/src/hooks/atlas/idle-event.ts +++ b/src/hooks/atlas/idle-event.ts @@ -1,6 +1,8 @@ import type { PluginInput } from "@opencode-ai/plugin" import { + completeBoulder, getPlanProgress, + getWorkForSession, getTaskSessionState, readBoulderState, readCurrentTopLevelTask, @@ -220,6 +222,12 @@ export async function handleAtlasSessionIdle(input: { const { boulderState, progress, appendedSession } = activeBoulderSession if (progress.isComplete) { + const work = getWorkForSession(ctx.directory, sessionID) + if (work) { + completeBoulder(ctx.directory, work.work_id) + } else { + completeBoulder(ctx.directory, boulderState.active_work_id) + } log(`[${HOOK_NAME}] Boulder complete`, { sessionID, plan: boulderState.plan_name }) return }