Skip to content
strandline / docs

Documentation

Strandline is the integration layer for parallel coding agents: it keeps them out of each other's way while they work, merges what they produce, proves the combination works, repairs it when it does not, and lands exactly what it verified.

Quickstart

Five minutes, one repository, no account. Strandline works locally first; the hosted service adds shared state across machines, durable merge workflows and landing through your merge queue.

preview

The CLI is in private preview for design partners. Request access to get the install token and release feed.

01Install

From a release:

# macOS / Linux
brew install strandline-dev/tap/strandline

# any platform; verifies the checksum (and signature, with cosign)
curl -fsSL https://github.com/strandline-dev/strandline/releases/latest/download/install.sh | sh

# the MCP server alone (`strandline-mcp`)
npm install -g @strandline-dev/mcp

Release archives (macOS and Linux, arm64 and x86_64) come with signed checksums and SLSA provenance; server images are on GHCR (see Self-hosting).

From source (Rust stable, Node.js 20 or newer for the MCP server):

git clone https://github.com/strandline-dev/strandline && cd strandline
make setup build                              # builds target/debug/strandline and the MCP server
cargo install --path crates/strandline-cli    # puts `strandline` on your PATH

strandline doctor checks a setup and tells you how to fix what is missing.

02Set up a repository

cd your-repo
strandline init
git add .gitattributes .strandline && git commit -m "Set up Strandline"

strandline init installs the git merge driver for code, manifests (package.json, Cargo.toml, go.mod, pyproject.toml, Gemfile, pom.xml, …), lockfiles (regenerated, never text-merged) and line-set files (.gitignore, CODEOWNERS), plus git hooks. It is local to the repository and changes nothing else. It also says which checks strandline verify will run:

  checks: strandline verify will run what it detects (`npm test --silent`); to change them, write
  .strandline/verify.toml and commit it

To pin them, write .strandline/verify.toml (see strandline verify):

[[check]]
name = "typecheck"
run = "npx tsc -p ."

[tests]
all = "npm test --silent"

03Connect your agents

strandline install --agent claude-code     # or codex, cursor, copilot, generic

This writes the agent's MCP configuration (.mcp.json, .codex/config.toml, .cursor/mcp.json, .vscode/mcp.json) and a managed block of instructions (CLAUDE.md, AGENTS.md, …), never touching your other servers or text. Each agent then has the MCP tools: it registers its task and scope, takes leases before editing hotspot files, hears about contract changes that affect it, and verifies before asking to merge.

strandline status shows the fleet at a glance: agents, leases, contract changes relevant to you, and (hosted) the merge queue.

04Verify a combination before merging

Two branches can each pass and still break when combined. strandline verify builds the combination in a scratch worktree (your checkout is untouched) and runs the checks on it:

$ strandline verify agent-a agent-b
merged   agent-a
merged   agent-b
FAIL  tests  (562 ms)  npm test --silent
alone    agent-a passes
alone    agent-b passes

Strandline combined `agent-a` and `agent-b` on top of `main` and the result fails `tests`.
Each branch passes on its own, so the failure comes from combining them.

Failing check `tests` (`npm test --silent`):
    AssertionError [ERR_ASSERTION]: 220 == 120

The conflicting changes:
- `agent-b` added `invoice` (src/invoice.js), which calls `tax`; `agent-a` changed what
  `tax` in src/pricing.js does (its signature is unchanged): it was `return a * 0.2;`
  and is now `return a * 1.2;`.

git merged these two branches without a conflict. strandline repair agent-a agent-b --provider … asks a repair provider for a fix, accepts it only if it passes the guard (no weakened tests, no protected files, no new network or process access) and the full test suite, and otherwise leaves it to you.

05Share across machines (hosted)

strandline login --url https://strandline.example.com     # paste an API key (or, where SSO is enforced, a session)
strandline whoami

Where a workspace enforces single sign-on, you sign in through your identity provider at /v1/sso/<workspace>/login and get a short-lived session; agents get their own keys, each bound to one agent. Coordination now goes through the service, so agents on different machines see each other's leases and contract changes.

Merge requests (strandline_request_merge from an agent, or a pull or merge request on GitHub or GitLab) run as durable workflows: verify, repair if needed, then land through your queue (GitHub's merge queue, Mergify, Graphite, GitLab merge trains, Strandline's own queue, or a direct merge), with a signed receipt on the PR. Running the service yourself: Self-hosting.

Concepts

Many agents (and people) change one repository at once. Strandline coordinates them while they work, merges what they produce, verifies the combination, repairs it when needed, and lands exactly what it verified.

The pieces

strandline CLIOne Rust binary: the index, the merge driver, coordination, verify and repair, setup and diagnostics. Works with no account.
strandline-mcpThe MCP server agents talk to. Every tool calls the CLI, so local and hosted mode behave the same (MCP tools).
Hosted servicestrandline-hosted serve: the API (Postgres), webhooks from GitHub and GitLab, billing. Holds metadata only, never your source.
Workerstrandline-worker: runs merge workflows durably (verify, repair, land) in a sandbox.

