summaryrefslogtreecommitdiff
path: root/internal/executor
AgeCommit message (Collapse)Author
2026-06-04fix(local-runner): retry on tool-400, fix preamble gate, set summary from ↵Claude Sonnet 4.6
text output - Retry Chat() without tools when error contains "does not support tools" (case-insensitive); tinyllama fast-path still skips the first round-trip. - Pass mcpEnabled=len(tools)>0 to buildAgentInstructions so tool-less models don't receive a planning preamble they can't act on; tools must be decided before messages are built, so reorder accordingly. - Collect all assistant text into fullText across turns; after a non-blocked run, if e.Summary is empty set it to the first 500 chars of fullText so handleRunResult has something to store when report_summary is never called. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04executor: don't send tool definitions to tinyllama modelsPeter Stone
2026-06-04cli: add --repository-url flag and improve error reportingPeter Stone
2026-06-03fix: release dispatch slot on all early-return paths in execute()Peter Stone
p.active was incremented by dispatch() before launching execute(), but execute() had four early-return paths (budget gate, dep terminal failure, deps not ready, per-agent limit) that returned before the defer decActiveAgent was registered. Each such return leaked a slot, eventually saturating maxConcurrent and permanently blocking dispatch() on doneCh. Introduce releaseDispatchSlot() (decrements p.active + signals doneCh) and defer it at the very top of execute() and executeResume(), so every exit path releases the dispatch slot. Remove p.active and doneCh management from decActiveAgent so it only handles the per-agent counter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03fix: embed execution log in checker instructions and add noopChannel to testsPeter Stone
The checker task previously had no context about what the agent did — it tried to reach the API (potentially unavailable), search /home for artifacts, and clone the repo, all of which fail for LocalRunner tasks. Now spawnCheckerTask reads the tail of the execution's stdout.log and embeds it directly in the checker's instructions so the checker can verify output without needing Docker, API access, or repo cloning. Also fixes container_test: adds noopChannel to satisfy the AgentChannel parameter added by the OSS Runner interface. 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-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-27refactor(executor): retire the file-based question/summary fallback (Phase 8)Claude
With the MCP agent channel validated (the spike confirmed real claude calls ask_user/report_summary through it), the container runner no longer needs the pre-MCP file fallback that read question.json/summary.txt after the agent exited. Removes that block and the now-orphaned isCompletionReport/ extractQuestionText helpers and their test. The MCP path (channelPendingQuestion) is now the sole question/summary transport. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
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-26feat(budget,storage,config): per-provider spend accounting substrate (Phase 6)Claude
Adds the budget accountant foundation: - storage: a per-execution `agent` column (additive migration, populated by the dispatcher) and SpendByProviderSince(since), summing cost_usd per provider in a window. Accurate attribution survives a task running on different providers across retries. - internal/budget: Accountant over a SpendSource + Limits, exposing Headroom (remaining/fraction per provider), Allow (would estCost breach the cap), and All (one query, sorted). Unconfigured/local providers are unlimited. - config: a [budget] section (window + provider_5h_usd map). No default cap — gating is opt-in by configuring limits. Fully unit-tested; dispatcher gating and the API/UI surface follow. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-26fix(executor): set IS_SANDBOX=1 so claude honors bypassPermissions under rootClaude
The spike found that `claude --permission-mode bypassPermissions` (emitted by buildInnerCmd) is rejected when the process runs as root, and buildDockerArgs maps the container --user to the host uid — which is 0 when claudomator runs as root, breaking all claude tool execution in production. The container is an isolated sandbox, exactly the case the claude CLI gates behind IS_SANDBOX=1; setting it in the container env makes bypassPermissions work regardless of the host uid. Verified empirically against claude 2.1.150 (rejected as root without it; succeeds with it). Harmless for gemini containers. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-26feat(executor,llm): LocalRunner agent-channel via OpenAI tool-use (Phase 5)Claude
LocalRunner previously ignored the AgentChannel and produced a single fire-and-forget completion. It now declares the four agent back-channel tools (ask_user/report_summary/spawn_subtask/record_progress) as OpenAI function-calling definitions and runs a tool-use loop: each turn feeds tool results back as message history (re-feed) until the model stops calling tools, bounded by maxLocalToolTurns. ask_user converts a buffered question into a *BlockedError so the task blocks like the container runners. Adds tool-use support to the llm client (Tool/ToolCall/ToolFunction types, Tools on ChatRequest, ToolCalls on ChatResponse + wire request/response). The loop uses non-streaming Chat (tool_calls don't stream cleanly); assistant text is still written to stdout.log in the Claude stream-json envelope so summary/ changestats parsing is unchanged. Fully tested against a mock OpenAI endpoint + storeChannel: spawn/summary/ progress dispatch, ask_user blocking, token accumulation, and the llm tools round-trip. NOTE: local resume re-feeds conversation state (Decision #8) — not yet wired, so a blocked local task resumes fresh for now. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-25feat(executor): wire the agent MCP back-channel for gemini containers (Phase 4)Claude
ContainerRunner previously skipped the MCP back-channel for gemini agents, so they ran tool-less. It now mints a per-task token for gemini too and registers the agent MCP server in the gemini CLI's user settings (agentHome/.gemini/settings.json → $HOME/.gemini in-container) using the gemini-cli mcpServers/httpUrl schema with a bearer header. With MCP enabled, gemini also receives the planning preamble that points at the ask_user/ report_summary/spawn_subtask/record_progress tools. Config generation is unit-tested (TestWriteGeminiMCPSettings) and the write is in the run setup path. CAVEAT: whether the gemini CLI actually invokes these tools in non-interactive (-p) mode — and how it handles tool auto-approval — is NOT verified against a live gemini binary in this environment; this lands the plumbing for a follow-up spike. No regression risk for gemini runs: a config issue degrades to the prior tool-less behavior. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-25refactor(executor): delete the dead GeminiRunner stub and its sandbox ↵Claude
helpers (Phase 4) The production "gemini" agent type is served by ContainerRunner (Docker), not by the standalone subprocess GeminiRunner, which was never wired into serve.go and returned canned/simulated output — the dangerous landmine called out in CLAUDE.md's design debt. Removing it (gemini.go + tests). The subprocess sandbox helpers (setupSandbox/teardownSandbox/sandboxCloneSource in sandbox.go), preserved in Phase 2 only because GeminiRunner used them, are now orphaned — ContainerRunner has its own git/workspace handling — so they go too. No production behavior change: nothing instantiated GeminiRunner. Build green; executor tests unchanged except the pre-existing git commit-signing failures. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-24refactor(executor): delete unused ClaudeRunner; keep sandbox helpers (Phase 2)Claude
ClaudeRunner was never instantiated in production (serve.go wires only ContainerRunner) and was not wired for MCP. Remove it and its CLI/buildArgs/ execOnce tests. The sandbox helpers it defined (sandboxCloneSource, setupSandbox, teardownSandbox) are still used by GeminiRunner, so they move to sandbox.go; their tests (plus the shared initGitRepo helper and the tailFile test) move to sandbox_test.go. No production behavior change — this removes dead code and the last file-based agent wire that lived in ClaudeRunner. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-24feat(executor): MCP-oriented planning preamble, applied by ContainerRunner ↵Claude
(Phase 2) Rewrites the planning preamble to point the agent at the four MCP tools (ask_user/report_summary/spawn_subtask/record_progress) instead of the CLAUDOMATOR_QUESTION_FILE / CLAUDOMATOR_SUMMARY_FILE conventions: ask_user ends the turn, report_summary replaces the summary file, spawn_subtask replaces the POST-to-/api/tasks planning step. Git discipline is retained. ContainerRunner previously applied no preamble at all; it now prepends this preamble (via buildAgentInstructions) when the MCP back-channel is active and skip_planning is false, so the in-container agent is actually guided to use the tools. Gemini agents (no MCP) are unaffected. 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): per-task agent MCP server + token registry (Phase 2)Claude
Adds the agent-facing MCP transport foundation: a Registry that mints a per-task bearer token bound to a freshly built MCP server exposing the four agent tools (ask_user, report_summary, spawn_subtask, record_progress), and an HTTP handler (StreamableHTTP) that resolves the token to that server. The server never trusts an agent-supplied task ID — context comes from the token. The default storeChannel now buffers summary and question signals under a mutex (an MCP tool call lands on an HTTP-handler goroutine mid-run), exposing ReportedSummary/PendingQuestion. The pool flushes the buffered summary onto the execution after the run, replacing the runner's direct exec.Summary write and keeping the read race-free. ask_user follows the record-and-resume model: it buffers the question, returns ErrAgentBlocked, and the tool tells the agent to end its turn; the run blocks and resumes later via claude --resume (no live slot held). Tests cover registry lifecycle, in-memory tool dispatch, and HTTP end-to-end with bearer auth (valid token dispatches; invalid token rejected). Not yet wired into the runners or mounted on the API server — next increment. 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-20fix: use git tracking branch instead of hardcoded origin/master in ↵Peter Stone
teardownSandbox Replace explicit 'git pull --rebase origin master' with 'git pull --rebase' (no remote/branch), which uses the upstream configured by git clone. This correctly handles repos where the default branch is 'main', 'trunk', or any other name — not just 'master'. Also apply gitSafe flags (-c safe.directory=* -c commit.gpgsign=false etc) consistently to the status, push, pull, and log commands in teardownSandbox that were missing them. The variable-shadowing bug noted in CLAUDE.md was already resolved in the github/main merge (both setupSandbox calls used '=' not ':='); strike it from the docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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-12feat(executor): bring GeminiRunner to sandbox-flow parity with ClaudeClaude
All coding tasks now follow the same flow regardless of runner: when project_dir is set, the agent runs in a temp clone, not in the user's working tree. On success, edits are autocommitted and pushed back to origin/master and the sandbox is removed. On failure or BLOCKED, the sandbox is preserved and its path surfaces in the error / BlockedError so the user can inspect partial work or resume in place. Before this commit, GeminiRunner.Run set cmd.Dir to project_dir directly, so an agent run could leave half-done edits in the user's working tree with no rollback. ClaudeRunner has had the full sandbox flow for a while; this commit closes the gap. Reused the existing package-level helpers from claude.go verbatim: setupSandbox, teardownSandbox, sandboxCloneSource, gitSafe, plus the resume/stale-sandbox/blocked-error patterns. No new shared abstraction needed — same package. LocalRunner intentionally not changed. The OpenAI chat path has no tool use, so the agent can't edit files; sandbox would be theater. Tests (6 new): - Run_ProjectDir_RunsInSandbox: cwd captured by fake binary is a sandbox path, not project_dir. - Run_BlockedError_IncludesSandboxDir: when question.json appears, BlockedError.SandboxDir is set and the dir exists. - Run_ExecError_PreservesSandbox: failing exit wraps error with "(sandbox preserved at <path>)" and the path exists on disk. - Run_ResumeUsesStoredSandboxDir: ResumeSessionID + SandboxDir → runs in that dir without re-cloning. - Run_StaleSandboxDir_ClonesAfresh: resume pointing at missing dir falls back to a fresh clone from project_dir. - Run_NoProjectDir_SkipsSandbox: tasks without project_dir don't trigger sandbox setup. https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
2026-05-07test(executor): verify explicit Claude commits are captured in execRecordClaude
Adds TestTeardownSandbox_CapturesExplicitCommits to cover the case where the agent explicitly commits changes (no autocommit needed). Previously only the autocommit path was tested; this confirms teardownSandbox populates Commits for any commits ahead of origin. https://claude.ai/code/session_01G4dT9JBWFFb8xGcSHenzRS
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-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(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-28feat(executor): add LocalRunner and OpenAI-compat LLM clientClaude
Phase 1 of "local OSS models as agents" plan. Adds a third Runner backed by any OpenAI-compatible HTTP server (Ollama, vLLM, LM Studio, llama.cpp), and migrates the Gemini-CLI classifier to route through the same client when configured. Two-layer split: internal/llm.Client is the workhorse (HTTP, no Pool, no DB) used directly by the classifier and any future internal helper that needs cheap reasoning. internal/executor.LocalRunner is a thin adapter implementing Runner for user-facing tasks. This avoids Pool reentrancy/deadlock when sub-second internal calls fire from inside Pool.execute(). Highlights: - internal/retry: relocated runWithBackoff/IsRateLimitError/ParseRetryAfter into a shared package reused by executor and llm. - internal/llm: Chat (non-streaming) and ChatStream (SSE) over /chat/completions with optional bearer auth, json_object response format, retry on 429/503, Retry-After parsing. - internal/executor/LocalRunner: streams deltas into stdout.log in the same stream-json envelope ClaudeRunner emits, then writes one consolidated assistant block plus a result terminator so existing parsers (extractSummary, ParseChangestatFromOutput) work unchanged. - internal/executor/Classifier: gains optional LLM field; uses json_object response format (no markdown-fence cleanup needed). Falls back to Gemini-CLI subprocess when LLM is nil. - Pool.skipClassification: now skips only when the requested agent type is registered, so unknown types still reach the load balancer. - Storage: additive tokens_in/tokens_out ALTERs on executions; CLI runners record cost_usd as before, LocalRunner records 0 + tokens. - Config: [local_model] section (endpoint, model, timeout_seconds, default_temperature, api_key). Empty endpoint = no LocalRunner registered, classifier falls back to Gemini. Pre-existing test issues fixed in passing: - claude_test.go setupSandbox callsites updated to current signature. - gemini_test.go TestParseGeminiStream skipped (asserts unimplemented GeminiRunner stream-error parsing; tracked separately). Plan: docs/plans/local-oss-runner.md. 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: ShipStory — use context.Background() to avoid request context ↵Peter Stone
cancellation killing deploy
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-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: ensure story branch exists before cloning at task startPeter Stone
Add ensureStoryBranch() that runs git ls-remote to check, then clones into a temp dir to create and push the branch if missing. Called before the task's own clone so checkout is guaranteed to succeed. Removes the post-checkout fallback hack added in the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26fix: auto-create story branch if missing at clone timePeter Stone
If git checkout of the story branch fails (branch never pushed to bare repo), create it from HEAD and push to origin instead of hard-failing the task. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26fix: cancel waiting tasks when dep hits terminal failure (QUEUED→CANCELLED)Peter Stone
QUEUED→FAILED is not a valid state transition. When a dependency enters a terminal failure state, cancel the waiting task instead. 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-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: expose drained state in agent status API; fix AgentEvent JSON casingPeter Stone
AgentStatusInfo was missing drained field so UI couldn't show drain lock. AgentEvent had no JSON tags so ev.agent/event/timestamp were undefined in the stats timeline. UI now shows "Drain locked" card state with undrain CTA. 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>