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:
- the owner DID — the person's AT identity;
- the repo-record AT-URI —
at://<owner-did>/sh.tangled.repo/<rkey>; - 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:
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 #
- Tangled writes an
sh.tangled.reporecord to the owner's PDS. - That record names the knot holding Git objects and includes
repoDid. - A spindle is registered separately under the operator's identity.
- The spindle must verify that
GET /xrpc/sh.tangled.ownerreports the same owner DID. - A repository owner selects the verified spindle in repository pipeline settings. Registration and repository selection are separate operations.
- 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/spindleshttps://tangled.org/<owner>/<repo>/settings?tab=pipelineshttps://tangled.org/<owner>/<repo>/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:
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_reporesolvesowner/repo, repo-record AT-URIs, and bare repo DIDs;list_files,read_file,commit_log, andcompareread 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:
curl -fsSG 'https://<pds>/xrpc/com.atproto.repo.getRecord' \
--data-urlencode 'repo=<owner-did>' \
--data-urlencode 'collection=sh.tangled.repo' \
--data-urlencode 'rkey=<repo-name>' |
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:
curl -fsSG 'https://<spindle>/xrpc/sh.tangled.ci.queryPipelines' \
--data-urlencode 'repo=<repo-did>' \
--data-urlencode 'commits=<full-sha>' \
--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:
curl -fsS 'https://<spindle>/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:
ssh -t -p 3333 tangled.org <repo-did> <commit-sha>
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
- confirm the workflow file existed in the pushed commit;
- confirm its event and branch/tag constraints match;
- read the repo record's
spindlefield—do not assume the hosted spindle; - query that spindle with the repository DID;
- 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
- register it in spindle settings;
- verify its public
sh.tangled.ownerresponse matches the operator DID; - press retry if verification has not completed;
- 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-mcpREADME, current implementation, anddocs/bobbin-api.md;docs/architecture.mddocuments the older service-auth read path and is historical contexttangled-infra/NOTES.mdandnotes/spindle-diagnosis-evolution.mdfor incident history; treat pre-v1.16 behavior as historical, not current API truth