Skip to content

Decision Record: Backlog Stubs Become GitHub Issues #192

Description

@sameera

Decision Record: Backlog Stubs Become GitHub Issues

Summary

Backlog stubs stop being markdown blocks in per-feature committed files and become epic issues
born unplanned
— carrying the epic's own classification from birth plus a single backlog label
meaning "not yet planned". Both writers — oversized-scope decomposition and deferred scope at close —
file them through the existing batch filing path, and promotion populates that same issue in place
rather than creating a second one, so a piece of deferred scope keeps one issue number from the
moment it is deferred through planning, delivery and close. The twenty-two live stubs migrate in one
batch before the committed files are deleted last.

Chosen Approach

A stub is not a new species of issue. It is an epic that has been identified but not yet planned, so
it is filed as an epic and marked with one label denoting that unplanned state; everything else about
it — feature, estimate, candidate stories, originating intent, date — lives in the issue body's meta
block. Both writers reuse the one existing batch filing path with transient work-items rather than
growing a second filing component, which preserves the retry, resume-ledger and idempotency
behaviour that makes irreversible bulk creation safe. Promotion removes the label and fills in the
body; nothing is closed and nothing is created. The deferred-scope writer moves from the local
pre-checkpoint half of close into the post-checkpoint publishing phase, as its first step, because
issue creation is not reversible and because the close record must be able to name the resulting
numbers.

The identity consequence is the load-bearing one. Because the number survives promotion, every
reference written when scope was deferred — a dependency edge between sibling stubs, a mention from
the epic that spawned one, a line in a sequencing table — stays valid for the life of the work
instead of being orphaned the moment the stub is promoted.

Key Decisions

A stub is an epic issue born unplanned, not a separate kind of issue

  • Decision: Filing a stub creates an epic issue carrying the epic classification the repository
    declares, plus one label meaning "not yet planned". Promotion clears that label. No other kind of
    issue is introduced.
  • Why: A stub already is an epic — a functional goal, sized at or below the epic ceiling,
    destined to become exactly one epic. Modelling it as its own kind forces every consumer to learn a
    third species, and forces the classification question to be answered twice: once for the label-mode
    marker and again for the issue-type-mode marker, where no suitable type exists and none can be
    minted without organisation-level administration. Filing it as what it already is answers both at
    once and costs nothing.
  • Refuted alternative: A distinct backlog kind, marked by its own label in label mode and its own
    issue type in type mode. It reads cleanly in a triage list and keeps unplanned work out of the epic
    query by construction. It loses on three counts: the issue type cannot be created in a repository
    whose organisation has not defined one, so the type mode degrades to filing stubs unmarked; it
    forces a second identity when the stub is promoted, orphaning every reference to the first; and it
    asserts a distinction that dissolves the moment anyone asks what a promoted stub became.

Exactly one label denotes the unplanned state; the feature lives in the body

  • Decision: One label, with no per-feature and no per-status variants, denotes that an epic has
    not yet been planned; feature, estimate, candidate stories and provenance are body content.
  • Why: One label makes the cross-feature backlog one query and makes exclusion one negated
    filter — the two outcomes this epic is measured on.
  • Refuted alternative: A per-feature label family, which gives free per-feature filtering and
    renders directly in the issue list. It loses because the label set then grows with the feature
    count, every new feature needs an upsert before its first stub, and exclusion costs one negation
    per feature — precisely the noise this epic exists to remove.

The batch filing path takes its classification from the caller

  • Decision: The shared batch filing path stops treating everything it creates as a story and
    takes the canonical classification — label in label mode, issue type in type mode — from its
    caller, defaulting to today's story value so existing behaviour is unchanged. Stub filing passes
    the epic classification.
  • Why: The path currently stamps the story marker unconditionally, so stubs filed through it
    would be classified as stories — wrong under any model, and doubly wrong here, where the whole
    point is that a stub is already an epic.
  • Refuted alternative: File stubs through the single-epic filing path instead, which already
    applies the epic classification correctly. It loses because that path creates one issue at a time
    with no resume ledger, no retry, and no dependency wiring — exactly the machinery a
    twenty-two-issue migration and every multi-goal decomposition depend on.

Promotion populates the issue in place; it creates nothing and closes nothing

  • Decision: Promoting a stub fills in the epic body, writes the planning meta block, files the
    story sub-issues, and removes the unplanned label — all on the issue that already exists. The
    single-epic filing path gains an update-in-place mode to support this.
  • Why: This is what makes the issue number durable. Under a create-and-close promotion every
    reference to a stub breaks at the moment of promotion, which is precisely why the historical
    dependency edges in this repository point at issues that are closed and superseded. One issue with
    one number for the whole life of the work removes that failure mode rather than managing it.
  • Refuted alternative: Create the epic fresh and close the stub as completed with a comment
    naming its successor, which needs no change to the filing path. It loses because it reintroduces an
    identity hop on every promotion, leaving a permanent trail of closed placeholders that every
    reader and every dependency edge must indirect through.

