diff --git a/docs/agents/WORKFLOW-gitea.md b/docs/agents/WORKFLOW-gitea.md new file mode 100644 index 00000000..e645999c --- /dev/null +++ b/docs/agents/WORKFLOW-gitea.md @@ -0,0 +1,133 @@ +# The working surface: Gitea issues, pull requests, and where things live + +**Set by the human, 2026-09-04.** Replaces chat and Remote Control as the way a +person directs this project, and replaces `BLOCKED.md` as the way agents track +what is open. + +## Why not a new tool + +We looked. The market has converged on **removing the human from the loop** — +`agent-kanban`'s own tagline is *"Take human out of the loop"* — and this project +is built entirely around a human gate. Meanwhile every candidate adds a second +store of truth to keep in sync with git, and **documents drifting out of sync is +this project's defining failure mode**: a 1,227-line `BLOCKED.md` whose +anti-staleness convention was constant by construction, 41 % of citations not +resolving, 21 inter-agent messages sent into a void with no delivery feedback. + +Gitea is already deployed, already holds the code, and its first-party MCP server +(`gitea/gitea-mcp` v1.7.0) exposes issues, labels, milestones, pull requests, +attachments and notifications. So: **no new store.** + +## The four surfaces, and what belongs in each + +| surface | holds | why not somewhere else | +|---|---|---| +| **Issues** | work items, asks between agents, defects | durable, stateful, owned, and **dependency edges close themselves** when the blocking issue closes — the thing prose could never do | +| **Pull requests** | every change to `main` | the human gate becomes **native** instead of a label convention | +| **Git (`docs/`)** | RE findings, decisions, evidence | a finding must be versioned **with the code that consumes it** | +| **Wiki** | orientation for a person: runbook, navigation, container notes | browsable and branch-independent, but **unreviewed** — see below | + +### Issues = bundles and items + +Milestones are **bundles** (the human defines them). Issues are **items** (agents +propose, the human approves). Labels carry the state: + +``` +state/proposed → state/approved → state/in-progress → state/needs-human → closed + ↘ state/blocked +``` + +`state/needs-human` is the one the whole model turns on, and the one no +off-the-shelf tool models. Its issue body must say **what to look at** and **what +pass and fail look like** — a person should be able to judge it in under a minute +without reading anything else. + +⚠️ **`state/blocked` uses Gitea's dependency edges, not prose.** *"The Port is +blocked on the Decoder answering X"* becomes a queryable link that resolves +itself. That is the single highest-value change here after PRs. + +### Pull requests = how work reaches `main` + +**Adopted 2026-09-04, the human's proposal, and it is a bigger improvement than +it looks.** Today agents commit to long-lived `auto/*` branches that a human +merges by hand — and those branches have drifted **280 and 373 commits** apart, +which is unreviewable by construction. + +One PR per item, closing its issue: + +* the review surface is a **diff in a browser**, not a human reading commits in a + terminal; +* `Closes #123` binds the change to the item, so "what did this fix" stops being + archaeology; +* **PRs enforce the sizing rule.** An item too big to review in one sitting was + too big to be an item. The discipline stops depending on an agent's judgement. + +🔴 **Agents must not merge their own pull requests.** The MCP's +`pull_request_write` includes `merge`, so this cannot be left to instruction — +it goes in **branch protection on `main`**, requiring review. Same principle that +fixed the build-jobs cap: policy belongs where the agent cannot reach it, not in +a document asking it not to. + +### 🔴 The wiki is NOT for the RE corpus + +The human suggested it for RE findings. **Half right, and the wrong half is worth +saying plainly**, because it would undo two things we paid for: + +1. **A finding's value is that it sits next to its evidence, versioned with the + code that consumes it.** *"Decoded, with a disc-wide check"* is backed by a + test in this repository. A wiki is a **separate git repo**, so a decode + correction and the exporter change that depends on it could never be one + atomic commit, or one reviewable PR. +2. **Wiki edits bypass review.** The `REFUTED.md` R1 reclassification changed the + file both agents read to decide what *not* to try. It was a reviewed commit + with a stated rationale. As a wiki edit it would have been an unreviewed + mutation of shared ground truth by whoever typed last. + +So the corpus stays in `docs/`, reached through PRs. + +**What the wiki IS good for** — human-facing orientation that is not evidence and +should not be branch-dependent: + +* the runbook (`docs/port/RUNNING.md`'s content — how to actually play the port) +* `docs/game/navigation.md` — how the game is navigated, written for a person +* container notes, credentials setup, the things a human reads once +* a landing page: current bundles, what each agent is on, links into git + +That last one addresses a real gap: **there is no view of what is happening** +except container logs and multi-megabyte transcripts. + +### Where files go — three needs, three homes + +Currently everything transient goes to `/exchange`, and a human cannot browse it +at all. + +| the file is | goes to | +|---|---| +| agent → agent, transient, no human involved | **`/exchange`** via `share`, unchanged — it records sender, time, commit and dirty-tree | +| **evidence a human must look at** (a screenshot, a film, a capture behind a `state/needs-human` item) | **attached to the issue** it is evidence for | +| evidence a finding cites | **git**, beside the finding. It is the proof | + +Attaching to the issue is strictly better than both alternatives for the middle +case: it travels with the item, a person sees it in the browser, and it cannot be +orphaned from the claim it supports. + +⚠️ The MCP exposes `attachment_read` only — **uploading needs a direct REST call** +(`POST /repos/{owner}/{repo}/issues/{index}/assets`). Worth a small helper rather +than each agent re-deriving it. + +### Notifications = the wake-up, with delivery you can inspect + +`notification_read` / `notification_write` replace the message channel that lost +**21 consecutive messages to a stale session ID with no error of any kind**. An +`@mention` on an issue is durable, addressed by *name*, and has a read state a +supervisor can inspect. The old rule still stands and gets easier: **the message +carries a pointer — now an issue number — and the repository holds what was +found.** + +## What does not change + +* Findings are still classified **decoded / measured / undecodable**. +* An agent still cannot verify its way out of its own role. +* `REFUTED.md` is still the file to grep before proposing anything, and entries + still name their `⟨instrument⟩`. +* The oracle is still the real game in Xenia Canary. diff --git a/tools/gitea-setup b/tools/gitea-setup new file mode 100755 index 00000000..ebff4650 --- /dev/null +++ b/tools/gitea-setup @@ -0,0 +1,119 @@ +#!/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 + echo "Remaining, by hand in the web UI (the API for Projects lags it):" + echo " Projects -> New Project -> columns:" + echo " Proposed | Approved | In progress | Needs human | Done" +fi