OrcAI.Tool
0.9.0
See the version list below for details.
dotnet tool install --global OrcAI.Tool --version 0.9.0
dotnet new tool-manifest
dotnet tool install --local OrcAI.Tool --version 0.9.0
#tool dotnet:?package=OrcAI.Tool&version=0.9.0
nuke :add-package OrcAI.Tool --version 0.9.0
OrcAI CLI
<p align="center"> <img src="assets/orcai_banner.png" alt="OrcAI" /> </p>
A CLI tool for orchestrating bulk GitHub work across many repositories. From a single YAML config, OrcAI creates a GitHub Project, opens templated issues in every target repo, and hands them off to whoever (or whatever) does the work — a human teammate, a bot, or an AI agent like GitHub Copilot or OpenCode.
Features
- Declarative YAML jobs — one config defines the project, target repos, issue template, assignment behaviour, nudge policy, and notification template.
- Bulk, idempotent issue creation across any number of repos — a lock file makes re-runs free; glob and brace expansion (
"jobs/**/*.{yml,yaml}") for fanning out. - GitHub Project auto-management — finds or creates the board and links every issue to it.
- Assign to anyone —
assign.toaccepts any GitHub user, bot, or GitHub App handle. Default is@copilot, but humans, OpenCode, and custom App bots all work. - Comment-based triggers —
assign.via: commentposts a slash command (e.g./opencode) instead of assigning, for agents that listen for mentions.comment-and-assigndoes both. - Tag anyone in templated comments —
assign.comment,nudge.comment, andnotifyall support{assignee},{job.owner}, and{repo.codeowners}tokens, resolved from YAML and CODEOWNERS files at runtime. orcai nudge— re-trigger stale issues (no linked PR yet) by reassignment, comment, or both.orcai notify— broadcast a templated comment to issues and/or PRs from the lock file; filter by state, dry-run, and inject extra--data key=valuetemplate variables.- Auto issue-body updates — when the Markdown template changes, existing issues' bodies are updated without re-running the structural work (hash-based detection).
- Dependent jobs —
dependsOngates a downstream job on the completion of an upstream one (pr_mergedorissue_closed), either per-repo (filter eligible repos individually) or all-repos (block the run until the entire upstream batch is done).orcai runresolves the full dependency chain automatically;orcai graphrenders it as an ASCII tree;orcai validatecatches cycles and missing upstream files. - Robust at scale — built-in rate limiting (60 writes/min, configurable) with exponential-backoff retry; closed-issue policy (
create/reopen/skip/fail); concurrency control;--continue-on-error; JSON output for CI. - Multiple auth methods — ambient
ghCLI, PAT, or GitHub App (manifest flow supported viaorcai auth create-app). A PAT is only required when the assignee is@copilot.
Installation
OrcAI is distributed as a .NET global tool. Requires .NET 10 or later.
dotnet tool install --global OrcAI.Tool
Then run it as orcai.
Prerequisites
ghCLI: Install from cli.github.com — must be installed and onPATH- Authentication: The easiest option is to ensure
ghis authenticated (gh auth login). OrcAI will use it automatically. For other methods see docs/cli-reference.md.
Quick start
1. Authenticate
The simplest option — if you already use the gh CLI, just make sure it's authenticated:
gh auth login
That's it. OrcAI will pick up the token automatically.
For PAT, GitHub App, or environment variable auth see docs/cli-reference.md.
2. Run a job
# Single config file
orcai run jobs/my-upgrade.yml
# All configs in a directory (quote the glob to prevent shell expansion)
orcai run "jobs/*.yml" --continue-on-error --json
# Limit concurrency to avoid rate limits
orcai run "jobs/*.yml" --max-concurrency 2
run finds or creates a GitHub Project, creates issues from your template, adds them to the project, and triggers the configured assignee — whether that's @copilot, another bot, an AI agent like OpenCode, or a human teammate. Triggering can be via assignment, a templated comment (e.g. a slash command), or both. On success a lock file (<basename>.lock.json) is written alongside the YAML for fast idempotent re-runs.
Commands
| Command | Description |
|---|---|
orcai auth pat/app/create-app/switch |
Store credentials or switch profiles for all other commands |
orcai generate |
Scaffold a YAML job config and stub issue template |
orcai run |
Execute a bulk upgrade job (supports globs, concurrency control, JSON output) |
orcai nudge |
Re-trigger stale issues with no linked PR (reassign, comment, or both) |
orcai notify |
Post a templated comment to issues and/or PRs from the lock file |
orcai validate |
Validate YAML config(s) and verify all repos are accessible |
orcai info |
Display the current state of a job |
orcai cleanup |
Tear down everything created by run |
orcai graph |
Render the dependsOn dependency graph as an ASCII tree |
For full flag details, output formats, lock file schema, and advanced usage see docs/cli-reference.md. For config file settings see docs/config.md.
The original Nushell scripts (orca.nu, cleanup.nu) are documented in docs/nushell-scripts.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.10.5 | 104 | 9/7/2026 |
| 0.10.4 | 119 | 9/3/2026 |
| 0.10.3 | 100 | 9/2/2026 |
| 0.10.2 | 87 | 9/2/2026 |
| 0.10.1 | 98 | 9/1/2026 |
| 0.10.0 | 105 | 8/30/2026 |
| 0.9.0 | 103 | 8/30/2026 |
| 0.8.1 | 297 | 6/16/2026 |
| 0.8.0 | 171 | 6/9/2026 |
| 0.7.5-beta6 | 121 | 6/9/2026 |
| 0.7.4-beta5 | 117 | 5/21/2026 |
| 0.7.3-beta4 | 126 | 5/19/2026 |
| 0.7.2-beta3 | 127 | 5/18/2026 |
| 0.7.1-beta2 | 136 | 5/18/2026 |
| 0.7.0-beta1 | 128 | 5/13/2026 |
| 0.6.0 | 168 | 5/7/2026 |
| 0.5.1 | 304 | 3/17/2026 |
| 0.5.0 | 176 | 3/16/2026 |
| 0.4.4 | 178 | 3/16/2026 |
| 0.4.3 | 167 | 3/16/2026 |
`provider:` YAML field — selects the issue/project tracking backend for a job.
`type: github` (default; omitting `provider:` entirely preserves current behaviour, no change needed to existing YAMLs).
`type: local` — tracks project/issue state as YAML + Markdown files on disk instead of calling the GitHub API. Useful for trying out a job's fan-out behaviour without touching GitHub, or for tracking work in a repo synced via git rather than the GitHub API.
`root:` (optional, `local` only) — where the local store is written, resolved relative to the YAML file's directory (same convention as `issue.template`). Defaults to `.orcai-local` next to the YAML file.
A job using `provider: local` never requires or resolves GitHub authentication — `orcai run`/`generate`/etc. work with no `GH_TOKEN`, PAT, App credentials, or `gh` CLI login at all.
Known limitations: `provider: local` has no PR tracking or bulk repo inspection. `orcai nudge`'s closed-PR handling and `orcai info`'s PR summary always show zero PRs for local jobs, and any `dependsOn` condition needing bulk repo state is unavailable.
`--provider local` flag on `orcai generate` — scaffolds a job YAML with the `provider: { type: local }` block instead of the default (GitHub).
`action:` YAML field — typed, explicit action to execute after issue creation. Supported types:
`assign-copilot` (default when `action:` is absent) — assigns `@copilot`, with an optional trigger comment.
`assign` — assigns any GitHub user or bot (`to` required, `comment` optional).
`comment` — posts a comment only, no assignment (`comment` required).
`comment-and-assign` — posts a comment then assigns (`to` and `comment` required).
`cmd` — runs a shell command or script per repo (`execute` for a script path, `run` for an inline command; mutually exclusive). Supports `args` and `cwd`. Template variables use `{{var}}` syntax: `{{repo}}`, `{{org}}`, `{{issue_number}}`, `{{issue_url}}`, `{{job_title}}`, `{{issue_text}}`, `{{issue_hash}}`, `{{yaml_hash}}`, `{{project_number}}`, `{{run_datetime}}`.
`noop` — skip the action step entirely (replaces `job.skipCopilot: true`).
`cmd-checkout` — clones the target repo (bare, `--depth 1`) and runs the command inside it. Worktrees are reused when the same repo appears across multiple jobs. Extra template variables: `{{checkout_path}}` and `{{job_title_slug}}`.
`cmd-to-github` — checkout → run → commit all changes → push branch → open PR. Supports three write-back modes: `open-pr` (default), `push-branch`, and `fork-and-pr`. Optional fields: `branch`, `commitMessage`, `prTitle`, `prBody`, `errorIfNoDiff`.
`open-pr`/`fork-and-pr` append a `Closes #{{issue_number}}` line to the PR body by default (unless the body already references the issue), so GitHub links the PR to the issue and auto-closes it on merge.
The push (`open-pr`/`push-branch`/`fork-and-pr`) always uses `git push --force`. The orcai-owned branch is always force-pushed by design, and every run starts from a fresh clone with no local remote-tracking ref for it, so `--force-with-lease` would reject the push as stale as soon as any prior run had already pushed to that branch.
Per-step failures (`CmdToGithubCheckoutFailed`/`CmdToGithubNoDiff`/`CmdToGithubPushFailed`/`CmdToGithubOpenPrFailed`) are persisted in the lock file and cleared again the next time the step succeeds, so a one-off failure doesn't get reported forever.
`copy:` list on `cmd`, `cmd-checkout`, and `cmd-to-github` actions — Docker-`COPY`-style staging of input files (e.g. a helper script or prompt file) from where `orcai` is invoked into the command's working directory before it runs. Each entry has `from` (static path or glob; zero matches is a hard error), `to` (exact file path on a single match, destination directory on a glob match), and `keep` (default `false` — deletes the copied file(s) again after the command finishes; for `cmd-to-github` this happens before the commit, so scratch files never leak into the PR diff).
New global config fields (`~/.config/orcai/config.json` / `.orcai/config.json`):
`checkoutRoot` — override the directory where repos are cloned for `cmd-checkout` and `cmd-to-github`. Defaults to an OS temp directory scoped to the run.
`action.writeBack` — global default write-back mode for `cmd-to-github` (`open-pr` | `push-branch` | `fork-and-pr`). Nested under `action` in the config JSON. Overridden by `writeBack` in the job YAML.
`action.onClosedPr` — global default for `cmd-to-github`'s `onClosedPr` (`skip` | `recreate` | `reopen` | `fail`). Nested under `action` in the config JSON. Overridden by `onClosedPr` in the job YAML.
`cmd-to-github` (`open-pr`/`fork-and-pr` write-back modes) is now idempotent by design instead of only by accident of a clean lock file. Before cloning, `orcai run` checks live GitHub state (a PR linked to the issue via `closingPullRequests`, and whether the branch already exists on the remote) — never the lock file, so behaviour is identical whether or not one is present:
An **open** PR with unchanged YAML/template hashes is left alone entirely — no clone, no re-run of `execute`, no push. This holds even with no lock file present at all (deleted, or a fresh checkout on CI): with no prior lock to compare against, the content is assumed unchanged rather than treated as changed, so a missing lock file never forces a redo/force-push of an already-open PR.
A **merged** PR is always left alone, regardless of hash changes.
A **closed** (unmerged) PR is handled per the new `onClosedPr` field (see below).
When no PR is found but the branch already exists on the remote and hashes are unchanged, only `gh pr create` is retried against the existing branch — still no clone/execute/push.
`onClosedPr` field on `cmd-to-github` (job YAML, mirrors `job.onClosedIssue`'s shape) — controls what happens when the only PR found for the branch is closed without merging. Values: `skip` (default — treat as an intentional decision, do nothing), `recreate` (redo the full run and open a brand-new PR), `reopen` (`gh pr reopen` plus a force-push of fresh content to the existing branch, no new PR), `fail` (record a failure requiring manual intervention). Has no effect with `writeBack: push-branch`, which never creates a PR.
**GitHub App permission**: The **Contents** permission on the GitHub App must now be set to **Read & write** (instead of Read) to support push-based action types (`cmd-to-github` with `open-pr` or `push-branch`). OrcAI injects the resolved App/PAT token into the checkout git/PR subprocesses automatically — no separate credential setup is required.
`dependsOn` YAML field — gates a downstream job on the completion state of one or more upstream jobs. Each entry specifies a `job` (relative path), `condition` (`pr_merged` | `issue_closed`), `scope` (`per_repo` | `all_repos`), and `untrackedRepos` (`include` | `skip`). Multiple entries use AND logic.
`orcai run` now resolves `dependsOn` chains in topological order before executing. Passing a downstream YAML is sufficient — upstream dependencies are discovered and run automatically. The `scope: all_repos` option blocks the entire downstream run when any upstream repo has not met the condition; `scope: per_repo` (default) filters the downstream repo list individually.
`orcai graph <yaml>` — new command that renders the `dependsOn` dependency tree as an ASCII diagram. File-system only; no GitHub API calls. Supports `--json` output.
`orcai validate` now detects circular `dependsOn` references and missing upstream files, reporting them as configuration errors.
`orcai nudge --on-closed-pr` — controls what happens when the only PRs found for an issue are closed without merging. Values: `skip` (default — don't nudge), `nudge` (re-trigger the assignee anyway), `fail` (report as a failure). Merged PRs are always treated as done and never trigger this flag.
`orcai migrate <yaml>` — upgrades a job YAML and its sibling `.lock.json` in place to the current schema version, preserving old runtime behaviour (e.g. `assign:`/`skipCopilot` → the equivalent `action:` block, `onClosedIssue` default preserved explicitly). The lock file step is purely local — no GitHub calls — so migrating never forces a job's repos back onto the live lookup path the way deleting the lock file would. Supports `--dryrun` and `--json`; always backs up any file it changes to `<file>.bak` first. Designed to extend to future schema hops (v2→v3, etc.) without restructuring — each hop is an independent step.
**BREAKING**: Issue and project identifiers are now opaque strings instead of GitHub-shaped integers (internal groundwork for supporting non-GitHub providers in the future). User-visible effects:
**Lock files**: the `.lock.json` format has changed. Existing lock files fail to load with a message pointing at `orcai migrate <yaml>` (upgrades the lock file in place, no GitHub calls) as the recommended fix; deleting the file and re-running `orcai run` still works but re-fetches state from GitHub for every repo.
**`--json` output**: issue and project numbers in `orcai run --json`, `orcai info --json`, and `orcai cleanup --json` are now emitted as JSON strings instead of numbers (e.g. `"issueNumber": "42"` instead of `"issueNumber": 42`). Human-readable console output (e.g. `#42`) is unchanged.
**BREAKING**: `onClosedIssue` default changed from `create` to `skip`. Previously, when a closed issue with a matching title was found, OrcAI would open a new issue alongside it. Now it treats the closed issue as already done and skips the repo. To restore the old behaviour, add `onClosedIssue: create` to the `job:` block in your YAML. The `redoOnClosed` YAML field and config option (added as a workaround for the wrong default on checkout actions) have been removed; use `onClosedIssue: create` instead.
**BREAKING**: `assign:` YAML block removed. Validation fails with a migration message when `assign:` is present. Migrate to `action: { type: assign-copilot, ... }` or the appropriate action type.
**BREAKING**: `job.skipCopilot` removed. Validation fails with a migration message when present. Use `action: { type: noop }` to skip assignment, or omit `action:` to assign `@copilot`.
**BREAKING**: `--skip-copilot` CLI flag removed from `orcai run` and `orcai generate`. Use `action: { type: noop }` in the YAML instead.
**BREAKING**: `skipCopilot` and `assign` fields removed from the global/local JSON config (`~/.config/orcai/config.json`). `action:` is per-job only.
**BREAKING**: Top-level `writeBack` config key moved to `action.writeBack`. Update config files from `"writeBack": "..."` to `"action": { "writeBack": "..." }`.
`orcai generate` no longer generates a `skipCopilot` comment line; generates an `action:` comment block instead.
`orcai generate` now scaffolds a `version: 2` field at the top of every generated job YAML, so newly-created files self-declare their schema version the same way `orcai migrate` stamps it onto upgraded ones. The field is inert today (ignored by the parser) but lets future schema migrations detect a file's version without a structural heuristic.
`orcai nudge` and `orcai notify` derive the `{assignee}` template variable from the job's `action:` type rather than `assign.to`.
`orcai nudge` now surfaces PR state when checking for existing PRs. The `state` field (`OPEN`, `CLOSED`, `MERGED`) is stored on PR entries in the lock file; old lock files without the field default to `OPEN` on load.
`orcai nudge` no longer treats a closed PR in the lock file as "PR exists — skip". Only open PRs in the lock suppress the live check. Closed PR entries (e.g. written by `orcai info --save-lock`) are now ignored by the lock-file fast-path, so nudge correctly proceeds to a live GitHub check for those issues.
`orcai nudge --save-lock` now persists all discovered PRs with their state to the lock file, so closed PRs are visible via `orcai info`.