42 KiB
status, created, owner
| status | created | owner |
|---|---|---|
| draft | 2026-05-02 | cfl |
Office Tasks
Why
Office tasks are the unit of work in office mode: a single task carries a description, a workspace, an assignee, optional reviewers and approvers, blockers, parent/child relationships, a chat thread, and a per-agent session of working memory. Users and agents need to drive these tasks end-to-end - hand off context across a tree of subtasks, request review and approval, edit properties inline, see the agent's work in the chat, react to property changes without an agent having to poll, and preserve each participant's conversation across many short-lived agent runs.
This spec consolidates the office task surface: lifecycle, parent/child handoffs, approval flow, advanced execution mode, blocker cycle detection, the reactivity pipeline, per-(task, agent) session identity, inline editable properties, and the chat / activity views.
What
A. Lifecycle and identity
- A task progresses through statuses
todo → in_progress → in_review → done, withblockedandcancelledas branchable states. Status transitions are user- or agent-driven and feed the reactivity pipeline (section E). - Every office task has zero or more task sessions: one per
(task_id, agent_instance_id)pair. A session represents one agent's persistent conversation thread on the task, not a single launch. - Sessions cycle through
CREATED → STARTING → RUNNING → IDLE → RUNNING → IDLE → ...for as many turns as the agent is woken. A session is terminal (COMPLETED/FAILED/CANCELLED) only when the agent leaves the task's participants list. - Wakeups (section E) drive transitions IDLE → RUNNING. Turn-complete events drive RUNNING → IDLE, tearing down the executor and agent process entirely; the conversation is preserved via the stored ACP session token.
- Kanban / quick-chat sessions keep their per-launch model and
WAITING_FOR_INPUTsemantics - office's IDLE state is office-scoped.
B. Parent / child handoffs
- Tasks form a tree via
parent_id. Parent tasks act as the default home for shared specs, plans, and coordination documents. - Child tasks can read parent-owned documents and write parent-owned coordination documents by default. Document handoffs reuse the existing blocker mechanism: a consumer task is blocked-by the producer task and reads the resulting documents from its wakeup prompt context. There is no separate "required documents" data type.
- Agents can list related tasks (parent, children, siblings, blockers, blocked) and can read/write allowed task documents via MCP or CLI tools.
- A parent task defines a default child workspace policy for its tree: children inherit the parent workspace OR create their own workspace.
- A parent task defines a default child ordering policy: children are created with dependency edges (sequential) or without (parallel).
- Each child task can override the parent workspace policy and run in the parent workspace, a new workspace, or an explicit shared workspace group.
- When a user creates a subtask from a Kanban task detail dialog, the dialog lets them choose to inherit the parent materialized workspace or create a new workspace from repositories, local folders, or remote URLs.
- When the user chooses to inherit the parent workspace, the dialog can identify the parent session branch because the child shares that worktree and file state.
- When the user chooses a new workspace, the dialog shows only the selected source and base-branch controls. It does not present the parent session branch as the branch of the isolated child workspace.
- When an agent creates a subtask through
create_task_kandev, the MCP surface mirrors that choice: subtasks inherit the parent materialized workspace by default, andworkspace_mode="new_workspace"requests a fresh materialized workspace/worktree. - Subtasks inheriting a parent materialized workspace are represented through the same shared workspace group model used by Office-created tasks.
- Shared workspaces are visible in the UI: source task, member tasks, materialized path/environment, and whether tasks are ordered by dependencies.
- Workspace sharing does not lock execution in v1. Sequential behavior is expressed through task blocker / dependency edges.
- Wakeup prompt context names whether the agent should read parent documents, source-task documents, or shared workspace files; document bodies are NOT injected inline.
- Archiving or deleting a task releases it from shared workspace membership and can trigger cleanup when no active, non-archived members remain.
- Archiving a parent task cancels active descendant runs, recursively archives descendants, releases their workspace memberships, and runs cleanup.
- Deleting a parent task cancels active descendant runs, recursively deletes descendants, releases their workspace memberships, and runs cleanup.
C. Approval flow
- A task carries two participant lists: reviewers (advisory) and approvers (gating). The lists are edited inline from the Properties panel.
- When a task enters
in_review, every reviewer AND approver is woken withtask_review_requested. The wakeup carriesroleso the agent's prompt renders an appropriate hint. - A task SHALL NOT transition
in_review → donewhile it has any approvers without a currentdecision = approved. Reviewers are advisory and do not gate completion. - Transitioning
in_review → todo|in_progress(rework) supersedes all prior decisions; re-enteringin_reviewstarts a fresh round. - Approving a task while approvers are pending wakes the assignee with
task_changes_requested(when changes are requested) ortask_ready_to_close(when the final approval lands). - Recording a decision does NOT terminate the deciding agent's session. The reviewer's session stays IDLE between cycles and is re-used on a subsequent
task_review_requestedwakeup. - Tasks awaiting the current user's review/approval surface in the inbox as item type
task_review_requestonly when the user/agent is a reviewer or approver participant with a required decision. Runner/assignee participation alone does not create a review inbox item. - v1 approval is unanimous: every listed approver must approve. There is no quorum / any-of-N mode.
D. Inline editable properties
- Every property row that holds an enumerable, lookup-able, or free-text value is editable inline by clicking the value: Status, Priority, Assignee, Project, Parent task, Blocked by, Reviewers, Approvers, Labels.
- Read-only rows stay read-only: Created by, Started, Completed, Created, Updated, Tree cost metrics. They reflect events, not user intent.
- Edits are optimistic: the new value renders immediately; on API failure the row reverts to the prior value and a toast surfaces the error.
- Pickers are searchable when the candidate list could exceed roughly 10 items (Assignee, Project, Parent, Blocked-by). Status and Priority pickers use a fixed list. The threshold for switching to a search input is around 8 candidates.
- The editor works inline on the task detail page - no separate dialog, no navigation.
- Property changes broadcast over the existing WS event stream so other open clients update reactively. No polling.
E. Reactivity pipeline
A backend pipeline runs synchronously on every relevant task property change. Property mutators (UpdateTaskStatus, MoveTask, SetTaskAssignee, CreateTaskComment, blocker mutations, participant mutations) feed the pipeline; the pipeline returns wakeups, patches, and decision records that are persisted in one DB transaction alongside the user's change.
Wakeup triggers:
status → done: query tasks blocked by this task; wake each with{reason: "blocker_resolved", resolved_blocker_task_id}. Wake the parent task with{reason: "child_completed"}if all siblings are done.status: blocked → unblocked: wake assignee with{reason: "task_unblocked"}.status: done|cancelled → todo|in_progress: wake assignee with{reason: "task_reopened", actor_id, actor_type}.- Assignee change: wake new assignee with
{reason: "assigned", comment_id?, actor_id}. If old assignee had an active session, cancel it cleanly (session.cancellednotification) and surface that to the new assignee's context. - Created task with assignee: if a newly-created task or subtask already has a runner, queue the same assigned wakeup idempotently with
{reason: "assigned", task_id}. - Comment created by user: wake assignee with
{reason: "user_comment", comment_id}. Wake any @-mentioned agents in the same workspace with{reason: "mentioned", comment_id}. status → in_review: wake every reviewer and approver with{reason: "task_review_requested", role}.decision = changes_requested: wake assignee with{reason: "task_changes_requested", comment}.- All approvers have approved AND status is
in_review: wake assignee with{reason: "task_ready_to_close"}. status → cancelled: cancel the task's active execution immediately (interrupt the current turn, mark the session cancelled) and emitoffice.task.cancelled.
Execution-policy transition engine runs alongside wakeups: reads the task's execution_policy (work / review / approval stages) and returns (1) a patch to apply alongside the user's change (e.g. advance execution_state to review_pending), (2) wakeups to queue, (3) a decision record if the transition encodes an approval verdict. The engine is invoked from every mutation entry point and MUST NOT poll or schedule.
Wakeup context carries a structured JSON payload: reason, task_id, optional actor_id/actor_type, optional comment_id, cascade-specific fields (resolved_blocker_task_id, child_task_id), policy fields (stage_id, allowed_actions). The agent runtime reads context.reason to select the system prompt template.
F. Blocker cycle detection
- Adding a blocker
B blocks ASHALL be rejected when a path already exists fromBback toAthrough existing blockers. This catches cycles of length 3 or more (existing checks already cover self-blocking and direct two-node cycles). - The detection runs as a breadth-first walk starting at the proposed
blockerTaskID, traversing the blocker chain via existing repository queries, bounded by a visited set. - A rejected attempt returns a typed error including the cycle path (e.g.
"would create cycle: A → B → C → A"). The frontend blockers picker surfaces the message as a toast and rolls back its optimistic chip.
G. Per-(task, agent) session lifecycle
task_sessionsrows are keyed by(task_id, agent_instance_id)for office tasks. The same pair reuses one row across many wakeups. Kanban / quick-chat sessions leaveagent_instance_idNULL and keep their per-launch +is_primarymodel.- A single task may carry multiple sessions: assignee, each reviewer, each approver, each @-mentioned agent. Each maintains its own conversation; one agent's notes never appear in another's buffer.
- On wakeup,
EnsureSessionForAgent(task, agent_instance):- Looks up the row by
(task_id, agent_instance_id). - If found and IDLE: flip to RUNNING.
- If found and RUNNING/STARTING: return as-is (idempotent).
- If found and terminal: create a new row (prior pair was retired).
- If not found: insert with state
CREATED.
- Looks up the row by
- ACP init: if
acp_session_idis stored, callsession/load. On failure (expired session, agent CLI version mismatch), fall back tosession/newand overwrite the stored token. Row identity is preserved at the kandev level even when the underlying conversation is reset. - On turn complete for office sessions, the agent process AND executor backend (container, standalone process, sprites instance, agentctl) tear down completely. The conversation is preserved via the stored ACP token. The next wakeup recreates the executor cold.
- A session goes terminal (
COMPLETED) only on participation removal:- Reassignment retires the previous assignee's session.
- Removing a reviewer/approver via the picker retires that agent's session.
- Deleting the agent instance at workspace level cascades and retires all of its sessions across all tasks.
- Decisions (approve / request-changes) do NOT terminate the deciding agent's session - they cycle RUNNING → IDLE so a subsequent review round resumes the same conversation.
- Re-adding the same agent after a terminal row creates a fresh row with a fresh conversation thread.
H. Advanced mode
/office/tasks/[id]?mode=advancedexposes dockview panels (files, terminal, changes) backed by a running agentctl execution.- Office tasks usually have no warm execution (the agent ran and tore down), so entering advanced mode SHALL ensure an execution exists for the resolved session without sending a new prompt to the agent.
- The session resolved for advanced mode is:
- The current viewer's session if the viewer is an authenticated agent.
- Otherwise (singleton human user), the current assignee's session.
- If no session exists yet (user enters advanced mode before the first wakeup), create one for the assignee on demand, then ensure the executor.
- Panels gate on
agentctlStatus.isReady; existing WS eventssession.agentctl_starting/session.agentctl_readydrive their loading states. No panel directly creates executions. - If executor resume fails, the call is non-fatal: the session still returns and panels show their existing "not available" states.
I. Task chat
The Chat tab on the office task detail page is a unified timeline of comments, agent work, and system events.
Auto-bridging agent responses to comments:
- When an agent completes a turn, its final text message is auto-posted as a
task_commentwithauthor_type=agentandsource=session. - Only messages of type
message(notstatus,script_execution, etc.) are bridged. - The comment carries the agent's name as
author_id. - Duplicate comments are prevented by checking for an existing
source=sessioncomment for the same turn.
Merged thread:
The Chat tab displays a chronological merge of:
- User comments posted via the comment input or API.
- Agent comments auto-bridged from session messages, or posted via
kandev comment add. Agent comments from auto-bridge show a "via session" indicator. - Timeline events for status changes (SCHEDULING, IN_PROGRESS, REVIEW, DONE) and assignee changes, rendered as compact system entries.
- Inline approval-decision events ("CEO approved this task" / "Eng Lead requested changes: '...'").
Expandable agent session in chat (v2):
- Between comments, each agent work session appears as a collapsible entry, one per (task, agent) pair, ordered by most-recent activity.
- Collapsed: shows "Agent worked for Xm Ys" with a chevron. If running, shows a spinner and "Agent working..." and auto-expands.
- Expanded: renders the
AdvancedChatPanelcomponent (reused from advanced mode) with the full session transcript - tool calls, thinking, messages. - A task with assignee + 2 reviewers shows up to 3 entries. Each entry collapses when its agent's session is IDLE; expanded by default while RUNNING.
- The "ran N commands" header derives from messages on that session, accumulated across launches.
Reply wakes agent:
- Posting a comment on a task with an
assignee_agent_instance_idqueues atask_commentwakeup for that assignee. - The agent receives the comment text in its wakeup context and can respond on its next turn.
Activity tab:
- Task status transitions (CREATED → SCHEDULING → IN_PROGRESS → REVIEW → DONE) are logged to
office_activity_logwithtarget_type=taskandtarget_id=taskID. - The Activity tab renders these entries with the status transition and timestamp.
Sidebar counters:
- The office sidebar shows count badges next to Tasks, Skills, and Routines, fetched from the dashboard API or dedicated count endpoints, updated on navigation or data change.
Data model
task_sessions
id TEXT PK
task_id TEXT FK -> office_tasks.id
agent_instance_id TEXT nullable -- office: set; kanban/quick-chat: NULL
acp_session_id TEXT nullable -- preserved across IDLE turns
state enum CREATED | STARTING | RUNNING | IDLE
| WAITING_FOR_INPUT | COMPLETED | FAILED | CANCELLED
is_primary bool -- kanban resume; office never reads this
...
UNIQUE INDEX uniq_office_task_session
ON task_sessions(task_id, agent_instance_id)
WHERE agent_instance_id IS NOT NULL;
office_task_approval_decisions
id TEXT PK
task_id TEXT FK -> office_tasks.id ON DELETE CASCADE
decider_type enum user | agent
decider_id TEXT '' for user
role enum reviewer | approver
decision enum approved | changes_requested
comment TEXT default ''
created_at DATETIME
superseded_at DATETIME nullable -- non-NULL when replaced by a newer decision
INDEX idx_task_decisions_task (task_id)
office_task_execution_decisions
id TEXT PK
task_id TEXT FK -> office_tasks.id
stage_id TEXT
verdict enum approved | changes_requested | rejected
actor_id TEXT
actor_type enum user | agent
comment_id TEXT nullable
created_at DATETIME
task_blockers -- existing
task_id TEXT FK
blocker_task_id TEXT FK
CHECK (task_id != blocker_task_id)
task_comments -- existing, extended
...
author_type enum user | agent
source enum manual | session -- session = auto-bridged from agent turn
office_activity_log -- existing, extended
target_type enum ... | task
target_id TEXT
event_type TEXT -- 'status_changed', ...
...
Multiple rows per (task, decider, role) in office_task_approval_decisions are allowed; only the most recent (superseded_at IS NULL) counts toward the gate.
API surface
HTTP (office task service):
POST /tasks/:id/approve { comment?: string }- caller approves. 403 if caller is not in reviewers or approvers. Returns the created decision row.POST /tasks/:id/request-changes { comment: string }- caller requests changes. Comment required. Same 403 rule. Returns the decision row.GET /tasks/:id/decisions- current (non-superseded) decisions. Also surfaced on the task detail endpoint as adecisions: TaskDecision[]field.PATCH /tasks/:id- inline property edits (status, priority, assignee, project, parent, blocked-by, reviewers, approvers, labels). Returns the updated task.POST /tasks/:id/blockers { blocker_task_id }- returns 400 with a body carrying the cycle path when the addition would create a cycle.DELETE /tasks/:id/reviewers/:agentId/DELETE /tasks/:id/approvers/:agentId- removes the participant AND terminates that agent's session for the task.
WebSocket:
session.ensure { task_id, ensure_execution?: bool }- idempotent; returns the resolved session. Whenensure_executionis true and the session exists without a running execution, the backend ensures the executor resumes. Non-fatal on resume failure.office.task.decision_recorded { task_id, decision_id, role, decider_type, decider_id, decision, created_at }.office.task.review_requested { task_id, role, reviewer_agent_id }- fans out per reviewer for client-side inbox refresh.office.task.cancelled { task_id }.office.task.updated/office.task.status_changed- existing events continue to fire on property and status mutations.session.agentctl_starting/session.agentctl_ready- existing events drive advanced-mode panel readiness.
Go interfaces:
EnsureSessionForAgent(ctx, task, agentInstance) (*TaskSession, error)- office wakeup launch path.applyTaskExecutionPolicyTransition(task, change) (patch, []Wakeup, *Decision)- policy engine, invoked synchronously inside every mutator that can advance a stage.- Wakeup queue producers emit a structured
contextJSON (reason,task_id, optional fields per section E).
State machine
Task session (office):
CREATED ──► STARTING ──► RUNNING ──► IDLE ──► RUNNING ──► IDLE ──► ...
│ │
▼ ▼
COMPLETED COMPLETED (on participation removal)
/ FAILED / FAILED
/ CANCELLED / CANCELLED
IDLE → RUNNING: a wakeup for the matching(task, agent_instance)fires.EnsureSessionForAgentflips the row.RUNNING → IDLE: turn-complete event for an office session. The agent process and executor backend tear down.acp_session_idis preserved.RUNNING → CANCELLED: status set tocancelled, or an explicit hard cancel from the reactivity pipeline (e.g. reassignment of the prior assignee).* → COMPLETED: participation removal (reassignment of assignee, picker-removal of reviewer/approver, workspace-level deletion of the agent instance). Plain decisions do NOT cause this.
The updateTaskSessionState terminal-state guard relaxes for office: COMPLETED / FAILED / CANCELLED stay terminal, but IDLE → RUNNING is allowed.
Kanban / quick-chat sessions keep CREATED → STARTING → RUNNING → WAITING_FOR_INPUT → ... and warm executors between turns; office-only states are not used.
Task status (relevant transitions for reactivity):
todo ──► in_progress ──► in_review ──► done
│ │ │ ▲
│ ▼ │ │
└───────► blocked └── todo / in_progress (rework, supersedes decisions)
└───────► cancelled (from any non-terminal state)
Permissions
There are two authorization layers: agent capability (what a running agent can do via runtime APIs) and HTTP caller identity (what a UI user or JWT-bearing agent can do via task endpoints). See agents.md for the agent role / permission model that supplies the capability set on each run.
Agent runtime capabilities (resolved from role + permission overrides; enforced by the office runtime handlers):
| Task action | Required capability | Default holders |
|---|---|---|
| Post a comment / reply | post_comment |
All roles (assignee, reviewers, approvers, mentioned agents). |
| Change task status | update_task_status |
All roles. |
| Create a subtask | create_subtask |
Roles with can_create_tasks (CEO, worker). |
| Request approval on a task | request_approval |
Roles with can_approve (CEO). |
| Spawn an agent run on a task (assign and wake) | spawn_agent_run |
Roles with can_assign_tasks (CEO, worker). |
An agent's run JWT also pins an AllowedTaskIDs scope. The runtime rejects mutations against any task outside that scope with ErrTaskOutOfScope (403, shared.ErrForbidden), even when the capability is granted - this prevents a worker on task T1 from mutating task T2 just because both belong to its workspace.
HTTP endpoint gates (internal/office/approvals, task service):
POST /tasks/:id/approve,POST /tasks/:id/request-changes: when called by an agent (caller has an agent JWT), the handler additionally checks: (1)caller.WorkspaceID == task.WorkspaceID(403 otherwise), (2)has_permission(can_approve)(403 otherwise), (3) the caller is not the requester of the approval (403 self-decide). Reviewers / approvers list membership is the user-facing gate; thecan_approvepermission is the underlying agent gate. UI callers (no agent JWT) are accepted without these checks today.DELETE /tasks/:id/reviewers/:agentId,DELETE /tasks/:id/approvers/:agentId: caller must be a workspace UI user; no agent-side endpoint exposes participant removal.PATCH /tasks/:id(inline property edits) andPOST /tasks/:id/blockers: no per-field authorization in v1; any UI user with workspace access can edit any task, and any agent with the appropriate runtime capability can mutate within its task scope.- Approver gate on
done: independent of caller identity. Adonetransition is rejected 409 while any approver lacks a currentapproveddecision (see Failure modes).
Out of scope for this iteration:
- A "no permission" UI state for fields a user cannot edit, and per-field user-side permission rules (see
out of scope). - Granular per-task ACLs beyond workspace membership.
- Workspace-level rules for who can be added as assignee / reviewer / approver - those follow the existing agent / workspace membership rules and are not redefined here.
Failure modes
- Approver gate violation. A
donetransition while approvers are pending is rejected with 409 and a typed error naming the missing approvals. The UI redirects the transition toin_reviewas a convenience. - Blocker cycle. Adding a blocker that would create a cycle returns 400 with the cycle path. The frontend rolls back the optimistic chip and toasts the message.
- Optimistic property edit failure. The UI reverts the row to the prior value and shows a toast. No partial server state.
- ACP
session/loadfailure. Fall back tosession/new, overwrite the storedacp_session_id, treat the conversation as fresh. Row identity at the kandev level persists; chat history at the office level (comments, decision rows, timeline events) is unaffected. session.ensure { ensure_execution: true }resume failure. The session is still returned; the executor is not started; panels show their normal "not available" / "Preparing workspace..." states.- Reactivity pipeline DB transaction failure. The user's change rolls back along with any pending wakeups, patches, and decision records produced by the policy engine - all writes are in one transaction. No partial reactions.
- Wakeup queue worker not running. Reactions do not fire. Already the case today; no polling fallback is introduced.
- Workspace cleanup with active sessions. Cleanup is deferred until no active, non-archived members reference the workspace AND no active sessions hold it.
- User-owned local folders. Never deleted by workspace cleanup, even when the last task referencing them is deleted.
Persistence guarantees
Survives restart:
- Task rows, comments, decision rows, blocker edges, parent/child links, project / assignee / reviewers / approvers assignments.
task_sessionsrows includingagent_instance_id,acp_session_id, andstate. Office IDLE sessions resume on the next wakeup viasession/load.- Activity log entries.
- Materialized workspaces owned by Kandev (worktrees, clones, plain folders) - lifecycle tied to task membership, not process lifetime.
- Shared workspace group membership.
Does NOT survive restart:
- In-memory execution entries in
lifecycle.Manager. Office sessions in IDLE are expected to have zero in-memory executions until the next wakeup. - Agent subprocesses, executor backends (container, standalone, sprites), agentctl HTTP connections for IDLE office sessions.
- Optimistic UI state for unsaved property edits.
Cleanup rules:
- Kandev-owned materialized workspaces are cleaned up after the last active member is archived or deleted AND no active sessions reference the workspace. Kandev does NOT snapshot workspace contents before cleanup; files in cleaned folders, clones, or worktrees are intentionally discarded.
- Unarchiving a task with a cleaned Kandev-owned workspace recreates a fresh materialized workspace from stored source configuration when possible. If reconstruction is impossible, the task becomes active with a workspace-requires-configuration status visible to the user.
- User-owned local folders and existing local checkouts are never deleted by workspace cleanup.
Scenarios
Handoffs:
- GIVEN a coordinator creates a planning task and an implementation task with the implementation blocked-by the planner, WHEN the planner writes
specandplandocuments up to the parent and completes, THEN the blocker resolves and the implementation task wakes; its prompt names the parent's available document keys so the agent fetches them via the task document tool. - GIVEN a parent policy says children inherit the parent workspace and run sequentially, WHEN the coordinator creates child tasks, THEN the children reuse the parent materialized workspace and receive dependency edges that order their execution.
- GIVEN a child task inherits a parent materialized workspace, WHEN the child is archived or deleted, THEN the inherited parent workspace remains available to the parent and any other active child that still references it.
- GIVEN a user opens a Kanban task detail page, WHEN they create a subtask, THEN they can choose to inherit the parent task workspace or create a new workspace by selecting repositories, local folders, or a remote URL.
- GIVEN a parent session has an active worktree branch, WHEN the user selects Inherit parent workspace in the subtask dialog, THEN the dialog identifies the parent branch as shared with the child.
- GIVEN a parent session has an active worktree branch, WHEN the user selects Create new workspace in the subtask dialog, THEN the parent branch indicator is absent and the selected source's base-branch control is the only branch context shown for the child.
- GIVEN an agent creates a subtask via MCP, WHEN it omits
workspace_mode, THEN the subtask inherits the parent materialized workspace; WHEN it setsworkspace_mode="new_workspace", THEN the subtask launches in its own materialized workspace/worktree. - GIVEN two tasks share a workspace group without dependency edges, WHEN the scheduler starts both, THEN they may run concurrently in the same materialized workspace.
- GIVEN a parent task has descendant tasks, WHEN the user deletes the parent, THEN Kandev cancels active descendant runs, deletes every descendant, releases their workspace memberships, and runs cleanup.
- GIVEN an archived task's workspace cannot be recreated from stored configuration, WHEN the task is unarchived, THEN the task becomes active with a workspace-requires-configuration status visible to the user.
Approval flow:
- GIVEN a task with CEO as the only approver in
todo, WHEN the user attempts to move it directly todone, THEN the transition is rejected 409 with a toast "Cannot mark done: awaiting approval from CEO" and the status is redirected toin_review. - GIVEN a task moves to
in_review, WHEN the reactivity pipeline runs, THEN each reviewer and approver receives atask_review_requestedwakeup AND an inbox item of typetask_review_request. - GIVEN CEO is the sole approver and approves via
POST /tasks/:id/approve, WHEN the decision is recorded, THENoffice.task.decision_recordedfires, the assignee receivestask_ready_to_close, and the comments timeline shows "CEO approved this task". - GIVEN an approver requests changes with comment "please update the docs", WHEN the decision is recorded, THEN the assignee receives
task_changes_requestedcarrying the comment AND thedonetransition remains gated. - GIVEN the assignee returns the task to
in_reviewafter rework, WHEN the transition happens, THEN all prior decisions are superseded and approvers must approve again.
Editable properties:
- GIVEN a task is
in_review, WHEN the user clicks the Status value and selects "Done", THEN the row updates optimistically within ~100ms and other open clients on the same task observe the change viaoffice.task.status_changed. - GIVEN the user clicks Priority and chooses "High", WHEN the request fails, THEN the priority reverts to its previous value and a toast says "Failed to update priority".
- GIVEN the user clicks Parent, WHEN they search "KAN" and pick a candidate task, THEN the row shows the new parent and
parent_idupdates server-side.
Blocker cycles:
- GIVEN existing rows
A blocks BandB blocks C, WHENPOST /tasks/A/blockers { blocker_task_id: C }, THEN the response is 400 with a body containing the cycle path "A → B → C → A". - GIVEN the blockers picker, WHEN a cycle is rejected, THEN the optimistic chip is removed and the toast displays the cycle path.
- GIVEN the existing single-step rejection
A blocks BthenB blocks A, WHEN the second insert is attempted, THEN rejection continues to work (no regression). - GIVEN a non-cycling addition
D blocks AwhileA blocks B blocks C, WHEN the insert is attempted, THEN it succeeds.
Reactivity pipeline:
- GIVEN task A is blocked-by task B, WHEN B moves to
done, THEN A's assignee receives a wakeup withcontext.reason = "blocker_resolved"andcontext.resolved_blocker_task_id = B.id. - GIVEN task A has children B, C, D all
done, WHEN the last child becomesdone, THEN A's assignee receivescontext.reason = "child_completed". - GIVEN task A is assigned to agent X with a session running, WHEN the user reassigns to agent Y, THEN X's session is cancelled cleanly, Y receives
context.reason = "assigned"withcontext.actor_id = <user>. - GIVEN the assignee is agent X, WHEN the user adds a comment "@reviewer please look at this", THEN X receives
context.reason = "user_comment"AND the agent namedreviewer(if it exists in the workspace) receivescontext.reason = "mentioned". - GIVEN task A is
in_progresswith stage statework_in_progress, WHEN the worker advances status toin_review, THEN the policy engine advances stage state toreview_pending, the reviewer receivescontext.reason = "stage_pending"withcontext.stage_id = "review", and nooffice_task_execution_decisionsrow is yet written. - GIVEN task A has an active session, WHEN status is set to
cancelled, THEN the active turn is interrupted within 2 seconds and the session showscancelledstate.
Session lifecycle:
- GIVEN a task with no prior session, WHEN the first wakeup runs, THEN a single
task_sessionsrow is created withagent_instance_idmatching the assignee and an emptyacp_session_iduntil ACP handshake fills it. - GIVEN an office session in IDLE, WHEN a second wakeup for the same agent fires, THEN no new row is created; the state cycles RUNNING → IDLE → RUNNING; the agent process is launched with
session/loadand resumes the conversation. - GIVEN a task with assignee CEO and reviewer QA, WHEN both have been woken, THEN
(TES-1, CEO)and(TES-1, QA)are distinct rows with distinctacp_session_ids and CEO's working notes do not appear in QA's chat embed. - GIVEN the agent finishes a turn, WHEN turn-complete fires for an office session, THEN the state flips RUNNING → IDLE BEFORE the workflow handler runs, the agent process exits, the executor backend tears down, and the topbar spinner disappears without a refresh.
- GIVEN a reviewer approves, WHEN the decision is recorded, THEN the reviewer's session goes RUNNING → IDLE (not COMPLETED) and the row keeps its
acp_session_idfor the next review cycle. - GIVEN the task assignee is reassigned, WHEN the change is applied, THEN the prior assignee's session goes COMPLETED.
- GIVEN a reviewer is removed via the picker, WHEN
DELETE /tasks/:id/reviewers/:agentIdruns, THEN that agent's session for the task goes COMPLETED. - GIVEN an office session is in IDLE, WHEN
lifecycle.Manageris queried, THEN it reports zero in-memory executions for the (task, agent) pair until the next wakeup. - GIVEN the stored
acp_session_idis rejected by the agent CLI, WHEN ACP init runs, THEN the runtime falls back tosession/new, the stored token is overwritten, the conversation is treated as fresh, but the kandev session row identity persists. - GIVEN a kanban or quick-chat task, WHEN any wakeup or turn runs, THEN the per-launch +
is_primary+ WAITING_FOR_INPUT + warm-executor model applies unchanged.
Advanced mode:
- GIVEN an office task whose agent ran and completed (execution torn down), WHEN the user enters advanced mode, THEN
session.ensureis called withensure_execution: true, the backend resumes the executor for the resolved session, and files / terminal / changes panels load. - GIVEN the user leaves and re-enters advanced mode, WHEN
session.ensureruns again, THEN the call is idempotent and no duplicate execution is created. - GIVEN the backend restarts while the user is in advanced mode, WHEN the next workspace-oriented call is made, THEN
GetOrEnsureExecutionrecovers the execution on-demand. - GIVEN a task with no prior session, WHEN the user enters advanced mode, THEN
EnsureSessioncreates a new session for the assignee and the execution starts. - GIVEN executor resume fails, WHEN
session.ensure { ensure_execution: true }runs, THEN the session is still returned and panels show their existing "not available" states.
Task chat:
- GIVEN the CEO agent completes a turn on a "present yourself" task, WHEN the user opens the task detail page, THEN the Chat tab shows the agent's final text as a comment with the agent's name, timestamp, and a "via session" indicator.
- GIVEN a task with 2 agent comments and 1 user comment, WHEN the user views Chat, THEN all 3 entries appear chronologically with distinct styling for agent vs user.
- GIVEN a task in REVIEW, WHEN the user types a reply and sends, THEN a comment is created, the assignee is woken with
task_comment, and the comment appears in the chat immediately. - GIVEN a task that transitioned TODO → IN_PROGRESS → REVIEW, WHEN the user views Chat, THEN timeline events for each status change appear inline between comments.
- GIVEN a workspace with 5 tasks, 3 skills, 2 routines, WHEN the user views the office sidebar, THEN count badges show 5 / 3 / 2 next to Tasks / Skills / Routines.
- GIVEN an office task where an agent completed a turn, WHEN the user views Chat, THEN they see a collapsible "Agent worked for 4s" entry next to the auto-bridged comment; expanding it shows the full session transcript with tool calls.
- GIVEN an agent currently running on a task, WHEN the user views Chat, THEN they see "Agent working..." with a spinner, auto-expanded, showing the live session transcript.
- GIVEN a task with assignee + 2 reviewers each having had at least one turn, WHEN the user views Chat, THEN they see up to 3 collapsible agent-session entries, one per (task, agent) pair, ordered by most-recent activity.
- GIVEN a task that transitioned CREATED → REVIEW, WHEN the user views the Activity tab, THEN entries for each status change appear with timestamps from
office_activity_log.
Out of scope
- Conditional / quorum approval ("any 1 of 3 approvers"). v1 is unanimous.
- Approval delegation ("Eng Lead is OOO; let X approve in their place").
- Bulk approve / bulk edit across many tasks at once.
- External (non-workspace) approvers - v1 only supports workspace agents and the single human user.
- Approval expiry / TTL ("approval valid for 7 days"). Rework still clears decisions.
- Reviewer role gating completion - reviewers stay advisory. Use approvers for a hard gate.
- Preventing parallel editing of the same dirty workspace; workspace locks; active-holder recovery.
- Changing workspace-mode defaults, repository seeding, base-branch selection, generated task-branch naming, or whether uncommitted parent changes enter an isolated workspace.
- Automatic guessing between task documents and repo files.
- Replacing the existing task documents or task plans implementations.
- Detecting blocker cycles on parent / child relationships - that's a separate domain.
- A background job to scan existing data for pre-existing blocker cycles.
- Polling fallbacks for the wakeup queue.
- Webhook / external notifications (Slack, email) on reactivity events.
- Backfilling decisions for tasks closed before this lands.
- Cross-task session sharing (CEO on TES-1 is independent from CEO on TES-2).
- ACP session expiry / GC; a "GC stale IDLE sessions older than N days" sweep is a future spec.
- Conversation export / import.
- Auto-starting the agent (sending a prompt) on advanced-mode entry - "ensure execution" means agentctl is running, the agent is idle.
- Creating new sessions on advanced-mode entry beyond the on-demand assignee case - generally
ensure_executiononly resumes executions for existing sessions. - Changing how the kanban task details page works -
ensure_executionis opt-in. - A "no permission" UI state for fields the user cannot edit. Defer until a permission model exists.
- Drag-and-drop reordering of sub-tasks; this spec covers scalar properties only.
- Bulk-editing across multiple tasks.
- Editing reviewers / approvers when those roles aren't yet wired end-to-end at the backend.
- Provider / model fallback - covered by
../office-provider-routing/spec.md. - Live streaming of agent responses in the office chat (exists in the kanban task detail page).
- Related work tab (inbound / outbound task references).
- Thread interactions (suggest tasks, request confirmation).
- File attachments on comments.
- Multiple-session display in the v2 chat embed beyond one-per-(task, agent) entry; custom transcript parsing (reuse existing
MessageRenderer). - Run-level cost tracking per session.
Open questions
- Should each property edit fire its own focused WS event (
office.task.priority_changed, etc.) or piggyback on a genericoffice.task.updated? Current direction: generic event, sinceoffice.task.updatedalready triggers a refetch. - For
mentionedwakeups: do we resolve@nameagainst agent names, agent slugs, or both? The implementation plan picks one and documents the matching rule. - Decision records on policy transitions: one row per stage entry or one per stage exit (with verdict)? The implementation plan picks one.
- Use a new
office.task.cancelledevent subject, or piggyback onoffice.task.status_changedwith the new status? The implementation plan picks one.