1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
# 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 <file>` | `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.
|