A stub that proves oversized at promotion is closed as not planned, naming its successors

  • Decision: When planning reveals a stub is larger than a single epic, it re-decomposes into new
    stub-epics and the original is closed as not planned with a comment naming them.
  • Why: The issue cannot become an epic it is too large to be, and it must not be closed as
    completed, which would assert delivery that never happened. Not-planned is the platform's existing
    vocabulary for scope abandoned before it was built.
  • Refuted alternative: Keep the original open as a parent of its successors. It loses because a
    stub is never a parent of anything, and an epic that stays open while its scope lives entirely in
    other issues has no closing condition.

A stub is never a sub-issue; its link to its originating epic is a body mention

  • Decision: No parent relationship is ever created for a stub. The originating epic is named in
    the body, which the platform renders as a back-reference.
  • Why: Close hard-blocks until every sub-issue of the epic is closed, deliberately with no
    exemptions by kind. A stub filed by close as a sub-issue of the epic being closed would deadlock
    the very stage that filed it — and under this model that stub outlives the close by design, since
    it is the epic the deferred work will eventually be planned as.
  • Refuted alternative: A sub-issue link plus a kind exemption on that gate, which would buy the
    native rollup view. It loses because the gate's single virtue is having no bypass — the first
    exemption makes an unapproved decision record exemptible next.

Deferred-scope filing is the first step of the post-checkpoint phase, ahead of both the close comment and the close-record commit

  • Decision: Move filing after the consent gate, run it before the close record's deferred-scope
    section is finalised, and disclose it in the checkpoint's irreversible-actions list.
  • Why: Issue creation cannot be undone, so it belongs behind consent; but the record must name
    the filed numbers, and in pull-request mode the record is committed and pushed before the close
    comment is posted. First-step-after-consent is the only position satisfying both.
  • Refuted alternative: File after the epic close comment and leave the record describing the
    deferred scope in prose. It loses because the record then either ships without numbers or must be
    amended after it was already committed — reintroducing the two-copies problem this epic ends.

The issue number is the only identifier a stub ever has

  • Decision: A stub is identified solely by its issue number — as its identity, as the form
    dependency edges reference, and as the only input that promotes it. The title is a human-readable
    goal summary. No slug is recorded and no slug resolves a stub. A batch-local reference token
    survives only inside a single filing run's input, where edges between goals must be expressed
    before any issue exists; it never reaches an issue body and never reaches the lead.
  • Why: The slug existed only because the file format had no identifier — it was the block heading
    and the sole way to name a stub. An issue has an identifier, and under in-place promotion that
    identifier is stable for the entire life of the work, so a second lookup key would have no source
    of truth, no uniqueness enforcement, and no consumer.
  • Refuted alternative: Keep slug input as an undocumented convenience resolving by search over
    open stub bodies, preserving the habit through the transition. It loses because the habit is worth
    less than a single identifier space, and because a key nothing defines and nothing enforces drifts
    silently the moment two stubs collide.

The flag selects the operation; the unplanned label decides whether it is legal

  • Decision: A bare issue number always means "plan this epic", and is legal only for an epic
    still carrying the unplanned label; pulling an already-planned epic is selected only by its
    explicit flag. Any other input is a capability description. A number naming an already-planned
    epic fails closed with a diagnostic naming the flag.
  • Why: Both inputs now name the same kind of object, so the operation cannot be inferred from
    what is referenced — it must be stated. The label then answers a separate question: whether the
    stated operation applies to an epic in that state.
  • Refuted alternative: Infer the operation from whether the label is present, removing the flag
    entirely. It loses because the same command would silently do two different things depending on a
    label a third party can remove, and because "plan this" and "load this" differ in consequence.

Reuse the one batch filing path with transient work-items; add no stub filer

  • Decision: Both writers and the migration author transient work-items and invoke the existing
    batch filing path.
  • Why: Transient retry with backoff, a resume ledger, idempotent dependency wiring and idempotent
    re-runs are exactly what irreversible bulk creation needs; they already exist and are exercised.
  • Refuted alternative: A small purpose-built stub filer, simpler to read in isolation. It loses
    because it would duplicate the resume and idempotency machinery — the only reason filing
    twenty-two issues in one shot is safe at all.

