ADR-0034: Sessions bind to one issue; the issue owns the agent's todos and drives closure
- Status: Accepted
- Date: 2026-06-05
- Deciders: @karasu
- Supersedes: —
- Superseded by: —
- Amends: ADR-0020 (schema, lifecycle, un-forecloses links), ADR-0022 (new root-only
kaged.todotool; todo bubble-up as a sibling pattern)
(Next free ADR number — bump if 0034 is already taken in the index.)
Context
oh-my-pi, opencode, and the Claude-Code family all give the agent a todo tool: a structured checklist the model maintains to keep a long-horizon task on track. kaged needs the same capability. The agent loop is ours (ADR-0012), so we own where the checklist lives.
The reference implementation we looked at (oh-my-pi todo.ts) is richer than a flat three-state list. Its load-bearing properties:
- Op-based, not full-list-replace. Each call is an ordered batch of operations (
init/start/done/rm/drop/append/note), not a re-statement of the whole list. - Content-addressed. Tasks are referenced by their text, not by an ID the model has to track.
- Single-
in_progressinvariant. At most one task is active; the tool auto-promotes the first pending if none is. - Append-only notes per task.
abandoned, not deleted. Dropping a task keeps it for the record.- Markdown round-trip. The list serialises to and parses from a
- [ ] / [x] / [/] / [-]checklist. - Storage is
session | memory.
That last property is precisely the limitation worth fixing. In oh-my-pi a todo list dies with the session. In kaged the natural durable home already exists: the issue (ADR-0020). An issue is the unit of operator-meaningful work, it already has an append-only update stream, and it already stores markdown. Binding the checklist to the issue rather than the session gives us resumability for free — a new session on the same issue picks up the existing list — and turns "the issue" into the goal, the guide, and the storage mechanism all at once.
Several pieces this needs already exist in the codebase and don't have to be invented:
AssignmentTargetalready acceptssession:..., so an issue can already point at a session.- The issue status enum already includes
in_progress. kaged.checkpointis already the operator-yield-and-approve primitive ("pause execution and yield control to the operator… when you need human review, approval, or input").kaged.ask/kaged.formalready provide structured operator interaction with checkpoint-like pause/resume.- The
issue_updatestable is already an append-only audit ledger. - The
kaged.issuetool was just unified from five split tools into one action-based tool, matching thedap.*/lsp.*pattern. The todo tool follows the same single-tool shape.
So this ADR is mostly composition and one new sub-resource, not a new subsystem.
There is one principle this design protects above convenience: the operator owns the definition of done. The agent's working plan is volatile and agent-owned; the acceptance criteria for an issue are stable and operator-owned. Collapsing those two into one list would let the agent decide when work is finished, which is exactly the operator-agency promise the project is built to keep. They share one structure but not one owner.
A second principle: the agent does not need to know how issues work to keep a checklist. The kaged.todo surface operates implicitly on the session's bound issue. The agent never passes an issue number to it and never learns the checklist is issue-backed. Issue mechanics live behind the separate kaged.issue tool, which the root agent uses deliberately when it actually means to touch the tracker.
Decision
A session binds to at most one issue. The agent's working checklist is stored as issue-owned todos, mutated through a new root-only
kaged.todotool that operates implicitly on the bound issue. Todos come in two kinds — operator-ownedcriterionand agent-ownedstep— and issue closure is driven off operator-confirmed criteria via the existingkaged.checkpointprimitive. Issues gain a small typed-link vocabulary and a session-scoped parent/child relationship. Subagents contribute todos by bubble-up and never touch storage directly.
Session ↔ issue binding
- A session has at most one bound issue (
session.bound_issue, nullable). An issue may be worked by many sessions over its lifetime, one active at a time. - Binding is a session-side pointer, not a state transition on the issue or the todos. Binding and unbinding from the UI/sidebar attaches or detaches the pointer; it does not mutate, clear, or complete stored todos. Unbinding simply puts the existing (possibly incomplete) todos out of reach of the current session. Re-binding the same issue rehydrates them.
kaged.todois only available when a session has a bound issue. With no binding, the tool resolves to an error that tells the agent to ask the operator to bind an issue. This is deliberate: the issue is the storage, so no issue means no todos.- The "send to agent" action on the issue screen is the create-and-bind path: it opens a new session titled from the issue, binds it to that issue, and seeds the first message with whatever the operator typed plus a hint to fetch issue
#N. From there the operator steers — discovery (the agent gathers information and records it viakaged.issue comment) or implementation (the agent works a todo list) — by prompting. There is no discovery/implementation mode state; it is behavioural. - A "promote to issue" action exists for a session that started without one and discovers mid-flight that it needs a durable plan: it files an issue and binds the session to it in one step.
kaged.todo tool
A single root-only tool with an action parameter, consistent with the unified kaged.issue shape:
const kagedTodoSchema = z.object({
action: z.enum([
"view", // return the current list (also returned implicitly after every mutation)
"set", // replace the working list (init); accepts phased input
"add", // append task(s) to a phase
"start", // mark a task in_progress (enforces single in_progress)
"done", // mark a task completed
"drop", // mark a task abandoned (not deleted)
"note", // append a note to a task
]),
content: z.string().optional(), // target task, addressed by text (start/done/drop/note)
items: z.array(z.string()).optional(),// task contents (set/add)
phase: z.string().optional(), // optional grouping (set/add)
kind: z.enum(["step", "criterion"]).optional(), // default "step"
text: z.string().optional(), // note body
});
Surface notes:
- No
issue/numberparameter. The target is always the session's bound issue, resolved server-side. The agent never names it. - Content-addressed, like oh-my-pi: tasks are referenced by their text. Storage assigns stable IDs (below); the agent never sees them.
- Single-
in_progressinvariant is enforced server-side on every mutation, including auto-promotion of the first pending task. dropisabandoned, never a hard delete — consistent with the rollback-supersedes-not-deletes posture from the checkpoint design.- The result of every action returns the full current list (markdown-renderable), so the model always sees the post-mutation state.
principal_scope: "root-only", enforced at parse time by the same mechanism askaged.issueandkaged.workflow.
Todo storage (issue-owned)
A new daemon table, sketched here; the full shape lives in the spec.
// issue_todos
{
id: ULID,
issue_id: ULID, // FK -> issues
content: string,
status: "pending" | "in_progress" | "completed" | "abandoned",
kind: "step" | "criterion", // default "step"
origin_agent: string, // caller tree-path: "primary", "primary.subagents.backoffice", ...
phase: string | null, // optional work-stage grouping (oh-my-pi style)
position: number, // ordering within (issue, phase)
notes: string[], // append-only, volatile scratch
created_at, updated_at, completed_at
}
- Volatile scratch vs durable audit. Per-task
notesand routine status churn stay on the todo row and do not spam theissue_updatesstream. Only significant lifecycle events emit anissue_updatesrow ofkind=system_note: a criterion completed, a subagent sublist approved, the issue closed against its criteria. This keeps the operator-visible ledger meaningful and addresses the obvious churn risk of a live checklist living inside an issue. - Markdown round-trip. The list serialises to a checklist the operator can read and hand-edit in the issue UI, and parses back. Operator edits and agent ops mutate the same representation — nothing hidden, the artifact is shared.
Two kinds of todo, and closure
step— the agent's working plan. The agent freelyset/add/start/done/drop/notes steps. This is the oh-my-pi checklist.criterion— acceptance criteria; the definition of done. Authored by the operator (UI) or proposed by the agent during discovery. A criterion can bedone-claimed by the agent but onlyrm/drop-ed by the operator. A criterion'sdoneis a claim, not an autonomous close.- Closure flow, built from existing primitives. When the agent has marked the last open
criteriondone, it callskaged.checkpointwith a reason like "acceptance criteria met, requesting sign-off." The session pauses. The operator reviews (and at sign-off implicitly ratifies the criteria set), then resumes; the resume path transitions the issue toresolved(the agent does this viakaged.issue transition, or the operator does it from the UI) and the session may conclude.kaged.ask/kaged.formcarry any structured sign-off detail. No new closure primitive is introduced. - This resolves the standing open question in
session-manager.md("should manual session termination auto-resolve an assigned issue, or should the operator confirm?"): the operator confirms, at the closing checkpoint. Ending a session never silently resolves its issue.
Issue links and session-scoped hierarchy
A new table for typed relationships between issues:
// issue_links
{
id: ULID,
project_id: string,
from_issue: ULID,
to_issue: ULID,
kind: "child_of" | "duplicate_of" | "blocked_by" | "relates_to",
created_by: string,
created_at
}
- The vocabulary is deliberately small (consistent with ADR-0020's "status + assignment + comments is the whole vocabulary" restraint).
child_of,duplicate_of, andblocked_byare directional;relates_tois symmetric. Inverse views (blocks,parent_of) are computed, not stored. - Links are navigational metadata, not a workflow engine. A
blocked_bylink does not gate transitions; achild_oflink does not roll up status. Links inform the operator and power UI affordances (close-as-duplicate setsduplicate_ofand transitions the source torejected/resolvedwith a note). We are not becoming a project-management tool — see What this forecloses. - Auto-parenting. An issue created within a session that has a bound issue — whether by the agent via
kaged.issue createor by the operator from that session's surface — is automatically given achild_oflink to the bound issue. This captures "while working #42 I found we also need #57" as provenance. Issues filed from the global issue list, outside a bound session, are not auto-parented.
Subagent todos (bubble-up)
This is the issue bubble-up pattern of ADR-0022 rule 10 applied to todos, and it preserves that rule rather than weakening it.
- Subagents have neither
kaged.todonorkaged.issue. They remain storage-blind and domain-blind. - A subagent expresses a proposed checklist as structured content in its delegation return. The root agent reviews it: accept as-is, modify the approach, reject, or escalate to the operator (escalation is a
kaged.checkpoint/kaged.ask). Only what the root accepts is persisted, recorded withorigin_agentset to the subagent's tree-path so the issue shows it as that subagent's sublist. - The subagent only ever sees the slice of the issue the root forwards in its delegation message — never the issue itself.
- "The root is the smartest agent" is a policy, not a law. Operators routinely put the strong model on a specialised subagent and a cheap one at the root. Whether the root reviews or auto-accepts a subagent's proposal is a per-agent policy knob (
autovsreview), not a hard-wired gate. Default isreview. - The proposal travels in the delegation return rather than via a new subagent-facing tool, specifically to keep subagents free of any
kaged.*surface. A structuredkaged.todo proposetool for subagents is the fallback if natural-language returns prove too lossy in practice — see Open questions.
Consequences
What this commits us to
- New
issue_todosandissue_linkstables (SQLite per ADR-0005), with a storage migration and CRUD. - A
bound_issuecolumn on the session record, with bind/unbind endpoints and UI/sidebar controls. - A new root-only
kaged.todotool (action-based) in the built-in registry, with the single-in_progressinvariant and markdown round-trip enforced server-side. - A "send to agent" action on the issue detail screen (the create-bind-seed flow) and a "promote to issue" action on the session surface.
- Auto-parenting behaviour on issue creation inside a bound session.
issue_updatesrows for significant todo lifecycle events only (criterion completed, sublist approved, closure).- A per-agent subagent-todo review policy (
auto|review, defaultreview). - Spec work, not in this ADR: amendments to
issues.md(todos + links schema, the additive-only note still holds forkaged.issue— todos are a separate resource),session-manager.md(binding, the auto-conclude answer), and akaged.tododefinition inagent-tooling.md. A dedicatedspecs/todos.mdif the todo lifecycle outgrows the issues spec.
What this forecloses
- Todos with no issue. There is no session-scoped or ephemeral todo list. If you want a checklist, you bind an issue. The friction is intentional and mitigated by one-click "send to agent" / "promote to issue".
- Agent-owned definition of done. The agent can author and complete
steps and claimcriterions, but it cannot close an issue against criteria without the operator's checkpoint. - Subagents touching storage. No
kaged.todoorkaged.issuefor non-root agents; ADR-0022 rule 10 stands. - A dependency / project-management engine. Links are metadata. No automatic blocking, no status roll-up from children, no epics, milestones, story points, or roadmaps. This is the same line ADR-0020 drew; we are widening it by exactly one table and stopping there.
What becomes easier
- Resumable work: close a session, open a new one on the same issue next week, the plan is intact.
- Operator oversight: the working plan is visible and hand-editable as markdown inside the issue, and the audit ledger records the decisions, not the keystrokes.
- Multi-agent work with provenance: subagent contributions appear as attributed sublists, reviewed by the root, on a single-rooted audit trail.
- Triage hygiene: close-as-duplicate, blocked-by, and decomposition-into-children are now first-class instead of folklore in comment text.
What becomes harder
- The issue surface grows a todos panel and a links panel — real UI, plus mobile parity (release gate per ADR-0002).
- The session lifecycle gains a binding dimension; the state machine in
session-manager.mdneeds the bind/unbind transitions and the "todos out of reach when unbound" semantics documented. - Two kinds of todo with different mutation rules is a subtlety the model has to respect; the
kaged.todotool description and prompt guidance carry weight here.
Alternatives considered
A — Session-scoped todos (the oh-my-pi default)
Why tempting: Zero new schema; the list is just session state.
Why rejected: Todos die with the session — the exact limitation we set out to fix. No resumability, no operator-editable durable plan, no place to hang acceptance criteria. Binding to the issue costs one table and buys all three.
B — One list, one owner (collapse criteria into todos)
Why tempting: Simpler. "All todos done → close."
Why rejected: It hands the agent the definition of done and makes the operator's contract churn on every plan reshuffle. The step / criterion split is the smallest thing that keeps the operator owning closure.
C — Todos as a sub-action of kaged.issue
Why tempting: One fewer tool; we just merged kaged.issue, why add a sibling.
Why rejected: kaged.issue is additive-only for agents by design (no update/delete) — it is the deliberate, operator-meaningful tracker surface. Todos need free mutation (start/done/drop). Folding them in would either break the additive-only invariant or contort the schema. They are different resources with different mutation rules; they get different tools. The agent also shouldn't have to know it's writing to an issue to keep a checklist.
D — Give subagents their own kaged.todo
Why tempting: Subagents could manage their own checklists directly.
Why rejected: It breaks ADR-0022 rule 10 (subagents are storage-blind and domain-blind) and forks the audit trail. Bubble-up keeps a single root, lets the strongest reviewer shape the plan, and costs only an attribution field.
E — A bespoke approval mechanism for closure
Why tempting: A purpose-built "criteria met → close" gate reads cleanly.
Why rejected: kaged.checkpoint already is the operator-approval primitive, and kaged.ask/kaged.form already carry structured sign-off. A bespoke gate is a duplicate. When the first-class approval-gate primitive lands (currently on the roadmap for the workflow DSL), the closing checkpoint and the subagent-review gate are the natural things to migrate onto it — but neither blocks on it today.
Open questions
- Phases in v1, or defer?
phaseis lifted from oh-my-pi and is cheap (a nullable string), but it is a third grouping axis alongsidekindandorigin_agent. Lean ship it as optional — a flat list works if unused — but it could be deferred to keep the first cut minimal. - Agent-proposed criteria. Should the agent be able to create
criteriontodos directly during discovery, or onlysteps that the operator promotes to criteria? Current lean: the agent may propose criteria, the operator ratifies the set at the closing checkpoint, and only the operator candropone. A lighter-weight per-criterion approval might be warranted; revisit if sign-off feels too coarse. - Rejected-proposal audit. When the root rejects a subagent's proposed sublist, do we record the rejection? v1 leans no (the proposal lives only in the delegation return, in the run transcript). Add an
issue_updatesnote if operators want the paper trail. - Subagent proposal transport. Structured content in the delegation return (recommended) vs a
kaged.todo proposetool for subagents (keeps subagentskaged.*-free only if it's a return-shaping tool, not a storage tool). Decide after seeing how lossy free-form returns are in the affiliate worked example. - Bound-issue assignment coupling. When a session binds an issue, should the issue's
assignmentauto-set tosession:<id>? Lean yes — it keeps the existing assignment field honest and the issue's "who's on this" view correct — but it means unbinding has to decide whether to clear the assignment. - Closing a parent with open children. Does resolving a parent prompt on, block on, or ignore open
child_ofissues? Lean prompt, don't block — links are navigational, not gating — but the UI should surface the open children at closure time.
References
- ADR-0002 — UI/mobile parity release gate (the new panels inherit it)
- ADR-0005 — storage for the new tables
- ADR-0011 — issues, todos, and links are operator-state, not portable
- ADR-0012 — kaged owns the agent loop; checkpoints are kaged-native
- ADR-0019 —
confirm_required; the approval-gate primitive's eventual home - ADR-0020 — the issue model this extends; the no-dependencies foreclosure this amends
- ADR-0022 — recursive agents, root-only tool scope, issue bubble-up (rule 10)
docs/specs/issues.md— issue schema + bubble-up; gains todos and linksdocs/specs/session-manager.md— checkpoint lifecycle; gains binding + the auto-conclude answerdocs/specs/agent-tooling.md— unifiedkaged.issue; gainskaged.todo- oh-my-pi
todo.ts— reference todo tool (op-based, single-in_progress, markdown round-trip): https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/tools/todo.ts - RFC-0003 / ADR-0023 — agent memory; distinct from issue-todos (private recall vs operator-visible ledger), called out to prevent conflation
Amendments to existing documents (recorded here; applied in those documents)
- ADR-0020 /
issues.md: addissue_todosandissue_linkstables; lifecycle gains criteria-met semantics and the operator-confirmed closure path; the "Open in new session" action becomes "send to agent" (create-bind-seed); the no issue dependencies foreclosure is narrowed to "no dependency engine" — typed navigational links are now permitted. The additive-only rule forkaged.issueis unchanged; todos are a separate resource. - ADR-0022 /
agent-tooling.md/project-dsl.md: add the root-onlykaged.todotool; document todo bubble-up as a sibling of issue bubble-up under rule 10; add the per-agent subagent-todo review policy. session-manager.md: addbound_issue, bind/unbind transitions, "todos out of reach when unbound" semantics, and the answer to the auto-conclude open question.agent-tooling.md(2026-06-05): live todo surfacing amendment, sticky reminder, echo notes, content-addressing coaching. See the spec's amendments section for details.