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 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-08-14 21:12:15 -07:00
parent 0188d9427b
commit 52a28d39ff
7 changed files with 167 additions and 5 deletions

View File

@@ -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`.

View File

@@ -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) |

View File

@@ -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

View File

@@ -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();

View File

@@ -518,6 +518,9 @@ Options:
--attach <file> Attach file(s) on task create (repeatable)
--depends <id> 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 <owner/repo> Repository override for the task's tracking issue
--feedback <text> Refinement feedback (non-interactive mode)
--yes Skip confirmation prompts (planning mode)
--limit, -l <n> 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": {

View File

@@ -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,

View File

@@ -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 =