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 6bda79239..c565e5c2d 100644
--- a/src/hooks/atlas/tool-execute-after-task-timers.test.ts
+++ b/src/hooks/atlas/tool-execute-after-task-timers.test.ts
@@ -292,4 +292,142 @@ describe("createToolExecuteAfterHandler task timers", () => {
expect((taskSession?.elapsed_ms ?? 0) > 0).toBe(true)
})
+ it("tracks parallel delegated tasks by task label from TASK section", async () => {
+ // given
+ const parentSessionID = "ses_parent_parallel"
+ const planPath = join(testDirectory, "task-timer-parallel-plan.md")
+ writeFileSync(
+ planPath,
+ "# Plan\n\n## TODOs\n- [ ] 1. First task\n- [ ] 2. Add tests\n- [ ] 3. Write docs\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-parallel-plan",
+ works: {
+ "work-1": {
+ work_id: "work-1",
+ active_plan: planPath,
+ plan_name: "task-timer-parallel-plan",
+ started_at: "2026-01-02T10:00:00Z",
+ session_ids: [parentSessionID],
+ status: "active",
+ },
+ },
+ })
+ const { beforeHandler, afterHandler } = createHandlers({
+ ses_child_parallel_2: parentSessionID,
+ ses_child_parallel_3: parentSessionID,
+ })
+
+ await beforeHandler(
+ { tool: "task", sessionID: parentSessionID, callID: "call-task-parallel-2" },
+ {
+ args: {
+ prompt: "## 1. TASK\n- [ ] 2. Add tests\n\n## 2. CONTEXT\n...",
+ },
+ },
+ )
+ await beforeHandler(
+ { tool: "task", sessionID: parentSessionID, callID: "call-task-parallel-3" },
+ {
+ args: {
+ prompt: "## 1. TASK\n- [ ] 3. Write docs\n\n## 2. CONTEXT\n...",
+ },
+ },
+ )
+
+ // when
+ await afterHandler(
+ { tool: "task", sessionID: parentSessionID, callID: "call-task-parallel-2" },
+ {
+ title: "Sisyphus Task",
+ output: "Task completed\n\nsession_id: ses_child_parallel_2\n",
+ metadata: {
+ sessionId: "ses_child_parallel_2",
+ agent: "sisyphus-junior",
+ category: "deep",
+ },
+ },
+ )
+ await afterHandler(
+ { tool: "task", sessionID: parentSessionID, callID: "call-task-parallel-3" },
+ {
+ title: "Sisyphus Task",
+ output: "Task completed\n\nsession_id: ses_child_parallel_3\n",
+ metadata: {
+ sessionId: "ses_child_parallel_3",
+ agent: "sisyphus-junior",
+ category: "deep",
+ },
+ },
+ )
+
+ // then
+ const taskSessions = readBoulderState(testDirectory)?.works?.["work-1"]?.task_sessions
+ expect(taskSessions?.["todo:2"]?.task_key).toBe("todo:2")
+ expect(taskSessions?.["todo:3"]?.task_key).toBe("todo:3")
+ expect(taskSessions?.["todo:1"]).toBeUndefined()
+ })
+
+ it("falls back to current top-level task when TASK section label is missing", async () => {
+ // given
+ const parentSessionID = "ses_parent_fallback"
+ const childSessionID = "ses_child_fallback"
+ const planPath = join(testDirectory, "task-timer-fallback-plan.md")
+ writeFileSync(planPath, "# Plan\n\n## TODOs\n- [ ] 1. First task\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-fallback-plan",
+ works: {
+ "work-1": {
+ work_id: "work-1",
+ active_plan: planPath,
+ plan_name: "task-timer-fallback-plan",
+ started_at: "2026-01-02T10:00:00Z",
+ session_ids: [parentSessionID],
+ status: "active",
+ },
+ },
+ })
+ const { beforeHandler, afterHandler } = createHandlers({
+ [childSessionID]: parentSessionID,
+ })
+
+ await beforeHandler(
+ { tool: "task", sessionID: parentSessionID, callID: "call-task-fallback-1" },
+ {
+ args: {
+ prompt: "No structured header in this prompt",
+ },
+ },
+ )
+
+ // when
+ await afterHandler(
+ { tool: "task", sessionID: parentSessionID, callID: "call-task-fallback-1" },
+ {
+ title: "Sisyphus Task",
+ output: "Task completed\n\nsession_id: ses_child_fallback\n",
+ metadata: {
+ sessionId: childSessionID,
+ agent: "sisyphus-junior",
+ category: "deep",
+ },
+ },
+ )
+
+ // then
+ const taskSessions = readBoulderState(testDirectory)?.works?.["work-1"]?.task_sessions
+ expect(taskSessions?.["todo:1"]?.task_key).toBe("todo:1")
+ })
+
})
diff --git a/src/hooks/atlas/tool-execute-before.ts b/src/hooks/atlas/tool-execute-before.ts
index 88e31fc13..d6e4789ee 100644
--- a/src/hooks/atlas/tool-execute-before.ts
+++ b/src/hooks/atlas/tool-execute-before.ts
@@ -11,6 +11,49 @@ import { isSisyphusPath } from "./sisyphus-path"
import type { PendingTaskRef, TrackedTopLevelTaskRef } from "./types"
import { isWriteOrEditToolName } from "./write-edit-tool-policy"
+const TASK_SECTION_HEADER_PATTERN = /^##\s*1\.\s*TASK\s*$/i
+const TODO_TASK_LINE_PATTERN = /^(?:[-*]\s*\[\s*\]\s*)?(\d+)\.\s+(.+)$/
+const FINAL_WAVE_TASK_LINE_PATTERN = /^(?:[-*]\s*\[\s*\]\s*)?(F\d+)\.\s+(.+)$/i
+
+function parseTrackedTaskFromPrompt(prompt: string): TrackedTopLevelTaskRef | null {
+ const lines = prompt.split(/\r?\n/)
+ const taskHeaderIndex = lines.findIndex((line) => TASK_SECTION_HEADER_PATTERN.test(line.trim()))
+ if (taskHeaderIndex < 0) {
+ return null
+ }
+
+ const startIndex = taskHeaderIndex + 1
+ const endIndex = Math.min(lines.length, startIndex + 5)
+ for (let index = startIndex; index < endIndex; index += 1) {
+ const candidate = lines[index]?.trim()
+ if (!candidate) {
+ continue
+ }
+
+ const finalWaveMatch = candidate.match(FINAL_WAVE_TASK_LINE_PATTERN)
+ if (finalWaveMatch?.[1] && finalWaveMatch[2]) {
+ const label = finalWaveMatch[1].toUpperCase()
+ return {
+ key: `final-wave:${label.toLowerCase()}`,
+ label,
+ title: finalWaveMatch[2].trim(),
+ }
+ }
+
+ const todoMatch = candidate.match(TODO_TASK_LINE_PATTERN)
+ if (todoMatch?.[1] && todoMatch[2]) {
+ const label = todoMatch[1]
+ return {
+ key: `todo:${label}`,
+ label,
+ title: todoMatch[2].trim(),
+ }
+ }
+ }
+
+ return null
+}
+
export function createToolExecuteBeforeHandler(input: {
ctx: PluginInput
pendingFilePaths: Map
@@ -84,15 +127,30 @@ export function createToolExecuteBeforeHandler(input: {
reason: "explicit_resume",
})
} else {
+ const prompt = typeof toolOutput.args.prompt === "string" ? toolOutput.args.prompt : ""
+ const taskFromPrompt = parseTrackedTaskFromPrompt(prompt)
const boulderState = readBoulderState(ctx.directory)
const currentTask = boulderState
? readCurrentTopLevelTask(resolveBoulderPlanPath(ctx.directory, boulderState))
: null
- if (currentTask) {
+ const resolvedTask = taskFromPrompt ?? (currentTask
+ ? {
+ key: currentTask.key,
+ label: currentTask.label,
+ title: currentTask.title,
+ }
+ : null)
+ if (resolvedTask) {
+ if (!taskFromPrompt) {
+ log(`[${HOOK_NAME}] TASK section parse failed; falling back to current top-level task`, {
+ sessionID: toolInput.sessionID,
+ callID: toolInput.callID,
+ })
+ }
const trackedTask = {
- key: currentTask.key,
- label: currentTask.label,
- title: currentTask.title,
+ key: resolvedTask.key,
+ label: resolvedTask.label,
+ title: resolvedTask.title,
}
const hasExistingClaim = [...pendingTaskRefs.values()].some((pendingTaskRef) => (
pendingTaskRef.kind === "track" && pendingTaskRef.task.key === trackedTask.key