20 KiB
status, created, owner
| status | created | owner |
|---|---|---|
| draft | 2026-05-05 | cfl |
Task model unification
Why
Kandev today carries two execution models on top of one task table.
Kanban tasks flow through a configurable workflow with per-step
prompts and a single agent runner. Office tasks live indefinitely
with comment-driven wakeups, multi-agent stages, and IDLE-between-turns
sessions. The two share tasks, task_sessions, comments, and the
executor record, which forces every shared code path to branch on
IsOfficeTask(). Each branch is a regression risk.
Looking at office and kanban side by side, they're not actually two models — they're two configurations of the same model. Both are event-driven state machines over a step graph; both produce agent runs in response to events; both record outcomes. The differences are which events count as triggers (kanban: step transitions; office: also comments, blockers, children, approvals, heartbeats) and how many agents participate per step (kanban: one runner; office: a primary plus reviewers / approvers).
The unification: extend the workflow engine to subsume office's event-driven model. Office becomes a workflow style — a set of step shapes (work / review / approval) plus event triggers and multi-agent participants — not a separate execution model.
We have no production office users. No data migration; this is greenfield from office's perspective. The constraints that matter: the existing kanban experience must keep working unchanged, and the kanban homepage's per-workflow swimlane UX is unchanged.
What
Tasks always have a workflow
Every task has a workflow_id and a workflow_step_id. The workflow
defines the lifecycle: linear steps for kanban, event-driven steps with
multi-agent participation for office.
A workflow has a UX hint — style — with values kanban, office,
custom. This drives default presentation in the UI; the engine
doesn't branch on it.
Built-in workflow templates ship with kandev:
- Kanban Default — Backlog → In Progress → In Review → Done. Single
primary agent per step.
on_enterfires the agent.on_turn_completemay transition. No event triggers. - Office Default — Backlog → Work → Review → Approval → Done.
Work(stage_type='work'): a primary agent (the assignee). Triggers:on_enter,on_comment,on_blocker_resolved,on_children_completed,on_heartbeat,on_agent_error. Each queues a run for the primary. Transitions: agent callsmove_task_kandev(or workflow rule fires) to push toReview.Review(stage_type='review'): no primary; multi-agent participants (reviewers) withdecision_required=1each. Triggers:on_enterdoesclear_decisions(so quorum starts fresh on re-entry) thenqueue_run_for_each_participant{ role: reviewer }. Transitions:wait_for_quorum{ all_approve }→Approval;wait_for_quorum{ any_reject }→ back toWork.Approval(stage_type='approval'): same shape asReviewwithrole: approver. Quorum on all_approve →Done; any_reject →Work.Done(stage_type='custom'): terminal.
Note: there is no Ship step. Shipping work (opening a PR, deploying)
happens inside Work directly — the assignee opens the PR as part of
its turn — or in a delegated child task with the Kanban Default
workflow.
Users can copy either template and edit it; existing custom kanban workflows continue to work unchanged.
New trigger types
The workflow engine learns seven new trigger types in addition to
today's four (on_enter, on_turn_start, on_turn_complete,
on_exit):
on_comment— a user (or external) comment landed on the task while it's in this step. Trigger context carries the comment id, body, and author.on_blocker_resolved— all blockers on the task cleared.on_children_completed— all child tasks of this task reached terminal state. Payload includes the child summaries (identifiers, last comments, PR links if any).on_approval_resolved— an approval request the task owns resolved.on_heartbeat— periodic timer (cron-driven) for background check-ins when nothing else is happening. Fires per-(task, step) pair where the step has this trigger configured. Default cadence 60s. Most steps don't need this: comments, blocker clears, child completions, approvals all fire their own dedicated triggers immediately, so heartbeats are reserved for "wake me even with no external event" patterns — primarily on coordination tasks for senior-agent oversight.on_budget_alert— budget threshold crossed. Fires per task with payload{budget_pct, scope}.on_agent_error— fired only after the runs queue's retry policy exhausts (4 attempts at [2m, 10m, 30m, 2h]). Fires on the failing task itself.
Each new trigger fires through Engine.HandleTrigger plumbing. Steps
that don't configure these triggers behave identically to today.
Multi-agent steps
A workflow step has a participants list (zero or more) in addition to
its primary_agent_profile_id. Each participant has:
role—reviewer,approver,watcher,collaborator.agent_profile_id— the actual agent.decision_required— boolean; if true, the agent's run must record a verdict before quorum can be evaluated.
A step with an empty participants list is a single-agent step = today's kanban behaviour, unchanged.
A step with participants supports new actions:
queue_run(generalises today'sauto_start) — targetsprimary,participant_role:<role>, or a specificagent_profile_id. Optionaltask_idresolver: defaults tothis(trigger's task) but can take a literal task id for cross-task wakeups.queue_run_for_each_participant— fans out runs aton_enter(or any other trigger) for all participants matching a role.wait_for_quorum— a transition guard that blocks until N-of-M decisions are recorded. Used asif:clauses on transitions:if:'wait_for_quorum{role:reviewer, threshold:all_approve}'thengoto:'approval'.clear_decisions— clearsworkflow_step_decisionsrows for the trigger's(task, step)pair. Used byReview.on_enterso quorum starts fresh when the task re-enters Review after a rejection.record_participant_decision— callback used by the office service when a reviewer's run completes with a verdict. Records intoworkflow_step_decisions.
Agent-driven transitions: move_task_kandev
Agents transition tasks by calling the existing move_task_kandev MCP
tool. The repo updates tasks.workflow_step_id; the engine fires
on_exit on the old step and on_enter on the new step automatically.
This is the same path as engine-driven transitions (where a transition
rule on on_turn_complete fires). Agent-driven and rule-driven
transitions both go through the same plumbing.
A reviewer rejecting a Review:
- Reviewer's run completes with verdict
rejected. record_participant_decisionwrites intoworkflow_step_decisions.wait_for_quorum{any_reject}evaluates true; transition toWorkfires.Review.on_exit,Work.on_enter. The work step'son_enterqueues a run for the assignee with the rejection feedback in the trigger payload.- Assignee fixes the issues, calls
move_task_kandevto push back to Review. Work.on_exit,Review.on_enter. The review step'son_enterfirst callsclear_decisionsto reset quorum, thenqueue_run_for_each_participant{role:reviewer}re-queues runs for all reviewers.
One generic runs queue
office_runs is renamed runs. Universal queue for engine-emitted
launches. Every queue_run action creates a row. Coalescing (5s
window for same agent + reason), idempotency (24h dedup window),
per-agent serialisation (one claimed run per agent), cooldown
(per-agent, default 10s), and atomic task checkout (one agent per
task at a time) all apply — same machinery as today's office_runs.
User-initiated launches (clicking Start on a kanban task) bypass the
queue and call runtime.Launch directly — those don't need
coalescing and shouldn't pay the scheduler's tick latency.
The queue has one backend-wide consumer across all workspaces; there
is no scheduler per Office workspace. Assignment alone does not make
an ordinary Kanban task autonomous. Office assignment recovery uses
the authoritative Office-task identity, while a workflow of any style
can opt into queued automation explicitly with queue_run. Shared
scheduler lifecycle, Office maintenance activation, and shutdown
ordering are specified in queued run scheduling
and ADR-2026-08-01-global-run-scheduler-ownership.
run_events (the audit log per run) keeps its current shape and
event-streaming behaviour. WS event subjects (office.run.queued,
office.run.processed, office.run.event_appended.<run_id>) keep
the office. prefix for now to avoid frontend churn; we can rename
later if desired.
Cron-driven trigger handlers
Three handlers run on a shared cron tick loop:
- Heartbeat handler. Iterates
(task, step)pairs where the step hason_heartbeatconfigured AND the agent's runtime allows (cooldown, status). FiresEngine.HandleTrigger(TriggerOnHeartbeat, task). The step's action emits a run. - Budget handler. Detects budget threshold crossings (workspace,
project, agent). For each affected task, fires
Engine.HandleTrigger(TriggerOnBudgetAlert, task). - Routine handler. Reads
office_routines/office_routine_triggers/office_routine_runsexactly as today. On firing it creates a real task with the routine's template; the new task'son_enterdoes the rest. (Routines are upstream of the engine; today's routines spec needs no behavioural change.)
There is no "workspace-scoped scheduler" for waking agents without a task. Every wakeup is task-scoped.
Coordination tasks for senior-agent oversight
The CEO (and other senior agents) need to do workspace-level reasoning: survey active tasks, decide priorities, intervene on failures, allocate budget. Today this happened via "wake the CEO with no task." In the unified model we model it as a real task: a coordination task.
A coordination task is a real, standing task created at office
workspace setup, assigned to a senior agent, on a workflow whose step
has on_heartbeat (and optionally on_budget_alert,
on_agent_error) configured. Cron fires the heartbeat trigger; the
senior agent wakes on the coordination task with workspace-summary
context (active runs, pending wakeups, recent failures, budget) and
can comment on it, delegate child tasks from it, transition it.
Multiple senior roles → multiple coordination tasks (CEO has "Workspace coordination," QA Lead has "QA coordination," etc.). Office onboarding creates them when the workspace is set up. A "Done" transition isn't expected on a coordination task; it's a standing thread.
on_agent_error — fire on the failing task
on_agent_error fires on the failing task itself, after the runs
queue exhausts retries (4 attempts with backoff). The CEO (or chosen
target) wakes up on that task, in that task's session, with full
history visible. Default actions for office workflows:
[
{ "kind": "pause_agent", "agent_id": "{failed_agent_id}" },
{ "kind": "queue_run",
"target": "agent_profile_id:{workspace.ceo_agent}",
"task_id": "this",
"reason": "agent_error",
"payload": { "failed_agent_id": "...", "error_message": "..." } },
{ "kind": "create_inbox_item", "kind": "agent_error" }
]
Fallbacks: no CEO in the workspace → queue_run no-ops; the inbox
item is the only signal. The failing agent IS the CEO → same
fallback.
The default fires on the failing task because that gives the CEO
maximum context. For workflows that prefer a coordination-task view,
override the action's task_id: "{coordination_task.id}".
Two routes, shared body. Kanban board unchanged.
/t/:id (kanban shell, advanced default) and /office/tasks/:id
(office shell, simple default) both render any task. The body —
<TaskHeader>, <TaskMetaRail>, <TaskBody> — is shared. The meta
rail branches on workflow.style for default chrome; the body's mode
(simple / advanced) is per-route default with a URL override toggle.
/tasks/:id remains a compatibility alias that redirects to /t/:id.
The dockview (file tree, terminal, agent chat, changes) binds to
task_sessions.current_execution_id. When the agent is between turns
the dockview renders dormant (worktree readable, no live agentctl).
The kanban homepage's board UX is unchanged. Tasks are grouped by
workflow step within their workflow. Each workflow appears as its own
swimlane; an office workflow shows up as a new swimlane with its own
columns (Backlog | Work | Review | Approval | Done). Office tasks are
draggable across the columns the same way kanban tasks are draggable
across their workflow's columns; the drag fires the same task.moved
event the engine consumes.
Cross-strategy delegation
A new workflow action create_child_task queues a new task as a child
of the current one with a chosen workflow. The Office Default's
Work step uses it for delegation: the agent emits a delegate
signal (a tool call defined in the office MCP toolset), the engine
creates a child task with the Kanban Default workflow, the worker
runs it, the existing on_children_completed trigger fires on the
parent and posts a bridged comment ("KAN-43 done → PR #198").
Reverse path (kanban task promoted to office) is a workflow swap:
change the task's workflow_id and workflow_step_id to match an
office workflow's starting step. Identity, comments, sessions are
preserved.
Scenarios
-
GIVEN a fresh workspace with the Kanban Default workflow, WHEN the user creates a task and clicks Start, THEN the agent runs through the linear steps as today. No regression. No new triggers fire because the kanban template doesn't configure them.
-
GIVEN Office mode is enabled but every workspace contains only ordinary Kanban workflows, WHEN scheduler maintenance runs, THEN no Kanban assignment is recovered as autonomous Office work. Explicit user Start and configured Kanban workflow actions retain their existing behavior.
-
GIVEN a workspace that adopts the Office Default workflow, WHEN the user creates a task assigned to the CEO, THEN the task lands in the
Workstep. The CEO runson_enter. When the user posts a comment,on_commentfires and the CEO runs again through the unifiedrunsqueue. -
GIVEN a task in the Office Default's
Reviewstep with two reviewers configured, WHEN the step is entered, THENon_enterfirst callsclear_decisions, then queues a run for each reviewer in parallel. As each reviewer's run completes with a verdict,record_participant_decisionwrites toworkflow_step_decisions. When alldecision_requiredreviewers have approved,wait_for_quorum:all_approveclears and the transition toApprovalfires. -
GIVEN the Office Default's
Reviewstep, WHEN any reviewer rejects with feedback, THENwait_for_quorum:any_rejectfires, the task moves back toWork, and the primary agent receives the feedback as the next trigger context. The assignee fixes, callsmove_task_kandevto push back to Review; Review'son_enterclears decisions and re-runs all reviewers. -
GIVEN the workspace has a CEO with a coordination task, WHEN the heartbeat handler ticks every 60s, THEN the coordination task's
on_heartbeattrigger fires, queues a run for the CEO with workspace-summary context (active runs, pending wakeups, recent failures, budget), and the CEO wakes up on the coordination task. The CEO can comment, delegate, or take no action. -
GIVEN a single-task workspace where agent A is running on task T, WHEN Anthropic returns 429 (rate limit), THEN:
- The runs queue retries with backoff (2m, 10m, 30m, 2h).
- Most cases: a retry succeeds; the agent resumes via ACP
session/load; the user sees a brief delay; no engine involvement. - If all 4 retries exhaust, the engine fires
on_agent_erroron T at its current step. Default actions: pause A, queue a run for the CEO on T, create an inbox item. - The CEO wakes on T with full session history visible and decides next steps (reassign, comment, ask user, mark blocked).
- If no CEO exists, only the inbox item surfaces; the user picks it up.
-
GIVEN a CEO agent in the Office Default
Workstep decides to delegate, WHEN it emits acreate_child_tasksignal, THEN the engine creates a child task withparent_id = T_office.idand the Kanban Default workflow. The worker runs it, opens a PR, completes.on_children_completedfires on the parent and posts a bridged comment summarising the result. -
GIVEN a workspace with both a kanban workflow and the Office Default workflow in use, WHEN the user opens the kanban homepage, THEN they see two swimlanes — one per workflow — each with its own columns. Office tasks appear in the office workflow's columns; kanban tasks appear in the kanban workflow's columns. Same per-workflow swimlane UX as today.
-
GIVEN a routine "Daily Dep Update" with cron
0 9 * * *and assignee Frontend Worker, WHEN the clock reaches 09:00 UTC, THEN a real task is created with the routine's template, the task's first step'son_enterqueues a run for Frontend Worker. -
GIVEN the user opens any task's advanced mode, WHEN the task's primary agent has an active execution, THEN the dockview binds to
task_sessions.current_execution_idand shows live state. When the execution ends, the dockview renders dormant. -
GIVEN advanced mode is showing an unpinned
IDLEsession, WHEN a workflow transition starts a different session for the same task, THEN the dockview selects the newly started session. An explicitly selected live session remains selected. -
GIVEN advanced mode is showing a parked
IDLEsession, WHEN the user changes its model, mode, or dynamic runtime option before sending the next message, THEN the selection is persisted and applied when the agent process resumes. A running session applies the same selection immediately and persists it for future recovery.
Out of scope
- Per-workspace strategy lock UI. The unified model serves mixed workspaces without one.
- Editing participants while a step is in flight. Static for v1; new participants take effect at the next step entry.
- Workspace-scoped wakeups (no task_id). Every wakeup is task-scoped; workspace-level reasoning lives on coordination tasks.
- Cross-workspace
parent_id. - Auto-resume an office agent on user comment when the assignee is
paused (
office-optimistic-commentsPhase 2 territory). - Editing comments after submission, richer attachments — separate features.
- Migration of existing office data. None exists in production; the dev DB is wiped on rebuild.
- Board grouping changes. Existing per-workflow swimlane UX unchanged.
Open questions
These are flagged in the spec; the plan resolves them.
-
Quorum semantics edge cases. When a
decision_requiredreviewer is removed mid-flight, does the quorum advance? When a reviewer's run fails after recording a partial verdict, does it retry or accept the verdict? The plan picks defaults; we may revisit. -
Coordination task creation policy. Does office onboarding always create a CEO coordination task, or only if the workspace has a CEO agent? Plan default: only when a CEO agent exists; user can create coordination tasks for other senior roles manually.
-
on_heartbeatcadence config. Per-step config drives the default cadence; per-agent override viaoffice_agent_runtime. Resolved: most steps don't configureon_heartbeatat all — event triggers (on_comment, etc.) drive runs immediately. The 60s default applies only to coordination tasks. Heartbeats are NOT the latency floor for new work; the runs scheduler's event-driven claim path (plan B3.5) keeps the latency floor under 100ms. -
Backward compat for office service's existing event subscribers. The comment-created subscriber today posts directly to
office_runs. After unification, it must instead triggerEngine.HandleTrigger("on_comment", task)and let the engine emitqueue_run. The plan tracks this rewiring carefully because the office UI's reactive paths depend on the resulting WS events.