Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

agent-arranger

An AI Runtime Host — every coding-agent execution is a first-class, identified, managed Runtime: RID-keyed events, lifecycle FSM, recoverable session, isolated worktree, HITL signals. Submit a DAG, get audited runs.

License: Apache-2.0 Go Latest release ADR Status Race tests

English · 中文 · Why · Getting started · Core model · Architecture · CLI · HTTP API · Web UI · Design principles · Releases


Why agent-arranger

A coding-agent CLI (opencode, claude code, codex, …) runs one conversation in one shell. Driving many runs across many tasks, surviving host restarts, gating risky tool calls behind human approval, and producing an audit trail — that is the host's job, not the agent's. Most setups bolt these on per-run with shell glue and a tail -f of stdout:

                ┌─ agent CLI run #1 ─ stdout · cwd · ad-hoc retry · lost on crash
   N runs   ─── ┼─ agent CLI run #2 ─ stdout · cwd · ad-hoc retry · lost on crash
                └─ agent CLI run #N ─ stdout · cwd · ad-hoc retry · lost on crash

    no identity · no FSM · no signal track · no replay · no recovery

agent-arranger collapses that into one process. Each run becomes a Runtime with a stable RID. Events are append-only and keyed by RID; state is a projection of those events in the same transaction. Worktrees are RID-rooted and recreatable. A lifecycle FSM gates transitions; a signal track delivers HITL approvals and operator pause/resume. Crash recovery reads the durable provider session handle and continues — same RID, same attempt.

                                  ┌─ FSM (lifecycle) ──── projection ──┐
   YAML plan (DAG) ─► submit ──►  ├─ event log (RID) ──── recover ─────┼──► coding agent
                                  ├─ signal dispatcher ── HITL/pause ──┤   (opencode today;
                                  └─ workspace (RID) ──── git worktree ┘    SBI for more)

    one process · one binary · loopback HTTP + HTMX UI + 19-verb CLI

The host carries no model knowledge. Providers (and their authentication, models, billing) live behind the SBI — today OpenCode Driver; tomorrow peer drivers (PI, claude code, …) behind the same contract. See ADR 0001.

Getting started

Install

git clone https://github.com/SeasonTemple/agent-arranger.git
cd agent-arranger
go build -o bin/arranger ./cmd/arranger

Requires Go ≥ 1.25 and a working opencode install (≥ 1.4.7) with at least one provider authenticated (opencode auth list).

Pin a release tag for reproducibility:

git clone https://github.com/SeasonTemple/agent-arranger.git && cd agent-arranger
git checkout v1.8.1   # latest
go build -o bin/arranger ./cmd/arranger

For humans — submit a plan

bin/arranger serve --port 7777 &                  # start the host
bin/arranger submit examples/chain.yaml           # 3-task linear DAG
bin/arranger list                                 # see plan + task status
bin/arranger logs <plan_id> --follow              # tail events

Open http://127.0.0.1:7777/plans for the Web UI: plan list, plan detail with DAG state, live SSE events, HITL approve/reject chip.

A minimal plan:

plan_id: chain-demo
name: "3-step chain"
max_concurrent_tasks: 4
tasks:
  - id: t1
    prompt: "Reply: HELLO"
    on_failure: continue
    config:
      model: zhipuai-coding-plan/glm-4.7
  - id: t2
    depends_on: [t1]
    prompt: "Saw ${tasks.t1.output}. Reply: WORLD"
    config:
      model: zhipuai-coding-plan/glm-4.7

Multi-dep tasks (depends_on: [t1, t2]) octopus-merge into a fan-in branch before the worktree is created; merge conflicts surface as workspace_error and skip downstream.

For AI agents — drive the host

Every verb the UI calls has a CLI/HTTP twin (agent-native parity). Machine drivers should prefer the HTTP API with X-Requested-With: arranger (CSRF) and --json on CLI verbs that support it.

