fix(core): restore title-redirect planning hold and stamp workflow-create partition

Full-suite repair, core cluster. Two real product regressions caught by
the red tests: (1) the wave-18 refactor dropped the task.title argument
FN-8840 added to isDuplicateRedirectOnlyPrompt inside
isTaskAwaitingPlanning, so a title-only DUPLICATE:<ID> redirect read as
executable Ready instead of awaiting planning; (2) FN-8998 scoped every
workflow-definition read by layer.projectId but left the INSERT on the
session-GUC default, so a JS-bound layer created workflow rows it could
never read back — the insert now stamps the bound projectId like the
FN-8997 workflowSteps insert. Test-lag fixes: analytics renamed-lanes
harnesses now bind projectId 'p1' to match FN-8957/FN-8998 scoping,
project-ownership-runtime-scope replaces a hardcoded expired approval
date, schema-applier learns migrations 0059/0060 (115 project tables,
baseline 0060), and builtin-workflow-settings-triage learns FN-8932's
memoryConsolidationEnabled with position-independent lookups.
Verified: 10 files / 128 tests green, core typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-15 15:22:33 -07:00
parent 13525fc0f0
commit f11bb2e899
11 changed files with 118 additions and 12 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Fix title-only duplicate redirects showing as Ready and workflows created invisible to their own project.
category: fix
dev: Restores the `task.title` argument to `isDuplicateRedirectOnlyPrompt` in `isTaskAwaitingPlanning` (dropped by a refactor after FN-8840) and stamps `layer.projectId` on the `project.workflows` INSERT so FN-8998's project-scoped reads see a bound layer's own create.

View File

