feat(FN-3882): document clearStaleBlockedBy() in SelfHealingManager section
Added a single line documenting `clearStaleBlockedBy()` in the SelfHealingManager section of the architecture docs. Fusion-Task-Id: FN-3882
This commit is contained in:
5
.changeset/FN-3862-merge-attribution.md
Normal file
5
.changeset/FN-3862-merge-attribution.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Stop overwriting canonical merge commit SHAs on already-done tasks during self-healing reconciliation. Confirmed `mergeDetails.commitSha` is now preserved as authoritative; rediscovery for unconfirmed done tasks prefers the earliest owned commit so the original merge commit wins over later follow-up commits sharing the same `Fusion-Task-Id` trailer.
|
||||
5
.changeset/FN-3868-github-tracking-settings.md
Normal file
5
.changeset/FN-3868-github-tracking-settings.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Add global and project settings for GitHub issue tracking: global default tracking repo, project-level default tracking repo, per-project tracking toggle for new tasks, GitHub auth mode (`gh-cli` | `token`), and optional stored personal access token. This is foundational settings work for FN-3868 → FN-3876; behavior wiring ships in downstream subtasks.
|
||||
5
.changeset/FN-3877-triage-spec-compaction.md
Normal file
5
.changeset/FN-3877-triage-spec-compaction.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Triage: progressively compact large optional sections (subtask guidance, attachments, existing spec, user comments) of the spec prompt when the model's context window overflows, in addition to the existing project-memory compaction. Fixes failures on small-context models such as local vLLM Qwen3-30B (issue Runfusion/Fusion#62, FN-3877).
|
||||
7
.changeset/FN-3879-checkout-columns-self-heal.md
Normal file
7
.changeset/FN-3879-checkout-columns-self-heal.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Add a compatibility self-heal for legacy task databases that report `schemaVersion >= 20` but are missing checkout lease columns (`checkedOutBy`, `checkedOutAt`, `checkoutNodeId`, `checkoutRunId`, `checkoutLeaseRenewedAt`, `checkoutLeaseEpoch`).
|
||||
|
||||
On initialization, missing lease columns are now added idempotently before version-guarded migrations, matching the earlier `nodeId` mitigation pattern and preventing `no such column: checkoutNodeId` crashes in task listing paths.
|
||||
5
.changeset/fn-3783-reports-review-panel.md
Normal file
5
.changeset/fn-3783-reports-review-panel.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Add a multi-agent report review panel flow to the bundled reports plugin, including parallel reviewer orchestration, structured feedback parsing with retry, deterministic aggregation, and documented timeout/failure semantics.
|
||||
5
.changeset/fn-3864-paused-scope-exclusion.md
Normal file
5
.changeset/fn-3864-paused-scope-exclusion.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Scheduler: exclude paused in-review tasks from `activeScopes`. Paused failed-merge tasks no longer block dispatch of overlapping todo tasks via `blockedBy` re-stamping. (FN-3867)
|
||||
5
.changeset/fn-3864-recover-already-merged-review.md
Normal file
5
.changeset/fn-3864-recover-already-merged-review.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Add `recoverAlreadyMergedReviewTasks()` self-healing sweep to recover phantom-merge-guard false positives. Detects tasks whose content already landed on the integration branch (via Fusion-Task-Id trailer, branch ancestry, or git patch-id walk) and reconciles them to `done` with proper merge metadata.
|
||||
5
.changeset/fn-3878-restore-commitsha.md
Normal file
5
.changeset/fn-3878-restore-commitsha.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Restore canonical mergeDetails.commitSha for tasks FN-3794, FN-3814, FN-3829 whose attribution had been overwritten by self-healing reconciliation prior to the FN-3862 fix. Adds an idempotent restoration script (`scripts/restore-merge-sha-fn-3878.mjs`) for operators to re-verify or repair similar drift.
|
||||
5
.changeset/fn-3879-chat-rooms-wired.md
Normal file
5
.changeset/fn-3879-chat-rooms-wired.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Wire chat rooms UI to backend. Creating a room now persists via /api/chat/rooms, the sidebar lists real rooms, room threads load history and stream new messages over chat:room:* SSE events, and the FN-3807 "Coming soon" placeholder is gone.
|
||||
@@ -158,15 +158,24 @@ describe("CLI bundle output", () => {
|
||||
expect(existsSync(join(stagedRoot, "src", "process-manager.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("dist/plugins/fusion-plugin-dependency-graph/ is staged with a valid manifest", () => {
|
||||
it("dist/plugins/fusion-plugin-dependency-graph/ is staged as bundled runtime output", () => {
|
||||
const stagedRoot = join(cliRoot, "dist", "plugins", "fusion-plugin-dependency-graph");
|
||||
const manifestPath = join(stagedRoot, "manifest.json");
|
||||
const packageJsonPath = join(stagedRoot, "package.json");
|
||||
|
||||
expect(existsSync(manifestPath)).toBe(true);
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
|
||||
expect(manifest.id).toBe("fusion-plugin-dependency-graph");
|
||||
expect(typeof manifest.name).toBe("string");
|
||||
expect(manifest.name?.length).toBeGreaterThan(0);
|
||||
|
||||
expect(existsSync(join(stagedRoot, "bundled.js"))).toBe(true);
|
||||
expect(existsSync(join(stagedRoot, "src"))).toBe(false);
|
||||
|
||||
const stagedPkg = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
||||
exports?: { "."?: { import?: string } };
|
||||
};
|
||||
expect(stagedPkg.exports?.["."]?.import).toBe("./bundled.js");
|
||||
});
|
||||
|
||||
it("dist/plugins/fusion-plugin-whatsapp-chat/ is staged with a valid manifest", () => {
|
||||
|
||||
@@ -98,17 +98,20 @@ describe("CLI package.json publishing config", () => {
|
||||
);
|
||||
|
||||
function extractStringArray(name: string): string[] {
|
||||
const m = tsupRaw.match(new RegExp(`${name}:\\s*\\[([\\s\\S]*?)\\]`, "m"));
|
||||
if (!m) return [];
|
||||
return [...m[1].matchAll(/["']([^"']+)["']/g)].map((mm) => mm[1]);
|
||||
const matches = [...tsupRaw.matchAll(new RegExp(`${name}:\\s*\\[([\\s\\S]*?)\\]`, "gm"))];
|
||||
const values = matches.flatMap((m) =>
|
||||
[...m[1].matchAll(/["']([^"']+)["']/g)].map((mm) => mm[1]),
|
||||
);
|
||||
return [...new Set(values)];
|
||||
}
|
||||
|
||||
function extractRegexes(name: string): RegExp[] {
|
||||
const m = tsupRaw.match(new RegExp(`${name}:\\s*\\[([\\s\\S]*?)\\]`, "m"));
|
||||
if (!m) return [];
|
||||
const matches = [...tsupRaw.matchAll(new RegExp(`${name}:\\s*\\[([\\s\\S]*?)\\]`, "gm"))];
|
||||
// Match `/PATTERN/flags` where PATTERN may contain escaped slashes (`\/`).
|
||||
return [...m[1].matchAll(/\/((?:\\\/|[^/\n])+)\/[gimsuy]*/g)].map(
|
||||
(mm) => new RegExp(mm[1].replace(/\\\//g, "/")),
|
||||
return matches.flatMap((m) =>
|
||||
[...m[1].matchAll(/\/((?:\\\/|[^/\n])+)\/[gimsuy]*/g)].map(
|
||||
(mm) => new RegExp(mm[1].replace(/\\\//g, "/")),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,6 +128,8 @@ describe("CLI package.json publishing config", () => {
|
||||
"cpu-features": "transitive dep of dockerode (via ssh2)",
|
||||
"@homebridge/node-pty-prebuilt-multiarch":
|
||||
"aliased as node-pty in dependencies; the alias entry satisfies the import",
|
||||
"@fusion/core": "plugin-entry bundling external only; not a runtime dep of the CLI bin",
|
||||
"@fusion/engine": "plugin-entry bundling external only; not a runtime dep of the CLI bin",
|
||||
};
|
||||
|
||||
it("parses externals from tsup.config.ts", () => {
|
||||
|
||||
@@ -195,9 +195,9 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("resolvePluginEntryPath", () => {
|
||||
it("prefers src/index.ts over bundled.js when both exist in workspace contexts", () => {
|
||||
it("prefers bundled.js when both bundled and source entries exist", () => {
|
||||
mockExistsSync.mockImplementation((p: string) => p.endsWith("/src/index.ts") || p.endsWith("/bundled.js"));
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/src/index.ts");
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/bundled.js");
|
||||
});
|
||||
|
||||
it("prefers bundled.js when source entry is unavailable", () => {
|
||||
@@ -205,14 +205,14 @@ describe("resolvePluginEntryPath", () => {
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/bundled.js");
|
||||
});
|
||||
|
||||
it("prefers src/index.ts over dist/index.js in workspace contexts", () => {
|
||||
it("prefers dist/index.js when bundled.js is unavailable", () => {
|
||||
mockExistsSync.mockImplementation((p: string) => p.endsWith("/src/index.ts") || p.endsWith("/dist/index.js"));
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/src/index.ts");
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/dist/index.js");
|
||||
});
|
||||
|
||||
it("falls back to dist/index.js when source entry is unavailable", () => {
|
||||
mockExistsSync.mockImplementation((p: string) => p.endsWith("/dist/index.js"));
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/dist/index.js");
|
||||
it("falls back to src/index.ts for workspace-dev plugins without build outputs", () => {
|
||||
mockExistsSync.mockImplementation((p: string) => p.endsWith("/src/index.ts"));
|
||||
expect(resolvePluginEntryPath("/tmp/plugin")).toBe("/tmp/plugin/src/index.ts");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,10 +412,11 @@ describe("ensureBundledDependencyGraphPluginInstalled", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("registers Hermes from source entry when both src and dist entries exist", async () => {
|
||||
it("registers Hermes from bundled.js when bundled, src, and dist entries all exist", async () => {
|
||||
const manifest = makeManifest({ id: HERMES_PLUGIN_ID, name: "Hermes Runtime" });
|
||||
mockExistsSync.mockImplementation((p: string) => {
|
||||
if (p.endsWith("manifest.json") && p.includes(HERMES_PLUGIN_ID)) return true;
|
||||
if (p.endsWith("/bundled.js") && p.includes(HERMES_PLUGIN_ID)) return true;
|
||||
if (p.endsWith("/src/index.ts") && p.includes(HERMES_PLUGIN_ID)) return true;
|
||||
if (p.endsWith("/dist/index.js") && p.includes(HERMES_PLUGIN_ID)) return true;
|
||||
return false;
|
||||
@@ -434,6 +435,6 @@ describe("ensureBundledDependencyGraphPluginInstalled", () => {
|
||||
|
||||
expect(result).toBe("installed");
|
||||
const registerCall = store.registerPlugin.mock.calls[0]?.[0] as { path: string };
|
||||
expect(registerCall.path).toContain(`${HERMES_PLUGIN_ID}/src/index.ts`);
|
||||
expect(registerCall.path).toContain(`${HERMES_PLUGIN_ID}/bundled.js`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,16 +64,16 @@ function resolveBundledPluginDir(pluginId: string): string | null {
|
||||
* Resolve the actual loadable entry FILE path for a plugin directory. Node ESM
|
||||
* does not allow directory imports, so we must register the explicit file the
|
||||
* loader will dynamic-import. Preference order:
|
||||
* 1. ./src/index.ts (workspace/dev source of truth)
|
||||
* 2. ./bundled.js (esbuild-bundled, ships in npm tarball)
|
||||
* 3. ./dist/index.js
|
||||
* 1. ./bundled.js (esbuild-bundled, shipped in npm tarball)
|
||||
* 2. ./dist/index.js (legacy prebuilt fallback)
|
||||
* 3. ./src/index.ts (workspace/dev fallback when no bundle exists)
|
||||
* 4. fall back to the directory itself
|
||||
*/
|
||||
export function resolvePluginEntryPath(pluginDir: string): string {
|
||||
const candidates = [
|
||||
join(pluginDir, "src", "index.ts"),
|
||||
join(pluginDir, "bundled.js"),
|
||||
join(pluginDir, "dist", "index.js"),
|
||||
join(pluginDir, "src", "index.ts"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (existsSync(candidate)) {
|
||||
|
||||
@@ -54,6 +54,76 @@ const dashboardClientStub = `<!doctype html>
|
||||
</html>
|
||||
`;
|
||||
|
||||
type BundlePluginEntryOptions = {
|
||||
pluginId: string;
|
||||
srcDir: string;
|
||||
destDir: string;
|
||||
withMcpAsset?: boolean;
|
||||
};
|
||||
|
||||
async function bundlePluginEntry({ pluginId, srcDir, destDir, withMcpAsset = false }: BundlePluginEntryOptions) {
|
||||
if (existsSync(destDir)) {
|
||||
rmSync(destDir, { recursive: true, force: true });
|
||||
}
|
||||
if (!existsSync(srcDir)) {
|
||||
console.warn(
|
||||
`WARNING: Plugin source not found at ${srcDir}; ${pluginId} will be unavailable in the published package.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
cpSync(join(srcDir, "manifest.json"), join(destDir, "manifest.json"));
|
||||
|
||||
const srcPkg = JSON.parse(readFileSync(join(srcDir, "package.json"), "utf-8"));
|
||||
const destPkg = {
|
||||
name: srcPkg.name,
|
||||
version: srcPkg.version,
|
||||
type: "module",
|
||||
exports: { ".": { import: "./bundled.js" } },
|
||||
private: true,
|
||||
};
|
||||
writeFileSync(join(destDir, "package.json"), JSON.stringify(destPkg, null, 2));
|
||||
|
||||
const srcEntry = join(srcDir, "src", "index.ts");
|
||||
const builtEntry = join(srcDir, "dist", "index.js");
|
||||
const entry = existsSync(srcEntry) ? srcEntry : builtEntry;
|
||||
if (!existsSync(entry)) {
|
||||
throw new Error(`No entry found for ${pluginId} (looked for src/index.ts and dist/index.js)`);
|
||||
}
|
||||
|
||||
await esbuildBuild({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
outfile: join(destDir, "bundled.js"),
|
||||
external: ["@fusion/core", "@fusion/engine"],
|
||||
alias: {
|
||||
"@fusion/plugin-sdk": join(__dirname, "..", "plugin-sdk", "src", "index.ts"),
|
||||
},
|
||||
logLevel: "warning",
|
||||
});
|
||||
|
||||
if (withMcpAsset) {
|
||||
const mcpServerAsset = join(srcDir, "src", "mcp-schema-server.cjs");
|
||||
if (!existsSync(mcpServerAsset)) {
|
||||
throw new Error(
|
||||
`[tsup] Missing required bridge asset for ${pluginId} at ${mcpServerAsset}; expected committed source file mcp-schema-server.cjs.`,
|
||||
);
|
||||
}
|
||||
cpSync(mcpServerAsset, join(destDir, "mcp-schema-server.cjs"));
|
||||
}
|
||||
|
||||
const bundledOutput = join(destDir, "bundled.js");
|
||||
if (!existsSync(bundledOutput)) {
|
||||
throw new Error(`[tsup] Missing bundled output for ${pluginId}: expected ${bundledOutput}`);
|
||||
}
|
||||
|
||||
console.log(`Bundled plugin ${pluginId} to dist/plugins/${pluginId}/bundled.js`);
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/bin.ts", "src/extension.ts"],
|
||||
format: ["esm"],
|
||||
@@ -137,20 +207,11 @@ export default defineConfig({
|
||||
);
|
||||
}
|
||||
|
||||
if (existsSync(dependencyGraphPluginDest)) {
|
||||
rmSync(dependencyGraphPluginDest, { recursive: true, force: true });
|
||||
}
|
||||
if (existsSync(dependencyGraphPluginSrc)) {
|
||||
mkdirSync(dependencyGraphPluginDest, { recursive: true });
|
||||
cpSync(join(dependencyGraphPluginSrc, "manifest.json"), join(dependencyGraphPluginDest, "manifest.json"));
|
||||
cpSync(join(dependencyGraphPluginSrc, "package.json"), join(dependencyGraphPluginDest, "package.json"));
|
||||
cpSync(join(dependencyGraphPluginSrc, "src"), join(dependencyGraphPluginDest, "src"), { recursive: true });
|
||||
console.log("Copied dependency graph plugin to dist/plugins/fusion-plugin-dependency-graph/");
|
||||
} else {
|
||||
console.warn(
|
||||
`WARNING: Dependency graph plugin source not found at ${dependencyGraphPluginSrc}; bundled auto-install will be unavailable.`,
|
||||
);
|
||||
}
|
||||
await bundlePluginEntry({
|
||||
pluginId: "fusion-plugin-dependency-graph",
|
||||
srcDir: dependencyGraphPluginSrc,
|
||||
destDir: dependencyGraphPluginDest,
|
||||
});
|
||||
|
||||
if (existsSync(whatsappChatPluginDest)) {
|
||||
rmSync(whatsappChatPluginDest, { recursive: true, force: true });
|
||||
@@ -200,71 +261,12 @@ export default defineConfig({
|
||||
// Bundle each runtime plugin into a self-contained ESM file so npm/npx
|
||||
// installs can load them without the workspace `@fusion/plugin-sdk`.
|
||||
for (const pluginId of RUNTIME_PLUGIN_IDS) {
|
||||
const pluginSrcDir = join(__dirname, "..", "..", "plugins", pluginId);
|
||||
const pluginDestDir = join(__dirname, "dist", "plugins", pluginId);
|
||||
|
||||
if (existsSync(pluginDestDir)) {
|
||||
rmSync(pluginDestDir, { recursive: true, force: true });
|
||||
}
|
||||
if (!existsSync(pluginSrcDir)) {
|
||||
console.warn(
|
||||
`WARNING: Runtime plugin source not found at ${pluginSrcDir}; ${pluginId} will be unavailable in the published package.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
mkdirSync(pluginDestDir, { recursive: true });
|
||||
|
||||
cpSync(join(pluginSrcDir, "manifest.json"), join(pluginDestDir, "manifest.json"));
|
||||
|
||||
// Stripped package.json: no dependencies (workspace SDK is inlined into
|
||||
// bundled.js), exports point at the bundle. Keeps the published tarball
|
||||
// self-contained without leaking workspace-only metadata.
|
||||
const srcPkg = JSON.parse(readFileSync(join(pluginSrcDir, "package.json"), "utf-8"));
|
||||
const destPkg = {
|
||||
name: srcPkg.name,
|
||||
version: srcPkg.version,
|
||||
type: "module",
|
||||
exports: { ".": { import: "./bundled.js" } },
|
||||
private: true,
|
||||
};
|
||||
writeFileSync(join(pluginDestDir, "package.json"), JSON.stringify(destPkg, null, 2));
|
||||
|
||||
// Pick the best available entry: built dist/index.js if present, else
|
||||
// raw src/index.ts (esbuild can transpile TS).
|
||||
const builtEntry = join(pluginSrcDir, "dist", "index.js");
|
||||
const srcEntry = join(pluginSrcDir, "src", "index.ts");
|
||||
const entry = existsSync(builtEntry) ? builtEntry : srcEntry;
|
||||
|
||||
if (!existsSync(entry)) {
|
||||
console.warn(`WARNING: No entry found for ${pluginId} (looked for dist/index.js and src/index.ts)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await esbuildBuild({
|
||||
entryPoints: [entry],
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node22",
|
||||
outfile: join(pluginDestDir, "bundled.js"),
|
||||
// @fusion/core and @fusion/engine are loaded by the host process at
|
||||
// runtime; the SDK is inlined.
|
||||
external: ["@fusion/core", "@fusion/engine"],
|
||||
logLevel: "warning",
|
||||
await bundlePluginEntry({
|
||||
pluginId,
|
||||
srcDir: join(__dirname, "..", "..", "plugins", pluginId),
|
||||
destDir: join(__dirname, "dist", "plugins", pluginId),
|
||||
withMcpAsset: RUNTIME_PLUGINS_WITH_MCP_SCHEMA_SERVER.has(pluginId),
|
||||
});
|
||||
|
||||
if (RUNTIME_PLUGINS_WITH_MCP_SCHEMA_SERVER.has(pluginId)) {
|
||||
const mcpServerAsset = join(pluginSrcDir, "src", "mcp-schema-server.cjs");
|
||||
if (!existsSync(mcpServerAsset)) {
|
||||
throw new Error(
|
||||
`[tsup] Missing required bridge asset for ${pluginId} at ${mcpServerAsset}; expected committed source file mcp-schema-server.cjs.`,
|
||||
);
|
||||
}
|
||||
cpSync(mcpServerAsset, join(pluginDestDir, "mcp-schema-server.cjs"));
|
||||
}
|
||||
|
||||
console.log(`Bundled runtime plugin ${pluginId} to dist/plugins/${pluginId}/bundled.js`);
|
||||
}
|
||||
|
||||
if (existsSync(dashboardClientDest)) {
|
||||
|
||||
Reference in New Issue
Block a user