While agents work: coordination

  • Agent. One working copy (typically a git worktree) doing one task. Identity: --agent <id>, else STRANDLINE_AGENT_ID, else the current branch name. An agent is active if seen in the last STRANDLINE_AGENT_TTL (default 2 h); every command it runs refreshes last_seen.
  • Registration and scope. An agent registers its task and scope: the files and symbols predicted from its --task text plus explicit --scope paths. Scope is what makes the rest precise.
  • Base. The revision an agent's work is compared against: --base <rev>, else the registered base, else git merge-base HEAD main (falling back to master, then HEAD).
  • Index and hotspots. Strandline indexes symbols, imports and calls for TypeScript/JavaScript, Python, Go, Rust, Ruby and Java (tree-sitter). Files many others depend on are hotspots.
  • Leases (claims). An exclusive, time-limited claim by one agent on a path (file or directory prefix), default TTL 30 min. Another active agent asking for it is refused and told who holds it and why. Expired or released leases are inert.
  • Footprint. What an agent has changed relative to its base: changed files and changed symbols. Recomputed by contract publish and collisions.
  • Contracts. A contract change is a change to a public interface in an agent's working tree relative to its base: an exported symbol added, removed or with a changed signature (functions, methods of exported classes, classes, types, interfaces, enums, exported variables), or a route added or removed. Agents publish them to a shared contract log so others learn about them before merging.
  • Relevance. A change by agent A to symbol S in file F is relevant to agent B if F is among B's files of interest (the files it changed, its declared scope, and paths it holds live leases on), or one of those files imports F or calls S, or (routes) mentions the route path, or B's task mentions S. Only relevant changes are announced, over subscriptions/listen. Callers that already exist in the base are A's responsibility.
  • Retirement. A contract change leaves the current set once it reaches the integration branch (main, else master): the publishing branch was merged, or main already has the same signature (squash merge, cherry-pick). Retired changes are never announced again; contract changes --retired lists them with the reason.
  • Collisions and plans. strandline collisions shows whose changes or leases overlap with yours (severity symbol > file > lease > dependency); strandline plan forecasts collisions for a task list and suggests an order, splits and leases.

Locally, this state lives in the repository's git directory (one SQLite file shared by all worktrees, <git-common-dir>/strandline/state.sqlite, override STRANDLINE_STATE; WAL mode, safe for concurrent processes). Hosted, it lives in the service, keyed by the repository's root-commit SHA, so agents on different machines share it. Only metadata is sent.

Merge driver

strandline init makes Strandline git's merge driver. For each file:

  1. Code: the structural engine (merging by syntax units) and the optional pinned Weave engine, in the order measured best for each language, each falling back to the other; then, when both sides only added imports or module declarations at the same place, both are kept. Every clean result must pass the guard (no lost lines, no duplicated or orphaned declarations, still parses); otherwise it is a conflict, never a silently wrong merge.
  2. Manifests (package.json, Cargo.toml, go.mod, pyproject.toml, Gemfile, pom.xml, version catalogs) merge by key; lockfiles are regenerated by the package manager, never text-merged.
  3. Line sets (.gitignore, .gitattributes, CODEOWNERS) merge as unions, with order-sensitive rules kept as conflicts; generated files you list in .strandline/merge.toml are regenerated, not merged.

The guard is also available on its own (strandline guard) to judge a merged file whichever tool produced it, and strandline merge-check checks a whole merge for stale references to a name one branch renamed.

Before it lands: verification

A clean merge is not a working one. strandline verify <branches…> builds the combination in a scratch worktree and runs the checks from .strandline/verify.toml:

  • If it fails, each branch is run alone and so is the base, so the report can say whether the failure comes from combining them, from one branch, or was already there.
  • The failure is explained in terms of the changes: who changed what the failing code depends on.
  • Test selection runs only affected tests, using Nx, Turborepo, Bazel, Cargo/Go/npm/pnpm workspaces where present, else the import graph.
  • Flaky tests are rerun, recorded and quarantinable; a flake is never reported as a conflict and never triggers a repair.
  • Your code runs in a sandbox: containers with no network, a read-only root, resource and time limits, or a microVM.

When it fails: repair

strandline repair asks a provider (Claude, a command, or a scripted mock) for a fix to the combination. A fix is accepted only if it passes the guard (it may not weaken or delete tests, touch protected files including every file a check command runs, special-case tested values, or add network, process, eval or environment access) and then the full test suite. At most a few attempts; otherwise a human gets the evidence.

Landing

