0x2F is a local task runtime for coding agents. You give it a piece of engineering work; it routes that work to a coding harness on your machine, runs it in the background, and tracks it until you close it.
You work in terms of tasks, not agent sessions. A task is the object that persists: it has an id, a lifecycle, a history of runs, a result, and — when it cannot proceed without you — a request for your input. Which harness executed it is recorded underneath.
0x2F is hex for the ASCII / (47). The CLI is 2f. One dependency (ws), no build step, local-first.
00.2 INSTALL
Two prerequisites: Node.js ≥ 20, and at least one coding harness for the work to run through. claude (Claude Code), codex (Codex CLI), dsh (DeepSeek Harness) and gemini (Gemini CLI) are built in; anything else is added per project by manifest — see 02 EXECUTION.
npm install -g 0x2f the `2f` command lands on your PATH
npx --yes 0x2f ... works for a one-off, but install globally for regular use — 2f is a local app you invoke repeatedly, not a one-shot script. No accounts, no daemon.
00.3 QUICK START
2f init create .work/ in this repository 2f new "Investigate why retries restart the whole run" 2f list today's tasks 2f open 1 run detail, history, result 2f ui open the Web UI
2f init writes .work/ with project.md, rules.md, knowledge.md, decisions.md and providers/. Edit project.md and rules.md once — every prompt 0x2F builds is assembled from them, so the harness starts with your project's context instead of none. It also tells you which provider will run, or what to install if none is available.
If 2f new refuses with Execution provider "claude-code" is unavailable, no harness is on PATH — install one, or configure a provider (see 02 EXECUTION).
00.4 2f ui
2f ui behaves like launching a local application. If a 0x2F runtime is already healthy on 127.0.0.1:4242 it reuses that one and opens the browser; otherwise it starts the runtime in the background, waits until it is healthy, then opens the UI. Runtime output goes to .work/ui.log.
The Web UI is a control surface over tasks that are already running. Closing the browser does not stop anything. 2f ui <port> or 2f ui --port <n> serves another port; --no-browser starts it without opening a browser.
00.5 YOUR FIRST TASK
Write the task the way you would write it for a colleague — the outcome you want, not the steps to get there. 2f new returns immediately; the run is detached.
2f new "…"
↓
WORKING ──────→ READY 2f open <id> for the result
↓
NEEDS YOU ── 2f allow | 2f reject | 2f answer ──→ …
The task is the product object. Everything else — runs, events, results, provider sessions — hangs off it.
01.1 A TASK IS NOT A SESSION
A task persists. A provider session is metadata under one of its runs. This is the distinction the whole system is built on: you can run the same task again, through a different harness, and compare — because the task outlived the session that executed it.
Task "Investigate why retries restart the whole run"
├── run 01 · claude-code a fresh session
├── run 02 · deepseek-harness a fresh session
└── run 03 · claude-code your notes and prior results
in context
the task persists · a session is metadata under one run
01.2 LIFECYCLE
Five states, normalized by 0x2F regardless of what the provider underneath reports. A provider that emits nothing but a final result still moves its task through the same lifecycle.
WORKING ───────────────────────────→ READY │ │ ├── permission ──→ NEEDS YOU ──→ allow / reject │ │ ├── decision ─────────→│ ──────→ answer │ └───────────────────────────────→ FAILED any state ─────── close ──────────→ DONE
01.3 RUNS
Each execution is recorded as a run under the task, numbered from 01. Re-running is explicit and never automatic, and runs of one task are strictly sequential — a rerun refuses while the task is working.
2f rerun 1 --provider deepseek-harness run 02 under task #1 2f open 1 --run 2 one run's factual detail
Run history is for inspection. There are no scores, no winners and no recommendation — comparison is your judgment, not a ranking.
01.4 CONTINUING THROUGH ANOTHER AGENT
A new run is a continuation of the task, not a blank attempt. Its input (runs/<n>/prompt.md) is rebuilt from current task state — the original request plus your constraints and answers, and prior runs' results, verification and changed files — and handed to a fresh provider session. The task is persistent; provider sessions are disposable.
Add a constraint with 2f note <id> "<constraint>" (or 2f answer on a decision block); it becomes part of the next run's context with no manual copying. The original prompt.md is never overwritten.
01.5 EVENTS
Runs append normalized events to .work/tasks/<slug>/events.jsonl. The CLI and the Web UI subscribe to the same stream; neither reads provider processes directly. Providers that emit structured events produce a fine-grained stream; providers that do not produce a sparse one. 0x2F does not invent the difference away.
01.6 RESULTS & CLOSING
A finished run writes runs/<n>/result.md — prose written by the harness, not a diff or a score. 2f open <id> shows it.
2f close <id> removes a task from active attention — a finished run you have read, a failure you have understood, a stop you do not want to answer. It never resumes a provider and never starts an execution.
0x2F has three provider integration paths behind one contract. To 0x2F they are all just providers — the harness underneath differs, and the difference is declared, not hidden.
02.1 THE THREE PATHS
NATIVE a deep adapter for one harness's capabilities
claude-code · codex · deepseek-harness · gemini built in
claude-code: permissions → needs_you → same-session resume
ACP one generic provider, Agent Client Protocol v1 over stdio
any ACP-compatible agent — configured by manifest
COMMAND one generic provider for headless executables
any CLI that takes a prompt and prints a result — by manifest
2f providers lists every provider with its integration type and availability.
02.2 MANIFESTS
Everything that is not built in is added declaratively: drop one JSON manifest into .work/providers/ and it becomes a provider — no source changes.
{ "id": "cursor",
"displayName": "Cursor",
"transport": "acp",
"command": ["cursor", "--acp"] }
ACP manifests may set "permissions": interactive (default — a permission request pauses the run and asks you), deny, or approve (headless auto-resolution). Command manifests must pass the task through the {prompt} placeholder. Commands are spawned as argv arrays, never through a shell.
02.3 AVAILABILITY
Availability is a deterministic, cheap fact: whether the provider's executable can be resolved — a PATH lookup, never a spawn. 2f providers shows it; AUTO routing (see 03) uses it; nothing ever runs against a provider that is not available.
02.4 WHEN NONE IS AVAILABLE
2f init reports what is ready, or what to install. 2f new refuses before persisting anything — a run against an unavailable provider would be doomed, so no task is created:
Execution provider "claude-code" is unavailable on this machine. Expected executable: claude Install or configure Claude Code, then retry.
Install a harness, add a manifest (02.2), or point routing at what you have (03).
By default 2f new uses the configured routing default. When that default is auto, 0x2F picks a harness deterministically — it does not read the task text and never claims a provider is "best".
03.1 CONFIG
// .work/routing.json
{ "default": "auto", // "auto" or a provider id
"prefer": ["claude-code", "deepseek-harness"] }
03.2 THE DECISION
auto selects available providers first, then the prefer list in order, then registry order. The decision — provider, node, reason, considered — is persisted with the run and shown by 2f open and the Web UI.
Same state + same policy → same routing decision. No randomness, and no automatic failover: a routed run that fails is failed, not secretly retried elsewhere.
03.3 OVERRIDE
2f new "Audit the auth flow" --provider auto deterministic routing 2f new "Audit the auth flow" --provider claude-code explicit
0x2F interrupts you only when your input is genuinely required. Everything else it finishes on its own and tells you afterwards.
There are two reasons a task stops, and they are not interchangeable.
04.2 PERMISSION
A concrete operation needs authorization — an edit, a command. The request names the operation.
2f allow <id> 2f reject <id>
Where the provider supports it, answering continues the same run — nothing restarts. claude-code works this way natively.
04.3 DECISION
The harness cannot proceed without your judgment. It asks one concrete question.
2f answer <id> "<your answer>"
A decision is never allowed or rejected. Your answer is recorded with the task; whether execution can continue from it depends on the provider's resume capability — see 04.5.
04.3.1 THE DECISION PROTOCOL
A decision request is machine-read, not prose. A harness signals one by ending its run with exactly this block:
## Needs human decision REQUIRED: yes QUESTION: <the concrete question a human must answer>
Anything else — a bare heading, "None", "No decision required", or any amount of prose — is read as no decision. A finished run completes READY instead of interrupting you for work that did not need you. This is deliberate: arbitrary provider chatter must not be able to summon a human.
04.4 CLOSE
2f close <id> — or CLOSE in the Web UI — belongs to the 0x2F lifecycle, not to the provider, so it is available whatever the harness underneath can do: a stop you do not want to answer, a wrong request, a failure, a result you have read. It never resumes and never starts a run.
04.5 RESUME CAPABILITY
Providers differ, and 0x2F does not pretend otherwise. Answering always records your input against the task; continuing the same run is a provider capability.
PROVIDER CONTRACT
│
┌─────────────────┼─────────────────┐
NATIVE ACP COMMAND
adapter manifest manifest
rich broad minimal
events · resume structured start → result
permissions permissions* no interruption
* an ACP manifest sets "permissions": interactive (default) · deny · approve
The CLI and the Web are two surfaces over the same core. Both call the same shared actions and read the same normalized events — the browser never implements lifecycle or provider logic of its own.
05.1 THE LOCAL RUNTIME
2f ui behaves like opening a local application. If a 0x2F runtime is already healthy on 127.0.0.1:4242 (localhost only) it reuses it; otherwise it starts the runtime in the background, waits until it is healthy, and opens the UI. Runtime output lands in .work/ui.log. A second 2f ui probes the port and reuses the running runtime — another tab is fine.
Port: 2f ui <port> or 2f ui --port <n>. --no-browser starts it without opening a browser.
05.2 WHAT THE UI SHOWS
The browser is a control surface over tasks that are already running: see NEEDS YOU / WORKING / READY / FAILED, open a task and its run history, and ANSWER / ALLOW / REJECT / NOTE / SEND BACK / ACCEPT. The ledger renders the same normalized events the CLI reads. Closing the browser does not stop anything.
05.3 LOCAL ONLY
The API binds to 127.0.0.1, is token-authenticated (the shell sets an HttpOnly SameSite=Strict cookie), and refuses cross-site browser requests — 0x2F serves only its own loopback origin. Remote control is an outbound control layer, not remote execution (see 07 REMOTE).
Everything 0x2F knows about a project lives in .work/ inside that repository. Nothing is stored globally, and nothing leaves your machine unless you opt into remote pairing (see 07).
06.1 WHAT .WORK/ CONTAINS
06.2 WHAT STAYS LOCAL
Task prompts and results may quote repository code, so they are written owner-only (mode 0600). Nothing is sent anywhere unless you pair. Execution is local-only: the local API binds 127.0.0.1 and never listens on other interfaces.
06.3 REMOVING STATE
Delete .work/ to remove 0x2F's state from a project — your source files are never touched. Project .work/ also survives uninstalling the CLI (see 08).
Remote control lets a phone drive the 0x2F running on your Mac — the Mac keeps running the work; the phone is a compact control surface. v0.5 pairs over the local network: the phone and the Mac must be on the same Wi-Fi, and no relay, account, or configuration is needed.
07.1 WHAT IT DOES
2f pair
Run it in the project you want to control. It detects the Mac's private LAN address, makes the Mac's runtime its own relay for the pairing window, and prints a phone-openable URL plus a one-time pairing code (with a "same Wi-Fi required" note). Open the URL on your phone — same network — and type the code into the trusted page served by the Mac itself. The phone then speaks the same Web UI against the Mac: see NEEDS YOU / WORKING / READY / FAILED, open a task, and ANSWER / ALLOW / REJECT / NOTE / SEND BACK / ACCEPT.
Every command, ack, event and snapshot between the phone and the Mac is end-to-end encrypted (AES-256-GCM keyed by the pairing code): a passive observer on the Wi-Fi sees only ciphertext, and nothing on the network can forge a command.
07.2 OFFLINE
While the Mac is offline the phone shows its own last-known state with a MAC OFFLINE banner and disables actions. Commands are never queued, and a retried command reuses its requestId so it can never execute twice.
07.3 PREREQUISITES
The Mac and the phone on the same Wi-Fi / private LAN (macOS may ask to allow incoming connections for node — allow it). The LAN surface is bounded: it exists only while pairing is active, only on private-LAN addresses, and the normal local API stays loopback-only — 2f ui is never reachable from other devices.
Away from the LAN, the hosted path remains available: 2f pair --relay https://… --client https://… (or the 0X2F_RELAY_URL / 0X2F_CLIENT_ORIGIN env vars) against a relay you deploy. That hosted relay stays private infrastructure — its deployment details live in the repository (relay/README.md and docs/remote-control.md).
07.4 LIFECYCLE & REVOKE
Pairing tokens are one-time and expire in 10 minutes; phone sessions live 30 days. 2f pair --off revokes remote access — a real revocation, not just a local disconnect — and on the LAN also closes the pairing surface within a second. Re-pairing rotates the Mac's credential and the encryption key, so a stale phone session can never silently come back after the Mac reconnects.
0x2F updates and uninstalls through npm like any global CLI. Project state belongs to the project, not to the install.
08.1 UPDATE
npm update -g 0x2f or: npm install -g 0x2f@latest
08.2 UNINSTALL
npm uninstall -g 0x2f removes the CLI; project .work/ stays
Uninstalling never touches task state. To remove state from a project, delete .work/ (06.3).
The current, honest boundaries of the product.