@@ -142,7 +142,14 @@ describe("workflow-native built-in workflow settings", () => {
const movedIds = new Set(BUILTIN_MOVED_WORKFLOW_SETTINGS.map((setting) => setting.id)); const movedIds = new Set(BUILTIN_MOVED_WORKFLOW_SETTINGS.map((setting) => setting.id));
const movedKeyIds = new Set(MOVED_SETTINGS_KEYS); const movedKeyIds = new Set(MOVED_SETTINGS_KEYS);
/*
FNXC:MemoryAgent 2026-08-15-22:10:
FN-8932 declares memory consolidation workflow-native (it resolves through the default workflow
on a no-task heartbeat, like patrol), so it belongs in this catalog and stays out of
moved/project settings like every other key asserted below.
*/
expect(BUILTIN_OVERSIGHT_SETTINGS.map((setting) => setting.id)).toEqual([ expect(BUILTIN_OVERSIGHT_SETTINGS.map((setting) => setting.id)).toEqual([
"memoryConsolidationEnabled",
"plannerOversightLevel", "plannerOversightLevel",
"plannerOversightNotificationLevel", "plannerOversightNotificationLevel",
"plannerOverseerExecutorStuckAfterMs", "plannerOverseerExecutorStuckAfterMs",
@@ -156,7 +163,13 @@ describe("workflow-native built-in workflow settings", () => {
type: "boolean", type: "boolean",
default: false, default: false,
}); });
const oversight = BUILTIN_OVERSIGHT_SETTINGS[0]; /* FNXC:MemoryAgent 2026-08-15-22:10: resolve by id, not position — FN-8932 prepended
memoryConsolidationEnabled to this catalog and positional reads silently drifted. */
const memoryConsolidation = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "memoryConsolidationEnabled");
expect(memoryConsolidation).toMatchObject({ type: "boolean", default: true });
expect(movedIds.has("memoryConsolidationEnabled")).toBe(false);
expect(movedKeyIds.has("memoryConsolidationEnabled")).toBe(false);
const oversight = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOversightLevel")!;
expect(oversight).toMatchObject({ expect(oversight).toMatchObject({
type: "enum", type: "enum",
default: "autonomous", default: "autonomous",
@@ -180,7 +193,7 @@ describe("workflow-native built-in workflow settings", () => {
"plannerOversightLevel should not be in MOVED_SETTINGS_KEYS", "plannerOversightLevel should not be in MOVED_SETTINGS_KEYS",
).toBe(false); ).toBe(false);
const notificationLevel = BUILTIN_OVERSIGHT_SETTINGS[1]; const notificationLevel = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOversightNotificationLevel")!;
expect(notificationLevel).toMatchObject({ expect(notificationLevel).toMatchObject({
id: "plannerOversightNotificationLevel", id: "plannerOversightNotificationLevel",
type: "enum", type: "enum",
@@ -213,7 +226,7 @@ describe("workflow-native built-in workflow settings", () => {
// FN-7743: executor-stall recovery threshold, declared alongside the other // FN-7743: executor-stall recovery threshold, declared alongside the other
// workflow-native oversight settings. // workflow-native oversight settings.
const executorStuckAfterMs = BUILTIN_OVERSIGHT_SETTINGS[2]; const executorStuckAfterMs = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerOverseerExecutorStuckAfterMs")!;
expect(executorStuckAfterMs).toMatchObject({ expect(executorStuckAfterMs).toMatchObject({
id: "plannerOverseerExecutorStuckAfterMs", id: "plannerOverseerExecutorStuckAfterMs",
type: "number", type: "number",
@@ -232,7 +245,7 @@ describe("workflow-native built-in workflow settings", () => {
"plannerOverseerExecutorStuckAfterMs should not be in MOVED_SETTINGS_KEYS", "plannerOverseerExecutorStuckAfterMs should not be in MOVED_SETTINGS_KEYS",
).toBe(false); ).toBe(false);
const heartbeatPatrol = BUILTIN_OVERSIGHT_SETTINGS[6]; const heartbeatPatrol = BUILTIN_OVERSIGHT_SETTINGS.find((s) => s.id === "plannerHeartbeatPatrolEnabled")!;
expect(heartbeatPatrol).toMatchObject({ expect(heartbeatPatrol).toMatchObject({
id: "plannerHeartbeatPatrolEnabled", id: "plannerHeartbeatPatrolEnabled",
type: "boolean", type: "boolean",

View File

@@ -30,8 +30,16 @@ const IN_RANGE = "2026-06-15T12:00:00.000Z";
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" }; const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
pgDescribe("issue analytics under a renamed board vocabulary", () => { pgDescribe("issue analytics under a renamed board vocabulary", () => {
/*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
FN-8957 scoped analytics task reads to `layer.projectId`. This file aggregates under projectId
"p1", so the harness binds the same project: an unbound harness writes store rows under the GUC
default partition and the scoped aggregate reads zero rows — an isolation false-negative, not the
lane-vocabulary defect this file pins.
*/
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({ const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_issue_analytics_lanes", prefix: "fusion_issue_analytics_lanes",
projectId: "p1",
}); });
beforeAll(h.beforeAll); beforeAll(h.beforeAll);

View File

@@ -28,8 +28,16 @@ const IN_RANGE = "2026-06-15T12:00:00.000Z";
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" }; const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z" };
pgDescribe("productivity analytics under a renamed board vocabulary", () => { pgDescribe("productivity analytics under a renamed board vocabulary", () => {
/*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
FN-8957 scoped analytics task reads to `layer.projectId`. This file aggregates under projectId
"p1", so the harness binds the same project: an unbound harness writes store rows under the GUC
default partition and the scoped aggregate reads zero rows — an isolation false-negative, not the
lane-vocabulary defect this file pins.
*/
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({ const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_productivity_lanes", prefix: "fusion_productivity_lanes",
projectId: "p1",
}); });
beforeAll(h.beforeAll); beforeAll(h.beforeAll);

View File

@@ -24,7 +24,13 @@ pgDescribe("project ownership runtime scope", () => {
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({ const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_project_ownership_runtime_scope", prefix: "fusion_project_ownership_runtime_scope",
}); });
const now = "2026-08-12T14:15:00.000Z"; /*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
Real wall-clock, not a literal: `decideApprovalRequest` rejects a pending request older than
APPROVAL_REQUEST_PENDING_TTL_MS against Date.now(), so a hardcoded seed date expired the day
after this file was written and turned the whole isolation case into a time bomb.
*/
const now = new Date().toISOString();
const bind = (projectId: string): AsyncDataLayer => ({ ...h.layer(), projectId }); const bind = (projectId: string): AsyncDataLayer => ({ ...h.layer(), projectId });
beforeAll(h.beforeAll); beforeAll(h.beforeAll);

View File

@@ -101,6 +101,8 @@ import {
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION, PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION, PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
MESSAGE_ARCHIVE_SCHEMA_VERSION, MESSAGE_ARCHIVE_SCHEMA_VERSION,
TASK_SOURCE_AGENT_INDEX_VERSION,
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
} from "../../postgres/schema-applier.js"; } from "../../postgres/schema-applier.js";
import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js"; import { ProjectPartitionRekeyError, rekeyFallbackProjectPartition } from "../../postgres/migration-stamping.js";
import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.js"; import type { PluginSchemaInitHook } from "../../postgres/plugin-schema-hook.js";
@@ -135,7 +137,11 @@ describe("schema-applier: immutable migration identities", () => {
expect(PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION).toBe("0056"); expect(PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION).toBe("0056");
expect(PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION).toBe("0057"); expect(PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION).toBe("0057");
expect(MESSAGE_ARCHIVE_SCHEMA_VERSION).toBe("0058"); expect(MESSAGE_ARCHIVE_SCHEMA_VERSION).toBe("0058");
expect(SCHEMA_BASELINE_VERSION).toBe("0058"); /* FNXC:PgSchemaApplier 2026-08-15-22:10: 0059 (FN-9037 recommendation source-agent index) and
0060 (FN-9059 workspace coordination leases/intents) advance the baseline to 0060. */
expect(TASK_SOURCE_AGENT_INDEX_VERSION).toBe("0059");
expect(WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION).toBe("0060");
expect(SCHEMA_BASELINE_VERSION).toBe("0060");
}); });
it("keeps monitor and approval isolation assigned to version 0003", () => { it("keeps monitor and approval isolation assigned to version 0003", () => {
@@ -771,10 +777,11 @@ pgDescribe("schema-applier: VAL-SCHEMA-001 final-schema parity (table counts)",
outbox tables; 0041 adds 4 lifecycle consumer tables; 0043 adds the durable unplanned-dispatch outbox tables; 0041 adds 4 lifecycle consumer tables; 0043 adds the durable unplanned-dispatch
refusal marker (100 → 105); later baseline additions bring the count to 106; and 0048 adds refusal marker (100 → 105); later baseline additions bring the count to 106; and 0048 adds
GitHub check state (106 → 107); 0049 adds the agent-activity outbox and counter (→ 109); GitHub check state (106 → 107); 0049 adds the agent-activity outbox and counter (→ 109);
0050 adds immutable lock, evidence, and report history (109 → 112); 0052 adds recall records (→ 113). Plugin tables are added separately 0050 adds immutable lock, evidence, and report history (109 → 112); 0052 adds recall records (→ 113);
0060 adds workspace coordination leases and land intents (→ 115). Plugin tables are added separately
by the schema-init hook and are excluded here. by the schema-init hook and are excluded here.
*/ */
expect(bySchema.project).toBe(113); expect(bySchema.project).toBe(115);
/* /*
FNXC:CapacityModel 2026-07-29-08:10 (drop the cross-project cap — table half): FNXC:CapacityModel 2026-07-29-08:10 (drop the cross-project cap — table half):
17, not 18: `central.global_concurrency` is dropped by migration 0037. A fresh 17, not 18: `central.global_concurrency` is dropped by migration 0037. A fresh
@@ -1644,7 +1651,13 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
updated_at text NOT NULL updated_at text NOT NULL
); );
/* FNXC:GitHubImportTranslate 2026-07-16-23:30: Later durable-task migrations run after this historical 0000 fixture, so retain their required task table surface. */ /* FNXC:GitHubImportTranslate 2026-07-16-23:30: Later durable-task migrations run after this historical 0000 fixture, so retain their required task table surface. */
CREATE TABLE project.tasks (id text PRIMARY KEY); /*
FNXC:PgSchemaApplier 2026-08-15-22:10:
Migration 0059 (FN-9037) builds a partial index on tasks(project_id, source_agent_id).
Real 0000 databases have source_agent_id (baseline since the PG cutover), so this
historical fixture must retain it; project_id arrives via the 0006 ownership migration.
*/
CREATE TABLE project.tasks (id text PRIMARY KEY, source_agent_id text);
/* /*
FNXC:Ideation 2026-07-18-13:25: FNXC:Ideation 2026-07-18-13:25:
FN-8295 migration 0022 FKs ideation rows to missions/mission_features on (project_id, id). FN-8295 migration 0022 FKs ideation rows to missions/mission_features on (project_id, id).
@@ -1809,6 +1822,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION, PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION, PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
MESSAGE_ARCHIVE_SCHEMA_VERSION, MESSAGE_ARCHIVE_SCHEMA_VERSION,
TASK_SOURCE_AGENT_INDEX_VERSION,
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
]); ]);
expect((await applySchemaBaseline(ctx.db, { pluginHooks: [] })).applied).toBe(false); expect((await applySchemaBaseline(ctx.db, { pluginHooks: [] })).applied).toBe(false);
}); });
@@ -1893,6 +1908,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION, PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION, PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
MESSAGE_ARCHIVE_SCHEMA_VERSION, MESSAGE_ARCHIVE_SCHEMA_VERSION,
TASK_SOURCE_AGENT_INDEX_VERSION,
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
]); ]);
}); });
@@ -2110,6 +2127,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION, PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION, PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
MESSAGE_ARCHIVE_SCHEMA_VERSION, MESSAGE_ARCHIVE_SCHEMA_VERSION,
TASK_SOURCE_AGENT_INDEX_VERSION,
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
]); ]);
}); });
@@ -2208,6 +2227,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION, PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION, PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
MESSAGE_ARCHIVE_SCHEMA_VERSION, MESSAGE_ARCHIVE_SCHEMA_VERSION,
TASK_SOURCE_AGENT_INDEX_VERSION,
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
]); ]);
}); });
@@ -2306,6 +2327,8 @@ pgDescribe("schema-applier: automation project-isolation upgrade", () => {
PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION, PROJECT_OWNERSHIP_DECLARATION_DRIFT_VERSION,
PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION, PROJECT_OWNERSHIP_DEFAULT_RECONCILIATION_VERSION,
MESSAGE_ARCHIVE_SCHEMA_VERSION, MESSAGE_ARCHIVE_SCHEMA_VERSION,
TASK_SOURCE_AGENT_INDEX_VERSION,
WORKSPACE_COORDINATION_LEASES_SCHEMA_VERSION,
]); ]);
}); });
}); });

View File

@@ -32,8 +32,18 @@ const IN_RANGE = "2026-06-15T12:00:00.000Z";
const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z", now: Date.parse(IN_RANGE) }; const RANGE = { from: "2026-06-01T00:00:00.000Z", to: "2026-06-30T23:59:59.999Z", now: Date.parse(IN_RANGE) };
pgDescribe("team analytics under a renamed board vocabulary", () => { pgDescribe("team analytics under a renamed board vocabulary", () => {
/*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
FN-8957 scoped every agent/task analytics read to `layer.projectId`. This file aggregates under
projectId "p1", so the harness must BIND that same project: an unbound harness writes store rows
under the GUC default partition ('__legacy_unscoped__' / '') and the scoped aggregate then reads
zero rows — an isolation false-negative, not the lane-vocabulary defect this file pins.
Raw adminDb seeds below carry an explicit project_id = 'p1' for the same reason (adminDb bypasses
the bound GUC, so column defaults would land the row in the legacy partition).
*/
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({ const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_team_analytics_lanes", prefix: "fusion_team_analytics_lanes",
projectId: "p1",
}); });
beforeAll(h.beforeAll); beforeAll(h.beforeAll);
@@ -71,8 +81,8 @@ pgDescribe("team analytics under a renamed board vocabulary", () => {
const store = h.store(); const store = h.store();
const adminDb = h.adminDb(); const adminDb = h.adminDb();
await adminDb.execute(sql` await adminDb.execute(sql`
INSERT INTO project.agents (id, name, role, state, created_at, updated_at) INSERT INTO project.agents (id, project_id, name, role, state, created_at, updated_at)
VALUES ('agent-1', 'Agent One', 'executor', 'idle', ${IN_RANGE}, ${IN_RANGE})`); VALUES ('agent-1', 'p1', 'Agent One', 'executor', 'idle', ${IN_RANGE}, ${IN_RANGE})`);
for (const [id, lane] of [["KB-DONE", completeLane], ["KB-WIP", wipLane]] as const) { for (const [id, lane] of [["KB-DONE", completeLane], ["KB-WIP", wipLane]] as const) {
await store.createTaskWithReservedId( await store.createTaskWithReservedId(

View File

@@ -52,8 +52,16 @@ const LEGACY_FLAGS = new Map<string, { countsTowardWip?: boolean; humanReview?:
]); ]);
pgDescribe("workflow analytics under a renamed board vocabulary", () => { pgDescribe("workflow analytics under a renamed board vocabulary", () => {
/*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
FN-8957 scoped analytics task reads to `layer.projectId`. This file aggregates under projectId
"p1", so the harness binds the same project: an unbound harness writes store rows under the GUC
default partition and the scoped aggregate reads zero rows — an isolation false-negative, not the
lane-vocabulary defect this file pins.
*/
const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({ const h: SharedPgTaskStoreHarness = createSharedPgTaskStoreTestHarness({
prefix: "fusion_workflow_analytics_lanes", prefix: "fusion_workflow_analytics_lanes",
projectId: "p1",
}); });
beforeAll(h.beforeAll); beforeAll(h.beforeAll);

View File

@@ -107,7 +107,15 @@ pgTest("workflow definition create (PostgreSQL backend mode)", () => {
const projectA = "proj_workflow_allocator_a"; const projectA = "proj_workflow_allocator_a";
const projectB = "proj_workflow_allocator_b"; const projectB = "proj_workflow_allocator_b";
const now = new Date().toISOString(); const now = new Date().toISOString();
/*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
FN-8998 gave workflows composite (project_id, id) identity and project-scoped reads. The
occupied row must therefore be stamped into project A's partition explicitly — adminDb has no
bound GUC, so omitting project_id would strand the row in '__legacy_unscoped__' where the bound
project-A read below could never prove it survived.
*/
await h.adminDb().insert(schema.project.workflows).values({ await h.adminDb().insert(schema.project.workflows).values({
projectId: projectA,
id: "WF-002", id: "WF-002",
name: "project A occupied workflow", name: "project A occupied workflow",
description: "", description: "",

View File

@@ -108,7 +108,13 @@ export function isTaskAwaitingPlanning(
FNXC:DuplicateIntake 2026-08-01-19:24: FNXC:DuplicateIntake 2026-08-01-19:24:
A duplicate-only PROMPT is unplanned for execution — badge and triage must agree with A duplicate-only PROMPT is unplanned for execution — badge and triage must agree with
scheduler filesystem validation so the card shows "Queued to plan", not Ready. scheduler filesystem validation so the card shows "Queued to plan", not Ready.
FNXC:DuplicateIntake 2026-08-15-22:10:
`task.title` is a REQUIRED argument here, not an optional nicety: FN-8840 recognizes an exact
`DUPLICATE: <ID>` redirect declared in the task TITLE even when PROMPT.md carries a real plan.
A refactor once dropped the argument and title-only redirects silently read as executable
("Ready") on the badge/triage surface — do not remove it again.
*/ */
if (isDuplicateRedirectOnlyPrompt(promptContent)) return true; if (isDuplicateRedirectOnlyPrompt(promptContent, task.title)) return true;
return isUnplannedSeedPrompt(promptContent, task.id, task.title, task.description); return isUnplannedSeedPrompt(promptContent, task.id, task.title, task.description);
} }

View File

@@ -828,7 +828,16 @@ export async function createWorkflowDefinitionImpl(store: TaskStore, input: Work
try { try {
await workflowDefinitionBeforeInsertForTesting?.(id, store.backendMode); await workflowDefinitionBeforeInsertForTesting?.(id, store.backendMode);
/*
FNXC:MultiProjectIsolation 2026-08-15-22:10:
Stamp the bound layer's project explicitly, like the FN-8997 workflowSteps insert does.
FN-8998 scopes every workflow-definition READ by `layer.projectId`; leaving the INSERT to
the session GUC default splits the write/read authority, so a layer bound in JS over a
bypass connection creates a row it can never read back ('' normalizes to the GUC/legacy
partition via the fusion_assign_project_id trigger, preserving unbound behavior).
*/
await store.asyncLayer!.db.insert(schema.project.workflows).values({ await store.asyncLayer!.db.insert(schema.project.workflows).values({
projectId: store.asyncLayer!.projectId?.trim() ?? "",
id: definition.id, id: definition.id,
name: definition.name, name: definition.name,
description: definition.description, description: definition.description,