summaryrefslogtreecommitdiff
path: root/internal
AgeCommit message (Collapse)Author
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
13 daysfeat(provider): add native Google Gemini API adapter (Phase 4)Claude Sonnet 5
Adds internal/provider/google, the second native cloud adapter (following internal/provider/anthropic's pattern) on top of Phase 1's provider-neutral tool-use loop, wired to a Docker-sandboxed NativeRunner under agent.type: "google" -- a separate execution path and budget bucket from the existing CLI-subprocess "gemini" ContainerRunner, which is untouched. Wire-format research (the highest-risk part of this adapter): Gemini's multi-turn function-calling shape was resolved by cross-referencing the REST API reference's own generateContent example against the go-genai SDK's struct tags on GitHub -- both agree on functionCall/functionResponse parts keyed by "name" (with an optional "id" for round-tripping ToolCall.ID), with the response fed back inside a "user"-role Content (Gemini has no tool/function role, mirroring Anthropic's lack of one). A separate fetched source (the function-calling guide page) was deliberately discarded as a reference for this shape -- it documents a different, newer "Interactions API" whose call_id/type:"function_result" structure doesn't fit the contents/parts/candidates shape used everywhere else. - internal/provider/google: request/response translation, systemInstruction handling, role mapping (assistant->model, tool-results->user role), per-model-prefix pricing table (2.5 Pro/Flash/Flash-Lite, 2.0, 1.5 tiers) - internal/retry: IsRateLimitError additively extended for RESOURCE_EXHAUSTED - internal/config: RunnersConfig.Google/GoogleEnabled() - internal/cli/serve.go, run.go: runners["google"] construction mirroring the Anthropic wiring exactly (Docker sandbox default) - docs/api-keys-setup.md: Google marked wired-up, budget-bucket/disable/ verify guidance added matching the Anthropic section go build/vet/test -race all pass. No live Gemini API key available in this environment; verified via fake-httptest-server adapter tests (plain text, tool-use round-trip, multi-turn tool-result, rate-limit error matching) plus a Pool/NativeRunner routing test. Live E2E is a follow-up once a key is configured, same as Phase 2's Anthropic adapter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
13 daysfeat(sandbox): add DockerSandbox + pre-tool-use guardrail hooks (Phase 3)Claude Sonnet 5
Gives native-API-driven agents (currently just the Phase 2 Anthropic adapter) real container isolation, decoupled from model invocation -- the model call happens in the Go process via provider.Provider, only tool execution happens in the container, unlike the CLI-subprocess ContainerRunner (left completely untouched) where the claude/gemini CLI runs inside the container. - internal/sandbox/dockersandbox.go: Sandbox via a long-lived `docker run -d ... sleep infinity` container (started once per execution, not per tool call), host-side git clone + bind-mount matching ContainerRunner's existing pattern, docker exec for read/write/bash/glob. Reuses images/agent-base (claudomator-agent:latest) rather than standing up a second image. WorkDir()/resume persists the host bind-mount directory (matching HostSandbox's contract); a resumed sandbox lazily starts a fresh container against that directory rather than trying to reattach to a possibly-gone one. - internal/sandbox/guard.go, hooks.go: Hook interface (CheckBash/CheckWrite), Guarded wrapper, DenylistBashHook (rm -rf /, force-push, curl|sh, sudo, chmod 777, dd if=) and ProtectedPathHook (.git/**, .env*, credentials/, .github/workflows/**). A rejection returns *RejectionError, which agentloop/tools.go now recognizes and feeds back to the model as a normal (non-fatal) tool-error result instead of aborting the run. - NativeRunner wraps whichever Sandbox it builds (Host or Docker) in Guarded{Hooks: DefaultHooks()} uniformly. The "anthropic" runner now uses DockerSandbox; "local" stays on HostSandbox by design (local models are the harness's more-trusted, lower-stakes-to-run tier). Docker is not installed in this dev environment (no docker/podman/containerd on PATH), so DockerSandbox's real container-lifecycle behavior is verified via mocked-command unit tests only -- go test -race ./... passes throughout, with the two real-daemon integration tests gated behind a dockerAvailable(t) check and skipping here. Live verification against an actual Docker host is a follow-up before relying on the "anthropic" agent type in production. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
13 daysfeat(provider): add native Anthropic Messages API adapter (Phase 2)Claude Sonnet 5
Adds internal/provider/anthropic, the first genuinely new provider.Provider implementation on top of Phase 1's provider-neutral tool-use loop, alongside (not replacing) the existing Docker/CLI-subprocess ContainerRunner path for the "claude" agent type: - internal/provider/anthropic: translates the neutral ChatRequest/ChatResponse shape to/from Anthropic's Messages API content-block format (system as a top-level field, tool_use/tool_result blocks, no "tool" role -- tool results become user-role messages), with a per-model-prefix pricing table for CostUSD - internal/retry: IsRateLimitError additively extended to recognize Anthropic's rate_limit_error/overloaded_error/529 shapes - internal/config: RunnersConfig.Anthropic/AnthropicEnabled() gate - internal/cli/serve.go, run.go: register runners["anthropic"] as a NativeRunner when [providers.anthropic].api_key is set and enabled -- tracked as a distinct executions.agent="anthropic" budget bucket, separate from the CLI-subprocess "claude" runner even though both bill the same Anthropic account go build/vet/test -race all pass. No live Anthropic API key is available in this environment, so verification is via fake-httptest-server adapter tests (12 cases, incl. multi-turn tool_result round-trip and rate-limit error matching) plus a Pool/NativeRunner routing test proving agent.type: "anthropic" actually reaches the new provider. Live end-to-end verification against the real API is a follow-up once a key is configured. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
13 daysrefactor(executor): extract provider-neutral tool-use loop (Phase 1)Claude Sonnet 5
Splits LocalRunner's OpenAI-specific agentic loop into reusable, provider- agnostic pieces so later phases can add native Anthropic/OpenAI/Google/Groq/ OpenRouter adapters without duplicating the control flow: - internal/provider: neutral Provider/ChatRequest/ChatResponse types, plus an openaicompat adapter wrapping the existing internal/llm.Client unchanged - internal/sandbox: Sandbox interface + HostSandbox (git clone/push/cleanup, read_file/write_file/run_bash/glob), lifted verbatim from local.go/localtools.go - internal/agentloop: the extracted tool-use loop (request/response/tool- dispatch/loop, ask_user blocking, stream-json envelope, summary fallback) - internal/agentchannel: AgentChannel/SubtaskSpec/BlockedError/ErrAgentBlocked moved out of internal/executor so agentloop can use them without an import cycle; internal/executor re-exports via type aliases, so no call site changes - internal/executor/nativerunner.go: NativeRunner replaces LocalRunner, wiring agentloop.Loop + openaicompat + HostSandbox together - config.Providers map[string]ProviderConfig added (unused until Phase 2+) Zero intended behavior change: go test -race ./... passes across all packages, and end-to-end stream-json/summary/changestats output was verified byte-compatible against a fake OpenAI-compatible server. Adds test coverage for sandbox tool-dispatch (git clone/push, read/write/bash/glob) that LocalRunner never had. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
2026-06-05feat(local-runner): add file I/O tools and git sandboxClaudomator Agent
Add read_file, write_file, run_bash, and glob tool definitions to agentToolDefs() in localtools.go, with dispatch in dispatchAgentTool() (signature now includes workDir). File and bash tools return an error when workDir is empty. LocalRunner.Run() clones t.Agent.ProjectDir into a temp dir when set, passes workDir to dispatchAgentTool, pushes commits back on success, preserves the sandbox in e.SandboxDir on BLOCKED, and cleans up on completion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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-04executor: deduplicate worker slots and fix git command executionPeter Stone
2026-06-04fix(validator): require repository_url for claude/gemini agent typesPeter Stone
ContainerRunner fails at runtime with a non-descriptive error if repository_url is missing. Catch it at task creation time instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04fix(executor): skip checker task for local-runner executionsClaude
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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-04test: fix createTask signature in testsPeter Stone
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: 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-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-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-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(config,cli): loopback-default binding with fail-loud on external (Phase 7)Claude
The server now defaults to binding 127.0.0.1 and refuses to bind a non-loopback address (:8484, 0.0.0.0, a LAN IP, …) unless external_bind_allowed = true is set in config — failing loud at startup instead of silently exposing itself. External access is expected to go through a reverse proxy (Tailscale / Cloudflare Access / Caddy), per the locked auth model. - config: ExternalBindAllowed flag + ValidateBindAddr/isLoopbackHost (tested across loopback, all-interfaces, LAN, and override cases). - cli: --addr default is now 127.0.0.1:8484; serve() validates before binding. Per-client bearer rotation stays deferred (single shared token for now). https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
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-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-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-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-24feat(storage): emit subtask_spawned events on the parent streamClaude
CreateTask is now transactional and, when a task has a parent_task_id, records a subtask_spawned event (actor agent) on the parent's stream so the parent's history shows the children it dispatched. Top-level task creation (no parent) emits nothing. This closes the one agent-originated signal that flows during a run today (subtasks POSTed to the API per the planning preamble) but previously left no trace in the event stream. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
2026-05-23feat(storage): dual-write question/summary/interaction flows to eventsClaude
The legacy task columns (question_json, summary, interactions_json) now also emit events in the same transaction as the column update: - UpdateTaskQuestion(non-empty) -> clarification_request (agent); the empty-string clear-on-answer emits nothing - UpdateTaskSummary -> summary (agent) - AppendTaskInteraction -> clarification_answer (user) This populates the event stream from the existing Q&A and summary paths without removing the old columns yet, setting up their eventual deletion in the cleanup phase. 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-22feat(event,storage): add events table substrate for OSS redesignClaude
New internal/event package defines the Event type with canonical Kind and Actor constants. New events table is appended via additive migration (id, task_id, seq, ts, kind, actor, payload_json) with seq as per-task monotonic, assigned by the storage layer inside a transaction. CreateEvent + ListEvents support the future migration of question_json, interactions_json, summary, and elaboration_input off the tasks table into a unified, replayable event stream. No existing code paths are modified yet; this is pure substrate for Phase 1. 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-20feat: add [runners] config section to toggle each runner on/offPeter Stone
Adds RunnersConfig{Claude,Gemini,Local,Container *bool} to Config, parsed from a [runners] TOML section. Each field uses the *bool pointer pattern (nil = enabled by default, false = disabled) so existing installs require no config changes. serve.go / run.go now gate each runner registration on the corresponding Enabled() method. The pool's runners map remains the authoritative routing table — absent entries are already skipped by pickAgent and fail-fast in getRunner, so no executor changes are needed. Example: [runners] gemini = false # disable cloud gemini runner local = true # explicit (same as default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20fix: auto-load default config file and relax repository_url requirementPeter Stone
root.go: PersistentPreRunE was only reading the config file when --config was explicitly passed. This left LocalModel.Endpoint empty so the local runner was never registered and the Classifier overrode agent.type:local. Now always tries ~/.claudomator/config.toml; silently ignores ENOENT. validator.go: repository_url was unconditionally required but is irrelevant for LocalRunner tasks (pure chat completions). The executor already handles an empty RepositoryURL via the Project registry lookup at dispatch time. 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>