Two routes in. Both end at the same gate.
The ordinary route: I write an issue in an onboarded repository and put the label on it —
robot-army by default, [github] label to change it. The next poll sees it, checks the
issue is mine, and queues it.
Changing the label needs no manual step. Each repository’s saved ETag belongs to
the query built from the old label. The poll sees that the query has changed, skips the
ETag, and makes one full request, so issues with the new label show up on the next poll. It
used to send the old ETag anyway, and GitHub’s 304 hid every new issue without any error.
The github.poll record now says when this happens
(audit log).
Work already queued under the old label does not start. Each queued item’s issue is checked against the label that is configured now. One that no longer carries it is held until I label the issue or change the label back (when the label changes).
Two things are checked and neither can be turned off:
[github] author are ever dispatched. There is
deliberately no “any author” value. The check lives in one place, poll.evaluate, and
every path that can put an item in the queue goes through it — including retry, which
re-reads the issue from GitHub rather than trusting the body it stored.Being on a project board’s ready column is not an admission — see what runs next. The board narrows and orders; it never admits.
Optional, and absent by default — an installation with no [trello] section makes no
board request at all and behaves exactly as it did before this existed.
I put a card on a private Trello board from my phone, tag it, and it becomes a GitHub issue in the repository the card names. That issue is unlabelled, so nothing runs: labelling it is still the human gate, and the board cannot reach past it. The card then follows its issue — into the in-progress list while a session runs, into the done list when the issue closes, and back where it came from if the work is abandoned. If I have moved the card by hand since it was last placed, it stays where I put it, and gets a comment naming — by the column’s name — the list it would have gone to and why.
[trello]
board_id = "5f3a..." # required when the section is present
label = "AI-task" # the tag that marks a card as work
in_progress_list = "In Progress"
done_list = "Done"
ignore_lists = ["Icebox"] # columns whose cards are NOT intake; empty by default
poll_seconds = 300 # slower than GitHub's 60, deliberately
key_env = "TRELLO_API_KEY" # the NAME of a variable, never the value
token_env = "TRELLO_API_TOKEN" # or key_file / token_file, mode 0600
export TRELLO_API_KEY=... # https://trello.com/power-ups/admin
export TRELLO_API_TOKEN=...
uv run robot-army doctor # now checks the board too — every check must be green
uv run robot-army cards # what is on the board and what became of it
doctor verifies at startup, and refuses to ingest — not to run — if any of these fail:
ignore_lists exists. This one refuses for a reason worth stating:
a warning would leave intake silently widened back to what it was, the excluded column
would start filing issues, and nothing would look broken.The board’s member list is recorded and never gated on. Who else may see my own private board is my decision, and a second member can at most cause an unlabelled issue to be filed — only I can cause one to run.
ignore_lists names columns whose cards are not intake. A tagged card sitting in one
produces no issue, no comment and no move. Drag it out and it is picked up on the next poll —
no re-tag, no rescan, no restart.
That reversibility is the whole point. Before this, the only way to stop a tagged card being filed was to remove the tag: a one-way answer about what the card is, to a question that is almost always about when. Columns are where I already say when. So an icebox column is a parking space, and parking spaces have to work in both directions.
It gates intake only. A card that already has an issue is untouched in either direction —
its mapping, its session and its remaining board moves all continue — which is also why
listing in_progress_list or done_list here is harmless rather than contradictory: by the
time the daemon puts a card in either, that card is already linked.
That includes the one thing a linked card most often gets when it is dragged into an ignored
column: the “robot-army did not move this card” comment (003’s FR-030). Dragging a card
with work in flight into the icebox is the natural way to say stop, so the daemon still
declines to drag it on to Done or back to where it came from — and still says so on the card,
exactly as it would for any other column. Parking stops a card from becoming work; it does
not stop the daemon explaining a decision about work that already exists. #82 reported this
comment as missing; the daemon’s log from that run shows it posted, and
test_ignored_lists.py now pins it.
A parked card shows as parked in 'Icebox' in robot-army cards and on /cards, alongside
whatever else it is rather than instead of it: a card can be awaiting clarification and parked
at once, which is exactly what writing a vague card and shelving it produces. The poll record
counts them ({"tagged": 140, "ignored": 100, ...}) and logs one line when a card is parked
and one when it is released — not one per card per cycle, which on a full icebox would be the
majority of the log saying nothing happened.
Whether a card is parked is the running daemon’s call, not the listing’s. The daemon is
what excludes the card, so both listings judge it against the ignore list the daemon
publishes on its heartbeat, and fall back to their own configuration only when no daemon is
running. That matters because robot-army web reads its configuration once: before #74, a web
interface started before Icebox was ignored counted every card in it under awaiting
clarification, offered it a rescan, and never said it was parked. On /cards a parked card
is left out of that count and has no rescan button — moving it out of the column is what
brings it back.
When the two lists differ, both listings say so in one line:
IGNORE LIST MISMATCH: the running daemon is parking cards in ['Icebox'], and this process is
configured for []. …
It does not say which process is behind, because either can be — a web interface started before the edit, or a daemon nobody restarted after it. Restart whichever that is.
A card that names no onboarded repository, or names two, is held rather than guessed at.
It gets one comment saying what is missing, it appears in robot-army cards and on /cards
with its reason, and editing the card to name a repository — or adding the
robot-army: line — resolves it on the next pass with
no further action.
One hold is never the card’s fault. When no repository is onboarded at all — a fresh
installation, or one whose state database was lost, which takes every onboarding with it — no
card can resolve however well it is written. Those cards are held with a reason that says so and
names robot-army onboard, their comment says nothing on the card needs to change, and doctor
fails its onboarded repositories check. Onboarding the repository is the whole fix: a card held
for that reason is re-evaluated on the first pass after something is onboarded, without anyone
touching it. Until issue #83 these cards were told to edit themselves, with onboarded: none
buried mid-sentence as the only clue.
uv run robot-army cards --state needs_info
uv run robot-army rescan <card-id> # or --all-needs-info
A repository reference only counts if it is already onboarded. A card description is
often pasted from a log, and src/robot_army and docs/roadmap.md both look exactly like an
owner/name; filtering against the onboarded set means an unknown reference cannot select
anything, so the worst case is a held card rather than an issue filed somewhere I never named.
The reference can sit anywhere in the title or the description, and a card is prose: ending a
sentence with the repository is the ordinary way to write one. Punctuation after the reference
ends it rather than joining it, so Names jantman/demo. counts, as do a trailing comma, colon,
bracket, question mark, and a clone URL’s .git suffix on either spelling. Until issue #71
the full stop was read as part of the name, and such a card was held asking for the repository
it had just named — silently, since nothing can be filed in the wrong place by it.
Plenty of cards mention two or three onboarded repositories on purpose — a traceback from one and a link to the other is the ordinary shape of a card worth writing. Editing that text down until one reference survives throws away the context. Instead, put a line on the card that is nothing but this:
robot-army: jantman/demo
The reference takes any of the three spellings the rest of the card accepts — a GitHub URL, an
owner/name, or the local clone path — and the line may be written in backticks, in any case,
with whatever spacing. It decides, and every other repository reference on the card is
disregarded. It is an override rather than a tie-breaker on purpose: a line that only worked
when the system happened to be confused could never be tested, because I cannot see the
confusion until the card is already held.
Three things it deliberately does not do:
see robot-army: jantman/demo for context is prose and does nothing — though the mention of
jantman/demo still counts as a mention. Only a whole line counts.
The audit log records which way each card was decided: trello.evaluated carries
"source": "declaration" or "source": "scan" — or "source": "onboarding" when nothing was
onboarded to match against, and neither had a say.
Enforced by two unique indexes rather than by code that has to remember, so a path that skipped its check fails loudly instead of quietly duplicating. Creation is four steps with the intent written first, and every seam between them is separately resumable — including the dangerous one, where the issue exists and nothing local knows it yet.
If the database is lost entirely, each card’s own comment names its issue, and the next poll rebuilds the mapping from it rather than filing a second one. The one gap left open is a crash between creating the issue and recording it combined with losing the database, and it is written down in state rather than pretended away.
Onboarding does not come back with it, and that is on purpose: onboarding is consent, and
re-granting it from anything recovered would defeat the point of asking. Run robot-army
onboard again for each repository. Until then nothing is dispatched, doctor fails, and every
card is held saying so rather than asking to be edited.
Below live the number in that index is invented rather than GitHub’s — it starts at 900001,
high enough that no rehearsal number can be mistaken for a real issue in a log. It is allocated
above the highest already recorded for that repository, so a rehearsal files each card on its
first pass however many simulated cards the repository already holds, and restarting the daemon
continues the numbering rather than replaying it. purge-simulated clears those rows, and the
next simulated card is 900001 again.
Next: what runs next.