skip to content
$worker

worktree

v0.1.1

Git worktree lifecycle for parallel agents — mint, claim, and track isolated worktrees per repo, emit lifecycle trigger types, and land branches back through a per-repo FIFO queue (rebase, test gate, ff-only merge).

iiiverified
3 installs0 in 7d0 today
install
$iii worker add worktree@0.1.1
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64 · x86

exact versions are immutable; binary and bundle artifacts are digest-pinned.

README.md

worktree

Git worktree lifecycle for parallel agents. worktree::create mints an isolated, locked worktree off any ref and registers it in a cross-agent registry; agents work inside it through their turn's filesystem scope (metadata.fs_scope.root); worktree::land rebases the branch onto a target, runs an optional test gate through the shell worker, fast-forwards the target with an atomic compare-and-swap, and cleans up. Every lifecycle change emits a trigger type sibling workers can subscribe to, so a Slack bot can announce lands or a harness can react when a sibling's branch merges.

Install

iii worker add worktree

iii worker add fetches the binary, writes a config block into ~/.iii/config.yaml, and the engine starts the worker on the next iii boot. The land test gate delegates to shell::exec, so install the shell worker alongside it:

iii worker add shell

Quickstart

use iii_sdk::{register_worker, InitOptions};
use iii_sdk::protocol::TriggerRequest;
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let iii = register_worker("ws://localhost:49134", InitOptions::default());

    // 1. Mint an isolated worktree and hand its path to an agent.
    let wt = iii.trigger(TriggerRequest {
        function_id: "worktree::create".into(),
        payload: json!({ "repo_path": "/home/me/project", "session_id": "sess_1" }),
        action: None,
        timeout_ms: Some(30_000),
    }).await?;
    println!("agent fs_scope root: {}", wt["path"]);

    // 2. ... the agent commits work inside the worktree ...

    // 3. Land it: rebase onto main, gate on tests, ff-merge, clean up.
    let job = iii.trigger(TriggerRequest {
        function_id: "worktree::land".into(),
        payload: json!({
            "worktree_id": wt["worktree_id"],
            "target_branch": "main",
            "test_cmd": "cargo test",
        }),
        action: None,
        timeout_ms: Some(30_000),
    }).await?;
    println!("queued land job: {}", job["job_id"]);
    Ok(())
}

The land completes asynchronously; subscribe to worktree::landed / worktree::land-blocked (below) or poll worktree::get for the outcome.

Configuration

Runtime settings live in the configuration worker under id worktree and hot-reload on change (a prune_schedule change re-binds the cron live).

worktree_root: "~/.iii/worktrees"   # where managed worktrees are created
branch_prefix: "iii/"               # auto-minted branch names
prune_schedule: "0 0 * * * *"       # six-field cron for the reconcile sweep
prune_expire_hours: 72              # idle hours before a clean, unclaimed worktree is prunable
land_queue: "worktree-land"         # engine queue name for land jobs
max_land_retries: 3                 # rebase retries when the target branch moves mid-land
git_timeout_ms: 60000               # per-git-subprocess bound
test_timeout_ms: 600000             # land test gate bound

Filesystem scope interplay

Agents are scoped through metadata.fs_scope.root on the turn: the console picker (or harness::send options) sets it to the worktree path, and the harness stamps fs_scope: { root, grants } onto every scoped shell::* / coder::* call the agent makes. The land test gate does the same directly: it runs shell::exec with the worktree as cwd and as the fs_scope root, so the shell worker enforces the scope for gated tests exactly as it does for agent calls.

worktree_root must still resolve inside the shell worker's fs.host_roots: a root outside the jail fails with S215 (a path inside the jail but outside the scope root fails with S220), and the test gate surfaces the S215 misconfiguration as a W411 land block naming it.

Engine queue config

Land jobs serialize per repository through an engine FIFO queue. Add this block to the engine config (the repo_key group field is carried on every job payload):

queue_configs:
  worktree-land:
    type: fifo
    message_group_field: repo_key
    concurrency: 1
    max_retries: 2

Without it the queue still delivers, but ordering falls back to this worker's in-process per-repo lock, which only holds within a single instance.

Functions

  • worktree::create — mint a locked worktree off a ref; auto-claims when session_id is given.
  • worktree::list — registry view with filters and optional git status; reports unmanaged worktrees of a repo without adopting them.
  • worktree::get — one worktree with status.
  • worktree::validate — picker-style path validation; reconciles records whose directories are gone.
  • worktree::claim / worktree::release — session ownership with force takeover and W210/W211 mismatch errors.
  • worktree::status — clean, ahead/behind, staged/unstaged/untracked, diffstat, rebase-in-progress.
  • worktree::remove — guarded removal (W220 dirty, W221 unmerged), optional branch deletion.
  • worktree::prune — reconcile sweep, cron-bound; {} payload works.
  • worktree::land — queue a land job; returns { job_id, queued }.
  • worktree::land-step — internal queue consumer; not agent-callable.

Errors carry stable W### codes (src/error.rs). One instance per engine: iii-state has no compare-and-set, so run a single worktree worker (shard by repo if you need more).

Custom trigger types

Trigger type Fires when Payload highlights
worktree::created A worktree was minted worktree_id, path, branch, base_sha
worktree::claimed A session took ownership session_id, previous_session_id
worktree::released A claim was released session_id
worktree::removed A worktree was removed or pruned branch_deleted
worktree::landed A land fast-forwarded the target target_branch, merged_sha
worktree::land-blocked A land ended blocked reason, code, conflict_files, exit_code

Bindings accept optional equality filters: repo_path, worktree_id, session_id.

use iii_sdk::protocol::RegisterTriggerInput;

iii.register_trigger(RegisterTriggerInput {
    trigger_type: "worktree::landed".to_string(),
    function_id: "notify::on-land".to_string(),
    config: serde_json::json!({ "repo_path": "/home/me/project" }),
    metadata: None,
})?;

Conflict resolution flow

When a land's rebase hits conflicts, the rebase is left IN PROGRESS inside the worktree and a worktree::land-blocked event fires with reason: "rebase_conflict" and the conflicted paths. The owning agent resolves in place (its filesystem scope already roots at the worktree), finishes with git rebase --continue, and reruns worktree::land. A rerun while the rebase is still unresolved returns W402; pass force_restart: true to abort the rebase, reset, and start the land over.

HTTP endpoints

With the http worker (or any provider of the http trigger type) installed, the read surface is also served over HTTP: worktree/list, worktree/get, worktree/status, and worktree/validate (POST, JSON bodies matching the function schemas). Registration is best-effort; without an http provider the worker logs a warning and keeps serving the bus.

Local development and testing

cargo run --release -- --url ws://127.0.0.1:49134
cargo test

The end-to-end test boots a real engine and self-skips when iii is not on PATH. Regenerate wire-schema goldens with UPDATE_GOLDENS=1 cargo test.