ubiquitous-invention/config/CursorSync.md
Randall Stillwell 6581f6a482 wip: agent-coordination iteration (runs+backlog+plans UI + 2 task plans + db-drift SQL)
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>
2026-06-15 15:35:39 -05:00

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

  1. App → Cursor: Tasks and status updates in the app appear as Cursor to-dos / plan items where configured.
  2. Cursor → App: To-dos created or completed in Cursor are mirrored into the correct plan/epic in the app.
  3. 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: readystatus: 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.ts is covered by 10 Vitest cases for this.
  • updated_at only bumps when something else changes. A "phantom" rewrite that bumps updated_at and nothing else is treated as a bug.
  • Atomic writes. Write to .tmp next to the target file, then rename(). On POSIX inside the same directory this is atomic — partial reads are impossible.
  • Block-literal preservation for agent_prompt. Multi-line prompts get agent_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/cursor for 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_idmarkdown_backlog_items.workspace_id (workspace object UUID)
  • plan_slug, epic_slug, slug (filesystem / frontmatter alignment)
  • cursor_plan_id / cursor_item_idcursor_sync_mappings (nullable until connected)
  • last_pulled_at, last_pushed_at, sync_content_hash on the mapping row
  • content_hash on 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/