@tokenfactory/acc-runner
v0.47.14
Published
Agent Control Center CLI (`acc`). Sign in, read the task board, ask ACC, and run agents on this machine. `acc-runner` is retained as a deprecated alias.
Readme
@tokenfactory/acc-runner
The Agent Control Center CLI. Sign in as yourself, read the task board, ask ACC, and run agents on this machine.
Install
npm i -g @tokenfactory/acc-runner
# or
pnpm add -g @tokenfactory/acc-runnerThis installs two commands:
| Command | Status |
| ------------ | ------------------------------------------------------------------- |
| acc | The CLI. Use this. |
| acc-runner | Deprecated alias. Everything still works; prints a notice on stderr. |
Requires Node.js ≥ 20.6. Running agents on this machine additionally needs 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 doctor after install to verify everything resolves.
The two lanes
There are two different things a terminal can be, and they are separate credentials with separate lifecycles. A workstation can hold both at once.
You, signed in as yourself — a device-code login that leaves you with your own session (the same one your browser holds) and no runner registration:
acc login # device-code sign-in; stores a session in the OS keychain
acc whoami # who this terminal is signed in as
acc logout # revoke this terminal's session, server-side and locallyThe session shows up on Settings → Sessions as acc CLI · <hostname>, so you
can revoke it from the browser like any other.
This machine, enrolled as a runner — the fleet credential that lets ACC assign tasks here:
acc runner login # enrol this host, register a runner
acc runner logout # clear the local session, mark this runner offline
acc watch # long-running: receive assignments and execute themReading the board
Read-only, under your own token — RLS decides what you can see, exactly as in the UI.
acc task list # 25 most recently updated
acc task list --status in_progress --limit 50
acc task list --repo owner/name --json
acc task show T-1234Attaching a local repo (no GitHub App)
The other way to give ACC a repo. Instead of installing the GitHub App on it, point the CLI at a git working copy on this machine:
acc repo add-local ~/code/my-app # register it
acc repo add-local ~/code/my-app --project my-project # …and attach to a project
acc repo add-local ~/code/scratch-2 --name pricing-service
acc repo list-local # what this machine servesThe server records three things — a write grant for you (nobody else), a
binding from the repo to this machine, and optionally the project attach. It
does not record the path: that stays in this machine's config.json under
local-repos, and the runner resolves it at task time.
Consequences worth knowing before you run it:
- only your own runner can serve the repo, and only while it is online;
- the slug the server mints is
local.<fragment>/<name>— a reserved namespace derived from your user id, so it can never collide with (or grant access to) a real GitHub repo; - PRs need a push remote you set up yourself. Without one, tasks run and commit
in the working copy and the push step fails;
add-localwarns at registration.
Full walkthrough: docs/acc/LOCAL_REPO_CLI_SETUP.md.
Asking ACC
Hits the same /api/chat/stream endpoint the browser uses, so the same tools,
delegation ceiling and audit trail apply.
acc chat "which tasks are blocked?" # one shot
acc chat # interactive session on a TTY
git log --oneline -20 | acc chat # piped
acc chat --conversation <id> "and the risky ones?"If no provider key resolves for you, the turn is queued for a companion runner
to drain and acc chat says so — read the reply in the ACC conversation.
Migrating from acc-runner
The alias is a true alias: every command keeps the meaning it already had, so no script needs changing today.
| You typed | Keeps working | New name |
| ------------------------ | ------------- | --------------------- |
| acc-runner login | yes | acc runner login |
| acc-runner logout | yes | acc runner logout |
| acc-runner auth login | yes | acc login |
| acc-runner auth status | yes | acc whoami |
| acc-runner auth logout | yes | acc logout |
| acc-runner <anything> | yes | acc <anything> |
Note the one thing a blind rename would get wrong: acc login and
acc-runner login are different lanes. acc-runner login enrols a machine
(what it always did); acc login signs a person in. Use acc runner login when
you mean the machine.
Set ACC_SUPPRESS_ALIAS_NOTICE=1 to silence the deprecation line — worth doing
in launchd plists, docker entrypoints and CI where the output was quiet before.
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 | canonical production origin (https://acc.getset.live) | 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_REVIEWER_TIMEOUT_MS | 600000 (10 min) | Wall-clock time limit for a single reviewer child process. The review lane runs reviews one at a time, so a child that wedges would otherwise block every later queued review indefinitely while heartbeats stay online. On expiry the child is killed (SIGTERM, then SIGKILL) and the attempt settles as a budget-exempt infra reviewer_error — never a verdict, never a capacity pause — so the lane moves on and the server re-dispatches the review. Parsed with Number.parseInt; blank / negative / non-numeric falls back to the default; 0 disables the limit. |
| ACC_RUNNER_CONCURRENCY | 5 | Max tasks this runner processes simultaneously. Parsed with Number.parseInt, clamped to 1..50, falls back to 5 on non-numeric input (default raised from 2 in T-1784135000001, operator decision 2026-07-14). With the default ACC_REVIEW_RESERVED_SLOT=1 this leaves 4 parallel tasks + 1 review slot out of the box. 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 — subscription runners should set this back down to fit their plan's concurrent-session capacity. 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. |
| local-repos | { "local.<frag>/<name>": "/abs/path" } — written by acc repo add-local. No env form. Edit by hand only if you have moved a working copy. |
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. local-repos[task.repo]in~/.config/acc-runner/config.json, for alocal.…slug registered byacc repo add-local. A local repo has no remote to clone from, so this also suppresses the clone cache below.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.
Recovering stranded worktrees
If a task fails at pr_open, inspect the task event payload for
worktree_path. To recover completed work deliberately:
cd <worktree_path>
git status --short
git add -A
git commit -m "chore(acc): recover stranded runner work for <task_id>"
git push -u origin HEAD
gh pr create --base <integration_branch> --head "$(git branch --show-current)"Use the files named in the pr_open event as the audit list before committing.
If the branch has no commits and the worktree is dirty, the runner normally
auto-commits before opening the PR; this procedure is for manual recovery after
a hard process exit or an operator-aborted run.
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.
Named instances (keychain mode)
The ACC_RUNNER_ID recipe above suits env-token runners. A keychain-mode
runner cannot use it: the keychain holds one runner session per OS user, and
the server dedups runner identity on (user, machine) — a second login on
one Mac resolves back to the first runner's id. Name the instance instead:
--instance <name> (env ACC_RUNNER_INSTANCE, ^[a-z0-9][a-z0-9-]{0,31}$)
on login, watch, logout and doctor:
acc-runner login --instance dev2 # own keychain slot, own runner row
acc-runner watch --instance dev2 &The name folds into all four identity surfaces at once: the keychain account
(instance:dev2), the machine string sent to the server (…#dev2, so the
dedup mints each name its own stable runner id — re-login reuses it), the
cache root (…/acc-runner/runners/instance-dev2) and the singleton lock key.
logout --instance dev2 clears only that slot; a second watch under the
same name is still refused; doctor reports the instance, its slot and the
resolved id. Unnamed, every surface keeps its historic value byte-for-byte,
and ACC_RUNNER_ID / an explicit ACC_RUNNER_CACHE_DIR still win where set.
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.
node itself is the exception: the repo pins node 22 and launchd sources
neither ~/.zshrc nor ~/.zprofile, so the LaunchAgent bakes node@22's bin
dir onto PATH (and the ACC_* config vars into EnvironmentVariables)
instead of inheriting them. Install it with
packages/acc-runner/scripts/install-launchd.sh; see
docs/acc/RUNNER_LAUNCHD.md for the install, reboot-survival check and
rollback.
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 repo add-local <path> · acc repo list-local
Register a git working copy on this machine as a repo ACC can run tasks against,
with no GitHub App installed anywhere. --project <slug> also attaches it to a
project; --name <name> overrides the name derived from the directory.
list-local prints what this machine has registered and flags entries the server
no longer grants. Both run in the person lane (acc login), not the machine
lane — the grant being written is yours. See "Attaching a local repo" above.
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
Governed AUTO execution
AUTO terminal execution is a separate, fail-closed lane. Before a PTY starts, the runner consumes one signed 0393 lease and binds it to the exact user, organization, runner, repository, path set, command digest/class, destination set, and resource ceilings. A consumed, expired, revoked, altered, or profile-drifted lease cannot run again.
At execution time the runner also rejects shell composition, nested shells,
privilege tools, destructive commands, inline interpreters, path traversal,
and a cwd outside the lease paths. AUTO additionally requires both an active
runtime egress boundary (DestinationPolicy.activate) and a resource
controller; authorization without runtime enforcement is refused. Output,
input, wall time, CPU and memory remain bounded, runner-local secrets are
stream-redacted, quarantine kills the PTY, and the frozen completion receipt is
reported with a stable idempotency key. A failed audit delivery is queued for
retry. None of these rules change the manually operated bounded console.
- 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.
Governed staging migrations
acc watch can serve approved staging migration plans alongside its normal
task, review, chat, and console lanes. The lane is off by default. Configure a
runner-local database profile and opt in explicitly:
export ACC_DB_PROFILE_ACC_STAGING_URL='postgresql://...qfevnwuybyygexkcbdfy...'
export ACC_STAGING_MIGRATION_SERVICE=1
acc watchACC stores only the opaque profile name. The database URL remains on the runner and is redacted from errors. The runner advertises configured non-production profiles, polls a bounded oldest-first batch, claims each plan atomically, requires the exact repository commit and file hashes, pins the database project, uses a database advisory lock, and verifies the canonical migration ledger after every file. It stops on the first error and safely skips only exact ledger matches on a resumed plan. Cancellation and shutdown are observed between files; an in-flight SQL transaction is never interrupted halfway.