bin/arranger runtime ls                                # list runtimes by RID
bin/arranger runtime inspect <rid>                     # full RID-keyed snapshot
bin/arranger runtime approve <rid>                     # HITL sig.approve
bin/arranger runtime reject  <rid> --reason "stop"     # HITL sig.reject
bin/arranger runtime suspend <rid>                     # operator pause (sig.suspend)
bin/arranger runtime resume  <rid>                     # operator resume (sig.resume)
bin/arranger chain --plan <plan_id> --from-task <task_id> "next goal"   # output-driven chain-append
bin/arranger artifacts --plan <plan_id> --task <task_id>                # enumerate worktree products

Core model

These six terms anchor everything else. The canonical, full-length glossary lives in CONTEXT.md; ambiguities and retired vocabulary (session as arranger execution, WAIT_HUMAN, etc.) are documented there too.

Term Meaning
Runtime An identified, managed execution — the first-class entity the host governs. Wave-1 every Runtime is task-bearing (carries task_id + attempt as attributes, never as identity).
RID The opaque, stable, sole identity of a Runtime (rt-<short-id>). Carried in every event's aggregate_id. Never derived from (task, attempt).
Task A node in a plan's DAG with a prompt and a retry budget. Holds an attempt counter; one Task produces one or more Runtimes over its lifetime.
Plan A submitted YAML DAG of Tasks. Keeps its original id; correlation_id binds a Runtime's events back to its Plan.
SBI Stable Driver Boundary Interface — the host-facing, provider-agnostic, transport-invariant contract every Runtime Driver implements. Today: in-process Go calls; wave-3f: IPC to arranger-runtimed, same contract shape.
FSM Wave-3 lifecycle state machine on runtimes.state (SPAWNING, WAIT_TOOL, HITL, SUSPENDED, CHECKPOINT, terminal). Projects down to wave-0 tasks.status (pending / running / completed / …) so the frozen replay oracle byte-equality holds. state ≠ status — see CONTEXT.md § Lifecycle model.

Relationships:

   Plan (DAG of Tasks)
      │ correlation_id
      ▼
   Task ─── attempt 0, 1, 2 …
              │            each attempt = a new Runtime (new RID)
              ▼
        Runtime (RID = identity)
              │
              ├─► events  (aggregate_id = RID, append-only)
              ├─► state   (projection upserted by PostInsertHook in same tx)
              ├─► worktree at .arranger/runtimes/<RID>/  (recreatable, never identity)
              └─► durable provider session handle (recover-load-bearing)

A retried Task is a different Runtime with the same task_id and a higher attempt. A chain is a new Task (new task_id, attempt 0, derived_from set) — not a retry. Workspace, pid, and pgid are recreatable attachments, not identity.

Architecture

The host evolved across five waves. Each landed as a stand-alone release; every wave passed go test -race ./... and kept the replay oracle (legacyFoldQuerySnapshot) byte-identical to the prior wave.

Wave Tag Theme
wave-1 v1.5.0-rt1 Runtime aggregate rekey to RID. (task_id, attempt) becomes attribute; identity = RID. runtimes projection supersedes the dead sessions table.
wave-2 v1.6.0-rt2 SBI hardening + recoverable session. Driver capabilities (Continuator, TokenAccountant, SessionIDLookup); recover-reattach / recover-resume / recover-fail.
wave-3 v1.7.0-rt3 Lifecycle FSM + HITL signal track + workspace re-root by RID. Conservative-extension contract vs the frozen oracle.
HITL UI v1.8.0-ui1 Operator surface — chip on plan detail, SSE-driven, approve/reject in one click. Inherits dispatcher's race posture.
v1.4a fix v1.8.1 Multipart POST /plans/new staging→worktree pipe wired end-to-end. .arranger/staging/<plan>/<task>/ + tasks.staging_path + executor copy-on-dispatch.

The five ADR 0001 addenda carry the design ratchet: runtime-supersedes-session (wave-1) → SBI transport-invariance (wave-2) → FSM conservative-extension (wave-3) → HITL signal dispatcher (wave-3b) → workspace re-root + retry-branch reuse (wave-3c) → HITL UI chip (HITL UI). See docs/adr/0001-relayer-from-orchestrator-to-runtime-host.md.

