# tangled: authority follows the object Tangled is a Git collaboration system built across AT Protocol records and specialized servers. The practical mistake is to treat `tangled.org` as the single authority. It is the browser-facing appview; repository metadata, Git objects, pipeline state, and collaboration records have different owners. ## the map | object | authority | useful interface | |---|---|---| | account identity | the account's DID document and PDS | handle resolution, PLC directory, PDS XRPC | | repository metadata | `sh.tangled.repo` in the owner's AT repo | Bobbin or the owner's PDS | | Git objects and refs | the repository's knot | Git transport or knot XRPC | | current CI execution and logs | the repository's selected spindle | spindle `sh.tangled.ci.*` XRPC | | issues, pulls, comments, labels | AT records, with some appview-derived state | Bobbin for indexed reads; PDS for writes | | browser auth and settings | the Tangled appview | `tangled.org` | Bobbin at `https://api.tangled.org` is Tangled's public, unauthenticated, read-only index and Git proxy. It is the convenient discovery layer, not a replacement for every object's owner. In particular, a current spindle is the authority for pipeline history since Tangled v1.16. ## three identifiers that look interchangeable but are not A repository normally involves: 1. the **owner DID** — the person's AT identity; 2. the **repo-record AT-URI** — `at:///sh.tangled.repo/`; 3. the **repository DID** (`repoDid`) — the stable identity of the Git repository itself. New repo records usually use the repository name as their rkey and omit `value.name`. Legacy records use a TID rkey and carry the human name in `value.name`. Code that resolves repositories must support both. The DID embedded in a modern Tangled Git remote can be the **repository DID**, not the owner's DID: ```text git@tangled.org:did:plc:... ``` Issue, pull, and pipeline lists are generally keyed by the bare `repoDid`. Owner-level record discovery is keyed by the owner DID. Do not substitute one for the other because both happen to be PLC DIDs. ## lifecycle of a repository and its CI 1. Tangled writes an `sh.tangled.repo` record to the owner's PDS. 2. That record names the knot holding Git objects and includes `repoDid`. 3. A spindle is registered separately under the operator's identity. 4. The spindle must verify that `GET /xrpc/sh.tangled.owner` reports the same owner DID. 5. A repository owner selects the verified spindle in repository pipeline settings. Registration and repository selection are separate operations. 6. A push, pull request, or manual dispatch matching a file in `.tangled/workflows/` creates a pipeline on the selected spindle. An `sh.tangled.spindle` record proves registration, not that a particular repository selected it. A verified spindle missing from a repository dropdown usually means the page needs refreshing, verification has not completed, or the viewer is not the repository owner. Useful browser routes: - `https://tangled.org/settings/spindles` - `https://tangled.org///settings?tab=pipelines` - `https://tangled.org///pipelines` ## workflow files Each YAML file under `.tangled/workflows/` is an independent workflow. A pipeline may contain several workflows, which run in parallel; steps within a workflow run serially. A small check workflow: ```yaml when: - event: ["push", "manual"] branch: ["main"] - event: ["pull_request"] branch: ["main"] engine: nixery dependencies: nixpkgs: - nodejs_22 - bun steps: - name: install dependencies command: npm ci - name: check command: npm run check ``` Choose the engine by workload: - **nixery** builds an environment from Nix packages and runs steps in containers. It is the lighter default for linting, tests, and ordinary builds. - **microvm** runs the whole workflow in a VM. Use it when the workload needs a full machine boundary, KVM-backed execution, services, or Docker inside the guest. Availability and image names depend on the spindle operator. Pipeline environment values in workflow YAML are public. Repository secrets belong in the repository's pipeline settings and are injected by the spindle. ## inspect repositories and collaboration state `tangled-mcp` provides the ergonomic read path: - `get_repo` resolves `owner/repo`, repo-record AT-URIs, and bare repo DIDs; - `list_files`, `read_file`, `commit_log`, and `compare` read Git data from the knot; - issue and pull tools read indexed collaboration records; - writes put records on the authenticated user's PDS. The MCP's Bobbin client deliberately resolves metadata first, then talks to the knot directly for Git data. Bobbin can rate-limit or lag, and a legacy repo record can require the TID-rkey fallback. Its `list_pipelines` tool reads Bobbin's `sh.tangled.pipeline.listPipelines` index. Treat that as discovery, not an authoritative fresh-run oracle. An empty Bobbin result does not prove that the selected spindle has no run. ## check current CI without the interactive viewer First resolve the repo record and obtain both `repoDid` and `spindle`. The record itself is the source of the selected spindle: ```bash curl -fsSG 'https:///xrpc/com.atproto.repo.getRecord' \ --data-urlencode 'repo=' \ --data-urlencode 'collection=sh.tangled.repo' \ --data-urlencode 'rkey=' | jq '{uri, repoDid: .value.repoDid, spindle: .value.spindle, knot: .value.knot}' ``` For a legacy TID-rkey record, use Bobbin or page `com.atproto.repo.listRecords` and match `value.name`. Then ask the selected spindle directly: ```bash curl -fsSG 'https:///xrpc/sh.tangled.ci.queryPipelines' \ --data-urlencode 'repo=' \ --data-urlencode 'commits=' \ --data-urlencode 'limit=5' | jq '{total, pipelines: [.pipelines[]? | {id, trigger, workflows}]}' ``` Each workflow carries `status`, `startedAt`, and `finishedAt`. This is a finite, machine-readable status check and the right source for a recent run. The spindle owner is independently inspectable: ```bash curl -fsS 'https:///xrpc/sh.tangled.owner' | jq . ``` It must match the DID configured as `SPINDLE_SERVER_OWNER` for verification to succeed. ## the SSH log viewer is a TUI A push may print a command like: ```bash ssh -t -p 3333 tangled.org ``` This opens an interactive pipeline viewer. It is useful in a real terminal or tmux, but it remains open after the workflow reaches a terminal state and waits for `q`. Therefore: - do not use the viewer as a finite completion watcher; - do not put it in a background-job abstraction that wakes only on process exit; - use spindle XRPC for machine status; - use the SSH viewer when a human wants live logs, then quit it explicitly. `ssh -tt` may be necessary through a wrapper with no local terminal, but forced PTY allocation does not make the viewer non-interactive. ## troubleshooting by authority **No workflow appeared** 1. confirm the workflow file existed in the pushed commit; 2. confirm its event and branch/tag constraints match; 3. read the repo record's `spindle` field—do not assume the hosted spindle; 4. query that spindle with the repository DID; 5. if the spindle was rebuilt, re-saving the repository's spindle selection may be required so it ingests the repo again. **A self-hosted spindle is not selectable** 1. register it in spindle settings; 2. verify its public `sh.tangled.owner` response matches the operator DID; 3. press retry if verification has not completed; 4. refresh repository pipeline settings and select it there. **Bobbin and the spindle disagree** This is possible: Bobbin is an indexed AT-record view, while post-v1.16 current pipeline state belongs to the spindle. Use Bobbin for discovery and the selected spindle for the run verdict. **Git reads work but metadata reads fail** The knot and Bobbin are separate layers. Query the knot by `repoDid` for Git data; inspect Bobbin coverage or rate limiting for indexed records. ## sources and local evidence - current docs: https://docs.tangled.org/spindles - Bobbin docs: https://docs.tangled.org/bobbin - source: https://tangled.org/tangled.org/core - `tangled-mcp` README, current implementation, and `docs/bobbin-api.md`; `docs/architecture.md` documents the older service-auth read path and is historical context - `tangled-infra/NOTES.md` and `notes/spindle-diagnosis-evolution.md` for incident history; treat pre-v1.16 behavior as historical, not current API truth