#!/bin/sh set -eu : "${KNOT_URL:?KNOT_URL must be set}" : "${KNOT_HOSTNAME:?KNOT_HOSTNAME must be set}" : "${PDS_HOSTNAME:?PDS_HOSTNAME must be set}" KNOT_DID=did:web:$KNOT_HOSTNAME SHARED_DIR="${SHARED_DIR:-/shared}" . /scripts/lib.sh [ -f "${SHARED_DIR}/owner-did" ] || fail "no owner-did in $SHARED_DIR." \ ' init-accounts.sh writes it into the init-state volume, so it either never ran or failed.' OWNER_DID=$(cat "${SHARED_DIR}/owner-did") OWNER_JWT=$(login "$OWNER_DID") BOB_DID=$(require_handle "bob.${PDS_HOSTNAME}") BOB_JWT=$(login "$BOB_DID") CHARLIE_DID=$(require_handle "charlie.${PDS_HOSTNAME}") DAVID_DID=$(require_handle "david.${PDS_HOSTNAME}") CORE_SOURCE=https://knot1.tangled.sh/did:plc:ioighzh4jnrhybayw65gfbia # every record this script seeds, and who authors it REPO_CORE=at://$OWNER_DID/sh.tangled.repo/core REPO_EMPTY=at://$BOB_DID/sh.tangled.repo/empty-repo ISSUE_1=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6f3aia222 ISSUE_2=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6guhjy222 ISSUE_3=at://$OWNER_DID/sh.tangled.repo.issue/3lzg6inolq222 STATE_1=at://$OWNER_DID/sh.tangled.repo.issue.state/3lzg6kgvni222 PULL_1=at://$OWNER_DID/sh.tangled.repo.pull/3lzg7tkxvq222 PULL_2=at://$OWNER_DID/sh.tangled.repo.pull/3ms6rk5d6gk22 PULL_3=at://$OWNER_DID/sh.tangled.repo.pull/3ms6rk5d6gk33 PULL_4=at://$OWNER_DID/sh.tangled.repo.pull/3mszlqgjlzsdn STRING_CODE=at://$OWNER_DID/sh.tangled.string/3lzg6ma4pa222 STRING_MARKDOWN=at://$OWNER_DID/sh.tangled.string/3lzg6nzdqy222 STRING_TEXT=at://$BOB_DID/sh.tangled.string/3lzg6psksq222 COMMENT_ISSUE=at://$BOB_DID/sh.tangled.feed.comment/3lzg6rlrui222 COMMENT_ISSUE_REPLY=at://$OWNER_DID/sh.tangled.feed.comment/3lzg6teywa222 COMMENT_STRING=at://$BOB_DID/sh.tangled.feed.comment/3lzg6v67xy222 COMMENT_STRING_REPLY=at://$OWNER_DID/sh.tangled.feed.comment/3lzg6wxgzq222 COMMENT_PULL_V0=at://$BOB_DID/sh.tangled.feed.comment/3mszlqk2a4c22 COMMENT_PULL_V0_REPLY=at://$OWNER_DID/sh.tangled.feed.comment/3mszlqm7b6d22 COMMENT_PULL_V1=at://$OWNER_DID/sh.tangled.feed.comment/3mszlqp3c5e22 COMMENT_PULL_V2=at://$BOB_DID/sh.tangled.feed.comment/3mszlqr5d2f22 REACT_ISSUE_A=at://$BOB_DID/sh.tangled.feed.reaction/3lzg6yqo3i222 REACT_ISSUE_B=at://$OWNER_DID/sh.tangled.feed.reaction/3lzg72jv5a222 REACT_COMMENT_A=at://$OWNER_DID/sh.tangled.feed.reaction/3lzg74d46y222 REACT_COMMENT_B=at://$OWNER_DID/sh.tangled.feed.reaction/3lzg764daq222 FOLLOW_ALICE_BOB=at://$OWNER_DID/sh.tangled.graph.follow/3lzg77vkci222 FOLLOW_ALICE_CHARLIE=at://$OWNER_DID/sh.tangled.graph.follow/3lzg7borea222 FOLLOW_BOB_ALICE=at://$BOB_DID/sh.tangled.graph.follow/3lzg7dhyfy222 FOLLOW_CHARLIE_ALICE=at://$CHARLIE_DID/sh.tangled.graph.follow/3lzg7fb7hq222 FOLLOW_DAVID_ALICE=at://$DAVID_DID/sh.tangled.graph.follow/3lzg7h2gji222 FOLLOW_DAVID_BOB=at://$DAVID_DID/sh.tangled.graph.follow/3lzg7itnla222 # a vouch's subject is its rkey, not a record field VOUCH_ALICE_BOB=at://$OWNER_DID/sh.tangled.graph.vouch/$BOB_DID VOUCH_BOB_CHARLIE=at://$BOB_DID/sh.tangled.graph.vouch/$CHARLIE_DID VOUCH_CHARLIE_DAVID=at://$CHARLIE_DID/sh.tangled.graph.vouch/$DAVID_DID VOUCH_DAVID_ALICE=at://$DAVID_DID/sh.tangled.graph.vouch/$OWNER_DID VOUCH_BOB_DAVID=at://$BOB_DID/sh.tangled.graph.vouch/$DAVID_DID # knot2 ( # idempotent: the knot no-ops a subject that is already a member or an admin for did in "$BOB_DID" "$CHARLIE_DID" "$DAVID_DID"; do token=$(service_token "$OWNER_JWT" sh.tangled.knot.addMember "$KNOT_DID") curl -fsS -o /dev/null -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d "$(jq -nc --arg subject "$did" '{subject:$subject}')" \ "${KNOT_URL}/xrpc/sh.tangled.knot.addMember" printf '[knot2] member %s\n' "$did" >&2 # addMember only *offers* membership. until the subject accepts, the knot still # answers 403 "only knot admin or member may create repositories" for them. the # acceptance record is rkey'd by the knot did and has to be published in the # invitee's own repo before acceptMembership will look at it. acceptance=at://$did/sh.tangled.knot.memberAcceptance/$KNOT_DID put_record "$acceptance" \ "$(jq -nc --arg createdAt "$CREATED_AT" '{createdAt:$createdAt}')" >/dev/null token=$(service_token "$(login "$did")" sh.tangled.knot.acceptMembership "$KNOT_DID") curl -fsS -o /dev/null -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d "$(jq -nc --arg acceptance "$acceptance" '{acceptance:$acceptance}')" \ "${KNOT_URL}/xrpc/sh.tangled.knot.acceptMembership" printf '[knot2] accepted %s\n' "$did" >&2 done ) # addMember only invites -- bob creates a repo below, which needs the knot to # see a completed membership: publish the acceptance record on his own repo, # then call acceptMembership so the knot picks it up. BOB_ACCEPTANCE=at://$BOB_DID/sh.tangled.knot.memberAcceptance/$KNOT_DID put_record "$BOB_ACCEPTANCE" "$(jq -nc --arg createdAt "$CREATED_AT" \ '{createdAt:$createdAt}')" >/dev/null accept_token=$(service_token "$BOB_JWT" sh.tangled.knot.acceptMembership "$KNOT_DID") curl -fsS -o /dev/null \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $accept_token" \ -d "$(jq -nc --arg acceptance "$BOB_ACCEPTANCE" '{acceptance:$acceptance}')" \ "${KNOT_URL}/xrpc/sh.tangled.knot.acceptMembership" printf '[knot2] bob accepted membership\n' >&2 # --- repo fixtures --- # a fork adopts the source's object format, so this lands as sha1 despite knot2 # defaulting to sha256. attempt=1 while :; do token=$(service_token "$OWNER_JWT" sh.tangled.repo.create "$KNOT_DID") resp=$(curl -sS -w '\n%{http_code}' \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d "$(jq -nc --arg source "$CORE_SOURCE" \ '{rkey:"core", name:"core", defaultBranch:"master", source:$source}')" \ "${KNOT_URL}/xrpc/sh.tangled.repo.create") body=$(printf '%s\n' "$resp" | sed '$d') status=$(printf '%s\n' "$resp" | tail -n1) [ "$status" = 503 ] && [ "$attempt" -lt 30 ] || break printf '[knot] registry projection warming, retrying repo.create (%s/30)\n' "$attempt" >&2 attempt=$((attempt + 1)) sleep 2 done case "$status" in 200) REPO_DID=$(printf '%s\n' "$body" | jq -er '.repoDid') ;; 409) REPO_DID=$(curl -fsS \ "${PDS_URL}/xrpc/com.atproto.repo.getRecord?repo=${OWNER_DID}&collection=sh.tangled.repo&rkey=core" \ | jq -er '.value.repoDid') || fail \ "core exists on the knot, and the pds doesn't have a core record with a repoDid in it." \ " The knot volume outlived the pds one, so the seed can't recover the repo DID." \ ' Wipe both with "compose down -v", or delete core on the knot and rerun.' refs_status=$(curl -sS -o /dev/null -w '%{http_code}' \ "${KNOT_URL}/xrpc/sh.tangled.git.listRefs?repo=${REPO_DID}&limit=1") [ "$refs_status" = 200 ] || fail \ "the pds has $REPO_DID as core, and the knot answered HTTP $refs_status for its refs." \ " The pds volume outlived the knot one, so that repo DID doesn't point at a repository." \ ' Wipe both with "compose down -v".' ;; 503) fail "the knot stayed warming through a minute of repo.create retries: $body" ;; *) fail "repo.create core: HTTP $status: $body" ;; esac printf '[knot] core = %s\n' "$REPO_DID" >&2 put_record "$REPO_CORE" "$(jq -nc \ --arg knot "$KNOT_HOSTNAME" \ --arg repoDid "$REPO_DID" \ --arg source "$CORE_SOURCE" \ --arg createdAt "$CREATED_AT" \ '{knot:$knot, name:"core", repoDid:$repoDid, source:$source, createdAt:$createdAt}')" >/dev/null token=$(service_token "$BOB_JWT" sh.tangled.repo.create "$KNOT_DID") resp=$(curl -sS -w '\n%{http_code}' \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $token" \ -d '{"rkey":"empty-repo", "name":"empty-repo", "defaultBranch":"master"}' \ "${KNOT_URL}/xrpc/sh.tangled.repo.create") body=$(printf '%s\n' "$resp" | sed '$d') status=$(printf '%s\n' "$resp" | tail -n1) case "$status" in 200) EMPTY_REPO_DID=$(printf '%s\n' "$body" | jq -er '.repoDid') ;; 409) EMPTY_REPO_DID=$(curl -fsS \ "${PDS_URL}/xrpc/com.atproto.repo.getRecord?repo=${BOB_DID}&collection=sh.tangled.repo&rkey=empty-repo" \ | jq -er '.value.repoDid') ;; *) fail "repo.create empty-repo: HTTP $status: $body" ;; esac printf '[knot] empty-repo = %s\n' "$EMPTY_REPO_DID" >&2 put_record "$REPO_EMPTY" "$(jq -nc \ --arg knot "$KNOT_HOSTNAME" \ --arg repoDid "$EMPTY_REPO_DID" \ --arg createdAt "$CREATED_AT" \ '{knot:$knot, name:"empty-repo", repoDid:$repoDid, createdAt:$createdAt}')" >/dev/null # --- issue fixtures --- put_record "$ISSUE_1" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'Repo tree does not render symlinks' \ --arg body "$(cat <<'MARKDOWN' Symlinked entries in the file tree show up with a blank size and a link that 404s. Expected them to render with the target path, the way a plain file does. Reproduced on the default branch with `contrib/` present. MARKDOWN )" \ --arg createdAt '2025-09-22T10:14:35Z' \ '{repo:$repo, title:$title, body:$body, createdAt:$createdAt}')" >/dev/null ISSUE_2_CID=$(put_record "$ISSUE_2" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'Document the knot bootstrap flow' \ --arg body "$(cat <<'MARKDOWN' ## What's missing > blah blah blah ``` I won't let claude to yap some nonsense here. ``` --- something _something_ MARKDOWN )" \ --arg createdAt '2025-09-22T10:15:35Z' \ '{repo:$repo, title:$title, body:$body, createdAt:$createdAt}')") put_record "$ISSUE_3" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'Clone over ssh fails on first push' \ --arg body "$(cat <<'MARKDOWN' Fresh clone, first push to a branch nobody has pushed before, and the remote hangs up: ``` $ git push origin sl/my-branch Enumerating objects: 12, done. remote: error: cannot lock ref 'refs/heads/sl/my-branch' To git@knot.tngl.boltless.dev:alice.pds.tngl.boltless.dev/core ! [remote rejected] sl/my-branch -> sl/my-branch (failed to update ref) ``` Second attempt succeeds, so it looks like a race in the hook rather than a permissions problem. MARKDOWN )" \ --arg createdAt '2025-09-22T10:16:35Z' \ '{repo:$repo, title:$title, body:$body, createdAt:$createdAt}')" >/dev/null put_record "$STATE_1" "$(jq -nc \ --arg issue "$ISSUE_1" \ --arg state "sh.tangled.repo.issue.state.closed" \ --arg createdAt '2025-09-22T10:17:35Z' \ '{issue:$issue, state:$state, createdAt:$createdAt}')" >/dev/null # --- pull request fixtures --- PULL_BASE=01117fddd3502c57bc20a65a75a66a4900d5d67d PULL_HEAD_1=f3c356fc8da46818a8ab0c24f22ee61854fe4b5f PULL_HEAD_2=9a925efef6a0e6dfcd2d4317b4a1eee8752928b8 patch_blob() { pb_patch=$(mktemp) curl -fsS "${KNOT_URL}/xrpc/sh.tangled.repo.compare?repo=${REPO_DID}&rev1=${PULL_BASE}&rev2=$1" \ | jq -je '.patch | select(length > 0)' >"$pb_patch" || fail \ "repo.compare $PULL_BASE..$1 didn't give back any patch text." \ ' An empty patch uploads as a valid blob, so the seeded rounds would show an empty diff.' \ " Both revisions have to be in the clone the knot took from $CORE_SOURCE." gzip -c "$pb_patch" | curl -fsS --data-binary @- \ -H "Content-Type: application/gzip" \ -H "Authorization: Bearer ${OWNER_JWT}" \ "${PDS_URL}/xrpc/com.atproto.repo.uploadBlob" \ | jq -ec '.blob' || fail "uploadBlob for the $1 patch failed." rm -f "$pb_patch" } ROUND_1=$(patch_blob "$PULL_HEAD_1") ROUND_2=$(patch_blob "$PULL_HEAD_2") put_record "$PULL_1" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'legacy PR' \ --arg body 'two rounds, each a gzipped format-patch blob' \ --argjson round1 "$ROUND_1" \ --argjson round2 "$ROUND_2" \ --arg createdAt1 '2025-09-22T10:40:35Z' \ --arg createdAt2 '2025-09-22T10:41:35Z' \ '{title:$title, body:$body, createdAt:$createdAt1, source:{repo:$repo, branch:"sl/ref-based-pr"}, target:{repo:$repo, branch:"master"}, rounds:[{patchBlob:$round1, createdAt:$createdAt1}, {patchBlob:$round2, createdAt:$createdAt2}]}')" >/dev/null keep_commit() { kc_did=${3#at://} kc_did=${kc_did%%/*} kc_token=$(service_token "$(login "$kc_did")" sh.tangled.git.keepCommit "$KNOT_DID") kc_kept=$(curl -sS -w '\n%{http_code}' \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $kc_token" \ -d "$(jq -nc --arg repo "$1" --arg oid "$2" --arg record "$3" \ '{repo:$repo, record:$record, source:{"$type":"sh.tangled.git.keepCommit#commit", repo:$repo, oid:$oid}}')" \ "${KNOT_URL}/xrpc/sh.tangled.git.keepCommit") kc_status=$(printf '%s\n' "$kc_kept" | tail -n1) [ "$kc_status" = 200 ] || fail \ "keepCommit $2 for $3: HTTP $kc_status: $(printf '%s\n' "$kc_kept" | sed '$d')" \ ' The record cites this commit, and a commit the knot never kept is one it may gc.' printf '[keep] %s %s\n' "$2" "$3" >&2 } keep_commit "$REPO_DID" "$PULL_HEAD_1" "$PULL_2" keep_commit "$REPO_DID" "$PULL_HEAD_2" "$PULL_2" put_record "$PULL_2" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'ref based PR' \ --arg body 'something something markdown' \ --arg head1 "$PULL_HEAD_1" \ --arg head2 "$PULL_HEAD_2" \ --arg base "$PULL_BASE" \ --arg createdAt1 '2025-09-22T10:42:35Z' \ --arg createdAt2 '2025-09-22T10:43:35Z' \ '{title:$title, body:$body, createdAt:$createdAt1, source:{repo:$repo, branch:"sl/ref-based-pr"}, target:{repo:$repo, branch:"master"}, rounds:[], versions:[{head:$head1, base:$base, createdAt:$createdAt1}, {head:$head2, base:$base, createdAt:$createdAt2}]}')" >/dev/null # a stale PR: sv-fe has moved on, so this base is no longer an ancestor of it PULL_3_BASE=a30e171f6297ac08777ce78bfea3fc9de27be8be PULL_3_HEAD=7a486c07f881800c5dd5eec98dfb5fb10d96bddc keep_commit "$REPO_DID" "$PULL_3_BASE" "$PULL_3" keep_commit "$REPO_DID" "$PULL_3_HEAD" "$PULL_3" put_record "$PULL_3" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'Add profile activity feed' \ --arg body "$(cat <<'MARKDOWN' Fixture for a PR whose merge-base went stale because the *target* branch moved, not the source: 1. `sv-fe` branches off `master` 2. `dwn/profile-activity` branches off `sv-fe` — merge-base recorded as `a30e171f` 3. `sv-fe` is rebased wholesale onto a newer `master` 4. `a30e171f` is no longer an ancestor of `sv-fe`, so the recorded base is stale This is why the diff says 62 files changed while merge-check says 1155 conflicted files MARKDOWN )" \ --arg head "$PULL_3_HEAD" \ --arg base "$PULL_3_BASE" \ --arg createdAt '2025-09-22T10:44:35Z' \ '{title:$title, body:$body, createdAt:$createdAt, source:{repo:$repo, branch:"dwn/profile-activity"}, target:{repo:$repo, branch:"sv-fe"}, rounds:[], versions:[{head:$head, base:$base, createdAt:$createdAt}]}')" >/dev/null # a long-running PR: four versions, each rebased onto a target that kept moving PULL_4_BASE_1=912ecc1546e8a3698d7d68a9120ddc78fbf934d9 PULL_4_HEAD_1=0e96abbc399191e105951f0fc3338b7936451ca5 PULL_4_BASE_2=8aec7d7c0e1eedf7dbbbe89f6d4f6048e8eb4eef PULL_4_HEAD_2=778fe6786af294b13c74ee45973cf6a8791dd0bb PULL_4_BASE_3=928e8aca42bbbb8514cd666a780ac12f340fff96 PULL_4_HEAD_3=a30623cbe5ea1e0e945cc94d8e9529320fe25577 PULL_4_BASE_4=244eca1ec71c6c236118634ca3a6194a39f42246 PULL_4_HEAD_4=5d79aca95ea16e4f5e46bb3e5e00e7775252ba8e keep_commit "$REPO_DID" "$PULL_4_BASE_1" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_HEAD_1" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_BASE_2" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_HEAD_2" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_BASE_3" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_HEAD_3" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_BASE_4" "$PULL_4" keep_commit "$REPO_DID" "$PULL_4_HEAD_4" "$PULL_4" PULL_4_CID=$(put_record "$PULL_4" "$(jq -nc \ --arg repo "$REPO_DID" \ --arg title 'lexicons: Tangled 1.0' \ --arg body "$(cat <<'MARKDOWN' I made few opinionated questionable choices here. See the commit message for each changes. It might be helpful for ask AI agent to summarize all changes. Included: - User-owned record lexicons - Repo-owned record lexicons - Spindle xrpc lexicons (ci, webhook, secret) Knot xrpc lexicons are not included. We can finalize them after playing with COBs. MARKDOWN )" \ --arg base1 "$PULL_4_BASE_1" \ --arg head1 "$PULL_4_HEAD_1" \ --arg base2 "$PULL_4_BASE_2" \ --arg head2 "$PULL_4_HEAD_2" \ --arg base3 "$PULL_4_BASE_3" \ --arg head3 "$PULL_4_HEAD_3" \ --arg base4 "$PULL_4_BASE_4" \ --arg head4 "$PULL_4_HEAD_4" \ --arg createdAt1 '2026-08-14T07:04:02.394Z' \ --arg createdAt2 '2026-08-19T13:00:41.843Z' \ --arg createdAt3 '2026-09-04T19:06:18.222Z' \ --arg createdAt4 '2026-09-14T07:09:10.682Z' \ '{title:$title, body:$body, createdAt:$createdAt1, source:{repo:$repo, branch:"sl/org-tangled"}, target:{repo:$repo, branch:"sv-fe"}, rounds:[], versions:[{head:$head1, base:$base1, createdAt:$createdAt1}, {head:$head2, base:$base2, createdAt:$createdAt2}, {head:$head3, base:$base3, createdAt:$createdAt3}, {head:$head4, base:$base4, createdAt:$createdAt4}]}')") # --- string fixtures --- STRING_CODE_CID=$(put_record "$STRING_CODE" "$(jq -nc \ --arg filename 'sieve.go' \ --arg description 'primes below n, no allocations past the sieve' \ --arg contents "$(cat <<'CODE' package main import "fmt" func sieve(n int) []int { composite := make([]bool, n+1) var primes []int for i := 2; i <= n; i++ { if composite[i] { continue } primes = append(primes, i) for j := i * i; j <= n; j += i { composite[j] = true } } return primes } func main() { fmt.Println(sieve(50)) } CODE )" \ --arg createdAt '2025-09-22T10:18:35Z' \ '{filename:$filename, description:$description, contents:$contents, createdAt:$createdAt}')") put_record "$STRING_MARKDOWN" "$(jq -nc \ --arg filename 'notes.md' \ --arg description 'scratch notes from bringing up the local stack' \ --arg contents "$(cat <<'MARKDOWN' # Local stack notes Things that bit me, in order: 1. the dev CA has to be in the **system** trust store, not just the browser 2. `TANGLED_APPVIEW_HOST` must be a loopback IP, not `localhost` 3. the knot wants `/shared/owner-did` before it will start ## Handy ```sh docker compose logs -f appview | grep ingest ``` Still unclear: how [spindle](https://tangled.org/tangled.org/core) picks up `.tangled/workflows`. MARKDOWN )" \ --arg createdAt '2025-09-22T10:19:35Z' \ '{filename:$filename, description:$description, contents:$contents, createdAt:$createdAt}')" >/dev/null put_record "$STRING_TEXT" "$(jq -nc \ --arg filename 'todo.txt' \ --arg description '' \ --arg contents "$(cat <<'TEXT' - [ ] figure out why the first ssh push races - [x] get the local knot talking to the mirror - [ ] write up the bootstrap order somewhere findable - [ ] ask about symlinks in the tree view TEXT )" \ --arg createdAt '2025-09-22T10:20:35Z' \ '{filename:$filename, description:$description, contents:$contents, createdAt:$createdAt}')" >/dev/null # --- comment fixtures --- COMMENT_ISSUE_CID=$(put_record "$COMMENT_ISSUE" "$(jq -nc \ --arg subjectUri "$ISSUE_2" \ --arg subjectCid "$ISSUE_2_CID" \ --arg text "$(cat <<'MARKDOWN' Worth calling out the ordering explicitly — I lost an afternoon to the knot exiting because `/shared/owner-did` wasn't there yet. MARKDOWN )" \ --arg createdAt '2025-09-22T10:21:35Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')") put_record "$COMMENT_ISSUE_REPLY" "$(jq -nc \ --arg subjectUri "$ISSUE_2" \ --arg subjectCid "$ISSUE_2_CID" \ --arg replyUri "$COMMENT_ISSUE" \ --arg replyCid "$COMMENT_ISSUE_CID" \ --arg text 'Agreed. The compose `depends_on` encodes it already, just nowhere a human would look.' \ --arg createdAt '2025-09-22T10:22:35Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, replyTo:{uri:$replyUri, cid:$replyCid}, createdAt:$createdAt, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null COMMENT_STRING_CID=$(put_record "$COMMENT_STRING" "$(jq -nc \ --arg subjectUri "$STRING_CODE" \ --arg subjectCid "$STRING_CODE_CID" \ --arg text 'Starting the inner loop at `i*i` is the bit everyone forgets.' \ --arg createdAt '2025-09-22T10:23:35Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')") put_record "$COMMENT_STRING_REPLY" "$(jq -nc \ --arg subjectUri "$STRING_CODE" \ --arg subjectCid "$STRING_CODE_CID" \ --arg replyUri "$COMMENT_STRING" \ --arg replyCid "$COMMENT_STRING_CID" \ --arg text 'Only correct because the outer loop skips composites — otherwise it would miss factors.' \ --arg createdAt '2025-09-22T10:24:35Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, replyTo:{uri:$replyUri, cid:$replyCid}, createdAt:$createdAt, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null # review comments pinned to heads that later versions superseded: the embed keeps # them on their own commit while createdAt files them under the version they landed in COMMENT_PULL_V0_CID=$(put_record "$COMMENT_PULL_V0" "$(jq -nc \ --arg subjectUri "$PULL_4" \ --arg subjectCid "$PULL_4_CID" \ --arg repo "$REPO_DID" \ --arg oid "$PULL_4_HEAD_1" \ --arg text 'Splitting the user-owned and repo-owned records this early feels right, but `repo.declaration` naming the repo by DID means the slug lives in the rkey. Deliberate?' \ --arg createdAt '2026-08-15T09:12:00.000Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, pullRoundIdx:0, embed:{"$type":"sh.tangled.embed.commit", repo:$repo, commit:{"$type":"sh.tangled.git.oid", oid:$oid}}, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')") put_record "$COMMENT_PULL_V0_REPLY" "$(jq -nc \ --arg subjectUri "$PULL_4" \ --arg subjectCid "$PULL_4_CID" \ --arg replyUri "$COMMENT_PULL_V0" \ --arg replyCid "$COMMENT_PULL_V0_CID" \ --arg repo "$REPO_DID" \ --arg oid "$PULL_4_HEAD_1" \ --arg text 'Deliberate. The rkey is the slug so a rename is a record move, not a field edit.' \ --arg createdAt '2026-08-15T10:03:00.000Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, replyTo:{uri:$replyUri, cid:$replyCid}, createdAt:$createdAt, pullRoundIdx:0, embed:{"$type":"sh.tangled.embed.commit", repo:$repo, commit:{"$type":"sh.tangled.git.oid", oid:$oid}}, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null put_record "$COMMENT_PULL_V1" "$(jq -nc \ --arg subjectUri "$PULL_4" \ --arg subjectCid "$PULL_4_CID" \ --arg repo "$REPO_DID" \ --arg oid "$PULL_4_HEAD_2" \ --arg text 'The spindle secret lexicons landed in this round — worth a line in the description saying they are xrpc-only and never hit a repo.' \ --arg createdAt '2026-08-20T08:45:00.000Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, pullRoundIdx:1, embed:{"$type":"sh.tangled.embed.commit", repo:$repo, commit:{"$type":"sh.tangled.git.oid", oid:$oid}}, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null put_record "$COMMENT_PULL_V2" "$(jq -nc \ --arg subjectUri "$PULL_4" \ --arg subjectCid "$PULL_4_CID" \ --arg repo "$REPO_DID" \ --arg oid "$PULL_4_HEAD_3" \ --arg text 'Rebase looks clean. Only thing still open from my side is whether the knot xrpc lexicons block this or ride a follow-up.' \ --arg createdAt '2026-09-05T11:20:00.000Z' \ '{subject:{uri:$subjectUri, cid:$subjectCid}, createdAt:$createdAt, pullRoundIdx:2, embed:{"$type":"sh.tangled.embed.commit", repo:$repo, commit:{"$type":"sh.tangled.git.oid", oid:$oid}}, body:{"$type":"sh.tangled.markup.markdown", text:$text, original:$text}}')" >/dev/null # --- reaction fixtures --- put_record "$REACT_ISSUE_A" "$(jq -nc \ --arg subject "$ISSUE_2" \ --arg reaction '🎉' \ --arg createdAt '2025-09-22T10:25:35Z' \ '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null put_record "$REACT_ISSUE_B" "$(jq -nc \ --arg subject "$ISSUE_2" \ --arg reaction '👀' \ --arg createdAt '2025-09-22T10:26:35Z' \ '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null put_record "$REACT_COMMENT_A" "$(jq -nc \ --arg subject "$COMMENT_ISSUE" \ --arg reaction '👍' \ --arg createdAt '2025-09-22T10:27:35Z' \ '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null put_record "$REACT_COMMENT_B" "$(jq -nc \ --arg subject "$COMMENT_STRING" \ --arg reaction '🚀' \ --arg createdAt '2025-09-22T10:28:35Z' \ '{subject:$subject, reaction:$reaction, createdAt:$createdAt}')" >/dev/null # --- follow fixtures --- put_record "$FOLLOW_ALICE_BOB" "$(jq -nc \ --arg subject "$BOB_DID" \ --arg createdAt '2025-09-22T10:29:35Z' \ '{subject:$subject, createdAt:$createdAt}')" >/dev/null put_record "$FOLLOW_ALICE_CHARLIE" "$(jq -nc \ --arg subject "$CHARLIE_DID" \ --arg createdAt '2025-09-22T10:30:35Z' \ '{subject:$subject, createdAt:$createdAt}')" >/dev/null put_record "$FOLLOW_BOB_ALICE" "$(jq -nc \ --arg subject "$OWNER_DID" \ --arg createdAt '2025-09-22T10:31:35Z' \ '{subject:$subject, createdAt:$createdAt}')" >/dev/null put_record "$FOLLOW_CHARLIE_ALICE" "$(jq -nc \ --arg subject "$OWNER_DID" \ --arg createdAt '2025-09-22T10:32:35Z' \ '{subject:$subject, createdAt:$createdAt}')" >/dev/null put_record "$FOLLOW_DAVID_ALICE" "$(jq -nc \ --arg subject "$OWNER_DID" \ --arg createdAt '2025-09-22T10:33:35Z' \ '{subject:$subject, createdAt:$createdAt}')" >/dev/null put_record "$FOLLOW_DAVID_BOB" "$(jq -nc \ --arg subject "$BOB_DID" \ --arg createdAt '2025-09-22T10:34:35Z' \ '{subject:$subject, createdAt:$createdAt}')" >/dev/null # --- vouch fixtures (subject is the rkey, not a record field) --- put_record "$VOUCH_ALICE_BOB" "$(jq -nc \ --arg reason 'Reviewed his bootstrap-ordering work on core, knows the stack.' \ --arg evidence "$COMMENT_ISSUE" \ --arg createdAt '2025-09-22T10:35:35Z' \ '{kind:"vouch", reason:$reason, evidences:[$evidence], createdAt:$createdAt}')" >/dev/null put_record "$VOUCH_BOB_CHARLIE" "$(jq -nc \ --arg reason 'Worked together on the knot ACL rewrite.' \ --arg createdAt '2025-09-22T10:36:35Z' \ '{kind:"vouch", reason:$reason, createdAt:$createdAt}')" >/dev/null put_record "$VOUCH_CHARLIE_DAVID" "$(jq -nc \ --arg reason 'Long-time collaborator, reviews carefully.' \ --arg createdAt '2025-09-22T10:37:35Z' \ '{kind:"vouch", reason:$reason, createdAt:$createdAt}')" >/dev/null put_record "$VOUCH_DAVID_ALICE" "$(jq -nc \ --arg reason 'Runs the knot, has never lost a repo.' \ --arg createdAt '2025-09-22T10:38:35Z' \ '{kind:"vouch", reason:$reason, createdAt:$createdAt}')" >/dev/null put_record "$VOUCH_BOB_DAVID" "$(jq -nc \ --arg reason 'Force-pushed over master twice in one week.' \ --arg createdAt '2025-09-22T10:39:35Z' \ '{kind:"denounce", reason:$reason, createdAt:$createdAt}')" >/dev/null printf 'done.\n' >&2