summaryrefslogtreecommitdiff
path: root/docs/workflow-and-entry-points.md
blob: d9312c5ec04d43db1e3a94c0986d8b83e8916a54 (plain)
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.