@@ -53,7 +53,7 @@ src/clayde/
5353 git.py # ensure_repo() — clone or update repos under REPOS_DIR
5454 safety.py # Content filtering & plan approval: is_comment_visible(),
5555 # filter_comments(), is_issue_visible(),
56- # has_visible_content (), is_plan_approved ()
56+ # get_new_visible_comments (), has_visible_content ()
5757 responses.py # Pydantic response models + parse_response() for structured JSON
5858 claude.py # invoke_claude(prompt, repo_path) — dual backend:
5959 # ApiBackend (Anthropic SDK tool-use loop) or
@@ -62,17 +62,11 @@ src/clayde/
6262 # FileSpanExporter (JSONL)
6363 orchestrator.py # main() — single cycle, run_loop() — container entry point
6464 prompts/
65- preliminary_plan.j2 # Jinja2 template for short preliminary plan
66- thorough_plan.j2 # Jinja2 template for detailed thorough plan
67- update_plan.j2 # Jinja2 template for updating a plan on new comments
68- implement.j2 # Jinja2 template for implement prompt
69- address_review.j2 # Jinja2 template for addressing PR review comments
70- plan.j2 # Legacy template (kept for reference)
65+ work.j2 # Jinja2 template for the unified work prompt
7166 tasks/
7267 __init__.py
73- plan.py # run_preliminary(url), run_thorough(url), run_update(url, phase)
74- implement.py # run(issue_url) — implement + open PR + assign reviewer
75- review.py # run(issue_url) — address PR review comments
68+ work.py # run(issue_url) — unified: Claude decides next action
69+ # (ask, plan, implement, open PR, or address review)
7670 webhook/
7771 __init__.py
7872 app.py # FastAPI app, /webhook/pebble, /health, OTel enqueue span
@@ -129,43 +123,36 @@ Config is loaded via `get_settings()` (singleton). `GH_TOKEN` is exported at sta
129123
130124---
131125
132- ## State Machine
133-
134- Issue lifecycle stored in ` state.json ` under ` {"issues": {"<html_url>": {...}}} ` .
135-
136- ```
137- (none) → preliminary_planning → awaiting_preliminary_approval
138- → planning → awaiting_plan_approval → implementing → pr_open → done
139- ↘ failed
140- ```
141-
142- New comments in ` awaiting_preliminary_approval ` or ` awaiting_plan_approval `
143- trigger plan updates (edit existing plan comment + post change summary).
144-
145- PR reviews in ` pr_open ` trigger ` addressing_review ` → back to ` pr_open ` .
146-
147- | Status | Meaning |
148- | --------| ---------|
149- | ` preliminary_planning ` | Claude is producing a short preliminary plan |
150- | ` awaiting_preliminary_approval ` | Preliminary plan posted; waiting for 👍 |
151- | ` planning ` | Claude is producing a thorough implementation plan |
152- | ` awaiting_plan_approval ` | Thorough plan posted; waiting for 👍 |
153- | ` implementing ` | Claude is implementing the approved plan |
154- | ` pr_open ` | PR exists; monitoring for review comments |
155- | ` addressing_review ` | Claude is addressing review comments |
156- | ` done ` | PR approved or complete; issue finished |
157- | ` failed ` | Error during any phase; cleared manually to retry |
158- | ` interrupted ` | Claude usage/rate limit hit mid-task; retried automatically |
159-
160- State entries store: ` owner ` , ` repo ` , ` number ` , ` preliminary_comment_id ` ,
161- ` plan_comment_id ` , ` pr_url ` , ` branch_name ` , ` last_seen_comment_id ` ,
162- ` last_seen_review_id ` .
163-
164- Interrupted entries also store: ` interrupted_phase ` (` "preliminary_planning" ` ,
165- ` "planning" ` , ` "implementing" ` , or ` "addressing_review" ` ).
166-
167- Backward compatibility: old ` awaiting_approval ` status is mapped to
168- ` awaiting_plan_approval ` .
126+ ## Work Loop (event-driven)
127+
128+ There is no rigid status state machine. Each tick, the orchestrator iterates
129+ the issues assigned to the bot and, for each, decides whether anything has
130+ happened since last cycle. If so, it hands the issue to the unified ** work
131+ task** , which lets Claude choose the next action — ask questions, post a
132+ plan, implement, open a PR, or address review comments.
133+
134+ Per-issue state is stored in ` state.json ` under
135+ ` {"issues": {"<html_url>": {...}}} ` . Fields written by the current code:
136+
137+ | Field | Meaning |
138+ | -------| ---------|
139+ | ` owner ` , ` repo ` , ` number ` | Issue identity |
140+ | ` issue_title ` | Title (for log labels) |
141+ | ` branch_name ` | Working branch (` clayde/issue-<N> ` by default) |
142+ | ` pr_url ` | PR opened for this issue, once detected via ` find_open_pr() ` |
143+ | ` in_progress ` | ` True ` while the work task runs; a crash leaves it set so the next cycle retries |
144+ | ` last_seen_at ` | ISO-UTC timestamp of the last completed cycle; used to detect new activity |
145+
146+ ** Activity detection** (` _handle_issue ` ): the work task is invoked when any of
147+ — ` in_progress ` is set (retry), ` last_seen_at ` is ` None ` (never processed),
148+ there are new whitelist-visible comments, or there is new PR review activity
149+ (inline comments or a review body). A pure PR approval with no comments does
150+ ** not** invoke Claude — it just advances ` last_seen_at ` .
151+
152+ ** Limits & retries** : ` UsageLimitError ` / ` InvocationTimeoutError ` from Claude
153+ leave ` in_progress=True ` so the next cycle retries automatically. Other
154+ exceptions clear ` in_progress ` and log the error. Closed issues are pruned
155+ from state at the start of each tick.
169156
170157---
171158
1821692 . ** No visible content** → issue is skipped. If the issue body and all
183170 comments are from non-whitelisted users without any whitelisted 👍, there
184171 is nothing for the LLM to work with.
185- 3 . ** Plan approval gates** remain: preliminary plan needs 👍 to proceed to
186- thorough plan; thorough plan needs 👍 to proceed to implementation.
172+
173+ Only whitelist-visible content reaches the LLM; Claude decides within the work
174+ task whether it has enough to plan, implement, or must ask first.
187175
188176Whitelisted users: configured via ` CLAYDE_WHITELISTED_USERS ` in ` data/config.env ` .
189177
@@ -241,70 +229,44 @@ Key functions:
241229- ` is_comment_visible(comment) ` — True if comment author is whitelisted OR has 👍 from whitelisted user.
242230- ` filter_comments(comments) ` — returns only visible comments.
243231- ` is_issue_visible(issue) ` — True if issue author is whitelisted OR has 👍 from whitelisted user.
232+ - ` get_new_visible_comments(comments, last_seen_at) ` — visible comments created after ` last_seen_at ` .
244233- ` has_visible_content(issue, comments) ` — True if there is any visible content at all.
245- - ` is_plan_approved(g, owner, repo, number, comment_id) ` — True if a whitelisted user reacted +1 to the plan comment.
246-
247- ---
248-
249- ## Plan Task (` tasks/plan.py ` )
250-
251- Two-phase planning with update support:
252-
253- ### Phase 1: Preliminary Plan (` run_preliminary ` )
254- 1 . Fetch issue metadata and filtered comments
255- 2 . ` ensure_repo() ` to have the code on disk
256- 3 . Build prompt with filtered issue body, labels, visible comments, repo path
257- 4 . ` invoke_claude() ` — Claude explores the repo and returns a short overview with questions
258- 5 . Post preliminary plan as issue comment
259- 6 . Set status → ` awaiting_preliminary_approval `
260-
261- ### Phase 2: Thorough Plan (` run_thorough ` )
262- 1 . Fetch preliminary plan comment and discussion after it
263- 2 . Build prompt including preliminary plan + discussion
264- 3 . ` invoke_claude() ` — Claude produces the full detailed plan
265- 4 . Post thorough plan as issue comment
266- 5 . Set status → ` awaiting_plan_approval `
267-
268- ### Plan Updates (` run_update ` )
269- Triggered when new visible comments are detected in ` awaiting_preliminary_approval `
270- or ` awaiting_plan_approval ` states:
271- 1 . Fetch new visible comments since ` last_seen_comment_id `
272- 2 . Build update prompt with current plan + new comments
273- 3 . ` invoke_claude() ` — Claude produces summary + updated plan
274- 4 . ** Edit** the existing plan comment AND ** post** a new comment with change summary
275-
276- ---
277-
278- ## Implementation Task (` tasks/implement.py ` )
279-
280- 1 . Fetch plan comment text and filtered discussion comments after the plan
281- 2 . ` ensure_repo() ` to reset to latest default branch
282- 3 . Build prompt with issue body, plan, discussion, repo path
283- 4 . ` invoke_claude() ` — Claude creates a branch, implements, commits, and pushes
284- 5 . Python code creates PR via PyGitHub or finds an existing one
285- 6 . ** Assign the issue author as PR reviewer** via ` add_pr_reviewer() `
286- 7 . Post result comment on issue; set status → ` pr_open `
287234
288235---
289236
290- ## Review Task (` tasks/review.py ` )
291-
292- Handles PR review comments after implementation:
293-
294- 1 . Fetch PR reviews and review comments via PyGitHub
295- 2 . Filter to new reviews since ` last_seen_review_id ` , ignoring own reviews
296- 3 . If reviews have comments/body: invoke Claude with ` address_review.j2 ` prompt
297- 4 . Claude makes changes and pushes to the existing branch
298- 5 . Post summary comment on issue; update ` last_seen_review_id ` ; status stays ` pr_open `
299- 6 . If a review is "APPROVED" with no comments: set status → ` done `
237+ ## Work Task (` tasks/work.py ` )
238+
239+ A single ` run(issue_url) ` handles every phase. There is no separate
240+ plan/implement/review task — Claude decides what to do from the context it is
241+ given.
242+
243+ 1 . ` fetch_issue() ` + ` get_default_branch() ` ; ` ensure_repo() ` resets the clone
244+ to the latest default branch.
245+ 2 . Persist issue metadata and ` branch_name ` to state.
246+ 3 . Gather context: whitelist-filtered comments, and — if a PR already exists —
247+ its review bodies and inline review comments.
248+ 4 . Render ` work.j2 ` with the issue body, labels, comments, review text, repo
249+ path, ` branch_name ` , ` pr_url ` , and ` default_branch ` .
250+ 5 . ` invoke_claude() ` — Claude explores, then takes whatever action fits: post
251+ a plan/question comment, implement and push, open a PR via ` gh pr create ` ,
252+ or push fixes addressing review comments. It returns a JSON ` {summary} `
253+ (` WorkResponse ` ).
254+ 6 . Post the ` summary ` as an issue comment (best-effort: raw output snippet if
255+ JSON parsing fails).
256+ 7 . Detect a PR via ` find_open_pr(branch_name) ` . On first detection, ** assign
257+ the issue author as reviewer** ; persist ` pr_url ` to state.
258+
259+ Plans and questions are ordinary issue comments — there is no separate
260+ approval gate or 👍 reaction required to advance. Iteration happens through
261+ the normal comment/review activity-detection loop.
300262
301263---
302264
303265## Logging
304266
305267Format: ` [YYYY-MM-DD HH:MM:SS] [clayde.<module>] <message> `
306268File: ` /data/logs/agent.log ` (appended)
307- Logger names: ` clayde.orchestrator ` , ` clayde.tasks.plan ` , ` clayde.tasks.implement ` , ` clayde.tasks.review ` , ` clayde.github ` , ` clayde.claude `
269+ Logger names: ` clayde.orchestrator ` , ` clayde.tasks.work ` , ` clayde.github ` , ` clayde.claude ` , ` clayde.git ` , ` clayde.state `
308270
309271---
310272
0 commit comments