Autonomous, on a leash
The agent that closes your backlog while you sleep.
It reads the issue, writes the branch, opens the pull request, and stops at the merge button. You approve the three decisions that matter and skip the forty that do not.
Trusted by the people who had to approve it
No card. Every action logged. Revoke a tool and the next run respects it.
- 00:03github.search3 files reference buildCacheKey
- 00:18shell.test1 failing — the fixture never renames
- 00:34github.openPullRequest#1203 opened as a draft
- 00:36github.mergeNot called — requires approval
Pro
What this adds to the free templateRunning in production at
- Northgate
- Basalt
- Quorum Labs
- Ferrite
- Meridian
- Hollowpoint
How it works
It runs a loop, and it stops before the parts you care about.
Every run is the same shape. The interesting part is not that it is autonomous — it is where the autonomy ends.
Transcript
Pro only— not included in the free template at https://astroloop.larryxue.devOne real run, including the part where it got it wrong.
Every step it took on issue #482, in order, with the arguments in full. Step five failed. Step ten was never called.
11 steps · 36.5s wall clock
00:00.0Goal receivedClose240mssucceeded
issue: #482 repo: northgate/atlas opened: 6 days ago by @rina-k
00:00.2Planned 4 stepsReproduce, locate the cache key, fix, prove it with a test3.1ssucceeded
1. reproduce against a scratch index 2. find where the collection name is baked into the cache key 3. change the key to include the collection id 4. add a regression test that renames and re-queries
00:03.3github.search3 files reference buildCacheKey890mssucceeded
query: "buildCacheKey" in:file repo:northgate/atlas → src/search/cache.ts:41 → src/search/index.ts:118 → test/search/cache.test.ts:9
00:04.2github.readFilesrc/search/cache.ts410mssucceeded
export function buildCacheKey(collection: Collection) { return `idx:${collection.name}:${collection.version}` } // the name is the problem — it changes on rename, the id does not00:04.6shell.test1 failing — the fixture never renames anything14.2sfailed
$ npm test -- search/cache ✗ invalidates on rename Expected the key to change, received the same key twice. The fixture builds one collection and asserts on it. There is nothing here that renames, so the test cannot have been covering this path.
00:18.8github.readFileRe-read the fixture rather than the test2.3schanged approach
test/fixtures/collection.ts → the fixture is frozen at construction, so a rename is not expressible Adjusting the plan: the fixture has to change before the test can.
00:21.1shell.test214 passing, including the new rename case11.8ssucceeded
$ npm test -- search ✓ invalidates on rename ✓ keeps the key stable across a version bump 214 passing (11.8s)
00:32.9github.createBranchfix/482-cache-key-uses-id1.5ssucceeded
base: main @ 9f2c1ab branch: fix/482-cache-key-uses-id 3 files changed, 41 insertions, 12 deletions
00:34.4github.openPullRequest#1203 — Key the search index by collection id2.1ssucceeded
Opened as a draft, linked to #482, and the failing test from step 5 is included in the description so the reviewer can see what was actually broken.
00:36.5github.mergeNot called — merge requires approval—not called — needs approval
The tool definition lists `merge` under approval, so the call was never made. This is a line in the transcript rather than an absence of one: an absence looks like a bug.
00:36.5Waiting for a personAssigned to @rina-k—waiting for a person
The run stays open until someone approves or declines. Declining closes it unbilled.
01
Scoped at the call site
Permissions are enforced where the tool is invoked, not asked for politely in a system prompt. A revoked tool is gone on the next run, mid-task included.
02
Every run leaves a trace
Each step records the tool, the arguments, the result and the elapsed time. When something goes wrong you get a transcript, not a shrug.
03
Priced by work, not by seat
You pay for completed runs. A run that stalls, loops, or gets rejected at handoff does not bill.
Permissions
Everything it can reach, and everything it has to ask about first.
Scoped per tool, enforced at the call site rather than in the prompt. The blanks below are real: where there is no cell, there is no access.
| Tool | Reads | Writes | Needs approval | Audited |
|---|---|---|---|---|
| GitHub | repos, issues, pull requests | comments, branches | Waits for approval on: merge, force-push | audited |
| Slack | channels it is invited to | messages in those channels | no access | audited |
| Postgres | analytics.* (read replica) | no access | no access | audited |
| no access | send as agent@yourdomain | Waits for approval on: any external recipient | audited | |
| Filesystem | /workspace | /workspace | Waits for approval on: delete, chmod | audited |
| Shell | no access | no access | Waits for approval on: every command | audited |
Rows come from src/content/tools/tools.yaml. Leave a field empty and the cell stays empty — that is the honest version, and it is the one that gets believed.
And this is the whole of it.
One definition per tool. The four fields in the table above are the four fields here — there is no second place where permissions are configured, and no prompt that can widen them at runtime.
Delete a line and the capability is gone on the next run, mid-task included.
import { defineTool } from '@astroloop/sdk'
export const github = defineTool({
name: 'github',
reads: ['repos', 'issues', 'pulls'],
writes: ['comments', 'branches'],
// Anything listed here stops the run and waits for a person.
// Checked at the call site, so the agent cannot talk its way
// past it the way it can with an instruction in the prompt.
approval: ['merge', 'force-push'],
audit: true,
})Issue #482 · seven steps · 36 seconds
One run, end to end, including the part where it stops.
00:00
issue.readReads the issue
Title, body, labels and the three comments that changed the requirement.
00:03
github.searchFinds the code
Three files reference buildCacheKey. It opens all three before editing any.
00:11
shell.writeWrites the branch
A branch, a commit, and a message that says what changed rather than what was asked.
00:18
shell.testRuns the tests
One fails — the fixture never renames. It reads the failure instead of retrying.
00:26
shell.writeFixes the fixture
The narrower change. It does not touch the assertion to make the test pass.
00:34
github.openPullRequestOpens the pull request
As a draft, with the failing run and the fix both in the description.
00:36
github.mergeStops
Merge requires approval. It has not been called, and it will not be.
Results
Pro only— not included in the free template at https://astroloop.larryxue.devMeasured against a person doing the same 412 issues.
Including the row where we lose. Methodology is below the table, not behind a form.
| Task | Astroloop | Baseline | n | Note |
|---|---|---|---|---|
| Issue to merged pull request | 68% | 41% | 412 | Human review still required; measures reaching an approvable PR. |
| Correct on the first attempt | 44% | 39% | 412 | Retries are free, so this is a cost metric rather than a quality one. |
| Escalated to a person | 31% | 12% | 412 | Deliberately high. Every escalation is a run we chose not to guess on. |
| Actions taken without approval | 0 | 7 | 412 | Enforced in the tool layer, so this is a property rather than a result. |
| Median wall clock per run | 214s | 96s | 412 | We are slower. Planning and re-reading cost time we choose to spend. |
In production
1.24M
runs completed
last 30 days
3.1%
escalated to a person
and none of them silently
0
unapproved merges
since the check moved out of the prompt
11s
median time to first tool call
p95 is 34s
Logs and control
Pro only— not included in the free template at https://astroloop.larryxue.devWhat is written down, and who can read it back.
Every step, in full
Tool, arguments, result and elapsed time for each step. Arguments are never truncated — truncation is always where the interesting part was.
Approval is a person
Named actions suspend the run and wait. There is no timeout that approves for you, and no configuration that removes the gate.
Exportable audit log
Every permission change, approval and refusal, as NDJSON, with the actor attached. Ships to your SIEM or sits in object storage.
Your runner, optionally
Run the executor inside your own network. The control plane never sees the repository, only the run metadata.
Retention you set
Ninety days by default, anything from seven upward on Enterprise. Deletion is a hard delete, including from backups within thirty days.
Bring your own keys
Point it at your own model provider accounts. The permission layer does not change with the model.
Models
Pro only— not included in the free template at https://astroloop.larryxue.devWhich model sees what, and where it goes.
Named per stage rather than described in the aggregate. Bring your own keys and none of it changes.
| Stage | Model | Provider | Your keys | Retention | Sees |
|---|---|---|---|---|---|
| Planning | Claude Opus 5 | Anthropic | supported | Zero retention | Sees the issue text and the file tree, not file contents. |
| Editing | Claude Sonnet 5 | Anthropic | supported | Zero retention | Sees only the files the plan named. |
| Classification | Local rule set | Runs in your runner | not applicable | Never leaves | Regex and heuristics. No model involved. |
| Embeddings | text-embedding-3-large | OpenAI | supported | 30 days at provider | Optional. Disabled by default. |
What people say
The people who had to sign off on it.
I approved it for a two-week trial expecting to revoke it. What changed my mind was not the output, it was that every refusal showed up in the log with the reason attached.
Priya Raghunathan
Staff Security Engineer, Basalt
It closed forty-one issues in its first month and asked me eleven questions. The eleven were the right eleven, which is the part I did not expect.
Tom Íñigo
Engineering Manager, Northgate
We had already written the policy doc for what an agent would be allowed to do. Configuring it here took an afternoon because the fields were the same fields.
Wen Xiaoqing
Platform Lead, Quorum Labs
Pricing
You pay for work finished, not for people logged in.
A run that stalls, loops, or gets rejected at handoff does not bill.
Free
$0forever
20 runs a month
- All tools, same permissions
- Full run transcripts
- Community support
Team
Most teams$29per month
then $0.40 per run
- Unlimited approvers
- 90-day transcript retention
- Slack and email handoff
- Priority queue
Enterprise
Talk to us
volume pricing
- Self-hosted runners
- SSO and audit export
- Custom tool allowlists
- A person who answers
What it costs
Pro only— not included in the free template at https://astroloop.larryxue.devMove the slider. That is the whole pricing page.
Runs that stall or get declined at handoff are deducted rather than hidden, because they are the ones you would otherwise find on the invoice.
Estimated monthly
$148
- Plan
- $29
- Billable runs
- 312
- Not billed
- −28
- Per run after included
- $0.40
At this volume the Team plan is the cheaper of the two. Scale pays off above about 590 billable runs.
Questions
The ones we get asked before the trial, not after.
- What counts as a run?
- One goal, from the first plan to either a result or a rejected handoff. Retries inside a run are not billed separately.
- Can it act without asking?
- Only for actions you have not listed under approval. Everything on that list suspends the run and waits, and the list is enforced in the tool layer rather than the prompt.
- What happens when it stalls?
- The run is cancelled at the step budget and not billed. You get the transcript either way.
- Where does my code go?
- Into the runner for the length of the run, and nowhere else. Self-hosted runners are available if that is still one place too many.
- Which models does it use?
- Whichever you configure, including your own keys. The permission layer does not change with the model.
- Can I revoke a tool mid-task?
- Yes. Permissions are read at the call site, so the next call fails closed even in the middle of a run.
Writing
What we got wrong, in public.
Why approval cannot live in the prompt
An instruction that says "always ask before merging" is a suggestion. Here is where we moved the check, and what it cost us to move it.
What a run actually costs
We billed per seat for two months. Here is why we stopped, and what the numbers looked like on both sides of the change.
The transcript is the product
We built the run transcript as a debugging tool for ourselves. It turned out to be the thing customers actually evaluate us on.
Give it one repository and see what comes back.
Twenty runs a month, free, with the same permissions and the same transcripts as every other plan.