On by default. Set LOKI_WORKSPACES=0 to disable. Design: docs/v10/D51-PHASE-B.md.
Define a group of repos once in loki.yaml:
workspaces:
shop:
repos:
- {repo: acme/api, path: ~/src/api, setup: "npm ci"}
- {repo: acme/web, path: ~/src/web, after: [acme/api]}
integration: {command: "make e2e", timeout_s: 900, per_stage: false}
concurrency: 2
plan: false
The workspaces: block is validated and fails closed: a misspelled key (for example afer) or a wrong type exits 2 and names the key.
Repos with no pending after run in parallel, at most concurrency at a time (default 2; set LOKI_WORKSPACE_CONCURRENCY to change it).
Then:
loki workspace list
loki workspace show shop
loki workspace run shop acme/api#12
loki start --workspace shop "<brief>" # same runner, from the primary command
loki workspace infer shop # print suggested after: edges
loki workspace status # every recorded run, one row per repo
loki workspace status <run-id>
loki/ws-<run-id>-N branch and its own engine run. Your checkouts are not touched.after orders repos: a repo starts once its predecessors succeed. If a predecessor fails, its dependents are SKIPPED.integration command runs once from the run directory with LOKI_WS_DIR_<OWNER>_<REPO> pointing at each worktree. A timeout counts as failed. With no integration configured the status is not_configured..loki/workspaces/<name>/<run-id>/integration.json: head SHA per repo, exit code, log sha256. When every repo succeeded, integration did not fail and every child sealed a receipt, the run also seals a signed composite.json that binds integration.json (its status, exit code and log hash), integration.log, every repo head SHA and every child receipt (see “Verify a workspace run”). Sealing refuses a failed or interrupted integration run, a nonzero exit code, a missing integrated head and a log that does not match its recorded hash.loki workspace infer <name> prints suggested after: edges read from package.json, go.mod and pyproject.toml dependencies on other listed repos. It also suggests an edge from (1) a source import (TS/JS import or require, Python import, Go import path) of a sibling’s package or module name even when the manifest does not declare it, scanning at most 400 source files per repo, and (2) a client generator config (openapitools.json, openapi-generator.yaml, orval.config.) whose input spec path lies inside a sibling checkout. Each inferred edge prints its evidence as file:line. It is print-only and cycle-checked; it never edits loki.yaml.plan: true (or LOKI_WORKSPACE_PLAN=1) makes one provider call that decomposes the brief per repo into <run-dir>/plan.json. A valid plan gives each listed repo its own task; an invalid one falls back to the generic text and prints the reason.integration: {per_stage: true} also runs the integration command after each topological layer. A failing stage SKIPs every later layer, so a broken contract stops before downstream repos build on it.path, a repo is cloned to ~/.loki/repos/owner__name.gh login or GH_TOKEN, skips silently when there is no PR, never fails the run, and is disabled with LOKI_WORKSPACE_COMMENT=0.timing (per repo started_at and finished_at, epoch seconds) in integration.json. python3 autonomy/lib/workspace_metrics.py <run-dir> --attention-min 30 prints prs (repos with outcome ok), wall_clock_seconds (earliest start to latest finish), prs_per_hour and prs_per_attention_minute. Runs recorded before timing existed, and any run where --attention-min is not given, print unmeasured for the affected rate, never zero.loki backlog) and dashboard starts now share the workspace worktree prep: a per-repo lock, a clean start point and copied dependencies. LOKI_WORKSPACES=0 restores the previous worktree creation (D51-B05, D51-B06).After loki workspace run <name> <ref> finishes, check the evidence rather than trusting the exit code alone:
loki workspace status prints one row per repo and the integration status for each recorded run..loki/workspaces/<name>/<run-id>/integration.json holds status, the head SHA per repo and timing.python3 autonomy/lib/workspace_metrics.py <run-dir> --attention-min <minutes> prints prs, prs_per_hour and prs_per_attention_minute; any value it cannot measure prints unmeasured.loki verify <run-dir>/composite.json re-checks the composite offline (add --pubkey FILE on another machine). It recomputes the composite hash, re-verifies every child receipt from disk and checks the signature. It prints the integration result and every repo head SHA it attests. It is TAMPERED for a tampered or swapped child, a child head that is not the integrated head, a failed or interrupted integration status or nonzero exit code, an edited integration section, or an integration.log that no longer matches its recorded hash. It is UNCHECKED for an unsigned child under a signed composite, and for a legacy composite that predates the integration section and has no integrated head to compare. Deployed-SHA binding is partial: loki deploy --execute ships a signed .well-known/loki-provenance.json (head_sha, tree_sha, receipt_sha256, run_id, built_at) in the static root of a single-repo nextjs, Vite or built-static project (public/, dist/ or build/) and, when the provider prints a URL, fetches it back through loki verify --url --expect-sha. The deploy record’s live_binding is VERIFIED only when the served doc is signed and names the anchor SHA; a stale or forged doc is FAILED (FAILED_HEALTH, exit 4), an unreachable one is unchecked, and docker, python and node projects (no static root) are unsupported. A mismatch is retried inside the health attempts and interval budget before it is FAILED, so a URL still serving the previous build for a few seconds is not rolled back at once. A live doc that is unsigned or signed with an unknown key is unchecked, not FAILED, so a stale non-loki build serving such a doc passes silently. Under --allow-unanchored the anchor is UNPROVEN, so no doc is published and live_binding is unanchored. The deploy appends **/.well-known/loki-provenance.json to .git/info/exclude and leaves the file in the working tree. Every loki deploy --execute first removes that file from public/, dist/ and build/, so only a doc written by the current deploy ships; an unanchored, unsupported or unsigned deploy ships none. A provider run by hand outside loki deploy --execute uploads whatever doc the last loki deploy left, which names that deploy’s SHA, not the new build. If the provider rebuilds remotely from a gitignored root, the doc is lost and the result is unchecked. None of this binds a composite: the composite attests what was integrated and tested, not what is running anywhere, and no workspace deploy writes or probes a provenance doc. Exit codes are the single-receipt ones: 0 VERIFIED, 1 TAMPERED, 2 UNCHECKED, 3 UNSIGNED. The composite never reads higher than its weakest child: one PARTIAL child caps it at PARTIAL.composite not sealed: <reason> (for example a repo with no receipt). Seal by hand with loki verify --seal-composite <run-dir>; it refuses unless every child has a receipt.Not measured: no scale test exists for large repo counts. Claim only what you have run.
tests/workspace/99-e2e.sh runs this whole check through both autonomy/loki and bin/loki with a stub launcher and a stub gh.