feat(desktop,service,database): start a task from an issue and list it on the issue's page

Merged
DA
11d ago
+14446-82

Summary

This is the first of three PRs for "Link issues to tasks and agents" in the Improve Issues board plan: the link itself and Start task…. Start agent… and the move to In progress come in (b), and PR state on the card in (c).

  • Database: task.issue_id is a nullable reference to issue(id), ON DELETE SET NULL, with an index task_issue. A task links at most one issue, and an issue can have many tasks. Moving a task to another workspace clears its link, because an issue belongs to its workspace. StoredTask carries the issue as it reads now: number, title, description, and the workspace's name and slug.
  • Wire: taskCreateRequest gains an optional issueId, and TaskSummary gains an optional issueId.
  • Service: POST /plans with issueId reads the issue as the account sees it. If the account can't read it, or it belongs to another workspace than the one the task starts in, the request is refused as not-found and nothing is written. The window shows "That issue is gone, or it belongs to another workspace."
  • Brief and brain: an agent handed a plan started from an issue reads it after the plan, under "## The issue this task was started from". It gets the identifier, the title, a plain Luke link (https://tryluke.dev/<ws>/issue/<id>, since issue links aren't a mention kind), and the description fenced as data in <issue-description>. The Linear block and this block now share one shape (startingIssueBrief); the Linear block's text is unchanged. The brain's standing context gets a [workspace issue the task was started from] JSON line beside the Linear one.
  • Window:
    • The issue page's ⋯ menu and each card's or row's menu offer Start task…. It opens New task on the issue: titled "New task from ACME-7", named by the issue's title, fixed to the issue's workspace, with no Linear search.
    • The issue page lists the tasks started from it under its status and priority, one pill per task, which opens that task. The list comes from the task list the window already holds (every task in the reader's workspaces), so there's no new query.

Migration notes

20261018164541_task_issue_link:

  • The column is added with ADD COLUMN IF NOT EXISTS.
  • The index is built with CREATE INDEX IF NOT EXISTS inside the migration, with no manual CONCURRENTLY step. Production's task table held 161 rows (152 kB) when this was written (a read-only count on Neon), so the index scan is shorter than a request.
  • The FK is added by drop-if-exists and then add with NOT VALID, followed by VALIDATE CONSTRAINT. The validate step takes a lock that lets writes through.
  • There is no backfill.

Evidence

  • pnpm check:affected: lint, knip, discarded-effect, and the affected tests all passed. Three tasks failed in this sandbox for reasons outside this change, and each passed when re-run alone:
    • @luke/desktop#typecheck and @luke/service#typecheck were killed for running out of memory (exit 137) in the 8 GB sandbox.
    • discarded-effect ran out of heap.
    • apps/web/tests/build-output.test.ts hit its 180 s timeout under load. It passed in the previous full run.
  • New tests, each failing without this change:
    • packages/database/src/models/__tests__/task-store.test.ts: a task started from an issue names it to every member and to no account outside the workspace (list and read). A move to another workspace lets it go. A deleted issue lets it go and the task stays.
    • apps/web/tests/tasks-app.test.ts: a start from an issue of the workspace links it. A start from an issue of a workspace the account isn't in, or of another of its own workspaces, is refused as not found and starts no task.
    • packages/service/src/agents/coder-host/coder-start.test.ts: the agent's brief carries the issue as it reads at the start, with its identifier and link, and with the fence escaped.
    • packages/service/src/agents/brain-host/brain-host-standing-context.test.ts: the brain's context carries the issue as it reads now.
    • apps/desktop/src/renderer/desktop/issues-page.test.ts: Start task… from the issue's ⋯ and from a card's menu; the issue page lists only its own tasks and opens one; an issue with none draws no row.
    • apps/desktop/src/renderer/tasks/new-task-dialog.test.ts: opened on an issue, the card says so, is named by the issue, starts in its workspace, searches no Linear, and sends issueId.
  • JSON Schema goldens updated with LUKE_UPDATE_FIXTURES=1:
    • task-wire-taskCreateRequestSchema gains the optional issueId.
    • task-wire-taskSummarySchema, taskSchema, taskListAnswerSchema, and taskAnswerSchema gain the optional issueId on the task.
    • plan-edit-planEditAnswerSchema carries the task, so it gains the same field.
  • apps/web/tests/inbox-item-read-migration.test.ts now writes its task with SQL, as the schema of then holds it. Today's store writes issue_id, which doesn't exist before this migration.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens: diff explained above.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.runPromise/runSync/runFork outside the edges.
  • No new raw async primitives.
  • No discarded Effect.
  • Test count rises by the 11 tests listed above.
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • Package deps unchanged.
  • Barrel/door rule unchanged.

Notes

  • This changes what the hosted service builds, so it needs a ./scripts/run.sh --preview run on a Mac before merge. Don't arm auto-merge until that run is done.
  • Agents started with no task (automations) can't link an issue.

Made with Luke


Devin Review


Summary by cubic

Tasks can now be started from an issue and stay linked to it. The issue's page, and its card's or row's menu, offers Start task…, which opens New task named by the issue's title and fixed to its workspace, and the issue's page lists the tasks started from it, archived ones included, newest started first whatever the sidebar's pins put first. Previously a task could only reference a Linear issue, not the workspace's own issues.

Migration

  • task gains a nullable issue_id referencing issue with ON DELETE SET NULL, cleared when the issue is deleted or the task moves to another workspace.
  • The FK is added unvalidated then validated, so no writes are held back during the scan; no backfill.

Service and wire

  • POST /plans with issueId refuses as not found, writing nothing, when the account can't read the issue or it isn't in the task's workspace; the window says "That issue is gone, or it belongs to another workspace."
  • TaskSummary and the create request carry the optional issueId, and agents and the brain read the issue as it stands, with its identifier and Luke link.