ubiquitous-invention/plans/Plan-agent-coordination/Epic-task-as-runnable-unit/Task-add-workflow-prompt-to-backlog-items.md
Randall Stillwell 72aa2a5f0c feat(backlog): workflow_prompt with task → epic → plan inheritance
Adds the data layer for per-item agent prompts. Markdown frontmatter
gets an `agent_prompt:` block scalar that survives the importer
round-trip (newlines preserved), and `resolveWorkflowPrompt()` walks
task → epic → plan → built-in default returning both the resolved
string and the source level. Walk is slug-based, not parent_id-based,
because the importer leaves parent_id briefly null mid-transaction.

tRPC `backlog.getWorkflowPrompt` returns ownOverride + effectivePrompt
so future UI can render the override box + preview without two
queries. `backlog.updateWorkflowPrompt` is owner/admin-gated (prompts
change downstream Cursor/Claude behavior) and audit-logged on every
write.

UI deferred — apps/web doesn't have a backlog-item detail panel yet;
the existing object-detail panel is for the objects table. Follow-up
filed at Task-workflow-prompt-task-detail-ui.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 22:18:18 -05:00

6.5 KiB

kind slug title plan_slug epic_slug status priority tenant_id owner cursor_todo_id updated_at
task add-workflow-prompt-to-backlog-items Add workflow_prompt column to markdown_backlog_items, with inheritance agent-coordination task-as-runnable-unit in_progress P2 global unassigned null 2026-06-02

Task summary

Backlog items need a place to store a per-item agent prompt. Add a workflow_prompt text column to markdown_backlog_items, parse it from frontmatter, and define an inheritance chain (task → epic → plan → workspace default).

Description

Frontmatter contract

Add an optional agent_prompt: field to the YAML frontmatter of Plan/Epic/Task templates. It's a multiline string (use | block scalar in YAML). Example:

agent_prompt: |
  You are working on a focused implementation task in a multitenant TypeScript monorepo.
  Read the task body in full before writing any code.
  Validate every external input with zod.
  Run pnpm lint && pnpm type-check before declaring done.  

Update docs/templates/{plan,epic,task}-template.md to include the field (commented-out for tasks, since most tasks will inherit).

Schema

Add to markdown_backlog_items:

  • workflow_prompt text null

No new index needed; this column is rarely filtered on.

Parser

In packages/database/src/markdown-backlog/parse.ts, extract agent_prompt from the parsed frontmatter into workflow_prompt on the upserted row. Use the existing zod schema for backlog frontmatter — extend it; don't fork.

Inheritance helper

A new function resolveWorkflowPrompt(db, { workspaceId, backlogItemId }) that walks task → epic → plan and returns the first non-null workflow_prompt. If none exist, return a built-in default string (lives as a constant in packages/database/src/markdown-backlog/). Workspace-level overrides can come later.

Don't store the resolved prompt anywhere — always compute on read. Caching is premature; this is a small lookup.

Minimal UI

Add a "Workflow prompt" section to the task detail panel (apps/web/components/panels/object-detail.tsx or wherever the backlog-item detail lives). Show:

  • "Effective prompt" — the result of resolveWorkflowPrompt (read-only).
  • "Override" — a textarea bound to the item's own workflow_prompt. Empty = inherit.

Use a tRPC procedure backlog.updateWorkflowPrompt({ backlogItemId, workflowPrompt }) that re-renders the panel on success.

Anti-goals

  • Don't write a template engine for the prompt. Symphony uses Liquid; we don't need that yet. A plain string is fine.
  • Don't store prompts in objects. The agent flow is anchored on the markdown backlog, not the generic objects table.

Subtasks

  • No zod schema in parse.ts to extend (the parser uses plain readString helpers, not a zod schema). Added a sibling readMultilineString helper that preserves interior newlines for | block scalars and added workflowPrompt to ParsedBacklogFile.
  • Added workflow_prompt text column to markdown_backlog_items. Migration 0008_curly_zzzax.sql. Applied via psql against CT 102.
  • Parse agent_prompt from frontmatter, plumb through the importer (sync.ts insert + onConflictDoUpdate). Whitespace-only values normalize to null so an accidentally-blanked prompt doesn't silently shadow the epic/plan default.
  • resolveWorkflowPrompt(db, { workspaceId, backlogItemId }) lives at packages/database/src/markdown-backlog/resolve-prompt.ts. Returns { prompt, source: "task" | "epic" | "plan" | "default" }. Inheritance walk goes task → epic (slug-based lookup, NOT parent_id, because parent_id can be briefly null during importer transactions) → plan → built-in DEFAULT_AGENT_PROMPT. Workspace-scoped at every hop.
  • Updated Plan / Epic / Task templates in docs/templates/ with commented-out agent_prompt: examples. Task template leaves it commented (most tasks inherit); Epic and Plan templates suggest filling it.
  • DEFERRED to follow-up Task-workflow-prompt-task-detail-ui.md. There is no backlog-item detail panel in apps/web yet — the existing components/panels/object-detail.tsx is for the objects table, not for markdown_backlog_items. Adding an entirely new panel surface (with state, edit affordance, etc.) is a bigger UI task than this convoy bears. The data layer is fully in place — the follow-up just needs to render it.

The tRPC procedures are also ready (backlog.getWorkflowPrompt and backlog.updateWorkflowPrompt) so the future UI can render the override + effective preview with zero additional server work.

Design decisions captured

  • No template engine. Plain string, per spec. Symphony uses Liquid; we don't need that.
  • Slug-based walk, not parent_id-based. The importer sets parent_id only for the immediate parent (task → epic). Walking by (planSlug, epicSlug, slug) works even mid-transaction when parent_id is null, and survives importer re-ordering.
  • DEFAULT_AGENT_PROMPT is intentionally generic. The spec said "if you find yourself editing it frequently, that's a smell — fill in plan-level prompts instead." Documented in the constant's JSDoc.
  • Owner / admin only on updateWorkflowPrompt. Agent prompts change downstream Cursor/Claude behavior; this isn't a "any member can edit" surface. Audit-logged on every write (backlog.workflow_prompt_set / backlog.workflow_prompt_clear).
  • Empty string normalizes to null. A literally-empty override would shadow the epic/plan default with "no instructions at all" — bad UX. The procedure trims and treats empty as "clear the override."

Owner or assignee

Unassigned

Status

ready

Estimation

M

Acceptance criteria

  • A task with no override falls back to its epic's prompt; an epic with no override falls back to its plan; a plan with no override falls back to the built-in default. (Tested by inspection of resolve-prompt.ts; an end-to-end DB-fixture test belongs to Task-bootstrap-vitest-for-apps-web once that lands.)
  • Setting agent_prompt: in frontmatter and re-importing populates workflow_prompt. (parse.ts + sync.ts plumb the field; verified by 3 new vitest cases in parse.test.ts.)
  • UI shows effective prompt and override box. Deferred — see follow-up Task-workflow-prompt-task-detail-ui.md. Data layer (tRPC backlog.getWorkflowPrompt and backlog.updateWorkflowPrompt) is shipped so the UI is a pure rendering task.
  • Epic: ./Epic-task-as-runnable-unit.md
  • Plan: ../Plan-agent-coordination.md