<feed xmlns='http://www.w3.org/2005/Atom'>
<title>claudomator.git/internal/storage, branch fix/dockersandbox-gitpush-host-security</title>
<subtitle>claudomator — task automation server
</subtitle>
<id>https://git.terst.org/claudomator.git/atom?h=fix%2Fdockersandbox-gitpush-host-security</id>
<link rel='self' href='https://git.terst.org/claudomator.git/atom?h=fix%2Fdockersandbox-gitpush-host-security'/>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/'/>
<updated>2026-07-04T08:50:46+00:00</updated>
<entry>
<title>feat(web,api): add Budget/Roles dashboard -- final phase of the harness redesign (Phase 9b)</title>
<updated>2026-07-04T08:50:46+00:00</updated>
<author>
<name>Claude Sonnet 5</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-07-04T08:50:46+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=f8ae821240f33d615a9e91cdfeb6c026b7970782'/>
<id>urn:sha1:f8ae821240f33d615a9e91cdfeb6c026b7970782</id>
<content type='text'>
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|&lt;duration&gt;, 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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
</content>
</entry>
<entry>
<title>feat(story,scheduler): add epic-proposal tool + AskUser-timeout escalation (Phase 7c)</title>
<updated>2026-07-04T04:39:28+00:00</updated>
<author>
<name>Claude Sonnet 5</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-07-04T04:39:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=04b6e7eef473cb6eb69e345a4ea08243a8713077'/>
<id>urn:sha1:04b6e7eef473cb6eb69e345a4ea08243a8713077</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
</content>
</entry>
<entry>
<title>feat(story): add Epic/Story data model + REST CRUD (Phase 7a)</title>
<updated>2026-07-03T23:46:26+00:00</updated>
<author>
<name>Claude Sonnet 5</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-07-03T23:46:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=392c7c1ada310b2f928dca89b75ba628478f7694'/>
<id>urn:sha1:392c7c1ada310b2f928dca89b75ba628478f7694</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
</content>
</entry>
<entry>
<title>feat(executor): add DAG auto-cascade-fail + role-typed subtask spawning (Phase 6)</title>
<updated>2026-07-03T23:22:42+00:00</updated>
<author>
<name>Claude Sonnet 5</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-07-03T23:22:42+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=997cd8b56bc086a02b9c7c006dd62b07b9fcd2f3'/>
<id>urn:sha1:997cd8b56bc086a02b9c7c006dd62b07b9fcd2f3</id>
<content type='text'>
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-&gt;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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
</content>
</entry>
<entry>
<title>feat(role): add versioned role configs + escalation ladder + scheduler (Phase 5)</title>
<updated>2026-07-03T23:01:50+00:00</updated>
<author>
<name>Claude Sonnet 5</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-07-03T23:01:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=787b7fb1aed92c2b701724a7741576053b93cccb'/>
<id>urn:sha1:787b7fb1aed92c2b701724a7741576053b93cccb</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
</content>
</entry>
<entry>
<title>chore: remove stories/checker backend dead code</title>
<updated>2026-06-05T09:21:32+00:00</updated>
<author>
<name>Claudomator Agent</name>
<email>agent@claudomator</email>
</author>
<published>2026-06-05T09:21:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=0eb0b79396663c4901597becc6857a4cd795c58e'/>
<id>urn:sha1:0eb0b79396663c4901597becc6857a4cd795c58e</id>
<content type='text'>
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 -&gt; ensureBranch in ContainerRunner
- Fix git identity in bare-repo tests; fix initial-branch to use main

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(budget,storage,config): per-provider spend accounting substrate (Phase 6)</title>
<updated>2026-05-26T20:27:36+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-05-26T20:27:36+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=39f74dbbc7adca0e2058112408bb99694deefcaf'/>
<id>urn:sha1:39f74dbbc7adca0e2058112408bb99694deefcaf</id>
<content type='text'>
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
</content>
</entry>
<entry>
<title>feat(storage): emit subtask_spawned events on the parent stream</title>
<updated>2026-05-24T08:19:23+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-05-24T08:19:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=48a8e49cdda833f8af22e839ca3c102c40c97e17'/>
<id>urn:sha1:48a8e49cdda833f8af22e839ca3c102c40c97e17</id>
<content type='text'>
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
</content>
</entry>
<entry>
<title>feat(storage): dual-write question/summary/interaction flows to events</title>
<updated>2026-05-23T08:10:03+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-05-23T08:10:03+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=b72978f4454235a00a2e5c92b5332b985cc08fa9'/>
<id>urn:sha1:b72978f4454235a00a2e5c92b5332b985cc08fa9</id>
<content type='text'>
The legacy task columns (question_json, summary, interactions_json) now
also emit events in the same transaction as the column update:

- UpdateTaskQuestion(non-empty) -&gt; clarification_request (agent); the
  empty-string clear-on-answer emits nothing
- UpdateTaskSummary -&gt; summary (agent)
- AppendTaskInteraction -&gt; clarification_answer (user)

This populates the event stream from the existing Q&amp;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
</content>
</entry>
<entry>
<title>feat(storage): emit state_change events on task transitions</title>
<updated>2026-05-23T08:07:50+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-05-23T08:07:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/claudomator.git/commit/?id=dc97d7ae00bd25668ba495fba249ceade0c6b100'/>
<id>urn:sha1:dc97d7ae00bd25668ba495fba249ceade0c6b100</id>
<content type='text'>
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
</content>
</entry>
</feed>
