# Technical Documentation: coleam00/Archon

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/coleam00/Archon)

### Overview (`README.md`)

<p align="center">
  <img src="assets/logo.png" alt="Archon" width="160" />
</p>

<h1 align="center">Archon</h1>

<p align="center">
  The simplest way to build the software that builds your software.<br/>
  YAML workflows that can do anything.
</p>

<p align="center">
  <a href="https://trendshift.io/repositories/13964" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13964" alt="coleam00%2FArchon | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>

<p align="center">
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /></a>
  <a href="https://github.com/coleam00/Archon/actions/workflows/test.yml"><img src="https://github.com/coleam00/Archon/actions/workflows/test.yml/badge.svg" alt="CI" /></a>
  <a href="https://archon.diy/docs/"><img src="https://img.shields.io/badge/docs-archon.diy-blue" alt="Docs" /></a>
</p>

---

Archon is a factory builder. You describe how work should get done as YAML workflows, and Archon runs them with AI agents, scripts and your decisions in the places that need them. Software is the first factory: the bundled SDLC pack takes an issue to a merged pull request. The same engine runs any process you can describe.

Take yourself out of the loop and sit on top of it. Runs do the work, queues keep it moving, and a run comes to you only when it needs a decision you own.

## Why Archon

Ask a coding agent to fix a bug and the result depends on the run. It may skip the plan, forget the tests or ignore your PR template. You end up supervising every step.

Archon turns your process into a workflow you own. The workflow fixes the steps, the checks and the gates. The agent supplies the judgment inside each step. That gives you:

- **The same process every time.** Plan, implement, review, validate, open the PR, in the order you wrote down.
- **Human on the loop, not in it.** Queues take work forward on their own. When a run reaches a decision it can't make, it pauses as one attention item: what it is, why it matters, a recommendation and the choices. You answer and it carries on.
- **Governance built in.** Approval gates, a full event log and transcript for every run, and the cost of every run, all stored in your own database.
- **Your agents, your subscriptions.** Archon is not another coding agent. It runs Claude Code, Codex and Pi through their own CLIs and SDKs, with the accounts and project guidance you already have.
- **Self-hosted.** One install on your machine or server. SQLite by default, PostgreSQL if you want it.

## What it looks like

A workflow is a graph of nodes. Agents judge, code computes, and you decide where it matters:

```yaml
# .archon/workflows/build-feature.yaml
name: build-feature
description: Plan, implement until the tests pass, ask for approval, open the PR.
nodes:
  - id: plan
    prompt: "Read the request and the code. Write an implementation plan."

  - id: implement
    depends_on: [plan]
    loop:
      prompt: "Implement the next step of the plan."
      until_bash: "bun run test"          # your test command; code decides when it is done
      max_iterations: 10
      fresh_context: true

  - id: approve
    depends_on: [implement]
    wait:                                 # the run pauses and asks you
      attention: "Review the change. Approve, or reject to stop."
      form: approval

  - id: stop
    depends_on: [approve]
    when: "$approve.output.decision == 'reject'"
    cancel: "Rejected at review."

  - id: pr
    depends_on: [approve]
    when: "$approve.output.decision == 'approve'"
    prompt: "Push the branch and open a pull request."
```

```mermaid
flowchart LR
  plan["plan<br/>(agent)"] --> implement["implement<br/>(agent, loops until the tests pass)"]
  implement --> approve{"approve<br/>(waits for you)"}
  approve -- reject --> stop["stop"]
  approve -- approve --> pr["pr<br/>(agent)"]
```

Save it in your repository's `.archon/workflows/`, swap `bun run test` for your own test command, and run it:

```bash
archon workflow run build-feature "Add dark mode to the settings page"
```

Repository runs get their own git worktree by default, so several can work on one repository at once.

## A software factory: issue in, merged PR out

The bundled `sdlc` pack is a working factory for a repository:

1. **You open an issue.** `archon-intake` checks it against the current code and decides what it needs: a plan, an investigation, straight to delivery, or a maintainer's look.
2. **The build queue picks it up.** `archon-build-queue` claims the next ready issue, writes the brief and runs `archon-ship`: plan, implement, review through specialist lenses, fix the findings, wait for CI. It stops at a pull request whose review says Ready.
3. **The merge queue lands it.** `archon-merge-queue` merges the clear ready pull requests that merge cleanly, through a gate that checks the review verdict and CI at the head commit and that the base branch is not red. The queue judges open review threads itself and sends a pull request that conflicts back to the build queue.
4. **You answer what's unclear.** A scope question, a schema change or a stuck round becomes one attention item with a recommendation. Your answer is written into the issue as the new scope.
5. **Nothing found along the way is lost.** `archon-discoveries` reads what runs noticed outside their task and files it, tracks it on an existing issue, or drops it.

Archon builds Archon: of the 524 pull requests merged into this repository between 1 and 19 October 2026, Archon runs built 363.

You start the queues by hand or bind them to a schedule or issue events with a [trigger](https://archon.diy/guides/workflow-triggers/). The pack binds nothing by default. The pack works through the GitHub CLI (`gh`) today; see [the forge path](https://archon.diy/reference/forge/#use-the-forge-path-in-the-sdlc-pack) for the plugin route. [Set up the SDLC factory](https://archon.diy/getting-started/sdlc-factory/) runs it on your repository, and [SDLC pack workflows](https://archon.diy/reference/sdlc-workflows/) describes every workflow in it.

## Build any factory

Workflows are not tied to code. A node can be an agent prompt, a bash command, a script, a loop, a wait for a person or a child workflow, so any process with steps, checks and decisions fits: content production, research, operations, support. Write it as YAML, keep its commands and scripts beside it, and share it as a pack.

## A reliable core, extensible at the seams

The core stays small: the workflow engine, isolation, durable state, the attention queue and an audit trail. Everything that talks to the outside world plugs in through a published contract, and you can build your own plugin for any of them:

| Plugin kind | What it adds | Maintained here |
|---|---|---|
| Workflow pack | Workflows, commands and scripts | `sdlc` |
| Provider | An agent runtime | Claude Code, Codex, Pi |
| Forge | A code host | GitHub |
| Chat | A conversation surface | Slack |

GitHub is the registry: publish in your own repository and install with `archon plugin add owner/repo`. See [Publish a plugin](https://archon.diy/guides/publishing-plugins/) and the [plugin index](https://archon.diy/plugins/).

## Quickstart

1. Install [Claude Code](https://claude.ai/code) and sign in, or install Codex.
2. Install Archon. The installer asks which providers to add:

   ```bash
   curl -fsSL https://archon.diy/install | bash        # macOS / Linux
   irm https://archon.diy/install.ps1 | iex             # Windows (PowerShell)
   brew install coleam00/archon/archon                 # Homebrew, then: archon setup
   ```

3. Run a workflow in any git repository:

   ```bash
   cd your-project
   archon workflow list
   archon workflow run archon-investigate "Why does the login test fail?"
   ```

4. Watch runs and answer their questions in the web console with `archon serve`, or from the terminal with `archon workflow attention`.

To let your coding agent drive Archon for you, run `archon skill add` in your project, then ask it to "use archon to fix issue #42".

Other installs (from source, Docker, servers), provider setup and the Claude Code binary path are in the [installation guide](https://archon.diy/getting-started/installation/). The [quick start](https://archon.diy/getting-started/quick-start/) shows how to write a brief a run can deliver against.

## Connect a platform

The CLI and web console work out of the box. Connect a chat platform or forge to start and follow runs from there:

| Platform | Guide |
|---|---|
| Slack | [Slack guide](https://archon.diy/adapters/slack/) |
| Telegram | [Telegram guide](https://archon.diy/adapters/telegram/) |
| GitHub webhooks | [GitHub guide](https://archon.diy/adapters/github/) |
| Discord | [Discord guide](https://archon.diy/adapters/community/discord/) |

## Documentation

Full documentation is at **[archon.diy/docs](https://archon.diy/docs/)**.

| Topic | Description |
|---|---|
| [Quickstart](https://archon.diy/getting-started/quick-start/) | Install and first run |
| [Workflow language](https://archon.diy/reference/workflow-language/) | Write your own YAML workflows |
| [Operate runs](https://archon.diy/getting-started/operate/) | Watch runs, answer the ones that need you, read results |
| [CLI reference](https://archon.diy/reference/cli/) | Every command and flag |
| [Configuration](https://archon.diy/reference/configuration/) | Config options, model tiers and aliases, env vars |
| [AI assistants](https://archon.diy/getting-started/ai-assistants/) | Claude Code, Codex and Pi setup |
| [Deployment](https://archon.diy/deployment/) | Docker, VPS and production setup |
| [Architecture](https://archon.diy/reference/architecture/) | Packages and boundaries |
| [Troubleshooting](https://archon.diy/reference/troubleshooting/) | Common issues and fixes |

**For AI tools:** point your LLM at [`/llms.txt`](https://archon.diy/llms.txt) for an index of the docs, [`/llms-full.txt`](https://archon.diy/llms-full.txt) for all of them in one file, or [`/llms-small.txt`](https://archon.diy/llms-small.txt) for a condensed version.

## Previous version

Looking for the original Python-based Archon (task management and RAG)? It is preserved on the [`archive/v1-task-management-rag`](https://github.com/coleam00/Archon/tree/archive/v1-task-management-rag) branch.

## Telemetry

Archon sends a few anonymous events so maintainers can see which workflows get real usage, on what platforms, and whether runs succeed, and prioritize accordingly. **No PII, ever.** Events: `archon_started` (once per CLI invocation or server boot; `archon serve` reports as the server, and a detached run's worker process is counted by the command that started it), `archon_active` (daily heartbeat while a server is running, so long-running installs stay counted), `chat_turn_handled` (each direct AI chat turn: platform, provider, model, duration, and usage totals; never message content), `workflow_invoked` (each workflow start, and again for each resumed segment), `workflow_completed` / `workflow_failed` / `workflow_cancelled` (sent once when a run's final status is saved, whichever path ended it), `workflow_approval_resolved` (each human approve/reject decision: the binary resolution only, never comments or reasons), and `codebase_registered` (a pure count when a project is registered; no name, path, or URL).

**What's collected (categorical only):**
- **Workflow name**: the real name for *bundled* (Archon-authored) workflows; `"custom"` for your own workflows, so private names never leave your machine. The discovery source (`bundled`/`global`/`project`) is sent alongside.
- **Run shape & outcome**: platform (`cli`/`web`/`slack`/…), provider and model ids, node count, how many nodes of each type, graph depth and widest fan-out, how many distinct commands are referenced, the total inline prompt size as a bucket (`none`/`lt_1k`/`1k_5k`/`5k_20k`/`gte_20k` characters), which node types and features are used (loop/approval/script/bash, structured output, persisted sessions, MCP, skills, fresh-context loops), whether the run is interactive, isolated in a worktree, a resume, or a child of another run, the outcome, duration (from the run's first start to its end, including approval and wait time), a categorical exit or cancel reason, and a fixed-enum failure class recorded where the first failed node failed (`fatal`/`transient`/`unknown` for provider errors; `timeout`, `exec_failed`, `output_contract`, `max_iterations`, `child_failed`, `cancelled`, `config` for engine-detected causes; never raw error text) plus that node's type.
- **Run reference**: `run_ref`, a one-way hash of the install UUID and the run id, so a run's start, resumes and outcome can be joined. The run id itself is never sent.
- **Bundled ancestry**: when one of your workflows is a copy of a bundled workflow, the bundled workflow's name and whether the copy is `identical` or `modified` (`derived_from`, `derived_similarity`). Only the bundled name is sent; nothing about your copy's name, node ids or prompts.
- **Workflow no match**: `workflow_no_match`, a count of agent decisions that no available workflow fits, with only the transport (`native_tool` or `cli`).
- **Chat activity**: one event per direct-chat AI turn with platform, provider, model, duration, and completed/failed; a turn whose provider errors out mid-turn counts as failed. Message content, prompts, and conversation ids are never sent.
- **Aggregate usage**: provider-reported gross input, output, optional cache-read/cache-write token totals (with a flag when those totals are a floor), and cost (USD) per workflow run across all of its resumed segments (a parent run's total includes its child runs' spend, so sum spend over runs that are not children), plus direct-chat usage and total loop iterations. Numeric totals only, never the content the tokens represent.
- **Machine context**: OS, architecture, Archon version, runtime, whether it's a binary build, the install channel (`binary`/`docker`/`source`), the short commit of the running build when known, whether stderr is a terminal, and a CI flag.
- **Deployment shape** (server only): which adapters are enabled (booleans), how many chat plugins it hosts (a count; plugin names are not sent), database kind (`sqlite`/`postgresql`), whether web auth and multi-user mode are on, and the GitHub auth mode. Configuration *values* (tokens, URLs, hosts) are never sent.
- A random install UUID stored at `$ARCHON_HOME/telemetry-id` (`~/.archon/telemetry-id` by default, `/.archon/telemetry-id` in Docker). Nothing else.
- A `schema_version` number on every event, bumped whenever this list changes.

**What's *not* collected:** your code, prompts, messages, custom workflow names, workflow descriptions, git remotes, file paths, usernames, tokens, AI output, error message text, your IP address, your geographic location. None of it.

**Opt out:** set any of these in your environment:

```bash
ARCHON_TELEMETRY_DISABLED=1
DO_NOT_TRACK=1        # de facto standard honored by Astro, Bun, Prisma, Nuxt, etc.
POSTHOG_API_KEY=off   # off | 0 | false | disabled | "" all disable
```

CI environments (`CI=true`) are auto-disabled: forks running fixtures in GitHub Actions, CircleCI, etc. do not send events.

**Check the current state:** run `archon telemetry status` to see whether telemetry is enabled, why (if not), the install UUID, and the active host. Run `archon telemetry reset` to rotate the install UUID. `archon doctor` also surfaces the current state in its check list.

Shutdown gives pending telemetry a 75 ms flush window, then cancels outstanding requests. Slow or unreachable ingestion can lose events; it does not hold up command exit. Far from the ingest host (for example, about 250 ms TLS handshake from Europe to the default US host), events from short commands like `--help` are usually dropped; longer commands flush in the background while they run.

Self-host PostHog or use a different project by setting `POSTHOG_API_KEY` and `POSTHOG_HOST`.

## Contributing

Contributions welcome. See the open [issues](https://github.com/coleam00/Archon/issues) for things to work on, and read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.

## Star history

[](https://www.star-history.com/?repos=coleam00%2FArchon&type=date&legend=top-left)

## License

[MIT](LICENSE)

---

### Overview (`.archon/maintainer-standup/README.md`)

# Maintainer Standup

Daily morning briefing for Archon maintainers. Pulls latest `dev`, fetches all open PRs and assigned issues, classifies them **P1–P4** against `direction.md`, and surfaces progress versus the previous run (merged, closed, what you shipped).

## Files in this folder

| File | Committed? | Purpose |
|------|:---:|---------|
| `direction.md` | ✓ | Project north-star — what Archon IS / IS NOT. **Shared by all maintainers.** Drives PR triage and polite-decline classification. |
| `README.md` | ✓ | This file. |
| `profile.md.example` | ✓ | Template for new maintainers to copy. |
| `profile.md` | gitignored | Your personal config (gh handle, role, focus areas). |
| `state.json` | gitignored | Auto-written carry-over for the next run. |
| `briefs/YYYY-MM-DD.md` | gitignored | Daily prose briefs. Last 3 are read into the next run. |

`direction.md` is committed because triage decisions should be consistent across maintainers and across runs. `profile.md`, `state.json`, and `briefs/` are personal — your focus, your daily notes, your reading material — so each maintainer manages their own.

## Setup for a new maintainer

1. Copy the template:
   ```bash
   cp .archon/maintainer-standup/profile.md.example .archon/maintainer-standup/profile.md
   ```
2. Edit `profile.md`:
   - Set `gh_handle` to your GitHub login.
   - Set `role` and `scope` to match your maintainer focus (`main_maintainer` / `everything` for full coverage; narrower for sub-maintainers).
   - Optionally fill in **Currently focused on** — the synthesizer weights items toward what you list there.
3. Run it:
   ```bash
   archon workflow run maintainer-standup ""
   ```
4. The first run is a baseline (no prior state to diff). Subsequent runs compare against `state.json` and surface "Resolved since last run" / "What you shipped" / aged carry-over items.

## How it works (engine view)

1. **Three gather scripts** run in parallel (`bun`, no AI):
   - `maintainer-standup-git-status.ts` — fetches `origin/dev`, fast-forwards if safe, captures new commits + diff stat since the last recorded SHA.
   - `maintainer-standup-gh-data.ts` — pulls open PRs (full metadata), review-requested PRs, authored-by-me PRs, assigned issues, recently-filed unlabeled issues, and recently-closed PRs/issues since the last run.
   - `maintainer-standup-read-context.ts` — reads `direction.md`, `profile.md`, `state.json`, and the last 3 briefs.
2. **Synthesis node** (`command: maintainer-standup`, Claude Sonnet, structured output) reads everything, optionally drills into specific PRs/issues with `gh pr view` / `gh issue view`, classifies P1–P4 against `direction.md`, and returns `{ brief_markdown, next_state }`.
3. **Persist node** writes `brief_markdown` to `briefs/YYYY-MM-DD.md` and `next_state` to `state.json`.

The workflow runs **in the live checkout** (`worktree.enabled: false`) — it has to read this folder and pull `dev`. `--branch` and `--no-worktree` flags are rejected.

## Editing direction.md

`direction.md` is the source of truth for "what Archon is / isn't" during PR triage. Add a clause when a triage decision needs justification (so the next maintainer can reach the same conclusion). When declining a PR, cite the clause inline (e.g., `direction.md §single-developer-tool`).

The synthesizer also surfaces **Direction questions raised** — PRs that touch areas where `direction.md` has no stance yet. Use those to evolve the doc deliberately rather than deciding case-by-case.

## Customizing the brief format

The output structure is defined in `.archon/commands/maintainer-standup.md`. Adjust the Phase 3 template if you want different sections or a different P-tier scheme. The synthesizer's `output_format` schema lives in `.archon/workflows/maintainer-standup.yaml`.

---

### Overview (`examples/workflows/README.md`)

# Prerequisites

## Install Pi

```bash
npm install @earendil-works/pi-coding-agent
```

## Install Kiro provider and dependencies

```bash
pi install npm:pi-provider-kiro
npm install @earendil-works/pi-tui @earendil-works/pi-ai
```

## Install MCP for Pi

```bash
pi install npm:pi-mcp-adapter
```

---

### Overview (`packages/chat-contract/README.md`)

# @archon/chat-contract

The `archon-chat/1` contract for long-lived, out-of-process chat plugins. It depends
on `@archon/provider-contract` for the symmetric NDJSON JSON-RPC peer and token
usage schema. It has no engine, database, platform SDK or server dependency.

`serveChat(plugin, io?)` serves a `ChatPlugin`, using stdin/stdout by default.
`connectChat(io, expectedDescriptor)` initializes a host connection and checks
the live descriptor against the installed descriptor. Register `onInbound` and
`onRunAction` before calling `start()`. Close the connection to close plugin stdin;
`serveChat` drains in-flight requests and rendering before returning. Plugins own
shutdown of their platform connection after `serveChat` returns.

## Wire

| Direction | Method | Result |
| --- | --- | --- |
| Host to plugin | `initialize` (`{}`) | `ChatPluginDescriptor` |
| Host to plugin | `chat/start` (`{}`) | `{}` or RPC error `-32000` with `{retryable}` |
| Plugin to host | `chat/inbound` | Accepted or rejected (`not_allowed`) |
| Plugin to host | `chat/run_action` | Done, rejected, forbidden or refused |
| Host to plugin | `chat/send` | `{}` |
| Host to plugin | `chat/result_footer` | `{}`; requires `resultFooter` capability |
| Host to plugin | `chat/run_event` notification | Requires `runEvents` capability |
| Host to plugin | `chat/attention_notice` | `{conversationId?}`; requires `attentionNotices` capability |

All payloads have exported Zod schemas and inferred TypeScript types. Malformed
request arguments get JSON-RPC `-32602`. Malformed reply payloads reject the
affected call with `PluginProtocolError`; the connection remains usable. An
invalid initialization reply prevents connection setup and closes the peer.
Malformed JSON-RPC messages or run-event notifications fail the connection;
`closed` exposes that failure.
Rendering exceptions are contained and reported to stderr without event contents,
so a rendering failure does not disconnect chat. Start implementations can throw
`ChatStartError(message, retryable)`; unclassified start errors are non-retryable.

Plugins authenticate platform traffic and assert only `sender.platformUserId`
and an optional display name. The host owns allowlists, identity resolution and
authorization, stamps the platform from the installed descriptor, and dispatches
accepted messages without awaiting the full assistant turn. `ChatActor` contains
only `user` and `unidentified`; it is a host type, never a plugin-supplied actor.
Plugin message persistence is always owned by core.

`ChatRunEvent` is a presentation projection: workflow start, node state, approval
pending (including authored decisions, pause identity and the persisted authored
outcome), and terminal status with authored outcome and cost. Engine events and database rows stay on the host.
The host routes events only to the plugin owning the run's conversation.

`ChatAttentionNotice` goes to an install-level route the operator configured, not to
a run's own conversation. `destination` is the route's opaque value. A `gate` notice
carries the gate type, message and decisions; an `attention` notice carries the
attention wait's form (`message`, `approval`, `select` or `multi_select`) and its
options; a `terminal` notice closes a run the route was told about, with the
`conversationId` the plugin returned for that run's earlier notice. A plugin that can
thread a follow-up returns `conversationId`; otherwise it returns `{}`.

Run actions are approve, reject, respond, answer, resume and cancel. An optional
response carries node/pause identity and feedback; respond requires an explicit
decision. Answer carries a typed answer to an attention wait: the required
`nodeId` and `pauseId` of the occurrence it answers, the chosen option ids and
optional text. The host refuses an answer to an occurrence that is no longer open.
Resume continues a paused or failed run, such as an attention wait without a
`pauseId`. `RunActionResult` carries the approval/rejection presentation facts and
whether resume succeeded, an answer or resume and whether it resumed, cooperative
cancellation (including already-finished), or a stopped live owner with cleanup
warnings, cascade failures and a blocked parent.
Unexpected host operation failures should become RPC errors, not a successful
action result.

The descriptor declares retention, streaming defaults and their environment key,
an optional allowlist environment key, optional footer, run-event and attention-notice
support, and
`workflowCommand.prefix`. Hosts format workflow commands as `prefix + command`;
the prefix's trailing space is significant.

## Compatibility

Within `CHAT_PLUGIN_PROTOCOL` (`archon-chat/<n>`), changes are additive: every
message valid for the baseline shape stays valid for the current receiver.
Plugins parse host messages without rejecting unknown fields, so behavior a plugin
must honor is introduced behind a descriptor capability, as `resultFooter`,
`runEvents` and `attentionNotices` are. Removing or narrowing a field, making one
required, adding an enum value the receiver parses, or changing a field's meaning
bumps the protocol.

`connectChat` and `inspectChat` read the protocol a plugin advertises before
parsing its descriptor. A plugin on another protocol fails with
`PluginProtocolMismatchError` from `@archon/provider-contract/plugin`, whose
message and `hostProtocol`/`pluginProtocol` fields name both versions; the host
does not restart it.

`src/fixtures/archon-chat-1-baseline.schema.json` is the baseline the promise
starts from. `compat.test.ts` sends baseline-shaped host messages to the current
plugin and current host messages to a plugin pinned to the baseline. A protocol
bump replaces the snapshot; a test fails while its protocol differs from
`CHAT_PLUGIN_PROTOCOL`.

## Conformance and schema

`runChatConformance(connect)` is exported from `@archon/chat-contract/conformance`
and returns a list of violations. The factory returns a `ChatConformanceFixture`:
a connected host, plugin-owned drivers for inbound messages and actions, a
malformed inbound driver that bypasses local validation, a next-render failure
driver, and a promise that resolves once the plugin stops. Drivers must exercise
the real transport and platform implementation; they must not fabricate results.
The runner installs fixture host handlers for sender ids `allowed` and `denied`.
`PluginRemoteError` is exported from the conformance subpath for process-fixture
drivers that carry a remote error through a separate control channel.
It checks start success or typed failure, long sends, both sender outcomes,
invalid-params without delivery, render-error containment, optional footer, an
optional attention notice and its terminal follow-up, and stdin closure. Each check has a bounded grace period (5 seconds by default).

Generate the language-independent schema with
`bun run generate:chat-contract-schema` from the root; verify it with
`bun run check:chat-contract-schema`. The published document's `$defs` includes
each wire payload and the JSON-RPC envelope. Package tests run with
`bun run --cwd packages/chat-contract test`.

---

### Overview (`packages/forge/README.md`)

# `@archon/forge`

This optional leaf package owns Archon's forge-qualified identities and normalized inbound event contract. Forge adapters authenticate vendor deliveries, validate their payloads, and translate supported events into these schemas. Generic workflow admission does not import this package.

The maintained GitHub plugin owns the first source implementation, `normalizeGitHubWebhook`, its raw payload schemas, and its capability fixtures. Its inbound module entrypoint lives under `packages/adapters/src/forge/github/source-plugin.ts`; it exports its source capabilities and returns resolved receipts to the generic host and imports no core database or server implementation. It performs no network lookup. `@archon/forge/conformance` accepts source-owned fixtures and normalization callbacks to verify their normalized contract.

Authored selectors match exact event actions, repository and subject identity, plus the small variant-specific predicate set in `forgeEventSelectorSchema`. Input mappings accept direct fields or literals. The trigger host creates the literal binding schema with its existing workflow `jsonValueSchema`:

```ts
const bindingSchema = createForgeBindingSchema(jsonValueSchema);
```

A missing direct field rejects that binding. Source actors record forge provenance only; the trigger host separately resolves and authorizes the binding's Archon run-as identity. A label event's `actor` and a comment event's `author` are event facts a binding may map into inputs (`label.actor.id`, `label.actor.login`, `comment.author.id`, `comment.author.login`), so a workflow can check who acted against its own list; they never select an Archon identity.

## Outbound operations

`@archon/forge/operations` owns operation requests, results, metadata and audit payloads, re-exporting the work-item and pull-request lifecycle contract from `./lifecycle`. `@archon/forge/dispatch` executes optional plugins; generic workflow execution imports neither. The independently executable GitHub plugin in `packages/adapters/src/forge/github/plugin.ts` implements every operation through the same handshake as installed plugins.

Writes carry evidence rather than a bare failure. `workitem.create`, `workitem.labels.set`, `workitem.label.add`, `workitem.label.remove`, `workitem.comment.upsert`, `workitem.close`, `repo.label.ensure`, `pr.create`, `pr.edit-body`, `pr.ready`, `pr.draft` and `comment.upsert` each resolve to exactly one of four outcomes — applied and read back, refused before anything was written, applied but unverified, or unknown — so a caller can tell a refusal from a write whose fate it does not know, and never retries blindly past the last one. Dispatch checks an applied result against the request that asked for it instead of trusting the plugin's claim.

See the [forge reference](../docs-web/src/content/docs/reference/forge.md) for the CLI, trusted configuration, UTF-8 process protocol, credentials and check semantics. Run `bun run test` and `bun run type-check` from this package to exercise contract, mapping and process conformance.

---

### Overview (`packages/provider-contract/README.md`)

# `@archon/provider-contract`

This leaf package owns the shapes at the provider boundary: the events a provider streams during a turn, the typed failure it reports, the terminal result of a turn, the `settled` signal, token usage, capabilities, agent requests, execution contexts, native tools, provider registration and the effort ladder. It depends only on zod, so an out-of-repo provider can depend on it too. `@archon/providers`, `@archon/workflows`, `@archon/core` and `@archon/server` import these schemas instead of restating them.

A provider that knows why a turn failed sets `failure: { class, retryAfterMs?, resetAt?, evidence }` on its `result` chunk, and still sets `isError`. The class comes from the SDK's structured signals (error codes, HTTP status fields, typed exceptions), never from matching text; a setup error the provider detects (a missing binary, an unreadable config file, an unknown model) is `misconfigured`, and a failure the provider cannot classify is `unknown`. The engine decides retry from `class`. `evidence` is for the operator and the logs; nothing branches on it. A provider does not retry on its own: the engine owns the retry policy.

During a turn a provider streams `ProviderEvent`s (`src/events.ts`): message and thought text, tool calls and their updates, warnings, MCP server status, compaction, subtasks, hooks and state updates. Names follow the Agent Client Protocol where it has the concept, and each field documents whether it comes from ACP or is an Archon addition. `providerChunkSchema` is the whole stream: these events, then `result`, then `settled`.

- Providers coalesce text deltas and send blocks at block, tool or turn boundaries. An immediate warning may flush part of a reply block before that boundary; subsequent `agent_message_chunk` events in the same block set `continuesPreviousBlock: true` so consumers append their text to the preceding reply block. Omit the marker when a new reply block starts.
- A provider gives every tool call its own `toolCallId` and closes it with exactly one `tool_call_update` before the next `result`. An interrupted call closes as `cancelled`.
- Tool output is capped at `TOOL_OUTPUT_MAX_CHARS` code points, the unit JSON Schema's `maxLength` counts. The provider truncates with `truncateToolOutput`, which sets `outputTruncated` when it cuts.
- A warning carries a provider-namespaced `code`, such as `claude.node_config_ignored`. Readers branch on the code, never on the message.

The engine records every event a workflow node receives exactly as the provider yielded it, inside an envelope with the node attempt and an emission sequence number, in the run's JSONL log and in its database, and serves the same object from `GET /api/workflows/runs/{runId}/provider-events`. What a provider puts in an event is what operators and API readers see, so the cap and the codes above are the only shaping it gets.

Every normally completed turn, successful or failed, ends with one `{ type: 'settled' }` chunk after its final `result`. A `result` can arrive while work the turn started is still running, so the engine finishes a node on `settled`, not on `result`. Lost observation with live work and cancellation never settle. Workflow nodes suspend the idle watchdog only while a provider declaring `backgroundWork: reported` reports live subtasks; never-ending tasks stay running and cancellable. Any other provider's silent turn still times out. Final result text, structured output and session ID own the node output; cost sums across results. Tokens sum unless a result sets `tokensUnknown: true`; that signal discards earlier token totals and keeps the aggregate unknown across later results and queries. Omitted tokens alone do not invalidate a total. A shell process launched with `cmd &` inside a foreground command is invisible to every provider.

A provider that declares `sessionResume` names the session each turn ran in with the result's `sessionId`; a provider that cannot resume leaves it out rather than inventing one. A persistent session id can resume the conversation, so the engine stores the full id only on the node record, which is where a user finds it to continue a node outside Archon. Streams and logs carry `sessionPreview(id)`, its first `SESSION_PREVIEW_LENGTH` characters; a provider logs that preview, never the full id.

When a turn that asked to resume fails because the session no longer exists, a provider that can confirm it through a structured check, never the vendor's error text, sets `sessionNotFound: true` on that failed result. It sets it only for a rejection that did no model work. The engine owns the one retry on a fresh session; a provider that cannot tell leaves the field out and the turn fails.

Pi honors `SendQueryOptions.purpose: 'title-generation'` by using an in-memory session when no resume id is supplied. Ordinary turns and workflow nodes, including `context: fresh`, remain persistent. Resume, fork, and missing-resume fallbacks retain persistent state. An in-memory title session's result id cannot be resumed from disk.

`schema/provider-contract.schema.json` is generated from `src/` by `src/scripts/generate-schema.ts`. Run `bun run generate:provider-contract-schema` from the repository root after changing a schema; `bun run validate` fails while the file is stale.

`@archon/provider-contract/conformance` checks a provider against the contract from fixtures the provider owns. Failure cases drive the provider into one failure each and name the class it must report and the vendor text its evidence must keep. Turn cases, together with the failure cases, check that every turn settles exactly once, last, after its result. A provider with tools also supplies a `toolTurn`: a turn with two tool calls, one of them interrupted. Its stream must parse as provider chunks, close every tool call once before the next result, never update a call it did not start, and leave every subtask in a terminal status at `settled`, as reported by its runtime. Providers never invent terminal subtask statuses. A provider declaring `backgroundWork: reported` supplies `backgroundCases` with runtime status evidence sampled as chunks arrive; the checker rejects early settlement, invented closes, and missing background fixtures. `none` means verified absence of background work; `unobserved` makes no claim about work the adapter cannot see. When the suite's `capabilities` declare `sessionResume`, every result of a turn case, the tool turn, the fork turn and each background case must carry a non-empty `sessionId`; failure cases are not checked. When they declare `sessionFork`, the suite needs a `forkTurn`: a turn that forks an existing `source` session. Every result of it must report `resumed: true` and name a session other than `source`.

`@archon/provider-contract/plugin` serves and connects a provider over a pair of UTF-8 byte streams (`ReadableStream<Uint8Array>` and `WritableStream<Uint8Array>`). This is the contract slice for [#3642](https://github.com/coleam00/Archon/issues/3642); process spawning, installation and registry integration follow separately.

```ts
import { connectProvider, serveProvider } from '@archon/provider-contract/plugin';

const serving = serveProvider({ descriptor, create: () => provider }, providerIO);
const client = await connectProvider(hostIO);
try {
  for await (const chunk of client.sendQuery('Task', '/absolute/project/path')) {
    // Consume the same ProviderChunk the in-process provider emits.
  }
} finally {
  await client.close();
  await serving;
}
```

`serveProvider` defaults to stdin/stdout when no stream pair is supplied. Stdout must contain only protocol traffic; use the log sink supplied to `create(log)` for diagnostics. Plugin stderr is withheld by the process host. The connection uses ACP v1 `initialize`, `session/new`, `session/prompt` and `session/cancel`, with the descriptor advertised in `agentCapabilities._meta.archon`. The transport-local session handle is separate from the native session id carried in `result.sessionId`. Each `_archon/chunk` notification carries `{ sessionId, chunk }`; the client validates the chunk using `providerChunkSchema` and preserves its fields and order. It exposes `settled` only after `session/prompt` acknowledges successful iterator completion, so a provider rejection after emitting settlement still fails the turn. It finishes after that settlement, or when a cancelled stream ends. It never invents results, settlement or terminal subtask states. An uncancelled end before settlement fails.

Descriptors declare `PROVIDER_PLUGIN_PROTOCOL` (currently `2`), provider identity/version, capabilities, a static credential catalog and a config JSON Schema. ACP remains version 1; the Archon protocol travels in `_meta.archon.protocol`. Dynamic credential catalogs remain unsupported. Session requests carry native-tool specifications; handlers stay on the host and `_archon/tool_call` invokes them only for live sessions after validating the declared input schema. Credential checks and credential-model resolution use `_archon/check_credential` and `_archon/resolve_credential_model`. Unknown notifications are ignored; unsupported requests receive JSON-RPC `-32601`. This is an Archon extension, not a plain ACP agent adapter.

Session requests carry the data fields of `SendQueryOptions`. `abortSignal` becomes `session/cancel`; `onAdmission` stays with the host; `nativeTools` carries specifications only. Container requests carry optional `env` data while the plugin retains its host environment. For host requests, `serveProvider` gives the underlying provider its own process environment. A stream pair does not apply the client's `env` bag to the serving process. The process carrier owns termination when a provider does not respond to cancellation. Closing a connection aborts its provider work.

## Compatibility

Within a protocol version, changes are additive: every message valid for the version's baseline shape stays valid for the current receiver, and a new field is optional and sent only when it has a value. Plugins keep rejecting fields they do not know, so a newer host's request that uses a field an older plugin of the same version cannot honor fails with `-32602` instead of being dropped. Removing or narrowing a field, making one required, adding an enum value the receiver parses, or changing a field's meaning bumps `PROVIDER_PLUGIN_PROTOCOL`.

Version negotiation follows ACP. The host sends its protocol in `initialize`; the plugin always answers with its descriptor, but serves sessions and `_archon/*` requests only when both protocols match. On a mismatch the host fails with `PluginProtocolMismatchError` (a `PluginProtocolError`), whose message and `hostProtocol`/`pluginProtocol` fields name both versions. A peer released before this negotiation existed still rejects a mismatched `initialize` with `-32602`.

`src/plugin/fixtures/protocol-2-baseline.schema.json` is the published schema at the first protocol-2 release. `compat.test.ts` sends baseline-shaped requests to the current plugin and current requests to a plugin pinned to the baseline. A protocol bump replaces the snapshot; a test fails while its protocol differs from `PROVIDER_PLUGIN_PROTOCOL`.

The generated JSON Schema includes the descriptor, serializable request, lifecycle payloads, extension payloads and JSON-RPC envelopes. Messages are newline-terminated JSON, capped at `PROVIDER_PLUGIN_MAX_MESSAGE_BYTES` (16 MiB per line, excluding the newline). Malformed UTF-8, JSON, envelopes and chunk payloads fail with `PluginProtocolError`, including the plugin id and incoming line number. Its `detail` explains the failure without text from the line (malformed JSON, or each failing schema path and issue code, with keys the schema does not declare shown as `*`), and an error raised while a line is read carries that line's byte length and, when this reader handles it, its method name as `evidence`. The tests check emitted lifecycle payloads against the unmodified ACP v1 schema definitions from `@agentclientprotocol/sdk@1.7.0`, installed only as a development dependency.

`_archon/log` carries `level`, `msg`, and JSON `bindings` to the caller-supplied `onLog` sink. The process host logs a fixed `provider.plugin.log` through `provider.<id>`, using its own descriptor id and the supplied severity. It forwards `bindings.failureClass` (a contract failure class) and `bindings.lifecycle` (a contract lifecycle event, `providerLifecycleEventSchema`), each only when the value is in its enum, plus a lifecycle event's non-negative integer `bindings.waitedMs`, and drops a record with neither enum value, since severity alone tells the operator nothing. Plugin `msg`, module identity, and all other bindings remain withheld because any plugin-chosen string can encode credentials or message content.

---

### Overview (`.archon/workflows/rasmus-tests/README.md`)

# rasmus-tests

Test workflows running on Rasmus's config.

They exercise engine primitives against real GitHub issues, so they need a funded
provider and take minutes per run — that is why they are not in CI. The deterministic
counterparts that *are* in CI live in `../test-workflows/` (`e2e-joins`,
`e2e-fanout-alldone`, `e2e-fanout-allsuccess`).

| Workflow | Exercises |
| --- | --- |
| `t1-fix-issue` | structured output · `when:` · `cancel:` · `loop_group` self-heal via `$LOOP_PREV` |
| `t2-fix-issue-include` + `t2-review-block` | `include:` · `with:` → `$INPUTS` at load · namespacing |
| `t3-triage-fanout` + `t3-probe-issue` | `fan_out` over a runtime list · `max_parallel` · `parent_run_id` |
| `t4-subrun` | `workflow:` child run · `input:` forwarding · output threading |
| `t8-cascade` + `t8-slow-child` | cascade-cancel — needs a concurrent `archon workflow abandon` |

All AI nodes resolve through the `@mini` alias in `.archon/config.yaml`.

---

### Overview (`.archon/workflows/sdlc/README.md`)

# Working in the SDLC pack

Conventions for this pack specifically. Project-wide judgment lives in
[`AGENTS.md`](../../../AGENTS.md); the YAML surface is governed by
[`.archon/workflow-language-constitution.md`](../../workflow-language-constitution.md).
How we decide what a workflow should be, agents first, is in
[`CONSTITUTION.md`](CONSTITUTION.md).

## Guards

A guard here must protect an action the node it lives in takes.

**Keep** a guard when it:

- verifies the effect of something this node just did — exit 0 is not proof, and a
  forge can accept a write and silently not apply it; or
- refuses to proceed on a question it asked and could not get an answer to, where
  guessing is irreversible. `archon complete` blocking a branch delete it could not
  prove safe is the shape.

**Cut** a guard when it re-asserts an invariant something else already established.
That belongs where it is established, once — not at each node that depends on it.

**The test: a guard names the real run it would have saved, or it stands in front
of an irreversible action. Otherwise it is a comment; write it as one.** A guard
does not earn its place by having a fixture that turns it red.

### Why this rule exists

The pack once verified "the checkout is on the branch my PR is on" in three places:
a preflight node before review, the ready flip, and prose inside the correction
prompt for an agent to honour. Three copies, two languages, one of them dependent
on a model's diligence.

None of it was load-bearing. The engine gives a run its worktree, and the pull
request is created in that worktree, so the invariant holds by construction.
Nothing had ever gone wrong — the alarm that prompted the work was investigated
and closed invalid. And the copies did not even cover the steps that would have
suffered most from a drifted checkout: `impl` writes code to that checkout without
checking, and `validate` runs the project's tests against it without checking.

The preflight alone cost 31 lines and a stub in 17 fixtures, for a node no fixture
could ever run. All three copies are gone.

CI is the project's full validation; implementers run only focused checks. Whether
CI is green is judgment, so one agent prompt owns it (`deliver/commands/ci.md`), run
once a round's review passes. It never judges a stale head (it pushes the
checkout's `HEAD` when the pull request is behind it), refuses a head that
conflicts with its base, marks the pull request ready (a
project whose CI skips drafts starts it then), waits however long CI takes, and
answers green, pr_caused, or other. On anything but green it puts the pull request
back to draft before it answers, so a red PR never stays ready. A red the change
caused, or a conflict with the base, goes back to the implementer in the next
round, which merges the base on a conflict, and that correction is reviewed before
CI runs again. A flake or infrastructure failure outside the change gets one re-run
of its failed jobs before the agent answers, recorded as `rerun`; any other red ends
the run not ready, and a defect outside the change becomes a discovery.

Unresolved review threads on the pull request are work too, whoever opened them: a
person, a review bot, or no one in a project without outside reviewers. After CI,
each round's `threads` node lists the unresolved threads in `threads.md`, quoted as
untrusted review data, and puts a ready pull request back to draft while any is
open, so the round ends `threads_open` instead of ready. The next round's
implementer judges each thread like a blocking finding, fixing it or declining it
with evidence; `settle-threads` checks each disposition at the pushed head and
writes the reply, and `threads` posts the replies, resolves those threads, and reads
the pull request back. A thread about code the pull request doesn't change or make
reachable, such as one that arrived with a merge of the default branch, is settled
with a reply saying so; it belongs in its own issue. A thread no agent can settle
ends the run `ask`.
`settle-threads` runs before CI and a failed one skips it, so the pull request stays
the draft the previous round left; a failed `threads` node puts it back to draft
before the run fails. The
pack's own review verdict is unchanged: threads gate delivery, not the review.

A pull request counts as ready only at the commit its review comment stamps. The
comment's first lines are stamped by `publish-review` from the round's certified
output ([`.shared/review-comment.ts`](.shared/review-comment.ts)): the reviewed commit
and the verdict, which the merge queue's gate reads. CI never
marks ready a head carrying commits the round's review did not cover. Commits pushed
after a run ends, by a fixer or by hand, are reviewed by a continuation delivery of
that pull request, whose first round reviews the whole change before CI marks it
ready again, or by `archon-review` given the earlier round's `review/report.md` as
`prior_report`.

The rule is not "never defend against what has not happened" — the two Keep cases
above have not happened either, and both are worth their few lines. The question is
whether the guard is protecting *this node's own action*, or restating something
that was already true when the node started.

## Forge source

One switch selects the source for every pull-request, issue and check read and
write this pack makes. [`.shared/forge.ts`](.shared/forge.ts) owns which one a run
selected, [`.shared/pr.ts`](.shared/pr.ts) owns the pull-request reads and writes,
and [`.shared/issue.ts`](.shared/issue.ts) owns the issue read, each returning the
same shapes from either source:

- **`forge` (opt-in).** Set `ARCHON_SDLC_FORGE=forge`. Operations go through `archon forge`, which needs a forge
  plugin installed for the repository's host, a credential that plugin accepts
  (for GitHub, the host tries `GH_TOKEN`, `GITHUB_TOKEN`, then `gh auth login`;
  GitHub App mode supplies a run token; see the forge
  reference in the docs), and the `ARCHON_CLI_COMMAND` host command that the CLI
  and server publish at startup.
- **`gh` (default).** Optionally set `ARCHON_SDLC_FORGE=gh` in the environment Archon runs
  with, for example `~/.archon/.env`. The GitHub CLI then acts on the recorded
  qualified PR, with the authenticated `gh` login.

The preflight names every forge operation the delivery workflows use, and a plugin
missing any of them is refused before an agent spends. The merge queue checks its own
operations when it starts (below). Keep `gh` as the default until a released plugin
with all of them passes a real-PR check.

The merge queue reads and merges through the run's source. On `forge` its facts,
gate and record read through `pr.view`, `pr.merge-state`, `checks.state`,
`checks.commit`, `pr.threads` and `workitem.comments`, and the gate merges through
`pr.merge` pinned to the head; `facts` refuses a plugin missing any of them before it
claims an item. Own-PR enrollment does not run on `forge`, because the contract has
no read for a maintainer's open pull requests, and the sweep agent's failed-job
reruns, log reads and closing-reference body edit still use `gh`. The gate itself
doesn't refuse over threads; the sweep holds a merge only for a thread that raises a defect in code the
pull request changes or makes reachable. Delivery reads, answers and resolves review
threads through the run's source: on `forge`, its `threads` node uses `pr.threads`,
`pr.thread.reply` and `pr.thread.resolve`.

The switch does not cover standalone `archon-review`: given a pull-request number,
URL or branch, or run on a branch with an open pull request, it reads that pull
request with `gh pr view` and `gh pr diff`. Inside delivery it reviews the recorded
pull request and reads nothing from the forge.

The source is never picked from what happens to be installed. When the selected
source cannot answer (no host command, no plugin for the host, no credential, a
failed operation), the node refuses; neither source falls back to the other. Any
other value of `ARCHON_SDLC_FORGE` refuses too. A container execution receives no
`ARCHON_CLI_COMMAND`, so the forge source refuses there. A container also receives
only the project's configured env, never `~/.archon/.env`, so select `gh` for it in
the repository's `.archon/config.yaml` with `env: { ARCHON_SDLC_FORGE: gh }`.

`archon-ship-block`, `archon-deliver-block`, `archon-upkeep` and `archon-pr` start with a `forge` node
([`.shared/forge-check.ts`](.shared/forge-check.ts)) that proves the selected source
can act after worktree setup and before any agent spends: on `forge`, host metadata
must advertise every operation in `REQUIRED_FORGE_OPERATIONS` in `.shared/forge-check.ts`,
then one authenticated repository read for
the repositories the checkout's remotes name, at least one of which must answer
(`pr.view` by head, which every released GitHub plugin supports; it proves read
access, not write access); on
`gh`, active authentication (`gh auth status --active --hostname <host>`) plus a
repository read (`gh api --hostname <host> repos/<path>`) for the repositories the
remotes name. One readable repository is sufficient, including a readable mirror;
this proves read access, not write access, which is first proven by the first write.
The gh check first refuses a run whose `GH_TOKEN` is empty with no `GITHUB_TOKEN` set (gh would
otherwise act as the host's login): per-user GitHub mode scrubbed it, or an env file
has an empty `GH_TOKEN=` line to remove. The operations still refuse on their own. The node's typed
`source` is what the `ci` and `update-pr-body` agents act through.
Run `archon workflow run archon-forge-check` from a checkout to see the same check's
result without starting delivery.

## Public writes belong to a script

An agent judges and authors; one node owns the public writes and
proves they landed. `publish-pr` pushes the branch and opens or reuses the pull
request; on a pull request the run continues it pushes and opens nothing, and
`prepare` writes no body for it when the caller named the PR or selected no branch. If the caller
selected a branch without naming a PR, prepare writes a body even if it found an
existing PR: publish may need to open one from the selected branch. That body is
used only when opening a PR. In delivery, content the work requires in the
description is written during the round that prepared it, before that round's
review reads the pull request: `prepare` copies it to `pr-description.md`, and
`publish-pr` writes it between `archon-delivery-description` markers, keeps the
rest of the description as it is, and reads it back. Markers in Markdown code
examples and raw HTML containers are literal text; top-level markers are reserved
for the managed section and are refused in prepared content before pushing. The
composed body must expose exactly the intended pair before any description edit;
unclosed fences or HTML containers that hide either delimiter refuse publication.
A blank line separates the closing delimiter from prepared content, including terminal tables;
legacy sections whose closing delimiter follows a table row remain replaceable.
If publication verifies a created or reused PR but a later description edit fails,
the failure report still names that PR from its verified publication record. If writing that record
fails, publication fails and the node error names the verified PR URL and recording
error for explicit continuation. The failure report selects the latest verified publication
by artifact write time across the typed output and creation record, and reports an
unreadable creation record. An unresolved
creation claim reports `pr_create_unresolved`; the build queue refuses to select
another PR or branch until a newer triage settles it: it names the reconciled PR, or
names none and the next round opens one. `update-pr-body` corrects the
description's stale claims and links the issue once the rounds end. A standalone
`archon-pr` leaves a continued pull request's description as its author left it:
its `publish_description` input defaults off. `publish-review` upserts the one marked review
comment, and deliver's `threads` posts the thread replies `settle-threads`
declared and resolves those threads. Every pack label is declared in `.shared/labels.ts`, and every stage writes
labels through its `writeLabels`, which adds and removes them as a delta so labels
another writer adds survive, on both sources. No delivery node files
discoveries: the run's terminal report relays them to the operator, and
[`archon-discoveries`](#discoveries) drains them where a project runs it. In `archon-deliver-block` and
`archon-ship-block` that is the `report` node, which also runs when a node fails, so a
workflow that includes `archon-deliver-block` runs after a failed delivery and must gate
on `$deliver.output.ready`. Each acts on the values the agent
before it declared in its typed output, which the engine certified, writes through
the selected source, and fails unless the result reads back — so "the write
failed" and "the write may have landed" stay different outcomes, in the pack as in
the forge contract. Three agents write directly: the structure pass's implementer
pushes its own commit to the pull request's branch with `git`, and CI pushes a
head that was left behind before it judges anything, ending `other` if it cannot;
`update-pr-body` edits the description, because judging which claims are stale and
rewriting them is the whole job; and `ci` marks the pull request ready and back to
draft around its CI wait. The last two act through the source the `forge` node
certified, bound to them as a typed input. PR publication reads the agent-authored
`pr-body.md` and `pr-description.md` content through certified artifact pointers;
the publisher owns the writes and verifies the description edit.

Which remote holds a repository is decided by its configured URL, never by the
remote's name (`.shared/remote.ts`), so a fork checkout whose `origin` is the fork
and whose `upstream` is the canonical repository fetches the base and pushes the
head where they belong.

That split is also what keeps the source switch out of the prompts. A prompt that
branched on `ARCHON_SDLC_FORGE` would be an invented protocol; `forge.ts` reads it,
and an agent that acts on the forge receives the certified `source` value instead.
The two exceptions are triage's read of the repository's labels and implement's
check that it sits on the pull request it was launched onto, which name the switch
because `archon-triage` and `archon-implement` also run without a `forge` node
before them.

## Deterministic scripts

Every `script:` node here is TypeScript on Bun, under its own component's
`scripts/` directory. Logic more than one of them needs lives once in
[`.shared/`](.shared), imported by relative path with the extension written
(`../../.shared/report.ts`). That directory is reserved for modules and for the
pack's default policy documents under `.shared/policy/`: nothing in it is a workflow
or a named script target, and a node that names one fails at load.

The repository validates them where they live. `.archon/workflows/tsconfig.json`
is the owning configuration — `bun run type-check` compiles that project. Lint
covers every TypeScript file under `.archon/workflows/`, so it needs no globs of its
own. A script placed outside the type-check globs fails `pack-scripts-validation.test.ts`
rather than going quietly unchecked.

The scripts' tool-boundary tests live under `.archon/scripts/__tests__/`. CI runs the
merge-sweep conformance test through `merge-sweep-contract`; the other pack tests do not run
on pull requests or pushes. Run `bun run test --plan pack` when you change
`.archon/workflows/` or `.archon/scripts/`, or dispatch the Test Suite workflow with
`pack-tests` set. The workflow fixtures likewise run only locally
(`bun run validate --only workflow-fixtures`) or on a dispatch with `pack-fixtures` set.

Three rules, each protecting something a script cannot get back on its own:

- **Read every binding as a literal `process.env.INPUTS_<NAME>`.** The engine scans
  each script's own source at load and refuses a workflow whose script reads a
  binding no `with:` clause provides. It matches that literal form only, and it never
  follows imports — so a helper that built the key from a name would hide every read
  in the pack from that check, and a renamed binding would surface as a wrong result
  at the end of a paid run instead of a refusal before it started. Pass the value to
  `.shared/io.ts`, never the name.
- **Never call `process.exit()`.** Bun leaves without draining stdout — a 500 KB
  write to a pipe arrives as 131072 bytes, silently. Set `process.exitCode` and
  return; `.shared/io.ts` is the only place that should need to know this.
- **Nothing the target project provides is available.** No `package.json`, no
  `node_modules`, no `tsconfig.json`, no npm dependency. Relative imports within
  the pack and the standard library are the whole surface, which is what keeps
  these workflows runnable against a project in any language.

A vocabulary a node declares in YAML has exactly one owner, its `output_format`. A
script that consumes another node's certified value does not restate the list.

## Evidence never carries credentials

The engine retains what every exec node prints, so a node's output is the record
whether it set out to keep one or not. Never print a value that can contain a
secret: read it where it is normalized and pass on the normalized form. A remote
URL is the common one — `https://<token>@host/repo` is a perfectly ordinary origin
— so `.shared/remote.ts` reads remote URLs only to normalize them to `host` and
`owner/repo`, and only the remote's name and that identity reach a command line or
a message. Failure messages are the same
surface: interpolating the raw value into one leaks it just as effectively.

That retention is also why a node does not need its own log. The ready flip once
wrote one by hand — every command it ran, echoed into an artifact — which is what
the transcript now holds for free.

## The engineering-conventions sidecar

A repository may declare its architecture in an `architecture.md`, its
engineering conventions in an `engineering.md`, and its product direction in a
direction document, at the root, in a config directory such as `.archon/`, or
wherever its steering files point. The steering files themselves (`AGENTS.md` or
its equivalent) reach every node through the provider's own context, so no
prompt tells a node to read them. Each prompt that judges names the sidecar
documents its decision can depend on, and opens one only when a finding or
decision depends on it, reading the section it cites rather than the whole file.
Implement reads the short `engineering.md` up front. `architecture.md` is named
only where a decision is structural: seams, simplify, plan, investigate, implement,
and triage when fit with the codebase is in question. They are the project's
own values, so a taste finding cites one of them or is left out, and an owner
declines a finding by the same standard. Each read is conditional on the file
existing, so the pack stays portable: a repository without one loses nothing. A
new pack prompt that writes or judges code follows the same rule.

A project's policy for a decision the pack makes, such as what triage accepts, is
one more sidecar document, kept at `.archon/sdlc/policy/<topic>.md`. The pack ships
a default for each policy document a prompt names, in
[`.shared/policy/`](.shared/policy). A node reads the default first and then
the project's own file, whose lines add to the default and win where the two conflict;
with no project file, the default applies. To change one, write only the lines to add
or override in `.archon/sdlc/policy/<topic>.md`. A policy document may be only a pointer to
documents the project already has, and the agent reads what it points to. A prompt
names only the policy documents its decision needs, through the shared sentence
ending "the SDLC policy files this decision names, at `.archon/sdlc/policy/`,
falling back to the pack's defaults"; triage names `triage.md`, and plan and review
synthesis name `rulings.md`, the answers a project's maintainers have already given,
read before either stops to ask. The build queue's agents name `build.md`, what
the queue may decide on its own, and its route and answer agents `rulings.md` too. The
merge queue's sweep names `merge.md`, which pull requests merge on their own.
Agents interpret
them; anything that must be exact, such as label names, stays typed configuration.

A node reaches the defaults through a script node of its workflow: its `policy` node,
or the build or merge queue's `facts`. A run
captures its source before it starts, from a binary's embedded bundle as from a
source checkout, and runs its scripts from that capture, so
[`.shared/policy-defaults.ts`](.shared/policy-defaults.ts) finds `.shared/policy/`
from its own location and the node binds that directory into the prompt with `with:`. The script hands over
the directory only; the agent reads the default and the project's file, and no
script opens either. The default is a file rather than text in the prompt, so a node
whose decision names no policy carries none of it.

## A stage pauses where nothing takes it forward

Each stage a person runs, `archon-deliver`, `archon-plan`, `archon-triage`,
`archon-review` and `archon-ship`, is a public entry around a `-block` workflow that
holds the stage's graph. The entry runs the block and then
[`archon-attention`](attention/archon-attention.yaml), whose one `decide` agent judges
whether the ending has a next step. When it has none (a delivery that ended not
ready, a plan or triage stopped on a question for a person, a review that is Ready on
a draft pull request), the run pauses as one `select` attention item labelled
`stage: <workflow>`. It says what ended and why it matters, the agent's
recommendation with its confidence and evidence, the choices and what each does, what
waiting costs, and the links. The run continues on the answer: `rerun` runs the block
again with the item and the person's note added to its work, `ready` takes the pull
request out of draft, and `finish` keeps the outcome as it stands. The entry returns
the block's typed outcome as its last pass left it. A stage that fails raises no item:
the run fails at the failed node, as the block alone would.

A workflow that composes a stage includes its `-block`: the queues, intake, upkeep,
the review eval, and `archon-ship-block` and `archon-deliver-block` themselves. The
composer owns what follows the stage's ending and asks through its own attention item
when it needs a person, so one decision never raises two items.

## Models per stage

Each stage names the concern it serves as a model preference list. The first alias
your config defines wins. When it defines none, the node runs on the size tier it has
always used. A user who sets nothing gets the same models as before.

| Alias | Nodes | Falls back to |
|---|---|---|
| `@triage` | triage | `medium` |
| `@planner` | plan | `large` |
| `@investigator` | investigate | `medium` |
| `@implementer` | implement, including delivery's reshape | `large` |
| `@reviewer` | every agent node of review (scope, lenses, synthesis) and simplify | each node's tier |
| `@review-<lens>` | one lens: `seams`, `code`, `tests`, `errors`, `docs` or `simplify` | `@reviewer`, then the lens's tier (`large` for `code`, `medium` for the rest) |

Review's lens router stays on `small`, and the queue, attention and delivery judges
keep their tiers. Moving those means rebinding the tier.

Set an alias for every run with `archon ai alias set @reviewer codex gpt-5.5`, or in
`aliases:` in `.archon/config.yaml`. Set it for one run with
`archon workflow run ... --model @reviewer=codex/gpt-5.5`. A lens alias overrides only
its lens: with `@reviewer` on Codex and `@review-seams` on Claude, seams runs on
Claude and every other lens on Codex.

## A node's streams are the operator's channel

Retention is not the only reader. Anything a node writes to stderr is sent to the
operator as the run happens, even when the node succeeds — and that copy is not
redacted. So a node speaks for itself: capture what the commands inside it print,
and let only your own authored messages reach the streams. Re-emit a command's
output when it failed and its words are the diagnostic; drop it when it is just a
tool narrating itself. Capture a value's stderr separately rather than merging it,
too — a `gh` update notice merged into a read becomes the value.

## Intake

[`archon-intake`](intake/archon-intake.yaml) is the front of the label-driven pack.
Trigger bindings start it on issue events. Each issue has a work item, the state document `work/<number>` in the
project's scope, owned by [`.shared/workitem.ts`](.shared/workitem.ts). The
document is the item's state: its trust, stage, triage fields (the five judged
fields and the related work triage found: overlapping issues, earlier attempts and
merged changes on the same surface, which plan reads), promotion, claim and a short
history. The issue's pack labels mirror it, and a label a person adds is a
request, never the state.

- **Trust.** The `facts` script reads `.archon/sdlc/policy/maintainers.json` at the
  head of the default branch and records once whether the issue's author is on it.
  The file is a JSON array whose entries are forge logins, or objects naming one with
  opt-ins: `["alice", { "login": "bob", "merge_queue": "own-prs" }]`. Every listed
  login is a maintainer. `merge_queue: "own-prs"` also enrolls that maintainer's own
  open pull requests in the merge queue, except one a work item already names (see
  Pickup there); on the `gh` source only. No file means an empty list, so every author is outside. Any other
  shape refuses the run.
- **Outside items** get `archon-needs-maintainer` and nothing else: no agent node
  runs and nothing is posted.
- **Promotion.** A listed maintainer promotes an outside item by adding
  `archon-triage` or a route label. The label binding maps who added it from the
  forge event (`label.actor.login`); an event without an actor rejects the binding,
  and an empty or unlisted actor promotes nothing. A promotion pins the body's digest
  as it was when intake recorded the request. An edit after that demotes the item
  back to `archon-needs-maintainer`, and the history records both digests.
- **Requests and echoes.** On a maintainer's item, `archon-triage` asks for
  re-triage, and a route label that disagrees with the document is removed. A label
  already in the document's mirror is the pack's own write coming back as an event
  and is ignored. The outcome also refuses to add a request label to an outside item
  no maintainer promoted, so the install's own identity cannot promote anything.
- **Pickup.** Every run records its event as a request on its issue, then claims the
  waiting item that sorts first (most urgent, then most important, then smallest,
  then oldest; an untriaged item sorts after triaged ones, oldest first) with a
  conditional write. Every queue claims in this order. A request that
  arrives while its item is claimed waits for the next intake run. A request label
  whose request no run has claimed may have a request on its way, so a run leaves
  it for the run that claims that request. It waits one pickup; the next pickup
  that still finds no request for it removes it. On a maintainer's item only
  `archon-triage` waits, since a route label is reconciled away either way. An
  open, edit or
  reopen adds no request while another one waits, since the pickup reads the live
  issue; label requests keep the newest 50. A run clears its
  claim only after the item's labels are written. The next pickup releases a claim
  whose run the engine reports completed, failed or cancelled and queues that run's
  requests again; the item then sorts after every other waiting item until an
  attempt finishes, so one item that cannot be read or labelled never starves the
  rest. A claim whose run is pending, running, paused or unknown is never released,
  and there is no age timeout.
- **Closing.** When triage's verdict is `NO_ACTION`, the item is a duplicate of
  another open issue or a merged change already delivered it, and the triage policy
  lets it close without a person. Before writing anything, the outcome checks a
  duplicate's declared original: it must be another item, an issue rather than a pull
  request, open, and at the declared URL. When a check fails, the item is stored and
  labelled `BLOCKED` with the reason, for a maintainer to decide. Otherwise the
  outcome writes the work item and labels, then comments on the issue with a link to
  the original or the change and closes it as `duplicate` or `completed`. A
  duplicate's original gets a comment linking back. Each comment is one marked
  comment, so a repeated run edits it rather than adding another. An item already
  closed for the verdict's reason (an earlier run that stopped after its close)
  still records the work item and its labels, but posts no comment and doesn't close
  it again; one closed for another reason fails the run before any comment. A demoted item and every other verdict close nothing.
  Intake is the one workflow that closes: `archon-triage` run alone, even with
  `publish`, only labels the item `archon-close`, because inside intake triage runs
  unpublished so the outcome can withhold a verdict judged on text no maintainer
  promoted.
- **Closed issues.** Intake records the event and changes nothing else: no trust
  decision, no stage change, no label write.
- **Inputs.** `label_actor` is the one input that can create a promotion, never
  trust. Only the binding, or someone with access to the install, can supply it.

The pack installs no binding. Two bindings in the GitHub source plugin's configuration start
it; both share one resource so runs queue rather than overlap:

```json
{
  "bindingId": "intake-issues",
  "hostId": "build-host-1",
  "runAsUserId": "<archon user id>",
  "resource": "forge:github.com/acme/widgets:intake",
  "capacity": 2,
  "overlap": "queue",
  "launch": {
    "cwd": "/srv/archon/widgets",
    "workflowName": "archon-intake",
    "inputs": {},
    "isolation": { "kind": "default" }
  },
  "selector": {
    "kind": "issue.lifecycle",
    "actions": ["opened", "edited", "reopened"],
    "repository": { "host": "github.com", "path": "acme/widgets" }
  },
  "inputMapping": {
    "repo_host": { "source": "field", "field": "repository.host" },
    "repo_path": { "source": "field", "field": "repository.path" },
    "number": { "source": "field", "field": "subject.number" },
    "kind": { "source": "field", "field": "subject.kind" },
    "action": { "source": "field", "field": "event.action" }
  }
}
```

The label binding is the same with `"bindingId": "intake-labels"`, a selector of
`{"kind": "label.changed", "actions": ["added"], "predicates": [{"field": "label.name",
"in": ["archon-triage", "archon-ready", "archon-plan", "archon-investigate"]}]}`, and two
more mappings: `label` from `label.name` and `label_actor` from `label.actor.login`.
The predicate only saves runs; intake ignores any other label. A binding installed before the investigate label was renamed still lists `archon-issue`: replace it with `archon-investigate` in that binding, and in any build-queue binding that selects on it, because `label.name` predicates match exactly.

## Build queue

[`archon-build-queue`](build-queue/archon-build-queue.yaml) takes intake's triaged
items to a reviewed, ready pull request, and stops only for decisions that belong to a
person. It acts on its own when a case is clear and asks when it is not; the default
policy for which is which is [`.shared/policy/build.md`](.shared/policy/build.md),
which a project extends with its own `.archon/sdlc/policy/build.md`.

- **Pickup.** A run claims one item with intake's claim and order. An item waits for the build queue when its work item's stage is a READY
  route (`deliver`, `continue`, `plan` or `investigate`) and the queue has not
  recorded it, or recorded it as waiting for its next round. A re-triage after the
  queue's last decision starts the item over. The build queue's claim takes none of
  the item's intake requests, so an edit or label request recorded while it holds the
  item waits for intake's next pickup. The queue writes no labels; intake's mirror
  stays as it is.
- **A named item.** The `number` input makes the run claim that issue instead of the
  first in order, so an operator chooses what a run builds and, with the launch's
  config, which provider builds it: one launch per issue, such as
  `--input number=123` with the config that selects Codex. An issue that cannot be
  claimed refuses the run before any agent, with why: it has no work item, another
  run holds it, or its stage and build status show it does not wait.
- **Blocked items.** Intake's blocked verdict can go stale while work lands in
  parallel: a blocker's outcome can arrive another way while its issue is still open.
  When `retriage_as` names a maintainer login, a run that names no item also revisits
  the blocked items, beside the item it builds. `blocked` lists the unclaimed blocked
  items not revisited in the last six hours, independently of item pickup and PR
  resolution. An unresolved PR create refuses the item's continuation without
  suppressing these revisits. A scheduled queue does not judge the same items every
  run. One `revisit` agent reads each issue, what it waits on and the default branch, and names the items that no longer look blocked. `retriage` adds
  `archon-triage` to each and starts `archon-intake` detached with that label event,
  recorded as added by `retriage_as`. Intake stays the judge: it re-triages only when
  that login is on the maintainer list, and its verdict decides whether the item now
  waits for the build queue. Every listed item records the revisit.
- **Route.** The `route` agent checks that the issue body is a contract (the six
  elements, with its dated "Current scope" sections), proves a bug's symptom before
  its first round, and writes the brief `archon-ship` starts from. A bug that needs a
  browser is reproduced with the playwright-cli skill, which the prompt names rather
  than a `skills:` entry, because a provider that cannot load named skills refuses a
  whole run that declares one.
- **Rounds.** One round is one included `archon-ship-block` run. The `answer` agent reads how
  it ended and decides: `done` when the pull request's latest review says Ready at its
  head commit, `continue` with a brief when the contract already answers what stopped
  it (a correctable finding, CI red the change caused, a false blocker naming a later
  step), or `ask`. It declares the pull request when the round left commits unpushed.
  A `continue` starts the item's next round at once, a detached `archon-build-queue
  --input number=<n>` on the run config and the `retriage_as` and
  `round_limit` this run started with; if that launch fails, the item waits for the
  next queue run.
  Facts reports the open pull request in the item's repository as the candidate, and
  the `route` agent declares whether the round continues it: unless a dated scope
  section or a recorded answer supersedes it. A continued pull request goes to
  `archon-ship` as `pull_request`, and delivery finishes it whichever route triage
  takes, pushing onto its head so the same pull request updates. A superseded one is
  never checked out or pushed to: ship gets `none`, so its triage cannot route the
  round back onto it. The round starts fresh, refusing a checkout that is not on the
  default branch, and its brief asks for the old
  pull request to be closed as superseded once the replacement opens. A round that failed
  after publishing still records its pull request. A round that continues none opens its
  pull request from `build/<issue>-<slug>-<run>`, a name never reused. A closed or
  merged pull request, or one from a fork, is asked about; a round launched anyway
  never continues it, since ship gets `none` for it too.
- **Round cap.** When a round ends wanting another and `round_limit` rounds (default 3)
  have run since the item's scope last changed, by a re-triage or a maintainer's
  answer, the `answer` agent also compares their reviews. Each round finding new
  defects asks the scope question instead of running another round.
- **Writes.** The `apply` script refuses an attention item without the choice it
  recommends, pushes the declared commits (never forcing, and reading the remote head
  back), then records the decision on the work item and releases the claim, in one
  conditional write. A run that asks keeps its claim while it is paused.
  When route finds the pull request already reviewed at its current head, apply
  checks that head before and after taking it out of draft. A changed read-back
  head fails readiness and, unless already draft, triggers one draft correction
  through the selected forge source. The failure reports the observed head, state
  and draft status; a failed correction preserves its error and attempts one
  diagnostic read. If verification fails, inspect the PR, restore draft if still
  open and ready, and review its current head before readiness. These observations
  cannot prevent another actor changing it afterwards. The merge queue still owns
  exact-head review and merge checks.
- **Attention.** An unclear decision pauses the run as one `select` attention item
  labelled `build: route`, `build: stop` or `build: scope`. It says what the decision
  is, why it matters, the agent's recommendation with its evidence, confidence and
  what would change it, the choices and what each does, what happens if nobody
  answers, and the links to the evidence. The answer is one declared choice, or
  `other`, which every item offers: its text says what to do instead and becomes the
  ruling, and the item continues from it. The queue never reads the answer's prose for
  a decision. The `ruling` script writes the
  choice's ruling and the person's words into the issue body as one dated "Current
  scope" section, reads it back, and records the answer: a `continue` choice starts
  the item's next round at once, a detached `archon-build-queue --input number=<n>`
  run on the run config (`--config-from-run`) and the `retriage_as` and
  `round_limit` the answered run started with, and a
  `park` choice stops working the item until a re-triage. If that launch fails, the
  item waits for the next queue run. A run cancelled while it waits leaves the item asking, and only a
  re-triage starts it over.
- **Record.** The work item's `facts.build` holds the item's status (`next`,
  `asking`, `ready` or `parked`), the brief its next round starts from, its last ten
  rounds and its last twenty decisions with any maintainer's answer.

The pack installs no binding. A schedule keeps items moving, because a run works one
item and a continuation waits for the next run. Each run includes `archon-ship-block`, which
changes its checkout, so every run needs its own worktree:

```json
{
  "version": 1,
  "sourceInstanceId": "widgets-build-queue",
  "binding": {
    "bindingId": "build-queue",
    "bindingRevision": null,
    "hostId": "build-host-1",
    "runAsUserId": "<archon user id>",
    "resource": "forge:github.com/acme/widgets:build",
    "capacity": 2,
    "overlap": "skip",
    "launch": {
      "cwd": "/srv/archon/widgets",
      "workflowName": "archon-build-queue",
      "inputs": { "repo_host": "github.com", "repo_path": "acme/widgets" },
      "isolation": { "kind": "worktree" }
    }
  },
  "schedule": { "intervalSeconds": 1800, "runAtLoad": false }
}
```

To start a run as soon as intake routes an item, add a label binding on the same
resource, with intake's label selector narrowed to the route labels intake writes
(`archon-ready`, `archon-plan`, `archon-investigate`) and the same launch.

## Merge queue

[`archon-merge-queue`](merge-queue/archon-merge-queue.yaml) merges the pull requests
the build queue finished, and stops only for decisions that belong to a person. It
merges on its own when a case is clear and asks when it is not; the default policy for
which is which is [`.shared/policy/merge.md`](.shared/policy/merge.md), which a project
extends with its own `.archon/sdlc/policy/merge.md`: the sweep reads the default and
then the project's file, whose lines win where the two conflict.

- **Pickup.** A run claims every waiting item, up to `limit` (default 10), with
  intake's claim and order. An item waits for the merge queue when its build record is
  `ready` and the merge queue has not settled it: it never decided it, or left it
  waiting or asking. When the build queue readies an item again after the merge queue
  returned or parked it, it waits again. The pull request is the one the build
  queue's last round delivered. One already merged or closed is recorded without the
  agent. Before claiming, the run enrolls each open, non-draft pull request by a
  maintainer whose `maintainers.json` entry sets `merge_queue: "own-prs"` and that no
  work item names yet: it gets a work item of its own (`facts.own_pr`). A pull request
  a build round recorded is named by its build item and never enrolls. A build round
  records its pull request once the round ends, so one listed while the round is still
  running can enroll; that short window is an accepted gap, and enrollment after the
  round finds the pull request recorded. An own-PR
  item has no build record of its own, so where another would go back to the build queue
  (no verdict, a stale one, a conflict, a holding thread, red CI the change caused) the
  sweep asks instead, naming what is wrong and the command a maintainer can launch, and
  its question offers no `return`. The record never hands one to the build queue.
  The queue never starts that run itself. An own-PR item answered `park` or `other`
  waits again only when its current head differs from its latest merge decision's
  head. At the same head it stays parked. Merged own-PR items and other kinds of
  settlement, including unreadable and superseded parks, do not recover this way.
- **Facts.** The `facts` script reads each pull request in one GraphQL snapshot: its
  head, how many commits of the default branch it lacks, whether it merges cleanly, CI and unresolved review threads at the
  head, and the review verdict. Only a review comment a listed maintainer wrote
  (`.archon/sdlc/policy/maintainers.json` on the default branch) counts; verdicts
  from other authors are ignored. The login that authors the pack's published review comment
  must be listed in that file on the default branch for its verdict to count.
  With the default `gh` source, this is the identity `gh` acts as; with the opt-in
  `forge` source, it is the forge plugin's credential identity, which may differ.
  Check the published comment's author when choosing which login to list.
  A failed PR read waits once; a second consecutive failure
  parks the item with the read error recorded until the build queue readies it again.
  Renewed build readiness gives a parked item another first-read wait. A successful
  read resets that sequence even if later work fails. An unreadable own-PR park stays
  parked; it is not an answered park. To reconsider an answered own-PR park, facts
  reads its snapshot before claiming and reuses it in the sweep. A failed recovery
  read leaves that unclaimed item unchanged and reports the error. If the default branch
  cannot be read, facts passes its diagnostic and the batch to record without a sweep.
- **Sweep.** One agent decides every pull request and orders them, least risk first.
  The queue never pushes, never changes its checkout and never waits for CI. A Ready
  pull request with green CI that the forge reports merges cleanly into the default
  branch merges, reshape or not, even when it is behind; the default branch's CI on the
  merge is the next sweep's first fact. Every pull request that can merge in a sweep
  merges in it. What the build queue can fix goes back to it with a brief: a verdict
  that is not Ready or is for another commit (a push after the review), a conflict with
  the default branch (the round merges it in and resolves the conflict, regenerating
  generated files with their owning script), an unresolved thread that raises a defect
  in the change, and red the change caused. Red CI in code a pull request does not touch
  gets one rerun of its failed jobs; the record stores the head it re-ran at, and the
  sweep reads it so it never reruns twice at one head. The sweep measures each diff
  with git against the merge base, not the forge's counts. An unclear pull request is
  asked as soon as its verdict is Ready at its head, even while its CI is pending or it
  conflicts; the approval is pinned to that head and a later sweep merges it once the
  gate passes. An approval carries to a later head only when the sweep judges that
  everything since the approved head is merges of the default branch and their
  conflict resolutions, and the verdict is Ready at the later head; any doubt voids it.
- **Gate.** The sweep merges only through
  [`gate.ts`](merge-queue/scripts/gate.ts), which reads the pull request and the
  default branch fresh and refuses unless the verdict says Ready for the head, CI is
  green at the head, the default branch's CI is not red (pending does not hold a
  merge), and the pull request is open, not a draft, into the default branch, at the
  head the sweep named, and that head contains the default branch's head or the forge
  reports it merges cleanly. Open review threads are the sweep's judgment. It merges with `--match-head-commit`
  and reads the merge back. A pull request that reads back unmerged, as when the forge
  queues merges, fails the gate.
- **The default branch.** The sweep judges it first. While its CI is red the gate
  refuses every merge. Red in code no recent merge touched gets one rerun of its failed
  jobs; red after that, or red in code a recent merge touched, is raised to a
  maintainer, naming the pull requests merged since it was last green, with a revert
  or fix-forward recommendation. An unreadable default branch permits no sweep or merge;
  independent bookkeeping completes before the run fails with the read diagnostic.
- **Record.** When the default branch could not be read, `record` writes the independent
  settlements, releasing only claims it still owns. Open companions remain unchanged;
  the next pickup releases their claims after the run fails. It makes no further forge
  reads and fails with the original diagnostic and any settlement write failures.
  Otherwise, the `record` script skips and reports a decision for a pull request this
  run did not claim, because that item belongs to another run. It refuses a
  `return` without a brief, a `return` for an own pull request, an `ask` without its
  question, an `ask` that offers a `return` on an own pull request or with a blank
  brief, and a red default branch the sweep neither re-ran nor raised: the pull
  request waits for the next sweep, and the record fails, naming each, once every
  other decision is written. A claimed pull request
  the sweep left undecided waits for the next sweep, and an ask with no attention item
  waits the same way. It reads every pull request back: the forge, not the sweep, says
  what merged, at the head the forge merged. A sweep that says it merged a pull
  request the forge does not show merged fails the record after every other decision
  is written: that pull request is recorded as waiting, or parked if it was closed,
  and the failure names a red default branch the run could not raise and any work
  item it could not write. A pull
  request that cannot be read waits for the next sweep; a work item that cannot be written is reported, so one bad item never loses
  the rest of the batch's record.
  It writes one decision per item under
  `facts.merge` (status `waiting`, `asking`, `merged`, `returned` or `parked`, any
  approval pinned to the head it approved, and
  the last twenty decisions), and a return sets the build record to `next` with the
  brief, so the build queue picks it up.
- **Merged issues.** A pull request that links its issue with `Refs #N` merges
  without closing it, and the issue keeps its intake stage, so the build queue could
  claim it again. When `retriage_as` names a maintainer login, `retriage` reads the
  issue of each build item whose pull request merged, by the gate this run or before
  it. One still open gets `archon-triage` and an `archon-intake` run started detached
  with that label event, recorded as added by `retriage_as`. Intake stays the judge:
  it re-triages only when that login is on the maintainer list, and closes the issue
  as delivered or keeps it with what is still open. A closed issue and an own pull
  request, which has no issue, are left alone. A failed read, label write or launch
  is named in the node's summary and stops nothing else. Empty `retriage_as`
  re-triages nothing.
- **Attention.** A run raises at most one attention item: a red default branch, whose
  one choice holds merges until it is green, or else the first pull request the sweep
  asked about, labelled `merge: base` or `merge: pr`. It carries the build queue's
  fields: what it is, why it matters, the recommendation with its evidence, confidence
  and what would change it, the declared choices, what happens if nobody answers, and
  links. A build-owned ask with a blank brief still raises its question when it
  offers no `return`. Every pull-request item also offers `other`; for a build item with a nonblank
  brief, it returns the pull request to the build queue with the answer's text as
  the ruling. Without a brief, `other` records the text and parks until the build
  queue readies the item again; no build round starts. A red default branch is only
  ever held, so its item offers `hold` alone. On a pull request, `merge` approves it at the head that was asked about, and
  a sweep the answer starts at once (on the answered run's config and its
  `limit`, `merge_method` and `retriage_as`) merges it once the
  gate passes there; `return` writes the choice's
  ruling into the issue body as a dated "Current scope" section and hands the item
  back to the build queue, whose next round starts at once on the build queue's own
  input defaults; `park` stops working it until the build queue readies it
  again. For an own PR, both `park` and `other` stop queue work until a later sweep
  observes a different head from the one asked about. The `other` answer stays on
  the work item as its ruling; it writes no issue body and starts no build round.
  New CI or review results at the same head do not unpark it. Approval remains pinned
  to its approved head, subject to the existing conflict-only carry rule. Any other
  ask waits for a later sweep, which decides it again.

The pack installs no binding. A schedule sweeps the queue. A run never changes its
checkout, and the claim keeps two runs off the same item:

```json
{
  "version": 1,
  "sourceInstanceId": "widgets-merge-queue",
  "binding": {
    "bindingId": "merge-queue",
    "bindingRevision": null,
    "hostId": "build-host-1",
    "runAsUserId": "<archon user id>",
    "resource": "forge:github.com/acme/widgets:merge",
    "capacity": 1,
    "overlap": "skip",
    "launch": {
      "cwd": "/srv/archon/widgets",
      "workflowName": "archon-merge-queue",
      "inputs": { "repo_host": "github.com", "repo_path": "acme/widgets" }
    }
  },
  "schedule": { "intervalSeconds": 1800, "runAtLoad": false }
}
```

## Discoveries

[`archon-discoveries`](discoveries/archon-discoveries.yaml) drains the discoveries
finished runs recorded outside their own scope. A run's discoveries are its latest
review verdict's certified records plus any raw `discoveries/*.json` record a node
wrote after that review ([`.shared/report.ts`](.shared/report.ts)), the same set its
terminal report lists.

- **Queue.** A pass lists this project's runs that ended (completed, failed or
  cancelled) inside `since` and `until`, oldest first, and claims up to `limit` of
  them that still have undecided records. A run's state document
  `discoveries/runs/<run-id>` is its memory and its work item: the refs a pass decided
  for it, the questions a pass asked about it with their answers, and the claim of the
  pass working it. A record is decided once its run's document lists it. Every run
  document is read in one call. A claim is released only when the claiming run is
  completed, failed or cancelled, never on age. A run whose records cannot be read is
  left queued and named on the pass's stderr.
- **Decisions.** One agent decides every queued record, under the project's direction
  document and the policy files `discoveries.md`, `triage.md` and `rulings.md`. It
  verifies each claim against the default branch's head with `git show` and
  `git grep`, merges duplicates across runs, and searches the tracker before filing.
  There is no store of decisions across runs: a defect a later run finds again is
  judged again against the tracker. Each discovery ends in one decision: `file` an
  issue with a full brief, `track` it on the open issue that already covers it (with
  one marked evidence comment per key, only when it adds something), `drop` it with a
  reason, or `ask` a maintainer.
- **Writes.** The `outcome` script checks before any write that every queued record is
  decided exactly once and that a tracking issue is open. Then it makes the public
  writes. A filed issue's body begins with ``, and the
  create looks for that marker across every issue first; the evidence comment carries
  the same marker and is replaced in place. So a pass run again after a stop, or a key
  filed twice, never opens a second issue or comment. Last it records the decided refs
  and the questions on each run's document. Filed issues carry no labels: intake
  triages them like any other issue, which it does only when the install's forge
  identity is on `maintainers.json`.
- **Questions.** Every `ask` in a pass becomes one attention item labelled
  `discoveries: question`. The answer is stored on the asking run's document. The runs
  those records came from stay claimed by the asking run until it ends, so no other
  pass asks again; the next pass then decides them with the answer beside the records.

The pack installs no binding, so the workflow runs only when started by hand until an
operator adds one. A native schedule starts a pass every few hours; `overlap: skip`
skips a start while a pass still runs, and a pass paused on a question gives its slot
back:

```json
{
  "version": 1,
  "sourceInstanceId": "build-host-discoveries",
  "binding": {
    "bindingId": "discoveries",
    "bindingRevision": null,
    "hostId": "build-host-1",
    "runAsUserId": "<archon user id>",
    "resource": "discoveries:github.com/acme/widgets",
    "capacity": 1,
    "overlap": "skip",
    "launch": {
      "cwd": "/srv/archon/widgets",
      "workflowName": "archon-discoveries",
      "inputs": { "repo_host": "github.com", "repo_path": "acme/widgets" },
      "isolation": { "kind": "default" }
    }
  },
  "schedule": { "intervalSeconds": 14400, "runAtLoad": false }
}
```

## Worktree cleanup

[`archon-worktree-cleanup`](cleanup/archon-worktree-cleanup.yaml) runs the host CLI's
`archon isolation cleanup --merged` across every project registered on the install. It
removes each merged worktree with its local branch, including a branch the worktree
adopted rather than created. Git ancestry or patch equivalence proves a merge without a
pull request; otherwise the branch's merged pull request does. Remote
branches are retained with a warning: after registration retirement, a Git commit
ID cannot prove whether the branch belongs to a replacement registration. Force
does not bypass this refusal. For manual cleanup, follow the
[remote branch recovery procedure](https://archon.diy/reference/cli/#recovering-remote-branch-cleanup)
in the CLI reference. It covers destination inspection, ownership verification and leased deletion.
On GitHub, enable **Automatically delete head branches** in repository settings to have
GitHub remove merged pull requests' branches automatically.

A worktree with uncommitted changes, a run
that can still claim it, or a commit the merge does not cover stays and is reported. With `include_closed: "true"`
it also removes worktrees whose pull request was closed without merging. It runs in place
and creates no worktree of its own.

Who removes each checkout:

- Intake, triage, discoveries, the merge queue, forge check and this workflow create no
  worktree. They run in the launch checkout as it stands, so point a binding's `cwd` at a
  checkout that tracks the default branch.
- A run that creates a worktree on its own run-named branch, such as a build round,
  removes it when the run completes if the checkout is clean and every commit is on a
  remote; its pushed branch counts. The branch stays. Otherwise the run reports what
  removing it would lose, and the checkout stays listed by `archon isolation list`.
- A worktree launched with `--branch` waits for this workflow's merged sweep.

A server already runs a merged sweep on its cleanup schedule, so this binding is for
installs that run only the CLI. The pack installs no binding. A native schedule starts a
pass every few hours:

```json
{
  "version": 1,
  "sourceInstanceId": "build-host-worktree-cleanup",
  "binding": {
    "bindingId": "worktree-cleanup",
    "bindingRevision": null,
    "hostId": "build-host-1",
    "runAsUserId": "<archon user id>",
    "resource": "worktree-cleanup:build-host-1",
    "capacity": 1,
    "overlap": "skip",
    "launch": {
      "cwd": "/srv/archon/widgets",
      "workflowName": "archon-worktree-cleanup",
      "inputs": { "include_closed": "false" },
      "isolation": { "kind": "default" }
    }
  },
  "schedule": { "intervalSeconds": 21600, "runAtLoad": false }
}
```

---

### Overview (`.archon/workflows/sdlc/validate/README.md`)

# Validation

`archon-validate` discovers and runs the project's checks on the current
checkout. Its `scope` input narrows them. It has three steps:

1. `discover` (agent) reads the repository and declares the checks to run, in the
   project's own order, as argv lists, each in a group: independent gates (a
   service and a web app built separately, per-module gates) are separate groups.
   It prefers an aggregate gate script over its parts and puts a locked-mode
   install first when dependencies are missing.
2. `run` (script) runs those checks with no agent involved, so a gate that takes
   longer than an agent's shell tool allows still finishes. Each group stops at its
   first failing check, but every group runs, so one gate's failure never hides
   another's. It writes `validation.md`: each command, its group, its exit status,
   how long it took, and the output tail of a failure. Full output stays in
   `validation/`.
   The checks run without the node's run contract (`WORKFLOW_ID`, `ARTIFACTS_DIR`,
   generated input keys and the rest): they are the project's gate, not part of the run.
   The engine supplies the full run-contract key list in `ARCHON_RUN_ENV_KEYS`,
   including its fixed keys and generated input keys. An independently configured
   `INPUTS_*` setting survives. A generated binding wins a
   same-key project setting and is removed from the gate. The runner requires a valid
   key list and fails before starting checks if it is missing or malformed. Manual
   invocations must supply the full removal list, including `ARCHON_RUN_ENV_KEYS` itself.
3. `classify` (agent) runs only when a check failed, and judges every failure the
   record names. It decides whether the change caused it (`introduced`), the base already had it (`inherited`) or the machine
   did (`environment`).

Green comes from exit statuses alone: every declared check exited 0. When not every
check ran, the result is `green: false` with `red_cause: incomplete`, even if a check
that did run failed; its failure is named in the summary. That happens when a check cannot start, or when the `run`
node's timeout stops the gate. On that timeout the runner kills the running check's
whole process tree, restores anything it moved aside, and records the stop in
`validation.md`. On Windows, or after SIGKILL, the script gets no signal it can
catch: the check's process tree may outlive the timeout, and anything moved aside
stays in the run's artifacts until the next attempt of the run puts it back before
it starts. Restoring never overwrites a path the checkout has again; that moved
copy stays in the artifacts, and `validation.md` names where. SDLC delivery refuses an incomplete result as unfinished rather
than red; the action is to resume the run.

---

### Overview (`packages/docs-web/public/brand/README.md`)

# Brand foundation source

Live at https://archon.diy/brand/foundation.html. Embedded into the docs Brand page via iframe (see `packages/docs-web/src/content/docs/brand/index.md`).

These are the original Penpot-exported source files — plain JSX compiled in the browser by Babel-standalone. No build step, no bundling. **To change the brand sheet, edit these files directly and refresh the page.**

| File | Owns |
| --- | --- |
| `foundation.html` | HTML shell. Loads React + Babel from CDN, wires up the JSX scripts, and applies our docs-site overrides (hides the Tweaks toggle). |
| `brand-app.jsx` | The full brand sheet: header, sections, gradient swatches, type scale, do's & don'ts. Where you edit copy and section layout. |
| `logo.jsx` | `ArchonMark`, `ArchonGlyph`, `ArchonLockup` components. SVG path data lives here. |
| `app.css` | Design tokens (oklch palette, surfaces, typography). Where you edit colors and spacing. |
| `tweaks-panel.jsx` | Generic tweak-panel widget shipped with Penpot exports. Brand sheet uses `useTweaks` for live customisation in the editor; the floating toggle is hidden on archon.diy via `foundation.html`. |
| `standalone-tweaks-toggle.jsx` | Companion floating button for `tweaks-panel.jsx`. Hidden on archon.diy. |
| `assets/archon-logo.png` | Rasterised logo fallback (the inline SVGs in `logo.jsx` are the canonical mark). |

## Local customisations

We carry exactly one delta on top of the upstream Penpot export:

- `brand-app.jsx`: removed the top-right "Console →" cross-link in the page header, since the sibling "Archon Console" doc isn't published here.

If you re-export from Penpot, re-apply that single change (or accept the cross-link).

---

### Overview (`packages/adapters/src/community/chat/README.md`)

# Community Chat Adapters

This directory holds the Discord adapter, a bundled community integration kept for
existing installs. Bundled community integrations are a transition state
([`direction.md §community-plugins`](../../../../../.archon/direction.md#community-plugins)):
they get focused compatibility, correctness and security fixes, and no new adapter is
added here.

## Adding a chat platform

A new chat platform ships as a chat plugin from its own repository and attaches through
`ChatPlugin` from `@archon/chat-contract`, with no change to Archon:

- [`@archon/chat-contract`](../../../../chat-contract/README.md) owns the wire, the
  descriptor and the conformance suite.
- [Publish a plugin: chat plugins](../../../../docs-web/src/content/docs/guides/publishing-plugins.md#chat-plugins)
  covers the manifest, release assets and `archon plugin add`.
- [`@archon/chat-slack`](../../../../chat-slack/src/plugin.ts) is the maintained Slack chat
  plugin and a complete example.

---

### Overview (`packages/adapters/src/community/forge/README.md`)

# Community Forge Adapters

This directory holds the bundled Gitea and GitLab adapters for existing installs.
They receive focused compatibility, correctness and security fixes; new forges ship
as plugins in contributor-owned repositories
([`direction.md §community-plugins`](../../../../../.archon/direction.md#community-plugins)).

## Adding a forge

A new forge attaches through the `@archon/forge` contract without a new bundled
adapter or a platform-specific registration in the server:

- [`@archon/forge`](../../../../forge/src/index.ts) owns normalized forge operations.
- [Publish a plugin: tags and executable release assets](../../../../docs-web/src/content/docs/guides/publishing-plugins.md#tags-and-executable-release-assets)
  covers the forge manifest, release assets and installation.

## Maintaining bundled adapters

Existing bundled adapters implement `IPlatformAdapter` from `@archon/core` and
receive webhooks through the server's
[`webhook routes`](../../../../server/src/routes/webhooks.ts).
The [GitHub adapter](../../forge/github/adapter.ts) is the maintained bundled reference.

Runtime behavior is independent of the platform identifier. `messagePersistence: 'adapter'`
means the adapter persists direct-chat messages; `defaultWorkflowDispatch: 'background'`
backgrounds all fresh runs, including workflows that can pause. Pauses wait in the
attention queue until answered. Optional `sendStructuredEvent` delivers rich events;
`prepareBackgroundConversation` prepares worker integration and returns an awaited finalizer.
Hosts own platform policies in
[`platform-policies.ts`](../../platform-policies.ts).

## Testing

### Mock isolation (required)

Bun's `mock.module()` is process-global and irreversible — `mock.restore()` does NOT undo it. Your test file **must** run in its own `bun test` invocation to avoid polluting other tests.

When adding a test for a bundled adapter that mocks modules, put it in its own
`testGroups` entry in [`packages/adapters/package.json`](../../../package.json).
The package test runner starts each group in a separate process. Do not combine tests
that mock the same modules differently (such as `@archon/paths` or `@archon/git`).

### Lazy logger pattern

Always use a module-level `cachedLog` + `getLog()` getter so test mocks can intercept `createLogger` before the logger is instantiated:

```typescript
let cachedLog: ReturnType<typeof createLogger> | undefined;
function getLog(): ReturnType<typeof createLogger> {
  if (!cachedLog) cachedLog = createLogger('adapter.my-forge');
  return cachedLog;
}
```

### Log event naming

Follow the `{domain}.{action}_{state}` convention. Standard states: `_started`, `_completed`, `_failed`. Always pair `_started` with `_completed` or `_failed`.

```typescript
// ✅ CORRECT
getLog().info({ conversationId }, 'adapter.comment_post_completed');
getLog().error({ err, conversationId }, 'adapter.comment_post_failed');

// ❌ WRONG
getLog().info({ conversationId }, 'comment_posted');
getLog().error({ err }, 'error_posting');
```

## Reference

See the GitHub adapter (`packages/adapters/src/forge/github/`) for a complete working example.

---

### Overview (`packages/providers/src/codex/protocol/README.md`)

# Codex app-server protocol types

AUTO-GENERATED from `@openai/codex` 0.163.0 by `scripts/generate-codex-protocol.ts`. Do not edit.

Regenerate with `bun run generate:codex-protocol`; the version stamp is checked by `scripts/codex-protocol-version.test.ts`.

---

### Overview (`packages/web/src/experiments/console/README.md`)

# Console

The console is Archon's only shipped Web application. Its historical directory
name remains in place to avoid a mechanical move while the builder is changing.

## Routes

- `/console` → all runs
- `/console/settings` → assistant, provider, system, and identity settings
- `/console/builder` → experimental workflow builder and project picker
- `/console/builder/:name` → edit a project workflow selected by
  `?project=<id>`
- `/console/r/:runId` → run detail without requiring a project URL
- `/console/p/:projectId` → project runs
- `/console/p/:projectId/chat` → project operator chat
- `/console/p/:projectId/r/:runId` → project-scoped run detail

## Ownership

- Console API calls live in `skills/`.
- Reactive data lives in `store/cache.ts`.
- Generated API shapes come from `@/lib/api.generated`.
- Shared application code is limited to authentication, generated API types,
  node-reference parsing, IDE links, and global styling.
- The `builder/` subtree remains experimental and keeps its own pure model,
  validation, editor, and serialization layers.

## Chat behavior

The composer accepts up to five files of 10 MB each. A new conversation must be
created with a text-only first message because conversation creation uses JSON;
the UI asks the operator to attach files on the next turn.

On authenticated installations, the console requests the signed-in user's
project conversation and sends the active identity with each turn. Solo
installations operate without an identity.

## Persisted view preferences

| Key                              | Default | Purpose                                |
| -------------------------------- | ------- | -------------------------------------- |
| `archon.console.detailView`      | `log`   | Run-detail tab                         |
| `archon.console.showToolCalls`   | `1`     | Show tool calls in the stream          |
| `archon.console.showSystem`      | `0`     | Show system events                     |
| `archon.console.runNodeFilter`   | `all`   | Filter the run stream by node          |
| `archon.console.railWidth`       | unset   | Project rail width                     |
| `archon.console.lastWorkflow`    | unset   | Last selected workflow                 |
| `archon.console.builderProject`  | unset   | Builder project selection              |
| `archon.console.attentionAlerts` | `0`     | Alert when a run needs you or finishes |

Local storage reads are guarded and fall back to these defaults.

### Closed-console notifications

Settings → Alerts has a separate opt-in for Web Push. It requires an authenticated web identity, HTTPS or localhost, notification permission, and a browser with Push API support. Every opted-in browser receives run attention notices for this install. Notification clicks open `/runs/<id>`. On iOS, install the site as a home-screen web app first.

The first subscription quietly baselines existing pauses. Delivery is recorded per destination, after sending. A crash or a partial delivery failure can re-send to browsers that already received the notice; notifications use `tag: runId` so duplicates replace each other. Turning the toggle off deletes the subscription and unsubscribes the browser. The in-tab alert toggle stays independent.

---

### Overview (`packages/web/src/experiments/console/builder/README.md`)

# Workflow builder

The workflow builder is an experimental authoring surface inside the Archon
console. `/console/builder` selects a project and workflow;
`/console/builder/:name?project=<id>` opens the editor.

## Supported editing surface

The builder provides a React Flow canvas, node palette, inspector, validation
panel, and read-only YAML preview. It directly represents these node variants:

- prompt
- command
- bash
- script
- loop
- approval
- wait
- cancel

The builder does not represent include directives, `loop_group` nodes, or
`workflow` sub-run nodes. Use YAML for workflows that contain those forms.

Use the builder to draft new workflows from the supported node forms, validate
them, and inspect the generated YAML. Editing an existing workflow and relying on
a load/edit/save round trip is not supported until
[#3378](https://github.com/coleam00/Archon/pull/3378) and
[#3379](https://github.com/coleam00/Archon/pull/3379) land. Edit existing
workflows as YAML. Bundled workflows open read-only. The editor warns about
unsaved changes on reload and on its own navigation controls. Browser Back and
navigation through the project rail do not currently run that guard.

## Ownership

- `types/` owns builder and generated wire type aliases.
- `variants/` converts each supported node variant.
- `model/` imports and exports workflow definitions.
- `validation/` performs synchronous client validation.
- `flow/`, `editor/`, and `yaml/` contain pure canvas, history, and serialization
  logic.
- `components/` and `BuilderPage.tsx` render the controlled editor.
- `BuilderConnected.tsx` and `connect/` own project selection, API calls, and
  navigation.

Runtime API calls go through console skills, and reactive server state goes
through `store/cache.ts`. Generated API shapes are imported type-only through
`types/wire.ts`.

The builder's unit tests focus on pure conversions, validation, serialization,
history, clipboard, and layout logic. `/console/_preview` provides fixture-backed
visual examples.

## Current round-trip limit

The unit fixtures exercise conversion for the builder's supported generated
shape. They do not prove a lossless round trip for existing authored workflow
files. The normalized loader can discard authored structure, including fields
the builder does not represent. Use YAML to edit existing workflows until the
linked fixes land. Canvas positions are UI state and never enter workflow YAML.

Generated YAML normalizes key order. Workflow names are tied to their filenames.
Workflows in nested `.archon/workflows/` directories cannot be loaded through
the single-name route.

---

### Packages Docs Web Src Content Docs Adapters Index (`packages/docs-web/src/content/docs/adapters/index.md`)

---
title: Platform Adapters
description: Overview of all platform adapters available for connecting to Archon.
category: adapters
area: adapters
audience: [user, operator]
status: current
sidebar:
  order: 0
---

Archon supports multiple platform adapters. Each adapter connects Archon to a different communication channel, letting you trigger workflows and interact with AI agents from wherever you work.

## Core Adapters

| Adapter | Transport | Auth Required | Setup |
|---------|-----------|---------------|-------|
| [**Web UI**](/adapters/web/) | SSE streaming | None | Built-in |
| [**CLI**](/reference/cli/) | stdout | None | Built-in |
| [**Slack**](/adapters/slack/) | Socket Mode, as a chat plugin | Plugin + Bot and App tokens | [Setup guide](/adapters/slack/) |
| [**Telegram**](/adapters/telegram/) (deprecated) | Bot API polling | Bot token | [Setup guide](/adapters/telegram/) |
| [**GitHub**](/adapters/github/) | Webhooks | Token + webhook secret | [Setup guide](/adapters/github/) |

## Community Adapters

Community adapters follow the same `IPlatformAdapter` interface but target platforms outside the core set.

| Adapter | Transport | Auth Required | Setup |
|---------|-----------|---------------|-------|
| [**Discord**](/adapters/community/discord/) | WebSocket | Bot token | [Setup guide](/adapters/community/discord/) |
| [**Gitea**](/adapters/community/gitea/) | Webhooks | Token + webhook secret | [Setup guide](/adapters/community/gitea/) |
| [**GitLab**](/adapters/community/gitlab/) | Webhooks | Token + webhook secret | [Setup guide](/adapters/community/gitlab/) |

## How Adapters Work

Slack runs as a chat plugin: a separate process the server starts from an installed plugin, speaking the chat plugin protocol. The other adapters are built into the server. All bundled adapters implement the `IPlatformAdapter` interface. They handle:

- **Message ingestion** -- receiving messages from the platform and forwarding them to Archon's orchestrator
- **Response delivery** -- streaming or batching AI responses back to the platform
- **Authorization** -- optional user whitelists to restrict access
- **Conversation tracking** -- mapping platform-specific identifiers (thread IDs, chat IDs, issue numbers) to Archon conversations

## Choosing an Adapter

- **Web UI** is the fastest way to get started -- no tokens or external services needed.
- **Slack** is ideal for mobile access and team collaboration.
- **GitHub** integrates directly into your issue and PR workflow.
- **Discord** works well for community or team servers.

You can run multiple adapters simultaneously. Any bundled adapter with the required environment variables set will start automatically when you launch the server. Slack also needs its plugin installed; see the [Slack guide](/adapters/slack/).

---

### Packages Docs Web Src Content Docs Brand Index (`packages/docs-web/src/content/docs/brand/index.md`)

---
title: Brand
description: Archon brand foundation — colors, logo, and usage guidelines.
---

## Quick reference

The essentials at a glance:

- **Primary gradient** — <code style="color:#ED10EC">#ED10EC</code> → <code style="color:#8E40C8">#8E40C8</code> → <code style="color:#06CE94">#06CE94</code>
- **Surface** — <code style="color:#0F1115;background:#fff;padding:0 6px;border-radius:3px">#0F1115</code>
- **Logo** — the shield mark with the abstract Archon glyph, stroked with the primary gradient

Use the embedded foundation below for the canonical version. If you need a vector asset or have a question about an unusual usage, open a discussion on [GitHub](https://github.com/coleam00/Archon/discussions).

## Full brand sheet

The Archon brand foundation lives below. It's a single self-contained page covering the logo, color system, typography, and approved usage.

<iframe
  src="/brand/foundation.html"
  title="Archon Brand Foundation"
  style="width:100%;height:80vh;border:1px solid var(--sl-color-gray-5);border-radius:8px;background:#0F1115;"
  loading="lazy"
></iframe>

Prefer a full-window view? [Open the brand sheet in a new tab](/brand/foundation.html).

---

### Packages Docs Web Src Content Docs Contributing Index (`packages/docs-web/src/content/docs/contributing/index.md`)

---
title: Contributing
description: Developer guides for contributing to Archon — internals, testing, releasing.
category: contributing
audience: [developer]
sidebar:
  order: 0
---

Guides for developers contributing to the Archon codebase.

## Developer Guides

- **[New Developer Guide](/contributing/new-developer-guide/)** — Codebase orientation, architecture overview, and first steps
- **[CLI Internals](/contributing/cli-internals/)** — CLI package internals, command routing, and worktree logic
- **[Releasing](/contributing/releasing/)** — Version management and release process
- **[DX Quirks](/contributing/dx-quirks/)** — Known development experience issues and workarounds

---

### Packages Docs Web Src Content Docs Deployment Index (`packages/docs-web/src/content/docs/deployment/index.md`)

---
title: Deployment Overview
description: Overview of deployment options for running Archon locally, with Docker, or on a cloud VPS.
category: deployment
area: infra
audience: [operator]
status: current
sidebar:
  order: 0
---

Archon can run locally for development or be deployed to a server for always-on operation.

## Deployment Options

| Method | Best For | Guide |
|--------|----------|-------|
| **Local** | Development, personal use | [Local Development](/deployment/local/) |
| **Docker** | Self-hosted servers, CI environments | [Docker](/deployment/docker/) |
| **Cloud VPS** | 24/7 operation with automatic HTTPS | [Cloud Deployment](/deployment/cloud/) |
| **Windows** | Native Windows or WSL2 | [Windows](/deployment/windows/) |

Using an existing proxy? See [Running Archon behind your own reverse proxy](/deployment/reverse-proxy/) for routing, authentication, webhook, and streaming requirements.

## Database Options

| Option | Setup | Best For |
|--------|-------|----------|
| **SQLite** (default) | Zero config, just omit `DATABASE_URL` | Single-user, CLI usage, local development |
| **Remote PostgreSQL** | Set `DATABASE_URL` to hosted DB | Cloud deployments, shared access |
| **Local PostgreSQL** | Docker `--profile with-db` | Self-hosted, Docker-based setups |

SQLite stores data at `~/.archon/archon.db` (or `/.archon/archon.db` in Docker). It is auto-initialized on first run.

## Testing

| Guide | Audience |
|-------|----------|
| [E2E Testing](/deployment/e2e-testing/) | Developers and operators |
| [E2E Testing on WSL](/deployment/e2e-testing-wsl/) | Developers on Windows |

---

### Packages Docs Web Src Content Docs Guides Index (`packages/docs-web/src/content/docs/guides/index.md`)

---
title: Guides
description: How-to guides for authoring workflows, commands, and configuring node features in Archon.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 0
---

How-to guides for building and running AI coding workflows with Archon.

## Workflow Authoring

- [Build your first workflow](/getting-started/first-workflow/) — Build, validate, dry-run and test a workflow with an approval gate
- [Workflow language](/reference/workflow-language/) — Every workflow field, node type and engine behavior
- [Authoring Commands](/guides/authoring-commands/) — Write prompt templates that serve as building blocks for workflow nodes

## Node Types

- [Loop Nodes](/guides/loop-nodes/) — Iterative AI execution with completion conditions and deterministic exit checks
- [Approval Nodes](/guides/approval-nodes/) — Human review gates with optional AI rework on rejection
- [Script Nodes](/guides/script-nodes/) — TypeScript/JavaScript (bun) or Python (uv) as a deterministic DAG node, without AI

## Automation

- [Workflow Triggers](/guides/workflow-triggers/) — Start workflows from schedules and verified GitHub events
- [Proactive Workflows](/guides/proactive-workflows/) — Let a cheap agent decide whether an event matters, then keep a person on the loop before the run acts

## Node Features

Support varies by provider. See the [provider capability matrix](/reference/provider-capabilities/) for each feature.

- [Per-Node Hooks](/guides/hooks/) — Attach Claude SDK hooks for tool control, context injection, and input modification
- [Per-Node MCP Servers](/guides/mcp-servers/) — Connect external tools (GitHub, Postgres, etc.) to individual nodes
- [Per-Node Skills](/guides/skills/) — Preload specialized knowledge into node agents

## Bundled Workflows

Archon ships the `sdlc` workflow pack, a software factory from an incoming issue to a merged pull request. [Set up the SDLC factory](/getting-started/sdlc-factory/) runs it on your repository, and [SDLC pack workflows](/reference/sdlc-workflows/) describes each workflow.

To customize the pack, copy the whole `.archon/workflows/sdlc/` folder from the Archon repository into your project's `.archon/workflows/` and modify it -- same-named workflows override the bundled ones. Copy the pack rather than one workflow folder: its workflows include each other and share scripts in `sdlc/.shared/`.

## Advanced

- [Global Workflows](/guides/global-workflows/) — User-level workflows that apply to every project
- [Multi-Repo Projects](/guides/multi-repo-projects/) — Drive many service repos under one folder-project root

---

### Packages Docs Web Src Content Docs Reference Index (`packages/docs-web/src/content/docs/reference/index.md`)

---
title: Reference
description: Technical reference for Archon's architecture, CLI, commands, database, configuration, and troubleshooting.
category: reference
audience: [developer]
sidebar:
  order: 0
---

Technical reference documentation for Archon internals.

## Reference Docs

- **[Architecture](/reference/architecture/)** -- How the packages fit together, with a link to the maintained architecture map
- **[Archon Directories](/reference/archon-directories/)** -- Directory structure, path resolution, configuration system
- **[CLI Reference](/reference/cli/)** -- All CLI commands, flags, and usage examples
- **[Commands Reference](/reference/commands/)** -- Chat commands and how to submit them in each adapter
- **[Workflow language](/reference/workflow-language/)** -- Workflow fields, node types, gates, composition and result contracts
- **[Variables Reference](/reference/variables/)** -- Variable substitution in commands and workflows ($ARGUMENTS, $BASE_BRANCH, etc.)
- **[API Reference](/reference/api/)** -- REST API endpoints for programmatic access
- **[Database](/reference/database/)** -- Schema, migrations, SQLite/PostgreSQL setup
- **[Configuration Reference](/reference/configuration/)** -- Full config.yaml options, environment variables, streaming modes
- **[Security](/reference/security/)** -- Permission model, authorization, webhook verification, data privacy
- **[Troubleshooting](/reference/troubleshooting/)** -- Common issues and solutions

## For AI Tools

Point your AI assistant at these URLs for accurate, up-to-date documentation:

- [`/llms.txt`](https://archon.diy/llms.txt) -- Index of all documentation pages
- [`/llms-full.txt`](https://archon.diy/llms-full.txt) -- Full documentation in a single file
- [`/llms-small.txt`](https://archon.diy/llms-small.txt) -- Trimmed documentation variant
- `/<page-path>.md` -- Raw source for Markdown documentation pages (e.g., [`/getting-started/installation.md`](https://archon.diy/getting-started/installation.md)); use `/llms-full.txt` for pages authored in MDX

---

### .Claude Docs Adapter Implementation Guide (`.claude/docs/adapter-implementation-guide.md`)

# Adapter Implementation Guide

> **Purpose**: Reference for maintaining existing bundled platform adapters.
> **When to use**: Debugging bundled adapter behavior and understanding cross-platform message flow.

New chat platforms ship as plugins in contributor-owned repositories using `ChatPlugin` from `@archon/chat-contract`. Follow the [chat plugin publishing guide](../../packages/docs-web/src/content/docs/guides/publishing-plugins.md#chat-plugins) and the [chat contract](../../packages/chat-contract/README.md). New forges use the [forge plugin publishing guide](../../packages/docs-web/src/content/docs/guides/publishing-plugins.md#tags-and-executable-release-assets). Do not add a bundled adapter or register a new platform in the server.

---

## The Interface: `IPlatformAdapter`

Defined at `packages/core/src/types/index.ts` — search for `export interface IPlatformAdapter`.

| Method | Signature | Required |
|--------|-----------|----------|
| `capabilities` | `{ messagePersistence: 'core' \| 'adapter'; defaultWorkflowDispatch: 'foreground' \| 'background' }` (readonly property) | Yes |
| `sendMessage` | `(conversationId: string, message: string, metadata?: MessageMetadata): Promise<void>` | Yes |
| `ensureThread` | `(originalConversationId: string, messageContext?: unknown): Promise<string>` | Yes |
| `getStreamingMode` | `(): 'stream' \| 'batch'` | Yes |
| `getPlatformType` | `(): string` | Yes |
| `start` | `(): Promise<void>` | Yes |
| `stop` | `(): void` | Yes |
| `sendStructuredEvent` | `(conversationId: string, event: MessageChunk): Promise<void>` | Optional |
| `emitRetract` | `(conversationId: string): Promise<void>` | Optional |
| `prepareBackgroundConversation` | `(context: { workerConversationId; parentConversationId; conversationDbId }): Promise<() => Promise<void>>` — runs before a background worker starts; core awaits the returned finalizer when dispatch setup or the worker ends | Optional |

`WebAdapter` (`packages/server/src/adapters/web.ts`) adds web-only methods such as `setConversationDbId` and `emitLockEvent`. The server holds the concrete `WebAdapter` and calls them directly; core sees only `IPlatformAdapter`.

---

## Common Template (All Adapters Share)

### 1. Lazy Logger Pattern

Every adapter file uses this at module scope:

```typescript
let cachedLog: ReturnType<typeof createLogger> | undefined;
function getLog(): ReturnType<typeof createLogger> {
  if (!cachedLog) cachedLog = createLogger('adapter.<name>');
  return cachedLog;
}
```

Never initialize the logger at module scope — test mocks must intercept `createLogger` before first use.

### 2. Auth: Constructor Whitelist + Silent Rejection

Every adapter reads its env var in the constructor and stores the parsed whitelist:

| Adapter | Env Var | ID Type | Auth File |
|---------|---------|---------|-----------|
| Slack | `SLACK_ALLOWED_USER_IDS` | descriptor `allowlist`, enforced by the chat host | `packages/chat-slack/src/descriptor.ts` |
| Telegram | `TELEGRAM_ALLOWED_USER_IDS` | `number[]` (positive integers) | `chat/telegram/auth.ts` |
| GitHub | `GITHUB_ALLOWED_USERS` | `string[]` (lowercase normalized) | `forge/github/auth.ts` |
| Discord | `DISCORD_ALLOWED_USER_IDS` | `string[]` (numeric snowflakes) | `community/chat/discord/auth.ts` |

Each auth module exports `parseAllowedXxx(envValue)` and `isXxxAuthorized(userId, allowedIds)`. Empty whitelist = open access. On failure: log with `userId.slice(0, 4) + '***'` masking, then `return` silently (no error sent to user).

### 3. Message Handler Registration

```typescript
// In adapter class
onMessage(handler: MessageHandler): void {
  this.messageHandler = handler;
}

// In start() — fire-and-forget
void this.messageHandler(event);
```

### 4. Message Splitting

Bundled adapters use `splitIntoParagraphChunks(message, maxLength)` from `packages/adapters/src/utils/message-splitting.ts`:

| Platform | Limit | Buffer | Effective Split At |
|----------|-------|--------|-------------------|
| Telegram | 4,096 | 200 | 3,896 |
| GitHub | 65,000 | 500 | 64,500 |
| Discord | 2,000 | 100 | 1,900 |

Two-pass algorithm: split on `\n\n+` first, then `\n` for oversized paragraphs.

### 5. `ensureThread()` is Usually a No-op

Only Discord creates real threads. Telegram and GitHub have no thread concept (or use inherent threading).

---

## Per-Adapter Deep Dive

### Slack (chat plugin)

Slack is not a bundled adapter. It ships as the `@archon/chat-slack` chat plugin (`packages/chat-slack/`), which the server hosts through `@archon/chat-contract`. Its allowlist row above points at its descriptor; see `guides/publishing-plugins.md#chat-plugins` for the contract.

---

### Telegram (`TelegramAdapter`)

**File:** `packages/adapters/src/chat/telegram/adapter.ts`

**Receiving:** Long polling via Telegraf `bot.launch()`. Constructor sets `handlerTimeout: Infinity` (line 31) to avoid 90s timeout on long AI operations. Launches with `dropPendingUpdates: true` to discard offline messages.

**Conversation ID:** Numeric chat ID as string (`ctx.chat.id.toString()`).

**Sending:** Two-stage markdown: converts to MarkdownV2 via `convertToTelegramMarkdown()`, sends with `parse_mode: 'MarkdownV2'`, falls back to `stripMarkdown()` on rejection. Helper functions in `chat/telegram/markdown.ts`.

**Streaming mode:** `'stream'` by default, overridden via `TELEGRAM_STREAMING_MODE`.

**Unique:**
- Auth IDs are `number[]` (Telegram user IDs are numeric)
- `handlerTimeout: Infinity` for long-running AI operations
- `dropPendingUpdates: true` on launch

---

### GitHub (`GitHubAdapter`)

**File:** `packages/adapters/src/forge/github/adapter.ts`, which delegates to `webhook-intake.ts`, `check-run-signal.ts`, `registration.ts`, `repository-readiness.ts` and `comment-delivery.ts` in the same folder.

**Receiving:** Webhooks. `start()` is a no-op. Server registers `POST /webhooks/github` which calls `receiveWebhook()`; conversational events run `handleWebhook()` in the background.

**Webhook processing flow (`handleWebhook()`):**
1. HMAC-SHA256 signature verification
2. Auth check against `GITHUB_ALLOWED_USERS` (username from `event.sender.login`)
3. `parseWebhookEvent()` extracts fields — **only handles `issue_comment.created`, `issues.closed`, `pull_request.closed`** — NOT `issues.opened` or `pull_request.opened` (descriptions contain docs, not commands; see #96)
4. Self-loop prevention: ignores comments containing ``
5. Bot mention check via `hasMention()`
6. `getOrCreateCodebaseForRepo()` upserts codebase record
7. `ensureRepoReady()` clones (new) or syncs (existing) the repository
8. Builds `IsolationHints` for PR/issue workflows

**Conversation ID:** `owner/repo#number` (e.g., `acme/api#42`).

**Sending:** `octokit.rest.issues.createComment` with retry logic (3 attempts, exponential backoff). Appends `` marker to every comment.

**Streaming mode:** Always `'batch'` (hardcoded).

**Unique:**
- Only adapter with its own `ConversationLockManager` (injected in constructor)
- Only adapter that calls `handleMessage()` directly (not via server.ts)
- Has `ensureRepoReady()` for on-demand repo clone/sync
- Builds `IsolationHints` with PR branch, SHA, fork detection

---

### Discord (`DiscordAdapter`)

**File:** `packages/adapters/src/community/chat/discord/adapter.ts`

**Receiving:** discord.js WebSocket via `client.login()`. Gateway intents: `Guilds`, `GuildMessages`, `MessageContent`, `DirectMessages`. `Partials.Channel` required for DM support. Registers `Events.MessageCreate` listener in `start()`.

**Conversation ID:** Channel ID string (Discord snowflake). Thread messages use the thread's own channel ID, so each thread is a separate conversation automatically.

**`ensureThread()` — The only adapter with real implementation (line 195):**
1. If already in a thread → return thread's `channelId`
2. If in a DM → no threading, return unchanged
3. Deduplication: checks `this.pendingThreads` map to prevent double creation
4. Creates thread via `message.startThread()` with `autoArchiveDuration: OneDay`
5. Thread name: first 97 chars of message content, fallback `'Bot Response'`

**Streaming mode:** `'stream'` by default, overridden via `DISCORD_STREAMING_MODE`.

**Unique:**
- Real thread creation with deduplication map
- `isBotMentioned(message)` checks `message.mentions.has(botUser)`
- 2,000-char message limit (smallest of all platforms)

---

### Web (`WebAdapter`)

**File:** `packages/server/src/adapters/web.ts`

**Receiving:** None — purely output-driven. Messages arrive via REST API (`POST /api/conversations/:id/message`). The adapter's job is SSE streaming + message persistence.

**Architecture:** Facade over three injected components:
- `SSETransport` (`web/transport.ts`): manages `Map<string, SSEWriter>` of active connections
- `MessagePersistence` (`web/persistence.ts`): buffers text + tool calls, flushes to DB
- `WorkflowEventBridge` (`web/workflow-bridge.ts`): forwards workflow events from workers to parent SSE

**`sendMessage()` (line 44):**
1. Calls `persistence.appendText()` first (always buffer for DB)
2. Skips SSE for `tool_call_formatted` and `isolation_context` categories
3. Emits `{ type: 'text', content, isComplete: true }` via `SSETransport.emit()`
4. Forces immediate `persistence.flush()` for `workflow_result` category

**`sendStructuredEvent()` (line 81):** Handles `'tool'`, `'result'` (session info), `'workflow_dispatch'` message chunks.

**`emitRetract()` (line 212):** Removes last buffered segment from persistence, emits `{ type: 'retract' }` over SSE.

**Streaming mode:** Always `'stream'` (hardcoded).

**Unique:**
- No auth (single-developer tool on local machine)
- No polling/WebSocket — output-only
- Message persistence layer (other adapters don't persist)
- `emitLockEvent()` for UI lock indicators
- `SSETransport.scheduleCleanup()` with `RECONNECT_GRACE_MS = 5000ms`

---

## How Adapters Are Registered (`server/src/index.ts`)

```
1. Check env var: Boolean(process.env.XYZ_BOT_TOKEN)
2. Instantiate: new XyzAdapter(token, streamingMode)
3. Register handler: adapter.onMessage(async event => {
     conversationId = getConversationId(event)
     conversationId = await ensureThread(conversationId, event)
     history = await fetchThreadHistory(event)  // optional
     await lockManager.acquireLock(conversationId, async () => {
       await handleMessage(adapter, conversationId, content, context)
     })
   })
4. Start: await adapter.start()
```

GitHub is different — self-contained, handles its own lock acquisition internally.
Web is always enabled (not conditional on env vars).

---

---

### .Claude Docs Isolation And Worktree Guide (`.claude/docs/isolation-and-worktree-guide.md`)

# Isolation & Worktree System Guide

> **Purpose**: Complete reference for how Archon creates, manages, and cleans up git worktrees for parallel development.
> **When to use**: Working on worktree lifecycle, debugging isolation issues, understanding the resolution algorithm.
> **Size**: ~350 lines — use a scout sub-agent to check relevance before loading.

---

## Branded Types (`packages/git/src/types.ts`)

Three branded string types prevent mixing up paths at compile time:

| Type | Factory | Rejects |
|------|---------|---------|
| `RepoPath` | `toRepoPath(s)` | empty string |
| `BranchName` | `toBranchName(s)` | empty string |
| `WorktreePath` | `toWorktreePath(s)` | empty string |

All factories throw `Error` on empty input. Always use these — never pass raw strings where branded types are expected.

---

## Isolation Request Types (`packages/isolation/src/types.ts`)

Five request types, all extending `IsolationRequestBase` (which carries `codebaseId`, `canonicalRepoPath: RepoPath`, optional `description`):

| Type | `workflowType` | Branch Pattern | Usage |
|------|---------------|---------------|-------|
| `IssueIsolationRequest` | `'issue'` | `issue-{identifier}` | GitHub issue |
| `PRIsolationRequest` | `'pr'` | actual PR branch (same-repo) or `pr-{N}-review` (fork) | GitHub PR |
| `ReviewIsolationRequest` | `'review'` | `review-{identifier}` | PR review workflow |
| `ThreadIsolationRequest` | `'thread'` | `thread-{8-hex-hash}` | Slack/Telegram/Discord |
| `TaskIsolationRequest` | `'task'` | `task-{slugified}` (max 50 chars) | CLI or manual task |

`IsolationHints` carries the same data loosely typed, flowing from adapters through to the resolver.

---

## The 7-Step Resolution Order (`packages/isolation/src/resolver.ts`)

`IsolationResolver.resolve()` executes these steps in strict order. Each can short-circuit.

### Step 1: Existing Environment Reference
If `request.existingEnvId` is set, checks DB row + filesystem path via `worktreeExists()`. If both valid → `resolved`. If DB exists but filesystem gone → `stale_cleaned` (caller retries).

### Step 2: No Codebase
If `request.codebase` is `null` → `{ status: 'none', cwd: '/workspace' }`.

### Step 3: Workflow Reuse
`store.findActiveByWorkflow(codebaseId, workflowType, workflowId)` — finds existing env with same workflow identity. If filesystem valid → reuse it. If filesystem gone → mark destroyed, fall through.

### Step 4: Linked Issue Sharing
When `hints.linkedIssues` is non-empty, iterates issue numbers and tries `findActiveByWorkflow(codebaseId, 'issue', issueNum)`. First live match → reuse.

### Step 5: PR Branch Adoption
When `hints.prBranch` is set, calls `findWorktreeByBranch(canonicalPath, prBranch)` to scan `git worktree list --porcelain`. If found and path exists → creates DB row with `metadata: { adopted: true, adopted_from: 'skill' }`.

### Step 6: Limit Check + Auto-cleanup
Compares `store.countActiveByCodebase()` against `maxWorktrees` (default 25). If at limit → tries `cleanup.makeRoom()` (removes merged branches). If still at limit → `blocked` with formatted user message.

### Step 7: Create New Environment
1. Constructs concrete `IsolationRequest` from `workflowType` + hints
2. Calls `provider.create(isolationRequest)`
3. On known error → `blocked` with classified message
4. On unknown error → re-throws (crash)
5. On success → `store.create()` to register
6. If `store.create()` fails → destroys orphaned worktree before re-throwing

---

## Worktree Creation Flow (`packages/isolation/src/providers/worktree/create.ts`)

`WorktreeProvider.create(request)` delegates to `createWorktreeEnvironment()`:

**1. Generate names + check adoption**
- `generateBranchName(request)` → branch name based on request type
- `getWorktreePath(request, branchName)` → filesystem path
- `findExisting()` → checks if path already exists or PR branch already has a worktree

**2. Sync workspace**
- Calls `syncWorkspace(repoPath, baseBranch)` from `@archon/git/repo.ts`
- Runs `git fetch origin <branch>` (60s timeout)
- Auto-detects default branch: `symbolic-ref` → `origin/main` → `master`

**3. Create worktree**
- For PRs (same-repo): `git fetch origin prBranch` → `git worktree add path -b prBranch origin/prBranch`
- For PRs (fork): fetch `refs/pull/{N}/head` → create at SHA → checkout branch
- For all others: `git worktree add path -b branchName origin/{baseBranch}`
- Before creation: `cleanOrphanDirectoryIfExists()` removes stale non-worktree directories

**4. Copy configured files**
- Default: always copies `.archon/` directory
- User config: additional paths from `.archon/config.yaml` `worktree.copyFiles`
- Supports `"source -> destination"` arrow syntax
- Path traversal blocked (security check via `isPathWithinRoot()`)

---

## Worktree Path Layout

`getWorktreeBase()` in `packages/git/src/worktree.ts:29`:

- **Project-scoped** (repo under `~/.archon/workspaces/owner/repo/`):
  - Base: `~/.archon/workspaces/owner/repo/worktrees/`
  - Path: `<base>/<branchName>`

- **Legacy global** (repo elsewhere):
  - Base: `~/.archon/worktrees/`
  - Path: `<base>/owner/repo/<branchName>`

---

## Worktree Cleanup Flow

### `removeEnvironment()` (`packages/core/src/services/cleanup/remove-environment.ts`)

1. Fetch DB row. If `status === 'destroyed'` → return (idempotent)
2. Get `canonicalRepoPath` from codebase record
3. Check `worktreeExists()` for the path
4. If exists and `force` is false → check `hasUncommittedChanges()` (fail-safe: returns `true` on unexpected errors). If changes exist → skip removal
5. Call `provider.destroy(path, options)`

### `WorktreeProvider.destroy()` (`providers/worktree/destroy.ts`)

Returns `DestroyResult` tracking partial failures:

1. If path already gone → `worktreeRemoved: true`
2. If path exists → `git worktree remove [--force] path`
3. If directory persists after git removal (untracked files) → `rm -rf`
4. If `branchName` provided → `git branch -D branchName`
5. If `deleteRemoteBranch: true` → `git push origin --delete branchName`

---

## Error Classification (`packages/isolation/src/errors.ts`)

Isolation failures carry a typed kind set where they are thrown; message text never decides a kind. `IsolationFailure` and `GitFailure` carry a `kind`, `ConfiguredBaseBranchNotFoundError` is its own class, and filesystem errno (`EACCES`/`EPERM`, `ENOSPC`, `ETIMEDOUT`) maps directly. `failureKind()` follows the `cause` chain, so a wrapped failure keeps its kind.

`classifyIsolationError(err)` turns the kind into the user message, for example `permission_denied`, `timeout`, `no_space`, `not_repository`, `docker_unreachable` and `runner_image_missing`. Kinds without a static message (`branch_exists`, `configured_branch_missing`, ...) show the error's own message, which names the branch or path.

`isKnownIsolationError(err)` decides: any kind other than `unclassified` → produce the user message and block. An `unclassified` Git or Docker failure, or a programming error, is re-thrown as a crash with its original operation, stderr and exit code.

`IsolationBlockedError` signals ALL message handling must stop — the user was already notified.

---

## The Adoption Pattern

Two entry points for adopting worktrees created by external skills:

**Path-based adoption** (inside `WorktreeProvider.create()`):
- Before any git operations, checks if the expected worktree path already exists
- Also checks `findWorktreeByBranch()` for PR branch matches
- Returns adopted environment with `metadata: { adopted: true }`

**DB-level adoption** (in `IsolationResolver.tryBranchAdoption()`):
- Uses `findWorktreeByBranch()` to scan git's worktree list
- Creates DB row without calling `provider.create()` at all
- Returns `{ method: { type: 'branch_adoption', branch } }`

---

## Port Allocation (`packages/core/src/utils/port-allocation.ts`)

`getPort()`:
1. If `PORT` env var set → use it
2. If CWD is a worktree → MD5(cwd), `readUInt16BE(0) % 900 + 100` → port = `3090 + offset` (range: 3190-4089)
3. If not a worktree → `3090`

Same worktree always gets the same port (deterministic hash).

---

## Stale Environment Detection

`isEnvironmentStale(env, staleDays)` in `services/cleanup/scheduled-sweep.ts`:
1. Check `getLastCommitDate()` (`git log -1 --format=%ci`)
2. If recent commit exists and within threshold → not stale
3. Otherwise check `env.created_at` against threshold
4. Default threshold: 14 days (`STALE_THRESHOLD_DAYS`)

---

## Scheduled Cleanup

`runScheduledCleanup()` runs on startup + every 6 hours:

For each active environment:
1. Filesystem gone → `removeEnvironment()`
2. Merged (see "How cleanup decides a branch is merged") → remove (if no uncommitted changes, no workflow run can still claim it)
3. Open PR, failed PR lookup, or unverifiable merge state → kept and reported
4. Otherwise stale (14+ days, non-Telegram) → remove (same checks)

After environment cleanup: `sessionDb.deleteOldSessions(30)` (session retention).

---

## `makeRoom()` Strategy

When at worktree limit: only removes **merged branches** (not stale ones); it is `cleanupMergedWorktrees`, the same path as `archon isolation cleanup --merged`. For each environment:
- Judge the merge (below)
- Skip if uncommitted changes, or a workflow run can still claim it (running, pending, paused, or failed-but-resumable)
- Remove with `deleteRemoteBranch: true`

If merged cleanup frees space → proceed with creation. If not → block with formatted limit message showing breakdown of merged/stale/active counts.

### How cleanup decides a branch is merged

`judgeBranchForRemoval()` in `services/cleanup/merge-verdict.ts` is the one decision for the scheduled sweep, `makeRoom()` and `--merged`. Merged against the configured base ref means either:
- **git:** the branch ref exists and is merged by ancestry (`git branch --merged`) or patch equivalence (`git cherry`), and the worktree's HEAD also passes `git cherry <base> HEAD`; or
- **PR:** `getPrState()` reports MERGED (or CLOSED with `--include-closed`), and the worktree's HEAD and the branch ref, where each exists, are at or behind the PR head commit (`isRevCoveredBy`, which fetches a PR head missing locally).

Anything past the proving commit is unmerged work: run branch names get reused, and a detached HEAD can hold commits no ref protects. A failed `gh` lookup (`UNAVAILABLE`) or a git error keeps the worktree and reports it; it never counts as merged.

---

## Key Files

| Component | File |
|-----------|------|
| Branded types | `packages/git/src/types.ts` |
| `execFileAsync` wrapper | `packages/git/src/exec.ts` |
| Low-level worktree ops | `packages/git/src/worktree.ts` |
| Branch ops | `packages/git/src/branch.ts` |
| Repo ops (sync, clone) | `packages/git/src/repo.ts` |
| Isolation request types | `packages/isolation/src/types.ts` |
| `IIsolationStore` interface | `packages/isolation/src/store.ts` |
| Factory / singleton | `packages/isolation/src/factory.ts` |
| 7-step resolver | `packages/isolation/src/resolver.ts` |
| `WorktreeProvider` | `packages/isolation/src/providers/worktree.ts` |
| File copy utilities | `packages/isolation/src/worktree-copy.ts` |
| Error classification | `packages/isolation/src/errors.ts` |
| Cleanup service | `packages/core/src/services/cleanup-service.ts` (scheduled and stale sweeps, configuration), `packages/core/src/services/cleanup/` (one module per job) |
| Port allocation | `packages/core/src/utils/port-allocation.ts` |

---

### .Claude Docs Workflow Yaml Reference (`.claude/docs/workflow-yaml-reference.md`)

# Workflow YAML Reference

> **Purpose**: Complete specification of every field, option, and type in Archon's workflow system.
> **When to use**: Writing or debugging workflow YAML files, understanding execution modes, working on the workflow engine.
> **Size**: ~450 lines — use a scout sub-agent to check relevance before loading.

---

## Overview

Workflows are YAML files discovered from `.archon/workflows/` (recursively) plus bundled defaults. Each must have exactly one execution mode: `steps:`, `loop:` + `prompt:`, or `nodes:`. Parsed by `parseWorkflow()` in `packages/workflows/src/loader.ts:448`.

---

## Top-Level Fields (All Modes)

### `name` (required)
- **Type**: non-empty string
- **Used by**: Router for exact-match lookup; displayed in workflow list
- **Example**: `name: archon-ship`

### `description` (required)
- **Type**: non-empty string (multiline supported)
- **Used by**: Router prompt — this is the primary signal the AI uses to select a workflow. Include `Use when:` and `NOT for:` sections.

### `provider` (optional)
- **Type**: any registered provider id — `'claude'` | `'codex'` | `'pi'` | `'copilot'` | `'opencode'`
  (validated at load against the registry, so the list follows what is registered)
- **Default**: falls back to `.archon/config.yaml` assistants default (Claude)

### `model` (optional)
- **Type**: string — must be compatible with provider
- **Claude models**: `'sonnet'`, `'opus'`, `'haiku'`, `'inherit'`, or `'claude-*'`
- **Codex models**: anything that does NOT match Claude patterns
- **Validation**: incompatible provider/model fails loading

### `effort` (optional)
- **Type**: `'minimal'` | `'low'` | `'medium'` | `'high'` | `'xhigh'` | `'max'`
- **Applies to**: every provider with a reasoning control (Claude, Codex, Pi, Copilot).
  Pi takes all six; the others clamp a rung their SDK lacks to the nearest one
  they have (`max` → `xhigh` on Codex/Copilot; `minimal` → `low` on
  Claude/Copilot). OpenCode has none.
- **Also valid per-node**, where it overrides the workflow-level value.

### `modelReasoningEffort` (was Codex-only) — DEPRECATED
- **Type**: `'minimal'` | `'low'` | `'medium'` | `'high'` | `'xhigh'`
- **Use `effort:` instead.** Still accepted: the loader translates it into
  `effort:` and warns. If both are declared, `effort:` wins and this one is
  dropped. Will be removed.
- **Default**: from `.archon/config.yaml` `assistants.codex.modelReasoningEffort`
  (that config key is NOT deprecated)

### `webSearchMode` (optional, Codex only)
- **Type**: `'disabled'` | `'cached'` | `'live'`

### `additionalDirectories` (optional, Codex only)
- **Type**: `string[]` — absolute paths to other repos

---

## Steps Mode

```yaml
name: my-workflow
description: Sequential execution example
steps:
  - command: archon-plan
  - command: archon-implement
    clearContext: true
  - parallel:
      - command: archon-review-code
      - command: archon-review-tests
```

### Single Step Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `command` | string | required | Command file name (`.md` extension added automatically) |
| `clearContext` | boolean | `false` | `true` = fresh AI session for this step |
| `allowed_tools` | string[] | all tools | Claude only. `[]` = no built-in tools (MCP-only) |
| `denied_tools` | string[] | none | Claude only. Removes named tools from default set |
| `idle_timeout` | number (ms) | 300000 (5 min) | Per-step timeout for AI inactivity |

### Parallel Block

```yaml
- parallel:
    - command: task-a
    - command: task-b
```

All steps in a parallel block run concurrently via `Promise.all()`. Each gets a fresh AI session (no session sharing). Nested parallel blocks are rejected.

---

## Loop Mode

```yaml
name: my-loop
description: Iterative autonomous execution
loop:
  until: COMPLETE
  max_iterations: 10
  fresh_context: false
prompt: |
  Work on the task. Signal <promise>COMPLETE</promise> when done.
```

### Loop Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `loop.until` | string | required | Completion signal string |
| `loop.max_iterations` | number | required | Max iterations (>= 1) |
| `loop.fresh_context` | boolean | `false` | `true` = new session each iteration |
| `prompt` | string | required | Prompt template (supports `$VARIABLE` substitution) |

**Signal detection** supports two formats:
- `<promise>SIGNAL</promise>` (recommended, case-insensitive)
- Plain signal only as the trimmed final line of the output, with nothing else on it

---

## DAG Mode

```yaml
name: my-dag
description: Directed acyclic graph execution
provider: claude
nodes:
  - id: classify
    prompt: "Is this a bug? Answer JSON."
    output_format:
      type: object
      properties:
        type: { type: string, enum: ["BUG", "FEATURE"] }
      required: [type]
    allowed_tools: []
  - id: implement
    command: archon-implement
    depends_on: [classify]
    when: "$classify.output.type == 'BUG'"
  - id: lint
    bash: "bun run lint"
    depends_on: [implement]
```

Nodes are sorted topologically (Kahn's algorithm). Nodes in the same layer run concurrently via `Promise.allSettled`.

### Node Fields (All Types)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `id` | string | required | Unique identifier. Used in `$nodeId.output` references |
| `depends_on` | string[] | `[]` | IDs of upstream nodes. Determines execution order |
| `when` | string | always run | Condition expression (see below) |
| `trigger_rule` | string | `'all_success'` | Join semantics for upstream states |
| `provider` | string | inherited | Per-node provider override |
| `model` | string | inherited | Per-node model override |
| `idle_timeout` | number (ms) | 300000 | Inactivity timeout |

### Node Types (Mutually Exclusive)

**`command:`** — Named command file, AI-executed
```yaml
- id: plan
  command: create-plan
```

**`prompt:`** — Inline prompt string, AI-executed
```yaml
- id: classify
  prompt: "Classify this issue as BUG or FEATURE"
```

**`bash:`** — Shell script, no AI. Stdout captured as `$nodeId.output`
```yaml
- id: lint
  bash: "bun run lint 2>&1"
  timeout: 120000
```

### AI-Only Fields (command/prompt nodes)

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `context` | `'fresh'` | inherited | Forces new AI session |
| `output_format` | object | none | JSON Schema for structured output (Claude only) |
| `allowed_tools` | string[] | all | Tool whitelist (Claude only). `[]` = no tools |
| `denied_tools` | string[] | none | Tool blacklist (Claude only) |

### Bash-Only Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `timeout` | number (ms) | none | Total execution timeout for the subprocess. Omitted: the node runs until it exits |

---

## Trigger Rules

Controls when a node runs based on upstream states:

| Rule | Behavior |
|------|----------|
| `all_success` | All upstreams must be `completed` **(default)** |
| `one_success` | At least one upstream `completed` |
| `none_failed_min_one_success` | No upstream `failed` AND at least one `completed` |
| `all_done` | All upstreams finished (completed, failed, or skipped all count) |

---

## `when:` Condition Syntax

```yaml
when: "$classify.output.type == 'BUG'"
when: "$classify.output.complexity != 'trivial'"
```

**Pattern**: `$nodeId.output[.field] OPERATOR 'value'`
- **Operators**: `==` and `!=` only
- **Field access**: dot-notation into JSON from `output_format` nodes
- **Values**: single-quoted string literals
- **Fail behavior**: unparseable expressions → `false` (node skipped)

---

## Variable Substitution

### Standard Variables (all modes)

| Variable | Replaced With |
|----------|--------------|
| `$ARGUMENTS` | Full user message string |
| `$USER_MESSAGE` | Alias for `$ARGUMENTS` |
| `$WORKFLOW_ID` | Workflow run UUID |
| `$ARTIFACTS_DIR` | Absolute path to run artifacts directory |
| `$BASE_BRANCH` | The run's resolved or recorded base branch. See the [Variable Reference](https://archon.diy/reference/variables/#workflow-variables) for resolution and child inheritance rules |
| `$CONTEXT` / `$EXTERNAL_CONTEXT` / `$ISSUE_CONTEXT` | GitHub issue/PR context (empty string if none) |
| `$PLAN` | Previous plan from session metadata |
| `$IMPLEMENTATION_SUMMARY` | Previous execution summary |

### User Message Variables

| Variable | Replaced With |
|----------|--------------|
| `$ARGUMENTS` / `$USER_MESSAGE` | The user's whole trigger message (positional `$1`–`$9` are not supported) |

### DAG Node Output References

| Variable | Replaced With |
|----------|--------------|
| `$nodeId.output` | Full output string from completed node |
| `$nodeId.output.field` | JSON field value from structured output |

In `bash:` and `until_bash:`, small output references are single-quoted inline; large outputs use an unquoted file-read substitution. Assign each reference to a variable, then quote that variable when using it. See the [assignment-then-quote examples](../../packages/docs-web/src/content/docs/reference/variables.md#shell-quoting-in-bash-vs-script).

---

## Model Validation

Runs at load time. Invalid combinations fail workflow loading.

```
Claude models: 'sonnet', 'opus', 'haiku', 'inherit', or 'claude-*'
Codex models: anything NOT matching Claude patterns
```

Per-node overrides validated independently. If a node's model implies a provider (e.g., `'haiku'` → Claude), the provider is inferred.

---

## DAG Structural Validation

Four rules enforced at `loader.ts:370-439`:
1. **Unique IDs** — no duplicates
2. **Valid depends_on** — all referenced IDs must exist
3. **No cycles** — Kahn's algorithm; cycles fail with involved IDs
4. **Valid $nodeId.output references** — scanned in `when:` and `prompt:` fields

---

## Discovery & Loading

`discoverWorkflows(searchPaths, config)` in `loader.ts`:

1. Searches all paths recursively for `*.yaml` and `*.yml` files
2. Merges bundled defaults with repo-specific workflows (repo overrides by name)
3. One broken YAML doesn't abort discovery — errors returned in `WorkflowLoadResult.errors`
4. Opt-out: `defaults.loadDefaultWorkflows: false` in `.archon/config.yaml`

---

## Real Workflow Examples

| File | Mode | Key Features |
|------|------|-------------|
| `sdlc/ship/archon-ship-block.yaml` | `nodes:` | `include:` composition, `when:` routing on structured output, `trigger_rule: none_failed_min_one_success` |
| `sdlc/review/archon-review-block.yaml` | `nodes:` | Parallel `command:` fan-out with `context: fresh`, conditional lenses, `script:` publish |
| `sdlc/deliver/archon-deliver-block.yaml` | `nodes:` | Composed delivery tail, bounded correction loop, `script:` gates |

---

## Key Files

| Concern | File |
|---------|------|
| Type definitions | `packages/workflows/src/types.ts` |
| YAML parsing + validation | `packages/workflows/src/loader.ts` |
| Steps + loop execution | `packages/workflows/src/executor.ts` |
| DAG execution | `packages/workflows/src/dag-executor.ts` |
| Condition evaluation | `packages/workflows/src/condition-evaluator.ts` |
| Model compatibility | `packages/workflows/src/model-validation.ts` |
| Variable substitution | `packages/workflows/src/utils/variable-substitution.ts` |
| Idle timeout | `packages/workflows/src/utils/idle-timeout.ts` |
| Router | `packages/workflows/src/router.ts` |

---

### Packages Docs Web Src Content Docs Docs (`packages/docs-web/src/content/docs/docs.mdx`)

---
title: Archon
description: The simplest way to build the software that builds your software. YAML workflows that can do anything.
template: splash
hero:
  title: Archon
  image:
    file: ../../assets/logo.png
    alt: Archon Logo
  tagline: The simplest way to build the software that builds your software. YAML workflows that can do anything.
  actions:
    - text: Quickstart
      link: /getting-started/quick-start/
      icon: right-arrow
      variant: primary
    - text: Why Archon
      link: /getting-started/why-archon/
      icon: right-arrow
      variant: secondary
    - text: View on GitHub
      link: https://github.com/coleam00/Archon
      icon: external
      variant: minimal
---

import { Card, CardGrid } from '@astrojs/starlight/components';

Archon is a factory builder. You describe how work should get done as YAML workflows, and Archon runs them with AI agents, scripts and your decisions in the places that need them. Software is the first factory: the bundled `sdlc` pack takes an issue to a merged pull request. The same engine runs any process you can describe.

Take yourself out of the loop and sit on top of it.

<CardGrid>
  <Card title="Workflows you own" icon="document">
    The workflow fixes the steps, checks and gates. The agent supplies judgment inside each step.
  </Card>
  <Card title="Queues and attention" icon="list-format">
    Queues move work forward on their own. A run comes to you only when it needs a decision you own.
  </Card>
  <Card title="A reliable core, open at the seams" icon="puzzle">
    Providers, forges, chat platforms and workflow packs are plugins. Build your own for anything.
  </Card>
  <Card title="Your agents, self-hosted" icon="laptop">
    Runs Claude Code, Codex and Pi on the subscriptions you have. Every run is logged in your own database.
  </Card>
</CardGrid>

## Install

```bash
curl -fsSL https://archon.diy/install | bash        # macOS / Linux
irm https://archon.diy/install.ps1 | iex             # Windows (PowerShell)
brew install coleam00/archon/archon                 # Homebrew, then: archon setup
```

Then follow the [Quickstart](/getting-started/quick-start/) to run your first workflow, or read [Installation](/getting-started/installation/) for Docker and source installs.

---

### Packages Docs Web Src Content Docs Adapters Github App Setup (`packages/docs-web/src/content/docs/adapters/github-app-setup.md`)

---
title: GitHub App Setup
description: Run Archon's bot identity as a registered GitHub App with multi-installation routing.
category: adapters
area: adapters
audience: [operator]
status: current
sidebar:
  order: 5
---

This is the recommended GitHub auth mode for teams sharing one Archon instance. It replaces the shared `GITHUB_TOKEN` PAT with a registered GitHub App so that:

- Bot comments appear as `archon[bot]` with the App badge, not under an operator's personal account.
- Installation access tokens rotate automatically every ~1h (smaller blast radius if leaked).
- Webhooks centralise — one URL per App covers every installation.
- A team's repos can span multiple GitHub orgs (or a mix of orgs and personal accounts) — Archon routes per-(owner, repo) to the right installation transparently.

Solo installs that only need the PAT model can ignore this page; see [GitHub](/adapters/github/) for the legacy setup.

## When to use App mode vs. PAT mode

| Situation                                                           | Recommended mode |
| ------------------------------------------------------------------- | ---------------- |
| Solo developer, single GitHub account                               | PAT              |
| Team of 2+ sharing one Archon instance                              | App              |
| Repos across multiple orgs                                          | App              |
| You want bot comments to attribute as `<slug>[bot]` with App badge  | App              |
| You want short-lived (1h) tokens instead of long-lived PAT          | App              |

Archon refuses to start with **both** modes configured. Pick one set of env vars.

## Step 1: Register the GitHub App

1. Go to <https://github.com/settings/apps/new> (or `https://github.com/organizations/<org>/settings/apps/new` for an org-owned App).
2. Fill in:
   - **GitHub App name** — e.g. `Archon Bot`. The slug derived from this (visible in the App URL) is what you'll later set as `GITHUB_APP_SLUG`. Self-filter compares against `<slug>[bot]`.
   - **Homepage URL** — your team's Archon URL, e.g. `https://archon.example.com/`.
   - **Webhook URL** — `https://archon.example.com/webhooks/github`.
   - **Webhook secret** — same value as your `WEBHOOK_SECRET` env var.

   Webhook bodies are limited to **32 MiB (33,554,432 bytes)**, inclusive. Larger bodies receive HTTP **413** before the signature is checked; Archon stops reading them and creates no receipt or workflow start. This limit applies only to webhook routes.
3. Uncheck **Active** on the user authorisation callback URL — Archon doesn't use OAuth in PR-B.

## Step 2: Permissions (fine-grained)

**Repository permissions:**

| Permission       | Access       | Used for                                          |
| ---------------- | ------------ | ------------------------------------------------- |
| Actions          | Read & Write | Observing GitHub Actions workflow runs and rerunning failed CI |
| Checks           | Read         | Waking workflow CI waits when a check concludes   |
| Contents         | Read & Write | Cloning, reading repo metadata, pushing branches, and merging pull requests |
| Workflows        | Read & Write | Required only when runs create or change files under `.github/workflows/` |
| Issues           | Read & Write | `createComment` + `listComments`                  |
| Pull requests    | Read & Write | `pulls.get` + comment posting                     |
| Metadata         | Read         | Mandatory (auto-included)                         |

GitHub refuses pushes that create or change files under `.github/workflows/` unless the App has **Workflows: Read & Write**, even when **Contents: Read & Write** is granted. See [GitHub's Git access permissions](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app#choosing-permissions-for-git-access).

**Account permissions:** none.

## Step 3: Subscribe to webhook events

Subscribe to:

- Issue comments
- Check runs
- Pull request review comments
- Pull request
- Issues (used for `closed` cleanup)

Existing installations must accept the added Checks permission before check-run wake-ups work. If a check-run webhook is unavailable, delivery still progresses after the wait's five-minute deadline and probes CI again.

## Step 4: Generate a private key

1. After saving the App, scroll to **Private keys** and click **Generate a private key**.
2. Save the downloaded `.pem` file in a location only readable by the Archon process — e.g. `/etc/archon/github-app.pem`.

## Step 5: Install the App

Install the App on every org or personal account that holds repos your team operates on:

1. From the App settings page, click **Install App**.
2. Pick the org → grant access to all repos (or selected repos).
3. Repeat for every org/account.

> **Multi-installation:** Archon resolves `owner/repo → installation_id` via `GET /repos/{owner}/{repo}/installation` automatically. No per-install config is needed. `GITHUB_APP_INSTALLATION_ID` only pins the installation used for adapter API operations — see below.

## Step 6: Configure Archon

Add the following to `~/.archon/.env` for the install or `<cwd>/.archon/.env` for the project:

```dotenv
GITHUB_APP_ID=123456                 # numeric App ID, visible on the App settings page
GITHUB_APP_PRIVATE_KEY_PATH=/etc/archon/github-app.pem
WEBHOOK_SECRET=<same value as on the GitHub side>

# Optional:
# GITHUB_APP_SLUG=archon-bot         # defaults to 'archon'; set this if you named your App
#                                    # differently. The bot's posted-comment login is `<slug>[bot]`.
# GITHUB_APP_INSTALLATION_ID=98765   # use a fixed installation for adapter API operations.
#                                    # Repository tokens still verify the repository installation.
```

CLI workflow run, `--detach`, resume, wake, and signal use `GITHUB_APP_ID` plus either private-key setting. They do not require `WEBHOOK_SECRET`; that setting is required for the server's webhook adapter. Each detached child initializes its own provider from the inherited configuration.

Incomplete App settings, malformed App or installation IDs, an unusable RSA private key, and a malformed per-user encryption key fail before workflow execution. App/key correspondence and installation access are checked remotely when resolving the repository token. A resolution failure fails the run before any node executes; it cannot fall back to another credential. With no App settings, CLI behavior is unchanged. CLI runs follow the same per-user policy as server runs: connected user credentials override the bot, and unconnected users have GitHub credentials scrubbed unless org fallback is explicitly enabled.

Each workflow execution, including a resumed segment, resolves the codebase repository's installation token. The provider caches fresh tokens in memory and refreshes them near expiry; tokens are not cached on disk. A node's environment stays fixed during one execution, so this does not rotate tokens inside a workflow that runs longer than the token lifetime.

### Inline private key (alternative)

If you can't write a file (e.g. a managed PaaS), set the PEM contents inline:

```dotenv
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
```

Archon normalises the literal `\n` sequence in `.env`-quoted values into real newlines.

### Unset the PAT

Archon refuses to start if both modes are configured. Remove `GITHUB_TOKEN` from your env when switching to App mode.

## Step 7: Restart Archon and verify

1. Restart the server.
2. Confirm the startup log shows `github.adapter_mode_app` with your slug.
3. Trigger a webhook from a repo where the App is installed (e.g. comment `@archon ping` on an issue).
4. Confirm the bot's reply appears as `<slug>[bot]` with the App badge in the GitHub UI.

## Step 8 (optional): Enable per-user GitHub identity

By default GitHub API requests and pushes authenticate as the **bot** (`<slug>[bot]`), while commit authorship uses the checkout's ambient git identity. On a multi-user install teammates can connect their own GitHub identity for authentication and commit authorship.

Enable the feature by adding two env vars on top of App mode:

```bash
# The App's Client ID (App settings → "Client ID", starts with Iv1./Iv23…).
# Distinct from the numeric GITHUB_APP_ID.
GITHUB_APP_CLIENT_ID=Iv23xxxxxxxxxxxx
# 32-byte key used to encrypt stored per-user tokens at rest (AES-256-GCM).
TOKEN_ENCRYPTION_KEY=$(openssl rand -hex 32)
```

Then, on the GitHub App settings page, enable **Device Flow** (under "Identifying and authorizing users"). The feature gate activates when `GITHUB_APP_ID` and `TOKEN_ENCRYPTION_KEY` are both set; `GITHUB_APP_CLIENT_ID` is required for the connect flow itself.

Teammates connect once via any surface:

- **CLI:** `archon auth github`
- **Web UI:** Settings → **Connect GitHub**

Once enabled:

- Run commits use the initiating connected user's GitHub login as author name and `<numeric_id>+<login>@users.noreply.github.com` as author email. The committer remains the ambient git identity. This applies to new and adopted worktrees, in-place runs, and folder projects without changing repository git config. Unconnected users and runs without an initiating user keep ambient authorship.
- Workflows declaring `requires: [github]` **hard-block** unconnected users before any worktree/clone/AI cost.
- An unconnected user's workflow `gh`/`git` has its GitHub token **scrubbed** by default (rather than silently using the shared org/bot token). The host's `GH_ENTERPRISE_TOKEN`/`GITHUB_ENTERPRISE_TOKEN` are cleared for every per-user run. The scrub also hides the host's stored logins: `gh` gets an empty config directory, so a `gh auth login` on the server is not used, and git's credential helpers (keychain, credential store, `gh auth git-credential`) are cleared. GitHub commands in such a run fail with an authentication error until the user connects. SSH remotes still use the host's SSH keys. Opt back into the shared token with `ARCHON_ALLOW_ORG_GITHUB_TOKEN_FALLBACK=true`.
- Rotating `TOKEN_ENCRYPTION_KEY` invalidates all stored user tokens — everyone must reconnect.

This is fully backward compatible: leave the two vars unset and the App keeps working as the bot only. See the [configuration reference](/reference/configuration/#per-user-github-identity-app-mode-optional) for the full env var table.

## Operational notes

### Token rotation is invisible

Installation tokens are valid for 1h. Archon caches each `installation_id → token` pair and refreshes ~5 minutes before expiry on the next access. No background timer; no leaked handles.

### `event.installation.id` short-circuits the lookup

Every webhook delivery from a GitHub App carries `installation.id`. Archon primes its `owner/repo → installation_id` cache from the payload, so the next outbound call to that repo skips the `GET /repos/{owner}/{repo}/installation` round trip.

### 401 forces a single retry

A 401 from any installation Octokit evicts the cached token and the call is retried once with a fresh token. Persistent 401s propagate as the original error.

### Long-running workflow `git push`

Workflows that span >1h need a fresh token to push from the cloned worktree. In App mode, Archon installs a git credential helper after cloning and refreshes it whenever it sets up an existing repository, including webhook handling and adding a codebase by URL: the worktree's `.git/config` points at `~/.archon/bin/git-credential-archon`, which talks back to Archon's internal endpoint for a fresh installation token on each operation. Git skips credential helpers when a remote URL carries credentials, so the same setup removes any token embedded in the repository's `https://github.com` remote URLs, such as one left by an older PAT-mode clone. The helper reads the server's port from `ARCHON_PORT`, which Archon sets for the processes it launches. A `git` command run from an independent shell on the Archon host gets no credential unless it exports `ARCHON_PORT` with the server's port.

The helper is embedded in compiled binaries. Each setup replaces an outdated managed script and registers the helper and GitHub path forwarding in the repository's local Git config. Existing repositories are refreshed on their next setup, not swept at server startup. If helper installation or registration fails, repository setup stops before URL registration, sync, or workflow dispatch; a later setup retries registration and repairs partial config.

### Long-running workflow `gh`

A run's `GH_TOKEN` and `GITHUB_TOKEN` hold the installation token resolved when the run, or a resumed segment, starts. That token can have as little as 5 minutes left. When the run's process has `ARCHON_PORT` (every run the server dispatches, and a CLI run that exports the server's port), Archon also puts `~/.archon/bin/gh-shim` first on the run's `PATH`. On each call the shim asks the same internal endpoint for a current token for the run's repository and runs the real `gh` with it in the environment. If it gets no token, it exits 1 with a `gh (archon shim):` error instead of running `gh` with a stale token. A `GH_TOKEN` that is not an installation token, such as one a step sets itself, passes through unchanged. A per-user token or scrubbed token from [per-user GitHub identity](#step-8-optional-enable-per-user-github-identity) turns the shim off for that run.

`gh` keeps the start-time token, and fails once it expires, in these cases:

- **A CLI run without `ARCHON_PORT`.** No endpoint is reachable, so Archon logs `workflow.gh_token_refresh_unavailable` each time it resolves the run's GitHub environment. That happens when the run starts, when it runs worktree setup commands, and when it resumes, so one run can log it several times. The CLI mints its token at start, so it lasts about an hour. Export `ARCHON_PORT` with an App-mode server's port to cover longer runs.
- **`gh` called by a Codex agent.** Codex's shell snapshot restores a fixed `PATH`, so its shell never finds the shim. Archon does not change that Codex setting. Bash, script and loop nodes, and Claude and Pi agents, use the shim.
- **Windows.** The shim is a POSIX shell script, so a run on Windows logs the same warning and keeps the start-time token.

Run output redacts the start-time token only. A token the shim fetches later lives in the `gh` process's environment, so it is not redacted if a step prints it, for example with `gh auth token`.

### Internal endpoint security — REQUIRED

The credential-helper backend is exposed at `POST /internal/git-credential` and hands out live installation access tokens. **It MUST NOT be reachable from outside the Archon host.**

Archon enforces this at startup: with App mode active and the server bound to a non-loopback interface (e.g. `0.0.0.0`), the process **refuses to start** and exits with `github_app.internal_endpoint_public_bind_rejected`. Two correct configurations:

1. **Recommended (bare-metal / systemd) — bind Archon to `127.0.0.1`** (`HOST=127.0.0.1`) and put a reverse proxy in front. Configure the proxy to drop `/internal/*` paths.
2. **Opt-in escape hatch — `ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1`** combined with `HOST=0.0.0.0` (or unset). Use ONLY when your reverse proxy already drops `/internal/*` AND your deployment topology genuinely requires the upstream to bind non-loopback (e.g. a container network where loopback isn't reachable from the proxy). **Docker / docker-compose deployments fall here** — the app container *must* bind `0.0.0.0` inside the container for docker-proxy to forward traffic, so option 1 isn't directly usable. See the [Canonical Docker setup](#canonical-docker-setup) below. Startup logs `github_app.internal_endpoint_exposed_acknowledged` so the choice is auditable.

Example Caddy snippet that drops `/internal/*` (bare-metal):

```txt
example.com {
  @internal path /internal/*
  respond @internal 404
  reverse_proxy 127.0.0.1:3090
}
```

### Canonical Docker setup

The canonical Archon Docker stack (`docker-compose.yml` + `docker-compose.override.yml` + Caddy) publishes the app container's port `3000` and runs Caddy as a reverse proxy. Because the app container can't bind `127.0.0.1` and still have docker-proxy forward traffic to it, option 1 above isn't directly usable. Take option 2 with three layers of defense:

**1. In `docker-compose.override.yml` — bind the published host port to loopback only.**

```yaml
services:
  app:
    ports: !override
      - "127.0.0.1:${PORT:-3000}:${PORT:-3000}"
```

The `!override` tag (compose-spec) replaces the base file's `ports` list instead of merging. After this, port `3000` is reachable only via the Docker network (where Caddy lives) or from the host itself — not from the public internet.

**2. In `Caddyfile` — drop `/internal/*` requests.**

The maintained `Caddyfile.example` includes this denial. Keep it in your deployed `Caddyfile`; if you use an older or custom configuration, insert it before the fallthrough `handle { }` block:

```txt
handle /internal/* {
  respond "Not Found" 404
}
```

Caddy evaluates `handle` blocks by path specificity, so `/internal/*` (more specific) wins over the generic fallthrough regardless of order.

**3. In `.env` — opt out of the loopback-bind guard.**

```ini
# Safe because:
#  1. docker-compose binds port 3000 to 127.0.0.1 only (override above)
#  2. Caddy drops /internal/* (handle block above)
ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1
```

Without this flag, Archon refuses to start in App mode and exits with `github_app.internal_endpoint_public_bind_rejected`.

**Apply + verify:**

```sh
# Apply the override + Caddyfile + env changes, then restart app + caddy.
docker compose up -d --force-recreate app caddy

# From outside the host — must be 404/403 from the proxy.
# Use POST (the endpoint's actual method) — a GET probe can false-pass when
# the proxy 404s unmatched GETs while still forwarding the POST upstream.
curl -i -X POST https://your-archon/internal/git-credential \
  -H 'Content-Type: application/json' \
  -d '{"host":"github.com","path":"any/repo"}'

# On the host — port should bind to loopback only
ss -tlnp | grep :3000
# Expected: 127.0.0.1:3000   (NOT 0.0.0.0:3000)

# Startup log should contain:
docker compose logs app --tail 200 | grep "internal_endpoint_exposed_acknowledged"
# This event confirms the opt-out path was taken (audit trail).
```

If the external POST probe returns anything other than `404` or `403` from the proxy — i.e. anything that suggests the request reached the Archon process — **stop and fix the proxy config** before going live. The endpoint hands out live installation tokens to whoever can reach it.

## Migration from PAT mode

1. Register the App and install on your orgs (Steps 1–5 above).
2. Add `GITHUB_APP_*` env vars (Step 6). In Docker, put them in the container environment (the compose `.env`), not `/.archon/.env`; see [GitHub CLI authentication](/deployment/docker/#github-credentials).
3. **Remove** `GITHUB_TOKEN` from your env (or comment it out). Archon refuses to start if both are set.
4. **Remove** `GH_TOKEN` if you set it to the same PAT. Archon starts with it set, but the GitHub CLI in the container keeps using the old PAT.
5. Restart Archon.
6. Webhook URLs configured per-repo against the PAT-mode setup can stay or be removed — the App's single webhook URL covers everything once it's installed. New repos auto-join via App installation. While both webhooks are active, a comment delivered by each is deduplicated at ingest, so the bot still responds once.

In App mode, adding a private GitHub codebase by URL and inspecting its base branch use the repository's installation token; no PAT is needed. The clone keeps a credential-free remote URL and receives the credential helper before registration completes. If the App is not installed, Archon reports installation guidance. Adding an already-registered URL refreshes the helper without cloning again when the checkout exists. A missing Archon-managed checkout is cloned again at its registered path. A missing operator-owned checkout must be restored or repaired with `/update-project`.

Run `archon doctor` to validate local App configuration after migration. A passing App check confirms the ID and RSA key are valid locally; it does not confirm GitHub accepts them or that the App has repository access.

## Troubleshooting

### `AppPrivateKeyError: Provided value is not a valid PEM-encoded private key`

- Check the file content includes `-----BEGIN ... PRIVATE KEY-----` and `-----END ... PRIVATE KEY-----`.
- For inline keys, ensure the `.env` value preserves newlines (either literal newlines in a multi-line value or the `\n` escape inside double quotes).

### `AppNotInstalledError: The Archon GitHub App is not installed on "<owner>"`

- The App is not installed on that owner's org/account. Use the install link in the error message to add it.

### 401 loop on GitHub API calls

Repeated 401s on outbound API calls (`createComment`, `listComments`, `repos.get`, `pulls.get`) point at installation / token issues, not webhook config. Walk through:

- Verify the App is installed on the target `owner` and has not been suspended or uninstalled. Visit `https://github.com/settings/installations` (or the org equivalent) to confirm.
- Verify the private key in your env (`GITHUB_APP_PRIVATE_KEY` or `_PATH`) matches the same App that `GITHUB_APP_ID` points at. Mismatched key+ID is a common cause of "401 from JWT" errors at token-issuance time.
- Verify `GITHUB_APP_INSTALLATION_ID` (if set) still corresponds to a live installation — uninstall + reinstall assigns a new ID.

### GitHub refuses a branch push

An installation token needs Contents write access to push branches. If the branch creates or changes files under `.github/workflows/`, GitHub also requires Workflows write access. Check the App and installation permissions:

Verify the App's permissions against the [permissions table](#step-2-permissions-fine-grained). Permission changes require operators to **review and accept** the new permissions on every installation; until accepted, that installation keeps its old permissions.

### Webhook deliveries fail signature verification

A webhook-secret mismatch causes `github.signature_mismatch` / `github.signature_length_mismatch` errors at the `POST /webhooks/github` endpoint — distinct from outbound API 401s. If GitHub's webhook delivery page shows red ❌ next to the delivery (rather than your bot just silently not responding), check:

- `WEBHOOK_SECRET` in Archon's env matches the value entered in the GitHub App's webhook configuration page exactly.

### Bot comments still appear under your personal account

- You're still in PAT mode. Check `process.env.GITHUB_TOKEN` is unset and `GITHUB_APP_ID` is set; restart.

### Server refused to start: `github_app.internal_endpoint_public_bind_rejected`

- App mode is active but the server is bound to a non-loopback interface. This is a fail-fast guard — the `/internal/git-credential` endpoint hands out live installation access tokens and would leak credentials to the network. Either set `HOST=127.0.0.1` (bare-metal / systemd), or, if you're on Docker, follow the [Canonical Docker setup](#canonical-docker-setup) above (port→127.0.0.1 binding + Caddy `/internal/*` drop + `ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1`).

### Server log shows `github_app.internal_endpoint_exposed_acknowledged`

- You set `ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1`. Double-check that your reverse proxy actually drops `/internal/*` — a `curl https://your-archon/internal/git-credential -d '{"host":"github.com","path":"any/repo"}'` from outside the host must return 404 or 403 from the proxy (NOT a token from Archon). On the canonical Docker stack, also confirm `ss -tlnp | grep :3000` shows `127.0.0.1:3000` (not `0.0.0.0:3000`).

---

### Packages Docs Web Src Content Docs Adapters Github (`packages/docs-web/src/content/docs/adapters/github.md`)

---
title: GitHub
description: Connect Archon to GitHub via webhooks to interact from issues and pull requests.
category: adapters
area: adapters
audience: [user, operator]
status: current
sidebar:
  order: 4
---

Connect Archon to GitHub so you can interact with your AI coding assistant from issues and pull requests.

> **Teams sharing one Archon instance:** prefer the [GitHub App setup](./github-app-setup.md). The PAT-mode setup on this page is the legacy path — it still works for solo installs, but bot comments all post under the PAT owner's avatar and tokens never auto-rotate. App mode gives you `<slug>[bot]` attribution, 1h token rotation, multi-org support, and one webhook URL.

## Prerequisites

- Archon server running (`archon serve`; see [Web UI](/adapters/web/))
- GitHub repository with issues enabled
- A GitHub personal access token set as `GITHUB_TOKEN` in `~/.archon/.env`
- Public endpoint for webhooks (see ngrok setup below for local development)

## Step 1: Generate Webhook Secret

On Linux/Mac:
```bash
openssl rand -hex 32
```

On Windows (PowerShell):
```powershell
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Maximum 256) })
```

Save this secret -- you'll need it for steps 3 and 4.

## Step 2: Expose Local Server (Development Only)

### Using ngrok (Free Tier)

```bash
# Install ngrok: https://ngrok.com/download
# Or: choco install ngrok (Windows)
# Or: brew install ngrok (Mac)

# Start tunnel
ngrok http 3090

# Copy the HTTPS URL (e.g., https://abc123.ngrok-free.app)
# Free tier URLs change on restart
```

Keep this terminal open while testing.

### Using Cloudflare Tunnel (Persistent URLs)

```bash
# Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/
cloudflared tunnel --url http://localhost:3090

# Get persistent URL from Cloudflare dashboard
```

Persistent URLs survive restarts.

**For production deployments**, use your deployed server URL (no tunnel needed).

## Step 3: Configure GitHub Webhook

Go to your repository settings:
- Navigate to: `https://github.com/owner/repo/settings/hooks`
- Click "Add webhook"
- **Note**: For multiple repositories, you'll need to add the webhook to each one individually

**Webhook Configuration:**

| Field | Value |
|-------|-------|
| **Payload URL** | Local: `https://abc123.ngrok-free.app/webhooks/github`<br>Production: `https://your-domain.com/webhooks/github` |
| **Content type** | `application/json` |
| **Secret** | Paste the secret from Step 1 |
| **SSL verification** | Enable SSL verification (recommended) |
| **Events** | Select "Let me select individual events":<br>- Check runs<br>- Issues<br>- Issue comments<br>- Pull requests |

Click "Add webhook" and verify it shows a green checkmark after delivery.

Webhook bodies are limited to **32 MiB (33,554,432 bytes)**, inclusive. Larger bodies receive HTTP **413** before the signature is checked; Archon stops reading them and creates no receipt or workflow start. This limit applies only to webhook routes.

## Step 4: Set Environment Variables

```ini
WEBHOOK_SECRET=your_secret_from_step_1
```

**Important**: The `WEBHOOK_SECRET` must match exactly what you entered in GitHub's webhook configuration.

## Step 5: Configure Streaming (Optional)

The GitHub adapter always uses `batch` mode (hardcoded). It assembles the reply before delivery and splits long replies into multiple comments.

## Usage

Interact by @mentioning your bot in issue or PR **comments**:

```
@archon can you analyze this bug?
@archon prime the codebase
@archon review this implementation
```

**First mention behavior:**
- Reuses an existing local registration at its saved checkout path, including a subdirectory of a Git checkout. Otherwise, registers and clones the repository to `~/.archon/workspaces/owner/repo/source/`, or to a suffixed path such as `owner/repo-2/source/` when that project name is taken.
- For an existing registration, readiness skips syncing the checkout to preserve local changes.
- Detects and loads commands from `.archon/commands/` if present
- Injects full issue/PR context for the AI assistant

**Subsequent mentions:**
- Resumes existing conversation
- Maintains full context across comments

:::note
Only comments trigger the bot. @mentions in issue or PR descriptions are ignored -- descriptions often contain example commands or documentation that are not intended as bot invocations.
:::

## Adding Additional Repositories

Once your server is running, add more repos by creating a webhook with the same secret.

**Via GitHub UI:** Repo Settings > Webhooks > Add webhook
- **Payload URL**: Your server URL + `/webhooks/github`
- **Content type**: `application/json`
- **Secret**: Same `WEBHOOK_SECRET` from your `.env`
- **Events**: Check runs, Issues, Issue comments, Pull requests

**Via CLI:**

```bash
# Get your existing webhook secret
WEBHOOK_SECRET=$(grep WEBHOOK_SECRET .env | cut -d= -f2)

# Add webhook to new repo (replace OWNER/REPO)
gh api repos/OWNER/REPO/hooks --method POST \
  -f "config[url]=https://YOUR_DOMAIN/webhooks/github" \
  -f "config[content_type]=json" \
  -f "config[secret]=$WEBHOOK_SECRET" \
  -f "events[]=check_run" \
  -f "events[]=issues" \
  -f "events[]=issue_comment" \
  -f "events[]=pull_request"
```

**Important**: The webhook secret must be identical across all repos.

Check-run deliveries wake CI waits quickly. They are not required for correctness: when GitHub does not deliver one, the workflow reaches its deadline and probes CI again.

## GitHub Enterprise Server

To run the adapter against a self-hosted GitHub Enterprise Server, set its base URL:

```ini
GITHUB_URL=https://github.example.com
```

With `GITHUB_URL` set, the adapter calls `https://github.example.com/api/v3`, clones from `https://github.example.com/<owner>/<repo>.git`, and `GH_TOKEN`/`GITHUB_TOKEN` authenticate HTTPS clones of that exact scheme, host and port. Leave it unset for github.com. A value with a path, query or credentials is rejected: the adapter and the GitHub App fail startup, and clones to the Enterprise host get no GitHub token. Clones from other forges are unaffected.

- **Webhook:** create it on the Enterprise repository or organization, pointing at the same `/webhooks/github` endpoint with the same secret (Step 3).
- **GitHub App mode:** register the App on the Enterprise instance (**Settings → Developer settings → GitHub Apps**) with the permissions listed in [GitHub App Setup](/adapters/github-app-setup/). The App's API calls and installation tokens go to the Enterprise instance.
- **SDLC workflows:** the forge plugin needs a `forge.hosts` entry for the Enterprise host; see [forge plugin configuration](/reference/forge/#plugin-configuration).

Current limits: in App mode, git operations after the first clone (sync, workflow pushes) and per-user identity (`archon auth github`, the device flow) still target github.com. Use PAT mode on Enterprise Server for now.

## Further Reading

- [Configuration](/getting-started/configuration/)

---

### Packages Docs Web Src Content Docs Adapters Slack (`packages/docs-web/src/content/docs/adapters/slack.md`)

---
title: Slack
description: Install the Slack chat plugin and connect Archon to Slack over Socket Mode -- works behind firewalls with no public URL needed.
category: adapters
area: adapters
audience: [user, operator]
status: current
sidebar:
  order: 2
---

Connect Archon to Slack so you can interact with your AI coding assistant from any Slack workspace.

Slack runs as a chat plugin, `chat-slack`, released with Archon. The server starts the plugin as its own process and talks to it over the chat plugin protocol; the server itself contains no Slack code. You create a Slack app, set its two tokens in the server environment, and install the plugin. Docker provisions the image-provided plugin automatically when `SLACK_BOT_TOKEN` is configured.

New identities are created as members. The operator can designate admins with [`archon user role <id> admin`](/reference/security/#users-and-roles); only a run's starter or an admin can act on it. See [Who can act on a run](/reference/security/#who-can-act-on-a-run).

## Prerequisites

- Archon server running (`archon serve`; see [Web UI](/adapters/web/))
- A Slack workspace where you have permission to install apps
- An Archon release that publishes the Slack plugin asset (`archon-chat-slack-<os>-<arch>`)

## Overview

Archon uses **Socket Mode** for Slack integration, which means:

- No public HTTP endpoints needed
- Works behind firewalls
- Simpler local development
- Not suitable for Slack App Directory (fine for personal/team use)

## Step 1: Create a Slack App

1. Go to [Slack API Apps](https://api.slack.com/apps)
2. Log in if prompted
3. Choose the workspace for your app
4. Click **Create New App**
5. Choose **From scratch**
6. Enter:
   - **App Name**: Any name (this is what you will use to @mention the bot)
   - **Workspace**: Select your workspace
7. Click **Create App**

## Step 2: Enable Socket Mode

1. In the left sidebar, click **Socket Mode**
2. Toggle **Enable Socket Mode** to ON
3. When prompted, create an App-Level Token:
   - **Token Name**: `socket-mode`
   - **Scopes**: Add `connections:write`
   - Click **Generate**
4. **Copy the token** (starts with `xapp-`) -- this is your `SLACK_APP_TOKEN`

## Step 3: Configure Bot Scopes

1. In the left sidebar, click **OAuth & Permissions**
2. Scroll down to **Scopes** > **Bot Token Scopes**
3. Add these scopes to bot token scopes:
   - `app_mentions:read` -- Receive @mention events
   - `chat:write` -- Send and edit messages (covers `chat.update` on the bot's own messages)
   - `channels:history` -- Read messages in public channels (for thread context)
   - `channels:join` -- Allow bot to join public channels
   - `groups:history` -- Read messages in private channels (optional)
   - `im:history` -- Read DM history (for DM support)
   - `im:write` -- Send DMs
   - `im:read` -- Read DM history (for DM support)
   - `mpim:history` -- Read group DM history (optional)
   - `mpim:write` -- Send group DMs
   - `reactions:write` -- Add lifecycle reactions (🔄 / ✅ / ❌) to the triggering message
   - `commands` -- Required for the `/archon` and `/archon-workflow` slash commands
   - `users:read` -- Look up real names via `users.info` for user attribution. Without this scope messages still flow, but real names do not appear in the Archon database.

## Step 4: Subscribe to Events

1. In the left sidebar, click **Event Subscriptions**
2. Toggle **Enable Events** to ON
3. Under **Subscribe to bot events**, add:
   - `app_mention` -- When someone @mentions your bot
   - `message.im` -- Direct messages to your bot
   - `message.channels` -- Messages in public channels (optional, for broader context)
   - `message.groups` -- Messages in private channels (optional)
4. Click **Save Changes**

## Step 4b: Enable Interactivity

Interactive buttons (gate decisions and Cancel on workflow runs) ride the same
Socket Mode connection -- no public URL needed.

1. In the left sidebar, click **Interactivity & Shortcuts**
2. Toggle **Interactivity** to ON
3. Leave the **Request URL** field blank -- Socket Mode handles routing
4. Click **Save Changes**

## Step 4c: Register Slash Commands

Two slash commands give the team an alternative to @mention:

| Command | What it does |
| --- | --- |
| `/archon <message>` | Starts a new thread in the current channel. To continue an existing thread, post `@archon <message>` in that thread. |
| `/archon-workflow <subcommand>` | Direct workflow control. Supports `list`, `status`, `get <id>`, `run <name> <args>`, `approve <id> [comment]`, `reject <id> [reason]`, `respond <id> <decision> [text]`, `abandon <id>`, `resume <id>`. |

For each command:

1. In the left sidebar, click **Slash Commands**
2. Click **Create New Command**
3. Fill in:
   - **Command**: `/archon` (or `/archon-workflow`)
   - **Request URL**: leave blank -- Socket Mode handles routing
   - **Short Description**: e.g. "Talk to Archon" or "Archon workflow control"
   - **Usage Hint**: e.g. `<message>` or `<subcommand>`
4. Save

Reinstall the app (Step 5) after adding scopes or commands so Slack issues a
fresh token with the new permissions.

## Step 5: Install to Workspace

1. In the left sidebar, click **Install App**
2. Click **Install to Workspace**
3. Review the permissions and click **Allow**
4. **Copy the Bot User OAuth Token** (starts with `xoxb-`) -- this is your `SLACK_BOT_TOKEN`

## Step 6: Set Environment Variables

Add both tokens to the server's env file (`~/.archon/.env`, or the compose `.env` in Docker):

```ini
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
```

In Docker, a nonempty `SLACK_BOT_TOKEN` declares installation at startup, including tokens in `/.archon/.env`. Both tokens are required for a connection.

The plugin reads them from the environment the server starts it with. If either is missing, the plugin fails to start and the server does not retry it.

## Step 7: Install the Slack Plugin

With the release binary or Homebrew:

```bash
archon plugin add coleam00/Archon/plugins/chat-slack
```

In a source checkout, run `bun run cli plugin add coleam00/Archon/plugins/chat-slack`.

In Docker, restart the container with its tokens configured. The image-provided release installs offline before the server starts, writes a verified receipt to `/.archon/plugins`, and logs why it was provisioned. No manual install is needed. A valid installed Slack plugin is retained at its current version. See [Building the image](/deployment/docker/#building-the-image) for the release pin and build args.

Without a tag, manual `plugin add` installs the latest release. Restart the server; Slack appears in `/api/health` once the plugin connects. Keep the plugin current with `archon plugin update coleam00/Archon/plugins/chat-slack`, and remove it with `archon plugin remove coleam00/Archon/plugins/chat-slack`. In Docker, remove Slack tokens from every effective env source as well, or the image plugin is reinstalled on restart. Outside Docker, removal followed by a restart keeps Slack off. See [`archon plugin`](/reference/cli/#plugin).

## Step 8: Invite Bot to Channel

1. Go to the Slack channel where you want to use the bot
2. Type `/invite @your-bot` (your bot's display name)
3. The bot should now respond to @mentions in that channel

## Configure User Whitelist (Optional)

To restrict bot access to specific users:
1. In Slack, go to a user's profile > click "..." > "Copy member ID"
2. Add to environment:

```ini
SLACK_ALLOWED_USER_IDS=U01ABC123,U02DEF456
```

When set, only listed user IDs can interact with the bot. When empty/unset, the bot responds to all users. The server enforces this list, for messages and for button clicks alike, before anything reaches Archon.

## Configure Streaming Mode (Optional)

```ini
SLACK_STREAMING_MODE=batch  # batch (default) | stream
```

For streaming mode details, see [Configuration](/getting-started/configuration/).

## Usage

Select a registered project by posting `@your-bot /setproject <name>` in the target thread. To register a local directory first, post `@your-bot /register-project [--folder] <name> <path>`, then select it; registration alone does not bind the conversation. Projectless questions still work. Selection applies to that conversation, not every channel.

Slack registers `/archon` and `/archon-workflow`, not raw `/setproject` or `/register-project` slash commands. `/archon <message>` creates a new thread; use an @mention in the existing thread to scope that conversation.

Once scoped, the assistant launches and manages runs through its native tools (Claude, Codex, Pi) or the existing CLI (Copilot/OpenCode). CLI launches are detached and headless, use CLI identity and return a run ID for inspection; they do not deliver progress or results to the originating chat. See [Deprecated providers](/getting-started/ai-assistants/#deprecated-providers).

### @Mention in Channels

```
@your-bot /clone https://github.com/user/repo
```

### Continue Work in Thread

Reply in the thread created by the initial message:

```
@your-bot /status
```

### Start Parallel Work (Worktree)

```
@your-bot /worktree feature-branch
```

### Direct Messages

You can also DM the bot directly -- no @mention needed:

```
/help
```

## In-Thread UX

When a workflow runs in a Slack thread, Archon now:

- Adds 🔄 to your triggering message when the run starts, and swaps it for ✅
  on execution completion or ❌ on execution failure / cancellation. If a declared
  authored outcome disagrees, it also adds the outcome reaction, so both facts remain visible
- Posts a single status message in the thread that's edited in place as DAG
  nodes start, complete, fail, or get skipped
- When approval details are available, renders the gate's declared decisions as
  buttons, using labels when supplied and sending the exact decision ID.
  Legacy gates keep **Approve** / **Reject**.
  The fallback text lists `/archon-workflow respond <id> <decision> [text]` commands;
  use these to include optional text with a decision. For a parent blocked on a
  child run, these parent controls redirect to the child instead of recording a
  parent decision. Answer the child run using the displayed child command
- If approval details cannot be loaded, posts an **Approval details unavailable**
  notice naming the run instead of decision buttons or response commands. Check
  the run status before responding
- Adds a **Cancel** button on the status message while the run is
  non-terminal, so the run's starter or an admin can abandon a runaway run
  with a click
- Appends a small italic cost / token footer after direct-chat replies
  (e.g. `_cost: $0.0234 · 12.4k tokens · stop: end_turn_`) and on the
  final status message when a workflow completes
- Labels execution status and a non-null authored outcome separately in the final status message;
  workflows without an authored outcome retain the existing status-only message
- Annotates long responses split across multiple Slack messages with
  `_part i/n_` footers so it's clear they belong together

All gate-decision and Cancel clicks run through the same
`SLACK_ALLOWED_USER_IDS` whitelist as inbound messages -- clicks from users
outside it are dropped. A member who is not the run's starter or an admin sees
a refusal only they can see.

`/archon connect github` replies with a pointer to the Web UI or
`archon auth github`; it does not run the GitHub device flow in Slack.

## Upgrading From the Bundled Slack Adapter

Earlier Archon versions ran Slack inside the server whenever `SLACK_BOT_TOKEN` and
`SLACK_APP_TOKEN` were set. The server no longer contains that adapter: without the
plugin, Slack stays off. When `SLACK_BOT_TOKEN` is set and no Slack plugin is installed,
startup logs a `chat.bundled_adapter_retired` warning with Docker provisioning guidance and the manual install command.

In Docker, existing token configuration provisions the image-provided plugin offline on restart; no manual step is needed. The boot log names the release and why it was installed. Outside Docker, install the plugin (Step 7) and restart. Nothing else changes: the tokens, the Slack app
configuration, `SLACK_ALLOWED_USER_IDS` and `SLACK_STREAMING_MODE` stay as they are, and
existing conversations, linked identities and workspaces carry over because the plugin
keeps the platform id `slack`.

## Troubleshooting

### Bot Doesn't Respond

1. Check that the plugin is installed: `archon plugin list` shows `coleam00/Archon/plugins/chat-slack`
2. Check that Socket Mode is enabled
3. Verify both tokens are correct in `.env`, then check the server log for `chat.bundled_adapter_retired` or a plugin start failure
4. Ensure the bot is invited to the channel
5. Make sure you're @mentioning the bot (not just typing)

### "channel_not_found" Error

The bot needs to be invited to the channel:

```
/invite @your-bot
```

### "missing_scope" Error

Add the required scope in **OAuth & Permissions** and reinstall the app.

### Thread Context Not Working

Ensure these scopes are added:

- `channels:history` (public channels)
- `groups:history` (private channels)

## Security Recommendations

1. **Use User Whitelist**: Set `SLACK_ALLOWED_USER_IDS` to restrict bot access
2. **Private Channels**: Invite the bot only to channels where it's needed
3. **Token Security**: Never commit tokens to version control

## Reference Links

- [Slack API Documentation](https://api.slack.com/docs)
- [Bolt for JavaScript](https://tools.slack.dev/bolt-js/)
- [Socket Mode Guide](https://api.slack.com/apis/connections/socket)
- [Permission Scopes](https://api.slack.com/scopes)

---

### Packages Docs Web Src Content Docs Adapters Telegram (`packages/docs-web/src/content/docs/adapters/telegram.md`)

---
title: Telegram
description: Connect Archon to Telegram using the Bot API for mobile and desktop access.
category: adapters
area: adapters
audience: [user, operator]
status: current
sidebar:
  order: 3
---

Connect Archon to Telegram so you can interact with your AI coding assistant from any Telegram client.

:::caution[Deprecated]
The Telegram adapter is deprecated and will be removed in the next minor release, 0.13.0. No Archon maintainer owns it. It keeps working until then, and the server logs a deprecation notice when it starts the adapter. If you need Telegram after that, build and own it as a [chat plugin](/guides/publishing-plugins/#chat-plugins). [#4641](https://github.com/coleam00/Archon/issues/4641) tracks the removal.
:::

New identities are created as members. The operator can designate admins with [`archon user role <id> admin`](/reference/security/#users-and-roles); only a run's starter or an admin can act on it. See [Who can act on a run](/reference/security/#who-can-act-on-a-run).

## Prerequisites

- Archon server running (`archon serve`; see [Web UI](/adapters/web/))
- A Telegram account

## Create Telegram Bot

1. Message [@BotFather](https://t.me/BotFather) on Telegram
2. Send `/newbot` and follow the prompts
3. Copy the bot token (format: `123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)

## Set Environment Variable

```ini
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHI...
```

## Configure User Whitelist (Optional)

To restrict bot access to specific users:
1. Message [@userinfobot](https://t.me/userinfobot) on Telegram to get your user ID
2. Add to environment:

```ini
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321
```

When set, only listed user IDs can interact with the bot. When empty/unset, the bot responds to all users.

## Configure Streaming Mode (Optional)

```ini
TELEGRAM_STREAMING_MODE=stream  # stream (default) | batch
```

For streaming mode details, see [Configuration](/getting-started/configuration/).

## Project selection

Select a registered project by submitting `/setproject <name>` in the conversation or thread where you want to work. To register a local directory first, submit `/register-project [--folder] <name> <path>`, then select it; registration alone does not bind the conversation. Projectless questions still work. Selection applies to that conversation, not every channel.

Once scoped, the assistant launches and manages runs through its native tools (Claude, Codex, Pi) or the existing CLI (Copilot/OpenCode). CLI launches are detached and headless, use CLI identity and return a run ID for inspection; they do not deliver progress or results to the originating chat. See [Deprecated providers](/getting-started/ai-assistants/#deprecated-providers).

## Further Reading

- [Configuration](/getting-started/configuration/)

---

### Packages Docs Web Src Content Docs Adapters Web (`packages/docs-web/src/content/docs/adapters/web.md`)

---
title: Web UI
description: Built-in console for operating Archon from a browser.
category: adapters
area: adapters
audience: [user]
status: current
sidebar:
  order: 1
---

The Web UI is Archon's built-in management console. It uses Archon's public API
and does not require a separate hosted service.

## Start the console

For local development, start the API server and Vite frontend together:

```bash
bun run dev
```

The console is available at `http://localhost:5173`; the API server listens on
`http://localhost:3090`.

You can also run them separately:

```bash
bun run dev:server
bun run dev:web
```

For a production build, the API server serves the compiled console on port 3090:

```bash
bun run build
bun run start
```

The backend binds to `0.0.0.0` by default. The Vite development server binds to
localhost; pass `--host 0.0.0.0` only when you intentionally want to expose it:

```bash
bun run dev:web -- --host 0.0.0.0
```

## Authentication

Authentication is optional. A solo installation with Web authentication disabled
opens the console directly. When authentication is enabled, the same console
requires a session and provides sign-in, signup when allowed by server policy,
and sign-out.

## Console navigation

The project rail is the main navigation surface:

- **Runs** at `/console` shows runs across projects. Selecting a
  project opens its scoped runs at `/console/p/:projectId`.
- **Chat** at `/console/p/:projectId/chat` opens the operator conversation for
  that project.
- **Settings** at `/console/settings` manages assistant defaults, model tiers,
  aliases, provider credentials, system status, GitHub identity, and alerts.
- **Workflow builder** at `/console/builder` opens the experimental visual
  authoring surface. Select a project before opening or creating a workflow.

Register a project with the add button in the rail. Remote Git URLs are cloned;
local paths are registered in place. Removing a project removes its registration,
not its files.

## Workflows and runs

Choose a workflow from a project's run or chat view, supply its inputs, and
launch it. The console follows execution through live events and exposes the
governance actions that apply to the current state, including approval, rejection,
resume, cancel, and abandon.

A run can be opened from a project at
`/console/p/:projectId/r/:runId` or directly by ID at
`/console/r/:runId`. Direct links also work for runs that have no registered
project. Run details include the event log and artifacts; a graph is shown when
the run has the project context needed to load its workflow definition.

Turn on **Alerts** in Settings to hear a sound, and see a browser notification
where the browser allows it, when a run stops at an approval gate or an
attention wait, or when it completes, fails, or is cancelled. The setting
is saved in the current browser and is off by default. Runs that were already
waiting or finished when the console loaded do not alert. Settings shows when
the browser blocks the sound or notifications.

Old settings, workflow-list, workflow-builder, and run bookmarks redirect to the
closest console route. Old chat and other retired pages land on the console
overview because the conversation browser is no longer shipped.

## Project chat

Project chat streams assistant text and tool activity. When Web authentication
is enabled, the console requests the signed-in user's project conversation and
sends the active identity with each turn. On a solo installation, chat works
without a user identity.

Attachments are limited to five files of at most 10 MB each. The first message in
a new conversation cannot include attachments; create the conversation with a
text message, then attach files on the next turn.

## Experimental workflow builder

The builder is retained as a bounded experiment inside the console. Use its
canvas, inspector, validation, and YAML preview to draft new workflows with
prompt, command, bash, script, loop, approval, wait, and cancel nodes. Editing an
existing workflow and relying on a load/edit/save round trip is not supported
until [#3378](https://github.com/coleam00/Archon/pull/3378) and
[#3379](https://github.com/coleam00/Archon/pull/3379) land. Edit existing
workflows as YAML. The builder also does not represent include directives, loop
groups, or workflow sub-run nodes. Workflow YAML remains the authoritative
artifact.

## Further reading

- [Quickstart](/getting-started/quick-start/)
- [Configuration](/getting-started/configuration/)
- [Workflow language](/reference/workflow-language/)
- [API Reference](/reference/api/)

---

### Packages Docs Web Src Content Docs Contributing Adding A Community Provider (`packages/docs-web/src/content/docs/contributing/adding-a-community-provider.mdx`)

---
title: Build a community provider
description: Publish an agent provider as an independently installed process plugin.
---

Community providers ship from their authors' repositories as optional plugins. They
attach through the provider contract without an import in Archon's registry. Claude,
Codex, and Pi remain maintained in this repository. The bundled OpenCode and Copilot
providers are deprecated and will be removed in the next minor release.

## Implement the provider contract

Implement `IAgentProvider` from `@archon/provider-contract`. Its source and
[contract README](https://github.com/coleam00/Archon/blob/dev/packages/provider-contract/README.md)
own the interface and stream rules. A provider supplies its id, capabilities, a
`sendQuery` stream, and `checkCredential`; `resolveCredentialModel` is optional.
Preserve the runtime's native user settings, authentication, and project guidance.

A successful stream yields provider events, a `result`, then `settled` only after all
observed work ends. Report usage once. Include the native session id when declaring
session resume. Carry failures through the typed failure class; do not classify free
prose with keyword matching. Cancellation must stop the provider's own work and must
not manufacture `settled`.

Capabilities are promises. Declare only what the implementation enforces. A node
naming unsupported skills, plugins, or MCP servers fails before spend. A provider that
supports those capabilities must suppress ambient capabilities and load exactly the
ones the node names, while preserving unrelated native configuration and guidance.
`backgroundWork: 'reported'` requires observable task lifecycles; `none` means verified
absence, and `unobserved` means work may exist without lifecycle visibility.

A provider that loads named skills only from its own directories declares them in the
descriptor's optional `skillDiscovery`: `project` roots relative to the working
directory, `user` roots relative to the user directory, and an optional
`userDirectory: { env, home }` naming the environment variable that overrides it and
its home-relative fallback. Such a provider must refuse a named skill whose directory
exists but is not loadable from its enabled roots, and workflow validation reports that
case as an error. With the `settingSources` capability, the `project` and `user` setting
sources gate those roots. A provider without `skillDiscovery` is validated against
Archon's shared skill roots, and a missing skill is only a warning.

A request's `systemPrompt` uses the provider's native system-instruction behavior (Copilot appends to its foundation prompt). Its
`appendSystemPrompt` adds instructions to that prompt; a provider that cannot extend
its prompt uses them as its system prompt, and a `systemPrompt` takes precedence.

## Streaming event mapping

Each runtime reports its own event types, and the provider maps them to the contract's chunks. The maintained providers own the full mapping, including interrupted tool calls, subtasks, hooks, MCP status and tool-output limits.

**Claude Code SDK** ([`packages/providers/src/claude/stream.ts`](https://github.com/coleam00/Archon/blob/dev/packages/providers/src/claude/stream.ts)):

Assistant text becomes `agent_message_chunk`; tool use becomes `tool_call`. The provider reports the turn's `result` separately from completion of background work. It tracks visible tasks and live background tasks from the SDK's lifecycle events. An idle notification ends the turn only after a result has been observed and both task sets are empty. A running notification resets the result condition for the next turn.

If the stream closes while observed work remains, or before reaching idle after background work was observed, it ends without `settled`. Once those settlement conditions hold, a stream that closes without a result produces a failure result (`stream_ended_without_result`), then `settled`. An early idle notification alone produces neither completion nor failure.

**Codex app-server** ([`packages/providers/src/codex/stream.ts`](https://github.com/coleam00/Archon/blob/dev/packages/providers/src/codex/stream.ts)): Archon speaks Codex's JSON-RPC protocol over stdio, one `codex app-server` process per turn.

`streamTurn` maps parent-thread messages and foreground tools to provider events. The parent's `turn/completed` notification produces its successful `result`, but background work can still be live. Failure results wait until observed background work drains because the executor stops reading on failure; failure classification uses the typed `codexErrorInfo`.

Subagents and unified-exec processes report separate `subtask` lifecycles. The provider tracks starts for the parent thread and turn, registers subagent threads, and tracks further subagents and processes on those recognized descendant threads. Child replies do not replace the parent result.

While any observed task remains live, the provider keeps accepting background starts, including root subagents and unified-exec processes first observed after parent completion. Nested work is also drained, even after its direct parent task ends. Notification arrival order does not establish when the work began.

Once the parent has completed and the observed task set is empty, the provider stops reading notifications and shuts down the app-server. This can happen immediately at parent completion if no background work was observed. Starts beyond that observation boundary are not guaranteed to be consumed; the boundary does not establish that Codex cannot report another start later.

`sendQuery` emits `settled` only after a clean app-server shutdown and without lost observation. A late start observed while draining can therefore extend the wait. If the connection closes before observed work drains, the turn does not settle: before a result, the provider reports a failure result; after a result, it logs the connection failure and ends without `settled`.

## Serve it over stdio

Wrap the implementation with the contract's plugin entrypoint:

```ts
import { serveProvider, PROVIDER_PLUGIN_PROTOCOL, type ProviderPluginDescriptor } from '@archon/provider-contract/plugin';
import { ExampleProvider } from './provider';
import { capabilities } from './capabilities';

const descriptor: ProviderPluginDescriptor = {
  protocol: PROVIDER_PLUGIN_PROTOCOL,
  id: 'example',
  displayName: 'Example provider',
  version: '1.0.0',
  capabilities,
  credentials: { kind: 'static', specs: [] },
  configSchema: {
    type: 'object',
    properties: { model: { type: 'string' } },
    additionalProperties: false,
  },
};

await serveProvider({ descriptor, create: () => new ExampleProvider() });
```

`capabilities` must provide the full `ProviderCapabilities` shape. The descriptor's
`configSchema` is the fallback for all configuration scopes within protocol 2. Protocol-1
plugins and hosts must update to protocol 2 before they can initialize together.
Runtime discovery reports and skips incompatible or unusable installed receipts,
including providers with mismatched executable identities or missing executable ownership,
leaving other plugins available. Use `archon plugin update <id>` or `archon plugin remove <id>` to
repair the install. A provider can
add `config: { install, run, snapshot, stripUnknownKeys? }` to use separate JSON
Schemas for install defaults, per-run settings, and persisted snapshots. Newer hosts
prefer `config`. Generate these schemas from the same schemas used by the in-process
parser, using `z.toJSONSchema(schema, { io: 'input' })` for Zod schemas. Set
`stripUnknownKeys: true` to strip undeclared fields recursively where object member
rules omit `additionalProperties`. Unconstrained schemas such as `{}` preserve object
contents. Explicit `additionalProperties: false` still rejects
unknown fields; `true` or a schema preserves allowed fields, including record keys.
Snapshot projection retains fields declared by the snapshot schema, including
referenced schemas, all `allOf` conjuncts, the selected `anyOf` or `oneOf` alternative,
and matching `patternProperties`.
An explicit `additionalProperties: false` also rejects unknown snapshot fields.
For snapshots that validate install settings before excluding them, use
`snapshotConfigSchema(portableFields, localFields)` from `@archon/provider-contract`.
It generates a strict input schema and marks the local fields `writeOnly: true`;
newer hosts validate those fields before omitting them from the snapshot output.

JSON Schema does not encode Zod string transforms. Use
`normalizedConfigString(stringSchema, 'trim')` to trim a config string or
`normalizedConfigString(stringSchema, 'slash-separated')` to trim the two sides of
the first slash in a structured reference. These helpers generate the
`x-archon-normalize` annotation and the matching in-process transform from one
owner. Scoped process registration applies normalization after validation. Credential lookup has only a model: alternative run branches must agree on its normalization, or lookup returns no vendor.


Keep `configSchema` as the fallback schema for install, run, and snapshot when
`config` is omitted. This fallback is part of protocol 2; it does not provide
compatibility with protocol-1 peers. Providers that need scoped snapshot exclusion,
recursive stripping, or normalization annotations must declare scoped `config`
schemas and require a host version that supports those semantics.

Credentials are a static
list of vendor specs; declare only vendors the provider consumes. API-key vendors must
have an existing Archon delivery rule. With one vendor, all models use it. With several,
model-only lookup uses the `model` field rule from the descriptor's run schema
(`config.run`, else `configSchema`), following root references and composition. It validates
and normalizes that field without validating unrelated run fields or root constraints.
The text before the first `/` selects a declared vendor after credential alias
normalization; a model rejected by its field rule selects none. An empty list uses native auth.
`checkCredential` returns `CredentialStatus` for doctor and run preflight, and receives
its environment and abort signal from Archon. During run preflight it also receives the
run's `cwd`, `execContext` and `settingSources`: check authentication where the run will
execute (the checkout, or the container for a container `execContext`), not in the
host process directory. Return `not_checked` when the runtime cannot check without
spending on a session.

Optional `IAgentProvider.diagnose({ assistantConfig, signal })` and `listModels({ signal })` hooks
serve information to hosts through `_archon/diagnose` and `_archon/list_models`.
Diagnostics return `{ checks: [{ id, label, status, message, remedy? }] }`, with
`status` one of `ok`, `warn`, `fail`, or `skip`. Model lists return
`{ models: [{ id, label? }] }`. These hooks do not prompt or change configuration. The caller owns the deadline; both hooks receive an abort signal. A process-backed call stops its plugin process on cancellation.
An unsupported hook returns JSON-RPC `-32601`; it does not report an empty successful
result. Diagnostic messages and remedies must never include credentials, tokens, or
user message text. The host withholds plugin stderr and plugin-controlled errors.

The wire uses ACP v1's `initialize`, `session/new`, `session/prompt`, and
`session/cancel` lifecycle, with unchanged `ProviderChunk` values in `_archon/chunk`
notifications. Stdout is exclusively newline-delimited JSON-RPC. Native tool specifications
travel in the session request; `_archon/tool_call` invokes the host handler and returns
its text result. Calls require a live, unsettled session. Dynamic credential catalogs
and mid-run permission requests remain unsupported. A dynamic catalog is rejected.
`_archon/log` carries structured metadata logs (`level`, `msg`, JSON `bindings`).
`serveProvider` supplies an awaitable log sink to `create(log)`; `connectProvider` accepts
an `onLog` callback. Log metadata only, never credentials, tokens, or message text.
The process host logs a fixed `provider.plugin.log` event at the supplied severity
through `provider.<id>`, using its own descriptor id. The only plugin values it forwards
are `bindings.failureClass`, when it is one of the contract's failure classes, and
`bindings.lifecycle`, when it is one of the contract's lifecycle events
(`providerLifecycleEventSchema`), plus a lifecycle event's non-negative integer
`bindings.waitedMs`; the host drops a record with neither enum value. Plugin `msg`, module identity, and all other
bindings are withheld: any plugin-chosen string, even one that looks like a machine
token, can carry a credential.
Container requests carry their optional `env` bag as session data while the plugin
process retains the host environment.
This is an Archon provider protocol, not a generic plain-ACP adapter.

The language-neutral definitions are in
[`packages/provider-contract/schema/provider-contract.schema.json`](https://github.com/coleam00/Archon/blob/dev/packages/provider-contract/schema/provider-contract.schema.json).
They include the descriptor, lifecycle, request, and chunk schemas. Non-TypeScript
authors can implement that wire directly. npm publication waits for the versioned SDK;
until then, vendor the contract package or implement the generated schema.

## Prove conformance

Use `runProviderConformance` from `@archon/provider-contract/conformance` with fixture
turns and the declared capabilities. Include typed failure cases, structured output,
session handoff, and observable background cases for every capability claimed. Use
`checkCredentialStatuses` to exercise native credential states.

Run the same suite in process and against the executable. A binary connection uses
`connectProvider` with the child's stdio streams:

```ts
import { spawn } from 'node:child_process';
import { Readable, Writable } from 'node:stream';
import { connectProvider } from '@archon/provider-contract/plugin';
import { runProviderConformance } from '@archon/provider-contract/conformance';

const child = spawn('/absolute/path/archon-provider-example', [], {
  stdio: ['pipe', 'pipe', 'inherit'],
});
const closed = new Promise(resolve => child.once('close', resolve));
const client = await connectProvider({
  readable: Readable.toWeb(child.stdout),
  writable: Writable.toWeb(child.stdin),
});
try {
  const failures = await runProviderConformance({
    capabilities: client.getCapabilities(),
    turns: [{ name: 'turn', run: () => client.sendQuery('hello', process.cwd()) }],
    failureCases: [
      {
        name: 'auth',
        expected: 'auth',
        // Vendor text the typed failure's evidence must keep.
        evidence: 'HTTP 401',
        // A fixture prompt your executable answers with a vendor auth failure.
        run: () => client.sendQuery('failure', process.cwd()),
      },
    ],
  });
  if (failures.length) throw new Error(failures.join('\n'));
} finally {
  await client.close();
  await closed;
}
```

Add the capability-specific fixtures your implementation needs. Compare the chunks
with in-process output, and test cancellation with real descendants. Keep credentials
and user-message contents out of diagnostics. See the deterministic test provider in
`packages/provider-contract/src/fixtures/test-provider/` for the basic transport proof.

## Publish and install

Follow [Publish a plugin](/guides/publishing-plugins/#provider-plugins) for the manifest,
release assets, checksums, environment policy, and install commands. The manifest
executable must be `archon-provider-<descriptor id>`. Installation is the trust boundary:
the plugin runs as the operator's user with the same environment policy as in-process
providers. After installation, set `provider: <id>` in the workflow. Restart a running
server to load changed receipts; hot registration is outside this contract.

---

### Packages Docs Web Src Content Docs Contributing Cli Internals (`packages/docs-web/src/content/docs/contributing/cli-internals.md`)

---
title: CLI Internals
description: Technical reference for the Archon CLI package — entry point flow, command routing, worktree logic, and adapter details.
category: contributing
area: cli
audience: [developer]
status: current
sidebar:
  order: 2
---

Technical reference for understanding CLI internals.

## Package Structure

```
packages/cli/
├── src/
│   ├── cli.ts              # Entry point, argument parsing, routing
│   ├── commands/
│   │   ├── workflow.ts     # workflow route module: re-exports every subcommand; reset-sessions/cleanup/event
│   │   ├── workflow-control.ts # workflow resume/abandon/cancel/answer/approve/reject/respond (delegate to @archon/core/operations)
│   │   ├── workflow-query.ts # workflow list/test/status/get/runs/logs/wait
│   │   ├── workflow-run-*.ts # workflow run: select, admit, launch, execute
│   │   ├── isolation.ts    # isolation list/cleanup (list/merged-cleanup delegate to @archon/core/operations)
│   │   ├── setup.ts        # setup command implementation
│   │   ├── chat.ts         # chat command implementation
│   │   ├── validate.ts     # validate command implementation
│   │   └── version.ts      # version command
│   └── adapters/
│       └── cli-adapter.ts  # IPlatformAdapter for stdout
└── package.json            # Defines "archon" binary
```

## Entry Point Flow

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Code:** `packages/cli/src/cli.ts`

Repository env normally overrides home env. The CLI preserves the user-scope `GH_CONFIG_DIR` (including its absence), so repository env cannot redirect the stored gh login. Inside a workflow, inherited GitHub identity values take precedence over both env files, including empty values that disable host credentials. See [forge plugin configuration](/reference/forge/#plugin-configuration).

**Project resolution:**

- `workflow run --folder` uses the supplied canonical directory, even inside Git, and explicitly registers it as a folder project.
- A registered folder and its ordinary descendants resolve to the folder directory. Archon runs folder workflows in place without managing a worktree, branch or PR.
- A Git repository nested inside a registered folder retains its own repository identity. Other Git paths resolve to the nearest repository root, preserving linked-checkout ownership.
- Unregistered non-Git execution requires folder registration; first-use CLI execution uses `workflow run --folder`. Without registration or that flag, execution exits with code 1.
- File-only dry runs and fixture tests can inspect local files without registration. On a readable registry they honor folder ownership; if lookup fails they use the proven Git root or supplied directory. `workflow list` and `validate` retain their Git-root fallback when the registry cannot open.
- `version`, `help`, `setup`, and `chat` bypass project resolution.

---

## `workflow list` Flow

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Code:** `packages/cli/src/commands/workflow-query.ts`

---

## `workflow run` Flow

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Code:** `workflowRunCommand` in `packages/cli/src/commands/workflow-run-owned.ts`, which owns the captured source and the durable-wait loop. The phases live in `workflow-run-admission.ts`, `workflow-run-launch.ts` and `workflow-run-execution.ts` beside it.

**Worktree Provider:** `packages/isolation/src/providers/worktree.ts`

---

## `workflow event emit` Flow

```
┌──────────────────────────────────────────────────────────────────┐
│ archon workflow event emit --run-id <run-id> --type <type> [...] │
└──────────────────────────────┬───────────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────────┐
│ cli.ts  Validate --run-id, --type (required)                     │
│         Validate --type against WORKFLOW_EVENT_TYPES              │
│         Parse --data as JSON (warn + skip if invalid)            │
└──────────────────────────────┬───────────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────────┐
│ workflow.ts  workflowEventEmitCommand(..., cwd)                   │
│              Resolve an unambiguous run-id prefix                 │
│              Node state: persistWorkflowEvent(...)               │
│              Observability: createWorkflowEvent(...)             │
│              Run-ID resolution may fail                           │
└──────────────────────────────────────────────────────────────────┘
```

**Code:** `packages/cli/src/cli.ts` (case 'event'), `packages/cli/src/commands/workflow.ts:workflowEventEmitCommand`

**Contract:** The shared `isNodeStateEventType` predicate routes node-state events through
`persistWorkflowEvent`, which propagates storage failures. The CLI prints `Event persisted`
only after that write succeeds. Other events use `createWorkflowEvent` and retain best-effort
persistence: `Event submitted (best-effort)` does not guarantee storage. Run-ID resolution
can fail before either write.

---

## `isolation list` Flow

```text
archon isolation list → isolationListCommand()
  │
  ├─ @archon/core listEnvironments()
  │    ├─ isolationDb.listAllActiveWithCodebase()
  │    ├─ For each codebase: isolationDb.listByCodebaseWithAge()
  │    │    └─ Reconcile missing worktrees, preserving live-run ownership
  │    └─ Return environments grouped by codebase and reconciliation totals
  │
  ├─ Print worktree details and totals (return early if no codebases)
  │
  ├─ @archon/core listContainerEnvironments()
  │    ├─ isolationDb.listActiveContainerEnvironments()
  │    ├─ getRunByIsolationEnvId() for each container
  │    │    └─ Return matched run or null; throw on query/status errors
  │    └─ Build summaries; catch lookup errors and set runStatus: lookup-failed
  │
  └─ Print container details and totals: run status, no run (orphan),
     or run lookup failed
```

**Code:** `packages/cli/src/commands/isolation.ts` (`isolationListCommand`),
`packages/core/src/operations/isolation-operations.ts`
(`listEnvironments`), `packages/core/src/services/cleanup/containers.ts`
(`listContainerEnvironments`), `packages/core/src/db/workflow-run-reads.ts`
(`getRunByIsolationEnvId`).

The container listing matches `metadata.isolation_env_id`; no match does not
establish that cleanup can remove the container. Cleanup checks wider live
ownership. A failed lookup is logged and displayed as `run lookup failed`.

---

## `isolation cleanup` Flow

```text
archon isolation cleanup [days] (default: 7)
  │
  ├─ Reconcile missing worktrees (preserve container rows)
  │
  ├─ findStaleEnvironments(days)
  │    └─ Filter provider = 'worktree'
  │         └─ For each stale worktree:
  │              ├─ Skip environments owned by resumable runs
  │              ├─ Skip any with a commit on no remote (getCleanupRunLock)
  │              └─ provider.destroy(path, options)
  │                   └─ Mark destroyed only after complete removal
  │
  └─ cleanupContainerEnvironments(days), even when no worktrees are stale
       ├─ Skip containers owned by resumable runs
       └─ Container backend removes container and volume
            └─ Mark destroyed only after successful removal
```

**Code:** `isolationCleanupCommand` in `packages/cli/src/commands/isolation.ts`

---

## `isolation cleanup --merged` Flow

```
/* Detailed source-code truncated for AI context efficiency. */
```

Completion and merged cleanup retain remote branches, including with force,
because registration ownership cannot be proved after retirement. Retention is
logged as a warning and displayed in both completion and merged-cleanup reports.
Enable GitHub's **Automatically delete head branches** setting for
merged pull requests, or follow [manual remote branch recovery](/reference/cli/#recovering-remote-branch-cleanup).

Signals are evaluated in order — the first positive match short-circuits to avoid
unnecessary `gh` API calls. The `gh` CLI is a soft dependency: when it is not installed or
the remote is not GitHub, only git signals are used (`NONE`). When `gh` is there but fails
(auth, rate limit, unreadable output) the result is `UNAVAILABLE`, and the environment is
kept and reported rather than passed to the staleness sweep.

A multi-commit squash merge is invisible to both git signals, so PR state is the only
one that sees it. Both git signals also need the local branch ref: once it is gone, the
PR is the only judge, and a branch with no PR is reported as unverifiable instead of
failing the check. A MERGED or CLOSED PR only covers the commits it carried. Run
branch names are reused, and removal deletes both the worktree and the branch, so each
local tip that still exists, the worktree's HEAD and the branch ref, must be the PR's head
commit or an ancestor of it (`isRevCoveredBy`). A detached HEAD or a reused branch with
commits past the PR head is unmerged work. The same holds when git proves the merge: the worktree's
HEAD must also pass `git cherry <base> HEAD`, which answers for both git signals (an
ancestor of the base lists nothing, a squash-merged commit lists as `-`). A HEAD it
cannot answer for is reported as a failed merge check. A PR head pushed from elsewhere and never fetched here is
fetched by SHA from the branch's remote first; if that fetch fails, the check fails and
the worktree is kept, with the fetch error in the reason. The scheduled sweep (`runScheduledCleanup`) makes the same call, so
both paths agree on what counts as merged.

**Code:** `packages/core/src/services/cleanup/` — `judgeBranchForRemoval()` in `merge-verdict.ts`, `cleanupMergedWorktrees()` in `merged-sweep.ts`, `getCleanupRunLock()` in `run-lock.ts`; `runScheduledCleanup()` in `packages/core/src/services/cleanup-service.ts`
**Code:** `packages/isolation/src/pr-state.ts` — `getPrState()`
**Code:** `packages/git/src/branch.ts` — `isPatchEquivalent()`, `localBranchExists()`

---

## CLI Adapter

Implements `IPlatformAdapter` for terminal output.

```
┌─────────────────────────────────────────────────────────────────┐
│ CLIAdapter                                                      │
├─────────────────────────────────────────────────────────────────┤
│ sendMessage(convId, msg) → Output to stdout                     │
│ getStreamingMode()       → 'batch'                              │
│ getPlatformType()        → 'cli'                                │
│ ensureThread()           → passthrough                          │
│ start() / stop()         → no-op                                │
└─────────────────────────────────────────────────────────────────┘
```

**Code:** `packages/cli/src/adapters/cli-adapter.ts:13-47`

---

## Key Dependencies

| Function | Package | Location | Purpose |
|----------|---------|----------|---------|
| `discoverWorkflowsWithConfig(cwd, loadConfig, providerRegistry)` | `@archon/workflows/workflow-discovery` | `workflows/src/workflow-discovery.ts` | Find and parse workflow YAML |
| `executeWorkflow(...)` | `@archon/workflows/executor` | `workflows/src/executor.ts` | Run workflow steps |
| `getIsolationProvider()` | `@archon/isolation` | `isolation/src/factory.ts` | Get WorktreeProvider singleton |
| `conversationDb.*` | `@archon/core` | `core/src/db/conversations.ts` | Conversation CRUD |
| `codebaseDb.*` | `@archon/core` | `core/src/db/codebases.ts` | Codebase CRUD |
| `isolationDb.*` | `@archon/core` | `core/src/db/isolation-environments.ts` | Worktree tracking |
| `git.*` | `@archon/git` | `packages/git/src/` | Git operations |
| `closeDatabase()` | `@archon/core` | `core/src/db/connection.ts` | Clean shutdown |

---

## Conversation ID Format

CLI conversations use ID format: `cli-{timestamp}-{random}`

The random suffix contains 16 cryptographically random bytes encoded as 32 hex characters.

Generated at: `packages/cli/src/utils/conversation-id.ts`

---

## Worktree Reuse Logic

When `--branch` is provided:

1. **Lookup:** `isolationDb.findActiveByWorkflow(codebaseId, 'task', branchName)`
2. **Health check:** `provider.healthCheck(path)` on existing
3. **Reuse:** If found and healthy (warns if `--from` was specified but not applied)
4. **Create:** If not found or unhealthy -- passes a `taskBranch: { kind: 'new', fromBranch }`
   selection to the provider when `--from` is specified. Adoption instead passes
   `taskBranch: { kind: 'existing', branch }`, which checks out that exact local branch
   without creating a child branch or syncing it to a remote.

Worktrees stored at: `~/.archon/workspaces/<owner>/<repo>/worktrees/<branch-slug>/`

**Code:** `provisionWorktree` in `packages/cli/src/commands/workflow-run-launch.ts`

---

## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success, except for the JSON control refusals noted below. |
| 1 | Error. Answer commands (`answer`, `respond`, `approve`, `reject`) and `resume --detach` also return 1 on refusal. Human-mode diagnostics go to stderr; JSON refusals emit `ok: false` with the reason on stdout. |

Non-detached JSON `resume`, `abandon`, and `cancel` can return 0 with `ok: false`.
Check `ok` when consuming those acknowledgments.

---

## Database Connection

- Connection opened on first database call
- Always closed in `finally` block after command completes
- **Default: SQLite** at `~/.archon/archon.db` (zero setup, auto-initialized)
- **Optional: PostgreSQL** when `DATABASE_URL` is set (for cloud/advanced deployments)

**Code:** `packages/cli/src/cli.ts`

---

### Packages Docs Web Src Content Docs Contributing Dx Quirks (`packages/docs-web/src/content/docs/contributing/dx-quirks.md`)

---
title: DX Quirks
description: Known development experience quirks and workarounds when working on the Archon codebase.
category: contributing
audience: [developer]
status: current
sidebar:
  order: 6
---

Development experience notes and workarounds.

## Bun Log Elision

When running `bun dev` from the repo root, Bun's `--filter` truncates logs:

```
@archon/server dev $ bun --watch src/index.ts
│ [129 lines elided]
│ [Hono] Server listening on port 3090
└─ Running...
```

**To see full logs**, run directly from the server package:

```bash
cd packages/server && bun --watch src/index.ts
```

Or:

```bash
bun --cwd packages/server run dev
```

Note: The root `bun dev` uses `--filter` to fix hot reload path issues, but this comes with log condensing.

## `mock.module()` merges over the real module

Bun merges a `mock.module()` factory over the real module: an export the factory leaves out keeps its real implementation. When a mocked module gains an export, every factory that does not list it starts running real code (a database, the network, the filesystem) while its tests still pass, often until a 5-second timeout.

`bun run check:mock-modules` (validate id `mock-modules`, run in CI) fails on every factory that leaves out a value export of the module it mocks, naming the file, line, specifier and missing names. Mock each missing export, or spread the real module to keep the rest real on purpose:

```ts
mock.module('../db/workflows', async () => ({
  ...(await import('../db/workflows')),
  getWorkflowRun: mockGetWorkflowRun,
}));
```

A spreading factory provides every export, including ones the module gains later; that is the author's declaration. Where test files share a process (packages other than `@archon/core`, unless the file has its own group), the `import()` can return a module an earlier file already mocked.

Factories that were incomplete when the check landed are grandfathered in `scripts/mock-module-baseline.json`. A grandfathered site may omit only the exports its module's grandfathered sites omitted then, so a new export still fails at every one of them. The baseline only shrinks: after completing a factory, run `bun run check:mock-modules --write`. `bun run check:ratchet` refuses entries the baseline gains unless a commit in the change carries `Ratchet-Allow: scripts/mock-module-baseline.json`.

What the check does not prove:

- It reads factory keys from the factory's type, so an `as` cast can misstate them.
- It checks only literal specifiers, and refuses a non-literal one.
- It refuses a module TypeScript cannot read (no type declarations, or a path that does not resolve) unless the site is grandfathered.
- A grandfathered factory can drop a key that another grandfathered site of the same module already omits.
- It says nothing about a client a test never stubbed, or about I/O outside `mock.module()`. The adapters package preloads `no-network` (it traps only `globalThis.fetch`) and `no-archon-home-writes` guards for part of that; Bun reads `bunfig.toml` only from the working directory, so those guards apply only when the tests run from `packages/adapters`. Their headers list the limits.
- It runs in validate and CI, not inside `bun test`.

## `mock.module()` Pollution

Bun's `mock.module()` is process-global and irreversible — `mock.restore()` does NOT undo it.

- Never add `afterAll(() => mock.restore())` for `mock.module()` cleanup — it has no effect
- Use `spyOn()` for internal modules that other test files import directly (e.g., `spyOn(git, 'checkout')`) — `spy.mockRestore()` DOES work for spies
- Never `mock.module()` a module path that another test file also `mock.module()`s with a different implementation
- When adding a new test file with `mock.module()` to `packages/core`, make its first line exactly `// @archon-test-isolated` so it runs in its own process. Core discovers its tests, and discovery fails on a file that calls `mock.module()` without the marker. A file that mocks through a helper needs the marker too; discovery cannot see that call
- In the other packages, give the file its own entry in `testGroups` in the package's `package.json`, or its own `bun test` step for a package still using a `&&` chain
- A package's `testConcurrency` in `package.json` runs that many `testGroups` at once, each still in its own process; leave it out to run groups one at a time
- Groups start slowest first, by the times in `scripts/test-weights.json`. CI splits slow shards into parts with `ARCHON_TEST_PART=<index>/<count>`, using the same times; `ARCHON_TEST_PART=1/2 bun run test` in a package runs that part locally. A new group with no recorded time counts as one second, so refresh the file with `bun scripts/update-test-weights.ts <run-id>` when one part of a shard finishes well after its sibling

## Worktree Port Allocation

Worktrees auto-allocate ports (3190–4089 range, hash-based on path). Same worktree always gets same port.

- Main repo defaults to 3090
- Override: `PORT=4000 bun dev`
- Same worktree always gets same port (deterministic)
- If that port is already taken — another worktree hashed to it, or any unrelated
  process holds it — the server takes the next free port in the range and logs
  `worktree_port_reallocated` with both the port it wanted and the one it got.
  The root `bun dev` launcher passes that selected port to both the server and Web
  client, so the Vite proxy and SSE streams use the endpoint the server binds. It
  returns to the hashed port once that frees up.
- `PORT` is never moved: an explicit override binds where you said, or fails.

## `bun run test` vs `bun test`

**NEVER run `bun test` from the repo root** — it discovers all test files across all packages and runs them in one process, causing ~135 mock pollution failures.

Always use `bun run test`, which runs each package's own grouped suite for isolation.

### Running one test

`bun run test <path>` runs that path.

```bash
bun run test packages/paths/src/skills.test.ts   # from the repo root
cd packages/paths && bun run test src/skills.test.ts
```

From the repo root, an argument naming a path under `packages/<name>/`, `scripts/` or
`.archon/scripts/` decides where `bun test` runs; flags and substring filters are
forwarded unchanged. Paths naming several packages run one package at a time and stop at
the first failure. An argument that names no owner fails instead of running the suite.

A package whose `scripts.test` is still a `bun test ... && bun test ...` chain rather than
`bun run ../../scripts/package-tests.ts` ignores the argument when you run it from inside
the package: `bun run` appends arguments to the script, so the whole chain runs and a wrong
path still reports a pass. Run those from the repo root, which routes the path itself.

---

### Packages Docs Web Src Content Docs Contributing New Developer Guide (`packages/docs-web/src/content/docs/contributing/new-developer-guide.md`)

---
title: New Developer Guide
description: Codebase orientation for new Archon developers — architecture overview, workflows, platforms, and first steps.
category: contributing
audience: [developer]
status: current
sidebar:
  order: 1
---

> **TL;DR**: Archon lets you control AI coding assistants (Claude Code, Codex) from your phone via Telegram, Slack, Discord, or GitHub. Think of it as a remote control for AI pair programming.

---

## The Problem We Solve

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Core Concept: Message → AI → Code → Response

```
┌──────────────────────────────────────────────────────────────────────────┐
│                                                                          │
│   USER                    ARCHON                         CODEBASE        │
│                                                                          │
│   ┌─────────┐            ┌─────────────────┐            ┌──────────┐    │
│   │Telegram │            │                 │            │          │    │
│   │  Slack  │───Message─▶│   Orchestrator  │───Claude──▶│ Git Repo │    │
│   │ Discord │            │                 │   Code     │          │    │
│   │ GitHub  │◀──Response─│   (routes to    │◀──────────│ (files)  │    │
│   └─────────┘            │    AI client)   │            └──────────┘    │
│                          └─────────────────┘                             │
│                                                                          │
│   That's it. You message, AI works on code, you get results.            │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
```

---

## The Four Ways to Use Archon

### 1. Command Line (Local Execution)

Run workflows directly from your terminal without needing the server:

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Good for:** Running workflows locally, testing, automation scripts, CI/CD

### 2. Direct Chat (Simple Questions)

Just talk to the AI like you would in Claude Code terminal:

```
┌─────────────────────────────────────────────────────────────────┐
│ TELEGRAM CHAT                                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ You: What does the handleMessage function do?                   │
│                                                                 │
│ Archon: Looking at packages/core/src/orchestrator/orchestrator.ts...          │
│                                                                 │
│         The handleMessage function is the main entry point      │
│         that routes incoming messages. It:                      │
│         1. Checks if it's a slash command                       │
│         2. Loads conversation context from database             │
│         3. Routes to AI client for processing                   │
│         4. Streams responses back to platform                   │
│                                                                 │
│         See: packages/core/src/orchestrator/orchestrator.ts            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### 3. Slash Commands (Specific Operations)

Deterministic commands that don't involve AI:

```
┌─────────────────────────────────────────────────────────────────┐
│ SLASH COMMANDS                                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ /clone https://github.com/user/repo    Clone a repository      │
│ /status                                 Show current state      │
│ /repos                                  List available repos    │
│ /setcwd /path/to/dir                   Change working dir      │
│ /reset                                  Clear AI session        │
│ /help                                   Show all commands       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### 4. Workflows (Multi-Step Automation)

This is where Archon shines - automated multi-step AI workflows:

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## How Workflows Work (The Magic)

A workflow is a YAML file that chains AI prompts together:

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## The Router: How Archon Picks Workflows

When you send a message, an AI "router" decides what to do:

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Available Workflows

The table below lists the key workflows in the bundled `sdlc` pack. All bundled workflows are prefixed with `archon-`. Run `bun run cli workflow list` to see the full current list.

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Parallel Agents: The PR Review Example

A quick route agent picks which specialist lenses the change earns, and `archon-review` runs them in fresh sessions as shown below. The code lens uses the configured `large` model tier.

```
/* Detailed source-code truncated for AI context efficiency. */
```

Review never edits code. Fixing findings belongs to `archon-implement`, or to `archon-deliver`, which runs review and corrections in a loop.

---

## Platform Integration

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

Batch mode assembles the reply before delivery. Adapters may split long replies into multiple messages or comments.

## Isolation: Git Worktrees

Each conversation gets its own isolated copy of the repo:

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Configuration Hierarchy

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Directory Structure

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Quick Reference: Common Interactions

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Summary

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

## Next Steps

1. **Read**: [Quickstart](/getting-started/quick-start/) - Set up your first instance
2. **Explore**: `.archon/workflows/` - See example workflows
3. **Customize**: `.archon/commands/` - Create your own prompts
4. **Configure**: `.archon/config.yaml` - Tweak settings

Welcome to remote agentic coding!

---

### Packages Docs Web Src Content Docs Contributing Releasing (`packages/docs-web/src/content/docs/contributing/releasing.md`)

---
title: Releasing
description: How to create a new release of the Archon CLI — version management, release process, and troubleshooting.
category: contributing
area: infra
audience: [developer]
status: current
sidebar:
  order: 3
---

This guide covers how to create a new release of the Archon CLI.

## Version Management

Versions follow [Semantic Versioning](https://semver.org/):
- **Major** (1.0.0): Breaking changes to CLI interface or workflow format
- **Minor** (0.1.0): New features, new workflows, new commands
- **Patch** (0.0.1): Bug fixes, documentation updates

Version is stored in the root `package.json` only -- this is the single source of truth.

## Release Process

Releases are created by merging `dev` into `main`. Never commit directly to `main`.

### 1. Prepare the Release

Use the `/release` skill (or follow these manual steps):

```bash
# Ensure dev is up to date
git checkout dev
git pull origin dev

# Run default local validation
bun run validate
bun run validate --only format,workflow-fixtures
```

CI remains the full validation gate, including service and binary checks that local validation does not run.

The `/release` skill automates the following:
1. Compares `dev` to `main` to generate changelog entries
2. Bumps the version in root `package.json` (patch by default; use `/release minor` or `/release major` for other increments)
3. Updates `CHANGELOG.md` following Keep a Changelog format
4. Creates a PR from `dev` to `main`

### 2. Merge and Tag

Once the release PR is reviewed and merged:

```bash
# Create and push the tag from main
git checkout main
git pull origin main
git tag vX.Y.Z
git push origin vX.Y.Z
```

This triggers the GitHub Actions release workflow which:
1. Builds binaries for all platforms (macOS arm64/x64, Linux arm64/x64, Windows x64)
2. Generates checksums
3. Creates a GitHub Release with all artifacts

### 3. Update Homebrew Formula (Optional)

After the release workflow completes:

```bash
# Update checksums in the Homebrew formula
./scripts/update-homebrew.sh vX.Y.Z

# Review and commit
git diff homebrew/archon.rb
git add homebrew/archon.rb
git commit -m "chore: update Homebrew formula for vX.Y.Z"
git push origin main
```

If you maintain a Homebrew tap (`homebrew-archon`), copy the updated formula there.

### 4. Verify the Release

```bash
# Test the install script (only works if repo is public)
curl -fsSL https://raw.githubusercontent.com/coleam00/Archon/main/scripts/install.sh | bash

# Verify version
archon version
```

> **Note: Private Repository Installation**
>
> If the repository is private, the curl install script won't work for anonymous users.
> Use the GitHub CLI instead:
>
> ```bash
> # Download and install using gh (requires GitHub authentication)
> gh release download v0.2.0 --repo coleam00/Archon \
>   --pattern "archon-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')" \
>   --dir /tmp/archon-install
>
> # Install the binary
> chmod +x /tmp/archon-install/archon-*
> sudo mv /tmp/archon-install/archon-* /usr/local/bin/archon
>
> # Verify
> archon version
> ```

## Manual Release (When GitHub Actions Unavailable)

If GitHub Actions can't run (billing issues, private repo limits), create the release manually:

```bash
# 1. Build binaries locally (every target in the script's local TARGETS list).
#    TARGET or OUTFILE left set in the shell switches the script to single-target mode.
unset TARGET OUTFILE
./scripts/build-binaries.sh

# 2. Generate checksums.txt, which the install scripts verify against
./scripts/checksums.sh

# 3. Create the release with binaries and checksums
gh release create vX.Y.Z dist/binaries/* \
  --title "Archon CLI vX.Y.Z" \
  --generate-notes

# 4. Verify the release
gh release view vX.Y.Z
```

> **Note:** With `TARGET` and `OUTFILE` unset, `scripts/build-binaries.sh` cross-compiles
> the CLI and server executables for every target in its local `TARGETS` list into
> `dist/binaries/`. That list has no Windows target.
>
> A manual release is therefore incomplete. It lacks the assets only the CI release
> builds: the Windows binaries, the plugin executables (`archon plugin add`
> downloads these), and `archon-web.tar.gz` (`archon serve` downloads it). The `files:`
> list of the release step in `.github/workflows/release.yml` is the full set CI publishes.

## Manual Build (for Testing)

To build binaries locally without creating a release:

```bash
# Build every target in the script's local TARGETS list (no Windows)
unset TARGET OUTFILE
./scripts/build-binaries.sh

# Binaries are in dist/binaries/
ls -la dist/binaries/

# Generate checksums
./scripts/checksums.sh
```

## Release Workflow Details

The `.github/workflows/release.yml` workflow:

1. **Triggers on**:
   - Push of tags matching `v*`
   - Manual workflow dispatch with version input

2. **Build job** (runs in parallel for each platform):
   - Sets up Bun
   - Installs dependencies
   - Compiles binary with `bun build --compile`
   - Uploads as artifact

3. **Release job** (runs after all builds complete):
   - Downloads all artifacts
   - Generates SHA256 checksums
   - Creates GitHub Release with:
     - All binaries attached
     - checksums.txt
     - Auto-generated release notes
     - Installation instructions

## Troubleshooting

### Build Fails on GitHub Actions

Check the Actions tab for specific errors. Common issues:
- Dependency installation failure: Check `bun.lock` is committed
- Type errors: Run `bun run type-check` locally first

### Install Script Fails

The install script requires:
- `curl` for downloading
- `sha256sum` or `shasum` for verification
- Write access to `/usr/local/bin` (or custom `INSTALL_DIR`)

### Checksums Don't Match

If users report checksum failures:
1. Check the release artifacts are complete
2. Verify checksums.txt was generated correctly
3. Ensure binaries weren't modified after checksum generation

## Pre-release Versions

For testing releases before public announcement:

```bash
# Create a pre-release tag
git tag v0.3.0-beta.1
git push origin v0.3.0-beta.1
```

Pre-releases (tags containing `-`) are marked as such on GitHub.

## Hotfix Process

For urgent fixes to a released version:

```bash
# Create hotfix branch from tag
git checkout -b hotfix/0.2.1 v0.2.0

# Make fixes, then tag
git tag v0.2.1
git push origin v0.2.1

# Merge fixes back to dev
git checkout dev
git merge hotfix/0.2.1
git push origin dev
```

---

### Packages Docs Web Src Content Docs Deployment Cloud (`packages/docs-web/src/content/docs/deployment/cloud.md`)

---
title: Cloud Deployment
description: Prepare a cloud VPS for Archon, then run it with Docker Compose and automatic HTTPS.
category: deployment
area: infra
audience: [operator]
status: current
sidebar:
  order: 3
---

This page prepares a VPS: a user, SSH, a firewall and DNS. The [Docker guide](/deployment/docker/) then installs and runs Archon on it with Docker Compose and Caddy for HTTPS. Using your own proxy instead of Caddy? See [Running Archon behind your own reverse proxy](/deployment/reverse-proxy/).

For the fastest route, use the [cloud-init script](/deployment/docker/#cloud-init-fastest-setup) instead. It creates a user, opens the firewall and installs Docker when the VPS first boots. You still create the [DNS record](#point-your-domain-at-the-server).

## What you need

- A VPS from any provider (DigitalOcean, Linode, AWS EC2, Vultr and so on): 1 to 2 vCPUs, 2 GB RAM minimum (4 GB recommended), 20 GB SSD, Ubuntu 22.04 LTS.
- A domain or subdomain, such as `archon.yourdomain.com`.
- An SSH key. Create one before the VPS, and add the public key when you create it:

  ```bash
  ssh-keygen -t ed25519 -C "archon"
  cat ~/.ssh/id_ed25519.pub
  ```

When you create the VPS, choose the Ubuntu 22.04 image and your SSH key, and allow inbound SSH (22), HTTP (80) and HTTPS (443) if the provider has its own firewall or security group. Note the public IP.

## Secure the server

Connect with the image's default account: `root` on most VPS providers, `ubuntu` on AWS EC2, which disables root login. Create a deploy user with your key, and switch to it:

```bash
ssh root@your-server-ip            # or: ssh ubuntu@your-server-ip on EC2

sudo adduser deploy
sudo usermod -aG sudo deploy
sudo mkdir -p /home/deploy/.ssh
sudo cp ~/.ssh/authorized_keys /home/deploy/.ssh/
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
```

Test `ssh deploy@your-server-ip` in a second terminal before you go on. Then turn off password logins by setting `PasswordAuthentication no` in `/etc/ssh/sshd_config`, and restart SSH:

```bash
sudo systemctl restart ssh
sudo su - deploy
```

Open the firewall:

```bash
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443
sudo ufw --force enable
```

## Point your domain at the server

At your DNS provider, create an A record: name `archon` (or `@` for the root domain), value the server's public IP. With Cloudflare, set the record to DNS only, not proxied, so Caddy can obtain its certificate. Check it with `dig archon.yourdomain.com`.

## Install and run Archon

Follow the Docker guide's [manual server setup](/deployment/docker/#manual-server-setup). It installs Docker, clones the repository, sets up `.env` and the `Caddyfile`, and starts the stack. Before you expose the Web UI, choose a login in [Protect the Web UI](/deployment/docker/#protect-the-web-ui).

Check the deployment from your machine:

```bash
curl https://archon.yourdomain.com/api/health
```

---

### Packages Docs Web Src Content Docs Deployment Docker (`packages/docs-web/src/content/docs/deployment/docker.md`)

---
title: Docker Guide
description: Run Archon with Docker Compose, locally or on a server with PostgreSQL and automatic HTTPS.
category: deployment
area: infra
audience: [operator]
status: current
sidebar:
  order: 2
---

This page installs and runs Archon with Docker Compose: on your own machine, or on a server with PostgreSQL and Caddy for automatic HTTPS. To prepare a VPS first (a user, SSH, the firewall and DNS), see [Cloud Deployment](/deployment/cloud/).

The image includes the Claude Code binary from the Claude Agent SDK. On start, the entrypoint points `CLAUDE_BIN_PATH` at it unless you set `CLAUDE_BIN_PATH` yourself.

## Cloud-init (fastest setup)

`deploy/cloud-init.yml` prepares a new VPS when it first boots. Paste its contents into the provider's User Data field when you create the server (Ubuntu 22.04+ or Debian 12+), add your SSH key, and point your domain's A record at the server's IP.

| Provider | Where to paste it |
|---|---|
| DigitalOcean | Create Droplet, Advanced Options, User Data |
| AWS EC2 | Launch Instance, Advanced Details, User Data |
| Linode | Create Linode, Add Tags, Metadata (User Data) |
| Hetzner | Create Server, Cloud config, User Data |
| Vultr | Deploy, Additional Features, Cloud-Init User-Data |

The script installs Docker, opens ports 22, 80 and 443 (TCP, plus UDP 443 for HTTP/3), adds a 2 GB swapfile, clones the repository to `/opt/archon`, copies `.env.example` to `.env` and `Caddyfile.example` to `Caddyfile`, and builds the image. It also creates an `archon` user with Docker access but no sudo, and copies your SSH key to it. Use the default cloud user or root for system maintenance.

Setup takes 5 to 8 minutes and writes `/opt/archon/SETUP_COMPLETE` when it finishes. Then:

```bash
ssh archon@your-server-ip
nano /opt/archon/.env          # see "Configure .env" below
cd /opt/archon
docker compose --profile with-db --profile cloud up -d
```

## Local Docker Desktop (Windows and macOS)

Run Archon on your own machine with SQLite and the Web UI. No domain or VPS is needed.

```bash
git clone https://github.com/coleam00/Archon.git
cd Archon
cp .env.example .env           # set a Claude or Codex credential
docker compose up -d
```

Open `http://localhost:3000`. Add `--profile with-db` to use the PostgreSQL container instead of SQLite (see [Profiles](#profiles)).

On Windows, build from a WSL terminal (for example in `/mnt/c/Users/YourName/path/to/Archon`), not PowerShell. Docker Desktop cannot follow Bun workspace symlinks from a Windows build context and fails with `The file cannot be accessed by the system`. If the container fails with `exec docker-entrypoint.sh: no such file or directory`, the checkout has CRLF line endings. `.gitattributes` forces LF for shell scripts, so re-clone the repository.

## Manual server setup

Use this instead of cloud-init when you want each step under your control. Prepare the server with [Cloud Deployment](/deployment/cloud/) first.

1. Install Docker, then log out and back in so the group change applies:

   ```bash
   curl -fsSL https://get.docker.com | sh
   sudo usermod -aG docker $USER
   ```

2. Clone the repository and create the config files:

   ```bash
   git clone https://github.com/coleam00/Archon.git
   cd Archon
   cp .env.example .env
   cp Caddyfile.example Caddyfile
   ```

3. Edit `.env` as described in [Configure .env](#configure-env).

4. Start the app, PostgreSQL and Caddy:

   ```bash
   docker compose --profile with-db --profile cloud up -d
   ```

5. Check it:

   ```bash
   docker compose --profile with-db --profile cloud ps
   docker compose logs -f app
   curl https://archon.example.com/api/health    # from your machine
   ```

## Pre-built image

To run `ghcr.io/coleam00/archon:latest` without building from source:

```bash
mkdir archon && cd archon
curl -O https://raw.githubusercontent.com/coleam00/Archon/main/deploy/docker-compose.yml
curl -O https://raw.githubusercontent.com/coleam00/Archon/main/.env.example
cp .env.example .env           # see "Configure .env" below
docker compose up -d
```

This compose file has only the app service. To add PostgreSQL, uncomment the `postgres` service and the `postgres_data` volume, and set `DATABASE_URL`. It has no Caddy service.

### Verify image attestations

Published images carry BuildKit provenance and SPDX SBOM attestations. Resolve the tag to a digest, then inspect that digest so the image cannot change between inspection and deployment:

```bash
docker buildx imagetools inspect ghcr.io/coleam00/archon:latest
# Copy the top-level Digest, not a platform manifest's digest.
image='ghcr.io/coleam00/archon@sha256:<digest>'
docker buildx imagetools inspect "$image" --format '{{ json .Provenance }}'
docker buildx imagetools inspect "$image" --format '{{ json .SBOM }}'
```

Both results must be non-empty: each platform has a `SLSA` provenance object and an `SPDX` SBOM object. In the provenance, check three things:

- The source is `https://github.com/coleam00/Archon` and the revision is the release commit you intend to run. Current releases record them in `buildDefinition.externalParameters.request.args` as `label:org.opencontainers.image.source` and `label:org.opencontainers.image.revision`. Older BuildKit versions use `invocation.configSource` and `invocation.environment`.
- The workflow ref in `buildDefinition.internalParameters.github_workflow_ref` names `.github/workflows/publish.yml` at the expected ref.
- The builder ID (`builder.id` or `runDetails.builder.id`) links to the GitHub Actions run.

The package inventory is under `SPDX.packages`. The `unknown/unknown` entries in the manifest list are the attestation manifests. The attestations are unsigned metadata, so they do not authenticate the publisher by themselves. See [Docker's provenance documentation](https://docs.docker.com/build/metadata/attestations/slsa-provenance/).

## Configure .env

Docker Compose reads the repository's `.env`. Edit it directly. Don't run `archon setup` for a Docker install: the wizard writes Archon's CLI env files, not this one. `.env.example` documents every variable.

### AI credentials

Set at least one provider:

```ini
# Claude: an OAuth token from `claude setup-token` on your own machine
CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-xxxxx
# or an API key from console.anthropic.com
# CLAUDE_API_KEY=sk-ant-xxxxx

# Codex: values from ~/.codex/auth.json after `codex login`
# ARCHON_CODEX_ID_TOKEN=eyJhbGc...
# ARCHON_CODEX_ACCESS_TOKEN=eyJhbGc...
# ARCHON_CODEX_REFRESH_TOKEN=rt_...
# ARCHON_CODEX_ACCOUNT_ID=6a6a7ba6-...
```

`.env.example` sets `CLAUDE_USE_GLOBAL_AUTH=true`. That flag only satisfies the server's startup check: it tells Archon to rely on a `claude /login` session, and a fresh container has none. Set a token or key. When one is set, Archon uses it whatever the flag says. See [AI Assistants](/getting-started/ai-assistants/) for every provider.

### Server, domain and database

| Variable | Default in Docker | Purpose |
|---|---|---|
| `PORT` | `3000` | App port, published on the host. Local development defaults to 3090. |
| `DOMAIN` | none | Public hostname. Required for `--profile cloud`. |
| `DATABASE_URL` | unset (SQLite) | PostgreSQL connection string. |
| `POSTGRES_PORT` | `5432` | Host port for the PostgreSQL container, bound to `127.0.0.1`. |
| `LOG_LEVEL` | `info` | `fatal`, `error`, `warn`, `info`, `debug` or `trace`. |

For the PostgreSQL container, use the service name as the host:

```ini
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/remote_coding_agent
```

Platform tokens (`TELEGRAM_BOT_TOKEN`, `SLACK_BOT_TOKEN`, `GH_TOKEN`, `WEBHOOK_SECRET` and so on) are covered in each adapter's page under [Adapters](/adapters/). For a GitHub webhook, the payload URL is `https://archon.example.com/webhooks/github`; see [GitHub](/adapters/github/).

## Profiles

Compose profiles add PostgreSQL, HTTPS or form login to the base app service:

| Command | What runs |
|---|---|
| `docker compose up -d` | App with SQLite |
| `docker compose --profile with-db up -d` | App and PostgreSQL 17 |
| `docker compose --profile cloud up -d` | App and Caddy |
| `docker compose --profile with-db --profile cloud up -d` | App, PostgreSQL and Caddy |

Add `--profile auth` for the [form login sidecar](#form-login-sidecar). For an external database such as Supabase or Neon, set `DATABASE_URL` and use no database profile. There is no `external-db` profile.

- **No profile:** SQLite, stored in the data volume at `/.archon/archon.db`.
- **`with-db`:** a PostgreSQL 17 container. The app creates and upgrades the schema on every start. If you start this profile without setting `DATABASE_URL`, the app logs a warning and uses SQLite, and the container sits unused.
- **`cloud`:** [Caddy](https://caddyserver.com/) in front of the app, with Let's Encrypt certificates, HTTP to HTTPS redirects, HTTP/3 and unbuffered `/api/stream/*` for live updates. It needs a `Caddyfile` (copied from `Caddyfile.example`), `DOMAIN` set, the DNS A record in place, and ports 80 and 443 open. Caddy waits for the app's health check before it starts.

## Protect the Web UI

A Web UI on a public domain needs a login. Choose one of these:

- **Better Auth** (PostgreSQL only, recommended): per-user accounts, and the server rejects every `/api/*` request without a session. Set `BETTER_AUTH_SECRET`; see [Web UI login](/reference/configuration/#web-ui-login-better-auth-optional). Signup is closed by default: invite people with `ARCHON_AUTH_ALLOWED_EMAILS`, or open it with `ARCHON_AUTH_OPEN_SIGNUP=true`. With Better Auth on, you don't need either Caddy option below; the [Security reference](/reference/security/#adapter-authorization) explains how to retire them.
- **Caddy basic auth:** the browser's own login prompt, with no extra container.
- **Form login sidecar:** a styled login page and a session cookie, served by the `auth-service` container.

Use one Caddy option at a time, not both. Caddy always leaves `/webhooks/*` and `/api/health` open, and webhooks verify their own signatures. Caddy answers `/internal/*` with a 404 and never forwards it.

### Caddy basic auth

1. Generate a bcrypt hash. Never put a plaintext password in `.env`.

   ```bash
   docker run --rm caddy caddy hash-password --plaintext 'YOUR_PASSWORD'
   ```

2. Set `CADDY_BASIC_AUTH` in `.env`. Replace `admin` with your username and paste the complete hash unchanged:

   ```ini
   CADDY_BASIC_AUTH='basic_auth @protected {
     admin REPLACE_WITH_COMPLETE_HASH_OUTPUT
   }'
   ```

   This is a single-quoted multiline Compose value, so newlines and `$` characters survive as written. Don't double the dollar signs, and don't source the file in a shell. See [Compose interpolation](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/) and [Caddy's basic_auth](https://caddyserver.com/docs/caddyfile/directives/basic_auth).

3. Recreate Caddy so it reads the new value:

   ```bash
   docker compose --profile cloud up -d --force-recreate caddy
   ```

To turn it off, leave `CADDY_BASIC_AUTH` empty or unset.

### Form login sidecar

1. Generate a bcrypt hash. The first run builds the `auth-service` image.

   ```bash
   docker compose --profile auth run --rm auth-service \
     node -e "require('bcryptjs').hash('YOUR_PASSWORD', 12).then(h => console.log(h))"
   ```

2. Generate a cookie signing secret:

   ```bash
   docker run --rm node:22-alpine \
     node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
   ```

3. Set them in `.env`, writing every `$` in the hash as `$$` so Compose does not interpolate it:

   ```ini
   AUTH_USERNAME=admin
   AUTH_PASSWORD_HASH=$$2b$$12$$REPLACE_WITH_YOUR_HASH
   COOKIE_SECRET=REPLACE_WITH_64_HEX_CHARS
   # COOKIE_MAX_AGE=86400   # session length in seconds
   ```

4. In `Caddyfile`, uncomment the Option A block (`handle /login`, `handle /logout` and the `forward_auth` handle), and comment out the "No auth" `handle` block below it.

5. Start with the `auth` profile:

   ```bash
   docker compose --profile with-db --profile cloud --profile auth up -d
   ```

Your domain now redirects to `/login`. Visit `/logout` to end the session.

## Data and home volumes

The container keeps Archon's data in `/.archon`: workspaces, worktrees, artifacts, logs, plugins and the SQLite database. The user's home, `/home/appuser`, holds provider and tool state:

| Path | What it holds |
|---|---|
| `~/.claude/` | Claude Code skills, commands, agents, hooks, MCP config, sessions, memory and OAuth state |
| `~/.codex/` | Codex `auth.json` from `codex login`. With the `ARCHON_CODEX_*` variables set, startup overwrites it each time. |
| `~/.pi/agent/` | Pi `auth.json` from `pi /login`, `models.json`, `settings.json` and sessions |
| `~/.gitconfig` | Git identity, signing config, aliases and `safe.directory` entries |
| `~/.config/gh/` | GitHub CLI login from `gh auth login` |
| `~/.bash_history` | Shell history from `docker compose exec app bash` |

Both are Docker named volumes by default (`archon_data` and `archon_user_home`), so they survive restarts and rebuilds. To keep them at a host path instead, set `ARCHON_DATA` or `ARCHON_USER_HOME` in `.env`, and make the directory writable by UID 1001, the container user:

```bash
sudo mkdir -p /opt/archon-data /opt/archon-user-home
sudo chown -R 1001:1001 /opt/archon-data /opt/archon-user-home
```

```ini
ARCHON_DATA=/opt/archon-data
ARCHON_USER_HOME=/opt/archon-user-home
```

On every start the entrypoint fixes ownership of both paths, touching only files with the wrong owner.

- `ARCHON_HOME` has no effect inside the container, which always uses `/.archon`. Use `ARCHON_DATA` to choose where that lives on the host.
- A bind-mounted home starts with an empty `~/.gitconfig`, because Docker copies image content into new named volumes but never into bind mounts. The entrypoint still adds the `safe.directory` entries Archon needs. Set your Git identity with `git config --global` inside the container.
- `safe.directory` entries added at runtime stay in the home volume, even after the checkout is gone. Archon never removes them, because concurrent setup may rely on the same entry. See [Git directory trust](/reference/security/#git-directory-trust).
- To keep Pi's data on the data volume, set `PI_CODING_AGENT_DIR=/.archon/pi` before the container starts.
- To wipe the home volume: `docker compose down && docker volume rm archon_archon_user_home`.

### Root fallback for macOS bind mounts

On a macOS bind mount (VirtioFS), the ownership fix always fails: the host will not remap files to UID 1001. The entrypoint prints the `chown` error and exits 1, so the container restarts in a loop. Read-only mounts and SELinux or AppArmor denials on Linux fail the same way.

`ARCHON_ALLOW_ROOT_FALLBACK=1` in `.env` makes the entrypoint print a warning and keep running as root instead. Any other value, or none, keeps the exit. The variable does nothing when the ownership fix succeeds.

:::caution
Running as root also sets `IS_SANDBOX=1`, which disables the Claude provider's refusal to run `bypassPermissions` as root, so AI subprocesses run as root in the container. That is acceptable on a single-operator Mac where the bind mount already limits what the container can reach. On Linux the failure means the volume ownership is wrong: run `sudo chown -R 1001:1001 <path>` on the host instead.
:::

## GitHub credentials

The container uses `GH_TOKEN` from `.env`. You can also log in inside the container, and the login lands on the home volume:

```bash
docker compose exec -u appuser -e HOME=/home/appuser app gh auth login
```

- With `GH_TOKEN` set and GitHub App mode off, every start resets Git's `https://github.com` credential helper to one that uses `GH_TOKEN`. A helper written by `gh auth login` applies only without `GH_TOKEN`.
- In [GitHub App mode](/adapters/github-app-setup/) (`GITHUB_APP_ID`, a private key and `WEBHOOK_SECRET` set, `GITHUB_TOKEN` unset), startup removes only the PAT helper that the entrypoint wrote earlier. An incomplete App configuration leaves it in place. Other helpers are yours and stay.
- The entrypoint reads these variables from the container environment, not from `/.archon/.env`. Keep `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY` or `GITHUB_APP_PRIVATE_KEY_PATH`, `WEBHOOK_SECRET`, `GITHUB_TOKEN` and `GH_TOKEN` in the compose `.env`. If they are only in `/.archon/.env`, the server runs in App mode while clones keep using the old PAT.
- With `ARCHON_SDLC_FORGE=gh`, delete an empty `GH_TOKEN=` line. The SDLC workflows refuse an empty `GH_TOKEN` when `GITHUB_TOKEN` is unset, rather than let `gh` fall back to the stored login.

## Plugins

Install plugins inside the running container, as `appuser` so the server owns the files:

```bash
docker compose exec -u appuser app bun run cli plugin add coleam00/Archon/plugins/forge-github
docker compose exec -u appuser app bun run cli plugin add Wirasm/archon-video
```

`plugin update`, `plugin remove` and `plugin list` work the same way. Plugins live in `/.archon/plugins` on the data volume, so they survive restarts and rebuilds. See [Forge operations](/reference/forge/#install-the-github-plugin) and [Installed workflow packs](/guides/global-workflows/#installed-workflow-packs).

The bundled SDLC workflows use `gh` by default. To use the forge plugin instead, set `ARCHON_SDLC_FORGE=forge` and install forge-github v0.12.1 or later; some pack steps still need `gh`. The plugin looks for `GH_TOKEN`, then `GITHUB_TOKEN`, then the container's `gh` login. Your host's `gh` login is not visible inside the container. See [Use the forge path in the SDLC pack](/reference/forge/#use-the-forge-path-in-the-sdlc-pack).

The image includes a verified Slack plugin outside the data volume. When `SLACK_BOT_TOKEN` is set, in the container environment or in `/.archon/.env`, the entrypoint installs it offline into `/.archon/plugins` before the server starts, and logs the release and commit. You don't run `plugin add` for Slack. Slack still needs both `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` to connect; see [Slack](/adapters/slack/).

- An installed, verified Slack plugin stays as it is, including a release you pinned from another source. The image does not update it.
- Missing or changed Slack executables, ownership conflicts, or a broken image receipt when a new install is needed stop provisioning with an error in the boot logs. Repair the install before restarting.
- To turn Slack off, remove its tokens from every env source, run `docker compose exec -u appuser app bun run cli plugin remove coleam00/Archon/plugins/chat-slack`, and restart. Removing only the plugin reinstalls it on the next start. Removing only the tokens leaves it installed and reporting missing credentials.

The image has `git` and Bun, but not `uv`, Python, or tools a pack's scripts call, such as ffmpeg. Add them with [Dockerfile.user](#customizing-the-image).

## Container isolation is unavailable

`archon workflow run ... --container` starts a sibling Docker container for each run. Inside this compose stack it fails at preflight with "Cannot connect to the Docker daemon": the image has no `docker` CLI, and the stack does not mount `/var/run/docker.sock`. Worktree isolation and in-place folder runs work normally.

:::caution
Mounting the Docker socket into the app container is not part of the supported stack. The socket is root-equivalent: anything that can reach it can start a privileged container and take over the host. If you accept that on a single-operator host, read `packages/isolation/docker/SECURITY.md` first. To use `--container` without that risk, run Archon directly on a host with a local Docker daemon.
:::

## Building the image

```bash
docker build -t archon .
docker run --env-file .env -e ARCHON_DOCKER=true -p 3000:3000 archon
```

The Dockerfile builds in these stages:

1. `manifests` and `sources` derive the workspace layout from `packages/`, so a new package needs no Dockerfile edit.
2. `deps` installs all dependencies, including the dev dependencies the web build needs.
3. `image-plugins` downloads the pinned Slack release, checks its commit and checksum, and records a verified receipt.
4. `web-build` builds the Web UI with Vite.
5. `production` keeps production dependencies and the built Web UI.

The production image runs Bun as the non-root `appuser` (UID 1001), which Claude Code requires. It includes `git`, `curl`, `gh`, `postgresql-client`, `ripgrep`, `jq`, Chromium and [agent-browser](https://github.com/vercel-labs/agent-browser), which drives the system Chromium for browser testing. It ships the source of every workspace package except `docs-web`, `web` (only the built UI ships) and `chat-slack` (shipped as the verified release under `/opt/archon/image-plugins`).

The Slack release is pinned separately from Archon's version by the build args `SLACK_PLUGIN_TAG` (default `v0.12.1`) and `SLACK_PLUGIN_COMMIT` (default `5f2b4841aaca38180c0abbd32bc6345593292019`). Override both together; the build rejects a tag that resolves to a different commit. Downloads happen only at build time.

### Customizing the image

To add tools without editing the tracked Dockerfile:

1. Copy `Dockerfile.user.example` to `Dockerfile.user` (use `deploy/Dockerfile.user.example` for a server deploy) and edit it.
2. Copy `docker-compose.override.example.yml` to `docker-compose.override.yml` (or `deploy/docker-compose.override.example.yml`).
3. Run `docker compose up -d`. Compose merges the override file automatically.

Both files are gitignored, so your changes stay local.

## Maintenance

```bash
docker compose logs -f app            # follow app logs
docker compose logs --tail=100 app    # last 100 lines
docker compose restart app
docker compose down                   # stop, keep data
docker compose down -v                # stop and delete volumes, including data
docker system df                      # Docker disk usage
```

### Update

```bash
git pull
docker compose --profile with-db --profile cloud up -d --build
```

Use the same profiles you deployed with. The app applies schema changes on startup: it runs the idempotent `migrations/000_combined.sql` under a PostgreSQL advisory lock, so there is no manual migration step.

## Troubleshooting

### App exits with `no_ai_credentials`

The server found no Claude credential and no Codex credential. Set `CLAUDE_CODE_OAUTH_TOKEN`, `CLAUDE_API_KEY`, or `ARCHON_CODEX_ID_TOKEN` with `ARCHON_CODEX_ACCESS_TOKEN`. See [AI credentials](#ai-credentials).

### Caddy fails with "not a directory"

```
error mounting "Caddyfile": not a directory
```

`Caddyfile` did not exist, so Docker created a directory in its place:

```bash
rm -rf Caddyfile
cp Caddyfile.example Caddyfile
docker compose --profile cloud up -d
```

### Caddy gets no certificate

Check that `dig archon.example.com` returns the server's IP, that `sudo ufw status` shows 80 and 443 open, and that `DOMAIN` in `.env` is spelled right. Then read `docker compose logs caddy`. DNS changes can take up to an hour to propagate.

### Health check fails

The container health check calls `/api/health` on `PORT`, not `/health`:

```bash
curl http://localhost:3000/api/health
```

### PostgreSQL connection refused

With `--profile with-db`, `DATABASE_URL` must use the service name `postgres` as the host, not `localhost`. Check the container with `docker compose ps postgres`.

### Permission errors in `/.archon`

The entrypoint exits 1 with the `chown` error when it cannot give `/.archon` or `/home/appuser` to UID 1001. On Linux with a bind mount, run `sudo chown -R 1001:1001 /path/to/archon-data` on the host. On a macOS bind mount, `chown` on the host does not help; see [Root fallback for macOS bind mounts](#root-fallback-for-macos-bind-mounts).

### Port already in use

Set another `PORT` in `.env`, such as `PORT=3001`.

### Container keeps restarting

Run `docker compose ps` and `docker compose logs --tail=50 app`. The usual causes are a missing `.env`, an invalid credential or an unreachable database.

---

### Packages Docs Web Src Content Docs Deployment E2e Testing Wsl (`packages/docs-web/src/content/docs/deployment/e2e-testing-wsl.md`)

---
title: E2E Testing on WSL
description: Run agent-browser inside WSL for end-to-end testing when dev servers run on Windows.
category: deployment
area: infra
audience: [developer]
status: current
sidebar:
  order: 6
---

`agent-browser` (Vercel) has a [known Windows bug](https://github.com/vercel-labs/agent-browser/issues/56) where the daemon fails to start due to Unix domain socket incompatibility. The workaround is to run agent-browser inside WSL while the dev servers run on Windows.

> **General setup:** For non-WSL platforms (macOS, Linux, Docker), see the [E2E Testing Guide](/deployment/e2e-testing/) instead.

## Prerequisites

- WSL2 with Ubuntu installed (`wsl --list --verbose`)
- agent-browser installed in WSL: `npm install agent-browser`
- Playwright chromium installed: `agent-browser install --with-deps` (needs sudo)

## Setup

### 1. Find the Windows host IP accessible from WSL

```bash
ipconfig | findstr "IPv4" | findstr "WSL"
# Example output: IPv4 Address. . . . . . . . . . . : 172.18.64.1
```

Or from inside WSL:
```bash
wsl -d Ubuntu -- bash -c "cat /etc/resolv.conf | grep nameserver"
```

The Windows host IP for this system is `172.18.64.1`.

### 2. Start dev servers on Windows (bound to all interfaces)

```bash
# Backend (Hono on port 3090) - already binds to 0.0.0.0 by default
bun run dev:server &

# Frontend (Vite on port 5173) - needs --host flag
cd packages/web && bun x vite --host 0.0.0.0 &
```

### 3. Verify WSL can reach the servers

```bash
wsl -d Ubuntu -- curl -s http://172.18.64.1:3090/api/health
wsl -d Ubuntu -- curl -s -o /dev/null -w "%{http_code}" http://172.18.64.1:5173
```

## Running agent-browser Commands

All commands are run from the Windows terminal, prefixed with `wsl -d Ubuntu --`:

```bash
# Open a page
wsl -d Ubuntu -- agent-browser open http://172.18.64.1:5173

# Take interactive snapshot (get element refs like @e1, @e2)
wsl -d Ubuntu -- agent-browser snapshot -i

# Click, fill, press
wsl -d Ubuntu -- agent-browser click @e1
wsl -d Ubuntu -- agent-browser fill @e2 "some text"
wsl -d Ubuntu -- agent-browser press Enter

# Wait for content to load
wsl -d Ubuntu -- agent-browser wait 3000

# Reload page (hard refresh)
wsl -d Ubuntu -- agent-browser reload

# Close browser
wsl -d Ubuntu -- agent-browser close
```

## Taking Screenshots

Screenshots must be saved to a WSL-native path first, then copied to the Windows filesystem via the `/mnt/c/` mount:

```bash
# Save to WSL home, then copy to project
wsl -d Ubuntu -- bash -c '
  agent-browser screenshot /home/user/screenshot.png 2>&1 &&
  cp /home/user/screenshot.png /path/to/archon/e2e-screenshots/my-test.png
'
```

**Why not save directly to `/mnt/c/...`?** agent-browser resolves paths through its Node.js process, which on some setups mangles `/mnt/c/` paths (e.g., prepending `C:/Program Files/Git/`). Saving to a WSL-native path and copying avoids this.

## Gotchas

- **`localhost` doesn't work from WSL2** - must use the Windows host IP (`172.18.64.1`)
- **Vite must bind to `0.0.0.0`** - default `localhost` isn't reachable from WSL
- **Git Bash path expansion** - `/status` gets expanded to `C:/Program Files/Git/status` when passed through Git Bash. Not an agent-browser issue; it's the shell expanding `/` paths
- **SSE `Connected` indicator** - only shows for `web` platform conversations; Telegram/Slack conversations show `Disconnected` (expected)
- **Daemon startup** - if `agent-browser open` fails with "Daemon failed to start", kill stale daemons: `wsl -d Ubuntu -- pkill -f daemon.js` and retry

---

### Packages Docs Web Src Content Docs Deployment E2e Testing (`packages/docs-web/src/content/docs/deployment/e2e-testing.md`)

---
title: E2E Testing
description: Set up agent-browser for end-to-end browser testing in Archon workflows.
category: deployment
area: infra
audience: [developer, operator]
status: current
sidebar:
  order: 5
---

Archon uses [agent-browser](https://github.com/vercel-labs/agent-browser) (by Vercel Labs) for end-to-end browser testing in workflows. It is an **optional** external dependency — core Archon functionality works without it.

## Installation

```bash
# Install globally
npm install agent-browser

# Download browser engine (Chrome for Testing)
agent-browser install
```

## Verify Installation

```bash
agent-browser --version
# Expected: prints version number (e.g., 0.x.x)

# Quick smoke test — opens a page and closes
agent-browser open https://example.com
agent-browser close
```

## Where It's Used

No bundled workflow requires agent-browser. Your own workflows can call it from a `bash:` node or from an AI node's instructions, for example to reproduce a UI bug in a real browser or to check a change against a running dev server.

## Platform-Specific Notes

### Docker

agent-browser is **pre-installed** in the Archon Docker image. No action needed.

### macOS / Linux

Works natively after running the install commands above. If the daemon fails to start:

```bash
# Kill stale daemons and retry
pkill -f daemon.js
agent-browser open http://localhost:3090
```

### Windows

agent-browser has a [known bug](https://github.com/vercel-labs/agent-browser/issues/56) where the daemon fails to start due to Unix domain socket incompatibility on Windows.

**Workaround:** Run agent-browser inside WSL while dev servers run on Windows. See the [E2E Testing on WSL](/deployment/e2e-testing-wsl/) guide for detailed setup instructions.

## Running Without agent-browser

If agent-browser is not installed, the E2E workflow nodes will fail when the agent tries to invoke `agent-browser`. The AI agent is instructed (via prompt) to stop after 2 failed connection attempts and produce a code-review-only report — but this is a prompt-level instruction, not automated workflow logic. Results may vary depending on the AI model's adherence to the instruction.

You can safely run all non-E2E workflows without agent-browser installed.

---

### Packages Docs Web Src Content Docs Deployment Local (`packages/docs-web/src/content/docs/deployment/local.md`)

---
title: Local Development
description: Run the Archon server and Web UI on your own machine, with SQLite or PostgreSQL.
category: deployment
area: infra
audience: [operator]
status: current
sidebar:
  order: 1
---

The CLI needs no server. Run the server when you want the Web UI, chat adapters or webhooks. For a VPS with HTTPS, see [Cloud Deployment](/deployment/cloud/); for containers, see [Docker](/deployment/docker/).

## Start the server

With an installed binary:

```bash
archon serve                 # API and Web UI on http://localhost:3090
archon serve --port 4000     # another port
```

A binary install downloads the server and Web UI on the first run. `archon serve --download-only` fetches them without starting.

From a source checkout, `bun run dev` starts the API on port 3090 and the Vite dev server for the Web UI on port 5173; see the [new developer guide](/contributing/new-developer-guide/). A run inside a worktree gets an automatic port from 3190 to 4089, based on the path. `PORT` overrides the port in any context.

Check it:

```bash
curl http://localhost:3090/health               # {"status":"ok"}
curl http://localhost:3090/health/db            # database connectivity
curl http://localhost:3090/health/concurrency   # concurrency status
```

## Choose a database

| Option | Setup | Best for |
|---|---|---|
| SQLite (default) | Nothing. Leave `DATABASE_URL` unset | One operator, CLI use, local work |
| PostgreSQL | Set `DATABASE_URL` | Shared or server installs |

SQLite stores data at `~/.archon/archon.db` and creates it on first run. For a local PostgreSQL container, run `docker compose --profile with-db up -d postgres` from a source checkout and set `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/remote_coding_agent`. The server creates and upgrades the schema on startup. See [Database](/reference/database/).

## Troubleshooting

If the port is taken, find the process with `lsof -i :3090` on macOS or Linux, or `netstat -ano | findstr :3090` on Windows, or start on another port with `--port`.

---

### Packages Docs Web Src Content Docs Deployment Reverse Proxy (`packages/docs-web/src/content/docs/deployment/reverse-proxy.md`)

---
title: Running Archon behind your own reverse proxy
description: The routing, authentication, webhook, and streaming contract for an Archon reverse proxy.
category: deployment
area: infra
audience: [operator]
status: current
sidebar:
  order: 4
---

You can use your own reverse proxy if it satisfies the contract below. Caddy is Archon's single maintained reference proxy: start with [Caddyfile.example](https://github.com/coleam00/Archon/blob/dev/Caddyfile.example) and the [Cloud deployment guide](/deployment/cloud/). Other proxies are community examples, not supported Compose profiles.

## Upstream and route contract

Forward to Archon's **configured app port** (`PORT`), preserving the request path. Docker defaults to **3000**; local development defaults to **3090** (worktrees may allocate another port). See [Start the server](/deployment/local/#start-the-server). Serve the UI and API at the same public origin.

Keep the upstream private so clients cannot bypass the proxy. For a proxy on the same host, use `HOST=127.0.0.1`. A container proxy needs a reachable container interface, but the app's host port must not be exposed publicly. The base Compose file publishes the app port; follow the [GitHub App Docker setup](/adapters/github-app-setup/#canonical-docker-setup) to replace that mapping with a loopback-only mapping.

| Route group | Proxy behavior |
| --- | --- |
| `/internal/*` | **Never forward**, for any HTTP method. Return a proxy-generated 404 or 403. |
| `GET /api/health` | Forward without interactive authentication for load-balancer and uptime checks. This is the Docker/production health endpoint. |
| `/webhooks/*` | Forward without interactive authentication. Preserve the raw body and the provider's signature, secret, event, and delivery headers. |
| `/api/stream/*` | Forward with response buffering off and timeouts that allow long-lived SSE connections. Apply the same access boundary as other protected API routes. |
| `/api/auth/*` | Forward login/session requests and cookies to Archon when using web auth; do not put them behind a proxy login that prevents users reaching Archon's login. |
| Other `/api/*`, UI pages, and assets | Forward normally, with the authentication posture described below. |

`/health` is also registered by the server and used as a local-development convenience endpoint. Use **`/api/health`** for production checks, matching Docker and the Caddy reference.

### Internal credentials: deny before forwarding

In GitHub App mode, `POST /internal/git-credential` hands out live installation tokens. It is registered only when the GitHub adapter is in App mode. The server refuses a non-loopback bind in App mode unless `ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1` explicitly acknowledges it. **That flag does not protect the endpoint.** Set it only when the topology requires a non-loopback upstream, the upstream is private, and the proxy already denies `/internal/*`.

The Caddy reference denies `/internal/*` with a proxy-generated 404 before its catch-all forwards other paths. **Keep that denial in your deployed configuration.** Follow [Internal endpoint security](/adapters/github-app-setup/#internal-endpoint-security--required) for the Docker port restrictions and external **POST** probe, too. A GET returning 404 does not prove that the credential endpoint is blocked.

### Webhooks: preserve the signed payload

Archon reads webhook bodies as raw text before verification. Do not parse and reserialize JSON, rewrite the body, or strip verification headers. Proxy login pages and redirects must not intercept webhook deliveries; the configured adapter or webhook source verifies them. Preserve the original `/webhooks/...` path.

### Live streams: flush each event

Both the dashboard stream (`/api/stream/__dashboard__`) and conversation streams (`/api/stream/:conversationId`) send an initial heartbeat and another every **30 seconds**. Disable response buffering and caching for these routes. Set any idle/read timeout in the proxy chain above 30 seconds with margin, for example 60 seconds or longer, and avoid a fixed response-duration limit that cuts off a healthy stream.

The Caddy reference uses `flush_interval -1` for `/api/stream/*`. Verify events arrive as they are emitted, rather than appearing in a batch when the connection closes.

## Authentication and headers

With [Web UI login](/reference/configuration/#web-ui-login-better-auth-optional) enabled (`DATABASE_URL` and `BETTER_AUTH_SECRET`), Better Auth handles sessions and Archon's server-side gate protects `/api/*`. The gate is on by default; `ARCHON_WEB_AUTH_REQUIRED=false` disables it and leaves login as a UI feature only. `/api/auth/*` and `/api/health*` are public exceptions. Static UI files can be served without proxy authentication; the API gate is the access boundary.

The proxy does not need its own login when that gate is enabled, but **it must strip client-supplied `X-Archon-User`** (or the name configured by `ARCHON_WEB_AUTH_HEADER`). Archon accepts this trusted identity header as a fallback even with Better Auth enabled; allowing clients to supply it bypasses the API gate. If you deliberately use proxy authentication and identity attribution, strip incoming values and set the header only from the authenticated identity. Keep the upstream reachable solely through that trusted boundary.

Without web auth, or with its API gate disabled, the proxy must provide the access boundary for a network-accessible UI and API. Use the authentication options in the [Docker guide](/deployment/docker/#protect-the-web-ui), or your proxy's equivalent, while keeping health checks and webhooks reachable. A reverse proxy alone does not add authentication.

Archon's application code does not read `X-Forwarded-*` headers as an access or identity contract. For Better Auth behind a fixed-origin proxy, set **`BETTER_AUTH_URL=https://archon.example.com`** to the public origin instead of relying on the upstream HTTP URL. Preserve session cookies. `BETTER_AUTH_TRUSTED_ORIGINS` is for additional allowed origins, not a replacement for the public URL. See the [configuration reference](/reference/configuration/#web-ui-login-better-auth-optional) for signup policy and auth settings.

## Community example: Traefik labels

This **community example is not a supported profile**. It assumes Traefik v3 with the Docker provider, a `websecure` HTTPS entry point and TLS certificates already configured, and a shared Docker network named `archon-network`. It uses **Archon's web auth with the API gate enabled**, `BETTER_AUTH_URL` set to the public origin, and the default identity header. Add these labels to your app service; adjust the host, network, and port to your deployment.

```yaml
labels:
  - "traefik.enable=true"
  - "traefik.docker.network=archon-network"
  - "traefik.http.routers.archon.rule=Host(`archon.example.com`) && !PathPrefix(`/internal`)"
  - "traefik.http.routers.archon.entrypoints=websecure"
  - "traefik.http.routers.archon.tls=true"
  - "traefik.http.routers.archon.service=archon"
  - "traefik.http.routers.archon.middlewares=archon-strip-identity"
  - "traefik.http.middlewares.archon-strip-identity.headers.customrequestheaders.X-Archon-User="
  - "traefik.http.services.archon.loadbalancer.server.port=3000"
  - "traefik.http.services.archon.loadbalancer.healthcheck.path=/api/health"
  - "traefik.http.services.archon.loadbalancer.responseforwarding.flushinterval=-1ms"
```

The [router rule](https://doc.traefik.io/traefik/reference/routing-configuration/http/routing/rules-and-priority/) excludes the entire `/internal` prefix. Ensure no other router or fallback forwards it to Archon. The [headers middleware](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/headers/) removes the identity header, and the [service flush setting](https://doc.traefik.io/traefik/reference/routing-configuration/http/load-balancing/service/) flushes responses immediately. Do not add a buffering middleware to the stream routes. Keep the HTTPS entry point's [`transport.respondingTimeouts.writeTimeout`](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/) at `0s` (no response-duration limit) for long-lived streams, and check any outer load balancer's idle timeout as well.

Keep the app port private as described above. For GitHub App mode, complete the internal-endpoint setup and POST probe before exposing the deployment. Verify `/api/health`, a real webhook delivery, an unauthenticated API request (401 with the gate enabled, including when a forged identity header is sent), and a live stream through the public proxy.

---

### Packages Docs Web Src Content Docs Deployment Windows (`packages/docs-web/src/content/docs/deployment/windows.md`)

---
title: Windows Setup
description: Run Archon on Windows natively with Bun or with full WSL2 compatibility.
category: deployment
area: infra
audience: [operator]
status: current
sidebar:
  order: 4
---

Archon runs on Windows in two ways:

- **Native Windows with Bun**: Works for basic usage (server, Web UI, simple workflows). No WSL2 required. Install [Bun for Windows](https://bun.sh), clone the repo, and run `bun install && bun run dev`.
- **WSL2 (recommended)**: Required for full compatibility, especially git worktree isolation, shell-based workflow steps, and CLI features that depend on Unix tooling.

The rest of this guide covers the WSL2 setup for full compatibility.

## Why WSL2?

The Archon CLI relies on Unix-specific features and tools:
- Git worktree operations with symlinks
- Shell scripting for AI agent execution
- File system operations that differ between Windows and Unix

WSL2 provides a full Linux environment that runs seamlessly on Windows.

## Quick WSL2 Setup

1. **Install WSL2** (requires Windows 10 version 2004+ or Windows 11):
   ```powershell
   wsl --install
   ```
   This installs Ubuntu by default. Restart your computer when prompted.

2. **Set up Ubuntu**:
   Open "Ubuntu" from the Start menu and create a username/password.

3. **Install Bun in WSL2**:
   ```bash
   curl -fsSL https://bun.sh/install | bash
   source ~/.bashrc
   ```

4. **Clone and install Archon**:
   ```bash
   git clone https://github.com/coleam00/Archon
   cd Archon
   bun install
   ```

5. **Make CLI globally available**:
   ```bash
   cd packages/cli
   bun link
   ```

6. **Verify installation**:
   ```bash
   archon version
   ```

## Working with Windows Files

WSL2 can access your Windows files at `/mnt/c/` (for C: drive):
```bash
archon workflow run investigate --cwd /mnt/c/Users/YourName/Projects/my-repo "Why does the build fail on Windows?"
```

For best performance, keep projects inside the WSL2 file system (`~/projects/`) rather than `/mnt/c/`.

## Stale Processes (Native Windows Only)

:::note
This section applies to native Windows (`bun run dev` in PowerShell or CMD). If you're using WSL2, use `pkill -f bun` instead.
:::

**Symptom:** The Web UI shows a spinning indicator with no response after starting `bun run dev`, or you see `EADDRINUSE` errors on startup.

**Cause:** A previous `bun` or `node` process is still holding the port, typically because the terminal was closed without stopping the server.

**Diagnose:**

```powershell
netstat -ano | findstr :3090
```

Note the PID in the last column, then confirm which process it is:

```powershell
tasklist | findstr 12345
```

(Replace `12345` with the actual PID from `netstat`.)

**Fix — kill by PID** (preferred):

```powershell
taskkill /F /PID 12345
```

If there are multiple stale processes:

```powershell
taskkill /F /IM bun.exe
taskkill /F /IM node.exe
```

:::caution
Do not kill `claude.exe` processes — those are active Claude Code sessions.
:::

See also: [Port Conflicts](/reference/troubleshooting/#port-conflicts) in the troubleshooting guide.

## Sleep During Workflow Runs (Native Windows Only)

While a workflow run is active on native Windows, Archon holds the system awake (via `SetThreadExecutionState`) so Modern Standby cannot freeze the executor mid-run — the display can still turn off, only system sleep is inhibited. This is automatic and best-effort: it releases as soon as the last active run finishes, requires no configuration, and if the OS call is unavailable Archon simply runs without it.

## Tips

- **VS Code Integration**: Install the "Remote - WSL" extension to edit WSL2 files from VS Code
- **Terminal**: Windows Terminal provides excellent WSL2 support
- **Git**: Use Git inside WSL2 for consistent behavior with Archon

---

### Packages Docs Web Src Content Docs Getting Started Ai Assistants (`packages/docs-web/src/content/docs/getting-started/ai-assistants.md`)

---
title: AI Assistants
description: Install, authenticate and configure Claude Code, Codex, Pi, OpenCode and GitHub Copilot for Archon.
category: getting-started
area: clients
audience: [user]
status: current
sidebar:
  order: 4
---

Archon needs at least one AI assistant. You can configure several and mix them in one workflow. This page covers what each provider needs installed, how it authenticates, and its `assistants.<provider>` keys in `.archon/config.yaml`.

Which workflow features each provider supports is in the generated [Provider Capability Matrix](/reference/provider-capabilities/). Every config key, with the other settings around it, is in [Configuration](/reference/configuration/).

## Install a provider

Release binaries (quick install, Homebrew) ship without Claude, Codex and Pi. Add the ones you use:

```bash
archon provider add claude codex pi   # any subset
```

`archon setup` asks which to add and then sets up their authentication. An added provider must match the CLI version, so add it again after you upgrade Archon. Adding a provider connects Archon to the agent; it does not install the agent's own CLI. Your native configuration and login (`~/.claude`, `~/.codex`, Pi's `auth.json`) keep working. Source checkouts and Docker images run Claude, Codex and Pi from source with no add step. See [Installation → Providers](/getting-started/installation/#providers).

## Deprecated providers

OpenCode and GitHub Copilot are deprecated and will be removed in the next minor release, 0.13.0. No Archon maintainer owns them. Both keep working until that release; [#4641](https://github.com/coleam00/Archon/issues/4641) tracks the removal. To keep using one after that, build and own it as a provider plugin: see [Publish a plugin](/guides/publishing-plugins/#provider-plugins) and [Adding a Community Provider](/contributing/adding-a-community-provider/).

In chat, Claude, Codex and Pi launch and manage runs through native `invoke_workflow`, `register_project` and `manage_run` tools. Copilot and OpenCode have no native tools, so after you select a project with `/setproject <name>` they run `archon workflow run <workflow> "<message>" --detach` in its directory and return the run ID. Those runs are headless: they use CLI identity and send no progress or results back to the chat.

Archon shows the deprecation notice only to someone who uses one of them, at most once per command or process. `archon doctor` reports it when one is your default assistant, `archon ai default` prints it when you select one, and the first run in a process that uses one prints it on stderr. The server writes it to its log.

## Structured output guarantees

When a node sets `output_format`, the parsed output is validated against the schema before downstream nodes see it. A node that produces no schema-valid output fails. How hard the provider works to produce valid output depends on its tier, which `GET /api/providers` reports as `capabilities.structuredOutput`:

| Provider | Tier | How it works | On a validation miss |
|---|---|---|---|
| Claude, Codex, OpenCode | enforced | The SDK constrains decoding to the schema. | The node fails. A refusal or `max_tokens` truncation can still bypass the constraint, which is why the output is validated anyway. |
| Pi | best-effort | The schema, which must have `type: object` at its root, becomes the parameters of a `structured_output` tool the prompt asks the model to call. Pi validates each call and returns errors to the model in the same session; after 3 invalid calls the turn ends without output. A model that answers in text instead has JSON extracted from the reply, never from its thinking. The tool call cannot be forced, so the tier stays best-effort. | The executor asks again with the schema errors, up to 3 times, then fails the node. |
| Copilot | best-effort | The schema is appended to the prompt. JSON is extracted from the reply and repaired (trailing commas, single quotes, truncated tails). | The executor asks again with the schema errors, up to 3 times, then fails the node. |

See [Workflow language → `output_format`](/reference/workflow-language/#output_format-for-structured-json) for field access.

## Claude Code

Use Claude Code with a Claude Pro or Max subscription, an OAuth token, or an Anthropic API key.

### Install Claude Code

Archon does not bundle Claude Code. Install it with Anthropic's native installer:

```bash
# macOS / Linux / WSL
curl -fsSL https://claude.ai/install.sh | bash

# Windows (PowerShell)
irm https://claude.ai/install.ps1 | iex
```

Homebrew (`brew install --cask claude-code`), npm (`npm install @anthropic-ai/claude-code`) and winget (`winget install Anthropic.ClaudeCode`) work too. See [Anthropic's setup guide](https://code.claude.com/docs/en/setup).

### Binary path configuration (compiled binaries only)

A compiled Archon finds Claude Code in this order:

1. `CLAUDE_BIN_PATH` in the environment.
2. `assistants.claude.claudeBinaryPath` in `~/.archon/config.yaml` or a repository `.archon/config.yaml`.
3. The native installer's path: `~/.local/bin/claude`, or `%USERPROFILE%\.local\bin\claude.exe` on Windows.

If none resolves, the first Claude query fails with install instructions. Run `which claude` (or `where claude` on Windows) to find the path. Archon accepts the native binary, an npm `cli.js` (`$(npm root -g)/@anthropic-ai/claude-code/cli.js`), or an npm platform-package directory such as `@anthropic-ai/claude-code-win32-x64`, which it expands to the `claude` executable inside. The Docker image sets `CLAUDE_BIN_PATH` when the container starts.

A source install (`bun run`) uses the binary bundled with the Claude Agent SDK. Set `CLAUDE_BIN_PATH` or `claudeBinaryPath` only to override it, most often on glibc Linux, where the SDK can pick the musl build first and fail to start. An explicit path that is invalid fails in both modes instead of falling back.

Archon asks Claude Code for session-state events and finishes a node when the session goes idle after its final result. The CLI the bundled SDK ships emits them. A configured binary that does not still works if its process exits after the turn. If the process hangs instead, the node fails at the idle timeout, saying the provider never signalled that its turn settled.

### Authenticate Claude Code

Claude Code authenticates from its environment, or from its own login when the environment has no credential. Pick one:

- **Subscription login.** Run `claude /login` once. This is the default for a solo install.
- **OAuth token.** Run `claude setup-token` and set `CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-...` in `~/.archon/.env`.
- **API key.** Create a key at [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) and set `CLAUDE_API_KEY=sk-ant-...`. Archon hands it to Claude Code as `ANTHROPIC_API_KEY` only when neither `CLAUDE_CODE_OAUTH_TOKEN` nor `ANTHROPIC_API_KEY` is set, so an OAuth token wins over `CLAUDE_API_KEY`.

`CLAUDE_USE_GLOBAL_AUTH=true` tells the server's startup check that you rely on `claude /login`. Claude Code never reads it. The CLI sets it when no Claude token or key is set, and so does the server unless `TOKEN_ENCRYPTION_KEY` is set. With `store: database`, a credential a user connects in Archon takes precedence over these install-wide settings for that user's runs (see [Per-user credentials](#per-user-credentials-and-ai-settings)).

### Claude config keys

```yaml
assistants:
  claude:
    model: sonnet          # sonnet, opus, haiku, a claude-* id, or inherit
    settingSources:        # default: [project, user]
      - project            # <cwd>/.claude/
      - user               # ~/.claude/
    claudeBinaryPath: /absolute/path/to/claude   # optional, see above
```

`settingSources` sets where the Claude Agent SDK discovers `CLAUDE.md`, skills, commands, agents and hooks. Use `[project]` to leave out your personal `~/.claude/`, or `[]` for a node with no setting sources. Discovery only makes a skill eligible: a workflow node activates exactly the skills its `skills:` list names. See [Claude settingSources](/reference/configuration/#claude-settingsources).

## Codex

Use Codex with a ChatGPT subscription login or an OpenAI API key.

### Install the Codex CLI

Archon does not bundle the Codex CLI in a compiled install:

```bash
npm install @openai/codex   # any platform; on Windows OpenAI recommends WSL2
brew install codex             # macOS
```

Native binaries are on the [Codex releases page](https://github.com/openai/codex/releases). See [OpenAI's Codex CLI docs](https://developers.openai.com/codex/cli).

A compiled Archon finds Codex in this order:

1. `CODEX_BIN_PATH` in the environment.
2. `assistants.codex.codexBinaryPath` in config.
3. A binary you place at `~/.archon/vendor/codex/codex` (`codex.exe` on Windows).
4. Common npm global locations: `~/.npm-global/bin/codex`, `/opt/homebrew/bin/codex` and `/usr/local/bin/codex`, or `%APPDATA%\npm\codex.cmd` and `%USERPROFILE%\.npm-global\codex.cmd` on Windows. For any other npm prefix, set the path explicitly.

A source install runs the binary from the `@openai/codex` package Archon depends on, unless you set a path.

### Authenticate Codex

By default Codex uses the login in your Codex home (`~/.codex`, or `CODEX_HOME`), exactly as `codex login` left it. Outside Docker, Archon never writes to that directory.

- **Login on the host.** Run `codex login` on the machine that runs Archon. This works for a server too.
- **API key.** Set `CODEX_API_KEY` in Archon's environment, for example `~/.archon/.env`. Archon passes it to the Codex process in memory. Codex does not read `OPENAI_API_KEY`.
- **Copied login tokens, Docker only.** For the Docker image, run `codex login` on another machine and copy the four values from `~/.codex/auth.json` into `.env`:

  ```ini
  ARCHON_CODEX_ID_TOKEN=eyJhbGc...
  ARCHON_CODEX_ACCESS_TOKEN=eyJhbGc...
  ARCHON_CODEX_REFRESH_TOKEN=rt_...
  ARCHON_CODEX_ACCOUNT_ID=6a6a7ba6-...
  ```

  At each start, the container's `setup-auth` step writes them to `~/.codex/auth.json`. Outside Docker nothing reads them at run time: the server's startup check passes, but every Codex run fails with `auth`. Use a host login or `CODEX_API_KEY` there instead. The `ARCHON_` prefix keeps the values from overriding a Codex login, because Codex reads `CODEX_ACCESS_TOKEN` itself. If your `.env` still has the old `CODEX_*` names, run `archon setup` to rename them or rename all four yourself. Until the next release Archon still reads the old names, with a deprecation warning, but only when `CODEX_ID_TOKEN` is set and no `ARCHON_CODEX_*` variable is.
- **Connected credential.** With `store: database`, an `openai` credential connected in Archon (`archon ai key set openai` or `archon ai login openai`) takes precedence over the native login, in workflows and in chat. An API key reaches Codex as `CODEX_API_KEY`; a ChatGPT subscription logs the Codex process in, in memory. A subscription token that expires during a run fails the turn with `auth`, and resuming the run picks up the refreshed token.

### How Archon runs Codex

Each Codex turn runs on its own `codex app-server` process. Your Codex config, hooks and `AGENTS.md` guidance load as they do for `codex` itself. In direct chat your plugins and MCP servers load too. A workflow node loads only the MCP servers its `mcp:` file declares and the plugins its `plugins:` list names, with ChatGPT apps off (see [Plugins](/reference/workflow-language/#plugins)). Failed turns carry a typed failure class from Codex's own error code; the classes are listed under [Error classification](/reference/workflow-language/#error-classification).

Codex chats get Archon's `invoke_workflow` and `register_project` tools, plus `manage_run` in project-scoped chats, through Codex's experimental dynamic-tool API. The tool catalog is fixed when a thread starts. If a chat predates Archon's record of that catalog, or the tools have changed since, Archon asks you to start a new chat before it runs a model turn.

In workflow nodes Archon turns Codex's automatic skill catalog off, so a prompt invokes an installed skill from `.agents/skills/` explicitly with `$skill-name`. Direct chat keeps native discovery. See [Per-Node Skills](/guides/skills/#codex-compatibility).

### Codex config keys

```yaml
assistants:
  codex:
    model: gpt-5.6-sol
    modelReasoningEffort: medium  # minimal, low, medium, high, xhigh, max, ultra or persistent
    webSearchMode: live           # disabled, cached or live
    additionalDirectories:
      - /absolute/path/to/other/repo
    codexBinaryPath: /absolute/path/to/codex   # optional, see above
    toolFreeStructuredOutputModels:            # routes that reject tools and a strict schema together
      - glm-5.3
```

A workflow's `effort:` overrides `modelReasoningEffort`.

Some model routes reject a request that carries both tools and a strict output schema. On a model listed in `toolFreeStructuredOutputModels`, a node with `output_format` runs its tool turn without the schema, then one final turn on the same thread with the schema and no tools. That costs one extra model request per node. The list matches only a model Archon resolves (`assistants.codex.model`, a tier, an alias or the node's `model:`), not one set in Codex's own `config.toml`. The list is read live, so a resumed run uses the current list.

The final turn drops the tools a Codex model catalog entry adds (`tool_mode`, `experimental_supported_tools`, `apply_patch_tool_type`): its process loads a catalog whose only entry is that model with no tools. The entry keeps the context window Codex reported for the tool turn, so Codex compacts the conversation before the final turn at about the size it would for the tool turn. Codex multi-agent is off on these threads, with one limit: Codex keeps a catalog entry's `multi_agent_version: v2` with the thread, so that entry's collaboration tools (`spawn_agent`, `send_message` and the rest) stay on both turns, including the final one. If such a model's route rejects them, drop `multi_agent_version` from that model's entry in the catalog Codex loads (`model_catalog_json` in Codex's `config.toml`; `codex debug models` prints the current one).

## Pi

Pi (`@earendil-works/pi-coding-agent`) is a coding-agent harness the Archon core team maintains alongside Claude and Codex. One `provider: pi` reaches Anthropic, OpenAI, Google (Gemini and Vertex), Groq, Mistral, Cerebras, xAI, OpenRouter, Hugging Face, and local models such as LM Studio, ollama and llamacpp.

### Install Pi

Add Pi with `archon provider add pi`. The provider embeds the Pi SDK, so you need the Pi CLI only to log in with `pi /login` or to refresh the model catalog. Source installs include the provider already. Install the CLI with `npm install @earendil-works/pi-coding-agent`.

`archon setup` can configure Pi: pick **Pi**, choose a backend and paste its key. Setup writes the key to `~/.archon/.env` and the model to `~/.archon/config.yaml`.

### Authenticate Pi

Archon reads your Pi credentials from `~/.pi/agent/auth.json`, written by `pi /login`, and from environment variables. Environment variables win per request, so codebase-scoped overrides work. A per-user credential that Archon delivers wins over all of them.

`pi /login` covers these OAuth subscriptions: OpenAI ChatGPT Plus or Pro, GitHub Copilot, Google Gemini CLI and Google Antigravity.

Key-based backends read these variables:

| Pi provider id | Env var |
|---|---|
| `anthropic` | `ANTHROPIC_API_KEY` |
| `openai` | `OPENAI_API_KEY` |
| `google` | `GEMINI_API_KEY` |
| `groq` | `GROQ_API_KEY` |
| `mistral` | `MISTRAL_API_KEY` |
| `cerebras` | `CEREBRAS_API_KEY` |
| `xai` | `XAI_API_KEY` |
| `openrouter` | `OPENROUTER_API_KEY` |
| `huggingface` | `HF_TOKEN` |

The full map is generated from the installed Pi SDK (`bun run generate:pi-vendor-map`) and covers every other key-based backend, such as DeepSeek, Together, Fireworks and Azure OpenAI. Amazon Bedrock and Google Vertex use ambient cloud credentials (the AWS chain or gcloud ADC).

For Anthropic models, use `provider: claude` with a Claude subscription, or give Pi an `ANTHROPIC_API_KEY`. Claude Pro or Max OAuth through Pi, including a native `pi /login` session, is not supported: Anthropic can reject Pi's default system prompt on subscription tokens. Archon never delivers its stored Claude subscription to Pi.

Local and custom endpoints need no Archon configuration. Register them in `~/.pi/agent/models.json` as Pi documents, then reference them as `<pi-provider-id>/<model-id>`. With no credential found, Archon continues and Pi connects to the endpoint.

Pi caches the result of an API key stored as `!command` in `auth.json` for the life of the process, keyed by the command text. All Pi nodes in one Archon server share that cache. If the command returns a short-lived token, the server keeps sending it after it expires. Restart the server to run the command again, or use a longer-lived credential. Keys and headers from `models.json` commands are not cached this way.

### Pi config keys

```yaml
assistants:
  pi:
    model: anthropic/claude-haiku-4-5   # <pi-provider-id>/<model-id>
    enableExtensions: true              # default true; false skips extension discovery
    interactive: true                   # default true; false loads extensions with no UI bridge
    extensionFlags:                     # per-extension flags, like Pi's --flag switches
      plan: false
    nodes:                              # per-node extension posture, keyed by node id
      plan: { interactive: true, extensionFlags: { plan: true } }
    env:                                # set in process.env for extensions; shell env wins
      PLANNOTATOR_REMOTE: "1"
    maxConcurrent: 2                    # cap on concurrent Pi prompts in this process
```

`env` and `maxConcurrent` apply to the whole process, so a per-run `--config` layer refuses them. Use codebase env vars, not `assistants.pi.env`, for secrets that vary per project.

Model references can nest slashes, as in `openrouter/qwen/qwen3-coder`. Archon reads Pi's model catalog from `~/.pi/agent/models-store.json`, or `$PI_CODING_AGENT_DIR/models-store.json` when that variable is set. If a model is newer than your catalog, run `pi update --models` and retry. Archon never refreshes the catalog itself and never substitutes another model when a lookup fails.

Pi names Azure models `azure/<model-id>`, including Foundry Chat Completions deployments. If you are upgrading from Pi 1.0.2 or earlier, change `azure-openai-responses` to `azure` in your Archon model references and in Pi's own `auth.json`, `models.json` and `settings.json`. Archon does not rewrite Pi's files. See the [Pi 1.0.3 release notes](https://github.com/earendil-works/pi/releases/tag/v1.0.3).

### Pi settings and context files

Every session starts from `~/.pi/agent/settings.json`, with `<repo>/.pi/settings.json` on top. Edit those files for retries, transport, compaction, thinking budgets and the default model; you do not restate them in Archon's config. Archon never writes to them, and changes made during a session are discarded when it ends. Missing files fall back to Pi's defaults, and a file that fails to parse is logged as `pi.settings_load_error` without stopping the session.

Pi keeps its native context-file discovery: `~/.pi/agent/AGENTS.md`, plus `AGENTS.md` or `CLAUDE.md` in the project and its parent directories. `AGENTS.override.md` replaces the ordinary file in the same directory.

### Extensions

Pi extensions are packages installed with `pi install npm:<package>`, or your own local files, that hook into the agent's lifecycle. They can add tools, intercept tool calls or block for human review. Archon loads them by default.

:::warning[Repo-controlled `.pi/` directories]
Extension discovery includes `<cwd>/.pi/extensions/*.ts` and `<cwd>/.pi/settings.json` in the run's working directory. When you run a workflow against a repository you did not write, anything in its `.pi/` runs with the Archon server's OS permissions. Audit that directory first, or set `enableExtensions: false` globally or on the node.
:::

Set an extension's posture (`enableExtensions`, `interactive`, `extensionFlags`) on the node that needs it, not install-wide. A planning flag set under `assistants.pi` also reaches the implement node, which then starts in planning mode, has its edits blocked and waits for a review nobody asked for. Three layers resolve per node, later ones winning:

1. `assistants.pi.*`, the install-wide default.
2. `assistants.pi.nodes.<nodeId>`, for a workflow you cannot edit. It lives in one machine's config and is keyed by node id, so renaming the node orphans it.
3. A `pi:` block on the node in the workflow YAML. It travels with the workflow and is the one to use.

Each layer's `extensionFlags` merge over the layer below key by key, so a node can turn off an inherited flag with `plan: false`. The `pi:` block works on `prompt`, `command` and `loop` nodes. On a `loop_group` it is ignored with a warning; put it on the body nodes.

For example, with the [plannotator](https://github.com/dmcglinn/plannotator) review extension installed (`pi install npm:@plannotator/pi-extension`), turn planning mode on for the planner only:

```yaml
name: plan-then-build
provider: pi
nodes:
  - id: plan
    prompt: "Draft a plan for: $ARGUMENTS"
    pi:
      interactive: true          # plannotator opens its review server
      extensionFlags: { plan: true }

  - id: implement
    depends_on: [plan]
    loop:
      prompt: "Implement the approved plan. Print DONE when finished."
      until: "DONE"
      max_iterations: 10
    pi:
      interactive: false
      extensionFlags: { plan: false }
```

The `plan` node prints a review URL and blocks until you approve or deny in the browser. Archon flushes that URL to the CLI or chat straight away.

Archon adds neither web search nor MCP to Pi; Pi rejects MCP by design. Both come from extensions that call `pi.registerTool`, such as a search API wrapper or an MCP bridge, and the tools reach workflow nodes with no Archon configuration. A tool's `execute` must return `{ content, details }`; a bare string reaches the model as an empty result. Search the [package ecosystem](https://shittycodingagent.ai/packages) before writing one.

An exception that escapes a detached extension callback, such as a timer or a watcher, fails the Pi node that loaded the extension instead of the Archon process. Archon routes it by the extension path in the stack. If several running Pi nodes loaded that extension, each fails with an error that says so. An escaped error that names no loaded extension still ends the process.

See the [Pi documentation](https://pi.dev) for extensions, the model registry and settings.

## OpenCode

:::caution[Deprecated]
OpenCode will be removed in the next minor release. It keeps working until then. See [Deprecated providers](#deprecated-providers).
:::

Archon runs OpenCode through `@opencode-ai/sdk` as an embedded server that it starts, protects with a random password and stops after its last request. Connecting to an external OpenCode server is not supported.

`@archon/providers` brings the SDK, but not the `opencode` executable. Install the [OpenCode CLI](https://opencode.ai/docs/#install) on the Archon process's `PATH`. In Docker, install it on `appuser`'s `PATH` in a derived image (see [Customizing the image](/deployment/docker/#customizing-the-image)). `archon doctor` checks both without starting the server.

OpenCode handles its own authentication; Archon passes no keys. Run `opencode` and use `/connect`, or store credentials in `~/.config/opencode/opencode.json` with `{env:VAR}` or `{file:PATH}` substitution. Connected credentials persist in `~/.local/share/opencode/auth.json`.

All nodes share one embedded server, so codebase env vars do not reach OpenCode, and Archon warns when a node would have received any.

```yaml
assistants:
  opencode:
    model: anthropic/claude-3-5-sonnet   # required format: <provider>/<model>; list them with `opencode models`
```

## GitHub Copilot

:::caution[Deprecated]
GitHub Copilot will be removed in the next minor release. It keeps working until then. See [Deprecated providers](#deprecated-providers).
:::

Archon drives the Copilot CLI through `@github/copilot-sdk`. You need an active Copilot subscription.

A source install gets the CLI with `bun install`. For a compiled Archon, install it with `npm install @github/copilot`. Archon finds it through `COPILOT_BIN_PATH`, then `assistants.copilot.copilotCliPath`, then `~/.archon/vendor/copilot/copilot` (`copilot.exe` on Windows), then common install locations.

Copilot picks its credential in this order:

1. `COPILOT_GITHUB_TOKEN` in the environment.
2. With `useLoggedInUser: false`, a generic `GH_TOKEN` or `GITHUB_TOKEN`.
3. The login from `copilot login`, the default.

Archon ignores `GH_TOKEN` and `GITHUB_TOKEN` unless you opt in, because a classic GitHub token has no Copilot entitlement and fails with a misleading SDK error.

```yaml
assistants:
  copilot:
    model: gpt-5-mini              # for example gpt-5, gpt-5-mini, claude-sonnet-4.5; unset passes auto
    modelReasoningEffort: medium   # any Archon effort, clamped to the SDK's low..max
    copilotCliPath: /absolute/path/to/copilot
    configDir: /absolute/path/to/copilot-config   # default: the SDK's own, usually ~/.copilot
    enableConfigDiscovery: false   # default false; true loads the repo's .copilot/ MCP servers and skills
    useLoggedInUser: true          # default true; false allows GH_TOKEN or GITHUB_TOKEN
    logLevel: error                # none, error, warning, info, debug or all
```

Copilot has no session-fork API. When Archon asks for a fork on retry, it starts a fresh session and posts a warning.

## Per-user credentials and AI Settings

Everything above configures install-wide credentials that every run shares. On a shared Archon server, each user can instead connect their own API key or subscription, so their runs and chats use their account. A solo install does not need this.

The credential vault works on every install. Archon creates a local encryption key at `~/.archon/credential-key` on first use. To manage the key yourself, for example to share it across containers or keep it in a secrets manager, set `TOKEN_ENCRYPTION_KEY` (64 hex characters, `openssl rand -hex 32`) and the key file is not used. Changing the key, or deleting the key file, makes every stored credential unreadable and everyone must reconnect. `archon doctor` then lists each of your credentials as "cannot be used", with the `archon ai login <vendor>` or `archon ai key set <vendor>` command that reconnects it.

### Connect a credential

Credentials are stored per vendor (`anthropic`, `openai`, `github-copilot`, `openrouter` and others), and one credential serves every agent that uses that vendor. Every vendor accepts an API key. `anthropic`, `openai` and `github-copilot` also accept a subscription login. The legacy ids `claude`, `codex` and `copilot` are accepted and normalized.

An `anthropic` subscription runs only on Claude Code. Because one credential is stored per vendor, connecting an Anthropic API key replaces the subscription. To give Pi an Anthropic key while Claude Code uses the subscription, set `ANTHROPIC_API_KEY` in the install or codebase env.

In the console, open **Settings**. The **Agents** section has one card per agent with its credentials and a ready or needs-credential state. The Pi card lists its backends behind **Add backend…** and shows Bedrock and Vertex as status-only rows. OpenCode's backends are install-wide, not per user.

From the CLI ([`archon ai`](/reference/cli/#ai)):

```bash
echo "$MY_KEY" | archon ai key set openrouter   # API key for any vendor
archon ai login anthropic                        # subscription: anthropic, openai or github-copilot
archon ai list
```

### When credentials are checked

A run checks the credentials its AI nodes need before it creates isolation, and again before any node runs when execution starts or resumes. Vendors the run does not use are not checked, and a child workflow checks its own graph when it starts.

- With `store: database`, a connected credential that cannot be read, is rejected, or cannot be verified blocks the run. Archon never falls back to another account.
- With `store: files`, or with no connected credential, runs use the provider's native or environment account, and a missing login blocks launch. A host run checks that login before isolation; a container run checks it inside the container.
- Claude checks account information in the target directory with each node's setting sources, without a model request. An `apiKeyHelper` must exit successfully with a non-empty credential, unless an environment credential authenticates instead.
- Providers that cannot check without a model session, including Copilot, OpenCode and Claude cloud backends, report "not checked" and may proceed.

A key that resolves is only usable; only a model request proves the vendor accepts it.

### Model tiers, aliases and defaults

The `small`, `medium` and `large` tiers map to a provider and model. Only `claude` and `codex` have built-in tier defaults; every other provider must configure each tier it uses. Set tiers and `@alias` references in the console's **Model Tiers** and **Model Aliases** sections, with `archon ai tier set` and `archon ai alias set`, or in the `tiers:` and `aliases:` blocks described in [Configuration](/reference/configuration/). These need no encryption key.

When you are signed in to the web UI, those sections and **Defaults** have a **This install / Just me** toggle. Just-me settings are stored in Archon's database and win over the install config for runs and chats you start. The CLI equivalent is `--scope user`:

```bash
archon ai tier set large claude opus --scope user
archon ai default pi openrouter/minimax/minimax-m2 --scope user   # pin your chat provider and model
```

Chat picks its provider from your personal default, then the conversation's recorded assistant, then the install default. Its model is your personal pin when the pin's provider matches, then the configured `large` tier (yours, then repo, then global), then `assistants.<provider>.model` when no `large` tier is configured, then the built-in default. If chat asks for `large` and only another tier is configured, Archon uses the nearest one and says which. Workflow nodes ignore the chat pin; `model: large` always means the tier.

For a single run, `archon workflow run --model <name>=<spec>` or the console's **Start a new run** card rebinds named tiers or aliases without touching shared config. See [`workflow run`](/reference/cli/#workflow-run-name-message).

## How assistant selection works

A new conversation uses the repository's `assistant` in `.archon/config.yaml`, then `defaultAssistant` in `~/.archon/config.yaml`, then the `DEFAULT_AI_ASSISTANT` environment variable. Each must name a registered provider. Once a conversation starts, its assistant is fixed.

Workflows set `provider` and `model` at the workflow or node level. Tier and alias references resolve from your personal preferences, then repository and global config, then built-in defaults. A literal model id passes unchanged to the provider.

---

### Packages Docs Web Src Content Docs Getting Started Configuration (`packages/docs-web/src/content/docs/getting-started/configuration.md`)

---
title: Configuration
description: Configure Archon with API keys, assistants, and project settings.
category: getting-started
area: config
audience: [user, operator]
sidebar:
  order: 3
---

## Environment Variables

Set these in your shell, `~/.archon/.env` for user-wide settings, or `<cwd>/.archon/.env` for project overrides. The CLI strips keys named in a target repository's `<cwd>/.env`; use the Archon-owned files for credentials. See [environment file locations and load order](/reference/configuration/#env-file-locations), including the source-server exception.

| Variable | Required | Description |
|----------|----------|-------------|
| `CLAUDE_BIN_PATH` | No (binary builds autodetect `~/.local/bin/claude`) | Absolute path to the Claude Code binary, SDK `cli.js`, or the npm platform-package directory (e.g. `@anthropic-ai/claude-code-win32-x64`, auto-expanded to `claude`/`claude.exe`). Takes precedence over `assistants.claude.claudeBinaryPath` in both source and compiled installs. Without either pin, compiled builds autodetect the native-installer path; dev mode (`bun run`) lets the SDK resolve via `node_modules`. Invalid explicit pins fail in either mode. |
| `CLAUDE_USE_GLOBAL_AUTH` | No | Startup-check marker for a `claude /login` session; Claude Code never reads it. Set automatically when no other Claude token is set. A Docker container has no login session, so set a token or key there. |
| `CLAUDE_CODE_OAUTH_TOKEN` | No | OAuth token from `claude setup-token` |
| `CLAUDE_API_KEY` | No | Anthropic API key for pay-per-use |
| `CODEX_BIN_PATH` | No | Absolute path to the Codex CLI binary. Overrides auto-detection in compiled Archon builds. |
| `ARCHON_CODEX_ACCESS_TOKEN` | No | Docker only: one of four copied Codex login values the container writes to `~/.codex/auth.json`. Elsewhere use `codex login` or `CODEX_API_KEY` (see [AI Assistants](/getting-started/ai-assistants/)) |
| `DATABASE_URL` | No | PostgreSQL connection string (default: SQLite) |
| `GH_TOKEN` | No | GitHub personal access token — used to authenticate when cloning private repos from `github.com` or the `GITHUB_URL` host |
| `GITHUB_URL` | No | GitHub Enterprise Server base URL (e.g. `https://github.example.com`); unset means github.com. See [GitHub Enterprise Server](/adapters/github/#github-enterprise-server) |
| `GITLAB_TOKEN` | No | GitLab personal/project access token — used to authenticate when cloning private repos from `gitlab.com` or the `GITLAB_URL` host (also used by the GitLab adapter) |
| `GITEA_TOKEN` | No | Gitea/Forgejo access token — used to authenticate when cloning private repos from `gitea.com` or the `GITEA_URL` / `FORGEJO_URL` host (also used by the Gitea adapter) |
| `LOG_LEVEL` | No | `debug`, `info` (default for the server and `archon serve`), `warn` (default for other CLI commands), `error`. See [CLI logs](/reference/cli/#logs). |
| `PORT` | No | Server port (default: 3090, Docker: 3000) |
| `WSL_DISTRO_NAME` | No (WSL sets it automatically) | WSL distro name; Archon reads it to emit Windows-host-friendly `vscode://vscode-remote/wsl+<distro>/...` "Open in IDE" links. Override only to force a specific distro name into the URI. |

## Project Configuration

Create `.archon/config.yaml` in your repository:

```yaml
assistants:
  claude:
    model: sonnet  # or 'opus', 'haiku', 'inherit'
    settingSources:
      - project
  codex:
    model: gpt-5.6-sol
    modelReasoningEffort: medium

# docs:
#   path: packages/docs-web/src/content/docs  # Optional: default is docs/
```

See the [full configuration reference](/reference/configuration/) for all options.

---

### Packages Docs Web Src Content Docs Getting Started First Workflow (`packages/docs-web/src/content/docs/getting-started/first-workflow.md`)

---
title: Build your first workflow
description: Build an agent-first workflow that plans a change, waits for your approval, implements it and fixes failing tests, and check every route before it spends a token.
category: getting-started
audience: [user]
sidebar:
  order: 3
---

In this tutorial you build `build-change`, a workflow that takes a brief for a small change to your repository and:

1. has an agent plan the change and save the plan as an artifact;
2. pauses until you approve the plan, or cancels if you reject it;
3. has an agent implement the approved plan;
4. runs your test command and records whether it passed;
5. has a fresh agent fix the code if the tests failed.

Before you run it, you check it three ways that cost nothing: `archon validate workflows`, a dry run with stub outputs, and fixtures that `archon workflow test` replays on every change.

You need Archon installed and a provider added; see the [Quickstart](/getting-started/quick-start/). Work in a git repository that has a test command.

## Who does what

Archon's rule for a workflow is **YAML coordinates. Code computes. Agents judge.** `build-change` follows it:

- **Agents judge.** Planning a change, implementing it and working out why a test fails all need judgment, so each is an agent node. When a later node needs the agent's decision, the agent returns it as a small typed result through `output_format`, never as prose for code to parse.
- **Scripts give facts and act at boundaries.** Whether the tests pass is a fact, so a script runs the test command and reports the exit code and output. It does not interpret the failure; that is the fixing agent's job. Use a `bash:` or `script:` node only for work with a fixed rule like this one.
- **YAML coordinates.** The file holds the order, the approval gate and the `when:` conditions on typed fields. It decides nothing on its own.

Tests follow the same split. The fixtures you write later check outcomes at those seams: which nodes run, whether the run completes or is cancelled, and whether the test output reached the fixing agent. They never pin what an agent writes.

## Lay out the workflow folder

A workflow lives in its own folder under `.archon/workflows/<pack>/<workflow>/`, with the commands and fixtures it uses beside it. Create this layout in your repository:

```text
.archon/workflows/my-team/build-change/
├── build-change.yaml
├── commands/
│   └── fix-tests.md
└── fixtures/
```

`my-team` is the pack name; choose your own. A command or script that `build-change.yaml` names resolves only from its own `commands/` and `scripts/` folders, so two workflows can each have a `fix-tests.md` without colliding.

## Write a command

A command is a markdown file of instructions for an agent. Use one when the instructions are long or shared by several nodes. Create `commands/fix-tests.md`:

```markdown
---
description: Find why the tests fail after a change and fix the cause
---

# Fix failing tests

The change described in $ARTIFACTS_DIR/plan.md was implemented, and the
project's tests now fail. The test output is:

$INPUTS.test_output

Decide whether the failure comes from the change or from a test that the plan
says should change. Fix the cause, not the symptom: do not delete or skip a
test to make it pass. Run the tests again, then commit the fix.
```

`$ARTIFACTS_DIR` is a directory Archon creates for each run, outside your repository. `$INPUTS.test_output` is a value the workflow passes to this command; the next section shows where it comes from.

## Write the workflow

Create `build-change.yaml`:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Each node does one job:

- **`plan`** is an agent node. `$ARGUMENTS` is the brief you pass to the run. `output_format` makes the agent return JSON matching the schema, so the gate can show `$plan.output.summary`. The node fails if the agent returns anything else.
- **`approve-plan`** is the gate. A `wait:` node with `attention:` pauses the run until someone answers it, and `form: approval` means the answer is `approve` or `reject` plus optional text. A paused run holds no process; answer it minutes or days later. Pause only where a person should decide. A check that code or another agent can make belongs in a node.
- **`stop`** cancels the run when you reject. A rejection is an answer, not a cancellation, so the workflow decides what happens next.
- **`implement`** runs only after an approval. `$approve-plan.output.text` passes on any notes you gave with the answer.
- **`test`** is a script node. It runs the command from the `test_command` input, which defaults to `npm test`, and prints `passed` and the last 4,000 characters of output as JSON. `output_format` on a script certifies its stdout: output that does not match the schema fails the node.
- **`fix`** runs only when `test` reports a failure. It loads `commands/fix-tests.md`, and `with:` binds the test output to the command's `$INPUTS.test_output`. `context: fresh` starts a new agent session, so the agent works from the real test output instead of what the implementer believed it wrote.

Nodes without a dependency between them run at the same time. Here every node depends on the one before it, so they run in order.

## Validate it

```bash
archon validate workflows build-change
```

```text
  build-change                             ok

Results: 1 valid, 0 with errors
```

Validation loads the workflow the way a run would. It reports a misspelt key, a `when:` that references a field the producer never declares, a command file that does not exist, and a key that the node's type cannot use. For example, `timeout:` on a prompt node fails:

```text
ERROR [validation_error] DAG node validation failed: Node 'plan': 'timeout' is only supported on bash, loop, loop_group or script nodes, not on prompt nodes.
```

## Dry-run it with stubs

A dry run walks the graph with outputs you supply instead of calling a model or running scripts. It never creates a run, so it is free. Generate a stub file that has one entry per node that needs one:

```bash
archon workflow run build-change --dry-run --stubs-init stubs.yaml
```

Edit the values to describe the route you want to see. This one simulates failing tests:

```yaml
plan: { summary: "Add a --version flag that prints the version from package.json." }
implement: "Added the flag and a test."
test: { passed: false, output: "version.test.ts: expected 1.2.0, got undefined" }
fix: "Read the version from package.json at startup."
```

Run the simulation:

```bash
archon workflow run build-change --dry-run --stubs stubs.yaml "Add a --version flag"
```

```text
STUBBED   plan (prompt)
PAUSED    approve-plan (wait) — durable wait
Simulation outcome: paused
```

The simulation stops at the gate, as a real run would. A stub for the wait node stands in for your answer. Add one and run it again:

```yaml
approve-plan: { decision: approve, text: "" }
```

```text
STUBBED   plan (prompt)
STUBBED   approve-plan (wait) — answered by stub
SKIPPED   stop (cancel) — when_condition_false
STUBBED   implement (prompt)
STUBBED   test (script)
STUBBED   fix (command)
Simulation outcome: completed
```

The trace also prints each agent node's resolved prompt and the provider and model it will run on. A dry run checks the wiring: routing, gates, `when:` conditions and variable substitution. It does not check what the model will write. To run the `test` script for real while the agents stay stubbed, delete its stub and add `--exec-code --input test_command="npm test"`. That runs your test command on your machine.

## Keep the routes as fixtures

A fixture is a stub file with an expected result, saved in the workflow's `fixtures/` folder. `archon workflow test` replays every fixture, so a later edit that breaks a route fails the test. Add one fixture per route.

`fixtures/approved.stubs.yaml`: the tests pass, so `fix` must not run.

```yaml
# The tests pass, so `fix` is skipped. It has no stub, so the fixture fails if it runs.
plan: { summary: "Add a --version flag that prints the version from package.json." }
approve-plan: { decision: approve, text: "" }
implement: "Added the flag and a test."
test: { passed: true, output: "12 pass, 0 fail" }
fixture:
  expect: completed
  reached: [implement, test]
```

`fixtures/red.stubs.yaml`: the tests fail, so `fix` runs and receives the test output.

```yaml
# The tests fail, so `fix` runs and receives the test output.
plan: { summary: "Add a --version flag that prints the version from package.json." }
approve-plan: { decision: approve, text: "" }
implement: "Added the flag."
test: { passed: false, output: "version.test.ts: expected 1.2.0, got undefined" }
fix: "Read the version from package.json at startup."
fixture:
  expect: completed
  reached: [fix]
  resolved-text-contains:
    fix: "expected 1.2.0, got undefined"
```

`fixtures/rejected.stubs.yaml`: you reject the plan, so the run is cancelled before any code changes.

```yaml
# You reject the plan: the run is cancelled before any code changes.
# `implement` has no stub, so the fixture fails if it runs.
plan: { summary: "Rewrite the CLI parser." }
approve-plan: { decision: reject, text: "Too broad. Only add the flag." }
fixture:
  expect: cancelled
  reached: [stop]
```

The `fixture:` block holds the expectations. `expect` is the simulation outcome: `completed`, `failed`, `paused` or `cancelled`. `reached` lists nodes that must run. `resolved-text-contains` checks that a fact reached a node: here, that the failing test's output reached the fixing agent's instructions. A node the route reaches without a stub fails the fixture, so leaving out the stub for `fix` in `approved` is what proves `fix` stays skipped.

```bash
archon workflow test build-change
```

```text
✔ my-team/build-change/fixtures/approved.stubs.yaml → build-change
    Simulation outcome: completed
✔ my-team/build-change/fixtures/red.stubs.yaml → build-change
    Simulation outcome: completed
✔ my-team/build-change/fixtures/rejected.stubs.yaml → build-change
    Simulation outcome: cancelled

3 passed, 0 failed
```

`workflow test` never creates a run or calls a provider. Run it after every edit to the workflow, and in CI.

## Run it with a real brief

The brief is the input the whole run is measured against. A vague brief gets a confident plan for a change you didn't want. State the problem, why it matters, the outcome you want, what must not change, and how you will know it is done; see [The input is the contract](/getting-started/quick-start/#the-input-is-the-contract).

```bash
archon workflow run build-change --branch feat/version-flag --input test_command="npm test" "$(cat <<'BRIEF'
Problem: the CLI has no way to print its version, so bug reports never say
which release they were filed against.

Outcome: `mycli --version` prints the version from package.json and exits 0.

Invariants: no new dependency; every other flag behaves as before.

Acceptance: a test runs `mycli --version` and checks the output.
BRIEF
)"
```

The run works in its own git worktree on the branch `feat/version-flag`, so its edits stay out of your checkout. After `plan` finishes, the command prints `Workflow paused — waiting for approval.` and returns. Read the plan and answer:

```bash
archon workflow attention                       # paused runs that need you, with their IDs
archon workflow artifacts <run-id> plan.md      # print the plan
archon workflow approve <run-id> "Keep the flag handling in cli.ts"
```

Approving continues the run in your terminal until it finishes. To stop it instead, run `archon workflow reject <run-id> "Too broad"`. The answer's text reaches the next node as `$approve-plan.output.text`. Afterwards, `archon workflow get <run-id>` shows each node's status, and the commits are on `feat/version-flag`.

## Where to go next

- **Every field and node type:** [Workflow language](/reference/workflow-language/).
- **Repeat work until a check passes:** [Loop nodes](/guides/loop-nodes/).
- **Write script nodes that supply facts:** [Script nodes](/guides/script-nodes/).
- **Start a workflow from an event, such as a new issue:** [Proactive workflows](/guides/proactive-workflows/).
- **Write better commands:** [Authoring commands](/guides/authoring-commands/).
- **A full delivery pipeline to copy from:** the bundled `sdlc` pack; see [SDLC workflows](/reference/sdlc-workflows/).

---

### Packages Docs Web Src Content Docs Getting Started Installation (`packages/docs-web/src/content/docs/getting-started/installation.md`)

---
title: Installation
description: Install Archon on macOS, Linux, or Windows.
category: getting-started
audience: [user, operator]
sidebar:
  order: 0
---

## Quick Install

Release binaries ship without AI providers. The quick installers ask which of
Claude, Codex and Pi to add and run `archon provider add` for your
selection; Homebrew prints the command instead. See [Providers](#providers).

### macOS / Linux

```bash
curl -fsSL https://archon.diy/install | bash

# Choose providers without the question
curl -fsSL https://archon.diy/install | bash -s -- --providers codex
```

> **x64 compatibility:** Compiled x64 binaries require AVX2. Older Intel/AMD
> hardware and virtual machines that mask AVX2 cannot use the x64 quick install;
> use [From Source](#from-source) instead. ARM64 quick installs are unaffected.

### Windows (PowerShell)

```powershell
irm https://archon.diy/install.ps1 | iex

# Choose providers without the question
& ([scriptblock]::Create((irm https://archon.diy/install.ps1))) -Providers codex
```

### Homebrew (macOS / Linux)

```bash
brew install coleam00/archon/archon
archon setup        # or: archon provider add <claude|codex|pi>
```

### Docker

```bash
docker run --rm -v "$PWD:/workspace" ghcr.io/coleam00/archon:latest workflow list
```

## Providers

Adding a provider connects Archon to your agent CLI or SDK; it does not install that CLI.

A release binary runs only the providers you add. Each one is a separate
executable from the same release, verified against its `checksums.txt` and
recorded under `~/.archon/plugins/`:

```bash
archon provider add codex          # one provider
archon provider add claude codex pi
```

`archon setup` asks the same question and then walks through each provider's
authentication. The installers take the selection as `--providers` (shell),
`-Providers` (PowerShell) or the `ARCHON_PROVIDERS` environment variable, as a
comma-separated list; an empty value installs none. With no selection and no
terminal to ask on, the installer installs none and prints the command.
Archon never picks a provider for you: a workflow or chat message that needs a
provider you have not installed fails before it starts and names the add
command.

An installed provider must match the CLI version. After upgrading Archon, run
`archon provider add` again for each provider you use; until then those
runs fail with the command to run. Rerunning the quick installer asks again.

Releases up to 0.11.1 predate `archon provider` and bundle their providers. To
pin one with `VERSION`, skip the provider step with an empty selection
(`--providers=` or `-Providers ''`); otherwise the installer runs a command that
release does not have and exits with an error.

Docker images and source checkouts run Claude, Codex and Pi from source and need
no install step.

## Using Archon from a GUI or service

The quick installer and Homebrew install a native, compiled `archon` executable;
they do not require Bun at runtime. Each compiled invocation records its absolute
path and version in `<ARCHON_HOME>/install.json`:

```json
{
  "binary": "/usr/local/bin/archon",
  "version": "0.9.0"
}
```

Treat this file as a discovery hint, not executable configuration. An explicit
path configured by the user always wins. Otherwise, read the manifest, require
`binary` to be an absolute executable path, and launch it directly. If the file
is absent or invalid, fall back to the documented platform default or ask the
user to choose a binary. The quick-installer defaults are `/usr/local/bin/archon`
on macOS/Linux and `%USERPROFILE%\.archon\bin\archon.exe` on Windows; expand
`%USERPROFILE%` to the user's home directory before invoking the Windows path.

The last compiled Archon invoked updates the record when its path or version
changes. Quick installers create it during their final version check. Homebrew
creates it on first use. Source and Bun-linked invocations never write it.

The source-install and `bun link` workflow is for terminal development and
requires Bun on `PATH`.

## From Source

```bash
git clone https://github.com/coleam00/Archon
cd Archon
bun install
```

### Prerequisites (Source Install)

- [Bun](https://bun.sh) >= 1.0.0
- [GitHub CLI](https://cli.github.com/) (`gh`)
- [Claude Code](https://claude.ai/code) (`claude`), if you use the Claude provider

## Claude Code (Claude provider only)

The Claude provider orchestrates Claude Code; it does not bundle it. If you use
Claude, install Claude Code separately:

```bash
# macOS / Linux / WSL (Anthropic's recommended installer)
curl -fsSL https://claude.ai/install.sh | bash

# Windows (PowerShell)
irm https://claude.ai/install.ps1 | iex
```

The installed Claude provider finds Claude Code at the native installer's path (`~/.local/bin/claude`, or `%USERPROFILE%\.local\bin\claude.exe` on Windows) without configuration. Source installs (`bun run`) find it through `node_modules`. For any other install, such as `npm install @anthropic-ai/claude-code`, point Archon at it:

```bash
export CLAUDE_BIN_PATH="$(npm root -g)/@anthropic-ai/claude-code/cli.js"
```

Or set it durably in `~/.archon/config.yaml`:

```yaml
assistants:
  claude:
    claudeBinaryPath: /absolute/path/to/claude
```

Docker images (`ghcr.io/coleam00/archon`) ship with Claude Code pre-installed and
`CLAUDE_BIN_PATH` pre-set — no configuration needed.

See [AI Assistants → Claude Code](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only)
for full details and install-layout paths.

## Verify Installation

```bash
archon version
archon doctor     # lists installed providers and their diagnostics
```

## Next Steps

See [Updating Archon](/getting-started/updating/) for your install method.

- [Why Archon](/getting-started/why-archon/) — Workflows, nodes, commands and isolation
- [Quick Start](/getting-started/quick-start/) — Run your first workflow
- [Configuration](/getting-started/configuration/) — Set up API keys and preferences

---

### Packages Docs Web Src Content Docs Getting Started Operate (`packages/docs-web/src/content/docs/getting-started/operate.md`)

---
title: Operate runs
description: Watch runs, answer the ones that need you, resume or stop them, read what they produced, and hand classes of questions to your own handler workflows.
category: getting-started
area: workflows
audience: [user]
status: current
sidebar:
  order: 5
---

A run ends in one of three ways: it finishes, it fails, or it pauses because it needs something from outside. This page covers what you do with each: see what is running, answer the runs that wait for you, resume or stop a run, and read what a finished run left behind. The second half is the JSON contract for scripts and agents that work the queue for you.

Every `<run-id>` below accepts the short 8-character id that `workflow runs` prints, when you run the command from the project's directory.

## See what is running

```bash
archon workflow status                         # running and paused runs in this project
archon workflow status --all                   # the whole install
archon workflow runs                           # recent runs of every status
archon workflow runs --status failed --limit 50
archon workflow get <run-id>                   # one run: status, nodes, outcome
archon workflow logs <run-id> --follow --format text
```

`get` reports two separate facts. The status (`running`, `paused`, `completed`, `failed`, `cancelled`) says how execution went. The outcome (`succeeded` or `failed`), when the workflow declares one, is the workflow's verdict on the work. A `completed` run with outcome `failed` ran to the end and judged the work unsuccessful.

To block until one run finishes or needs you, use `archon workflow wait <run-id>`. It is the partner of `archon workflow run ... --detach`: take the run id from the launch and wait on it instead of polling `get`. It has no default timeout. It exits 0 when the run finishes or needs you, whatever the run's status; the status is on stdout.

To browse runs in a browser, start the console with `archon serve`.

## Find the runs that need you

```bash
archon workflow attention                      # this project
archon workflow attention --all                # the whole install
archon workflow attention --all --watch        # stay open and print each change
```

Each item names the run, its workflow, its project, how long it has waited, the question, and the exact command for each choice. The item's `kind` says what it needs:

| Kind | What it is | What to do |
|---|---|---|
| `attention` | A `wait: attention` node asked a question | Answer it in its form |
| `awaiting_response` | An approval gate | `approve`, `reject` or `respond` |
| `usage_limit` | A node hit a provider usage limit, or a rate limit that outlasted its retries | Nothing. It retries at `resetAt`. `resume` tries sooner |
| `unreadable` | A paused run Archon cannot describe | Inspect it, then resume or abandon it |

A `usage_limit` wait retries from the node that hit the limit, at the provider's reset time or a 5 to 15 minute backoff, and pauses again if the limit is still there. With `workflows.autoResumeOnQuotaReset: false`, or for a container run, the run fails instead of waiting.

An `unreadable` item carries `reason` and `detail`. A container paused with reason `source_release_not_continued` could not read its frozen source on this host. Resume it by hand on a host that can.

## Answer a run

Copy the command the item prints, or write it yourself:

```bash
archon workflow respond <run-id> <choice> "why"                # any declared choice
archon workflow approve <run-id> "Looks good"
archon workflow reject <run-id> --reason "Not this week"
archon workflow answer <run-id> --occurrence <id> --choose use-dev
archon workflow answer <run-id> --occurrence <id> --text "Rotated the key"
```

`respond`, `approve` and `reject` work on both gates and attention waits: each verb picks the option with its id. `answer` speaks the attention wait's own form. `--choose` names a declared option, repeated for a `multi_select` and left out for a `message`. `--text` records free text with the answer. An undeclared choice is refused and the run stays paused.

`--occurrence` pins the answer to one pause. If the run has moved on to a different pause, the answer is refused and the refusal names the current occurrence. A second answer to the same pause is refused too, so two answerers never both win.

In a terminal, an answer continues the run in place. With `--json`, the answer is recorded and the run waits for a continuation host: `archon serve`, or `archon workflow wake --watch`. A container run needs `archon workflow resume <run-id>`.

## Resume, cancel or abandon a run

```bash
archon workflow resume <run-id>       # rerun a failed or paused run, skipping completed nodes
archon workflow resume <run-id> --detach
archon workflow end-turn <run-id>     # end a running run's stalled turn
archon workflow cancel <run-id>       # stop a running run
archon workflow abandon <run-id>      # give up on a run for good
```

Pick the command by what is wrong:

- **A turn is stuck, but the run should go on.** When `workflow wait` or `status` reports `stalled_tool_calls`, a tool or reported background work has made no progress. `end-turn` records a request to end that turn. Only the run's live owner acts on it: it ends the turn if the same stall is still active, the node fails as `timeout`, and the node's retry policy decides whether to try again. The command itself stops nothing, and it refuses a run that is not `running`.
- **The run should stop.** Use `cancel`.
- **The run failed or paused.** Use `resume` to continue it.

`cancel` works only on a `running` run. It stops the process that owns the run, confirms it stopped, and then records `cancelled`. When no owner answers, it refuses and changes nothing, because a database update cannot prove the work on the host stopped. A foreground `archon workflow run` can't be stopped from another shell: interrupt it in its terminal.

`abandon` is for a paused or failed run, or one whose owner is gone. It stops a live owner on this host first, records `cancelled`, and removes the worktree Archon created for the run. Removal is forced: uncommitted and untracked files in that worktree are deleted. The branch stays, so committed work is recoverable. Archon never decides a run is dead from its age.

The [CLI reference](/reference/cli/#workflow-cancel) covers sub-run trees, containers and the Windows job rules.

## Read what a run produced

```bash
archon workflow get <run-id>                          # status, outcome, per-node summary with --verbose
archon workflow artifacts <run-id>                    # list the files in $ARTIFACTS_DIR
archon workflow artifacts <run-id> review/report.md   # print one
archon workflow logs <run-id> --format text           # the transcript, readable
```

Stages hand each other files in `$ARTIFACTS_DIR`, so the artifacts are usually where the answer is: a report, a plan, a review. They live under `~/.archon/workspaces/<project>/artifacts/runs/<run-id>/`. The transcript is JSONL with every node, tool call and subprocess output. Treat it as sensitive: it holds prompts and command output.

## Hand a class of questions to a workflow

Archon routes nothing: every answerer sees every item. Labels are how your own handler workflows find the items they own. A `wait: attention` node can label its pause, with values that may reference earlier nodes:

```yaml
  - id: ask
    depends_on: [classify]
    wait:
      attention: "$classify.output.summary"
      form: select
      options: [{ id: retry }, { id: stop }]
      labels:
        ci: "$classify.output.cause"     # e.g. "pr" or "other"
        area: delivery
```

Labels are a flat map from string to string. A key is one token with no `=` or whitespace. Only attention waits carry labels; a gate's item has none. To see which classes recur often enough to deserve a handler, count them:

```bash
archon workflow attention --json --all --count-by label:ci
```

This handler answers delivery runs whose CI went red for a reason outside the change, labelled `ci=other`. Start it from a [schedule](/guides/workflow-triggers/) so it polls the queue.

```
/* Detailed source-code truncated for AI context efficiency. */
```

An answer command exits non-zero when the answer is refused, including with `--json` (`ok: false`). An answer accepted while the run's resource is full exits 0: the run keeps its place and a continuation host continues it when a slot frees. If two handlers watch the same class, the occurrence check refuses the second answer.

A handler can also classify instead of answer. A cheap classifier lists the `kind: attention` items with no `class` label, asks a small model for a typed class, urgency and whether an agent can answer, and writes the result back:

```bash
archon workflow attention label <run-id> --occurrence <occurrence> \
  --set class=ci-other --set urgency=high --set answerable=agent --json
```

The write merges: a key it sets replaces that key, a key it unsets is removed, and nothing else changes. A second handler then selects its class with `--label class=ci-other --label answerable=agent`. The labels are yours. Archon never reads `class`, `urgency` or `answerable`, and never decides whether a person or an agent answers.

## The queue contract

Everything above is also JSON, for scripts and agents. The list, the single read, the stream and `workflow wait` derive from the same run projection, so they never disagree about what a run needs.

| Operation | CLI | HTTP |
|---|---|---|
| List | `archon workflow attention --json [--project <id\|name\|key> \| --all] [--label k=v]… [--sort age\|label:<key>]` | `GET /api/runs/attention?project=…&label=…&sort=…` |
| Count | `archon workflow attention --json --count-by project\|label:<key>` | `GET /api/runs/attention?countBy=…` |
| Stream | `archon workflow attention --watch --json [--project … \| --all] [--label k=v]… [--after <cursor>]` | Not yet |
| Read one | `archon workflow attention <run-id> --json` | `GET /api/runs/<run-id>/attention` |
| Label | `archon workflow attention label <run-id> --occurrence <id> [--set k=v]… [--unset k]…` | `POST /api/runs/<run-id>/attention/labels` |
| Answer | `archon workflow answer <run-id> --occurrence <id> …`, or `respond`, `approve`, `reject` | `POST /api/workflows/runs/<run-id>/answer` |
| Give up on the run | `archon workflow abandon <run-id>` | `POST /api/workflows/runs/<run-id>/abandon` |

With explicit chat scope (`--chat-project-id <id>`), CLI list, count and watch use the selected project regardless of the working directory. Its ID, name or key is accepted by `--project`; other projects and `--all` are rejected. Without explicit chat scope, the CLI defaults to the working directory's project, `--project` names another project and `--all` covers the install. Every `workflow attention` form runs from any directory, registered or not. Over HTTP there is no working directory, so the default is the install. `--label k=v` is an exact match, and repeated filters must all match. `age` sorts the oldest pause first; `label:<key>` sorts by that label's value as a plain string. A single-run read returns null when the run has no item. The [CLI reference](/reference/cli/#workflow-attention) lists every flag.

### Queue items

```json
{
  "kind": "attention",
  "runId": "7c1e…",
  "workflow": "deliver",
  "project": { "key": "acme/web", "codebaseId": "c41f…", "name": "acme/web" },
  "fanOut": null,
  "revision": 1,
  "labels": { "ci": "other" },
  "pausedAt": "2026-10-14T07:58:02.000Z",
  "context": {
    "artifactsDir": "/home/me/.archon/workspaces/acme/web/artifacts/runs/7c1e…",
    "transcript": "/home/me/.archon/workspaces/acme/web/logs/7c1e….jsonl",
    "workingPath": "/home/me/.archon/workspaces/acme/web/worktrees/fix-ci",
    "branch": "fix-ci"
  },
  "node": "base",
  "occurrence": "e0f4…",
  "message": "CI on windows failed twice with EPERM. Retry, or stop?",
  "form": "select",
  "options": [{ "id": "retry" }, { "id": "stop" }]
}
```

- **`kind`** is one of the four kinds above. A `usage_limit` item carries `node` and `resetAt` and has no `occurrence`. An `unreadable` item carries `reason` and `detail`; it also covers a parent whose sub-run tree cannot be read: a sub-run with no row, or a tree that revisits a run or exceeds 500 runs.
- **`occurrence`** identifies an answerable pause. Pass it back with `--occurrence`. If a refusal does not name the current occurrence, read the item again before retrying.
- **`revision`** starts at 1 and grows by one with every label write on an attention wait. It stays 1 for other kinds, so it does not identify every change to an item.
- **`project`** is the project the run belongs to, never a label. `key` is the storage key [state documents](/reference/cli/#state) scope by, so `archon state --project <item.project.key>` reaches the same project. Copy it exactly when you pass `--project`. `codebaseId` is null for a project with no codebase row, such as a run in an unregistered directory (`@cwd/<basename>-<digest>`). A run started before its codebase was renamed keeps the key it started with. `project` is null only for a run with no trusted storage root.
- **`fanOut`** is `{ parentRunId, nodeId, index }` for a child of a `fan_out:` node, and null otherwise.
- **`pausedAt`** is when the pause opened. Gates do not record it, so it is null for them and they sort after timed items.
- **`context`** holds pointers, never pasted content.

### Several items from one workflow

A workflow can [fan out one child per question](/reference/workflow-language/#several-independent-questions) with `max_parallel: all`. Each child pauses at its own attention wait, with its own question, choices and occurrence. A parent blocked on its children is never an item, and reading the parent returns only its first actionable descendant. To find every question, list the queue and select the items whose `fanOut.parentRunId` is the parent; `fanOut.index` keeps the original order.

```bash
archon workflow attention --json --all
archon workflow answer <child-run-id> --occurrence <occurrence> --choose accept
```

Answer the children in any order. Each child applies its own answer at once. The parent stays paused until every child finishes, then reads their ordered results.

### Stream the queue

`--watch --json` turns the list into JSON lines across every run in scope, including runs that start after the stream does. It replaces polling the list and diffing it. It runs until Ctrl-C, SIGTERM, or its reader closes stdout, then exits 0. On Windows a process kill is not a signal, so it ends the stream without that exit.

A stream starts with one `attention` line per open item, with `snapshot: true`, then a `ready` line. The lines before `ready` are the complete open set: on every connect, an item you held that is not among them is gone. After `ready`, the stream follows every change the store commits, whichever process made it: a detached run, a trigger, the server, another host on the same database.

| `type` | When | Fields |
|---|---|---|
| `attention` | An item opened or changed | `cursor`, `snapshot`, `project`, `item` |
| `resolved` | An item this stream sent closed or was replaced | `cursor`, `project`, `runId`, `occurrence`, `reason`; with `reason: "answered"`, also `answeredVia` and `answeredAt` |
| `unmatched` | A label write moved an open item outside `--label` | `cursor`, `project`, `runId`, `occurrence`, `revision` |
| `terminal` | A run ended | `cursor`, `project`, `runId`, `status`, `outcome` |
| `resumed` | A failed run resumed, an engine-cancelled fan-out child recovered, or a paused or stale-running run resumed and cleared a stored error | `cursor`, `project`, `runId`, `status: "running"` |
| `owner_lost` | A running run's owner process on this host is gone | `cursor`, `project`, `runId`, `observedStatus` |
| `ready` | The snapshot is complete | `cursor` |
| `bookmark` | 30 seconds passed with no other line, and the stream read the store up to `cursor` | `cursor` |
| `degraded` | Repeated failure to project a run; coverage may be incomplete. The stream keeps running | `cursor`, `reason: "run_unreadable"` |
| `error` | `--after` named a cursor this install did not issue, or the store no longer reads the stream's position. The last line; exit 4 | `error: "cursor_invalid"` |

- **Keep an open set keyed by `runId`.** Each `attention` line replaces that run's item, including when its kind or `resetAt` changes while `revision` stays 1. `resolved` removes it. Its `reason` is `answered`, or `withdrawn` when the run resumed, ended, was deleted or moved to a different pause. For a fan-out child, remove only that child.
- **Deliveries are at least once.** Replacing by `runId` is idempotent. Dedupe answerable pauses on `(runId, occurrence, revision)`, and `terminal` and `resumed` on `(runId, cursor)`. Don't dedupe every kind by occurrence: usage-limit waits have none. `owner_lost` is a live observation and is not replayed. Only a run whose recorded owner is on this host and user can be reported lost.
- **Reconnect from a cursor before any event you did not finish.** A cursor names a feed event, and one event can emit two lines with the same cursor (`resolved` then `attention`, or `resolved` then `terminal`). `--after <cursor>` skips the whole event, so checkpoint only on a handled `bookmark` or from before the partly handled event. The new stream sends the snapshot and `ready`, then replays readable changes after the cursor. If the cursor is rejected, reconnect without `--after`.
- **A silent stream is not reading.** A stream that cannot read the store sends no `bookmark`, so no line for well over 30 seconds means it is stuck.
- **`degraded` means gaps.** The operator log names the run that failed to project; the warning does not. Repair the read failure, reconnect to refresh the open set, and replay from a cursor before the gap to recover missed transitions. The stream sends no recovered signal.
- **`--label` filters `attention` and `resolved` only.** `terminal`, `resumed` and `owner_lost` arrive for every run in scope. A label write that leaves an item outside the filter sends `unmatched`.
- **Items report the row when the stream read it.** The stream reads about once a second, and PostgreSQL wakes it on each commit. A pause opened and answered between two reads may never appear.

The stream only reads. Stopping it affects no run.

---

### Packages Docs Web Src Content Docs Getting Started Quick Start (`packages/docs-web/src/content/docs/getting-started/quick-start.md`)

---
title: Quickstart
description: Install the Archon binary, add a provider and run your first workflow.
category: getting-started
audience: [user]
sidebar:
  order: 2
---

This page takes you from nothing to a finished run in any git repository.

## 1. Install an agent

Archon runs agents you install yourself. Install at least one and sign in:

- **Claude Code:** `curl -fsSL https://claude.ai/install.sh | bash` on macOS, Linux or WSL, or `irm https://claude.ai/install.ps1 | iex` in Windows PowerShell. Then run `claude` and sign in with your Claude subscription.
- **Codex:** install the Codex CLI and run `codex login`.

Archon finds Claude Code at the native installer's path (`~/.local/bin/claude`) on its own. Only an npm or other custom install needs a path; see [Claude Code binary path](/getting-started/ai-assistants/#binary-path-configuration-compiled-binaries-only).

## 2. Install Archon

```bash
curl -fsSL https://archon.diy/install | bash        # macOS / Linux
irm https://archon.diy/install.ps1 | iex             # Windows (PowerShell)
```

The installer asks which providers to add (`claude`, `codex`, `pi`) and runs `archon provider add` for them. With Homebrew, install and then add providers yourself:

```bash
brew install coleam00/archon/archon
archon setup        # or: archon provider add claude
```

Check the install:

```bash
archon version
archon doctor       # lists installed providers and their diagnostics
```

Docker, source installs and older x64 CPUs without AVX2 are covered in [Installation](/getting-started/installation/).

## 3. Run a workflow

In any git repository:

```bash
cd your-project
archon workflow list
archon workflow run archon-investigate "Why does the login test fail?"
```

`archon-investigate` traces the question through your code until it can prove the answer, and leaves the repository as it found it. The run streams to your terminal and ends with its report.

## 4. Read the result

```bash
archon workflow runs              # recent runs in this project, with their IDs
archon workflow get <run-id>      # one run: status, nodes and outcome
archon workflow attention         # runs paused for your decision
```

A run that needs a decision pauses as an attention item. Answer it with `archon workflow respond <run-id> <decision> "<reason>"`. See [Operate runs](/getting-started/operate/).

To browse runs in a browser instead, start the Web UI with `archon serve`.

## The input is the contract

The message you pass to a run is the specification it is measured against. Archon governs how the work happens: isolation, gates, retries and the audit trail. It cannot supply what you wanted. A vague brief produces a confident answer to a question you didn't ask.

Wherever the run reads its input, an issue body, a message or a document, make it state:

| | |
|---|---|
| **Problem** | What is wrong today, concretely |
| **Why it is worth solving** | The cost of leaving it alone |
| **Why now** | What makes this the moment |
| **Desired outcome** | What is observably different afterwards |
| **Invariants** | What must stay true in any acceptable implementation |
| **Acceptance** | How you will know it is done |

Solution steering is optional and goes last. Leave it out and the run chooses its own approach. If you hold a constraint, such as "reuse this helper" or "no new dependency", say it, because the run can't honor a preference you didn't state.

A brief worth running:

```bash
archon workflow run archon-ship --branch fix/upload-timeout "$(cat <<'BRIEF'
Problem: uploads over ~8 MB fail with a 504 after 30s. The proxy read timeout is
30s and the upload handler streams to disk before responding.

Why it matters: this is the top support complaint this month.

Why now: the export feature ships next week and its files are 20-50 MB.

Outcome: a 50 MB upload completes, and a stuck upload still fails instead of hanging.

Invariants: no change to the storage layout or the public upload API; memory use
must not scale with file size.

Acceptance: a 50 MB upload succeeds end to end; a test covers the timeout path.

Steering: prefer streaming straight to storage over raising the timeout.
BRIEF
)" --detach
```

`archon-ship` takes an issue or request to a reviewed pull request. It works through the GitHub CLI, so run `gh auth login` first. `--detach` starts the run in the background and returns at once.

## Next steps

- [Build your first workflow](/getting-started/first-workflow/)
- [Set up the SDLC factory](/getting-started/sdlc-factory/): the bundled software factory, from issue to merge
- [Configuration](/getting-started/configuration/): project settings and environment variables

---

### Packages Docs Web Src Content Docs Getting Started Sdlc Factory (`packages/docs-web/src/content/docs/getting-started/sdlc-factory.md`)

---
title: Set up the SDLC factory
description: Take a GitHub repository from nothing to an issue built by the build queue and merged by the merge queue, and know everything the pack writes.
category: getting-started
audience: [user]
sidebar:
  order: 4
---

The bundled `sdlc` pack is a software factory made of four queues. You write an issue; intake triages it, the build queue works it to a reviewed pull request, and the merge queue merges it. When a run reaches a decision it can't make, it pauses as one attention item for you to answer. This page sets that up on one GitHub repository, runs it by hand once, and then shows how to let it run on its own.

| Queue | What it does |
|---|---|
| `archon-intake` | Checks the issue's author against your maintainer list, triages a maintainer's issue and labels its route |
| `archon-build-queue` | Takes one triaged issue through `archon-ship` rounds until its pull request is reviewed Ready |
| `archon-merge-queue` | Merges the Ready, green pull requests the build queue finished, and sends back what it can't merge |
| `archon-discoveries` | Files, tracks or drops what runs noticed outside their own task |

Every workflow in the pack is described in [SDLC pack workflows](/reference/sdlc-workflows/).

## Before you start

You need:

- Archon with at least one provider, from the [Quickstart](/getting-started/quick-start/).
- A local clone of a GitHub repository you can push to.
- The GitHub CLI signed in as the account the pack will act as: `gh auth login`. Every label, comment, push and merge is made as that account.
- CI that reports checks on pull requests. The merge queue merges only when the checks at the pull request's head are green, so a repository with no checks never merges.

## 1. Check the forge

```bash
archon workflow run archon-forge-check     # from the clone
```

It proves that `gh` is signed in and can read the repository the clone's remotes name. It proves read access only; the first write proves the rest. The pack talks to GitHub through `gh` by default. Setting `ARCHON_SDLC_FORGE=forge` routes it through `archon forge` and an installed [forge plugin](/reference/forge/) instead, and `archon-forge-check` then also checks that the plugin offers every operation the delivery workflows use. It does not check the merge queue's operations: the merge queue checks those itself when it starts and refuses a plugin without them before it claims any work. Stay on `gh` for the factory until the installed plugin offers them too.

## 2. Name your maintainers

Create `.archon/sdlc/policy/maintainers.json` and commit it to the default branch. The queues read it from the default branch's head on the remote, so a local copy does nothing.

```json
["your-login", { "login": "teammate", "merge_queue": "own-prs" }]
```

Each entry is a GitHub login, or an object naming one. With no file, every author is outside. The list decides three things:

- **Whose issues get triaged.** An issue from anyone else gets `archon-needs-maintainer` and starts no agent until a listed maintainer adds `archon-triage` or a route label to it.
- **Whose review verdicts count.** The merge queue trusts only a review comment written by a listed login. **The login that authors the pack's published review comment must be on the list on the default branch**, or that verdict is ignored. With the default `gh` source, this is the identity `gh` acts as; with the opt-in `forge` source, it is the forge plugin's credential identity, which may differ. Check the published comment's author when choosing which login to list.
- **Whose own pull requests enroll.** `"merge_queue": "own-prs"` enrolls that maintainer's own open, non-draft pull requests in the merge queue. A pull request a work item already names, such as one the build queue delivered, is not enrolled again.

## 3. Adjust policy and models (optional)

The queues decide clear cases on their own and ask about unclear ones. The pack's default policy for each decision lives in [`.shared/policy/`](https://github.com/coleam00/Archon/tree/dev/.archon/workflows/sdlc/.shared/policy):

| File | Decides |
|---|---|
| `triage.md` | What triage accepts, and what may close without a person |
| `build.md` | What the build queue continues on its own and what it asks |
| `merge.md` | Which pull requests merge on their own: by default small fixes, small features, tests and docs. Schemas, auth, large or partial work are asked |
| `rulings.md` | Answers your maintainers already gave, read before plan or review stops to ask |
| `discoveries.md` | When a discovery is filed, tracked or dropped |

To change one, put only the lines to add or override in `.archon/sdlc/policy/<file>`. The agent reads the default first, then your file, and your line wins where they conflict. A policy file can be a pointer to documents the project already has.

Each stage picks its model from a concern alias, falling back to the tier it has always used. With nothing set, you get the tier models.

| Alias | Nodes | Falls back to |
|---|---|---|
| `@triage` | triage | `medium` |
| `@planner` | plan | `large` |
| `@investigator` | investigate | `medium` |
| `@implementer` | implement | `large` |
| `@reviewer` | every review agent and simplify | each node's tier |
| `@review-<lens>` | one lens: `seams`, `code`, `tests`, `errors`, `docs` or `simplify` | `@reviewer`, then the lens's tier |

```bash
archon ai alias set @reviewer codex gpt-5.5                                   # every run
archon workflow run archon-build-queue --model @reviewer=codex/gpt-5.5 ...    # one run
```

## 4. Write an issue the factory can build

The issue body is the contract every round is measured against. State the problem, why it is worth solving, why now, the desired outcome, the invariants, and acceptance; see [The input is the contract](/getting-started/quick-start/#the-input-is-the-contract). The build queue asks you for a missing element instead of guessing it.

## 5. Run the queues by hand

Run each queue once yourself before you automate anything. Replace `acme/widgets` with your repository and `42` with the issue number.

Triage the issue:

```bash
archon workflow run archon-intake \
  --input repo_host=github.com --input repo_path=acme/widgets --input number=42
```

Intake labels the issue with its route (`archon-ready`, `archon-plan` or `archon-investigate`), its type and its size, creating any pack label the repository lacks, and records a work item. Read the item with `archon state get work/42`.

Build it:

```bash
archon workflow run archon-build-queue \
  --input repo_host=github.com --input repo_path=acme/widgets --input number=42 --detach
```

A round runs `archon-ship`: investigate or plan if the route needs it, implement, open a draft pull request, review it, fix the findings, wait for CI and mark it ready. When a round ends on something the contract already answers, such as a review finding or red CI the change caused, the queue starts the next round at once as a new detached run. The item is done when the pull request's latest review says Ready at its head. Follow it with `archon workflow runs` and answer anything it asks (step 6).

Merge it:

```bash
archon workflow run archon-merge-queue --input repo_host=github.com --input repo_path=acme/widgets
```

One sweep agent decides every pull request the build queue made ready. It never pushes, never builds or tests, and never waits for CI. Each merge goes through a gate script that reads the pull request fresh and refuses unless:

- the trusted review verdict says Ready for the current head (a push after the review voids it);
- CI is green at that head;
- the default branch's CI is not red (pending does not hold a merge);
- the head contains the default branch, or GitHub reports it merges cleanly.

The gate merges pinned to that head (`gh pr merge --match-head-commit`, squash by default; `--input merge_method=merge` for a merge commit). A conflict, a stale verdict, a review thread that raises a defect, or red the change caused goes back to the build queue with a brief. Red CI in code the pull request doesn't touch gets one rerun of its failed jobs. If the default branch is red, nothing merges, and the queue raises one `merge: base` item whose only choice is `hold`. A pull request it fails to read twice in a row is parked until the build queue readies it again.

Drain what the runs noticed outside their task:

```bash
archon workflow run archon-discoveries --input repo_host=github.com --input repo_path=acme/widgets
```

## 6. Answer what the factory asks

```bash
archon workflow attention --all --watch
```

Each item says what is being decided, why it matters, the agent's recommendation with its evidence and confidence, the choices and what each does, and what happens if nobody answers. Its label names the queue and decision, such as `build: scope` or `merge: pr`. Answer with the choice and a reason:

```bash
archon workflow respond <run-id> <choice> "why"
```

The reason matters. A build or merge queue answer is written into the issue body as a dated "Current scope" section, and later rounds work from it. Every build item and merge pull-request item offers `other`: its text says what to do instead and becomes the ruling. `park` stops the queue working that item. [Operate runs](/getting-started/operate/) covers answering, resuming and stopping runs in full.

## 7. Let it run on its own

The pack installs no trigger. Nothing starts until you start it, by hand as above or with a binding you add. A binding names the workflow, its inputs, the Archon user it runs as (`archon trigger whoami` prints the ID) and how starts may overlap.

- **Build, merge and discoveries** run on a schedule. Save the binding, fire it once, then install it:

  ```bash
  archon trigger fire --config ~/.archon/triggers/widgets-build-queue.json
  archon trigger schedule install --config ~/.archon/triggers/widgets-build-queue.json   # macOS
  ```

  On Linux or Windows, run the same `trigger fire` command from cron, systemd or Task Scheduler.
- **Intake** starts on GitHub issue and label events, through the GitHub source plugin on a running server. It needs a webhook, so it is the last piece most people add. Until then, run intake by hand for each new issue.

The pack's README has a binding for each queue: [Intake](https://github.com/coleam00/Archon/blob/dev/.archon/workflows/sdlc/README.md#intake), [Build queue](https://github.com/coleam00/Archon/blob/dev/.archon/workflows/sdlc/README.md#build-queue), [Merge queue](https://github.com/coleam00/Archon/blob/dev/.archon/workflows/sdlc/README.md#merge-queue) and [Discoveries](https://github.com/coleam00/Archon/blob/dev/.archon/workflows/sdlc/README.md#discoveries). [Workflow triggers](/guides/workflow-triggers/) covers schedules, webhooks, capacity and overlap.

## What the pack writes

Everything below is written as the `gh` account, to the repository the queues name.

| Where | What | Who |
|---|---|---|
| Labels | Creates the pack's own `archon-*` labels when missing. Adds and removes them on issues as a delta, so labels others add survive | Intake; `archon-triage` with `--input publish=true` |
| Issues | Closes a duplicate of an open issue, or an issue a merged change already delivered, with one comment linking the cause, and a link-back comment on the original. Intake closes issues by triage verdict, as `triage.md` allows, and a merged pull request closes the issue it names with `Closes #N` | Intake; merges |
| Issue bodies | Appends a dated "Current scope" section with your answer | Build queue, merge queue |
| New issues | Files an issue for a verified discovery, marked so a rerun never files it twice. Adds one evidence comment to an open issue it tracks | Discoveries |
| Branches | Pushes `build/<issue>-<slug>-<run>`, or pushes onto an open pull request's existing head. Never force-pushes or rebases | Build queue rounds |
| Pull requests | Opens a draft, updates its description, posts one review comment and edits it in place, replies to and resolves review threads, marks it ready and back to draft. The description says `Closes #N` only when the review found every acceptance item met. Before merging, the merge queue changes `Refs #N` to `Closes #N` when the Ready review left nothing open, and changes it back if the merge doesn't land | Build queue rounds; merge queue |
| CI | Reruns the failed jobs of a run once, when red is outside the change or on the default branch | Build queue rounds, merge queue |
| Merges | Merges through the gate, pinned to the reviewed head. Deletes no branch | Merge queue |
| Worktrees and branches | If you schedule it, removes each merged worktree with its local branch, including a branch Archon adopted. Remote branches stay, with a warning | `archon-worktree-cleanup` |

On your machine it keeps state documents (`archon state list --prefix work/`), worktrees and run artifacts under `~/.archon/`.

## Pause or stop it

- **Stop new runs.** Remove the schedules (`archon trigger schedule remove --config <file>`, or the cron or systemd entry) and the intake webhook bindings. Removing a schedule does not cancel work already accepted.
- **Stop runs in flight.** A build round that continues starts its next round as a new detached run, and answering `merge` on a merge item starts a sweep at once, so removing schedules alone does not stop a chain. List the runs with `archon workflow status --all` and `archon workflow cancel` or `archon workflow abandon` them.
- **Stop one item.** Answer its attention item with `park`. A paused run waiting on you does nothing until you answer.

---

### Packages Docs Web Src Content Docs Getting Started Updating (`packages/docs-web/src/content/docs/getting-started/updating.md`)

---
title: Updating Archon
description: Update an existing Archon installation.
category: getting-started
audience: [user, operator]
sidebar:
  order: 1
---

Use the same method you used to [install Archon](/getting-started/installation/).
Read the [release notes](https://github.com/coleam00/Archon/releases) before updating.
Let active workflows finish and stop any running Archon server before replacing
its executable or source checkout. Restart the server afterward so the console
and new runs use the updated version.

A release binary checks that each installed provider matches its own version.
After a Homebrew or quick-installer update, reinstall the providers you use
(the quick installer asks; Homebrew does not):

```bash
archon provider add claude codex   # the providers you use
```

Until you do, a run that needs one fails before it starts and prints this
command. `archon doctor` lists each provider's installed version. Docker and
source installs run providers from source and skip this step.

## Homebrew

```bash
brew update
brew upgrade archon
archon version
```

## Quick installer

Re-run the installer to download and replace the executable with the latest release.

### macOS / Linux

```bash
curl -fsSL https://archon.diy/install | bash
archon version
```

### Windows (PowerShell)

```powershell
irm https://archon.diy/install.ps1 | iex
archon version
```

## Docker

For the release image shown in the installation guide, pull the new image before
starting another container:

```bash
docker pull ghcr.io/coleam00/archon:latest
docker run --rm -v "$PWD:/workspace" ghcr.io/coleam00/archon:latest version
```

For a long-running container, stop and recreate it using the same volumes,
environment, and ports as the original container. Restarting an existing container
does not change its image. Keep your data volumes.

The repository's Docker Compose deployment builds from source. Follow its
[update instructions](/deployment/docker/#update), retaining the profiles you
used for deployment; rebuilding and recreating the app loads the updated source.

## From source

In your Archon checkout, first check for local changes:

```bash
git status
```

Commit or stash your changes before updating. The `main` branch contains releases;
`dev` is the working development branch.

```bash
git switch main
git pull --ff-only
bun install --frozen-lockfile
bun run cli version
```

If you serve the console from source, rebuild it with `bun run build:web` before
restarting your server.

## Update notices

Compiled CLI installs check for updates in the background during `workflow run`
and `workflow resume`. When fresh release data is available, they print update
instructions to stderr at most once every 24 hours, even when the command fails.
`--quiet` and `--json` suppress this notice. An unavailable network or stale cache
does not delay the command.

The console shows a banner when a newer release is available. Dismissing it is
remembered in your browser for that release; a later release shows the banner again.

---

### Packages Docs Web Src Content Docs Getting Started Why Archon (`packages/docs-web/src/content/docs/getting-started/why-archon.md`)

---
title: Why Archon
description: Archon is a factory builder. You write how work gets done as YAML workflows, and Archon runs them with agents, scripts and your decisions.
category: getting-started
audience: [user]
sidebar:
  order: 1
---

Archon is a factory builder. You describe how work should get done as YAML workflows, and Archon runs them with AI agents, scripts and your decisions in the places that need them. Software is the first factory: the bundled `sdlc` pack takes an issue to a merged pull request. The same engine runs any process you can describe.

## The problem it solves

Ask a coding agent to fix a bug and the result depends on the run. It may skip the plan, forget the tests or ignore your pull request template. Your process lives in the model's head, so you supervise every step.

Archon moves the process into a file you own. The workflow fixes the steps, the checks and the gates. The agent supplies judgment inside each step. You get:

- **The same process every time.** Plan, implement, review, validate and open the pull request, in the order you wrote down.
- **Human on the loop, not in it.** You take yourself out of the loop and sit on top of it. Queues move work forward on their own. When a run reaches a decision it can't make, it pauses as one [attention item](/getting-started/operate/): what it is, why it matters, a recommendation and the choices. You answer and it carries on.
- **Governance built in.** Approval gates, an event log and transcript for every run, and the cost of every run, stored in your own database.
- **Your agents and subscriptions.** Archon runs Claude Code, Codex and Pi through their own CLIs and SDKs, with the accounts and project guidance you already have.
- **Self-hosted.** One install on your machine or server. SQLite by default, PostgreSQL if you want it.

## Core concepts

A **workflow** is a YAML file in `.archon/workflows/`. It lists **nodes** and the dependencies between them, and Archon runs them as a graph. Nodes without a dependency between them normally run in parallel. [Loop bodies settle ordinary work before interactions](/guides/loop-nodes/#how-it-works); checkout guards can also require sequential execution.

Each node does one thing:

| Node | What it does |
|---|---|
| `prompt:` or `command:` | Sends an inline prompt, or a markdown command file, to an agent |
| `bash:` or `script:` | Runs code. No agent, no model cost |
| `loop:` | Repeats an agent step until a condition holds |
| `wait:` | Pauses for a time, an event or your answer to an attention item |
| `include:` | Inlines another workflow as a block of this one |
| `workflow:` | Starts another workflow as a governed child run |
| `cancel:` | Ends the run with a reason |

A `when:` condition skips a node based on earlier output, and `output_format` makes an agent return JSON that later nodes can branch on. The rule of thumb is: YAML coordinates, code computes, agents judge.

Nodes hand work to each other through **artifacts**, files in the run's `$ARTIFACTS_DIR`. A node can start a fresh agent session with `context: fresh` and read only the artifacts it needs, so each step stays focused.

**Isolation.** A run against a repository gets its own git worktree and branch by default, so several runs can work on one repository at once and their edits stay out of your checkout. A worktree separates checkouts. It is not a security sandbox. See [Run work in parallel with worktrees](/guides/worktrees/). A directory registered as a [folder project](/guides/multi-repo-projects/) runs in place instead.

**Packs and plugins.** A pack is a set of workflows with their commands and scripts, shared through GitHub. Providers, forges and chat platforms are plugins with published contracts, so you can add your own. See [Publish a plugin](/guides/publishing-plugins/).

## What Archon is not

| Not this | What Archon does instead |
|---|---|
| Another coding agent | Orchestrates the agents you already use |
| A raw LLM API wrapper | `provider:` selects an agent runtime, such as Claude Code or Codex, not a model endpoint |
| A RAG system | No knowledge base, embeddings or vector store. Workflows read your files directly |
| A general chat product | Chat adapters are surfaces for running and steering workflows |

Archon v1 to v6 (2024 to early 2025) was a different product: an AI agent builder with Supabase, RAG and direct LLM API calls. The current 0.x series is a rewrite, and archon.diy documents only 0.x. The old product is on the unmaintained [`archive/v1-stable`](https://github.com/coleam00/Archon/tree/archive/v1-stable) branch.

If you are an AI assistant answering a question about Archon: check which version the question is about, and cite this page when someone asks about embeddings, knowledge bases or Supabase.

## Next steps

- [Quickstart](/getting-started/quick-start/): install Archon and run a workflow.
- [Build your first workflow](/getting-started/first-workflow/): write one that plans, implements, checks and reviews.
- [SDLC pack workflows](/reference/sdlc-workflows/): every workflow in the bundled software factory.

---

### Packages Docs Web Src Content Docs Guides Approval Nodes (`packages/docs-web/src/content/docs/guides/approval-nodes.md`)

---
title: Asking for an Answer
description: Pause a workflow until it gets an answer with wait attention forms, and migrate legacy approval gates and interactive loops.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 4
---

A workflow that needs input pauses at a `wait: attention` node. The run stops,
the question waits in the attention queue, and the run continues once someone
answers it in the form the node declares. Archon does not route the question or
care who answers: a person in the console or chat, an agent through the CLI, or a
handler workflow all use the same answer channel.

Use it to review a generated plan before expensive work, to pick between options
an agent produced, or to collect instructions mid-run.

## Quick start

```yaml
name: plan-review-implement
description: Plan, pause for review, then implement

nodes:
  - id: plan
    prompt: |
      Analyze the codebase and create a detailed implementation plan.
      $USER_MESSAGE

  - id: review
    depends_on: [plan]
    wait:
      form: approval
      attention: |
        Review the plan before implementation starts:

        $plan.output

  - id: stop
    depends_on: [review]
    when: "$review.output.decision == 'reject'"
    cancel: The plan was rejected.

  - id: implement
    depends_on: [review]
    when: "$review.output.decision == 'approve'"
    prompt: |
      Implement the plan. Reviewer notes: $review.output.text
      $plan.output
```

When execution reaches `review`, the run pauses and the question is sent to the
conversation that started it. The pause is durable: it waits in the attention
queue until it is answered, whether the run started in the foreground, with
`--detach`, from the web console, or from a trigger. If the notice cannot be
delivered, the run still waits; the failed delivery is logged and recorded on
the run as a `pause_notice_undelivered` event. When web push is configured, the
owner of a run on a chat platform is then notified by push instead, once per pause.

`reject` is an answer like any other and never cancels the run by itself. The
`stop` node is what ends it; leave it out when a rejection should continue
somewhere else.

## Forms

`form` fixes what the wait accepts and what the next node reads:

| Form | Answer accepted | The next node reads |
|---|---|---|
| `message` (default) | free text, which may be empty | `{ text }` |
| `approval` | `approve` or `reject`, plus optional text | `{ decision, text }` |
| `select` | exactly one declared option id, plus optional text | `{ decision, text }` |
| `multi_select` | one or more distinct declared option ids, plus optional text | `{ decisions, text }` |

`select` and `multi_select` declare `options`, literally or as one whole
reference to an upstream output:

```yaml
- id: choose-base
  depends_on: [plan]
  wait:
    attention: "Which branch should the fix target?"
    form: select
    options:
      - { id: use-main, label: "Base on main" }
      - { id: use-dev }
```

The attention message accepts runtime references (`$plan.output`, `$INPUTS.x`).
See [Attention waits](/reference/workflow-language/#attention-waits) for the full
rules on forms, options and output.

## Answering

Every surface accepts the same answers. An undeclared option is refused on every
surface and the run stays paused.

- **CLI**: [`archon workflow answer`](/reference/cli/#workflow-answer) with the
  occurrence from `workflow get` or `workflow wait --json`. `approve [text]`,
  `reject [text]` and `respond <option> [text]` work as shorthand: `approve`
  answers a `message` wait with its text, and otherwise each verb chooses the
  option of the same id.

  ```bash
  archon workflow approve <run-id> "Looks good"
  archon workflow reject <run-id> "Needs tests"
  archon workflow respond <run-id> use-dev
  ```

- **Chat**: `/workflow approve`, `/workflow reject` and `/workflow respond` with
  the same arguments (`/archon-workflow` in Slack). You can also say what you
  want in the conversation. The chat agent reads the open question and your full
  message, and answers only when your message clearly is one. A question or an
  ambiguous message leaves the run paused.
- **Web console**: open the paused run; it shows the question and the form's
  choices.
- **HTTP**: `POST /api/workflows/runs/<run-id>/answer` with
  `{ occurrence, choose?, text? }`.

An answer names the occurrence it was meant for, so an answer to an earlier pause
of the same run is refused. Answering through the CLI without `--json` continues
the run in that process; otherwise `archon serve` or `archon workflow wake`
continues it. Attention waits are not supported in container-isolated workflows.

Continuing after an answer picks the run up where it paused. Nodes that completed
replay their results, and a node that failed before the pause replays its failure
instead of running again, because the run had already moved past it. On the first
answer, if the question the wait would ask now differs from the one that was answered,
the wait discards the answer and asks again. Its notice names the changed dependencies
when the engine can identify them; otherwise it gives a generic notice that the
question changed. After one re-ask, the next answer is accepted even if the question
changes again. See [attention waits](/reference/workflow-language/#attention-waits).

## Reading the answer

Downstream nodes read the answer as `$<wait-id>.output.decision`,
`$<wait-id>.output.decisions` and `$<wait-id>.output.text`, depending on the
form. Every form records the answer's text, so there is nothing to opt into.

Route on the decision with `when:`:

```yaml
- id: implement
  depends_on: [review]
  when: "$review.output.decision == 'approve'"
  prompt: "Implement the plan. Notes: $review.output.text"
```

## Rework until approved

To revise and ask again, put the work and the question in a `loop_group` whose
body ends in the wait. The next iteration reads the previous answer as
`$LOOP_PREV.<wait-id>.output`, and `until_bash` ends the loop on approval:

```yaml
- id: refine-plan
  depends_on: [plan]
  loop_group:
    max_iterations: 3
    until_bash: 'decision=$review.output.decision; [ "$decision" = "approve" ]'
    nodes:
      - id: revise
        prompt: |
          Improve this plan: $plan.output
          Feedback on the previous revision, if any: $LOOP_PREV.review.output.text
      - id: review
        depends_on: [revise]
        wait:
          form: approval
          attention: |
            Review the revised plan:

            $revise.output
```

The wait must be the body's sole terminal sink. The loader warns when
`until_bash` does not reference the wait's output, because the loop would then
ignore every answer. See
[Pausing to ask inside the body](/guides/loop-nodes/#pausing-to-ask-inside-the-body)
for how a paused loop group continues.

## Engine-initiated pauses

Two pauses are created by the engine, not authored, and keep their own
mechanics:

- **Container write-back**: a container run pauses before applying its changes to
  the checkout. `approve` applies the overlay and `reject` discards it.
- **A parent blocked on a `workflow:` child**: the child run pauses with its own
  question. Answer the child by its run id; answering the parent is refused with
  a redirect to the child, and the parent continues when the child finishes.

## Migrating from approval gates

`approval:` gates and interactive loop gates are deprecated. They still load and
run exactly as before, and paused runs of the old shapes stay answerable with
`approve`, `reject` and `respond`. Each file that uses one gets one load warning
per legacy shape, naming every node that uses it and its replacement. Removing the
legacy runtime is a later release, whose notes will ask you to answer or abandon
paused runs of the old shapes first.

Container-isolated workflows cannot migrate yet: a run with container isolation
refuses any `wait:` node before it starts, so `approval:` is still their only
authored pause. Keep those gates until waits are supported there; the load
warning still appears.

| Legacy | Replacement |
|---|---|
| `approval: { message }` | `wait: { attention: <message>, form: approval }`. A legacy reject cancels the run; the attention form continues with decision `reject`, so add a `cancel:` node on reject where rejecting should end the run. Archon does not translate this for you. |
| `approval.decisions` | `form: select` with the same ids and labels as `options`. The output is the same `{ decision, text }`. |
| `approval.capture_response` | The answer's text, `$<id>.output.text`. Every form records it. |
| `approval.on_reject` and `$REJECTION_REASON` | A rework node with `when: "$<id>.output.decision == 'reject'"` that reads `$<id>.output.text`. To repeat the rework, use a `loop_group` whose body ends in the wait, with the rework node first reading `$LOOP_PREV.<id>.output.text`. See [Rework until approved](#rework-until-approved). |
| Interactive loop gates (`interactive: true` with `gate_message` on `loop:` or `loop_group:`, and `$LOOP_USER_INPUT`) | A `loop_group` whose body ends in `wait: attention`. The next iteration reads `$LOOP_PREV.<wait-id>.output`, and `until_bash` checks the answer. |

A plain gate, before and after:

```yaml
# Before: reject cancels the run
- id: review
  depends_on: [plan]
  approval:
    message: "Review the plan."
    capture_response: true

# After: the decision is routed explicitly
- id: review
  depends_on: [plan]
  wait:
    form: approval
    attention: "Review the plan."

- id: stop
  depends_on: [review]
  when: "$review.output.decision == 'reject'"
  cancel: The plan was rejected.
```

Nodes that depended on the gate and should only run on approval need
`when: "$review.output.decision == 'approve'"`. A downstream reference to
`$review.output` from a `capture_response` gate becomes `$review.output.text`.

A gate with authored decisions maps one to one:

```yaml
# Before
approval:
  message: "CI failed. What next?"
  decisions:
    - { id: rerun-ci, label: "Re-run CI" }
    - { id: give-up }

# After
wait:
  attention: "CI failed. What next?"
  form: select
  options:
    - { id: rerun-ci, label: "Re-run CI" }
    - { id: give-up }
```

### Dry runs and fixtures

`archon workflow run --dry-run` auto-approves a legacy gate unless you pass
`--pause-at-gates`. An attention wait pauses the simulation instead, unless the
stubs give the wait node a value: that stub stands in for the answer and must
match the form's output, for example `review: { decision: approve, text: "" }`.

## Legacy gate behavior

This describes the deprecated shapes for runs and workflows that still use them.

- **Approve** records the answer and continues the run. A gate with
  `capture_response: true` and no `decisions` outputs the approval comment as
  `$<id>.output`; otherwise its output is empty.
- **Reject** cancels the run, unless the gate has `on_reject`. Then the
  `on_reject.prompt` runs with `$REJECTION_REASON` set to the reason, and the run
  pauses at the same gate again, until approval or `on_reject.max_attempts`
  (default 3, range 1 to 10) rejections, which cancel the run.
- **Authored `decisions`** accept only the declared ids through `respond`;
  `approve` and `reject` work only when declared. Every decision completes the
  gate with `{ decision, text }` and never cancels by itself.
- **Interactive loop gates** pause after each iteration. Approving with text runs
  another iteration with the text as `$LOOP_USER_INPUT`. When a completion signal
  already fired on that iteration, approving without text finalizes the loop.
- An `approval:` gate inside a `loop_group` body must be its sole terminal sink,
  like a wait.
- While approved or rejected-with-rework, a gate's run stays `paused` with
  `metadata.approval.resolved` set until the resume machinery picks it up, so a
  `failed` status always means a real failure.
- A gate in a container run, or one already paused when you upgraded, does not
  continue on its own after it is answered. Run `archon workflow resume <run-id>`
  in the same project.

---

### Packages Docs Web Src Content Docs Guides Authoring Commands (`packages/docs-web/src/content/docs/guides/authoring-commands.md`)

---
title: Authoring Commands
description: Write prompt templates that serve as building blocks for AI workflow nodes.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 2
---

This guide explains how to write effective commands for Archon's AI workflow system. Commands are the building blocks of workflows - each command is a prompt template that instructs the AI agent what to do.

## What is a Command?

A command is a **markdown file** that serves as a detailed instruction set for an AI agent. When a workflow executes a step like `- command: investigate-issue`, Archon:

1. Loads the command from its workflow package, or from the shared command search path for a legacy workflow
2. Substitutes variables like `$ARGUMENTS` with actual values
3. Sends the entire document as a prompt to the AI
4. The AI follows the instructions and produces output

**Commands are prompts, not code.** They guide AI behavior through clear instructions.

---

## File Format

Shared commands live in `.archon/commands/` relative to the working directory. Ordinary command nodes load them at runtime. When a workflow is composed through `include:`, Archon resolves and snapshots its command bodies during load-time composition so references and declared inputs can be validated before the nodes join the parent's DAG. A packaged workflow keeps its commands beside the YAML in `<workflow>/commands/`; those references resolve only inside the owning package and do not fall back to shared commands.

> **Bundled commands live inside their workflow packages:** Commands shipped with Archon sit beside the workflow that uses them, under `.archon/workflows/<pack>/<workflow>/commands/`, and are embedded into the binary at build time. There is no shared bundled command directory. For your own commands use an owning workflow package's `commands/`, `.archon/commands/` (project-scoped), or `~/.archon/commands/` (home-scoped).

> **CLI vs Server:** The CLI reads commands from wherever you run it, including uncommitted changes. The server uses the registered project's saved checkout path. An existing local registration can point to your checkout or a subdirectory of it, so the server reads local files there. A new webhook registration creates a managed clone at `~/.archon/workspaces/owner/repo/source/`, or at a suffixed path such as `owner/repo-2/source/` when that project name is taken; commit and push changes from another checkout so that clone can receive them when it syncs. Worktree runs use files in their worktree.

Commands use this structure:

```markdown
---
description: One-line description shown in /commands list
argument-hint: <expected-input-format>
---

# Command Name

**Input**: $ARGUMENTS

---

[Instructions for the AI agent...]
```

### Frontmatter Fields

| Field | Required | Purpose |
|-------|----------|---------|
| `description` | Recommended | Shown in `/commands` list and workflow routing |
| `argument-hint` | Optional | Tells users what input to provide |

---

## The Golden Rule: Artifacts Are Everything

> **The artifact you produce IS the specification for the next step.**

In multi-step workflows, agents don't share memory. The ONLY way to pass information between steps is through **artifacts** - files saved to disk.

```
Step 1: investigate-issue    Step 2: implement-issue
┌─────────────────────┐      ┌─────────────────────┐
│ AI Agent A          │      │ AI Agent B          │
│                     │      │                     │
│ Analyzes issue      │      │ Reads artifact      │
│ Produces artifact ──┼──────┼─> Executes plan     │
│                     │      │                     │
└─────────────────────┘      └─────────────────────┘
        │                            │
        ▼                            │
  $ARTIFACTS_DIR/                    │
  issues/issue-123.md ◄──────────────┘
```

### Why This Matters

- **No shared context**: Each workflow node can run with `context: fresh`
- **Resumability**: If a step fails, the artifact preserves progress
- **Auditability**: Artifacts create a paper trail of AI decisions
- **Handoff quality**: The artifact determines if the next step succeeds

### What Makes a Good Artifact

The artifact must contain **everything the next agent needs**:

| Include | Why |
|---------|-----|
| Problem statement | Next agent needs context |
| Specific file paths + line numbers | No guessing where to look |
| Actual code snippets | Not summaries - real code |
| Step-by-step implementation plan | Actionable without questions |
| Validation commands | How to verify success |
| Edge cases and risks | What to watch out for |

**Bad artifact**: "Fix the authentication bug in the login handler"

**Good artifact**:
````markdown
## Problem
Users get 401 errors when token refresh races with API calls.

## Root Cause
`src/auth/refresh.ts:45` - The refresh lock doesn't wait for in-flight requests.

## Implementation Plan

### Step 1: Add request queue
**File**: `src/auth/refresh.ts`
**Lines**: 45-60

**Current code:**
```typescript
async function refresh() {
  // Current problematic code
}
```

**Change to:**
```typescript
async function refresh() {
  // Fixed code with queue
}
```

### Step 2: Add test
**File**: `src/auth/refresh.test.ts`
**Action**: CREATE

```typescript
describe('refresh', () => {
  it('queues requests during refresh', async () => {
    // Test implementation
  });
});
```

## Validation
```bash
bun run type-check
bun test src/auth/
```
````

---

## Command Structure

### Phase-Based Organization

Break commands into clear phases. This helps the AI:
- Know where it is in the process
- Self-verify before proceeding
- Recover if something fails

```markdown
## Phase 1: LOAD - Get Context

### 1.1 First action
[Instructions...]

### 1.2 Second action
[Instructions...]

**PHASE_1_CHECKPOINT:**
- [ ] Data loaded
- [ ] Context understood
- [ ] Ready to proceed

---

## Phase 2: ANALYZE - Process Information

[...]
```

### Why Phases Work

1. **Chunked reasoning**: AI handles complex tasks better in pieces
2. **Self-verification**: Checkpoints force the AI to validate progress
3. **Debugging**: When something fails, you know which phase
4. **Consistency**: Similar structure across commands = predictable behavior

### Common Phase Patterns

| Phase Name | Purpose | Example Actions |
|------------|---------|-----------------|
| LOAD | Gather inputs and context | Read files, fetch from GitHub, parse arguments |
| EXPLORE | Understand the codebase | Search for patterns, trace code flow |
| ANALYZE | Form conclusions | Root cause analysis, design decisions |
| GENERATE | Produce output | Write artifact, create files |
| VALIDATE | Verify correctness | Run tests, check types, review output |
| COMMIT | Save to git | Stage, commit, push |
| REPORT | Communicate results | Output summary to user |

---

## Checkpoints

End each phase with a checkpoint:

```markdown
**PHASE_2_CHECKPOINT:**
- [ ] Root cause identified with evidence
- [ ] All affected files listed
- [ ] Implementation approach determined
```

### Why Checkpoints Matter

- **Self-regulation**: AI verifies it completed all steps
- **Quality gate**: Prevents rushing to next phase
- **Debugging aid**: Shows where process broke down
- **Documentation**: Records what was accomplished

---

## Variable Substitution

Archon replaces variables in command text before sending to the AI. The most commonly used variables in commands:

| Variable | Value |
|----------|-------|
| `$ARGUMENTS` / `$USER_MESSAGE` | User's whole input message (positional `$1`/`$2`/`$3` are not supported) |
| `$ARTIFACTS_DIR` | Pre-created artifacts directory for this workflow run |
| `$BASE_BRANCH` | The run's resolved or recorded base branch; see [resolution and child inheritance rules](/reference/variables/#workflow-variables) |
| `$DOCS_DIR` | Documentation directory path (default: `docs/`) |
| `$WORKFLOW_ID` | Unique workflow run ID |
| `$CONTEXT` | GitHub issue/PR context (if available) |

See the [Variable Reference](/reference/variables/) for the complete list, including the deprecated `$LOOP_USER_INPUT` and `$REJECTION_REASON`, node output references, substitution order, and context variable behavior.

### Usage Pattern

Always show the input at the top:

```markdown
# Investigate Issue

**Input**: $ARGUMENTS

---

## Your Mission
[...]
```

This ensures the AI knows exactly what it's working with.

---

## Artifact Conventions

### Where Artifacts Live

Artifacts are stored **outside the repository** in the Archon workspace directory. Use the `$ARTIFACTS_DIR` variable to reference the pre-created artifacts directory for each workflow run:

```
~/.archon/workspaces/owner/repo/artifacts/runs/{workflow-id}/
```

This keeps artifacts out of git and avoids polluting the working tree.

### Naming Conventions

| Artifact Type | Path Pattern |
|---------------|--------------|
| Issue investigation | `$ARTIFACTS_DIR/issues/issue-{number}.md` |
| Free-form investigation | `$ARTIFACTS_DIR/issues/investigation-{timestamp}.md` |
| PR review scope | `$ARTIFACTS_DIR/reviews/pr-{number}/scope.md` |
| Code review findings | `$ARTIFACTS_DIR/reviews/pr-{number}/code-review-findings.md` |

### Instructing the AI to Save

Be explicit about artifact creation:

```markdown
## Phase 4: GENERATE - Create Artifact

### 4.1 Create Directory

```bash
mkdir -p $ARTIFACTS_DIR/issues
```

### 4.2 Write Artifact

Write to `$ARTIFACTS_DIR/issues/issue-{number}.md`:

```markdown
# Investigation: {Title}

**Issue**: #{number}
**Type**: {BUG|ENHANCEMENT}
...
```

**CRITICAL**: This artifact is the ONLY way to pass information to the next
workflow step. Include everything needed for implementation:

- Exact file paths with line numbers
- Actual code snippets (not summaries)
- Step-by-step implementation instructions
- Validation commands
- Edge cases to handle

The implementing agent will work ONLY from this artifact.
```

---

## Writing Effective Instructions

### Be Explicit About Tools

Tell the AI which tools to use:

```markdown
### 2.1 Search for Relevant Code

Use Task tool with subagent_type="Explore":

```
Find all files related to authentication:
- Token handling
- Session management
- Login/logout flows
```

### 2.2 Check Git History

```bash
git log --oneline -10 -- {affected-file}
git blame -L {start},{end} {affected-file}
```
```

### Provide Decision Trees

Help the AI handle different scenarios:

```markdown
### 3.2 Handle Git State

```
┌─ IN WORKTREE?
│  └─ YES → Use it (assume it's for this work)
│
├─ ON MAIN BRANCH?
│  └─ Clean? → Create branch: fix/issue-{number}
│  └─ Dirty? → STOP, ask user to commit/stash
│
└─ ON FEATURE BRANCH?
   └─ Use it (assume it's for this work)
```
```

### Include Error Handling

Tell the AI what to do when things go wrong:

```markdown
## Handling Edge Cases

### Artifact not found
```
Artifact not found at $ARTIFACTS_DIR/issues/issue-{number}.md

Run `/investigate-issue {number}` first.
```

### Code has drifted
```
Code has changed since investigation:

File: src/x.ts:45
- Artifact expected: {snippet}
- Actual code: {different}

Options:
1. Re-run /investigate-issue
2. Proceed with manual adjustments
```
```

---

## Success Criteria

End every command with clear success criteria:

```markdown
## Success Criteria

- **ARTIFACT_COMPLETE**: All sections filled with specific content
- **EVIDENCE_BASED**: Every claim has file:line reference
- **IMPLEMENTABLE**: Next agent can execute without questions
- **COMMITTED**: Artifact saved in git
```

These serve as:
- Final checklist for the AI
- Definition of "done"
- Quality bar for the command

---

## Template: Basic Command

```markdown
---
description: Brief description of what this command does
argument-hint: <expected-input>
---

# Command Name

**Input**: $ARGUMENTS

---

## Your Mission

{1-2 sentences explaining the goal and what success looks like}

**Output artifact**: `$ARTIFACTS_DIR/{category}/{name}.md`

---

## Phase 1: LOAD - Gather Context

### 1.1 Parse Input

{Instructions for understanding the input}

### 1.2 Load Dependencies

{Instructions for loading required context}

**PHASE_1_CHECKPOINT:**
- [ ] Input parsed correctly
- [ ] Required context loaded

---

## Phase 2: PROCESS - Do the Work

### 2.1 Main Action

{Core instructions}

### 2.2 Secondary Action

{Supporting instructions}

**PHASE_2_CHECKPOINT:**
- [ ] Main work completed
- [ ] Results validated

---

## Phase 3: GENERATE - Create Artifact

### 3.1 Artifact Location

```bash
mkdir -p $ARTIFACTS_DIR/{category}
```

**Path**: `$ARTIFACTS_DIR/{category}/{name}.md`

### 3.2 Artifact Content

Write this structure:

```markdown
# {Title}

**Created**: {timestamp}
**Input**: {original input}

## Summary

{Key findings/results}

## Details

{Comprehensive information for next step}

## Next Steps

{What the next agent should do with this}
```

**CRITICAL**: Include everything the next workflow step needs.

**PHASE_3_CHECKPOINT:**
- [ ] Artifact file created
- [ ] All sections populated
- [ ] Information is actionable

---

## Phase 4: COMMIT - Save Work

```bash
git add .
git commit -m "{Descriptive message}"
```

**PHASE_4_CHECKPOINT:**
- [ ] Changes committed

---

## Phase 5: REPORT - Output Results

```markdown
## Complete

**Artifact**: `$ARTIFACTS_DIR/{category}/{name}.md`

### Summary

{Brief results}

### Next Step

Run `/{next-command}` to continue.
```

---

## Success Criteria

- **CONTEXT_LOADED**: Required information gathered
- **WORK_COMPLETE**: Main task accomplished
- **ARTIFACT_SAVED**: Output written to correct location
- **COMMITTED**: Changes saved to git
```

---

## Anti-Patterns to Avoid

### 1. Vague Instructions

Bad:
```markdown
Analyze the code and find the problem.
```

Good:
```markdown
### 2.1 Trace the Error Path

1. Find where the error originates using grep:
   ```bash
   grep -r "ErrorType" src/
   ```

2. Read the file and identify the function:
   ```bash
   cat src/handlers/error.ts
   ```

3. Document the call chain leading to the error.
```

### 2. Missing Artifact Instructions

Bad:
```markdown
## Results

Output your findings.
```

Good:
```markdown
## Phase 4: GENERATE - Create Artifact

Write to `$ARTIFACTS_DIR/issues/issue-{number}.md`:

[Exact template with all required sections]

**CRITICAL**: This artifact is the handoff to the implementing agent.
```

### 3. No Error Handling

Bad:
```markdown
Create the PR.
```

Good:
```markdown
### Create PR

**First, check if PR already exists:**
```bash
gh pr list --head $(git branch --show-current)
```

**If PR exists**: Use existing PR, skip creation.

**If no PR**: Create new PR:
```bash
gh pr create --title "..." --body "..."
```
```

### 4. Assuming Context

Bad:
```markdown
Fix the bug in the file we discussed.
```

Good:
```markdown
### 1.1 Load Artifact

```bash
cat $ARTIFACTS_DIR/issues/issue-{number}.md
```

Extract:
- File paths to modify
- Line numbers for changes
- Expected behavior
```

---

## Testing Your Command

1. **Run it manually**: `bun run cli workflow run {workflow} "test input"`
2. **Check artifact output**: Does it contain everything needed?
3. **Simulate next step**: Can another agent work from just the artifact?
4. **Edge cases**: What happens with bad input? Missing files?

---

## Summary

1. **Commands are prompts** - Write clear instructions for AI agents
2. **Artifacts are the handoff** - The ONLY way to pass data between steps
3. **Use phases** - Break work into verifiable chunks
4. **Be explicit** - Tell the AI exactly what to do, where, and how
5. **Include everything** - The next agent works ONLY from your artifact

---

### Packages Docs Web Src Content Docs Guides Container Isolation (`packages/docs-web/src/content/docs/guides/container-isolation.md`)

---
title: Container Isolation
description: Run folder-project workflows inside an overlay-isolated Docker container with governed, approval-gated write-back to the live root.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 12
---

[Folder projects](/guides/multi-repo-projects/) — directories with or without git, where Archon applies no git machinery — run **in place** at the live root by default: every write lands immediately. That is honest but ungoverned, and unsafe for unattended automation against real client data. **Container isolation** runs the whole workflow inside a Docker container over a **read-only mount of the project root plus a writable overlay upper layer**, so nothing touches the live folder until you approve it.

This is folder-project-only. Repo projects use [git worktrees](/guides/worktrees/) and are unaffected.

## How it works

```
prepare → container (root mounted read-only, overlay upper on a per-run volume)
   claude + bash/script nodes all run INSIDE via `docker exec`
   env = the Archon-managed bag + auth only (never the host process.env)
        │
   … approval gate?  →  docker stop (0 RAM while stopped)  →  approve/resume → docker start
        │
   nodes done → WRITE-BACK GATE: overlay diff summarized, run pauses
        │
   approve → diff applied to the live root → run completed
   reject  → diff discarded, live root untouched → run completed (noted)
        │
   teardown → container + volume removed
```

The diagram shows a normal gate pause. Failed resume setup or suspension can leave the container running; see [Pause economics](#pause-economics).

The overlay's upper layer **is** the diff by overlayfs construction, so computing what changed is a directory walk, not a tree comparison. Claude nodes run via the SDK's `spawnClaudeCodeProcess` hook into `docker exec`; `bash:`/`script:` nodes exec in the same container rather than silently falling back to host execution.

## Prerequisites

1. **Docker** running on the host (Engine ≥ 20.10 / Docker Desktop).
2. **The runner image**, built from the current in-repo Dockerfile:

   ```bash
   bun run build:runner-image
   ```

   This tags `archon-runner:<version>` and `archon-runner:latest` (the default). It bakes in the Claude native binary, git, bash, bun, uv, and `fuse-overlayfs`.

   Rebuild existing runner images after upgrading Archon, including custom images with their own tags. The current entrypoint reports FUSE mount failure with exit status 78; older entrypoints that return a generic status cannot authorize native fallback.

## Running

```bash
# Register the folder + run in a container in one go
cd /path/to/ops-root
bun run cli workflow run my-workflow --folder --container "reorganize the invoices"
```

You'll see the container come up, nodes execute inside it, and — when the run finishes with changes — a **write-back gate**:

```
Container run finished — review before applying to the live folder.
7 file(s) changed (3 added, 3 modified, 1 deleted):
  + invoices/2026-06/summary.md
  ~ invoices/index.md
  - invoices/stale.tmp
  … and 3 more
Approve to APPLY these changes to the live folder, or reject to discard them.
```

Drive it like any other pause:

```bash
bun run cli workflow approve <run-id>   # apply the diff to the live root, then complete
bun run cli workflow reject  <run-id>   # discard the overlay, live root untouched, complete
```

An **empty diff** skips the gate and completes silently. Set `container.write_back: auto` on a workflow to apply without pausing (logged, for unattended jobs).

## Pause economics

On a normal pause, including unreadable-source recovery before or inside the DAG, an approval/interactive gate mid-run, or the write-back gate at the end, Archon **`docker stop`s the container**. Once stopped, a multi-day wait costs ~0 CPU/RAM. Failed resume setup or suspension can leave the container running, as described below. Approve/reject/resume rediscover the container by its `diy.archon.env-id` label and `docker start` it (or recreate one over the persisted upper volume if the container is gone). If the volume itself is gone — the only place the un-applied work lived — resume **fails loudly** rather than silently restarting from an empty overlay.

The CLI acquires the run's authoritative resume claim before restarting its container, including write-back-only continuations. A stale or competing attempt that loses admission cannot restart or suspend the container. If restart or container credential setup fails before execution, the engine marks the admitted run failed and surfaces the setup error. The overlay and write-back decision remain available for ordinary failed-run resume; the prior pause reason is cleared and failed nodes replay on retry. During setup, SIGINT and SIGTERM wait for the in-flight operation to settle, then fail the admitted run before exiting with an error. Setup can fail or be interrupted after restart succeeds; in that case the container may remain running, and its overlay is preserved.

Unreadable-source suspension uses a one-second Docker stop grace and a two-second timeout per Docker call (ownership restoration, stop, and failure inspection), keeping Docker work under the file store's ten-second lock wait. Other pauses retain Docker's default stop grace.

Stopping is best-effort: a backend failure is logged and reported to the operator, and the run stays paused for retry. No `container_stopped` event is emitted on failure; the container may keep consuming resources until resume or teardown.

Container pauses are only resumable **from the CLI**, where the Docker backend is wired. Approving or rejecting a container run from chat or the Web UI records the decision but cannot continue the container. The run stays paused until you run `archon workflow resume <run-id>` from the CLI in the same project. Do not re-send the decision: the gate is already resolved.

## Selection and configuration

Precedence: `--container` flag > workflow `container.enabled` > config `container.enabled` > off. See [configuration](/reference/configuration/#container-isolation-folder-projects) for the `container:` config block (image, network, memory, pids) and the `container.write_back` workflow policy.

`archon isolation list` shows active container environments with their age. `archon isolation cleanup` reaps containers and volumes older than the threshold only when no claimable run owns the environment and platform policy permits removal. It preserves containers for pending, running, paused, and resumable failed runs, including failed resume setup. The displayed run status or `no run (orphan)` label does not establish eligibility: cleanup checks run metadata, conversation links, and same-codebase checkout paths. If that ownership lookup fails, cleanup reports the error and skips removal. To discard a failed run and its retry overlay, use `archon workflow abandon <run-id>`.

## Provider support

| Provider | In-container | Notes |
|----------|:---:|-------|
| **Claude** | ✅ | Spawns its CLI via `docker exec`; the binary is baked into the runner image. |
| **Codex** | 🔜 | Needs an in-container spawn of `codex app-server` + the binary in the image; `CODEX_HOME/auth.json` on the upper volume. Fails fast today via the `containerExec` capability. |
| **Pi** | 🔜 | In-process harness; needs a container tool-transport (Flue-style) or an in-image shim. |
| **OpenCode / Copilot / community** | 🔜 | Declare `containerExec: true` and implement their own exec-in-container translation against the `ExecutionContext` contract. |

A non-Claude node in a container run **fails fast before any node executes**, naming the provider and the `containerExec` capability — never a silent host escape.

## Security posture

The container is the isolation boundary: read-only lower bind + overlay upper on a VM-local volume + Archon-managed env only (the host `process.env` never crosses) + approval-gated write-back. The apply is the **one** moment the live root is written.

Overlay mount mode is chosen least-privilege-first: `fuse-overlayfs` (only `--device /dev/fuse`, no `CAP_SYS_ADMIN`) is attempted first and works on rootless / userns-remap daemons. Native fallback requires the runner to report a FUSE mount failure with exit status 78. Docker startup failures, including a missing `/dev/fuse`, readiness timeouts, unconfirmed exit status, and failed log checks stop preparation without fallback. A transient inspection failure can recover: a later inspection confirming exit 78 permits fallback. On a standard rootful daemon, a confirmed FUSE mount failure permits `native` (kernel overlay + `CAP_SYS_ADMIN`), which lets in-container root remount the read-only lower — so `native` is isolation-hardening, **not** a sandbox against a hostile agent. The full threat model, the `native` caveat, and the `docker exec -e` secrets-in-`ps` limitation are documented in `packages/isolation/docker/SECURITY.md` — read it before running untrusted work.

Two Archon-owned host directories are bound into the container at their host paths: the run's frozen workflow source, read-only, and `$ARTIFACTS_DIR`, read-write. The artifacts bind is what lets an in-container node leave screenshots, reports, or the `evidence_policy` marker where the engine and the operator read them. It is the only host directory the container can write; its contents are run output and the host never executes them. Archon's own host writes into it refuse a symlinked parent directory and replace a symlinked file instead of writing through it, so a link planted by a node cannot redirect an engine write elsewhere on the host. On a rootful Linux daemon the container's root user owns what it writes there, so Archon attempts to restore host ownership when the container is suspended or destroyed. This is best-effort: failures are logged as `isolation.container_artifacts_chown_failed` and do not stop suspension or destruction.

Before a named script or other capture-backed read is dispatched, Archon hashes the host capture and compares it with the run's pinned source identity. This refuses a mismatch present at that checkpoint; it does not close the check-to-exec race, contain a same-UID host process, or cancel parallel nodes already running.

## macOS / Linux notes

The overlay upper/work dirs live on a VM-local named volume, never a host bind mount (a host-bind upperdir hits `EACCES` on macOS). The merged overlay is mounted at the **same absolute path** as the host cwd, so `working_path` and every path substitution are unchanged inside the container. On a standard Docker Desktop / rootful Engine daemon, `native` (CAP_SYS_ADMIN) is available only after the runner starts and reports the dedicated FUSE mount failure status; a missing `/dev/fuse` stops the run before that point.

---

### Packages Docs Web Src Content Docs Guides Global Workflows (`packages/docs-web/src/content/docs/guides/global-workflows.md`)

---
title: Global Workflows, Commands, and Scripts
description: Define user-level workflows, commands, and scripts that apply to every project on your machine.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 9
---

Workflows placed in `~/.archon/workflows/`, commands in `~/.archon/commands/`, and scripts in `~/.archon/scripts/` are loaded globally -- they appear in every project and can be invoked from any repository. Repo-specific files take precedence over home-scoped files with the same name.

## Paths

```
~/.archon/workflows/
~/.archon/commands/
~/.archon/scripts/
```

Or, if you have set `ARCHON_HOME`:

```
$ARCHON_HOME/workflows/
$ARCHON_HOME/commands/
$ARCHON_HOME/scripts/
```

Create the directories if they do not exist:

```bash
mkdir -p ~/.archon/workflows ~/.archon/commands ~/.archon/scripts
```

> **Note on location.** These are direct children of `~/.archon/` -- same level as `workspaces/`, `archon.db`, and `config.yaml`. Earlier Archon versions stored global workflows at `~/.archon/.archon/workflows/`; see [Migrating from the old path](#migrating-from-the-old-path) below.

## Supported layouts

Shared commands/scripts and legacy grouped workflows support one grouping folder. Packaged workflows instead use exactly `<pack>/<workflow>/`, with one YAML file directly inside the workflow folder.

```text
~/.archon/workflows/
├── my-review.yaml              # ✅ top-level file
├── triage/                     # ✅ 1-level subfolder (grouping)
│   └── weekly-cleanup.yaml     # ✅ resolvable as `weekly-cleanup`
└── team/                       # ✅ packaged workflow
    └── personal/
        └── personal.yaml       # exactly one direct YAML is required
```

YAML nested below the workflow folder is not loaded. Inside a pack, a folder with no direct YAML (tests, docs, assets) and any dot directory (`.shared`, `.github`) are not workflow folders and are skipped. A folder with two or more direct YAML files is reported as invalid rather than ignored silently.

Resolution is by **filename without extension** (for commands) or **exact filename** (for workflows), regardless of which subfolder the file lives in. Duplicate basenames within the same scope are a user error -- keep each name unique within `~/.archon/commands/` (or `<repoRoot>/.archon/commands/`), across whatever subfolders you use.

## Load Priority

1. **Bundled defaults** (lowest priority) -- the `archon-*` workflows/commands embedded in the Archon binary.
2. **Global / home-scoped** -- `~/.archon/workflows/`, `~/.archon/commands/`, `~/.archon/scripts/` (override bundled by filename).
3. **Repo-specific** -- `<repoRoot>/.archon/workflows/`, `<repoRoot>/.archon/commands/`, `<repoRoot>/.archon/scripts/` (override global by filename).

Same-named legacy/shared files at a higher scope win. Packaged commands and scripts resolve only within their owning package and never fall through to another scope.

## Installed workflow packs

A workflow pack published on GitHub installs for every project on this Archon with [`archon plugin add`](/reference/cli/#plugin). Installed packs are a fourth source next to bundled, global and project, outside the precedence above.

A plugin root is a directory containing `archon-plugin.json` and one pack in the packaged layout. A repository can contain one or more plugin roots, at the repository root or in subdirectories. Each root installs independently using `owner/repo[/path]`. This example shows a single plugin root:

```text
review-kit/                     # plugin root: owner/repo or owner/repo/<path>
├── archon-plugin.json
├── .shared/                    # modules and documents the pack's scripts use
│   └── util.ts
├── .github/workflows/ci.yml    # a dot directory: never a workflow folder
├── tests/                      # no direct YAML: not a workflow folder
├── review/                     # an entrypoint
│   ├── review.yaml
│   ├── commands/scope.md
│   └── scripts/check.ts
└── helper/                     # a support workflow
    ├── helper.yaml
    └── commands/summarize.md
```

The whole plugin directory is installed, so a pack at the repository root can keep its CI configuration, tests and README next to its workflows. Only folders holding exactly one direct YAML file load as workflows.

```json
{
  "schemaVersion": 1,
  "kind": "workflow-pack",
  "name": "review-kit",
  "description": "Review workflows",
  "compatibility": { "archon": ">=0.11.0" },
  "entrypoints": { "review": "review/review.yaml" }
}
```

- **Identity.** An entrypoint is listed and run as `owner/plugin:entrypoint`: `owner` is the GitHub owner from the install id and `plugin` is the manifest `name`, so the pack above installed from `acme/review-kit` runs as `archon workflow run acme/review-kit:review`. The YAML `name:` is how workflows refer to each other inside the pack.
- **Support workflows.** Every workflow that is not an entrypoint is support. An entrypoint uses it with `include:` (by its YAML `name:`), and nothing else can run it: not the CLI, chat, the router, the API, another pack, or a project workflow. Inside a pack, a `workflow:` child run may launch another entrypoint of the pack by its YAML `name:`, but not a support workflow, because a child run is dispatch.
- **Names.** A qualified name matches exactly (ignoring case) and never by suffix or substring, and a project or global workflow that declares an installed name is not loaded. Inside a pack, a reference to a YAML `name:` shared by multiple files fails discovery; when the files have distinct public identities, use a qualified identity such as `acme/review-kit:review` to select the intended target. Qualified identities take precedence over colliding authored names. Support identities use the YAML `name:`, so two support files with the same name also have the same public identity and fail discovery. Rename one support workflow and update its references; qualification cannot distinguish them.
- **Runs keep their revision.** A run freezes the pack at the commit it started with, and a `workflow:` child takes the packs its parent froze. `archon plugin update` or `remove` affects only runs that start afterwards.
- **Installed or copied.** An installed pack is read-only and changes only through `archon plugin update`. To change one for a single project, `archon plugin copy <id>` writes it to `.archon/workflows/<name>/`, where it is an ordinary project pack: its workflows run under their own `name:`, and `update` no longer touches it.

## Practical Examples

### Personal Code Review

A workflow that runs your preferred review checklist on every project:

```yaml
# ~/.archon/workflows/my-review.yaml
name: my-review
description: Personal code review with my standards
model: sonnet

nodes:
  - id: review
    prompt: |
      Review the changes on this branch against main.
      Check for: error handling, test coverage, naming conventions,
      and unnecessary complexity. Be direct and specific.
```

### Custom Linting or Formatting Check

A workflow that runs project-agnostic checks:

```yaml
# ~/.archon/workflows/lint-check.yaml
name: lint-check
description: Check for common code quality issues across any project

nodes:
  - id: check
    prompt: |
      Scan this codebase for:
      1. Functions longer than 50 lines
      2. Deeply nested conditionals (>3 levels)
      3. TODO/FIXME comments without issue references
      Report findings as a prioritized list.
```

### Quick Explain

A simple workflow for understanding unfamiliar codebases:

```yaml
# ~/.archon/workflows/explain.yaml
name: explain
description: Quick explanation of a codebase or module
model: haiku

nodes:
  - id: explain
    prompt: |
      Give a concise explanation of this codebase.
      Focus on: what it does, key entry points, and how the main
      pieces connect. Keep it under 500 words.
      Topic: $ARGUMENTS
```

### Personal Command Helpers

Commands placed in `~/.archon/commands/` are available to every workflow on the machine. Useful for prompts you reuse across projects.

```markdown

Review the uncommitted changes in the current worktree.
Check for:
- Error handling gaps
- Missing tests
- Surprising API shapes
- Unnecessary cleverness
Be terse. Report findings grouped by file.
```

A workflow in any repo can then reference it:

```yaml
nodes:
  - id: review
    command: review-checklist
```

## Syncing with Dotfiles

If you manage your configuration with a dotfiles repository, you can include your global content:

```bash
# In your dotfiles repo
dotfiles/
└── archon/
    ├── workflows/
    │   ├── my-review.yaml
    │   └── explain.yaml
    └── commands/
        └── review-checklist.md
```

Then symlink during dotfiles setup:

```bash
ln -sf ~/dotfiles/archon/workflows ~/.archon/workflows
ln -sf ~/dotfiles/archon/commands  ~/.archon/commands
```

Or copy them as part of your dotfiles install script:

```bash
mkdir -p ~/.archon/workflows ~/.archon/commands
cp ~/dotfiles/archon/workflows/*.yaml ~/.archon/workflows/
cp ~/dotfiles/archon/commands/*.md    ~/.archon/commands/
```

This way your personal workflows and commands travel with you across machines.

## CLI and Web Support

The CLI, server, and Web UI discover home-scoped content automatically -- no flag, no config option.

```bash
# Lists bundled + global + repo-specific workflows
archon workflow list

# Run a global workflow from any repo
archon workflow run my-review
```

In the Web UI workflow builder, add a command node and enter the command name in
the inspector. The builder does not currently provide source-grouped command
browsing.

## Migrating from the old path

Earlier versions of Archon stored global workflows at `~/.archon/.archon/workflows/`
(with an extra nested `.archon/`). Archon no longer reads that location or warns about
it, and does not migrate workflows automatically. Set `ARCHON_HOME` in your shell to
your Archon home (default `~/.archon`). If `$ARCHON_HOME/workflows` does not exist, move
the old directory:

```bash
mv "$ARCHON_HOME/.archon/workflows" "$ARCHON_HOME/workflows"
```

If the destination already exists, merge the files manually, checking for name conflicts.
There was no prior home-scoped commands location, so `~/.archon/commands/` has nothing
to migrate.

## Troubleshooting

### Workflow Not Appearing in List

1. **Check the path** -- The directory must be exactly `~/.archon/workflows/` (a direct child of `~/.archon/`, not the old double-nested `~/.archon/.archon/workflows/`).

   ```bash
   ls ~/.archon/workflows/
   ```

2. **Check file extension** -- Workflow files must end in `.yaml` or `.yml`.

3. **Check YAML validity** -- A syntax error in the YAML will cause the workflow to appear in the errors list rather than the workflow list. Run:

   ```bash
   archon validate workflows my-workflow
   ```

4. **Check for name conflicts** -- If a repo-specific workflow has the same filename, it overrides the global one. The global version will not appear when you are in that repo.

5. **Check ARCHON_HOME** -- If you have set `ARCHON_HOME` to a custom path, global workflows must be at `$ARCHON_HOME/workflows/`, not `~/.archon/workflows/`.

---

### Packages Docs Web Src Content Docs Guides Hooks (`packages/docs-web/src/content/docs/guides/hooks.md`)

---
title: Per-Node Hooks
description: Attach Claude Agent SDK hooks to individual workflow nodes for tool control, context injection, and input modification.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 6
---

DAG workflow nodes support a `hooks` field that attaches Claude Agent SDK hooks
to individual nodes. Hooks fire during the node's AI execution and can control
tool behavior, inject context, modify inputs, and more.

**Claude only** — other providers warn and ignore `hooks:` (see the
[capability matrix](/reference/provider-capabilities/)).

These YAML hooks are one of three places hook-like logic can live. See
[Choosing where hook logic lives](#choosing-where-hook-logic-lives) before
reaching for them.

## Quick Start

```yaml
name: safe-migration
description: Generate SQL with guardrails
nodes:
  - id: generate
    prompt: "Generate a database migration for $ARGUMENTS"
    hooks:
      PreToolUse:
        - matcher: "Bash"
          response:
            hookSpecificOutput:
              hookEventName: PreToolUse
              permissionDecision: deny
              permissionDecisionReason: "No shell access during SQL generation"
```

## How It Works

Each hook matcher has three fields:
- `matcher` (optional): Regex pattern to filter by tool name. Omit to match all tools.
- `response` (required): The SDK `SyncHookJSONOutput` returned when the hook fires.
- `timeout` (optional): Seconds before the hook times out (default: 60).

At runtime, each YAML hook is wrapped in a trivial callback:
```
async () => response
```
No custom DSL — `response` IS the SDK type, passed through unchanged.

**Important**: When using `hookSpecificOutput`, you must include a `hookEventName`
field that matches the event key (e.g., `hookEventName: PreToolUse` inside a
`PreToolUse` hook). This is an SDK requirement — it uses this field to determine
which event-specific fields to process.

## Choosing where hook logic lives

| You need | Use | Owned by |
|----------|-----|----------|
| A fixed response the workflow author can read in the YAML: deny `Bash`, add a reminder after every `Read` | YAML `hooks:` on the node | The workflow |
| Logic that inspects the hook input at runtime: allow `git status` but block `git push`, lint the file a `Write` just touched | A native Claude hook in `.claude/settings.json` or `~/.claude/settings.json` | Claude configuration |
| A deterministic step in the workflow: run tests, parse JSON, decide whether a later node runs | A `bash:` or `script:` node, gated with `when:` on its output | The workflow |

YAML `hooks:` are deliberately static. Each `response` is returned unchanged
every time its matcher fires, so the engine and anyone reading the workflow can
see exactly what the node will do. Workflow YAML has no executable hook types
(`command:`, `http:`, `prompt:`, `agent:`) by design: computation
belongs in a node body, not in the YAML. The
[workflow language constitution](https://github.com/coleam00/Archon/blob/dev/.archon/workflow-language-constitution.md)
explains that boundary.

When a hook has to compute something from its input, write it as a native Claude
hook. Claude Code runs it inside the node's session, and Archon decides which
settings files that session reads.

### Native Claude hooks and `settingSources`

The node's `settingSources` controls which Claude settings files load, and so
which native hooks run:

| `settingSources` | Settings file loaded | Native hooks that run |
|------------------|----------------------|------------------------|
| `['project', 'user']` (default) | `<cwd>/.claude/settings.json` and `~/.claude/settings.json` | Project and user hooks |
| `['project']` | `<cwd>/.claude/settings.json` | Project hooks only |
| `['user']` | `~/.claude/settings.json` | User hooks only |
| `[]` | Neither | None |

`<cwd>` is the node's working directory, the run's worktree when it has one, so
project hooks come from the checked-out branch. Archon never loads
`.claude/settings.local.json`. `settingSources` also controls CLAUDE.md, skills,
commands, and agents discovery; see
[`settingSources`](/reference/workflow-language/#claude-sdk-advanced-options).

A project hook that inspects every `Bash` command, in `.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-bash.sh" }
        ]
      }
    ]
  }
}
```

A workflow that runs the project hook in one node, keeps the operator's user
hooks out, and runs another node with no native hooks at all:

```yaml
name: guarded-fix
description: Fix a bug with the project's Bash guard active
provider: claude
nodes:
  - id: fix
    prompt: "Fix the bug described in $ARGUMENTS"
    settingSources: ['project']   # project hooks run; ~/.claude hooks do not

  - id: test
    bash: "bun run test"
    depends_on: [fix]

  - id: summarize
    prompt: "Summarize the fix and the test result: $test.output"
    depends_on: [test]
    settingSources: []            # no settings files, so no native hooks
    hooks:                        # YAML hooks still apply
      PreToolUse:
        - matcher: "Write|Edit"
          response:
            hookSpecificOutput:
              hookEventName: PreToolUse
              permissionDecision: deny
              permissionDecisionReason: "Summary node is read-only"
```

Omitting `settingSources` inherits `assistants.claude.settingSources` from
`.archon/config.yaml`, and then the `['project', 'user']` default. YAML `hooks:`
run whatever `settingSources` says, alongside any native hooks that load. See
[Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) for the
native hook format.

## Supported Hook Events

| Event | Fires When | Matcher Filters On |
|-------|-----------|-------------------|
| `PreToolUse` | Before a tool executes | Tool name (e.g. `Bash`, `Write`, `Read`) |
| `PostToolUse` | After a tool succeeds | Tool name |
| `PostToolUseFailure` | After a tool fails | Tool name |
| `Notification` | System notification | Notification type |
| `Stop` | Agent stops | N/A |
| `SubagentStart` | Subagent spawned | Agent type |
| `SubagentStop` | Subagent finishes | Agent type |
| `PreCompact` | Before context compaction | Trigger (`manual`/`auto`) |
| `SessionStart` | Session begins | Source (`startup`/`resume`/`clear`/`compact`) |
| `SessionEnd` | Session ends | Exit reason |
| `UserPromptSubmit` | User prompt submitted | N/A |
| `PermissionRequest` | Permission prompt would appear | Tool name |
| `Setup` | SDK initialization | Trigger (`init`/`maintenance`) |
| `TeammateIdle` | Agent teammate goes idle | N/A |
| `TaskCompleted` | Background task finishes | N/A |
| `Elicitation` | MCP server requests user input | N/A |
| `ElicitationResult` | Elicitation response received | N/A |
| `ConfigChange` | Settings/config file changed | Source (`user_settings`/`project_settings`/etc.) |
| `WorktreeCreate` | Git worktree created | Worktree name |
| `WorktreeRemove` | Git worktree removed | Worktree path |
| `InstructionsLoaded` | CLAUDE.md/instructions loaded | Memory type (`User`/`Project`/`Local`/`Managed`) |

Tool names: `Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebFetch`,
`Agent`, plus MCP tools as `mcp__<server>__<action>`.

## Response Format (SDK `SyncHookJSONOutput`)

The `response` object supports these fields:

| Field | Type | Effect |
|-------|------|--------|
| `hookSpecificOutput` | object | Event-specific response (see below) |
| `systemMessage` | string | Inject a message visible to the model |
| `continue` | boolean | `false` stops the agent |
| `decision` | `'approve'` / `'block'` | Top-level approve/block |
| `stopReason` | string | Reason when stopping |
| `suppressOutput` | boolean | Suppress output emission |

### PreToolUse `hookSpecificOutput`

```yaml
hookSpecificOutput:
  hookEventName: PreToolUse
  permissionDecision: deny | allow | ask  # Control whether tool runs
  permissionDecisionReason: "..."         # Why (shown in logs)
  updatedInput:                           # Modify tool arguments
    file_path: "/sandbox/output.ts"
  additionalContext: "..."                # Text injected into model context
```

### PostToolUse `hookSpecificOutput`

```yaml
hookSpecificOutput:
  hookEventName: PostToolUse
  additionalContext: "..."         # Text injected after tool result
  updatedMCPToolOutput: ...        # Override what model sees from tool
```

### PostToolUseFailure `hookSpecificOutput`

```yaml
hookSpecificOutput:
  hookEventName: PostToolUseFailure
  additionalContext: "..."         # Context after tool failure
```

### Elicitation `hookSpecificOutput`

```yaml
hookSpecificOutput:
  hookEventName: Elicitation
  action: accept | decline | cancel  # Respond to MCP elicitation
  content: { ... }                   # Form field values
```

### ElicitationResult `hookSpecificOutput`

```yaml
hookSpecificOutput:
  hookEventName: ElicitationResult
  action: accept | decline | cancel  # Override elicitation result
  content: { ... }                   # Modified response values
```

## Examples

### Deny a tool entirely

```yaml
hooks:
  PreToolUse:
    - matcher: "Bash"
      response:
        hookSpecificOutput:
          hookEventName: PreToolUse
          permissionDecision: deny
          permissionDecisionReason: "Shell access not allowed in this node"
```

### Deny tools with a reason message

```yaml
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      response:
        hookSpecificOutput:
          hookEventName: PreToolUse
          permissionDecision: deny
          permissionDecisionReason: "Only read operations are allowed — do not modify files"
```

### Inject context before tool use (without blocking)

Note: this does NOT block the tool — it adds guidance the model sees before the tool runs.

```yaml
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      response:
        hookSpecificOutput:
          hookEventName: PreToolUse
          additionalContext: "Only write to files in the src/ directory"
```

### Redirect file writes (modify tool input)

```yaml
hooks:
  PreToolUse:
    - matcher: "Write"
      response:
        hookSpecificOutput:
          hookEventName: PreToolUse
          permissionDecision: allow
          updatedInput:
            file_path: "/sandbox/output.ts"
```

### Inject steering instructions after every tool call

```yaml
hooks:
  PostToolUse:
    - response:
        systemMessage: "Check: is this output relevant to the task? If not, stop and explain why."
```

### Inject context after reading files

```yaml
hooks:
  PostToolUse:
    - matcher: "Read"
      response:
        hookSpecificOutput:
          hookEventName: PostToolUse
          additionalContext: "You just read a file. Do NOT modify it — analysis only."
```

### Emergency stop on shell access

```yaml
hooks:
  PreToolUse:
    - matcher: "Bash"
      response:
        continue: false
        stopReason: "Emergency halt — shell access attempted"
```

### Multiple hooks on one node

```yaml
hooks:
  PreToolUse:
    - matcher: "Bash"
      response:
        hookSpecificOutput:
          hookEventName: PreToolUse
          permissionDecision: deny
          permissionDecisionReason: "No shell"
    - matcher: "Write|Edit"
      response:
        hookSpecificOutput:
          hookEventName: PreToolUse
          additionalContext: "Only write to files in src/"
  PostToolUse:
    - response:
        systemMessage: "Verify output before continuing"
```

### Full workflow example

```yaml
name: safe-code-review
description: Review code with guardrails
nodes:
  - id: fetch-diff
    bash: "git diff main...HEAD"

  - id: review
    prompt: "Review this diff for bugs and security issues: $fetch-diff.output"
    depends_on: [fetch-diff]
    hooks:
      PreToolUse:
        - matcher: "Bash"
          response:
            hookSpecificOutput:
              hookEventName: PreToolUse
              permissionDecision: deny
              permissionDecisionReason: "Code review should not execute commands"
        - matcher: "Write|Edit"
          response:
            hookSpecificOutput:
              hookEventName: PreToolUse
              permissionDecision: deny
              permissionDecisionReason: "Code review is read-only"
      PostToolUse:
        - matcher: "Read"
          response:
            hookSpecificOutput:
              hookEventName: PostToolUse
              additionalContext: "Focus on security issues in this file"

  - id: summarize
    prompt: "Summarize the review findings from $review.output"
    depends_on: [review]
    allowed_tools: []
```

## Hooks vs allowed_tools/denied_tools

| Feature | `allowed_tools`/`denied_tools` | `hooks` |
|---------|-------------------------------|---------|
| Block a tool entirely | Yes | Yes |
| Inject context | No | Yes (`additionalContext`, `systemMessage`) |
| Modify tool input | No | Yes (`updatedInput`) |
| Override tool output | No | Yes (`updatedMCPToolOutput`) |
| Stop the agent | No | Yes (`continue: false`) |
| React after tool use | No | Yes (`PostToolUse`) |

Use `allowed_tools`/`denied_tools` for simple include/exclude. Use `hooks` when you
need context injection, input modification, or post-tool-use reactions.

## Hook events in the run record

When a native Claude hook runs (a `command`, `http`, or MCP-tool hook from a
settings file), Archon records a `hook` provider event when it starts and another
when it ends. The end event carries the hook's name, the event that fired it
(such as `PreToolUse`), a status of `succeeded`, `failed`, or `cancelled`, and the
exit code when there is one. Like other provider events, they are saved with the
node, streamed live, served by `GET /api/workflows/runs/{runId}/provider-events`,
and written by `archon workflow logs`, whose `--format text` view prints only
failed and cancelled hooks. The console's run stream shows each hook event beside
tool calls, with its name, triggering event, status, and exit code when present.
Hook events remain visible when tool calls or system messages are hidden, and
follow the selected node filter.

YAML `hooks:` record no `hook` events. They are in-process SDK callbacks, and the
Claude Agent SDK emits lifecycle events only for hooks it executes itself.

Archon does not record `hook_progress` output, which Claude emits only for async
native hooks.

## Limitations

- **Static responses only in YAML** — hooks return the same response every time.
  For input-aware hook logic, use a [native Claude hook](#native-claude-hooks-and-settingsources).
  For workflow logic, use a `bash:` or `script:` node and gate downstream nodes with `when:`.
- **Claude only** — other providers warn and ignore `hooks:`.

## SDK Reference

Refer to the [Anthropic Claude Agent SDK documentation](https://docs.anthropic.com/en/docs/claude-code/sdk) for the authoritative `SyncHookJSONOutput` type, hook event reference, and matcher patterns.

## Related

- [Per-Node MCP Servers](/guides/mcp-servers/) — `mcp:` field for external tool access
- [Per-Node Skills](/guides/skills/) — `skills:` field for domain knowledge injection

---

### Packages Docs Web Src Content Docs Guides Loop Nodes (`packages/docs-web/src/content/docs/guides/loop-nodes.md`)

---
title: Loop Nodes
description: Configure iterative AI execution nodes that repeat until a completion condition is met.
category: guides
area: workflows
audience: [user]
status: current
sidebar:
  order: 3
---

DAG workflow nodes support a `loop` field that runs an AI prompt repeatedly
until a completion condition is met. Each iteration is a full AI agent session
that can read files, write code, run commands, and produce output.

Use loop nodes for autonomous multi-step work: implement N stories from a PRD,
iterate on a design until validation passes, or refine output until quality
criteria are met.

A loop node's iteration prompt can live **inline** (`loop.prompt`) or in a
**command file** (`loop.command`, resolved the same way
[`command:` nodes](/guides/authoring-commands/) load their text). Provide
exactly one — both at once, or neither, is rejected at workflow load time.

## Quick Start

```yaml
name: iterate-until-done
description: Implement stories one at a time
nodes:
  - id: setup
    bash: |
      echo "Found 3 stories to implement"

  - id: implement
    depends_on: [setup]
    loop:
      prompt: |
        Read the PRD and implement the next unfinished story.
        Validate your changes before committing.

        Setup context: $setup.output
        User request: $USER_MESSAGE

        When all stories are done, output: <promise>COMPLETE</promise>
      until: COMPLETE
      max_iterations: 10
      fresh_context: true

  - id: report
    depends_on: [implement]
    prompt: |
      Summarize what was implemented: $implement.output
```

## How It Works

A loop node iterates its prompt until one of these conditions is met:

1. **LLM completion signal** — the AI outputs `<promise>SIGNAL</promise>` where
   SIGNAL matches the `until` value
2. **Deterministic bash check** — an `until_bash` script exits with code 0
3. **Structured field** — the iteration's validated `output_format` payload has the
   `until_field` property set to `true`
4. **Max iterations reached** — the node fails with a clear error

A loop must declare **at least one** of `until` / `until_bash` / `until_field` — none
is required on its own. They are OR'd: whichever fires first ends the loop, and the
cheap channels are checked before `until_bash`, which is skipped once another one
fired (it cannot change the outcome, and skipping it avoids an extra run of a
side-effecting script).

Each iteration is a full AI agent invocation with tool access. Between iterations,
the executor checks for workflow cancellation.

### Choosing a completion channel

| Your completion condition | Declare |
|---|---|
| Externally checkable — tests pass, a file exists, a state field flipped | `until_bash` **only**. No prose is matched, so a sentinel the model happens to emit while reasoning about the criteria cannot end the loop early. |
| A judgment the model makes, with no answer asked for | `output_format` + `until_field`. The decision becomes a schema-validated boolean instead of a string match — the model cannot end the loop by mentioning a word. |
| An answer the run asks for between iterations | A `loop_group` whose body ends in a `wait: attention` node, with `until_bash` reading the answer. See [Pausing to ask inside the body](#pausing-to-ask-inside-the-body). |

Declaring more than one is fine and means "whichever fires first ends it".

## Configuration Fields

```yaml
- id: my-loop
  loop:
    prompt: "..."           # Inline prompt. Exactly one of `prompt` or `command` is required.
    # command: <name>       # Alternative to `prompt`: package-local or shared command name,
    #                       # loaded once per run and reused for every iteration.
    #                       # Never combine with `prompt` — the loader rejects both together.
    until: COMPLETE         # Prose completion signal.
    max_iterations: 10      # Required. Bounds autonomous iterations — node fails if exceeded.
    fresh_context: true     # Optional. Default: false.
    until_bash: "..."       # Bash script checked after each iteration; exit 0 = complete.
    until_field: done       # Property in this node's `output_format` whose validated
                            # value `true` ends the loop.
                            #
                            # At least ONE completion channel is required — any of
                            # until / until_bash / until_field. None is required alone.
```

`interactive`, `gate_message`, and `signal_completes` still load but are deprecated;
see [Legacy interactive loops](#legacy-interactive-loops-deprecated).

### `prompt`

The prompt text sent to the AI each iteration. Supports all standard variable
substitution:

| Variable | Value |
|----------|-------|
| `$ARGUMENTS` / `$USER_MESSAGE` | Original user message |
| `$ARTIFACTS_DIR` | Workflow artifacts directory |
| `$BASE_BRANCH` | Repository base branch |
| `$DOCS_DIR` | Documentation directory path (default: `docs/`) |
| `$WORKFLOW_ID` | Current workflow run ID |
| `$nodeId.output` | Output from upstream nodes |
| `$LOOP_USER_INPUT` | Deprecated. The approve comment from a [legacy interactive loop](#legacy-interactive-loops-deprecated) gate, on the first iteration after the resume only. In a `loop_group`, read the answer to a terminal attention wait as `$LOOP_PREV.<wait-id>.output.text` instead. |
| `$LOOP_PREV_OUTPUT` | Cleaned output of the previous loop iteration. Empty string on the first iteration. Useful for `fresh_context: true` loops that need to reference what the previous pass produced or why it failed. |

`$USER_MESSAGE` is particularly important for `fresh_context: true` loops —
the agent has no memory of prior iterations, so the prompt must include all
context needed to continue the work. `$LOOP_PREV_OUTPUT` complements this by
exposing the previous iteration's own output without forcing the engine to
thread the session.

### `command`

Alternative to `prompt` — names a command file
whose body is loaded as the iteration prompt. **Exactly one of `prompt` or
`command` is required**; specifying both, or neither, is rejected at workflow
load time with a clear error.

The named command resolves exactly like a [`command:` node](/guides/authoring-commands/).
In a packaged workflow it resolves only from that workflow's `commands/`
directory, including after `include:` expansion. In a legacy workflow it uses
repo → home → bundled precedence. The same command-name safety
rules apply — no path separators, no `..`, no leading `.` — and unsafe names
are rejected at parse time. Static workflow validation also flags a
`loop.command` that points at a missing file, with guidance to create it in the
owning workflow's `commands/` directory for packaged workflows or in
`.archon/commands/` for legacy workflows, the same way it does for `command:`
nodes.

For an ordinary workflow, the file is **read once per run** when the loop node
starts. For a workflow composed through `include:`, Archon resolves and compiles
the command body during load-time composition so its node references and declared
inputs are proven before the child joins the parent's flat DAG. A matching file
that is unreadable fails closed; Archon never falls through to a lower-precedence
command with the same name. A missing, empty, or non-hermetic included command fails before a fresh AI turn.
If an admitted run cannot read its captured command on this host, Archon pauses
it for source continuation; a later host that can read the capture can resume it.

Once loaded, the text behaves identically to an inline `prompt`: all the
variable substitution above applies unchanged (including `$LOOP_PREV_OUTPUT`),
as do all the iteration semantics (`until`, `until_bash`, `max_iterations`,
`fresh_context`).

```yaml
- id: implement
  model: opus
  depends_on: [generate-prd]
  loop:
    command: archon-ralph-implement   # resolved repo → home → bundled (see precedence above)
    until: COMPLETE
    max_iterations: 15
    fresh_context: true
```

Use this when the iteration prompt is long enough that keeping it inline
obscures the shape of the pipeline — Ralph-style implement/build loops and
iterate-until-valid loops are the typical case. It is the loop-node parallel
of moving a `prompt:` node to a `command:` node.

### `until`

Deprecated prose completion signal. Prefer `until_bash` for deterministic checks or
`until_field` for model judgment. While `until` remains supported, the executor checks
each iteration's output for:

1. **Tag format (recommended):** `<promise>COMPLETE</promise>` — case-insensitive
   match (both tags and signal value), whitespace-tolerant. Prevents false
   positives from the AI mentioning the signal word in discussion.
2. **Plain signal (fallback):** A final line containing only `COMPLETE`, with
   surrounding line whitespace allowed. Inline mentions, including a final sentence
   ending in `COMPLETE`, do not complete the loop.

The `<promise>` tags are automatically stripped from output sent to the user
and to downstream nodes.

**Optional, like every completion channel.** Omit `until` entirely whenever another
channel decides completion — the executor then never matches prose at all, so the
signal cannot false-positive on a model that mentions its own exit criterion. What
the loader requires is that *at least one* channel is declared; a loop with none is
rejected at load.

### `max_iterations`

Bounds autonomous iterations. If the loop reaches this count without meeting any
declared completion channel, the node **fails** (not succeeds). This prevents runaway
loops from burning tokens indefinitely.

Only an explicit answer runs an iteration past the bound: a comment on a legacy
[interactive gate](#interactive-and-gate_message), or an accepted answer to a `loop_group`'s
[terminal attention wait](#pausing-to-ask-inside-the-body).
Each accepted answer permits one more iteration, and each further iteration needs
another accepted answer. A changed question can pause again in the same iteration.

Choose based on the work scope:
- Simple refinement loops: 3–5
- Multi-story implementation: 10–15
- Long-running autonomous agents: 15–20

### `fresh_context`

Controls session continuity between iterations:

| Value | Behavior | Use when |
|-------|----------|----------|
| `true` | Each iteration starts a fresh AI session. No memory of prior iterations. | Work state lives on disk (files, git). Prevents context window exhaustion on long loops. |
| `false` (default) | Sessions thread — each iteration resumes the prior conversation. | Iterative refinement where the agent needs to remember what it tried before. |

The first iteration is always fresh regardless of this setting.

A structured-output re-ask (see [`until_field`](#until_field)) does **not** disturb
threading. It runs in its own throwaway session so an invalid turn is not carried
forward as context, and that session is discarded — the next iteration still resumes
the conversation the loop was already threading. The repaired answer still reaches it
through `$LOOP_PREV_OUTPUT`.

### `until_bash`

Bash script executed after each iteration. If it exits with code 0, the loop
completes — even if the AI didn't output the completion signal. Optional when
`until` is set; **on its own it is the whole completion channel**, which is the
preferred shape whenever the condition is externally checkable:

```yaml
loop:
  prompt: "Fix the failing tests"
  max_iterations: 5
  until_bash: "bun run test"  # Loop ends when tests pass. No `until:` — nothing to
                              # false-positive on, and no dead field to invent.
```

`until_bash` runs only on iterations that no other completion channel already
ended, so a loop declaring more than one never pays for a redundant check.

The check uses the node's `timeout`: a positive, finite number of milliseconds,
with no limit when omitted. This applies to `loop:` and `loop_group:`, including the
completion recheck after a group continues past a terminal pause. It limits each check,
not the AI iteration or the group's body nodes. To bound a check, declare the budget on
the node:

```yaml
- id: fix-tests
  timeout: 900000             # The suite takes ~4 min; a hung check fails after 15 min.
  loop:
    prompt: "Fix the failing tests"
    max_iterations: 5
    until_bash: "bun run test"
```

:::caution[If your `until_bash` accumulates state]
The skip means the script does not run on an iteration another channel already
completed, so a check that *mutates* state each time it runs — a counter, an append,
a cursor — advances once fewer than it would have. The completion verdict is unaffected either way (the
channels are OR'd), and the loop ends on that same iteration, so
nothing observable differs. A [legacy interactive loop](#legacy-interactive-loops-deprecated)
is the exception: it can gate on a completed iteration and run another one when
approved with text, and that iteration's `until_bash` then sees state one increment behind.

Prefer a `until_bash` that only *reads* state — `test`, `grep`, a test suite — and
let a `bash:` node inside the loop own any mutation.
:::

This is useful for deterministic completion criteria: test suites, lint checks,
build success. The bash script supports the workflow variables and node output
references `prompt` does (`$ARTIFACTS_DIR`, `$nodeId.output`, etc.), but it reads a
declared input from the environment as `$INPUTS_<UPPER_SNAKE>`, not as `$INPUTS.<name>`.
Inside an included block `$INPUTS.<name>` is shorthand for that variable; a directly run
workflow leaves it literal (see
[Passing values into an included block](/reference/workflow-language/#passing-values-into-an-included-block)).
In `until_bash`, small output references are single-quoted inline; large outputs use
an unquoted file-read substitution. Assign each reference to a variable, then quote
that variable when using it. See the
[assignment-then-quote examples](/reference/variables#shell-quoting-in-bash-vs-script).

### `until_field`

Names a property in this node's [`output_format`](/reference/workflow-language/) schema.
The loop ends on the first iteration whose **validated** payload has that property
set to exactly `true`.

```yaml
- id: triage
  output_format:
    type: object
    properties:
      done: { type: boolean }
      remaining: { type: integer }
    required: [done]
  loop:
    prompt: |
      Work the next item in the backlog. Report whether the backlog is now empty.
    max_iterations: 20
    until_field: done
```

Use this when completion is a **judgment the model makes** and there is nothing on
disk to test. It replaces a prose sentinel with a schema-validated boolean, so the
model cannot end the loop by mentioning a word while reasoning about its criteria.

Four rules are checked at load time, so a mistake fails the workflow rather than
looping silently:

1. the node must declare `output_format`;
2. the property must appear in `output_format.properties`;
3. it must be listed in `output_format.required` — an optional property the model
   omits would read as "not complete" and burn `max_iterations`;
4. if it declares a `type`, that type must be `boolean`.

Termination is strict identity: `true` ends the loop, and nothing else does — not
`"true"`, not `1`. A payload that fails schema validation is a failure, not a
quiet "not done yet" (see below).

**The loop's output becomes the validated JSON.** With `output_format` declared,
`$loopId.output` is the payload rather than the prose, `$loopId.output.<field>` gets
the same strict field access every other producer enforces, and `$LOOP_PREV_OUTPUT`
carries the same value.

**Per-provider behavior is the same as any other node.** On a provider with
*enforced* structured output (Claude, Codex, OpenCode) the schema constrains
decoding. On a *best-effort* provider the model is asked for the schema (Pi through
a `structured_output` tool call, Copilot through the prompt), and a payload that fails
validation is re-asked up to three times **within the same iteration** — a reask does not consume a loop iteration. If those are
exhausted, the node **fails** with the validation errors; it is never treated as an
incomplete iteration. See
[provider capabilities](/reference/provider-capabilities/).

`until_field` is **not supported on `loop_group`**, and deliberately so: a group's
body node can already declare `output_format` and be read by a deterministic check,
which is the existing way to express the same thing.

```yaml
# loop_group equivalent — no new field needed
loop_group:
  max_iterations: 10
  until_bash: '[ $decide.output.done = "true" ]'
  nodes:
    - id: decide
      output_format:
        type: object
        properties: { done: { type: boolean } }
        required: [done]
      prompt: Is the backlog empty?
```

## Patterns

### Stateless agent (Ralph pattern)

Each iteration reads state from disk, does one unit of work, writes state back.
The prompt tells the agent it has no memory and must bootstrap from files.

```yaml
- id: implement
  depends_on: [setup]
  idle_timeout: 600000
  loop:
    prompt: |
      You are in a FRESH session — no memory of previous iterations.
      Read the PRD tracking file to find the next unfinished story.
      Implement it, validate, commit, update tracking.
      When all stories are done: <promise>COMPLETE</promise>

      Project context: $setup.output
    until: COMPLETE
    max_iterations: 15
    fresh_context: true
```

**When to use:** Multi-story implementation, long-running tasks where context
window exhaustion is a risk. The agent reads `.archon/ralph/*/prd.json` or
similar tracking files to know what's done and what's next.

### Retry-on-failure with `$LOOP_PREV_OUTPUT`

When `fresh_context: true` is needed (to keep each iteration's context window
small) but the agent still benefits from knowing what the previous pass said —
typical of implement→validate or generate→review loops — inject the previous
iteration's output via `$LOOP_PREV_OUTPUT`:

```yaml
- id: implement-and-qa
  loop:
    prompt: |
      Implement the plan, then run `bun run validate`.
      If checks fail, fix the failures.

      Previous iteration output (empty on first pass):
      $LOOP_PREV_OUTPUT

      Use the above to focus your fixes. When all checks pass output:
      <promise>QA_PASS</promise>
    until: QA_PASS
    fresh_context: true
    max_iterations: 3
```

In a continuous run, the first iteration sees `$LOOP_PREV_OUTPUT` substituted
to an empty string; iterations 2+ see the previous iteration's cleaned output
(after `<promise>` tags are stripped).

### Accumulating context

The agent builds on its own prior work across iterations. Good for iterative
refinement where remembering previous attempts matters.

```yaml
- id: refine
  loop:
    prompt: |
      Review the current implementation and improve it.
      Run validation after each change.
      When validation passes with zero issues: <promise>DONE</promise>
    until: DONE
    max_iterations: 5
    fresh_context: false
```

**When to use:** Fix-iterate cycles, design refinement, test-driven development
where the agent needs to remember what it already tried.

### Deterministic exit with `until_bash`

Combine LLM work with a deterministic completion check:

```yaml
- id: fix-tests
  loop:
    prompt: |
      Run the test suite. Read the failures. Fix them one at a time.
      If all tests pass: <promise>TESTS_PASS</promise>
    until: TESTS_PASS
    max_iterations: 8
    until_bash: "bun run test"
    fresh_context: false
```

The loop ends either when the AI signals completion or when the bash check
succeeds — whichever comes first.

**No channel vetoes another.** They are OR'd, so an AI that emits `TESTS_PASS`
while tests still fail ends the loop anyway — the bash check never gets to disagree. If the tests
are the real exit criterion, say so by dropping `until` and the sentence that
teaches the model to emit it:

```yaml
- id: fix-tests
  loop:
    prompt: Run the test suite. Read the failures. Fix them one at a time.
    max_iterations: 8
    until_bash: "bun run test"
    fresh_context: false
```

Now the only way out is a passing suite.

## Node Features

### What works on loop nodes

- `depends_on` — upstream dependencies
- `when` — conditional execution
- `trigger_rule` — join semantics
- `idle_timeout` — per-iteration timeout (default: 30 minutes)
- `provider` / `model` — node-level overrides are resolved and used for every iteration
- `allowed_tools` / `denied_tools` — passed to the selected provider on every iteration,
  just as on a `prompt:` node. Enforcement depends on the provider's tool-restriction
  support; Archon warns when the provider declares that it cannot enforce them
- `$nodeId.output` — downstream nodes receive the last iteration's output

### Asking for an answer between iterations

Apart from the deprecated `interactive` mode, a `loop:` node does not pause. To ask a
question after each pass and feed the answer into the next one, use a [`loop_group`](#cross-node-loops-with-loop_group) whose body
ends in a `wait: attention` node. See
[Pausing to ask inside the body](#pausing-to-ask-inside-the-body).

### What is NOT supported on loop nodes

- `context: fresh` — ignored. Session control is handled exclusively by `fresh_context` within the `loop:` config
- `hooks` — per-node SDK hooks are not passed through to loop iterations
- `mcp` — per-node MCP server configs are not loaded for loop nodes
- `skills` — skill preloading is not applied to loop iterations

These fields are discarded at parse time — the workflow still
loads but the fields have no effect. The drop is reported as a warning naming the
node and the fields, both in the loader log and in the author-facing output of
`archon validate workflows` and `archon workflow list`.

The loop executor manages its own AI sessions independently from the standard
node executor. If you need hooks, MCP, or skills, consider using a command node
that wraps the iterative logic in a command file.

## Output

A loop node's output (available via `$nodeId.output` to downstream nodes) is
the **last iteration's output only** — not a concatenation of all iterations.

If you need to accumulate results across iterations, write them to files in
`$ARTIFACTS_DIR` and have the downstream node read from there.

## Error Handling

| Scenario | Behavior |
|----------|----------|
| Iteration fails | A transient, rate-limited or capacity (`overloaded`) failure retries the iteration. Any other failure fails the node (no more iterations) |
| Max iterations exceeded | Node fails with descriptive error |
| Workflow cancelled | Detected between iterations, node stops |
| Idle timeout per iteration | The iteration is retried as a transient failure, whether or not its provider produced output first |
| `retry` configured on node | Applies to each failed iteration; successful iterations are not replayed |

By default, each iteration gets two retries after its initial attempt, with a 3000 ms base delay that doubles on each ordinary transient retry. Configure the existing node-level `retry:` block to change the budget and base delay:

```yaml
nodes:
  - id: work
    retry:
      max_attempts: 1
      delay_ms: 0
    loop:
      prompt: "Complete the task and output <promise>DONE</promise>"
      until: DONE
      max_iterations: 5
```

`max_attempts` is required when declaring `retry:` and accepts integers 1–5, counting retries after the initial attempt. `delay_ms` accepts 0–60000 on loops and defaults to 3000 when omitted. Zero skips ordinary transient retry waits. The budget resets for each iteration; retries do not consume `max_iterations`.

Only `on_error: transient` or omission is supported on loops; `all` is a load error. Unknown, fatal and output-contract failures are not retried by this policy. Structured-output reasks have their own separate budget.

Rate limits and capacity retain the shared AI-node exceptions: at least five retries, regardless of `max_attempts`. Rate limits wait around 45 seconds with ±50% jitter and keep the larger budget for the rest of the iteration. Capacity waits use centers of 45, 90, 180, 300 and 300 seconds with ±50% jitter; its larger budget applies only while the current failure is capacity. `delay_ms: 0` does not bypass either schedule. See [Retry Configuration](/reference/workflow-language/#retry-configuration).

## Cross-Node Loops with `loop_group`

A `loop` node iterates a **single prompt**. A `loop_group` node iterates a
**multi-node sub-DAG** — a sealed body of nodes that re-runs in full each
iteration until a completion condition is met. Use it when "iterate until done"
needs a *pipeline* of steps (e.g. `implement → test → review`), not just one
repeated step.

```yaml
name: fix-until-green
description: Implement, test, and review until tests pass
nodes:
  - id: fix-loop
    loop_group:
      until: TESTS_PASS        # completion signal in the body's terminal output
      max_iterations: 5
      fresh_context: false      # false (default) = body AI sessions continue across iterations
      nodes:                    # sealed sub-DAG body — repeats as a unit
        - id: implement
          prompt: Fix the failing tests. Emit TESTS_PASS only when all pass.
          depends_on: []
        - id: test
          bash: |
            if bun test; then
              printf 'green'
            else
              printf 'red'
            fi
          depends_on: [implement]
        - id: review
          prompt: Summarize the result; echo TESTS_PASS if tests are green.
          depends_on: [test]
```

The legacy prose `until:` channel cannot detect a signal when the node the
group's output is read from (see below) produces non-string structured output. The
loader rejects that combination when `until:` is the only completion channel, for
every node the output can be read from. Use `until_bash` to read that node's
structured field instead. Nodes without `output_format`, or with a string schema,
keep loading. Declaring `until_bash`
alongside `until:` also keeps loading, with the existing prose-channel deprecation
warning.

### How it works

- From the **outer DAG's** perspective, `fix-loop` is one node. The cycle is
  *encapsulated* inside it — the outer DAG stays acyclic.
- Each iteration runs the body's topological layers in full. Within a layer,
  ordinary nodes settle before nodes that can suspend, including nested groups
  containing interactions. Nodes within each phase can run in parallel. Before an
  interaction pauses the run, sibling work must either finish or reach its own
  interaction, including work inside nested groups. Any failure skips those
  interactions.
- The body is **sealed**: a body node's `depends_on` may only reference sibling
  body nodes, not outer-DAG nodes. Outer context is still reachable via `$nodeId.output`
  refs in body prompts.
- Loop-control and body lifecycle events use the namespaced step name
  `{groupId}.{nodeId}` in the persisted event log, with the iteration recorded in
  event data. Nested groups compose the prefix.
- A body node that **fails** fails the whole group immediately with that node's
  error (no further iterations run) — same semantics as a failed node in a
  top-level DAG. Runnable cleanup still runs, including independent branches inside
  nested groups. After a body failure, gates, waits, and interactive loops are
  skipped, even with `trigger_rule: all_done`. Nested groups containing interactions
  run their body once, then skip their interaction and further iterations. This applies at any depth
  and preserves the original failure without creating an attention item.
- `$fix-loop.output` (visible to the outer DAG) is the **final iteration's
  output**: the first completed, non-empty output among the body's terminal nodes,
  in body order. A terminal pause is read past, as described next.

### Pausing to ask inside the body

To ask for an answer after each pass, end the body in a
[`wait: attention`](/reference/workflow-language/#attention-waits) node. The run pauses
there until someone answers. On resume with the first answer, the engine compares the
current message, form, and options against the paused question. If they changed, it
discards the stale answer and asks again in the same iteration. This re-ask happens
at most once per occurrence chain: the next answer is accepted even if the question
changes again. See [attention waits](/reference/workflow-language/#attention-waits).
Once an answer is accepted, the group checks `until_bash`; if it does not pass,
the next iteration runs with the answer available as `$LOOP_PREV.<wait-id>.output`.

```yaml
- id: refine
  loop_group:
    max_iterations: 5
    until_bash: 'decision=$review.output.decision; [ "$decision" = approve ]'
    nodes:
      - id: draft
        prompt: |
          Write the release notes for this change: $USER_MESSAGE
          Feedback on the previous draft (empty on the first pass):
          $LOOP_PREV.review.output.text
      - id: review
        depends_on: [draft]
        wait:
          attention: "Approve these release notes, or reject with what to change: $draft.output"
          form: approval
```

- Once an answer is accepted, `until_bash` reads it as
  `$review.output.decision`. Approving ends the group;
  rejecting runs another draft, which reads any text sent with the answer through
  `$LOOP_PREV.review.output.text`. A reject is an answer, so it never cancels the run.
- Assign the reference, then quote the shell variable, as above. Archon already
  single-quotes a substituted value, so `"$review.output.decision"` compares
  `'approve'` with the quotes included and never matches. See
  [Shell Quoting in `bash:` vs `script:`](/reference/variables#shell-quoting-in-bash-vs-script).
- `$group.output` is the draft, not the answer (see below).
- Any [form](/reference/workflow-language/#attention-waits) works. A `message` wait
  produces only `{ text }`, so with it decide completion from the work instead of the answer.
- `max_iterations` bounds autonomous iterations. Each accepted answer to the terminal
  attention wait permits one more iteration even past that bound, so this loop runs
  until someone approves.
- The loader warns when the terminal attention wait has no `when:` and `until_bash`
  does not reference its `$<wait-id>.output`: the answers would be recorded but never
  end the loop.

A wait pauses the group only as the body's **sole terminal sink**: nothing depends on
it, and every other body node leads to it. Any other placement is a load error, because
resuming in the middle of an iteration would replay or skip sibling work. The group
compares the question on the first answer, with the one-reask bound described above.
If it asks again, the iteration stays paused; otherwise, if `until_bash` does not
pass, the next iteration
starts with the accepted answer available. A deprecated `approval:` gate is accepted
in the same place; see
[Migrating from approval gates](/guides/approval-nodes/#migrating-from-approval-gates).

A pause asks about the work; it is not the work. When the sink is an attention wait,
the iteration's output is read from the nodes the wait depends on, so `$group.output`
is the same whether the wait was skipped by its `when:` or answered. The answer stays
on the wait node: `until_bash` reads it as `$<wait-id>.output`, and the next iteration
reads it as `$LOOP_PREV.<wait-id>.output`. A skipped wait has no output, so an
`until_bash` that reads a wait its own `when:` can skip fails the group. To pause only
when needed, decide completion from the work instead:

```yaml
- id: plan
  loop_group:
    until_bash: 'blocked=$draft.output.blocked; [ "$blocked" = false ]'
    max_iterations: 5
    nodes:
      - id: draft
        prompt: |
          Draft the plan. Answer to your last question, if any: $LOOP_PREV.plan-question.output.text
          Set blocked to true only if you need an answer before you can continue.
        output_format:
          type: object
          properties:
            plan: { type: string }
            blocked: { type: boolean }
          required: [plan, blocked]
      - id: plan-question
        depends_on: [draft]
        when: "$draft.output.blocked == 'true'"
        wait:
          attention: "The draft needs a decision: $draft.output.plan"
```

When the draft is not blocked, the wait is skipped and the group completes. When it is
blocked, the answer reaches the next draft through `$LOOP_PREV`.
`$plan.output` is the latest draft either way.

### Reusing a block in the body

`include:` is valid in a `loop_group` body. Discovery expands the block before the run starts,
so each iteration executes ordinary namespaced body nodes — there is no child run or
runtime-resolved graph:

```yaml
- id: converge
  loop_group:
    until_bash: test $review.output.ready = "true"
    max_iterations: 5
    nodes:
      - id: review
        include: reusable-review
        with:
          request: $INPUTS.request
```

The block's `returns:` node supplies `$review.output`, including in `until_bash`. Internal
dependencies, inputs, gates, and namespacing behave the same as for a top-level include. A
`workflow:` node still cannot appear in a group body because it creates a separately governed
runtime sub-run rather than static composition.

### Cross-iteration references: `$LOOP_PREV`

A body node can reference a sibling's output from the **previous iteration**
with `$LOOP_PREV.<nodeId>.output` (and `$LOOP_PREV.<nodeId>.output.<field>`
for structured output):

```yaml
nodes:
  - id: fix-loop
    loop_group:
      # The terminal node's own verdict ends the loop; nothing here emits a
      # prose sentinel, so `until:` would run to max_iterations on a green run.
      until_bash: test $test.output = "green"
      max_iterations: 5
      nodes:
        - id: implement
          prompt: |
            Previous attempt's test output:
            $LOOP_PREV.test.output
            Fix what failed.
          depends_on: []
        - id: test
          bash: |
            if bun test; then
              printf 'green'
            else
              printf 'red'
            fi
          depends_on: [implement]
```

On iteration 1 (no prior iteration), `$LOOP_PREV.*` resolves to an empty
string. Field access uses the same strict semantics as `$nodeId.output.field`
(a field not in the producer's declared schema fails the consuming node rather
than silently degrading).

The group's `until_bash` sees current outputs through `$nodeId.output` and the
preceding iteration through `$LOOP_PREV.*`, including after a terminal pause is
answered. `LOOP_PREV_OUTPUT` contains the preceding iteration's chosen output with
completion tags removed. The next body iteration instead receives the answered
iteration's outputs as its previous snapshot.

A `command:` body node's command file gets the same `$LOOP_PREV.*` substitution as
the same text written as an inline `prompt:`.

A body node can also gate on the previous iteration's typed output with `when:`:

```yaml
- id: repair
  prompt: Repair the failing tests.
  depends_on: [test]
  when: "$LOOP_PREV.test.output == 'red'"
```

This is a condition reference, not text substitution. On iteration 1 it resolves to
`''`, so the non-empty equality is false. See [Condition Syntax](/reference/workflow-language/#when-condition-syntax).

An `include:` inside the body can use the same `$LOOP_PREV` condition to gate its
entire block. A false gate skips the whole block; when true, each node still follows
its own conditions and trigger rule. If the block contains a nested loop, the include's
gate still reads the previous iteration of the loop containing that include.

`$LOOP_PREV` works in a body node's `with:` binding **string values** too, with the
same text semantics as every other body surface: the previous iteration's output is
substituted into the value each pass (so `with: { prev: $LOOP_PREV.test.output }`
delivers last iteration's text, `''` on iteration 1 — never a typed value). It is
**not** a node reference, so a binding directive's `from:` cannot read it — the
loader rejects that at load and names the string form instead.

### Configuration fields

`loop_group` shares the same iteration-control fields as `loop`:
[`until`](#until), [`max_iterations`](#max_iterations),
[`fresh_context`](#fresh_context), and [`until_bash`](#until_bash), including the
at-least-one-of `until` / `until_bash` rule and the short-circuit between them. The
difference is the body: `loop` takes a single `prompt`; `loop_group` takes a
`nodes` array.

As with a `loop:` node (whose node-level `model`/`provider` are resolved and
applied to every iteration), `model` and `provider` set on a `loop_group` node
**are honored**: they become the default for every body AI node, overridable per
body node.

### Named owner sessions

A command or prompt in the body can continue a statically named pre-loop session:

```yaml
name: review-and-correct
description: Keep one implementation owner while independent reviews repeat
nodes:
  - id: implement
    prompt: Implement the requested change.
  - id: corrections
    depends_on: [implement]
    loop_group:
      max_iterations: 3
      fresh_context: true
      until_bash: 'test $review.output.done = true'
      nodes:
        - id: owner
          context: { resume: implement }
          prompt: Correct any remaining problems in the implementation.
        - id: review
          depends_on: [owner]
          context: fresh
          prompt: Review the change and report whether it is ready.
          output_format:
            type: object
            properties:
              done: { type: boolean }
            required: [done]
            additionalProperties: false
```

The owner's first executed turn immutably forks `implement`, preserving the source.
Subsequent iterations continue that private branch with the same session ID. Reviewers
start fresh and cannot replace the owner's cursor. Explicit named ownership also wins
over `fresh_context` and parallel-layer resets. Multiple owners fork separate branches;
a skipped owner keeps its cursor, or waits to fork until its first executed turn.

The source must be a command, prompt, or plain `loop:` node in an enclosing scope with
a dependency path proving it completes before the group starts. Unordered, non-session,
and loop-group sources are rejected. A source in the same body works differently; see
[Same-iteration sessions](#same-iteration-sessions). Nested groups get separate owner
cursors for each enclosing invocation. Nested pauses retain every enclosing
owner cursor and resume within the paused enclosing iteration before advancing.
The resolved provider must match the source and
support session resume and immutable forks. Missing context or a replacement owner
session ID fails the node rather than starting a new conversation.

### Same-iteration sessions

A command or prompt in the body can also continue a session produced earlier in the
same iteration, for example to ask an agent to finish a step it skipped:

```yaml
nodes:
  - id: rounds
    loop_group:
      max_iterations: 3
      until_bash: 'test $work.output.done = true'
      nodes:
        - id: work
          prompt: Review the change, write review.md, and report what you did.
          output_format:
            type: object
            properties:
              done: { type: boolean }
              wrote_report: { type: boolean }
            required: [done, wrote_report]
        - id: follow-up
          depends_on: [work]
          when: '$work.output.wrote_report != true'
          context: { resume: work }
          prompt: You did not write review.md. Write it now from your review.
```

In each iteration, `follow-up` forks that iteration's `work` session. It never continues
its own branch from an earlier iteration, and it has no owner cursor. The fork contract is
the same as for any named resume: the source session stays unchanged, the providers must
match and support immutable forks, and a missing source handle fails the node.

The source must be a command, prompt, or plain `loop:` node transitively upstream of the
consumer within the body. A body node that is not upstream, a node in a different
`loop_group` body, and a consumer that is not a command or prompt node are rejected when
the workflow loads.

### Resume

Suspension and failure have different recovery rules:

- **Resume after a terminal pause** (the body's terminal attention wait is
  answered): the engine compares the question on the first answer and may ask again
  in the same iteration. After one re-ask, it accepts the next answer even if the
  question changes again, as described [above](#pausing-to-ask-inside-the-body). Once an
  answer is accepted, the group rechecks `until_bash` against the answered iteration's
  current outputs and its own preceding snapshot. If historical ownership or full
  output bytes cannot be restored, the group fails restoration rather than treating
  the history as empty. Only iteration one has a known empty preceding snapshot;
  explicitly skipped nodes have no output. If the check does not pass, the group
  continues with the **next** iteration's whole body. With
  `fresh_context: false`, the body's AI session continues from where it paused (the
  session cursor is persisted across the pause). `$LOOP_PREV.*` refs on the first resumed
  iteration resolve to the prior iteration's real body outputs, restored from
  the persisted run history — the same as an un-paused next iteration would
  have seen.
- **Named owners after suspension**: terminal body waits and the deprecated
  interactive and approval gates persist private owner cursors independently of the ambient
  cursor. The next iteration continues each owner even with `fresh_context: true`.
  Missing or invalid owner state fails restoration. A wait that suspends again retains
  the same snapshot.
- **Same-iteration sessions after a nested pause**: a nested group's pause between a
  same-body source and its consumer resumes within the paused iteration, which re-runs
  the source. The consumer forks the re-run, matching the `$source.output` it reads.
- **Failure resume** (`/workflow resume <id>` after a crash/failure): there is
  no persisted iteration cursor — the loop_group node restarts from
  **iteration 1**. Per-body-node resume granularity is not supported in v1.

### What is NOT supported on loop_group nodes (v1)

- `until_field` / `output_format` — the group never calls the provider itself, so it
  has no payload of its own to read. Express the same thing with a body node that
  declares `output_format` plus `until_bash` reading `$node.output.field` — see
  [`until_field`](#until_field).
- `retry` (the loop manages its own iteration) — rejected at parse time.
- Cross-run `persist_session` for body AI nodes. In-run iterations use the ambient
  cursor governed by `fresh_context`, or explicit named owner cursors described above.
- Per-body-node resume (skip-to-failed-body-node) — the whole iteration re-runs.
- `$LOOP_PREV.<id>.output[N]` history indexing — only the immediately prior
  iteration is reachable.

Nested `loop_group` inside a `loop_group` body is supported by construction
(the body is a normal `nodes` array), but is not hardened in v1.

## Legacy interactive loops (deprecated)

`interactive: true` with `gate_message` on a `loop:` or `loop_group:`, the
`$LOOP_USER_INPUT` variable, and `signal_completes` still load and run unchanged, but the
loader warns once per workflow file, naming every node that uses them. Rewrite each one as
a `loop_group` whose body ends in a `wait: attention` node:

| Legacy | Replacement |
|---|---|
| `interactive: true` + `gate_message` | a terminal `wait: { attention: <message>, form: approval }` in the body (or another [form](/reference/workflow-language/#attention-waits)) |
| `$LOOP_USER_INPUT` | `$LOOP_PREV.<wait-id>.output.text` |
| approve with no comment to finalize | `until_bash` that checks the answer, e.g. `decision=$ask.output.decision; [ "$decision" = "approve" ]` |
| `signal_completes: true` | a `when:` on the wait so it pauses only when the work needs an answer, with `until_bash` deciding completion from the work |

See [Pausing to ask inside the body](#pausing-to-ask-inside-the-body) for both patterns and
[Migrating from approval gates](/guides/approval-nodes/#migrating-from-approval-gates) for
the other legacy pauses.

### `interactive` and `gate_message`

Runs already paused at a legacy loop gate stay answerable as before. The gate message
starts with a status line naming the completion channel that ended the iteration
(`✅ Completion condition met via ...`), or `⚠️ No completion condition met in this
iteration`, followed by an excerpt of the output and the `gate_message`.

- On a completed iteration, approving with no comment finalizes the node from the
  output already computed, with no re-run. Approving with a comment runs another
  iteration with the comment as `$LOOP_USER_INPUT`.
- On an iteration that did not complete, both forms run another iteration. On the final
  iteration (`max_iterations`), only a comment runs one more; approving with no comment
  fails the node with `max_iterations`.

The rule is the same on every surface: chat `/workflow approve <id> [comment]`,
`archon workflow approve <id> [comment]`, the HTTP endpoint (omit `comment` to
finalize), the console, and the `manage_run` chat tool (`accept: true` finalizes). A chat
message that asks the agent to approve passes your words as the comment, so it iterates;
see [Answering](/guides/approval-nodes/#answering). An orchestrating agent can read
`.metadata.approval.completionSignaled` from `archon workflow get <id> --json` before it
decides. `--json` records the decision without resuming; `archon serve`,
`archon workflow wake`, or `archon workflow resume` then continues the run.

A paused interactive loop resumes from the prompt template captured at the gate, so
editing the YAML or command file does not change a run that is already paused. The first
iteration after the resume sees the gated iteration's output as `$LOOP_PREV_OUTPUT`, and
`until_bash` sees it as `LOOP_PREV_OUTPUT`. A `loop:` node paused by an earlier Archon
release did not record that output, so its first resumed iteration sees both as empty.

### `signal_completes` — autonomous completion

By default a legacy interactive loop gates even on an iteration that met its completion
condition. `signal_completes: true` completes the node on that iteration instead, so the
gate appears only on iterations that did not complete. Setting it without
`interactive: true` has no effect and emits a loader warning. Replace it with a terminal
wait whose `when:` skips it on success, as in the second example under
[Pausing to ask inside the body](#pausing-to-ask-inside-the-body).

## See Also

- [Workflow language](/reference/workflow-language/): the full workflow reference
- [Per-Node Hooks](/guides/hooks/) — SDK hooks for command/prompt nodes
- [Per-Node MCP Servers](/guides/mcp-servers/) — external tool integration
- [Per-Node Skills](/guides/skills/) — skill preloading

---

--- METRICS ---
- Files Extracted: 57
- Estimated Token Budget: ~132559 tokens
- Canonical Reference: https://codewiki.google/github.com/coleam00/Archon
