From 9f6aaa933d033257ae0f404bc0e59343682325d4 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 25 Jul 2026 20:46:07 -0700 Subject: [PATCH] fix(engine): save worktree-written plans to the project and the database Planning sessions run in the task's own worktree with the coding tool surface, but the spec path handed to the planner is relative (.fusion/tasks//PROMPT.md) while finalization reads it against rootDir. A planner using the generic write tool instead of fn_task_prompt_write stranded the spec inside the worktree, where finalization could not see it and worktree disposal destroyed it. project.tasks also has no `prompt` column, so PROMPT.md was filesystem-only and the project checkout was the sole durable copy of every plan. Add plan-artifact-writeback.ts: - reconcileWorktreePlanArtifact copies a worktree-stranded PROMPT.md back into the main project .fusion folder through store.updateTask({ prompt }), keeping File Scope validation, the root write, and the task.json sync atomic. Empty, absent, and identical worktree copies are no-ops so a correct spec is never clobbered. - mirrorPlanToProjectDb mirrors the authoritative plan into the `plan` task document, which triage already reads as a planning-draft fallback, making that recovery path DB-backed. Identical content is skipped so revisions do not churn. Both are best-effort: a failure never turns a good planning pass into an error. Wired at the reconcile-before-finalize-read seam, at finalization with the post-hygiene accepted content, and inside fn_task_prompt_write. Covers the invariant rather than the repro: tests assert worktree- stranded, root-only, empty worktree file, absent file, identical content, persistence failure, redundant-mirror skip, and mirror failure. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/plan-artifact-writeback.md | 7 + .../__tests__/plan-artifact-writeback.test.ts | 211 ++++++++++++++++++ packages/engine/src/agent-tools.ts | 12 + .../engine/src/plan-artifact-writeback.ts | 181 +++++++++++++++ packages/engine/src/triage.ts | 44 +++- 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 .changeset/plan-artifact-writeback.md create mode 100644 packages/engine/src/__tests__/plan-artifact-writeback.test.ts create mode 100644 packages/engine/src/plan-artifact-writeback.ts diff --git a/.changeset/plan-artifact-writeback.md b/.changeset/plan-artifact-writeback.md new file mode 100644 index 0000000000..ec1476537d --- /dev/null +++ b/.changeset/plan-artifact-writeback.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Plans written inside a task worktree are now saved to the main project and stored in the database. +category: fix +dev: New `packages/engine/src/plan-artifact-writeback.ts` exposes `reconcileWorktreePlanArtifact`, `mirrorPlanToProjectDb`, and `persistPlanArtifact`. Planning sessions run in the task worktree with the coding tool surface, so a planner using the generic write tool resolved the relative `.fusion/tasks//PROMPT.md` against the worktree; triage finalization reads `/` and saw nothing. Triage now reconciles the worktree copy through `store.updateTask({ prompt })` before the finalize read. `project.tasks` has no `prompt` column, so the authoritative plan is also mirrored into the `plan` task document from triage finalization and from `fn_task_prompt_write`. diff --git a/packages/engine/src/__tests__/plan-artifact-writeback.test.ts b/packages/engine/src/__tests__/plan-artifact-writeback.test.ts new file mode 100644 index 0000000000..0fdff18826 --- /dev/null +++ b/packages/engine/src/__tests__/plan-artifact-writeback.test.ts @@ -0,0 +1,211 @@ +import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + PLAN_DOCUMENT_KEY, + mirrorPlanToProjectDb, + persistPlanArtifact, + reconcileWorktreePlanArtifact, + relativePromptPath, +} from "../plan-artifact-writeback.js"; + +/* +FNXC:PlanArtifactPersistence 2026-07-26-03:55: +Invariant under test (not just the reported repro): a plan written anywhere a planning session can write it +ends up (a) in the MAIN project `.fusion/` folder and (b) in the project database. Surfaces covered here: +worktree-stranded write, root write, empty/absent worktree file, identical content, and persistence failure. +*/ + +const TASK_ID = "FN-9001"; +const REAL_SPEC = "# Task: FN-9001 - Real spec\n\n## Mission\n\nDo the thing.\n"; + +interface FakeStore { + updateTask: ReturnType; + upsertTaskDocument: ReturnType; + getTaskDocument: ReturnType; + documents: Map; +} + +function createFakeStore(rootDir: string): FakeStore { + const documents = new Map(); + return { + documents, + // Mirrors the real `updateTask({ prompt })` contract: the project-root PROMPT.md is the artifact it writes. + updateTask: vi.fn(async (id: string, updates: { prompt?: string }) => { + if (updates.prompt !== undefined) { + const dir = join(rootDir, ".fusion", "tasks", id); + await mkdir(dir, { recursive: true }); + await writeFile(join(dir, "PROMPT.md"), updates.prompt); + } + }), + upsertTaskDocument: vi.fn(async (id: string, input: { key: string; content: string }) => { + documents.set(`${id}:${input.key}`, input.content); + return { key: input.key, content: input.content }; + }), + getTaskDocument: vi.fn(async (id: string, key: string) => { + const content = documents.get(`${id}:${key}`); + return content === undefined ? null : { key, content }; + }), + }; +} + +async function writeSpec(baseDir: string, taskId: string, content: string): Promise { + const path = join(baseDir, relativePromptPath(taskId)); + await mkdir(join(baseDir, ".fusion", "tasks", taskId), { recursive: true }); + await writeFile(path, content); +} + +describe("plan artifact write-back", () => { + let rootDir: string; + let worktreeDir: string; + let store: FakeStore; + + beforeEach(async () => { + rootDir = await mkdtemp(join(tmpdir(), "fusion-plan-root-")); + worktreeDir = await mkdtemp(join(tmpdir(), "fusion-plan-worktree-")); + store = createFakeStore(rootDir); + }); + + it("copies a worktree-stranded plan back into the main project .fusion folder", async () => { + await writeSpec(worktreeDir, TASK_ID, REAL_SPEC); + + const result = await reconcileWorktreePlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: worktreeDir, + }); + + expect(result.outcome).toBe("recovered"); + expect(result.content).toBe(REAL_SPEC); + // Persisted through the single validated path, not a raw copy. + expect(store.updateTask).toHaveBeenCalledWith(TASK_ID, { prompt: REAL_SPEC }); + await expect(readFile(join(rootDir, relativePromptPath(TASK_ID)), "utf-8")).resolves.toBe(REAL_SPEC); + }); + + it("stores the plan in the project database as the plan task document", async () => { + await writeSpec(worktreeDir, TASK_ID, REAL_SPEC); + + const result = await persistPlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: worktreeDir, + author: "triage", + }); + + expect(result.outcome).toBe("recovered"); + expect(result.mirrored).toBe(true); + expect(store.documents.get(`${TASK_ID}:${PLAN_DOCUMENT_KEY}`)).toBe(REAL_SPEC); + }); + + it("mirrors a root-written plan into the database even when planning never used a worktree", async () => { + await writeSpec(rootDir, TASK_ID, REAL_SPEC); + + const result = await persistPlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: rootDir, + }); + + expect(result.outcome).toBe("not-worktree"); + expect(result.mirrored).toBe(true); + expect(store.documents.get(`${TASK_ID}:${PLAN_DOCUMENT_KEY}`)).toBe(REAL_SPEC); + // Nothing to copy back — the root copy is already authoritative. + expect(store.updateTask).not.toHaveBeenCalled(); + }); + + it("never overwrites an authoritative root plan with an empty worktree file", async () => { + await writeSpec(rootDir, TASK_ID, REAL_SPEC); + await writeSpec(worktreeDir, TASK_ID, " \n"); + + const result = await reconcileWorktreePlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: worktreeDir, + }); + + expect(result.outcome).toBe("worktree-artifact-empty"); + expect(store.updateTask).not.toHaveBeenCalled(); + await expect(readFile(join(rootDir, relativePromptPath(TASK_ID)), "utf-8")).resolves.toBe(REAL_SPEC); + }); + + it("is a no-op when the planner used the durable writer and left nothing in the worktree", async () => { + await writeSpec(rootDir, TASK_ID, REAL_SPEC); + + const result = await reconcileWorktreePlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: worktreeDir, + }); + + expect(result.outcome).toBe("no-worktree-artifact"); + expect(result.content).toBe(REAL_SPEC); + expect(store.updateTask).not.toHaveBeenCalled(); + }); + + it("does not rewrite when the worktree copy already matches the root copy", async () => { + await writeSpec(rootDir, TASK_ID, REAL_SPEC); + await writeSpec(worktreeDir, TASK_ID, REAL_SPEC); + + const result = await reconcileWorktreePlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: worktreeDir, + }); + + expect(result.outcome).toBe("already-authoritative"); + expect(store.updateTask).not.toHaveBeenCalled(); + }); + + it("leaves the root copy untouched and stays non-fatal when persistence fails", async () => { + await writeSpec(rootDir, TASK_ID, REAL_SPEC); + await writeSpec(worktreeDir, TASK_ID, "# Task: FN-9001 - Rewritten\n"); + store.updateTask.mockRejectedValueOnce(new Error("invalid file scope")); + const warn = vi.fn(); + + const result = await reconcileWorktreePlanArtifact({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: store as any, + taskId: TASK_ID, + rootDir, + planningCwd: worktreeDir, + logger: { warn }, + }); + + expect(result.outcome).toBe("recovery-failed"); + expect(warn).toHaveBeenCalled(); + await expect(readFile(join(rootDir, relativePromptPath(TASK_ID)), "utf-8")).resolves.toBe(REAL_SPEC); + }); + + it("skips redundant document revisions when the mirrored plan is unchanged", async () => { + await expect(mirrorPlanToProjectDb(store as never, TASK_ID, REAL_SPEC)).resolves.toBe(true); + await expect(mirrorPlanToProjectDb(store as never, TASK_ID, REAL_SPEC)).resolves.toBe(false); + expect(store.upsertTaskDocument).toHaveBeenCalledTimes(1); + }); + + it("never mirrors empty plan content", async () => { + await expect(mirrorPlanToProjectDb(store as never, TASK_ID, " \n ")).resolves.toBe(false); + expect(store.upsertTaskDocument).not.toHaveBeenCalled(); + }); + + it("stays non-fatal when the database mirror fails", async () => { + store.upsertTaskDocument.mockRejectedValueOnce(new Error("db down")); + const warn = vi.fn(); + await expect(mirrorPlanToProjectDb(store as never, TASK_ID, REAL_SPEC, { logger: { warn } })).resolves.toBe(false); + expect(warn).toHaveBeenCalled(); + }); +}); diff --git a/packages/engine/src/agent-tools.ts b/packages/engine/src/agent-tools.ts index b808f98cd7..79119c4b1a 100644 --- a/packages/engine/src/agent-tools.ts +++ b/packages/engine/src/agent-tools.ts @@ -24,6 +24,8 @@ import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { Type, type Static } from "@earendil-works/pi-ai"; import type { AgentReflectionService } from "./agent-reflection.js"; import { createLogger } from "./logger.js"; +// FNXC:PlanArtifactPersistence 2026-07-26-03:55: PROMPT.md is filesystem-only; mirror plan writes into the DB. +import { mirrorPlanToProjectDb } from "./plan-artifact-writeback.js"; import { fetchWebContent, WebFetchError } from "./web-fetch.js"; import type { RunAuditor } from "./run-audit.js"; import { computeApprovalDedupeKey } from "./agent-action-gate.js"; @@ -1913,6 +1915,16 @@ export function createTaskPromptWriteTool(store: TaskStore, taskId: string, runC if (persisted?.prompt !== params.content) { throw new Error("authoritative PROMPT.md read-back did not match the requested content; persistence could not be verified"); } + /* + FNXC:PlanArtifactPersistence 2026-07-26-03:55: + `updateTask({ prompt })` writes the project-root PROMPT.md and task.json, but `project.tasks` has + no `prompt` column — the spec would live only as a file in the project checkout. Mirror it into the + `plan` task document so the plan is durable in the project database too. Best-effort: a mirror + failure must not fail a write whose authoritative persistence was just verified above. + */ + await mirrorPlanToProjectDb(store, taskId, params.content, { + author: runContext?.agentId ?? "agent", + }); return { content: [{ type: "text" as const, text: `Updated PROMPT.md for ${taskId}.` }], details: {}, diff --git a/packages/engine/src/plan-artifact-writeback.ts b/packages/engine/src/plan-artifact-writeback.ts new file mode 100644 index 0000000000..3b09c4d79e --- /dev/null +++ b/packages/engine/src/plan-artifact-writeback.ts @@ -0,0 +1,181 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +import type { TaskStore } from "@fusion/core"; + +/* +FNXC:PlanArtifactPersistence 2026-07-26-03:55: +Planning sessions run in the TASK's own worktree (see FNXC:NodeWorktreeIsolation in triage.ts), and they +carry the full coding tool surface. The system prompt tells the planner to persist through +`fn_task_prompt_write`, but that is a soft instruction: a planner that reaches for the generic write tool +writes the relative spec path (`.fusion/tasks//PROMPT.md`) against its own cwd, so the spec lands +INSIDE the worktree. Triage then finalizes by reading `/`, sees nothing, and fails +deterministic validation — and the worktree copy is destroyed with the worktree. + +Two durability requirements follow, and this module owns both: +1. A plan written inside a worktree is copied back into the main project `.fusion/` folder. The copy goes + through `store.updateTask({ prompt })`, which is the single validated persistence path (File Scope + validation, root PROMPT.md write, and task.json sync stay together and stay atomic). +2. The authoritative plan is mirrored into the project database. `project.tasks` has no `prompt` column — + PROMPT.md is filesystem-only and is hydrated on read — so losing the project checkout loses every spec. + The mirror uses the existing `task_documents` store under the `plan` key, which triage already reads as + a planning-draft fallback (`readNonEmptyPlanningDraft`), so recovery has a DB-backed source of truth. +*/ + +/** Relative, cwd-anchored path handed to the planning agent for a task's spec. */ +export function relativePromptPath(taskId: string): string { + return `.fusion/tasks/${taskId}/PROMPT.md`; +} + +/** The `task_documents` key used to mirror the authoritative plan into the project DB. */ +export const PLAN_DOCUMENT_KEY = "plan"; + +export interface PlanWritebackLogger { + log?: (message: string) => void; + warn?: (message: string) => void; +} + +export interface ReconcileWorktreePlanArtifactOptions { + store: TaskStore; + taskId: string; + /** Project root checkout — the authoritative `.fusion/` location. */ + rootDir: string; + /** cwd the planning session ran in. Equal to `rootDir` when planning did not get a worktree. */ + planningCwd: string; + logger?: PlanWritebackLogger; +} + +export type PlanWritebackOutcome = + /** Planning ran in the project root; there is no separate copy to reconcile. */ + | "not-worktree" + /** No spec file inside the worktree — the planner used the durable writer, as instructed. */ + | "no-worktree-artifact" + /** Worktree copy is empty/whitespace; nothing worth rescuing. */ + | "worktree-artifact-empty" + /** Worktree copy matches the authoritative root copy already. */ + | "already-authoritative" + /** Worktree copy was copied back into the project `.fusion/` folder. */ + | "recovered" + /** A worktree copy existed but persisting it failed; the root copy is untouched. */ + | "recovery-failed"; + +export interface ReconcileWorktreePlanArtifactResult { + outcome: PlanWritebackOutcome; + /** Authoritative plan content after reconciliation, when one could be resolved. */ + content?: string; +} + +async function readIfPresent(path: string): Promise { + try { + return await readFile(path, "utf-8"); + } catch { + return null; + } +} + +/** + * FNXC:PlanArtifactPersistence 2026-07-26-03:55: + * Copy a worktree-local PROMPT.md back into the main project `.fusion/` folder. + * + * Only rescues a STRANDED plan: when the root copy already matches, or the worktree holds nothing, this + * is a no-op. The root copy is never overwritten with an empty or whitespace-only worktree file, so a + * planner that used `fn_task_prompt_write` correctly cannot have its spec clobbered by a stale stub the + * worktree happened to inherit. + * + * Failure is non-fatal: triage's own deterministic validation still owns the "no usable spec" verdict, and + * a failed rescue must not convert a recoverable planning pass into a hard error. + */ +export async function reconcileWorktreePlanArtifact( + options: ReconcileWorktreePlanArtifactOptions, +): Promise { + const { store, taskId, rootDir, planningCwd, logger } = options; + const relPath = relativePromptPath(taskId); + + const rootContent = await readIfPresent(join(rootDir, relPath)); + if (planningCwd === rootDir) { + return { outcome: "not-worktree", content: rootContent ?? undefined }; + } + + const worktreeContent = await readIfPresent(join(planningCwd, relPath)); + if (worktreeContent === null) { + return { outcome: "no-worktree-artifact", content: rootContent ?? undefined }; + } + if (worktreeContent.trim().length === 0) { + return { outcome: "worktree-artifact-empty", content: rootContent ?? undefined }; + } + if (rootContent === worktreeContent) { + return { outcome: "already-authoritative", content: rootContent }; + } + + try { + // The single validated persistence path: File Scope validation + root PROMPT.md write + task.json sync. + await store.updateTask(taskId, { prompt: worktreeContent }); + logger?.log?.( + `${taskId}: recovered a worktree-local PROMPT.md into the project .fusion folder (${planningCwd})`, + ); + return { outcome: "recovered", content: worktreeContent }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger?.warn?.( + `${taskId}: failed to copy the worktree-local PROMPT.md back into the project .fusion folder: ${message}`, + ); + return { outcome: "recovery-failed", content: rootContent ?? undefined }; + } +} + +/** + * FNXC:PlanArtifactPersistence 2026-07-26-03:55: + * Mirror the authoritative plan into the project database under the `plan` task document. + * + * `project.tasks` carries no `prompt` column, so without this the spec exists only as a file in the + * project checkout. The `plan` document is already the draft surface triage falls back to when PROMPT.md + * is absent, so mirroring here makes that recovery path DB-backed instead of filesystem-only. + * + * Best-effort by design: a mirror failure must never fail the planning pass that just produced a good spec. + * Re-mirroring identical content is skipped so repeated prompt writes do not churn document revisions. + */ +export async function mirrorPlanToProjectDb( + store: TaskStore, + taskId: string, + content: string, + options: { author?: string; logger?: PlanWritebackLogger } = {}, +): Promise { + if (content.trim().length === 0) return false; + if (typeof store.upsertTaskDocument !== "function") return false; + + try { + if (typeof store.getTaskDocument === "function") { + const existing = await store.getTaskDocument(taskId, PLAN_DOCUMENT_KEY); + if (typeof existing?.content === "string" && existing.content === content) return false; + } + await store.upsertTaskDocument(taskId, { + key: PLAN_DOCUMENT_KEY, + content, + author: options.author ?? "agent", + }); + return true; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + options.logger?.warn?.(`${taskId}: failed to mirror the plan into the project database: ${message}`); + return false; + } +} + +/** + * FNXC:PlanArtifactPersistence 2026-07-26-03:55: + * Combined post-planning durability pass: rescue a worktree-stranded spec into the project `.fusion/` + * folder, then mirror whatever is authoritative afterwards into the project database. Callers run this + * BEFORE reading the finalized spec so the read observes the recovered content. + */ +export async function persistPlanArtifact( + options: ReconcileWorktreePlanArtifactOptions & { author?: string }, +): Promise { + const result = await reconcileWorktreePlanArtifact(options); + const mirrored = result.content + ? await mirrorPlanToProjectDb(options.store, options.taskId, result.content, { + author: options.author, + logger: options.logger, + }) + : false; + return { ...result, mirrored }; +} diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index 2e8adb1f0b..9d3e5bdafc 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -146,6 +146,9 @@ import { import { buildPromptLayers, collapsePromptLayers } from "./prompt-layers.js"; import { createFallbackModelObserver } from "./fallback-model-observer.js"; import { planLog, formatError } from "./logger.js"; +// FNXC:PlanArtifactPersistence 2026-07-26-03:55: worktree-stranded plans are copied back into the project +// .fusion folder and mirrored into the project DB before finalization reads the spec. +import { mirrorPlanToProjectDb, persistPlanArtifact, relativePromptPath } from "./plan-artifact-writeback.js"; import { resolveMcpServersForStore } from "./mcp-resolution.js"; import { isUsageLimitError, @@ -1549,7 +1552,9 @@ export class TriageProcessor { // planning-phase reads (requirePlanApproval, planning/validator model lanes) // pick up workflow values. Behavior-inert when nothing is customized. const settings = await mergeEffectiveSettings(this.store, currentTask, await this.store.getSettings()); - const promptPath = `.fusion/tasks/${task.id}/PROMPT.md`; + // FNXC:PlanArtifactPersistence 2026-07-26-03:55: one definition of the cwd-relative spec path, shared + // with the worktree write-back so the rescue reads exactly the path the planner was handed. + const promptPath = relativePromptPath(task.id); /* FNXC:PlanReview 2026-07-19-00:22 (U3): @@ -2087,6 +2092,31 @@ export class TriageProcessor { Workflow Plan Review is the single operator-controlled AI plan gate. Triage must not remind agents to call fn_review_spec or retry planning only because that legacy tool was not approved; after PROMPT.md is written, triage itself runs optional Plan Review before releasing the task to execution. */ + /* + FNXC:PlanArtifactPersistence 2026-07-26-03:55: + Planning ran with the coding tool surface inside the task worktree, so a planner that ignored + `fn_task_prompt_write` and used the generic write tool resolved the relative spec path against + the WORKTREE. Finalization reads `/`, so that spec would read as missing, + fail deterministic validation, and then be destroyed with the worktree. Copy any worktree-local + spec back into the project `.fusion/` folder BEFORE the finalize read, and mirror whatever is + authoritative into the project database (PROMPT.md has no `tasks` column and is otherwise + filesystem-only). Both halves are best-effort — validation below still owns the verdict. + */ + const planPersistence = await persistPlanArtifact({ + store: this.store, + taskId: task.id, + rootDir: this.rootDir, + planningCwd, + author: "triage", + logger: { log: (m: string) => planLog.log(m), warn: (m: string) => planLog.warn(m) }, + }); + if (planPersistence.outcome === "recovered") { + await this.store.logEntry( + task.id, + "Recovered the plan written inside the task worktree into the project .fusion folder", + ).catch(() => undefined); + } + const written = await readFile( join(this.rootDir, promptPath), "utf-8", @@ -3098,6 +3128,18 @@ export class TriageProcessor { } } + /* + FNXC:PlanArtifactPersistence 2026-07-26-03:55: + Finalization is where the ACCEPTED spec content is known (post hygiene rewrite), and it is the last + writer that touches the root PROMPT.md on a planning pass. Mirror it into the project database here so + the DB copy is the finalized plan, not the pre-hygiene draft. Identical content is skipped, so a pass + whose hygiene rewrite was a no-op produces exactly one document revision. + */ + await mirrorPlanToProjectDb(this.store, task.id, written, { + author: "triage", + logger: { warn: (m: string) => planLog.warn(m) }, + }); + let taskIntentSignature: ReturnType = { routePaths: [], filePaths: [],