epic #185: Backlog Stubs Become GitHub Issues - #217
Conversation
Oversized scope now leaves GitHub issues rather than a committed backlog.md. A stub is an epic identified but not yet planned: it carries the repository's declared epic classification plus exactly one label denoting the unplanned state (record invariants 1 and 2). - delivery_config: resolve the unplanned label through the one shared chain (github.unplanned-label, default `backlog`); `label_exists` and the batch `ensure_labels` preflight. - create_gh_issues: the canonical classification is a caller argument (--classification-label / --classification-type) defaulting to the story values, so /nxs.epic files stubs as epics through the same filer. Every label that will be applied is upserted before any issue is created; one that can be neither created nor found stops the run with nothing filed (invariants 3 and 19). - /nxs.epic Phase 2b: write transient stub work-items and file them — parentless, ordering wired as native blocked_by, no project touched when none is configured, and no backlog.md written. Promotion by issue number, the close-side writer, the migration and the retirement of the files are the following stories. Closes #186
…kpoint /nxs.close no longer appends deferred scope to a committed backlog.md. It authors transient stub work-items locally (Phase 5) and files them as open issues in a new Phase 7.4 — the first step after the close checkpoint. The phase position is forced from both sides: issue creation cannot be undone, so it must follow consent; and the close record must name the resulting issue numbers, so it must precede the Phase 7.6 commit that pushes that record in --pr mode. Declining the checkpoint now leaves GitHub untouched (record invariant 7). - create_gh_issues: refuse, in the preflight, any work-item carrying the unplanned label together with a `parent:`. A stub filed beneath the epic being closed would deadlock the all-sub-issues-closed gate that stage hard-blocks on, and that gate deliberately has no exemptions by kind (invariant 6). The whole batch stops with nothing created. - nxs.close: Phase 5 authors work-items and writes no backlog.md; the checkpoint discloses the filing as irreversible; Phase 7.4 files the batch and fills the numbers into the close record; the --pr commit, the close comment's durable pointers and the final report all name stub issues instead of a file path. - close-record template: Deferred Scope carries issue numbers, not a backlog.md pointer. Promotion by issue number, the cross-feature query and the migration are the following stories. Closes #187
Promotion is now "plan this epic", addressed by the stub's issue number. The record's scope edit stands: closing the stub is not the promotion — `/nxs.epic <n>` populates that same issue with the epic body, the planning meta block and the epic classification, and takes the unplanned label off. Nothing is created and nothing is closed, so the number the scope was deferred under is the number it ships and closes under, and every dependency edge and body mention written at decomposition stays valid (invariant 4). The flag selects the operation and the shape of the argument never does: a bare number always means "plan this epic", `--from #<n>` always means "load this already-planned epic". Slug lookup is retired with the file it globbed — a stub's issue number is its only identifier. - nxs_gh_create_epic: `--promote <n>` populates in place via `gh issue edit`. Legality is re-checked in the filer before any write — a target that no longer carries the unplanned label, or a number that does not resolve, is refused with nothing written, and the two are told apart (invariants 12/13). - epic-resolve: an unplanned epic is refused by name, `epic-not-planned`, on every path — `--require-epic` or not. Resolving one would emit an epic with an empty story set, which downstream reads as "plans nothing" rather than "nobody has planned this" (invariant 14). The label comes from the shared publishing resolver, never hard-coded (invariant 18). - /nxs.epic: promotion mode reads issue state only and never globs the docs tree; the feature container comes from the stub's meta block and is created on demand. A promoted stub that proves oversized is re-decomposed and closed as not planned, never as completed. The migration and the cross-feature query are the remaining stories. Closes #188
The pin recorded c51f750012af… while a fresh build hashes bddc67f6f416… — stale since this epic started editing `.claude/`, and never re-pinned by the story commits. Regenerated via the named remedy (pnpm nexus:vendor-tools; pin updated in place, no artifact copied). The old value was also machine-local. `listComponentFiles` walks the managed subtrees with no exclusions, so the 17 gitignored `__pycache__/*.pyc` files that the skills' python tests leave behind were being hashed into the manifest: c51f750012af… and the fc01f39589… this first regenerated to are both pins of a tree that no clean checkout has. The value committed here is hashed with those directories removed, so it is the one a fresh clone reproduces. That fragility is not fixed here — it is outside this epic, and running the skills' python tests before `nexus:vendor-tools` will re-break the pin the same way until the walk skips `__pycache__`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ludes it With one label and the feature held in the issue body, the whole backlog across every feature is open issues carrying the unplanned label — no file glob and no hand-maintained table — and removing it from any view costs one negated filter. The query is asked for, not spelled out: `delivery_config.py backlog-query` resolves the label through the one shared chain and emits it in the three shapes it is needed in — a `gh issue list` (targeted at wherever epics are filed, since a stub is an epic), the `is:issue is:open label:<l>` fragment a link carries, and the `-label:<l>` negation (invariant 18). A repository that renames the label renames its queries with it. - features index: `docs/features/README.md` is created and carries the backlog link — once, in one place. Per-feature nav indexes gain no backlog section: with the feature in the body, a per-feature view is a text search over issue bodies, and ten links that all mean the same thing. - every epic query filters stubs out. A stub is an epic issue, so any query enumerating epics for planned work carries the negation — stated where /nxs.epic describes what it does, with the exclude form to hand. That is the accepted price of a stub keeping its number through promotion. - stage reports: /nxs.epic's decomposition report and /nxs.close's final report both name the cross-feature query beside the numbers they just filed. - sequencing.md: one line at its head names the query as the authoritative inventory. Its wave ordering and rationale are untouched — deciding that page's fate is out of scope. The migration and the retirement of the committed backlogs are what remains. Closes #190
The seven per-feature backlog.md files are gone. Every block they carried as `proposed` is now an open `backlog`-labelled epic issue (#193–#216), filed in one batch through the existing filer, with the dependency graph preserved. Counts, against what planning assumed: **24** proposed blocks, not 22 — two were appended to the PR-Driven backlog on 2026-07-30, after the epic was sized. 13 promoted and 3 superseded blocks were not refiled (invariant 15); git history is their provenance and refiling them would add noise to the query this epic exists to make useful. The dependency graph needed judgement, as the record said it would. Five edges pointed out of the batch at work promoted under the old model and are now literals — #38 (workspace manifest) ×2, #54 (multi-repo distill), #139 (record sub-issue). One was stale: story-analyze-hub was blocked by `hub-born-queue`, abandoned before implementation, and now points at the epic that superseded it, #114. The rewrite is recorded in that issue's body, not just in this message. - create_gh_issues: a `blocked_by` entry may be a literal `#<n>`, resolved against the platform (the migration affordance). Only the sigil form — a bare number is indistinguishable from a batch ref, and an unresolvable literal fails the reference closed rather than dropping an ordering. Its `--dry-run` also stopped working when #186 made the classification a caller argument; it rendered `story` on a stub batch, which is precisely the rehearsal 24 irreversible creations needed. - delivery_config: `resolve epic-label` returned empty where the library returns `epic`, because the CLI's generic path does not know the built-in. /nxs.epic Phase 2b hands that value to the filer, so stubs would have filed unclassified. - multi-paragraph rationale carried over intact — the ten prose-heavy multi-repo entries, and the dated notes on the analyze/close chain. - references repointed ahead of the deletion: sequencing.md names issue numbers beside the slugs it orders, /nxs.epic no longer describes the feature container as holding a backlog file, and the --pr acceptance harness seeds none into its throwaway repo. Left alone deliberately: `libs/origin/v2/.nexus/decisions/`. Those numbered logs are the record of decisions as they were made — including the one that created backlog.md — and are superseded by record #192. Rewriting them to match a later decision would falsify the trail. The empty prime-workspace-instrumentation container went with its file, per the record; it is recreated at that feature's first epic filing. Closes #189
`/nxs.analyze` on #185 found the last committed references to `docs/features/<feature>/backlog.md` still standing in `manual/`, which is a live surface, not the frozen v2 archive. Invariant 17 puts the deletion after every committed reference has been repointed, so this closes the gap c164dbf left — #189 AC5. Slide 11 is the substantive one. It described an append-only file with a `proposed -> promoted` status field; it now describes what shipped — an open epic issue carrying the single `backlog` label, planned in place by `/nxs.epic <issue-number>`. Its "why" bullets gained the two properties the file never had: the whole backlog is one query across every feature, and the issue number survives promotion, so every dependency edge written when the scope was deferred stays valid. Status became issue state. Four smaller repoints: the asset table's "Feature backlog" row is now a "Backlog stub" living at `label:backlog` rather than a path; the surface map no longer lists backlogs under `docs/`, because that surface no longer holds one; and the close-record slide plus its summary card say deferred scope is filed as stub issues after the checkpoint. `.nexus/anchors/workspace-resolution.md` still names the retired glob and is left alone — anchors are regenerated by the distillation PR, and writing the concept store on a feature branch is what /nxs.distill forbids. Recorded in the queue scratch so close reads it as a decision, not a miss. Story issues #188 and #190 were amended on the platform in the same pass: record #192 edited their scope after they were filed, and three acceptance criteria contradicted the shipped design. #188 said closing the stub was the promotion — invariant 4 forbids exactly that — and #188/#190 carried slug-matching and per-feature-README criteria the record retired. The code follows the record; it was the issue text that was stale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
epic: "#185"
pr: 217
date: 2026-08-02
head: 9d42385a3585858bf599987598a214d4daa37968
mode: full
record: "#192"
record_hash: 55493c77228b32ef5e33fdcbb5f03535c3d71b31d90245ba0215ece30a6b011e
findings: { critical: 0, high: 0, medium: 1, low: 2 } |
MEDIUM — two committed documents still point at the retired backlog by slug (#189 AC5, invariants 17 + 11)
Both name a stub by a slug that nothing resolves any more — invariant 11 made the issue number a LOW —
|
Implements the story sub-issues of #185, one commit per story in blocked_by order.
Each commit body carries its own
Closes #<story>line, so merging this PRinto
maincloses the stories it implements. The epic itself closes through/nxs.close, not by merge.Draft opened by
utils/implement-epic.sh;/nxs.analyzeruns against it next.Post-analyze follow-ups (2026-08-02)
/nxs.analyzereported 2 high, 1 medium, 1 low againstc164dbf— no critical, noinvariant violation. All three of its AC divergences were record #192's own scope
edits reaching the code but not the issues. Resolved as follows.
HIGH — the manual still documented the retired file. A real defect, fixed in
9d42385:
manual/assets.htmlslide 11, the asset table, the surface map and theclose-record slide now describe the backlog as a labelled issue query rather than
docs/features/<feature>/backlog.md. Closes the #189 AC5 / invariant 17 gap.HIGH — #188 and #190 carried acceptance criteria the shipped design contradicts.
Planning-artifact gap, not a code defect; both issues amended in place rather than
closed, since both stories shipped:
old title said closing the stub was the promotion, which invariant 4 forbids.
AC4 now states the in-place contract (same issue populated,
backloglabel removed,nothing created, nothing closed); the not-planned close on an oversized stub is its
own criterion (invariant 5); and the three slug-matching criteria collapse into one —
any non-numeric input is a capability description (invariant 11).
README, and a new criterion requires the negated filter on every epic-enumerating
query, not only the default triage view.
Both carry a dated scope-edit note naming the record and its invariants, so the
divergence is legible rather than looking like drift.
MEDIUM — epic #185 success metric 3 still describes the closed-as-completed
lifecycle superseded by in-place promotion. Open; it is an epic-body edit, not a code
change. LOW — #189's 22/12 counts vs the actual 24/13/3 — already corrected in the
record and explained in
c164dbf.Not repointed:
.nexus/anchors/workspace-resolution.mdstill names the retiredstub-promotion glob. Anchors are regenerated by the distillation PR, and writing the
concept store on a feature branch is what
/nxs.distillforbids. Recorded as adecision stub in the queue scratch.