{
"meta": {
"agent": "planner",
"task_id": "458",
"title": "Verify PhaseRouterService.route() idempotency for BullMQ retry",
"created_at": "2026-04-13T12:00:00Z"
},
"inputs": [
{
"name": "GitHub Issue #458",
"type": "context",
"ref": "AgentSDE/agent-core#458",
"notes": "AW-12: Idempotency audit for route() and executePhase()"
},
{
"name": "AGENTS.md",
"type": "context",
"ref": "AGENTS.md",
"notes": "Codebase conventions and known patterns"
},
{
"name": "PhaseRouterService source",
"type": "file",
"ref": "src/phase-router/phase-router.service.ts",
"notes": "Main orchestration entry point"
},
{
"name": "PhaseHooksService source",
"type": "file",
"ref": "src/hooks/phase-hooks.service.ts",
"notes": "Side-effect hooks (comments, labels)"
},
{
"name": "TaskStateService source",
"type": "file",
"ref": "src/task-state/task-state.service.ts",
"notes": "Task persistence and createTask()"
}
],
"outputs": [
{
"name": "plan.md",
"type": "plan",
"format": "md",
"content": "Implementation plan for idempotency hardening"
}
],
"files": [
{
"path": "src/phase-router/phase-router.service.ts",
"action": "modify",
"reason": "Add createTask() race guard and defensive checks"
},
{
"path": "src/hooks/phase-hooks.service.ts",
"action": "modify",
"reason": "Guard hooks against duplicate comment posting"
},
{
"path": "src/task-state/task-state.service.ts",
"action": "modify",
"reason": "Convert createTask() to upsert or add catch for unique constraint"
},
{
"path": "src/phase-router/phase-router.service.spec.ts",
"action": "modify",
"reason": "Add retry-idempotency test cases"
},
{
"path": "src/hooks/phase-hooks.service.spec.ts",
"action": "modify",
"reason": "Add duplicate-call safety tests"
}
],
"steps": [
{
"id": "S1",
"summary": "Guard createTask() against unique-constraint race condition",
"acceptance": [
"createTask() wrapped with try/catch or converted to upsert in TaskStateService",
"Calling route() twice with same event creates exactly one task row",
"Existing task state is not reset on retry"
],
"depends_on": []
},
{
"id": "S2",
"summary": "Guard phase hooks against duplicate comments and labels",
"acceptance": [
"onPlanComplete(), onDeliverComplete(), onCompoundComplete() check state before posting comments",
"onPhaseBlocked() skips duplicate blocked comments",
"Calling hooks twice for same phase produces exactly one comment"
],
"depends_on": [
"S1"
]
},
{
"id": "S3",
"summary": "Verify invoke() cannot double-enqueue and add defensive guard",
"acceptance": [
"Phase-status 'active' guard confirmed in route() prevents second executePhase()",
"Defensive early-return added in executePhase() if phase already active on re-entry"
],
"depends_on": [
"S1"
]
},
{
"id": "S4",
"summary": "Write retry-idempotency unit tests for route() and hooks",
"acceptance": [
"Test: route() called twice with same event → createTask once, invoke once, hooks once",
"Test: onPhaseComplete() called twice → one comment posted",
"npm run test passes",
"npm run lint passes",
"npm run build passes"
],
"depends_on": [
"S1",
"S2",
"S3"
]
}
],
"risks": [
{
"risk": "createTask() upsert may reset existing task phase columns",
"mitigation": "Use INSERT OR IGNORE / findOrCreate pattern that preserves existing rows"
},
{
"risk": "Comment-existence checks add GitHub API calls",
"mitigation": "Use task-state phase-status flags instead of API calls where possible"
}
],
"assumptions": [
"Concurrent multi-worker calls for the same job are out of scope (per issue)",
"getOrCreateWorktree() is already idempotent and needs no changes",
"releaseService.autoLinkToRelease() is not present in the current codebase (removed or not yet implemented)",
"The existing phase-status 'active' guard is the primary defense against double-execution"
],
"open_questions": []
}