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:
Decomposition is explicit and idempotent. The endpoint is
POST /api/v1/items/{epic_id}/decomposeand re-running it spawns onlypendingrequirements. 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 withstatus = 'pending', the handler creates one child item withkind = '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'srequirements[i]tostatus = 'spawned'andticket_id = <new_id>in the same transaction so the epic and the children commit together. Re-decomposing skips every requirement whose status isspawnedordoneand only spawns the ones stillpending. 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 anagentoradminscoped key (per the existing key-scope contract from the warp env block), which is the same scope already required forPOST /itemsso no new key shape is introduced.Progress is computed at read time, not denormalised.
GET /api/v1/items/{epic_id}adds aprogressblock to the response:{ total, done, in_progress, blocked, ratio }wheretotalis the count of children withparent_id = <epic_id>, the per-status counts come from the same query grouped bystatus, andratio = done / total(zero when total is zero). The query is one indexed scan againstparent_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 — achildren_done/children_totalcounter 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.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
dueBandcolour palette from ADR-097 — green for done, yellow for this-week, orange for next-week, red for overdue — but applied toprogress.ratiorather thantarget_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 allblockedis red. The bar replaces theN itemscount 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
pendingrequirement. The button firesPOST /api/v1/items/{epic_id}/decomposewith 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 isspawnedordone. 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 therequirementsarray. Each row carries the slug, the summary (editable inline), the status badge (pending/spawned/done), a drag handle to reorder, a checkbox to mark apendingrequirement asdonewithout spawning a ticket, and a link-out to the spawned ticket whenticket_idis 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 therequirementsarray on save; the slug-as-stable-id property means re-ordering never re-spawns work. Deleting aspawnedrequirement 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 iskind = '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'sN itemscount.
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
/pmdashboard. 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-addfive 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
spawnedrequirement without a confirm dialog naming the ticket id; the rollup never readsrequirementsdirectly 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" andrequirementsis 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
progressblock 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
kindenum 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
kindandrequirementscolumns onwarp_itemsand the migration that adds them with safe defaults so every existing item keeps working. - The
POST /items/{id}/decomposeendpoint 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
/pmdashboard's epic-card progress bar, Decompose button, and requirements editor — three additive surfaces on the existing dashboard, gated onkind = '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
kindorrequirements. 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. Namingepicstructurally 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
kindenum is the place to addstoryif 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 everypendingrequirement 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 optionaldepends_onfield to each requirement and gates the decomposer on it. The non-negotiable rule from §Decision rule 1 — "Decomposition is explicit and idempotent. The endpoint isPOST /api/v1/items/{epic_id}/decomposeand re-running it spawns onlypendingrequirements" — survives verbatim; this amendment narrows the "pending" set further by also requiring every dependency to bedone.
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_onentries, 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_claimreturns 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-upwarp_getagainst 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.jsonin 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}}