Dependency references accept a literal issue number, as a migration affordance

  • Decision: The dependency input accepts a literal issue number alongside a batch-local
    reference, resolved directly against the platform.
  • Why: The historical backlog contains edges pointing at work promoted under the old
    create-and-close model, whose stubs are now closed behind different issue numbers; those edges can
    only be expressed as literals. Going forward the need largely disappears — because promotion
    preserves the number, an edge written at decomposition stays valid through promotion without
    rewriting — so this is a migration affordance and a convenience, not a structural pillar.
  • Refuted alternative: Express out-of-batch dependencies as prose in the issue body. It loses
    because it silently drops ordering the migration must preserve, and because accepting a literal
    number is a small, generally useful extension rather than a special case.

Migration is one batch across all backlogs, and only proposed blocks migrate

  • Decision: A single filing run covers all twenty-two proposed stubs; promoted and superseded
    blocks are left to git history and never refiled.
  • Why: Cross-feature dependencies exist among the proposed set, and batch-local wiring resolves
    them only if they are filed together. Refiling closed history would add noise to exactly the query
    this epic is built to make useful.
  • Refuted alternative: Per-feature migration runs, easier to review and to roll back one feature
    at a time. It loses on precisely those cross-feature edges.

The backlog surfaces as one link in one place, and the unplanned filter belongs on every epic query

  • Decision: The cross-feature backlog query is linked once from the features index and named in
    the stage reports; feature navigation indexes gain no backlog section. Because stubs are epics,
    every query that enumerates epics for planned work carries the negated unplanned filter, not only
    the default triage view. Where deleting a backlog file empties its feature container entirely, the
    container goes with it and is recreated at that feature's first epic filing.
  • Why: With one label and the feature held in the body, a per-feature view is a text search over
    issue bodies — brittle and misleading. No feature index links a backlog file today, so there is no
    existing surface to repoint; one feature has no index at all and its container holds nothing but
    its backlog file, and its stubs carry the feature path in their bodies regardless. The
    epic-query cost is the accepted price of a durable identity, and naming it here is what stops it
    being discovered later as a defect.
  • Refuted alternative: Per-feature index entries each linking a filtered search, preserving the
    per-feature navigation path. It loses on brittleness, and on being several links that all mean the
    same thing.

Constraints & Invariants

  1. A stub is an epic issue carrying the repository's declared epic classification; no third kind of
    issue is introduced by this epic.
  2. Exactly one label denotes the unplanned state; feature, estimate, candidate stories, originating
    intent and date are body content, never labels.
  3. The label is upserted before it is applied on every filing path, so filing never fails on a
    missing label in a repository that has never seen it.
  4. Promotion never creates a second issue for the same scope and never closes the stub; the issue
    number a stub is filed under is the number its epic, its close, and every reference to it use for
    the life of the work.
  5. A stub that cannot become a single epic is closed as not planned with its successors named, and
    is never closed as completed.
  6. No stub is ever a sub-issue of any issue, and no stub ever enters an epic's sub-issue set.
  7. No filing path creates any issue before the consent gate for that stage has been passed; declining
    the gate leaves the platform untouched.
  8. Filing is resumable and idempotent — a re-run after partial failure completes only the remainder
    and never duplicates an issue.
  9. Filing succeeds with no project configured, adds the stub to no project, and performs no project
    discovery in that case.
  10. Stub resolution reads issue state only and never globs the documentation tree; promotion works in
    a repository containing no backlog file at all.
  11. The issue number is a stub's only identifier; no second lookup key is recorded in the body or
    accepted as input.
  12. A bare issue number always means "plan this epic" and is legal only while the unplanned label is
    present; loading an already-planned epic is selected only by its explicit flag; any other input
    is a capability description.
  13. An input that fails to resolve to exactly one unplanned epic reports why and creates nothing.
  14. Any stage that reconstructs an epic from its issue refuses an unplanned one with a diagnostic
    naming that state, rather than failing on the absent planning meta block or empty story set.
  15. Only proposed blocks are migrated; promoted and superseded blocks are left to git history and are
    never refiled.
  16. A migrated stub preserves its original recorded date and its full original rationale text, not a
    one-line reduction of it.
  17. The file deletion lands only after both writers, the single reader, and every committed reference
    have been repointed.
  18. Every platform write in this epic resolves its label, classification mode, project target and
    repository target through the shared publishing resolver; no call site hardcodes any of them.
  19. The only new permission required is label creation; where the token lacks it, the stage reports
    the missing label before attempting any issue creation, so a permission gap can never leave a
    half-filed batch.