CLI

19 verbs. Every verb is exit-code-clean and a UI affordance shares its path (agent-native parity).

Verb Purpose
arranger serve Start the host on a loopback port (default 7777).
arranger submit <plan.yaml> Submit a DAG; returns plan_id.
arranger list Recent plans + statuses.
arranger get <plan_id> Plan + per-task snapshot.
arranger show <plan_id> [--events N] [--json] Snapshot + recent events.
arranger watch <plan_id> SSE stream; exits on plan terminal.
arranger logs <plan_id> [--follow] Tail events for one plan.
arranger events [--from N --limit M] Bulk event replay (NDJSON).
arranger cancel <plan_id> Cancel a running plan.
arranger pause <plan_id> Stop new dispatch (running tasks finish).
arranger resume <plan_id> Release a paused plan.
arranger doctor Environment + setup diagnostic.
arranger validate <plan.yaml> Pre-submit YAML check.
arranger output <plan_id> <task_id> Task output (text/plain).
arranger gc [--apply] [--aggressive] DB-driven reaper for terminal-plan worktrees + branches + staging dirs.
arranger chain --plan <id> --from-task <id> "goal" Output-driven chain-append (v1.4b).
arranger artifacts --plan <id> --task <id> Enumerate worktree products (v1.4b).
arranger runtime ls|inspect|approve|reject|suspend|resume RID-keyed runtime operations (wave-3b).
arranger smoke-test Single-task diagnostic (phase 1).

Environment

Var Effect
ARRANGER_UI_DEV=1 Read UI templates/static from disk; per-request re-parse.
ARRANGER_MAX_PLAN_BYTES=N Override default 1 MiB submit cap (positive integer, max 50 MiB).

arranger gc is standalone (no serve required) and reaps RID-keyed worktrees + branches + .arranger/staging/<plan>/<task>/ dirs owned by terminal plans. Default is dry-run; --apply removes; --aggressive also removes plan branches. Safe alongside arranger serve — terminal plans have no live workers and SQLite WAL handles concurrent reads.

HTTP API

Method Path Notes
POST /api/plans Submit plan (YAML body or multipart with files). 1 MiB default cap → 413.
GET /api/plans List recent plans.
GET /api/plans/{id} Plan + task snapshot.
DELETE /api/plans/{id} Cancel running plan.
GET /api/plans/{id}/events?from=N SSE stream (JSON data).
GET /api/plans/{id}/tasks/{task}/output Task output (text/plain).
POST /api/runtimes/{rid}/signal HITL + operator signals (sig.approve / sig.reject / sig.suspend / sig.resume).
GET /api/events/replay?from=N&limit=M Bulk event replay.
GET /api/health {ok, version, recovery?}; bypasses CSRF.

