Until now the markdown importer was one-way (plans/*.md → DB). Any
agent-driven status flip via claim_task / complete_task would be
clobbered on the next importer sweep. This change closes the loop: the
DB now projects status, priority, agent_prompt, and updated_at back
into the file's frontmatter, preserving body bytes, key order, and
every other frontmatter key.
New: packages/database/src/markdown-backlog/export.ts
- `rewriteFrontmatter()` — pure function, covered by 10 Vitest cases
(round-trip identity, status flip, priority flip, agent_prompt
null/block-scalar/single-line variants, body preservation,
trailing-newline preservation, idempotent re-application).
- `exportBacklogItemToMarkdown()` — DB-loading wrapper with atomic
write (tmp + rename) and tenant fencing. Returns a structured
result so callers can surface what happened in their response.
Wired into:
- `claim_task` MCP tool — exports on the ready → in_progress flip.
- `complete_task` MCP tool — exports on any finalStatus transition.
- `backlog.updateWorkflowPrompt` tRPC mutation — exports on prompt
edits made through the app UI.
Robust repo-root resolution (`apps/{mcp-server,web}/src/lib/repo-root.ts`,
plus a copy in `import-markdown-backlog.ts`): walk up from the source
file looking for `pnpm-workspace.yaml`, falling back to env var or cwd.
This fixes a class of bug where `pnpm --filter <pkg>` cd's into the
package directory and breaks naive cwd-based path resolution — the
importer was deleting all 44 rows during smoke testing before this fix
because it found zero files in `packages/database/plans/`.
`config/CursorSync.md`: documents the new two-way contract, the
DB-wins-on-allow-list conflict policy, and the
MARKDOWN_BACKLOG_REPO_ROOT=off escape hatch for production deployments
where `plans/` isn't checked out.
Smoke verified end-to-end against the homelab DB: claim flips file
status to in_progress, complete flips it back to ready, importer
round-trips with stable content_hash (true no-op), agent identity
preserved throughout.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds level-B agent attribution: every MCP tool call can identify itself
with a `client` (e.g. cursor-ide, codex, echodo-orchestrator) and `model`
(e.g. claude-4.6-sonnet) string. Both optional, both stored in
agent_runs.metadata as JSONB so the schema doesn't move.
- claim_task: new optional `client` + `model` args. Written into the
inserted agent_runs row's metadata, and mirrored into the audit log
entry. On idempotent re-claim, incoming values are merged into
existing metadata (existing keys override only when explicit), so a
mid-session model switch updates attribution without losing earlier
context.
- complete_task: same optional args, with merge-on-close semantics —
late-bound values override the earlier claim's values so the run row
reflects whichever model actually closed the session. Audit row also
carries the merged identity.
- /settings/runs UI: stack the client/model under the actor name in the
table so attribution is visible at a glance without adding a column.
Smoke-tested: claim with {cursor-ide, claude-4.6-sonnet} then complete
with only model={claude-4.6-sonnet-medium-thinking} yields final
metadata {client: cursor-ide, model: claude-4.6-sonnet-medium-thinking}
on both the run row and the task.completed audit entry.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pairs with the parallel complete_task commit. claim_task opens an
agent_runs row, flips status to in_progress only when it's safe
(ready/draft → in_progress, never overwriting a deliberate
blocked/done/in_progress), and returns the resolved workflow prompt
+ source level. Idempotent re-claim by the same actor returns the
existing run with reused=true and refreshes notes only — started_at
is sacred. Different-actor re-claim errors with ALREADY_CLAIMED
naming the existing actor and run id.
Tenancy fence: if the backlog item exists but in a different workspace
than the resolved handle, we refuse with "doesn't belong to workspace"
rather than 404. Prevents cross-tenant existence fishing.
All three writes (run insert + status flip + audit insert) happen in
one db.transaction() so a partial claim is unreachable.
tools/index.ts now registers both claim_task and complete_task.
Co-authored-by: Cursor <cursoragent@cursor.com>
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>