Stargate
Your coding agents, working as a team. On one task. Without stepping on each other.
Stargate is a small, local orchestrator that gives four roles — architect,
developer, reviewer, fixer — to whichever agent CLIs you already pay for, and
runs them against your repository with Git as the isolation boundary. No
framework, no SDK, no daemon, no cloud: one Python package, one runtime
dependency (PyYAML), and a pipx install.
cd ~/dev/my-project
stargate run "Add pagination to the users endpoint"
That plans in your real checkout (read-only), implements in a throwaway worktree, runs your tests, reviews the diff, fixes what the review flags, and leaves you a commit on its own branch. Your working tree never moves.
Why it exists
One agent in one terminal is a single point of judgment: the author reviews its own work, and every retry costs the same context again. Adding terminals does not fix it — N agents on one checkout is a race, and the more of them you run, the more of your attention goes to refereeing instead of the work.
Stargate splits the roles instead. The planner cannot write. The reviewer has never been the author. The developer works in an isolated worktree it cannot escape. And an agent is a command prefix, not an integration: anything you can run non-interactively and read the output of qualifies, configured as an argument list. So you can put the sharpest model on review and a cheaper one on typing, mix vendors, or run all four roles through one CLI — and adding a new one is a few lines of YAML, not a plugin.
The flow
you
│
▼
Claude / architect (read-only planning)
│
▼
Git worktree + dedicated branch
│
▼
Codex / developer (implementation)
│
▼
tests (optional)
│
▼
Kiro / reviewer
│
├── APPROVED ─────────────► orchestrator commit ──► done
│
└── CHANGES_REQUESTED
│
▼
Codex / fixer
│
▼
tests
│
└──────────────► review again
Got work that splits? --fan-out turns the plan into a validated
DAG: ready nodes run concurrently, each on its own branch and worktree, a
dependent node starts from its dependencies' commits so it sees their files,
and the merged tree goes through review once as a whole.
What you can count on
- Nothing is ever lost. Every terminal result with changes is committed on the run's own branch — including a reviewer that still requests changes, a failing test command, or a token-budget stop. The verdict goes in the commit message. Fan-out also commits each completed task and the integration verdict.
- A crash is resumable, not wasted. A crashed run creates no terminal
commit;
resumereuses the frozen config, prompts, branches, worktrees and completed commits, and commits when the run reaches a verdict. It even re-enters a recorded review/fix cycle, so an interrupted fixer does not pay for a fresh review. (A fan-out token-budget stop before integration writes its RESULT and stays resumable, without an integration commit yet.) - Your branch is untouchable. The orchestrator never merges into, rebases,
pushes, or deletes the branch checked out in your repository, and never
removes a worktree mid-run. Publishing requires
--prin that invocation and only pushes the run's own branch. The agents are forbidden to commit or push; the orchestrator performs those actions. - The budget is yours. Token budget per task and per run, timeouts, retries
with backoff, and
--max-review-loops. Exit codes tell you which wall you hit.
Agents
Claude Code, Codex CLI, Kiro CLI, Gemini CLI, and opencode are validated
integrations, not a closed list of supported agents. The packaged configuration
uses Claude for the architect and reviewer and Codex for the developer and fixer; the
examples/ directory includes verified single-vendor
configurations for all five CLIs and documents the differences between them
(who needs {output}, who reports usage, who needs a wrapper). Anything with a
non-interactive command prefix works — stargate doctor --probe tells you
whether yours does, in one real call per agent.
Requirements
- Python 3.10+
- Git
- The agent CLI or CLIs referenced by your effective configuration, installed and authenticated
The packaged default requires Claude Code and Codex CLI. A single-vendor setup
can instead start from one of the verified configurations in
examples/; the Kiro configuration also uses its included wrapper.
Install (global)
pipx install stargate-cli # or: uv tool install stargate-cli
From this checkout, or straight from Git:
pipx install .
pipx install git+https://github.com/dogo/stargate.git
The distribution is named stargate-cli because stargate on PyPI belongs to
DataStax. Nothing else takes the longer name: the command you run is stargate,
the package you import is stargate, and pipx uninstall / uv tool uninstall
are the only places it reappears. Both prompts and the default
config ship inside the package, so once installed the source directory can be
deleted or moved. make install / make install-uv are the same commands;
make uninstall removes it.
Hacking on the orchestrator itself:
make dev # editable install into ./.venv
make test # smoke test with fake agents
Configure
Choose the agent CLI for each role and write a user-level config:
stargate init-config # writes ~/.config/stargate/agents.yaml
stargate init-config --force # replace it, keeping a timestamped backup
On a terminal, setup asks one question for each role: architect, developer, reviewer, and fixer. Enter keeps the suggested vendor (the packaged default's vendor when installed, otherwise the first available one). Choose a number or vendor name to change it, for example:
architect [claude]: codex
developer [codex]: claude
Only installed, verified command prefixes are offered: Claude Code (claude),
Codex (codex), Kiro through kiro-stargate, Gemini (gemini), and opencode
through opencode-stargate. Finding kiro-cli or opencode alone is not enough:
install the wrapper described in examples/kiro (which uses
macOS paths) or examples/opencode first.
Other known CLIs may be reported as
“detected, not configurable”; their vendor-specific flags cannot be guessed.
Detection only checks PATH and never runs an agent or makes a billable probe.
Architect and reviewer use the vendor's reader block; developer and fixer use
its writer block. The Claude reviewer also retains the packaged default's
scoped Bash({test_command}) grant, so it can verify the approved test command.
Generated configs inherit settings from the packaged layer and reproduce those
defaults as comments for reference. Uncomment settings: and the desired setting
to override it. Vendor comments and example-level settings, including Kiro's
timeout and opencode's 420s probe timeout, are not copied; see the
linked vendor notes before customizing those settings.
Existing configs are refused unless you pass --force, which prints the path
of a timestamped backup. Without a terminal on stdin, setup skips questions and
copies the packaged default verbatim. If no verified prefix is installed, it
also copies that default and lists the missing executables; install the needed
CLIs before running it. Inspect the result with stargate doctor. To verify
credentials and capabilities, explicitly run stargate doctor --probe (one
real, billable call per distinct agent).
Without --config, every existing file in this lookup chain is layered, most
specific first:
./.stargate.yamlin the repo you are standing in~/.config/stargate/agents.yaml(or$XDG_CONFIG_HOME)- the
agents.yamlpackaged with the install
settings and workflow merge by key, and agents merges by agent name. An
agent entry is replaced as a whole, so redefining one must include its complete
command, probe, probe_expect, usage_pattern and env as needed; entries
for all other agents are inherited. Top-level scalar values use the
most-specific value. A blank mapping section such as settings: does not erase
inherited settings.
The top-level task_sources list is replaced whole by the most-specific layer.
--config <path> is the exception: that file is used exactly as given, with no
project, user or packaged config layered under it. This is also the escape hatch
for resuming with a repaired agent definition.
stargate doctor numbers every source and marks the source of each effective
setting, agent entry and workflow remap. Per-project overrides use
.stargate.yaml, not agents.yaml, and lookup never walks to a parent directory
or follows paths named by a config, so a global install cannot accidentally read
configuration from an unrelated repository.
Settings reference
Everything under settings: in the layered effective config. All are optional.
task_sources is a separate top-level block, like agents and workflow;
see Read the task from a source.
| key | default | meaning |
|---|---|---|
test_command |
"" |
Shell command run in the worktree after the developer and after every fixer pass. The same effective command is available to agent commands through {test_command}, which the packaged reviewer uses so it can verify the suite. An explicit value always wins over detection. |
test_command_detection |
report |
What to do when test_command is empty: report shows likely commands without running one, auto runs the highest-priority match, and off skips detection. |
commit |
true |
Commit results on the run's own branch. Linear runs can set this to false, or pass --no-commit, to leave the worktree dirty. Fan-out requires true and rejects --no-commit. |
max_review_loops |
2 |
Fixer passes allowed after the first review. 0 reviews once and stops. Overridable per run with --max-review-loops. |
blocking_severities |
[] |
Opt-in severity policy. Empty or absent: the reviewer's explicit verdict decides. A YAML list of high, medium, low derives the verdict from JSON findings. A bare string or an unknown severity is rejected before any agent runs. See Severity policy. |
max_fanout_tasks |
8 |
Maximum number of DAG nodes the fan-out architect may return. Must be a YAML integer: a quoted "8" is rejected, not coerced. |
max_parallel_tasks |
2 |
Maximum ready fan-out tasks run concurrently. Must be a YAML integer, like max_fanout_tasks. Overridable for fan-out runs and resumes with --max-parallel-tasks; linear invocations reject that flag. |
max_task_tokens |
0 |
Stop between phases once agents have reported this many tokens. 0 means no limit. |
agent_timeout_seconds |
1800 |
Kills a single agent invocation. 0 means no timeout. |
agent_retries |
0 |
Retries after a failed agent invocation. 0 preserves the single-attempt behavior; 2 allows up to three attempts. |
agent_retry_backoff_seconds |
10 |
Initial retry wait. It doubles after each failure: the default waits 10s, then 20s, then 40s. |
test_timeout_seconds |
900 |
Kills the test command; a timeout counts as exit 124. |
probe_timeout_seconds |
120 |
Kills a doctor --probe call. Deliberately far shorter than the agent timeout. |
worktree_root |
"" |
Where worktrees are created. Empty means <repo-parent>/.stargate-worktrees/<repo-name>/. |
prompts_dir |
"" |
Directory of custom <role>.md prompts, checked before the user and packaged ones. Relative paths resolve against the repo you run in. |
Per-agent keys live on the agent entry, not here: command, probe,
probe_expect, usage_pattern, env.
Exit codes
| code | meaning |
|---|---|
0 |
Approved, and the test command passed or no command ran (report, off, or no match). |
1 |
An agent errored or timed out, configuration was invalid, or another operational error stopped the command. resume is offered when a run was already created. |
2 |
The CLI arguments were invalid, the reviewer still requested changes after the last allowed fixer pass, or the fixer changed nothing and the loop stopped early. |
3 |
Approved, but the explicit test command or an auto-detected command failed. |
4 |
max_task_tokens was reached; the run stopped between phases. A linear stop goes through the normal RESULT/summary/commit path. A fan-out stop before integration also prints a RESULT block and writes summary.md, but records resumable task state without an integration terminal commit. |
5 |
The run reached a verdict, but Git could not create its commit. The work remains intact and staged where possible; the error prints a manual recovery command. |
6 |
The run reached an APPROVED result, but publishing was refused or failed. The verdict, commit and summary are intact; the message prints the manual command. |
129 |
The run received SIGHUP. Its state is recorded as failed and resume is offered. |
130 |
The run was interrupted with Ctrl-C/SIGINT. Its state is recorded as failed and resume is offered. |
143 |
The run received SIGTERM. Its state is recorded as failed and resume is offered. |
doctor exits 1 when a binary is missing, a prompt cannot be resolved, or a
--probe call fails.
list exits 0 even when a run has an unreadable or stale state.json. It
exits 1 when the current directory is not inside a Git repository or the runs
directory itself cannot be read.
Check setup
stargate doctor
It prints the numbered config layers and provenance of the effective settings
and agents, each role's resolved command, all five resolved prompt files
(including fanout.md), and the configured or detected project test commands
with their evidence. It makes no external calls, so FOUND means only that the
executable resolves in every agent environment that requires it — see
Probing agents to actually verify that an agent can run.
It also lists the coding-agent CLIs it recognises that are installed but unused
by the effective config — the answer to a MISSING claude on a machine that has
gemini. It never rewrites the config from that: the flags that make a role
read-only, or put the final message in a file, are vendor-specific, and a guessed
command would run and do the wrong thing. See
Adding a different agent CLI.
It checks the executable each role names and the CLI required by a known wrapper:
for example, MISSING claude -- required by claude-json-stargate means the wrapper
is configured but claude must also be installed on PATH. kiro-stargate resolves
its own CLI, so kiro-cli need not be on PATH. opencode-stargate additionally needs python3 on
PATH, because the adapter reads opencode's JSON event stream with it. A CLI
reached through a wrapper of your own is still reported as unused — the three
wrappers under examples/ are the only ones it knows to look
through.
Each role's executable and its wrapper's required CLI are resolved using that
entry's env:, so a private PATH can make a CLI outside stargate's PATH FOUND.
Removing or narrowing PATH can instead make a locally installed CLI
MISSING -- not on the PATH of: <role>, with exit code 1.
The effective-settings table reports values and provenance. Most settings are only
validated by the command that uses them, but an invalid blocking_severities is reported
here as an ERROR and exits 1, with the offending value still shown in the table —
finding out from doctor is the point of doctor.
Probing agents
doctor alone never calls an agent. doctor --probe makes one real,
billable call per distinct agent command. It tests the file capability that
role depends on, so a working credential paired with a broken tool layer fails
in seconds instead of being discovered after a paid planning stage:
$ stargate doctor --probe
Agent probes:
OK architect, reviewer (read) [4.3s]
FAIL developer, fixer (write) [6.1s]
agent exited 0 but did not write probe-1.txt; its file-editing tools are not working
The sample shows the probe results; each probe also echoes its command as
$ <command> and, while waiting, reports elapsed time and captured bytes every
30 seconds.
The prompt and required capability live in the config, so the orchestrator
stays vendor-agnostic. {probe_file} becomes an absolute path inside the
throwaway repository, and probe_expect may be read or write:
architect:
command: [claude, -p, --output-format, text, --model, opus]
probe: "Read the file {probe_file} and reply with its contents, nothing else."
probe_expect: read
For a read probe, stargate seeds the file with a random marker and requires the
agent to return it. For a write probe, the file starts absent and the agent must
create it non-empty. The packaged architect and reviewer use read: their
--disallowedTools "Edit Write NotebookEdit" setting intentionally prevents
writes, so demanding one would be a false alarm. The developer and fixer use
write because editing is their job.
Because the probe runs the agent's real command, it also catches a wrong
model name or unsupported flag. The existing {output} contract remains in
force: an agent that declares it and exits 0 without writing the final-message
file fails the probe. Agents with no probe key report SKIP and do not fail
the exit code. Probes remain opt-in, run in a throwaway Git repository rather
than yours, and use probe_timeout_seconds (120) rather than the much longer
agent timeout. Agent-command placeholders are expanded for probes too, so
doctor --probe verifies the same effective command that a real role uses.
Use it against a repository
Stand in the repository you want the agents to modify:
cd ~/dev/my-project
stargate run "Add pagination to the users endpoint"
Before creating run artifacts or calling an agent, Stargate resolves the base ref to a commit and checks a local branch against its configured upstream. It stops if the branch is behind or diverged, then uses the frozen commit for the whole run so a concurrently moving branch cannot change the developer's base. For a remote upstream it queries the remote directly without fetching; if the remote cannot be checked, the run does not start.
Per-project settings (usually just the test command) go in a .stargate.yaml
at the repo root:
settings:
test_command: "npm test"
Choose another starting ref:
stargate run \
--base-ref main \
"Add passkey authentication"
Limit review/fix loops:
stargate run \
--max-review-loops 1 \
"Refactor the image cache"
Give the run and branch a short name before anything is created:
stargate run --name "passkey auth" "Add passkey authentication to account settings"
--name takes precedence over the architect's suggestion and uses at most five
whole words (32 characters), never a word truncated in the middle.
Use --from URL instead of the positional task to read it from a configured source.
Read the task from a source
stargate run --from https://tracker.example/team/project/items/42
Configure the commands that print the task as plain text. task_sources goes at
the top level of your config:
task_sources:
- hosts: [tracker.example, tracker.internal.example]
commands:
- [tracker-cli, show, "{url}", --text]
- [fetch-task, "{url}"] # your fallback wrapper script
env:
SOURCE_PROFILE: work
SOURCE_TOKEN: null # remove an inherited variable
These executable names are examples; supply your installed CLI or wrapper. Each entry contains a list of commands, each an argument list. Stargate matches the URL's hostname case-insensitively, ignoring the port, and uses the first matching entry. An unconfigured host is refused by name: Stargate never fetches a URL itself.
Commands run in order from the invocation directory. The first command that
exits 0 with non-empty output wins. Missing executables, nonzero exits, and a
120-second timeout advance to the next command. Empty or whitespace-only
output is a failed fetch, even with exit 0. If all commands fail, the error
names every attempt with its own stderr; no run artifacts or agents are started.
stargate doctor lists all hosts and commands and checks every command's first
element against PATH. A missing fallback executable makes doctor exit 1 too.
The only placeholders are {url} (the supplied URL), {host} (its hostname),
and {path} (its path, excluding query and fragment). Substitution is literal
within each argument; there is no shell or environment-variable expansion.
Stargate never derives an API URL from a web URL. Put URL conversion, JSON
extraction, or authentication logic in a wrapper and list its executable as a
command so doctor can check it. env: works as on agent entries: values override
the inherited environment, and null removes a variable. Existing CLI login or
inherited tokens work too; doctor prints variable names, never their values.
The fetched text is untrusted input that the operator owns. There is no confirmation gate: it enters the architect's prompt verbatim, like a typed task. The operator is responsible for what the command prints, including changes made at the source since they last read it. A source can describe an issue or a pull request; its command decides whether to include a description, a diff, or both.
--from and a positional task are mutually exclusive; their conflict is rejected
before any run is reserved. The initial branch uses the URL's last non-empty path
segment, for example stargate/42-<task-slug>-<timestamp>. If the segment has no
usable characters, ordinary task naming applies silently. --name still wins,
and the architect can still suggest a branch name as usual. --from also works
with --fan-out.
The exact fetched task and its separate task_source URL are frozen in
state.json for audit; summary.md records Task source: too. resume reuses
the recorded text and never re-fetches, even if the source is now unreachable.
Open a pull request
stargate run --pr "Add the requested feature"
stargate run --pr --from https://tracker.example/team/project/items/42
stargate resume <run-id> --pr
The top-level pull_request: block says how to publish. Only --pr says
whether to publish, for that invocation:
pull_request:
command: [review-cli, create, --head, "{branch}", --title, "{title}", --body-file, "-"]
env:
REVIEW_PROFILE: work
REVIEW_TOKEN: null # remove an inherited variable
review-cli is an example: supply your installed CLI or wrapper. The command
must be a non-empty argument list containing {branch}. {title} is the task's
first non-empty line with whitespace collapsed, whether typed or fetched, falling
back to stargate run if none exists. Stargate never parses source text for title
fields and never uses the architect's branch name as the title.
Placeholders are substituted once within each argument, without shell expansion.
Embedding {title} in a /bin/sh -c script turns task text into shell code,
including text supplied by whoever filed a --from tracker item. Pass placeholders
as positional arguments and quote their expansions inside the script:
pull_request:
command:
- /bin/sh
- -c
- 'exec review-cli create --head "$1" --title "$2" --body-file -'
- _
- "{branch}"
- "{title}"
The command runs in the original repository with the PR body on stdin.
env: overrides or removes inherited variables just as it does for agents.
doctor validates the block and checks the command's executable without running it.
Stargate pushes; the configured command only opens the PR. Stargate chooses
origin, otherwise the sole remote; multiple remotes without origin are
refused. It requires one push destination and checks that destination (including
pushurl) before pushing. No remote, an unqueryable remote, or a branch already
there causes a refusal before attempting a push. The push creates only the
run's stargate/* branch, with an empty-ref lease that also prevents overwriting
one created after the check. It never force-updates an existing branch, pushes
other branches or tags, or changes remote configuration. Git credential prompts
are disabled and publication commands have a 300-second timeout.
The echoed command shows the remote's name rather than its URL, so a credential
embedded in that URL does not appear in the terminal. Git's own output is written
to pull-request-remote.log and pull-request-push.log verbatim, though, so a
URL that carries a token can still reach those files if Git repeats it in an
error. Use a credential helper rather than a token in the remote URL — a URL with
a credential is also visible to anything that can read the process list, which no
amount of redaction here changes.
Only APPROVED with no failing test command and a successful commit can
publish. CHANGES_REQUESTED, BUDGET_EXCEEDED, failed tests, or a failed commit
publish nothing. Stargate prints the branch and worktree location plus the exact
manual push and PR commands, leaving that decision to the operator. Their normal
exit codes are unchanged. An approved run whose publication fails exits 6; its
verdict, commit and summary remain intact. If the push succeeded but the PR command
failed, the branch stays on the remote and the message gives the command to retry.
With --pr, .stargate/runs/<run-id>/pull-request.md holds the body: verdict,
findings table, test command and exit code (or why tests did not run), task text,
and the source URL when present. It is also the input file for the printed manual
command. A run without --pr creates no publication artifacts and publishes
nothing, even when the block is configured. This works with --fan-out too;
only the reviewed integration branch is published.
Resume requires --pr again. Publish intent is never written to state.json.
Resuming a finished run with stargate resume <run-id> --pr re-runs the review at
token cost and may replace the recorded verdict in state.json and summary.md.
If the fresh result is unapproved, nothing is published.
A resumed run uses its frozen config unless --config overrides it, and validates
that actual config before any agent runs. --pr without pull_request.command,
with settings.commit: false, or with --no-commit is rejected before new run
artifacts or agent calls. Malformed publication blocks are also rejected early.
There is no setting that publishes automatically, by design: standing config must not become standing authorization. Stargate never publishes on its own — it publishes when a person says so, in that invocation.
Fan-out
Use --fan-out when one request contains work that can be split across
independent branches:
stargate run --fan-out --max-parallel-tasks 3 \
"Add passkey authentication across the backend, frontend, and docs"
The architect returns a validated tasks.json instead of a prose plan. Each
node has an ID and a self-contained task; acceptance and depends_on are
optional lists, and an explicit empty list is equivalent to omitting either
field. Stargate rejects duplicate or malformed IDs, unknown, duplicate or
self-dependencies, cycles, malformed JSON, and graphs larger than
max_fanout_tasks before a developer starts.
Ready nodes run concurrently, each on its own branch and worktree. A dependent node starts from the commits produced by its dependencies, so it sees their files rather than merely waiting for them. Completed nodes are committed by the orchestrator, merged in topological order into the run's integration branch, tested together, and passed through the normal reviewer/fixer loop as one combined tree.
Fan-out requires commit: true: commits are the protocol that moves work
between isolated worktrees, even though nothing is merged into the user's
original branch. Publishing the integration branch requires --pr in that
invocation. --no-commit is rejected before a new run is
created. resume restores fan-out mode from state.json; it reuses the frozen
config and prompts, tasks.json, task branches/worktrees and every completed
task commit, plus the integration branch/worktree if they were already created.
Only unfinished or failed nodes are retried. The effective parallelism can be
changed when resuming with --max-parallel-tasks; --redo is not supported for
fan-out resumes.
The contract produced by the architect is:
{
"name": "passkey authentication",
"tasks": [
{
"id": "api-contract",
"task": "Define the passkey data model and API contract.",
"depends_on": [],
"acceptance": ["Request and response shapes are documented"]
},
{
"id": "backend",
"task": "Implement the passkey endpoints and focused tests.",
"depends_on": ["api-contract"],
"acceptance": ["Registration and authentication tests pass"]
}
]
}
Tasks that modify the same files should be joined by a dependency or kept as one node. Git conflicts during dependency preparation or final integration stop the run without merging into the original checkout; fix the task split or the preserved branch, then resume.
What gets created
Suppose the architect names the work passkey auth. The implementation gets a
short branch similar to:
stargate/passkey-auth-20260830-181500
When the run reaches a terminal result, this branch also receives a local
commit. The user's main (or other checked-out branch), index and worktree do
not move. A follow-up can branch directly from it:
stargate run --base-ref stargate/passkey-auth-20260830-181500 "Add recovery codes"
For a linear run, the packaged architect prompt asks for a NAME: first line.
Stargate strips a valid line before forwarding the plan to the developer,
reviewer, and fixer. A custom/older prompt that omits it, or a malformed name,
safely falls back to the task slug and leaves the plan untouched. The fan-out
prompt carries the corresponding name in the top-level JSON name field.
The run ID and artifacts directory must exist before the architect runs, so an
architect suggestion renames the branch only. Use --name "passkey auth" when
the run ID should also be short; that produces
20260830-181500-passkey-auth. Existing run IDs are not parsed or migrated, so
they continue to work with list and resume. If a name is reserved twice in
the same second, both the run ID and branch receive the same -2, -3, …
discriminator.
And its own worktree outside the target repository:
<repo-parent>/
├── my-project/
└── .stargate-worktrees/
└── my-project/
└── 20260830-181500-add-passkey-authentication/
Run artifacts are stored under the target repo:
my-project/.stargate/runs/<run-id>/
├── state.json # stage, status, commit, review cycle -- what `resume` reads
├── config.yaml # the fully merged effective config, frozen at run start
├── prompts/ # role and fan-out prompts, frozen at run start
├── plan.md
├── plan.md.log # the agent's full trace, written live
├── plan.md.attempt-2.log # later attempts keep separate traces
├── developer.txt
├── developer.txt.log
├── review-1.md
├── fix-1.txt # only when needed
├── tests-*.txt # if an explicit or auto-detected command runs
├── commit-message.txt # exact traceable message passed to Git, if attempted
└── summary.md
A fan-out run additionally stores tasks.json and one tasks/<id>/ artifact
directory per node. Each task branch is normally <run-branch>-<task-id> (with
a numeric suffix if that branch already exists).
The integration worktree uses the normal run path. Each task worktree is its
sibling at <run-worktree-parent>/<run-id>-<task-id>.
The tree above shows the linear stage filenames. Fan-out uses
architect-tasks.json and tasks.json in place of plan.md, then records each
task separately:
my-project/.stargate/runs/<run-id>/
├── architect-tasks.json # architect's raw final response
├── architect-tasks.json.log # architect's full trace
├── tasks.json # validated, normalized DAG reused by resume
└── tasks/
└── <id>/
├── developer.txt
├── developer.txt.log
├── state.json
├── tests-task.txt # when a test command runs
├── commit-message.txt # when the task commit is attempted
└── result.json # after successful task completion
Integration tests, reviews, fixes, the terminal commit message and summary.md
remain at the run-artifact root. A fan-out budget stop before integration writes
summary.md; resume continues from the saved task state.
Each role prints its .log path before starting, so it can be followed with
tail -f, and its exit code and duration when it ends. While an agent runs it
prints a heartbeat every 30 seconds — elapsed time and bytes written — so a
long silent stage is visibly moving rather than possibly hung:
=== DEVELOPER ===
trace: tail -f .../developer.txt.log
... 30s elapsed, 41,238 bytes written
... 60s elapsed, 96,004 bytes written
[developer] exit 0 in 512s
The trace itself is never echoed to the terminal.
.stargate/ ignores itself (it writes its own .gitignore), so run
artifacts never show up in the target repo's git status.
Configure tests
When test_command is empty, stargate inspects the repository and prints every
likely test command and the file that supplied the evidence. stargate doctor
shows the same information before a run. Detection has a fixed priority rather
than accidental first-match-wins:
make testfor atesttarget inMakefile,makefile, orGNUmakefilenpm test,pnpm test, oryarn testfor a non-placeholderpackage.jsonscripts.test(lock files choose pnpm/yarn)cargo testforCargo.tomlgo test ./...forgo.modswift testforPackage.swiftpytest -qonly with explicit pytest configuration/dependencies ortests/test_*.py
A root-level test_*.py alone is deliberately not a pytest signal: projects
often have executable smoke tests with custom arguments that pytest cannot
collect. Detection reads the original repository once, before agent edits, so
a developer cannot cause a newly written command to execute later in the same
run.
The default mode is report. A detected command is a command the user did not
write into stargate configuration, and the wrong guess could invoke a watcher,
an unexpectedly broad suite, or a misleading reviewer verdict several times.
Report-only mode fixes the previous silent failure without taking that extra
authority: it prints the top choice and alternatives, and tells the reviewer
what was found but not run.
Confirm the project command explicitly in .stargate.yaml:
settings:
test_command: "pytest -q"
Or opt into automatically running the highest-priority match:
settings:
test_command_detection: auto
Set the mode to off when the repository intentionally has no orchestrator
test command. An explicit test_command always wins and suppresses detection.
Examples:
test_command: "swift test"
test_command: "npm test"
test_command: "xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17 Pro'"
The command is intentionally project-specific. It runs through /bin/sh -lc
inside the agent worktree, after the developer and after every fixer pass. The
tail of its output is fed into the reviewer and fixer prompts, so a red suite
is a blocking review finding rather than a number nobody reads. The packaged
reviewer may also run that exact command in the worktree, letting it reproduce
the result itself instead of trusting the pasted report alone.
The reviewer grant follows only a command Stargate itself selected. An explicit
test_command is granted, as is the top detection candidate in auto mode.
report, off, an empty setting, or no detected command grants nothing. A
report-only candidate remains unapproved even though doctor and the reviewer
prompt show it.
Timeouts (seconds) cap a stuck agent or suite:
settings:
agent_timeout_seconds: 1800
test_timeout_seconds: 900
Customize prompts
The version 5 packaged configuration includes five prompts to tune: four role prompts plus the fan-out architect contract. Copy them somewhere writable:
stargate init-prompts # writes ~/.config/stargate/prompts/*.md
Then delete the ones you don't want to change. Lookup is per-file, first hit wins:
settings.prompts_dirfrom the active config, if set~/.config/stargate/prompts/- the prompts packaged with the install
So keeping only a custom reviewer.md leaves the other four on the defaults.
stargate doctor prints the file each prompt resolved to.
The packaged architect.md also asks the architect to begin its response with
NAME: <two to four words>. Stargate treats that line as optional for custom
and frozen prompts: only a valid first non-empty NAME: line is removed, and
the rest of the plan is forwarded unchanged.
fanout.md is separate because its output is machine-readable. It requires a
bare JSON object and receives {max_tasks} in addition to the task and base
ref; malformed output stops before any implementation work starts.
Prompts committed with a project:
# .stargate.yaml
settings:
prompts_dir: .stargate-prompts # relative to the repo you run in
Two things the prompt templates have to respect:
- Only known placeholders are substituted, by literal replacement:
{task}and{base_ref}everywhere, plus{plan}(developer, reviewer, fixer),{tests}(reviewer, fixer),{review}(fixer) and{known_findings}(architect only). An architect template that omits{known_findings}simply receives no inherited section, including custom templates and older runs' frozen prompts. Every other brace is left alone, so a prompt may contain JSON, CSS or an f-string example verbatim. The fan-out prompt additionally receives{max_tasks}. These are separate from the agent-command placeholders described below. reviewer.mdis a contract with the orchestrator, and either form satisfies it: one JSON object as described in Structured review findings, or a response whose last line is exactlyVERDICT: APPROVEDorVERDICT: CHANGES_REQUESTED. Anything else aborts the run.
Structured review findings
The packaged reviewer.md asks for one JSON object and nothing else:
{
"verdict": "CHANGES_REQUESTED",
"findings": [
{
"severity": "high",
"file": "app/storage.py",
"line": 42,
"finding": "Saving truncates the existing file before validating the replacement.",
"why": "Invalid input permanently destroys previously saved user data."
}
]
}
verdict is required and must be exactly APPROVED or CHANGES_REQUESTED. By default,
the orchestrator acts on the reviewer's decision as given. Only an explicit
settings.blocking_severities policy can override it. findings may be empty. Each entry needs a
severity of high, medium or low and a non-empty finding; file, line and why
are optional, and unknown keys are ignored. A CHANGES_REQUESTED with no findings, or an
APPROVED carrying a high one, is accepted rather than treated as an error.
Without a policy, severity is reported but does not decide approval or select what the fixer works on. It is classified by demonstrated impact — the packaged prompt carries the full rubric. It orders the summary table, and a value outside the three labels aborts the run as a malformed review.
The findings reach three places:
{review}in the fixer prompt receives the reviewer's response with no findings renderer applied, JSON or prose. Its content is never rewritten — a finding containing the literal text{tests}reaches the fixer as the reviewer wrote it — though the outer whitespace is trimmed.summary.mdgets a## findingstable when the findings are non-empty, highest severity first. The stored order remains the reviewer's.state.jsonrecords them underfindings, separate from thereviewcheckpoint thatresumeuses. An empty list is stored asnull, like the neighbouringfanoutandreviewkeys, so a consumer must not assume the value is iterable. The checkpoint ends with the run — except when the commit failed, which keeps it soresumecan retry only the commit — while the findings stay. That is what lets the summary of an approved resume that skips the review still show its table.
Both hold the most recent completed review only, which is not always the tree the run
ends on: a fixer edit followed by a token-budget stop finishes with the previous review's
findings, and the summary does not label which pass they came from. A later review replaces
them. The per-pass review-N.md artifacts hold the
passes still available: resuming a finished run starts the numbering over and can overwrite
review-1.md, so they are not an immutable history.
Who decides approval did not change with the V1 contract, though what the parser accepts
did: bare JSON now succeeds where it used to abort, and a malformed payload is a new way to
stop. A reviewer that answers in prose, ending
in VERDICT: APPROVED or VERDICT: CHANGES_REQUESTED, is still read exactly as before —
which is what keeps a custom prompts_dir reviewer working, and what lets a run created
before this existed still resume against its frozen prompt. What a new prompt can change is
the model's own judgment, and malformed output is a new way for a run to stop: neither is a
change to the orchestrator's rules.
Severity policy (opt-in)
The default is unchanged: absent settings.blocking_severities, [], or YAML null
keeps the reviewer's explicit verdict. To opt in, configure a list:
settings:
blocking_severities: [high, medium]
For a JSON review with findings, the orchestrator derives CHANGES_REQUESTED when any
finding has a severity in that list, and APPROVED otherwise. The derived verdict
controls the review/fix loop, summary, exit code and terminal commit message. Findings
remain in the report even when they do not block. This applies to both linear runs and
the combined integration review in fan-out; task scheduling is unchanged.
- A review with no findings always keeps its explicit verdict, including
CHANGES_REQUESTED: there is no evidence to justify deriving an approval. - Prose always keeps its literal final
VERDICT:line, regardless of policy. It carries no structured findings to derive from. - The fixer still receives the reviewer's response unchanged, with no findings renderer. When the policy is active, a separate orchestrator note accompanies the review in the fixer prompt, naming the blocking severities and explaining these exceptions.
The setting must be a YAML list: a bare high, an empty string, or an unknown name such
as critical fails before any agent is called. Names are case-insensitive, surrounding
whitespace is ignored, and duplicates are collapsed. stargate doctor shows the value
and which config layer supplied it.
A resume normally uses the run's frozen config. An explicit --config can tighten,
loosen, or disable the policy. While the worktree fingerprint still matches, Stargate
re-evaluates the saved review-<n>.md under that policy: saved findings can go straight
to the fixer or become approved without paying for another review of the same tree.
Re-evaluating an active or newly disabled policy needs a usable artifact; a missing or
malformed one requires a fresh review. Be aware of what that re-reading implies: while a
policy is active the artifact outranks the recorded verdict in both directions, so a
hand-edited review-<n>.md claiming APPROVED with no findings can approve a run that
recorded CHANGES_REQUESTED, with only the worktree fingerprint as a guard. Without a
policy, V1's rule still holds — an artifact can confirm recorded changes but never create
an approval. An active policy is recorded as
review.blocking_severities in the checkpoint so later resumes can recognize its removal,
including when it was enabled by an earlier resume. Runs that never used a policy keep
V1's replay rules: an approval retries the commit without reading the artifact, and
recorded changes reuse only an artifact that still requests changes.
The existing review-loop budget still applies: if a saved pass falls outside a
reduced budget, it is not replayed.
There is no behavior-neutral derived default. A lenient list such as [high, medium]
can approve a low-only CHANGES_REQUESTED; a strict list such as [high, medium, low]
can block an APPROVED carrying findings. Activating either direction changes approval
behavior. That is why the policy is off by default. The model still judges severity:
a misclassified severity can end a run before the fix.
Inheriting findings across runs
For linear runs, stargate run --base-ref stargate/<previous-branch> "next task"
automatically includes the previous run's findings in the architect prompt. This is the
continuation command printed at the end of a committed run; no opt-in flag is needed.
The resolved base ref must match a recorded state.json branch field exactly: Stargate
scans .stargate/runs/*/state.json, without trying to reconstruct a run id from the branch
name. The current branch also qualifies when --base-ref is omitted. A SHA, tag or
refs/heads/stargate/... spelling does not qualify.
Only the immediately previous run's last completed review travels, including findings
from an APPROVED review. All findings pass through, unfiltered and without a size cap,
so a large review can lengthen the prompt; earlier runs are not accumulated. The
## Known unresolved findings section names the source run and warns that the findings
describe the tree that review saw: an edit after that review may already have resolved one.
The architect checks the current code and plans a fix or explains why a finding stays.
Inheritance injects text only into the architect prompt and does not change verdict rules.
A non-Stargate base ref, no matching run, unreadable state, or absent, null or empty
findings silently adds nothing: no heading, empty section, warning or error. Fan-out runs
do not inherit findings.
Inheritance therefore requires commit: true, the same way fan-out does. A run whose
commit was disabled or failed leaves a branch that still points at the base, so it does not
carry the work its findings describe; inheriting from it would hand the architect a review
of a tree the new run cannot see. Such a run is skipped like any other case inheritance
cannot answer. With commit: false, --base-ref stargate/<branch> gives you neither the
previous run's code nor its findings.
Final message vs. stdout
What an agent prints is not what it answered. codex exec streams the whole
session to stdout — reasoning, every command it ran, a token footer — while
claude -p --output-format text prints only the final message.
That matters because the architect's answer becomes {plan} in three later
prompts and the reviewer's becomes {review} in the fixer prompt. Forward a
trace and every hop pays for the previous hop's trace.
So an agent can be handed a file to write its last message to. Put {output}
anywhere in its command and the orchestrator substitutes a path, forwards
whatever lands in that file, and keeps stdout beside it as a .log:
developer:
command: [codex, exec, --sandbox, workspace-write, --output-last-message, "{output}"]
Agents without {output} keep using stdout, which is correct for the Claude
roles as configured. If a command declares {output} and writes nothing there,
the run stops rather than silently forwarding an empty plan.
This also protects the verdict: a reviewer's trailing session footer would
otherwise sit after the VERDICT: line the orchestrator parses.
Agent commands have two placeholders:
{output}becomes the final-message path described above.{test_command}becomes the exact explicit orauto-detected command that Stargate will run. The packaged reviewer places it in Claude Code'sBash(...)allow rule.
When no test command will run, Stargate removes the argument containing
{test_command} and, for a separate option/value pair, its introducing option.
This avoids Bash(), an empty argv item, or a dangling option consuming the
prompt. Put the placeholder in an option value, as the packaged config does;
it cannot be the executable. Commands without the placeholder are unchanged,
including custom configs and configs frozen by older runs.
The test command is arbitrary shell text, but an allow rule is a pattern. To
keep interpolation from silently widening the reviewer's authority, Stargate
does not interpolate commands containing (, ), ,, *, or control
characters. Stargate can still run such a configured command itself; the
reviewer receives only the test report. stargate doctor prints every expanded
agent command and says whether the grant is effective, absent, or refused.
Stages that produce nothing
The architect already has to return a non-empty plan. The developer has a
different output contract: it must change the worktree. Tracked edits,
deletions, commits and untracked new files all count. If it exits 0 without any
change, the run fails before tests or review and the developer is not recorded
as complete, so a normal resume reruns it.
A fixer can legitimately conclude that a review finding needs no code change,
so the same situation is not treated as an agent failure. Re-reviewing an
identical worktree would only buy the same verdict again; stargate stops the
loop immediately with CHANGES_REQUESTED instead.
Committing the result
Committing is on by default. This changes older behavior deliberately: a
default-off feature would leave every user who did not discover the setting
with the same manual branching and committing step, while completed work would
still exist only in a removable worktree. The scope of the mutation stays
narrow: Git advances only Stargate-created branches inside Stargate worktrees.
For a linear run, use settings.commit: false or --no-commit when that
tradeoff is not wanted. Fan-out requires commits and rejects both configurations.
The linear workflow commits only after a terminal verdict, never after
individual fixer passes, and therefore normally produces one commit. A resume
of already committed work finds an empty index and produces no second commit; a
later --redo that genuinely adds work can produce a new terminal commit.
Empty commits are not created in the linear workflow. Fan-out commits completed
tasks, merges those commits into the integration branch, then records the
integration verdict in a terminal commit; that final commit can be empty when
the merges already contain every file change.
Red outcomes that reach the finish path are durable too. CHANGES_REQUESTED, a
failed test command and a linear BUDGET_EXCEEDED are recorded in the subject
and Stargate-Verdict trailer rather than being presented as success. A fan-out
budget stop before integration writes a RESULT and summary but deliberately
does not put a terminal commit at the base of the integration branch. Crashes,
agent errors, invalid reviewer output and catchable signals also have no
terminal commit; a successful resume records the eventual integration verdict.
The commit stages tracked edits and deletions plus new untracked implementation
files. Gitignored files remain ignored. Stargate also snapshots untracked
entries around every orchestrator test command and excludes entries that first
appeared during testing, so an incomplete .gitignore does not silently add
build/, .venv/ or similar output. Tracked files rewritten by a test,
such as an intentional snapshot or lockfile, remain part of the reviewed diff
and are committed. The untracked snapshot uses Git's directory grouping: if a
suite creates an artifact inside an untracked directory that already existed
before the suite, Git cannot distinguish the nested artifact from the
implementation directory; the durable fix for that mixed case is a project
.gitignore.
The subject begins with stargate:, includes the terminal verdict, and stays
within 72 characters. The body names the task, test result and base ref, while
Git trailers carry the run ID, verdict, base ref and test exit when available.
That run ID maps the commit back to .stargate/runs/<run-id>/. Git still uses
the repository's configured user, signing policy and hooks: Stargate supplies
neither --author, --no-gpg-sign nor --no-verify.
A failed hook, signing problem or missing Git identity returns exit code 5.
The result remains staged where possible, state.json keeps the terminal
verdict plus commit_error, and the terminal names both recoveries: stargate resume <run-id>, which reuses the recorded verdict and retries only the
commit, and a manual git commit in the intact worktree. A hook that succeeds but rewrites files
after staging leaves those rewrites uncommitted with a warning; Stargate never
makes an automatic second commit to conceal that state.
Retrying a transient failure
Retries are off by default. Enable them per project with a count of retries after the first attempt and a base wait:
settings:
agent_retries: 2
agent_retry_backoff_seconds: 10
This allows at most three attempts of the agent that failed, waiting 10 seconds
and then 20 seconds. A successful attempt is never repeated, nor are agents
from an already completed stage. Each attempt announces its number and wait in
the terminal and writes a separate trace such as plan.md.log,
plan.md.attempt-2.log, and plan.md.attempt-3.log.
Non-zero agent exits and timeouts are retried because rate limits, server
errors and dropped connections all appear that way at this boundary. A binary
that cannot start is not retried, nor is an agent that exits successfully but
violates its {output} contract. Stargate does not maintain a brittle list of
vendor error messages. Instead, if two consecutive failures have the same
normalized trace tail, it treats the failure as deterministic and stops early;
this avoids repeatedly paying for a wrong model name or unsupported flag. The
tradeoff is that an identical transient response twice also stops the retries.
Every retry is a fresh, potentially billable call. Reported usage is counted
once for every attempt actually made, including failed attempts, so retries can
reach max_task_tokens sooner. The agent timeout applies independently to each
attempt; configure the retry count and backoff with the resulting worst-case
runtime in mind. After the last attempt, the normal error and Resume with:
hint are preserved.
Listing runs
stargate list shows the runs recorded in the current repository, newest
first, without requiring an active config:
$ stargate list
Runs in /home/me/project (newest first):
* 20260831-101304-add-passkey-authentication [failed]
updated 2026-08-31 10:19:42 | stage developer
task Add passkey authentication
branch stargate/add-passkey-authentication-20260831-101304
worktree /home/me/.stargate-worktrees/project/20260831-101304-add-passkey-authentication (MISSING)
20260830-181500-update-docs [approved]
updated 2026-08-30 18:22:11 | stage review
task Update docs
branch stargate/update-docs-20260830-181500
worktree /home/me/.stargate-worktrees/project/20260830-181500-update-docs
* resumable. Resume the newest with: stargate resume 20260831-101304-add-passkey-authentication
The former stargate runs spelling remains an alias.
Each run appears in a separate block. Task summaries wrap to the terminal width
(80 columns when unavailable), with ... after three lines if truncated.
Branch names and worktree paths remain complete for copying.
Failed runs and rows still reading running are marked with *. A running
row normally means the run is still in flight, but it can also mean the process
was stopped by an uncatchable hard kill such as SIGKILL or power loss. Missing
or corrupt state.json files are shown as unknown rather than crashing or
hiding the run. If .stargate/runs/ does not exist or is empty, the command
reports that there are no runs. Listing only reads recorded state: it never
creates, repairs or deletes run artifacts.
Cleaning runs
stargate clean <run-id> removes one run. stargate clean --all preflights
every recorded run and removes each run that passes; runs that fail a safety
check are skipped, each reported with its reason, and the command exits 1.
Atomicity is per run: a single run is never partially removed, so a fan-out
run keeps all of its worktrees, branches and artifacts if any one of its
targets fails. A run is removed only when its branch
is merged into the current HEAD and Git accepts the worktree as clean. There
is no force option. After those checks, Stargate removes the linked worktree,
its stargate/* branch and the run's .stargate/runs/<run-id>/ artifacts. If a
worktree is missing but still registered, Stargate removes only that stale Git
registration before removing the remaining branch and artifacts.
Resuming a failed run
Every stage is recorded in the run's state.json before and after it runs. If
a stage fails — the CLI is out of credits, the machine sleeps, a package
upgrade removes the install mid-run — the run stops and prints:
Resume with: stargate resume 20260831-101304-add-passkey-authentication
For a linear run, resume reuses the plan, branch, worktree, frozen config and
frozen prompts, and skips the stages already marked complete. It does not
produce a second plan, branch or worktree. A developer that exited successfully
but changed nothing is deliberately not marked complete, so plain resume
reruns it. Fan-out reuse is described in Fan-out.
Untracked test-artifact exclusions are stored in state.json, so output
created before a crash remains excluded when a later process resumes. If a
previous invocation already committed and no new work was added, the resumed
finish detects the empty index and does not make a duplicate commit.
By default it runs under the config frozen into the run, so resuming cannot
silently change the agents the earlier stages ran under. Pass --config to
override that, which is how you resume past a broken agent definition:
stargate --config ./fixed.yaml resume <run-id>
To rerun a linear stage that was already recorded as complete, use the supported
--redo option instead of editing state.json:
stargate resume <run-id> --redo developer
For linear runs, --redo accepts architect or developer and is repeatable.
Fan-out resumes reject it. Redoing only the
architect leaves the developer marked complete, which means the new plan would
not be implemented; stargate warns in that case, and you can pass both
--redo architect --redo developer.
Resuming into the review/fix loop
The review/fix cycle is recorded in state.json alongside a fingerprint of the
worktree the reviewer judged, so a resume re-enters the loop where it stopped
instead of buying a second opinion on the same tree:
- A fixer that died — a vendor usage limit, a crash — resumes straight into
that fixer, reusing the
review-<n>.mdalready on disk. The pass is announced asREVIEW <n> (skipped, reusing …). - A run that reached its verdict but could not commit — a signing prompt
that timed out, a hook that rejected the tree — resumes straight into the
commit, announced as
REVIEW <n> (skipped, already APPROVED). Such a run is marked resumable bystargate listeven though it recorded a terminal verdict, and the commit error namesstargate resume <run-id>as the cheap recovery. - A policy changed by
--config, including disabling it, re-evaluates the saved review against the effective policy. With an unchanged fingerprint, a recorded approval can now go to the fixer and recorded changes can now approve, without another review. - Anything that changed the worktree since the verdict — a hand edit, a fixer that got partway through — invalidates the record, and the loop starts over with a real review. A verdict never covers a tree its reviewer did not see.
max_review_loops is a budget for the run, not a fresh allowance per resume:
a resumed loop continues the count rather than restarting it. Resuming a run
that actually finished discards the record and reviews from scratch, which is
what makes --config with a corrected reviewer take effect.
Terminating a run
Ctrl-C already follows the normal failure path. SIGTERM and SIGHUP do too: the
direct agent process stargate started is killed, state.json is changed from
running to failed with the signal recorded, and the same Resume with: hint
is printed. The orchestrator exits with the conventional 128 + signal code:
130 for SIGINT, 143 for SIGTERM and 129 for SIGHUP. Cleanup is bounded, and a
second termination signal uses the operating system's default action so it
cannot leave the orchestrator stuck.
SIGKILL and sudden power loss cannot be handled by any process. A run stopped
that way can still show as running; inspect it before resuming. Only the direct
agent process is killed during catchable-signal cleanup. Subprocesses it spawned,
such as test runs or tool calls, may be orphaned and continue running, so check
the worktree before resuming. Signal handling also starts only after the CLI has
enough information to enter the run path, so a termination during very early
setup may leave no state file rather than a false running state.
Token budget
settings:
max_task_tokens: 120000 # 0 or unset = no limit
Be clear about what this can and cannot do. Most of a run's tokens are the model reading the repository — that never crosses this process, so the orchestrator cannot meter it and cannot interrupt an agent mid-flight. The number it tracks is whatever the agent's CLI prints, extracted by a regex the config supplies:
developer:
command: [codex, exec, --sandbox, workspace-write, --output-last-message, "{output}"]
usage_pattern: 'tokens used\s+([\d,]+)'
The totals are summed across phases and checked at each phase boundary. So a
budget stops the next agent from starting, never one already running. Parallel
fan-out work can overshoot by the agents that were already scheduled; the RESULT
and summary.md report the run's true totals as Tokens: X of Y, so the
overshoot is visible there as a total above the cap, and the scheduler explains
it on stderr when it stops. Hitting the cap ends the
run with verdict BUDGET_EXCEEDED and exit code 4, leaving its worktree state
intact. A linear stop commits changes through its normal finish path. Completed
fan-out tasks already have task commits, but a stop before integration does not
create a terminal integration commit; resume continues from the saved task
state.
For a genuine in-flight cap, use a vendor flag in the command itself. Claude has one; Codex has no equivalent today:
architect:
command: [claude, -p, --output-format, text, --max-budget-usd, "0.50"]
An agent with no usage_pattern contributes zero to the total, which makes the
budget blind to it. stargate doctor flags that per role whenever a cap is set.
Per-agent environment
An agent inherits the orchestrator's environment. env overrides it for that
agent only, and a null value removes a variable:
architect:
command: [claude, -p, --output-format, text]
env:
ANTHROPIC_API_KEY: null # use the CLI's own login instead
ANTHROPIC_BASE_URL: "https://internal.example/v1"
The null case is the one that earns the feature. A globally exported
ANTHROPIC_API_KEY takes precedence over the Claude CLI's claude.ai login, so
every Claude role fails with Credit balance is too low while the Codex roles
are fine — and the only other fix is unsetting it for the whole orchestrator.
Two consequences worth knowing:
- Probe deduplication keys on command and environment. Two roles running the same command under different credentials are two separate probes, not one; deduping on the command alone would report an agent that was never called.
doctorprints the variable names an agent overrides, never the values.
Change who does what
The roles are aliases:
workflow:
architect: architect
developer: developer
reviewer: reviewer
fixer: fixer
Every agent is just a command prefix. For example, assigning the reviewer role to a Kiro agent only requires its agent definition plus:
workflow:
reviewer: kiro_reviewer
The mapping is vendor-agnostic: Claude, Codex, Kiro, Gemini, opencode, or another compatible CLI can fill any role whose permission and output requirements it supports.
Adding a different agent CLI
The CLI names in stargate/doctor.py only support PATH detection; they do not
compose commands. stargate/vendors.yaml packages the five verified vendor
blocks from examples/ for init-config. Other CLIs still need a command
prefix you configure and verify yourself. An agent is six YAML keys —
command, env, probe, probe_expect, usage_pattern, and the {output} /
{test_command} placeholders — so adding a CLI is a config change.
Finding out what to put in that config is the actual work, and doctor only
catches the first of the four things that go wrong:
- It is not on PATH, or the prompt is not the last argument.
doctorreports this immediately, along with any recognised CLI that is installed;doctor --probealso proves the CLI can read and write when its role needs to. - Its stdout is a session trace, not the answer. Then it needs
{output}(see Final message vs. stdout). A CLI with no flag for that needs a small wrapper. usage_patternmatches nothing, or matches the wrong number. This fails silently: the run finishes and reportsTokens reported: 0, so amax_task_tokensbudget quietly never applies.- Its permission flags are coarser than the packaged ones.
doctorvalidates that the{test_command}placeholder expanded, not what the vendor's flag actually grants.
Points 2 through 4 only surface in a real run, so finish with one against a throwaway repository before trusting a new agent with your own.
examples/ has a folder per CLI — claude,
codex, kiro, gemini,
opencode — each
running every role on that one vendor, with a table comparing how they map onto
the knobs above and notes on which of the four problems each one actually hit.
Why command prefixes instead of SDKs?
The CLI boundary is what keeps this small:
- it uses the authentication you already have in each CLI;
- upgrading an agent CLI does not couple the orchestrator to an SDK;
- each role can have independent permissions/model flags;
- replacing one agent is just YAML;
- the Git worktree remains the shared protocol between agents.
The cost is that agent output is parsed as text rather than consumed as a typed event stream. A typed SDK is the natural next step if that parsing ever becomes the thing that breaks.
Safety model
The important separation is:
- Text fetched through
--fromis untrusted input that the operator owns. It enters the architect's prompt without confirmation;state.jsonretains the task and its separate source URL for audit. - Claude architect/reviewer runs with
--disallowedTools "Edit Write NotebookEdit"in the default config. The packaged reviewer additionally may execute only the effective test command, and it runs in the isolated worktree. - The architect runs in your real repository, not in the worktree, so the
packaged architect deliberately has no
{test_command}grant.doctorwarns if a custom config adds one because that grants project-command execution in the real checkout. - Codex developer/fixer gets workspace write access in the isolated worktree.
- Git commit, destructive and publishing actions remain forbidden to agents by prompt.
- At a terminal result, the orchestrator creates a local commit on the run's own branch using the repository's identity, signing configuration and hooks.
- A fan-out run merges task branches only into its own integration branch. The
orchestrator never pushes anything unless
--pris given in that invocation, and then only the run's ownstargate/*branch, never force-updating or overwriting an existing remote branch. It never merges into the user's original branch, rebases, deletes worktrees during a run, modifies remotes, or touches the branch/index/worktree in the user's original checkout. - The final branch/worktree and its traceable commit are left for human inspection.
The agents' own CLI sandbox and permission settings remain the real enforcement boundary; prompts are guidance, not a security boundary.
License
MIT. See LICENSE.
Metadata
Release files for stargate-cli 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stargate_cli-1.1.0.tar.gz | 203.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stargate_cli-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 313.2 kB
Release files / stargate_cli-1.1.0.tar.gz
| Download URL | stargate_cli-1.1.0.tar.gz |
|---|---|
| Size | 203.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9afab8e43c1fc1b2ea21ee33adecec453ba58ddb431aba8820ad39bcd60d0c94
|
|
BLAKE2b-256 checksum How to use checksums |
28ba6f0b98666be9955062e20d684ee5ad8faf58ebc43e101b6b418d2660d3b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / stargate_cli-1.1.0-py3-none-any.whl
| Download URL | stargate_cli-1.1.0-py3-none-any.whl |
|---|---|
| Size | 109.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
480c8b8fd969a0175dfd604487ef0c74eedba967c59461e77c2385a8906ba701
|
|
BLAKE2b-256 checksum How to use checksums |
6b37261b858f219e230907c4bf31a5c707a026dcb4d396ca43d6fd5a7c58ac4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log