Two things that read as protection while being none. Phase 2's rule binds everyone who reaches Gitea through the API or the web, and does not bind anyone with `gitea admin` in the container -- which includes the supervising agent that created the agent accounts and minted their tokens. From that shell the rule is editable and an admin token is one command away. That is the boundary of what the phase buys, not a hole to plug there, and the document read as though the gate were universal. Phases 1 and 2 gate the two CONTAINERISED agents, whose design assumption is that policy lives where they cannot reach it; a supervisor with a host shell is not in that set. And `gitea-setup` finished by telling the reader to go and build a Gitea project board by hand, four sections after the doc explains that a board is a second copy of the state to hand-sync and is precisely the failure that produced a 1,227-line BLOCKED.md. A tool instructing you to do the thing its own documentation argues against is the drift this whole surface exists to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
125 lines
6.4 KiB
Bash
Executable File
125 lines
6.4 KiB
Bash
Executable File
#!/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 <<EOF
|
|
gitea-setup: no API token at $TOKFILE
|
|
|
|
Create one in Gitea: Settings -> Applications -> Generate New Token
|
|
Scopes needed: write:issue (and read:repository, to see the repo)
|
|
Then: echo '<token>' > $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
|