You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation #150
The oversight primitive. Humans run work on a kanban of discrete, addressable tickets — not a flat activity log (an infinite, indistinguishable stream you can't point at or interrogate). Every unit of agent work should be a Ticket on the board.
The Ticket (three properties — flat, no epics for now)
Granular. One ticket per unit of work. Deliberately no parent/epic hierarchy for now (keep it simple — revisit only if a real need appears). Fine-grained is fine; the board just holds many tickets.
Status / lifecycle. Flows through the kanban columns (what tasks already do).
Reasoning. Each ticket carries why — the decision/audit — shown in the ticket, not buried in Data.
Conversational. Open a ticket → a chat thread scoped to that ticket: ask why a decision was made, request changes, get more info. The agent answers with that ticket's reasoning + data as context.
What today's system lacks (verified)
AgentTask = {id,title,description,status,assignedBy,timestamps} — no structured reasoning, no per-ticket conversation. The board (lib/board.ts + BoardTab.tsx) already renders flat task cards, so granularity is free; the two new pieces are reasoning + the per-ticket thread.
Plan (no hierarchy → smaller)
P1 — Reasoning on tickets. Add a reasoning field to the task model; BoardTab shows it in the card/detail. (Board already renders flat tickets, so this is the only structural change.)
P2 — Per-ticket conversation. Open a ticket → a scoped chat thread; agent answers with the ticket's reasoning/data as context.
P3 — Agents emit tickets. e.g. the lead-finder creates one ticket per business (carrying its 5-step audit as reasoning). The board fills with real, interrogable tickets.
Supersedes the flat "Activity" tab (PR 5af86ed) as the oversight surface. First-party platform primitive (tasks live in the AgentDO + lib/board.ts).
changed the title [-]Agent work as first-class Tickets — granular + epics + reasoning + per-ticket conversation[/-][+]Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation[/+]on Aug 3, 2026
P1 and P3 were already shipped; P2 landed in 66d06f6. Leaving this open for the two follow-ups below.
Status against the plan in the description
P1 — Reasoning on tickets. ✅ Already done before this pass. reasoning is carried on the runtime task payload, surfaced by lib/board.ts (BoardItemView.reasoning), rendered on the Kanban card and the List row (BoardTab.tsx) and as "Why — what the agent did & decided" on the ticket detail page (RunDetail.tsx).
P3 — Agents emit tickets. ✅ Already done. POST /v1/instances/:id/tasks/direct creates a board ticket with no runner, and the registry tool create_ticket lets a pipeline raise its own. Actionable tickets (lib/actionable-ticket.ts) took it further than the plan asked: a ticket can carry the work it stands for and approving runs exactly that.
P2 — Per-ticket conversation. ✅ Shipped in 66d06f6.
What P2 does
Open a ticket → GET/POST /v1/instances/:id/tasks/:taskId/thread. You ask; the agent answers from that ticket's record only — its reasoning, its declared action, its activity — so the same question on two tickets gets two answers, and a follow-up no longer has to go to the Assistant chat where it is detached from the work it is about.
Two rules carry the weight, both in lib/ticket-chat.ts:
Grounded. The thread has no tools; the run is over and nothing can be re-inspected. An unrecorded detail comes back as "that isn't recorded", never as a plausible reconstruction. A confabulated answer here is worse than no thread at all, because it is read as the audit trail.
No action. A thread explains, it never does. The declared action is fixed at creation and approval remains the only thing that runs it — so the approval gate does not gain a free-text bypass.
No new storage, and deliberately so.instance_runtime_task_events (migration 0012) is already a per-task, append-only, tenant-scoped log, so a turn is just another event on the ticket (ticket.question / ticket.answer). No migration; the thread is deleted with its ticket; every existing reader of the task-event stream already carries it. This repo has four records of "what happened" (agent_events, board_items, coding_timeline, delegation runs) and adding a fifth for a conversation would have been the wrong trade.
The question is persisted before the model call: losing what the owner typed to a missing-key 402 is the difference between "add a key and retry" and "retype it and hope".
What remains
MCP surface for the thread. A supervisor agent can already read a subordinate's board (instance_board, subordinate_status) but cannot ask a ticket anything — the thread is console-only. ask_ticket / ticket_thread MCP tools would make agent-to-agent review symmetric with human review. Small, and the natural next step.
The thread is reachable only from the run-detail page. The board card has no affordance saying a ticket can be questioned (or that it already has unread answers). A turn count on the card + a direct "Ask" action would make the feature discoverable; today you have to open a ticket to find out it exists.
Not part of #150, but adjacent and worth its own issue
The gap flagged when closing #276 — a delegated run merging its own PRs to main unattended, invisible in the supervisor view — is not fixed by any of P1–P3. A delegation ticket records the objective and the terminal outcome, not the consequential acts taken along the way. That is a question of what a run writes down, not of what a ticket can show; the ticket surface is now ready to display it the moment something records it.
Both remaining follow-ups shipped in 8ecb10a. Closing — P1, P2 and P3 are all done.
1. MCP surface for the thread — ticket_thread + ask_ticket
A supervisor agent could read a subordinate's board but not question anything on it, so
agent review was strictly weaker than human review while the delegation model assumes a
supervisor can interrogate what a subordinate did. It can now:
instance_board -> ticket_thread (what was already asked) -> ask_ticket
Both are thin proxies onto the SAME routes the console calls, which is how P2's two rules
carry onto this surface instead of being restated in a second place and drifting:
Grounded. The answer is still built server-side from that ticket's record — its
reasoning, its declared action, its activity, including the act.consequential events a
run now records (A delegated run records its outcome but not its consequential acts — merging to main is invisible #294) — by a model call with no tools. Because the caller here is a model, the constraint is also restated in the returned payload, next to the answer it
is about to summarise: that is where "not recorded" gets smoothed into a plausible
account of what happened. The answer text is passed through verbatim (the tool never
softens a refusal), and an API error surfaces as an error rather than an empty thread —
an invented empty thread is itself a claim about the audit trail.
No action. Structural, not promised in prose. Neither tool accepts an argument that
could act — a test pins ask_ticket's schema to exactly {token, instance_id, task_id, question, dry_run} — and there is no path from the thread route to executeTriggerAction. Running a ticket's declared work stays with approve_instance_task / run_instance_task, so the approval gate gains no free-text
bypass.
ask_ticket is write scope, not runtime: it persists a turn and spends the owner's
tokens, but executes nothing. ticket_thread is an ungated read and stays readable under MCP_READ_ONLY — that mode exists to stop writes, not to blind a reviewer.
2. Board-card discoverability
BoardItemView.threadTurns plus an Ask control on both the Kanban card and the List
row, deep-linking to …/tasks/:id?ask=1, which lands the ticket scrolled to its
conversation with the box focused (the thread sits below the activity log and the
screenshot replay, so without that the button looks like it did nothing).
Shown on every openable card, not only ones with turns — a control that appears only after
you have used it cannot be how you find it. The count is for latestTaskId only, the
ticket the card opens: summing every attempt would show "3" and then open a thread with one
turn in it.
Migration 0088, and one thing worth writing down
The count is one grouped read per board build, against instance_runtime_task_events —
the runtime's firehose — on a 2.5s poll, so it needs a partial index on the two
conversation types.
SQLite only uses a partial index when it can prove the query's WHERE implies the index's,
and it does that by matching the predicate at prepare time. The natural type IN (?3, ?4) therefore defeats it silently: EXPLAIN QUERY PLAN gives SCAN for the
bound form and SEARCH … USING INDEX idx_runtime_task_events_thread for the literal one.
The query inlines the two constants (from ticket-chat.ts, never user input), and board.test.ts pins that they stay quote-free, that the predicate matches the migration
character-for-character, and that nobody "fixes" it back to parameters — the query returns
the right answer either way, so nothing else would have caught the regression.
Activity
[-]Agent work as first-class Tickets — granular + epics + reasoning + per-ticket conversation[/-][+]Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation[/+]feat(board): show per-ticket reasoning on kanban cards (#150 P1)
feat(board): runner-less "create ticket" endpoint — POST /instances/:…
feat(mcp): create_instance_ticket — put a reasoned ticket on the boar…
feat(board): a ticket you can question, not just read (#150 P2)
serge-ivo commentedon Aug 6, 2026
P1 and P3 were already shipped; P2 landed in 66d06f6. Leaving this open for the two follow-ups below.
Status against the plan in the description
reasoningis carried on the runtime task payload, surfaced bylib/board.ts(BoardItemView.reasoning), rendered on the Kanban card and the List row (BoardTab.tsx) and as "Why — what the agent did & decided" on the ticket detail page (RunDetail.tsx).POST /v1/instances/:id/tasks/directcreates a board ticket with no runner, and the registry toolcreate_ticketlets a pipeline raise its own. Actionable tickets (lib/actionable-ticket.ts) took it further than the plan asked: a ticket can carry the work it stands for and approving runs exactly that.What P2 does
Open a ticket →
GET/POST /v1/instances/:id/tasks/:taskId/thread. You ask; the agent answers from that ticket's record only — itsreasoning, its declared action, its activity — so the same question on two tickets gets two answers, and a follow-up no longer has to go to the Assistant chat where it is detached from the work it is about.Two rules carry the weight, both in
lib/ticket-chat.ts:No new storage, and deliberately so.
instance_runtime_task_events(migration 0012) is already a per-task, append-only, tenant-scoped log, so a turn is just another event on the ticket (ticket.question/ticket.answer). No migration; the thread is deleted with its ticket; every existing reader of the task-event stream already carries it. This repo has four records of "what happened" (agent_events,board_items,coding_timeline, delegation runs) and adding a fifth for a conversation would have been the wrong trade.The question is persisted before the model call: losing what the owner typed to a missing-key 402 is the difference between "add a key and retry" and "retype it and hope".
What remains
instance_board,subordinate_status) but cannot ask a ticket anything — the thread is console-only.ask_ticket/ticket_threadMCP tools would make agent-to-agent review symmetric with human review. Small, and the natural next step.Not part of #150, but adjacent and worth its own issue
The gap flagged when closing #276 — a delegated run merging its own PRs to
mainunattended, invisible in the supervisor view — is not fixed by any of P1–P3. A delegation ticket records the objective and the terminal outcome, not the consequential acts taken along the way. That is a question of what a run writes down, not of what a ticket can show; the ticket surface is now ready to display it the moment something records it.feat(supervision): a delegated run now records what it DID, not just …
feat(mcp,console): let an agent question a ticket, and let the board …
serge-ivo commentedon Aug 6, 2026
Both remaining follow-ups shipped in
8ecb10a. Closing — P1, P2 and P3 are all done.1. MCP surface for the thread —
ticket_thread+ask_ticketA supervisor agent could read a subordinate's board but not question anything on it, so
agent review was strictly weaker than human review while the delegation model assumes a
supervisor can interrogate what a subordinate did. It can now:
Both are thin proxies onto the SAME routes the console calls, which is how P2's two rules
carry onto this surface instead of being restated in a second place and drifting:
reasoning, its declared action, its activity, including the
act.consequentialevents arun now records (A delegated run records its outcome but not its consequential acts — merging to main is invisible #294) — by a model call with no tools. Because the caller here is a
model, the constraint is also restated in the returned payload, next to the answer it
is about to summarise: that is where "not recorded" gets smoothed into a plausible
account of what happened. The answer text is passed through verbatim (the tool never
softens a refusal), and an API error surfaces as an error rather than an empty thread —
an invented empty thread is itself a claim about the audit trail.
could act — a test pins
ask_ticket's schema to exactly{token, instance_id, task_id, question, dry_run}— and there is no path from the thread route toexecuteTriggerAction. Running a ticket's declared work stays withapprove_instance_task/run_instance_task, so the approval gate gains no free-textbypass.
ask_ticketiswritescope, notruntime: it persists a turn and spends the owner'stokens, but executes nothing.
ticket_threadis an ungated read and stays readable underMCP_READ_ONLY— that mode exists to stop writes, not to blind a reviewer.2. Board-card discoverability
BoardItemView.threadTurnsplus an Ask control on both the Kanban card and the Listrow, deep-linking to
…/tasks/:id?ask=1, which lands the ticket scrolled to itsconversation with the box focused (the thread sits below the activity log and the
screenshot replay, so without that the button looks like it did nothing).
Shown on every openable card, not only ones with turns — a control that appears only after
you have used it cannot be how you find it. The count is for
latestTaskIdonly, theticket the card opens: summing every attempt would show "3" and then open a thread with one
turn in it.
Migration 0088, and one thing worth writing down
The count is one grouped read per board build, against
instance_runtime_task_events—the runtime's firehose — on a 2.5s poll, so it needs a partial index on the two
conversation types.
SQLite only uses a partial index when it can prove the query's
WHEREimplies the index's,and it does that by matching the predicate at prepare time. The natural
type IN (?3, ?4)therefore defeats it silently:EXPLAIN QUERY PLANgivesSCANfor thebound form and
SEARCH … USING INDEX idx_runtime_task_events_threadfor the literal one.The query inlines the two constants (from
ticket-chat.ts, never user input), andboard.test.tspins that they stay quote-free, that the predicate matches the migrationcharacter-for-character, and that nobody "fixes" it back to parameters — the query returns
the right answer either way, so nothing else would have caught the regression.
fix(console): let the board card's action row wrap (#150)
feat(coding): a run that hit its step limit tells its successor what …