#!/usr/bin/env bash # Create the work-item structure in Gitea: labels, milestones, and the board. # # tools/gitea-setup create anything missing (idempotent) # tools/gitea-setup --dry-run say what it would create, change nothing # # Needs a token with `write:issue`. The existing git credential is scoped # `write:repository`, which pushes fine and is REFUSED by every issue endpoint -- # checked, not assumed: # # {"message":"token does not have at least one of required scope(s), # required=[read:issue], token scope=write:repository"} # # So this reads a SECOND token from ~/.sylph-gitea-api-token, deliberately # separate from the push credential: different blast radius, and rotating one # does not break the other. # # ── Why Gitea rather than a new tracker ───────────────────────────────────── # # The failure this replaces is a 1,227-line hand-maintained `BLOCKED.md` whose # anti-staleness convention turned out constant by construction, plus 21 # inter-agent messages sent into a void with no delivery feedback. Both are # solved by items that live in a database with state, an owner and dependency # edges -- and Gitea is already deployed here, so it adds no second store to # drift out of sync with the first. That drift is this project's defining # failure mode; adding a tool with its own copy of the truth would be choosing # more of it. set -euo pipefail HOST="${SYLPH_GITEA_HOST:-git.mc02.dev}" REPO="${SYLPH_GITEA_REPO:-fabi/Sylpheed}" TOKFILE="${SYLPH_GITEA_API_TOKEN:-$HOME/.sylph-gitea-api-token}" DRY=0; [ "${1:-}" = "--dry-run" ] && DRY=1 [ -f "$TOKFILE" ] || { cat >&2 < Applications -> Generate New Token Scopes needed: write:issue (and read:repository, to see the repo) Then: echo '' > $TOKFILE && chmod 600 $TOKFILE This is NOT the push credential. That one is scoped write:repository and is refused by every issue endpoint. EOF exit 2 } TOK=$(tr -d '[:space:]' < "$TOKFILE") API="https://$HOST/api/v1/repos/$REPO" AUTH="Authorization: token $TOK" api() { curl -sS --max-time 30 -H "$AUTH" -H 'Content-Type: application/json' "$@"; } # Fail loudly and specifically on the one error everyone hits. probe=$(api "$API/labels" || true) case "$probe" in *'required scope'*) echo "🔴 the token at $TOKFILE lacks issue scope:" >&2 echo " $probe" >&2 echo " Regenerate it with write:issue." >&2 exit 2 ;; '') echo "🔴 no response from $API -- host or network" >&2; exit 2 ;; esac say() { [ "$DRY" = 1 ] && echo " would create $*" || echo " created $*"; } # ── Labels ────────────────────────────────────────────────────────────────── # The state set encodes the working model the human set on 2026-09-02: a human # defines a bundle, agents decompose it, and each item ends in a HUMAN check. # `needs-human` is the important one -- it is the state the whole model turns on # and the one no off-the-shelf agent tool models, because the market has # converged on removing the human rather than gating on them. existing=$(printf '%s' "$probe" | python3 -c "import json,sys;print('\n'.join(l['name'] for l in json.load(sys.stdin)))" 2>/dev/null || true) mklabel() { # name colour description printf '%s\n' "$existing" | grep -qxF "$1" && return 0 if [ "$DRY" = 0 ]; then api -X POST "$API/labels" -d "$(python3 -c " import json,sys; print(json.dumps({'name':sys.argv[1],'color':sys.argv[2],'description':sys.argv[3]}))" "$1" "$2" "$3")" >/dev/null fi say "label $1" } mklabel "state/proposed" "d4c5f9" "Agent proposed this item; awaiting the human's approval to start" mklabel "state/approved" "0e8a16" "Human approved the shape; an agent may start" mklabel "state/in-progress" "1d76db" "An agent is working it now" mklabel "state/needs-human" "fbca04" "Done as far as an agent can tell -- a person must look. The body says what to look at" mklabel "state/blocked" "b60205" "Waiting on another item; use the Depends-On field, not prose" mklabel "agent/decoder" "5319e7" "Owned by the Decoder (disc to meaning; runs the emulator)" mklabel "agent/port" "006b75" "Owned by the Port (disc to playable; no RE)" mklabel "kind/bundle" "c2e0c6" "A bundle the human defined; agents decompose it into items" mklabel "kind/item" "bfd4f2" "One unit of work, small enough to finish in a single session" mklabel "kind/ask" "e99695" "One agent asking the other for something it cannot answer in role" mklabel "kind/defect" "d93f0b" "Found by a play-test or a check" # ── Milestones = bundles ──────────────────────────────────────────────────── ms=$(api "$API/milestones?state=all" | python3 -c "import json,sys;print('\n'.join(m['title'] for m in json.load(sys.stdin)))" 2>/dev/null || true) mkms() { printf '%s\n' "$ms" | grep -qxF "$1" && return 0 if [ "$DRY" = 0 ]; then api -X POST "$API/milestones" -d "$(python3 -c " import json,sys; print(json.dumps({'title':sys.argv[1],'description':sys.argv[2]}))" "$1" "$2")" >/dev/null fi say "milestone (bundle) $1" } mkms "Menus" "The menu shell: title, main menu, submenus, navigation, audio." mkms "Title screen" "Title timing and animation: the sweep onset, the plate, what (A) does." mkms "Graphics pipeline" "Decoder: disc -> decode -> per-frame update -> submitted draws -> Canary -> screen." mkms "Infrastructure" "Containers, supervision, auth, work tracking. Not game work." echo if [ "$DRY" = 1 ]; then echo "dry run -- nothing was created." else echo "labels and bundles are in place at https://$HOST/$REPO/issues" echo # No board, and this used to say the opposite. Gitea's project board does not # follow labels, so it would be a SECOND copy of the state to hand-sync -- the # exact failure that produced a 1,227-line BLOCKED.md. Labels are the truth and # a saved issue filter gives the same view for nothing. Leaving the old # "remaining, by hand: Projects -> New Project" line here would have had the # tool instructing the reader to build the thing the doc argues against. echo "No project board, deliberately -- labels are the truth. See" echo "docs/agents/GITEA-SETUP.md Phase 4. Use a saved issue filter instead." fi