@tokenfactory/acc-runner
v0.37.1
Published
Agent Control Center local runner. Spawns Claude Code sessions assigned via ACC.
Readme
@tokenfactory/acc-runner
CLI runner that polls Agent Control Center for queued tasks and executes them via Claude Code.
Install
npm i -g @tokenfactory/acc-runner
# or
pnpm add -g @tokenfactory/acc-runnerRequires Node.js ≥ 20.6, plus these binaries on PATH:
claude— Claude Code CLI, signed ingh— GitHub CLI, signed in (gh auth login)git— withuser.emailconfigured- A local clone of the target repo (the
repoPath, see below)
Run acc-runner doctor after install to verify everything resolves.
Configuration
The runner resolves its settings from three layers, highest precedence first:
- Environment variables (
ACC_*) ~/.config/acc-runner/config.json(or$XDG_CONFIG_HOME/acc-runner/config.json)- Built-in defaults — production endpoints baked into the CLI so a
fresh
npm i -gworks out of the box
Production defaults are baked in; you only need to set anything if you want to point the runner at a preview deploy, change the repo clone location, or declare capabilities.
Environment variables
| Variable | Default | Notes |
| ------------------------- | ---------------------------------------- | --------------------------------------------------------------------- |
| ACC_PUBLIC_URL | production Vercel URL | Used for cost-pricing fetches and OAuth callback links. |
| ACC_SUPABASE_URL | production project URL | Supabase project the runner talks to. |
| ACC_SUPABASE_ANON_KEY | production anon key | Public; RLS does the gating. |
| ACC_REPO_PATH | ~/work/TechArch | Local clone of the target repo. See "Repo path resolution" below. |
| ACC_TARGET_REPO | tokenfactory-pvt-ltd/TechArch | Slug fallback. A task's own repo wins when present. |
| ACC_INTEGRATION_BRANCH | acc/integration | Base branch for new PRs. A task's integration_branch wins. |
| ACC_RUNNER_CAPABILITIES | (unset) | Comma-separated tags, e.g. code,docs. See "Capabilities" below. |
| ACC_RUNNER_PROFILE | (unset) | Profile slug (developer, designer, tester, manager). |
| ACC_RUNNER_TOKEN_MODE | keychain | env sources credentials from env vars instead of the OS keychain. See "Ephemeral runners" below. |
| ACC_RUNNER_ACCESS_TOKEN | (unset) | Required when ACC_RUNNER_TOKEN_MODE=env. The Supabase JWT minted by the device-code exchange or autoscale provider. |
| ACC_RUNNER_REFRESH_TOKEN| (unset) | Optional. Enables in-process token rotation in env mode; rotated successors stay in memory only. |
| ACC_RUNNER_ID | minted acc-eph-<hex8> | Optional pre-assigned runner id (env mode). The acc-eph- prefix marks the ephemeral fleet — don't reuse it for desktop runners. Also keys the singleton lock and (when no ACC_RUNNER_CACHE_DIR) namespaces the cache dir, so distinct ids coexist on one host. |
| ACC_RUNNER_CACHE_DIR | ~/.cache/acc-runner (or …/runners/<ACC_RUNNER_ID>) | Cache root for locks, worktrees, quarantine, and the singleton lock. When unset and ACC_RUNNER_ID is set, defaults to a per-identity subdir. Set explicitly to override. |
| ACC_RUNNER_CONCURRENCY | 2 | Max tasks this runner processes simultaneously. Parsed with Number.parseInt, clamped to 1..50, falls back to 2 on non-numeric input. Each slot is a live Claude session plus a git worktree, so high values are only viable on API-key runners on high-resource hosts. On a Claude subscription they will 429. The serial integration-merge queue still caps end-to-end throughput, so real-world gains plateau around ~2–2.5× regardless of how high this is set. |
~/.config/acc-runner/config.json
For settings you don't want to repeat in your shell profile (especially
when acc-runner watch runs under launchd and inherits a stripped
environment), drop a JSON file at
~/.config/acc-runner/config.json. All fields are optional.
{
"repo-path": "/Users/you/code/TechArch",
"capabilities": ["code", "migration"],
"runner-profile": "developer"
}| Field | Notes |
| ---------------- | -------------------------------------------------------------------------------------- |
| repo-path | Local clone path. Overrides the ~/work/TechArch default; ACC_REPO_PATH overrides this. |
| capabilities | Array of capability tags. Overridden by ACC_RUNNER_CAPABILITIES when set. |
| runner-profile | Profile slug; overridden by ACC_RUNNER_PROFILE when set. |
The file is read at process startup. Missing file or malformed JSON falls back to the next layer silently — the runner never crashes on a config-file read error.
Repo path resolution
acc-runner picks the local repo clone in this order — the first
non-empty value wins:
- The task row's
repo_path_hint(set by the operator when assigning the task) — lets a single runner span repos. ACC_REPO_PATHenvironment variable.repo-pathfield in~/.config/acc-runner/config.json.~/work/TechArch— the built-in default.
Until v0.8.1, only steps 1, 2, and 4 were consulted: a repo-path in
config.json was silently ignored, and a runner launched by launchd
(which strips most environment variables) would default to
~/work/TechArch and fail every task pickup with spawn git ENOENT.
Set either ACC_REPO_PATH or repo-path in config.json and verify
with acc-runner doctor.
Capabilities
Tags declare what kinds of tasks this runner is willing to pick up. The
server's task-router only dispatches a task to a runner whose
capability set covers the task's required_capabilities. The canonical
vocabulary is:
code, design, test, docs, migration, review, orchestrationAn empty list (the default) matches any task with an empty
required_capabilities — that's backward-compatible with pre-v0.6.4
behavior. Resolution order, highest first:
ACC_RUNNER_CAPABILITIES(comma-separated)capabilitiesfield in config.json- The active runner profile's
capabilities []
Runner profiles
A runner profile bundles capabilities and model defaults under a single
name. Profiles ship inside the package at
packages/acc-runner/runner-profiles/<name>.json. Available profiles:
developer— code + migration work (default fleet member)designer— design worktester— test authoring + reviewmanager— orchestration
Select a profile via ACC_RUNNER_PROFILE=designer or
"runner-profile": "designer" in config.json. Unknown profile names
abort CLI startup with a clear error rather than failing at task
pickup. Env vars and config.json capabilities still override the
profile's defaults when both are set.
Ephemeral runners (env-token mode)
Desktop runners read their session from the OS keychain (the default,
ACC_RUNNER_TOKEN_MODE=keychain). Containerized / autoscaled runners have
no keychain, so set ACC_RUNNER_TOKEN_MODE=env and source the session from
environment variables instead:
export ACC_RUNNER_TOKEN_MODE=env
export ACC_RUNNER_ACCESS_TOKEN=<jwt-from-device-code-exchange>
export ACC_RUNNER_REFRESH_TOKEN=<refresh-token> # optional; enables rotation
export ACC_RUNNER_ID=acc-eph-1a2b3c4d # optional; minted if unset
acc-runner doctor # validates the env-token config (see below)
acc-runner watchACC_RUNNER_ACCESS_TOKENis required.user_id/email/ expiry are decoded from the JWT — no extra env needed. A missing or undecodable token failsdoctorand abortswatchat startup, not on the first RPC.ACC_RUNNER_REFRESH_TOKENis optional. With it, the runner rotates the access token in process memory (never written to disk). Without it, the runner simply exits when the access token expires — fine for an idle-TTL-bounded ephemeral runner. Provision a fresh access/refresh pair per container: replaying a previously-rotated refresh token trips the server's reuse detection and revokes the chain.ACC_RUNNER_IDlets the orchestrator pre-assign a stable id. When unset, the runner mints oneacc-eph-<hex8>per process; theacc-eph-prefix is the autoscale sweep's ephemeral-fleet discriminator.
acc-runner doctor adds an env-token config check in this mode: it
confirms the access token is present and decodable, then reports the
resolved runner id (and whether it came from ACC_RUNNER_ID or was
minted), whether refresh is wired, and the access-token expiry. The check
is a silent no-op for keychain runners.
Running multiple runners on one host
Each runner keeps its locks, per-task worktrees, quarantine sentinel, and
single-instance lock under a cache root that defaults to
~/.cache/acc-runner. As of v1.02-A, giving each co-located runner a
distinct ACC_RUNNER_ID is enough to run them concurrently on one host:
ACC_RUNNER_ID=acc-eph-aaaa0001 acc-runner watch &
ACC_RUNNER_ID=acc-eph-bbbb0002 acc-runner watch &The single-instance lock is keyed by (repo path + runner id), so two
distinct ids serving the same repo each acquire their own lock and coexist;
only a true duplicate — same repo, same identity — is refused. With no
explicit ACC_RUNNER_CACHE_DIR, each id also gets its own cache subdir
(~/.cache/acc-runner/runners/<id>), so their locks, worktrees, and
quarantine state never collide. You can still pin an explicit
ACC_RUNNER_CACHE_DIR per runner if you want the cache somewhere specific:
ACC_RUNNER_ID=acc-eph-aaaa0001 ACC_RUNNER_CACHE_DIR=/var/run/acc/a acc-runner watch &
ACC_RUNNER_ID=acc-eph-bbbb0002 ACC_RUNNER_CACHE_DIR=/var/run/acc/b acc-runner watch &Two runners with the same identity (or both with no ACC_RUNNER_ID)
serving the same repo are still caught by the singleton lock. acc-runner
doctor prints the resolved token mode and cache dir in its Runtime
block, and names the holding identity if a duplicate is detected.
PATH under launchd
launchd resets PATH to /usr/bin:/bin:/usr/sbin:/sbin, which omits
the Homebrew bin dirs where git and gh typically live. The runner
prepends /opt/homebrew/{bin,sbin} and /usr/local/{bin,sbin} to its
own PATH before spawning these binaries — no launchd plist edits
required. If acc-runner doctor reports git or gh as missing under
launchd, install them via Homebrew and re-run.
Quickstart
acc-runner login # device-code OAuth in your browser
acc-runner doctor # verify config + binaries are all green
acc-runner watch # long-running; receives + executes tasksThen, in ACC, assign a task to this machine and click Start. The runner will:
- Transition the task to
running. git fetch && git checkout -B acc/T-XX-<slug>in the resolvedrepoPath.- Pipe the rendered prompt into
claude --print --dangerously-skip-permissions. - Stream stdout/stderr to
acc.task_events(visible in the History tab in ACC). git pushand open a PR titled[T-XX] <task title>viagh.
Commands
acc-runner login
Device-code OAuth flow. Stores a Supabase JWT in your OS keychain
(acc-runner service) and registers this machine in acc.runners with
its capability tags.
acc-runner logout
Clears the keychain entry and marks this runner offline.
acc-runner watch
Subscribes to a Realtime broadcast channel scoped to your runner. Sends
a heartbeat every 4s. Cancellation is honored via task_cancelled
broadcasts (sends SIGTERM to the in-flight claude child).
acc-runner doctor
Runs every environment check (binaries, keychain access, env-token config,
Supabase reachability, repo path, orphan worktrees) and prints a green/red
list, preceded by the resolved Config and Runtime (token mode +
cache dir) blocks. Exits 0 if all pass, 1 otherwise. A single check that
throws is reported as one [FAIL] row rather than crashing the run.
Suitable for CI and for diagnosing a silent failure mode after install.
acc-runner version
Prints the CLI version and the protocol version it speaks. The server
exposes a minimum-protocol-version pin at /api/runner/min-version;
the CLI refuses to start if it's behind.
Troubleshooting
spawn git ENOENT on every task pickup — repoPath resolves to a
directory that doesn't exist. Set ACC_REPO_PATH or repo-path in
config.json to the absolute path of your local clone, then re-run
acc-runner doctor.
**✗ Not signed in. Run \acc-runner login` first.** — keychain entry
was cleared or never created. Run acc-runner login`.
✗ keychain read/write (FAIL) — on Linux, install libsecret. On
macOS, ensure your login keychain is unlocked. On Windows, run from a
session with credential manager access.
✗ register_runner failed — the migration shipping
acc.register_runner may not be applied to the deploy you're pointed
at. Coordinate with the operator who applies migrations.
Task transitions to failed immediately — open the History tab in
ACC. Common causes: git fetch permission denied, claude --print
returned non-zero, or the rendered prompt lacked acceptance criteria.
Runner version rejected (426 Upgrade Required) — the server's
MIN_VERSION was bumped past your installed CLI. Run
npm i -g @tokenfactory/acc-runner@latest.
Runner quarantined after a single flaky run (v0.53+) — a lone
instant-empty claude exit no longer benches the machine: the runner logs
an env_broken_blip event and retries the spawn once. Only two
consecutive instant-empty exits — or a definitive failure (missing dyld
library, OOM, exit 127) — quarantine the runner. acc-runner doctor and
acc-runner quarantine status print the cause, the consecutive count, and
the acc-runner quarantine clear command to resume.
GitHub rate-limit awareness (v0.53)
src/github-client.ts is a budget-aware GitHub REST wrapper (ported from
the server's T-52-4 client): per-resource quota tracking, one jittered
backoff retry on a 429 / secondary-limit 403, and a per-task-execution soft
call budget that fails clean as a retryable infra error (never quarantines).
Scope: the runner currently performs all of its GitHub work through
the gh CLI — gh pr create/list (src/gh.ts), gh api conflict
resolution (src/runtime/conflict-resolver.ts), gh pr view head-ref
lookup (src/runtime/rework.ts), and gh pr view/gh pr diff for reviews
(src/runtime/reviewer.ts). Those are separate processes with their own
rate-limit handling and are not routed through github-client.ts — they
remain uncovered by its budget/backoff. The client is the sanctioned path
for any future direct REST call (fetch against api.github.com) the
runner adds.
Conflict-resolution integrity guardrail (v0.57, T-57-2)
When a Claude/worktree task whose job is to resolve a merge conflict
finishes, the runner does not trust the agent's self-reported gate results.
Before the resolution is pushed, src/runtime/resolution-integrity.ts
re-verifies it from the runner's own vantage point. The invariant —
a conflict resolution is shippable only if all three hold:
- Zero conflict markers remain in any tracked file
(
<<<<<<</=======/>>>>>>>). pnpm typecheckpasses (or the task'stypecheck_cmd).- No version regression: if a
package.json"version"changed in the resolution it must move forward (semver-greater than the base ref, never backward) andCHANGELOG.mdmust carry a matching entry for the new version.
On ANY failure the resolution is rejected: the runner does not push and
does not mark the task resolved. The task transitions to failed (so it is
reworked, not merged broken), an append_task_event of kind
resolution_integrity_failed records the offending assertion(s), and a
conflict.integrity_rejected activity event surfaces it to the operator. A
guardrail that itself errors fails closed (rejects). The check is a no-op
for every non-conflict task — it is gated by isConflictResolutionTask.
This exists because v0.55–v0.56 agent resolutions repeatedly shipped broken
merges that were still marked "resolved": stale-variable splices
(task-runner.ts), duplicate declarations (health.ts), and a silently dropped
version bump (#496 reverted package.json 0.14.0 → 0.13.0).
Security notes
- The CLI never reads or writes your git, gh, or Anthropic credentials. It calls binaries that are already authenticated on your machine.
- The Supabase JWT is short-lived and stored in your OS keychain via
keytar, never on disk in plaintext. - Heartbeat interval is 4s — do not tighten this. Tighter intervals consume the Realtime budget.
Runner sandbox
acc-runner spawns claude --print --dangerously-skip-permissions so
non-interactive task execution doesn't hang on per-edit approval
prompts. The permission boundary is enforced by the surrounding
context, not by claude itself:
- cwd boundary: the spawn is bounded to the resolved
repoPath. Claude cannot read or edit files outside this repo. - allowed/forbidden globs: the task's
acc.tasks.allowedandacc.tasks.forbiddenarrays are rendered into the prompt; claude is instructed which paths it may or may not touch. - operator-controlled install: an operator deliberately runs
acc-runner watchon their own laptop. The runner is not a daemon and exits when the operator stops it.
License
Proprietary. Not for redistribution.
