agents: move the working surface to Gitea -- issues, PRs, and where files live
The human wants to direct this project from a web UI rather than chat or Remote
Control, so Gitea becomes the working surface. No new store: adding a second
copy of the truth is this project's defining failure mode, and Gitea already
holds the code. Its first-party MCP server (gitea/gitea-mcp v1.7.0, checked) has
issues, labels, milestones, PRs, attachments and notifications.
ISSUES replace BLOCKED.md. Milestones are bundles the human defines; issues are
items agents propose and the human approves. The state labels end in
`needs-human`, which is the state the whole model turns on and the one no
off-the-shelf tool models -- the market has converged on removing the human.
`blocked` uses Gitea's DEPENDENCY EDGES rather than prose, so "the Port is
blocked on the Decoder answering X" becomes queryable and closes itself.
PULL REQUESTS, the human's proposal, adopted -- and a bigger improvement than it
looks. Today's long-lived auto/* branches have drifted 280 and 373 commits apart,
which is unreviewable by construction. One PR per item makes the human gate
NATIVE rather than a label convention, binds the change to its item, and enforces
the sizing rule: an item too big to review in one sitting was too big to be an
item.
🔴 Agents must not merge their own PRs, and pull_request_write includes merge --
so this goes in BRANCH PROTECTION on main, not in a document asking them not to.
Same principle that fixed the build-jobs cap: policy where the agent cannot reach
it.
WIKI -- the human suggested it for RE findings, and that half is declined with
reasons. A finding's value is that it sits beside its evidence, versioned with
the code that consumes it; the wiki is a separate git repo, so a decode
correction and the exporter change depending on it could never be one reviewable
PR. And wiki edits bypass review: the REFUTED.md reclassification changed the
file both agents read to decide what not to try, and as a wiki edit it would have
been an unreviewed mutation of shared ground truth. The wiki takes human-facing
orientation instead -- runbook, navigation, container notes, and a landing page,
which closes the real gap that there is no view of what is happening except
container logs.
FILES: three needs, three homes. Agent-to-agent transient stays in /exchange.
Evidence a HUMAN must look at attaches to the issue it belongs to -- it travels
with the item and cannot be orphaned from the claim. Evidence a finding cites
stays in git. Note the MCP exposes attachment_read only; upload needs a direct
REST call.
tools/gitea-setup creates the labels and bundles, idempotently, with --dry-run.
Blocked on a token with write:issue -- the push credential is write:repository
and every issue endpoint refuses it, checked rather than assumed.
This commit is contained in:
133
docs/agents/WORKFLOW-gitea.md
Normal file
133
docs/agents/WORKFLOW-gitea.md
Normal file
@@ -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.
|
||||
119
tools/gitea-setup
Executable file
119
tools/gitea-setup
Executable file
@@ -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 <<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
|
||||
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
|
||||
Reference in New Issue
Block a user