Risks (BLOCKER / ADDRESS only)

  • ADDRESS — promotion in place requires an update path the epic filing surface does not have.
    That surface is create-only, and the planning meta block is written at creation. Promotion must
    instead populate an existing issue's body, write the meta block onto it, and file its stories
    beneath it. Mitigation: add the update-in-place mode as part of the promotion story and treat "no
    second issue exists after promotion" as its acceptance signal. This is the change that most
    pressures that story's sizing, and it is the price of the durable identity — worth paying, but it
    should be sized deliberately rather than absorbed.

  • ADDRESS — the batch filing path classifies everything it creates as a story. The story marker
    is applied unconditionally rather than supplied by the caller, so stubs filed through it would be
    classified as stories in both modes. Mitigation: parameterize the classification with today's value
    as the default and pass the epic classification when filing stubs. It changes shared code other
    stages depend on and needs its own regression check.

  • ADDRESS — stages that reconstruct an epic from its issue will meet epics that were never
    planned.
    An unplanned stub has no planning meta block and no story sub-issues, which those stages
    currently assume. Mitigation: guard with an explicit "not yet planned" refusal, and confirm no
    enumeration of epics silently treats a stub as a planned one.

  • ADDRESS — migration's dependency graph points outside the batch and contains one stale edge.
    Five proposed stubs depend on blocks promoted under the old model, whose issues are closed behind
    different numbers, and one depends on an item recorded as abandoned and superseded by the
    issue-sourced-planning work. Unresolved references make the batch report incomplete and exit
    non-zero. Mitigation: resolve each outward edge to the issue number that work became, and rewrite
    the stale edge to the epic issue that superseded it. This is judgement work that must precede the
    migration run, and it is the last time it will be needed — once promotion preserves numbers, these
    edges stop breaking.

  • ADDRESS — the close record is authored, and in pull-request mode committed and pushed, before
    the publishing phase.
    Today's phase order assumes everything before the checkpoint is local and
    reversible. Mitigation: finalise the record's deferred-scope section between filing and the commit,
    and treat "the record names the filed numbers" as the acceptance signal.

  • ADDRESS — twenty-two irreversible creations in one run. A wrong body shape or wrong
    classification is twenty-two corrections after the fact, not a revert. Mitigation: run the full
    batch in dry-run and compare one rendered stub against a real stub filed by the first story before
    the live run; the sequencing already places migration last, which makes that comparison possible.

Open Clarifications

None. Five were raised during analysis and resolved at the approval gate; their answers are folded
into the decisions above.

Story Coverage

Every user story in the epic is covered, with the scope edits noted.

  • Decomposition stubs are filed as backlog issues — covered by the stub-is-an-epic decision, the
    single-label decision, the caller-supplied-classification decision, the filing-path reuse decision
    and the surfacing decision; invariants 1, 2, 3, 7, 8, 9, 18, 19. Scope note: the right-size gate's
    option text now discloses an irreversible platform write rather than a local file append, and the
    stubs path writes nothing to the working tree — including the feature container, which is not
    needed until an epic is planned.

  • Deferred scope at close becomes stub issues, filed after the checkpoint — covered by the
    body-mention decision, the phase-position decision and the filing-path reuse decision; invariants
    6, 7, 8, 17. Scope note: the close record's deferred-scope section is finalised between filing and
    the commit, and the checkpoint's irreversible-actions list gains the filing.

  • A stub is promoted by issue number, and closing it is the promotion — covered by the
    populate-in-place decision, the oversized-stub decision, the issue-number-identity decision and the
    flag-selects-the-operation decision; invariants 4, 5, 10, 11, 12, 13, 14. Two scope edits. First,
    the story's title and its promotion criterion are wrong under this design: closing the stub is not
    the promotion — promotion populates the same issue and removes the unplanned label, and the only
    close a stub ever receives is a not-planned close when it proves oversized. Second, retiring the
    slug removes the three slug-matching criteria and replaces them with one, that any non-numeric
    input is a capability description. Net: the criteria go from eight to six, and the story absorbs
    the update-in-place work on the epic filing surface.

  • The committed backlogs are migrated and the files retired — covered by the literal-reference
    decision and the one-batch/proposed-only decision; invariants 15, 16, 17. Scope edit: the
    dependency graph is judgement work alongside the prose-heavy entries — five edges point outside the
    batch at work promoted under the old model, and one stale edge is rewritten to the epic that
    superseded it. Factual correction: there are thirteen promoted blocks, not twelve; twenty-two
    proposed and three superseded are confirmed.

  • The backlog is one cross-feature query, and triage excludes it — covered by the single-label
    decision and the surfacing decision; invariants 2, 17. Scope edits: the per-feature navigation
    criterion is replaced by a single features-index entry linking the backlog query, since no feature
    index links a backlog file today; the feature container holding nothing but its backlog file is
    allowed to go with it; and the story gains the negated-filter requirement on every epic-enumerating
    query, not only the default triage view. Added to this story: one line at the head of the
    sequencing table naming the backlog query as the authoritative inventory, leaving its wave ordering
    and rationale untouched.

Metadata

Metadata

Assignees

No one assigned

    Labels

    decision-recordEpic decision record (why: key decisions, invariants, risks)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions