summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md59
1 files changed, 56 insertions, 3 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 32bca69..a728f1f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -78,7 +78,7 @@ Config defaults to `~/.claudomator/config.toml`. Data is stored in `~/.claudomat
| `internal/agentloop` | Provider-neutral tool-use control-flow loop shared by all `NativeRunner`s |
| `internal/sandbox` | `Sandbox` interface (`HostSandbox`/`DockerSandbox`) + pre-tool-use guardrail hooks |
| `internal/role` | `RoleConfig`/`Tier`/`Rung` — per-role system prompt + provider/model escalation ladder |
-| `internal/scheduler` | `Scheduler` — polls role-typed FAILED tasks and retries/escalates them per their role's ladder; `StoryOrchestrator` (Phase 7b) — polls stories and drives Builder → Evaluators → Arbitration → REVIEW_READY |
+| `internal/scheduler` | `Scheduler` — polls role-typed FAILED tasks and retries/escalates them per their role's ladder, and (Phase 7c) role-typed BLOCKED tasks whose ask_user question has timed out; `StoryOrchestrator` (Phase 7b) — polls stories and drives Builder → Evaluators → Arbitration → REVIEW_READY |
| `internal/story` | `Epic`/`Story` — planning layer above the flat task tree; data model + CRUD only (see `internal/scheduler.StoryOrchestrator` for the orchestration that drives it) |
| `web` | Embedded static UI (`embed.go`) |
@@ -186,7 +186,7 @@ Schema is auto-migrated additively on `storage.Open()` — new columns are `ALTE
tasks: id, name, description, config_json, priority, timeout_ns, retry_json,
tags_json, depends_on_json, parent_task_id, state, rejection_comment,
question_json, summary, elaboration_input, interactions_json,
- created_at, updated_at
+ needs_review, created_at, updated_at
executions: id, task_id, start_time, end_time, exit_code, status, stdout_path,
stderr_path, artifact_dir, cost_usd, error_msg, session_id,
@@ -469,6 +469,55 @@ spawned here with no matching active `role_configs` row dispatches in the
same degraded (no role resolution) mode `Pool.execute()` already logs a
warning and falls back to for any other role-typed task.
+### Epic-proposal tool (Phase 7c)
+
+A 5th `AgentChannel` method, `ProposeEpic(ctx, agentchannel.EpicProposal{Name,
+Description, StoryIDs})`, lets a discovery/planner-role agent that has been
+handed several story IDs act on its own judgment that they form a cohesive
+initiative. `storeChannel.ProposeEpic` (`internal/executor/channel.go`)
+matches an existing epic by exact `Name` (`Store.GetEpicByName`) or creates a
+new one, then sets `epic_id` on each resolved story via `Store.GetStory` +
+`Store.UpdateStory` — a story ID that doesn't resolve is skipped, not
+fatal to the call. Emits `event.KindEpicProposed` attached to the epic's own
+ID (payload `{epic_id, name, story_ids}`, `story_ids` being only the ones
+actually grouped), matching the story orchestrator's convention of attaching
+planning-layer ceremony events to the entity they're about. Exposed as a
+`propose_epic` tool on both transports — `internal/agentloop/tools.go` (native
+tool-use loop) and `internal/executor/agentmcp.go` (MCP) — with the same
+`{name, description?, story_ids}` schema Phase 6 used for `spawn_subtask`'s
+`role` parameter. This phase only builds the mechanism; judging *whether*
+stories belong together is the calling agent's job via its instructions/model.
+
+### AskUser-timeout escalation (Phase 7c)
+
+`internal/scheduler.Scheduler` (the same retry-then-escalate watcher described
+above) also polls BLOCKED role-typed tasks whose `question_json` has been
+outstanding longer than `config.SchedulerConfig.AskUserTimeout()` (default 10
+minutes; `[scheduler] ask_user_timeout_seconds` in TOML) — `tickAskUserTimeouts`,
+called from `Tick` alongside the existing FAILED-task pass. The "outstanding
+since" timestamp is `task.UpdatedAt` itself, not a new column:
+`storage.DB.UpdateTaskQuestion` (the last write made to a task's row on its
+way into BLOCKED) already stamps `updated_at` the moment the question was
+recorded, and nothing else touches the row while it sits BLOCKED. For each
+timed-out question, the scheduler resolves the role's active escalation
+ladder from the tier the task was dispatched at (`latest execution's
+EscalationRung`) and, if a higher tier exists, escalates to its first
+candidate (mirroring `processTask`'s existing "always `Candidates[0]`"
+simplification); if not (ladder exhausted, no active role config, etc.), it
+still resumes the task at its current tier rather than leaving it stuck
+forever, marking that escalation `final: true`. Either way it records the
+system-authored answer `"[auto-escalated: no human response within timeout;
+proceeding with best judgment]"` as a `task.Interaction` (mirroring
+`api.answerTaskQuestion`'s audit trail for a real human answer), clears
+`question_json`, sets `tasks.needs_review = true`, and resumes the task via
+`Pool.SubmitResume` — the same mechanism `POST /api/tasks/{id}/answer` uses,
+just at the escalated tier's provider/model instead of the one that asked.
+Emits `event.KindEscalated` with a `trigger` field (`"failure"` for the
+existing FAILED-task path, `"ask_user_timeout"` for this one) so the two are
+distinguishable in the event stream. `GET /api/tasks?needs_review=true`
+surfaces flagged tasks for a human to double-check the system's stand-in
+answer later.
+
---
@@ -488,6 +537,10 @@ Same pattern as `task.Priority`/`RetryConfig` below: `internal/role.RoleConfig.T
`Scheduler.handled` (keyed by execution ID) prevents re-emitting a `final: true` `KindEscalated` event on every poll tick once a role-typed task's ladder is exhausted, but it's per-process and resets on restart. A restart can produce one extra "reconsideration" (and, if still exhausted/denied, one more `KindEscalated` event) — not an infinite loop, just not persisted. A future phase could persist this via a `tasks` column if that turns out to matter.
+### AskUser-timeout escalation resumes at the same tier when the ladder is exhausted
+
+The phase description for `Scheduler.escalateAskUserTimeout` didn't specify behavior when a BLOCKED task is already at its role's top escalation tier (or has no active role config at all). Leaving the task stuck BLOCKED forever seemed worse than the alternative, so it still resumes the task (unblocking it is the priority) at its *current* provider/model — no `UpdateTaskAgent` call — and marks that `KindEscalated` event `final: true` so it's visible in the event stream as "resumed without escalating" rather than a genuine tier bump. A later phase could reconsider this (e.g. leaving the task BLOCKED and only flagging it) if it turns out best-effort auto-resumption at the same tier isn't the right call for every role.
+
### StoryOrchestrator's Arbitration outcome always routes to REVIEW_READY
`internal/scheduler.StoryOrchestrator.finalizeArbitration` does not parse the
@@ -560,7 +613,7 @@ In `executor.go`, `withFailureHistory` creates a copy of the task struct (`copy
| Method | Endpoint | Description |
|--------|----------|-------------|
-| GET | `/api/tasks` | List tasks; `?state=RUNNING&since=<RFC3339>&limit=50` |
+| GET | `/api/tasks` | List tasks; `?state=RUNNING&since=<RFC3339>&limit=50&needs_review=true` |
| POST | `/api/tasks` | Create task (JSON body) |
| GET | `/api/tasks/{id}` | Get task |
| DELETE | `/api/tasks/{id}` | Delete task + subtasks + executions |