Decisions

ADR-098: Epics with structured requirements, agent-driven decomposition, live progress rollup

SWARM-AA landed this evening and gave warp items three structural fields they were missing — `parent_id` (the link from a child item up to the work it belongs to), `size` (an estimate, S/M/L/XL), and `target_date` (when…

#098

ADR-098: Epics with structured requirements, agent-driven decomposition, live progress rollup

Status: Accepted Date: 2026-04-25 Related: ADR-077 (warp as session-independent agent coordination layer — the claim/heartbeat/complete protocol every item already obeys), ADR-090 (AI is the scaffold; pipelines are the runtime — the lens this ADR uses to decide which surfaces stay AI and which graduate to deterministic pipelines), ADR-096 (warp as fleet PM via one-way GitHub-issue mirror — defines source and the read-only mirror cut this ADR composes with), and ADR-097 (PM-in-factory — the /pm dashboard surface this ADR adds epics to). SWARM-AA shipped today added parent_id, size, and target_date to warp_items; this ADR is the next layer up.

Context

SWARM-AA landed this evening and gave warp items three structural fields they were missing — parent_id (the link from a child item up to the work it belongs to), size (an estimate, S/M/L/XL), and target_date (when the operator wants the work landed by). Today's /pm dashboard groups items by parent_id, paints due-band colours from target_date, and totals the open work per group. The grouping works; what it groups is structurally thin. A "parent" today is just an item that happens to have other items pointing at it — there is no shape that says "this is the umbrella, those are the steps", no place to record what the umbrella is supposed to deliver beyond a free-text title, and no way to look at the umbrella card and read off "3 of 7 done" without counting children by hand.

The operator's request, captured verbatim from tonight's session: "I'm thinking of creating epic with requirements and then tickets from it and keeping track of the progress or the agents will". The shape that fits the request is an epic — an explicit kind of warp item that carries a list of structured requirements, can be decomposed by an agent into one child ticket per requirement, and surfaces a live progress rollup (children done over total) on its card. None of the three pieces is novel as a concept; the discipline this ADR introduces is making them structural in warp's schema rather than leaving them as conventions on top of parent_id that drift the moment two operators (or two agents) disagree on how to apply them.

There are three reasons to do this now rather than defer. First, the swarm is already producing parent/child item families informally — tonight's session opened roughly fifteen items with ad-hoc "SWARM-XX: parent" / "SWARM-XX-a: child" naming, which is the third repeat of this pattern across the last four sessions and the threshold ADR-090 names for "graduate the convention into structure". Second, the /pm dashboard is the surface where the operator actually reads fleet state between sessions, and "N items in this group" is the wrong information density when the operator wants to know how close a piece of work is to landing. Third, the decomposer step is the cleanest case in the fleet for ADR-090's two-phase pattern (an AI agent does the first decomposition by reading the epic and proposing requirements; by the third epic that AI scaffolding graduates into a deterministic "Suggest requirements" pipeline). Naming the epic shape now makes the graduation path obvious; deferring it locks the fleet into ad-hoc parent/child for another N sessions.

The cut this ADR does not take, equally important: stories as a third level. Two levels (epic → ticket) is enough for the swarm's actual workload at current scale. A third level pays a permanent schema/UI tax for a value (mid-tier grouping) that nothing in the current workload demands. If a future workload demands it, a follow-up ADR re-opens the question; until then, two levels is the contract.

Decision

A warp item gains an explicit kind (ticket | epic, default ticket), a structured requirements list (only meaningful when kind = 'epic'), a decomposer endpoint that creates one child ticket per pending requirement, and a progress rollup computed at read time from the children that point at the epic via parent_id. The cut from today's schema is additive — every existing warp item keeps working unchanged because every existing item is kind = 'ticket' and has empty requirements. The cut from today's /pm dashboard is additive in the same way — every existing parent/child grouping keeps rendering, and epic groups simply gain a progress bar where the count used to be.

The schema additions on warp_items:

kind           TEXT NOT NULL DEFAULT 'ticket'   -- 'ticket' | 'epic'
requirements   JSONB NOT NULL DEFAULT '[]'      -- only populated when kind = 'epic'

The shape of one entry in the requirements JSON array is fixed:

{
  "id": "auth-flow",                  // slug, stable across decompositions
  "summary": "Implement login + JWT issuance",
  "status": "pending",                // 'pending' | 'spawned' | 'done'
  "ticket_id": "01HZ..."              // set after decompose; nullable until spawned
}

id is a slug the operator (or the agent that proposed the requirement) chooses; it is stable across re-decompositions so the requirements editor can preserve order and so re-running decompose is idempotent. summary is the free-text human-readable line. status carries the requirement's lifecycle independently of the child ticket's own status — pending means no child has been spawned yet, spawned means a child ticket exists and ticket_id points at it, done means the operator (or agent) has marked the requirement satisfied without (or after) a ticket. The done state matters for requirements that turn out not to need a ticket — operator wants the affordance to mark a requirement done by hand without spawning work — so the rollup's denominator stays honest.

parent_id, size, target_date, priority, tags, status, and the rest of the existing item shape work for both kinds without change. An epic has all of those fields; a child ticket of an epic has all of those fields too, and points up via parent_id = <epic_id>. The rollup never reads requirements directly — it reads the children at WHERE parent_id = <epic_id>. Requirements are the plan; children are the work. A requirement with status = 'spawned' whose ticket_id was deleted is a stale plan entry, not a stale rollup; the UI surfaces it as "spawned but no longer present" so the operator can re-spawn or mark it done.

Three rules follow from the cut and are non-negotiable:

  1. Decomposition is explicit and idempotent. The endpoint is POST /api/v1/items/{epic_id}/decompose and re-running it spawns only pending requirements. The endpoint takes an optional body ({ tags: [...], priority: 'low' | 'normal' | 'high' }) for fields the operator wants to override on the spawned children; everything unset inherits from the epic. For each requirement with status = 'pending', the handler creates one child item with kind = 'ticket', parent_id = <epic_id>, title = "<epic.title>: <requirement.summary>", tags = epic.tags ++ requirements.body.tags, priority = body.priority ?? epic.priority, status = 'ready'. Then it patches the epic's requirements[i] to status = 'spawned' and ticket_id = <new_id> in the same transaction so the epic and the children commit together. Re-decomposing skips every requirement whose status is spawned or done and only spawns the ones still pending. This is the property that makes the endpoint safe to re-run — an operator who edits an epic to add a new requirement and clicks Decompose again gets one new child for the new requirement and zero churn on the existing ones. Auth: the endpoint requires an agent or admin scoped key (per the existing key-scope contract from the warp env block), which is the same scope already required for POST /items so no new key shape is introduced.

  2. Progress is computed at read time, not denormalised. GET /api/v1/items/{epic_id} adds a progress block to the response: { total, done, in_progress, blocked, ratio } where total is the count of children with parent_id = <epic_id>, the per-status counts come from the same query grouped by status, and ratio = done / total (zero when total is zero). The query is one indexed scan against parent_id — at the fleet's actual scale (epics carry under 100 children typically; the largest in the swarm's history was 23) this costs less than the JSON serialisation of the response. The denormalised alternative — a children_done / children_total counter on the epic row, updated by triggers on the children — pays a permanent tax (every child write fans out to a parent update; the trigger is one more thing that can drift) for a performance win that the workload doesn't demand. We pay the read-time cost always so we never debug a drifted counter. If a future workload (say, an epic with a thousand children) makes the read-time cost real, denormalisation is one ADR away; until then, the simpler shape wins.

  3. The decomposer is auto-only for the spawn step, never for the create-the-epic step. The operator (or an agent acting on the operator's behalf) creates the epic with its requirements list explicitly; the system never auto-creates an epic when an item starts collecting children, never auto-promotes a ticket to an epic when its description grows past a threshold, never auto-fills requirements from the epic's title via a one-shot LLM call at create time. The decomposition step itself is invoked explicitly via the endpoint or the UI's Decompose button — never on epic creation, never on epic update, never on a schedule. The reason is the same one ADR-079 names for the approval substrate: any auto-side-effect on a piece of work the operator is editing destroys the operator's ability to review the plan before the work is spawned. The operator writes the requirements, reviews them, then clicks Decompose. The agent that proposed the requirements (in the AI-scaffolding phase, see below) is one click away from the operator's review at every transition.

UI surfaces in factory /pm

The /pm dashboard already groups items by parent_id (per ADR-097); this ADR adds four affordances to that grouping when the parent is kind = 'epic':

  • Epic card carries a coloured progress bar. The bar uses the existing dueBand colour palette from ADR-097 — green for done, yellow for this-week, orange for next-week, red for overdue — but applied to progress.ratio rather than target_date. Specifically: ratio 1.0 is solid green; ratio in (0.5, 1.0) is green-on-yellow proportional fill; ratio in (0, 0.5] is yellow-on-grey proportional fill; ratio 0 with no children is grey "not started"; ratio 0 with children all blocked is red. The bar replaces the N items count that today's parent-with-children groups show; for non-epic parents the existing count keeps rendering unchanged.

  • Decompose button on epic cards with at least one pending requirement. The button fires POST /api/v1/items/{epic_id}/decompose with the epic's existing tags and priority (no body fields populated — the inheritance defaults work for the common case). Disabled with a tooltip ("no pending requirements") when every requirement is spawned or done. Confirms with a one-line dialog naming how many children will be spawned ("Spawn 3 child tickets for this epic?") so an operator who clicks the button by accident has a back-out.

  • Edit-epic drawer has a requirements editor. The drawer (the existing edit-item drawer from ADR-097, conditional on kind = 'epic') gains a section labelled Requirements with one row per entry in the requirements array. Each row carries the slug, the summary (editable inline), the status badge (pending / spawned / done), a drag handle to reorder, a checkbox to mark a pending requirement as done without spawning a ticket, and a link-out to the spawned ticket when ticket_id is set. An "Add requirement" row at the bottom opens a two-field form (slug + summary) and appends to the list. Reordering is a drag-and-drop that re-writes the requirements array on save; the slug-as-stable-id property means re-ordering never re-spawns work. Deleting a spawned requirement is gated behind a confirmation that names the child ticket id, because the child ticket is the actual work and orphaning it is rarely what the operator means.

  • Group-by-epic on the dashboard already groups by parent_id. That grouping continues unchanged. The header of each group is what changes: for groups whose parent is kind = 'epic', the header carries the progress bar described above plus the epic's title and target_date; for groups whose parent is not an epic (a regular parent ticket with children), the header keeps today's N items count.

Pattern fit — AI scaffolding to deterministic pipeline

The decomposer is a textbook case for ADR-090's two-phase pattern, and naming the path now means the graduation happens by design rather than by accident.

Phase 1 — AI scaffolding (today, first three epics). The first time an operator creates an epic, the workflow is: operator writes the epic title and a one-paragraph description, fires off an agent (existing gyrum-implement shape works; or a one-shot prompt against an LLM) with the prompt "look at this epic, propose 3-7 requirements as JSON matching the schema in ADR-098", reviews the proposed JSON, pastes it into the requirements editor, clicks Decompose. This is the AI-as-scaffolding shape: a one-shot LLM call whose output the operator reviews before any side effect lands. The decomposer endpoint itself is deterministic — the LLM only proposes the plan; the spawn is structural.

Phase 2 — graduation trigger. By the third epic where the operator has gone through Phase 1 and the proposed requirements have landed without significant edits, the pattern is stable enough to graduate. The graduation looks like: the requirements editor gains a "Suggest requirements" button that calls a deterministic prompt (the same prompt Phase 1 used, now lifted into the codebase as a versioned template) → returns a proposed list → operator reviews and accepts (or edits, or rejects) → the accepted list is saved to the epic's requirements. The LLM is still in the loop, but the prompt is fixed, the input shape is fixed, the output is schema-validated, the operator's review is explicit, and the path through the system is one named pipeline rather than ad-hoc agent invocation. Cross-link: this is the exact lifecycle the memory note "AI is scaffolding, pipelines are the target" describes — third-repeat heuristic, ADR-090 lens, three-layer model from ADR-086.

What the graduation rules out. Auto-suggesting requirements on epic creation (ADR-079: never side-effect on the operator's edit). Auto-applying suggested requirements without review (same). Letting the LLM call the decomposer endpoint directly (the spawn step is structural; the LLM proposes, the operator decomposes). Hiding the prompt template — once graduated, the prompt lives in dark-factory/prompts/suggest-requirements.md so an operator who disagrees with a suggestion can read why the LLM proposed it and edit the prompt rather than the output.

Consequences

What becomes easier:

  • Status report at session-start is one glance at the /pm dashboard. The operator who walks in cold sees five epics with their progress bars (3/7, 5/5, 0/4, 12/12, 1/9) and instantly knows where the fleet is. The same information today requires opening five parent items, counting children, and mentally totalling — the cost is small per epic and large across the fleet.

  • Agents pick up child tickets without losing the epic context. A child ticket has parent_id = <epic_id>; the agent's claim handler can fetch the epic in one extra read and surface its title, requirements, and target_date in the agent's session prompt. The operator stops having to write "this belongs to SWARM-XX, see that item" in every child's description because the link is structural.

  • Decomposition becomes a one-click step rather than a five-minute paste-spawn-link loop. Today an operator who wants to spawn five children for a parent runs warp-add five times, copies each id, edits the parent's description to list them, and hopes nothing got lost. The decomposer endpoint collapses the loop to one button.

  • Requirements stay attached to the work even after the children land. A child ticket that ships and closes leaves a permanent record in the epic's requirements[i].status = 'done' — the epic becomes its own one-page log of what was scoped and what landed. The current ad-hoc "list children in the parent's description" loses that log the moment the description gets edited for any other reason.

What becomes harder:

  • Two write paths to keep in sync (epic.requirements + child.parent_id). The decomposer endpoint writes both in one transaction so they commit together, but every later edit to either side has to consider whether the other side needs updating. Mitigation: the requirements editor never lets the operator orphan a spawned requirement without a confirm dialog naming the ticket id; the rollup never reads requirements directly so a drifted requirement entry doesn't corrupt the progress bar; the read-time rollup is the source of truth for "how done is this epic" and requirements is the source of truth for "what was the plan".

  • The progress bar's colour rule has to land alongside the schema migration. Shipping the schema first and the UI later means epic cards render with no progress information for one release window — confusing rather than broken, but real cost. Mitigation: the three SWARM follow-ups queued at the bottom of this ADR are deliberately ordered (schema migration → endpoint + rollup in the API → frontend), and the API change is backward-compatible (a progress block on the response that older clients ignore), so the only window with mixed shape is between the second and third merge.

  • The AI-scaffolding phase has to be honestly time-boxed. ADR-090's hazard is "scaffolding stays scaffolding forever because graduation is never anyone's job." Mitigation: the third-repeat heuristic is named here so the operator (or a future me reading this ADR) knows what triggers the graduation; the graduation work is itself a queued epic ("SWARM-AW: graduate decompose-suggest from one-shot prompt to deterministic pipeline") that gets created the moment the third epic ships from Phase 1.

  • Two levels of hierarchy is a bet. Stories-as-a-third-level remains explicitly out of scope. The bet is that the swarm's workload doesn't grow past two levels in the next quarter; the loss condition is an operator who keeps creating "epic of epics" by abusing the one parent_id pointer. Mitigation: the cut is named in this ADR's Context section so a future PR proposing stories gets reviewed against the rule rather than approved on its own merits; the API schema's kind enum is a forward-compatible place to add a third value if and when the workload demands it.

What we have signed up to operate:

  • The kind and requirements columns on warp_items and the migration that adds them with safe defaults so every existing item keeps working.
  • The POST /items/{id}/decompose endpoint and its idempotency property, including the test that asserts re-running on a fully-spawned epic is a no-op.
  • The progress rollup query and its index on parent_id (already exists from SWARM-AA, but this ADR depends on it).
  • The /pm dashboard's epic-card progress bar, Decompose button, and requirements editor — three additive surfaces on the existing dashboard, gated on kind = 'epic'.
  • The "Suggest requirements" prompt template (Phase 2), once the third Phase-1 epic ships and the graduation happens.

What we revisit:

  • If decomposing into more than ~50 children per epic becomes routine, the read-time rollup may need to graduate to a denormalised counter. Until then, the simpler shape wins.
  • If the operator finds themselves consistently wanting an intermediate level of grouping (epic → story → ticket), the two-level rule comes back to this ADR for amendment. Not before then.
  • If the AI-scaffolding phase has not graduated to a deterministic Suggest-requirements pipeline by the fifth epic, the graduation work goes back on top of the queue — the third-repeat heuristic isn't optional.

Alternatives considered

  • Leave it at parent_id; don't add kind or requirements. Cheapest schema change (zero), and the swarm has been functioning on the convention. Lost because the convention drifts the moment two operators (or two agents) disagree on how to apply it, and the progress rollup has nowhere to compute against — counting children with no structural sense of "is this an epic" means painting progress bars on every parent, which is wrong for the parent-with-three-related-tickets case that isn't an umbrella. Naming epic structurally is the cheapest way to say "this one is the umbrella."

  • Three-level hierarchy: epic → story → ticket. What Jira does. Lost on YAGNI: the swarm's actual workload at current scale (under fifty open items, handful of epics, under twenty-five children per epic) doesn't have a real story-level. A third level pays a permanent schema/UI tax for grouping that nothing demands. Re-open if the workload demands it; the kind enum is the place to add story if and when.

  • Denormalise progress into the epic row (children_done, children_total counters updated by triggers). Lower read cost, higher write cost, plus a trigger to debug. Lost because the read-time cost is genuinely cheap at the workload's actual scale and the denormalised counter is a class of bug (drift) we don't want to take on. The simpler shape wins until the workload changes.

  • Auto-decompose on epic creation: run the LLM at POST /items time and create children atomically. Saves the operator one click. Lost on ADR-079: any auto-side-effect on the operator's edit destroys the operator's ability to review the plan before the work spawns. The plan is what the LLM is best at proposing and worst at being trusted with; the operator's review is the gate that makes the LLM's proposal safe.

  • Bidirectional sync between epic.requirements and child.title (edit either, both update). Avoids the two-write-paths problem. Lost on the same shape ADR-096 names: bidirectional sync is a permanent tax for a value (one-click edit-anywhere) that doesn't justify it at this workload's scale. The decomposer-writes-both-once-then-they-evolve-independently shape is honest about the cut.

  • Use GitHub issues with parent/child via the Projects v2 API instead of warp-native epics. Mirror is already one-way per ADR-096. Lost because the ADR-096 cut explicitly leaves epic-level structure to warp; GitHub issues are operator-tracked work that mirrors in. Asking GitHub to be the structural home of fleet-level epics re-opens the bidirectional-sync question that ADR-096 closed.


Amendment 1 — depends_on on requirements (2026-04-25)

Amendment note (2026-04-25, SWARM-BE). The original Decision § fixed the requirements entry shape at { id, summary, status, ticket_id } and made the decomposer spawn a child for every pending requirement in one pass. That works for the parallel case (every requirement is independent) but is wrong for the strict-ordering case (each requirement builds on the previous one). The operator's framing, verbatim from tonight's session: "if we have epic, we need ordering because that will be steps that build on each other, unless they are depends on". This amendment adds an optional depends_on field to each requirement and gates the decomposer on it. The non-negotiable rule from §Decision rule 1 — "Decomposition is explicit and idempotent. The endpoint is POST /api/v1/items/{epic_id}/decompose and re-running it spawns only pending requirements" — survives verbatim; this amendment narrows the "pending" set further by also requiring every dependency to be done.

The requirements entry shape gains one optional field:

{
  "id": "auth-flow",
  "summary": "Implement login + JWT issuance",
  "status": "pending",
  "ticket_id": "01HZ...",
  "depends_on": ["schema-migration", "user-table"]
}

depends_on is a list of requirement ids within the same epic. Empty or absent means the requirement has no prerequisites and is eligible to spawn immediately. A non-empty list means the decomposer only spawns a child ticket for this requirement when every referenced requirement has status = 'done'. spawned is not enough — a spawned-but-not-yet- done dependency means the prerequisite work is still in flight, and spawning the dependent ticket now would put an agent on work whose inputs don't exist. The decomposer treats unmet depends_on exactly the way it treats status != 'pending': silently skip, no error, re-run later when the dependency lands.

Two common patterns fall out of this single field:

  • Strict linear ordering = every requirement (after the first) carries depends_on: [<previous_req_id>]. The decomposer spawns one child at a time; each child's completion triggers the next spawn on the next decompose run. This is the "steps that build on each other" case.
  • Fully parallel = every requirement carries depends_on: [] (or the field is omitted). The decomposer spawns every child in one pass. This is today's behaviour — backward-compatible by construction.
  • Mixed DAG = some requirements carry one or more depends_on entries, others don't. The decomposer spawns the unblocked subset per run, and the operator (or the agent on the next decompose) sees the rest catch up as their dependencies land.

The requirements editor in the /pm dashboard's edit-epic drawer (per §UI surfaces) gains a depends_on multi-select on each row — populated from the slugs of other requirements in the same epic, sorted by their position in the list. Rows whose depends_on set has any unmet entries render with a "waiting on X, Y" badge in the status column, so an operator scanning the editor sees at a glance which requirements are gated and on what. Epics with simple linear dependencies render visually as a list (each row's badge points at the row above); epics with empty depends_on everywhere render the way they do today (a flat list with no badges); epics with branching DAGs render as a list whose badges fan out to multiple parents.

A dependency cycle is a write-time error in the requirements editor and a 400 response on the decompose endpoint — the validator rejects any requirements array whose depends_on graph is not a DAG (a directed acyclic graph — every edge points from a requirement to one of its prerequisites, and no path of edges loops back to its starting requirement). The slug-as-stable-id property from the original Decision § means re-ordering the list never invalidates a depends_on reference, and deleting a requirement that something else depends on is gated behind a confirm dialog naming every dependent.

The progress rollup from §Decision rule 2 — "Progress is computed at read time, not denormalised" — is unchanged. depends_on shapes the plan (which requirements are eligible to spawn now), not the rollup (what fraction of children are done). The rollup keeps counting children at WHERE parent_id = <epic_id> exactly as before; it does not need to know about dependencies because every spawned child is real work whose status the rollup already reads.

Amendment 2 — Claim returns a context bundle (2026-04-25)

Amendment note (2026-04-25, SWARM-BE). The original Decision § said child tickets carry parent_id = <epic_id>, and §Consequences noted "the agent's claim handler can fetch the epic in one extra read and surface its title, requirements, and target_date in the agent's session prompt". That extra read is real cost paid by every agent on every claim, and the "where am I in the plan" question requires a third read against the sibling tickets. The operator's framing, verbatim from tonight's session: "with an epic and a ticket when a claude claims it you can provide not just the ticket, but the epic and it's place in the process". This amendment makes the claim response itself carry the epic and the position, so the agent gets ticket + parent + place-in-line in one round-trip. The non-negotiable rule from ADR-077 — claim is atomic and lease-bearing — is unchanged; only the shape of the response changes.

POST /api/v1/items/{id}/claim returns a bundle:

{
  "item": { "id": "...", "title": "...", "status": "in_progress", "parent_id": "..." },
  "epic": {
    "id": "...",
    "title": "...",
    "target_date": "2026-05-01",
    "requirements": [ { "id": "...", "summary": "...", "status": "...", "depends_on": [] } ]
  },
  "position": {
    "requirement_id": "req-3",
    "index": 3,
    "total": 7,
    "depends_on_done": ["req-1", "req-2"],
    "depends_on_pending": [],
    "next_requirements": ["req-4"]
  },
  "sibling_progress": {
    "ready": 2,
    "in_progress": 1,
    "blocked": 0,
    "done": 3
  }
}

item is the claimed ticket exactly as it exists today — the existing shape is preserved so older clients that ignore the new keys keep working. epic is the parent item (resolved via item.parent_id) with kind = 'epic' and its requirements array; if parent_id is null or the parent is not an epic, epic is null. position locates the claimed ticket inside the epic's plan: requirement_id is the slug whose ticket_id matches item.id, index and total are the 1-based position and length of the requirements array, depends_on_done and depends_on_pending partition the requirement's depends_on list by the dependency's current status, and next_requirements lists the slugs of requirements whose depends_on includes this one (i.e. what unlocks when this ticket completes). sibling_progress is the per-status count of every other child ticket pointing at the same epic — the same numbers the /pm dashboard's progress bar paints, returned in the claim response so the agent doesn't have to fetch the epic's full child list to know "am I the last one or are there three others in flight".

Standalone tickets — parent_id = null — get the same response shape with epic: null and position: null and sibling_progress: null. The shape is the same so client code branches on epic === null once rather than handling two response shapes; the keys are present so a JSON-schema validator never sees a missing field.

Three integration points carry the bundle through to the agent:

  • The MCP (Model Context Protocol — the structured tool-call interface Claude Code sessions use to talk to external services) tool warp_claim returns the bundle verbatim, so a Claude Code session that calls the tool has the epic, the position, and the sibling progress in its tool-result context immediately — no follow-up warp_get against the parent.
  • The gyrum-start-work --warp-id <id> command (when claiming an item via warp at start-of-work time) writes the bundle to .gyrum/context.json in the freshly-created worktree. The agent's first prompt can then reference "you're at step 3 of 7 of the <epic.title> epic, requirements 1 and 2 produced , this ticket unblocks requirement 4" without any extra API calls.
  • The shell warp-claim <id> wrapper prints the bundle as pretty JSON to stdout (today it prints just the item) — operators eyeballing a claim see the epic and the position too, which closes the gap between "I claimed this" and "I know what this is part of".

The bundle is computed at read time from the same indexed scan the progress rollup uses (WHERE parent_id = <epic_id>), one extra fetch for the parent row, and one in-memory partition of depends_on against the requirements array. Cost is bounded by the size of one epic's requirements + children — at the workload's actual scale (under 25 children, under 10 requirements per epic) this is one extra row and a small map lookup per claim. The denormalised alternative — caching the bundle on the child row at decompose time — is rejected for the same reason §Decision rule 2 rejected denormalised progress: a cached bundle drifts the moment a sibling's status changes, and the bundle's value is precisely that it reflects the live state of the epic at claim time.

The auth contract from ADR-077 is unchanged — claim still requires an agent-scoped key, the bundle is computed on the server using the caller's read permissions, and an item the caller could not normally read remains unreadable inside the bundle (the parent epic is filtered out the same way the item itself would be).


Supersedes: none. Amends: ADR-077 (extends the warp item schema with kind and requirements; the claim/heartbeat/complete protocol is unchanged for both kinds; the claim response gains an epic + position + sibling-progress bundle per Amendment 2), ADR-097 (adds three new affordances — progress bar, Decompose button, requirements editor — to the /pm dashboard surface that ADR-097 introduces; the requirements editor gains a depends_on multi-select per Amendment 1). Superseded by: {{leave blank until a later ADR reverses this one}}