Make the issue tracker the source of truth #93

Merged
yonlu merged 2 commits from docs/issue-driven-workflow into main 2026-08-18 20:35:45 +00:00
Owner

Work has been starting from a chat message and a plan file, so two people could pick up the same thing and neither could see the other. This makes the tracker the source of truth, and moves everything unimplemented out of .planning/ and into it.

What is in it

Commit
chore(scripts): reach the issue tracker from the command line scripts/issue.sh + issue_fmt.py
docs: make the issue tracker the source of truth CLAUDE.md, .planning/ reorganisation

Both types release nothing, so this cuts no version.

The workflow

Search before starting; claim before the first edit, not before the commit — the point is that the work is visibly taken while it is being done. A claim is an assignee, Status/In Progress and a comment naming the branch, all three. If no issue covers the work, open one first. Findings get filed, including decisions not to do something.

The conventions were already right and are written down rather than reinvented: the Kind/Area/Priority/Platform/Reviewed/Status taxonomy and its exclusive scopes, #73 as the roadmap, real Gitea dependencies for hard blockers, and this repo's own PR #83 as the body shape.

Unchanged: the commit grammar, which is load-bearing for semantic-release. The issue number lives in the branch name and here, not in the commit subject.

.planning/ stops being a queue

It keeps design documents and measured history — NOTES.md, the audits, the completed plans and the arguments in them. plans/pending/ is gone, because a plan nobody is executing is an issue. Five plans moved to completed/, each stamped with which issue carries its remainder; autotag.md is kept as a historical record, marked stale where the scoring overhaul overtook it.

Filed from it, all with the argument preserved rather than a one-line summary:

Issue From
#85 warm the release cache for owned, not-known-complete albums plan 010 (deleted)
#86 six bound-but-uncalled backend methods — wire or remove plan 012, finding 3
#87 Android: tag writing has never been run on a device plan 016, B3
#88 run the credit ingest against the real dump and publish it plan 015 multi-artist
#89 materialise credits into file_artists — depends on #88 plan 015 multi-artist, phase 3
#90 autotag strict auto-accept as a background job autotag 011
#91 autotag settings panel — depends on #90 autotag 012

Five issues were closed that had already shipped

#83's Closes list named ten and exactly half of them fired. #13, #19, #26, #29 and #37 were in main for a fortnight and still open; each is now closed with a comment naming the commit that shipped it. That is why the workflow says closing is a step you take and verify, not a keyword you trust.

The wrapper, and what it pins

list mine search show new claim unclaim comment close label depends labels. Bodies cross to the API as JSON and never as shell, which is why the formatting half is its own Python file — same reasoning that keeps release notes out of gitea-release.sh's argument list.

Four faults found by self-testing it against the live tracker, all fixed and each pinned with its reason:

  • claim could half-claim — the assignee and label landed, then the comment was rejected as empty, leaving the issue saying it was taken without saying by what work. The comment is resolved before anything mutates.
  • label accepted names that do not exist. Gitea answers 200 and applies none of them, so a typo reported success and did nothing. Names resolve to ids first.
  • depends needs a whole IssueMeta, not an index. A body of {"index": 88} answers 404, which reads exactly like a Gitea build without the feature.
  • close drops Status/In Progress, or a claim outlives the work.

Verified

  • make skill-check — 47 documented make targets, all present (no new target added)
  • bash -n scripts/issue.sh, python3 -m py_compile scripts/issue_fmt.py
  • Every subcommand exercised against the live tracker, destructive ones on a scratch issue that was then deleted
  • commit-check on both commits via the commit-msg hook

No Go, TypeScript or SQL changed, so the app-facing suites are untouched by this branch — CI will confirm.

Closes #92

