diff --git a/.changeset/fn-343-merge-worktree-cleanup.md b/.changeset/fn-343-merge-worktree-cleanup.md index 2b6d3982fc..d5386b6638 100644 --- a/.changeset/fn-343-merge-worktree-cleanup.md +++ b/.changeset/fn-343-merge-worktree-cleanup.md @@ -1,5 +1,5 @@ --- -"@fusion/engine": patch +"@runfusion/fusion": patch --- Classify harmless temporary merge worktree cleanup failures after `git worktree prune`/porcelain inspection while keeping still-registered worktree leaks visible in merger diagnostics. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 12bad910f7..125f458a40 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2368,6 +2368,8 @@ export interface Task { sourceMessageId?: string; sourceParentTaskId?: string; sourceMetadata?: Record; + /** Reconstructed task prompt content when available on in-memory execution tasks. */ + prompt?: string; /** Explicitly assigned user ID for task-user linking. Used during review handoff to indicate * which user should review the task. The sentinel value "requesting-user" indicates the * user who created or steered the task. */ diff --git a/packages/engine/src/__tests__/executor-task-done-invariant.test.ts b/packages/engine/src/__tests__/executor-task-done-invariant.test.ts index 0670657153..9c3d68a9b6 100644 --- a/packages/engine/src/__tests__/executor-task-done-invariant.test.ts +++ b/packages/engine/src/__tests__/executor-task-done-invariant.test.ts @@ -191,6 +191,48 @@ Atlas Notes task-board artifacts only: expect(store.handoffToReview).not.toHaveBeenCalled(); }); + it("FN-350 refuses contradictory implementation plus coordination fallback prompts", async () => { + const prompt = `# Task: FN-350 - Route Ready Swift Tasks to Executor Owner + +## Review Level: 1 (Plan Only) + +**Assessment:** This is a coordination/routing task that should not change product source. + +## Mission +Implement the source fix if possible, or record the intentional block if no safe candidate exists. Do not change product source. + +## File Scope + +- FN-350 task document \`docs\` via \`fn_task_document_write\` + +## Steps + +### Step 1: Decide +- [x] Decision recorded. +`; + const { store, tool } = await setup({ + id: "FN-350", + title: "Route Ready Swift Tasks to Executor Owner", + description: "Coordination/routing task with task-document evidence only.", + prompt, + branch: "fusion/fn-350", + noCommitsExpected: undefined, + steps: [{ name: "Decide", status: "done" as const }], + }); + mockedExecSync.mockImplementation((cmd: string) => { + if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/swift-falcon\n"); + if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-350\n"); + if (cmd.includes("rev-list --count")) return Buffer.from("0\n"); + if (cmd.includes("rev-parse HEAD")) return Buffer.from("def456\n"); + return Buffer.from(""); + }); + + const result = await tool.execute("id", {}); + + expect(result.content[0].text).toContain("fn_task_done refused: no_commits"); + expect(store.moveTask).toHaveBeenCalledWith("FN-350", "todo", { preserveProgress: true }); + }); + it("FN-4114 still refuses source-changing implementation tasks with zero commits and no explicit no-commit contract", async () => { const implementationPrompt = `# Task: FN-4114 - Implement source change diff --git a/packages/engine/src/executor.ts b/packages/engine/src/executor.ts index 1ad795e847..8450ae2bcd 100644 --- a/packages/engine/src/executor.ts +++ b/packages/engine/src/executor.ts @@ -257,7 +257,11 @@ function promptLooksCoordinationOnly(prompt: string): boolean { const assessment = prompt.match(/^\*\*Assessment:\*\*\s*([^\n]+)/im)?.[1] ?? ""; const coordinationText = `${titleMatch}\n${mission}\n${assessment}`.toLowerCase(); const hasCoordinationIntent = /\b(coordination|routing|route|handoff|assign(?:ment)?|owner|triage|select exactly one|record (?:the )?intentional block)\b/.test(coordinationText); - const hasImplementationDirective = /\b(implement|fix|add|change|modify|refactor|build|create|delete|remove)\b/.test(mission.toLowerCase()) && !/record (?:the )?intentional block/.test(mission.toLowerCase()); + const missionLower = mission.toLowerCase() + .replace(/do\s+not\s+(?:edit|modify|change)\s+(?:product\s+)?source/g, "") + .replace(/should\s+not\s+change\s+(?:product\s+)?source/g, "") + .replace(/must\s+not\s+(?:edit|modify|change)\s+(?:product\s+)?(?:source|code)/g, ""); + const hasImplementationDirective = /\b(implement|fix|add|change|modify|refactor|build|create|delete|remove)\b/.test(missionLower); return hasCoordinationIntent && !hasImplementationDirective; } @@ -268,13 +272,13 @@ function promptFileScopeIsBoardOnly(prompt: string): boolean { const sourcePathPattern = /(?:^|[\s`'"(])(?:packages|src|source|sources|app|apps|lib|libs|components|scripts|docs|\.github|config|test|tests|__tests__)\//m; const sourceExtensionPattern = /\.(?:ts|tsx|js|jsx|mjs|cjs|swift|kt|java|py|go|rs|rb|php|cs|cpp|c|h|hpp|json|ya?ml|toml|mdx?|css|scss|html|sql|sh)\b/m; if (sourcePathPattern.test(normalized) || sourceExtensionPattern.test(normalized)) return false; - const allowedBoardOnlyPattern = /\b(task[- ]?board|board task|task document|task documents|task metadata|task logs|fusion task tools|fn_task_|\.fusion\/tasks|attachments?)\b/; + const allowedBoardOnlyPattern = /(?:^|[^\w/])(?:task[- ]?board|board task|task document|task documents|task metadata|task logs|fusion task tools|fn_task_[\w-]*|\.fusion\/tasks|attachments?)(?=$|[^\w/-])/; return allowedBoardOnlyPattern.test(normalized); } function getNoCommitEligibilityReason(task: Task): "explicit noCommitsExpected=true" | "prompt-derived coordination-only no-source scope" | null { if (task.noCommitsExpected === true) return "explicit noCommitsExpected=true"; - const rawPrompt = (task as { prompt?: unknown }).prompt; + const rawPrompt = task.prompt; const prompt = typeof rawPrompt === "string" ? rawPrompt : ""; if (!prompt.trim()) return null; if (