fix(FN-7952): migrate bundled plugins to PostgreSQL#2111
Conversation
|
Warning Review limit reached
Next review available in: 16 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (23)
📝 WalkthroughWalkthroughPostgreSQL becomes the runtime persistence authority. Plugin-owned schemas and stores now enforce project isolation, legacy plugin state is migrated into central PostgreSQL tables, plugin schema execution is transactional, and bundled plugins adopt async PostgreSQL persistence with updated tests, tools, and documentation. ChangesPostgreSQL runtime cutover
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR moves bundled plugin runtime state to project-scoped PostgreSQL storage. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (7): Last reviewed commit: "fix(FN-7952): fail closed across migrati..." | Re-trigger Greptile |
4cf7da9 to
bff151c
Compare
## Summary Operators and contributors now have one current contract for the completed PostgreSQL cutover: PostgreSQL is mandatory runtime authority, legacy SQLite files are read-only migration evidence, and rollback is restore-based rather than a fallback toggle. The major changeset communicates the same boundary to published CLI users. ## Operational guidance - Documents the authoritative storage inventory and the five intentional legacy SQLite readers. - Defines maintenance-window migration, backup, restore-test, ownership, and isolation checks. - Clarifies lifecycle ownership for CLI, desktop, dashboard, engine, and bundled plugins. - Supersedes the earlier readiness review without deleting its historical record. ## Validation - Changeset format check passes. - `pnpm test:gate` passes all 478 gate tests on the completed stack. - Every file from the original oversized PR is represented in the replacement stack; this PR changes 22 files. ## Stack - Depends on #2111 → #2110 → #2109 → #2108. - Merge bottom-up in that order. Related: #2105
## Summary Operators and contributors now have one current contract for the completed PostgreSQL cutover: PostgreSQL is mandatory runtime authority, legacy SQLite files are read-only migration evidence, and rollback is restore-based rather than a fallback toggle. The major changeset communicates the same boundary to published CLI users. ## Operational guidance - Documents the authoritative storage inventory and the five intentional legacy SQLite readers. - Defines maintenance-window migration, backup, restore-test, ownership, and isolation checks. - Clarifies lifecycle ownership for CLI, desktop, dashboard, engine, and bundled plugins. - Supersedes the earlier readiness review without deleting its historical record. ## Validation - Changeset format check passes. - `pnpm test:gate` passes all 478 gate tests on the completed stack. - Every file from the original oversized PR is represented in the replacement stack; this PR changes 22 files. ## Stack - Depends on #2111 → #2110 → #2109 → #2108. - Merge bottom-up in that order. Related: #2105
f43af48 to
b8b1f49
Compare
Move bundled plugin persistence to project-scoped PostgreSQL contracts and keep plugin SDK/package dependencies aligned with the new runtime. Fusion-Task-Id: FN-7952
Fusion-Task-Id: FN-7952
Fusion-Task-Id: FN-7952
## Summary Operators and contributors now have one current contract for the completed PostgreSQL cutover: PostgreSQL is mandatory runtime authority, legacy SQLite files are read-only migration evidence, and rollback is restore-based rather than a fallback toggle. The major changeset communicates the same boundary to published CLI users. ## Operational guidance - Documents the authoritative storage inventory and the five intentional legacy SQLite readers. - Defines maintenance-window migration, backup, restore-test, ownership, and isolation checks. - Clarifies lifecycle ownership for CLI, desktop, dashboard, engine, and bundled plugins. - Supersedes the earlier readiness review without deleting its historical record. ## Validation - Changeset format check passes. - `pnpm test:gate` passes all 478 gate tests on the completed stack. - Every file from the original oversized PR is represented in the replacement stack; this PR changes 22 files. ## Stack - Depends on #2111 → #2110 → #2109 → #2108. - Merge bottom-up in that order. Related: #2105
b8b1f49 to
058e891
Compare
Keep PluginLoader as the single schema-contract owner so CLI and desktop hosts do not replay PostgreSQL transactions after onLoad preparation. Fusion-Task-Id: FN-7952
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
plugins/fusion-plugin-compound-engineering/src/sync/pipeline-store.ts (1)
351-377: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winRace condition in async state upsert (TOCTOU).
The conversion from synchronous SQLite to async PostgreSQL introduces a Time-of-Check to Time-of-Use (TOCTOU) race condition here. Because
await this.getStateAsync(input.cePipelineId)yields the event loop, concurrent async calls toupsertStateAsyncfor the same pipeline ID can both seeexistingasundefinedand attempt toinsertthe same record, crashing the request with a unique constraint violation.While the read-modify-write pattern was practically safe in the synchronous SQLite path due to avoiding event-loop yields, you can solve this in the async PostgreSQL path by switching to a single atomic
INSERT ... ON CONFLICT DO UPDATEusing Drizzle, which also allows you to skip thegetStateAsyncquery completely.🛠️ Proposed fix using Drizzle's `onConflictDoUpdate`
By conditionally applying the updates in the
setobject, you can preserve the existing row values automatically if the caller omits them:async upsertStateAsync(input: UpsertCePipelineStateInput): Promise<CePipelineState> { if (!this.asyncLayer) return this.upsertState(input); const now = new Date().toISOString(); - const existing = await this.getStateAsync(input.cePipelineId); - const status = input.status ?? existing?.status ?? "running"; - const lastArtifactPath = - input.lastArtifactPath !== undefined ? input.lastArtifactPath : existing?.lastArtifactPath ?? null; - if (existing) { - await this.dbAsync().update(cePipelineStateTable).set({ - currentStage: input.currentStage, - status, - lastArtifactPath, - updatedAt: now, - }).where(and(eq(cePipelineStateTable.projectId, this.projectId()), eq(cePipelineStateTable.cePipelineId, input.cePipelineId))); - } else { - await this.dbAsync().insert(cePipelineStateTable).values({ - projectId: this.projectId(), - cePipelineId: input.cePipelineId, - currentStage: input.currentStage, - status, - lastArtifactPath, - createdAt: now, - updatedAt: now, - }); - } + + await this.dbAsync().insert(cePipelineStateTable).values({ + projectId: this.projectId(), + cePipelineId: input.cePipelineId, + currentStage: input.currentStage, + status: input.status ?? "running", + lastArtifactPath: input.lastArtifactPath ?? null, + createdAt: now, + updatedAt: now, + }).onConflictDoUpdate({ + // Target your unique constraint/PK combination here + target: [cePipelineStateTable.projectId, cePipelineStateTable.cePipelineId], + set: { + currentStage: input.currentStage, + ...(input.status !== undefined ? { status: input.status } : {}), + ...(input.lastArtifactPath !== undefined ? { lastArtifactPath: input.lastArtifactPath } : {}), + updatedAt: now, + } + }); + return (await this.getStateAsync(input.cePipelineId))!; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/fusion-plugin-compound-engineering/src/sync/pipeline-store.ts` around lines 351 - 377, Replace the read-then-insert/update flow in upsertStateAsync with a single atomic insert using Drizzle’s onConflictDoUpdate for the projectId and cePipelineId key. Preserve existing status and lastArtifactPath values when the corresponding input fields are omitted, while updating supplied fields and timestamps, then retain the final getStateAsync return.packages/core/src/postgres/schema/plugin.ts (1)
132-175: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftDuplicate Drizzle table definitions for
ce_pipeline_links/ce_pipeline_state/ce_pipeline_sync_queueacross two files. The same three tables (columns, composite primary keys, foreign keys, indexes) are hand-maintained in both the canonical migration schema and a plugin-local copy, with no shared source of truth enforcing consistency.
packages/core/src/postgres/schema/plugin.ts#L132-L175: keep as the canonical definition (owns DDL/migration via the plugin-schema-hook).plugins/fusion-plugin-compound-engineering/src/sync/pg-schema.ts#L20-L63: stop re-declaringcePipelineLinks/cePipelineState/cePipelineSyncQueuelocally; import the table objects frompackages/core/src/postgres/schema/plugin.ts(ascli-press-store.tsalready does for its tables) sopipeline-store.ts's runtime queries can never drift from the migrated schema.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/postgres/schema/plugin.ts` around lines 132 - 175, Keep cePipelineLinks, cePipelineState, and cePipelineSyncQueue in packages/core/src/postgres/schema/plugin.ts as the canonical definitions. In plugins/fusion-plugin-compound-engineering/src/sync/pg-schema.ts, remove the duplicate local table declarations and import these table objects from the core plugin schema, preserving pipeline-store.ts runtime query usage and preventing schema drift.plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts (1)
724-749: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUse an atomic upsert here. The read-then-insert/update sequence can race under concurrent async callers, causing one writer to fail on the unique constraint instead of updating the existing setting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts` around lines 724 - 749, Update setSetting’s asyncLayer database path to use a single atomic upsert for cliPressSettings instead of selecting first and branching between update and insert. Preserve the existing conflict key (projectId, serviceId, key, and scope), values, timestamps, and returned ServiceSetting behavior while ensuring concurrent callers cannot hit a unique-constraint failure.packages/core/src/postgres/plugin-schema-hook.ts (1)
155-161: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winValidate Roadmap relationships using both
project_idand ID.These joins count a valid hierarchy as conflicting when two projects reuse the same roadmap or milestone ID. Use composite
NOT EXISTSchecks so only missing or cross-project parents fail.Proposed validation
- JOIN project.roadmaps roadmap ON roadmap.id = milestone.roadmap_id - WHERE milestone.project_id IS DISTINCT FROM roadmap.project_id + WHERE NOT EXISTS ( + SELECT 1 FROM project.roadmaps roadmap + WHERE roadmap.project_id = milestone.project_id + AND roadmap.id = milestone.roadmap_id + )Apply the equivalent check to features.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/postgres/plugin-schema-hook.ts` around lines 155 - 161, Update the roadmap relationship validation query to use composite NOT EXISTS checks rather than ID-only joins. For milestones, require a matching roadmap with both roadmap.id = milestone.roadmap_id and roadmap.project_id = milestone.project_id; apply the equivalent ID-and-project check for features against milestones. Count only missing or cross-project parents as conflicts, allowing reused IDs across projects.
🧹 Nitpick comments (2)
plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts (1)
24-32: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPer-dispatch catalog load is unbounded, not just query-count-bounded.
The FNXC comment states this keeps round trips constant "as services and historical specs grow," but
listAllSpecs()/listAllArtifacts()still return the full historical set on every dispatch — onlystatus === "generated"specs andexecutableartifacts are ever used here. As spec/artifact history accumulates per project, this hot path (every dispatched task) will transfer and process an ever-growing payload despite the fixed query count.Consider pushing the
status/executablefilters (or a "latest generated spec per service" query) down to SQL incli-press-store.tsrather than filtering after loading full history into memory.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts` around lines 24 - 32, Update the catalog-loading flow around the Promise.all call and the store methods it invokes so SQL returns only generated specs and executable artifacts needed by dispatches, preferably using a latest-generated-spec-per-service query where required. Replace the listAllSpecs/listAllArtifacts usage with filtered store APIs, preserve the existing specsByService and artifactsBySpec grouping behavior, and avoid loading historical records into memory.packages/core/src/postgres/plugin-schema-hook.ts (1)
261-296: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftLead shared-table secondary indexes with
project_id.The CE, Reports, and CLI tables are now project-partitioned, but their list/order indexes remain global. Project-scoped reads can consequently scan and filter rows belonging to every project. Rebuild these indexes as
(project_id, ...).Also applies to: 443-470, 563-617
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/postgres/plugin-schema-hook.ts` around lines 261 - 296, Update the CE secondary index definitions in the schema hook, including idxCePipelineLinksPipeline, idxCePipelineLinksTask, idxCePipelineStateStatus, idxCePipelineSyncQueuePending, and idxCePipelineSyncQueuePipeline, to lead with project_id before their existing columns. Apply the same project_id-leading index change to the corresponding Reports and CLI index definitions in the referenced sections, preserving each index’s remaining column order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/__tests__/postgres/plugin-schema-hook.test.ts`:
- Around line 137-139: Remove the duplicate envelope and ddl declarations in the
affected test scopes, retaining only one declaration of each. Simplify the
repeated queryChunks type usage so each type property is declared once,
including the corresponding sections around the additional referenced locations,
while preserving the existing execute mock assertions and envelope extraction.
In `@packages/core/src/plugin-store.ts`:
- Around line 203-212: Update the outdated FNXC comment near the backend
initialization flow to remove the claim that backend mode skips legacy
migration. Align the comment with the migrateLegacyProjectPluginRows bridge,
which now runs during initialization to recover project plugin state without
overwriting newer central changes.
---
Outside diff comments:
In `@packages/core/src/postgres/plugin-schema-hook.ts`:
- Around line 155-161: Update the roadmap relationship validation query to use
composite NOT EXISTS checks rather than ID-only joins. For milestones, require a
matching roadmap with both roadmap.id = milestone.roadmap_id and
roadmap.project_id = milestone.project_id; apply the equivalent ID-and-project
check for features against milestones. Count only missing or cross-project
parents as conflicts, allowing reused IDs across projects.
In `@packages/core/src/postgres/schema/plugin.ts`:
- Around line 132-175: Keep cePipelineLinks, cePipelineState, and
cePipelineSyncQueue in packages/core/src/postgres/schema/plugin.ts as the
canonical definitions. In
plugins/fusion-plugin-compound-engineering/src/sync/pg-schema.ts, remove the
duplicate local table declarations and import these table objects from the core
plugin schema, preserving pipeline-store.ts runtime query usage and preventing
schema drift.
In `@plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts`:
- Around line 724-749: Update setSetting’s asyncLayer database path to use a
single atomic upsert for cliPressSettings instead of selecting first and
branching between update and insert. Preserve the existing conflict key
(projectId, serviceId, key, and scope), values, timestamps, and returned
ServiceSetting behavior while ensuring concurrent callers cannot hit a
unique-constraint failure.
In `@plugins/fusion-plugin-compound-engineering/src/sync/pipeline-store.ts`:
- Around line 351-377: Replace the read-then-insert/update flow in
upsertStateAsync with a single atomic insert using Drizzle’s onConflictDoUpdate
for the projectId and cePipelineId key. Preserve existing status and
lastArtifactPath values when the corresponding input fields are omitted, while
updating supplied fields and timestamps, then retain the final getStateAsync
return.
---
Nitpick comments:
In `@packages/core/src/postgres/plugin-schema-hook.ts`:
- Around line 261-296: Update the CE secondary index definitions in the schema
hook, including idxCePipelineLinksPipeline, idxCePipelineLinksTask,
idxCePipelineStateStatus, idxCePipelineSyncQueuePending, and
idxCePipelineSyncQueuePipeline, to lead with project_id before their existing
columns. Apply the same project_id-leading index change to the corresponding
Reports and CLI index definitions in the referenced sections, preserving each
index’s remaining column order.
In
`@plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts`:
- Around line 24-32: Update the catalog-loading flow around the Promise.all call
and the store methods it invokes so SQL returns only generated specs and
executable artifacts needed by dispatches, preferably using a
latest-generated-spec-per-service query where required. Replace the
listAllSpecs/listAllArtifacts usage with filtered store APIs, preserve the
existing specsByService and artifactsBySpec grouping behavior, and avoid loading
historical records into memory.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3a047b39-12a6-4688-8882-f764a3dad623
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (82)
.changeset/finish-postgres-runtime-cutover.md.changeset/isolate-postgres-plugin-state.mdCONCEPTS.mdREADME.mddocs/PLUGIN_AUTHORING.mddocs/README.mddocs/architecture.mddocs/cli-reference.mddocs/dashboard-guide.mddocs/dashboard-realtime.mddocs/multi-project-sequencing.mddocs/multi-project.mddocs/performance/dashboard-load.mddocs/plugin-management.mddocs/postgres-migration-review-2026-06-26.mddocs/postgres-migration-review-2026-07-14.mddocs/research.mddocs/sandbox.mddocs/secrets.mddocs/settings-reference.mddocs/storage.mddocs/task-management.mddocs/todo-view.mdpackages/cli/src/commands/dashboard.tspackages/cli/src/commands/db.tspackages/cli/src/commands/serve.tspackages/core/src/__tests__/postgres/plugin-schema-hook.test.tspackages/core/src/__tests__/postgres/sqlite-migrator.test.tspackages/core/src/plugin-store.tspackages/core/src/plugin-types.tspackages/core/src/postgres/plugin-schema-hook.tspackages/core/src/postgres/schema/plugin.tspackages/core/src/postgres/sqlite-migrator.tspackages/core/src/postgres/startup-factory.tspackages/desktop/src/__tests__/local-runtime.test.tspackages/desktop/src/__tests__/local-server.test.tspackages/desktop/src/local-runtime.tspackages/desktop/src/local-server.tspackages/plugin-sdk/src/index.tsplugins/fusion-plugin-cli-printing-press/src/__tests__/cli-press-store.pg.test.tsplugins/fusion-plugin-cli-printing-press/src/__tests__/executor-runtime-env.test.tsplugins/fusion-plugin-cli-printing-press/src/__tests__/tools.test.tsplugins/fusion-plugin-cli-printing-press/src/index.tsplugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.tsplugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.tsplugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.tsplugins/fusion-plugin-cli-printing-press/src/tools.tsplugins/fusion-plugin-compound-engineering/src/__tests__/pipeline-store.pg.test.tsplugins/fusion-plugin-compound-engineering/src/index.tsplugins/fusion-plugin-compound-engineering/src/schema.tsplugins/fusion-plugin-compound-engineering/src/session/session-store.tsplugins/fusion-plugin-compound-engineering/src/sync/pg-schema.tsplugins/fusion-plugin-compound-engineering/src/sync/pipeline-store.tsplugins/fusion-plugin-even-realities-glasses/package.jsonplugins/fusion-plugin-even-realities-glasses/src/__tests__/index.test.tsplugins/fusion-plugin-even-realities-glasses/src/__tests__/notification-store.pg.test.tsplugins/fusion-plugin-even-realities-glasses/src/__tests__/notification-store.test.tsplugins/fusion-plugin-even-realities-glasses/src/__tests__/notifier.test.tsplugins/fusion-plugin-even-realities-glasses/src/index.tsplugins/fusion-plugin-even-realities-glasses/src/notifications/store.tsplugins/fusion-plugin-even-realities-glasses/src/notifier.tsplugins/fusion-plugin-even-realities-glasses/tsconfig.jsonplugins/fusion-plugin-reports/README.mdplugins/fusion-plugin-reports/src/__tests__/report-store-provider.test.tsplugins/fusion-plugin-reports/src/__tests__/report-store.pg.test.tsplugins/fusion-plugin-reports/src/__tests__/tools.test.tsplugins/fusion-plugin-reports/src/index.tsplugins/fusion-plugin-reports/src/routes/report-approval-routes.tsplugins/fusion-plugin-reports/src/routes/report-export-routes.tsplugins/fusion-plugin-reports/src/routes/report-list-routes.tsplugins/fusion-plugin-reports/src/store/report-store-provider.tsplugins/fusion-plugin-reports/src/store/report-store.tsplugins/fusion-plugin-reports/src/tools.tsplugins/fusion-plugin-roadmap/src/__tests__/roadmap-store.pg.test.tsplugins/fusion-plugin-roadmap/src/routes/roadmap-routes.tsplugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.tsplugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.tsplugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.tsplugins/fusion-plugin-whatsapp-chat/src/__tests__/persistence.pg.test.tsplugins/fusion-plugin-whatsapp-chat/src/auth-state.tsplugins/fusion-plugin-whatsapp-chat/src/index.tsplugins/fusion-plugin-whatsapp-chat/src/persistence.ts
Make plugin schema application idempotent, enforce tenant-leading indexes, remove duplicate test declarations, and make concurrent plugin writes atomic. Fusion-Task-Id: FN-7952
Keep dry runs non-mutating, make retained plugin SQLite a one-time input, and derive dashboard health and compaction from the live PostgreSQL layer. Fusion-Task-Id: FN-7952
|
Addressed the latest CodeRabbit review in
Verification: |
## Summary Bundled plugins now persist shared runtime state in project-scoped PostgreSQL tables instead of maintaining independent SQLite authority. Reports, CLI Printing Press, Compound Engineering, Roadmap, Even Realities, and WhatsApp all follow the same ownership and startup contract as Fusion core. ## Design decisions - Plugin schema hooks run through the host’s PostgreSQL owner and enforce project isolation. - The SDK exposes the host contract needed by bundled plugins without importing engine internals. - Legacy Roadmap ownership fixtures use the supported empty-owner sentinel, preserving current composite primary/foreign keys while exercising backfill behavior. - The lockfile travels with the Even Realities PostgreSQL dependency so packaged installs remain reproducible. ## Validation - All six affected plugin builds pass. - Affected plugin suites pass: 773 tests across Printing Press, Compound Engineering, Even Realities, Reports, Roadmap, and WhatsApp. - `pnpm test:gate` passes all 478 gate tests. - This PR changes 40 files. ## Stack - Depends on #2110 → #2109 → #2108. - The documentation/release PR completes the stack. Related: #2105 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Breaking Changes** * PostgreSQL is now required for runtime storage; SQLite files are used only as one-time migration inputs. * The legacy `FUSION_NO_EMBEDDED_PG` fallback has been removed. * **New Features** * Added project-isolated PostgreSQL storage for plugins, reports, tasks, notifications, and other plugin data. * Added agent tools for reports and CLI service drafts. * Added PostgreSQL schema initialization support for plugin authors. * **Bug Fixes** * Improved migration and recovery of legacy plugin state. * Prevented cross-project data access and strengthened transactional schema updates. * **Documentation** * Updated storage, migration, deployment, plugin authoring, CLI, and dashboard guidance for PostgreSQL. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
Bundled plugins now persist shared runtime state in project-scoped PostgreSQL tables instead of maintaining independent SQLite authority. Reports, CLI Printing Press, Compound Engineering, Roadmap, Even Realities, and WhatsApp all follow the same ownership and startup contract as Fusion core.
Design decisions
Validation
pnpm test:gatepasses all 478 gate tests.Stack
Related: #2105
Summary by CodeRabbit
Breaking Changes
FUSION_NO_EMBEDDED_PGfallback has been removed.New Features
Bug Fixes
Documentation