Field report for QEP-6 — Project Tracker Structure and Order (Draft, #18). Filed as an issue rather than a PR comment because of its length; proposed amendments to the draft follow as a comment below, so they can be discussed separately from the findings.
What was tested
On 2026-08-26 we built a QEP-6-conformant project tracker from scratch against a real, pre-existing issue set: QuantEcon/cli#20 — 17 work items, five phases, over a repo whose issues had accumulated since 2026-06. This is the "registered existing tracker brought into layout" case from Adoption clause 2, not a greenfield one: the items already existed, already had a parent, and already carried their own history.
Method and its ceiling
The tracker itself was built for real, and the ordering behaviour in finding 3 was verified live. The gh capability claims below are source-derived, not executed — no gh binary was available in the session, and api.github.com was repo-gated. They come from cli/cli source at pinned release tags, github/rest-api-description OpenAPI 1.1.4, and github/docs markdown — the artefacts the rendered doc sites are generated from, which is stronger provenance than the doc pages, but is still reading rather than running. One smoke test on a throwaway repo should confirm before anything is built on them.
What the draft got right
Worth recording first, because it is the larger share of the result.
- The
Project issue type applied cleanly at creation and reads back reliably. Unit identity is the least troublesome carrier in the set.
- Appendix A's body skeleton was sufficient — nothing missing, nothing to mirror. Goal → stamp →
Next: → phase table → gates → sequencing rationale → out-of-scope covered every fact the structure could not hold. We never once wanted a field it lacked.
- Order is positional, and the mechanism works end to end. See finding 3 — this is now verified rather than assumed.
- The restraint on dependencies proved correct at scale. Of 17 items, only four pairs were genuine hard blocks. "Edges are reserved for constraints that are real" is not merely defensible; soft ordering is the overwhelming majority, and position absorbed it without strain.
Findings
1. Creating a tracker is a destructive operation on whatever hierarchy the items already belonged to — silently
The ten items claimed from QuantEcon/meta#358 were detached from it by the act of linking them to the new tracker. Sub-issues are single-parent, so this is inherent. What makes it a finding is that it is silent and non-optional in the standard tooling: both gh issue edit P --add-sub-issue C and gh issue edit C --parent P pass replace_parent=true unconditionally, with no opt-out flag, while the REST endpoint exposes it as optional. Adding an item and stealing it from another tracker are the same gesture, indistinguishable at the call site.
The consequence in the field: meta#358 — the org's QEP-2 rollout tracker — was left holding only the one item that had been closed, and therefore reported {total: 1, completed: 1, percent_completed: 100}. A programme whose pilot has not started displayed as complete. No error, no warning; the rollup simply changed in a repository nobody was looking at while the new tracker was being built.
Repaired 2026-08-26: cli#20 is now a sub-issue of meta#358, and the closed item moved into cli#20 where its history is legible. meta#358 now reads 0% of 1 child.
2. Progress arithmetic breaks in two ways the draft does not cover
- Mixed-granularity children. A programme tracker holding one project tracker (17 items) alongside leaf issues weights them equally. The draft's existing rule — measure direct children only, never deeper — is right, but needs a companion: a tracker's direct children should be homogeneous in kind, all leaf work items or all project trackers. Mixing them makes the percentage meaningless rather than merely coarse.
- Closed-but-not-done children.
sub_issues_summary.completed appears to count every closed child regardless of state_reason, so scope cut with not_planned or duplicate would read as progress. Unverified — a cheap addition to the smoke test.
Observed rather than theorised: meta#358 is a hybrid — part programme (delegating to cli), part project (org default labels is real leaf work), with four of its six sections tracked as body checkboxes. The draft's clean programme/project split has no slot for this shape, and this shape is what exists.
3. Order is the weakest-supported carrier — but the read-back contract holds ✅
Of the six carriers, order is the only one with no gh verb at all (a repo-wide grep for reprioriti|after_id|before_id across cli/cli returns zero matches). It is reachable only through gh api, with the most error-prone signature in the set: the parent is an issue number in the path while the children are integer database IDs in the body. Both are bare integers; the API cannot distinguish them. And no --json field yields an issue's database ID (fullDatabaseId exists on PullRequestFields only), so each reference costs a resolution call unless batched off the sub-issues list endpoint.
The open question was whether the read-back reflects position — the draft assigns order to "sub-issue list position", and the write mechanism being GA says nothing about the reader. Verified live on cli#20:
initial: 14,15,16,17,19,13,12,18,7,9,6,2,3,4,5,11,10,8
move #10 → first: 10,14,15,16,17,19,13,12,18,7,9,6,2,3,4,5,11,8
restore + place #8: 14,15,16,17,19,8,13,12,18,7,9,6,2,3,4,5,11,10
A reprioritize write is reflected in the list read-back, every other item holds its relative position, and the resulting order is neither ascending nor descending by issue number. QEP-6's ordering thesis has a verified reader. (Verified via REST; the UI and GraphQL orderings were not cross-checked.)
4. The status stamp is the only carrier where a second writer destroys the first's work
There is no partial-body update and no compare-and-swap on either the CLI or REST path — body writes are whole-replace. Two agents, or an agent and a human, refreshing the same tracker inside one window clobber each other, including body content unrelated to the stamp. The draft places the stamp in the body correctly — GitHub has no freshness primitive — but should say that a refresh is a read-modify-write inside a single turn, and that everything outside the stamp must survive it.
5. The milestone naming rule is vindicated on a second, purely mechanical ground
gh issue list --milestone runs strconv.ParseInt on its argument first, so a milestone titled "2026" is looked up as milestone number 2026 and fails. The draft already bans sequence tokens for identity reasons; purely numeric names break tooling for an unrelated reason and should be banned alongside them.
6. Templates reach further than expected, but cannot emit the draft's own stamp
type: is a real issue-template front-matter key (GA 2025-04-09), so a tracker template can stamp type: Project at creation — Adoption clause 1's identity requirement, satisfied for free. Templates provably cannot set milestone, parent/sub-issue edge, dependencies, or list order; those are post-creation for every producer.
The trap: an issue form converts field labels into body headings (### <label>), so it structurally cannot emit ## Where we stand (verified YYYY-MM-DD) — wrong heading level, and the date is dynamic while a label is static. A markdown template passes body text through verbatim and can still set type:. Two corollaries: markdown: elements in a form are never submitted, so skeleton text must be a prefilled textarea value; and the issues webhook payload carries no template identifier at all, so automation should key on issue.type.name — the type is the template's signature.
7. Two facts the draft leaves open, both with native answers
- Superseded items. "Closed" is not "done", and the list cannot hold a third state — encoding one would repeat the error the draft rejects for sequence tokens. GitHub already has the carrier:
state_reason (completed / not_planned / duplicate with duplicate_of). The item keeps its place; why it closed is the item's business, not the list's. Subject to the caveat in finding 2.
- Parked vs. merely last. Parked-ness is not a fact to carry — it is a query: an open item with an open blocker is parked. Where the gate is an issue, a dependency edge says it exactly (
--add-blocked-by is a core gh verb, so this is cheap). Where the gate is a decision, create the issue for the decision. Where it is phase-level, the body's gates section already covers it. Stating the derivation explicitly stops anyone inventing a carrier for it.
8. Scope: the order carrier has no known writer on GHES
The four sub-issue REST endpoints are absent from every GHES OpenAPI description 3.14–3.18 (verified against a control — ordinary issue paths are present in the same files). The native gh flags route through GraphQL and may work, but reordering exists only as a REST endpoint that is not in the GHES spec. The draft should say GitHub.com explicitly rather than let GHES support be inferred from a release note.
On the org's working assumption
The assumption under test was: any mechanism not supported by GitHub infrastructure must instead be carried in the issue body via templates.
On GitHub.com, that trigger never fires for any QEP-6 carrier. All six are natively readable and writable through gh. The body carries exactly one thing — the stamp — and does so because GitHub has no freshness primitive, by design rather than as fallback.
The test is worth keeping, with its two halves kept distinct: does GitHub have a carrier? (no → body, legitimately) versus can our tooling write it? (no → fix the tooling; do not move it to the body). Body-carriage of a natively-carried fact manufactures precisely the mirror the draft's Alternatives considered section rejects: the fact would live natively for producers that can write it and in prose for those that cannot, and the two would diverge — with the draft itself declaring the prose non-authoritative. cli#20 is currently in that degraded state, carrying its four constraints as body prose because the session's tool surface exposes no dependency write. That paragraph becomes a lie the moment anyone adds a real edge, and it is labelled as provisional for exactly that reason.
Tooling consequence
Most of the friction we first attributed to the platform was an artefact of one agent surface. gh gained native sub-issue support in v2.94.0, and dependencies and issue types are core verbs. What genuinely warrants a wrapper is narrow: number → database-id (with the batch form), milestone-title → number (state-aware), reorder-children, and a re-parent guard. QuantEcon/cli#11 has been rescoped accordingly — its original premise, that sub-issue and dependency linking needs bespoke helpers, no longer holds.
Open verification items
- Does
percent_completed count not_planned / duplicate closures as completed? (finding 2)
- Is the template
type: key silently dropped for openers without push access, as the REST API is?
- Does a markdown template's
type: key work as documented in prose?
- Do the UI and GraphQL sub-issue orderings agree with the REST ordering verified in finding 3?
On the fixture. Adoption clause 4 calls for the dashboard ordering change to be verified "against a deliberate re-order on a public tracker." QuantEcon/cli is private, so cli#20 exercises the redaction path but cannot serve as that fixture. QuantEcon/meta#358 is public and now carries a child, so it is a candidate once it has enough children to re-order meaningfully.
Field report for QEP-6 — Project Tracker Structure and Order (Draft, #18). Filed as an issue rather than a PR comment because of its length; proposed amendments to the draft follow as a comment below, so they can be discussed separately from the findings.
What was tested
On 2026-08-26 we built a QEP-6-conformant project tracker from scratch against a real, pre-existing issue set: QuantEcon/cli#20 — 17 work items, five phases, over a repo whose issues had accumulated since 2026-06. This is the "registered existing tracker brought into layout" case from Adoption clause 2, not a greenfield one: the items already existed, already had a parent, and already carried their own history.
Method and its ceiling
The tracker itself was built for real, and the ordering behaviour in finding 3 was verified live. The
ghcapability claims below are source-derived, not executed — noghbinary was available in the session, andapi.github.comwas repo-gated. They come fromcli/clisource at pinned release tags,github/rest-api-descriptionOpenAPI 1.1.4, andgithub/docsmarkdown — the artefacts the rendered doc sites are generated from, which is stronger provenance than the doc pages, but is still reading rather than running. One smoke test on a throwaway repo should confirm before anything is built on them.What the draft got right
Worth recording first, because it is the larger share of the result.
Projectissue type applied cleanly at creation and reads back reliably. Unit identity is the least troublesome carrier in the set.Next:→ phase table → gates → sequencing rationale → out-of-scope covered every fact the structure could not hold. We never once wanted a field it lacked.Findings
1. Creating a tracker is a destructive operation on whatever hierarchy the items already belonged to — silently
The ten items claimed from QuantEcon/meta#358 were detached from it by the act of linking them to the new tracker. Sub-issues are single-parent, so this is inherent. What makes it a finding is that it is silent and non-optional in the standard tooling: both
gh issue edit P --add-sub-issue Candgh issue edit C --parent Ppassreplace_parent=trueunconditionally, with no opt-out flag, while the REST endpoint exposes it as optional. Adding an item and stealing it from another tracker are the same gesture, indistinguishable at the call site.The consequence in the field: meta#358 — the org's QEP-2 rollout tracker — was left holding only the one item that had been closed, and therefore reported
{total: 1, completed: 1, percent_completed: 100}. A programme whose pilot has not started displayed as complete. No error, no warning; the rollup simply changed in a repository nobody was looking at while the new tracker was being built.Repaired 2026-08-26: cli#20 is now a sub-issue of meta#358, and the closed item moved into cli#20 where its history is legible. meta#358 now reads 0% of 1 child.
2. Progress arithmetic breaks in two ways the draft does not cover
sub_issues_summary.completedappears to count every closed child regardless ofstate_reason, so scope cut withnot_plannedorduplicatewould read as progress. Unverified — a cheap addition to the smoke test.Observed rather than theorised: meta#358 is a hybrid — part programme (delegating to
cli), part project (org default labels is real leaf work), with four of its six sections tracked as body checkboxes. The draft's clean programme/project split has no slot for this shape, and this shape is what exists.3. Order is the weakest-supported carrier — but the read-back contract holds ✅
Of the six carriers, order is the only one with no
ghverb at all (a repo-wide grep forreprioriti|after_id|before_idacrosscli/clireturns zero matches). It is reachable only throughgh api, with the most error-prone signature in the set: the parent is an issue number in the path while the children are integer database IDs in the body. Both are bare integers; the API cannot distinguish them. And no--jsonfield yields an issue's database ID (fullDatabaseIdexists onPullRequestFieldsonly), so each reference costs a resolution call unless batched off the sub-issues list endpoint.The open question was whether the read-back reflects position — the draft assigns order to "sub-issue list position", and the write mechanism being GA says nothing about the reader. Verified live on cli#20:
A reprioritize write is reflected in the list read-back, every other item holds its relative position, and the resulting order is neither ascending nor descending by issue number. QEP-6's ordering thesis has a verified reader. (Verified via REST; the UI and GraphQL orderings were not cross-checked.)
4. The status stamp is the only carrier where a second writer destroys the first's work
There is no partial-body update and no compare-and-swap on either the CLI or REST path — body writes are whole-replace. Two agents, or an agent and a human, refreshing the same tracker inside one window clobber each other, including body content unrelated to the stamp. The draft places the stamp in the body correctly — GitHub has no freshness primitive — but should say that a refresh is a read-modify-write inside a single turn, and that everything outside the stamp must survive it.
5. The milestone naming rule is vindicated on a second, purely mechanical ground
gh issue list --milestonerunsstrconv.ParseInton its argument first, so a milestone titled"2026"is looked up as milestone number 2026 and fails. The draft already bans sequence tokens for identity reasons; purely numeric names break tooling for an unrelated reason and should be banned alongside them.6. Templates reach further than expected, but cannot emit the draft's own stamp
type:is a real issue-template front-matter key (GA 2025-04-09), so a tracker template can stamptype: Projectat creation — Adoption clause 1's identity requirement, satisfied for free. Templates provably cannot set milestone, parent/sub-issue edge, dependencies, or list order; those are post-creation for every producer.The trap: an issue form converts field labels into body headings (
### <label>), so it structurally cannot emit## Where we stand (verified YYYY-MM-DD)— wrong heading level, and the date is dynamic while a label is static. A markdown template passes body text through verbatim and can still settype:. Two corollaries:markdown:elements in a form are never submitted, so skeleton text must be a prefilledtextareavalue; and theissueswebhook payload carries no template identifier at all, so automation should key onissue.type.name— the type is the template's signature.7. Two facts the draft leaves open, both with native answers
state_reason(completed/not_planned/duplicatewithduplicate_of). The item keeps its place; why it closed is the item's business, not the list's. Subject to the caveat in finding 2.--add-blocked-byis a coreghverb, so this is cheap). Where the gate is a decision, create the issue for the decision. Where it is phase-level, the body's gates section already covers it. Stating the derivation explicitly stops anyone inventing a carrier for it.8. Scope: the order carrier has no known writer on GHES
The four sub-issue REST endpoints are absent from every GHES OpenAPI description 3.14–3.18 (verified against a control — ordinary issue paths are present in the same files). The native
ghflags route through GraphQL and may work, but reordering exists only as a REST endpoint that is not in the GHES spec. The draft should say GitHub.com explicitly rather than let GHES support be inferred from a release note.On the org's working assumption
The assumption under test was: any mechanism not supported by GitHub infrastructure must instead be carried in the issue body via templates.
On GitHub.com, that trigger never fires for any QEP-6 carrier. All six are natively readable and writable through
gh. The body carries exactly one thing — the stamp — and does so because GitHub has no freshness primitive, by design rather than as fallback.The test is worth keeping, with its two halves kept distinct: does GitHub have a carrier? (no → body, legitimately) versus can our tooling write it? (no → fix the tooling; do not move it to the body). Body-carriage of a natively-carried fact manufactures precisely the mirror the draft's Alternatives considered section rejects: the fact would live natively for producers that can write it and in prose for those that cannot, and the two would diverge — with the draft itself declaring the prose non-authoritative. cli#20 is currently in that degraded state, carrying its four constraints as body prose because the session's tool surface exposes no dependency write. That paragraph becomes a lie the moment anyone adds a real edge, and it is labelled as provisional for exactly that reason.
Tooling consequence
Most of the friction we first attributed to the platform was an artefact of one agent surface.
ghgained native sub-issue support in v2.94.0, and dependencies and issue types are core verbs. What genuinely warrants a wrapper is narrow:number → database-id(with the batch form),milestone-title → number(state-aware),reorder-children, and a re-parent guard. QuantEcon/cli#11 has been rescoped accordingly — its original premise, that sub-issue and dependency linking needs bespoke helpers, no longer holds.Open verification items
percent_completedcountnot_planned/duplicateclosures as completed? (finding 2)type:key silently dropped for openers without push access, as the REST API is?type:key work as documented in prose?On the fixture. Adoption clause 4 calls for the dashboard ordering change to be verified "against a deliberate re-order on a public tracker."
QuantEcon/cliis private, so cli#20 exercises the redaction path but cannot serve as that fixture. QuantEcon/meta#358 is public and now carries a child, so it is a candidate once it has enough children to re-order meaningfully.