Files
chemenu/instructions/dev/issue-tracking.md
T
torben 87a47cc237
CI / verify (push) Successful in 56s
Release / release (push) Successful in 35s
instructions/dev/issue-tracking.md: destructive-step invariants, comment-vs-body authority, rename sweep (#47 Block 1, #29)
Files changed:
- CHANGES.md
- VERSION
- instructions/dev/issue-tracking.md
2026-09-04 14:59:30 +02:00

285 lines
16 KiB
Markdown

---
type: types/instruction.md
name: issue-tracking
description: Where open work on this stack is tracked, what the four mandatory area/kind/prio/size labels and the two status flags on a Gitea issue mean, and how to keep an issue body current across sessions.
---
# Track open work as Gitea issues, not as prose in the repo
Open work on this stack lives at
<https://gitea.nehmer.net/torben/chemenu/issues>, one issue per work
package, and nowhere else. There is no `TODO.md`; there was, and every item in
it either became an issue or was already one, described twice.
That is the whole reason for this file: a second list is a second thing to
maintain, and the one that drifts is always the one nobody reads first. The
issue tracker wins that comparison outright - it has state, comments, labels,
and a link that survives the change it describes. A markdown file in the repo
has none of it, and it costs a publish to touch.
This instruction exists only in the dev repo. A distributed instance has no
issues at that URL, which is exactly why `dist export` excludes
`instructions/dev/` wholesale (see [tools/CONTRACT.md](../../tools/CONTRACT.md)).
## When to run
- Something is worth doing but not now. Open an issue; do not write it down in
the repo.
- A session's findings outgrow the change it was making - a gap in the tooling,
an assumption nobody has checked, a decision that needs the user.
- Picking an issue up: before doing anything else, read the body as the current
spec, and re-label it if the ground has moved since.
- **While working on one:** the body is updated as the state moves, not at the
end (step 2). A session that is interrupted leaves the body as its handover.
- Prioritising: deciding what to pick up next, or re-labelling after the ground
moved.
- Closing one: the body is rewritten to its final state first, and only then
closed (step 7).
- A rename or move ships: sweep the open issues for text that assumed the old
name or path (§ Renames and other decay in the tracker).
## Steps
1. **Write the issue so it survives without you.** What is broken or missing,
why it matters, what "done" looks like as acceptance criteria, and the
specific files or commands involved. An issue that only makes sense to
whoever wrote it is a note, and notes were the problem.
**Destructive steps carry the invariant they must not violate.** A body
that prescribes a mechanism gets built as prescribed - including its
bugs. Where a step deletes, overwrites, resets or moves, name the
property that must still hold afterwards, not only the command that gets
there. "Remove the working directory, then `git checkout HEAD --
<stage>`" is a mechanism; "the content stages must afterwards match
`HEAD` exactly, without any untracked or ignored file being touched" is
the same instruction plus its test - a build instruction and an
acceptance criterion at once, so the defect surfaces while the test is
written rather than in review afterwards. #30's `upstream merge` body
wrote the mechanism and got exactly that bug: a working-directory removal
that took a stage's gitignored, unrecoverable data with it.
**An acceptance criterion states a checkable property, not an activity.**
"Implement X" is done when someone says so; "after `upstream merge`,
`reports/` still holds every file it held before" is done when it is
true. This is not a ban on imperative steps - a numbered procedure can
still produce a correct control flow, and that is its merit - it binds
the destructive steps, and every box in the criteria list.
2. **The body is the working state, not a historical first post - keep it
current as you go.** It is this stack's plan file: the same thing a harness's
own plan document is, and it is maintained the same way. Not written once,
not brought up to date at the end, but **updated whenever something in it
stops being true** - a decision made, a criterion met, an approach ruled out,
a new constraint found.
The test is an abort, not a milestone. A session can end at any moment - an
interrupt, a context limit, a crash, a human walking away - and whatever the
body says at that instant is the entire handover. So the standard is: **at
every point, a fresh session must be able to open the body and pick the work
up from there**, without a human re-explaining it and without reading back
through the comments. If the body would mislead someone who read it right
now, it is already out of date, whether or not the work is finished.
That means updating *during* the work, not only at its end:
- a decision gets made → the decision and its reasoning replace the question
- an acceptance criterion is done → tick it, in the same session that did it
- something turns out differently than the issue assumed → the assumption is
corrected where it stands, not contradicted three paragraphs later
- work is deferred or dropped → say so, with the reason, where the criterion is
**Rewrite, never append.** Do not add to a text that has become wrong: an
additively grown log forces every later reader to reconstruct the current
state by filtering the whole history, which is the exact cost the body exists
to remove. Comments carry the history (step 3); the body carries the state.
Body rewrites and comments are an LLM session's job. A human normally
touches only labels and metadata directly.
**Reading an issue, the body is the state and comments are history.** A
session picking an issue up reads the body as the spec; comments are read
for provenance - why something was decided, what was tried - never as
the current instruction. A recommendation in a comment can be older than
the body's decision and read just as convincingly: on #30 an earlier
comment recommended a smaller, `verify`-only command, while the body had
since settled on building the full `merge` command. A session trusting
the comment would have built the wrong thing, with a plausible
justification out of this repo's own tracker.
**A body that is demonstrably wrong is corrected first, not worked
around.** "Body beats comment" is a rule of precedence, not a licence to
execute a stale spec. Where a comment or the tree proves a claim in the
body false, the body is rewritten before the work starts - the rewrite
above is the fix; leaning on the comments as the "real" state is not.
#10 is the case: its body claimed coverage had never been measured while
three comments carried a percentage, a statement count and a CI run
number.
**Where two comments contradict each other, evidence decides, not
recency.** On #10, one comment showed a retrieved artifact with zero
items on a finished run - the report was not actually retrievable - and
a later comment declared the same criterion met without re-checking. The
later comment is not the newer truth, only the unchecked one. Resolve it
into the body with the evidence named, or mark the point open.
3. **Comment a changelog, never a copy.** A body rewrite gets one short comment
naming only what changed against the previous state - what is new, what is
gone, what was corrected. Do not snapshot the old body into a comment: a full
copy per revision forces a human to diff two prose texts, which is not a
readable history, only another copy.
One comment per *session's worth* of change, not per edit. Step 2 asks the
body to be kept current continuously, and a comment for every tick would bury
the board in noise; the changelog line summarises what that session moved.
Trivial upkeep - a typo, a tightened sentence - needs no comment at all.
```
**Changelog:** Decision 2 tightened - `kind/` may now change over an
issue's life. Old acceptance criterion 3 dropped (covered by #42).
```
4. **Give it all four mandatory labels: one `area/`, one `kind/`, one `prio/`,
one `size/`.** All four, always. Machine maintenance by an LLM session is
what makes four axes affordable - the original objection to a third and
fourth axis was the upkeep cost for a single human maintainer, and that
objection no longer holds.
| `area/` | Means |
|---|---|
| `area/kb` | The `kb/` schema, contract, confidence machinery, lint - the knowledge base as a system. |
| `area/distribution` | Shipping, upgrading and versioning an instance. |
| `area/corpus` | The content and scope of `kb/` in this instance, and the demo/testbed question. |
| `area/workflow` | Git, merging, branching, publish, PRs. |
| `area/process` | The development process itself, rather than the stack as an artefact. |
There is deliberately no `area/tools`: tooling is filed under the domain it
serves, not under where its code sits. The axis follows the stage split in
[AGENTS.md](../../AGENTS.md).
| `kind/` | Means |
|---|---|
| `kind/decision` | Waiting on an operator decision. |
| `kind/build` | Specified; waiting only on implementation time. |
| `kind/defect` | A finding: documentation and reality, or two documents, contradict each other. |
`kind/` is expected to change over an issue's life - `decision` becomes
`build` once the decision is made. That is session memory working, not a
labelling failure.
| `prio/` | Means |
|---|---|
| `prio/blocking` | Blocks or damages work in progress. Next. |
| `prio/planned` | Accrues interest. Planned. |
| `prio/waiting` | Worth doing, waiting on a trigger. |
`prio/waiting` is not a graveyard. It means the issue's value is real but
gated on something outside it - a decision, another issue, a second instance
existing. Name that trigger in the issue, or the label is a polite no.
| `size/` | Means |
|---|---|
| `size/S` | One session, one publish, a clear cut. |
| `size/M` | Several files; a contract or instruction change; its own test effort. |
| `size/L` | Several sessions, or open design questions before the first commit. |
Size is effort, not importance. A `prio/blocking size/S` is the best thing
on the board; a `prio/waiting size/L` is a thing to talk about before anyone
starts.
5. **Add a `status/` flag only when it applies.** Both are optional, because
each describes a temporary condition rather than a property every issue has.
| `status/` | Means |
|---|---|
| `status/blocked` | Waiting on another, still-open issue - not workable on its own, whatever its `prio/` says. Name the blocking issue in the body. |
| `status/unconfirmed` | A reported suspicion, not yet checked against actual behaviour. Applies to any `kind/`, not just `kind/defect`. |
While `status/unconfirmed` is set, `size/` and `prio/` are provisional. Triage
ends it one of two ways: the flag comes off and `size`/`prio` are set for
real, or the issue is closed with the reason. An unverified suspicion does not
stay open indefinitely - the process-level analogue of AGENTS.md invariant 3.
6. **Re-label when the ground moves, and say why in a comment.** A trigger that
fired turns `prio/waiting` into `prio/planned`. A design question that got
answered can drop a size and move `kind/decision` to `kind/build`. Silent
re-labelling is how a board stops meaning anything.
7. **Closing is the last body update, not a comment.** If step 2 was followed
the body is already nearly there, and closing only settles what the final
run established. If it was not, closing is where the whole debt comes due -
and it comes due at the worst moment, because a closed body is the version
everyone reads afterwards and nobody revisits.
Either way the body reaches its final state *before* the issue closes:
proposals that were decided read as decided, a "to decide" section has become
the decision with its reasoning, acceptance criteria are ticked or struck with
a reason, and what was verified is named. Then close, with the one-line
changelog comment step 3 asks for.
Record what actually happened, not a commit hash alone: which proposals were
implemented, which were deliberately left out and why, and what was verified.
The issue is the only place that record survives - a changelog entry says
what changed, not what was decided against.
**A closing report in a comment does not satisfy this.** It reads as
complete to whoever writes it and leaves a body still phrased as open work:
unticked boxes, an undecided decision section, present tense about a defect
that no longer exists. #44 closed exactly that way, with a thorough comment
above a body that still asked for a decision that had already been made and
shipped. Nothing mechanical catches it (see below), which is why it is a step
rather than a habit.
## Renames and other decay in the tracker
A rename is not finished when the tree is green. Renaming a package, a path,
a command, a flag or the repository itself moves text that lives outside the
working tree, and the open issues are the largest such text. Nothing catches
them - `wikitool` does not know this tracker exists and must not learn (see
"What no tool checks" below) - so a pass over the open issues is part of the
rename, in the session that did it, not a follow-up someone remembers.
Distinguish a wayfinder from a piece of evidence: a path meant to point at
where something *is* gets pulled through; a path quoted for what was true at
a time is left standing and dated. Note per corrected body what was pulled
through and when, so the next pass can tell a checked body from one that
merely looks right. Closed issues are out of scope - they guide nobody.
Renames are not the only thing that ages an issue text. A page a body cites
can vanish from `kb/` (`wikitool search` against the cited titles is the
second pass), and an old body can carry private infrastructure detail into
what is now a public tracker - both found in the same issue, both worth the
same look.
## What no tool checks
`wikitool` does not know this tracker exists, and should not learn. It ships to
instances that have no issues at that URL, while this file and the workflow it
describes are pruned by `dist export` - a Gitea client inside the shipped tool
would be a dev-only dependency carried by every instance, to check a board none
of them have. The tracker is reachable only through the `gitea-mcp` server, in a
session, by an agent.
So there is no `docs verify` for the board. Nothing reports a closed issue whose
body still reads as open, a body that contradicts its own comments, or an issue
missing one of the four mandatory labels. Every one of those is caught by a
session following this file, or not at all - which is the argument for the
sequence in step 7 being explicit about the order (body first, then close),
rather than leaving it to be inferred from step 2.
## Decision points
- **Issue or changelog?** An issue is work that is *not done*. `CHANGES.md` is
what shipped. A finished change needs both: the entry, and the issue closed
with the reasoning.
- **Issue or `kb/` page?** An issue is about *this stack* and is ephemeral - it
closes, and it records a wish. A `kb/` page is verified knowledge that stays
true. Never put wiki content findings in an issue, and never file a work item
as a page.
- **Rewrite the body, or add a comment?** Rewrite whenever a reader of the body
alone would otherwise be misled - a changed decision, a dropped criterion, a
new constraint. A comment carries the changelog line for that rewrite, and
nothing else that a future session needs in order to act. Closing an issue is
always a rewrite - see step 7.
- **An old issue carries only `prio/` and `size/`?** Complete it to all four
when you touch it, rather than in a sweep. The board reaches the new scheme
issue by issue, as each is picked up.