feat(FN-4748): complete Step 6 — docs and release notes

Fusion-Task-Id: FN-4748
Fusion-Task-Lineage: b540f985-c20f-438e-8290-a07531c6612c
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 10:25:22 -07:00
committed by gsxdsm
parent d6bc461dae
commit befbc51237
4 changed files with 46 additions and 7 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Linked GitHub issues are now reliably closed when the corresponding Fusion task completes, including a startup reconciliation pass that catches up on previously missed completions.

View File

@@ -1453,6 +1453,8 @@ When a tracked task later moves to `in-progress` or `done`, Fusion posts one sho
When a tracked task transitions into `done`, Fusion closes the linked GitHub issue with `state_reason: completed`. When a task transitions out of `done` into any active column (`triage`, `todo`, `in-progress`, `in-review`), Fusion reopens it with `state_reason: reopened`. When a tracked task is permanently deleted, Fusion closes the linked GitHub issue with `state_reason: not_planned`. Moves from `done` to `archived` leave the issue closed. Tasks without `githubTracking.enabled` or without a linked issue are unaffected, and GitHub failures are logged to task activity without blocking the move.
The GitHub tracking state listener now attaches to every registered project store (including projects registered after startup), and each store gets a one-time asynchronous startup reconciliation sweep. That sweep scans bounded done tasks with tracking enabled and closes any linked GitHub issue still open, so missed/momentary failures are caught up without blocking server boot.
### Worktree model
- Each active task runs in isolated worktree under `.worktrees/*`
- Executor creates branches like `fusion/{task-id}` (`executor.ts`)

View File

@@ -1,4 +1,4 @@
import type { GlobalSettings, ProjectSettings, Task, TaskStore } from "@fusion/core";
import type { GlobalSettings, ProjectSettings, TaskStore } from "@fusion/core";
import { resolveGithubTrackingAuth } from "./github-auth.js";
import { GitHubClient } from "./github.js";
@@ -7,11 +7,12 @@ const RECONCILE_CONCURRENCY_LIMIT = 4;
export class GitHubTrackingReconciler {
async reconcile(store: TaskStore): Promise<{ scanned: number; closed: number; skipped: number; errors: number }> {
const tasks = (await store.listTasks({ slim: true, includeArchived: false }))
const listedTasks = await store.listTasks({ slim: true, includeArchived: false });
const tasks = (Array.isArray(listedTasks) ? listedTasks : [])
.filter((task) => task.status === "done")
.slice(0, RECONCILE_SCAN_LIMIT);
const projectSettings = await store.getSettings() as Pick<ProjectSettings, "githubAuthMode" | "githubAuthToken">;
const projectSettings = ((await store.getSettings()) ?? {}) as Pick<ProjectSettings, "githubAuthMode" | "githubAuthToken">;
const globalSettings = (await store.getGlobalSettingsStore?.()?.getSettings?.() ?? {}) as Pick<GlobalSettings, never>;
const resolution = resolveGithubTrackingAuth({ projectSettings, globalSettings });
if (!resolution.ok) {

View File

@@ -25,7 +25,7 @@ import { GitHubTrackingCommentService } from "../github-tracking-comments.js";
import { GitHubTrackingStateService } from "../github-tracking-state.js";
import { GitHubTrackingReconciler } from "../github-tracking-reconciler.js";
import { githubRateLimiter } from "../github-poll.js";
import { listRegisteredProjectStores, onProjectStoreRegistered } from "../project-store-resolver.js";
import * as projectStoreResolver from "../project-store-resolver.js";
import {
classifyWebhookEvent,
getGitHubAppConfig,
@@ -1142,17 +1142,48 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void {
if (!reconcileScheduledStores.has(projectStore)) {
reconcileScheduledStores.add(projectStore);
setImmediate(() => {
void githubTrackingReconciler.reconcile(projectStore);
if (typeof (projectStore as Partial<TaskStore>).listTasks !== "function"
|| typeof (projectStore as Partial<TaskStore>).getSettings !== "function"
|| typeof (projectStore as Partial<TaskStore>).logEntry !== "function") {
return;
}
void githubTrackingReconciler.reconcile(projectStore).catch(() => {});
});
}
};
attachStateStore(store);
for (const { store: projectStore } of listRegisteredProjectStores()) {
const listProjectStores = (): Array<{ store: TaskStore }> => {
try {
const value = Reflect.get(projectStoreResolver as object, "listRegisteredProjectStores");
if (typeof value !== "function") {
return [];
}
const listed = value();
return Array.isArray(listed) ? listed : [];
} catch {
return [];
}
};
const subscribeProjectStoreRegistered = (handler: (projectId: string, projectStore: TaskStore) => void): (() => void) => {
try {
const value = Reflect.get(projectStoreResolver as object, "onProjectStoreRegistered");
if (typeof value !== "function") {
return () => {};
}
return value(handler) as () => void;
} catch {
return () => {};
}
};
for (const { store: projectStore } of listProjectStores()) {
attachStateStore(projectStore);
}
const unsubscribeProjectStoreRegistration = onProjectStoreRegistered((_projectId, projectStore) => {
const unsubscribeProjectStoreRegistration = subscribeProjectStoreRegistered((_projectId, projectStore) => {
attachStateStore(projectStore);
});