| title | Workspaces (Multi-Repo Fleet) |
|---|---|
| description | Drive many repositories with one devintern worker: a single workspace.toml, routing rules, and per-task worktrees |
| section | Server Automation |
| order | 1 |
| dateModified | 2026-08-28 |
Workspace mode lets one devintern worker process serve every repository you automate. Instead of one worker per repo, you describe your repos once in ~/.devintern/workspace.toml, point the worker at one tracker query, and route each ready task to the right repository with explicit rules when there is more than one repo.
The shortest path is devintern worker init inside a checkout: that writes a 1-repo workspace (import + [defaults].task_query) and you add more repos later with devintern workspace import.
Workspace mode runs under the same automation license as the rest of the worker: any Supporter, Team, or Business key (or an active trial) covers it — one license spans all of your own repos in the fleet.
- The worker polls your tracker with one fleet-wide query (a detect-then-evaluate loop with one cursor).
- Each ready task is matched against your routing rules. A task runs only when the rules agree on exactly one repository. The worker never guesses: tasks that match no rule, or rules for different repositories, are skipped and recorded, and are retried only after the task changes again. A 1-repo workspace needs no routing rules — N=1 already implies the only checkout (
devintern worker initstarts this way). - The worker manages a bare clone of each repository under
~/.devintern/repos/and runs every task in a fresh, disposable worktree under~/.devintern/worktrees/. Your own checkouts are never touched. Worktrees are removed after a successful run, kept for debugging when a run fails, and swept afterworktrees_ttl_days— at worker startup and then hourly while the worker runs. - All worker state (queue, cursors, agent PR registry, run records, routing skips) lives in one database at
~/.devintern/state/queue.db. - Runs are serialized: one task at a time, with a per-repository lock. One systemd unit (or one terminal) drives the whole fleet.
[workspace]
worktrees_ttl_days = 7
dashboard = true
# dashboard_port = 4400
# Batch automatic conflict resolution off-peak instead of instant (default "auto"):
# conflict_resolution = "scheduled"
# conflict_resolution_cron = "0 3 * * *" # worker host timezone
# conflict_resolution_interval = "1d" # exactly one of cron / interval
# Or turn it off entirely: conflict_resolution = "disabled"
[defaults]
tracker = "jira"
task_query = "sprint in openSprints() AND labels = devintern"
worker_task_args = "--create-pr"
poll_interval = 60
default_branch = "main"
# pr_labels = ["devintern", "auto-pr"]
[[repos]]
name = "backend"
remote = "git@github.com:acme/backend.git"
default_branch = "main"
# pr_labels = ["backend"]
# env_file = "env/backend.env" # optional, relative to ~/.devintern
[repos.env] # optional per-repo overrides
GITHUB_REPO = "acme/backend"
[[repos]]
name = "frontend"
remote = "git@github.com:acme/frontend.git"
[[routing.rules]]
repo = "backend"
project = "BACK"
[[routing.rules]]
repo = "frontend"
project = "WEB"
labels = ["frontend"]
[worker.schedule]
active = ["22:00-06:00"] # optional quiet hours: drain new tasks only at night
blocked = [] # subtract from active windows (conflicts resolve to quiet)
timezone = "" # blank = worker machine's local time
catch_up_missed = true
[[automations]]
id = "backend-maintenance"
enabled = true
interval = "6h"
repo = "backend"
prompt = "Inspect the backend and implement one safe maintenance improvement."
[[automations]]
id = "weekly-frontend-cleanup"
enabled = true
cron = "0 9 * * 1"
repo = "frontend"
prompt = "Review the frontend and clean up one source of recurring noise."[defaults].trackerpicks the tracker for the fleet query; any tracker with polling support works (Jira, Linear, GitHub Issues, Azure DevOps, Asana, Trello, Markdown).pr_labelsapplies labels to every PR the fleet creates (GitHub only). A repo'spr_labelsoverrides[defaults].pr_labels. Outside a workspace, single-repo users get the same behavior by settingPR_LABELS(comma-separated) in.devintern-code/.env.- Repo names must be unique and filesystem-safe; they become directory names under
repos/andworktrees/. - Rule criteria combine with AND; list values (
components,labels) match when the task carries any of them. Comparisons are case-insensitive.projectmatches the task key prefix forPROJ-123style keys (Jira, Linear); trackers with numeric or opaque ids route via labels or components. [worker.schedule]gates only new-task pickup: multiple windows union, windows may cross midnight,blockedwins on overlap, and a missed whole window triggers one catch-up drain at startup. Timezone/DST semantics anddevintern worker run-noware covered in Automated Task Processing → Working windows.[[automations]]uses the same schema as single-repo.devintern-code/automations.toml. An entry must namerepowhen the workspace has more than one repository. See Worker Daemon → Recurring automations for prompt-writing guidance and schedule semantics.[[estimations]]schedules unattended story-point sweeps (tracker query + cron/interval, noprompt, norepo). The workspace tracker must support estimation. See Worker Daemon → Scheduled story-point estimation.
When a watched PR conflicts with its base branch, the worker normally resolves it right away (conflict_resolution = "auto", the default — no behavior change on upgrade). Every resolution hands the conflicted files to the AI agent, which consumes tokens — even at 3am when nobody is reviewing the PR anyway.
Set conflict_resolution = "scheduled" to batch those resolutions into an off-peak window. Polling still detects every conflict immediately and queues it (the PR stays conflicted until then, and the worker logs which mode is active at startup); the agent only runs inside the window:
[workspace]
conflict_resolution = "scheduled"
conflict_resolution_cron = "0 3 * * *" # or conflict_resolution_interval = "1d"The schedule uses the same format as [[automations]]: a five-field cron expression (worker host timezone) or a positive 15m/6h/1d interval — exactly one of the two. Exactly one window pass runs per occurrence; if the worker is down when the window arrives (a missed nightly run), the queued conflicts resolve on the first tick after restart. Inside a window the usual safety rules still apply: failed attempts wait out their retry backoff, PRs whose head is still moving wait out the quiet period, and anything not finished before the window closes (60 minutes by default, WORKER_RESOLVE_WINDOW_GRACE_MINUTES) waits for the next one. PRs merged upstream before the window opens are skipped — the worker re-checks GitHub's mergeability before invoking the agent.
Two things are never delayed by scheduled mode: review feedback on the agent's PRs is addressed immediately as usual, and you can always run devintern resolve-conflicts <pr-url> by hand to fix one PR without waiting for the window — once GitHub reports the PR conflict-free, the queued event never triggers an agent run.
The setting is workspace-wide (per-repo overrides are not supported in v1) and live-reloads with the rest of the runtime configuration. The tradeoff to keep in mind: between windows a conflicted PR cannot be merged, so on fast-moving branches where an instant rebase unblocks a waiting reviewer, auto stays the better choice. See Worker Daemon → Merge conflicts on the agent's PRs for how resolution itself works.
Set conflict_resolution = "disabled" to turn automatic conflict resolution off entirely: the worker stops watching for conflicts on the agent's PRs altogether — no detection, no queuing, no agent runs. A PR that conflicts with its base simply stays conflicted until someone resolves it (by hand, or on demand via devintern resolve-conflicts <pr-url>). Review feedback and @mention handling are unaffected. This is a valid choice when the team prefers to rebase manually, or when the agent is not trusted to resolve conflicts in a sensitive repository.
The scheduling is identical; only where the work runs changes:
- Each occurrence runs in the repo's persistent base worktree (
~/.devintern/worktrees/<repo>/base) with the same layered environment as review work: shared.env→ repoenv_file→[repos.env]. - It takes the normal per-repo run lock, so it never mutates a checkout concurrently with a task or PR run.
- Occurrence task files land under the workspace home (
~/.devintern/automations/<id>/), next torepos/,worktrees/, and the central database — not inside the repo worktrees.
devintern workspace init # scaffold ~/.devintern/workspace.toml and .env
cd ~/code/backend
devintern workspace import # add this repo to the workspace
cd ~/code/frontend
devintern workspace importworkspace import reads the repo's origin remote and its .devintern-code/.env:
- The remote becomes a
[[repos]]entry (name derived from the remote, unique and filesystem-safe; default branch fromorigin/HEADwhen it differs from the workspace default). - Env keys the workspace does not have yet are merged into the shared
.env. Values that conflict with the workspace.envare kept repo-local in that repo's[repos.env]; nothing is silently overwritten. - When the repo's env carries a default project key (Jira or Linear), a starter routing rule is seeded for it.
- Re-running import for the same repo is a no-op. Hand-written comments in
workspace.tomlare preserved; new entries are appended. .devintern-code/settings.jsonneeds no migration: it travels with the repo and applies inside each task worktree.
Secrets live in one shared ~/.devintern/.env (tracker credentials, GITHUB_TOKEN and/or GitHub App credentials, agent settings). Each repo can layer more on top:
- Shared workspace
.env - The repo's
env_file(if set) - Inline
[repos.env]values (highest precedence)
For GitHub remotes the worker fills GITHUB_REPO automatically from the remote URL.
devintern worker # auto-detects ~/.devintern/workspace.toml
devintern worker --workspace /path/to/workspace.tomlThe fleet query comes from [defaults].task_query. A workspace with automations or estimations can omit the query and run as a schedules-only worker. Poll interval, per-task flags, and the embedded dashboard are also set in workspace.toml (poll_interval, worker_task_args, [worker.schedule] quiet hours, [workspace].dashboard / dashboard_port). Direct webhooks are an advanced repo-local service: run devintern webhook serve from that repository as a separate process. Automation and estimation schedule state and leases, plus the task-polling timestamp used for missed-window catch-up, live in the central workspace database.
While the daemon is running you can request one immediate drain (for example while quiet hours are closed) with devintern worker run-now; see Working windows.
The worker watches workspace.toml and reloads it automatically a moment after you save — no restart, and no missed tracker events or relay messages during the bounce:
- Routing rules, repos,
task_query,[[automations]],[[estimations]],worker_task_args,poll_interval,worktrees_ttl_days, and conflict-resolution mode/schedules apply to subsequent work. Runs already in progress finish under the configuration they started with; everything picked up afterwards uses the new one. Changing a repo'sremoteupdates its managed bare clone the next time that repo is prepared. - A broken edit never takes the daemon down. The reload validates the file first; parse or schema errors are logged (naming the offending entries) and the last valid configuration keeps serving until you fix it. Rewriting identical content is ignored.
- Manual fallback: send SIGHUP (
kill -HUP <pid>) to force an immediate reload if file watching is unavailable on your system. - Startup-only settings still require a restart: tracker credentials in the workspace
.envand[defaults].tracker(the tracker client and its detector are built once),[worker.schedule]quiet hours (the working-window gate is built once at startup), plus[workspace].dashboard/dashboard_port. A reload that changes one of these settings is rejected in full, so the active config remains internally consistent.
devintern worker init can generate a user-level systemd unit on Linux or launchd agent on macOS. One service runs the whole workspace. For a hand-written Linux unit:
[Unit]
Description=DevIntern fleet worker
After=network-online.target
[Service]
ExecStart=/usr/local/bin/devintern worker
Restart=on-failure
WorkingDirectory=/home/you/.devintern
[Install]
WantedBy=multi-user.targetWith GitHub credentials in the workspace .env, the fleet worker also reacts to PR activity across every GitHub repo in the workspace:
- The agent's own PRs: one poller watches every PR the fleet created (the registry is shared across repos) and addresses actionable review feedback automatically. Entries for repos no longer in
workspace.tomlare unwatched at startup. - @mentions on any PR: each GitHub repo gets a mention sweep. Mention-triggered runs are permission gated: the mentioning user needs write, maintain, or admin access, and the gate fails closed on API errors. Fork PRs are skipped unless maintainer edits are allowed. Mention matching needs the GitHub App (
GITHUB_APP_ID+ private key);worker initoffers to install and connect it whenever the repo has a GitHub remote. - Relay (instant events): accept relay setup in
devintern worker init; its durable pairing is stored under the workspace home and starts automatically with the worker. Relay envelopes carry the repository, so events route to the right repo automatically; task events re-run the fleet query and go through the same routing rules. Tracker relay events work even when GitHub polling credentials are not configured. Events for repositories not in the workspace are ignored.
Review and mention runs execute as subprocesses in the repo's persistent base checkout under ~/.devintern/worktrees/<repo>/base, with the same layered environment as task runs.
Ambiguous and unmatched tasks are recorded in the workspace database with the rules that matched. Fix the routing rules (or the task's labels), touch the task, and the worker picks it up on the next change. Skips never loop: a skipped task is not retried until it changes.