--- name: backlog-sync description: Work with the plans/ markdown backlog - understand the strict layout (Plan/Epic/Task hierarchy), parse frontmatter, sync to markdown_backlog_items, and maintain the DB ↔ .md contract. Use when creating or modifying backlog items, understanding the import process, or working with the markdown-backlog package. --- # Backlog Sync The `plans/` directory is the **source of truth** for product work. It's a strict markdown hierarchy parsed into the `markdown_backlog_items` database table. This skill teaches you the layout rules, frontmatter contract, and how the markdown ↔ DB sync works. ## When to Activate Use this skill when: - Creating or editing files under `plans/` - Working with `markdown_backlog_items` or `cursor_sync_mappings` tables - Running or debugging the markdown import (`pnpm --filter @tasks/database watch:markdown-backlog`) - Understanding how `agent_prompt` inheritance works (plan → epic → task) - Troubleshooting backlog sync issues - Implementing features that read/write backlog data ## Core Rules ### 1. Directory Structure is Strict The parser depends on this **exact** layout. Do not improvise. ``` plans/ Plan-/ Plan-.md ← Plan overview Epic-/ Epic-.md ← Epic definition Task-.md ← Executable work item Task-.md ``` **Key constraints:** - Slugs are lowercase-hyphenated (`feature-x`, not `Feature_X` or `featureX`) - Slugs are stable — renaming breaks Cursor sync mappings - One task per file - File names must match the pattern: `Plan-.md`, `Epic-.md`, `Task-.md` ### 2. Frontmatter Contract Every Plan/Epic/Task markdown file must start with YAML frontmatter. The parser extracts these fields: **Required across all kinds:** ```yaml --- kind: plan | epic | task slug: matches-the-filename-slug title: Human-readable title status: draft | ready | in_progress | blocked | done | cancelled priority: P0 | P1 | P2 | P3 tenant_id: UUID or "global" updated_at: "YYYY-MM-DD" --- ``` **Plan-specific additions:** ```yaml cursor_plan_id: null # populated by Cursor sync ``` **Epic-specific additions:** ```yaml plan_slug: parent-plan-slug cursor_epic_id: null ``` **Task-specific additions:** ```yaml plan_slug: parent-plan-slug epic_slug: parent-epic-slug owner: unassigned | github-username cursor_todo_id: null agent_prompt: | Optional multi-line prompt for agents working on this task. Inherits from epic → plan if not set here. ``` **What the parser does:** - Extracts frontmatter via regex: `/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/` - Parses YAML (tolerates syntax errors → empty object) - Infers `kind` from filename if frontmatter omits it (`Task-*.md` → `task`) - Infers `slug` from filename if frontmatter omits it (strips `Plan-`/`Epic-`/`Task-` prefix) - Infers `plan_slug` from path if frontmatter omits it (`plans/Plan-foo/...` → `foo`) - Infers `epic_slug` from path for tasks if frontmatter omits it (`Epic-bar/Task-x.md` → `bar`) - Computes `contentHash` (SHA-256 of full file) for idempotency - Stores `workflowPrompt` from `agent_prompt:` frontmatter field ### 3. Agent Prompt Inheritance Tasks can define `agent_prompt:` in frontmatter. If missing, the system inherits: ``` Task (no agent_prompt) → Epic (check agent_prompt) → Plan (check agent_prompt) → Default ``` The `resolveWorkflowPrompt` function in `packages/database/src/markdown-backlog/resolve-prompt.ts` implements this cascade. When an agent claims a task via MCP `claim_task`, the resolved prompt is returned as context. **Default prompt** (when nothing is set): ``` You are an AI assistant helping implement a software task. Follow the acceptance criteria and constraints in the task description. Run pnpm lint && pnpm type-check after code changes. ``` ### 4. Database Sync Flow **Import (Markdown → DB):** 1. Run `pnpm --filter @tasks/database watch:markdown-backlog` 2. Scanner walks `plans/` for `*.md` files matching the pattern 3. Parser extracts frontmatter + body + contentHash 4. Syncer upserts rows to `markdown_backlog_items` by `(workspace_id, repo_path)` unique key 5. Hierarchical `parent_id` links are computed (task → epic → plan) **Key columns in `markdown_backlog_items`:** | Column | Source | |--------|--------| | `workspace_id` | Env var `MARKDOWN_BACKLOG_WORKSPACE_ID` | | `repo_path` | Relative posix path: `plans/Plan-foo/Epic-bar/Task-baz.md` | | `kind` | Frontmatter `kind:` or inferred from filename | | `slug` | Frontmatter `slug:` or inferred from filename | | `title` | Frontmatter `title:` or first `# Heading` or filename | | `body` | Markdown body after frontmatter | | `content_hash` | SHA-256 of full file | | `parent_id` | Computed by matching `plan_slug`/`epic_slug` to parent rows | | `workflow_prompt` | Frontmatter `agent_prompt:` (null if not set) | **Export (DB → Markdown):** Not yet fully automated. The `exportBacklogItemToMarkdown` function can generate markdown from a DB row, but writes are manual or triggered via tRPC `backlog.createPlan/createEpic/createTask`. ### 5. Don't Break the Contract **What NOT to do:** - Don't nest tasks under other tasks (only Plan → Epic → Task) - Don't put secrets or API keys in frontmatter (files are committed) - Don't hand-edit `cursor_plan_id`, `cursor_epic_id`, `cursor_todo_id` (Cursor owns these) - Don't rename slugs without understanding the Cursor sync impact (breaks mappings) - Don't mix `snake_case` and `camelCase` in frontmatter keys (parser expects `plan_slug`, `epic_slug`, `agent_prompt`) - Don't create files outside the `plans/Plan-*/` structure (parser ignores them) **What TO do:** - Match existing style (see `docs/templates/plan-template.md`, `epic-template.md`, `task-template.md`) - Keep tasks **bead-scale** (one agent session can complete or materially advance) - Cross-link with relative paths: tasks link to epic via `./Epic-.md`, to plan via `../Plan-.md` - Use `tenant_id: global` for repo-wide work that's not tenant-specific - Run `pnpm --filter @tasks/database watch:markdown-backlog` after adding/editing files to sync ## Common Patterns ### Creating a New Task 1. Navigate to the Epic folder: `plans/Plan-/Epic-/` 2. Create `Task-.md` (lowercase-hyphenated slug) 3. Copy frontmatter template from `docs/templates/task-template.md` 4. Fill required fields: `kind: task`, `slug`, `title`, `status: draft`, `priority`, `plan_slug`, `epic_slug`, `tenant_id`, `updated_at` 5. Write task body (## Description, ## Acceptance criteria, etc.) 6. Run `pnpm --filter @tasks/database watch:markdown-backlog` to import 7. Verify row exists: `SELECT * FROM markdown_backlog_items WHERE slug = ''` ### Adding an Agent Prompt to a Task If the task needs custom context for agents: ```yaml --- kind: task slug: implement-feature-x # ... other required fields ... agent_prompt: | This task extends the existing authentication system. Key constraints: - Use NextAuth v5 (not v4) - Follow patterns in apps/web/lib/auth.ts - Add tests for new session logic --- ``` When an agent calls MCP `claim_task` with this task's `backlogItemId`, the prompt is returned and injected into the session context. ### Checking Sync Status ```bash # Watch mode (auto-imports on file change) pnpm --filter @tasks/database watch:markdown-backlog # One-shot import MARKDOWN_BACKLOG_WORKSPACE_ID= node packages/database/dist/markdown-backlog-cli.js ``` **What it logs:** - Created: new rows inserted - Updated: contentHash changed - Skipped: contentHash matches (idempotent) - Errors: parse failures, missing parent refs ### Understanding the Hierarchy via SQL ```sql -- Get full tree for a plan WITH RECURSIVE tree AS ( SELECT id, kind, slug, title, parent_id, 0 AS depth FROM markdown_backlog_items WHERE kind = 'plan' AND slug = 'automation-foundation' UNION ALL SELECT c.id, c.kind, c.slug, c.title, c.parent_id, p.depth + 1 FROM markdown_backlog_items c JOIN tree p ON c.parent_id = p.id ) SELECT kind, slug, title, depth FROM tree ORDER BY depth, slug; ``` ## Examples ### Example 1: Valid Task Frontmatter ```yaml --- kind: task slug: db-convoy-events-migration title: Drizzle convoy_events table and migration plan_slug: automation-foundation epic_slug: convoy-lifecycle-mcp status: done priority: P1 tenant_id: global owner: unassigned cursor_todo_id: null updated_at: "2026-08-12" --- # Task summary Add Drizzle schema + migration for convoy_events... ``` ### Example 2: Task with Agent Prompt ```yaml --- kind: task slug: trpc-backlog-tree title: tRPC backlog.list / tree / get for markdown_backlog_items plan_slug: automation-foundation epic_slug: plans-roadmap-ui status: ready priority: P1 tenant_id: global owner: unassigned cursor_todo_id: null updated_at: "2026-08-12" agent_prompt: | Extend apps/web backlog router: list/tree/get for markdown_backlog_items. Always filter by workspace_id. Zod inputs. No Plans UI yet — API only. Reuse existing schema. claim_task / complete_task; lint + type-check + test. --- ``` ### Example 3: Debugging Import Issues **Problem:** Task not appearing in DB after markdown edit. **Diagnosis:** 1. Check file structure: `ls plans/Plan-/Epic-/Task-.md` 2. Validate frontmatter YAML syntax (common issue: unbalanced quotes, missing colons) 3. Check watcher logs: `pnpm --filter @tasks/database watch:markdown-backlog` (look for parse errors) 4. Query DB: `SELECT * FROM markdown_backlog_items WHERE slug = '' AND workspace_id = ''` 5. Check `content_hash`: if it matches, sync is idempotent (no change detected) **Fix:** Correct frontmatter YAML, re-run import, verify row updated. ## References - **Parser source:** `packages/database/src/markdown-backlog/parse.ts` - **Sync logic:** `packages/database/src/markdown-backlog/sync.ts` - **Prompt resolution:** `packages/database/src/markdown-backlog/resolve-prompt.ts` - **Templates:** `docs/templates/plan-template.md`, `epic-template.md`, `task-template.md` - **Glossary:** `docs/Glossary.md` (backlog hierarchy, sync vocabulary) - **Cursor sync notes:** `config/CursorSync.md` (push/pull behavior, mappings) - **Schema:** `packages/database/src/schema/markdown_backlog_items.ts` ## Related Skills - `convoy-lifecycle` — when backlog tasks map to convoy briefs - `model-routing` — how agent_prompt + model_policy interact - `code-review-two-axis` — reviewing changes to backlog sync code