Agent runs and skills registry¶
How a skill goes from a file on disk (SKILL.md) to a running,
budget-gated, resumable agent process, on either the Claude or Codex
backend. This page complements
dispatch, claim-lease and heartbeats, which
covers the claim mechanics generically; here the focus is backend
selection, the skills registry itself, and completion/failover.
flowchart TD
SYNC["cmd/skill-sync:\nSkills.Sync -> SyncScoped"] --> SCAN["ScanSkillDir:\nparse each SKILL.md"]
SCAN --> SKILLTBL[("skills table\n(name, scope)\norphan=true if missing")]
SYNC --> PROJSYNC["syncProjectAgents:\nper-project agents_path\nscope=project-slug"]
PROJSYNC --> SKILLTBL
REQ["POST /runs/agent\nor POST /skills/{name}/run"] --> GATE{"GateForProject:\nunknown/orphan/\nnot executable?"}
SKILLTBL --> GATE
GATE -->|"404/409/400"| REJECT["rejected"]
GATE -->|ok| CAP{"resolveClaudeCapBackend:\nClaude capped/stale?"}
CAP -->|yes| CODEXPRE["enqueue with\nBackend=codex"]
CAP -->|no| CLAUDEPRE["enqueue with\nBackend=claude (default)"]
CODEXPRE --> ENQ["AgentRunStore.Enqueue"]
CLAUDEPRE --> ENQ
ENQ --> QROW[("agent_runs\nstatus=queued")]
QROW --> CLAIM["Runner: RunClaimLanes\n-> claimLane -> ProcessAgentRun"]
CLAIM --> BUDGET{"Claude + not forced:\nBudgetGate.Allow?"}
BUDGET -->|"over budget"| RELEASE["Release: status=queued\ndaemon paused until reset"]
BUDGET -->|ok, or codex, or forced| EXEC["backend branch"]
EXEC -->|codex| CODEXRUN["buildCodexPrompt ->\ncodex exec --json\n(ChatGPT subscription billing)"]
EXEC -->|claude| CLAUDERUN["claude -p\n--session-id / --resume\nup to 3 attempts, backoff on 429"]
CODEXRUN --> MIDRUN["mid-run: skill may invoke\nthe Skill tool using\nstored SKILL.md content"]
CLAUDERUN --> MIDRUN
MIDRUN --> HBLOOP["heartbeat + milestone\nposts (see dispatch page)"]
HBLOOP --> RETURN["process returns"]
RETURN --> COMPLETE["POST /runs/agent/{id}/complete"]
COMPLETE --> XCHECK{"output_kind=tasks AND\nclaimed done but no PR?"}
XCHECK -->|yes| DOWNGRADE["downgrade to failed"]
XCHECK -->|no| STATUS{"final status"}
DOWNGRADE --> STATUS
STATUS -->|done| ARTIFACT[("agent_artifacts row\n(unless output_kind=tasks)")]
STATUS -->|"failed, backend=claude,\nisUsageCap"| CODEXFALLBACK["RequeueCodexFallbackOnce:\nauto-enqueue Codex retry"]
CODEXFALLBACK --> QROW
ARTIFACT --> USAGE["AgentRunUsage ->\nSubscriptionUsageStore.RollingUsage\n(7-day per-backend spend)"]
USAGE --> FAILOVER{"failoverDecision:\nover threshold?"}
FAILOVER -->|yes| CROSSFAIL["cross-backend failover\nfor future dispatch"]
QROW --> REAPSTALE["ReapStale:\nno heartbeat 5m"]
REAPSTALE -->|"attempts left"| REQUEUE["status=queued,\nsession_id preserved"]
REQUEUE -.->|"resume: --resume <session>"| CLAUDERUN
REAPSTALE -->|exhausted| FAILEXH["status=failed"]
Node annotations¶
Skills.Sync -> SyncScoped¶
cmd/skill-sync/main.go:main (host cron on pve-claude-code) calls
Skills.Sync -> internal/hub/skills.go:SyncScoped (L217), which runs
ScanSkillDir (L160) over ~/.claude/skills (global scope), parsing each
SKILL.md's frontmatter (parseSkillManifest, parseSkillExec,
parseSkillTelegram, L95-142) and upserting into the skills table keyed
(name, scope). Anything missing from a scan is marked orphan=true,
never deleted.
syncProjectAgents¶
cmd/skill-sync/main.go:84 repeats the same scan per project, reading each
project's agents_path from project_registry, stamped
scope=<project-slug> -- this is how project-scoped skills coexist with the
global catalog in the same table. Table: skills
(migrations/0040_skills.sql).
GateForProject¶
internal/hub/skills.go:GateForProject (L531) -> gateByScope (L547),
called from handleSkillRun (skills_run_api.go:22) before any run is
enqueued: 404 for an unknown skill, 409 for orphan/deprecated, 400 if not
marked executable.
resolveClaudeCapBackend¶
handleSkillRun checks the cached claudeCapSignal: if Claude is capped or
the signal is stale, the run is pre-enqueued with Backend: "codex" instead
of the default "claude" (ARGUS-448 pre-routing) -- a decision made before
the run ever reaches a claim loop.
AgentRunStore.Enqueue¶
internal/hub/agent_runs.go:Enqueue (L216). Validates skill/prompt/
output_kind/model/backend, inserts a queued row
(migrations/0042_agent_runs.sql plus many additive migrations for
backend/model/session/schedule/mcp_endpoint/progress_trail/context_bundle
fields).
Runner claims / BudgetGate¶
cmd/agent-runner/main.go:main -> RunClaimLanes
(agent_runner.go:262) -> claimLane (L291) -> ProcessAgentRun (L141).
A non-forced, non-codex run is checked against BudgetGate.Allow
(L172); over budget -> Release back to queued and the daemon pauses
(PauseUntil, L330). The cap signal itself is pushed by the runner's own
ticker, RunClaudeCapHeartbeat (L101) -> POST /runner/claude-cap ->
RunnerClaudeCapSignal.Update (runner_claude_cap.go:127,49), read back on
the hub side for both claim filtering and dispatch decisions.
Backend branch¶
internal/hub/agent_runner.go:DefaultResearchRunner (L656). codex:
builds a Codex-native prompt (codex_backend.go:buildCodexPrompt, L124),
execs codex exec --json (runCodexExec/execCodex, L179), parses the
JSONL stream (parseCodexResult, L62) -- no OPENAI_API_KEY is inherited,
so billing hits the ChatGPT subscription rather than the API. claude:
execs claude -p (runClaudeResearch, L902) with --session-id/
--resume for the ARGUS-308 resume breadcrumb, up to 3 attempts with
backoff on transient 429s, a typed *UsageCapError on a real usage cap
(isUsageCap, L997).
Mid-run skill invocation¶
The running headless claude -p process may itself invoke the "Skill" tool
as part of its allow-list (DefaultResearchTools, L650), using the stored
SKILL.md content (Skills.Prompt, skills.go:282) as its instructions --
this is how a nested skill call surfaces inside an already-running agent
run, not a separate agent_runs row.
POST /runs/agent/{id}/complete¶
agent_runs_api.go:handleAgentComplete (L362), the largest branch point:
validates status, cross-checks an hub-build "done" claim against
actual task progress (downgrading to failed if no PR was opened, L400),
stores the report as an agent_artifacts row (unless output_kind=tasks),
files audit findings for output_kind=tasks, and on failed +
backend=claude + isUsageCap calls RequeueCodexFallbackOnce
(agent_runs.go:326) to auto-enqueue a Codex retry of the same work.
AgentRunStore.Complete¶
agent_runs.go:368. running -> done|failed, records AgentRunUsage,
closes the ledger row, pushes a notification (gated by notifyPolicy), and
posts the outcome back to the requesting chat if there was one.
ReapStale / resume¶
agent_runs.go:482. Sweeps running rows with no heartbeat past the
staleness window, requeuing (attempts++, session_id preserved) or
failing at maxAttempts. A resumed attempt
(Attempts>0 && SessionID!="") sets spawn.ResumeSessionID so the next
execution runs claude -p --resume <session> instead of starting cold.
SubscriptionUsageStore.RollingUsage / failoverDecision¶
internal/hub/subscription_usage.go:RollingUsage (L109). A 7-day
per-backend spend aggregate (Codex token counts converted to a dollar
proxy) compared against subscription_thresholds to drive cross-backend
failover (failoverDecision, L175) -- separate from the live per-run
Claude-cap gate above; this one shapes future dispatch/enqueue decisions,
not the current run.
Key tables and endpoints¶
agent_runs(0042+ ~15 additive migrations),agent_artifacts(0043),skills(0040),agent_sessions(0077),channel_skills(0044).POST /runs/agent,POST /runs/agent/claim,POST /runs/agent/{id}/progress,POST /runs/agent/{id}/session,POST /runs/agent/{id}/release,POST /runs/agent/{id}/complete,GET /runs/agent/forced,GET /runs/agent/{id}.json,POST /skills/{name}/run,POST /runner/claude-cap,GET /runner/claude-cap,POST /agents/heartbeat,GET /agents.json.
Related pages¶
- Dispatch, claim-lease and heartbeats -- the generic claim/heartbeat/reap mechanics this page assumes.
- Task lifecycle -- what an
hub-buildrun actually changes on the board.