MIP-0063: A GitHub-native tracking standard — issues as the unit of work, milestones as deliverables¶
| Status | Partially implemented (tasks 1–7 of 8 — #423 #429 #430 #435 #436 #443 #450) — Tasks: docs/MIPs/MIP-0063.tasks.md |
| Author | Claude (Opus 5), for B. Valério |
| Created | 2026-09-27 |
| Phase | 0 (dev-loop; no user-facing surface) |
| Related | DEV-FLOW.md §1–§3 (this MIP inserts an issue step ahead of the MIP step, and files each task row as an issue), CONTRIBUTING.md, AGENTS.md (phase discipline, the docs table), the mip-tasks skill, brunogbv/cv (prior art: GitHub spec-kit, speckit-taskstoissues) |
| Effort | M — one script family (scripts/issues.sh, scripts/lib/tasks_issues.py), four issue forms, a labels.yml manifest, one new skill and one extended skill, four doc edits. No Scala, no new module, no CI workflow (§11.1 defers those) |
| Gain | infra/dev-loop (the backlog stops being 58 Draft design docs and a prose ROADMAP, and becomes a queue an agent can query in one call); infra/dev-loop again on the intake side (a contributor, human or agent, has one standard way to propose work instead of four half-documented ones) |
| Effort vs Gain | do next — every later harness layer the maintainer wants (validation gates, graphify, autonomous triage) needs a machine-readable work queue to operate on, and there isn't one today |
| Depends on | No other MIP. Not gated by Phase 1: this is dev-loop work, which ARCHITECTURE.md §11 puts outside the phase sequence. Creates no paid resource, so AGENTS.md's cost gate does not apply. Two actions only the maintainer could take, both done 2026-09-28: gh auth refresh -s project on the host (§4.4) and enabling Discussions (§4.6) |
| Blocked by | none |
| Risk | Taxonomy nobody maintains. Filing ~6 issues per accepted MIP turns a 58-MIP backlog into several hundred issues; if triage lapses, agent-ready rots and the queue becomes actively misleading — worse than the prose ROADMAP it replaced, because it looks authoritative |
| Cost so far | — |
1. Summary¶
marola already does spec-driven development: a MIP is the spec, MIP-NNNN.tasks.md is the plan,
a stacked PR is the increment. All three live in files, so the backlog is invisible in GitHub's UI
and unqueryable by anything that isn't already inside the repo. This MIP puts the work into
GitHub — issues as stories and tasks, milestones as deliverables, one org project board,
native dependency edges so "ready" is computed rather than asserted — and defines a five-rule
Definition of Ready that gates what an agent is allowed to pick up. The spec layer does not
move: MIPs stay files, and tasks.md stays the authored plan.
2. Motivation¶
Three concrete gaps, all visible in the repo today.
The backlog is not addressable. 58 MIPs (42 Draft, 3 Accepted, 12 Implemented), six open
issues (#398–#412) none of which link to a MIP, zero milestones, zero projects. "What next" is
answered by reading docs/4-Research-and-plans/ROADMAP.md, a prose document dated 2026-09-06 whose §2a tracks six bugs
in a markdown table with a State column — a hand-maintained issue tracker inside a file, in a
repo whose issue tracker is empty.
There is no intake standard. feature_request.yml tells a contributor that non-trivial work
"goes through a MIP first" and stops there: nothing says what happens to the issue, who decides, or
what the bar is. For a public project that is a dead end at the front door.
Nothing an agent can query. DEV-FLOW.md §7's unattended runner works through a
MIP-NNNN.tasks.md because that is the only machine-readable queue there is. It cannot ask what
is ready and unclaimed, cannot avoid colliding with another session, and cannot be handed work
that did not come from a MIP. Every later harness layer — validation gates, dependency graphs,
autonomous triage — needs that queue to exist first.
3. User-visible change¶
The "user" here is a contributor, human or agent.
Before, a stranger who wants to help reads CONTRIBUTING.md → AGENTS.md (180 lines of hard
rules) → DEV-FLOW.md (idea → MIP → acceptance → tasks → stacked PRs), concludes that
contributing requires writing a design doc, and leaves. After, they open the board's "Good
first issues" view, pick one whose body already carries acceptance criteria and a named test, run
just issue-claim 431, and go red → green → just pr.
For an agent, the change is one queryable call replacing a file read:
$ just issue-queue
#431 size/S area/map-site layer/site Add hreflang tags to the generated board pages
#428 size/S area/safety layer/core SafetyFooter drops the source line when notes is empty
#433 size/M area/conditions layer/core Cache Open-Meteo responses for the 3h board build
3 ready · 1 blocked · 2 in triage (milestone: Water quality on the map)
The footer's three counts partition the unassigned open issues, derived from the labels and the
dependency edges of §4.2 — not read from the board's Status, which needs the project scope
§4.4 leaves to a human.
And the Definition of Ready is enforced, not documented:
$ just issue-ready 435
✗ #435 is not agent-ready
✓ 1. acceptance criteria present
✗ 2. named test — no "### Named test" section in the body
✓ 3. area/* and layer/* labels set
✗ 4. size/* label missing
✓ 5. no open blocked-by dependency
label `agent-ready` not added
4. Data sources and dependencies reviewed¶
Every subsection below is a GitHub platform feature this design leans on. No third-party service, no new runtime dependency, nothing paid.
4.1 Sub-issues (REST) — adopted¶
Native parent/child issues with a rollup progress field. Verified live on this repo:
GET /repos/h0ffmann/marola/issues/412/sub_issues returns [] (live, not 404), and the payload
carries sub_issues_summary: {completed, percent_completed, total}. Documented limits — 100
sub-issues per parent, 8 levels — are comfortably above anything a MIP task list produces.
The trap: POST /issues/{n}/sub_issues takes sub_issue_id, documented verbatim as "The id
of the sub-issue to add" — the database id, not the issue number. The POST itself is executed
as of 2026-09-27 (Appendix) and ids on this repo are ten digits against three-digit issue numbers,
so a number passed here addresses an issue in some unrelated repository: refused or attached to
the wrong thing, with nothing in the response to say which. Gets a comment in the script
(AGENTS.md's "a trap" category) and a self-test.
4.2 Issue dependencies — adopted, and it replaces the blocked label¶
Native blocked-by / blocking edges between issues, generally available and included on GitHub
Free. Verified live: GET /repos/h0ffmann/marola/issues/412/dependencies/blocked_by → [] (live,
not 404), and this host's gh 2.95.0 carries gh issue create --blocked-by/--blocking plus
gh issue edit --add-blocked-by/--add-blocking/--remove-*. Up to 50 issues per relationship type.
A blocked issue shows a Blocked marker on the board and the issues list with no label set.
The same trap, a second time: POST .../dependencies/blocked_by takes issue_id — the
database id again, not the issue number. scripts/issues.sh therefore resolves numbers to ids in
exactly one place, used by both this and §4.1's sub-issue call.
Neither the feature docs nor the REST reference places any restriction on which issues may carry
dependencies — nothing about hierarchy, parents or sub-issues — and the dependency response schema
itself carries parent_issue_url and sub_issues_summary, which is what one would expect if a
sub-issue is simply an issue. §5.3 relies on that; §7 step 7 proves it.
Consequence: §5.2 drops the blocked label, and §5.4's readiness rule is checked against the API
instead of against a label somebody has to remember to remove.
4.3 Issue types — rejected, unavailable¶
GitHub's typed issues (task / bug / feature, up to 25) would carry the story-vs-task distinction
better than labels, but they are an organization feature. Verified empirically:
GET /orgs/h0ffmann/issue-types → 404 (h0ffmann is a user account), and no
/users/{user}/issue-types endpoint exists. issue.type is present in the payload but reads
null and cannot be set here. Labels carry the roles instead. Revisit if marola moves to an org.
4.4 Projects v2 and token scopes — adopted; the human prerequisite is done¶
Scopes verified against GitHub's OAuth table: project = "read/write access to user and
organization projects", read:project = "read only access". The prerequisite was real and
has been met: the maintainer ran gh auth refresh -s project, the host token now carries
project, and gh project list --owner marola-dev returns the org board (Marola, number 1).
Widening the scope stays a human action by design — an agent should not widen its own token — but
nothing in v1 waits on it any more. The 2026-09-27 reads that recorded the earlier state
(--owner h0ffmann → totalCount 0, read:project only) are kept in the Appendix as dated
history. Projects' built-in workflows (auto-add, closed → Done) are configured in the
project UI; this MIP uses them rather than reimplementing them as Actions.
4.5 Issue forms → parseable bodies — adopted, verified on real data¶
GitHub renders each form field into the body as a ### <Label> heading plus the value. Verified
against this repo rather than the docs: issue #412's body begins ### Problem /
### Proposed behaviour, matching its form's field labels exactly. This is what makes the
Definition of Ready checkable by grep rather than by a model, and why §5.6 fixes the heading
spellings across all four forms.
4.6 Discussions — adopted, and on¶
has_discussions: true (2026-09-28); it read false when this section was written, and the
maintainer enabled it. Narrow purpose: keep "how do I…" out of a tracker meant to be a work
queue. config.yml's contact link points at it and no longer caveats it.
4.7 Prior art — brunogbv/cv and GitHub spec-kit — borrowed, not vendored¶
brunogbv/cv runs GitHub spec-kit (.specify/ plus nine speckit-* skills). Its
speckit-taskstoissues is the closest existing thing to §5.4, and three of its decisions are
adopted directly: a feature-scoped task id in the title (003-T001: …) so per-feature ids
restarting at T001 do not collide during dedup; rewriting the row into a link to its issue
rather than keeping two states; and declaring that afterwards issues own status while the file owns
the plan. What does not transfer is the shape: spec-kit's tasks.md is a checkbox list, so its
rewrite targets - [ ] T001 …. marola's twelve task files are all markdown tables with an
explicit depends on column, which is both a different rewrite target and a better dependency
source. An earlier draft of this MIP copied the checkbox mechanism unchecked; the 2026-09-27 run
caught it. Its harness label is not adopted — area/dev-tooling covers it.
Read 2026-09-27: the skill source, the label set, and 15 issues showing 003-T0NN: titles in
practice. Not adopted: spec-kit itself. marola's MIP + tasks.md + mip-tasks pipeline holds
that role, and replacing it would invalidate five skills and DEV-FLOW.md for no gain.
5. Design¶
5.1 The object model¶
| Object | Means | Carried by |
|---|---|---|
| Milestone | one deliverable / use case, spanning many issues and possibly several MIPs | native milestone + its progress bar |
| Issue | a story or task — the claimable, PR-closable unit | labels, assignee, milestone |
| Sub-issue | a subtask, only when a story genuinely splits | native sub-issues (§4.1) |
Milestone names are descriptive and carry no sequence — Telegram bot answers a real shared
location, Water quality on the map, Trilingual marola — pt-BR, English, Japanese. Those three
are illustrations of the shape, not a proposal; the real ones are named by the maintainer when the
first deliverables are cut. Ordering between milestones is a board decision, never a property of
the milestone, because priorities change as the repo learns what must come first.
Milestone and MIP are n:m: a deliverable is a use case, a MIP is a design. Water quality on
the map spans MIP-0016 and MIP-0031; one MIP files its tasks into one milestone.
Three intake tiers (AGENTS.md's MIP trigger list is unchanged; this names the two tiers below it):
- Tier 1 — bug, chore, docs → one issue, no milestone, no spec.
- Tier 2 — small enhancement (≤ ~2 tasks, no new dependency) → one issue whose body is the spec, sub-issues if it splits. No MIP file.
- Tier 3 — initiative (new data source, scoring change, new integration, anything paid) → MIP proposal issue → MIP PR → Accepted → milestone → N issues. The proposal issue is relabelled, not replaced, so its design discussion stays attached.
5.2 Labels, milestones, the board¶
Four new labels at design time: agent-ready, size/S, size/M, size/L. Existing area/*
(12), layer/* (8), kind/* and priority/high are untouched. layer/azure is orphaned —
993b469 removed Azure — and is deleted here. mip was named here as existing and is not
(GET /labels/mip → 404, 2026-09-28): mip_proposal.yml applies it and §5.4 dispatches on it, so
.github/labels.yml defines it and just labels-sync creates it. Two more arrived with the tasks
rather than with this section: mip itself, and phase/0–phase/4 for §5.3's gate issues. The
manifest defines 45 labels today, and it — not this paragraph — is the list.
Two labels an earlier draft of this design carried are gone: blocked, because §4.2's native
dependencies express it better and without hand-maintenance, and epic, because §5.1 makes the
milestone the epic and leaves no issue role for it to mark.
Size carries a rule, not a feeling: S < 100 changed lines, M 100–400, L means split it.
DEV-FLOW.md §3 caps a task at ~400 lines, so size/L is only ever legal on a parent issue.
.github/labels.yml becomes the versioned manifest — every label with colour and description in
one reviewable file rather than state that exists only in a settings UI. just labels-sync
reconciles and reports orphans; it never deletes without --prune.
The board is one org-level project, Marola
(marola-dev/projects/1), with one custom
field (Status: Triage / Spec / Ready / In progress / In review / Done) and four views:
Triage (Status:Triage, the public inbox and the one view a maintainer reads daily), Now
(by Status, filtered to the milestone currently being pushed), Agent queue
(label:agent-ready, sorted size then priority/high) and Good first issues.
An earlier draft said user-level, written before the repo moved from h0ffmann to the
marola-dev org. The board was read on 2026-09-28, and the GraphQL schema introspected with it;
what this section assumed differs from what is there in four ways, and issues.sh board setup
reconciles the first three:
- It already exists. On 2026-09-27 it held 21 items and GitHub's six template views (Current
iteration, Next iteration, Prioritized backlog, Roadmap, In review, My items), and this
section's four views are added to those six, never in place of them; nothing here deletes or
renames a view. Read again on 2026-09-28, after task 6: 31 items and five views — the four
added here plus My items. The other five templates were deleted by hand in the UI;
scripts/issues.shhas nodeleteProjectV2Viewcall, so the promise holds and the count above is what the board looked like on one day. - Its
Statusalready has Ready, In progress, In review and Done — only Triage and Spec are missing, and it also carries Backlog, which this section does not name.updateProjectV2Fieldtakes the complete option list, so the two are appended to what is there andBacklogstays: dropping it from the list would delete it and the Status of every item holding it. - The view mutations exist and are current:
createProjectV2View,updateProjectV2ViewanddeleteProjectV2View, none deprecated. Two details shape the code.CreateProjectV2ViewInputcarries onlyprojectId,name,layoutandconfiguration— nofilter— so a view is created and then filtered by a secondupdateProjectV2View, which does take one. And neither input type carries a sort:ProjectV2View.sortByFieldsis readable but not writable, so this section's "Agent queue, sorted size thenpriority/high" is the one part of the view spec the API cannot express, and the ordering stays a UI action. So is Now's milestone filter, for a different reason: which milestone is "currently being pushed" is not something a script knows. - It is private; an earlier draft of this section said public. Making it public is a UI action, and it has not been taken: the board is private today.
When this was written none of the three mutations had been called: the host token had
read:project, so every write failed on scope before reaching the API. The maintainer has since
granted project and board setup has run — the Status options and the four views of this
section exist on the board (read 2026-09-28), so the mutations are exercised, not merely offered
by the schema. board setup still refuses with the gh auth refresh -s project line when the
scope is absent, and --dry-run prints the plan regardless, since the reads it is computed from
need only read:project.
One piece of state is deliberately duplicated: agent-ready is a label and Ready is a Status
value. The reason is AGENTS.md's jail rule — a session inside ai-jail has no gh login of its
own and a narrow GH_TOKEN; gh issue list --label agent-ready is one REST call on repo scope,
while a Project field needs project scope and GraphQL. The just commands own keeping the two in
step; nothing else may write either.
Which command writes which half, and which one wins. The label is the half every command can write; the Status half needs a scope a human grants (§4.4), so until they do, only the label moves — and the board must be able to catch up afterwards without undoing anything.
| Command | Label | Status |
|---|---|---|
issues.sh ready |
adds on an all-pass, removes on a regression | — (it has no board to write, by design: it must work from a jail) |
issues.sh claim |
removes | sets In progress |
issues.sh board sync |
— | sets from the issue's own state (assigned → In progress, agent-ready → Ready, otherwise Triage) on an item with no Status, or with Backlog and only Backlog |
So the label is authoritative and the board follows it, once, when an issue first reaches the
board. After that the Status belongs to whoever moves the card: sync never overwrites a Status a
maintainer set by hand, because deriving In review or Spec from an issue's state is not
possible and guessing it back to Triage every run would undo their work. The consequence is that a
label and a Status can still disagree afterwards — that is the soft spot §8 names and the
reconciliation job §11.1 defers, and it is now stated rather than implied.
Why Backlog is the one exception, and it is not an arbitrary one: it is not a state anybody
chose. The project's built-in auto-add workflow (§4.4) writes it on every issue the moment it
reaches the board, so on this board it means nobody has looked at this yet — the same thing an
absent Status means. Treating it as unset is what makes an issue's first contact with the board
mean anything; without it every card the workflow touched would be frozen at Backlog forever and
sync would be inert by construction. The exception is exactly that one literal value. It is
not "any Status §5.2 does not name": Ready, In progress, In review, Done, Triage and
Spec are all somebody's decision and none of them is ever overwritten. And it is a once: after
one sync an adopted card carries a real Status, so the next run adopts nothing.
Two things follow that the command says out loud rather than leaving to be noticed. It names
every card it moves (#421 Backlog → In progress), because the first real run moves the whole
board at once. And it runs after board setup, not before: until the Status field has the
options §5.2 names, an issue whose state calls for Triage cannot be given one, so sync counts
those as skipped rather than failed, says so once instead of once per issue, and exits nonzero
naming the command to run first.
5.3 Dependencies¶
Dependency management exists so that whoever picks work up — a contributor, or an agent running unattended — can tell what is ready without reading anything else. §4.2's native edges make that a computed property, and they cost nothing to apply, so they are used at every level:
- Between issues — what makes "ready" computable rather than a guess, and what the board's Blocked marker reads.
- Between sub-issues of the same parent — the same mechanism, for the same reason. An earlier draft of this design left these untracked, on the reasoning that one owner drives a story and can hold the order in their head. That reasoning only holds while recording the order is expensive. It isn't: GitHub gives the edge away, so the ordering stops being something the owner has to remember and becomes something the tracker answers. Proven live on 2026-09-27 (§7 step 7, Appendix): GitHub accepts the edge between two sub-issues of the same parent and reads it back from both ends.
Because the edges are free, nothing asks a human to draw them by hand: tasks-to-issues
derives each edge from the depends on column that MIP-NNNN.tasks.md already carries. That
column is a DAG, not a chain — MIP-0034 has 6 ← 1, MIP-0031 has three roots and two parallel
branches, MIP-0056 has 7 ← 4, and MIP-0011's task 11 says in words that it "can be built any time
relative to 1-10". A dependency graph marola has been writing by hand for twelve task lists becomes
machine-readable at no authoring cost, and it is a truer graph than the branch stack, which only
ever expresses merge order.
Phase discipline falls out of the same mechanism. Each of ARCHITECTURE.md §11's phases gets
one tracking issue ("Phase 1 — Telegram bot working"), and every phase/2 issue is blocked by
it. Closing that one issue unblocks the whole phase at once, visibly and without editing anything
else. The five gates were filed by task 6 and the first real edge wired in task 8 — #398 is
phase/2, blocked by #447, and ready refuses it naming the gate (Appendix, 2026-09-28). phase/1..4 stay pure facet labels with no ordering power, and AGENTS.md's hard rule
stops being a paragraph people skim. Five gate issues total; they hold no work, only the edge.
5.4 The Definition of Ready¶
The DoR governs claimable work, and it reads rules 1 and 2 from the headings the issue's own
form renders (§5.6). An issue is agent-ready when all five hold:
- Acceptance criteria present, as testable checkboxes (
### Acceptance criteria). - A named test — file plus test name (
### Named test). This is MIP §7's discipline pulled forward onto the issue, so the test is decided before the work is claimed, not after. area/*andlayer/*set.size/*set.- No open
blocked bydependency, read from the API (§4.2) — not from a label.
A section counts as present only when it has content: GitHub renders an optional form field
that was left blank as its heading followed by _No response_, so "the heading is there" and "the
author answered" are different questions.
Two of §5.6's four forms carry neither heading, and they are not the same case:
mip(tier 3) is not claimable. A MIP proposal is a design request, relabelled once the MIP PR opens (§5.1), so it can never beagent-readyand its lack of the two headings is correct.issues.sh readysays exactly that in one line rather than reporting rule failures whose obvious remedy — adding acceptance criteria to a proposal — is the wrong thing to do.bug(tier 1) is claimable, and refusing every bug forever would be a real gap.bug_report.ymlasks for the same two things under the names that fit a bug, so for abugrule 1 reads### What you expected instead(required by the form; a bug's statement of done) and rule 2 reads### Failing test(§5.6). A mapping, not a waiver: Failing test is an optional field, so a bug filed without one still does not pass, which is what makesAGENTS.md's "reproduce a bug with a failing test before fixing it" checkable at claim time rather than at review time.
task.yml and story.yml — the two forms that spell both headings — are unaffected.
AGENTS.md gains one hard rule: an agent may only begin implementation on an issue carrying
agent-ready. Everything else is a human's problem first.
5.5 The command surface¶
Seven recipes over one scripts/issues.sh, each with the repo's usual --dry-run:
| Command | Does |
|---|---|
just tasks-to-issues MIP-NNNN --milestone "<name>" |
creates missing issues titled 0063-T3: …, dedups on \b0063-T\d+\b, rewrites each row's # cell into a link to its issue, wires each blocked by edge from the depends on column (§5.3); idempotent |
just issue-queue |
agent-ready, unassigned, sorted size then priority — the read an agent makes; the ready/blocked/in-triage counts under it are derived from labels and dependency edges, not from the board's Status |
just issue-ready <n> |
runs the five rules, adds/removes agent-ready, prints which rule failed |
just issue-claim <n> |
assigns, drops agent-ready, sets board Status, prints the scripts/stack.sh start line |
just milestone-new "<name>" [--mip MIP-NNNN] |
thin, but keeps deliverables discoverable from just |
just board-sync |
adds un-added open issues to the project, sets Status from state |
just labels-sync [--prune] |
reconciles the repo against .github/labels.yml, the manifest; orphans are reported, never deleted without --prune |
Five subcommands have no recipe, for two different reasons. board setup (the Status options
§5.2 lacks, the four views) and board gates (§5.3's five phase gate issues) are one-time
bootstraps rather than anything in the loop: filing issues is human-gated in this repo (§5.6,
Decision 2), reshaping a shared board is the same kind of act, so both stay something a person runs
with --dry-run first rather than something just offers. sub add <parent> <child>,
deps add <issue> --blocked-by <n> and deps list <issue> are not bootstraps at all:
tasks-to-issues calls deps add for a whole task table, and all three are called directly for a
one-off edge.
Direction of truth: mip-tasks authors tasks.md; tasks-to-issues projects it into GitHub;
after that, issues own status and the file owns the plan. One-way, re-runnable. An agent in
ai-jail with no token can still read the whole plan from disk — which is why the file is not
replaced by the issues.
5.6 Forms and skills¶
Four forms, blank issues still disabled. Each field label renders as a literal ### <Label>
heading, and §5.4's checks are a grep for the spelling that form renders — task.yml and
story.yml share ### Acceptance criteria / ### Named test, a bug is read on the two below,
and a mip proposal carries neither because it is not claimable work:
| Form | Tier | Change |
|---|---|---|
bug_report.yml |
1 | gains Failing test — the repo already requires reproducing a bug with a failing test, so the form asks |
task.yml (new) |
1 | chore/refactor/docs: what, acceptance criteria, named test, size |
story.yml (replaces feature_request.yml) |
2 | Problem · Proposed behaviour · Acceptance criteria · Named test · Out of scope · Deliverable |
mip_proposal.yml |
3 | kept; now states what happens next (MIP PR → milestone → tasks-to-issues) |
config.yml points questions at Discussions (§4.6).
Skills: mip-tasks gains a final step that calls tasks-to-issues — no new skill, since it
already authors the file. New triage skill: takes a raw idea, voice-note fragment or bug
report, picks the tier, drafts the body in the matching form's heading shape, runs the DoR check,
files it.
triage stays human-invoked — decided, not deferred. No label, no webhook and no schedule may
start it. AGENTS.md requires a human-confirmation gate for proactive agent behaviour unless a MIP
decides otherwise, and this MIP declines to lift it: an agent that files its own issues, against a
standard whose whole purpose is to say what is real and ready, would pollute the queue faster than
anyone can triage it.
6. Scoring / safety impact¶
None. No Scala changes, no scoring/ changes, no change to any user-facing output. Nothing in
this MIP goes near Swimability or the safety footer.
7. Verification plan¶
In the order that proves the most for the least:
scripts/lib/tasks_issues.py --self-test— the dedup regex (\b0063-T\d+\bmatches0063-T001,[0063-T1],0063-T1:; does not matchS0063-T1or0063-T0010), the table-row parser, the#-cell→link rewrite including idempotence on an already-linked row, and thedepends oncolumn parser against a non-linear graph (–,—,1,5, 6). No network, no GitHub. Runs underjust quality-other, so.githooks/pre-pushexercises it every push.scripts/issues.sh --self-test— argument handling and each of the five DoR rules against fixture bodies, including rule 5 reading a stubbedblocked_bylist rather than a label.just labels-sync --dry-run— must print exactly the four additions and flaglayer/azureas orphaned. Nothing else moves.just tasks-to-issues MIP-0060 --dry-runagainst the real existingMIP-0060.tasks.md— inspect titles and rewritten lines before anything is created.- Live idempotence check: re-run against this MIP's own task list, whose issues were filed by
hand on 2026-09-27. The second run must create zero issues and rewrite zero rows — the strongest
available fixture, since the data is real and already linked. This is also where §4.1's
sub_issue_idtrap is confirmed against the live API or corrected. - The first real use, on real debt:
docs/4-Research-and-plans/ROADMAP.md§2a's six findings, drafted throughtriageand filed as Tier-1 issues. Run 2026-09-28, and the premise did not survive it: five of the six were fixed on 2026-09-07 (#224), the day after they were found, and §2a still called them open three weeks later, because nothing forced the State column to move — the failure §2 describes, found by the thing meant to replace it. One issue was filed, thecost-fillgit-version bug (#451), and it passesissues.sh readyunedited. Acceptance therefore rests on five real issues, #431–#434 plus #451, not six; §2a now carries thefile:linethat fixed each finding, and the one open item as a link, so it is no longer a tracker either way. - The one assumption §5.3 rests on, proven in the same pass: give one of those six a parent
and a sibling sub-issue, add a
blocked byedge between the two siblings, and read it back. If GitHub refuses a dependency between sub-issues, §5.3 falls back to the earlier position — edges between issues only, sub-issue ordering left to whoever owns the story — and nothing else in the design moves.
Done looks like: the four forms in place, labels.yml synced, the board created with its four
views, the five phase gate issues open with phase/* work blocked by them, just issue-queue
returning a real list, and docs/3-Working-on-the-repo/ISSUE-FLOW.md describing exactly what the scripts do.
8. Risks, limitations, and honest caveats¶
- Taxonomy rot (the metadata
Risk). Mitigation is scope: the board starts empty. v1 files issues only for MIPs accepted from here on, never retroactively for the 42 Draft MIPs. Consolidating the existing backlog is a deliberate later step, taken once this standard has been validated in use and not before — filing several hundred issues against an unvalidated taxonomy is precisely how it rots. - The duplicated
agent-ready/Status pair (§5.2) is the design's one soft spot. If anything other thanissues.shwrites either, they drift. A CI reconciliation job is the fix, deferred to §11.1. - Two manual steps blocked parts of v1:
gh auth refresh -s projectand enabling Discussions. Both were taken on 2026-09-28. The shape of the risk stands for the next such step: everything except the board works onreposcope, by design. - Nothing here enforces itself yet. With CI gates out of v1, a PR can still merge without
linking an issue, and an issue can be labelled
agent-readyby hand without passing the check. v1 makes the standard checkable; it does not make it unavoidable. - Public means public. An open triage queue exposes what is not being done as clearly as what is. That is the point, and it is worth stating before switching it on. The issues are public; the board is not, and making it public stays a UI action nobody has taken.
- The edges are only as good as the
depends oncolumn. §5.3 reads that column rather than inferring a chain, so parallelism survives — but a task list that overstates a dependency will serialise work that could have run at once, and one that understates it will show an issue as ready when it is not. Nothing checks the column against reality;mip-tasksauthors it by hand and review is the only gate. That is the same trust the stacked-PR workflow already places in it, now with more consequence attached. - 50 edges per relationship type (§4.2). Far above a MIP's task count, but a milestone that accumulated dependencies across many MIPs could reach it. Nothing in v1 checks for the ceiling.
9. Alternatives considered¶
- Do nothing. ROADMAP prose and
tasks.mdkeep working for one maintainer with one agent, and fail the moment the audience is public or the agents are concurrent. Blocks every later harness layer. - Adopt spec-kit wholesale, as
brunogbv/cvdoes —.specify/, nine skills,taskstoissuesfor free. marola's MIP pipeline already holds that role and five skills are built on it, so the migration is pure churn. Ideas borrowed instead (§4.7). - Milestones as phases (this design's first draft). Rejected by the maintainer during brainstorming, correctly: it bakes sequence into milestone identity, so re-prioritising means renaming milestones. Phases became facet labels; deliverables became milestones.
- Epic issues as a separate object, milestones reserved for releases. The milestone is the epic; a parallel epic layer means two progress bars to keep honest.
- Status as a label family (
status/triage, …) instead of a board field. That is what a board is for, and six more labels per issue makes the list unreadable. The one exception (agent-ready) is argued in §5.2. - A
blockedlabel with the blocker named in prose, which is what this design carried until the maintainer's review of PR #413 asked how a contributor is supposed to know what is ready. Rejected once §4.2's native dependencies turned out to be GA on Free: a label has to be added and removed by hand, says nothing about what blocks, and goes stale the moment the blocker closes. The native edge is computed, shows a Blocked marker on the board, and lets readiness be derived instead of asserted.
11. Open questions¶
Decisions taken during review are recorded where they belong — the board starts empty (§8),
triage stays human-invoked (§5.6), dependencies are tracked at every level and derived from the
depends on column rather than drawn by hand (§5.3), and §5.1's milestone names are illustrations
— the first real one, Issue tracking standard live, was named by the maintainer on 2026-09-27.
What is left genuinely open:
- CI gates — deferred, not dropped. PR-must-link-an-issue, parent-can't-close-with-open-
children, auto-add-to-project, and the
agent-ready↔Status reconciliation of §8. Scoped out of v1 by the maintainer. §5.6's fixed heading spellings andlabels.ymlexist so these drop in later without re-templating anything. What is unanswered is the trigger: what evidence says the standard has earned enforcement — a count of issues filed, a stretch with no drift, or a second contributor arriving? docs/4-Research-and-plans/ROADMAP.md's long-term role — deliberately not resolved here. The maintainer's stated direction (PR #413 review): the roadmap's source of truth should become the project and its milestones in the long run, but nothing moves until this standard has been validated in use. Until then ROADMAP keeps its narrative: Decision 5 of the task list allows §2a's tracker table to go, and task 8 replaced it, which is not the same as moving the roadmap's source of truth. The consolidation is a later decision with its own evidence.
Appendix¶
Checked live¶
All on 2026-09-27, from this checkout.
GET /repos/h0ffmann/marola/issues/412/sub_issues→[]. Endpoint live on this repo.GET /repos/h0ffmann/marola/issues/412→sub_issues_summary: {completed:0, percent_completed:0, total:0},type: null.https://docs.github.com/en/rest/issues/sub-issues→sub_issue_iddocumented verbatim as "The id of the sub-issue to add. The sub-issue must belong to the same repository owner as the parent issue". No limits or token permissions stated on that page.docs.github.com"Adding sub-issues" (via search) → 100 sub-issues per parent, 8 levels of nesting.GET /repos/h0ffmann/marola/issues/412/dependencies/blocked_by→[]. Issue-dependencies endpoint live on this repo.gh issue create --help→--blocked-by,--blocking;gh issue edit --help→--add-blocked-by,--add-blocking,--remove-blocked-by,--remove-blocking. Present in this host'sgh2.95.0.docs.github.com"Creating issue dependencies" +github.blog"Dependencies on issues" (2025-08-21) and "Manage sub-issues, types, and dependencies from GitHub CLI" (2026-06-10) → generally available, included on GitHub Free, up to 50 issues per relationship type, blocked issues marked on the board and the issues list.docs.github.com"Creating issue dependencies" and.../rest/issues/issue-dependencies, read for restrictions → none stated on which issues may carry dependencies (no mention of hierarchy, parents or sub-issues); the RESTPOST .../dependencies/blocked_bybody takesissue_id, the database id; the dependency response schema includesparent_issue_urlandsub_issues_summary.GET /orgs/h0ffmann/issue-types→ 404 (h0ffmannis a user account).GET /users/h0ffmann/issue-types→ 404, no such endpoint. Issue types unavailable here.https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps→project= "Grants read/write access to user and organization projects";read:project= "Grants read only access to user and organization projects".gh auth status→ host token scopes includeread:project, notproject.gh project list --owner h0ffmann→{"projects":[],"totalCount":0}.GET /repos/h0ffmann/marola→has_discussions: false,has_projects: true,has_issues: true.gh label list→ 35 labels;layer/azurestill present after 993b469 removed Azure.gh issue view 412 --json body→ body begins### Problem,### Proposed behaviour— form fields render as###headings, confirmed on real data.github.com/brunogbv/cv→.specify/tree, ninespeckit-*skills,.claude/hooks/review-gate.sh;speckit-taskstoissues/SKILL.mdread in full; label set includesphase:*andharness; 15 issues sampled showing003-T0NN: …titles. No milestones.github.com/brunogbv/marilegal-frontend→ stock.mdissue templates, default label set, no milestones, no.specify/. Nothing adopted from it.- Dogfood run, 2026-09-27 — this MIP's own task list filed through its own design, by hand,
because
tasks-to-issuesis task 5. Created milestone #1Issue tracking standard live, the four new labels, and issues #414–#421 (0063-T1…T8) with bodies in thetask.ymlheading shape. Tenblocked byedges created and read back:415←414,416←414,417←415,416,418←415,417,419←417,420←418,419,421←420. The graph round-tripped exactly as authored, and the computed ready set was{#414}alone — rule 5 of §5.4 working against the live API with no script in between.#414then passed all five rules by hand and was labelledagent-ready. The run also found the two defects corrected in §4.7, §5.3, §5.5, §7 and §8: marola's task files are tables, not checkbox lists, and theirdepends oncolumn is a DAG rather than thek ← k-1chain this MIP first assumed. scripts/docs-mip-stack.sh list→docs/mip-0061-swim-brief-agentis an unmerged draft holding MIP-0061; 0062 is inmain. 0063 is the first free number.- Task 1 run, 2026-09-27 (PR #423) —
.github/labels.ymlgenerated from the live repo, thenlabels sync --dry-runreportedlayer/azureas the only orphan with nothing to create or edit, andlabels sync --prunedeleted it. The repo went 40 labels → 39, matching the manifest exactly.layer/azurewas carried by four closed PRs (#3, #47, #48, #286), which lost it. Recorded here because the check is not repeatable: once the orphan is pruned the same command printsin sync, so the manifest-vs-repo drift path lives on in--self-test's fixtures only. -
Label names are case-insensitive, label paths are not — 2026-09-27, probed on this repo because neither the REST reference nor the labels docs say either way:
gh label create zz-probe-case -> created gh label create ZZ-PROBE-CASE -> "already exists; use --force" gh label delete ZZ-PROBE-CASE -> HTTP 404 gh label delete zz-probe-case -> deleted (probe labels removed)
Lookup on create folds case; the DELETE (and so PATCH) path does not. A sync that matched names
case-sensitively would therefore plan a create that fails and a delete of the real label, so
§5.2's manifest reconciliation has to fold case on the name and address every edit and delete by
the spelling the repo currently holds. Nothing in the design moves; the constraint is on any
script that writes labels.
- A dependency between two sub-issues of the same parent is allowed — 2026-09-27, §7 step 7,
the one assumption §5.3 leans on. Three throwaway issues, since closed as not_planned: #426
parent, #427 child A, #428 child B (database ids 5606620709, 5606620805, 5606620913).
POST /issues/426/sub_issues {"sub_issue_id":5606620805}
-> the *parent* #426, sub_issues_summary {total:1, completed:0}
POST /issues/426/sub_issues {"sub_issue_id":5606620913} (issues.sh sub add)
-> accepted
POST /issues/428/dependencies/blocked_by {"issue_id":5606620805} (issues.sh deps add)
-> accepted: #428 blocked by #427, two sub-issues of one parent
GET /issues/428/dependencies/blocked_by
-> [{"number":427, "state":"open", "parent_issue_url":".../issues/426",
"sub_issues_summary":{"total":0,...}}]
GET /issues/427/dependencies/blocking -> [{"number":428}]
GET /issues/427 and GET /issues/428 (checked separately)
-> parent_issue_url .../issues/426 on both, so the edge above really is between two
sub-issues of one parent and not between two loose issues
§5.3 stands as written and Decision 8's fallback does not apply. Three things worth carrying:
the POST to sub_issues answers with the parent, not the sub-issue; parent_issue_id is
null in the sub_issues listing while parent_issue_url is set on the issue itself, so a
reader of the wrong field sees an orphan; and the edge survives closing both issues and then
reads "state":"closed", which is exactly what §5.4's rule 5 needs to distinguish. The parent's
rollup went to {completed:2, total:2, percent_completed:100} on closing the children.
- POST /issues/{n}/sub_issues executed — same probe. The sub_issue_id in each call above is
a database id resolved from the issue number, and the attachment landed on the issue asked for.
scripts/issues.sh resolves numbers to ids in one place and refuses anything under nine digits;
live ids on this repo are ten (5606620913 against issue number 428).
- Task 4 run, 2026-09-28 (issues.sh ready, --dry-run, no labels written). GET
/repos/marola-dev/marola/labels/mip → 404; the mip label §5.2 called existing does not
exist, so §5.4's tier dispatch needed it in the manifest. ready against the eight live
fixtures: #431–#434 and #417 pass all five; #418 fails rule 5 alone, naming #417 and not
#415, which is closed — the closed-blocker distinction of the 2026-09-27 probe, now read
through the script; #412 fails 1, 2 and 4; #411 fails 1, 2, 3 and 4. issues.sh queue over 16
open issues → 0 ready · 0 blocked · 10 in triage (the other six are assigned), one
dependencies/blocked_by call per unassigned issue, 6s.
-
Task 8 run, 2026-09-28 — the first real use of the standard.
docs/4-Research-and-plans/ROADMAP.md§2a's five "open" findings were each re-checked before being written down, and all five were already fixed:.claude/hooks/stop-gate.sh:23(ls-files --others --exclude-standard),:30/:34(|| return 0, with an unwritable-/procself-test),.claude/hooks/format.sh:51/:77(command -v csguard),"Bash(git rev-parse*)"in.claude/settings.json:39, and the §7→§8 sweep atscripts/deps-stack.sh:3/:391andCONTRIBUTING.md:85. Only the sixth was real:git cherry-pick --allow-empty --empty=dropexits 129 with usage text on git 2.34.1 (--emptyis a cherry-pick option from git 2.45), reproduced in a scratch repo, filed as #451 andagent-readyon the firstissues.sh readywith no hand-editing.issues.sh queuethen returned five ready, sorted size then priority. #431–#434, filed by hand through these same forms, still pass all five rules unedited. -
Board and gates, 2026-09-28, after tasks 6 and 8. The two bullets this Appendix used to carry under "Not checked" — Projects v2 writes unexercised, the four views specified rather than built — are removed because both are now false.
gh auth status -> token scopes include
projectgh project list --owner marola-dev -> Marola open PVT_kwDOE_GJ6c4Bk4CS (PVT_ = private) GET repos/marola-dev/marola -> has_discussions: true, visibility: public projectV2(number:1) -> public:false, items 31, views 5: My items (assignee:@me) · Triage (status:"Triage") · Now (BOARD, no filter) · Agent queue (label:"agent-ready") · Good first issues (label:"good first issue")
Three of the four views this MIP specifies carry their filter; Now is unfiltered, exactly as
§5.2 predicted (which milestone is "currently being pushed" is not something a script knows).
Five of GitHub's six template views are gone and only My items survives; scripts/issues.sh
has no deleteProjectV2View call, so that was a human in the UI.
-
§5.3's phase gate proven, 2026-09-28. The five gate issues existed (#445 closed, #446–#449 open) but
blockingreturned[]on every one and no issue outside them carried aphase/*label, so "closing that one issue unblocks the phase at once" was scaffolding. #398 (Q&A/ELI5 bot on GCP: Cloud Run, a GCS-hosted index, Besom) is Phase 2 byARCHITECTURE.md§11 — first real cloud spend, MIP-0057's backend — so it was labelledphase/2and wired to gate #447:issues.sh deps add 398 --blocked-by 447 issues.sh deps list 398 -> #447 open Phase 2 — Go live on a cloud backend, deliberately GET /issues/447/dependencies/blocking -> [398] issues.sh ready 398 -> ✗ 5. blocked by #447 (still open) (1–4 also fail: #398 is a MIP brief, not a filled task form)
The gate is a real edge now, and §5.3 describes something that has happened. One issue labelled, one edge added; nothing else was touched.
Not checked¶
- The mis-attach half of §4.1's trap was not probed. That passing an issue number as
sub_issue_idsilently attaches a different issue is still the documented reading, not an observed one: the ids that collide with marola's three-digit numbers belong to issues in unrelated repositories, and the probe would have written a sub-issue link onto a stranger's issue.scripts/issues.sh's guard is therefore built on the parameter's documented semantics plus the order-of-magnitude gap between the two spaces, not on a reproduction. - Projects' built-in workflows (auto-add, closed → Done) are taken from general knowledge of the feature, not re-read from the docs this session.
- Whether GitHub renders every issue-form field type as a
###heading — verified fortextareaon #412 only.dropdownandcheckboxesrendering is assumed, not confirmed. brunogbv/cv's.specify/extensions.ymlhook mechanism is described in the skill text that was read, but no extensions file was fetched; nothing in this MIP depends on it.