A merge attempt (from an agent's strandline_request_merge, a GitHub pull request or a GitLab merge request) runs as a durable workflow: it survives worker crashes and never verifies, repairs or lands twice. It lands through your queue (GitHub's merge queue, Mergify, Graphite, GitLab merge trains) or Strandline's own. Batches and repairs land through a batch PR of exactly the commit that was verified. The PR gets a signed receipt (what was combined, checked, repaired, and the digest of the result); each member PR gets a note linking to it.

Hosted service

  • Workspaces, roles, keys. Owner, admin, member, agent; API keys with scopes (read, write, coordinate, merge, webhooks, admin); agent keys bound to one agent id.
  • Isolation. Postgres row-level security on every tenant table; the service connects as a non-owner role.
  • SSO. OIDC and SAML per workspace, with domain claims, just-in-time members and enforcement. An audit log of every privileged action and retention settings.
  • Integrations. GitHub App, GitLab, outgoing signed webhooks, a GitHub Action and GitLab CI component (strandline ci verify), an OpenAPI description of the API.
  • Billing. Landed, verified merges are metered; a free tier is enforced at admission.
  • Operations. Metrics, dashboards, alerts, SLOs, Helm and Compose installs: Self-hosting.

Security model and threat analysis, including prompt injection through code and comments: Security.

CLI reference

One binary, strandline. Every command below links to itself; the common conventions come first.

Conventions

