summaryrefslogtreecommitdiff
path: root/internal/executor/executor_test.go
AgeCommit message (Collapse)Author
7 daysfeat(task,executor): add CurrentAttempt resolution, wire maybeUnblockParent ↵Claudomator Agent
to resolve through it
7 daysstyle: gofmt executor.go, executor_test.go, task.goPeter Stone
Pre-existing gofmt debt on all three files (confirmed dirty before this session's nested-subtask-completion commit too, not a regression) -- cleaned up while already touching these files.
7 daysfix(executor): support nested subtask decomposition ↵Claudomator Agent
(subtask-with-own-subtasks correctly blocks, cascades, and recovers)
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
13 daysfeat(executor): add DAG auto-cascade-fail + role-typed subtask spawning ↵Claude Sonnet 5
(Phase 6) Two prerequisites for safe parallel evaluator fan-out (later phase): 1. Auto-cascade-fail: previously, a failed task's dependents just sat PENDING/QUEUED forever (or until something eventually tried to dispatch them and discovered the dependency was dead). Pool.cascadeFail now fires right after a task lands in a terminal failure state (FAILED/TIMED_OUT/ CANCELLED/BUDGET_EXCEEDED, from handleRunResult, the budget-gate reject path, and the checkDepsReady dependency-failure path), recursively cancelling every not-yet-run dependent (PENDING/QUEUED only -- RUNNING and terminal states are left alone) with a message referencing the upstream failure. A visited-set guards recursion, which turned out to be load-bearing rather than defense-in-depth: task creation does not prevent dependency cycles anywhere in this codebase. Correction to an earlier assumption: internal/executor's waitForDependencies is dead code, never called. The live mechanism is checkDepsReady, invoked synchronously in execute() with a self-requeue via time.AfterFunc. Added a freshness re-check (GetTask, bail if no longer QUEUED) at the top of that block so a task cascade-cancelled while sitting in the requeue loop stops silently instead of hitting an invalid CANCELLED->CANCELLED transition or, worse, still getting dispatched. 2. storeChannel.SpawnSubtask hardcoded Agent.Type: "claude" on every spawned child regardless of what role it should play -- a hard blocker for a Planner/Builder task spawning role-typed evaluator subtasks. SubtaskSpec (internal/agentchannel) gains a Role field; when set, the child task gets Agent.Role instead of a hardcoded Type, so Phase 5's role-resolution picks provider/model from that role's escalation ladder. spec.Role == "" (every existing caller) preserves today's exact behavior byte-for-byte -- proven by an explicit regression test, not just new-feature coverage. Threaded the new `role` parameter through both spawn_subtask transports: the native tool-use loop (internal/agentloop/tools.go) and the MCP tool exposed to ContainerRunner-driven claude/gemini agents (internal/executor/agentmcp.go). 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(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-05-26feat(executor): budget-gate the dispatcher — reroute to local or block ↵Claude
(Phase 6) The pool now consults a BudgetGate before taking an agent slot. If running on the chosen paid provider could breach its rolling window (estimated by the task's max_budget_usd), it reroutes to the free local runner when one is registered; otherwise it stops before running and marks the task BUDGET_EXCEEDED. serve.go builds a budget.Accountant from [budget] config and wires it into the pool (only when limits are configured). Allows the QUEUED → BUDGET_EXCEEDED transition, since the gate blocks a queued task before it ever reaches RUNNING. Covered by pool tests for both the block and reroute paths against a fake gate. 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-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: atomic execution creation + RUNNING state transitionPeter Stone
Add CreateExecutionAndSetRunning to storage.DB and Store interface, replacing the two sequential CreateExecution/UpdateTaskState calls in executor.go. Eliminates the crash window where a task stays PENDING with an orphaned RUNNING execution record. 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-02feat(executor): synthesize execution summary via local LLM fallbackClaude
Phase 4 of "local OSS models as agents" plan. Closes the epic. When an execution finishes and the agent did NOT write a "## Summary" heading in its stdout (so the existing extractSummary path returns empty), and the Pool has a local LLM configured, we now synthesize a 2-4 sentence summary from the assistant text content of the log tail. Behavior: - Primary path unchanged: if the agent wrote "## Summary", that wins byte-for-byte (TestPool_HandleRunResult_ExtractSummaryWins guards). - Fallback path: empty extractSummary + Pool.LLM != nil → synthesize. - All-empty path: when no LLM is configured, summary stays empty — identical to pre-Phase-4 behavior. Implementation: - Pool gains an LLM *llm.Client field, wired in serve.go and run.go alongside Classifier.LLM (same localClient used everywhere). - New synthesizeSummary in internal/executor/summary.go: * 6s timeout so a slow local model can't stall finalization * 16 KB tail cap on the stdout log * readAssistantTextTail seeks to the last 16 KB and skips the first (likely partial) line, parses each line as a stream-json event, joins assistant `text` blocks (skips system/result/etc). * Returns "" on any error so the caller's behavior never regresses. - handleRunResult: 3-tier summary resolution — exec.Summary set by runner → extractSummary → synthesizeSummary → empty. - minimalMockStore now records UpdateTaskSummary calls (additive; existing tests unaffected) so integration tests can assert. Tests (9 new): - synthesizeSummary nil client / empty path / missing file all return "" without HTTP calls. - empty assistant content short-circuits without LLM call. - success path returns trimmed body, with both assistant texts in the user prompt. - LLM 500 returns "" (caller handles same as no-summary). - readAssistantTextTail seeks past early content in a large file. - Pool integration: ## Summary present → LLM not called, agent text used. ## Summary absent + LLM set → LLM called, synthesized summary recorded against the right task ID. Plan: docs/plans/local-oss-runner.md. Epic complete. Post-epic deep cleanup queue captured in the same plan file for follow-up. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
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-04fix: executor checker — close race, test flakiness, checker report for all ↵Peter Stone
failure states Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: spawn checker task on READY; auto-accept on pass; attach report on failPeter Stone
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-26fix: story stays PENDING when all subtasks complete but parent task stays QUEUEDPeter Stone
When a story is approved with pre-created subtasks, parent tasks are QUEUED but never run. Their subtasks complete, but: - maybeUnblockParent only handled BLOCKED parents, not QUEUED ones - checkStoryCompletion required ALL tasks (incl. subtasks) to be done Fixes: - maybeUnblockParent now also promotes QUEUED parents to READY when all subtasks are COMPLETED - checkStoryCompletion only checks top-level tasks (parent_task_id="") - RecoverStaleBlocked now also scans QUEUED parents on startup and triggers checkStoryCompletion if it promotes them - Add QUEUED→READY to valid state transitions (subtask delegation path) 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-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-26test: add TestPool_DependsOn_NoDeadlockstory/repo-urlPeter Stone
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-24feat: merge story branch to master before deploy, add doot project to registryPeter Stone
- triggerStoryDeploy: fetch/checkout/merge --no-ff/push before running deploy script (ADR-007) - executor_test: TestPool_StoryDeploy_MergesStoryBranch proves merge happens - seed.go: add doot project with deploy script; wire claudomator deploy script Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24Merge branch 'master' of /site/git.terst.org/repos/claudomatorPeter Stone
2026-03-24feat: validation result transitions story to REVIEW_READY or NEEDS_FIX (ADR-007)Claudomator Agent
Add checkValidationResult which inspects the final task.State of a completed validation task and updates the story to REVIEW_READY (pass) or NEEDS_FIX (fail). Wire into handleRunResult so stories in VALIDATING state are dispatched to checkValidationResult instead of checkStoryCompletion, covering both success and FAILED terminal paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24feat: auto-create validation task on story DEPLOYED (ADR-007)Claudomator Agent
2026-03-24feat: trigger deploy script on SHIPPABLE → DEPLOYED (ADR-007)Claudomator Agent
Add triggerStoryDeploy to Pool: fetches story's project, runs its DeployScript via exec.CommandContext, and advances story to DEPLOYED on success. Wire into checkStoryCompletion with go p.triggerStoryDeploy after the SHIPPABLE transition. Covered by TestPool_StoryDeploy_RunsDeployScript. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24fix: resolve merge conflict — integrate agent's story-aware ContainerRunnerPeter Stone
Agent added: Store on ContainerRunner (direct story/project lookup), --reference clone for speed, explicit story branch push, checkStoryCompletion → SHIPPABLE. My additions: BranchName on Task as fallback when Store is nil, tests updated to match checkout-after-clone approach. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24feat: clone story branch in ContainerRunner (ADR-007)Peter Stone
- Add BranchName field to task.Task (populated from story at execution time) - Add GetStory to executor Store interface; resolve BranchName from story in both execute() and executeResume() parallel to RepositoryURL resolution - Pass --branch <name> to git clone when BranchName is set; default clone otherwise 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-23feat: populate RepositoryURL from project registry in executor (ADR-007)Peter Stone
- Add GetProject to Store interface used by executor - Resolve RepositoryURL from project registry when task.RepositoryURL is empty - Call SeedProjects at server startup so the project registry is populated - Add GetProject stub to minimalMockStore in executor tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22fix: make requeueDelay configurable to fix test timeout in ↵Peter Stone
TestPool_MaxPerAgent_BlocksSecondTask
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-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: 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.
2026-03-16fix: clean up activePerAgent before sending to resultChClaudomator Agent
Move activePerAgent decrement/deletion out of execute() and executeResume() defers and into the code paths immediately before each resultCh send (handleRunResult and early-return paths). This guarantees that when a result consumer reads from the channel the map is already clean, eliminating a race between defer and result receipt. Remove the polling loop from TestPool_ActivePerAgent_DeletesZeroEntries and check the map state immediately after reading the result instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16fix: eliminate flaky race in TestPool_ActivePerAgent_DeletesZeroEntriesPeter Stone
The deferred activePerAgent cleanup in execute() runs after resultCh is sent, so a consumer reading Results() could observe the map entry before it was removed. Poll briefly (100ms max) instead of checking immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15fix: promote stale BLOCKED parent tasks to READY on server startupPeter Stone
When the server restarts after all subtasks complete, the parent task was left stuck in BLOCKED state because maybeUnblockParent only fires during a live executor run. RecoverStaleBlocked() scans all BLOCKED tasks on startup and re-evaluates them using the existing logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14feat(Phase4): add file changes for changestats executor wiringClaude Sonnet 4.6
Files changed: CLAUDE.md, internal/api/changestats.go, internal/executor/executor.go, internal/executor/executor_test.go, internal/task/changestats.go (new) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14fix: surface agent stderr, auto-retry restart-killed tasks, handle stale ↵Peter Stone
sandboxes #1 - Diagnostics: tailFile() reads last 20 lines of subprocess stderr and appends to error message when claude/gemini exits non-zero. Previously all exit-1 failures were opaque; now the error_msg carries the actual subprocess output. #4 - Restart recovery: RecoverStaleRunning() now re-queues tasks after marking them FAILED, so tasks killed by a server restart automatically retry on the next boot rather than staying permanently FAILED. #2 - Stale sandbox: If a resume execution's preserved SandboxDir no longer exists (e.g. /tmp purge after reboot), clone a fresh sandbox instead of failing immediately with "no such file or directory". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14testClaudomator Agent
2026-03-14feat: add agent selector to UI and support direct agent assignmentPeter Stone
- Added an agent selector (Auto, Claude, Gemini) to the Start Next Task button. - Updated the backend to pass query parameters as environment variables to scripts. - Modified the executor pool to skip classification when a specific agent is requested. - Added --agent flag to claudomator start command. - Updated tests to cover the new functionality.
2026-03-13fix: resubmit QUEUED tasks on server startup to prevent them getting stuckPeter Stone
Add Pool.RecoverStaleQueued() that lists all QUEUED tasks from the DB on startup and re-submits them to the in-memory pool. Previously, tasks that were QUEUED when the server restarted would remain stuck indefinitely since only RUNNING tasks were recovered (and marked FAILED). Called in serve.go immediately after RecoverStaleRunning(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10executor: explicit load balancing — code picks agent, classifier picks modelPeter Stone
pickAgent() deterministically selects the agent with the fewest active tasks, skipping rate-limited agents. The classifier now only selects the model for the pre-assigned agent, so Gemini gets tasks from the start rather than only as a fallback when Claude's quota is exhausted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10executor: extract handleRunResult to deduplicate error-classification logicClaudomator Agent
Both execute() and executeResume() shared ~80% identical post-run logic: error classification (BLOCKED, TIMED_OUT, CANCELLED, BUDGET_EXCEEDED, FAILED), state transitions, result emission, and UpdateExecution. Extract this into handleRunResult(ctx, t, exec, err, agentType) on *Pool. Both functions now call it after runner.Run() returns. Also adds TestHandleRunResult_SharedPath which directly exercises the new function via a minimalMockStore, covering FAILED, READY, COMPLETED, and TIMED_OUT classification paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09executor: unblock parent task when all subtasks completeClaudomator Agent
Add maybeUnblockParent helper that transitions a BLOCKED parent task to READY once every subtask is in the COMPLETED state. Called in both execute() and executeResume() immediately after a subtask is marked COMPLETED. Any non-COMPLETED sibling (RUNNING, FAILED, etc.) keeps the parent BLOCKED. Tests added: - TestPool_Submit_LastSubtask_UnblocksParent - TestPool_Submit_NotLastSubtask_ParentStaysBlocked - TestPool_Submit_ParentNotBlocked_NoTransition
2026-03-09executor: BLOCKED→READY for top-level tasks with subtasksClaudomator Agent
When a top-level task (ParentTaskID == "") finishes successfully, check for subtasks before deciding the next state: - subtasks exist → BLOCKED (waiting for subtasks to complete) - no subtasks → READY (existing behavior, unchanged) This applies to both execute() and executeResume(). Adds ListSubtasks to the Store interface. Tests: - TestPool_Submit_TopLevel_WithSubtasks_GoesBlocked - TestPool_Submit_TopLevel_NoSubtasks_GoesReady Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09executor: log errors from all unchecked UpdateTaskState/UpdateTaskQuestion callsClaudomator Agent
All previously ignored errors from p.store.UpdateTaskState() and p.store.UpdateTaskQuestion() in execute() and executeResume() now log with structured context (taskID, state, error). Introduces a Store interface so tests can inject a failing mock store. Adds TestPool_UpdateTaskState_DBError_IsLoggedAndResultDelivered to verify that a DB write failure is logged and the result is still delivered to resultCh. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09executor: fix map leaks in activePerAgent and rateLimitedClaudomator Agent
activePerAgent: delete zero-count entries after decrement so the map doesn't accumulate stale keys for agent types that are no longer active. rateLimited: delete entries whose deadline has passed when reading them (in both the classifier block and the execute() pre-flight), so stale entries are cleaned up on the next check rather than accumulating forever. Both fixes are covered by new regression tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>