diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 183cfed1d..c2b464407 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,20 @@ jobs: env: ORCA_INTEGRATION: '1' + # Builds the documentation site the way Read the Docs does, with warnings + # fatal, so a broken cross-reference or toctree entry fails here. + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + cache-dependency-path: docs/requirements.txt + - run: pip install -r docs/requirements.txt + - run: sphinx-build -W --keep-going -b html docs docs/_build/html + publish: needs: [test] if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags/v') diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..f42c1a505 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +version: 2 + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt + +build: + os: ubuntu-24.04 + tools: + python: "3.12" diff --git a/AGENTS.md b/AGENTS.md index 114c7f0ba..3cb5245c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,8 +2,8 @@ Internals, architecture, and coding conventions for hacking on the library itself. Build/test commands and the recipes for running a locally modified -orca live in [CONTRIBUTING.md](CONTRIBUTING.md); end-user documentation in -the [README](README.md). +orca live in [CONTRIBUTING.md](CONTRIBUTING.md); end-user documentation at +[orca.virtuslab.com](https://orca.virtuslab.com) (source in `docs/`). Orca is implemented in Scala 3 on top of [Ox](https://ox.softwaremill.com/) for structured concurrency, [tapir](https://tapir.softwaremill.com/) for @@ -151,7 +151,7 @@ most easily broken: turn here, that the tree holds only what earlier stages committed — the re-seeded case needs no telling, its preamble already says so. - The user surface is three rungs (README "Sessions"): `agent.run` (one-shot) + The user surface is three rungs (docs, "Talking to agents"): `agent.run` (one-shot) / `agent.chat()` (ephemeral `Chat`, fork-safe, `InStage`-only) / `agent.session(name, seed)` (durable `FlowSession`, flow-thread-only — the owner-thread assert on every `FlowSession` turn enforces it at runtime, and @@ -311,10 +311,10 @@ Three location classes decide what survives: | `.orca/cache/pi-sessions//` | cache | pi's own `--session-dir` transcripts | pi | `PiSessionStore` (resume probe), shell pi resume | `PiSessionStore.prune` after 30 days untouched | | `.orca/cache/lint-*.txt` | cache | lint output too large to inline in a prompt | `Lint` | the summarising agent | `lint`'s `finally` | | `.orca/cache/{,runs/,attempts/}...tmp` | cache | in-flight temp of an `OrcaFile` replace: beside a cache file, in `.orca/cache/` for a committed one (progress log, settings) so it is never committed | `OrcaDir.OrcaFile` | — (`AttemptManifestWriter`'s pruning skips dot-files) | the rename that completes the write | -| `.orca/worktrees//` (+ branch `orca-worktree-`) | worktrees | a `--worktree` run's checkout, with its own `.orca/` inside | `WorktreeRun` | `WorktreeScan` (shell) | never — see README | +| `.orca/worktrees//` (+ branch `orca-worktree-`) | worktrees | a `--worktree` run's checkout, with its own `.orca/` inside | `WorktreeRun` | `WorktreeScan` (shell) | never — see the docs, "Branches, resume and worktrees" | | `/.gemini/settings.json` | user tree | an `mcpServers.orca` entry for one interactive gemini turn | `GeminiSettings` | gemini | restored at turn end, and a stale entry from a crash dropped at the next interactive turn; a `.gemini/` orca created is removed when left empty | | `$TMPDIR/orca-*` (system prompts, claude MCP config, codex schema, pi extension) | temp | per-turn IPC files handed to a CLI on argv | each backend | the CLI | turn end | -| `$TMPDIR/orca-authoring-/` | temp | the authoring flow's sandbox repo; `.orca/cache/orca-api-/` inside holds the README + example flows (+ `fork-source/`) | `AuthoringSandbox`, `FlowAuthoring` | the coding agent | success or cancel; kept on failure, and nothing else prunes it | +| `$TMPDIR/orca-authoring-/` | temp | the authoring flow's sandbox repo; `.orca/cache/orca-api-/` inside holds the bundled docs + example flows (+ `fork-source/`) | `AuthoringSandbox`, `FlowAuthoring` | the coding agent | success or cancel; kept on failure, and nothing else prunes it | | `$XDG_CACHE_HOME/orca/shell//flows/` | XDG cache | built-in flows extracted from the jar | `BuiltInFlows` | `FlowCatalog`, scala-cli | never; nothing prunes older versions | | `$XDG_CACHE_HOME/orca/shell/workspace/` | XDG cache | scala-cli `--workspace` build state | scala-cli | scala-cli | never; nothing prunes it | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5fcccbda..7f1d4062a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,9 @@ How to build, test, and run a locally modified Orca. Internals, architecture, and coding conventions live in [AGENTS.md](AGENTS.md); end-user documentation -in the [README](README.md). **sbt 1.12+** is needed in addition to the -runtime requirements listed in the README. +at [orca.virtuslab.com](https://orca.virtuslab.com), with its source and build +instructions in [`docs/`](docs/README.md). **sbt 1.12+** is needed in addition +to the runtime requirements listed in the README. ## Build and test diff --git a/README.md b/README.md index e5064e6db..cb2f5e2d2 100644 --- a/README.md +++ b/README.md @@ -19,73 +19,59 @@ bootstrapped. Scala 3 looks like Python, but with types - so you get quick feedback if your flow script has any problems. Orca's development flows are resumable, so that if work is interrupted mid-flow -for any reason, it can be continued from the last commit. +for any reason, it can be continued from the last commit. You can use Orca to orchestrate development in any language and ecosystem. Orca assumes that it has configured, logged-in access to Claude, Codex, -OpenCode, or Pi (depending which backend you use), as well as `gh` and `git`. +OpenCode, Pi or Gemini (depending which backend you use), as well as `gh` and +`git`. -Install with one command, which installs `scala-cli` (via its official -installer) if you don't have it already, and writes the `orca` executable to -`~/.local/bin/orca`: +**Documentation: [orca.virtuslab.com](https://orca.virtuslab.com)** + +## Install + +One command installs `scala-cli` (via its official installer) if you don't have +it already, and writes the `orca` executable to `~/.local/bin/orca`: ```bash curl -fsSL https://raw.githubusercontent.com/VirtusLab/orca/master/install.sh | bash ``` -See [Orca Shell](#orca-shell) for the details and the full command-line -reference, or just run `orca` / `orca help`. - -## Three ways to work with Orca - -**Interactively**: install the CLI, run `orca`, pick a flow (`implement.sc` -comes first in the list) and enter your prompt. Non-interactively, use `orca run - ""`. See [Orca Shell](#orca-shell) for installation and the full -command-line reference. +## Start using -> [!WARNING] **Orca is designed to work in a sandboxed environment!** Coding -> agent tool usage is auto-approved by default (`tools = ToolSet.Full`, -> `autoApprove = AutoApprove.All`): write-capable turns let the agent edit files -> and run shell commands without prompting. This can be changed by changing the -> flow's options in code. Alternatively, use a VPS or local sandbox such as -> [Sandcat](https://github.com/VirtusLab/sandcat), [Docker -> Sandboxes](https://docs.docker.com/ai/sandboxes/), or any other. +Run `orca` in your repository. The first run asks which agent and model to use +for planning, coding and review. Then pick a flow (`implement.sc` comes first) +and enter your prompt. -**Driven by an agent (headless)**: a coding agent or harness invokes the CLI -non-interactively to implement a change, e.g. from CI or as a sub-task of another -agent: +The same, without the menu, for example from a coding agent or CI: ```bash orca run implement.sc "add a rate limiter to /login" ``` -Useful flags: `--branch ` (name the branch the run creates), -`--skip-branch` (continue on the current branch instead of creating one), -`--keep-changes` (leave uncommitted files in place instead of -stashing them) and `--worktree` (run in a git worktree of this repository -instead of the current checkout). - -In every mode, which agent (and model) handles the planning, coding, and review -roles comes from `settings.properties` — written for you by the shell's -first-run wizard or `orca config`, hand-editable too; see [Settings](#settings). +Orca creates a feature branch, plans the change into tasks, implements and +reviews each one, reviews the whole change, and opens a PR when the repository +is on GitHub. Each stage is committed as it finishes; if the run is interrupted, +run the same command again and it continues from the last commit. -Agents can load [`skills/orca`](skills/orca/SKILL.md) to know when -and how to delegate here; in Claude Code, `/orca [prompt]` asks which flow to run -and where, then starts it — installable as a Claude Code plugin, a Pi package, or -by symlinking into any harness's skills directory; see [its -README](skills/orca/README.md) for specifics. - -**As a script**: run a flow directly with `scala-cli`, no install required — see -[An example flow](#an-example-flow). +A flow is a scala-cli script, so it also runs with no install: ```bash scala-cli run --workspace "$(mktemp -d)" implement.sc -- "add a rate limiter to /login" ``` -## An example flow +> [!WARNING] **Orca is designed to work in a sandboxed environment!** Coding +> agent tool usage is auto-approved by default: write-capable turns let the +> agent edit files and run shell commands without prompting. Use a VPS or a +> local sandbox such as [Sandcat](https://github.com/VirtusLab/sandcat) or +> [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/), or narrow the +> agents' tools in the flow. -Save this as `implement.sc` and run it with your prompt: +## A flow + +Flows are ordinary Scala scripts. This one plans, implements each task with a +review, and opens a PR: ```scala //> using scala 3.9.0 @@ -94,1265 +80,38 @@ Save this as `implement.sc` and run it with your prompt: import orca.{*, given} -// Roles (planning / coding / review) come from settings.properties — -// per-project `.orca/settings.properties`, else ~/.config/orca/settings.properties, -// else claude for everything. Bodies can still name a concrete harness -// (`claude`, `codex.mini`, …) where a flow wants one — details under "Coding -// agent tools". flow(OrcaArgs(args)): - // `stage` is the committing, resumable unit of work. The plan is produced in - // one agentic turn and recorded in the progress log; a re-run with the same - // prompt skips this stage and reads the stored Plan back. val plan = stage("Plan"): - Plan.autonomous.from(userPrompt, planningAgent).value + Plan.autonomous.from(userPrompt, planningAgent).value - // One stage per task: each stage commits its work + a progress-log entry as - // one commit. Completed stages are skipped on resume — re-running the same - // prompt picks up from the first incomplete task. A session is keyed by the - // stage that mints it, so each task gets its own, seeded with the plan's - // brief (which primes it on first use, and is replayed if the backend session - // is lost on resume). for task <- plan.tasks do stage(s"Task: ${task.title}"): val session = codingAgent.session("implementer", seed = plan.brief) session.run(task.description) - reviewThenFix( - coderSession = session, - reviewers = allReviewers(reviewAgent), - // One review round, one fix turn. Reviewers are picked by a picker LLM - // on reviewAgent.cheap (see "Review utilities"); format and lint - // default to the project's stack settings - // (`.orca/settings.properties`, auto-discovered on first run) — see - // "Settings" below. The whole task goes in: reviewers are shown its - // title and description, plus the run's prompt, each labelled. - task = task - ) + reviewThenFix(coderSession = session, reviewers = allReviewers(reviewAgent), task = task) - // Each task's single pass took the fixer's word for its own fixes; this loop - // over everything the run changed is what checks them. val openFindings = stage("Final review"): + val finalFixer = codingAgent.session("final-fixer", seed = plan.brief) reviewAndFixLoop( - coderSession = session, + coderSession = finalFixer, reviewers = allReviewers(reviewAgent), task = Task(Title("The whole planned change"), plan.brief), - diff = ReviewDiff.WholeRun, - maxFixTurns = 5 + diff = ReviewDiff.WholeRun ) - // Best effort: opens a PR when the checkout is on a GitHub `gh` can reach, - // and says why in one line when it isn't. What the loop left open is listed - // in the PR body. - openPrIfGitHub( - summarisingAgent = codingAgent.cheap, - openFindings = openFindings - ) -``` - -```bash -scala-cli run --workspace "$(mktemp -d)" implement.sc -- "Add a rate-limiter to the /login endpoint" -``` - -Each flow starts by creating a feature branch, named by a short -cheap-model-generated label derived from the prompt (slugged; pass `branchNaming -= ...` to override, or `--branch ` to name it yourself). On success the flow opens a PR when the repository is on a -GitHub `gh` can reach, and hands you back the branch you started on — the work -is on the PR. Otherwise it says so in one line and leaves you on the feature -branch, ready to test or open a PR by hand — see [The flow -lifecycle](#the-flow-lifecycle) for the full success/failure/resume behavior. - -If the flow is interrupted — user intervention or an intermittent error — just -run the same command again: it resumes from the last committed set of changes, -so only a small amount of work is repeated. Orca borrows ideas from durable -computing: which stages have completed, and with what results, is tracked in a -progress file committed alongside the modified code, making commits the unit of -atomicity — the progress log can't drift from the changes in the repository. -When the flow is done, the progress log is removed from the branch in one last -commit, which is pushed too if the flow had already pushed the branch. - -There are two runnable examples under -[`examples/runnable/`](examples/runnable/): -* [01-simple](examples/runnable/01-simple/) (in-memory plan + review, autonomous - planner), -* [02-interactive](examples/runnable/02-interactive/) (same shape as 01, but the - planner can ask clarifying questions via `ask_user`). - -More flow scripts — `issue-pr.sc`, `issue-pr-bugfix.sc`, -`implement-enhanced.sc`, `review.sc` — live in [`flows/`](flows/); run them -against your own git repo. - -For convenient editing of Orca flow scripts, with code-completion, you can try -the [Metals](https://scalameta.org/metals/) VSCode extension. - -## Vocabulary - -The words this README uses. Orca's internals have their own, in -[AGENTS.md](AGENTS.md#conventions). - -**Flows and runs** - -- **flow** — a Scala script whose body is `flow(OrcaArgs(args)): ...`; see [An - example flow](#an-example-flow). -- **flow args** — `OrcaArgs`: the prompt and the command-line flags; see - [Command-line usage](#command-line-usage). -- **prompt** — the user's input text, `userPrompt` in a flow body. -- **stage** — `stage(name)(body)`: a unit of work that commits on completion and - is skipped on resume; see [Flow methods](#flow-methods). -- **plan task** — one `orca.plan.Task` of a `Plan`. The **plan brief** - (`Plan.brief`) is the planner's codebase briefing; see [Data - structures](#data-structures). -- **run** — one prompt's flow execution, across every process it takes to - finish; see [The flow lifecycle](#the-flow-lifecycle). -- **attempt** — one of those processes: one `orca run`, one `flow(...)` call. -- **re-run / resume** — another attempt of an unfinished run, with the same - prompt; it skips the stages already recorded. -- **progress log** — `.orca/runs/.progress.json`, committed with each - stage: which stages finished, and their results. -- **run target** — where a run works: a new branch (the default), the current - branch (`--skip-branch`) or a worktree (`--worktree`). -- **worktree** — a second checkout of the repository under `.orca/worktrees/`. - -**Agents and conversations** - -- **harness** (also **backend**) — the coding-agent CLI orca drives: `claude`, - `codex`, `opencode`, `pi` or `gemini`. -- **agent** — a harness with a model and tool settings (`claude`, `codex.mini`, - `codingAgent`, …); see [Built-in tools](#built-in-tools). -- **role agent** — `planningAgent`, `codingAgent` or `reviewAgent`, resolved - from settings; see [Coding agent tools](#coding-agent-tools). -- **cheap tier** — `agent.cheap`: the harness's cheaper model; see - [Sessions](#sessions). -- **turn** — one prompt to an agent and its reply. -- **one-shot / chat / session** — `agent.run` (one turn), `agent.chat()` (a - conversation for this attempt) and `agent.session(name, seed)` (a - conversation that survives resume). A **conversation** is the history the - harness keeps across turns. [Sessions](#sessions) says which to use. -- **session name / session key** — the name is the session's role - (`"implementer"`), which `orca continue` matches; the key is the name plus the - stage the session is created in. -- **seed / re-seed** — the context a session starts from, usually the plan - brief. A session whose conversation is lost is re-seeded: started again from - its seed; see [Sessions](#sessions). -- **structured output** — `resultAs[O]`: a reply parsed into an `O`, which needs - a `JsonData[O]`; see [Built-in tools](#built-in-tools). - -**Review** - -- **reviewer** — a reviewer prompt paired with a read-only agent. -- **reviewer catalog** — `reviewerCatalog`: every reviewer a run can use; see - [Settings](#settings). -- **roster** — the reviewers one review call is given. -- **review round** — one pass of the picked reviewers, the lint gate and any - checks over the change; see [Review utilities](#review-utilities). -- **fix turn** — the coder session's `.run` that fixes a round's findings; - `maxFixTurns` caps how many. -- **finding / declined / open finding** — a problem a reviewer, the lint gate or - a check reported; one the fixer refused, with a reason; one the review ends - without resolving. See [Data structures](#data-structures). -- **gate** — a stack command: `format`, `lint` or `test`. The lint gate runs - each review round. -- **stack settings** — the project's gate commands, from - `.orca/settings.properties`; see [Settings](#settings). -- **`Configured`** — how a review call takes a gate: from settings (the - default), off, or a given value; see [Review utilities](#review-utilities). -- **`OpenFindings`** — what a review returns: the findings it left open; see - [Data structures](#data-structures). - -**Safety** - -- **capability** — a compile-checked token a call needs: `InStage` (agent - calls) and `WorkspaceWrite` (git, `gh` and file writes) come from a - `stage(...)` body, `FlowControl` (starting stages, minting sessions) from the - `flow(...)` body. `FlowContext` (reads) is not one. See - [capabilities](#experimental-capabilities--compile-time-concurrency-checking). -- **`ToolSet`** — which tools an agent has: `ReadOnly`, `NetworkOnly`, `Full` - (the default) or `NoTools`. **Enforcement** is how strictly each harness holds - that limit; see [Coding agent tools](#coding-agent-tools). - -## Built-in tools - -The following are available inside a `flow(...) { ... }`. - -The five coding agents — `claude`, `codex`, `opencode`, `pi`, `gemini` — share -one call surface. Durable: `session(name, seed): FlowSession` → -`.run(prompt)` / `.resultAs[O].run(input)`. One-shot: `run(prompt)`, -`resultAs[O].{autonomous,interactive}.run(input)`. Ephemeral multi-turn: -`chat(): Chat` → `.run(prompt)` / `.resultAs[O]...run(input)`. Common tuning: -`withModel`, `withCheapModel`, `withAutoApprove`, `withSystemPrompt`, `withName`, -`withReadOnly`, `withNetworkOnly`, `withSelfManagedGit`. The table lists each -backend's model accessors and backend-specific extras: - -| Tool | Backend-specific methods | Purpose | -|---|---|---| -| `claude` | `haiku`/`sonnet`/`opus`/`fable`, `cheap` (→ haiku), `withModel(Model)`, `withNetworkTools` | Claude Code coding/reviewing agent. Bare `claude` is **Opus with the 1M-token context window** (the coder; reviewers share it); use `claude.sonnet`/`claude.haiku` for cheap one-shot calls, or `claude.fable` for the hardest ones. `interactive` mode lives only on `resultAs[O]`. See [Sessions](#sessions) for durable (`session`) vs ephemeral (`run`/`chat`). | -| `codex` | `mini`, `cheap` (→ mini), `withModel(Model)` | OpenAI Codex coding/reviewing agent. Bare `codex` pins **GPT-6 Sol** (needs a codex CLI that offers it); use `codex.mini` (GPT-6 Luna) for cheap one-shot calls. | -| `opencode` | `anthropicOpus`/`anthropicSonnet`/`anthropicHaiku`, `openaiAstra`/`openaiSol`/`openaiLuna`, `cheap` (provider-matched: openai→luna, else anthropicHaiku), `withModel(providerModel)` / `withModel(provider, modelId)` | [OpenCode](https://opencode.ai) coding/reviewing agent, driven over HTTP+SSE against a headless `opencode serve` (started lazily, shared for the attempt; sessions survive it — see [Sessions](#sessions)). Spans providers, so models are provider-qualified: use an accessor (`opencode.openaiLuna`) or `opencode.withModel("openai/gpt-5-mini")` / `opencode.withModel("ollama", "llama3.1")`. Inherits the user's configured `opencode` providers/auth. | -| `pi` | `withModel(Model)` | [Pi](https://pi.dev/) coding agent backend, driven through `pi --mode rpc`. Pi handles provider/model selection through its own CLI configuration; pin a model with `pi.withModel(Model("provider/model"))`. Interactive calls can ask clarifying questions via Orca's `ask_user` bridge. | -| `gemini` | `flash`, `cheap` (→ flash), `withModel(Model)` | Google Gemini CLI coding/reviewing agent, driven via `gemini --output-format stream-json`. Bare `gemini` pins **Gemini 3.1 Pro (preview)**; use `gemini.flash` (Gemini 3.8 Flash) for cheaper one-shot calls. Structured output is prompt-enforced (Gemini has no schema flag); `withReadOnly` maps to `--approval-mode plan`. See [ADR 0015](adr/0015-gemini-stream-json-driver.md). | -| `git` | `push`, `head`, `headCommit`, `isAncestorOfHead`, `branchExists`, `isIgnored`, `uncommittedDiff`, `changedFiles`, `reviewChanges`, `pendingChanges`, `diffVsBase`, `defaultBase`, `show`, `fileAt` | Git reads against the working tree, plus `push`. The runtime owns the run's branch and commits, so branch switching and committing are not on `git`. Commits and branch names are typed (`orca.gitref.CommitHash`, `orca.gitref.BranchName`); `head` answers the branch HEAD is on or the commit it is detached at (`orca.gitref.Head`). Recoverable failures (`NoDefaultBase`, `PushFailure` — `NonFastForward`/`RemoteDeclined`, `GitReadFailed`) surface as `Either`; `.orThrow` converts a `Left` back to an exception when the case is unexpected. `uncommittedDiff` covers the whole repository minus `.orca/` bookkeeping, tracked files only, and is empty once the work is committed — `diffVsBase` is the branch-wide view. `reviewChanges` is what `reviewAndFixLoop` hands reviewers: that diff plus the contents of files new to the repo, together with the list of every path in the change set, how much of each changed, and each file's own part of the diff. It takes an optional commit to compare against (`headCommit` reads one) so work already committed still shows up. `changedFiles` is the path list on its own, for a consumer gating on file names — the diff text alone names neither a binary change nor a rename, and leaves a trailing tab on a path containing a space. `pendingChanges` describes what the next commit will include: a `--stat` summary, the new files, and the diff. `isIgnored` answers `false` when git cannot answer. | -| `gh` | `availability`, `createPr`, `updatePr`, `readIssue`, `readIssueComments`, `readPrComments`, `writeComment(pr, body)` / `writeComment(issue, body)`, `upsertComment(pr, marker, body)` / `upsertComment(issue, marker, body)`, `buildStatus`, `waitForBuild` | GitHub PR + CI integration via the `gh` CLI. `availability` is a read-only probe of whether a PR can be opened from this checkout, answering with a [`GitHubAvailability`](#data-structures). `createPr` is idempotent by branch (returns the existing PR if one is open); `upsertComment` finds a prior comment carrying `marker` and edits it in place (see [Authoring rules](#authoring-rules) for the re-run pattern). `updatePr` replaces a PR's title + body. `waitForBuild` returns `Either[BuildWaitFailed, …]`. | -| `fs` | `read`, `write`, `list` | Working-tree file I/O. `read` returns `Option[String]` so a missing file is a branch point, not an exception. `write` refuses a path outside the working tree or under `.orca/runs`, `.orca/cache` or `.orca/worktrees`. | - -The runtime owns git: every write-capable agent turn is told not to commit, -push, or switch branches — it edits the working tree; the runtime commits each -stage and owns the run's branch, and the flow pushes via `git.push`. Opt out per-tool with -`claude.withSelfManagedGit`. - -For the LLM interfaces, `resultAs[O]` defines the shape of the structured -output. The `O` type needs a `JsonData[O]` (provided by `derives JsonData` on a -case class) for schema generation and deserialization. A parameterless enum that -`derives JsonData` travels as its case name, and the schema lists every name; a -sum type whose cases carry fields cannot be an `O`. Additionally, you might -define an `Announce[O]` so that a friendly summary is printed in the event log, -instead of a raw json. - -A minimal Pi-backed flow looks the same; Pi reads your normal Pi configuration: - -```scala -flow(OrcaArgs(args)): - stage("Run"): - val session = pi.session("run", seed = userPrompt) - session.run(userPrompt) -``` - -## Coding agent tools - -There are two ways to drive a model in a flow: - -- **The role agents — `planningAgent`/`codingAgent`/`reviewAgent`.** - Backend-agnostic: each is resolved from settings (see [Settings](#settings)), - defaulting to claude. Use `planningAgent` for `Plan.*` calls, `codingAgent` - for the implementer's durable session, and `reviewAgent` for - `allReviewers(...)` and the review machinery's defaults. Edit settings and the - whole flow follows; you never name a backend in the body. -- **A specific agent + model — `claude.opus`, `codex.mini`, - `opencode.openaiLuna`.** Use a concrete accessor when you want a particular - backend or tier regardless of settings — say `claude.opus` for a step that - must have the strongest model even where the coding role is a cheaper backend. - None of the shipped flows do this; they all follow the roles. The tier - accessors (`.opus`/`.sonnet`/…) live on the concrete agents, not on the role - accessors — so `codingAgent.opus` won't compile; that's the cue to name the - backend. Pin any other model with `withModel(Model("…"))`. - -Two axes constrain an agent. **`ToolSet`** is which tools exist -at all: - -```scala -// ReadOnly — reads only, no shell, no edits (reviewers, plan review, brief). -val reviewer = claude.withReadOnly - -// NetworkOnly — reads plus read-only network (web, and on claude a host-served -// GitHub issue/PR read), for planners that must read an issue/PR. How strongly -// each backend blocks edits varies — see the enforcement matrix in AGENTS.md. -val planner = claude.withNetworkOnly - -// Full (the default) — write-capable. -``` - -**Prompting** (`autoApprove`) is which of the available tools auto-approve -without a y/n prompt — only meaningful for interactive turns, and consulted only -on `Full`: - -```scala -// Restrict auto-approval to a named tool set (honoured by claude). -val limited = - claude.withAutoApprove(AutoApprove.Only(Set("Read", "Edit", "Grep"))) -``` - -`AutoApprove.Only` fits interactive flows, where a human answers anything -outside the set; an autonomous turn has no one to approve, so an out-of-set call -blocks. Only claude enforces the set per tool — codex and gemini have no -per-tool granularity, so there `Only` widens to full auto-approve. For an -unattended run the practical boundary is a sandbox: -[Sandcat](https://github.com/VirtusLab/sandcat), [Docker -Sandboxes](https://docs.docker.com/ai/sandboxes/), or any other. - -## Your own agent setup - -Orca's agents are ordinary harness sessions — `claude`, `gemini`, `codex`, -`opencode` or `pi` — started in your repository. They load the same instruction -files (`~/.claude/CLAUDE.md`, `CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md`, -`GEMINI.md`, …), MCP servers, plugins and hooks as your own sessions. - -Autonomous turns have no one to answer prompts (see above), so: - -- Coding turns auto-approve everything by default (`withAutoApprove` - narrows it). -- On claude, read-only roles (planner, reviewers, reviewer picker) deny any - tool outside orca's list, your MCP tools included, unless your claude - settings `permissions.allow` them. -- On claude, opencode and pi, cheap one-shots (branch names, default commit - messages) run with no tools and no MCP servers. - -Check your instructions for: - -- **Mandatory tool calls.** "Always call X first" needs X allowed (see above), - or write "if available". -- **A human in the loop.** In autonomous flows, "ask me before X" or "wait for - confirmation" cannot work: no one is there to answer. - -## Flow methods - -Top-level, available via `import orca.*`: - -| Method | Signature | Use | -|---|---|---| -| `flow(args, ...)(body)` | `flow(args: OrcaArgs, branchNaming?, stackSettings?, planningAgent?, codingAgent?, reviewAgent?)(body)` | Entry point. Creates one feature branch + one progress log for the run. The three role agents (below) resolve from settings — see [Settings](#settings) — defaulting to claude; `planningAgent`/`codingAgent`/`reviewAgent` here are per-role programmatic overrides (`Some(_.claude.opus)`) that win over both settings files. Branch naming defaults to a short cheap-model-generated label (slugged); pass `branchNaming = Some(BranchNamingStrategy.issue(handle))` to override (e.g. for issue flows). `stackSettings = Some(StackSettings(...))` pins the run's [stack settings](#settings) — the settings file's stack portion is then neither read nor written (the escape hatch for a language-specific flow; its agent keys are still honoured). See [The flow lifecycle](#the-flow-lifecycle) for the full branch/teardown behavior. | -| `planningAgent` (in-body accessor) | `planningAgent: Agent[?]` | The planning-role agent, resolved from settings — see [Coding agent tools](#coding-agent-tools). Hand it to `Plan.*`. | -| `codingAgent` (in-body accessor) | `codingAgent: Agent[?]` | The coding-role agent — the run's primary: implementer sessions, branch naming, stack discovery, default commit messages. | -| `reviewAgent` (in-body accessor) | `reviewAgent: Agent[?]` | The review-role agent: `allReviewers(reviewAgent)`, the reviewer-picker and the lint summariser default to its tiers. | -| `stage[T: JsonData](name, commitMessage?)(body)` | `(name: String, commitMessage: Option[T => String] = None)(body): T` | The committing, resumable unit of work. On success, records the result, force-adds the progress log, and commits (code changes + log delta = one commit). On re-run, a stage whose result is still recorded is skipped and the stored value is returned. `T` must have `JsonData` — `case class Foo(...) derives JsonData` is enough. Commit message defaults to a `codingAgent.cheap` summary of the diff; override via `commitMessage`. | -| `display(message)` | `(message: String): Unit` | Progress-only output: no stage, no commit, no log entry. Callable anywhere — outside a stage or inside a fork. | -| `Par.mapUnordered(n)(items)(f)` | `(parallelism: Int)(items: Seq[A])(f: A => R): List[R]` | The sanctioned script fan-out (no Ox import needed). Ephemeral agent turns (`codingAgent.run`, `chat.run`) work inside `f`; the durable, flow-thread-only operations (`stage`, `codingAgent.session`, `session.run`) throw if called from a fork. Results arrive in completion order. | -| `fail(message)` | `(message: String): Nothing` | Abort with a message. Triggers failure teardown: stays on the feature branch so a re-run resumes. | - -### Overriding tools and agents - -Any tool (except `git`) or agent `flow(...)` builds by default can be replaced -by a named argument. Plain tools take the value directly (`gh = Some(myGh)`, `interaction -= Some(myInteraction)` — your own `orca.backend.Interaction` implementation, -e.g. for Slack; not exported from `orca.*`, so import it by its full path). -Agents take a **factory** that receives the attempt's `AgentWiring` (event sink, -interaction, workDir, prompts), so a tuned agent lands on the same dispatcher -as the defaults: - -```scala -flow(OrcaArgs(args), claude = Some(w => ClaudeAgents.default(w).opus)) -``` - -Factories exist for all five backends: `ClaudeAgents.default(w)`, -`CodexAgents.default(w)`, `GeminiAgents.default(w)`, `PiAgents.default(w)`, and -`OpencodeAgents.default(w, launcher)` — opencode's factory is applied where the -run's `Ox` scope exists (it pins a shared `opencode serve` to the scope), so its -slot is typed `AgentWiring => Ox ?=> OpencodeAgent`. - -`git` has no override because the runtime owns the run's branch and commits -through it. The backend SPI is internal: a new coding-agent harness is added as -an orca module. - -### Side effects happen inside stages - -Every side-effecting call — `git.push`, -`fs.write`, `gh` writes, every `agent.*.run` — must happen inside a `stage` -body, and **the compiler enforces it**: a mutation outside a stage doesn't -compile. Pure reads (`git.uncommittedDiff`, `git.changedFiles`, `gh.readIssue`, -`gh.availability`, `fs.read`), -`display`, and `fail` run anywhere; `agent.session(name, seed)` runs inside or -outside a stage — it records a session, not a side effect. Where to -*place* effects is covered by the [Authoring rules](#authoring-rules). - -### The flow lifecycle - -Two words this section leans on: a **run** is one prompt's flow execution, across -however many processes it takes to finish it; an **attempt** is one of those -processes — one `orca run`, or one `flow(...)` call. An interrupted run is -resumed by attempting it again with the same prompt. - -Each run is bound to exactly one feature branch and one progress log -(`.orca/runs/.progress.json`, where `` is derived from the prompt): - -- **Start:** stash a dirty working tree with a warning (recover with `git stash - pop`); create + checkout the feature branch; write and commit the progress log - header. `--branch ` (`OrcaArgs.branch`) names that branch, winning - over `branchNaming`; a protected or already existing name is refused rather - than renamed. The three flags below reach a flow as one `OrcaArgs.target` - (`RunTarget`), which has no case for a combination orca refuses. A script can - also set that field itself — `flow(OrcaArgs(args).copy(target = - RunTarget.Worktree))` — which overrides whatever the flags said. - `--skip-branch` (`RunTarget.CurrentBranch`) binds the run to the CURRENT - branch instead of creating one — for continuing work already planned on a - branch — refusing on a protected branch or detached HEAD. On a FRESH - `--skip-branch` run a dirty tree is tolerated, not stashed: uncommitted or - untracked files (e.g. plan files left by a planning harness) stay in place for - the flow, and get swept into the first stage's commit. `--keep-changes` - (`Uncommitted.Keep` on either branch case) does the same on a FRESH run in - either branch mode — in normal mode the files survive branch creation and - reach the new branch in that first stage commit. With neither flag, a dirty - tree on a fresh run is put to the user: stash (the default), keep, or abort; - with no terminal to ask, it stashes. A run that already has a progress log — - a resume, or one too broken to read — always stashes and ignores - `--keep-changes`, so an interrupted stage's partial work can't leak into the - stage that re-runs. - `--worktree` (`RunTarget.Worktree`) runs the whole flow in - `.orca/worktrees/` of this repository — a second checkout, keyed on the - same prompt hash as the progress log, created on the first attempt and reused by - every later attempt for that prompt. It isolates the run: two runs never share a - checkout or a branch. Uncommitted work does NOT come along — a worktree is - made from a commit — so `--worktree` is refused with - `--skip-branch` and with `--keep-changes`: `RunTarget.Worktree` carries - neither a branch mode nor an `Uncommitted`, so the pair is refused while argv - is parsed and has no representation after that. The first attempt in a worktree - pays a cold build (no build outputs, no dependencies, none of the untracked - local config a project may need), an editor or indexer that ignores - `.gitignore` will see the second checkout, and orca never removes it. The run - starts on an `orca-worktree-` branch orca also never deletes, so full - cleanup is `git worktree remove .orca/worktrees/` **and** `git branch -d - orca-worktree-`; a re-run of the prompt refuses rather than moving that - branch if it has gained commits since. -- **Resume:** a re-run with the same prompt finds the progress log and resumes - from the first incomplete stage (a `--branch` naming a different branch than - the log's is refused). It says once which branch it bound, how many - stages are already recorded, and that the interrupted stage's uncommitted work - was not carried over; every durable session it re-enters through `session.run` - is told the same — a re-seeded one in its preamble, a still-live one once, on - its first turn (a conversation continued through `session.chat` is not - told). A corrupt or truncated progress log is detected at startup — orca - warns and starts fresh (previous stages re-run) rather than silently - mis-resuming. -- **Success teardown:** remove the progress-log file in a final commit, and push - it when the remote branch still carries the log (i.e. the flow pushed). A - throwaway feature branch (no substantive changes vs the starting branch) is - deleted and HEAD returns to the starting branch. Otherwise the feature branch - is kept, and where HEAD lands follows the run: a run that created a branch and - **opened a PR** hands you back the branch you started on (the work is on the - PR). Every other run leaves you where you were — on the feature branch when no - PR was opened or under `--skip-branch`, and untouched under `--worktree`, - where the work is in the separate checkout the summary names. The run then - closes by naming the branch you are left on, the PR it opened if it opened - one, how many files changed since the commit it started from, and the `git - diff` that shows them. -- **Failure teardown:** while HEAD is on the feature branch, discard the - failed stage's uncommitted partial edits — - `git reset --hard` for tracked files, plus `git clean -fd` for the files it - newly created; stay on the feature branch so a re-run resumes in place. - Gitignored paths and `.orca/` are never removed. Whether the clean runs at - all is decided once, at setup, for the whole run: a FRESH run that kept a - dirty tree instead of stashing it (`--skip-branch`, `--keep-changes`, or the - interactive keep answer) leaves orca unable to tell those files apart from the - run's own — no untracked file is deleted, in any stage, including ones the - failed stage created. Kept edits to tracked files that no stage has - committed yet are put back after the reset; a re-run stashes them before it - resumes. When the failed body left HEAD elsewhere, teardown touches nothing - and says so. - -### Settings - -Two files, both plain `key = value` lines, parsed once per attempt before setup: - -- **`{workDir}/.orca/settings.properties`** — committed, hand-editable project - settings: the stack commands (`format`/`lint`/`test`) and, per role, which - agent to use. -- **`$XDG_CONFIG_HOME/orca/settings.properties`**, defaulting to - `~/.config/orca/settings.properties` (also on macOS) — a per-user default, - agent keys only. An absent global file is simply skipped. - -Precedence, code always winning over files: - -- **Roles:** `flow(planningAgent = ...)` (and `codingAgent`/`reviewAgent`) - programmatic override > project file > global file > built-in default (claude, - no model pin). -- **Stack commands:** `reviewAndFixLoop(formatCommands = Use(...)/Off)` > - `flow(stackSettings = Some(...))` > project file > auto-discovery (which - writes the file). - -An unreadable or malformed file — project or global — aborts the run before any -tree mutation; the global file may contain ONLY agent keys, so a stack key there -is also an error. - -**Stack commands.** Keys `format`, `lint`, and `test`. Each value is one shell -command, run via `bash -c` in the flow's working directory; everything after the -first `=` is command text (`lint = FOO=bar cargo check` works). Repeating a key -appends: its commands run in file order, so a multi-stack repo lists one -line per stack half. A key's value may also be the literal `off`, which -explicitly disables that gate; a missing key has the same runtime effect (the -gate is skipped) but, unlike `off`, does not count as "configured" — see -Auto-discovery below. `#` lines are comments; commenting out a line is the same -as deleting it. A typical discovered project file: - -```properties -# orca settings — edit freely, commit with the project. -# format/lint/test: one shell command per key; `off` disables the gate. Delete the stack lines (or the whole file) to re-run auto-discovery. -# planningAgent/codingAgent/reviewAgent (harness[:model]): override the global settings file; a flow's own code overrides both. -# Cargo.toml; via rustfmt -format = cargo fmt -# Cargo.toml -lint = cargo check --tests -# no test config found -test = off -``` - -**Agent keys.** `planningAgent`, `codingAgent`, and `reviewAgent`, valid in both -files, single-valued (a repeated agent key is an error). Value grammar: -`harness[:model]`, split at the first `:` so a model id containing `:` survives; -`harness` is one of `claude`, `codex`, `opencode`, `pi`, `gemini` (an -unrecognised name is an error naming the valid set). The model part is passed -**verbatim** to the harness's `withModel` — orca does not normalise or validate -model ids, except that claude's bare `haiku` alias is sent as -`claude-haiku-4-5`, so a `claude:haiku` pin cannot land on a pricier tier when -the CLI resolves the alias. For example: - -```properties -planningAgent = claude:opus -codingAgent = codex:gpt-5-mini -reviewAgent = opencode:anthropic/claude-haiku-4-5 -``` - -Agent keys are read even when `flow(stackSettings = Some(...))` overrides the -stack commands — that override governs the stack portion only, and a malformed -project or global file still aborts the run either way. `setup` announces the -resolved roles and where each came from: - -```text -agents: planning=claude:claude-opus-5-5[1m] (default), coding=codex:gpt-5-mini (project), review=opencode: (global) -``` - -`` marks a role where nothing pins a model, so the harness -picks one itself. - -**Auto-discovery.** Discovery runs when the project file is absent or has no -stack line; discovered entries are appended below any existing content, so -agent lines are never touched. Delete the stack lines (or the whole file) to -re-run it. Discovery spends one cheap-model, read-only agent call inspecting -the repo, then writes the file and announces every guess in the event log: - -```text -no .orca/settings.properties — discovering how to format, lint & test this project - format = cargo fmt # Cargo.toml; via rustfmt - lint = cargo check --tests # Cargo.toml -warning: stack settings: no test command — gate disabled -written to .orca/settings.properties — review and edit as needed. -``` - -Runs with an existing, stack-complete file — the steady state, including CI — -make no model call. - -**Reviewer prompts.** Reviewers come from three tiers, read once per attempt before -setup like the settings files: - -- **`{workDir}/.orca/reviewers/*.md`** — committed project reviewers. -- **`$XDG_CONFIG_HOME/orca/reviewers/*.md`**, defaulting to - `~/.config/orca/reviewers/` — your own, across every project. -- The eight reviewers orca ships with. - -A reviewer's identity is its filename stem — `.orca/reviewers/orca.md` is the -reviewer `orca` — compared case-insensitively. A file whose stem matches a -lower tier replaces it, keeping its position in the catalog; anything else is -appended, sorted by name. Project beats global beats built-in, so -`.orca/reviewers/scala-fp.md` retunes the shipped `scala-fp` for this project -without changing how many reviewers run. A reviewer that adds a new name joins -both `allReviewers` and `minimalReviewers`; one that shadows a shipped reviewer -runs wherever that shipped reviewer runs, so shadowing `scala-fp` leaves -`minimalReviewers` — correctness, clarity, tests — alone. The picker narrows -per task as usual. - -Each file is frontmatter plus a body, the same shape the shipped ones use: - -```markdown ---- -description: Checks the project's own layering rules. -files: \.scala$ ---- - -## Scope - -Review only the layering of the changed files... -``` - -`description:` is required and must be a single line — the reviewer-picker -decides from it. The value is the rest of that line: a YAML block scalar (`>`, -`|`, `>-`, `|-`) or a value wrapped onto the next line aborts the run. -`files:` is optional: a regex matched against each changed path, so the -reviewer is only offered when the change touches a file it applies to. The body -is the reviewer's system prompt. A `name:` key, if present, is ignored. - -`README.md` and any `_`-prefixed name sit in the directory as documents. Every -other `.md` must parse as a reviewer: a missing or unterminated frontmatter -block, a missing `description:`, an empty body, an invalid `files:` regex, or -two files claiming one name abort the run before any tree mutation, naming every -bad file at once — a reviewer silently dropped from the catalog would read as a -clean review. A symlinked prompt aborts too, but only in `.orca/reviewers/`: -that directory is committed and orca runs against repos it did not write, while -the global tier is your own config home and is read through links like -`settings.properties` beside it. When a tier contributes -anything, the run says so: - -```text -discovered reviewers: orca (project); scala-fp (project, shadows built-in) -``` - -
-Discovery internals and the .orca/ directory - -Every discovered command cites the file that evidences it, and two checks run -before the file is written: the command's executable must be on `PATH`, and the -cited evidence file must exist. A command failing either is kept only as a -comment (`# skipped: lint = just check (just: not found on PATH)`), never run -silently; a key left with no command gets a live `key = off` line. A discovery -failure (backend unavailable, invalid output) aborts the run rather than writing -a "gates off" file. - -`.orca/` is committed by default: settings and each run's progress log -(`runs/.progress.json`) ride the branch, while machine-local state lives -under `.orca/cache/`, which writes its own `.gitignore`: each run's durable -session records (`runs/.sessions.json`, whose backend ids mean nothing in -another checkout), and per attempt a manifest -(`attempts/.manifest.json`, what `orca continue` lists) and a cost log -(`attempts/.cost.jsonl`: one line per agent turn with agent, role, model, -stage, token usage and cost — the per-agent and per-model detail the closing -summary leaves out). The cache is safe to delete; only the newest 20–40 -attempts are kept. If -your `.gitignore` covers all of `.orca/`, every attempt warns to remove that line so -settings can be committed — the cache stays ignored on its own. - -
- -Within a flow body the resolved stack settings are available as -`summon[FlowContext].stackSettings` — a `StackSettings(format, lint, test: -List[String])`. The `test` commands are not consumed by `reviewAndFixLoop` (the -lint gate stays deliberately cheap); they're there for a flow's own verification -stages. - -### Sessions - -Every way to talk to an agent, by what the conversation must do: - -| Call | Conversation | Survives crash/resume | Mode | Output | Needs | In a fork | -|---|---|---|---|---|---|---| -| `agent.run(prompt)` | new, one turn | no | autonomous | text | `InStage` | yes | -| `agent.resultAs[O].{autonomous,interactive}.run(input)` | new, one turn | no | both | `O` | `InStage` | yes* | -| `agent.chat()` → `chat.run(prompt)` / `chat.resultAs[O].{autonomous,interactive}.run(input)` | new, then continued by every turn | no | both | text or `O` | `InStage` | yes* | -| `agent.session(name, seed)` → `session.run(prompt)` / `session.resultAs[O].run(input)` | named; continued, or re-seeded if lost | yes | autonomous | text or `O` | `FlowContext`, `FlowControl`, `InStage`, `WorkspaceWrite` | no | -| `session.chat` → as `Chat` | the session's; refused while the backend doesn't hold it (never run, or lost on resume) | no (turns not recorded) | both | text or `O` | `InStage` | yes* | -| `Plan.{autonomous,interactive}.*` → `WithChat`; `.reviewed()`, `.chat` | new planning conversation, continued by `.reviewed()` and `.chat` | no | as named | `O` | `FlowContext`, `InStage` | yes* | -| `reviewAndFixLoop` / `reviewThenFix` | new reviewer chats; continues `coderSession` | the coder session does | autonomous | findings | `FlowContext`, `FlowControl`, `InStage`, `WorkspaceWrite` | no | -| `lint(commands, agent)` | new (or continues a `Lint.summariser`) | no | autonomous | `ReviewResult` (`LintReport` with a summariser) | `FlowContext`, `InStage` | yes | - -\* Interactive turns share your terminal: run them one at a time, never from -parallel forks. - -Use: - -- **`agent.run` / `agent.resultAs[O]`** for a one-shot question. -- **`agent.chat()`** for follow-ups within this attempt, including inside forks - (each fork mints its own). -- **`agent.session(name, seed)`**, on the flow thread, for work that edits the - tree and must pick up after a crash. -- **`session.chat`** to continue a durable conversation from a fork, once the - session has run on the flow thread (a chat can't seed it). -- **`resultAs[O].interactive`** on an agent or a chat when a human steers the - turn. `session.run` has no interactive mode, since a steered turn can't be - replayed from a seed; steer a session's conversation through `session.chat` - after it has run. - -- **Durable — `agent.session(name, seed)`.** A get-or-create keyed by the - `name` and the stage the call sits in, returning a `FlowSession` handle that - survives crash/resume: the same key resumes the same session (with a warning - if this call's seed differs, rather than silently resuming the wrong one). - `name` is the role, and what `orca continue ` matches. The stage half is - implicit — a per-task loop mints `implementer` inside each task's stage and - gets one session per task, with nothing to name them by hand. Two stages can - therefore never reach one conversation, and minting one name twice in the same - stage is an error rather than silent sharing: give each its own `stage(...)`, - or rename one. - Rename the stage and the key moves with it, so a re-plan that rewords a task - gives that task a fresh session primed from the seed rather than resuming the - old wording's conversation. Mint it where it is used: inside the stage that - drives it, or outside every stage when several stages share one session. What - you cannot do is return a handle from one stage as its result and drive it in a - later one — `FlowSession` has no `JsonData`. Minting and running both happen on - the flow thread. The record behind the handle is machine-local, not branch - history: it lives in `.orca/cache/`, so the stage that minted it can fail and - its retry still resumes the same conversation. -- **Ephemeral — `agent.chat()`.** A `Chat` handle continuing one conversation - across `.run` calls *within this attempt only* — no seeding, no persistence. Its - calls need only the shared `InStage` capability, so chats work inside a - `Par.mapUnordered` fork: parallel reviewers each holding a multi-turn - conversation is the canonical use. `session.chat` is a durable session's - conversation as an ephemeral chat (one live continuation at a time). - `chat.withAgent(f)` continues the same conversation on a variant of the - chat's agent — `_.withReadOnly`, `_.cheap`, `_.withName("…")` — for turns - that need other tools, a cheaper model or their own cost line. The variant - must be built from the chat's agent (a different backend is refused). - -```scala -val session = agent.session("implementer", seed = plan.brief) -session.run(task.description) - -val chats = Par.mapUnordered(4)(reviewers): r => - val c = r.chat() - c.run(s"review the diff: $diff") - c // keep the conversation for a later re-review turn -``` - -The `seed` is the essential context to rebuild the agent — typically the **plan -brief**, or the issue body when there is no brief. A fresh session is primed -with it on first use; if the backend lost the conversation on resume, the -session is re-seeded (with a warning: history is gone, only the seed plus a -preamble naming completed stages are rebuilt), while a live session continues -with its full history — told once, on this attempt's first turn against it, that the -tree holds only what earlier stages committed, since a conversation a previous -attempt opened remembers writing files that are no longer there. - -**How long a session should live.** A backend conversation is re-sent whole on -every API call it makes, so what a session costs grows with everything it has -already done. Scope one to a unit of work — a task, a review stage — not to the -run: the shipped flows mint a session per task and another for the final review, -and each new one is primed from its seed and the completed-stage preamble. - -`agent.cheap` returns the backend's cheap/fast variant (claude → haiku, codex → -mini, gemini → flash, opencode → anthropicHaiku, others → self) — used by the -runtime for branch naming and default commit messages. - -**Backend swaps across attempts.** If a settings edit changes a role's agent -between attempts (e.g. `codingAgent = codex` becomes `codingAgent = claude`), a -session recorded under the old backend isn't resumed against the new one — -orca mints a fresh session from the seed and warns. - -## Authoring rules - -Mutations outside a stage body are compile errors (see [Side effects happen -inside stages](#side-effects-happen-inside-stages)). The rules below are the -structural conventions you choose to follow as a flow author. - -1. **Reads outside, mutations inside.** Only side-effecting work goes in a - stage. Pure reads (`git.uncommittedDiff`, `gh.readIssue`, `fs.read`, `gh.waitForBuild`) - run outside stages — staging them wastes commits and checkpoints. - `agent.session(name, seed)` is neither — it records a session — so put it - where the session is used (see [Sessions](#sessions)). - -2. **Push lives in a later stage than the edit that produced it.** A stage - commits only on completion: a `git.push()` in the same stage as the edit - would push nothing (the edit isn't committed yet). The push must be in a - *separate, later* stage: - - ```scala - stage("Write failing test"): - session.run("Write the failing test …") // commits on completion - - val pr = stage("Push + open PR"): // LATER stage — the test commit exists now - git.push().orThrow - gh.createPr(title = …, body = …).orThrow - ``` - -3. **One commit per stage.** Each stage produces exactly one commit (code - changes + the progress-log entry), made by the runtime when the stage - completes. - -4. **Idempotent external effects, each in its own stage.** Put each PR-open, - comment-post, or push in a dedicated stage so it's checkpointed. - `gh.createPr` is idempotent by branch (an open PR is reused, not duplicated) - and `gh.upsertComment(target, marker, body)` edits a prior comment carrying - `marker` in place — so if a crash re-opens the stage on resume, the re-run - reuses the PR/comment instead of duplicating it. Use - `orcaCommentMarker(userPrompt, purpose)` so the marker is unique to this run. - -5. **Name stages descriptively.** The stage name appears in the event log, the - commit message (when no override is provided), and the progress preamble on - resume. A name like `"Push + open PR"` lets a reader (and the resuming agent) - understand the checkpoint without reading code. - -## Experimental: capabilities & compile-time concurrency checking - -Orca gates side effects behind three capability tokens. You normally never -construct one — `stage(...)` bodies provide them, and a missing token is a -compile error with a message telling you where the call belongs: - -| Capability | Kind | Gates | Provided by | Misuse caught by | -|---|---|---|---|---| -| `InStage` | shared (`caps.SharedCapability`) | LLM runs (`agent.*.run`, `session.run`) | `stage(...)` bodies | missing-given compile error | -| `WorkspaceWrite` | exclusive (`caps.ExclusiveCapability`) | git/`gh` writes, `fs.write`, progress-log writes | `stage(...)` bodies | missing-given compile error + a runtime owner-thread check (never cross a `fork`) | -| `FlowControl` | exclusive (`caps.ExclusiveCapability`) | starting stages, minting sessions | the `flow(...)` body (not forks) | missing-given compile error + a runtime owner-thread check | - -(`FlowContext` — reads and `display` — is deliberately *not* a capability: -it is thread-safe and forks receive it freely.) A helper that starts stages -declares `(using FlowContext, FlowControl)`. - -The runtime always guards this at run time — a fork that calls -`stage(...)`/`session(...)` or makes a workspace write fails immediately, a second `flow(...)` in the same -working tree is refused, an agent used after its flow ended throws — so you get -the safety without any setup. - -
-Compile-time checking (Scala's experimental capture checking) - -The shared/exclusive split is [capture -checking](https://docs.scala-lang.org/scala3/reference/experimental/cc.html) -vocabulary. Beyond the always-on runtime guards, enforcement moves to compile -time in two more places: - -- **Inside the library:** orca's own parallel code (the reviewer fan-out) is - compiled under capture + separation checking, so a change that captured a - `WorkspaceWrite` into that fan-out would not compile (pinned by a compile-time - test suite). -- **Opt-in, in your script:** add the two language imports to have the compiler - check *your* code too — today that enforces, e.g., that a custom - `ReviewerSelector`'s per-round function stays pure: - - ```scala - import language.experimental.captureChecking - import language.experimental.separationChecking - ``` - - Full fork-boundary checking in scripts arrives when Ox itself adopts capture - checking; until then the runtime guard covers that case. - -The imports cost nothing when omitted — scripts without them compile and run -identically (see ADR 0018 §6). - -
- -## Planning utilities - -Available via `import orca.plan.*`: - -The planning entry points form a **mode × operation grid**. The two axes are -orthogonal — every combination is valid. Mode is picked at the call site -(`Plan.autonomous.*` vs `Plan.interactive.*`), mirroring how `Agent` itself -splits `autonomous` / `interactive`: - -| Operation | Result | `autonomous` (read-only + network, no human) | `interactive` (agent can `ask_user`) | -|---|---|---|---| -| `from(userPrompt, agent, instructions?)` | `Plan` | plan in one agentic turn | drive the planner conversationally | -| `assessThenPlan(userPrompt, agent, instructions?)` | `Verdict[Plan]` | assess, then `Proceed(plan)` or `Rejection(kind, body)` | same, but can ask the reporter to clarify instead of rejecting | -| `triage(report, agent, instructions?)` | `Triage` | classify a bug report (not-a-bug / untestable / testable) | same, with clarifying questions | - -Every cell returns `WithChat[]` — the result paired with the -(ephemeral) `Chat` that produced it. Continue that conversation in-run -(`chat.run(task)`; continuations have write access), or `.value` it and start a -fresh, durable implementer session via `agent.session("implementer", seed = -plan.brief)` — the chat does not survive a crash/resume, so every -shipped example takes `.value`. Destructure when you want both: `val -WithChat(chat, plan) = Plan.autonomous.from(...)`. - -From a `WithChat[Plan]`, an optional `.reviewed()` step refines the plan -before implementing — the planner critiques its own draft, read-only, producing -an improved `Plan`. Chain it: `Plan.autonomous.from(...).reviewed().value`. -`.reviewed(variant = _.cheap)` runs the review on a variant of the read-only -planner. - -`assessThenPlan` returns a `Verdict`: `Verdict.Proceed(plan)` to implement, or -`Verdict.Rejection(kind, body)` — a follow-up question, critique, or rebuff the -caller surfaces back to the reporter. `triage` returns a `Triage` sum type the -caller pattern-matches (`NotABug` / `Untestable` / `Testable`). - -### Review utilities - -Available via `import orca.review.*`: - -| Method | Use | -|---|---| -| `lint(commands, agent, instructions?)` | Run shell lint commands (in order, each via `bash -c`; every one runs even if an earlier one fails) and have `agent` summarise their labelled, concatenated output as a `ReviewResult`. Short output is inlined into the prompt; anything larger is written to a file under `.orca/cache/` for the agent to read, so unbounded output can't overflow the context. | -| `lint(commands, summariser, instructions)` | As above, but summarising into an existing `Lint.summariser(agent)` conversation instead of a fresh one per call, so a gate run several times within one stage resumes the session rather than re-establishing it each round. Stop reusing a summariser once it has reported: it can repeat those findings on a later call whose commands no longer show them. `reviewAndFixLoop` does this for you. | -| `reviewAndFixLoop(coderSession, reviewers, task, userRequest?, ..., formatCommands?, lint?, checks?, maxFixTurns?, fixInstructions?)` | Run reviewers against `task: Task`, collect their findings, hand them to the `coderSession` (a `FlowSession`) to fix, re-evaluate. Reviewers are asked to report only what they believe should be fixed, and every finding they report reaches the fixer — nothing filters them in between. Reviewers see the task's title and description under separate labels, plus the user's request — the run's prompt by default, or `userRequest` when the prompt is only a pointer, like an issue reference. Keeping them apart is what lets a reviewer report a finding against the planner's choice rather than only against the code. A flow with no planning stage passes its prompt as the title and an empty description. Halts when reviewers come back clean, the fixer reports no fixes, or `maxFixTurns` fix turns have run (default 3, so up to four review rounds). Every exit names the findings it leaves open and why each is still open. Whatever is still open at that point — the findings the fixer declined, didn't account for, or that were first reported in the round that hit the cap — comes back in the returned `OpenFindings` with a reason. `formatCommands: Configured[List[String]]` runs before each review round; `lint: Configured[Lint]` runs alongside the reviewers each round — both default to the project's [stack settings](#settings), see below. `checks: List[ReviewCheck]` (default none) run after formatting and before the reviewers, see below. | -| `reviewThenFix(coderSession, reviewers, task, userRequest?, formatCommands?, lint?, checks?)` | One round of the above and, if it found anything, one fix turn — then done. Nothing re-reviews a reviewer finding, so the fixer's claim that it fixed one is taken on trust; the lint gate and `checks` are the exception, re-run over the fixer's edits and given one more fix turn if they still fail, so a check can run three times in one pass. Reviewers are picked once (`ReviewerSelector.agentDriven`) and the change set is the enclosing stage's, as above. What the fixer declined, what it never reported on, and what the lint gate or a check still fails on, come back in the returned `OpenFindings` with a reason. Use it per task where a later stage reviews the same code again — a whole-run `reviewAndFixLoop`, below — and pay for the loop where nothing else re-reviews the fixes. | -| `ReviewCheck` | A check written in Scala — a benchmark, an HTTP probe, a scripted assertion: `name` plus `evaluate(): ReviewResult`. Pass it in `reviewAndFixLoop`'s or `reviewThenFix`'s `checks`; its findings go to the fixer with the reviewers'. | -| `OpenFinding.custom(title, reason, location)` | An open finding a flow records itself — say, a gate it runs outside the loop still failing. Add it to `OpenFindings` for the PR body, or pass it in `priorOpenFindings` so a loop's reviewers see it. | -| `allReviewers(base)` | Every reviewer in the run's catalog (the eight canonical ones — code-functionality, test, readability, code-structure, simplicity, performance, security, scala-fp — plus whatever `.orca/reviewers/` and the global tier add, see [Settings](#settings)) as `ReviewerAgent`s: each one its `Reviewer` definition plus a read-only agent built from `base`. | -| `minimalReviewers(base)` | Universally-applicable subset (code-functionality, readability, test) plus every discovered reviewer, same shape. Pair with the default LLM-driven selector when the full set is overkill. | -| `reviewerCatalog` (in-body accessor) | The run's resolved reviewer definitions — `.all` and `.minimal` are what the two above build from. Filter it to pick a subset yourself. | - -`reviewAndFixLoop`'s stack-dependent parameters are three-state -(`orca.Configured`), so omission means "from the project's [stack -settings](#settings)" while "explicitly off" stays expressible: - -```scala -enum Configured[+A]: - case FromSettings // resolve from the run's stack settings (the default) - case Off // explicitly disabled for this call - case Use(value: A) // explicit value; settings ignored -``` - -`FromSettings` resolves `formatCommands` to `stackSettings.format` and builds -the lint gate as `Lint(stackSettings.lint, reviewAgent.cheap)` — commands plus -the summariser agent bundled in one value (`Lint(commands: List[String], -agent)`). An empty list resolves to no gate at all: `FromSettings` over empty -settings behaves exactly like `Off`. A script that omits `lint` gets a lint gate -whenever the target project's settings define one; for format-only, pass `lint = -Configured.Off`. - -Each round runs its `checks` one at a time, after the format commands and -before the reviewers and the lint gate start, so a check that builds or times -the code has the machine to itself. A check must not modify sources. Keep a -finding's title the same across rounds and put measurements in its description: -the loop recognises a finding it already holds as open by its title and file. -With no reviewers, the loop just evaluates the check and fixes: - -```scala -val benchmark = new ReviewCheck: - def name = "benchmark" - def evaluate()(using ctx: FlowContext, ev: InStage): ReviewResult = - val ms = os.proc("./bench.sh") - .call(cwd = ctx.workDir, stderr = os.Pipe).out.trim().toInt - if ms <= 200 then ReviewResult.empty - else ReviewResult(List(ReviewFinding(Title("Request too slow"), - s"p99 is $ms ms; the target is 200 ms", location = None, - suggestion = None, reopens = None))) - -stage("Speed up"): - reviewAndFixLoop( - coderSession = session, - reviewers = Nil, - task = Task(Title("Make requests faster"), ""), - lint = Configured.Off, - checks = List(benchmark) - ) + openPrIfGitHub(summarisingAgent = codingAgent.cheap, openFindings = openFindings) ``` -The whole loop is one stage, so one commit. When each iteration is long, write -the loop in the flow instead, one stage per iteration calling -`coderSession.run`, so a resume picks up at the last finished iteration. - -The change set reviewers are shown — and that the selector picks from — is -everything the enclosing `stage` has produced since it began, so it is the same -whether or not the coding agent committed its own work along the way. It is -re-sampled each round and sent to every reviewer that runs, resumed ones -included, so each round's reviewers see the fixes made before it. Pass -`diff = ReviewDiff.Pinned(...)` to pin it instead: reviewers are then not told a -base commit, the selector's changed-file list is scraped from the diff text, and -every later round finds the same text, so a resumed reviewer is told there is no -new change set. - -`diff = ReviewDiff.WholeRun` widens it to everything the run has changed since -it started — since the commit HEAD pointed at when the run bound its branch, -recorded in the progress log — so a stage placed after the per-task work -reviews the whole branch, earlier stages' commits included. Reviewers are told -the change set spans every stage. A run whose log records no usable commit (a -log from before orca recorded one, or one whose commit no longer sits behind -HEAD after a rebase) has no base: the call says so in a step and returns without -reviewing. - -That is the final-review half of the shape every task-based built-in flow uses -— `reviewThenFix` per task, then this once: - -```scala -stage("Final review"): - reviewAndFixLoop( - coderSession = session, - reviewers = allReviewers(reviewAgent), - task = Task(Title("The whole planned change"), plan.brief), - diff = ReviewDiff.WholeRun, - maxFixTurns = 5 - ) -``` - -The cap is above the library default of 3 because nothing reviews again after -this loop. Each flow hands what it returns to its PR step -(`openPrIfGitHub`/`openPrFromBranch`), which lists every finding still open in -the PR body. - -A change set past 128 KiB is cut down before it is sent: the reviewer gets as -many whole files as fit, then a list naming every other changed file with its -line counts, and reads those files itself. Without that, the largest change sets -make a request no model can accept. A pinned diff is sent as given. - -`reviewAndFixLoop`'s `reviewerSelection` defaults to `ReviewerSelector.default`, -which narrows twice: a picker LLM on `reviewAgent`'s cheap tier chooses from the -supplied list for round one, seeing each reviewer's description and the changed -file paths; every later round then re-runs only the reviewers that reported a -finding in the previous one. A reviewer that stays quiet stops costing a turn -per round — the trade-off is that it won't see the fixes made after it stopped. -If narrowing would leave no reviewer at all (everyone quiet, while a lint finding -keeps the loop going), the round's full selection runs again and a step says so. - -| Selector | Behaviour | -|---|---| -| `default` | The above: `narrowingAcrossRounds(agentDriven)`. | -| `allEveryRound` | The whole supplied roster, every round; no picker. | -| `agentDriven` | Pick once with `reviewAgent.cheap`, replay that pick every round. | -| `agentDriven(agent, instructions?)` | As above with a chosen picker model and brief. | -| `narrowingAcrossRounds(base)` | Adds the per-round narrowing over any `base`. | - -A reviewer declaring a `files:` pattern in its frontmatter (of the shipped set, -only `scala-fp`) is offered to the picker only when a changed file matches it — -unless nothing is known about the change set, in which case it stays eligible. -The selector reads each reviewer's name, description and pattern off its -`Reviewer`, so your own reviewers are described and gated the same way. - -To swap or extend the reviewer set for one project, drop `.md` files in -`.orca/reviewers/` — no code changes (see [Settings](#settings)). To do it from -the flow, compose your own `List[Reviewer]` from `reviewerCatalog.all` (the -run's resolved set), `ReviewerPrompts` (the shipped entries alone), and/or your -own `Reviewer(ReviewerSlug(name), description, systemPrompt)`, then turn it into -`ReviewerAgent`s with `buildReviewers(base, list)`. - -PR utilities, available via `import orca.pr.*`: - -| Method | Use | -|---|---| -| `summarisePr(agent, diff, context?, instructions?)` | Fold a branch diff into a `PrSummary(title, body)` for `gh.createPr`. `context` is an optional preamble (originating issue link, user prompt, etc.) the model anchors the description to. A diff too large to send is cut short. Use a cheap model (`claude.cheap`, `codingAgent.cheap`). | -| `openPrFromBranch(summarisingAgent, openFindings, title?, body?, context?, instructions?): PrHandle` | Push the feature branch and open a PR for it, as three stages: push → summarise → create. Requires a GitHub remote and a logged-in `gh` — without either the run fails. `openFindings` is the `OpenFindings` the run's final review returned; each entry is listed under "Open review findings" as its title, where it points if the reviewer named a place, and the reason, verbatim, after a line saying so if the review was skipped (none open and not skipped: no section). The same section is printed to the run output, also when the PR fails. `title`/`body` rewrite the generated text. `context` defaults to the run's user prompt, and then the summariser adds a `Closes #N` line per issue the prompt says to fix; a flow that passes `context` adds its own `Closes` line through `body` (`body = s => s"${s.body}\n\nCloses #42."`). Opening the PR is a top-level step of a flow and this runs its own stages, so it does not compile inside one. | -| `openPrIfGitHub(summarisingAgent, openFindings, title?, body?, context?, instructions?): Option[PrHandle]` | Probes `gh.availability` before its push stage runs (a resume that replays the push skips it), then runs `openPrFromBranch`'s push → summarise → create when the checkout is on GitHub. Where it isn't — no remote, a remote that isn't GitHub, a GitHub `gh` cannot reach, a run that changed no code, or a push/create the remote refuses — it emits one `Step` saying why, returns `None`, and the run finishes. The open findings are printed to the run output either way. A resume replays what its push and create stages recorded, a refusal included. The step every code-producing built-in flow ends with; like `openPrFromBranch`, it does not compile inside a stage. | -| `bodyWithOpenFindings(body, open)` | `body` with the "Open review findings" section appended, or `body` unchanged when nothing is open and the review ran — the assembly `openPrFromBranch`/`openPrIfGitHub` use, for a flow that writes its own PR body (`gh.updatePr`). | -| `reportOpenFindings(open)` | Print the "Open review findings" section to the run output; nothing when nothing is open and the review ran. `openPrFromBranch`/`openPrIfGitHub` do this themselves, before their PR step; a flow that writes its own PR body calls it before its PR step. | -| `recordOpenedPr(pr)` | Record the PR's URL as the run's published work, so the run hands the checkout back on the branch it started from and the closing summary names the PR. Only for a flow that opens its PR with a bare `gh.createPr` — `openPrFromBranch`/`openPrIfGitHub` record it themselves. Call it inside the stage that opened the PR (it needs that stage's `WorkspaceWrite`): the stage's commit carries the record, and a resume reads it back without re-running the body. | - -### Customising prompts - -Every domain helper that bundles an LLM brief takes its prompt as a -default-valued `instructions: String`; the default lives on a sibling -`XxxPrompts` object. Override it, or compose with the default to extend it: - -```scala -import orca.plan.{Plan, PlanPrompts} - -Plan.interactive.from( - userPrompt, - claude, - instructions = PlanPrompts.Planning + "\n\nPrioritise observability tasks first." -) -``` - -
-Where the defaults live - -- `orca.plan.PlanPrompts` — `Planning`, `AssessThenPlan`, `Triage`, `Review` -- `orca.pr.PrPrompts` — `Summarise` -- `orca.review.ReviewLoopPrompts` — `Fix`, `SelectReviewers`, `SummariseLint` -- `orca.review.ReviewerPrompts` — per-reviewer system prompts (compose your own - list to swap or extend `allReviewers`/`minimalReviewers`) - -The lower-level per-call wrappers (autonomous/interactive/retry) are a separate -layer — replace the whole set via `flow(prompts = ...)`. See [ADR -0010](adr/0010-prompts-and-helpers-convention.md) for the full convention. - -
- -## Data structures - -Common types you'll see in flow scripts. Most `derives JsonData`, making them -valid stage results (the progress log can record and replay them) and usable as -structured LLM output via `claude.resultAs[T]`. Exceptions: `WithChat` and -`Verdict` do not derive `JsonData` — they are intermediate values, not stage -results. - -
-The types, in detail (click to expand) - -- **`orca.plan.Plan(epicId, description, tasks, brief)`** — the task list the - agent generates in one round-trip. `epicId` is a kebab-case identifier for the - plan itself (heads its markdown render) — NOT the git branch name; the flow - derives and announces its own branch separately (see - [`BranchNamingStrategy`](#the-flow-lifecycle)). `description` is the planner's - epic summary; `brief` is a concise codebase briefing always included (feed it - to `agent.session("implementer", seed = plan.brief)`, which threads it as the - seed). `taskPrompt(task)` prepends the brief to a task's - description. -- **`orca.plan.Task(title, description)`** — `title` is the human-readable label - shown in the event log. -- **`orca.plan.WithChat(chat, value)`** — every `Plan.{autonomous, - interactive}.*` operation returns one: the result paired with the (ephemeral) - `Chat` that produced it, so the caller can continue that conversation in-run - or `.value` it and start fresh. Only the library builds one; destructure it - with `val WithChat(chat, plan) = ...`. -- **`orca.plan.Verdict[A]`** — `Verdict.Proceed(value)` or - `Verdict.Rejection(kind, body)` (kind ∈ Question / Critique / Rebuff). - Returned by `assessThenPlan` as `Verdict[Plan]`. -- **`orca.plan.Triage`** — sum type returned by `triage`: `NotABug`, - `Untestable`, or `Testable` — each carrying exactly the fields its branch - needs. -- **`orca.plan.BugReportMatch`** — the agent's decision on whether a CI failure - matches the original report. -- **`orca.FlowSession`** — durable, resumable session handle returned by - `agent.session(name, seed)`. Call `.run(prompt)` or `.resultAs[O].run(input)` - on it to drive the agent, with automatic seed/preamble replay (when the - backend conversation isn't live) and resume-wire-id persistence. - `session.chat` is its conversation as an ephemeral `Chat` (the fork-side - escape hatch). -- **`orca.agents.Chat[B]`** — ephemeral multi-turn conversation handle from - `agent.chat()`: tool-using and workspace-editing like any agent turn ("chat" - names its lifetime, not its powers), in-run only, fork-safe. Also carried by - `WithChat` for planning-conversation continuations. -- **`orca.Title`** — opaque `String` alias for short labels (`Task.title`, - `ReviewFinding.title`); `Title("…")` to construct, `.value` to read. -- **`orca.tools.PrHandle`** — handle to an open pull request (`host`, `owner`, - `repo`, `number`), returned by `gh.createPr`. Build one with - `PrHandle.from(host, owner, repo, number)` (a `Left` names the field that is - not a valid host, owner, repo or PR number) or `PrHandle.fromUrl(url)`. - `host` is `github.com` or a GitHub Enterprise hostname, and every `gh` call - taking the handle is routed to it. Has a `JsonData` (it travels as its URL) - so a stage can record it: a push-and-open-PR stage is the checkpoint before a - CI wait. `IssueHandle` carries no host, so the issue flows read their issue - from gh's default host (`GH_HOST`, else the host gh is logged in to). -- **`orca.tools.GitHubAvailability`** — what `gh.availability` answers with. - `Available(host, owner, repo)`: the repository gh resolves, on github.com or a - GitHub Enterprise host. `Unavailable(why)`: no PR can be opened; `why` is a - `GitHubUnavailable` — `NoRemote` (no `origin`), `NoHost(remote)` (`origin` - has no host, a local path), `NotGitHub(host)` (gh has no login for that host, - so a GHES host needs `gh auth login --hostname `), `Unreachable(host, - reason)` (the host is GitHub, but gh could not answer for it — `reason` is - gh's own words), or `GitUnusable(reason)` (git itself could not be run, so - nothing is known about the checkout). `why.explanation` renders that as one - line to put a flow's own next action after. -- **`orca.pr.PrSummary(title, body)`** — what `summarisePr` returns. The two - fields feed `gh.createPr(title = …, body = …)` directly. -- **`orca.review.ReviewFinding` / `ReviewResult`** — what reviewer agents - return. A finding carries a `title` (shown), a long `description` (sent to - the fixer), an optional `location`, and `reopens`: the `FindingId` of the - still-open finding it reports again, if any. -- **`orca.review.OpenFindings(findings, skipped)`** — accumulated - `OpenFinding(id, title, reason, location)` entries surfaced by - `reviewAndFixLoop` once it halts: every finding the run did not resolve, each - with where it points and an `OpenReason` — `Declined(text)` (the fixer's own - words), `NoFixes`, `Unaccounted`, `CapReached(max)`, `StillFailing(sources)` or - `Custom(text)` (from `OpenFinding.custom`). - `reason.describe` is the sentence shown to a reader. `id` (`FindingId`) is - what entries merge by across rounds; two findings sharing a title stay two. - `skipped` is `Some(SkippedReview)` when the review never ran. -- **`orca.StackSettings(format, lint, test)`** — the resolved per-project - tooling commands (each field a `List[String]`, run via `bash -c`; empty = gate - disabled). Resolved once per attempt — see [Settings](#settings) — and read back - via `summon[FlowContext].stackSettings`; pass `flow(stackSettings = - Some(...))` to pin it. -- **`orca.Configured[A]`** — three-state default for `reviewAndFixLoop`'s - stack-dependent parameters: `FromSettings` (the default — resolve from the - run's stack settings), `Off` (explicitly disabled for this call), or - `Use(value)` (explicit value; settings ignored). -- **`orca.review.Lint(commands, agent)`** — the lint gate bundle - `reviewAndFixLoop` runs alongside the reviewers: the shell commands plus the - (cheap) agent that summarises their output into a `ReviewResult`. - -
- -## Output - -While Orca runs the terminal output is split into two zones: an **event log** -that grows top-to-bottom as stages and tools fire, and a **status line** pinned -to the bottom, showing the active stage breadcrumb with a spinner. Nested stages -are indented. - -
-Glyph legend - -| Glyph | Meaning | -| ----- | ------- | -| `▶` | Stage start, or a `Step` (single-line note like a branch switch) | -| `▸` | The prompt sent to an agent | -| `●` | Assistant prose | -| `⏺` | Tool call (path / command / query in grey). A read-only call shows as a bare `⏺ read`, with no filename and no agent name, so a burst of them folds into one line; the trace file has both | -| `⎿` | How many times the line above repeated (`⎿ ×12`) | -| `✖` | Error | -| `?` | Approval request, or a question for you (interactive calls only) | -| `!` | Caveat about what Orca can enforce for this run (never indented under a stage) | - -
- -Colours and animation auto-disable when stderr isn't a terminal. Set -`NO_COLOR=1` or `ORCA_NO_ANIMATION=1` (suppresses the spinner) to force them -off. - -## Authenticating the coding agents - -Each CLI manages its own auth; Orca stores no secrets. Before running a flow, -log in to the backend you use — `claude`, `codex`, `opencode`, or `pi` — and to -`gh` (for the GitHub helpers), each per its own instructions. - -
-OpenCode with a local Ollama model - -- **Launcher (zero config):** `flow(OrcaArgs(args), opencode = Some(w => - OpencodeAgents.default(w, OpencodeLauncher.ollama("qwen3-coder"))))`. Orca - starts the server via `ollama launch opencode`, which injects Ollama's - provider config and pins that one model — use bare `opencode`, no `withModel`. - Needs the `ollama` CLI and the model pulled. -- **Manual config:** declare an `ollama` provider in - `~/.config/opencode/opencode.json` (baseURL `http://localhost:11434/v1`, your - models, `num_ctx` raised for tool use), then `opencode.withModel("ollama", - "qwen3-coder")`. Supports several models and per-turn switching. - -
- -## Getting set up - -Orca is published to Maven Central — `scala-cli` fetches the artifacts on first -run: - -```bash -scala-cli run --workspace "$(mktemp -d)" implement.sc -- "your prompt here" -``` - -`--workspace` keeps scala-cli's build output out of your repository; without it -you get a `.scala-build` directory next to the script. - -For a guided start, install [Orca Shell](#orca-shell) instead: its first-run -wizard configures the role agents and models for you. - -## Orca Shell - -Orca Shell is an interactive terminal front-end for the same flow scripts: a -first-run wizard picks a harness and model for each of the -planning/coding/review roles — writing the same global `settings.properties` -described under [Settings](#settings) — then a menu lets you discover flows -(project, global, and built-in), run one, view or edit its source, create a new -flow (or fork an existing one) with the configured role agents' help, or -continue a session left by a previous run. It launches flows the same way -`scala-cli run` does — direct `scala-cli run flow.sc -- "prompt"` keeps working -unchanged. - -### Command-line usage - -Every action in the interactive menu also has a scriptable subcommand — `orca` -with no arguments starts the interactive shell; `orca ...` runs one -action non-interactively and exits. - -| Command | Key flags | Does | -|---|---|---| -| `orca run [prompt]` | `--prompt ` (the prompt, for text starting with `-`; not with the positional), `--verbose` (stack trace on abort), `--branch ` (create the run's branch under this name; refused with `--skip-branch`), `--skip-branch`, `--keep-changes` (leave uncommitted files in place), `--worktree` (run in a git worktree of this repository), `--honor-pin` (use the flow's own pinned orca version) | run a flow, propagating its exit code; the prompt is read from stdin when omitted and piped | -| `orca view ` | `--plain`, `--color` | print a flow's source (highlighted when stdout is a terminal) | -| `orca edit ` | `--to project\|global` | open a flow in `$VISUAL`/`$EDITOR`/vi (`--to` required to customize a built-in) | -| `orca create ""` | `--name `, `--global` | author a new flow: the built-in `simple.sc` flow writes it in an isolated sandbox with the configured role agents; `--name` is auto-derived when omitted. The sandbox is a fresh repository with no remote, so the flow's closing PR step opens nothing and says so | -| `orca fork ""` | `--name `, `--global` | fork an existing flow, the same way | -| `orca continue [selector]` | `--list`, `--json` | resume a recorded harness session (no selector = newest); `selector` is an id from `--list` (it keeps naming the same session while other attempts record theirs), a session name, or a branch — a name matching several sessions in one working tree resumes the most recent of them; a selector matching both a name and a branch is refused | -| `orca config` | `--planning-agent`, `--coding-agent`, `--review-agent`, each taking `harness[:model]`; or `--edit project\|global` | show the configured role agents, set any subset, or hand-edit that tier's settings file in `$VISUAL`/`$EDITOR`/vi (created from its template if absent) | -| `orca list` | `--json` | list discovered flows across the project/global/built-in tiers | -| `orca clear-stack` | `--yes` | clear discovered stack settings so the next flow run re-detects them | - -`create`, `fork`, `edit`, `continue`'s resume, and `config --edit` each need a -real terminal and error cleanly if run without one; `run`, `view`, `list`, -`config` (without `--edit`), and `clear-stack --yes` work fine piped or in CI. - -Examples: - -```bash -orca run implement.sc "add a rate limiter to /login" -echo "add a rate limiter" | orca run implement.sc -orca list --json | jq -r '.[].name' -orca create "add a token-bucket limiter" --name rate-limit.sc -orca continue # resume the last session -orca continue --list -orca continue feat/rate-limiter -orca config --coding-agent codex -orca config --review-agent claude:sonnet -orca view implement.sc -``` - -Run `orca --help` for the full command list, or `orca --help` for a -command's own flags. Exit codes: 0 success, 1 action failure, 2 usage error — -`orca run` propagates the flow's own exit code, which makes it CI-friendly. - -Install it with: - -```bash -curl -fsSL https://raw.githubusercontent.com/VirtusLab/orca/master/install.sh | bash -``` - -The script does exactly two things: - -1. If `scala-cli` isn't on your `PATH`, it downloads and runs scala-cli's - official installer (which places scala-cli in its own versioned location and - updates your shell profile; scala-cli then manages its own JVM). -2. It writes the `orca` executable to `~/.local/bin/orca` — a short launcher - script that runs the latest released `orca-shell` via `scala-cli`. Nothing - else is downloaded at install time; the artifacts are fetched on the first - `orca` run, and the launcher never needs a version bump. - -Add `~/.local/bin` to your `PATH` if the installer says it isn't there yet, then -run `orca`. - -To avoid installing anything, or to pin a version (e.g. in CI), run the shell -directly instead. The pinned form works from the first release that includes the -shell; the version below always tracks the latest release. `--workspace` keeps -scala-cli's own build metadata out of the current directory (it lands under the -given directory instead): - -```bash -scala-cli run --workspace "${XDG_CACHE_HOME:-$HOME/.cache}/orca/shell/workspace" --jvm 21 --quiet --verbose --dep "org.virtuslab::orca-shell:0.1.10" --main-class orca.shell.Main -``` +Each `stage` commits on completion and is skipped on resume. The tutorial at +[orca.virtuslab.com](https://orca.virtuslab.com) explains every line; the +shipped flows live in [`flows/`](flows/). ## Documentation -- [`adr/`](adr/) — architecture decision records. [ADR - 0018](adr/0018-stage-bound-flow-runtime.md) describes the current stage-bound - runtime; the ADR index covers module layout, backends, the flow DSL, and - reviewers. +- [orca.virtuslab.com](https://orca.virtuslab.com) — user documentation: usage, + built-in flows, settings, authoring flows, API reference, glossary. Source in + [`docs/`](docs/). +- [`adr/`](adr/) — architecture decision records. - [`CONTRIBUTING.md`](CONTRIBUTING.md) — building, testing, and running a locally modified orca. - [`AGENTS.md`](AGENTS.md) — internals, architecture, and coding conventions; diff --git a/build.sbt b/build.sbt index a17c0214d..ac9a70d3f 100644 --- a/build.sbt +++ b/build.sbt @@ -257,19 +257,22 @@ lazy val shell = (project in file("shell")) IO.write(indexFile, flowFiles.map(_.getName).mkString("\n")) copied.toSeq :+ indexFile }.taskValue, - // Bundles the README plus two example flows as jar resources under + // Bundles the documentation plus two example flows as jar resources under // orca/shell/api/ (ADR 0021 §9), so `CreateFlow` can extract them into the - // authoring harness's workspace as its API reference material. + // authoring harness's workspace as its API reference material. The docs + // pages are concatenated into one file in toctree order, each headed by + // its path, since the authoring prompt points at a single reference file. Compile / resourceGenerators += Def.task { val base = (ThisBuild / baseDirectory).value val outDir = (Compile / resourceManaged).value / "orca" / "shell" / "api" IO.createDirectory(outDir) - val sources = List( - base / "README.md", + val docsFile = outDir / "orca-docs.md" + IO.write(docsFile, ConcatDocs(base / "docs")) + val examples = List( base / "flows" / "implement.sc", base / "flows" / "implement-interactive.sc" ) - sources.map { f => + docsFile :: examples.map { f => val target = outDir / f.getName IO.copyFile(f, target) target @@ -302,6 +305,7 @@ lazy val orcaRoot = (project in file(".")) List( file("README.md"), file("AGENTS.md"), + file("docs"), file("examples"), file("flows") ) diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..b38170e56 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +_build +_build_html +.venv diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 000000000..298fb88ed --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = python -msphinx +SPHINXPROJ = Orca +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..7c98692d6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,39 @@ +# Orca documentation + +Source for the Orca documentation site, built with Sphinx + MyST and hosted on +Read the Docs. Read the Docs builds straight from this folder (`.readthedocs.yaml` +at the repo root), so committing a change here is all it takes to publish it. + +## Run locally + +From this folder: + +``` +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +./watch.sh +``` + +Open . Edits to `.md` files live-reload in the browser. + +Next time, just: + +``` +source .venv/bin/activate +./watch.sh +``` + +`make html` builds once into `_build/html`; CI runs `sphinx-build -W` so a +broken cross-reference or toctree entry fails the build. + +## Notes + +- Orca's version in code snippets (`//> using dep "org.virtuslab::orca:…"`) is + bumped by the release (`sbt updateDocs`), which walks this folder like it + walks `flows/` and `examples/`. +- The pages here are also bundled into the `orca-shell` jar as the API + reference `orca create` / `orca fork` hand to the authoring agent + (`build.sbt`), so keep them accurate for a reader that has no other source. +- `plans/`, `research/` and `superpowers/` in this directory are internal + project documents — they are excluded from the published site. diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 000000000..7037cae4e --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,6 @@ +/* The RTD theme sets white-space: nowrap on responsive tables, so a table + with long cells scrolls sideways instead of wrapping. Let cells wrap. */ +.wy-table-responsive table td, +.wy-table-responsive table th { + white-space: normal !important; +} diff --git a/docs/api/backends.md b/docs/api/backends.md new file mode 100644 index 000000000..510669ec0 --- /dev/null +++ b/docs/api/backends.md @@ -0,0 +1,86 @@ +# Backends + +A **backend** is a harness as a flow sees it. There are five of them, +available in a flow body as the agents `claude`, `codex`, `opencode`, `pi` and +`gemini`. They all expose the same calls, described in +[Talking to agents](../authoring/talking-to-agents.md): + +- durable: `session(name, seed): FlowSession`, then `.run(prompt)` or + `.resultAs[O].run(input)` on the session +- one-shot: `run(prompt)`, or `resultAs[O].{autonomous,interactive}.run(input)` +- ephemeral multi-turn: `chat(): Chat`, then `.run(prompt)` or + `.resultAs[O].{autonomous,interactive}.run(input)` on the chat + +Note that `interactive` exists only on `resultAs[O]`. [`FlowSession` and +`Chat`](data-structures.md#conversations) are the handles you get back. + +All five backends also share the builders `withModel`, `withCheapModel`, +`withAutoApprove`, `withSystemPrompt`, `withName`, `withReadOnly`, +`withNetworkOnly` and `withSelfManagedGit`; see +[Choosing agents](../authoring/choosing-agents.md) for what each one does. +`Model` wraps a model id string. + +The sections below describe what differs between the backends: which model +accessors each one has, and how it is driven. A "bare" backend is the accessor +with no model chosen, for example plain `claude`; `cheap` is the model that +`agent.cheap` picks for that backend. + +## `claude` + +Claude Code. Bare `claude` is Opus with the 1M-token context window. The model +accessors are `haiku`, `sonnet`, `opus` and `fable`; `cheap` picks haiku, and +`withModel(Model)` sets any model id. + +Use `claude.sonnet` or `claude.haiku` for cheap one-shot calls, and +`claude.fable` for the hardest ones. + +`withNetworkTools(...)` replaces the tools the `NetworkOnly` +[tool set](../authoring/choosing-agents.md#tool-sets) grants. + +## `codex` + +OpenAI Codex. Bare `codex` pins GPT-6 Sol, which needs a codex CLI version that +has this model. The model accessor is `mini`, which is GPT-6 Luna; `cheap` +picks mini, and `withModel(Model)` sets any model id. + +## `opencode` + +[OpenCode](https://opencode.ai), driven over HTTP and SSE against a headless +`opencode serve`. The server is started lazily and shared for the +[attempt](../glossary/users.md#flows-and-runs); sessions survive the server. +OpenCode inherits your configured providers and auth. + +OpenCode spans providers, so models are provider-qualified. The model accessors +are `anthropicOpus`, `anthropicSonnet`, `anthropicHaiku`, `openaiAstra`, +`openaiSol` and `openaiLuna`. `cheap` is provider-matched: openai maps to luna, +anything else to anthropicHaiku. To choose another model, use +`withModel(providerModel)` or `withModel(provider, modelId)`, for example: + +- `opencode.withModel("openai/gpt-5-mini")` +- `opencode.withModel("ollama", "llama3.1")` + +## `pi` + +[Pi](https://pi.dev/), driven through `pi --mode rpc`. Provider and model +selection follow Pi's own configuration; to pick one, use +`withModel(Model("provider/model"))`. Interactive calls can ask clarifying +questions through Orca's `ask_user` bridge. + +## `gemini` + +Google Gemini CLI, driven via `gemini --output-format stream-json`. Bare +`gemini` pins Gemini 3.1 Pro (preview). The model accessor is `flash`, which is +Gemini 3.8 Flash; `cheap` picks flash, and `withModel(Model)` sets any model +id. + +Structured output is prompt-enforced, since Gemini has no schema flag. +`withReadOnly` maps to `--approval-mode plan`. + +## Tool set enforcement and settings + +How strongly each backend enforces a `ToolSet` is tabulated in +[Choosing agents](../authoring/choosing-agents.md#how-strongly-each-harness-enforces-a-limit). + +In `settings.properties` a backend is named by the same word, with an optional +model after a colon, for example `codingAgent = codex:gpt-5-mini`. See +[Settings](../using/settings.md). diff --git a/docs/api/data-structures.md b/docs/api/data-structures.md new file mode 100644 index 000000000..c41847e6e --- /dev/null +++ b/docs/api/data-structures.md @@ -0,0 +1,118 @@ +# Data structures + +This page lists the types you meet in flow scripts. Most of them derive +`JsonData`, which makes them valid stage results (the progress log can record +and replay them) and lets you ask for them as structured LLM output via +`agent.resultAs[T]`. Handles (`FlowSession`, `Chat`) and intermediate values +(`WithChat`, `Verdict`) do not derive `JsonData`, so they cannot be stage +results. + +## Planning + +These are the types the planning calls described in +[Planning](../authoring/planning.md) produce. + +- **`orca.plan.Plan(epicId, description, tasks, brief)`** is the task list the + planner generates in one round trip. `epicId` is a kebab-case identifier for + the plan, not the branch name, and `description` is the planner's epic + summary. `brief` is the planner's short codebase briefing; use it as the + session seed, as in `agent.session("implementer", seed = plan.brief)`. + `plan.taskPrompt(task)` prepends the brief to a task's description. +- **`orca.plan.Task(title: Title, description: String)`** is one task of a + plan. Its `title` is the label shown in the event log. +- **`orca.plan.WithChat(chat, value)`** is returned by every `Plan.*` planning + call: the result together with the `Chat` that produced it. You never build + one yourself; destructure it instead: `val WithChat(chat, plan) = ...`. +- **`orca.plan.Verdict[A]`** is either `Verdict.Proceed(value)` or + `Verdict.Rejection(kind, body)`, where `kind` is one of `Question`, + `Critique` or `Rebuff`. `assessThenPlan` returns a `Verdict[Plan]`; see + [Verdicts and triage](../authoring/planning.md#verdicts-and-triage). +- **`orca.plan.Triage`** is what `triage` returns: `NotABug`, `Untestable` or + `Testable`, each case carrying its own fields. +- **`orca.plan.BugReportMatch`** is the agent's decision on whether a CI + failure matches the original report. It is a structured-output type the + bug-fix flow asks for. + +## Conversations + +Two handles represent a conversation with an agent. + +- **`orca.FlowSession`** is the durable, resumable session handle you get from + `agent.session(name, seed)`. You drive the agent with `.run(prompt)` or + `.resultAs[O].run(input)`. When the harness no longer holds the + conversation, the turn is prefixed with the seed and a list of the completed + stages. `session.chat` exposes the session's conversation as an ephemeral + `Chat`. +- **`orca.agents.Chat[B]`** is the ephemeral multi-turn handle you get from + `agent.chat()`. A chat uses tools and edits the workspace like any agent + turn; "chat" describes its lifetime, not its powers. It lives for one attempt + and may be used from a fork. `WithChat` also carries one. + +## Labels and handles + +- **`orca.Title`** is a wrapper around `String` for short labels, such as + `Task.title` and `ReviewFinding.title`. `Title("…")` constructs one and + `.value` reads it. +- **`orca.tools.PrHandle`** identifies an open pull request by `host`, + `owner`, `repo` and `number`. `gh.createPr` returns one. You can also build + one with `PrHandle.from(host, owner, repo, number)`, where a `Left` names the + invalid field, or with `PrHandle.fromUrl(url)`. `host` is `github.com` or a + GitHub Enterprise hostname, and every `gh` call taking the handle is routed + to it. Its `JsonData` form is the PR URL, so a push-and-open-PR stage can + return it. +- **`orca.tools.IssueHandle`** identifies a GitHub issue. It carries no host: + issue flows use gh's default host, which is `GH_HOST` if set, else the host + gh is logged in to. +- **`orca.tools.GitHubAvailability`** is what `gh.availability` answers. + `Available(host, owner, repo)` is the repository gh resolves. + `Unavailable(why)` means no PR can be opened, and `why` is a + `GitHubUnavailable`, one of: + - `NoRemote`: there is no `origin` + - `NoHost(remote)`: `origin` has no host, because it is a local path + - `NotGitHub(host)`: gh has no login for the host; a GHES host needs + `gh auth login --hostname ` + - `Unreachable(host, reason)`: the host is GitHub, but gh could not answer; + `reason` is gh's own words + - `GitUnusable(reason)`: git itself could not be run + + `why.explanation` renders the reason as one line. +- **`orca.pr.PrSummary(title, body)`** is what `summarisePr` returns. It feeds + `gh.createPr(title = …, body = …)` directly. + +## Review + +- **`orca.review.ReviewFinding`** is one problem a reviewer reported. It has a + `title`, which is shown, a long `description`, which is sent to the fixer, an + optional `location`, and `reopens`: the `FindingId` of the still-open finding + it reports again, if any. +- **`orca.review.ReviewResult(findings)`** is what one reviewer, the lint gate + or a check returns: a list of findings. `ReviewResult.empty` is a clean + result. +- **`orca.review.OpenFindings(findings, skipped)`** is what a review loop + leaves open once it halts. Each `OpenFinding(id, title, reason, location)` + carries an `OpenReason`, one of: + - `Declined(text)`: the fixer refused, in its own words + - `NoFixes`: the fixer reported no fixes at all + - `Unaccounted`: the fixer never mentioned the finding + - `CapReached(max)`: the finding was first reported in the round that hit + `maxFixTurns` + - `StillFailing(sources)`: lint or a check still fails after the fix turn + - `Custom(text)`: recorded by the flow with `OpenFinding.custom` + + `reason.describe` is the sentence shown to a reader. Entries merge across + rounds by `id`, never by title. `skipped` is `Some(SkippedReview)` when the + review never ran. +- **`orca.review.Lint(commands, agent)`** is the lint gate bundle: the shell + commands plus the cheap agent that summarises their output into a + `ReviewResult`. + +## Settings + +- **`orca.StackSettings(format, lint, test)`** holds the resolved per-project + [gate](../glossary/users.md#review) commands. Each is a `List[String]` run + via `bash -c`, and an empty list means the gate is disabled. Read it via + `summon[FlowContext].stackSettings`, or pin it with + `flow(stackSettings = Some(...))`. +- **`orca.Configured[A]`** is how a review call takes a gate's commands: + `FromSettings` (the default), `Off`, or `Use(value)`. See + [Gates and checks](../authoring/gates-and-checks.md). diff --git a/docs/api/tools.md b/docs/api/tools.md new file mode 100644 index 000000000..c5606e67e --- /dev/null +++ b/docs/api/tools.md @@ -0,0 +1,93 @@ +# Git, GitHub and file tools + +Three tools are available inside a `flow(...)` body: `git`, `gh` and `fs`. You +can read through them anywhere in a flow, but writes compile only inside a +`stage(...)` body; see [Stages](../authoring/stages.md). + +The runtime owns git. Agents edit the working tree, and they are told not to +commit, push or switch branches. The runtime commits each stage and owns the +run's branch, and the flow pushes with `git.push`. This is why `git` has no +commit or checkout methods. + +Note that `git` cannot be swapped out in `flow(...)`, while `gh`, `fs` and the +agents can; see [Extending flows in code](../authoring/extending.md). + +## `git` + +`git` offers reads against the working tree, plus `push`. Commits and branch +names are typed: `orca.gitref.CommitHash`, `orca.gitref.BranchName`, and +`orca.gitref.Head`, which is a branch or a detached commit. + +| Method | Returns | +|---|---| +| `push()` | `Either[PushFailure, Unit]` | +| `head()` | the branch HEAD is on, or the commit it is detached at | +| `headCommit()` | the commit HEAD points at, if any | +| `isAncestorOfHead(commit)`, `branchExists(name)`, `isIgnored(path)` | booleans | +| `uncommittedDiff()` | the diff of the whole repository, tracked files only | +| `defaultBase()` | the default base branch, or `Left(NoDefaultBase)` | +| `diffVsBase(base)` | the branch-wide diff against `base` | +| `changedFiles(since?)` | the changed paths | +| `reviewChanges(since?)` | what reviewers get: the diff, new files, changed paths and per-file diffs | +| `pendingChanges()` | what the next commit will include | +| `show(rev, paths?)` | `git show` of a revision, optionally limited to paths | +| `fileAt(rev, path)` | one file's contents at `rev` | + +A few of these deserve more detail: + +- `PushFailure` is either `NonFastForward` or `RemoteDeclined`. +- `isIgnored` answers `false` when git cannot answer. +- `uncommittedDiff()` leaves out the `.orca/` bookkeeping, and is empty once + the work is committed. +- `changedFiles(since?)` is what to use when you decide by file name; do not + parse the diff text for that. The diff does not show binary changes or + renames, and leaves a trailing tab on a path containing a space. +- `reviewChanges(since?)` returns the diff, the full contents of new files, + every changed path with its change size, and each file's own diff. `since` + is a commit to compare against, so committed work is included. +- `pendingChanges()` returns a `--stat` summary, the new files and the diff. +- `show` cuts long output and says so. + +`NoDefaultBase`, `PushFailure` and `GitReadFailed` come back as `Left`. Where +you do not expect a `Left`, call `.orThrow`, which throws instead of returning +it. + +## `gh` + +`gh` is the GitHub PR and CI integration, going through the `gh` CLI. In the +methods below, `pr` is a [`PrHandle`](data-structures.md#labels-and-handles) +and `issue` an `IssueHandle`. + +| Method | Does | +|---|---| +| `availability()` | probes whether a PR can be opened from this checkout | +| `createPr(title, body)` | opens a PR; `Either[PrCreateFailed, PrHandle]` | +| `updatePr(pr, title, body)` | replaces a PR's title and body | +| `readIssue(issue)`, `readIssueComments(issue)`, `readPrComments(pr)` | read an issue, its comments, or a PR's comments | +| `writeComment(pr, body)`, `writeComment(issue, body)` | posts a comment | +| `upsertComment(pr, marker, body)`, `upsertComment(issue, marker, body)` | edits the prior comment carrying `marker`, else posts one | +| `buildStatus(pr)`, `waitForBuild(pr, ...)` | CI status | + +`availability()` is read-only and answers with a +[`GitHubAvailability`](data-structures.md#labels-and-handles). `waitForBuild` +returns `Either[BuildWaitFailed, BuildStatus]`. + +`createPr` is idempotent by branch: if a PR is already open for the branch, it +returns that one. `upsertComment` finds a prior comment carrying `marker` and +edits it in place, posting a new one only when there is none. `updatePr` is +harmless to re-run. This idempotency is what makes a resumed stage safe, see +[Pull requests](../authoring/pull-requests.md). + +## `fs` + +`fs` is working-tree file I/O. + +| Method | Does | +|---|---| +| `read(path)` | `Option[String]`; `None` for a missing file | +| `write(path, content)` | writes a file; refuses paths outside the working tree | +| `list(glob)` | the paths matching a glob, as `List[String]` | + +`read` does not throw for a missing file; you get `None`. `write` refuses a +path outside the working tree, and also one under `.orca/runs`, `.orca/cache` +or `.orca/worktrees`. diff --git a/docs/authoring/capabilities.md b/docs/authoring/capabilities.md new file mode 100644 index 000000000..25e020218 --- /dev/null +++ b/docs/authoring/capabilities.md @@ -0,0 +1,58 @@ +# Capabilities and compile-time checking + +```{note} +This feature is experimental. The runtime checks described here are always on; +the compile-time part is opt-in. Scripts without the opt-in imports compile +and run identically. +``` + +Orca gates side effects behind three capability tokens. The bodies of +`stage(...)` and `flow(...)` provide them, and you never construct one +yourself. Calling a gated function without its token is a compile error, and +the error message says where the call belongs. + +The capabilities come in two kinds. A *shared* capability may be captured by a +[fork](stages.md#parallel-work); an *exclusive* one must stay on the flow +thread. + +| Capability | Kind | Gates | Provided by | Misuse caught by | +|---|---|---|---|---| +| `InStage` | shared (`caps.SharedCapability`) | LLM runs: `agent.*.run`, `session.run` | `stage(...)` bodies | missing-given compile error | +| `WorkspaceWrite` | exclusive (`caps.ExclusiveCapability`) | git and `gh` writes, `fs.write`, progress-log writes | `stage(...)` bodies | missing-given compile error; using it from a fork fails at runtime | +| `FlowControl` | exclusive (`caps.ExclusiveCapability`) | starting stages, creating [sessions](talking-to-agents.md) | the `flow(...)` body, not forks | missing-given compile error; using it from a fork fails at runtime | + +`FlowContext` gives you reads and `display`. It is not a capability: it is +thread-safe, and forks may use it. A helper that starts stages therefore takes +`(using FlowContext, FlowControl)`. + +The runtime checks are always on: a fork that calls `stage(...)` or +`session(...)`, or that writes to the workspace, fails at once; a second +`flow(...)` in the same working tree is refused; and an agent used after its +flow has ended throws. + +## Compile-time checking + +Shared and exclusive are terms from +[capture checking](https://docs.scala-lang.org/scala3/reference/experimental/cc.html). +Two things are checked at compile time: + +- **Inside the library.** Orca's own parallel code, the reviewer fan-out, is + compiled under capture and separation checking. A change that captured a + `WorkspaceWrite` into that fan-out would not compile, and a compile-time + test suite pins this. +- **Opt-in, in your script.** Add two language imports to have the compiler + check your code too. Today this checks, for example, that a custom + [`ReviewerSelector`](review.md#selecting-reviewers-per-round)'s per-round + function stays pure: + + ```scala + import language.experimental.captureChecking + import language.experimental.separationChecking + ``` + +```{note} +Scripts will get compile-time fork checks once +[Ox](https://ox.softwaremill.com/) adopts capture checking. Until then, the +runtime check covers forks. See +[ADR 0018](https://github.com/VirtusLab/orca/blob/master/adr/0018-stage-bound-flow-runtime.md). +``` diff --git a/docs/authoring/choosing-agents.md b/docs/authoring/choosing-agents.md new file mode 100644 index 000000000..162356d8b --- /dev/null +++ b/docs/authoring/choosing-agents.md @@ -0,0 +1,122 @@ +# Choosing agents + +Every call that talks to an agent takes the agent as a parameter. You can name +it in one of two ways. + +**The role agents: `planningAgent`, `codingAgent`, `reviewAgent`.** These are +harness agnostic. Each is resolved from [settings](../using/settings.md) and +defaults to claude. Use `planningAgent` for `Plan.*` calls, `codingAgent` for +the implementer's session, and `reviewAgent` for `allReviewers(...)`; the +review helpers default to it. When you change the settings, the whole flow +follows. The shipped flows use only role agents. + +`codingAgent` is also the run's primary agent: its cheap tier names the +branch, discovers the stack settings and writes the default commit messages. + +**A specific harness and model: `claude.opus`, `codex.mini`, +`gemini.flash`.** Use a concrete accessor when a step needs a particular +harness or tier regardless of the settings, for example `claude.opus` for a +step that must have the strongest model. Note that `codingAgent.opus` does not +compile: model accessors exist only on concrete harnesses, so if you need a +specific model, name the harness. Any other model can be pinned with +`withModel(Model("…"))`. The models and accessors of each harness are listed in +[Backends](../api/backends.md). + +## The cheap tier + +`agent.cheap` is the harness's cheaper model: + +- claude: haiku +- codex: mini +- gemini: flash +- opencode: luna when the provider is openai, else haiku +- pi: no cheaper model; `pi.cheap` is `pi` + +It is useful for one-shot summaries and pickers. + +## Tool sets + +`ToolSet` decides which tools exist at all for a given agent: + +```scala +// ReadOnly: reads only, no shell, no edits. Reviewers, plan review, briefs. +val reviewer = claude.withReadOnly + +// NetworkOnly: reads plus read-only network. Planners that must read an issue or PR. +val planner = claude.withNetworkOnly + +// Full (the default): write-capable. +``` + +`NoTools` exists too; the runtime uses it for cheap one-shots. + +What `NetworkOnly` grants differs per harness: + +- claude: `WebFetch` and `WebSearch`, replaceable with `claude.withNetworkTools(...)`, + plus Orca's own GitHub issue and PR read tool. +- codex: network access inside the `workspace-write` sandbox. Codex has no + read-only-with-network sandbox. +- gemini: `web_fetch`. +- opencode: `webfetch` is left to the server's default. +- pi: `bash`. Pi has no web tool, and `bash` also writes. + +## Auto-approval + +`AutoApprove` decides which of the available tools run without a y/n prompt. +It matters only for interactive turns, and only with the `Full` tool set: + +```scala +val limited = + claude.withAutoApprove(AutoApprove.Only(Set("Read", "Edit", "Grep"))) +``` + +`AutoApprove.Only` fits interactive flows, where a human answers anything +outside the set. Only claude enforces the set per tool; codex and gemini +cannot restrict per tool, so `Only` becomes full auto-approval there. + +```{warning} +In an autonomous turn nobody can answer the prompt, so a call outside the set +hangs. For an unattended run, the practical boundary is a sandbox, see +[Agent CLIs](../using/agent-clis.md). +``` + +## How strongly each harness enforces a limit + +A `ToolSet` requests a restriction, and each harness enforces it differently. +When a harness cannot enforce the requested limit, the turn's output is marked +with `!`. + +| `ToolSet`, `AutoApprove` | Claude Code | Codex | OpenCode | Pi | Gemini | +|---|---|---|---|---|---| +| ReadOnly, * | Hard | Hard | Hard | Hard | PromptOnly | +| NetworkOnly, * | Hard | PromptOnly | Hard | PromptOnly | PromptOnly | +| Full, All | Hard | Hard | Ignored | Ignored | Hard | +| Full, Only(_) | Hard | SandboxApprox | Ignored | Ignored | Ignored | +| NoTools, * | Hard | PromptOnly | Hard | Hard | PromptOnly | + +The values mean: + +- Hard: the CLI blocks the tools. +- PromptOnly: the agent is told about the restriction, but nothing blocks it. +- SandboxApprox: a sandbox approximates the set. +- Ignored: the harness cannot apply the restriction; auto-approval is always on + there. + +Note that a codex turn that continues a conversation with `Only` is Ignored +rather than SandboxApprox. + +## Tuning an agent + +Every harness shares these builders: `withModel`, `withCheapModel`, +`withAutoApprove`, `withSystemPrompt`, `withName` (which gives the agent its +own line in the cost log), `withReadOnly`, `withNetworkOnly` and +`withSelfManagedGit`. Each returns a new agent on the same harness. Agents on +the same harness can continue each other's conversations; see `chat.withAgent` +in [Talking to agents](talking-to-agents.md). + +By default, the runtime owns git: every write-capable turn is told not to +commit, push or switch branches. `withSelfManagedGit` opts one agent out of +this rule. + +To replace an agent for the whole flow, or to supply your own tool +implementations, see [Extending flows in code](extending.md). diff --git a/docs/authoring/extending.md b/docs/authoring/extending.md new file mode 100644 index 000000000..0b7d48152 --- /dev/null +++ b/docs/authoring/extending.md @@ -0,0 +1,102 @@ +# Extending flows in code + +Most of what `flow(...)` builds by default can be replaced from the script. +There are two exceptions. `git` cannot be replaced, because the runtime owns +the run's branch and commits through it. The harness SPI cannot be replaced +either; a new harness is added as an Orca module. + +## `flow(...)` parameters + +The full signature of `flow(...)` is: + +```scala +flow( + args: OrcaArgs, + workDir?, interaction?, extraListeners?, + branchNaming?, stackSettings?, + planningAgent?, codingAgent?, reviewAgent?, + claude?, codex?, opencode?, pi?, gemini?, + gh?, fs?, prompts?, pricing? +)(body) +``` + +The parameters are: + +- `args`: the parsed command line. A script can change it before passing it + on; for example `OrcaArgs(args).copy(target = RunTarget.Worktree)` overrides + the flags, see + [Branches, resume and worktrees](../using/run-lifecycle.md#run-targets). +- `workDir`: the repository to run in; defaults to the current directory. +- `interaction`: your own `orca.backend.Interaction`, the object that asks the + user questions and shows approval requests, for example over Slack instead + of the terminal. It is not exported from `orca.*`, so import it by its full + path. +- `extraListeners`: additional `OrcaListener`s, which receive the run's events + (stages, agent turns, tool calls). +- `branchNaming`: `Some(BranchNamingStrategy.issue(handle))`, with an + `IssueHandle`, names the branch after an issue instead of after a label that + a cheap model derives. `--branch ` still overrides it. +- `stackSettings`: `Some(StackSettings(...))` pins the format, lint and test + commands. The stack keys in the settings file are then ignored and never + written; the agent keys are still used. +- `planningAgent`, `codingAgent`, `reviewAgent`: per-role agent overrides, for + example `Some(_.claude.opus)`. They take precedence over the project and + global [settings](../using/settings.md). +- `claude`, `codex`, `opencode`, `pi`, `gemini`: agent factories, see + [Replacing an agent](#replacing-an-agent) below. +- `gh`, `fs`: your own tool implementations, for example `gh = Some(myGh)`. +- `prompts`: the per-call prompt wrappers (autonomous, interactive, retry), as + one set. +- `pricing`: the price table that the cost log and the closing summary use. + +## Replacing an agent + +Each agent slot takes a factory rather than an agent. The factory receives the +attempt's `AgentWiring` (the event sink, interaction, working directory and +prompts), so that your agent reports events the same way the defaults do. For +example: + +```scala +flow(OrcaArgs(args), claude = Some(w => ClaudeAgents.default(w).opus)) +``` + +Factories exist for all five harnesses: `ClaudeAgents.default(w)`, +`CodexAgents.default(w)`, `GeminiAgents.default(w)`, `PiAgents.default(w)` and +`OpencodeAgents.default(w, launcher)`. The accessors they offer are listed in +[Backends](../api/backends.md). Each slot has the type +`AgentWiring => Ox ?=> Agent`. The factory runs inside the run's +[Ox](https://ox.softwaremill.com/) scope, so a harness can tie a long-lived +process to it; OpenCode uses this for its shared `opencode serve`. + +`OpencodeLauncher.ollama("qwen3-coder")` starts that server via `ollama launch +opencode`, with that one model pinned, see +[Agent CLIs](../using/agent-clis.md). + +## Customising prompts + +Every helper that sends a prompt to an LLM has an `instructions: String` +parameter with a default. The default is a constant on a `XxxPrompts` object +that lives next to the helper. You can override it entirely, or compose with +the default: + +```scala +import orca.plan.{Plan, PlanPrompts} + +Plan.interactive.from( + userPrompt, + planningAgent, + instructions = PlanPrompts.Planning + "\n\nPrioritise observability tasks first." +) +``` + +The prompt objects and the constants they hold are: + +- `orca.plan.PlanPrompts`: `Planning`, `AssessThenPlan`, `Triage`, `Review` +- `orca.pr.PrPrompts`: `Summarise` +- `orca.review.ReviewLoopPrompts`: `Fix`, `SelectReviewers`, `SummariseLint` +- `orca.review.ReviewerPrompts`: the per-reviewer system prompts + +To retune a reviewer for one project without writing code, add a file under +`.orca/reviewers/` instead, see [Custom reviewers](../using/reviewers.md). The +convention behind these objects is +[ADR 0010](https://github.com/VirtusLab/orca/blob/master/adr/0010-prompts-and-helpers-convention.md). diff --git a/docs/authoring/gates-and-checks.md b/docs/authoring/gates-and-checks.md new file mode 100644 index 000000000..96b49613c --- /dev/null +++ b/docs/authoring/gates-and-checks.md @@ -0,0 +1,108 @@ +# Gates and checks + +Besides the reviewers, each review round can run three kinds of verification +that do not involve an LLM: a format gate, a lint gate, and Scala checks. This +page describes each of them, and how to turn the gates on and off. + +## Format + +The `formatCommands` run before each review round, in the flow's working +directory. This way the reviewers never see formatting noise in the change set. + +## Lint + +The `lint` gate runs alongside the reviewers, each round. It is described by a +`Lint(commands, agent)` value: the shell commands to run, plus the cheap agent +that summarises their output into a +[`ReviewResult`](../api/data-structures.md#review). + +The lint gate is also available as a standalone call, which you can use inside +a fork as well. It comes in two variants: + +- `lint(commands, agent, instructions?)` runs the commands in order with + `bash -c`, all of them even if one fails, and then has `agent` turn the + labelled output into a `ReviewResult`. Long output is written under + `.orca/cache/` for the agent to read, so it cannot overflow the context. +- `lint(commands, summariser, instructions)` works as above, but summarises + into an existing `Lint.summariser(agent)` conversation, so that a gate which + runs several times in one stage resumes the same session. It returns a + `LintReport`. The review loop does this for you. + +```{warning} +Do not reuse a summariser after it has reported findings: it may repeat them +when a later run no longer shows them. +``` + +Note that the review loop does not run the `test` commands, as it stays +deliberately cheap. If you want to run the tests, read them as +`summon[FlowContext].stackSettings.test` (see +[Data structures](../api/data-structures.md#settings)) and run them in a stage +of your own. + +## Checks + +A `ReviewCheck` is Scala code with a `name` and an `evaluate(): ReviewResult` +method: for example a benchmark, an HTTP probe, or an assertion. Pass it in the +`checks` list of either review call, and its findings go to the fixer together +with the reviewers' findings. + +Checks run one at a time, after the format commands and before the reviewers +and the lint gate, so a check that builds or times the code has the machine to +itself. A check must not modify sources. Keep a finding's title the same across +rounds and put the measurements in its description, because the loop matches a +check's finding to the one it already holds by its title and file. + +With no reviewers at all, the loop just evaluates the check and fixes. For +example: + +```scala +val benchmark = new ReviewCheck: + def name = "benchmark" + def evaluate()(using ctx: FlowContext, ev: InStage): ReviewResult = + val ms = os.proc("./bench.sh") // os-lib + .call(cwd = ctx.workDir, stderr = os.Pipe).out.trim().toInt + if ms <= 200 then ReviewResult.empty + else ReviewResult(List(ReviewFinding(Title("Request too slow"), + s"p99 is $ms ms; the target is 200 ms", location = None, + suggestion = None, reopens = None))) + +stage("Speed up"): + reviewAndFixLoop( + coderSession = session, + reviewers = Nil, + task = Task(Title("Make requests faster"), ""), + lint = Configured.Off, + checks = List(benchmark) + ) +``` + +`InStage` is the capability every agent run takes, see +[Capabilities](capabilities.md). Note that in one `reviewThenFix` call a check +can run up to three times: before the review, after the fix, and after the +second fix. + +## Turning gates on and off + +The `formatCommands` and `lint` parameters of `reviewThenFix` and +`reviewAndFixLoop` are `Configured` values. The default reads the project's +[stack settings](../using/settings.md); `Off` disables the gate; and +`Use(value)` gives one explicitly: + +```scala +enum Configured[+A]: + case FromSettings // resolve from the run's stack settings (the default) + case Off // explicitly disabled for this call + case Use(value: A) // explicit value; settings ignored +``` + +`FromSettings` uses `stackSettings.format` for `formatCommands`, and +`Lint(stackSettings.lint, reviewAgent.cheap)` for `lint`. An empty command list +means no gate, so empty settings behave like `Off`. If you want the format gate +only, pass `lint = Configured.Off`. + +## Recording your own findings + +`OpenFinding.custom(title, reason, location)` is an open finding that a flow +records itself, for example when a gate it runs outside the loop still fails. +Add it to the `OpenFindings` handed to the PR step, or pass it in +`priorOpenFindings` so that a loop's reviewers see it. diff --git a/docs/authoring/planning.md b/docs/authoring/planning.md new file mode 100644 index 000000000..d8d45c0cf --- /dev/null +++ b/docs/authoring/planning.md @@ -0,0 +1,81 @@ +# Planning + +`Plan` (available through `import orca.{*, given}`) holds the planning entry +points. Each call has the form `Plan..`, where the mode is +either `autonomous` or `interactive`, and every operation works in both modes. +Pass `planningAgent` as the `agent`. + +In the `autonomous` mode, the planner works with +[`NetworkOnly`](choosing-agents.md#tool-sets) tools and no human is involved. +In the `interactive` mode, the agent can ask the user questions. + +The operations are: + +- `from(userPrompt, agent, instructions?)` returns a `Plan`. Autonomously, it + plans in one turn; interactively, you drive the planner conversationally. +- `assessThenPlan(userPrompt, agent, instructions?)` returns a `Verdict[Plan]`. + It first assesses the prompt, then either continues with `Proceed(plan)` or + returns a `Rejection`. Interactively, it can ask the user to clarify instead + of rejecting. +- `triage(report, agent, instructions?)` returns a `Triage`, which classifies + a bug report as not a bug, untestable, or testable. Interactively, it can ask + clarifying questions. + +The `instructions` argument is optional and replaces the helper's prompt, see +[Customising prompts](extending.md#customising-prompts). + +## The plan + +A plan is a `Plan(epicId, description, tasks, brief)`, where: + +- `tasks` is the list of `Task(title: Title, description: String)` to + implement, in order. `Title` wraps a short label. +- `brief` is a concise briefing on the codebase. Feed it to the implementer + session as its seed; `plan.taskPrompt(task)` prepends the brief to a task's + description. +- `epicId` is a kebab-case identifier for the plan. It is not the branch name: + the run names its branch separately. + +## `WithChat` + +Every operation returns a `WithChat[]`: the result together with the +`Chat` that produced it (see +[Ephemeral chats](talking-to-agents.md#ephemeral-chats)). With it, you can: + +- keep talking to the planner using `chat.run(...)`. Note that these later + turns have the `Full` tool set, not the planner's read-only one. +- take `.value` and seed an implementer session with `plan.brief`. + +The chat is lost on resume, which is why the shipped flows take `.value` right +away. When you want both, destructure: + +```scala +val WithChat(chat, plan) = Plan.autonomous.from(userPrompt, planningAgent) +``` + +## Reviewing the plan + +Given a `WithChat[Plan]`, `.reviewed()` refines the plan before implementing +it: the planner reviews its own draft with read-only tools and returns an +improved `Plan`. It chains naturally: + +```scala +val plan = Plan.autonomous.from(userPrompt, planningAgent).reviewed().value +``` + +`.reviewed(variant = _.cheap)` runs the review turn on a variant of the +planner's agent, for example its cheap model. + +## Verdicts and triage + +`assessThenPlan` returns a `Verdict`: either `Verdict.Proceed(plan)`, meaning +the plan should be implemented, or `Verdict.Rejection(kind, body)`, where +`kind` says whether the rejection is a question, a critique or a refusal. The +flow shows a rejection to whoever asked, for example as an issue comment; +`flows/issue-pr.sc` does this. + +`triage` returns a `Triage` sum type to pattern-match on: the cases are +`NotABug`, `Untestable` and `Testable`, each carrying its own fields. +`flows/issue-pr-bugfix.sc` uses it to decide between a comment and a +reproduction test. The same flow asks the agent for a `BugReportMatch` to check +that a CI failure matches the report. diff --git a/docs/authoring/pull-requests.md b/docs/authoring/pull-requests.md new file mode 100644 index 000000000..e6a748867 --- /dev/null +++ b/docs/authoring/pull-requests.md @@ -0,0 +1,88 @@ +# Pull requests + +`import orca.pr.*` brings the PR helpers into scope. They build on the `gh` +tool described in [Git, GitHub and file tools](../api/tools.md). + +## Opening a PR + +There are two calls that open a PR, and one that only writes its description. + +### `openPrIfGitHub` + +`openPrIfGitHub(summarisingAgent, openFindings, title?, body?, context?, instructions?): Option[PrHandle]` +pushes the branch, summarises the change and opens the PR, provided that +`gh.availability` says the checkout is on a reachable GitHub. Otherwise it +emits one step saying why and returns `None`; the run then finishes. This is +how every code-producing built-in flow ends. + +The `None` cases are: no remote, a remote that is not GitHub, a GitHub host +that `gh` cannot reach, a run that changed no code, or a push or create that +the remote refused. The probe is skipped on a resume that replays the push +stage; a resume replays what its push and create stages recorded, a refusal +included. + +### `openPrFromBranch` + +`openPrFromBranch(summarisingAgent, openFindings, title?, body?, context?, instructions?): PrHandle` +runs the same three stages, but fails the run when there is no GitHub remote +or `gh` login. + +### `summarisePr` + +`summarisePr(agent, diff, context?, instructions?): PrSummary` turns a branch +diff into a [`PrSummary(title, body)`](../api/data-structures.md#labels-and-handles), +ready for `gh.createPr`. `context` is an optional preamble that the model +anchors the description to. An oversized diff is truncated. Use a +[cheap](choosing-agents.md#the-cheap-tier) model here. + +```{note} +Both open calls create stages, so they cannot be called inside a `stage(...)` +body. Call them at the top level of the flow. +``` + +### Findings, title, body and context + +`openFindings` is the final review's +[`OpenFindings`](review.md#what-comes-back). The PR body lists each entry under +"Open review findings": its title, its location if it has one, and the reason +it is open. If the review was skipped, a line says so. The same section also +goes to the run output, including when the PR fails. + +`title` and `body` are functions `PrSummary => String` that rewrite the +generated text. `context` defaults to the run's prompt, and with that default +the summariser adds `Closes #N` for each issue the prompt names. If you pass +your own `context`, you need to add the `Closes` line yourself, through +`body`: + +```scala +val issue = gh.readIssue(handle) // handle: an IssueHandle + +openPrIfGitHub( + summarisingAgent = codingAgent.cheap, + openFindings = openFindings, + context = Some(issue.body), + body = s => s"${s.body}\n\nCloses #42." +) +``` + +After a PR is opened, the run returns your checkout to the branch you started +on, see [Branches, resume and worktrees](../using/run-lifecycle.md). + +## Writing your own PR body + +A flow that opens or updates its PR with bare `gh` calls can still report the +open findings in the same way, using three helpers: + +- `bodyWithOpenFindings(body, open)` returns `body` with the "Open review + findings" section appended, or unchanged when nothing is open and the review + ran. +- `reportOpenFindings(open)` prints that section to the run output. Call it + before your PR step. +- `recordOpenedPr(pr)` records the PR URL as the run's published work, so that + the checkout returns to the starting branch and the closing summary names + the PR. Call it inside the stage that opened the PR, so that the stage's + commit carries the record for resume. + +The `gh` writes are idempotent, so a stage that opens a PR or posts a comment +is safe to re-run after a crash. See the +[authoring rules](stages.md#authoring-rules) and [`gh`](../api/tools.md#gh). diff --git a/docs/authoring/review.md b/docs/authoring/review.md new file mode 100644 index 000000000..9120d0b14 --- /dev/null +++ b/docs/authoring/review.md @@ -0,0 +1,142 @@ +# Review and fix loops + +`import orca.review.*` brings two review calls into scope. Both run reviewers +against a change, hand whatever they find to the coder's session to fix, and +return the findings that are still open at the end. Each round can also run +format and lint gates and Scala checks alongside the reviewers; these are +described in [Gates and checks](gates-and-checks.md). + +## The two calls + +### `reviewThenFix` + +`reviewThenFix(coderSession, reviewers, task, ...)` runs one review round, +followed by one fix turn if the review found anything. The reviewer findings +are not re-checked after the fix: the fixer's word is taken. The lint gate and +the checks, however, are re-run over the fix, with one more fix turn if they +still fail. Reviewers are picked once, with `ReviewerSelector.agentDriven`. + +### `reviewAndFixLoop` + +`reviewAndFixLoop(coderSession, reviewers, task, ..., maxFixTurns?)` reviews, +fixes and re-evaluates, until the reviewers come back clean, the fixer reports +no fixes, or `maxFixTurns` fix turns have run. The default is 3, so up to four +rounds. + +### Parameters + +Both calls share these parameters: + +- `coderSession`: the `FlowSession` that applies the fixes +- `reviewers`: the roster, a `List[ReviewerAgent]`, see [Rosters](#rosters) +- `task`: a `Task(Title(...), description)` describing the change, see + [Planning](planning.md) +- `userRequest`: the user's request, for when the prompt is only a pointer, + such as an issue number; defaults to the run's prompt +- `diff`: which change set the reviewers see, see + [What reviewers see](#what-reviewers-see) +- `formatCommands`, `lint`, `checks`: the gates and checks, see + [Gates and checks](gates-and-checks.md) +- `priorOpenFindings`: findings an earlier review left open; they are shown to + the reviewers +- `fixInstructions`: the fixer's prompt, see + [Customising prompts](extending.md#customising-prompts) +- `reviewerSelection` (`reviewAndFixLoop` only): how reviewers are picked each + round, see [Selecting reviewers per round](#selecting-reviewers-per-round) + +The built-in flows use `reviewThenFix` after each task, because the final +review sees that code again anyway. They end with a single `reviewAndFixLoop` +over the whole run, because nothing reviews the code after it. For example: + +```scala +stage("Final review"): + reviewAndFixLoop( + coderSession = session, + reviewers = allReviewers(reviewAgent), + task = Task(Title("The whole planned change"), plan.brief), + diff = ReviewDiff.WholeRun, + maxFixTurns = 5 + ) +``` + +## What comes back + +Both calls return [`OpenFindings`](../api/data-structures.md#review): every +finding the review left open, each with a reason. A finding stays open when the +fixer declined it or did not mention it, when it was first reported in the +round that hit the cap, or when lint or a check still fails on it. Hand the +result to the PR step, which lists the entries in the PR body, see +[Pull requests](pull-requests.md). A flow can also add entries of its own with +`OpenFinding.custom`, see [Gates and checks](gates-and-checks.md). + +## What reviewers see + +Each reviewer receives the task and the change set. + +- **The task.** The task's title and description, each under its own label, + plus the user's request. Keeping them apart lets a reviewer report a finding + against the planner's choice, and not only against the code. A flow with no + planning stage passes its prompt as the title and an empty description. +- **The change set.** By default, everything the enclosing stage has produced + since it began, whether or not the agent committed along the way. It is + re-sampled each round, so later rounds see the fixes. Passing + `diff = ReviewDiff.WholeRun` widens it to everything since the commit the + run started from, which is what you want for a stage that follows the + per-task work; reviewers are then told that the change spans every stage. + Passing `diff = ReviewDiff.Pinned(text)` sends exactly that text, every + round: reviewers are not told a base commit, the picker's changed-file list + is read off the diff text, and a reviewer resumed in a later round is told + there is no new change set. + +Note that a change set larger than 128 KiB is cut down: the reviewer gets as +many whole files as fit, followed by a list naming every other changed file +with its line counts, and reads those itself. A pinned diff is sent as given. + +```{note} +`WholeRun` needs the commit the run started from. If the progress log has none +(because the run predates that record), or it was rebased away, the call emits +a step saying so and returns without reviewing. +``` + +Every finding reaches the fixer unfiltered. + +## Rosters + +There are three ways to build the `reviewers` list: + +- `allReviewers(agent)` gives you every reviewer in the catalog, each as a + read-only agent built from `agent`. See + [Custom reviewers](../using/reviewers.md). +- `minimalReviewers(agent)` gives you code-functionality, readability and test, + plus every reviewer discovered in `.orca/reviewers/` or the global tier. +- `reviewerCatalog` holds the run's resolved definitions, as `.all` and + `.minimal`, so that you can filter them yourself. Compose a `List[Reviewer]` + from it, from `ReviewerPrompts` (the shipped entries alone), or from your own + `Reviewer(ReviewerSlug(name), description, systemPrompt)`, and then pass it + to `buildReviewers(agent, list)`. + +## Selecting reviewers per round + +`reviewerSelection` defaults to `ReviewerSelector.default`, which narrows the +roster in two ways. First, a picker running on `reviewAgent`'s +[cheap tier](choosing-agents.md#the-cheap-tier) chooses the reviewers for +round one, based on each reviewer's description and the changed paths. Second, +each later round re-runs only the reviewers that reported a finding in the +round before. In other words, a quiet reviewer stops costing a turn, but it +also does not see the later fixes. If the narrowing would leave no reviewer +while a lint finding keeps the loop going, the round-one selection runs again +and a step says so. + +The available selectors are: + +- `default`: the same as `narrowingAcrossRounds(agentDriven)` +- `allEveryRound`: the whole roster, every round, with no picker +- `agentDriven`: pick once with `reviewAgent.cheap`, and replay that pick every + round +- `agentDriven(agent, instructions?)`: as above, with a picker and brief of + your choice +- `narrowingAcrossRounds(base)`: adds the per-round narrowing on top of any + `base` selector + +Note that a reviewer's `files:` pattern gates whether the picker is offered it +at all, see [Custom reviewers](../using/reviewers.md#file-format). diff --git a/docs/authoring/stages.md b/docs/authoring/stages.md new file mode 100644 index 000000000..37bc906d0 --- /dev/null +++ b/docs/authoring/stages.md @@ -0,0 +1,88 @@ +# Stages + +`stage(name)(body)` is the committing, resumable unit of work. When the body +completes successfully, Orca records its result in the progress log and makes +a single commit, containing the code changes together with the log update. On +a re-run, a stage that already has a recorded result is skipped, and the stored +result is returned instead of running the body again. + +```scala +def stage[T: JsonData](name: String, commitMessage: Option[T => String] = None)(body: => T): T +``` + +The signature is simplified: the body also receives the capability tokens +described in [Capabilities](capabilities.md). + +A few things to note: + +- The result type `T` needs a `JsonData` instance, so that it can be stored in + the progress log. `case class Foo(...) derives JsonData` is enough; `Unit`, + `String` and the library's own types already have one. +- The commit message defaults to a summary of the diff, written by + `codingAgent.cheap`. Pass `commitMessage` to override it. +- Stages can nest; the output indents nested stages. +- The stage name appears in the event log, in the commit message and in the + resume preamble, so choose one that a reader understands without looking at + the code, for example `"Push + open PR"`. + +## Side effects happen inside stages + +Every side-effecting call must be made inside a `stage` body, and the compiler +enforces this: a mutation outside a stage does not compile. This covers +`git.push`, `fs.write`, `gh` writes, and every `agent.*.run`. + +The following can run anywhere, inside or outside a stage: + +- pure reads, such as `git.uncommittedDiff`, `git.changedFiles`, + `gh.readIssue`, `gh.availability` and `fs.read` +- `display(message)`, which only prints progress output; there is no stage, no + commit and no log entry +- `fail(message)`, which aborts the run with a message; the run stays on the + feature branch, so a re-run resumes +- `agent.session(name, seed)`, since creating the handle only registers a + name; running the session is the side effect + +## Authoring rules + +The compiler does not check the following rules, but they keep a flow +resumable. + +1. **Do not stage reads.** A stage that contains only reads wastes a commit and + a checkpoint. + +2. **Push in a later stage than the edit.** A stage commits only when it + completes, so a `git.push()` in the same stage as the edit pushes nothing. + Put the push in a separate, later stage. Note that `git.push()` and + `gh.createPr` return an `Either`; `.orThrow` fails the stage on a `Left`. + + ```scala + stage("Write failing test"): + session.run("Write the failing test ...") // commits on completion + + val pr = stage("Push + open PR"): // later stage: the test commit exists now + git.push().orThrow + gh.createPr(title = "...", body = "...").orThrow + ``` + +3. **Idempotent external effects, each in its own stage.** Put each PR-open, + comment or push in its own stage. `gh.createPr` reuses an open PR for the + branch, and `gh.upsertComment(target, marker, body)` edits an earlier + comment that carries `marker`. In other words, a resumed stage updates + instead of duplicating. `orcaCommentMarker(userPrompt, purpose)` gives you a + marker that is unique to the run. + +## Long loops + +A review loop is one stage, so it produces one commit. When each iteration is +long, it is better to write the loop in the flow itself, with one stage per +iteration calling the coder session's `.run`, so that a resume picks up at the +last finished iteration. + +## Parallel work + +The **flow thread** is the main thread of your script. A **fork** is a +function running in parallel under `Par.mapUnordered(n)(items)(f)`, which runs +`f` over `items` with at most `n` in parallel. Inside `f` you may call +`agent.run` and `chat.run`. You may not call `stage`, `agent.session` or +`session.run` there; these throw. Note that results come back in completion +order, not in input order. diff --git a/docs/authoring/talking-to-agents.md b/docs/authoring/talking-to-agents.md new file mode 100644 index 000000000..59611cc4a --- /dev/null +++ b/docs/authoring/talking-to-agents.md @@ -0,0 +1,139 @@ +# Talking to agents + +There are three ways to talk to an agent. Which one to pick depends on what the +conversation must survive and who steers it. All of them need to be called +inside a `stage(...)` body, see [Stages](stages.md); that page also defines the +flow thread and forks. + +- **`agent.run(prompt)` / `agent.resultAs[O]...run(input)`** asks a one-shot + question. +- **`agent.chat()`** keeps a conversation going for follow-ups within this + attempt, including inside forks. Each fork creates its own chat. +- **`agent.session(name, seed)`**, on the flow thread, is for work that edits + the tree and must pick up after a crash. +- **`session.chat`** continues a durable conversation from a fork, once the + session has run on the flow thread. + +There are two modes. **`.autonomous`** runs the turn unattended, while +**`.interactive`** lets the agent ask you questions in the terminal. Use +`resultAs[O].interactive` on an agent or a chat when a human steers the turn. +Note that `session.run` has no interactive mode: a turn that a human steered +cannot be rebuilt from a seed on resume. To steer a session, use `session.chat` +after it has run. + +```{warning} +Interactive turns share your terminal, so never run them in parallel. +``` + +## Durable sessions + +`agent.session(name, seed)` returns the session with the given `name` in the +current stage, creating it on the first call. The `FlowSession` handle survives +crash and resume: the same key resumes the same session, with a warning if this +call's seed differs from the recorded one. + +- `name` is the role, for example `"implementer"`, and it is what + [`orca continue `](../using/shell.md) matches. +- The stage half of the key is implicit. A per-task loop that creates + `implementer` inside each task's stage gets one session per task, with + nothing to name by hand. Sessions created in different stages are always + different sessions. Creating one name twice in one stage is an error: give + each its own stage, or rename one of them. +- If you rename the stage, the key moves with it. For example, a re-plan that + rewords a task gives it a fresh session rather than resuming the old + wording's conversation. +- Create the handle inside the stage that uses it. If several stages share one + session, create it outside all of them. A handle cannot be a stage result, + since `FlowSession` has no `JsonData`. Both creating and running happen on + the flow thread. +- The record behind the handle lives in `.orca/cache/`, not in branch history. + This means that the stage that created it can fail, and its retry still + resumes the same conversation. + +```scala +val session = codingAgent.session("implementer", seed = plan.brief) +session.run(task.description) +``` + +### Seeds + +The `seed` is the context needed to rebuild the agent: typically the plan +brief, or the issue body when there is no brief. A fresh session is primed with +the seed on first use. If the harness lost the conversation on resume, the +session is re-seeded, with a warning; the history is gone, and the new +conversation gets the seed and a list of the stages already completed. If the +harness still holds the conversation, the session continues with its full +history and is told once that the working tree only has what earlier stages +committed. A conversation continued through `session.chat` is not told this. + +### How long a session should live + +Every turn sends the whole conversation to the model again, so a session's +cost grows with everything it has done. For this reason, scope a session to a +unit of work, such as a task or a review stage, not to the whole run. The +shipped flows create a session per task, and another one for the final review. + +### Harness swaps + +If a settings edit changes a role's harness between attempts, a session +recorded under the old harness is not resumed against the new one. Instead, +Orca creates a fresh session from the seed and warns. + +## Ephemeral chats + +`agent.chat()` returns a `Chat`, which continues one conversation across +`.run` calls within this attempt only: there is no seeding and no persistence. +Chats work inside `Par.mapUnordered`. A typical use is parallel reviewers, each +with its own multi-turn conversation: + +```scala +val chats = Par.mapUnordered(4)(reviewers): r => + val c = r.chat() + c.run(s"review the diff: $diff") + c // keep the conversation for a later re-review turn +``` + +`chat.withAgent(f)` continues the same conversation on a variant of the chat's +agent (`_.withReadOnly`, `_.cheap`, `_.withName("…")`). This is useful for +turns that need other tools, a cheaper model or their own cost line. The +variant must be built from the chat's agent; another harness is refused. + +`session.chat` exposes a durable session's conversation as an ephemeral chat. +Only one such chat can be open per session at a time. It is refused while the +harness does not hold the conversation, that is before the session's first +run, or after it was lost on resume. + +## Structured output + +`resultAs[O]` defines the shape of the reply. `O` needs a `JsonData[O]` +instance, which is used for schema generation and parsing; `derives JsonData` +on a case class provides one. A parameterless enum that derives `JsonData` +travels as its case name, and the schema lists every name. A sum type whose +cases carry fields cannot be an `O`. + +For example, to ask a yes/no question with a reason attached: + +```scala +case class MergeCheck(ok: Boolean, reason: String) derives JsonData + +val check = reviewAgent.resultAs[MergeCheck].autonomous.run("Is this change safe to merge?") +``` + +If you define an `Announce[O]` instance, the event log prints a friendly +summary instead of raw JSON; the library's `Plan` has one. + +## Summary + +| Call | Conversation | Survives crash/resume | Mode | Output | Needs | In a fork | +|---|---|---|---|---|---|---| +| `agent.run(prompt)` | new, one turn | no | autonomous | text | `InStage` | yes | +| `agent.resultAs[O].{autonomous,interactive}.run(input)` | new, one turn | no | either, as called | `O` | `InStage` | yes* | +| `agent.chat()` → `chat.run(prompt)` / `chat.resultAs[O]....run(input)` | new, then continued by every turn | no | either, as called | text or `O` | `InStage` | yes* | +| `agent.session(name, seed)` → `session.run(prompt)` / `session.resultAs[O].run(input)` | named; resumed, or restarted from the seed if the harness lost it | yes | autonomous | text or `O` | `FlowContext`, `FlowControl`, `InStage`, `WorkspaceWrite` | no | +| `session.chat` → as `Chat` | the session's | no (turns not recorded) | either, as called | text or `O` | `InStage` | yes* | +| `Plan.{autonomous,interactive}.*` → `WithChat`; `.reviewed()`, `.chat` | new planning conversation | no | as named | `O` | `FlowContext`, `InStage` | yes* | +| `reviewAndFixLoop` / `reviewThenFix` | new reviewer chats; continues `coderSession` | the coder session does | autonomous | findings | `FlowContext`, `FlowControl`, `InStage`, `WorkspaceWrite` | no | +| `lint(commands, agent)` | new, or continues a `Lint.summariser` | no | autonomous | `ReviewResult` | `FlowContext`, `InStage` | yes | + +\* Interactive turns share your terminal: run them one at a time. The "Needs" +column lists the [capabilities](capabilities.md) each call takes. diff --git a/docs/authoring/tutorial.md b/docs/authoring/tutorial.md new file mode 100644 index 000000000..c4c347115 --- /dev/null +++ b/docs/authoring/tutorial.md @@ -0,0 +1,165 @@ +# Writing your first flow + +This page builds the shipped `implement.sc` piece by piece. The flow plans a +prompt into tasks, implements and reviews each task, reviews the whole change, +and finally opens a PR. + +## The header + +Save the file as `implement.sc`. Every flow starts with the same header, which +pins the Scala version, the Orca dependency and the JVM: + +```scala +//> using scala 3.9.0 +//> using dep "org.virtuslab::orca:0.1.10" +//> using jvm 21 + +import orca.{*, given} +``` + +If the first line of the file is a `//` comment, `orca list` shows it as the +flow's description. + +## The body + +`flow(OrcaArgs(args))` parses the command line and runs the body. Inside the +body, `userPrompt` is the prompt given on the command line, and +`planningAgent`, `codingAgent` and `reviewAgent` are the role agents resolved +from [settings](../using/settings.md). + +```scala +flow(OrcaArgs(args)): + val plan = stage("Plan"): + Plan.autonomous.from(userPrompt, planningAgent).value +``` + +`stage` is the committing, resumable unit of work. Here, the planner produces +the plan in a single turn. The result is recorded in the progress log, so if +you re-run the flow with the same prompt, this stage is skipped and the stored +plan is read back instead. `.value` drops the planning chat and keeps only the +`Plan`; see [Planning](planning.md) for what else you can do with the planner's +result. + +## One stage per task + +Next, we create one stage for each task in the plan: + +```scala + val taskOpenFindings = + for task <- plan.tasks yield + stage(s"Task: ${task.title}"): + val session = codingAgent.session("implementer", seed = plan.brief) + session.run(task.description) + reviewThenFix( + coderSession = session, + reviewers = allReviewers(reviewAgent), + task = task + ) +``` + +As with the planning stage, a re-run skips the tasks that are already +completed and picks up at the first incomplete one. + +`codingAgent.session("implementer", seed = plan.brief)` creates a durable +conversation. A session is keyed by its name plus the stage it is created in, +so this loop creates one session per task without any extra naming. On first +use, the session is primed with the plan's brief, and if the harness loses the +conversation, it is started again from that seed. [Talking to +agents](talking-to-agents.md) describes sessions in detail. + +`reviewThenFix` runs one review round followed by one fix turn. It returns the +findings that it left open, so that the final review below can be told about +them. See [Review and fix loops](review.md) for how reviewers are picked, and +how format and lint run. + +## The final review + +Each task ran only one review round, so nobody has checked the fixes +themselves. The final review loops over the whole change until the reviewers +are satisfied: + +```scala + val openFindings = stage("Final review"): + val finalFixer = codingAgent.session("final-fixer", seed = plan.brief) + reviewAndFixLoop( + coderSession = finalFixer, + reviewers = allReviewers(reviewAgent), + task = Task(Title("The whole planned change"), plan.brief), + diff = ReviewDiff.WholeRun, + maxFixTurns = 5, + priorOpenFindings = taskOpenFindings.flatMap(_.findings) + ) +``` + +This is a new session, because the per-task sessions are keyed to their own +stages; it is seeded the same way as they were. `Title` wraps a task title. +`ReviewDiff.WholeRun` shows the reviewers everything that changed since the run +started. `maxFixTurns = 5` is above the default of 3, because nothing reviews +the code after this loop. Finally, `priorOpenFindings` hands over what the +per-task reviews left open. + +## Open a PR + +The last step opens a pull request: + +```scala + openPrIfGitHub( + summarisingAgent = codingAgent.cheap, + openFindings = openFindings + ) +``` + +A PR is opened when the repository is on GitHub and `gh` can reach it. +Otherwise, the flow prints one line saying why not. Any findings that the +final review loop left open are listed in the PR body. See +[Pull requests](pull-requests.md) for the details. + +## Run it + +To run the flow, pass the file and a prompt to `orca run`: + +```bash +orca run implement.sc "Add a rate limiter to the /login endpoint" +``` + +or, without installing Orca: + +```bash +scala-cli run --workspace "$(mktemp -d)" implement.sc -- "Add a rate limiter to the /login endpoint" +``` + +The run creates a branch named from the prompt and commits after each stage. +On success, it switches you back to the branch you started on, while the work +stays on the run's branch and PR. If you interrupt the run and then run the +same command again, it resumes from the last committed stage. +[Branches, resume and worktrees](../using/run-lifecycle.md) covers the details. + +```{note} +For editing flows with code completion, the +[Metals](https://scalameta.org/metals/) VS Code extension works well. +``` + +## The smallest flow + +A flow does not need planning or review. For example, this one hands the +prompt to [Pi](../api/backends.md) and commits whatever it did: + +```scala +flow(OrcaArgs(args)): + stage("Run"): + val session = pi.session("run", seed = userPrompt) + session.run(userPrompt) +``` + +## Let an agent write it + +You do not have to write a flow by hand. `orca create ""` has your +configured agents write a flow for you, and `orca fork ""` does +the same, starting from an existing flow. See [Orca Shell](../using/shell.md). + +## Where to go next + +- [Stages](stages.md) describes the rules that keep a flow resumable. +- [Choosing agents](choosing-agents.md) covers roles, tiers and tool limits. +- The other [built-in flows](../using/built-in-flows.md) are worked examples of + issue handling, triage and review-only flows. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..d1db27d7a --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# +# Orca documentation build configuration file. + +# https://about.readthedocs.com/blog/2024/07/addons-by-default/ +import os + +# Define the canonical URL if you are using a custom domain on Read the Docs +html_baseurl = os.environ.get( + "READTHEDOCS_CANONICAL_URL", + "https://orca.virtuslab.com/", +) + +# Tell Jinja2 templates the build is running on Read the Docs +if os.environ.get("READTHEDOCS", "") == "True": + if "html_context" not in globals(): + html_context = {} + html_context["READTHEDOCS"] = True + +# -- General configuration ------------------------------------------------ + +extensions = ['myst_parser', 'sphinx_rtd_theme', 'sphinxcontrib.mermaid', 'sphinx_llms_txt'] + +myst_enable_extensions = ['attrs_block', 'colon_fence'] +myst_heading_anchors = 3 + +llms_txt_title = "Orca" +llms_txt_summary = "Deterministic, AI-driven development flows: Scala scripts that orchestrate coding agents (Claude, Codex, OpenCode, Pi, Gemini) through resumable plan-implement-review workflows" +llms_txt_full_file = True + +# The suffix(es) of source filenames. +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Orca' +copyright = u'2026, VirtusLab' +author = u'VirtusLab' + +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1' + +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. `plans`, `research` and +# `superpowers` are internal working documents, not part of the published site. +exclude_patterns = [ + '_build', 'Thumbs.db', '.DS_Store', + '.venv', 'venv', 'env', + '**/site-packages/**', + '**/node_modules/**', + '_templates', + 'requirements.txt', + 'README.md', + 'plans', + 'research', + 'superpowers', +] + +pygments_style = 'default' + +# Pygments has no lexer for `properties`, which the settings pages use for +# `settings.properties` fences; alias it to the INI lexer. +from pygments.lexers.configs import IniLexer +from sphinx.highlighting import lexers +lexers['properties'] = IniLexer() + +# -- Options for HTML output ---------------------------------------------- + +html_theme = 'sphinx_rtd_theme' + +html_static_path = ['_static'] +html_css_files = ['css/custom.css'] + +htmlhelp_basename = 'orcadoc' + +highlight_language = 'scala' + +# configure edit on github: https://docs.readthedocs.io/en/latest/guides/vcs.html +html_context = { + 'display_github': True, + 'github_user': 'VirtusLab', + 'github_repo': 'orca', + 'github_version': 'master', + 'conf_py_path': '/docs/', +} diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 000000000..897a7ca85 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,74 @@ +# Development + +This page describes how Orca itself is built. The full material lives in the +repository: +[CONTRIBUTING.md](https://github.com/VirtusLab/orca/blob/master/CONTRIBUTING.md) +has the build, test and local-run recipes, +[AGENTS.md](https://github.com/VirtusLab/orca/blob/master/AGENTS.md) describes +the internals and coding conventions, and +[`adr/`](https://github.com/VirtusLab/orca/tree/master/adr) holds the +architecture decision records. + +## Layout + +Orca is written in Scala 3. It uses [Ox](https://ox.softwaremill.com/) for +structured concurrency, [tapir](https://tapir.softwaremill.com/) for JSON +Schema derivation and +[jsoniter-scala](https://github.com/plokhotnyuk/jsoniter-scala) for codecs. + +The repository is organised into these modules: + +``` +orca/ +├── tools/ # tool traits + os-lib-backed impls (git/gh/fs), LLM SPI, session durability, events +├── flow/ # stage/display/fail, FlowContext/FlowControl; orca.{plan,review,pr,progress} +├── claude/ codex/ gemini/ opencode/ pi/ # one module per coding-agent backend +├── runner/ # flow() entry, default wiring, the run lifecycle, terminal UI +├── shell/ # orca-shell: the `orca` CLI +├── flows/ # the built-in flow scripts, bundled into the shell +└── docs/ # this site +``` + +The modules depend on each other as follows: + +``` +tools (standalone) + ├── flow → tools + ├── claude / codex / gemini / + │ opencode / pi → tools + ├── runner → tools + flow + all five backends (published as `orca`) + └── shell → runner (published as `orca-shell`) +``` + +The user-facing surface is `package orca`: `flow`, the tool and role-agent +accessors, `stage`/`display`/`fail`, `JsonData` and `OrcaArgs`. The flow +module adds `orca.plan`, `orca.review` and `orca.pr`. The stage-bound runtime +is specified in [ADR +0018](https://github.com/VirtusLab/orca/blob/master/adr/0018-stage-bound-flow-runtime.md); +read it before touching `stage`, the progress log or sessions. + +## Build and test + +Building Orca requires sbt 1.12+ and JDK 21. The commands you will use most +often are: + +```bash +sbt compile # every module +sbt test # unit tests; no network, no real CLIs +sbt "flow/testOnly *LintTest" # one suite +sbt scalafmtAll +sbt publishLocal # into ~/.ivy2/local, for flows using `//> using repository ivy2Local` +``` + +Integration suites that shell out to real CLIs are gated behind +`ORCA_INTEGRATION=1`. CI runs the unit tests and compiles every built-in flow +against a locally published build, so an API change that breaks a built-in +flow fails CI. CONTRIBUTING.md has the recipe for running a locally built +`orca` shell against a scratch project. + +## Documentation + +This site is built with Sphinx from `docs/`; its +[README](https://github.com/VirtusLab/orca/blob/master/docs/README.md) says +how to run it locally. The release process bumps the Orca version in every doc +snippet. diff --git a/docs/getting-started/how-it-works.md b/docs/getting-started/how-it-works.md new file mode 100644 index 000000000..e929ee448 --- /dev/null +++ b/docs/getting-started/how-it-works.md @@ -0,0 +1,75 @@ +# How Orca works + +## A flow is a script + +A flow is a Scala script whose body is `flow(OrcaArgs(args)): ...`, where +`OrcaArgs` parses the prompt and flags from the script's arguments. Inside the +flow you call agents, read the repository, and group the work into **stages**. +Orca runs the script with scala-cli. Since the script is ordinary code, loops, +conditions and helper functions decide what happens, rather than an agent's +judgement. + +For example, a flow that plans the work, implements and reviews each task, and +finishes with a final review has this shape: + +```{mermaid} +flowchart LR + P[Prompt] --> S1[Stage: Plan] + S1 --> S2[Stage: Task 1
code + review] + S2 --> S3[Stage: Task 2
code + review] + S3 --> S4[Stage: Final review] + S4 --> PR[Push + open PR] + S1 -. commit .-> G[(feature branch)] + S2 -. commit .-> G + S3 -. commit .-> G + S4 -. commit .-> G +``` + +## Stages commit + +The body of a `stage(name)` call is the unit of work. When it finishes, Orca +commits everything it changed, together with an entry in the **progress log**: +a file under `.orca/runs/` that is committed on the feature branch. In other +words, one stage produces one commit. + +Because the log is committed together with the code, the two cannot drift +apart. This is what makes a run **resumable**: if you run the same prompt +again, each stage that is already in the log is skipped and its recorded result +is reused, so work continues from the first unfinished stage. See +[Branches, resume and worktrees](../using/run-lifecycle.md) for details. + +## Orca owns git + +Orca creates the feature branch and, at the end of the run, removes the +progress log (there is nothing left to resume) and opens the PR. The agents are +told not to commit, push or switch branches: they only edit files, and the flow +decides what happens to the edits. + +Note that a flow can only push, write files, post to GitHub or run an agent +from inside a stage. Such a call anywhere else is a compile error, which +guarantees that every side effect is checkpointed by the stage's commit. See +[Stages](../authoring/stages.md). + +## Agents are yours + +Orca drives the coding-agent CLIs you already use, called +[harnesses](../glossary/users.md#agents-and-conversations): `claude`, `codex`, +`opencode`, `pi` and `gemini`. Which one handles each of the planning, coding +and review roles comes from [settings](../using/settings.md), so a flow never +needs to name a harness. The agents run in your repository with your +instruction files, MCP servers and hooks; see +[Agent CLIs](../using/agent-clis.md). + +A flow can talk to an agent in three ways: as a one-shot question, as a +conversation that ends when the script exits, or as a **session**, which +survives a crash and a resume. See +[Talking to agents](../authoring/talking-to-agents.md). + +## Review is code + +The shipped flows review every task with a set of reviewer agents, each with +its own prompt, and hand the findings back to the coder to fix. A final loop +then reviews the whole change. The reviewers, as well as the lint and format +commands, are configured per project. Any findings that are still open when +the review ends are listed in the PR body. See +[Review and fix loops](../authoring/review.md). diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 000000000..2188ccc61 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,71 @@ +# Quick start + +A flow is a Scala script that tells coding agents what to do. For example, the +built-in [`implement.sc`](https://github.com/VirtusLab/orca/blob/master/flows/implement.sc) +flow plans the work, hands each task to a coding agent, has every change +reviewed by another agent, and opens a pull request. Because the flow is a +program, these steps always happen; nothing depends on an agent remembering +them. + +## What you need + +Before you start, make sure you have: + +- a coding-agent CLI that you are logged in to. Orca calls such a CLI a + [harness](../glossary/users.md#agents-and-conversations); the supported ones + are `claude`, `codex`, `opencode`, `pi` and `gemini`. See + [Agent CLIs](../using/agent-clis.md) for details. +- `git`, and `gh` if you want Orca to open pull requests for you. + +The installer takes care of everything else, including scala-cli and a JVM. + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/VirtusLab/orca/master/install.sh | bash +``` + +The script installs `scala-cli` if it is missing and writes the `orca` +launcher to `~/.local/bin/orca`. If you would rather not install anything, or +want to run a pinned version, [Orca Shell](../using/shell.md) describes what +the script does and how to run the shell directly. + +## First run + +```bash +cd your-project +orca +``` + +On the first run, Orca asks which harness and model you want to use for each +of the planning, coding and review roles. It then shows a menu of flows +(`implement.sc` is first in the list); pick one and enter your prompt, for +example "add a rate limiter to /login". + +You can skip the menu and do the same thing from the command line: + +```bash +orca run implement.sc "add a rate limiter to /login" +``` + +## What happens + +Orca creates a feature branch and plans the change into tasks. Each task is +then implemented and reviewed, a final review runs over the whole change, and, +when the repository is on GitHub, a PR is opened. Each of these steps is a +**stage**, and Orca commits it as soon as it finishes. This is what makes a run +resumable: if it is interrupted, run the same command again and it continues +from the last commit. [How Orca works](how-it-works.md) has the full picture. + +```{warning} +By default, agents edit files and run shell commands without asking, so you +should [run Orca in a sandbox](../using/agent-clis.md#run-in-a-sandbox). +``` + +## Next steps + +- [Ways to use Orca](ways-to-use.md) describes how to run Orca interactively, + from another agent or CI, or as a plain script. +- [Built-in flows](../using/built-in-flows.md) lists what ships with Orca. +- [Writing your first flow](../authoring/tutorial.md) is for when the built-in + flows do not fit. diff --git a/docs/getting-started/ways-to-use.md b/docs/getting-started/ways-to-use.md new file mode 100644 index 000000000..95c3cdc16 --- /dev/null +++ b/docs/getting-started/ways-to-use.md @@ -0,0 +1,61 @@ +# Ways to use Orca + +## Interactively + +The simplest way to start is to run `orca` with no arguments. You get a menu +that lists the flows found in the project, in your global config, and built +into Orca. From the menu you can run a flow, view or edit its source, create a +new flow with an agent's help, or resume a session left by a previous run. +[Orca Shell](../using/shell.md) describes the shell in detail. + +## Headless, from the command line + +Every menu action also has a subcommand, so a script or a CI job can run Orca +without a terminal. For example, to run the `implement.sc` flow with a prompt: + +```bash +orca run implement.sc "add a rate limiter to /login" +``` + +`orca run` takes flags that pick the branch or worktree the run works on. They +are listed in [Orca Shell](../using/shell.md#commands) and explained in +[Branches, resume and worktrees](../using/run-lifecycle.md). Note that which +agent plans, codes and reviews is not passed on the command line: it comes from +[settings](../using/settings.md). + +## From a coding agent, with the skill + +If you already work in a coding agent, it can delegate work to Orca. The +[`skills/orca`](https://github.com/VirtusLab/orca/blob/master/skills/orca/SKILL.md) +skill tells the agent when and how to do that. In Claude Code, for example, +`/orca [prompt]` asks which flow to run and whether to run it on a new branch, +the current branch or a worktree, and then starts it. + +To install the skill into your harness: + +- **Claude Code**: run `/plugin marketplace add VirtusLab/orca`, then + `/plugin install orca@orca-skills`. Alternatively, copy or symlink the + `skills/orca` directory to `~/.claude/skills/orca`. +- **Pi**: run `pi install git:github.com/VirtusLab/orca`. +- **OpenCode**: copy or symlink `skills/orca` to `~/.config/opencode/skills/orca`. +- **Codex**: copy or symlink `skills/orca` to `~/.agents/skills/orca`. + +The skill's +[README](https://github.com/VirtusLab/orca/blob/master/skills/orca/README.md) +has the per-project variants of these paths. + +## As a script + +A flow is a scala-cli script, so you do not need to install Orca to run one; +scala-cli alone is enough: + +```bash +scala-cli run --workspace "$(mktemp -d)" implement.sc -- "add a rate limiter to /login" +``` + +Orca is published to Maven Central, and scala-cli fetches the artifacts on the +first run. The `--workspace` option keeps scala-cli's build output (a +`.scala-build` directory) out of your repository. + +Note that this is exactly how the shell runs flows too, so a script that works +with `orca run` works here unchanged. diff --git a/docs/glossary/developers.md b/docs/glossary/developers.md new file mode 100644 index 000000000..9670d0d49 --- /dev/null +++ b/docs/glossary/developers.md @@ -0,0 +1,99 @@ +# Glossary for developers + +This glossary defines Orca's internal vocabulary, which is used the same way in +identifiers, file names, screen output and prose. Only terms the +[user glossary](users.md) lacks are defined here; for the shared ones, this +page adds the codebase facts. The rest of the internals are described in +[AGENTS.md](https://github.com/VirtusLab/orca/blob/master/AGENTS.md). + +## Review + +The definitions are in the [user glossary](users.md#review); here is how they +map to the code. + +- **finding** — A finding is a `ReviewFinding`, found in + `ReviewResult.findings`. Note that `issue` is not a synonym: in this codebase + it means a GitHub issue (`orca.tools.Issue`, `IssueHandle`). +- **declined** — A declined finding is a `DeclinedFinding`, found in + `FixOutcome.declined`; it is one of several reasons a finding stays open. + This is the wire shape the fixing agent fills, so it carries a title and a + reason and nothing else. +- **open finding** — An open finding is an `OpenFinding`, found in + `OpenFindings` and paired with an `OpenReason`. It is identified by its + `FindingId`, never by its title. A review that could not run at all is + `OpenFindings.skipped`, not an open finding. + +Never name the open findings after one of the reasons. `OpenReason.describe` is +the only place where each reason's prose is written. + +## Persisted state + +Run and attempt are defined in the +[user glossary](users.md#flows-and-runs); here is what each one owns on disk. + +- **run** — A run is keyed by `RunKey`, the 12-hex prefix of SHA-256(prompt). + It owns one feature branch, one progress log + (`.orca/runs/.progress.json`), one session-records file + (`.orca/cache/runs/.sessions.json`) and, under `--worktree`, one + checkout. A successful run ends by deleting its progress log and + session-records file. +- **attempt** — An attempt is keyed by `AttemptId` (`-`). + It owns one manifest (`.orca/cache/attempts/.manifest.json`) and one + cost log (`.cost.jsonl`). A fresh attempt starts a run; a resumed attempt + continues one. + +Never call a process a run. "Task" means only a plan task, and a plan task has +no file of its own. + +## Backends + +These are the words for talking to a coding agent, from the outside in. + +- **call** — A call is one `agent.run`, `session.run` or `chat.run`. A retry + stays inside the call. The review loop's fix turn is a call. +- **turn** — A turn is one exchange that reaches the model: a prompt sent, + events streamed back, one outcome. A retry that reaches the model is a new + turn. `AgentBackend.open` returns one as a `LiveTurn`, the in-flight turn. +- **message** — A message is one assistant message inside a turn. It is closed + by `TurnEvent.AssistantMessageEnd` and shown as one + `OrcaEvent.AssistantMessage`. +- **decoder** — A decoder is a backend's wire protocol as a `LineDecoder`: a + fold over the lines of its stream. `DecodedTurn` runs the decoder over a + turn. +- **conversation** — A conversation is the history a backend keeps across + turns, which a session resumes. A `LiveTurn` is not a conversation. +- **client id** / **wire id** — The client id (`SessionId`) is Orca's own + handle for a session, stable across attempts. The wire id (`WireSessionId`) + is the id the backend knows the conversation by. `SessionId#onWire` is the + only crossing between the two. +- **`IdScheme`** — The `IdScheme` says how wire ids come to be: `ClientClaimed` + means the client id is the wire id (claude, pi), and `ServerMinted` means the + backend mints it on the first turn (codex, gemini, opencode). +- **conversation key** — The conversation key (`OrcaEvent.conversationKey`) is + the wire id, or the client id before one is known. It is the one key that + turns and sessions join on, in events and in the cost log. +- **dispatch** — A dispatch is `SessionSupport.dispatchFor`'s answer for the + next turn: `Fresh` opens a conversation, `Resume` continues one. + `ResumeOrigin` says whether this attempt or an earlier one opened it. +- **settle** — To settle is a decoder's `Step.Settle`: the turn's outcome is + known and later lines are ignored. Only the decoder settles; `SessionSupport` + only *confirms* a wire id restored from an earlier attempt. + +Note that a CLI's own "turn" can differ from Orca's: codex's `turn.completed` +ends Orca's turn, but claude's `num_turns` counts tool calls plus one. + +## Capabilities + +The definitions are in the +[user glossary](users.md#capabilities-and-tool-limits); here are the threading +rules for each token. + +- **`FlowContext`** — `FlowContext` is thread-safe, so forks receive it freely. +- **`FlowControl`** — `FlowControl` must stay on the thread that created it. +- **`InStage`** — `InStage` is the stage-bound token that may be shared: every + agent run takes it, and a fork may capture it. +- **`WorkspaceWrite`** — `WorkspaceWrite` is the stage-bound token that may not + be shared: every git, `gh`, `fs` and progress-log write takes it, and it must + not cross a fork. +- **`RuntimeInStage`** — `RuntimeInStage` is the only way production code mints + stage tokens outside a `stage(...)` body. diff --git a/docs/glossary/users.md b/docs/glossary/users.md new file mode 100644 index 000000000..6cf88cd93 --- /dev/null +++ b/docs/glossary/users.md @@ -0,0 +1,104 @@ +# Glossary for users + +This glossary defines the words these docs use. Orca's internal vocabulary is +in the [developer glossary](developers.md). + +## Flows and runs + +- **flow** — A flow is a Scala script whose body is `flow(OrcaArgs(args)): ...`. + See [Writing your first flow](../authoring/tutorial.md). +- **flow args** — The flow args are the `OrcaArgs`: the prompt and the + command-line flags. +- **prompt** — The prompt is the user's input text, available as `userPrompt` + in a flow body. +- **stage** — A stage, `stage(name)(body)`, is a unit of work that commits on + completion and is skipped on resume. See [Stages](../authoring/stages.md). +- **plan** — A plan, `orca.plan.Plan`, is the task list the planning agent + (the **planner**) produces. See [Planning](../authoring/planning.md). +- **plan task** — A plan task is one `orca.plan.Task` of a plan. The + **plan brief** (`Plan.brief`) is the planner's codebase briefing. +- **run** — A run is one prompt's flow execution, across every process it + takes to finish. See + [Branches, resume and worktrees](../using/run-lifecycle.md). +- **attempt** — An attempt is one of those processes: one `orca run`, one + `flow(...)` call. +- **re-run / resume** — A re-run, or resume, is another attempt of an + unfinished run with the same prompt. It skips the stages already recorded. +- **progress log** — The progress log is `.orca/runs/.progress.json`, + committed with each stage. It records which stages finished, and their + results. +- **run target** — The run target is where a run works: a new branch (the + default), the current branch (`--skip-branch`) or a worktree (`--worktree`). +- **worktree** — A worktree is a second checkout of the repository under + `.orca/worktrees/`. + +## Agents and conversations + +- **harness** (also **backend**) — A harness is the coding-agent CLI Orca + drives: `claude`, `codex`, `opencode`, `pi` or `gemini`. +- **agent** — An agent is a harness with a model and tool settings, such as + `claude`, `codex.mini` or `codingAgent`. See [Backends](../api/backends.md). +- **role agent** — A role agent is `planningAgent`, `codingAgent` or + `reviewAgent`, resolved from [settings](../using/settings.md). +- **cheap tier** — The cheap tier, `agent.cheap`, is the harness's cheaper + model. +- **turn** — A turn is one prompt to an agent and its reply. +- **conversation** — A conversation is the history a harness keeps across the + turns of one chat or session. +- **one-shot / chat / session** — These are the three ways of talking to an + agent: `agent.run` is one turn, `agent.chat()` is a conversation for this + attempt, and `agent.session(name, seed)` is a conversation that survives + resume. See [Talking to agents](../authoring/talking-to-agents.md). +- **session name / session key** — The session name is the session's role, + such as `"implementer"`, which `orca continue` matches. The session key is + the name plus the stage the session is created in. +- **seed / re-seed** — The seed is the context a session starts from, usually + the plan brief. A session whose conversation is lost is re-seeded, that is, + started again from its seed. +- **structured output** — Structured output, `resultAs[O]`, is a reply parsed + into an `O`, which needs a `JsonData[O]`. + +## Review + +- **reviewer** — A reviewer is a prompt saying what to look for, paired with a + read-only agent. +- **reviewer catalog** — The reviewer catalog, `reviewerCatalog`, is every + reviewer a run can use. See [Custom reviewers](../using/reviewers.md). +- **roster** — The roster is the set of reviewers one review call is given. +- **reviewer picker** — The reviewer picker is the cheap agent that chooses, + from the roster, which reviewers run for a task. +- **review round** — A review round is one pass of the picked reviewers, the + lint gate and any checks over the change. See + [Review and fix loops](../authoring/review.md). +- **fix turn** — A fix turn is the coder session's `.run` that fixes a round's + findings. `maxFixTurns` caps how many there are. +- **finding** — A finding is a problem a reviewer, the lint gate or a check + reported. +- **declined finding** — A declined finding is a finding the fixer refused, + with a reason. +- **open finding** — An open finding is a finding the review ended without + resolving. +- **`OpenFindings`** — `OpenFindings` is what a review returns: the findings it + left open. See [Data structures](../api/data-structures.md#review). +- **gate** — A gate is a stack command: `format`, `lint` or `test`. The lint + gate runs each review round. +- **stack settings** — The stack settings are the project's gate commands, + from `.orca/settings.properties`. +- **`Configured`** — `Configured` is how a review call takes a gate: from + settings (the default), off, or a given value. See + [Data structures](../api/data-structures.md#settings). + +## Capabilities and tool limits + +- **capability** — A capability is a compile-checked token a call needs. + `InStage` (for agent calls) and `WorkspaceWrite` (for git, `gh` and file + writes) come from a `stage(...)` body; `FlowControl` (for starting stages and + creating sessions) comes from the `flow(...)` body. `FlowContext`, which + grants reads, is not a capability. See + [Capabilities](../authoring/capabilities.md). +- **fork** — A fork is a function running in parallel under + `Par.mapUnordered`. See [Stages](../authoring/stages.md#parallel-work). +- **`ToolSet`** — A `ToolSet` says which tools an agent has: `ReadOnly`, + `NetworkOnly`, `Full` (the default) or `NoTools`. **Enforcement** is how + strictly each harness holds that limit. See + [Choosing agents](../authoring/choosing-agents.md). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..8795f8cce --- /dev/null +++ b/docs/index.md @@ -0,0 +1,78 @@ +# Orca: deterministic, AI-driven development flows + +Orca allows you to programmatically define software development workflows where +AI agents perform the coding. If you want AI-generated code to always be +reviewed by another agent, don't try to coerce the agents; just express that +requirement in code. Don't waste tokens on formatting, committing, or creating +PRs - all of this can be handled by an ordinary script. + +Orca comes with an `orca` cli, which can be used interactively by humans, or +headlessly by humans and agents alike. A number of built-in flows, implementing +e.g. a plan-implement-review loop, allow you to start using Orca right away. + +Orca flow scripts are written in Scala, and can be run with a single command +through [scala-cli](https://scala-cli.virtuslab.org). Orca's development flows +are resumable, so that if work is interrupted mid-flow for any reason, it can +be continued from the last commit. You can use Orca to orchestrate development +in any language and ecosystem. + +Orca is developed by [VirtusLab](https://virtuslab.com) and hosted on +[GitHub](https://github.com/VirtusLab/orca). + +```{eval-rst} +.. toctree:: + :maxdepth: 2 + :caption: Getting started + + getting-started/quickstart + getting-started/ways-to-use + getting-started/how-it-works + +.. toctree:: + :maxdepth: 2 + :caption: Using Orca + + using/shell + using/agent-clis + using/built-in-flows + using/settings + using/reviewers + using/run-lifecycle + using/output-and-files + +.. toctree:: + :maxdepth: 2 + :caption: Authoring flows + + authoring/tutorial + authoring/stages + authoring/choosing-agents + authoring/talking-to-agents + authoring/planning + authoring/review + authoring/gates-and-checks + authoring/pull-requests + authoring/extending + authoring/capabilities + +.. toctree:: + :maxdepth: 2 + :caption: API reference + + api/backends + api/tools + api/data-structures + +.. toctree:: + :maxdepth: 2 + :caption: Glossary + + glossary/users + glossary/developers + +.. toctree:: + :maxdepth: 2 + :caption: Development + + development +``` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..85e85d1f7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +sphinx_rtd_theme==3.0.2 +sphinx==8.2.3 +sphinx-autobuild==2024.10.3 +myst-parser==4.0.1 +sphinxcontrib-mermaid==1.0.0 +sphinx-llms-txt diff --git a/docs/using/agent-clis.md b/docs/using/agent-clis.md new file mode 100644 index 000000000..046d02ac5 --- /dev/null +++ b/docs/using/agent-clis.md @@ -0,0 +1,72 @@ +# Agent CLIs + +Orca drives the coding-agent CLIs you already have, which it calls +[harnesses](../glossary/users.md#agents-and-conversations): `claude`, `codex`, +`opencode`, `pi` and `gemini`. Each harness manages its own authentication, +and Orca stores no secrets. Before you run a flow, log in to the harness you +use, following its own instructions, and to `gh` if the flow opens PRs or reads +issues. + +## Run in a sandbox + +```{warning} +Run Orca in a sandbox. Flows run unattended by default: the coding agent has +the full tool set (`ToolSet.Full`) and every tool call is auto-approved +(`AutoApprove.All`), so it edits files and runs shell commands without asking. +``` + +You can narrow an agent's tools or auto-approval in the flow itself, see +[Choosing agents](../authoring/choosing-agents.md). For an unattended run, +however, the practical boundary is a VPS or a local sandbox such as +[Sandcat](https://github.com/VirtusLab/sandcat) or +[Docker Sandboxes](https://docs.docker.com/ai/sandboxes/). + +## Your instruction files apply + +Orca's agents are ordinary harness sessions started in your repository, so +they load the same instruction files (`~/.claude/CLAUDE.md`, `CLAUDE.md`, +`CLAUDE.local.md`, `AGENTS.md`, `GEMINI.md`, …), MCP servers, plugins and +hooks as your own sessions do. + +The difference is that no one is present to approve tool calls. This has a few +consequences: + +- Coding turns auto-approve every tool by default. +- On claude, the read-only roles (the planner, the reviewers, and the agent + that picks reviewers) can use only the tools Orca allows. In particular, + your MCP tools are blocked unless your claude settings `permissions.allow` + them. +- On claude, opencode and pi, cheap one-shots such as branch names and default + commit messages run with no tools and no MCP servers. + +Because of this, it is worth checking your instruction files for two things: + +- **Mandatory tool calls.** An instruction like "always call X first" only + works if X is allowed; otherwise, write "if available". +- **A human in the loop.** In an autonomous flow, "ask me before X" or "wait + for confirmation" cannot work, as there is nobody to answer. + +## OpenCode with a local Ollama model + +There are two ways to point OpenCode at a model served by Ollama. + +### Launcher, zero config + +Pass a launcher in the flow script: + +```scala +flow(OrcaArgs(args), opencode = Some(w => OpencodeAgents.default(w, OpencodeLauncher.ollama("qwen3-coder")))) +``` + +Orca then starts the server via `ollama launch opencode`, which injects +Ollama's provider config and pins that one model. Use bare `opencode` in this +case, without `withModel`. This needs the `ollama` CLI with the model already +pulled. See [Extending flows in code](../authoring/extending.md). + +### Manual config + +Declare an `ollama` provider in `~/.config/opencode/opencode.json` (with +baseURL `http://localhost:11434/v1`, your models, and `num_ctx` raised for tool +use), then select a model with `opencode.withModel("ollama", "qwen3-coder")` +(see [Backends](../api/backends.md)). This way you can declare several models +and switch between them per turn. diff --git a/docs/using/built-in-flows.md b/docs/using/built-in-flows.md new file mode 100644 index 000000000..cb1fe4126 --- /dev/null +++ b/docs/using/built-in-flows.md @@ -0,0 +1,97 @@ +# Built-in flows + +Orca ships with the flows described below. `orca list` shows them together +with your project and global flows, and `orca view ` prints a flow's +source. Every flow that changes code opens a PR if `gh` can reach the +repository on GitHub; otherwise it says so and leaves the committed work on the +feature branch. + +All of the flows take the +[role agents](../glossary/users.md#agents-and-conversations) from +[settings](settings.md), and those agents need to be logged in. `gh` is +optional unless a flow says otherwise. + +## `implement.sc` + +This is the default choice. You give it a description of what to build; it +plans the prompt into tasks, implements each task on the run's branch and +reviews it once, then runs a review-and-fix loop over the whole change. +[Source](https://github.com/VirtusLab/orca/blob/master/flows/implement.sc). + +## `implement-interactive.sc` + +The same as `implement.sc`, except that the planner can ask you clarifying +questions before it produces the plan. Note that on a re-run a finished +planning stage is skipped, so you are not asked again. +[Source](https://github.com/VirtusLab/orca/blob/master/flows/implement-interactive.sc). + +## `implement-enhanced.sc` + +`implement.sc` with two extra steps: the planner critiques and improves its +own draft, and a documentation stage updates the project's docs based on what +the tasks changed. +[Source](https://github.com/VirtusLab/orca/blob/master/flows/implement-enhanced.sc). + +## `simple.sc` + +There is no planning here: the prompt is the one task, handed straight to the +coder and then reviewed. This is useful for small, well-scoped changes, where a +plan would be overhead. It is also the flow that `orca create` and `orca fork` +run. [Source](https://github.com/VirtusLab/orca/blob/master/flows/simple.sc). + +## `issue-pr.sc` + +You give it an issue, as `owner/repo#N` or as a URL. The flow reads the issue +and checks it against the repository: are its claims right, is any detail +missing, is it a duplicate, is the scope sane. Depending on the outcome, it +either posts a rejection comment or plans, implements, reviews and opens a PR. +The branch is named `fix/issue-`. This flow needs `gh`. +[Source](https://github.com/VirtusLab/orca/blob/master/flows/issue-pr.sc). + +## `issue-pr-bugfix.sc` + +The bug-report variant of `issue-pr.sc`, taking the same prompt. It triages +the issue first, with three possible outcomes: it is not a bug (the flow +comments), it is a bug that no test can show (the flow comments with +reproduction steps), or it is a testable bug. For a testable bug, the flow +writes a failing test, opens a tentative PR, waits for CI to go red, confirms +that the failure matches the report, and only then fixes the bug and updates +the PR. This flow needs `gh`. +[Source](https://github.com/VirtusLab/orca/blob/master/flows/issue-pr-bugfix.sc). + +## `review.sc` + +Review only. The prompt says what to review: a PR reference or URL, a branch, +"the uncommitted changes", a commit range, or a diff on stdin. The flow picks +reviewers, runs them concurrently and prints every finding. When the target is +a PR, it also posts the report on it, and a re-run replaces the earlier report. +Nothing is fixed or committed. +[Source](https://github.com/VirtusLab/orca/blob/master/flows/review.sc). + +## Examples + +```bash +orca run implement.sc "Add a multiply function to the calculator crate" +orca run implement-interactive.sc "Add a new arithmetic operation. Ask the user which." +orca run issue-pr.sc "acme/widgets#42" +orca run review.sc "acme/widgets#42" +git diff | orca run review.sc +``` + +To resume a run, re-run it with the same prompt: the progress log, and for the +issue flows also the branch name and the marker that identifies their comment, +are derived from it. + +## Runnable examples + +Two self-contained examples under +[`examples/runnable/`](https://github.com/VirtusLab/orca/tree/master/examples/runnable) +seed a small Rust project into a temporary directory and run a flow against +it: + +- `01-simple` runs autonomous planning, then implements and reviews each task. +- `02-interactive` has the same shape, but the planner can pause to ask you + questions. + +Each example comes with a `create-test-project.sh` script and a README with +the exact commands to run. They need `cargo` on `PATH`. diff --git a/docs/using/output-and-files.md b/docs/using/output-and-files.md new file mode 100644 index 000000000..1d2fa018a --- /dev/null +++ b/docs/using/output-and-files.md @@ -0,0 +1,68 @@ +# Terminal output and files + +While Orca runs, the terminal is split into two zones. The **event log** grows +from top to bottom as stages and tools fire, and the **status line**, pinned to +the bottom, shows the active stage breadcrumb with a spinner. Nested stages are +indented. + +Each line of the event log starts with a glyph that says what kind of event it +is: + +- `▶` marks a stage start, or a step: a single-line note such as a branch + switch. +- `▸` is the prompt sent to an agent. +- `●` is assistant prose. +- `⏺` is a tool call, with the path, command or query in grey. A read-only call + shows as a bare `⏺ read`, so a burst of them folds into one line; the + [trace file](#files-under-orca) has the details. +- `⎿` says how many times the line above repeated, as in `⎿ ×12`. +- `✖` is an error. +- `?` is an approval request, or a question for you. It appears in interactive + turns only. +- `!` is a caveat about a tool limit that Orca cannot enforce for this run, + see [Choosing agents](../authoring/choosing-agents.md). It is never indented + under a stage. + +Colours and animation are turned off when stderr is not a terminal. You can +also force colours off with `NO_COLOR=1`, and suppress the spinner with +`ORCA_NO_ANIMATION=1`. + +## Closing summary + +When a run finishes, it names the branch you are left on, the PR it opened if +any, how many files changed since the commit it started from, and the +`git diff` that shows them. Open review findings are listed too; see +[Pull requests](../authoring/pull-requests.md). + +## Files under `.orca/` + +The `.orca/` directory holds both committed configuration and machine-local +state: + +| Path | What | Committed | +|---|---|---| +| `.orca/settings.properties`, `.orca/reviewers/` | [settings](settings.md) and [custom reviewers](reviewers.md) | yes | +| `.orca/runs/.progress.json` | a run's progress log, committed with each stage | yes | +| `.orca/cache/` | machine-local state; writes its own `.gitignore` | no | +| `.orca/worktrees/` | checkouts of [`--worktree` runs](run-lifecycle.md#worktrees) | no | + +In these paths, `` is derived from the prompt and `` from the +attempt's start time and pid. Under `.orca/cache/` you will find: + +- `runs/.sessions.json`: a run's durable session records. +- `attempts/.manifest.json`: the attempt's sessions and status. This is + what [`orca continue`](shell.md) lists. +- `attempts/.cost.jsonl`: one line per agent turn, with the agent, role, + model, stage, token usage and cost. This is the per-agent and per-model + detail that the closing summary leaves out. +- `attempts/.trace.log`: a DEBUG trace with prompts, agent output and tool + calls. Its path is printed at the start of a run, and it rolls over at 4 MB. + +The cache is safe to delete. Attempt files are pruned to the newest 20 that +recorded a session, plus the newest 20 of any kind. + +```{note} +If your `.gitignore` covers all of `.orca/`, every attempt warns you to remove +that line, so that settings and progress logs can be committed. The cache stays +ignored regardless. +``` diff --git a/docs/using/reviewers.md b/docs/using/reviewers.md new file mode 100644 index 000000000..55d70725a --- /dev/null +++ b/docs/using/reviewers.md @@ -0,0 +1,95 @@ +# Custom reviewers + +A reviewer is a prompt that says what to look for, paired with a read-only +agent. Orca ships eight of them: code-functionality, test, readability, +code-structure, simplicity, performance, security and scala-fp. You can add +your own, or retune a shipped one, by writing a Markdown file; no code changes +are needed. + +## Where reviewers come from + +Reviewers are collected from three tiers, read once per +[attempt](../glossary/users.md#flows-and-runs), before Orca changes anything in +the repository: + +| Tier | Location | Scope | +|---|---|---| +| project | `.orca/reviewers/*.md` | committed with the repository | +| global | `~/.config/orca/reviewers/*.md` (`$XDG_CONFIG_HOME/orca/reviewers/`) | your own, in every project | +| built-in | shipped with Orca | the eight above | + +The tiers merge into one **catalog**. A reviewer's name is its filename stem, +compared case-insensitively, so `.orca/reviewers/orca.md` is the reviewer +`orca`. When two tiers provide the same name, project beats global, and global +beats built-in: the file replaces the reviewer from the lower tier, in the same +position in the catalog. New names are appended, sorted by name. + +Flows take reviewers from the catalog through two rosters: `allReviewers`, +which holds every reviewer, and `minimalReviewers`, which holds +code-functionality, readability and test. A new name joins both rosters. A +shadowing file, on the other hand, runs only where the shipped reviewer runs. +For example, `scala-fp` is not in `minimalReviewers`, so +`.orca/reviewers/scala-fp.md` retunes it for this project without changing that +roster. Note that the [reviewer picker](../authoring/review.md) still chooses +from the roster per task. + +At the start of a run, Orca lists what the project and global tiers +contributed: + +```text +discovered reviewers: orca (project); scala-fp (project, shadows built-in) +``` + +## File format + +A reviewer file is frontmatter followed by a body, the same shape the shipped +reviewers use: + +```markdown +--- +description: Checks the project's own layering rules. +files: \.scala$ +--- + +## Scope + +Review only the layering of the changed files... +``` + +- `description:` is required and must be a single line. The reviewer picker + uses it to choose reviewers for a task. A YAML block scalar (`>`, `|`, `>-`, + `|-`), or a value wrapped onto the next line, aborts the run. +- `files:` is optional. It is a regex matched against each changed path: the + reviewer is only offered to the picker when the change touches a matching + file, unless nothing is known about the change set. Of the shipped + reviewers, only `scala-fp` declares one. +- The body is the reviewer's system prompt. +- A `name:` key is ignored. + +## Validation + +`README.md` and any file whose name starts with `_` are treated as documents +and skipped. Every other `.md` file must be a valid reviewer. Any of the +following aborts the run before Orca changes the repository, and all bad files +are reported at once: + +- a missing or unterminated frontmatter block +- a missing `description:` +- an empty body +- an invalid `files:` regex +- two files claiming one name + +Orca fails hard here because a silently dropped reviewer would look like a +clean review. + +```{note} +A symlink in `.orca/reviewers/` also aborts the run, since that directory comes +from a repository Orca did not write. Symlinks in the global tier are allowed, +as that is your own configuration. +``` + +## Using reviewers from a flow + +Within a flow, the reviewers are available as `allReviewers(agent)`, +`minimalReviewers(agent)` and `reviewerCatalog`. All three are described in +[Review and fix loops](../authoring/review.md#rosters). diff --git a/docs/using/run-lifecycle.md b/docs/using/run-lifecycle.md new file mode 100644 index 000000000..f5832f0f7 --- /dev/null +++ b/docs/using/run-lifecycle.md @@ -0,0 +1,135 @@ +# Branches, resume and worktrees + +A **run** is the execution of a flow for one prompt, across however many +processes it takes to finish. An **attempt** is one of those processes: a +single `orca run`, or a single `flow(...)` call. When a run is interrupted, you +resume it by attempting it again with the same prompt. + +Each run is bound to exactly one feature branch and one progress log, +`.orca/runs/.progress.json`, where `` is derived from the prompt. The +log records which [stages](../glossary/users.md#flows-and-runs) have finished, +and with what results. + +## Start + +On a fresh run, Orca: + +1. asks what to do with uncommitted changes; stashing is the default (see + [Run targets](#run-targets)), and stashed changes come back with + `git stash pop`, +2. creates and checks out the feature branch, +3. writes and commits the progress log header. + +The branch name comes from `codingAgent.cheap`, which derives a short label +from the prompt; the label's slug is the branch name. You can set the name +yourself with `--branch `, which overrides the flow's +[`branchNaming`](../authoring/extending.md). A name that is protected (`main`, +`master`, or the repository's default branch), or that already exists, is +refused; Orca does not pick another one. + +## Run targets + +Three flags decide where a run works, and what happens to uncommitted files on +a fresh run: + +- With no flag (the default), the run works on a new branch. You are asked + what to do with uncommitted files: stash (the default), keep or abort. When + there is no terminal, they are stashed. +- `--skip-branch` works on the current branch. Uncommitted files are kept and + swept into the first stage's commit. +- `--keep-changes` can be used with or without `--skip-branch`. Uncommitted + files are kept; in normal mode they reach the new branch in the first + stage's commit. +- `--worktree` works in a second checkout under `.orca/worktrees/`. + Uncommitted files are left behind. It cannot be combined with + `--skip-branch` or `--keep-changes`. + +`--skip-branch` is meant for continuing work that was already planned on a +branch. It refuses a protected branch or a detached HEAD, and cannot be +combined with `--branch`. + +```{note} +A re-run, that is any run that finds a progress log (even an unreadable one), +always stashes and ignores `--keep-changes`. This way the interrupted stage's +partial work cannot leak into the stage that runs again. +``` + +In the flow, the flags appear as `OrcaArgs.target`, of type `RunTarget`. Its +cases are `NewBranch(uncommitted)`, `CurrentBranch(uncommitted)` and +`Worktree`, where `uncommitted` is either `Uncommitted.Stash` or +`Uncommitted.Keep`. There is one case per allowed combination, so a refused +combination cannot be written in code. A script can also set the field itself, +which overrides the flags: + +```scala +flow(OrcaArgs(args).copy(target = RunTarget.Worktree)) +``` + +### Worktrees + +With `--worktree`, the whole flow runs in `.orca/worktrees/` inside the +repository. The checkout is keyed on the same prompt hash as the progress log: +it is created on the first attempt and reused by every later attempt for that +prompt. Two runs never share a checkout or a branch. + +A few things are worth knowing: + +- A worktree is made from a commit, so uncommitted work does not come along. +- The first attempt starts from a cold checkout: no build outputs, no + downloaded dependencies, no untracked local config. +- An editor or indexer that ignores `.gitignore` will see the second checkout. +- Orca never removes the worktree or its `orca-worktree-` branch. Full + cleanup is `git worktree remove .orca/worktrees/` followed by + `git branch -d orca-worktree-`. +- If the `orca-worktree-` branch gained commits outside Orca since the + worktree was created, a re-run refuses instead of moving it. + +## Resume + +A re-run with the same prompt finds the progress log and resumes from the first +incomplete stage. If `--branch` names a different branch than the one in the +log, the run is refused. On resume, Orca prints which branch the run is bound +to, how many stages are already done, and that the interrupted stage's +uncommitted work was dropped. Each +[durable session](../authoring/talking-to-agents.md) that is resumed gets the +same note. + +A corrupt or truncated progress log is detected at startup. In that case Orca +warns and starts fresh, re-running the previous stages, rather than resuming +from the wrong place silently. + +## Success + +When the flow succeeds, a final commit removes the progress log. It is pushed +if the flow already pushed the branch. If the feature branch has no real +changes against the starting branch, the branch is deleted and HEAD returns to +the starting branch. + +Otherwise the branch is kept, and where HEAD lands depends on the run: + +- A run that created a branch and opened a PR hands you back the branch you + started on; the work is on the PR. +- Every other run leaves you where you were: on the feature branch when no PR + was opened or under `--skip-branch`, and untouched under `--worktree`, where + the work is in the separate checkout that the summary names. + +In every case the [closing summary](output-and-files.md#closing-summary) names +the branch you are left on. + +## Failure + +While HEAD is on the feature branch, the failed stage's uncommitted partial +edits are discarded: `git reset --hard` for tracked files, plus `git clean -fd` +for the files the stage newly created. The run stays on the feature branch, so +a re-run resumes in place. Gitignored paths and `.orca/` are never removed. + +If a fresh run kept uncommitted changes (through `--skip-branch`, +`--keep-changes`, or by answering keep), Orca cannot tell your untracked files +from the run's own. In that case no untracked file is ever deleted, in any +stage, not even the ones the failed stage created. This is decided once, at +setup, for the whole run. Kept edits to tracked files that no stage has +committed are restored after the reset, and a re-run stashes them before it +resumes. + +If the flow moved HEAD off the feature branch before failing, Orca cleans +nothing and says so. diff --git a/docs/using/settings.md b/docs/using/settings.md new file mode 100644 index 000000000..a77e05519 --- /dev/null +++ b/docs/using/settings.md @@ -0,0 +1,132 @@ +# Settings + +Orca reads its configuration from two `settings.properties` files. Both are +plain `key = value` files, parsed once per +[attempt](../glossary/users.md#flows-and-runs), before Orca changes anything in +the repository: + +- `.orca/settings.properties` in the project holds the stack commands and, per + role, which agent to use. It is committed with the project. +- `~/.config/orca/settings.properties` (or `$XDG_CONFIG_HOME/orca/`; the same + location is used on macOS) holds agent keys only. It is per user and not + committed. + +A missing global file is fine. However, a file that exists but cannot be read +or parsed, in either place, aborts the run before Orca changes anything in the +repository. Note that a stack key (`format`, `lint`, `test`) in the global file +is also an error: stack commands belong to the project. + +## Stack commands + +The keys `format`, `lint` and `test` describe the project's stack. Each of them +is a **gate**: a command the review loop can run over the change. The value is +a single shell command, which Orca runs with `bash -c` in the flow's working +directory. Everything after the first `=` is taken as command text, so a line +such as `lint = FOO=bar cargo check` works as you would expect. + +A few rules apply to these keys: + +- Repeating a key appends another command; the commands run in file order. + This is useful when a repository has two stacks: list one line per stack. +- The value `off` disables the gate explicitly. A missing key skips the gate as + well. +- `#` starts a comment. Commenting a line out is the same as deleting it. + +For example, this is what a typical file written by +[auto-discovery](#auto-discovery) looks like: + +```properties +# orca settings — edit freely, commit with the project. +# format/lint/test: one shell command per key; `off` disables the gate. Delete the stack lines (or the whole file) to re-run auto-discovery. +# planningAgent/codingAgent/reviewAgent (harness[:model]): override the global settings file; a flow's own code overrides both. +# Cargo.toml; via rustfmt +format = cargo fmt +# Cargo.toml +lint = cargo check --tests +# no test config found +test = off +``` + +The [review loop](../authoring/review.md) runs `format` before each round and +`lint` together with the reviewers. It never runs `test`, so that the loop +stays cheap. A flow can read all three commands as +`summon[FlowContext].stackSettings` and run the tests in a stage of its own; +see [Gates and checks](../authoring/gates-and-checks.md). + +## Agent keys + +The keys `planningAgent`, `codingAgent` and `reviewAgent` choose which agent +plays each role. They are valid in both files and are single-valued, so a +repeated agent key is an error. The value has the form `harness[:model]` and is +split at the first `:`, which means that a model id containing `:` survives +intact. The `harness` part is one of `claude`, `codex`, `opencode`, `pi` or +`gemini`; any other name is an error that lists the valid ones. + +```properties +planningAgent = claude:opus +codingAgent = codex:gpt-5-mini +reviewAgent = opencode:anthropic/claude-haiku-4-5 +``` + +The model part is passed to the harness verbatim, and Orca does not validate +model ids. There is one exception: claude's bare `haiku` alias is sent as +`claude-haiku-4-5`, because the CLI may resolve the bare alias to a pricier +model. + +Note that agent keys are read even when `flow(stackSettings = Some(...))` pins +the stack commands. At setup, Orca announces where each role came from: + +```text +agents: planning=claude:claude-opus-5-5[1m] (default), coding=codex:gpt-5-mini (project), review=opencode: (global) +``` + +Here `` marks a role with no model pin, for which the harness +picks the model itself, and `[1m]` is claude's 1M-token context window variant. + +You can set the keys from the command line with +`orca config --coding-agent codex`, or open one of the files in your editor +with `orca config --edit project|global`. Both are described in +[Orca Shell](shell.md). + +## Precedence + +When a setting is given in more than one place, code always wins over files. +In more detail: + +- **Roles:** `flow(planningAgent = ...)` (or `codingAgent` / `reviewAgent`) > + project file > global file > built-in default (`claude`, no model pin). +- **Stack commands:** `reviewAndFixLoop(formatCommands = Use(...) / Off)` (see + [Gates and checks](../authoring/gates-and-checks.md)) > + `flow(stackSettings = Some(...))` > project file > auto-discovery, which + writes the project file. + +## Auto-discovery + +When the project file is absent, or has no stack line at all, Orca discovers +the stack commands itself. A file that already has some stack keys is left +alone. Discovery spends one cheap, read-only agent call inspecting the +repository, then writes the file and announces every guess it made: + +```text +no .orca/settings.properties — discovering how to format, lint & test this project + format = cargo fmt # Cargo.toml; via rustfmt + lint = cargo check --tests # Cargo.toml +warning: stack settings: no test command — gate disabled +written to .orca/settings.properties — review and edit as needed. +``` + +Discovered lines are appended below any existing content, so agent lines are +never touched. To run discovery again, delete the stack lines, delete the whole +file, or run `orca clear-stack`. With a complete file no model call is made at +all; this is the normal case, including in CI. + +Each discovered command names the file it was inferred from. Before the file is +written, two checks run on every command: the executable must be on `PATH`, +and the cited file must exist. A command that fails either check is written as +a comment, such as `# skipped: lint = just check (just: not found on PATH)`, +and is never run. A key left with no command gets a live `key = off` line. + +```{note} +If discovery itself fails, the run aborts rather than writing a "gates off" +file. +``` diff --git a/docs/using/shell.md b/docs/using/shell.md new file mode 100644 index 000000000..f47883801 --- /dev/null +++ b/docs/using/shell.md @@ -0,0 +1,136 @@ +# Orca Shell + +Orca Shell is the `orca` command: an interactive terminal front-end for flow +scripts, plus a scriptable subcommand for every action in its menu. + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/VirtusLab/orca/master/install.sh | bash +``` + +The script does two things: + +1. If `scala-cli` is not on your `PATH`, it runs scala-cli's official + installer. scala-cli then manages its own JVM. +2. It writes the `orca` launcher to `~/.local/bin/orca`. The launcher runs the + latest released `orca-shell` via `scala-cli`; the artifacts are downloaded + on the first `orca` run. The launcher itself never needs updating. + +If the installer says that `~/.local/bin` is not on your `PATH`, add it, then +run `orca`. + +If you would rather not install anything, or you want to pin a version (for +example in CI), you can run the shell directly with scala-cli. The version +below tracks the latest release, but any release that includes the shell +works. The `--workspace` option keeps scala-cli's build metadata out of the +current directory: + +```bash +scala-cli run --workspace "${XDG_CACHE_HOME:-$HOME/.cache}/orca/shell/workspace" --jvm 21 --quiet --verbose --dep "org.virtuslab::orca-shell:0.1.10" --main-class orca.shell.Main +``` + +## The interactive shell + +On the first run, a wizard asks you to pick a +[harness](../glossary/users.md#agents-and-conversations) and model for each of +the planning, coding and review roles, and writes them to the global +`settings.properties` (see [Settings](settings.md)). After that, a menu lets +you: + +- discover flows: project, global and built-in +- run a flow +- view or edit a flow's source +- create a new flow, or fork an existing one, with the configured agents' help +- continue a session left by a previous run + +## Commands + +`orca` with no arguments starts the interactive shell. `orca ...` +runs a single action and exits, which is what you want from a script or a CI +job. The commands are: + +- `orca run [prompt]` runs a flow and exits with the flow's exit code. + When no prompt is given, it is read from stdin. The flags are described + below. +- `orca view ` prints a flow's source, highlighted when stdout is a + terminal (see also `--plain` and `--color`). +- `orca edit ` opens a flow in `$VISUAL`, `$EDITOR` or `vi`. To + customise a built-in flow, `--to project` or `--to global` is required. +- `orca create ""` runs the built-in `simple.sc` flow in an isolated + sandbox to have the configured agents write a new flow. `--name ` + sets the file name, which is otherwise derived, and `--global` makes it a + global flow. +- `orca fork ""` does the same, but starts from an existing + flow. It takes the same `--name` and `--global` flags. +- `orca continue [selector]` resumes a recorded harness session; with no + selector, the newest one. `--list` shows the sessions, as JSON with + `--json`. +- `orca config` shows the role agents. `--planning-agent`, `--coding-agent` + and `--review-agent`, each taking `harness[:model]`, set any subset of them. + Alternatively, `--edit project|global` opens a settings file for + hand-editing, creating it from a template if it does not exist. +- `orca list` lists project, global and built-in flows, as JSON with `--json`. +- `orca clear-stack` forgets the detected `format` / `lint` / `test` commands, + so that the next run re-detects them (see [Settings](settings.md)). With + `--yes` it works without a terminal. + +### Flags of `orca run` + +| Flag | Effect | +|---|---| +| `--prompt ` | the prompt, for text starting with `-`; not together with the positional prompt | +| `--branch ` | name the branch the run creates; refused with `--skip-branch` | +| `--skip-branch` | continue on the current branch instead of creating one | +| `--keep-changes` | leave uncommitted files in place instead of stashing them | +| `--worktree` | run in a git worktree of this repository instead of the checkout | +| `--honor-pin` | use the flow's own pinned Orca version | +| `--verbose` | print a stack trace on abort | + +[Branches, resume and worktrees](run-lifecycle.md) explains the branch flags. + +### Selecting a session to continue + +The selector of `orca continue` is an id from `--list`, a session name, or a +branch. An id keeps naming the same session while other attempts record +theirs. If a name matches several sessions in one working tree, the most +recent one is resumed. A selector that matches both a name and a branch is +refused. + +### Running without a terminal + +Note that the authoring sandbox of `create` and `fork` is a fresh repository +with no remote, so the flow's closing PR step opens nothing and says so. + +`create`, `fork`, `edit`, `continue` when it resumes a session, and +`config --edit` need a real terminal, and error cleanly without one. `run`, +`view`, `list`, `config` without `--edit`, and `clear-stack --yes` work piped +or in CI. + +### Examples + +```bash +orca run implement.sc "add a rate limiter to /login" +echo "add a rate limiter" | orca run implement.sc +orca list --json | jq -r '.[].name' +orca create "add a token-bucket limiter" --name rate-limit.sc +orca continue # resume the last session +orca continue --list +orca continue feat/rate-limiter +orca config --coding-agent codex +orca config --review-agent claude:sonnet +orca view implement.sc +``` + +`orca --help` lists every command, and `orca --help` shows a +command's flags. + +## Exit codes + +| Code | Meaning | +|---|---| +| 0 | success | +| 1 | action failure | +| 2 | usage error | + +`orca run` exits with the flow's own exit code, so a failed run fails a CI job. diff --git a/docs/watch.sh b/docs/watch.sh new file mode 100755 index 000000000..24c43727c --- /dev/null +++ b/docs/watch.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sphinx-autobuild . _build/html diff --git a/examples/runnable/01-simple/README.md b/examples/runnable/01-simple/README.md index 2c2477409..4e722a55f 100644 --- a/examples/runnable/01-simple/README.md +++ b/examples/runnable/01-simple/README.md @@ -26,7 +26,7 @@ the variant where the planner can ask clarifying questions, see ## Prerequisites - JDK 21+, [scala-cli](https://scala-cli.virtuslab.org/). -- `claude` logged in (see the repo root README). +- `claude` logged in (see [Agent CLIs](../../../docs/using/agent-clis.md)). - `cargo` on PATH — the seed is a small Rust crate, so first-run auto-discovery resolves cargo commands into `.orca/settings.properties`, which the review loop reads by diff --git a/examples/runnable/02-interactive/README.md b/examples/runnable/02-interactive/README.md index 9eb38ca0c..f86f6d389 100644 --- a/examples/runnable/02-interactive/README.md +++ b/examples/runnable/02-interactive/README.md @@ -38,7 +38,7 @@ divide, modulo, power, …) before drafting tasks. ## Prerequisites - JDK 21+, [scala-cli](https://scala-cli.virtuslab.org/). -- `claude` logged in (see the repo root README). +- `claude` logged in (see [Agent CLIs](../../../docs/using/agent-clis.md)). - `cargo` on PATH — same Rust calculator starter as 01-simple. ## Run diff --git a/examples/runnable/README.md b/examples/runnable/README.md index 8aeadf576..ab4d79581 100644 --- a/examples/runnable/README.md +++ b/examples/runnable/README.md @@ -20,7 +20,7 @@ Both examples expect: - **JDK 21+** and [scala-cli](https://scala-cli.virtuslab.org/). - `claude` CLI logged in (`claude auth login` — see the - [repo root README](../../README.md#authenticating-the-coding-agents)). + [Agent CLIs](../../docs/using/agent-clis.md)). - `cargo` on PATH — both seed a small Rust calculator crate. Seed a project: @@ -73,6 +73,6 @@ Maven Central. ## Reading the output -The repo root README has a [glyph legend](../../README.md#how-it-works) for the +The docs have a [glyph legend](../../docs/using/output-and-files.md) for the rendered output. The full design rationale lives in [ADR 0008](../../adr/0008-terminal-output-design.md). diff --git a/flows/implement.sc b/flows/implement.sc index 196f472aa..e76370b37 100644 --- a/flows/implement.sc +++ b/flows/implement.sc @@ -3,7 +3,7 @@ //> using dep "org.virtuslab::orca:0.1.10" //> using jvm 21 -/** Autonomous planning + coding flow — the README example. +/** Autonomous planning + coding flow — the one the documentation's tutorial builds. * * The planner breaks the prompt into tasks; each task is implemented on the * run's feature branch and reviewed in a single pass. A final stage then loops diff --git a/project/ConcatDocs.scala b/project/ConcatDocs.scala new file mode 100644 index 000000000..fd2cc3c6a --- /dev/null +++ b/project/ConcatDocs.scala @@ -0,0 +1,21 @@ +import sbt.* + +/** Joins the documentation pages under `docs/` into one Markdown file, in the + * order `docs/index.md`'s toctrees list them, each page preceded by a heading + * naming its path. Pages the toctrees do not list are left out, so the + * bundle matches the published site. + */ +object ConcatDocs { + private val entry = """^\s{3}([\w./-]+)\s*$""".r + + def apply(docsDir: File): String = { + val index = IO.read(docsDir / "index.md") + val pages = index.linesIterator.collect { case entry(path) => path }.toList + val intro = index.split("```\\{eval-rst\\}").head.trim + val body = pages.map { page => + val f = docsDir / s"$page.md" + s"\n\n" + IO.read(f).trim + } + (intro :: body).mkString("\n\n---\n\n") + "\n" + } +} diff --git a/shell/src/main/scala/orca/shell/create/FlowAuthoring.scala b/shell/src/main/scala/orca/shell/create/FlowAuthoring.scala index 1aabc699b..89d9224e9 100644 --- a/shell/src/main/scala/orca/shell/create/FlowAuthoring.scala +++ b/shell/src/main/scala/orca/shell/create/FlowAuthoring.scala @@ -17,12 +17,12 @@ private[shell] object FlowAuthoring: private val resourcePrefix = "/orca/shell/api/" - /** The bundled files' basenames, matching the resource-generator's copy - * (build.sbt) — the README plus the two example flows used as few-shot - * material. + /** The bundled files' basenames, matching the resource-generator's output + * (build.sbt) — the documentation pages joined into one file, plus the two + * example flows used as few-shot material. */ private val bundledNames = - List("README.md", "implement.sc", "implement-interactive.sc") + List("orca-docs.md", "implement.sc", "implement-interactive.sc") /** Ensures a `.sc` suffix on a user-supplied filename. */ def normalizedFileName(raw: String): String = @@ -340,7 +340,7 @@ private[shell] object FlowAuthoring: case _: IllegalArgumentException => Left(s"'$fileName' isn't a valid flow filename") - /** Writes the bundled README + two example flows into + /** Writes the bundled documentation + two example flows into * `/orca-api-/`, returning that directory. `cacheBase` * must be the cache of a freshly created authoring sandbox * ([[orca.shell.actions.AuthorAction]]), so the material sits inside the @@ -371,10 +371,10 @@ private[shell] object FlowAuthoring: /** The authoring task handed to the built-in `simple.sc` flow as its * `userPrompt` (ADR 0021 §9): the goal and target path, the verbatim * version-pinned header to start the file with, the line-1 `//` description - * convention, pointers to the extracted README/examples, the `scala-cli + * convention, pointers to the extracted docs/examples, the `scala-cli * compile` verification step, the runtime-vs-compile-time rules caveat, and - * — last resort only — the raw README URL at `build`'s git ref. Kept in one - * place since the prompt text is itself the deliverable. + * — last resort only — the docs' source tree at `build`'s git ref. Kept in + * one place since the prompt text is itself the deliverable. */ def initialPrompt( goal: String, @@ -382,7 +382,7 @@ private[shell] object FlowAuthoring: apiDir: os.Path, build: OrcaBuild ): String = - val readme = apiDir / "README.md" + val docs = apiDir / "orca-docs.md" val example1 = apiDir / "implement.sc" val example2 = apiDir / "implement-interactive.sc" // The goal now comes from a multiline prompt (inputMultiline), so it's @@ -400,7 +400,7 @@ private[shell] object FlowAuthoring: |Line 1 of the file must be a `//` comment giving a one-line description |of the flow — the shell's flow listing uses it as the description. | - |The Orca API reference is at $readme — read it before writing the + |The Orca documentation is at $docs — read it before writing the |flow. Two example flows are at $example1 and $example2; start from |whichever is closer to the goal. | @@ -410,12 +410,12 @@ private[shell] object FlowAuthoring: |Caveat: some authoring rules (fork-boundary captures, stage |push-after-commit ordering, no concurrent stages) are enforced at runtime, |not by the compiler — a script can compile and still violate them. - |Follow the README's Authoring rules section beyond what the compiler + |Follow the documentation's "Stages" page beyond what the compiler |catches. | - |Last resort, only if the local README above is somehow missing: the - |reference is at - |https://raw.githubusercontent.com/VirtusLab/orca/${build.gitRef}/README.md + |Last resort, only if the local documentation above is somehow missing: + |its source is at + |https://github.com/VirtusLab/orca/tree/${build.gitRef}/docs |""".stripMargin /** Two-space-indents every line of `text` — the shared block-quoting used by @@ -442,11 +442,11 @@ private[shell] object FlowAuthoring: os.copy(sourcePath, copy, createFolders = true) copy - /** The shared tail of the fork/edit authoring task — API-reference pointers, - * the compile-check step, the runtime-rules caveat, and the last-resort - * README URL — appended after `opening` states what to do and to which - * paths. Shared by [[forkPrompt]] and [[editPrompt]] so the two prompts, - * which differ only in how they describe the action (copy-then-change vs. + /** The shared tail of the fork/edit authoring task — documentation pointers, + * the compile-check step, the runtime-rules caveat, and the last-resort docs + * URL — appended after `opening` states what to do and to which paths. + * Shared by [[forkPrompt]] and [[editPrompt]] so the two prompts, which + * differ only in how they describe the action (copy-then-change vs. * edit-in-place), can't drift on everything else. */ private def changePrompt( @@ -455,13 +455,13 @@ private[shell] object FlowAuthoring: apiDir: os.Path, build: OrcaBuild ): String = - val readme = apiDir / "README.md" + val docs = apiDir / "orca-docs.md" val example1 = apiDir / "implement.sc" val example2 = apiDir / "implement-interactive.sc" // `opening` already carries the user's typed changes: interpolating it here // would run a second `stripMargin` pass over that text. opening + "\n\n" + - s"""The Orca API reference is at $readme — read it if the changes need API + s"""The Orca documentation is at $docs — read it if the changes need API |surface the source doesn't already use. Two example flows are at |$example1 and $example2. | @@ -471,12 +471,12 @@ private[shell] object FlowAuthoring: |Caveat: some authoring rules (fork-boundary captures, stage |push-after-commit ordering, no concurrent stages) are enforced at runtime, |not by the compiler — a script can compile and still violate them. - |Follow the README's Authoring rules section beyond what the compiler + |Follow the documentation's "Stages" page beyond what the compiler |catches. | - |Last resort, only if the local README above is somehow missing: the - |reference is at - |https://raw.githubusercontent.com/VirtusLab/orca/${build.gitRef}/README.md + |Last resort, only if the local documentation above is somehow missing: + |its source is at + |https://github.com/VirtusLab/orca/tree/${build.gitRef}/docs |""".stripMargin /** The authoring task for a fork (ADR 0021 §9): states the source path and diff --git a/shell/src/test/scala/orca/shell/create/FlowAuthoringTest.scala b/shell/src/test/scala/orca/shell/create/FlowAuthoringTest.scala index 53e2fac02..a2eaad91e 100644 --- a/shell/src/test/scala/orca/shell/create/FlowAuthoringTest.scala +++ b/shell/src/test/scala/orca/shell/create/FlowAuthoringTest.scala @@ -26,7 +26,7 @@ class FlowAuthoringTest extends munit.FunSuite: val target = TempDirs.dir() val dir = FlowAuthoring.extractApiMaterial(target, "0.0.18") assertEquals(dir, target / "orca-api-0.0.18") - List("README.md", "implement.sc", "implement-interactive.sc").foreach: + List("orca-docs.md", "implement.sc", "implement-interactive.sc").foreach: name => assertEquals(os.read(dir / name), resourceText(name)) // --- initialPrompt --- @@ -71,8 +71,8 @@ class FlowAuthoringTest extends munit.FunSuite: assert(prompt.contains("""//> using dep "org.virtuslab::orca:0.0.18"""")) assert(prompt.contains("//> using jvm 21")) - test("initialPrompt points at the extracted README and both examples"): - assert(prompt.contains((apiDir / "README.md").toString)) + test("initialPrompt points at the extracted docs and both examples"): + assert(prompt.contains((apiDir / "orca-docs.md").toString)) assert(prompt.contains((apiDir / "implement.sc").toString)) assert(prompt.contains((apiDir / "implement-interactive.sc").toString)) @@ -104,10 +104,10 @@ class FlowAuthoringTest extends munit.FunSuite: "//> using repository ivy2Local" ) - test("initialPrompt's last-resort line is the tag-pinned raw README URL"): + test("initialPrompt's last-resort line is the tag-pinned docs source URL"): assert( prompt.contains( - "https://raw.githubusercontent.com/VirtusLab/orca/v0.0.18/README.md" + "https://github.com/VirtusLab/orca/tree/v0.0.18/docs" ) ) @@ -484,8 +484,8 @@ class FlowAuthoringTest extends munit.FunSuite: assert(fork.contains("Create the Orca flow")) assert(fork.contains("by copying")) - test("forkPrompt points at the extracted README and both examples"): - assert(fork.contains((apiDir / "README.md").toString)) + test("forkPrompt points at the extracted docs and both examples"): + assert(fork.contains((apiDir / "orca-docs.md").toString)) assert(fork.contains((apiDir / "implement.sc").toString)) assert(fork.contains((apiDir / "implement-interactive.sc").toString)) @@ -541,8 +541,8 @@ class FlowAuthoringTest extends munit.FunSuite: assert(!edit.contains("Create the Orca flow")) assert(!edit.contains("by copying")) - test("editPrompt points at the extracted README and both examples"): - assert(edit.contains((apiDir / "README.md").toString)) + test("editPrompt points at the extracted docs and both examples"): + assert(edit.contains((apiDir / "orca-docs.md").toString)) assert(edit.contains((apiDir / "implement.sc").toString)) assert(edit.contains((apiDir / "implement-interactive.sc").toString))