Work has been starting from a chat message and a plan file, so two people could pick up the same thing and neither could see the other. This makes the tracker the source of truth, and moves everything unimplemented out of `.planning/` and into it. **What is in it** | Commit | | |---|---| | `chore(scripts): reach the issue tracker from the command line` | `scripts/issue.sh` + `issue_fmt.py` | | `docs: make the issue tracker the source of truth` | CLAUDE.md, `.planning/` reorganisation | Both types release nothing, so this cuts no version. **The workflow** Search before starting; claim before the first edit, not before the commit — the point is that the work is visibly taken *while it is being done*. A claim is an assignee, `Status/In Progress` and a comment naming the branch, all three. If no issue covers the work, open one first. Findings get filed, including decisions not to do something. The conventions were already right and are written down rather than reinvented: the `Kind/Area/Priority/Platform/Reviewed/Status` taxonomy and its exclusive scopes, #73 as the roadmap, real Gitea dependencies for hard blockers, and this repo's own PR #83 as the body shape. Unchanged: the commit grammar, which is load-bearing for semantic-release. The issue number lives in the branch name and here, not in the commit subject. **`.planning/` stops being a queue** It keeps design documents and measured history — `NOTES.md`, the audits, the completed plans and the arguments in them. `plans/pending/` is gone, because a plan nobody is executing is an issue. Five plans moved to `completed/`, each stamped with which issue carries its remainder; `autotag.md` is kept as a historical record, marked stale where the scoring overhaul overtook it. **Filed from it, all with the argument preserved rather than a one-line summary:** | Issue | From | |---|---| | #85 warm the release cache for owned, not-known-complete albums | plan 010 (deleted) | | #86 six bound-but-uncalled backend methods — wire or remove | plan 012, finding 3 | | #87 Android: tag writing has never been run on a device | plan 016, B3 | | #88 run the credit ingest against the real dump and publish it | plan 015 multi-artist | | #89 materialise credits into `file_artists` — depends on #88 | plan 015 multi-artist, phase 3 | | #90 autotag strict auto-accept as a background job | autotag 011 | | #91 autotag settings panel — depends on #90 | autotag 012 | **Five issues were closed that had already shipped** #83's `Closes` list named ten and exactly half of them fired. #13, #19, #26, #29 and #37 were in `main` for a fortnight and still open; each is now closed with a comment naming the commit that shipped it. That is why the workflow says closing is a step you take and verify, not a keyword you trust. **The wrapper, and what it pins** `list mine search show new claim unclaim comment close label depends labels`. Bodies cross to the API as JSON and never as shell, which is why the formatting half is its own Python file — same reasoning that keeps release notes out of `gitea-release.sh`'s argument list. Four faults found by self-testing it against the live tracker, all fixed and each pinned with its reason: - **`claim` could half-claim** — the assignee and label landed, then the comment was rejected as empty, leaving the issue saying it was taken without saying by what work. The comment is resolved before anything mutates. - **`label` accepted names that do not exist.** Gitea answers 200 and applies none of them, so a typo reported success and did nothing. Names resolve to ids first. - **`depends` needs a whole `IssueMeta`, not an index.** A body of `{"index": 88}` answers **404**, which reads exactly like a Gitea build without the feature. - **`close` drops `Status/In Progress`**, or a claim outlives the work. **Verified** - `make skill-check` — 47 documented make targets, all present (no new target added) - `bash -n scripts/issue.sh`, `python3 -m py_compile scripts/issue_fmt.py` - Every subcommand exercised against the live tracker, destructive ones on a scratch issue that was then deleted - `commit-check` on both commits via the `commit-msg` hook No Go, TypeScript or SQL changed, so the app-facing suites are untouched by this branch — CI will confirm. **Closes** #92
yonlu self-assigned this 2026-08-18 20:24:41 +00:00
yonlu added 2 commits 2026-08-18 20:24:41 +00:00
Issues become this project's source of truth for what is wanted and what
is already being worked on, which puts "search the tracker" at the top of
every task rather than occasionally. Fifty-odd open issues make that a
real lookup, and a lookup nobody can remember the shape of is a lookup
that gets skipped -- the same way the CI log endpoint cost two sessions
to a tool that 404s.

Text reaches the API as JSON and never as shell, which is why the
formatting half is its own Python file: an issue body is arbitrary prose
carrying backticks, quotes and $, and every attempt to build that JSON
inside the shell ends in nested quoting nobody can verify. Same reasoning
that keeps release notes out of gitea-release.sh's argument list.

Claiming is an assignment, a label and a comment together, because any
one alone is a claim somebody has to go looking for. It resolves the
comment before it mutates anything -- reading it afterwards is how a
claim ends up half-made, with the issue saying it is taken without saying
by what work -- and refuses outright if somebody else holds it.

Three API shapes are pinned here because each fails quietly:

- Labels are resolved to ids rather than posted as names. Gitea accepts
  a list of unknown names with 200 and applies none of them, so a typo
  reports success and does nothing.
- The dependency endpoint takes a whole IssueMeta, not an index. A body
  of {"index": 88} answers 404, which reads exactly like a Gitea build
  without the feature.
- close drops Status/In Progress, or a claim outlives the work.

Refs #92
docs: make the issue tracker the source of truth
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m28s
CI / e2e (pull_request) Successful in 6m9s
eb139cf872
Work has been starting from a chat message and a plan file, so two
people could pick up the same thing and neither could see the other.
The tracker is where that is visible.

Search before starting, claim before the first edit -- not before the
commit, since the point is that the other person can see the work is
taken while it is being done. If no issue covers it, open one first:
that is what makes the tracker a description of the project rather than
a description of the past.

The conventions were already right and are written down rather than
reinvented -- the Kind/Area/Priority/Platform/Reviewed/Status taxonomy,
its exclusive scopes, #73 as the roadmap, real Gitea dependencies for
hard blockers, and PR #83's body shape.

What #83 also demonstrated is that a Closes list closes nothing
reliably: it listed ten and five of them sat open in main for a
fortnight. So closing is a step you take and verify, not a keyword you
trust.

.planning/ stops being a queue and keeps design documents and measured
history -- NOTES.md, the audits, the completed plans and the arguments
in them. plans/pending/ is gone, because a plan nobody is executing is
an issue; everything unimplemented in it is now #85-#91, and each
completed plan says which issue carries its remainder. autotag.md is
kept as a historical record, marked stale where the scoring overhaul
overtook it.

The commit grammar is unchanged and is load-bearing for a different
reason, so the issue number lives in the branch name and the PR body
rather than the commit subject.

Refs #92
yonlu merged commit 20139394f3 into main 2026-08-18 20:35:45 +00:00
Sign in to join this conversation.