In-flight work from 2026-06-05 that was sitting uncommitted on feat/agent-pipeline-bridge for 10 days. Moved here to a clean branch off main so the bridge branch can stay at its committed state (the DEFERRED Phase 2a snapshot, see stwl-labs/ubiquitous-invention#3). Touched areas: - apps/web/app/(app)/[workspaceSlug]/settings/runs/page.tsx — runs page UI iteration - apps/web/server/routers/{backlog,runs}.ts — router updates - apps/web/app/(app)/[workspaceSlug]/plans/[planSlug]/[epicSlug]/[taskSlug]/page.tsx — new deep-link route - apps/web/components/backlog/workflow-prompt-section.tsx — new component - config/CursorSync.md — coordination doc tweak - docs/operations/README.md + 2026-06-05-db-drift-reconcile-oauth-tables.sql — operations ops note + reconcile script - plans/Plan-agent-coordination/Epic-task-as-runnable-unit/Task-{workflow-prompt-task-detail-ui,deep-link-runs-to-task-detail}.md — 2 task specs This is a working-tree snapshot, not a finished PR. Rebase, split, or amend as needed when picking it back up. Co-authored-by: Cursor <cursoragent@cursor.com>
7.9 KiB
Cursor sync configuration
This document describes how the application and Cursor should keep plans, epics, and tasks aligned. Implementation is incremental; treat this as the contract the data layer and jobs will follow.
Goals
- App → Cursor: Tasks and status updates in the app appear as Cursor to-dos / plan items where configured.
- Cursor → App: To-dos created or completed in Cursor are mirrored into the correct plan/epic in the app.
- Markdown as source of truth (optional mode): Repo markdown can be authoritative; the app imports on change, or the app exports on change—policy is per tenant (see Modes).
Multitenancy
- Each tenant has its own:
- API credentials or OAuth connection to Cursor (when available).
- Mapping table: internal plan/epic/task id ↔ Cursor identifiers ↔ filesystem paths under
plans/.
- No cross-tenant sync or shared Cursor workspace.
Modes (to implement)
| Mode | Behavior |
|---|---|
markdown_authoritative |
Watch plans/**; import on save; push summaries to Cursor. |
app_authoritative |
UI/API edits win; export markdown + update Cursor on commit or interval. |
bidirectional |
Reconcile by updated_at and explicit conflict rules (last-write-wins per field or manual resolution). |
Two-way markdown contract (current implementation)
The repo today operates in a hybrid markdown / DB authoritative mode that approximates bidirectional for the small set of fields agents touch. The contract is:
| Direction | Trigger | Code path | Fields it can rewrite |
|---|---|---|---|
plans/**.md → DB |
pnpm --filter @tasks/database import:markdown-backlog (one-shot) or the optional watch:markdown-backlog script |
packages/database/src/markdown-backlog/sync.ts |
All frontmatter fields plus body bytes. The importer overwrites the DB row from the file. |
DB → plans/**.md |
claim_task (status flip only), complete_task (always when finalStatus is set), backlog.updateWorkflowPrompt tRPC mutation |
packages/database/src/markdown-backlog/export.ts |
A tight allow-list: status, priority, agent_prompt, updated_at. Body bytes and all other frontmatter keys are preserved verbatim. |
Conflict policy: DB wins on the allow-list, file wins on everything else
If a human edits status: ready → status: blocked in the .md between two MCP calls, the next complete_task will rewrite it (DB → file). If they edit the body, the title, the tenant_id, or any other frontmatter key, the exporter leaves their change in place. The next import:markdown-backlog run will then pull those file-side changes back into the DB.
This is a single-operator contract. In a multi-operator setup you'd need a true reconciliation policy with conflict detection (see Task-export-db-to-markdown-frontmatter for the deferred design notes); for now it's enough that any one operator picks "edit the file" OR "drive the app" per session.
Idempotence guarantees the exporter must keep
- No-diff re-export is a no-op. Running the export with desired state equal to current file state must produce the original bytes byte-for-byte. The pure rewriter in
export.tsis covered by 10 Vitest cases for this. updated_atonly bumps when something else changes. A "phantom" rewrite that bumpsupdated_atand nothing else is treated as a bug.- Atomic writes. Write to
.tmpnext to the target file, thenrename(). On POSIX inside the same directory this is atomic — partial reads are impossible. - Block-literal preservation for
agent_prompt. Multi-line prompts getagent_prompt: |so diffs stay readable. Single-line stays plain.
How to disable the export side
Set MARKDOWN_BACKLOG_REPO_ROOT=off (or 0, or empty string) in the environment of the MCP server or the Next dev server. Useful when:
- You're running the app in production (Coolify) where
plans/isn't checked out. - You're testing a destructive change and want to confirm the file write is the cause without rolling back.
When unset, the exporter defaults to process.cwd() — which works in dev because Cursor spawns the MCP from the repo root.
MCP server lifecycle (dev gotcha)
The MCP server is spawned once per Cursor chat session via .cursor/mcp.json as pnpm -s --filter @tasks/mcp-server mcp, which runs tsx --env-file=../../.env src/index.ts. It is a long-lived stdio process for the duration of the chat. It does not auto-reload on source changes.
Consequences when iterating on agent-side code:
- Editing any file under
apps/mcp-server/**or any imported library used by a tool (e.g.packages/database/src/markdown-backlog/export.ts,packages/database/src/schema/**,packages/database/src/client.ts) has no effect on tool calls until the server is restarted and Cursor reconnects. - A new Cursor chat does not always spawn a new server. Cursor may bind to an existing stale process, which means you can end up talking to code committed before that process started. Multiple stale servers from prior sessions can accumulate. Symptom: a tool that should return a new field returns the old shape, or status flips happen in the DB but the markdown file doesn't get re-exported.
How to recover
# 1. Find stale MCP servers (each pair is pnpm wrapper + tsx child)
pgrep -fa "apps/mcp-server.*src/index.ts"
# 2. Kill them
pkill -f "apps/mcp-server.*src/index.ts"
# 3. Open a new Cursor chat. Cursor will spawn a fresh server against current source.
When debugging "did the tool definitely run against the new code?", a useful check is whether the tool's response shape includes a field that only exists in the new version (e.g. the export field on claim_task / complete_task was added in commit 56b697b). Missing field = stale server.
Why we don't run tsx watch
A watch-mode restart would drop the stdio JSON-RPC connection mid-call, which the MCP client (Cursor) wouldn't gracefully recover from. A real fix would need a graceful-reload protocol or out-of-process tool execution. Documented gotcha is the cheap, correct first step.
API surface (target)
Lightweight endpoints or jobs (names indicative):
POST /api/v1/tenants/:tenantId/plans— create plan + optional seed markdown paths.GET/PATCH /api/v1/tenants/:tenantId/plans/:planId— read/update metadata and Cursor mapping.GET/PATCH /api/v1/.../epics/:epicId,.../tasks/:taskId— same for epics and tasks.POST /api/v1/tenants/:tenantId/sync/cursor/pull— ingest Cursor to-dos into tasks.POST /api/v1/tenants/:tenantId/sync/cursor/push— export task state to Cursor.- Webhook receiver (future):
POST /webhooks/cursorfor push notifications when Cursor exposes them; until then polling on a tenant schedule.
Mapping record (logical schema)
Implemented in Postgres as markdown_backlog_items plus cursor_sync_mappings (see docs/Glossary.md). Logical fields:
tenant_id→markdown_backlog_items.workspace_id(workspace object UUID)plan_slug,epic_slug,slug(filesystem / frontmatter alignment)cursor_plan_id/cursor_item_id→cursor_sync_mappings(nullable until connected)last_pulled_at,last_pushed_at,sync_content_hashon the mapping rowcontent_hashon the backlog row (file body hash for import idempotency)
Environment variables (placeholder)
Document only; wire in app config when implementing.
| Variable | Purpose |
|---|---|
CURSOR_SYNC_ENABLED |
true / false per environment. |
CURSOR_SYNC_POLL_INTERVAL_SEC |
Polling fallback interval. |
CURSOR_API_BASE_URL |
When a stable API exists for your integration tier. |
CURSOR_WEBHOOK_SECRET |
Verify inbound webhooks. |
Security
- Store tokens in tenant-scoped secrets (env, vault, or DB encrypted column)—never in markdown.
- Audit log for every push/pull with actor (user id or system job).
References
- Backlog layout:
plans/README.md - Terminology:
docs/Glossary.md - Templates:
docs/templates/