Skip to content

feat(platform): operate automation-owned tasks from the task board#2827

Open
larryro wants to merge 3 commits into
mainfrom
feat/task-automation-contract
Open

feat(platform): operate automation-owned tasks from the task board#2827
larryro wants to merge 3 commits into
mainfrom
feat/task-automation-contract

Conversation

@larryro

@larryro larryro commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

For automation-driven tasks (e.g. the VAT return desk), the task board was a second-class surface: the flow could only complete inside the automation's own desk view. From the board, a task like VAT return — 2026Q1 could not be started or re-run, dropped files landed in task Attachments (blobs the pipeline never reads — the run reads the quarter FOLDER), produced outcome files were invisible, and a board-created task became a zombie duplicate the desk's reconcile would never adopt. Root cause: the task-surface semantics lived only in view config (views/desk.json when/args) plus implicit conventions (externalId = folder id), which only the desk view can read.

Change

One declaration, every surface — and the board's NATIVE verbs stay the interface. Automations declare a subjects.task contract in automation.json (taskSubjectContractSchema: workflow, externalSystem, folder input, template create, start.when, review), exposed through listAutomations summaries, and the generic task surfaces consume it:

  1. Status IS the verb (useTaskStatusChoreography): no bolted-on Start button — a status change on an owned task routes through a pure, unit-tested transition matrix (decideTaskStatusTransition), wired at both native entry points (board drag useTaskBoardDnd + modal StatusPicker):
    • drag/set to In progress → start the run (contract start.when over {status, hasFiles}); when ONLY the input is missing, block with feedback and snap the card back instead of writing a status the run would never pick up
    • In review → In progress → request changes (the rerun reads the timeline feedback since the last anchor)
    • dragging a running task out of In progress → cancel first, then land the chosen column
    • everything else, and unowned tasks → the plain status write; the workflow's own ack step stays the only status writer for choreographed transitions
  2. Input lands where the run reads it (TaskSubjectFiles): for folder-input tasks the Attachments zone swaps to the bound folder's FolderUploadCard (now accepts explicit org/project ids outside an automation runtime); legacy attachments still list below when present.
  3. Template creation from the board: the create dialog offers create.enabled contracts as task types (single contract field with per-locale label/help, naming regex validated inline, title derived via titleTemplate). Submission goes through the SAME createTaskFromExternalIssue upsert seam the desk's New-quarter button uses, so board- and desk-created subjects converge on one task by (externalSystem, externalId) — the zombie-duplicate failure mode is structurally impossible.
  4. Ownership decoupled from run-on-create: agentUpsertTaskByExternalRef + createTaskFromExternalIssue accept automationSlug and stamp createdByType:'app' / createdBy:<slug> WITHOUT starting a workflow (the desks' create-now-run-later shape; runWorkflowSlug derivation unchanged). Tasks born before stamping resolve via a UNIQUE externalSystem match — ambiguity resolves to none.
  5. Outcome on the task modal (TaskOutcomeSection): the latest subject run's surface:"outcome" deliverables — documents preview, plain files link out, text renders as markdown, pending slots pulse — via OutcomeRows, extracted from the desk's OutcomeStrip (desk chrome/behavior unchanged).
  6. Task comments newest-first with the composer on top (task modal + runtime resource detail), matching the desk row's timeline ordering.

The vat-return-desk pack declares the contract in tale-project/configs (separate commits there; its desk New-quarter also passes automationSlug now). Manifest schema stays .passthrough() — packs with the declaration remain valid on older platforms.

Verification

  • Unit/component: contract schema parse+rejects, ownership stamping (app vs agent attribution), pure contract resolver (stamp wins / unique-externalSystem fallback / ambiguity refuses / invalid contracts ignored), the full transition matrix (start / blocked-missing-input / request-changes / cancel-then-move / reopen-done passthrough / no-gate never auto-starts), attachments↔folder-card swap, outcome section — 90 tasks-suite tests plus schema/server suites, all green.
  • Live, driven end-to-end in a dev org entirely from the task board: Create task → type "Swiss VAT return desk" → quarter-name validation (bad name rejected inline) → 2026Q3 created (folder + Setup binding + Author: vat-return-desk) → modal shows the folder card instead of Attachments → uploaded an invoice → set status to In progress → the run started and the workflow itself wrote the status (activity: "vat-return-desk: status changed Cancelled → In progress") → changed status to To do while running → run cancelled, task landed To do → empty-folder task (2026Q4) set to In progress → transition refused, status stayed Backlog.

Definition of done

  • Green gate — oxlint / tsc / touched vitest suites / commit hooks pass locally
  • Security gate — bun run lint:sast clean (0 findings)
  • Tests — schema, internal_mutations ownership, resolver, transition matrix, subject files, outcome section
  • Migration — migrations:check green: subjects + automationSlug args are data-safe growth (new optional fields), no data migration; baseline snapshot deliberately not refreshed here (a refresh rewrites ~19k lines of pre-existing drift — separate maintenance change)
  • Locales — tasks.create.*, tasks.run.*, tasks.detail.outcome in en/de/fr; parity suite green
  • Docs — N/A user docs; contract fields documented inline in taskSubjectContractSchema
  • Accessibility — template picker uses aria-pressed, contract field wires aria-invalid + described-by help, pending outcome slots keep role=status; status choreography rides the existing keyboard-reachable status controls
  • Sweep — both status entry points (board drag + modal picker), both task detail surfaces (modal + runtime resource-detail), both create entries (board dialog + desk New-quarter); desk view behavior unchanged
  • Observed — full board-only flow driven live (see Verification)
  • Commits — two conventional commits off main (contract surfaces; status-verb choreography)

Follow-ups (out of scope): assignee policy (starter-as-assignee vs pack-declared), a task-choreographed start_task_workflow agent tool (generic run_workflow lacks subject binding), adoption of hand-created tasks into a contract, vat pack task-description copy still says "press Start on this row" (desk wording), config snapshot baseline refresh.

larryro added 3 commits July 21, 2026 23:00
One declaration, every surface: an automation can declare a
subjects.task contract in its automation.json (workflow, folder input,
template create, start gating, review), and the generic task surfaces
consume it instead of desk-view-only config:

- Task modal run actions: Start / Request changes / Cancel, gated by
  the contract's when-predicate over live facts (status, hasFiles) and
  the subject run — same startTaskWorkflow choreography as the desk.
- The Attachments zone swaps to the bound folder's upload card for
  folder-input tasks, so input lands where the run actually reads it.
- The create dialog gains contract templates (board-side New quarter)
  through the same createTaskFromExternalIssue upsert seam the desk
  uses — board- and desk-created subjects converge on ONE task.
- App ownership decoupled from run-on-create: upsert + create accept
  automationSlug and stamp createdByType:'app' without starting a run;
  pre-stamp tasks resolve via a unique externalSystem match.
- Task modal Outcome section on the shared surface:"outcome"
  projection (OutcomeRows extracted from the desk's OutcomeStrip).
- Task comments render newest-first with the composer on top.
…flow

Drop the bolted-on Start/Cancel buttons: the board's NATIVE verbs are
the interface. A status change on an automation-owned task routes
through decideTaskStatusTransition (pure, unit-tested matrix):

- drag/set to In progress  -> start the run (contract start.when over
  {status, hasFiles}); when ONLY the input is missing, block with
  feedback and snap the card back instead of writing a status the run
  will never pick up
- In review -> In progress -> request changes (the rerun reads the
  timeline feedback since the last anchor)
- leaving a running In progress -> cancel first, then land the chosen
  column (cancel itself parks at Cancelled)
- everything else, and unowned tasks -> the plain status write

Wired at both native entry points: board drag (useTaskBoardDnd) and
the modal StatusPicker. The workflow's own ack step remains the only
status writer for choreographed transitions.
Close the discoverability gap the status choreography opened — special
behavior must announce itself before it fires, from data, never product
literals:

- TaskAutomationBadge (icon on the board card, icon + name in the
  modal): the visible 'this task is operated' signal, name resolved
  through the owning manifest's i18n, tooltip explains the status
  choreography.
- Status picker pre-flight hints: each option that would start /
  request changes / cancel says so inline, derived from
  plannedTransitionKind — the SAME matrix that executes the
  transition, so hint and behavior cannot drift.
- FolderUploadCard anchors to its folder on contract surfaces: name in
  the title (Files — 2026Q3) plus an open-in-Knowledge link.
- getTaskOpsIndicators unions subject-linked workflow executions into
  runningTaskIds, so a choreographed run pulses the board card's
  standard working indicator (active runs are capacity-bounded; the
  org-wide scan stays tiny).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant