feat(hooks/atlas): parse task_key from delegation prompt for parallel batches

This commit is contained in:
YeonGyu-Kim
2026-05-11 14:28:56 +09:00
parent e3cddb3650
commit cf5fe757df
2 changed files with 200 additions and 4 deletions
@@ -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<task_metadata>\nsession_id: ses_child_parallel_2\n</task_metadata>",
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<task_metadata>\nsession_id: ses_child_parallel_3\n</task_metadata>",
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<task_metadata>\nsession_id: ses_child_fallback\n</task_metadata>",
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")
})
})
+62 -4
View File
@@ -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<string, string>
@@ -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