Configuration reference
The manager reads a single YAML file at ~/.config/agentarivm/agentarivm.yml (override with --config <path> or $AGENTARIVM_CONFIG). It is validated at startup and the process refuses to run on a bad config, loudly — a control plane for remote code execution should not start half-configured.
The file is policy: paths, allowlists, cadences, and setup profiles. Repositories themselves live in the manager’s database and are added at runtime with agentarivm project add; the projects: block here only refines them.
This page summarises the sections that matter. Defaults shown are the shipped ones.
real (default) or fake. In fake mode every external adapter — git, GitHub, the agent — is swapped for a deterministic double, giving you an offline development loop.
server
Section titled “server”server: host: 127.0.0.1 # loopback by default port: 8787 publicUrl: https://your-host.example:8443 # what pairing tells devices to talk to tls: mode: managed # off | managed | files requireClientCert: true extraHosts: [] # managed: extra names the server certificate must coverhost— loopback by default. Binding anything else while TLS is off is refused at config load, because the misconfiguration looks perfectly healthy from the inside while your tokens are on the wire in cleartext.publicUrl— the origin devices should use. The manager cannot guess this; it is whatagentarivm pairprints, so an unset value means a pairing code with no address attached.tls.mode—off(plain HTTP, right for loopback and tests),managed(the manager runs its own private CA, issues its own server certificate and one client certificate per paired device), orfiles(bring your own certificate, e.g. Let’s Encrypt — setcertFile,keyFileandclientCaFile).tls.requireClientCert— on by default whenever TLS is on. This is what turns strangers away during the handshake.trustProxyandallowInsecureBindexist for deployments with their own proxy in front; leave them off unless that describes you.
All required, no defaults — these are decisions about your disk:
paths: gitRoot: /home/agent/git # canonical clones worktreeRoot: /home/agent/worktrees # one worktree per task, under here secretsRoot: /home/agent/secrets # per-project secret files copied by setup profiles stateDir: ... # the SQLite database runtimeDir: ... # sockets, runtime files logDir: ...Every generated worktree path is checked to remain inside worktreeRoot.
git: executable: git # absolute path recommended on a deployed machine signing: off # off | ssh | inherit # signingKey: ~/.ssh/id_ed25519.pub # required by signing: ssh # authorName / authorEmail # override commit attribution; usually unsetsigning: off is the default deliberately: a global commit.gpgsign=true inherited on a headless machine makes every commit die inside gpg, because pinentry has no terminal to prompt on. ssh is the mode that works unattended.
github
Section titled “github”github: ghExecutable: gh cloneProtocol: https # https | ssh — what clones and pushes use requireWritePermission: true # only repos the gh user can push to may be added expectedAccount: your-login # refused at boot if gh is logged in as anyone else issueTaskLabel: agent-task # marks an issue as agent work issueReadyLabel: agent:ready # authorises a task for it issueWatch: enabled: true # pause switch; see GitHub issue automation intervalSeconds: 60 allowedActors: [] # empty = the account gh is signed in as pushAccount: # optional: a second account, for the agent login: my-agent-bot tokenFile: ~/.config/agentarivm/secrets/push-token comments: post: true # the manager comments on decisions read: false # OFF by default — see below before turning it on maxInPrompt: 20 # most recent N comments reach a prompt maxCharsEach: 2000 # each comment is cut to this, and says socomments is the manager’s voice on GitHub, and its ears — with a switch each, because only one of them opens an untrusted-input surface. post governs speaking, read governs listening. With post: true it comments on the transitions that carry a decision — the independent review’s verdict with its findings, a gate failure, an escalation — and on nothing else; the event log already carries everything that merely happened. It reads the thread back into the next agent’s brief and the next review’s, which is what gives the reviewer a memory: a finding raised and then answered on the pull request is not raised cold again on the next round.
Only comments by someone in issueWatch.allowedActors reach a prompt, plus the manager’s own. Everyone else’s are dropped — not fenced, not summarised. Labelling an issue agent:ready vouches for what is there at that moment; a comment posted afterwards by somebody else is input that authorisation never covered.
read is off by default, and what makes it safe to turn on is a checklist, not a judgement call:
github.pushAccountnames a second account, and its token really belongs to that account.- That login is not the one the manager posts as, and is not in
issueWatch.allowedActors. agentarivm doctorreports agents as sandboxed — without that, an agent reads~/.config/ghand is the manager again whatever the config says.
doctor prints all three as its accounts and comment trust lines, and raises a problem when read is on and any of them is missing.
Which comments are the manager’s is a row it wrote when it posted, not an author check: the agent holds the same gh credential and can author a comment that looks like the manager’s, but cannot write the manager’s database. On a deployment where that login is also an allowed actor — the default — a comment from it that the manager did not post is admitted but marked UNVERIFIED, and the reviewer is told it is not evidence that anyone asked for anything. A separate machine account removes the ambiguity; see GitHub issue automation. Only its review comments are read back — a gate-failure comment quotes output from commands the agent itself ran, so it is posted for people and never returns. The two caps above are per-comment and per-thread; a hard aggregate ceiling sits under both, so raising them cannot produce a prompt section larger than the diff it accompanies. See GitHub issue automation.
pushAccount is the account the agent pushes and opens pull requests with. Leave it out and there is one credential on the machine doing both jobs — the agent pushes with the account the manager comments as, because it needs a credential to push at all. That is what makes comments.read untrustworthy: a comment’s author is then no evidence of who wrote it.
Set it and three things change together. Each worktree gets a git credential helper pinned to that token, replacing whatever the machine’s own config resolves to — appended rather than replaced, the machine’s helper would still win and the push would still authenticate as you. Every agent process is given the token as GH_TOKEN and a GH_CONFIG_DIR of its own, so gh pr create still works. And the sandbox denies ~/.config/gh, which it could not do while the push went through that credential.
The login must not be the one the manager posts as and must not appear in issueWatch.allowedActors; both are refused when the config loads, because either would read as separated and would not be. agentarivm doctor also asks GitHub whether the token really belongs to that login, and the manager refuses to start when it does not.
Use a fine-grained token with contents:write and pull_requests:write, confined to the repositories this manager works on. doctor raises a problem for a classic repo-scoped one: that is full control of every repository its account can reach, and the agent holds the token in GH_TOKEN.
No token scope protects the manager’s own review comments, and none can. It posts them on the pull request, and GitHub governs a pull request’s conversation comments through the Pull requests permission — which the agent must have in order to open a pull request at all. So the manager does not rely on GitHub for this: it records the body it posted and renders its own copy into the next prompt, comparing the two only in order to raise an audit event when they differ. Editing the comment on GitHub cannot put words in front of the reviewer whatever the token can do. See GitHub issue automation.
Pin expectedAccount: a manager authenticated as the wrong account works perfectly and simply does everything under somebody else’s name. See GitHub issue automation for issueWatch.
cloneProtocol decides what a clone uses, and therefore what the agent pushes with, because the agent inherits the remote the manager cloned. https is the default and needs no SSH key at all — the manager already depends on gh and its token for reading issues and opening pull requests, so the same credential does the git half too. It does require that gh auth setup-git has been run as the user the manager runs as, which gives git a credential helper; agentarivm doctor prints a clone protocol line and raises a problem when it has not, because the alternative is discovering it at project add as could not read Username for 'https://github.com'.
Choose ssh if you would rather use a deploy key. That is genuinely narrower than a repo-scoped token: a deploy key reaches one repository, a token reaches everything the account can push to. The cost is that GitHub will not accept the same key as a deploy key on a second repository, so every project needs its own.
Changing this only affects projects added afterwards. A project’s clone URL is recorded when it is added, and nothing rewrites an existing remote.
exec: path: /usr/local/bin:/usr/bin:/bin:/home/agent/.local/binThe PATH given to every subprocess, replacing the inherited one. Not optional in practice on a deployed machine: a daemon inherits a minimal PATH, and tools like gh, claude and codex are shims that call node, which is not on it — they exit 127 with no explanation. Unset means inherit, which is right for a development machine. See Troubleshooting.
engines
Section titled “engines”engines: claude: executable: /home/agent/.local/bin/claude permissionMode: bypassPermissions # maxBudgetUsd: 25 # hard ceiling per task; Claude only extraArgs: [] preTrustWorktrees: false # allowedModels: [opus, sonnet] # unset: whatever this machine's Claude Code offers # allowedEfforts: [low, medium, high, xhigh, max] # defaultModel: opus # unset: Claude Code's own default # defaultEffort: high # unset: Claude Code's own default
agent: defaultEngine: claude # what a request that names no engine gets stopGraceSeconds: 10One entry per agent engine; an entry existing is what enables it. Each entry carries its own model and effort policy, because the vocabularies are engine-scoped — the app renders whatever this block advertises and can only pick from the allowlists; there is no way to request an arbitrary model.
Unset, the model policy is whatever the installed CLI offers. At startup, and whenever its version changes, the manager asks Claude Code for its model list (no prompt is sent, so nothing is billed) and the app shows exactly that, named by what each choice runs on that machine: Default (Opus 5, 1M context), Sonnet 5, Haiku 4.5. Update Claude Code on the agent machine and the new models appear in the app within ten minutes, without a manager release. A task that picks default passes no --model or --effort at all, so Claude Code decides.
Set allowedModels only to narrow the list, or to add a model the CLI accepts but does not list in its picker (an older pinned id such as claude-opus-4-8). A list you write is kept as written. A model you name in defaultModel, loop.review.model or loop.fix.model is offered whatever the CLI lists. An id that follows the vendor’s pattern is named for you (claude-opus-6 shows as Opus 6).
Codex has no way to be asked for its models yet, so unset means default only, Codex’s own choice from its config.toml. List the models here to get a picker.
Once a day the manager also asks npm which Claude Code and Codex release is newest, and compares it with what the installed CLI reports. When the Mac is behind, the app shows a banner above every tab, for example “Claude Code 2.1.320 is available — the Mac runs 2.1.312”. Tapping it offers Update now: after you confirm with Face ID or your passcode, the manager runs the CLI’s own updater (claude update or codex update), and the app follows it until it is done. Any new models appear in the app as soon as it is. If the update fails, the app says why and offers it again, alongside the command to run on the Mac yourself; the installed version is re-read every ten minutes, so the banner also goes away soon after you update by hand.
Nothing is updated unless you ask, and the manager refuses while a task is running: a CLI that changes under a running task is worse than one that is a version behind.
An engine whose executable cannot be found is still shown in the app, greyed out as not installed, and a task that names it is refused with engine_not_installed rather than dying at launch.
Codex is the second engine, absent (disabled) unless its entry exists:
engines: codex: executable: /home/agent/.local/bin/codex sandbox: danger-full-access # or workspace-write # allowedModels: [gpt-6.1-sol, gpt-6-luna] # unset: `default`, Codex's own choice # allowedEfforts: [low, medium, high, xhigh] # defaultModel: gpt-6.1-sol # defaultEffort: mediumsandbox is Codex’s counterpart to permissionMode, and the same decision for the same reason. Credentials come from agentarivm engine-login codex (a ChatGPT subscription, signed in with a device code) or an OpenAI API key piped into agentarivm agent-token install --engine codex; when both exist the login wins, so plan quota is spent before metered credits.
executable is the agent itself, and an absolute path matters here more than anywhere else in this file. How the manager runs it differs by engine, which is a fact worth knowing rather than a detail: Claude Code stays alive for the whole task and takes messages over pipes, while Codex runs one process per turn and continues the conversation from disk. The manager hides the difference — a message you send mid-turn is queued either way — but it is why midTurnInput exists in what the app is told.
permissionMode (Claude) and sandbox (Codex) decide what an agent may do without asking, and the defaults are a real decision rather than an oversight. An unattended agent cannot answer a permission prompt, and anything short of bypassPermissions stalls Claude at the first git commit — so the honest options are “let it work inside its own worktree” or “do not run it unattended at all”. Codex’s workspace-write is the tighter of the two, at the cost of network access inside the sandbox being governed by Codex’s own policy.
What neither setting gives you is isolation from the machine — they govern what the agent asks permission for, not what its process can reach. That is sandbox, below.
A question the agent genuinely asks still reaches you: it arrives in the app as a card, with whatever answers the agent suggested, and the task waits.
preTrustWorktrees pre-accepts Claude Code’s “is this a project you trust?” dialog for each new worktree. Off by default: the manager runs the agent in print mode, where the dialog is skipped, so this would write to ~/.claude.json before every task for a prompt that cannot appear. Turn it on only if a task ever hangs before producing any output.
Which engine a task runs on is chosen when the task is created — in the app, or by an agent:engine/<id> label on an issue — and defaults to agent.defaultEngine. It is fixed for the life of the task.
task: staleMinutes: 20 # a running task with no activity is flagged launchTimeoutMinutes: 5 # stuck in `starting` this long → failed maxConcurrentPerProject: 3 # agents working on one repository at oncemaxConcurrentPerProject is how many agents may work on one repository at the same time. Label four issues and three start together, each in its own worktree on its own branch. agentarivm init writes 1, because the first thing worth knowing is what a single task costs on your machine; the built-in default when the key is absent is 3.
Three things about it are easier to read here than to discover:
- Nothing queues. Work past the limit is refused, not held — and the labels stay on, so the next poll tries again and starts it as soon as a slot frees. That also means order is not guaranteed: whichever issue GitHub lists first when a slot opens wins, not whichever waited longest.
- A finished task still holds its slot. The count is every task that has not reached
stopped,failed,mergedorcleaned_up— so one sitting atpr_openwaiting for your review occupies a slot. Three unreviewed pull requests stop new work from starting, which is the usual reason someone thinks the watcher has died. - It is per repository, and nothing caps the total. Two projects can each run this many. Each task is an agent process, a worktree, and a full run of your verify commands, so the real ceiling is what the machine can stand.
The autonomous loop: the manager runs each component’s verifyCommands itself on every commit, hands failures back in the gates’ own words, has a fresh context review the result, and keeps going until both are green at the same commit — or until it escalates with a reason.
loop: enabled: true # the DEFAULT for new tasks; the create form can say otherwise maxRepairCycles: 6 # failures handed back before the loop gives up maxRuntimeMinutes: 180 # wall clock minus the time at `pr_open`; a pending question is still billed repeatedFailureLimit: 2 # identical failures in a row before escalating nudgeLimit: 3 # idle-without-committing reminders before escalating gateRunLimit: 3 # gate runs one commit may consume without getting further
review: enabled: true requireClean: true # whether `changes_requested` BLOCKS ready blockAtSeverity: low # low | medium | high | blocking — what PRODUCES it # engine: codex # unset means the task's own engine # model: gpt-6.1-terra # that engine's vocabulary, not the task's # effort: high
# fix: # what repair turns run on; unset means the task's own # model: opus # effort: maxEvery limit escalates with a machine-readable reason instead of trying once more, and the app offers the safe actions — retry with a fresh budget, pause, or take over in the chat.
Retry grants a budget, not an agent. It clears the escalation, resets the counters and starts the runtime clock again — which is everything a task needs if there is an agent to drive. On a task whose agent is gone and cannot be brought back there is not, and the loop then waits for an idle agent that is never going to appear: it moves to testing and does nothing. That also ends the gating and reporting the escalation was doing, since judging a commit through an escalation is something only an escalated loop does. So on such a task Retry is the wrong button — read what the loop reported, then merge the work or stop the task. Both the escalation copy and the app’s card say so where it applies.
An escalated task is resumed by Retry, or by messaging the agent. A message from a paired device is a person acting on the task, so it clears the escalation and grants the same fresh budget Retry does — including restarting maxRuntimeMinutes and resetting the counters, because a limit forgiven without its counter would escalate again on the next tick. Answering a question the agent asked counts too, and behaves identically. Either way, an agent whose process has exited — which every manager restart does — is resumed with its conversation intact before what you said is delivered, so a restart while a task sat escalated costs you nothing. The exception is a task where there is no agent left to bring back — no conversation on disk, or an engine since removed from the config, which is what agent_unresumable is raised for. The manager decides that, and its escalation copy is derived from the same conditions sendMessage is refused on, plus the escalation code once a resume has actually been tried and failed. That is a narrower promise than it sounds and worth stating as such: nothing can know in advance that a respawn will succeed — a binary upgraded underneath it will still fail at the moment you press send — so what the copy guarantees is that it does not recommend the channel where the manager can already tell, and that a resume watched to fail stops it recommending the channel for as long as the agent stays gone. Only for as long: that code is durable and a successful resume does not clear it — the manager resumes every live task on each restart without consulting the loop — so fixing the binary that made a respawn throw and restarting leaves a live agent under a stale agent_unresumable. A live agent disproves the code, and everything that reads it treats that as decisive: the escalation copy, the withheld report, the app’s card and the chat composer all offer the channel again. With no agent there, the card offers only Retry and the composer says why. Where the app is optimistic and the manager refuses anyway, you get the manager’s own reason in the error bar rather than a field that silently does nothing. Pausing is left alone: a message says what you want, not “work on this now”. Until a task is resumed, the loop drives the agent no further — nothing is handed back, nudged, or charged to its budget — but it still gates and reviews any commit that appears and reports what it found on the task, because measuring is not driving and the result is exactly what you were summoned to look at.
Pausing an escalated task stops the measuring too. A paused loop takes no action of any kind, which is checked before anything else — so it does not gate, review or report on a new commit either. That is the one case where an escalated task goes genuinely quiet, and the app’s escalation card says so rather than promising a verdict that is not coming. Resume it to have commits judged again while you decide what to do.
maxRuntimeMinutes is wall clock with exactly one thing taken out: the time at pr_open. Stated that way round on purpose. It is tempting to call it “the minutes the task spends working”, and that is an over-claim — the honest description of a limit is the list of what it does not count, because that list is checkable and “working” is not.
What it excludes is the wait for your review. The clock stops when the task reaches pr_open and starts again only if the task goes back to running — which happens when the pull request is closed or deleted while its agent is still alive. Work the loop does at pr_open itself, such as answering review findings, is outside the budget too, and the limit is only checked while the task is running. So the hours a finished pull request spends in front of a person cost it nothing: a team that reviews in the morning does not come back to a wall of escalated tasks that each finished in forty minutes. If you want an unreviewed pull request to time out, that is a different limit and this is not it.
That is the only exclusion, and the notable thing it leaves in is the wait for you. A task stopped on a question stays running, so the minutes it spends waiting for your answer are billed against this limit and counted in the escalation’s “the loop has run for N minutes”. Fixing that is #86 and needs the clock changed, not the sentence; until then this is what the number means.
A task that goes quiet is reported as quiet, wherever it is. The limit above bounds work, and the other limits are counters — so neither catches an agent whose turn simply hangs, because nothing happens and no counter moves. That case is reported rather than escalated: task.staleMinutes without a report, a commit or a worktree change marks the task silent and moves it to the top of the dashboard. The window is counted from the moment the last turn began, not from whatever happened before it, so a turn resumed on a pull request that waited four hours is not reported silent a second into it.
The rule differs by where the task is, and the difference is the point. While the task is running it applies to the task, whatever the agent is doing — an agent that went idle and was never driven again is as silent as one mid-turn, and both are waiting on nobody. At pr_open it applies only while a turn is in flight, which is how a hung repair turn is caught after the pull request is already open. A pull request merely waiting for a reviewer is never silent: there is no turn to be quiet, and badging every unreviewed pull request would be the same false alarm in a quieter voice.
gateRunLimit is the one that catches a gate command misbehaving. A gate run that stops part-way — the worktree changed underneath the command, so its exit code describes a tree no commit names — leaves the commit unjudged, and the loop answers that the only honest way: run the gates that are missing. Right for a one-off, such as the agent committing mid-run. Never-ending for a cause that reproduces: a verify command that writes into the worktree — a coverage directory, a generated file, a toolchain cache — discards the gate it is running on every single attempt.
What it counts is runs that got nowhere. A run interrupted after judging a gate the one before it had not is converging, and hands the budget back rather than spending it, so a repository being worked through one gate at a time is not cut off by this. A new commit hands it back too. Spending it escalates gates_not_completing, instead of leaving you with a task that quietly re-ran your whole suite until the runtime ceiling three hours later. The message says what was observed and offers the likeliest cause rather than naming one it cannot see; if you get it, the fix is usually in the repository rather than in this file — have the command clean up after itself, or .gitignore what it writes.
A finding at medium or above blocks, by default. blockAtSeverity is the severity at which a finding produces changes_requested. The shipped value is medium: blocking, high and medium stop the work and are handed back to the agent, and low is recorded without stopping it. The verdict is computed from the findings rather than taken from the reviewer, so a model that answers pass while reporting a medium correctness problem does not get to decide; the findings do. blocked is the exception and is honoured as written, because it is not a claim about the findings — it is the reviewer saying it cannot judge, which escalates to you.
low is left non-blocking on purpose. A reviewer with no way to note something without costing a repair cycle stops noting things, and the observations you lose are the ones worth having cheaply: a name that reads wrong, a type that could be narrower, an assumption that holds today. They are not dropped — the verdict comment on the pull request carries every finding at any severity — so you read them where you review and ask for the ones you want. Set low if you would rather every finding stop the line.
Read as a threshold the severities run backwards — low, medium, high, blocking, strictest first — because a lower threshold catches more. high leaves medium and low recorded and acted on by nobody; blocking is the laxest setting there is. Whatever you choose is what the reviewer is told in its prompt and what the hand-back quotes back to the agent, so the rule the review is judged by and the rule it was given are one sentence written in one place.
blockAtSeverity and requireClean are orthogonal and compose: this one decides what produces changes_requested, that one decides whether changes_requested blocks ready.
Who reviews is a separate choice from who implements. review.engine unset means the task’s own engine; naming another is cross-engine review — a different vendor checking the work, which is worth what it costs on anything you would not merge unread. When it is set, review.model and review.effort belong to that engine’s vocabulary, not the task’s: model names are engine-scoped, so a Claude task reviewed by Codex gets Codex’s own default rather than a claude-opus-5-5 Codex has never heard of. A model that does not fit the engine it lands on is refused with a note in the task’s timeline rather than silently used.
fix is what repair turns run on, and it deliberately has no engine key. A repair resumes the implementer’s own conversation, and a conversation cannot change vendors — so the option that cannot work is absent rather than refused. Model and effort are honoured as soon as the engine’s process model allows: Codex retargets its next turn, Claude keeps the model it was launched with until the task’s agent is restarted.
Both blocks are deployment-wide defaults. A single task can override either from the create form, and what it overrides is validated against the engine it will actually run on.
The quality gates themselves — the commands the manager runs and judges, as opposed to loop, which is what it does with the verdicts.
gates: enabled: true # run and judge verifyCommands at all publishStatus: false # write a commit status to GitHub for each gateenabled: false turns gates off for the whole deployment. A looping task then behaves exactly like one in a repository that declares no verifyCommands: it goes to the review if you run one, and otherwise reaches ready on commit. Nothing is silently marked green — there is simply nothing being judged, which is what you asked for. The manual POST /tasks/:id/gates/run still refuses while gates are disabled, so a run cannot be started by the back door either.
publishStatus: true writes a commit status to GitHub for every gate, which is visible on the pull request and to anything else watching the branch. It is off by default deliberately: it is an outbound side effect on your repository, and it should be something you turn on rather than something you discover.
repoConfig
Section titled “repoConfig”Whether a repository’s own .agentarivm.yml is read at all, and how far it is allowed to go:
repoConfig: enabled: true # read .agentarivm.yml from each repository commandPolicy: append # append | replace provisionTimeoutSeconds: 1800 # clone-time toolchain installs are slowA repository’s file can tighten the loop — fewer repair cycles, less runtime, review where you run none — and can never loosen it. Anything above your ceiling is clamped, and the clamp is announced in the task timeline rather than applied in silence.
What a repository may say about the review, alongside its quality.commands and setup blocks, in its own .agentarivm.yml:
review: enabled: true # may be turned ON where you run none, never off require_clean_review: true block_at_severity: low # low | medium | high | blocking (deployment default: medium)block_at_severity follows the same one-directional rule, in the one direction severity reads backwards: a lower threshold blocks more findings, so lowering it is tightening and is granted, and asking to raise it is clamped with a note. With the shipped default at medium, low is a repository’s to take — a codebase that wants every finding to stop the line says so here — and nothing above medium is, which is what stops a repository opting out of being reviewed.
commandPolicy is the same rule for the two lists that name commands, quality.commands and setup.worktree:
append(the default) — your profile’sverifyCommandsalways run, and the repository’s are added to them. A command both sides name runs once. Whichever component the repository’s file speaks for gets a line in the timeline saying what was added.replace— the repository’s list wins outright. This lets any repository you add set its gates to something that always passes and have every commit it produces verified by nothing, so it is a statement that every repository on this machine is as trusted as you are.
Prefer granting it to the repositories that have earned it:
projects: owner/repo: repoCommandPolicy: replaceenabled: false is the blunt veto: no repository file is read, and every project is governed by your profiles alone. It also discards the per-repository setup and the tightened limits, which is usually the reason to have the overlay in the first place — commandPolicy exists so that containing the dangerous half does not cost you the useful half.
A repository without one
Section titled “A repository without one”Most repositories were not written for Agentarivm and have no .agentarivm.yml. Added with a profile whose verifyCommands is empty — none, the only profile a fresh install has — such a repository has no gates: the loop goes straight to the independent review, which reads the diff and does not run anything, so work reaches ready without having been built or tested.
The app says so. Before you confirm an add, the sheet reads the repository’s file from GitHub and, when neither it nor the chosen profile configures a check, says the work will be reviewed but not built or tested. The project page says the same for a repository already added.
Next to the warning is the fix: Add & propose an .agentarivm.yml in the sheet, Propose an .agentarivm.yml on the project page. Either starts an ordinary task whose brief is the manager’s own: inspect the repository, write .agentarivm.yml with its setup.worktree and quality.commands, run those commands in the task’s worktree to show they pass, and open a pull request listing each one and what it printed. Under the loop the manager runs the proposed quality.commands itself on every commit, as it would any gate. setup.provision is left out, because installing a toolchain is yours to decide.
Nothing in that request names a command. The agent writes the file and, like any agent’s work, it runs those commands in its own worktree; under the loop they also run as that pull request’s gates. The file reaches your default branch only when you merge the pull request, and only then does it govern other tasks. The manager reads the file again after every merge, so the project page follows without a refetch, and the next task’s gates come from the file — added to your profile’s under commandPolicy: append, never in place of them.
How often the manager re-reads reality — the trade between a responsive timeline and a machine that spends its day forking git and gh:
poll: enabled: true activeSeconds: 5 # a task that changed something recently idleSeconds: 30 # running but quiet pullRequestSeconds: 60 # waiting on review githubSeconds: 30 # floor between GitHub calls per taskretention
Section titled “retention”What a daemon that runs for years is allowed to keep:
retention: enabled: true taskHistoryDays: 365 # history kept, counted from when the task ENDED sweepHours: 24 logMaxMegabytes: 64Cleaning up a task removes its worktree — the part you can see. Its timeline and its full agent transcript used to stay in the database for good, so “I cleaned that up” and the disk disagreed permanently.
taskHistoryDays counts from when the task ended, not from when its worktree went. A merged task is cleaned up automatically, and dating the window from the cleanup would tie a transcript’s life to when a machine happened to run. Nothing outside cleaned_up is touched: while you still have the worktree you still have the whole record, so a stopped or failed task keeps everything until you tidy it up.
Upgrading re-dates existing history: the window used to run from the cleanup and now runs from the ending, which is earlier. With the default of a year that changes nothing you will notice, but if you have set a short window explicitly, the first sweep after upgrading prunes more than the last one did.
A year is deliberate, not inherited. The disk problem this exists for is worktrees — hundreds of megabytes each — and a transcript is well under a megabyte, so a hundred tasks of conversation is about 60 MB. The worktree can be rebuilt from the base branch; the transcript is the only record of what the agent was told, what it tried and what the reviewer said.
The sweep also runs wal_checkpoint(TRUNCATE), which is what actually hands the write-ahead log’s space back — SQLite checkpoints on its own, but into the same file, so it never shrinks.
logMaxMegabytes bounds manager.log, which nothing on macOS will bound for you. Over the cap, it is copied to manager.log.1 and truncated in place — never renamed, because launchd opens that file once and holds the descriptor for the life of the job, so a rename would leave the daemon writing into an unlinked inode and the visible log empty until the next restart.
agentarivm doctor reports both sizes, so growth is visible before it is a problem.
Notifications reach your phone when work stops for you — a question the agent is waiting on, a loop that gave up, a task that failed, a pull request that is ready to merge. Nothing else buzzes. They are on by default and need nothing configured:
push: enabled: true # false: nothing is sent off this machine for notificationsApple delivers a notification to the Agentarivm app only when it comes from the app’s publisher, so the manager hands each one to the Agentarivm push relay, which passes it to Apple. It sends as the account agentarivm login signed in — a signed-out manager sends nothing, and the app is told so rather than asking for a permission it cannot use.
The relay cannot read them. The app generates a key when it registers, on its own encrypted connection to your manager, and every notification is encrypted to that key before it leaves your machine. The relay and Apple see which kind of thing happened and nothing else; the phone opens it and shows the agent’s own words, and a tap opens the task. See Security.
agentarivm doctor reports whether notifications can be sent and how many paired phones receive the full text.
sandbox
Section titled “sandbox”Whether the operating system, rather than good intentions, keeps an agent away from the manager’s own credentials:
sandbox: mode: auto # auto | required | offauto is the default: confine agents and repository-declared commands wherever that is possible, and report it wherever it is not. That means a Seatbelt profile on macOS and bubblewrap on Linux — where bwrap is not installed by default, so apt install bubblewrap may be a prerequisite. See the security model for exactly what is denied, and for why the manager proves the sandbox on each machine instead of trusting the platform.
required refuses to start rather than run agents unconfined. It is the right setting on any machine that also holds something worth protecting, because it turns a silent gap into a startup error you cannot miss.
off exists for diagnosing a build that the sandbox broke. It should not survive the diagnosis.
Whatever the setting, agentarivm doctor prints a sandbox line and raises a problem when nothing is being enforced. Read it before you decide which machine — or which user account — this runs as.
setupProfiles and projects
Section titled “setupProfiles and projects”A setup profile prepares a worktree before the agent starts:
setupProfiles: none: {} # must exist; the default for new projects flutter: copyFiles: [.env] # copied from secretsRoot/<owner>/<repo>/ into the worktree setupCommands: - [flutter, pub, get] verifyCommands: # the definition of done, written into the prompt - [flutter, analyze] - [flutter, test] timeoutSeconds: 600Commands are argument arrays, never shell strings — nothing here is ever passed to sh -c.
timeoutSeconds bounds each setup and verify command. A gate that runs past it is killed — along with anything it spawned — and recorded as a failed gate, not a broken machine: a test suite that deadlocks is the agent’s to fix, so the loop hands it back with whatever the command had printed before it stopped.
The projects: block refines repositories already registered (or clones them at boot with bootstrap: true):
projects: owner/repo: displayName: My App components: - id: app dir: apps/mobile profile: flutter