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/api/backends.md b/docs/api/backends.md new file mode 100644 index 000000000..074dd3c86 --- /dev/null +++ b/docs/api/backends.md @@ -0,0 +1,35 @@ +# Backends + +A **backend** is a harness as a flow sees it: the five agents `claude`, +`codex`, `opencode`, `pi` and `gemini`. They expose the same calls, described +in [Talking to agents](../authoring/talking-to-agents.md): + +- 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].{autonomous,interactive}.run(input)` + +`interactive` exists only on `resultAs[O]`. [`FlowSession` and +`Chat`](data-structures.md#conversations) are the handles. All five share the +builders `withModel`, `withCheapModel`, `withAutoApprove`, `withSystemPrompt`, +`withName`, `withReadOnly`, `withNetworkOnly`, `withSelfManagedGit`, see +[Choosing agents](../authoring/choosing-agents.md). `Model` wraps a model id +string. + +The table lists what differs. "Bare" means the accessor with no model chosen; +`cheap` is the model `agent.cheap` picks. + +| Agent | Model accessors | Notes | +|---|---|---| +| `claude` | `haiku`, `sonnet`, `opus`, `fable`; `cheap` → haiku; `withModel(Model)` | Claude Code. Bare `claude` is Opus with the 1M-token context window. Use `claude.sonnet` or `claude.haiku` for cheap one-shot calls, `claude.fable` for the hardest ones. `withNetworkTools(...)` replaces the tools the `NetworkOnly` [tool set](../authoring/choosing-agents.md#tool-sets) grants. | +| `codex` | `mini`; `cheap` → mini; `withModel(Model)` | OpenAI Codex. Bare `codex` pins GPT-6 Sol, which needs a codex CLI version that has this model. `codex.mini` is GPT-6 Luna. | +| `opencode` | `anthropicOpus`, `anthropicSonnet`, `anthropicHaiku`, `openaiAstra`, `openaiSol`, `openaiLuna`; `cheap` is provider-matched (openai → luna, else anthropicHaiku); `withModel(providerModel)` or `withModel(provider, modelId)` | [OpenCode](https://opencode.ai), driven over HTTP and SSE against a headless `opencode serve`, started lazily and shared for the [attempt](../glossary/users.md#flows-and-runs); sessions survive the server. Spans providers, so models are provider-qualified: `opencode.withModel("openai/gpt-5-mini")`, `opencode.withModel("ollama", "llama3.1")`. Inherits your configured providers and auth. | +| `pi` | `withModel(Model("provider/model"))` | [Pi](https://pi.dev/), driven through `pi --mode rpc`. Provider and model selection follow Pi's own configuration. Interactive calls can ask clarifying questions through Orca's `ask_user` bridge. | +| `gemini` | `flash`; `cheap` → flash; `withModel(Model)` | Google Gemini CLI, driven via `gemini --output-format stream-json`. Bare `gemini` pins Gemini 3.1 Pro (preview); `gemini.flash` is Gemini 3.8 Flash. Structured output is prompt-enforced, since Gemini has no schema flag; `withReadOnly` maps to `--approval-mode plan`. | + +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: `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..91fc271c1 --- /dev/null +++ b/docs/api/data-structures.md @@ -0,0 +1,107 @@ +# Data structures + +The types you meet in flow scripts. Most derive `JsonData`, which makes them +valid stage results (the progress log can record and replay them) and usable as +structured LLM output via `agent.resultAs[T]`. Handles (`FlowSession`, `Chat`) +and intermediate values (`WithChat`, `Verdict`) do not, and cannot be stage +results. + +## Planning + +- **`orca.plan.Plan(epicId, description, tasks, brief)`** — the task list the + planner generates in one round trip. `epicId` is a kebab-case identifier for + the plan, not the branch name. `description` is the planner's epic summary. + `brief` is the planner's short codebase briefing; use it as the session + seed: `agent.session("implementer", seed = plan.brief)`. + `plan.taskPrompt(task)` prepends the brief to a task's description. +- **`orca.plan.Task(title: Title, description: String)`** — `title` is the + label shown in the event log. +- **`orca.plan.WithChat(chat, value)`** — returned by every `Plan.*` planning + call ([Planning](../authoring/planning.md)): the result and the `Chat` that + produced it. You never build one; destructure it: + `val WithChat(chat, plan) = ...`. +- **`orca.plan.Verdict[A]`** — `Verdict.Proceed(value)` or + `Verdict.Rejection(kind, body)`, with `kind` one of `Question`, `Critique`, + `Rebuff`. Returned by `assessThenPlan` as `Verdict[Plan]`, see + [Verdicts and triage](../authoring/planning.md#verdicts-and-triage). +- **`orca.plan.Triage`** — returned by `triage`: `NotABug`, `Untestable` or + `Testable`; each case carries its own fields. +- **`orca.plan.BugReportMatch`** — the agent's decision on whether a CI failure + matches the original report; a structured-output type the bug-fix flow asks + for. + +## Conversations + +- **`orca.FlowSession`** — the durable, resumable session handle from + `agent.session(name, seed)`. `.run(prompt)` or `.resultAs[O].run(input)` + drives the agent. When the harness no longer holds the conversation, the + turn is prefixed with the seed and a list of the completed stages. + `session.chat` is its conversation as an ephemeral `Chat`. +- **`orca.agents.Chat[B]`** — the ephemeral multi-turn handle 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. Also carried by `WithChat`. + +## Labels and handles + +- **`orca.Title`** — a wrapper around `String` for short labels (`Task.title`, + `ReviewFinding.title`). `Title("…")` constructs, `.value` reads. +- **`orca.tools.PrHandle`** — an open pull request: `host`, `owner`, `repo`, + `number`. Returned by `gh.createPr`. Build one with + `PrHandle.from(host, owner, repo, number)`, where a `Left` names the invalid + field, or `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`** — a GitHub issue. Carries no host: issue flows + use gh's default host (`GH_HOST`, else the host gh is logged in to). +- **`orca.tools.GitHubAvailability`** — what `gh.availability` answers. + `Available(host, owner, repo)` is the repository gh resolves. + `Unavailable(why)` means 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 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 it as one line. +- **`orca.pr.PrSummary(title, body)`** — what `summarisePr` returns; feeds + `gh.createPr(title = …, body = …)` directly. + +## Review + +- **`orca.review.ReviewFinding`** — one problem a reviewer reported: 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.ReviewResult(findings)`** — 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)`** — what a review loop leaves + open once it halts. Each `OpenFinding(id, title, reason, location)` carries an + `OpenReason`: + - `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)`: 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)`** — the lint gate bundle: the shell + commands plus the cheap agent that summarises their output into a + `ReviewResult`. + +## Settings + +- **`orca.StackSettings(format, lint, test)`** — the resolved per-project + [gate](../glossary/users.md#review) commands, each a `List[String]` run via + `bash -c`; empty means the gate is disabled. Read it via + `summon[FlowContext].stackSettings`; pin it with + `flow(stackSettings = Some(...))`. +- **`orca.Configured[A]`** — 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..aa708c727 --- /dev/null +++ b/docs/api/tools.md @@ -0,0 +1,64 @@ +# Git, GitHub and file tools + +Three tools are available inside a `flow(...)` body: `git`, `gh` and `fs`. +Reads work anywhere in a flow. Writes compile only inside a `stage(...)` body, +see [Stages](../authoring/stages.md). + +The runtime owns git. Agents edit the working tree; they are told not to +commit, push or switch branches. The runtime commits each stage and owns the +run's branch. The flow pushes with `git.push`. So `git` has no commit or +checkout methods. `git` also cannot be swapped out in `flow(...)`; `gh`, `fs` +and the agents can, see +[Extending flows in code](../authoring/extending.md). + +## `git` + +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]`. `PushFailure` is `NonFastForward` or `RemoteDeclined` | +| `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; `isIgnored` answers `false` when git cannot answer | +| `uncommittedDiff()` | the whole repository minus `.orca/` bookkeeping, tracked files only; empty once the work is committed | +| `defaultBase()` | the default base branch, or `Left(NoDefaultBase)` | +| `diffVsBase(base)` | the branch-wide diff against `base` | +| `changedFiles(since?)` | the changed paths. Use it, not the diff text, to decide by file name: the diff does not show binary changes or renames, and leaves a trailing tab on a path containing a space | +| `reviewChanges(since?)` | what reviewers get: 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()` | what the next commit will include: a `--stat` summary, the new files and the diff | +| `show(rev, paths?)` | `git show` of a revision, optionally limited to paths; long output is cut and says so | +| `fileAt(rev, path)` | one file's contents at `rev` | + +`NoDefaultBase`, `PushFailure` and `GitReadFailed` come back as `Left`. Call +`.orThrow` where you do not expect the `Left`; it throws instead. + +## `gh` + +GitHub PR and CI integration through the `gh` CLI. `pr` is a +[`PrHandle`](data-structures.md#labels-and-handles), `issue` an `IssueHandle`. + +| Method | Does | +|---|---| +| `availability()` | read-only probe of whether a PR can be opened from this checkout; answers with a [`GitHubAvailability`](data-structures.md#labels-and-handles) | +| `createPr(title, body)` | opens a PR, `Either[PrCreateFailed, PrHandle]`; idempotent by branch: returns the existing PR if one is open | +| `updatePr(pr, title, body)` | replaces a PR's title and body; harmless to re-run | +| `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)` | finds a prior comment carrying `marker` and edits it in place, else posts one | +| `buildStatus(pr)`, `waitForBuild(pr, ...)` | CI status; `waitForBuild` returns `Either[BuildWaitFailed, BuildStatus]` | + +`createPr` and `upsertComment` are idempotent, which is what makes a resumed +stage safe, see [Pull requests](../authoring/pull-requests.md). + +## `fs` + +Working-tree file I/O. + +| Method | Does | +|---|---| +| `read(path)` | `Option[String]`; `None` for a missing file, no exception | +| `write(path, content)` | refuses a path outside the working tree or under `.orca/runs`, `.orca/cache` or `.orca/worktrees` | +| `list(glob)` | the paths matching a glob, as `List[String]` | diff --git a/docs/authoring/capabilities.md b/docs/authoring/capabilities.md new file mode 100644 index 000000000..f51fea1ba --- /dev/null +++ b/docs/authoring/capabilities.md @@ -0,0 +1,53 @@ +# Capabilities and compile-time checking + +```{note} +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. `stage(...)` and +`flow(...)` bodies provide them; you never construct one. Calling a gated +function without its token is a compile error, and the message says where the +call belongs. + +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 reads and `display`. It is not a capability: it is +thread-safe and forks may use it. A helper that starts stages takes +`(using FlowContext, FlowControl)`. + +These runtime checks are always on: a fork that calls `stage(...)` or +`session(...)`, or writes to the workspace, fails at once; a second `flow(...)` +in the same working tree is refused; an agent used after its flow 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. 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 + ``` + +Scripts 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..7da086c2a --- /dev/null +++ b/docs/authoring/choosing-agents.md @@ -0,0 +1,111 @@ +# Choosing agents + +A call takes an agent. Name it one of two ways. + +**The role agents: `planningAgent`, `codingAgent`, `reviewAgent`.** Harness +agnostic. Each is resolved from [settings](../using/settings.md), defaulting to +claude. Use `planningAgent` for `Plan.*` calls, `codingAgent` for the +implementer's session, and `reviewAgent` for `allReviewers(...)`; the review +helpers default to it. Change settings and the whole flow follows. The shipped +flows use only role agents. + +`codingAgent` is also the run's primary: its cheap tier names the branch, +discovers the stack settings and writes 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 settings, say `claude.opus` for a step that must have the +strongest model. `codingAgent.opus` does not compile: model accessors exist +only on concrete harnesses. If you need a model, name the harness. Pin any +other model 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` + +Use it for one-shot summaries and pickers. + +## Tool sets + +`ToolSet` decides which tools exist at all: + +```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. In an autonomous turn nobody can answer, so a call outside the +set hangs. Only claude enforces the set per tool; codex and gemini cannot +restrict per tool, so `Only` becomes full auto-approval there. 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. 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 | + +Hard: the CLI blocks the tools. PromptOnly: the agent is told, nothing blocks +it. SandboxApprox: a sandbox approximates the set. Ignored: the harness cannot +apply the restriction; auto-approval is always on there. 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` (its own line in the cost +log), `withReadOnly`, `withNetworkOnly`, `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). + +`withSelfManagedGit` opts one agent out of the rule that the runtime owns git: +by default every write-capable turn is told not to commit, push or switch +branches. + +To replace an agent for the whole flow, or 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..a39dbcb7b --- /dev/null +++ b/docs/authoring/extending.md @@ -0,0 +1,81 @@ +# Extending flows in code + +Most of what `flow(...)` builds by default can be replaced from the script. +`git` cannot: the runtime owns the run's branch and commits through it. The +harness SPI cannot either; a new harness is added as an Orca module. + +## `flow(...)` parameters + +```scala +flow( + args: OrcaArgs, + workDir?, interaction?, extraListeners?, + branchNaming?, stackSettings?, + planningAgent?, codingAgent?, reviewAgent?, + claude?, codex?, opencode?, pi?, gemini?, + gh?, fs?, prompts?, pricing? +)(body) +``` + +| Parameter | Use | +|---|---| +| `args` | the parsed command line. A script can change it: `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. Not exported from `orca.*`; import it by its full path | +| `extraListeners` | additional `OrcaListener`s: receivers of 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 a label 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, below | +| `gh`, `fs` | your own tool implementations, `gh = Some(myGh)` | +| `prompts` | the per-call prompt wrappers (autonomous, interactive, retry) as one set | +| `pricing` | the price table the cost log and closing summary use | + +## Replacing an agent + +Each agent slot takes a factory. It receives the attempt's `AgentWiring` +(event sink, interaction, working directory, prompts) so your agent reports +events like the defaults do: + +```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 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 +next to the helper. Override it, or compose with the default: + +```scala +import orca.plan.{Plan, PlanPrompts} + +Plan.interactive.from( + userPrompt, + planningAgent, + instructions = PlanPrompts.Planning + "\n\nPrioritise observability tasks first." +) +``` + +| Object | Prompts | +|---|---| +| `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 code, add a file under +`.orca/reviewers/`, see [Custom reviewers](../using/reviewers.md). The +convention 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..74f8b7439 --- /dev/null +++ b/docs/authoring/gates-and-checks.md @@ -0,0 +1,91 @@ +# Gates and checks + +Each review round can run three kinds of non-LLM verification alongside the +reviewers: a format gate, a lint gate, and Scala checks. + +## Format + +`formatCommands` run before each review round, in the flow's working +directory, so reviewers never see formatting noise. + +## Lint + +`lint` runs alongside the reviewers each round. A `Lint(commands, agent)` is the +shell commands plus the cheap agent that summarises their output into a +[`ReviewResult`](../api/data-structures.md#review). + +The standalone call is available too, and works inside a fork: + +| Call | Does | +|---|---| +| `lint(commands, agent, instructions?)` | runs the commands in order with `bash -c`, all of them even if one fails, 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)` | as above, summarising into an existing `Lint.summariser(agent)` conversation, so a gate run several times in one stage resumes the session; returns a `LintReport`. Do not reuse a summariser after it has reported findings: it may repeat them when a later run no longer shows them. The review loop does this for you | + +The `test` commands are not run by the review loop, which stays deliberately +cheap. 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 `evaluate(): ReviewResult`: a +benchmark, an HTTP probe, an assertion. Pass it in the `checks` list of either +review call; its findings go to the fixer with the reviewers'. + +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 measurements in its description: the loop matches a check's +finding to the one it already holds 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") // 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). 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 + +`formatCommands` and `lint` on `reviewThenFix` and `reviewAndFixLoop` are +`Configured` values. The default reads the project's +[stack settings](../using/settings.md); `Off` disables the gate; `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`. For format-only, pass +`lint = Configured.Off`. + +## Recording your own findings + +`OpenFinding.custom(title, reason, location)` is an open finding a flow records +itself, say a gate it runs outside the loop still failing. Add it to the +`OpenFindings` handed to the PR step, or pass it in `priorOpenFindings` so a +loop's reviewers see it. diff --git a/docs/authoring/planning.md b/docs/authoring/planning.md new file mode 100644 index 000000000..05153c319 --- /dev/null +++ b/docs/authoring/planning.md @@ -0,0 +1,68 @@ +# Planning + +`Plan` (from `import orca.{*, given}`) has the planning entry points. Each +call is `Plan..`. Mode is `autonomous` or `interactive`. Every +operation works in both modes. Pass `planningAgent` as the `agent`. + +| Operation | Result | `autonomous` ([`NetworkOnly`](choosing-agents.md#tool-sets) tools, no human) | `interactive` (the agent can ask the user questions) | +|---|---|---|---| +| `from(userPrompt, agent, instructions?)` | `Plan` | plan in one turn | drive the planner conversationally | +| `assessThenPlan(userPrompt, agent, instructions?)` | `Verdict[Plan]` | assess, then `Proceed(plan)` or `Rejection` | same, but can ask the user to clarify instead of rejecting | +| `triage(report, agent, instructions?)` | `Triage` | classify a bug report: not a bug, untestable, or testable | same, with clarifying questions | + +`instructions` is optional and replaces the helper's prompt, see +[Customising prompts](extending.md#customising-prompts). + +## The plan + +A `Plan(epicId, description, tasks, brief)`: + +- `tasks` is the list of `Task(title: Title, description: String)` to + implement, in order. `Title` wraps a short label. +- `brief` is a concise codebase briefing. 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, not the branch name. The + run names its branch separately. + +## `WithChat` + +Every cell returns `WithChat[]`: the result and the `Chat` that +produced it (see [Ephemeral chats](talking-to-agents.md#ephemeral-chats)). You +can: + +- keep talking to the planner: `chat.run(...)`. 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, so the shipped flows take `.value`. Destructure +when you want both: + +```scala +val WithChat(chat, plan) = Plan.autonomous.from(userPrompt, planningAgent) +``` + +## Reviewing the plan + +From a `WithChat[Plan]`, `.reviewed()` refines the plan before implementing. +The planner reviews its own draft with read-only tools and returns an improved +`Plan`. Chain it: + +```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`: `Verdict.Proceed(plan)` to implement, or +`Verdict.Rejection(kind, body)`. `kind` says whether it is a question, a +critique or a refusal. The flow shows it to whoever asked, for example as an +issue comment. `flows/issue-pr.sc` does this. + +`triage` returns a `Triage` sum type to pattern-match: `NotABug`, `Untestable` +or `Testable`; each case carries 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..5a2b8d181 --- /dev/null +++ b/docs/authoring/pull-requests.md @@ -0,0 +1,59 @@ +# Pull requests + +`import orca.pr.*` gives you the PR helpers. They build on the `gh` tool +described in [Git, GitHub and file tools](../api/tools.md). + +## Opening a PR + +| Call | Does | +|---|---| +| `openPrIfGitHub(summarisingAgent, openFindings, title?, body?, context?, instructions?): Option[PrHandle]` | Pushes, summarises and opens the PR when `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. | +| `openPrFromBranch(summarisingAgent, openFindings, title?, body?, context?, instructions?): PrHandle` | The same three stages, but fails the run when there is no GitHub remote or `gh` login. | +| `summarisePr(agent, diff, context?, instructions?): PrSummary` | Turns a branch diff into a [`PrSummary(title, body)`](../api/data-structures.md#labels-and-handles) for `gh.createPr`. `context` is an optional preamble the model anchors the description to. An oversized diff is truncated. Use a [cheap](choosing-agents.md#the-cheap-tier) model. | + +`None` cases of `openPrIfGitHub`: no remote, a remote that is not GitHub, a +GitHub host `gh` cannot reach, a run that changed no code, or a push or create +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. + +Both open calls create stages, so they cannot be called inside a `stage(...)` +body. Call them at the top level of the flow. + +`openFindings` is the final review's [`OpenFindings`](review.md#what-comes-back). +The PR body lists each entry under "Open review findings": title, location if +any, and reason. If the review was skipped, a line says so. The same section +goes to the run output, also when the PR fails. + +`title` and `body` are functions `PrSummary => String` that rewrite the +generated text. `context` defaults to the run's prompt; with that default the +summariser adds `Closes #N` for each issue the prompt names. If you pass your +own `context`, 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 + +For a flow that opens or updates its PR with bare `gh` calls: + +| Call | Does | +|---|---| +| `bodyWithOpenFindings(body, open)` | `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 the checkout returns to the starting branch and the closing summary names the PR. Call it inside the stage that opened the PR, so 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..0d52d229e --- /dev/null +++ b/docs/authoring/review.md @@ -0,0 +1,109 @@ +# Review and fix loops + +`import orca.review.*` gives you two review calls. Both run reviewers against a +change, hand the findings to the coder's session to fix, and return what stays +open. The format and lint gates and Scala checks that run with them are +described in [Gates and checks](gates-and-checks.md). + +| Call | Does | +|---|---| +| `reviewThenFix(coderSession, reviewers, task, ...)` | One review round, then one fix turn if it found anything. Reviewer findings are not re-checked: the fixer's word is taken. The lint gate and checks are re-run over the fix, with one more fix turn if they still fail. Reviewers are picked once, with `ReviewerSelector.agentDriven`. | +| `reviewAndFixLoop(coderSession, reviewers, task, ..., maxFixTurns?)` | Review, fix, re-evaluate, until reviewers come back clean, the fixer reports no fixes, or `maxFixTurns` fix turns have run (default 3, so up to four rounds). | + +Parameters both calls share: + +| Parameter | Meaning | +|---|---| +| `coderSession` | the `FlowSession` that 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 when the prompt is only a pointer, such as an issue number; defaults to the run's prompt | +| `diff` | which change set reviewers see, see below | +| `formatCommands`, `lint`, `checks` | the gates and checks, see [Gates and checks](gates-and-checks.md) | +| `priorOpenFindings` | findings an earlier review left open, 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 below | + +The built-in flows use `reviewThenFix` per task, because the final review sees +that code again. They end with one `reviewAndFixLoop` over the whole run, +because nothing reviews after it: + +```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, 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 them in the PR body, see +[Pull requests](pull-requests.md). A flow can add its own entries with +`OpenFinding.custom`, see [Gates and checks](gates-and-checks.md). + +## What reviewers see + +- **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, not only 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. + `diff = ReviewDiff.WholeRun` widens it to everything since the commit the run + started from, for a stage after the per-task work; reviewers are told the + change spans every stage. `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. +- A change set past 128 KiB is cut down: the reviewer gets as many whole files + as fit, then a list naming every other changed file with its line counts, and + reads those itself. A pinned diff is sent as given. + +`WholeRun` needs the commit the run started from. If the progress log has none +(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 + +- `allReviewers(agent)`: every reviewer in the catalog, each as a read-only + agent built from `agent`. See [Custom reviewers](../using/reviewers.md). +- `minimalReviewers(agent)`: code-functionality, readability and test, plus + every reviewer discovered in `.orca/reviewers/` or the global tier. +- `reviewerCatalog`: the run's resolved definitions, `.all` and `.minimal`, to + filter yourself. Compose a `List[Reviewer]` from it, from `ReviewerPrompts` + (the shipped entries alone), or your own + `Reviewer(ReviewerSlug(name), description, systemPrompt)`, then + `buildReviewers(agent, list)`. + +## Selecting reviewers per round + +`reviewerSelection` defaults to `ReviewerSelector.default`. It narrows the +roster in two ways. First, a picker on `reviewAgent`'s +[cheap tier](choosing-agents.md#the-cheap-tier) chooses reviewers for round one +from 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. A quiet +reviewer stops costing a turn, but it does not see later fixes. If narrowing +would leave no reviewer while a lint finding keeps the loop going, the +round-one selection runs again and a step says so. + +| Selector | Behaviour | +|---|---| +| `default` | `narrowingAcrossRounds(agentDriven)` | +| `allEveryRound` | the whole 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 and brief | +| `narrowingAcrossRounds(base)` | adds the per-round narrowing over any `base` | + +A reviewer's `files:` pattern gates whether the picker is offered it, 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..3fdff53fd --- /dev/null +++ b/docs/authoring/stages.md @@ -0,0 +1,79 @@ +# Stages + +`stage(name)(body)` is the committing, resumable unit of work. On success Orca +records the body's result in the progress log and makes one commit: the code +changes plus the log update. On a re-run, a stage that already has a recorded +result is skipped, and the stored result is returned. + +```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). + +- The result type `T` needs a `JsonData`. `case class Foo(...) derives JsonData` + is enough; `Unit`, `String` and the library's types 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 them. +- The stage name appears in the event log, the commit message and the resume + preamble. Choose one a reader understands without the code: `"Push + open PR"`. + +## Side effects happen inside stages + +Every side-effecting call must be inside a `stage` body, and the compiler +enforces it. A mutation outside a stage does not compile. This covers +`git.push`, `fs.write`, `gh` writes, and every `agent.*.run`. + +These run anywhere: + +- pure reads: `git.uncommittedDiff`, `git.changedFiles`, `gh.readIssue`, + `gh.availability`, `fs.read` +- `display(message)`: progress output only, no stage, no commit, no log entry +- `fail(message)`: abort with a message; the run stays on the feature branch so + a re-run resumes +- `agent.session(name, seed)`: creating the handle only registers a name; + running it is the side effect + +## Authoring rules + +The compiler does not check these. They keep a flow resumable. + +1. **Do not stage reads.** A stage with only reads wastes a commit and a + checkpoint. + +2. **Push in a later stage than the edit.** A stage commits only on completion, + so a `git.push()` in the same stage as the edit pushes nothing. Put the + push in a separate, later stage. `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. `gh.upsertComment(target, marker, body)` edits an earlier comment + that carries `marker`. So a resumed stage updates instead of duplicating. + `orcaCommentMarker(userPrompt, purpose)` gives a marker unique to the run. + +## Long loops + +A review loop is one stage, so one commit. When each iteration is long, write +the loop in the flow instead: one stage per iteration calling the coder +session's `.run`, so 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; they throw. Results come back in completion order, not +input order. diff --git a/docs/authoring/talking-to-agents.md b/docs/authoring/talking-to-agents.md new file mode 100644 index 000000000..65e918ae1 --- /dev/null +++ b/docs/authoring/talking-to-agents.md @@ -0,0 +1,131 @@ +# Talking to agents + +There are three ways to talk to an agent. Pick by what the conversation must +survive and who steers it. All of them need a `stage(...)` body, see +[Stages](stages.md); that page also defines the flow thread and forks. + +- **`agent.run(prompt)` / `agent.resultAs[O]...run(input)`** for a one-shot + question. +- **`agent.chat()`** for follow-ups within this attempt, including inside forks. + Each fork creates 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. + +Two modes: **`.autonomous`** runs the turn unattended; **`.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. `session.run` has no +interactive mode: a turn a human steered cannot be rebuilt from a seed on +resume. Steer a session through `session.chat` after it has run. Interactive +turns share your terminal: never run them in parallel. + +## Durable sessions + +`agent.session(name, seed)` returns the session with this `name` in the current +stage, creating it on 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. + +- `name` is the role, `"implementer"`, and 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. +- Rename the stage and the key moves with it. 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 stages. A handle cannot be a stage result: + `FlowSession` has no `JsonData`. Creating and running happen on the flow + thread. +- The record behind the handle lives in `.orca/cache/`, not in branch history, + so 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 it on +first use. If the harness lost the conversation on resume, the session is +re-seeded, with a warning. The history is gone. 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 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. + +### 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. Scope one to a unit of work, a task or +a review stage, not to the run. The shipped flows create a session per task and +another 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. Orca creates +a fresh session from the seed and warns. + +## Ephemeral chats + +`agent.chat()` returns a `Chat` continuing one conversation across `.run` calls +within this attempt only: no seeding, no persistence. Chats work inside +`Par.mapUnordered`. Typical use: 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("…")`) 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` is 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: 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]`, +provided by `derives JsonData` on a case class, for schema generation and +parsing. 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`. + +Define an `Announce[O]` instance to print a friendly summary in the event log +instead of raw JSON; the library's `Plan` has one. + +```scala +case class MergeCheck(ok: Boolean, reason: String) derives JsonData + +val check = reviewAgent.resultAs[MergeCheck].autonomous.run("Is this change safe to merge?") +``` + +## 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..97c71f958 --- /dev/null +++ b/docs/authoring/tutorial.md @@ -0,0 +1,150 @@ +# Writing your first flow + +This page builds the shipped `implement.sc` piece by piece. It plans a prompt +into tasks, implements and reviews each one, reviews the whole change, and +opens a PR. + +## The header + +Save the file as `implement.sc`. Every flow starts the same way: + +```scala +//> using scala 3.9.0 +//> using dep "org.virtuslab::orca:0.1.10" +//> using jvm 21 + +import orca.{*, given} +``` + +If the first line 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, +`userPrompt` is the prompt and `planningAgent`, `codingAgent` and `reviewAgent` +are the role agents 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. The planner produces the +plan in one turn. The result is recorded in the progress log, so a re-run with +the same prompt skips this stage and reads the stored plan back. `.value` drops +the planning chat and keeps the `Plan`; see [Planning](planning.md). + +## One stage per task + +```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 before, a re-run skips completed tasks and picks up at the first incomplete +one. + +`codingAgent.session("implementer", seed = plan.brief)` is 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. 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. See [Talking to agents](talking-to-agents.md). + +`reviewThenFix` runs one review round and one fix turn. It returns the findings +it left open; the final review below is 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 one review round, so nobody 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. `Title` wraps a task title. +`ReviewDiff.WholeRun` shows reviewers everything since the run started. +`maxFixTurns = 5` is above the default of 3, because nothing reviews the code +after this loop. `priorOpenFindings` hands over what the task reviews left +open. + +## Open a PR + +```scala + openPrIfGitHub( + summarisingAgent = codingAgent.cheap, + openFindings = openFindings + ) +``` + +It opens a PR when the repository is on GitHub and `gh` can reach it. Otherwise +it prints one line saying why not. Findings the loop left open are listed in +the PR body. See [Pull requests](pull-requests.md). + +## Run it + +```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 each stage. On +success it switches you back to the branch you started on. The work stays on +the run's branch and PR. Interrupt it, and run the same command again: it +resumes from the last committed stage. +[Branches, resume and worktrees](../using/run-lifecycle.md) covers the details. + +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. 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 + +`orca create ""` has your configured agents write a flow for you. +`orca fork ""` does the same, starting from an existing flow. +See [Orca Shell](../using/shell.md). + +## Where to go next + +- [Stages](stages.md): the rules that keep a flow resumable. +- [Choosing agents](choosing-agents.md): 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..fbc0ddcc2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,91 @@ +# -*- 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' + +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..b5c8ce4f8 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,68 @@ +# Development + +How Orca itself is built. The full material lives in the repository: +[CONTRIBUTING.md](https://github.com/VirtusLab/orca/blob/master/CONTRIBUTING.md) +for build, test and local-run recipes, +[AGENTS.md](https://github.com/VirtusLab/orca/blob/master/AGENTS.md) for +internals and coding conventions, and +[`adr/`](https://github.com/VirtusLab/orca/tree/master/adr) for the +architecture decision records. + +## Layout + +Orca is Scala 3 on [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. + +``` +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 +``` + +``` +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`, `OrcaArgs`. The flow module +adds `orca.plan`, `orca.review`, `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 + +Requires sbt 1.12+ and JDK 21. + +```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..84797c469 --- /dev/null +++ b/docs/getting-started/how-it-works.md @@ -0,0 +1,64 @@ +# 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 it +you call agents, read the repository, and group work into **stages**. Orca runs +it with scala-cli. The script is ordinary code, so loops, conditions and helper +functions decide what happens, not an agent's judgement. + +```{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 + +A `stage(name)` body 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/` committed on the feature branch. One stage, one commit. + +Because the log is committed with the code, the two cannot drift apart. That +is what makes a run **resumable**: run the same prompt again and each stage +already in the log is skipped and its recorded result reused. Work continues +from the first unfinished stage. See +[Branches, resume and worktrees](../using/run-lifecycle.md). + +## Orca owns git + +Orca creates the feature branch and, at the end, removes the progress log +(there is nothing left to resume) and opens the PR. Agents are told not to +commit, push or switch branches. They edit files; the flow decides what +happens to the edits. The compiler helps: a call that writes to the repository +or runs an agent does not compile outside a stage. See +[Stages](../authoring/stages.md). + +## Agents are yours + +Orca drives the coding-agent CLIs you already use, the +[harnesses](../glossary/users.md#agents-and-conversations): `claude`, `codex`, +`opencode`, `pi`, `gemini`. Which one handles the planning, coding and review +roles comes from [settings](../using/settings.md), so a flow never needs to +name a harness. Agents run in your repository with your instruction files, MCP +servers and hooks, see [Agent CLIs](../using/agent-clis.md). + +A flow talks to an agent in three ways: a one-shot question, a conversation +that ends when the script exits, or a **session** that 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 +reviews the whole change. Reviewers, lint and format commands are configured +per project. Findings 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..c17664c66 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,65 @@ +# Quick start + +A flow is a Scala script that tells coding agents what to do. The built-in +`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 + +- A logged-in coding-agent CLI, called a [harness](../glossary/users.md#agents-and-conversations): + `claude`, `codex`, `opencode`, `pi` or `gemini`. See + [Agent CLIs](../using/agent-clis.md). +- `git`, and `gh` if you want pull requests opened for you. + +The installer sets up everything else, including scala-cli and a JVM. + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/VirtusLab/orca/master/install.sh | bash +``` + +This installs `scala-cli` if it is missing and writes the `orca` launcher to +`~/.local/bin/orca`. [Orca Shell](../using/shell.md) describes what the script +does and how to run a pinned version without installing. + +## First run + +```bash +cd your-project +orca +``` + +The first run asks which harness and model to use for planning, coding and +review. Then pick a flow (`implement.sc` is first in the list) and enter your +prompt, for example "add a rate limiter to /login". + +The same thing without the menu: + +```bash +orca run implement.sc "add a rate limiter to /login" +``` + +## What happens + +Orca creates a feature branch, plans the change into tasks, implements each +task and has it reviewed, runs a final review over the whole change, and opens +a PR when the repository is on GitHub. Each step is a **stage**, committed as +it finishes. If the run 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. Run Orca +in a sandbox: see [Run in a sandbox](../using/agent-clis.md#run-in-a-sandbox). +``` + +## Next steps + +- [Ways to use Orca](ways-to-use.md): interactively, from another agent or CI, + or as a plain script. +- [Built-in flows](../using/built-in-flows.md): what ships with Orca. +- [Writing your first flow](../authoring/tutorial.md): 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..040bac839 --- /dev/null +++ b/docs/getting-started/ways-to-use.md @@ -0,0 +1,47 @@ +# Ways to use Orca + +## Interactively + +Run `orca`. A menu lists the flows found in the project, in your global config, +and built in. From it 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) has the details. + +## Headless, from an agent or CI + +Every menu action has a subcommand, so a coding agent or a CI job can run Orca +without a terminal: + +```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). Which agent +plans, codes and reviews comes from [settings](../using/settings.md). + +Agents can load the +[`skills/orca`](https://github.com/VirtusLab/orca/blob/master/skills/orca/SKILL.md) +skill to know when and how to delegate to Orca. In Claude Code, `/orca [prompt]` +asks which flow to run and whether to run it on a new branch, the current +branch or a worktree, then starts it. The skill installs as a Claude Code +plugin, a Pi package, or by symlinking into any harness's skills directory; its +[README](https://github.com/VirtusLab/orca/blob/master/skills/orca/README.md) +has the specifics. + +## As a script + +A flow is a scala-cli script, so it runs without installing Orca. Only +scala-cli is needed: + +```bash +scala-cli run --workspace "$(mktemp -d)" implement.sc -- "add a rate limiter to /login" +``` + +Orca is published to Maven Central; scala-cli fetches the artifacts on first +run. `--workspace` keeps scala-cli's build output (a `.scala-build` directory) +out of your repository. + +This is 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..88f450161 --- /dev/null +++ b/docs/glossary/developers.md @@ -0,0 +1,89 @@ +# Glossary for developers + +Orca's internal vocabulary, 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 it adds the codebase facts. The rest of the +internals are in +[AGENTS.md](https://github.com/VirtusLab/orca/blob/master/AGENTS.md). + +## Review + +Definitions: [user glossary](users.md#review). + +- **finding** is `ReviewFinding`, in `ReviewResult.findings`. `issue` is not a + synonym: in this codebase it means a GitHub issue (`orca.tools.Issue`, + `IssueHandle`). +- **declined** is `DeclinedFinding`, in `FixOutcome.declined`: one of several + reasons a finding stays open. The wire shape the fixing agent fills, so it + carries title and reason and nothing else. +- **open finding** is `OpenFinding`, in `OpenFindings`, paired with an + `OpenReason`. 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 each reason's prose is written. + +## Persisted state + +Definitions of run and attempt: [user glossary](users.md#flows-and-runs). + +- **run** — keyed by `RunKey`, the 12-hex prefix of SHA-256(prompt). A run 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** — keyed by `AttemptId` (`-`). An attempt 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; a plan task has no +file of its own. + +## Backends + +Words for talking to a coding agent, from the outside in: + +- **call** — one `agent.run` / `session.run` / `chat.run`. A retry stays inside + the call. The review loop's fix turn is a call. +- **turn** — 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** — one assistant message inside a turn, closed by + `TurnEvent.AssistantMessageEnd` and shown as one `OrcaEvent.AssistantMessage`. +- **decoder** — a backend's wire protocol as a `LineDecoder`: a fold over the + lines of its stream. `DecodedTurn` runs the decoder over a turn. +- **conversation** — the history a backend keeps across turns, which a session + resumes. A `LiveTurn` is not a conversation. +- **client id** (`SessionId`) — Orca's own handle for a session, stable across + attempts. **wire id** (`WireSessionId`) — the id the backend knows the + conversation by. `SessionId#onWire` is the only crossing. +- **`IdScheme`** — how wire ids come to be: `ClientClaimed` (the client id is + the wire id: claude, pi) or `ServerMinted` (the backend mints it on the first + turn: codex, gemini, opencode). +- **conversation key** (`OrcaEvent.conversationKey`) — the wire id, or the + client id before one is known; the one key turns and sessions join on in + events and the cost log. +- **dispatch** — `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** — 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. + +A CLI's own "turn" can differ: codex's `turn.completed` ends Orca's turn, but +claude's `num_turns` counts tool calls plus one. + +## Capabilities + +Definitions: [user glossary](users.md#capabilities-and-tool-limits). + +- **`FlowContext`** — thread-safe; forks receive it freely. +- **`FlowControl`** — must stay on the thread that created it. +- **`InStage`** — the stage-bound token that may be shared: every agent run + takes it, and a fork may capture it. +- **`WorkspaceWrite`** — 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`** — 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..8fc396031 --- /dev/null +++ b/docs/glossary/users.md @@ -0,0 +1,91 @@ +# Glossary for users + +The words these docs use. Orca's internal vocabulary is in the +[developer glossary](developers.md). + +## Flows and runs + +- **flow** — a Scala script whose body is `flow(OrcaArgs(args)): ...`. See + [Writing your first flow](../authoring/tutorial.md). +- **flow args** — `OrcaArgs`: the prompt and the command-line flags. +- **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 [Stages](../authoring/stages.md). +- **plan** — `orca.plan.Plan`: the task list the planning agent (the + **planner**) produces. See [Planning](../authoring/planning.md). +- **plan task** — one `orca.plan.Task` of a plan. The **plan brief** + (`Plan.brief`) is the planner's codebase briefing. +- **run** — one prompt's flow execution, across every process it takes to + finish. See [Branches, resume and worktrees](../using/run-lifecycle.md). +- **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`, and so on. See [Backends](../api/backends.md). +- **role agent** — `planningAgent`, `codingAgent` or `reviewAgent`, resolved + from [settings](../using/settings.md). +- **cheap tier** — `agent.cheap`: the harness's cheaper model. +- **turn** — one prompt to an agent and its reply. +- **conversation** — the history a harness keeps across the turns of one chat + or session. +- **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). See + [Talking to agents](../authoring/talking-to-agents.md). +- **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. +- **structured output** — `resultAs[O]`: a reply parsed into an `O`, which + needs a `JsonData[O]`. + +## Review + +- **reviewer** — a prompt saying what to look for, paired with a read-only + agent. +- **reviewer catalog** — `reviewerCatalog`: every reviewer a run can use. See + [Custom reviewers](../using/reviewers.md). +- **roster** — the reviewers one review call is given. +- **reviewer picker** — the cheap agent that chooses, from the roster, which + reviewers run for a task. +- **review round** — 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** — the coder session's `.run` that fixes a round's findings. + `maxFixTurns` caps how many. +- **finding** — a problem a reviewer, the lint gate or a check reported. +- **declined finding** — a finding the fixer refused, with a reason. +- **open finding** — a finding the review ended without resolving. +- **`OpenFindings`** — what a review returns: the findings it left open. See + [Data structures](../api/data-structures.md#review). +- **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`. +- **`Configured`** — 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 compile-checked token a call needs. `InStage` (agent + calls) and `WorkspaceWrite` (git, `gh` and file writes) come from a + `stage(...)` body; `FlowControl` (starting stages, creating sessions) from + the `flow(...)` body. `FlowContext` (reads) is not one. See + [Capabilities](../authoring/capabilities.md). +- **fork** — a function running in parallel under `Par.mapUnordered`. See + [Stages](../authoring/stages.md#parallel-work). +- **`ToolSet`** — 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..f4f8cd502 --- /dev/null +++ b/docs/using/agent-clis.md @@ -0,0 +1,58 @@ +# Agent CLIs + +Orca drives the coding-agent CLIs you already have, the +[harnesses](../glossary/users.md#agents-and-conversations): `claude`, `codex`, +`opencode`, `pi` and `gemini`. Each one manages its own authentication; Orca +stores no secrets. Before running a flow, log in to the harness you use, and to +`gh` if the flow opens PRs or reads issues, each per its own instructions. + +## Run in a sandbox + +```{warning} +Run Orca in a sandbox. By default the coding agent edits files and runs shell +commands without asking. +``` + +You can narrow an agent's tools or auto-approval in the flow, see +[Choosing agents](../authoring/choosing-agents.md). For an unattended run 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. 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. + +No one is present to approve tool calls, so: + +- Coding turns auto-approve every tool by default. +- On claude, read-only roles (the planner, reviewers, and the agent that picks + reviewers) can use only the tools Orca allows. Your MCP tools are blocked + 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, or write + "if available". +- **A human in the loop.** In autonomous flows, "ask me before X" or "wait for + confirmation" cannot work. + +## OpenCode with a local Ollama model + +- **Launcher, zero config.** In the flow script, pass a launcher: + `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 with the model already pulled. See + [Extending flows in code](../authoring/extending.md). +- **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")` (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..5a5f1bf21 --- /dev/null +++ b/docs/using/built-in-flows.md @@ -0,0 +1,46 @@ +# Built-in flows + +Orca ships these flows. `orca list` shows them together with your project and +global flows; `orca view ` prints the 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 them take the [role agents](../glossary/users.md#agents-and-conversations) +from [settings](settings.md) and need those agents logged in. `gh` is optional +unless noted. + +| Flow | Prompt | Does | +|---|---|---| +| `implement.sc` | what to build | 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. The default choice. | +| `implement-interactive.sc` | what to build | Same as `implement.sc`, but the planner can ask you clarifying questions before producing the plan. On a re-run a finished planning stage is skipped, so you are not asked again. | +| `implement-enhanced.sc` | what to build | `implement.sc` plus two steps: the planner critiques and improves its own draft, and a documentation stage updates the project's docs from what the tasks changed. | +| `simple.sc` | one well-scoped task | No planning. The prompt is the one task, handed straight to the coder, then reviewed. For small changes where a plan is overhead. Also what `orca create` and `orca fork` run. | +| `issue-pr.sc` | `owner/repo#N` or an issue URL | Reads the issue and checks it against the repository: are its claims right, is detail missing, is it a duplicate, is the scope sane. Then either posts a rejection comment or plans, implements, reviews and opens a PR. Branch `fix/issue-`. Needs `gh`. | +| `issue-pr-bugfix.sc` | `owner/repo#N` or an issue URL | The bug-report variant. Triages first: not a bug (comments), a bug no test can show (comments with reproduction steps), or a testable bug. For a testable bug it writes a failing test, opens a tentative PR, waits for CI to go red, confirms the failure matches the report, then fixes and updates the PR. Needs `gh`. | +| `review.sc` | a PR reference or URL, a branch, "the uncommitted changes", a commit range, or a diff on stdin | Review only: picks reviewers, runs them concurrently, prints every finding. When the target is a PR, posts the report on it; a re-run replaces the earlier report. Nothing is fixed or committed. | + +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 +``` + +Re-run with the same prompt: the progress log, and for the issue flows 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 temp directory and run a flow against it: + +- `01-simple`: autonomous planning, then implement and review each task. +- `02-interactive`: the same shape, but the planner can pause to ask you + questions. + +Each has a `create-test-project.sh` and a README with the exact commands. 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..4f0deb6e5 --- /dev/null +++ b/docs/using/output-and-files.md @@ -0,0 +1,55 @@ +# Terminal output and files + +While Orca runs, the terminal 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 | Meaning | +|---|---| +| `▶` | stage start, or a step: a single-line note like a branch switch | +| `▸` | the prompt sent to an agent | +| `●` | assistant prose | +| `⏺` | 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 | +| `⎿` | how many times the line above repeated, as `⎿ ×12` | +| `✖` | error | +| `?` | approval request, or a question for you; interactive turns only | +| `!` | caveat about a tool limit Orca cannot enforce for this run, see [Choosing agents](../authoring/choosing-agents.md); never indented under a stage | + +Colours and animation turn off when stderr is not a terminal. `NO_COLOR=1` +forces colours off; `ORCA_NO_ANIMATION=1` suppresses the spinner. + +## Closing summary + +A finished run 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/` + +`.orca/` holds 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 | + +`` is derived from the prompt, `` from the attempt's start time and +pid. Under `.orca/cache/`: + +| Path | Holds | +|---|---| +| `runs/.sessions.json` | a run's durable session records | +| `attempts/.manifest.json` | the attempt's sessions and status; what [`orca continue`](shell.md) lists | +| `attempts/.cost.jsonl` | one line per agent turn: agent, role, model, stage, token usage and cost. The per-agent and per-model detail the closing summary leaves out | +| `attempts/.trace.log` | a DEBUG trace: prompts, agent output, tool calls. Its path is printed at the start of a run. 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. + +If your `.gitignore` covers all of `.orca/`, every attempt warns you to remove +that line so 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..ad0af6ce0 --- /dev/null +++ b/docs/using/reviewers.md @@ -0,0 +1,84 @@ +# Custom reviewers + +A reviewer is a prompt saying what to look for, paired with a read-only agent. +Orca ships eight: code-functionality, test, readability, code-structure, +simplicity, performance, security and scala-fp. Add your own, or retune a +shipped one, with a Markdown file; no code changes. + +## Where reviewers come 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: `.orca/reviewers/orca.md` is the reviewer `orca`. +Project beats global beats built-in. A file with the same name as a reviewer +in a lower tier replaces it, in the same position. New names are appended, +sorted by name. + +Flows take reviewers from the catalog in two rosters: `allReviewers` (every +reviewer) and `minimalReviewers` (code-functionality, readability and test). +A new name joins both. A shadowing file runs only where the shipped reviewer +runs: `scala-fp` is not in `minimalReviewers`, so +`.orca/reviewers/scala-fp.md` retunes it for this project without changing +that roster. The [reviewer picker](../authoring/review.md) still chooses from +the roster per task. + +The run lists what the project and global tiers contributed: + +```text +discovered reviewers: orca (project); scala-fp (project, shadows built-in) +``` + +## File format + +Frontmatter plus 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: 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 `_`-prefixed file are treated as documents and skipped. +Every other `.md` file must be a valid reviewer. Any of these aborts the run +before Orca changes the repository; 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 because a silently dropped reviewer would look like a clean +review. A symlink in `.orca/reviewers/` also aborts: that directory comes from +a repository Orca did not write. Symlinks in the global tier are allowed; that +is your own config. + +## Using reviewers from a flow + +`allReviewers(agent)`, `minimalReviewers(agent)` and `reviewerCatalog` 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..460bc465f --- /dev/null +++ b/docs/using/run-lifecycle.md @@ -0,0 +1,116 @@ +# Branches, resume and worktrees + +A **run** is one prompt's flow execution, across however many processes it +takes to finish. 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. +Which [stages](../glossary/users.md#flows-and-runs) finished, and with what +results, is recorded there. + +## Start + +On a fresh run Orca: + +1. Asks what to do with uncommitted changes; stash is the default (see + [Run targets](#run-targets)). Stashed changes come back with `git stash pop`. +2. Creates and checks out the feature branch. +3. Writes and commits the progress log header. + +`codingAgent.cheap` derives a short label from the prompt; its slug is the +branch name. `--branch ` sets the name and overrides the flow's +[`branchNaming`](../authoring/extending.md). A name that is protected (`main`, +`master`, or the repository's default branch) or already exists is refused; +Orca does not pick another. + +## Run targets + +Three flags decide where a run works. + +| Flag | Works on | Uncommitted files on a fresh run | +|---|---|---| +| none (the default) | a new branch | asked: stash (default), keep or abort; stashed when there is no terminal | +| `--skip-branch` | the current branch | kept; they are swept into the first stage's commit | +| `--keep-changes` | with or without `--skip-branch` | kept; in normal mode they reach the new branch in the first stage's commit | +| `--worktree` | a second checkout under `.orca/worktrees/` | left behind; cannot be combined with `--skip-branch` or `--keep-changes` | + +`--skip-branch` is for continuing work already planned on a branch. It refuses +a protected branch or a detached HEAD, and cannot be combined with `--branch`. + +A re-run (any run that finds a progress log, even an unreadable one) always +stashes and ignores `--keep-changes`, so 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`: +`NewBranch(uncommitted)`, `CurrentBranch(uncommitted)` or `Worktree`, where +`uncommitted` is `Uncommitted.Stash` or `Uncommitted.Keep`. It has one case +per allowed combination, so a refused combination cannot be written in code. A +script can set the field itself, and that overrides the flags: +`flow(OrcaArgs(args).copy(target = RunTarget.Worktree))`. + +### Worktrees + +`--worktree` runs the whole flow in `.orca/worktrees/` of the repository. +The checkout is keyed on the same prompt hash as the progress log, created on +the first attempt and reused by every later attempt for that prompt. Two runs +never share a checkout or a branch. + +Things to know: + +- 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/` and + `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. A `--branch` naming a different branch than the log's 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) it resumes +gets the same note. + +A corrupt or truncated progress log is detected at startup. Orca warns and +starts fresh, re-running previous stages, rather than mis-resuming silently. + +## Success + +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, it is deleted and HEAD returns to the starting branch. + +Otherwise the 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 [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 it 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 (`--skip-branch`, `--keep-changes`, or +answering keep), Orca cannot tell your untracked files from the run's own. +Then no untracked file is ever deleted, in any stage, even 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; 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..83b40be9e --- /dev/null +++ b/docs/using/settings.md @@ -0,0 +1,117 @@ +# Settings + +Orca reads two `settings.properties` files. Both are plain `key = value` lines, +parsed once per [attempt](../glossary/users.md#flows-and-runs), before Orca +changes anything in the repository. + +| File | Holds | Committed | +|---|---|---| +| `.orca/settings.properties` in the project | stack commands and, per role, which agent to use | yes, with the project | +| `~/.config/orca/settings.properties` (`$XDG_CONFIG_HOME/orca/`, also on macOS) | agent keys only | no, per user | + +A missing global file is fine. A file that cannot be read or parsed, in either +place, aborts the run before Orca changes anything in the repository. A stack +key (`format`, `lint`, `test`) in the global file is also an error. + +## Stack commands + +Keys `format`, `lint` and `test`. Each is a **gate**: a command the review loop +can run over the change. Each value is one shell command, run with `bash -c` +in the flow's working directory. Everything after the first `=` is command +text, so `lint = FOO=bar cargo check` works. + +- Repeating a key appends: the commands run in file order. A repository with + two stacks lists one line per stack. +- The value `off` disables that gate explicitly. A missing key skips the gate + too. +- `#` starts a comment. Commenting a line out is the same as deleting it. + +A typical discovered 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 +``` + +The [review loop](../authoring/review.md) runs `format` before each round and +`lint` with the reviewers. It never runs `test`, to stay cheap. A flow can read +all three as `summon[FlowContext].stackSettings` and run the tests in its own +stage, see [Gates and checks](../authoring/gates-and-checks.md). + +## Agent keys + +`planningAgent`, `codingAgent` and `reviewAgent`. Valid in both files, single +valued: a repeated agent key is an error. The value is `harness[:model]`, split +at the first `:`, so a model id containing `:` survives. `harness` is one of +`claude`, `codex`, `opencode`, `pi`, `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 verbatim to the harness. Orca does not validate model +ids, with 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. + +Agent keys are read even when `flow(stackSettings = Some(...))` pins the stack +commands. Setup 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) +``` + +`` marks a role with no model pin: the harness picks one. +`[1m]` is claude's 1M-token context window variant. + +Set the keys from the command line with `orca config --coding-agent codex` +or edit a file with `orca config --edit project|global`. See +[Orca Shell](shell.md). + +## Precedence + +Code always wins over files. + +- **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 + +Discovery runs only when the project file is absent or has no stack line at +all. A file with some stack keys is left alone. It spends one cheap, read-only +agent call inspecting the repository, then writes the file and announces every +guess: + +```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 existing content, so agent lines are never +touched. To run discovery again, delete the stack lines, delete the file, or +run `orca clear-stack`. With a complete file no model call is made; this is the +normal case, including in CI. + +Each discovered command names the file it was inferred from. Two checks run +before the file is written: 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 never +run. A key left with no command gets a live `key = off` line. 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..a5608f037 --- /dev/null +++ b/docs/using/shell.md @@ -0,0 +1,115 @@ +# 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. + +Add `~/.local/bin` to your `PATH` if the installer says it is not there, then +run `orca`. + +To avoid installing anything, or to pin a version (for example in CI), run the +shell directly. The version below tracks the latest release; any release that +includes the shell works. `--workspace` 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 first run a wizard picks 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)). Then 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 one action and exits. + +| Command | Key flags | Does | +|---|---|---| +| `orca run [prompt]` | see below | run a flow; exits with the flow's exit code; with no prompt, reads it from stdin | +| `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` is required to customise a built-in | +| `orca create ""` | `--name `, `--global` | run the built-in `simple.sc` flow in an isolated sandbox to have the configured agents write a new flow; `--name` is derived when omitted | +| `orca fork ""` | `--name `, `--global` | the same, starting from an existing flow | +| `orca continue [selector]` | `--list`, `--json` | resume a recorded harness session; no selector means the newest | +| `orca config` | `--planning-agent`, `--coding-agent`, `--review-agent`, each `harness[:model]`; or `--edit project\|global` | show the role agents, set any subset, or hand-edit a settings file, created from a template if absent | +| `orca list` | `--json` | list project, global and built-in flows | +| `orca clear-stack` | `--yes` | forget the detected `format` / `lint` / `test` commands so the next run re-detects them, see [Settings](settings.md) | + +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. + +`orca continue`'s selector is an id from `--list`, a session name, or a branch. +An id keeps naming the same session while other attempts record theirs. A name +matching several sessions in one working tree resumes the most recent. A +selector matching both a name and a branch is refused. + +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; `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))