OutputColour only on a terminal; NO_COLOR turns it off, CLICOLOR_FORCE=1 forces it.
--jsonEvery command that reports something takes --json and prints one JSON document on stdout. Exempt: merge-driver (git's driver protocol), mcp (the MCP protocol), completions (a script).
ErrorsTo stderr as error: <problem>, optional caused by: lines, and next: <what to run>; with --json, as {"error": {"message", "causes", "next"}}. Exit 1.
Exit codes0 ok; 1 error; 2 usage; 3 lease held by another agent; 4 the verified combination fails.
Completionsstrandline completions bash|zsh|fish|powershell|elvish

JSON shapes of the summary commands:

strandline status --json{mode, agent, repo, agents: [Agent], leases: [Lease], contract_changes: [ChangeView], merge_attempts: [MergeAttempt] | null, warnings: [string]}
strandline doctor --json{ok, fixed: [string], checks: [{id, title, level: pass|warn|fail, detail, fix}]}
strandline install --json{dry_run, agents: [{agent, scope, server: {command, args}, changes: [{path, kind, action, note}], docs}]}
strandline ci verify --json{system, branch, base, number, ok, combined, verification_id, verification}
strandline init --json{repo, extensions, notes}
strandline lock regen --json{regenerated, staged}; generated regen: {regenerated, ran, staged}

Setup and diagnostics

strandline init

#cli-init

Install Strandline as the git merge driver for supported languages in this repository, plus the hooks.

strandline init [OPTIONS]
--repoRepository (any path inside it). default .
--jsonPrint what was set up: {repo, extensions, notes}.
example
strandline init && git add .gitattributes .strandline

strandline doctor

#cli-doctor

Diagnose this setup: git, merge driver, hooks, config, toolchains, Weave, hosted service, MCP for your agents. Each problem comes with the command that fixes it.

strandline doctor [OPTIONS]
--repoRepository (any path inside the working tree). default .
--fixApply the safe fixes (strandline init), then check again.
--offlineDo not contact the service (skips the reachability and key checks).
--json{ok, fixed, checks: [{id, title, level, detail, fix}]}
example
strandline doctor --fix

strandline install

#cli-install

Configure a coding agent (Claude Code, Codex, Cursor, Copilot, generic) to use Strandline: its MCP server entry and agent instructions. Project files by default.

strandline install [OPTIONS] --agent <AGENTS>
--agentAgent(s) to configure (repeat, or comma-separate): claude-code, codex, cursor, copilot, generic.
--globalWrite your own configuration instead of the project's.
--dry-runShow what would change without writing anything.
--repoRepository root. default: the git repository of the current directory
--commandCommand that starts the server. default: strandline (project files); this binary's absolute path with --global
--jsonMachine-readable output.
example
strandline install --agent claude-code,cursor --dry-run

strandline status

#cli-status

The repository at a glance: the fleet, claims, contract changes for you, the queue.

strandline status [OPTIONS]
--repoRepository (any path inside the working tree). default .
--agentAgent id. default: $STRANDLINE_AGENT_ID, else the current branch name
--jsonOne JSON object (shape under Conventions).
example
strandline status --agent agent-a

strandline completions

#cli-completions

Print a shell completion script.

strandline completions <SHELL>
<shell>bash, zsh, fish, powershell or elvish.
example
strandline completions zsh > "${fpath[1]}/_strandline"

strandline mcp

#cli-mcp

Run the MCP server over stdio (what strandline install configures agents to launch).

strandline mcp [OPTIONS]
--repoRepository the server works on. default: the git repository of the current directory
example
strandline mcp --repo ~/src/your-repo

Index and merging

strandline index

#cli-index

Build or incrementally update the repository index (.strandline/index.json).

strandline index [OPTIONS]
--rootRepository root. default .
--jsonPrint the full linked index (symbols, edges, hotspots).
example
strandline index --json | jq '.hotspots[:5]'

strandline hotspots

#cli-hotspots

Show files many others depend on (registries, route tables, configs, schemas).

strandline hotspots [OPTIONS]
--rootdefault .
--topHow many to show. default 10
--min-scoreMinimum score. default: 95th percentile of scores, at least 10
--json
example
strandline hotspots --top 20

strandline merge-file

#cli-merge-file

Three-way merge of files; prints the result. Exit 1 if conflicts remain.

strandline merge-file [OPTIONS] <BASE> <OURS> <THEIRS>
--pathPath used to pick the language. default: the ours file name
-o, --outputWrite the result here instead of stdout.
--line-onlyDisable the structural stage.
--diff3Include the base section in conflict markers.
--jsonPrint a JSON summary (strategy, conflicts, notes) to stderr.
example
strandline merge-file base.ts ours.ts theirs.ts --path src/api.ts -o merged.ts

strandline merge-driver

#cli-merge-driver

Git merge driver entry point, installed by strandline init as strandline merge-driver %O %A %B %L %P. You rarely run it by hand.

strandline merge-driver [OPTIONS] <ANCESTOR> <CURRENT> <OTHER> [MARKER_SIZE] [PATH]
[marker_size]default 7
--ours-labeldefault ours
--theirs-labeldefault theirs
example
# what `strandline init` writes to .git/config
[merge "strandline"]
    name = Strandline structural merge
    driver = strandline merge-driver %O %A %B %L %P

strandline guard

#cli-guard

Judge one merged file with the guard, whichever tool produced it: parses, no lost lines (token-level combinations and rename propagation are fine), no new duplicate bindings, no stale reference to a name the other side renamed. Exit 1 = rejected.

strandline guard [OPTIONS] <BASE> <OURS> <THEIRS> <MERGED>
--pathPath used to pick the language. default: the merged file name
--jsonPrint the report as JSON on stdout.
example
strandline guard base.py ours.py theirs.py merged.py --json

strandline merge-check

#cli-merge-check

Check a merge result as a whole for stale references to a name one branch renamed. Run by the pre-merge-commit hook strandline init installs. Exit 1 if any are found.

strandline merge-check [OPTIONS]
--repoRepository (any path inside it). default .
--oursOur side. default HEAD
--theirsTheir side. default MERGE_HEAD (the merge in progress)
--mergedThe merged result: index (what the merge commit will contain), worktree, or a revision. default index
--dirsCheck four directory trees instead of git revisions: BASE OURS THEIRS MERGED.
--jsonPrint the findings as JSON.
example
strandline merge-check --ours main --theirs agent-b --merged worktree

strandline lock regen

#cli-lock-regen

Regenerate lockfiles with their package manager (npm, pnpm, uv) from the current manifests. Set STRANDLINE_LOCK_OFFLINE=1 to forbid network access.

strandline lock regen [OPTIONS] [PATHS]...
[paths]Lockfile paths relative to the repository root.
--pendingAlso regenerate the lockfiles a merge left pending.
--stagegit add each regenerated lockfile.
--json{regenerated, staged}
example
strandline lock regen --pending --stage

strandline generated regen

#cli-generated-regen

Run the generators of generated files ([[generated]] entries in .strandline/merge.toml), which are regenerated after a merge instead of text-merged.

strandline generated regen [OPTIONS] [PATHS]...
[paths]Generated file paths relative to the repository root.
--pendingAlso regenerate the files a merge left pending.
--stagegit add each regenerated file.
--json{regenerated, ran, staged}
example
# .strandline/merge.toml
[[generated]]
path = "src/schema.gen.ts"
run  = "npm run codegen"

strandline weave install

#cli-weave-install

Install the pinned, checksum-verified Weave merge engine under $STRANDLINE_HOME/tools (default ~/.strandline). Downloads the release archive unless --from gives a local archive or binary. Optional: Strandline works fully without it.

strandline weave install [OPTIONS]
--fromLocal archive or binary instead of a download.
--json
example
strandline weave install

strandline weave status

#cli-weave-status

Is Weave installed and verified?

strandline weave status [OPTIONS]
--json
example
strandline weave status --json

Coordination

All coordination commands accept --json (the shapes in Concepts), --repo <path> (any path inside the working tree, default .) and --agent <id> (default $STRANDLINE_AGENT_ID, else the current branch name). Those three are not repeated below.

strandline agent register

#cli-agent-register

Register (or refresh) this working copy as an agent. --task and --scope (re)compute its declared scope.

strandline agent register [OPTIONS]
--taskWhat this agent is doing; files and symbols are predicted from the text.
--baseThe revision this agent's work is compared against.
--scopeA file or directory this task will touch (repeatable); added to the predicted scope. Contract changes there reach you before you edit anything.
example
strandline agent register --task "Add invoice totals" --scope src/billing/

strandline agent list

#cli-agent-list

List active agents.

strandline agent list [OPTIONS]
example
strandline agent list --json | jq '.agents[].id'

strandline lease acquire

#cli-lease-acquire

Claim a path (file or directory prefix). Exit 3 if another active agent holds it.

strandline lease acquire [OPTIONS] <PATH>
--ttlTime to live, e.g. 30m, 2h, 90s. default 30m
--reasonShown to anyone refused the lease.
example
strandline lease acquire src/routes.ts --ttl 45m --reason "wiring /invoices"

strandline lease release

#cli-lease-release

Release your lease on a path (no-op if none).

strandline lease release [OPTIONS] <PATH>
example
strandline lease release src/routes.ts

strandline lease renew

#cli-lease-renew

Extend your lease on a path.

strandline lease renew [OPTIONS] <PATH>
--ttldefault 30m
example
strandline lease renew src/routes.ts --ttl 1h

strandline lease list

#cli-lease-list

List active leases.

strandline lease list [OPTIONS]
--allInclude expired and released leases.
example
strandline lease list --all

strandline contract publish

#cli-contract-publish

Detect public-interface changes in this working tree vs base and make them this agent's current set: unchanged ones keep their id, superseded or reverted ones are retracted, files that don't parse keep their earlier changes. Updates the footprint.

strandline contract publish [OPTIONS]
--baseCompare against this revision instead of the registered base.
example
strandline contract publish --json | jq '.published[] | {symbol, change}'

strandline contract changes

#cli-contract-changes

Contract changes published by other agents: relevant to you by default, unseen first.

strandline contract changes [OPTIONS]
--allAll changes, not only the relevant ones.
--mark-seenAcknowledge the listed changes.
--retiredList changes retired because they reached main, with the reason.
example
strandline contract changes --mark-seen

strandline collisions

#cli-collisions

Which other agents' changes or leases overlap with mine? Severity: symbol, file, lease, dependency.

strandline collisions [OPTIONS]
--baseCompute my footprint against this revision.
example
strandline collisions --base main

strandline plan

#cli-plan

Forecast collisions for a task list; suggest an order (waves that can run in parallel), splits and hotspot leases. Markdown: each top-level list item or ##/### heading is a task, optional files: lines list paths or globs. JSON: [{"id"?, "title", "description"?, "files"?}].

strandline plan [OPTIONS] <TASKS>
<tasks>Task list: markdown (.md) or JSON (.json).
--top-hotspotsHow many top hotspots to consider for lease suggestions. default 10
example
strandline plan tasks.md --top-hotspots 15

Verify and repair

strandline verify

#cli-verify

Build the combination of branches in a scratch worktree and run the checks on it. Exit 4 if the combination fails.

strandline verify [OPTIONS] <BRANCHES>...
<branches>Branches to combine, in merge order.
--baseRevision to combine them onto. default main
--fullRun the full test suite instead of the affected tests.
--repodefault .
--json
--idempotency-keyRun at most once per key (needs --json): a later run with the same key waits for a run still holding it, then prints the recorded result. Used by durable workflows so a replayed step never repeats a completed verify or repair.
--flake-historyThe repository's shared flake history (JSON), counted with the local history when classifying failures. In hosted mode it is fetched from the service.
--model-mergeOpt in to the model-driven merge path for conflicts the merge strategy leaves: mock:<script.json>, cmd:<command> or anthropic (needs ANTHROPIC_API_KEY). A candidate is used only if it passes the guard and the full checks. Off by default; also STRANDLINE_MODEL_MERGE or git config strandline.modelMerge (off disables).
--model-max-candidatesModel path: candidates evaluated at most. default 3
--model-max-costModel path: spend cap in USD, priced from reported token usage. default 1.00
--model-max-secondsModel path: wall-time cap in seconds, provider calls and verification. default 900
example
strandline verify agent-a agent-b --base main --full

strandline repair

#cli-repair

Verify, and on failure ask a repair provider for a guarded fix, re-verified on the full suite, capped at --max-attempts. Takes every strandline verify flag, plus:

strandline repair [OPTIONS] --provider <PROVIDER> <BRANCHES>...
--providermock:<script.json>, cmd:<command> or anthropic.
--max-attemptsdefault 3
example
strandline repair agent-a agent-b --provider anthropic --max-attempts 2

strandline flaky

#cli-flaky

The flake history of this repository's tests and quarantine suggestions.

strandline flaky [OPTIONS]
--repodefault .
--json
example
strandline flaky

strandline ci verify

#cli-ci-verify

Verify inside CI (GitHub Actions, GitLab CI): this pipeline's change combined with its base and any --with branches, results to the job summary and JUnit and, with STRANDLINE_API_KEY, metadata to the service.

strandline ci verify [OPTIONS]
--branchBranch under test. default: from the CI event
--baseBase to combine onto. default: the PR/MR target, else the default branch
--withOther branches landing together (repeatable, comma-separated).
--fullRun the full test suite instead of the affected tests.
--repodefault .
--remoteGit remote the refs come from. default origin
--no-fetchDo not fetch: the refs are already in the checkout.
--report-dirWhere strandline-verify.json, strandline-report.md and strandline-junit.xml go. default .strandline-ci
--no-failExit 0 even when the combination fails (report only).
--jsonPrint the result as JSON.
example
# .github/workflows/verify.yml (step)
- run: strandline ci verify --with release/2026-11 --report-dir .strandline-ci
  env:
    STRANDLINE_API_KEY: ${{ secrets.STRANDLINE_API_KEY }}

Hosted mode

Local mode needs no account. Hosted mode is on when credentials are saved, or STRANDLINE_SERVICE_URL and STRANDLINE_API_KEY are set; STRANDLINE_LOCAL=1 forces local mode.

strandline login

#cli-login

Enable hosted mode: check an API key against the service and save it (~/.config/strandline/credentials.json, mode 600).

strandline login [OPTIONS]
--urlService URL, e.g. https://strandline.example.com (env STRANDLINE_SERVICE_URL).
--api-keyAPI key (env STRANDLINE_API_KEY); read from stdin when absent.
--json
example
strandline login --url https://strandline.example.com < key.txt

strandline logout

#cli-logout

Remove saved hosted-mode credentials (back to local mode).

strandline logout [OPTIONS]
--json
example
strandline logout

strandline whoami

#cli-whoami

Show whether coordination is local or hosted, and the workspace.

strandline whoami [OPTIONS]
--json
example
strandline whoami --json

MCP tools

strandline-mcp is the MCP server your agents talk to. Every tool calls the strandline CLI, so it works the same in local and hosted mode.

It speaks MCP 2026-07-28 (stateless requests, server/discover, subscriptions/listen, the tasks extension for long-running tools, trace context in _meta) and 2025-era clients (initialize, resources/subscribe).

Running the server

Over stdio (strandline mcp, or the command strandline install --agent … writes into each agent's config) or Streamable HTTP:

strandline-mcp --http --port 8788     # endpoint /mcp, binds 127.0.0.1
STRANDLINE_BINThe CLI to run. default: strandline on PATH
STRANDLINE_REPORepository. default: cwd
STRANDLINE_AGENT_IDAgent identity when the client does not name one.
STRANDLINE_MCP_POLL_MSHow often to poll for contract changes while a client listens. default 2000
STRANDLINE_MCP_TASK_POLL_MSSuggested tasks/get interval. default 500
STRANDLINE_MCP_HTTP_TOKENBearer token for HTTP; required off loopback.
STRANDLINE_MCP_ROOTSThe working copies HTTP requests may name.
STRANDLINE_MCP_ALLOWED_ORIGINSAllowed browser origins for HTTP.
STRANDLINE_SERVICE_URL, STRANDLINE_API_KEYHosted mode, passed to the CLI. Local mode never uses the network.

On HTTP each request names its agent (X-Strandline-Agent) and working copy (X-Strandline-Worktree). Each tool runs strandline <cmd> --json --repo <repo> --agent <agent> and returns the JSON as structuredContent plus a short text summary. Each tools/call is a span continuing _meta.traceparent.

Server instructions sent to clients:

instructions

Strandline coordinates parallel agents on one repository. Register at session start, listen for changes to strandline://contracts/pending, check collisions and take a lease before editing hotspot files, publish contract changes after changing a public interface, and verify the combination before requesting a merge.

Tools

strandline_register

Register (or refresh) this agent: its task, base revision and scope. The task text and scope paths declare which files you will touch, so contract changes there reach you before you edit anything.

taskstring
basestring
scopestring[]files or directories this task will touch

strandline_status

Active agents, active leases, and contract changes by other agents that affect your work and you have not seen. No arguments.

strandline_lease_acquire

Claim an exclusive, time-limited lease on a file or directory. granted=false (with held_by) if another active agent holds it.

pathstringrequired
ttlstringe.g. 30m, 2h (default 30m)
reasonstring

strandline_lease_release

Release your lease on a path.

pathstringrequired

strandline_lease_renew

Extend your lease on a path.

pathstringrequired
ttlstring

strandline_lease_list

Active leases (all=true: include expired and released).

allboolean

strandline_publish_contracts

Detect public-interface changes (exported signatures, routes) in your working tree vs base and publish them to other agents.

basestring

strandline_contract_changes

Contract changes by other agents: those relevant to your work (default) or all; mark_seen=true acknowledges them.

allboolean
mark_seenboolean

strandline_check_collisions

Which other active agents' changes or leases overlap yours (symbol > file > lease > dependency).

basestring

strandline_plan

Forecast collisions for a task list: predicted files per task, pair scores, parallel waves, splits and hotspot leases.

tasksstringrequired · markdown or JSON task list text, or a path to a .md/.json file
top_hotspotsinteger

strandline_verify

Build the combination of branches in a scratch worktree and run build, type-check and the affected tests (full=true: the whole suite). Long-running: served as a task (io.modelcontextprotocol/tasks) when the client declares the extension; poll tasks/get, stop with tasks/cancel.

branchesstring[]required · in merge order
basestringrevision to combine onto (default main)
fullboolean

strandline_repair

Verify the combination; on failure ask the repair provider for a guarded fix, re-verify it on the full suite (at most max_attempts), and explain the conflict if no fix holds. Long-running: served as a task when the client supports it.

branchesstring[]required
basestring
providerstringrequired · mock:<script.json>, cmd:<command> or anthropic
max_attemptsinteger

strandline_request_merge

Hosted mode: ask the service to land these branches through its verified merge queue (verify the combination, repair if needed, land, attach a receipt). Returns the merge attempt; repeat calls with the same idempotency_key return the same attempt.

branchesstring[]required
basestringdefault main
idempotency_keystring

Resources and alerts

Both resources answer application/json: { "pending": [ContractChange, …] }.

strandline://contracts/pendingUnseen contract changes by other agents that affect your work.
strandline://agents/{agent}/contracts/pendingPending contract changes of one agent; on a shared HTTP endpoint each agent listens to its own URI.

A client opens subscriptions/listen with resourceSubscriptions: [<its pending URI>]. While it listens the server polls for contract changes every STRANDLINE_MCP_POLL_MS and, once per new relevant change, sends notifications/resources/updated for that URI; the client then reads the resource (symbols, signatures and why). A 2025-era client subscribes with resources/subscribe and gets notifications/resources/updated and a notifications/message (level warning) naming the changed symbols.

Self-hosting

Two supported installs, both tested end to end: a Helm chart for Kubernetes and a single-node Docker Compose install with TLS.

What runs

serviceghcr.io/strandline-dev/strandline-serviceAPI, GitHub/GitLab/Stripe webhooks, workflow starts; stateless, 2 or more replicas
workerghcr.io/strandline-dev/strandline-workerverify / repair / land (CLI + git); keeps checkouts on a volume
migrationsthe service imagenode packages/service/dist/main.js migrate up once per release
Postgres 16yours (managed)wal_level=logical, WAL archiving / PITR (below)
Restate 1.8restatedev/restatedurable workflows; BSL-1.1, not shipped by Strandline: run it yourself or use Restate Cloud

Images are multi-arch (amd64, arm64), signed with cosign (keyless, GitHub OIDC) and carry an SBOM.

Kubernetes (Helm)

kubectl create namespace strandline
kubectl -n strandline create secret generic strandline-secrets \
  --from-literal=DATABASE_URL='postgres://strandline_login:…@db:5432/strandline' \
  --from-literal=DATABASE_URL_MIGRATE='postgres://strandline_owner:…@db:5432/strandline' \
  --from-literal=STRANDLINE_RECEIPT_SIGNING_KEY="$(openssl rand -base64 48)" \
  --from-literal=STRANDLINE_ADMIN_TOKEN="$(openssl rand -hex 32)"
helm install strandline deploy/helm/strandline -n strandline \
  --set existingSecret=strandline-secrets \
  --set restate.ingressUrl=http://restate.restate:8080 --set restate.adminUrl=http://restate.restate:9070 \
  --set image.service.digest=sha256:… --set image.worker.digest=sha256:…
  • Secrets only from existingSecret (keys listed in values.yaml); the migration Job alone gets the owner URL (DATABASE_URL_MIGRATE); service and workers get the non-owner login role that row-level security applies to.
  • Migrations: a Job per release revision; new service pods stay not-ready (/readyz) until it has run, while the previous release keeps serving (migrations are expand/contract).
  • Hardening: non-root (uid 1000), read-only root filesystem, all capabilities dropped, RuntimeDefault seccomp, no service-account token; startup, liveness and readiness probes; resources; PodDisruptionBudgets (service minAvailable: 1, worker maxUnavailable: 1); an HPA for the service (2–6 replicas at 70 % CPU); NetworkPolicies (default deny, then exactly the ports each component needs; narrow networkPolicy.workerEgressCIDRs to your Git host).
  • Metrics: pods are annotated for Prometheus (prometheus.io/port 9464 / 9465); serviceMonitor.enabled=true with the Prometheus Operator. Alert rules and Grafana dashboards ship with the chart.
  • Evaluation: --set postgresql.enabled=true --set postgresql.password=… --set restate.enabled=true --set secrets.create=true --set secrets.values.DATABASE_URL=… runs a single Postgres and a single Restate in the namespace. No backups, no HA.
  • Sandbox: see Sandbox. The default is one pod per check under the gvisor RuntimeClass.
production

With STRANDLINE_ENV=production the chart refuses to render without a sandbox backend, and the kubernetes backend refuses to render without a RuntimeClass or worker.sandbox.image.

Single node (Docker Compose)

cd deploy/compose
cp .env.example .env            # STRANDLINE_DOMAIN, image tags
./init-secrets.sh               # random secrets in ./secrets (0600 dir, never in .env)
sudo install -d -o 1000 -g 1000 /var/lib/strandline/repos /var/lib/strandline/tmp   # STRANDLINE_DATA_DIR
echo "STRANDLINE_DOCKER_GID=$(stat -c %g /var/run/docker.sock)" >> .env
sandbox/build-images.sh v1      # the sandbox image; set STRANDLINE_SANDBOX_IMAGE
docker compose up -d --wait
docker compose exec service node packages/service/dist/main.js admin create-workspace --name my-team --plan team

Caddy terminates TLS: a public STRANDLINE_DOMAIN gets Let's Encrypt certificates automatically; localhost uses Caddy's internal CA. Everything else is on internal networks; secrets are Compose secrets read through *_FILE variables; every container has a healthcheck, restart: unless-stopped, a memory limit, and (except Postgres and Restate) a read-only root filesystem with all capabilities dropped. The service and worker log in to Postgres as strandline_svc, a non-owner role created on first start, so row-level security applies to them; only migrate uses the owner.

Upgrade: change the image tags in .env, then docker compose up -d (the migrate service runs first).

Sandbox

The worker runs your check commands (tests, builds, repairs), each in a sandbox: a container with no network, a read-only root, resource and time limits, as an unprivileged user, or a Firecracker microVM. In production the worker refuses to run without one.

  • Docker Compose sets this up: the worker gets the host's Docker socket and starts sandbox containers next to itself. The daemon resolves their bind mounts on the host, so checkouts and scratch worktrees live in STRANDLINE_DATA_DIR, mounted at the same path in the worker.
  • Kubernetes (the default): the worker never runs customer code itself. For each check it starts a pod next to itself under a RuntimeClass: gvisor (gVisor's runsc) by default, kata with worker.sandbox.isolation=kata, or your own class name with worker.sandbox.runtimeClassName (GKE Sandbox: gvisor). The check pod has its own service account with no token, none of the worker's secrets or environment, no network (a deny-all NetworkPolicy, no DNS) and only the run's worktree, mounted from the worker's volume by subPath; it runs as a non-root uid with a read-only root, no capabilities and CPU, memory and time limits. A ValidatingAdmissionPolicy (Kubernetes 1.30 or newer) refuses any other kind of pod from the worker's service account, whose Role covers only pods and their logs in the namespace.
  • What you provide on Kubernetes: gVisor or Kata on the nodes that run workers, registered as a containerd (or CRI-O) runtime handler, and the RuntimeClass (worker.sandbox.runtimeClass.create=true lets the chart create it with handler runsc / kata); the toolchain image (sandbox/build-images.sh, pushed to your registry, worker.sandbox.image by digest); persistence for the worker (on by default); a CNI that enforces NetworkPolicies (without one the check pod still has no credentials, but could reach in-cluster services). Settings: worker.sandbox.kubernetes.* (per-check CPU, memory, tmpfs, start timeout, API server egress).
  • Other backends: microvm on nodes with /dev/kvm (with sandbox/microvm/provision.sh output on the node in worker.sandbox.microvmDir, default /var/lib/strandline/microvm, mounted read-only and passed as STRANDLINE_MICROVM_DIR), or docker / podman where the worker pod reaches a runtime that sees worker.repoRoot at the same path (a dedicated sandbox node pool).
warning

Access to the Docker socket is root-equivalent on that host. On Compose, give the worker a host (or VM) of its own.

Backups and restore

  • Physical + WAL (recommended): archive_mode and an archive_command to an archive volume (ship it to object storage; archive_timeout = 60 gives an RPO of a minute or less) and a nightly pg_basebackup -Ft -z -X stream. Restore = base backup + restore_command from the archive (point in time with recovery_target_time). A reference postgresql.conf ships with the installs.
  • Managed Postgres: use the provider's PITR (it is the same mechanism); keep a logical pg_dump -Fc weekly as a second, provider-independent copy.
  • Drill: make restore-drill does both restores into fresh Postgres containers and verifies row counts, every receipt's digest and HMAC signature, metering, and that the service runs on the restored database. Run it after changing backup settings and quarterly.
  • Restate keeps workflow journals on its own volume; back it up with the same schedule, or accept that in-flight workflows restart from their last committed database state after a Restate loss (merge attempts are re-requested idempotently).

Sizing (measured)

Under load, two service replicas at 1 CPU / 512 MB and Postgres at 1.5 CPU / 1 GB carry 1,000 agents (about 15,000 requests/min plus about 5,000 webhook deliveries/min); Postgres CPU is the first limit. Start there and scale the database first, replicas second.