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 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>
First Path-B task. Path A landed daily-driver features without a
test runner; Path B is "harden so the next batch of changes can't
silently regress what just shipped." Step 1 is making `pnpm test`
real and gating CI on it.
Test runner:
* Install vitest + @vitest/coverage-v8 at the workspace root.
* Add vitest.config.ts (environment: "node", no JSDOM) + test /
test:watch scripts to packages/shared, packages/database,
packages/ai. Wire `test` into turbo.json with dependsOn: ^build
for future-proofing; add `pnpm test` to root package.json.
Three real tests (no snapshot theater — verified by mutation):
* packages/shared/src/utils/id.test.ts: asserts generateId() matches
the RFC 4122 v4 regex and produces 1000 distinct values. Mutating
generateId() to a constant fails both assertions.
* packages/shared/src/types/objects.test.ts: pins objectTypes and
objectStatuses arrays. These back the zod enum on objects.create
and the "open tasks" count on the workspace-home dashboard; a
silent reorder/rename would otherwise corrupt the dashboard math.
* packages/database/src/markdown-backlog/parse.test.ts: covers
parseBacklogMarkdown across three shapes (well-formed Task,
no-frontmatter Plan with path inference, malformed YAML that
must NOT throw — the importer runs in a file watcher). Plus
hashFileContents determinism.
* packages/ai/src/actions/index.test.ts: five tests across the
prompt builders (summarize/expand/rewrite × 3 tones / translate /
generateFromPrompt). Pure functions; no model mocking needed.
CI:
* .github/workflows/ci.yml runs on pull_request and push to main.
Node 20, pnpm 9 pinned explicitly (per AGENTS.md). Uses
setup-node's built-in pnpm cache. Steps: install --frozen-lockfile,
lint, type-check, test. Concurrency group cancels superseded runs
on non-main branches.
Docs:
* AGENTS.md: drop the "no test runner configured" disclaimer.
Document pnpm test / test:watch. Update the PR-readiness rule
from `pnpm lint && pnpm type-check` to
`pnpm lint && pnpm type-check && pnpm test`.
All 14 tests pass; lint + type-check still green across all 6
packages. The CI workflow's first run is gated on the operator
pushing this branch — that's the only acceptance criterion left
unverified in this commit.
Closes plans/Plan-multitenant-saas-hardening/Epic-test-foundation/
Task-bootstrap-vitest-and-ci.md.
Co-authored-by: Cursor <cursoragent@cursor.com>