summaryrefslogtreecommitdiff
path: root/internal/api
AgeCommit message (Collapse)Author
11 daysfix: task submission context cancellation, executions success-rate calcPeter Stone
submitTask was using the inbound HTTP/MCP request context to submit work to the executor pool, so every chatbot/REST-submitted task was cancelled the moment the request returned rather than actually running. Use the server's long-lived lifecycle context instead, matching the other Submit call sites. Separately, computeExecutionStats compared execution state against a lowercase 'completed' literal while the backend always emits uppercase state values, so success rate silently read 0% for every execution. Also treat READY as success alongside COMPLETED, since a top-level task's execution lands at READY on success — COMPLETED requires a later, separate accept step. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
12 daysfeat(web,api): add Budget/Roles dashboard -- final phase of the harness ↵Claude Sonnet 5
redesign (Phase 9b) Two new tabs, matching Phase 9a's conventions: Budget (per-provider spend meters, escalation funnel, spend-over-time) and Roles (version history, draft activation, readable escalation-ladder view) -- the human-facing surface for the token-husbanding value proposition and the Phase 5/8 versioned role-config system. New backend (minimal, additive, matching existing endpoint conventions -- unauthenticated like /api/budget and /api/roles/*): - internal/storage/dashboard.go: QueryEscalationFunnel (executions grouped by escalation_rung + agent, count + cost) and QuerySpendTimeseries (cost per provider bucketed hourly/daily, mirroring QueryDashboardStats' existing bucketing expressions). Documented honestly: rung 0 is not exclusively "resolved locally" -- escalation_rung defaults to 0 uniformly, so non-role-typed executions (which never climb a ladder) land there too, alongside role-typed tasks genuinely resolved at tier 0. A role-only variant would need a join against tasks.config_json with no queryable role column on executions -- intentionally out of scope. - internal/api/dashboard.go: GET /api/escalation-funnel, GET /api/spend-timeseries (both ?window=5h|24h|7d|<duration>, default 24h). - internal/storage.ListRoleNames + GET /api/roles: the "which roles exist" gap -- there was no way to discover role names before this, only to list versions for a role you already knew the name of. Chart-form decisions (dataviz skill, invoked before writing chart code): horizontal stacked bar for the escalation funnel (rung order already encodes the funnel shape positionally; color only needed for per-provider segments within each rung); multi-line for spend-over-time; a fixed-order categorical palette from the skill's validated palette.md slots for provider identity (re-validated against this app's dark surface, passing); a separate status (good/warning/critical) palette for budget meters, deliberately distinct from both --state-* and the provider palette. Caught and fixed a real bug during visual QA: converging near-zero end-labels on the spend chart were overlapping (an anti-pattern the skill explicitly flags) -- fixed with a 14px minimum-gap check before direct-labeling an endpoint, leaning on the legend/tooltip otherwise. Verified with a real running server, real seeded data (65 executions across rungs 0-2 with a realistic provider mix, 2 roles with active/draft/retired role_configs versions written directly via internal/storage), and a real headless-browser session (reusing Phase 9a's Chromium/proxy scaffinding): confirmed correct rung totals/percentages, provider legends, a 3-line spend chart, live window-selector re-render, correct active-version highlighting on the role panel, and a real Activate click on a draft version -- verified via both DOM re-render and a direct backend GET that it truly persisted. go build/vet/test -race -count=1 all pass, full suite. node --test web/test/*.mjs: 291/291 passing (16 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
12 daysfeat(story,role): add retro ceremony -- closes the self-improvement loop ↵Claude Sonnet 5
(Phase 8) The final mechanism the versioned role-config model (Phase 5) was built for: when a story reaches DONE, StoryOrchestrator spawns a retro-role task that reflects on the story's full history and proposes draft role_configs versions for a human to review and activate via the existing (unchanged) POST /api/roles/{role}/activate. - AgentChannel gains a 6th method, ProposeRoleConfig(ctx, role.RoleConfig) (version, err), following ProposeEpic's precedent (Phase 7c): a structured tool call, not summary-parsing. storeChannel.ProposeRoleConfig calls the same Store.CreateRoleConfig the human-facing POST /api/roles/{role}/versions endpoint already uses (proposed_by: "retro"), landing a new draft row without touching whatever's currently active. Wired through both transports exactly like ProposeEpic: internal/agentloop/tools.go (native loop) and internal/executor/agentmcp.go (MCP). - StoryOrchestrator.Tick now routes a story at status DONE to a new processRetro stage instead of processStory -- a sibling stage, not a continuation, since the Builder->Evaluators->Arbitration chain is long settled by then. processRetro only *reads* that settled pipeline (read-only findEvaluators/findArbitration counterparts to ensureEvaluators/ensureArbitration -- it never spawns/mutates Builder-pipeline tasks) to locate the Arbitration task the retro task depends on, then spawns (idempotently -- checks for an existing retro-role dependent first) one retro-role task with instructions assembled from the story's spec/acceptance-criteria, full task tree, per- task cost/escalation history, active role_configs per role encountered, and the story's own event stream (evaluator verdicts, arbitration decision). - event.KindRetroCaptured (attached to the story's ID, matching KindEvalVerdict/KindArbitrationDecided's convention) fires once the retro task completes (auto-accepted like every other pipeline task), aggregating every event.KindRoleConfigProposed the retro task recorded (one per propose_role_config call) into {task_id, proposals: [{role, version}], summary} -- the summary is the "capturing lessons" half of this ceremony, the proposals are the versioned-config half. - Human activation is completely untouched: drafts land through the identical CreateRoleConfig/config_json path Phase 5's endpoints already handle, confirmed via existing role-endpoint tests passing unmodified. go build/vet/test -race -count=1 all pass, full suite (20 packages) -- one run hit a known, pre-existing, intermittent flake under full-suite load (unrelated to this phase's files) that did not reproduce on two immediate reruns, both in isolation and full-suite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
12 daysfeat(story,scheduler): add epic-proposal tool + AskUser-timeout escalation ↵Claude Sonnet 5
(Phase 7c) Two independent pieces, completing Phase 7. Epic-proposal tool: AgentChannel gains a 5th method, ProposeEpic(ctx, EpicProposal{Name, Description, StoryIDs}) (epicID, err), implemented on storeChannel -- matches an existing epic by exact name or creates one (DiscoverySource: "agent"), sets epic_id on each resolvable story (skips, doesn't fail, on an unresolved ID), emits KindEpicProposed attached to the epic's own ID with payload {epic_id, name, story_ids}. Wired into both transports exactly like Phase 6 wired role into spawn_subtask: a new propose_epic tool in the native tool-use loop (internal/agentloop/tools.go) and the MCP transport (internal/executor/agentmcp.go). This is the mechanism for a discovery/planner-role agent to act on its own judgment that several stories it's been given form one cohesive initiative -- the judgment itself lives in the calling agent's instructions/model, not in this code. AskUser-timeout escalation: extends the existing Scheduler (Phase 5's retry-then-escalate watcher) rather than adding a new component, since "stuck task needs escalation" is exactly what it already does. Finds role-typed BLOCKED tasks whose question has been outstanding longer than SchedulerConfig.AskUserTimeoutSeconds (default 10 minutes) using task.UpdatedAt as the outstanding-since timestamp -- no new column needed, since UpdateTaskQuestion already stamps it the instant a question is recorded and nothing else touches the row while BLOCKED. Resolves the next ladder tier from the latest execution's EscalationRung, records the system-authored fallback answer as an audit-trail task.Interaction, clears the question, sets the new tasks.needs_review flag, emits KindEscalated (now carrying a trigger field: "failure" vs "ask_user_timeout" for the existing failure-retry path vs this one), and resumes via Pool.SubmitResume at the escalated tier -- degrading to same-tier resume with final:true if the ladder's exhausted or no role config exists, since unblocking the task takes priority over having somewhere higher to escalate to. GET /api/tasks?needs_review=true surfaces auto-decided tasks for human review. go build/vet/test -race -count=1 all pass, full suite (20 packages), run twice to rule out flakiness in the new tests. (One pre-existing, unrelated test -- TestHandleRunTask_CascadesRetryToFailedDeps, a tempdir-cleanup race -- appeared once under full-suite load per the implementing agent's report and did not reproduce in this verification's runs either; not a regression from this work.) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
12 daysfeat(story): add StoryOrchestrator -- ↵Claude Sonnet 5
Builder->Evaluators->Arbitration->accept (Phase 7b) A deterministic, poll-based watcher (internal/scheduler.StoryOrchestrator, sibling to the Phase 5 Scheduler) that drives a story.Story through its execution pipeline, rather than relying on an LLM agent to correctly orchestrate its own fan-out via tool calls. Mechanism: polling, not a handleRunResult hook. Every task the orchestrator watches (a story's root/Builder task, 4 Evaluators, Arbitration) is top-level (no ParentTaskID), and executor.Pool.handleRunResult only ever lands a top-level task at READY or BLOCKED -- never COMPLETED directly, since that transition normally requires a human/chatbot POST /api/tasks/{id}/accept in a different package. A handleRunResult hook would never observe it; polling doesn't care how/whether a task reached a given state. Stages: Builder COMPLETED -> spawn 4 role-typed Evaluator tasks (evaluator_quality/security/correctness/performance, DependsOn: [builder], no ParentTaskID -- true DAG siblings, not delegated subtasks) + story -> VALIDATING. Each Evaluator COMPLETED -> emit KindEvalVerdict (attached to the story's ID, so one GET /api/stories/{id}/events call surfaces every verdict). All 4 Evaluators COMPLETED -> spawn 1 Arbitration task (role: planner, DependsOn: all 4 evaluator IDs). Arbitration COMPLETED -> emit KindArbitrationDecided, story -> REVIEW_READY. POST /api/stories/{id}/accept (mirrors handleAcceptTask) -> DONE, emits KindHumanAccepted. Fixes a gap caught before merging: since none of Builder/Evaluators/ Arbitration have a ParentTaskID, none of them auto-complete -- each would otherwise need a separate manual /api/tasks/{id}/accept, meaning 6 human clicks per story before ever reaching the intended single story-level gate. StoryOrchestrator.autoAccept now transitions each of these specific tasks READY->COMPLETED itself (via the same validated Store.UpdateTaskState path acceptTask uses), scoped only to tasks already established as part of a story's pipeline (root task, or role-matched dependents from ensureEvaluators/ensureArbitration) -- never a blanket sweep of unrelated READY tasks. This makes POST /api/stories/{id}/accept the system's only required human touchpoint for the whole chain, matching the design goal that story (not task/subtask) is the human-interaction atom. Idempotency: structural for task-creation stages (ensureEvaluators/ ensureArbitration check ListDependents for already-existing role-matched tasks before creating -- crash/restart-safe); story.Status=="VALIDATING" gates the Arbitration->REVIEW_READY write (nothing further downstream to check structurally there); an in-memory handledVerdicts set (mirrors Scheduler.handled) dedupes per-evaluator KindEvalVerdict emission across poll ticks, resetting harmlessly on restart. Documented simplification: finalizeArbitration never parses the Arbitration summary for approve/reject -- always routes to REVIEW_READY; NEEDS_FIX is manually settable via PUT /api/stories/{id}. A later phase could close this with a dedicated verdict-reporting AgentChannel method instead of parsing free text. go build/vet/test -race -count=1 all pass, full suite (20 packages). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
13 daysfeat(story): add Epic/Story data model + REST CRUD (Phase 7a)Claude Sonnet 5
Pure data model + CRUD -- no orchestration behavior yet (that's Phase 7b). Revives ADR-007's stories concept (adapted; ADR-007 itself was deleted as "more machinery than the usage pattern needed") while staying lean: reuses the existing events/projects/task-tree machinery rather than building a parallel hierarchy. - internal/story: Epic/Story types. Epic is a loosely-scoped initiative (OPEN/CLOSED, no execution semantics). Story is a shippable slice of work realized by a task tree rooted at root_task_id; epic_id/project_id/ root_task_id are loose references (no FK enforcement, matching the codebase's existing tolerance for unmatched reference strings like tasks.project). - storage: new epics/stories tables (additive migrations) + CRUD methods. Story status is intentionally unvalidated pass-through in this phase -- no task.ValidTransition-style state machine, since there's no orchestrator yet to make transitions meaningful. - event: 9 new Kind constants for the ceremony this layer will eventually drive (epic_proposed, discovery_proposed, framing_decided, groomed, prioritized, eval_verdict, arbitration_decided, retro_captured, human_accepted) -- defined but nothing emits them yet. - api: GET/POST /api/epics, GET/PUT /api/epics/{id}, GET /api/epics/{id}/stories, GET/POST /api/stories, GET/PUT /api/stories/{id}, and GET /api/stories/{id}/task-tree -- BFS walk from root_task_id following both parent_task_id children and depends_on-edge dependents (visited-set guarded), returning a flat node list for a later UI phase to render as a graph. Unauthenticated, matching the existing projects/tasks endpoints' posture. go build/vet/test -race -count=1 all pass, full suite. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
13 daysfeat(role): add versioned role configs + escalation ladder + scheduler (Phase 5)Claude Sonnet 5
Two parts: Part A (fixes a gap from Phase 1): Groq/OpenRouter/OpenAI were documented in docs/api-keys-setup.md as usable once configured, but nothing actually constructed runners for them. internal/cli/cloudrunners.go consolidates anthropic/google/groq/openrouter/openai NativeRunner construction into one table-driven registerCloudRunners() helper, replacing the two hand-written per-provider blocks in serve.go/run.go. Groq/OpenRouter/OpenAI reuse openaicompat (no new adapter code) at SandboxKind: "docker". Part B: the token-husbanding harness's core routing mechanism. - internal/role: RoleConfig/Tier/Rung -- a role's system prompt and a multi-tier (provider, model) escalation ladder, versioned via config_json. - storage: new role_configs table (draft/active/retired, UNIQUE(role, version)) with transactional activate-retires-prior-active semantics; new executions.escalation_rung column. - task.AgentConfig.Role string -- purely additive; every existing task shape (Agent.Role == "") is unaffected, proven by TestPool_Execute_NonRoleTask_Unaffected plus the full pre-existing suite passing unchanged. - executor.Pool.execute(): role-typed tasks with no Agent.Type yet resolve tier 0 of their active ladder (round-robin across multi-candidate tiers, skipping rate-limited providers, falling back to soonest-clearing) before the existing pickAgent/Classifier path runs; SystemPrompt applies to Agent.SystemPromptAppend. Already-resolved role tasks (scheduler resubmits) get their escalation_rung re-derived read-only via findTierIndex. - internal/scheduler: polls role-typed FAILED tasks, retries at the same rung under MaxRetries or escalates to the next tier's first candidate when budget.Accountant.Allow() permits (emitting event.KindEscalated), else leaves the task FAILED with a final:true KindEscalated event. An in-memory per-execution-ID "handled" set keeps the poll loop convergent. Started by `serve` only, config knob [scheduler].poll_interval_seconds. - internal/api: POST/GET /api/roles/{role}/versions, POST /api/roles/{role}/activate -- unauthenticated, matching the existing projects/tasks REST endpoints' auth posture (only chatbot MCP, agent MCP, and WebSocket are api_token-gated in this codebase today). Documented as stored-but-not-yet-enforced (CLAUDE.md Design Debt, matching how task.Priority/RetryConfig are already documented): RoleConfig.Tools/ SandboxKind don't affect dispatch yet; DefaultBudgetUSD is read narrowly as the scheduler's escalation cost estimate, not enforced at initial dispatch; scheduler escalation always targets Candidates[0] (no round-robin, unlike initial-dispatch tier-0 resolution); the scheduler's dedupe is per-process and resets on restart (idempotent, harmless). go build/vet/test -race -count=1 all pass, 21 packages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
2026-06-05chore: remove stories/checker backend dead codeClaudomator Agent
Remove the dead stories/checker backend tracked as design debt in CLAUDE.md: - Delete internal/api/stories.go, stories_test.go, elaborate.go - Delete internal/task/story.go, story_test.go - Remove story/checker columns from storage schema and all CRUD methods - Remove checkStoryCompletion, spawnCheckerTask, triggerStoryDeploy, createValidationTask, checkValidationResult, ShipStory, CheckStoryCompletion from executor; simplify handleRunResult - Remove story/checker fields from task.Task (StoryID, AcceptanceCriteria, CheckerForTaskID, CheckerReport) - Remove story routes and geminiBinPath from API server - Move claudeJSONResult/extractJSON from elaborate.go to validate.go - Rename ensureStoryBranch -> ensureBranch in ContainerRunner - Fix git identity in bare-repo tests; fix initial-branch to use main Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03fix: serve log content for executions in READY and BLOCKED statesPeter Stone
terminalStates in logs.go only included Completed/Failed/TimedOut/Cancelled/ BudgetExceeded. Executions in READY state (task awaiting user accept/reject) fell through both branches, causing the log viewer to emit an immediate done event with no content — visually an empty log panel. BLOCKED is also added: ask_user suspends execution with the log fully written. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03merge: integrate oss branch — budget gating, MCP back-channel, events, ↵Peter Stone
loopback-only bind Key features from OSS branch: - Budget gating: rolling per-provider spend caps with BUDGET_EXCEEDED task state - Agent MCP back-channel: runners mint tokens; /mcp endpoint resolves them - Chatbot MCP server at /chatbot/mcp (requires api_token in config) - Event timeline: unified event stream replacing ad-hoc question/summary flows - Loopback-only default bind (127.0.0.1:8484); external_bind_allowed=true to expose - repository_url required on task creation (enforces traceability) - Auto-checker: spawns verification task after each execution Conflict resolutions: - serve.go: keep cfg.Runners.XEnabled() conditional registration from main + agentRegistry from oss - config.go: keep RunnersConfig from main + BudgetConfig/ExternalBindAllowed from oss - server.go: handleStaticFiles (base-path rewrite) kept; deduplicated duplicate from both sides - executor/claude.go: accepted oss deletion (ClaudeRunner replaced by ContainerRunner) - container_test.go: added noopChannel + AgentChannel parameter to Run call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03feat: add --base-path flag so UI routes API calls to correct prefixPeter Stone
Allows running multiple instances (e.g. /claudomator and /claudomator-oss) behind a reverse proxy. The server rewrites the base-path meta tag in index.html at request time rather than baking it into the embed. Also adds --branch support to deploy script for isolated branch builds via git worktrees. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03feat: add --base-path flag to configure UI URL prefixPeter Stone
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03fix: clone from local mirror and sync from GitHub push eventsPeter Stone
- ContainerRunner now resolves local project path for non-story (CI) tasks, cloning from the local bare repo instead of the HTTPS GitHub URL to avoid auth failures on the host. - CI failure tasks now use SSH URLs (git@github.com:...) instead of HTTPS to avoid credential prompts even when no local path is found. - GitHub push webhook events now trigger an async git fetch into the local bare repo, keeping the local mirror in sync when work happens directly on GitHub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26feat(api,web): budget headroom endpoint + UI chips (Phase 6)Claude
Adds GET /api/budget returning per-provider rolling-window headroom (empty when budget gating is unconfigured), wired from serve.go via SetBudget. The web UI polls it and renders a chip per limited provider in the header, flagging any under 20% remaining. New pure JS helpers formatBudgetHeadroom/ renderBudgetHeadroom are unit-tested; the endpoint is covered by Go handler tests (empty/reports/500). UI render not browser-verified in this environment. Completes Phase 6: spend accounting + dispatcher gating + observability surface. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-25feat(api,web): event-stream timeline (Phase 3)Claude
Adds GET /api/tasks/{id}/events (seq-ordered, ?since_seq for incremental polling) as the timeline data source, and a per-task Timeline section in the web UI that fetches and renders the observability event stream. New pure, unit-tested JS helpers: formatEventText, renderEventTimeline, fetchTaskEvents. The timeline load is async and guarded on a global fetch so the synchronous renderTaskPanel path (and its DOM-mock unit tests) is unaffected. Verified via Go endpoint tests and node --test (272 JS tests pass). Note: the live in-browser panel render was not exercised in this environment — only unit-level coverage. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-25refactor(api): remove the natural-language elaborate REST endpoint (Phase 3)Claude
Chatbots now elaborate tasks in-conversation and submit them via the chatbot MCP server, so the POST /api/tasks/elaborate shell-out path is no longer needed. Removes the route, handleElaborateTask, and the task-elaborate-only helpers (buildElaboratePrompt, sanitizeElaboratedTask, readProjectContext, appendRawNarrative, elaborateWith{Claude,Local,Gemini}, and the elaboratedTask/ elaboratedAgent types) plus their dedicated tests. Shared helpers (extractJSON, claude/geminiBinaryPath, claude/geminiJSONResult, elaborateTimeout, the per-IP limiter) are retained — the story-elaborate endpoint and task-validate endpoint still use them. Stories themselves are untouched here; their removal stays in the Phase 8 cleanup pass. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-25feat(api): chatbot-facing MCP server for task orchestration (Phase 3)Claude
Adds a chatbot-facing MCP server mounted at /chatbot/mcp, guarded by the shared API bearer token (new config api_token, wired through SetAPIToken). Tools: submit_task, list_tasks, get_task, get_events, answer_question, accept_task, reject_task, cancel_task. submit_task creates and immediately queues a task, resolving the repository URL from a named project when not given explicitly. To keep the REST API and the MCP surface from drifting, the accept/reject/ cancel/answer operations are extracted into a shared service layer (taskops.go) with typed errors mapped to REST status codes; the existing HTTP handlers now delegate to it. The endpoint is only served when a token is configured and presented as a bearer. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-24feat(executor,api): wire agent MCP into ContainerRunner + mount /mcp (Phase 2)Claude
ContainerRunner now mints a per-task MCP token (from an injected Registry), writes a claude mcp-config into the workspace pointing at the host agent MCP server over host.docker.internal with that bearer, and adds --mcp-config to the in-container claude invocation. The token is revoked when the run ends. After the run, a buffered ask_user (PendingQuestion on the channel) is converted into a BlockedError — the MCP path to BLOCKED — with the file-based question.json kept as a fallback for in-flight tasks started on the old wire. The API server mounts the StreamableHTTP MCP handler at POST/GET/DELETE /mcp when a registry is provided; serve.go constructs one Registry shared by the runners (mint) and the server (resolve). Minting is skipped for gemini agents. Tests: writeMCPConfig output shape, buildInnerCmd flag presence/absence, and an api-level end-to-end MCP tool call through NewServer/Handler proving the route is mounted (and absent without a registry). https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-24feat(executor): introduce AgentChannel seam for runner signalsClaude
Defines AgentChannel — the normalized interface by which a runner reports agent-originated signals (AskUser, ReportSummary, SpawnSubtask, RecordProgress) — plus a default storeChannel implementation backed by storage. Runner.Run now takes an AgentChannel; the pool constructs one per execution. The file transport routes its post-exit summary detection through ch.ReportSummary (buffered onto the execution so the pool still applies its extract/synthesize fallbacks, no double-write). AskUser returns ErrAgentBlocked since write-and-exit cannot answer in-session; question persistence stays with the pool's BlockedError handling. SpawnSubtask and RecordProgress are implemented and tested, ready for the MCP transport in Phase 2 where the channel becomes fully load-bearing. Store gains CreateEvent so the channel can emit agent_message events. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-23feat(storage): emit state_change events on task transitionsClaude
UpdateTaskState now writes a state_change event in the same transaction as the row update, so the event stream and task state never diverge. Adds UpdateTaskStateBy(id, newState, actor) for explicit actor attribution; UpdateTaskState delegates with ActorSystem (correct for the executor, which drives the state machine). RejectTask and ResetTaskForRetry also emit state_change events attributed to the user. API accept and cancel handlers now attribute their transitions to the user via UpdateTaskStateBy. The executor's Store interface is unchanged, so no test doubles needed updating. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-13merge: integrate github/main — LocalRunner, real GeminiRunner, llm clientPeter Stone
Merges 12 commits from github/main (formerly master) that were developed independently. Key additions: - LocalRunner: OpenAI-compatible local LLM execution (Ollama, LM Studio) - Real GeminiRunner with full sandbox parity to ClaudeRunner - llm.Client for enriching CI failures and elaboration via local model - retry.ParseRetryAfter moved to shared package - tokens_in/tokens_out columns in executions table Conflict resolutions: - Kept local main's VAPID/push, stories, projects, agent events schema - Merged both sets of Config fields (local + LocalModel from github/main) - Unified activePerAgent accounting (decActiveAgent helper) - Removed duplicate helpers from claude.go (now in helpers.go) - Fixed double-decrement bug in handleRunResult vs decActiveAgent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03fix: prevent SHIPPABLE stories and wrong READY state on failed tasksPeter Stone
Three related bugs fixed: 1. maybeUnblockParent: guard against promoting QUEUED leaf tasks (no subtasks) to READY. The vacuously-true 'all subtasks done' check was advancing tasks that stalled in QUEUED (due to a prior SQLite lock error) to READY on server restart via RecoverStaleBlocked, despite having only failed executions and no commits. 2. checkStoryCompletion: require COMPLETED (not just READY) for all top-level tasks before advancing a story to SHIPPABLE. READY means the checker agent is still pending or the task awaits human review; a story with READY tasks is not ready to ship. 3. handleAcceptTask: call CheckStoryCompletion after a task is accepted so stories with parent tasks (whose subtasks are all done and then the parent is manually accepted) can auto-advance to SHIPPABLE. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03chore: close deferred work — real GeminiRunner, Local UI option, db.go cleanupClaude
Closes the three items left on the deferred queue after the post-epic cleanup. GeminiRunner.execOnce now actually executes the gemini binary instead of writing hardcoded stream data. Mirrors ClaudeRunner.execOnce: - exec.CommandContext with the same env vars (CLAUDOMATOR_API_URL etc.) - process group SIGKILL on context cancel - stdout piped through parseGeminiStream → stdoutFile - stderr to file - exit codes captured, stderr tail surfaced on failure Test infrastructure bug uncovered in passing: testServerWithGeminiMockRunner's mock script used double-quoted echo with literal triple-backticks, which bash interpreted as command substitution. The script always produced empty output. The bug was invisible until now because GeminiRunner ignored the script entirely. Switched to a single-quoted heredoc. Frontend: index.html dropdown gains a "Local" option. No JS branching needed — the value flows through to agent.type verbatim and downstream display reads the type string as-is. storage/db.go: removed stale debug-comment scaffolding (the "TODO: Replace with proper logger" block) that was tracking a dead `fmt.Printf` call. The path it commented on is fine without logging — unmarshal errors are returned wrapped. Test status: `go test -race ./...` green across every package, zero skips, zero excluded tests. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
2026-05-03chore: post-epic cleanup — green test suite, no skipsClaude
Addresses the cleanup queue captured in docs/plans/local-oss-runner.md after the local-OSS-models epic landed. After this commit `go test -race ./...` is green across every package with zero `t.Skip` calls and no excluded tests. Real bugs fixed: - claude.go setupSandbox callsites used `sandboxDir, err := ...` which shadowed the outer variable, so BlockedError.SandboxDir was always empty. Resume-after-block was broken for both new and stale-sandbox paths. TestBlockedError_IncludesSandboxDir now exercises the right invariant. - TestPool_ActivePerAgent_DeletesZeroEntries flake under -race: the cleanup defer in execute()/executeResume() runs AFTER handleRunResult sends on resultCh, so consumers observing a result could see a still-counted activePerAgent entry. Extracted decActiveAgent(agentType, *cleaned) helper; called explicitly before every resultCh send, defer becomes a no-op via the cleaned flag. Verified clean over `go test -race -count=10`. Test infrastructure made hermetic: - gitSafe now also passes -c commit.gpgsign=false / -c tag.gpgsign=false so sandbox tests pass on hosts whose global config requires signing. - Bare repos in tests initialized with `-b main` (HEAD symbolic ref matched to the branch we push) so `git log` after push works. - TestSandboxCloneSource_FallsBackToOrigin uses a local-FS origin URL, matching sandboxCloneSource's intentional filter against network URLs. - TestGeminiLogs_ParsedCorrectly URL fixed to the actual log route (/api/executions/{id}/log). GeminiRunner gap closed (partial): - parseGeminiStream now walks lines for `result` events, surfacing is_error as an error and total_cost_usd as the float return value. - GeminiRunner.Run propagates parsed cost to Execution.CostUSD. - TestParseGeminiStream_ParsesStructuredOutput unskipped. Notes: - GeminiRunner is still simulated end-to-end (Run writes hardcoded stream data instead of execing the binary). The result/cost parser now exists; finishing the runner is a smaller, contained follow-up. Kept on the deferred queue. - Frontend "Local" agent option and a minor storage.db.go logger TODO remain on the deferred queue, both intentionally — neither blocks anything in flight. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
2026-05-02feat(api): enrich CI failure task instructions via local LLMClaude
Phase 3 of "local OSS models as agents" plan. When the webhook handler creates a task for a failed CI run AND a local LLM is configured on the server, the hardcoded 4-step investigation template is replaced with a project-aware investigation plan generated by the LLM. Scope adjustment from the original sketch: the original plan said "summarize fetched workflow logs", but fetching logs requires GitHub API auth that isn't wired. Narrowed to project-context triage — recent git log + CLAUDE.md content + webhook metadata, fed to the LLM with a system prompt asking for 6-12 lines of concrete next steps. Deferred GitHub log fetching to post-epic cleanup. Implementation: - New internal/api/webhook_llm.go holds enrichCIInstructions and its helpers (readRecentCommits via `git log`, readProjectDoc). - enrichCIInstructions is truly additive: any failure mode (no client, HTTP error, empty body, 10s timeout) returns the original fallback template unchanged. Existing webhook tests pass byte-for-byte. - Always preserves a metadata header (repo/branch/SHA/check/URL) ahead of the LLM body so investigators don't lose context if the LLM is terse. - Reuses s.llm (set via Server.SetLLM in Phase 2) — no new config knob, no per-feature gating. Asymmetric opt-out (yes-elaborate, no-CI-triage) deferred until there's actual demand. Tests: - enrichCIInstructions: nil client, LLM 500, empty body all return fallback unchanged. - enrichCIInstructions: success path produces enriched body with metadata header preserved; user prompt contains repo/branch/SHA. - enrichCIInstructions: real git repo (init + 2 commits) → recent commits appear in user prompt. - Webhook handler regression guard: no-LLM path produces the exact legacy template substrings. - Webhook handler with LLM stubbed: task instructions contain LLM body + metadata header. Plan: docs/plans/local-oss-runner.md. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
2026-04-28feat(api): route elaboration through local LLM when configuredClaude
Phase 2 of "local OSS models as agents" plan. Adds a third elaboration path that calls the local OpenAI-compatible LLM via the internal/llm client, and reorders dispatch so the cheap path is tried first: local → claude → gemini, with each next attempt only on hard failure of the prior. Wiring is opt-out, not opt-in: when [local_model].endpoint is set, elaboration prefers local by default. Users with a slow or low-quality local model can disable just elaboration via: [local_model] endpoint = "..." prefer_for_elaborate = false without giving up the runner or the classifier path. Implementation: - Server gains an optional *llm.Client field via SetLLM (matches the existing SetNotifier/SetWorkspaceRoot setter pattern, no NewServer signature break). - elaborateWithLocal() reuses buildElaboratePrompt verbatim and asks for response_format=json_object so we skip markdown-fence cleanup. - handleElaborateTask reorders try chain; existing Claude-first behavior is preserved exactly when SetLLM is not called. - LocalModel.UseForElaborate() encapsulates the default-true gating with a *bool so explicit-false survives TOML parse. Tests: - elaborateWithLocal: parses valid response, errors on nil client, errors on bad JSON. - handler: local preferred when wired; falls back to claude when local fails; unchanged behavior when no LLM is configured. - config: UseForElaborate gating across empty/default/explicit-true/ explicit-false cases. Pre-existing test failures noted in docs/plans/local-oss-runner.md (post-epic cleanup): TestGeminiLogs_ParsedCorrectly returns 404 for gemini execution log fetch — predates this change. Plan: docs/plans/local-oss-runner.md. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
2026-04-11fix: tie pool submissions to server lifecycle contextClaude Agent
Fix 1 (server.go): Replace context.Background() with s.ctx in handleAnswerQuestion, handleResumeTimedOutTask, and handleRunTask. Add a ctx field to Server (defaulting to context.Background()) and a SetContext method so the serve command can wire in the signal- cancellable lifecycle context. Fix 2 (serve.go): Call srv.SetContext(ctx) before StartHub so all pool submissions use the server's root context (already cancelled on SIGTERM/SIGINT). Pool.Shutdown and its wiring were already present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11cleanup: remove dead code (QuestionRegistry, changestats wrappers, scanner.Err)Claudomator Agent
Fix 1: Remove QuestionRegistry and related types (QuestionHandler, PendingQuestion) from question.go -- nothing reads Pool.Questions or uses the registry. Remove NewQuestionRegistry() call from NewPool and the Questions field from Pool. Remove the now-superfluous registry tests; keep stream/parse helpers which are still used by the claude runner. Fix 2: Check scanner.Err() after the parseStream loop so I/O errors from the scanner are not silently swallowed when streamErr is still nil. Fix 3: Delete internal/api/changestats.go -- the parseChangestatFromFile and parseChangestatFromOutput wrappers were only needed to support processResult(), which no longer calls them; they are unreachable dead code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: acceptance_criteria per story task in elaboration and approvalPeter Stone
Add AcceptanceCriteria field to elaboratedStoryTask struct, update buildStoryElaboratePrompt schema and rules, pass the value through handleApproveStory into task.Task, and add a test verifying the field is persisted on approved story tasks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: story ship gate — explicit POST /api/stories/{id}/ship; remove ↵Peter Stone
auto-deploy - checkStoryCompletion now guards against re-running on already-SHIPPABLE stories and no longer auto-triggers triggerStoryDeploy on completion - New Pool.ShipStory method validates SHIPPABLE state then fires triggerStoryDeploy - POST /api/stories/{id}/ship route registered and handleShipStory handler added - Two new tests: 202 for SHIPPABLE story, 409 for non-SHIPPABLE story Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: require repository_url on tasks; fix UpdateTask to persist it; fix ↵Peter Stone
cascade-retry test race Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fix: remove drain-lock circuit breaker that halted all executions after 3 ↵Peter Stone
consecutive failures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26feat: graceful shutdown — drain workers before exit (default 3m timeout)Peter Stone
- Add workerWg to Pool; Shutdown() closes workCh and waits for all in-flight execute/executeResume goroutines to finish - Signal handler now shuts down HTTP first, then drains the pool - ShutdownTimeout config field (toml: shutdown_timeout); default 3m - Tests: WaitsForWorkers and TimesOut Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26feat: cascade retry deps when running a task with failed dependenciesPeter Stone
When /run is called on a CANCELLED/FAILED task that has deps in a terminal failure state, automatically reset and resubmit those deps so the task isn't immediately re-cancelled by the pool's dep check. Also update reset-failed-tasks script to handle CANCELLED tasks and clean up preserved sandbox workspaces. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26fix: story branch push to bare repo; drain at 3 consecutive failuresPeter Stone
createStoryBranch was pushing to 'origin' which doesn't exist — branches never landed in the bare repo so agents couldn't clone them. Now uses the project's RemoteURL (bare repo path) directly for fetch and push. Raise drain threshold from 2 to 3 consecutive failures to reduce false positives from transient errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26fix: resolve dep-chain deadlock; broadcast task_started for UI visibilityPeter Stone
With maxPerAgent=1, tasks with DependsOn were entering waitForDependencies while holding the per-agent slot, preventing the dependency from ever running. Fix: check deps before taking the slot. If not ready, requeue without holding activePerAgent. Also accept StateReady (leaf tasks) as a satisfied dependency, not just StateCompleted. Add startedCh to pool and broadcast task_started WebSocket event when a task transitions to RUNNING, so the UI immediately shows the running state during the clone phase instead of waiting for completion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26fix: set RepositoryURL on tasks created via story approvePeter Stone
handleApproveStory was creating tasks without RepositoryURL, causing executions to fail with "task has no repository_url". Now looks up the project's RemoteURL and sets it on all tasks and subtasks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26fix: story tasks get Project field; elaborate reads worklog; deploy chmod ↵Peter Stone
scripts - handleApproveStory: set Project = input.ProjectID on tasks and subtasks so the executor can resolve RepositoryURL from the project registry (was causing "task has no repository_url" on every story task) - elaborate.go: read .agent/worklog.md instead of SESSION_STATE.md for project context injected into elaboration prompts - deploy: explicitly chmod +x all scripts before restart (same root cause as the binary execute-bit loss — chown -R was stripping it) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25chore: replace all master branch references with mainPeter Stone
- executor.go: merge story branch to main before deploy - container.go: error messages reference git push origin main - api/stories.go: create story branch from origin/main (drop master fallback) - executor_test.go: test setup uses main branch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23feat: Phase 4 — story-aware execution, branch clone, story completion ↵Claudomator Agent
check, deployment status - ContainerRunner: add Store field; clone with --reference when story has a local project path; checkout story branch after clone; push to story branch instead of HEAD - executor.Store interface: add GetStory, ListTasksByStory, UpdateStoryStatus - Pool.handleRunResult: trigger checkStoryCompletion when a story task succeeds - Pool.checkStoryCompletion: transitions story to SHIPPABLE when all tasks done - serve.go: wire Store into each ContainerRunner - stories.go: update createStoryBranch to fetch+checkout from origin/master base; add GET /api/stories/{id}/deployment-status endpoint - server.go: register deployment-status route - Tests: TestPool_CheckStoryCompletion_AllComplete/PartialComplete, TestHandleStoryDeploymentStatus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22feat: color logo based on build version hashPeter Stone
Adds GET /api/version endpoint and uses the first 6 hex chars of the commit hash to derive an HSL hue for the header h1 logo color. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22feat: Phase 5 — story elaboration endpoint, approve flow, branch creationClaudomator Agent
- POST /api/stories/elaborate: runs Claude/Gemini against project LocalPath to produce a structured story plan (name, branch_name, tasks, validation) - POST /api/stories/approve: creates story + sequentially-wired tasks/subtasks from the elaborate output and pushes the story branch to origin - createStoryBranch helper: git checkout -b + push -u origin - Tests: TestBuildStoryElaboratePrompt, TestHandleStoryApprove_WiresDepends Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22feat: surface error_msg on failed task cards in UIPeter Stone
2026-03-22feat: Phase 3 — stories data model, ValidStoryTransition, storage CRUD, ↵Claudomator Agent
API endpoints - internal/task/story.go: Story struct, StoryState constants, ValidStoryTransition - internal/task/task.go: add StoryID field - internal/storage/db.go: stories table + story_id on tasks migrations; CreateStory, GetStory, ListStories, UpdateStoryStatus, ListTasksByStory; update all task SELECT/INSERT to include story_id; scanTask extended with sql.NullString for story_id; added modernc timestamp format to GetMaxUpdatedAt - internal/storage/sqlite_cgo.go + sqlite_nocgo.go: build-tag based driver selection (mattn/go-sqlite3 with CGO, modernc.org/sqlite pure-Go fallback) so tests run without a C compiler - internal/api/stories.go: GET/POST /api/stories, GET /api/stories/{id}, GET/POST /api/stories/{id}/tasks (auto-wires depends_on chain), PUT /api/stories/{id}/status (validates transition) - internal/api/server.go: register all story routes - go.mod/go.sum: add modernc.org/sqlite pure-Go dependency Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21feat: executor reliability — per-agent limit, drain gate, pre-flight ↵Claudomator Agent
creds, auth recovery - maxPerAgent=1: only 1 in-flight execution per agent type at a time; excess tasks are requeued after 30s - Drain gate: after 2 consecutive failures the agent is drained and a question is set on the task; reset on first success; POST /api/pool/agents/{agent}/undrain to acknowledge - Pre-flight credential check: verify .credentials.json and .claude.json exist in agentHome before spinning up a container - Auth error auto-recovery: detect auth errors (Not logged in, OAuth token has expired, etc.) and retry once after running sync-credentials and re-copying fresh credentials - Extracted runContainer() helper from ContainerRunner.Run() to support the retry flow - Wire CredentialSyncCmd in serve.go for all three ContainerRunner instances - Tests: TestPool_MaxPerAgent_*, TestPool_ConsecutiveFailures_*, TestPool_Undrain_*, TestContainerRunner_Missing{Credentials,Settings}_FailsFast, TestIsAuthError_*, TestContainerRunner_AuthError_SyncsAndRetries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21feat: Phase 2 — project registry, legacy field cleanup, credential path fixPeter Stone
- task.Project type + storage CRUD + UpsertProject + SeedProjects - Remove AgentConfig.ProjectDir, RepositoryURL, SkipPlanning - Remove ContainerRunner fallback git init logic - Project API endpoints: GET/POST /api/projects, GET/PUT /api/projects/{id} - processResult no longer extracts changestats (pool-side only) - claude_config_dir config field; default to credentials/claude/ - New scripts: sync-credentials, fix-permissions, check-token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19feat: add errors, throughput, and billing sections to stats dashboardPeter Stone
- GET /api/stats?window=7d: pre-aggregated SQL queries for errors, throughput, billing - Errors section: category summary (quota/rate_limit/timeout/git/failed) + failure table - Throughput section: stacked hourly bar chart (completed/failed/other) over 7d - Billing section: KPIs (7d total, avg/day, cost/run) + daily cost bar chart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19feat: agent status dashboard with availability timeline and Gemini quota ↵Peter Stone
detection - Detect Gemini TerminalQuotaError (daily quota) as BUDGET_EXCEEDED, not generic FAILED - Surface container stderr tail in error so quota/rate-limit classifiers can match it - Add agent_events table to persist rate-limit start/recovery events across restarts - Add GET /api/agents/status endpoint returning live agent state + 24h event history - Stats dashboard: agent status cards, 24h availability timeline, per-run execution table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18fix: address final container execution issues and cleanup review docsPeter Stone
2026-03-18fix: comprehensive addressing of container execution review feedbackPeter Stone
- Fix Critical Bug 1: Only remove workspace on success, preserve on failure/BLOCKED. - Fix Critical Bug 2: Use correct Claude flag (--resume) and pass instructions via file. - Fix Critical Bug 3: Actually mount and use the instructions file in the container. - Address Design Issue 4: Implement Resume/BLOCKED detection and host-side workspace re-use. - Address Design Issue 5: Consolidate RepositoryURL to Task level and fix API fallback. - Address Design Issue 6: Make agent images configurable per runner type via CLI flags. - Address Design Issue 7: Secure API keys via .claudomator-env file and --env-file flag. - Address Code Quality 8: Add unit tests for ContainerRunner arg construction. - Address Code Quality 9: Fix indentation regression in app.js. - Address Code Quality 10: Clean up orphaned Claude/Gemini runner files and move helpers. - Fix tests: Update server_test.go and executor_test.go to work with new model.