# Claudomator: Workflow & Entry Points This document diagrams Claudomator's overall workflow, all entry points into the task-execution pipeline, the task state machine, and where the recursive arbitrated-review mechanism sits relative to that state machine. --- ## 1. Entry Points Overview Every arrow below eventually reaches `executor.Pool`, the single choke-point that bounds concurrency and gates spend. ```mermaid flowchart TD subgraph CLI["CLI (claudomator run)"] CLI_RUN["claudomator run <file.yaml>\n(internal/cli/run.go)"] end subgraph REST["REST API"] API_CREATE["POST /api/tasks\n(creates PENDING task)"] API_RUN["POST /api/tasks/{id}/run\n(submits PENDING → QUEUED)"] API_CREATE --> API_RUN end subgraph CHATBOT["Chatbot MCP (/chatbot/mcp)"] CB_SUBMIT["submit_task tool\n(internal/api/chatbotmcp.go)"] CB_STORY["create_story tool\n(creates Story row)"] CB_ACCEPT["accept_story tool\n(REVIEW_READY → DONE)"] CB_LIST["list_stories / get_story tools\n(read-only)"] end subgraph WEBHOOK["GitHub Webhook"] WH["POST /api/webhooks/github\ncheck_run / workflow_run failure\n(internal/api/webhook.go)"] end subgraph STORY_API["Story REST API"] ST_CREATE["POST /api/stories\n(creates Story row)"] ST_ACCEPT["POST /api/stories/{id}/accept\n(REVIEW_READY → DONE)"] end subgraph AGENT_TOOLS["Agent Back-channel Tools\n(MCP /mcp or native tool-use loop)"] AG_SPAWN["spawn_subtask\n(no role= → child with parent_task_id)\n(role= → sibling with depends_on)"] AG_EPIC["propose_epic\n(creates/updates Epic, groups Stories)"] AG_RC["propose_role_config\n(creates draft role_configs version)"] end subgraph ORCHESTRATOR["StoryOrchestrator (poll, 15 s)\n(internal/scheduler/story_orchestrator.go)"] SO_EVAL["ensureEvaluators\n(4 Evaluator tasks per builder node)"] SO_ARB["ensureArbitration\n(1 Arbitration/planner task)"] SO_FIX["ensureFixAttempt / spawnNestedFixAttempt\n(new builder task after rejection)"] SO_RETRO["processRetro\n(retro task after story DONE)"] end subgraph SCHEDULER["Scheduler (poll, ~10 s)\n(internal/scheduler/scheduler.go)"] SC_FAIL["processTask\n(retry / escalate FAILED role-typed tasks)"] SC_TIMEOUT["tickAskUserTimeouts\n(auto-answer + resume BLOCKED tasks\noutstanding > ask_user_timeout)"] end POOL(["executor.Pool\n(bounded dispatcher)"]) %% CLI path CLI_RUN -->|"Pool.Submit() directly"| POOL %% REST path API_RUN -->|"Pool.Submit()"| POOL %% Chatbot MCP paths CB_SUBMIT -->|"creates task + auto-submits"| POOL CB_STORY -->|"story row only;\nStoryOrchestrator spawns Builder"| ORCHESTRATOR CB_ACCEPT -->|"story REVIEW_READY → DONE;\ntriggers processRetro next tick"| ORCHESTRATOR %% Webhook path WH -->|"creates task + auto-runs"| POOL %% Story REST paths ST_CREATE -->|"story row only"| ORCHESTRATOR ST_ACCEPT -->|"REVIEW_READY → DONE"| ORCHESTRATOR %% Agent tool paths AG_SPAWN -->|"Pool.Submit() for new child/sibling"| POOL AG_EPIC -->|"storage only\n(no task spawned)"| POOL AG_RC -->|"storage only\n(no task spawned)"| POOL %% StoryOrchestrator → Pool SO_EVAL -->|"Store.CreateTask + Pool.Submit()"| POOL SO_ARB -->|"Store.CreateTask + Pool.Submit()"| POOL SO_FIX -->|"Store.CreateTask + Pool.Submit()"| POOL SO_RETRO -->|"Store.CreateTask + Pool.Submit()"| POOL %% Scheduler → Pool SC_FAIL -->|"Pool.Submit() or Pool.SubmitResume()"| POOL SC_TIMEOUT -->|"Pool.SubmitResume()"| POOL %% Orchestrator watches Pool results POOL -->|"task reaches READY/COMPLETED/FAILED;\norchestrator polls"| ORCHESTRATOR POOL -->|"task reaches FAILED;\nscheduler polls"| SCHEDULER POOL -->|"task stays BLOCKED;\nscheduler polls"| SCHEDULER ``` --- ## 2. Detailed Entry Point Reference | Entry Point | Code Location | What It Creates | Who Submits to Pool | |---|---|---|---| | `claudomator run ` | `internal/cli/run.go` | Task(s) from YAML | `cli` directly via `Pool.Submit` | | `POST /api/tasks` + `POST /api/tasks/{id}/run` | `internal/api/taskops.go` | PENDING task, then QUEUED | `api.Server` via `Pool.Submit` | | Chatbot MCP `submit_task` | `internal/api/chatbotmcp.go` | Task, immediately submitted | `chatbotmcp` via `Pool.Submit` | | Chatbot MCP `create_story` | `internal/api/chatbotmcp.go` | Story row | `StoryOrchestrator` (next tick) | | Chatbot MCP `accept_story` | `internal/api/chatbotmcp.go` | Story status DONE | `StoryOrchestrator` (triggers retro) | | `POST /api/stories` | `internal/api/stories.go` | Story row | `StoryOrchestrator` (next tick) | | `POST /api/stories/{id}/accept` | `internal/api/stories.go` | Story status DONE | `StoryOrchestrator` (triggers retro) | | `POST /api/webhooks/github` | `internal/api/webhook.go` | Task auto-tagged `["ci","auto"]` | `api.Server` via `Pool.Submit` | | Agent `spawn_subtask` (no `role=`) | `internal/executor/agentmcp.go`, `internal/agentloop/tools.go` | Child task with `parent_task_id` | `Pool.Submit` | | Agent `spawn_subtask` (with `role=`) | same | Sibling task with `depends_on`, `Agent.Role` set, no Type/Model | `Pool.Submit`; role resolved at dispatch | | Agent `propose_epic` | same | Epic row; groups existing stories | storage only, no task | | Agent `propose_role_config` | same | Draft `role_configs` version | storage only, no task | | `StoryOrchestrator.ensureEvaluators` | `internal/scheduler/story_orchestrator.go` | 4 Evaluator tasks per builder node | `Store.CreateTask` + `Pool.Submit` | | `StoryOrchestrator.ensureArbitration` | same | 1 `planner`-role Arbitration task | same | | `StoryOrchestrator.ensureFixAttempt` | same | New root builder task after rejection | same | | `StoryOrchestrator.spawnNestedFixAttempt` | same | New nested builder task after rejection | same | | `StoryOrchestrator.processRetro` | same | `retro`-role task after story DONE | same | | `Scheduler.processTask` | `internal/scheduler/scheduler.go` | Re-queues FAILED role-typed task at same or next tier | `Pool.Submit` / `Pool.SubmitResume` | | `Scheduler.tickAskUserTimeouts` | same | System-authored fallback answer; resumes BLOCKED task | `Pool.SubmitResume` | --- ## 3. Task State Machine ```mermaid stateDiagram-v2 [*] --> PENDING : task created PENDING --> QUEUED : /run submitted\n(Pool.Submit) QUEUED --> RUNNING : dispatcher picks slot RUNNING --> READY : execution succeeded\n(exit 0) RUNNING --> BLOCKED : agent called ask_user\nor subtasks outstanding RUNNING --> FAILED : execution failed\n(non-zero exit / error) RUNNING --> TIMED_OUT : timeout exceeded RUNNING --> CANCELLED : /cancel called RUNNING --> BUDGET_EXCEEDED : spend cap hit BLOCKED --> QUEUED : answer provided\n(POST /answer or\nscheduler timeout) BLOCKED --> READY : all subtasks COMPLETED\n(maybeUnblockParent) READY --> COMPLETED : human/chatbot accept\n(non-builder-role)\nOR\nfinalizeArbitration approves\n(builder-role) READY --> PENDING : human/chatbot reject FAILED --> QUEUED : /resume or\nScheduler retry/escalate TIMED_OUT --> QUEUED : /resume CANCELLED --> QUEUED : /resume BUDGET_EXCEEDED --> QUEUED : /resume COMPLETED --> [*] ``` ### Key: READY does not mean "done" for builder-role tasks For **non**-builder-role tasks (the common case), `READY → COMPLETED` is a simple human or chatbot accept via `POST /api/tasks/{id}/accept`. For **builder**-role tasks inside a story pipeline, `READY` means **"awaiting arbitration"** — the `StoryOrchestrator` intercepts every `READY` builder node and drives it through the review cycle before ever promoting it to `COMPLETED`. A builder task at `READY` without a completed arbitration is deliberately left there until the review is done. ``` builder READY ──► ensureEvaluators spawns 4 tasks │ ▼ 4 Evaluators → COMPLETED (auto-accepted) │ ▼ ensureArbitration spawns 1 planner task │ ▼ Arbitration → COMPLETED (auto-accepted) │ ┌───────────┴───────────┐ │ APPROVED │ REJECTED ▼ ▼ builder READY builder stays READY (superseded) → COMPLETED new fix-attempt task spawned (story → REVIEW_READY (story → NEEDS_FIX → IN_PROGRESS) if root node) ``` --- ## 4. Recursive Arbitrated-Review Cycle The review mechanism is **depth-agnostic**: it applies identically at the root and at any nested depth. `processStory` walks the entire task tree every tick and applies `processBuilderNode` to every `READY` builder-role node found. ```mermaid flowchart TD subgraph Story["Story (IN_PROGRESS)"] ROOT["root_task_id\n(Builder, READY)"] end subgraph ReviewCycle["Review Cycle (per builder node, any depth)"] EVAL["4 Evaluator tasks\nevaluator_quality\nevaluator_security\nevaluator_correctness\nevaluator_performance\n\n(depends_on: builder node)"] ARB["1 Arbitration task\nrole: planner\n\n(depends_on: all 4 evaluators)"] APPROVE{"Approved?"} FIX["Fix-attempt task\n(same role, new builder;\ndepends_on: rejected node)"] end subgraph Terminal["Terminal states"] COMP["builder node → COMPLETED\n(if root: story → REVIEW_READY)"] HUMAN["POST /api/stories/{id}/accept\n(REVIEW_READY → DONE)"] RETRO["processRetro: retro-role task\nreflects, proposes role_configs drafts"] DONE["story → DONE"] end ROOT -->|"StoryOrchestrator\nensureEvaluators"| EVAL EVAL -->|"all COMPLETED\nensureArbitration"| ARB ARB -->|"COMPLETED\nfinalizeArbitration reads\nreport_verdict event"| APPROVE APPROVE -->|"Yes"| COMP APPROVE -->|"No"| FIX FIX -->|"next tick: new node at READY\nrecursive review restarts here"| EVAL COMP --> HUMAN HUMAN --> DONE DONE -->|"processRetro"| RETRO NOTE["Nested builder nodes (parent_task_id != ''):\nsame cycle, spawnNestedFixAttempt\ninstead of ensureFixAttempt;\nno story-status change at nested level"] style NOTE fill:#f5f5f5,stroke:#999,color:#333 ``` --- ## 5. executor.Pool Internal Flow Once work reaches the pool, the internal path is: ```mermaid flowchart LR SUBMIT["Pool.Submit(task)\nor Pool.SubmitResume(task, exec)"] QUEUE["workCh buffered channel\n(FIFO, no priority ordering)"] DISPATCH["dispatch() goroutine\nwaits for semaphore slot\nchecks budget gate\nchecks depends_on satisfied"] RUNNER["ContainerRunner\n(Docker; git clone; claude -p)\nor LocalRunner (OpenAI-compatible HTTP)"] RESULT["handleRunResult\nwrite execution row\nupdate task state\nbroadcast WebSocket event\ncascade-fail dependents if terminal-fail"] SUBMIT --> QUEUE --> DISPATCH --> RUNNER --> RESULT RESULT -->|"READY or COMPLETED"| DONE2(["task observable\nvia WS / REST"]) RESULT -->|"BLOCKED"| BLOCKED2(["workspace preserved\nin sandbox_dir;\nawait answer"]) RESULT -->|"FAILED etc."| FAILED2(["Scheduler picks up\non next poll tick"]) ``` --- ## Notes - **`propose_epic` / `propose_role_config`** are agent tools that write to storage only — they do not create tasks or trigger any further scheduling. They exist to let a running agent record planning-layer artifacts (epic groupings; improved role system prompts for human review) as a side-effect of its execution. - **`story.RootTaskID` is immutable** after story creation. When a builder node is rejected and replaced, `task.CurrentAttempt(store, anchorID)` walks the `depends_on` chain forward to find the current live node. Every orchestrator caller resolves through this before acting. - **Scheduler vs. StoryOrchestrator**: the `Scheduler` handles generic role-typed task lifecycle (FAILED → retry/escalate; BLOCKED → timeout resume). The `StoryOrchestrator` handles story-specific pipeline orchestration (Builder → Evaluators → Arbitration). They run in the same process but poll independently and write to separate concerns.