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:
7
.changeset/task-create-github-tracking-flags.md
Normal file
7
.changeset/task-create-github-tracking-flags.md
Normal 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`.
|
||||||
@@ -637,6 +637,7 @@ Task lifecycle and task operations.
|
|||||||
fn task create "Fix login race condition"
|
fn task create "Fix login race condition"
|
||||||
fn task create "Fix bug" --attach screenshot.png --depends FN-010
|
fn task create "Fix bug" --attach screenshot.png --depends FN-010
|
||||||
fn task create "Investigate flaky runner" --node edge-runner
|
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"
|
fn task plan "Design a new authentication flow"
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1383,6 +1384,8 @@ Subcommands: `search`, `install`, `get`.
|
|||||||
| `--attach` | `fn task create` |
|
| `--attach` | `fn task create` |
|
||||||
| `--depends` | `fn task create` |
|
| `--depends` | `fn task create` |
|
||||||
| `--node` | `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` |
|
| `--feedback` | `fn task refine` |
|
||||||
| `--yes` | confirmation-skipping flows (`task plan`, `settings import`, git pull/push, etc.) |
|
| `--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) |
|
| `--limit`, `-l` | `fn task import`, `fn task import-gitlab` (default: 30, max: 100), `fn skills search` (default: 10, max: 50) |
|
||||||
|
|||||||
@@ -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') |
|
| `agentId` | string | — | Agent ID to assign this task to (e.g. 'agent-abc123') |
|
||||||
| `priority` | string(enum) | — | Task priority (low, normal, high, urgent) |
|
| `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. |
|
| `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
|
### fn_task_update
|
||||||
|
|
||||||
|
|||||||
@@ -3422,6 +3422,58 @@ pgTest("fn pi extension (runnable structured-output regression slice)", () => {
|
|||||||
expect(result.content[0].text).toContain("requires an \"executor\"-role agent");
|
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 () => {
|
it("fn_task_update rejects reviewer assignment for implementation tasks", async () => {
|
||||||
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion"), asyncLayer: h.store().getAsyncLayer() });
|
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion"), asyncLayer: h.store().getAsyncLayer() });
|
||||||
await agentStore.init();
|
await agentStore.init();
|
||||||
|
|||||||
@@ -518,6 +518,9 @@ Options:
|
|||||||
--attach <file> Attach file(s) on task create (repeatable)
|
--attach <file> Attach file(s) on task create (repeatable)
|
||||||
--depends <id> Declare dependency on task create (repeatable)
|
--depends <id> Declare dependency on task create (repeatable)
|
||||||
--no-dedup Bypass deterministic duplicate guard on task create
|
--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)
|
--feedback <text> Refinement feedback (non-interactive mode)
|
||||||
--yes Skip confirmation prompts (planning mode)
|
--yes Skip confirmation prompts (planning mode)
|
||||||
--limit, -l <n> Max issues to import (default: 30, max: 100)
|
--limit, -l <n> Max issues to import (default: 30, max: 100)
|
||||||
@@ -1266,6 +1269,8 @@ async function main() {
|
|||||||
const dependsIds: string[] = [];
|
const dependsIds: string[] = [];
|
||||||
let nodeName: string | undefined;
|
let nodeName: string | undefined;
|
||||||
let noDedup = false;
|
let noDedup = false;
|
||||||
|
let github: boolean | undefined;
|
||||||
|
let githubRepo: string | undefined;
|
||||||
const descParts: string[] = [];
|
const descParts: string[] = [];
|
||||||
for (let i = 0; i < createArgs.length; i++) {
|
for (let i = 0; i < createArgs.length; i++) {
|
||||||
if (createArgs[i] === "--attach" && i + 1 < createArgs.length) {
|
if (createArgs[i] === "--attach" && i + 1 < createArgs.length) {
|
||||||
@@ -1279,12 +1284,19 @@ async function main() {
|
|||||||
i++; // skip the value
|
i++; // skip the value
|
||||||
} else if (createArgs[i] === "--no-dedup") {
|
} else if (createArgs[i] === "--no-dedup") {
|
||||||
noDedup = true;
|
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 {
|
} else {
|
||||||
descParts.push(createArgs[i]);
|
descParts.push(createArgs[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const title = descParts.join(" ");
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "plan": {
|
case "plan": {
|
||||||
|
|||||||
@@ -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 { isInReviewMissingWorktreeSessionStartFailure, runAiMerge, landWorkspaceTask, installBaselineArchiveWorktreeDisposer } from "@fusion/engine";
|
||||||
import { createInterface } from "node:readline/promises";
|
import { createInterface } from "node:readline/promises";
|
||||||
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||||
@@ -444,9 +444,23 @@ async function runCliNearDuplicateCheck(args: {
|
|||||||
process.exit(0);
|
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;
|
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) {
|
if (!description) {
|
||||||
const rl = createInterface({ input: process.stdin, output: promptOutputStream() });
|
const rl = createInterface({ input: process.stdin, output: promptOutputStream() });
|
||||||
description = await rl.question("Task description: ");
|
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.
|
id, so a stale value can never make CLI create throw.
|
||||||
*/
|
*/
|
||||||
const originWorkflowId = await store.resolveOriginWorkflowOverrideId("task-create");
|
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({
|
const created = await store.createTask({
|
||||||
description: trimmedDescription,
|
description: trimmedDescription,
|
||||||
dependencies: depends,
|
dependencies: depends,
|
||||||
...(originWorkflowId ? { workflowId: originWorkflowId } : {}),
|
...(originWorkflowId ? { workflowId: originWorkflowId } : {}),
|
||||||
|
...(githubTracking ? { githubTracking } : {}),
|
||||||
source: {
|
source: {
|
||||||
sourceType: "cli",
|
sourceType: "cli",
|
||||||
sourceMetadata: Object.keys(sourceMetadata).length > 0 ? sourceMetadata : undefined,
|
sourceMetadata: Object.keys(sourceMetadata).length > 0 ? sourceMetadata : undefined,
|
||||||
|
|||||||
@@ -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.",
|
"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) {
|
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 {
|
try {
|
||||||
const globalSettings = await store.getGlobalSettingsStore().getSettings();
|
const globalSettings = await store.getGlobalSettingsStore().getSettings();
|
||||||
const resolvedTracking = resolveTaskGithubTracking(
|
const resolvedTracking = resolveTaskGithubTracking(
|
||||||
{ githubTracking: undefined },
|
{
|
||||||
|
githubTracking:
|
||||||
|
params.github_tracking !== undefined || githubRepoOverride
|
||||||
|
? {
|
||||||
|
...(params.github_tracking !== undefined ? { enabled: params.github_tracking } : {}),
|
||||||
|
...(githubRepoOverride ? { repoOverride: githubRepoOverride } : {}),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
projectSettingsForGate,
|
projectSettingsForGate,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
);
|
);
|
||||||
@@ -1760,7 +1798,9 @@ export default function kbExtension(pi: ExtensionAPI) {
|
|||||||
? { repoOverride: `${resolvedTracking.repo.owner}/${resolvedTracking.repo.repo}` }
|
? { repoOverride: `${resolvedTracking.repo.owner}/${resolvedTracking.repo.repo}` }
|
||||||
: {}),
|
: {}),
|
||||||
}
|
}
|
||||||
: undefined,
|
: params.github_tracking === false
|
||||||
|
? { enabled: false }
|
||||||
|
: undefined,
|
||||||
}, { rootDir: ctx.cwd, sourceAgentId: fnCtx.agentId, sourceTaskId: fnCtx.taskId });
|
}, { rootDir: ctx.cwd, sourceAgentId: fnCtx.agentId, sourceTaskId: fnCtx.taskId });
|
||||||
|
|
||||||
const label =
|
const label =
|
||||||
|
|||||||
Reference in New Issue
Block a user