FN-5780: inherit source GitHub tracking when refining tasks
Preserve GitHub linking intent when creating refinement tasks from completed work. - Set refinement `githubTracking` from the source task state during `refineTask` - Opt out refinements when the source task is unlinked or explicitly disabled - Carry forward `enabled` and optional `repoOverride` for linked sources without copying source issue metadata - Add store-level tests covering unlinked, disabled, linked, and issue-only source tracking states - Document refinement GitHub tracking inheritance behavior in task-management docs - Add a patch changeset for `@runfusion/fusion` Files changed: .changeset/fn-5780-refine-github-tracking.md | 5 ++ docs/task-management.md | 1 + packages/core/src/__tests__/store-ops.test.ts | 71 +++++++++++++++++++++++++++ packages/core/src/store.ts | 12 +++++ 4 files changed, 89 insertions(+) Fusion-Task-Id: FN-5780 Fusion-Task-Lineage: 88c1ce01-6efd-48c1-bbdd-704a821055d7
This commit is contained in:
5
.changeset/fn-5780-refine-github-tracking.md
Normal file
5
.changeset/fn-5780-refine-github-tracking.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Refinement tasks now inherit the source task’s GitHub tracking state, preventing auto-created tracking issues when the source task was not GitHub-linked.
|
||||||
@@ -600,6 +600,7 @@ Behavior:
|
|||||||
- New title format: `Refinement: <source label>`
|
- New title format: `Refinement: <source label>`
|
||||||
- New task depends on source task
|
- New task depends on source task
|
||||||
- Created in `planning`
|
- Created in `planning`
|
||||||
|
- Refinement tasks inherit the source task's GitHub tracking state (unlinked sources opt out; linked sources inherit `enabled` and optional `repoOverride`, but never copy the source issue link)
|
||||||
|
|
||||||
## Archive and Restore
|
## Archive and Restore
|
||||||
|
|
||||||
|
|||||||
@@ -511,6 +511,77 @@ describe("TaskStore", () => {
|
|||||||
expect(refined.status).toBeUndefined();
|
expect(refined.status).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opts out github tracking when source has no githubTracking", async () => {
|
||||||
|
const task = await store.createTask({ description: "Original task" });
|
||||||
|
await store.moveTask(task.id, "todo");
|
||||||
|
await store.moveTask(task.id, "in-progress");
|
||||||
|
await store.moveTask(task.id, "in-review");
|
||||||
|
await store.moveTask(task.id, "done");
|
||||||
|
|
||||||
|
const refined = await store.refineTask(task.id, "Need improvements");
|
||||||
|
|
||||||
|
expect(refined.githubTracking?.enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps github tracking disabled when source is explicitly disabled", async () => {
|
||||||
|
const task = await store.createTask({ description: "Original task" });
|
||||||
|
await store.updateGithubTracking(task.id, { enabled: false });
|
||||||
|
await store.moveTask(task.id, "todo");
|
||||||
|
await store.moveTask(task.id, "in-progress");
|
||||||
|
await store.moveTask(task.id, "in-review");
|
||||||
|
await store.moveTask(task.id, "done");
|
||||||
|
|
||||||
|
const refined = await store.refineTask(task.id, "Need improvements");
|
||||||
|
|
||||||
|
expect(refined.githubTracking?.enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("inherits enabled and repoOverride without copying linked issue", async () => {
|
||||||
|
const task = await store.createTask({ description: "Original task" });
|
||||||
|
await store.updateGithubTracking(task.id, {
|
||||||
|
enabled: true,
|
||||||
|
repoOverride: "owner/repo",
|
||||||
|
issue: {
|
||||||
|
number: 12,
|
||||||
|
url: "https://github.com/owner/repo/issues/12",
|
||||||
|
title: "Tracked",
|
||||||
|
state: "open",
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await store.moveTask(task.id, "todo");
|
||||||
|
await store.moveTask(task.id, "in-progress");
|
||||||
|
await store.moveTask(task.id, "in-review");
|
||||||
|
await store.moveTask(task.id, "done");
|
||||||
|
|
||||||
|
const refined = await store.refineTask(task.id, "Need improvements");
|
||||||
|
|
||||||
|
expect(refined.githubTracking).toEqual({ enabled: true, repoOverride: "owner/repo" });
|
||||||
|
expect(refined.githubTracking?.issue).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats linked issue without enabled flag as linked for refinement", async () => {
|
||||||
|
const task = await store.createTask({ description: "Original task" });
|
||||||
|
await store.updateGithubTracking(task.id, {
|
||||||
|
issue: {
|
||||||
|
number: 99,
|
||||||
|
url: "https://github.com/owner/repo/issues/99",
|
||||||
|
title: "Tracked",
|
||||||
|
state: "open",
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await store.moveTask(task.id, "todo");
|
||||||
|
await store.moveTask(task.id, "in-progress");
|
||||||
|
await store.moveTask(task.id, "in-review");
|
||||||
|
await store.moveTask(task.id, "done");
|
||||||
|
|
||||||
|
const refined = await store.refineTask(task.id, "Need improvements");
|
||||||
|
|
||||||
|
expect(refined.githubTracking?.enabled).toBe(true);
|
||||||
|
expect(refined.githubTracking?.issue).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("creates PROMPT.md for the refinement", async () => {
|
it("creates PROMPT.md for the refinement", async () => {
|
||||||
const task = await store.createTask({ description: "Original task" });
|
const task = await store.createTask({ description: "Original task" });
|
||||||
await store.moveTask(task.id, "todo");
|
await store.moveTask(task.id, "todo");
|
||||||
|
|||||||
@@ -4210,6 +4210,17 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
const removed = extractTaskIdTokens(`Refinement: ${sourceLabel}`).filter((token) => token !== newId.toUpperCase());
|
const removed = extractTaskIdTokens(`Refinement: ${sourceLabel}`).filter((token) => token !== newId.toUpperCase());
|
||||||
storeLog.log(`[title-id-drift] normalized title for ${newId}: removed=[${removed.join(",")}]`);
|
storeLog.log(`[title-id-drift] normalized title for ${newId}: removed=[${removed.join(",")}]`);
|
||||||
}
|
}
|
||||||
|
const sourceGithubLinked = sourceTask.githubTracking?.enabled === true || Boolean(sourceTask.githubTracking?.issue);
|
||||||
|
// FN-5780: refinement should inherit source linking intent so unlinked tasks stay opted out from auto-create defaults.
|
||||||
|
const refinementGithubTracking = sourceGithubLinked
|
||||||
|
? {
|
||||||
|
enabled: true,
|
||||||
|
...(sourceTask.githubTracking?.repoOverride
|
||||||
|
? { repoOverride: sourceTask.githubTracking.repoOverride }
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
: { enabled: false };
|
||||||
|
|
||||||
const newTask: Task = {
|
const newTask: Task = {
|
||||||
id: newId,
|
id: newId,
|
||||||
lineageId: generateTaskLineageId(),
|
lineageId: generateTaskLineageId(),
|
||||||
@@ -4220,6 +4231,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
dependencies: [id],
|
dependencies: [id],
|
||||||
sourceType: "task_refine",
|
sourceType: "task_refine",
|
||||||
sourceParentTaskId: id,
|
sourceParentTaskId: id,
|
||||||
|
githubTracking: refinementGithubTracking,
|
||||||
steps: [],
|
steps: [],
|
||||||
currentStep: 0,
|
currentStep: 0,
|
||||||
log: [{ timestamp: now, action: `Created as refinement of ${id}` }],
|
log: [{ timestamp: now, action: `Created as refinement of ${id}` }],
|
||||||
|
|||||||
Reference in New Issue
Block a user