Skip to content

Agent work as first-class Tickets — granular, with reasoning + per-ticket conversation #150

Closed

Description

@serge-ivo

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)

  1. 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.
  2. Status / lifecycle. Flows through the kanban columns (what tasks already do).
  3. Reasoning. Each ticket carries why — the decision/audit — shown in the ticket, not buried in Data.
  4. 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).

Activity

  1. 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
  2. serge-ivo commented on Aug 6, 2026

    @serge-ivo
    ContributorAuthor

    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

    1. 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.
    2. 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.

  3. serge-ivo commented on Aug 6, 2026

    @serge-ivo
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions