Add Task source column and filter
Epic 5336 — Event-driven Task triggers. Upstream (read, don't re-derive): PRD = Task 5337 artifact `prd.md`; Architecture = Task 5338 artifact `architecture.md` (id 3566) — fetch with `task_read` / `task_read_artifact`. § numbers below are the architecture's.
## Original requirement (epic body, verbatim)
> a `source` column on the filed Task so the board filters by origin.
## Requirement (PRD R17, verbatim)
A `source` column on `tasks`, set at filing and never changed. Values: `console`, `cron`, `agent`, and `trigger:<kind>` (`trigger:webhook`, `trigger:github`, `trigger:gmail`, `trigger:slack`). Existing rows are backfilled: `created_by_cron_id` set → `cron`; `created_by_task_id` set → `agent`; else `console`. A trigger-filed Task also links to its trigger.
## Approach (architecture §3.1 `tasks` C1 part, §3.2, §3.5 last paragraph, §7 "The board")
- Migration: `tasks.source` text NOT NULL, CHECK in (`console`,`cron`,`agent`,`trigger:webhook`,`trigger:github`,`trigger:gmail`,`trigger:slack`), server default `'console'` (additive rule — an old release still inserting stays valid). Backfill in batched UPDATEs, in this order: (1) a row with a parent epic takes the epic's source (after the epics are backfilled); (2) `created_by_cron_id` set → `cron`; (3) `created_by_task_id` set → `agent`; (4) `console`. Index (`project_id`, `source`).
- New `app/task_source.py` is the single writer: `SOURCES`, `SOURCE_LABELS = {console: Console, cron: Schedule, agent: Agent, trigger:webhook: Webhook, trigger:github: GitHub, trigger:gmail: Email, trigger:slack: Slack}`, `trigger_source(kind)`, and `source_for(parent_epic, filer_source)` — a row with a parent epic takes `epic.source`, otherwise the filer's own.
- `routes/tasks.py::new_top_level_task` (~:7017) gains required keyword `source: str`, validated against `SOURCES`; every caller passes one (console create → `console`, cron runner → `cron` — the ONLY permitted cron-runner edit, agent `task_create` → `agent`). Every child-row creation path (BA/architecture/plan/coder/finish children, plan/finish filings under an epic) applies `source_for`.
- Task read gains `source` + `source_label`. `GET /api/tasks` gains repeated `source` query param, applied in `board_filtered_select` (~:8337) as `Task.source.in_(...)`, same as `created_by_id`. `GET /api/tasks/sources` serves the labels — the web never copies them by hand.
- Web: `apps/web/src/lib/taskUrlFilters.ts` + `components/tasks/shared.tsx`: URL param `source` (repeated) → wire param `source`; a multi-select showing the readable labels; counts in the toolbar filter badge; combines with existing filters.
## Acceptance (PRD, verbatim)
- **AC-17.1** `/tasks` on dev has a **Source** filter (multi-select, shown as readable labels: "Console", "Schedule", "Agent", "Webhook", "GitHub", "Email", "Slack"). It is reflected in the URL, and it combines with the existing filters. → verify: filter to "Webhook" and observe only trigger-filed Tasks. Reload the URL and observe the filter kept.
- **AC-17.3** Every pre-existing Task has a non-null source after migration. → verify: on the dev DB, `select count(*) from tasks where source is null` returns 0.
Note: no trigger-filed Task exists yet in this phase, so verify AC-17.1 with "Schedule" and "Console" (the Webhook end-to-end re-verify is owned by the Triggers-console child). The "Webhook" option must still render and return an empty state worded as a sentence, never a blank list.
Out of scope: Do NOT touch the `crons` table, cron health, or the cron runner except setting `source='cron'` at filing. Do NOT build any trigger table, route or UI (later children). PRD §5 (verbatim, binds): No deferred source in this epic (no GitLab, Jira, Linear, PagerDuty or CloudWatch receivers, no `jaah-handoff` skill or personal-access-token mechanism). No skill attachment. No budget enforcement. No GitHub reads that decide filing (ADR-0112). No public exposure beyond `/api/public/triggers/*` and `/api/public/email/gmail-push` on dev. Cron behaviour is unchanged. No new licence for any persona other than advisor. Slack: no Events API and no HTTP Slack ingress. Secrets: no webhook secret, bearer token or credential in any prompt, task body, log, digest, deliveries log or artifact. R16 (memory/last report) is dropped entirely.
Done means:
- AC-17.3 — run `select count(*) from tasks where source is null` on the dev DB; observe 0. Also a migration test showing each backfill rule (epic child inherits epic; cron; agent; console).
- AC-17.1 — on dev in a real browser: open `/tasks`, select "Schedule", observe only cron-filed Tasks; add a second label and combine with `creator`; reload the URL and observe the filter kept; observe readable labels (never raw `trigger:webhook` codes) and the badge count.
- A test that every `new_top_level_task` caller passes a valid `source`, and that an epic's BA/architecture/plan children carry the epic's source.
- A test that `GET /api/tasks?source=cron&source=console` filters server-side and `GET /api/tasks/sources` returns all seven labels.
Outcome
Added tasks.source (revision 0383 with backfill, every epic child inherits its epic's source), source filtering on /api/tasks, its counts and tag-counts, GET /api/tasks/sources, and the /tasks Source filter. Full gate green; PR #4670 merged. Dev-DB and dev-browser checks are still to run after deploy.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)