From 52a28d39ff1ba89618e6cda0ad4863b918ddf856 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 14 Aug 2026 21:12:15 -0700 Subject: [PATCH] feat: add per-task GitHub tracking overrides to task create (fn_task_create + CLI) fn_task_create gains github_tracking/github_repo params and fn task create gains --github/--no-github/--github-repo flags, resolved through resolveTaskGithubTracking (task > project > global). CLI create now also honors the project/global tracking-enabled default it previously ignored; explicit disables persist enabled:false so later default flips cannot re-enable a task. Co-Authored-By: Claude Fable 5 --- .../task-create-github-tracking-flags.md | 7 +++ docs/cli-reference.md | 3 ++ .../fusion/references/extension-tools.md | 2 + packages/cli/src/__tests__/extension.test.ts | 52 +++++++++++++++++++ packages/cli/src/bin.ts | 14 ++++- packages/cli/src/commands/task.ts | 50 +++++++++++++++++- packages/cli/src/extension.ts | 44 +++++++++++++++- 7 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 .changeset/task-create-github-tracking-flags.md diff --git a/.changeset/task-create-github-tracking-flags.md b/.changeset/task-create-github-tracking-flags.md new file mode 100644 index 0000000000..7f68e64c06 --- /dev/null +++ b/.changeset/task-create-github-tracking-flags.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Task creation now accepts per-task GitHub tracking overrides (fn_task_create params and `fn task create --github`). +category: feature +dev: New `github_tracking`/`github_repo` params on fn_task_create and `--github`/`--no-github`/`--github-repo` flags on `fn task create`. CLI create now also applies the project/global "tracking enabled by default" setting it previously ignored; explicit disables persist `githubTracking.enabled:false`. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index ab600f8f0a..5f1ea03d3a 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -637,6 +637,7 @@ Task lifecycle and task operations. fn task create "Fix login race condition" fn task create "Fix bug" --attach screenshot.png --depends FN-010 fn task create "Investigate flaky runner" --node edge-runner +fn task create "Fix workspace revert" --github --github-repo acme/kb fn task plan "Design a new authentication flow" ``` @@ -1383,6 +1384,8 @@ Subcommands: `search`, `install`, `get`. | `--attach` | `fn task create` | | `--depends` | `fn task create` | | `--node` | `fn task create` | +| `--github` / `--no-github` | `fn task create` (per-task GitHub issue tracking override; default comes from project/global settings) | +| `--github-repo` | `fn task create` (`owner/repo` override for the tracking issue) | | `--feedback` | `fn task refine` | | `--yes` | confirmation-skipping flows (`task plan`, `settings import`, git pull/push, etc.) | | `--limit`, `-l` | `fn task import`, `fn task import-gitlab` (default: 30, max: 100), `fn skills search` (default: 10, max: 50) | diff --git a/packages/cli/skill/fusion/references/extension-tools.md b/packages/cli/skill/fusion/references/extension-tools.md index 11ce63e31e..ded37f06ee 100644 --- a/packages/cli/skill/fusion/references/extension-tools.md +++ b/packages/cli/skill/fusion/references/extension-tools.md @@ -19,6 +19,8 @@ Create a new task on the Fusion task board. The task enters the planning column | `agentId` | string | — | Agent ID to assign this task to (e.g. 'agent-abc123') | | `priority` | string(enum) | — | Task priority (low, normal, high, urgent) | | `workflow_id` | string | — | Workflow ID to select for the new task (e.g. 'WF-003' or 'builtin:coding'). Omit to inherit the project default workflow. Use fn_workflow_list to discover valid IDs. | +| `github_tracking` | boolean | — | Per-task GitHub issue tracking override. true links a tracking issue to this task; false disables tracking even when the project/global default enables it. Omit to inherit the project/global default. | +| `github_repo` | string | — | "owner/repo" override for the GitHub tracking issue's repository. Omit to use the project/global default repo. | ### fn_task_update diff --git a/packages/cli/src/__tests__/extension.test.ts b/packages/cli/src/__tests__/extension.test.ts index 07aa2d98a5..3743c2d677 100644 --- a/packages/cli/src/__tests__/extension.test.ts +++ b/packages/cli/src/__tests__/extension.test.ts @@ -3422,6 +3422,58 @@ pgTest("fn pi extension (runnable structured-output regression slice)", () => { expect(result.content[0].text).toContain("requires an \"executor\"-role agent"); }); + it("fn_task_create persists per-task github tracking overrides from github_tracking/github_repo", async () => { + const createTool = api.tools.get("fn_task_create")!; + const result = await createTool.execute( + "create-gh-on", + { description: "Track me on GitHub", github_tracking: true, github_repo: "acme/widgets" }, + undefined, + undefined, + makeCtx(tmpDir), + ); + const task = await h.store().getTask(result.details.taskId); + expect(task.githubTracking?.enabled).toBe(true); + expect(task.githubTracking?.repoOverride).toBe("acme/widgets"); + + const invalid = await createTool.execute( + "create-gh-bad-repo", + { description: "Bad repo slug", github_repo: "not a slug" }, + undefined, + undefined, + makeCtx(tmpDir), + ); + expect(invalid.isError).toBe(true); + expect(invalid.content[0].text).toContain("owner/repo"); + }); + + it("fn_task_create persists an explicit github_tracking:false even when the project default enables tracking", async () => { + await h.store().updateSettings({ githubTrackingEnabledByDefault: true }); + try { + const createTool = api.tools.get("fn_task_create")!; + const offResult = await createTool.execute( + "create-gh-off", + { description: "Opt out of GitHub tracking", github_tracking: false }, + undefined, + undefined, + makeCtx(tmpDir), + ); + const offTask = await h.store().getTask(offResult.details.taskId); + expect(offTask.githubTracking?.enabled).toBe(false); + + const defaultResult = await createTool.execute( + "create-gh-default", + { description: "Inherit project GitHub tracking default" }, + undefined, + undefined, + makeCtx(tmpDir), + ); + const defaultTask = await h.store().getTask(defaultResult.details.taskId); + expect(defaultTask.githubTracking?.enabled).toBe(true); + } finally { + await h.store().updateSettings({ githubTrackingEnabledByDefault: false }); + } + }); + it("fn_task_update rejects reviewer assignment for implementation tasks", async () => { const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion"), asyncLayer: h.store().getAsyncLayer() }); await agentStore.init(); diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 4ff2b6d731..f155b1692d 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -518,6 +518,9 @@ Options: --attach Attach file(s) on task create (repeatable) --depends Declare dependency on task create (repeatable) --no-dedup Bypass deterministic duplicate guard on task create + --github Enable GitHub issue tracking for the created task + --no-github Disable GitHub issue tracking (overrides project default) + --github-repo Repository override for the task's tracking issue --feedback Refinement feedback (non-interactive mode) --yes Skip confirmation prompts (planning mode) --limit, -l Max issues to import (default: 30, max: 100) @@ -1266,6 +1269,8 @@ async function main() { const dependsIds: string[] = []; let nodeName: string | undefined; let noDedup = false; + let github: boolean | undefined; + let githubRepo: string | undefined; const descParts: string[] = []; for (let i = 0; i < createArgs.length; i++) { if (createArgs[i] === "--attach" && i + 1 < createArgs.length) { @@ -1279,12 +1284,19 @@ async function main() { i++; // skip the value } else if (createArgs[i] === "--no-dedup") { noDedup = true; + } else if (createArgs[i] === "--github") { + github = true; + } else if (createArgs[i] === "--no-github") { + github = false; + } else if (createArgs[i] === "--github-repo" && i + 1 < createArgs.length) { + githubRepo = createArgs[i + 1]; + i++; // skip the value } else { descParts.push(createArgs[i]); } } const title = descParts.join(" "); - await runTaskCreate(title || undefined, attachFiles.length > 0 ? attachFiles : undefined, dependsIds.length > 0 ? dependsIds : undefined, projectName, nodeName, noDedup); + await runTaskCreate(title || undefined, attachFiles.length > 0 ? attachFiles : undefined, dependsIds.length > 0 ? dependsIds : undefined, projectName, nodeName, noDedup, github !== undefined || githubRepo !== undefined ? { github, githubRepo } : undefined); break; } case "plan": { diff --git a/packages/cli/src/commands/task.ts b/packages/cli/src/commands/task.ts index ca7e7ba46a..91dae0a8b7 100644 --- a/packages/cli/src/commands/task.ts +++ b/packages/cli/src/commands/task.ts @@ -1,4 +1,4 @@ -import { TaskStore, COLUMNS, COLUMN_LABELS, resolveProjectColumnsForRoles, TERMINAL_ROLES, resolveReviewColumns, resolveTaskLifecycleColumns, resolveWorkflowIrForTask, CentralCore, buildAutoPauseClearPatch, buildManualRetryResetPatch, extractIntentSignature, findNearDuplicates, getTaskDuplicateLineage, isWorkspaceTask, reconcileDeterministicDuplicate, resolveTaskGithubTracking, runDeterministicDuplicateGuard, type Settings, type Column, type ColumnId, type StepStatus, type AgentLogType, type AgentLogEntry, type IntentSignature, type NearDuplicateCandidate, type NearDuplicateMatch, type TaskDependencyMutation } from "@fusion/core"; +import { TaskStore, COLUMNS, COLUMN_LABELS, resolveProjectColumnsForRoles, TERMINAL_ROLES, resolveReviewColumns, resolveTaskLifecycleColumns, resolveWorkflowIrForTask, CentralCore, buildAutoPauseClearPatch, buildManualRetryResetPatch, extractIntentSignature, findNearDuplicates, getTaskDuplicateLineage, isValidRepoSlug, isWorkspaceTask, reconcileDeterministicDuplicate, resolveTaskGithubTracking, runDeterministicDuplicateGuard, type Settings, type Column, type ColumnId, type StepStatus, type AgentLogType, type AgentLogEntry, type IntentSignature, type NearDuplicateCandidate, type NearDuplicateMatch, type TaskDependencyMutation } from "@fusion/core"; import { isInReviewMissingWorktreeSessionStartFailure, runAiMerge, landWorkspaceTask, installBaselineArchiveWorktreeDisposer } from "@fusion/engine"; import { createInterface } from "node:readline/promises"; import type { PlanningQuestion, PlanningSummary } from "@fusion/core"; @@ -444,9 +444,23 @@ async function runCliNearDuplicateCheck(args: { process.exit(0); } -export async function runTaskCreate(descriptionArg?: string, attachFiles?: string[], depends?: string[], projectName?: string, nodeName?: string, noDedup = false) { +export async function runTaskCreate(descriptionArg?: string, attachFiles?: string[], depends?: string[], projectName?: string, nodeName?: string, noDedup = false, githubOpts?: { github?: boolean; githubRepo?: string }) { let description = descriptionArg; + /* + FNXC:GithubTracking 2026-08-15-03:50: + `fn task create --github [--github-repo owner/repo]` decides tracking at CREATE time, + because the tracking lifecycle keys off the persisted `task.githubTracking.enabled === true` + flag and never re-resolves project/global defaults for existing tasks. Before this flag, + CLI-created tasks could not opt in at all (only the dashboard and fn_task_create could). + Explicit `--no-github` persists `enabled:false` so a later default flip cannot re-enable it. + */ + const githubRepoOverride = githubOpts?.githubRepo?.trim() || undefined; + if (githubRepoOverride && !isValidRepoSlug(githubRepoOverride)) { + console.error(`Invalid --github-repo "${githubRepoOverride}" — expected "owner/repo".`); + process.exit(1); + } + if (!description) { const rl = createInterface({ input: process.stdin, output: promptOutputStream() }); description = await rl.question("Task description: "); @@ -511,10 +525,42 @@ export async function runTaskCreate(descriptionArg?: string, attachFiles?: strin id, so a stale value can never make CLI create throw. */ const originWorkflowId = await store.resolveOriginWorkflowOverrideId("task-create"); + /* + FNXC:GithubTracking 2026-08-15-03:50: + Same create-time resolution as fn_task_create: CLI flag > project default > + global default. Also fixes CLI creates silently ignoring a project's + "GitHub tracking enabled by default" setting (the persisted flag is the only + thing the tracking lifecycle reads). + */ + const globalSettingsForTracking = await store.getGlobalSettingsStore().getSettings(); + const resolvedTracking = resolveTaskGithubTracking( + { + githubTracking: + githubOpts?.github !== undefined || githubRepoOverride + ? { + ...(githubOpts?.github !== undefined ? { enabled: githubOpts.github } : {}), + ...(githubRepoOverride ? { repoOverride: githubRepoOverride } : {}), + } + : undefined, + }, + await store.getSettings(), + globalSettingsForTracking, + ); + const githubTracking = resolvedTracking.enabled + ? { + enabled: true as const, + ...(resolvedTracking.repo + ? { repoOverride: `${resolvedTracking.repo.owner}/${resolvedTracking.repo.repo}` } + : {}), + } + : githubOpts?.github === false + ? { enabled: false as const } + : undefined; const created = await store.createTask({ description: trimmedDescription, dependencies: depends, ...(originWorkflowId ? { workflowId: originWorkflowId } : {}), + ...(githubTracking ? { githubTracking } : {}), source: { sourceType: "cli", sourceMetadata: Object.keys(sourceMetadata).length > 0 ? sourceMetadata : undefined, diff --git a/packages/cli/src/extension.ts b/packages/cli/src/extension.ts index ed8230ca08..41094c876f 100644 --- a/packages/cli/src/extension.ts +++ b/packages/cli/src/extension.ts @@ -1664,6 +1664,21 @@ export default function kbExtension(pi: ExtensionAPI) { "Omit to inherit the project default workflow. Use fn_workflow_list to discover valid IDs.", }), ), + github_tracking: Type.Optional( + Type.Boolean({ + description: + "Per-task GitHub issue tracking override. true links a tracking issue to this task; " + + "false disables tracking even when the project/global default enables it. " + + "Omit to inherit the project/global default.", + }), + ), + github_repo: Type.Optional( + Type.String({ + description: + "\"owner/repo\" override for the GitHub tracking issue's repository. " + + "Omit to use the project/global default repo.", + }), + ), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { @@ -1727,10 +1742,33 @@ export default function kbExtension(pi: ExtensionAPI) { } } + /* + FNXC:GithubTracking 2026-08-15-03:50: + Per-task GitHub tracking is decided at CREATE time (the lifecycle hooks key off the + persisted `task.githubTracking.enabled === true` flag, never re-resolving defaults), + so callers need a create-time override. `github_tracking`/`github_repo` feed the + task-level slot of resolveTaskGithubTracking, which already gives task > project > + global precedence. An explicit `false` is persisted (not dropped) so a later flip of + the project default cannot retroactively enable tracking for this task. + */ + const githubRepoOverride = params.github_repo?.trim() || undefined; + if (githubRepoOverride && !fusionCore.isValidRepoSlug(githubRepoOverride)) { + const error = `Invalid github_repo "${githubRepoOverride}" — expected "owner/repo".`; + return { content: [{ type: "text", text: `ERROR: ${error}` }], isError: true, details: { error } }; + } + try { const globalSettings = await store.getGlobalSettingsStore().getSettings(); const resolvedTracking = resolveTaskGithubTracking( - { githubTracking: undefined }, + { + githubTracking: + params.github_tracking !== undefined || githubRepoOverride + ? { + ...(params.github_tracking !== undefined ? { enabled: params.github_tracking } : {}), + ...(githubRepoOverride ? { repoOverride: githubRepoOverride } : {}), + } + : undefined, + }, projectSettingsForGate, globalSettings, ); @@ -1760,7 +1798,9 @@ export default function kbExtension(pi: ExtensionAPI) { ? { repoOverride: `${resolvedTracking.repo.owner}/${resolvedTracking.repo.repo}` } : {}), } - : undefined, + : params.github_tracking === false + ? { enabled: false } + : undefined, }, { rootDir: ctx.cwd, sourceAgentId: fnCtx.agentId, sourceTaskId: fnCtx.taskId }); const label =