| Age | Commit message (Collapse) | Author |
|
ContainerRunner preserves a failed execution's workspace indefinitely for
debugging, with no expiry -- this accumulated 161 stale directories
(~17.6GB) and took the host to 100% disk full on 2026-07-10. Pool.RunWorkspaceCleanup
sweeps claudomator-workspace-* dirs older than 24h every hour, started from
serve.go, mirroring StoryOrchestrator.Run's ticker shape. Never removes a
directory still referenced as a currently-BLOCKED task's sandbox_dir,
regardless of age.
|
|
requiring external arbitrated review
|
|
to resolve through it
|
|
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.
|
|
(subtask-with-own-subtasks correctly blocks, cascades, and recovers)
|
|
(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
|
|
(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
|
|
(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
|
|
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
|
|
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>
|
|
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
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>
|
|
(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
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
cancellation killing deploy
|
|
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>
|
|
failure states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
consecutive failures
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
- 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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
- 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>
|
|
- 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>
|
|
|
|
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>
|
|
|
|
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>
|
|
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>
|
|
- 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>
|
|
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>
|
|
- 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>
|
|
TestPool_MaxPerAgent_BlocksSecondTask
|
|
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>
|
|
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>
|
|
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>
|