/api/* is wrapped by CSRFCheck — send X-Requested-With: arranger (CLI client does this automatically) or hit from Origin: http://127.0.0.1:7777. /ui/* + /static/* are wrapped by BrowserHostCheck (loopback Host allow-list — defeats DNS rebinding).

Web UI

Single-binary HTMX-driven UI on the same loopback port. No build pipeline; layout, partials, and HTMX 2.x are embedded via go:embed.

Path Description
GET / Redirects to /plans.
GET /plans Plan list + inline submit.
GET /plans/{id} Plan detail (DAG state, recent events, HITL chip).
GET /ui/plans/{id}/events UI-specific SSE: named events with rendered HTML fragments.
GET /static/* Embedded HTMX, SSE extension, CSS.

The HITL chip (v1.8.0-ui1) appears on plan detail when a Runtime enters HITL. One click → HTMX POST to /api/runtimes/{rid}/signal → SSE updates the chip; race posture inherits the dispatcher's (first-signal-wins, late losers are no-ops).

Design principles

  1. Identity = RID, nothing else. (task_id, attempt) is a wave-1 attribute, not identity. Process, pgid, worktree, session handle are all recreatable attachments. (ADR 0001 wave-1.)
  2. Event store is the source of truth. In-memory state is derivable; projections are upserted by the PostInsertHook in the same transaction as the event insert — never recompute-on-read, never a separate writer. (internal/orchestrator/replay.go.)
  3. Frozen replay oracle. legacyFoldQuerySnapshot's body is never edited. Every wave's correctness ratchet is byte-equality vs the prior wave on the oracle's accepted sequences. Conservative-extension applies to the wave-3 FSM. (ADR 0001 wave-3.)
  4. state ≠ status. Wave-3 FSM states live on runtimes.state and project down to the wave-0-bounded tasks.status snapshot the oracle gates. HITL → running, SUSPENDED → running, etc. (CONTEXT.md § Lifecycle model.)
  5. SBI is transport-invariant. Today in-process Go; wave-3f IPC. Same contract shape — no live pointers, channels, or closures cross the boundary. Deferring arranger-runtimed (the process) is non-breaking because the SBI (the contract) is wave-2 frozen. (ADR 0001 wave-2.)
  6. Single active arranger. Recovery handshake is a defensive guard against a stale reaper, not multi-arranger coordination. Leader election and shared-state arbitration are out of scope. (CONTEXT.md § Relationships.)
  7. Agent-native parity. Every UI affordance has a matching CLI/HTTP path. Machine drivers and humans drive the same surface.

Tests

go test -race ./...                  # unit + integration (Go)
bash scripts/chain_test.sh           # real-LLM 3-task chain
bash scripts/parallel_test.sh        # real-LLM forest + fan-out
bash scripts/cancel_test.sh          # mid-run cancel
bash scripts/stall_test.sh           # session_init_timeout pipeline
bash scripts/pause_resume_test.sh    # pause + resume lifecycle

Shell harnesses require an authenticated opencode install.

Repo layout

cmd/arranger/             CLI entry (19 verbs)
internal/
  api/                    HTTP server + CSRF + BrowserHostCheck + SSE
  cli/                    cobra commands
  ui/                     Web UI (templates + static + embed.FS + UI SSE + HITL chip)
  dag/                    graph + executor (pendingApproval + OnSignalResume)
  reconciler/             retry queue + stall tracker + DispatchSignals callback
  worker/                 pool + per-task subprocess + cancel-with-cause
  workspace/              RID-keyed git worktree manager + global git mutex
  orchestrator/           top-level coordinator + recover + FSM + PostInsertHook
  adapter/opencode/       OpenCode Driver (today's SBI implementor)
  capability/             SBI capabilities (Continuator / TokenAccountant / SessionIDLookup)
  store/                  SQLite event log + runtimes projection + workspace_path
  eventbus/               outbox pattern (control-plane + data-plane)
  plan/                   YAML parser + validator + template (`${tasks.X.output}`)
examples/                 demo plans
scripts/                  integration harnesses
docs/
  adr/0001-…              relayer ADR + five addenda
  brainstorms/            ce-brainstorm artifacts
  plans/                  ce-plan artifacts
  references/             architecture references
CHANGELOG.md              release log
CONTEXT.md                canonical glossary + relationships
CLAUDE.md                 contributor conventions

Releases

Tag Date Headline
v1.8.1 2026-05-21 v1.4a multipart staging→worktree pipe fix
v1.8.0-ui1 2026-05-21 HITL UI chip — w3b functional-layer closeout
v1.7.0-rt3 2026-05-20 wave-3 lifecycle FSM + HITL signal track + workspace re-root
v1.6.0-rt2 2026-05-19 wave-2 SBI hardening + recoverable-session
v1.5.0-rt1 2026-05-19 wave-1 runtime-host relayering (ADR 0001)

Full history in CHANGELOG.md.

Project status

Incubating. Pre-stable; the incubation posture is deliberate — no compat shims, no env-flag feature gates, no additive-only phasing. Correctness is non-negotiable (the replay oracle); API surface and CLI verbs may change without deprecation cycles until a 2.0 marker. See feedback_no_conservatism_incubation in the project memory and CLAUDE.md for contributor conventions.

License

Copyright 2026 Season Temple.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. A copy of the License is in LICENSE or at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages