diff --git a/src/hooks/atlas/resolve-active-boulder-session.test.ts b/src/hooks/atlas/resolve-active-boulder-session.test.ts index d42027a3c..ad7cf372f 100644 --- a/src/hooks/atlas/resolve-active-boulder-session.test.ts +++ b/src/hooks/atlas/resolve-active-boulder-session.test.ts @@ -97,6 +97,74 @@ describe("resolveActiveBoulderSession", () => { expect(result?.boulderState.session_ids).toContain("ses_appended") }) + test("returns null for tracked session when mirror boulder is paused", async () => { + // given + const planPath = join(testDirectory, "paused-plan.md") + writeFileSync(planPath, "# Plan\n- [ ] Task 1\n", "utf-8") + writeBoulderState(testDirectory, { + active_plan: planPath, + started_at: "2026-01-02T10:00:00Z", + status: "paused", + session_ids: ["ses_tracked"], + session_origins: { ses_tracked: "direct" }, + plan_name: "paused-plan", + }) + + // when + const result = await resolveActiveBoulderSession({ + client: { session: { get: async () => ({ data: {} }) } } as never, + directory: testDirectory, + sessionID: "ses_tracked", + }) + + // then + expect(result).toBeNull() + }) + + test("returns null for tracked work session when resolved work is abandoned", async () => { + // given + const activePlanPath = join(testDirectory, "active-work-plan.md") + const abandonedPlanPath = join(testDirectory, "abandoned-work-plan.md") + writeFileSync(activePlanPath, "# Plan\n- [ ] Active task\n", "utf-8") + writeFileSync(abandonedPlanPath, "# Plan\n- [ ] Abandoned task\n", "utf-8") + writeBoulderState(testDirectory, { + schema_version: 2, + active_work_id: "work-active", + active_plan: activePlanPath, + started_at: "2026-01-02T10:00:00Z", + session_ids: ["ses_active"], + plan_name: "active-work-plan", + works: { + "work-active": { + work_id: "work-active", + active_plan: activePlanPath, + plan_name: "active-work-plan", + started_at: "2026-01-02T10:00:00Z", + session_ids: ["ses_active"], + status: "active", + }, + "work-abandoned": { + work_id: "work-abandoned", + active_plan: abandonedPlanPath, + plan_name: "abandoned-work-plan", + started_at: "2026-01-02T10:05:00Z", + session_ids: ["ses_abandoned"], + status: "abandoned", + }, + }, + }) + + // when + const result = await resolveActiveBoulderSession({ + client: { session: { get: async () => ({ data: {} }) } } as never, + directory: testDirectory, + sessionID: "ses_abandoned", + }) + + // then + expect(result).toBeNull() + }) + test("returns complete progress when a mirrored worktree plan is complete", async () => { // given const mainPlanPath = join(testDirectory, ".omo", "plans", "worktree-plan.md") diff --git a/src/hooks/atlas/resolve-active-boulder-session.ts b/src/hooks/atlas/resolve-active-boulder-session.ts index 85a4bb583..97f23dcb4 100644 --- a/src/hooks/atlas/resolve-active-boulder-session.ts +++ b/src/hooks/atlas/resolve-active-boulder-session.ts @@ -8,6 +8,10 @@ import { } from "../../features/boulder-state" import type { BoulderState, PlanProgress } from "../../features/boulder-state" +function isInactiveBoulderStatus(status: BoulderState["status"]): boolean { + return status === "paused" || status === "abandoned" +} + export async function resolveActiveBoulderSession(input: { client: PluginInput["client"] directory: string @@ -45,6 +49,10 @@ export async function resolveActiveBoulderSession(input: { } : boulderState + if (isInactiveBoulderStatus(nextBoulderState.status)) { + return null + } + const progress = getPlanProgress( sessionWork ? resolveBoulderPlanPathForWork(input.directory, sessionWork) diff --git a/src/hooks/atlas/tool-execute-after-task-timers.test.ts b/src/hooks/atlas/tool-execute-after-task-timers.test.ts index dfc8a1625..26dd4d346 100644 --- a/src/hooks/atlas/tool-execute-after-task-timers.test.ts +++ b/src/hooks/atlas/tool-execute-after-task-timers.test.ts @@ -291,6 +291,156 @@ describe("createToolExecuteAfterHandler task timers", () => { expect((taskSession?.elapsed_ms ?? 0) > 0).toBe(true) }) + it("does not end task timer when only a nested checkbox with the same label is checked", async () => { + // given + const parentSessionID = "ses_parent_nested_checkbox" + const planDirectory = join(testDirectory, ".omo", "plans") + mkdirSync(planDirectory, { recursive: true }) + const planPath = join(planDirectory, "task-timer-nested-checkbox-plan.md") + writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. Implement auth flow\n - [ ] 1. nested evidence\n", "utf-8") + writeBoulderState(testDirectory, { + schema_version: 2, + active_work_id: "work-1", + active_plan: planPath, + started_at: "2026-01-02T10:00:00Z", + session_ids: [parentSessionID], + plan_name: "task-timer-nested-checkbox-plan", + task_sessions: { + "todo:1": { + task_key: "todo:1", + task_label: "1", + task_title: "Implement auth flow", + session_id: "ses_child_nested_checkbox", + started_at: "2026-01-02T10:00:00Z", + status: "running", + updated_at: "2026-01-02T10:00:00Z", + }, + }, + works: { + "work-1": { + work_id: "work-1", + active_plan: planPath, + plan_name: "task-timer-nested-checkbox-plan", + started_at: "2026-01-02T10:00:00Z", + session_ids: [parentSessionID], + status: "active", + task_sessions: { + "todo:1": { + task_key: "todo:1", + task_label: "1", + task_title: "Implement auth flow", + session_id: "ses_child_nested_checkbox", + started_at: "2026-01-02T10:00:00Z", + status: "running", + updated_at: "2026-01-02T10:00:00Z", + }, + }, + }, + }, + }) + const { beforeHandler, afterHandler } = createHandlers() + + await beforeHandler( + { tool: "edit", sessionID: parentSessionID, callID: "call-task-timer-edit-nested" }, + { args: { filePath: planPath, oldString: " - [ ] 1. nested evidence", newString: " - [x] 1. nested evidence" } }, + ) + + writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. Implement auth flow\n - [x] 1. nested evidence\n", "utf-8") + + // when + await afterHandler( + { tool: "edit", sessionID: parentSessionID, callID: "call-task-timer-edit-nested" }, + { + title: "Edit", + output: "Updated file", + metadata: { + filePath: planPath, + }, + }, + ) + + // then + const taskSession = readBoulderState(testDirectory)?.works?.["work-1"]?.task_sessions?.["todo:1"] + expect(taskSession).toBeDefined() + expect(taskSession?.ended_at).toBeUndefined() + expect(taskSession?.status).toBe("running") + }) + + it("ends task timer when a top-level checkbox with the tracked label is checked", async () => { + // given + const parentSessionID = "ses_parent_top_level_checkbox" + const planDirectory = join(testDirectory, ".omo", "plans") + mkdirSync(planDirectory, { recursive: true }) + const planPath = join(planDirectory, "task-timer-top-level-checkbox-plan.md") + writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. Implement auth flow\n - [ ] 1. nested evidence\n", "utf-8") + writeBoulderState(testDirectory, { + schema_version: 2, + active_work_id: "work-1", + active_plan: planPath, + started_at: "2026-01-02T10:00:00Z", + session_ids: [parentSessionID], + plan_name: "task-timer-top-level-checkbox-plan", + task_sessions: { + "todo:1": { + task_key: "todo:1", + task_label: "1", + task_title: "Implement auth flow", + session_id: "ses_child_top_level_checkbox", + started_at: "2026-01-02T10:00:00Z", + status: "running", + updated_at: "2026-01-02T10:00:00Z", + }, + }, + works: { + "work-1": { + work_id: "work-1", + active_plan: planPath, + plan_name: "task-timer-top-level-checkbox-plan", + started_at: "2026-01-02T10:00:00Z", + session_ids: [parentSessionID], + status: "active", + task_sessions: { + "todo:1": { + task_key: "todo:1", + task_label: "1", + task_title: "Implement auth flow", + session_id: "ses_child_top_level_checkbox", + started_at: "2026-01-02T10:00:00Z", + status: "running", + updated_at: "2026-01-02T10:00:00Z", + }, + }, + }, + }, + }) + const { beforeHandler, afterHandler } = createHandlers() + + await beforeHandler( + { tool: "edit", sessionID: parentSessionID, callID: "call-task-timer-edit-top-level" }, + { args: { filePath: planPath, oldString: "- [ ] 1. Implement auth flow", newString: "- [x] 1. Implement auth flow" } }, + ) + + writeFileSync(planPath, "# Plan\n\n## TODOs\n- [x] 1. Implement auth flow\n - [ ] 1. nested evidence\n", "utf-8") + + // when + await afterHandler( + { tool: "edit", sessionID: parentSessionID, callID: "call-task-timer-edit-top-level" }, + { + title: "Edit", + output: "Updated file", + metadata: { + filePath: planPath, + }, + }, + ) + + // then + const taskSession = readBoulderState(testDirectory)?.works?.["work-1"]?.task_sessions?.["todo:1"] + expect(taskSession).toBeDefined() + expect(taskSession?.ended_at).toBeString() + expect(taskSession?.status).toBe("completed") + }) + it("tracks parallel delegated tasks by task label from TASK section", async () => { // given const parentSessionID = "ses_parent_parallel"