feat(FN-1808): merge fusion/fn-1808

This commit is contained in:
gsxdsm
2026-04-16 04:53:09 -07:00
parent 06383521b3
commit 72842d8687
10 changed files with 128 additions and 31 deletions

View File

@@ -49,6 +49,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
globalPause: false,
enginePaused: false,
maxConcurrent: 2,
maxTriageConcurrent: 2,
globalMaxConcurrent: 4,
maxWorktrees: 4,
pollIntervalMs: 15000,

View File

@@ -234,4 +234,94 @@ describe("getTaskCompletionBlocker", () => {
}, { resolveTask }))
.resolves.toBeUndefined();
});
// ── in-review as resolved dependency ───────────────────────────────────
it("returns undefined when a dependency is in-review", async () => {
const resolveTask = async (taskId: string) => {
if (taskId === "FN-001") {
return { id: "FN-001", column: "in-review" as const };
}
return null;
};
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
dependencies: ["FN-001"],
}, { resolveTask }))
.resolves.toBeUndefined();
});
it("returns undefined when dependencies are a mix of done and in-review", async () => {
const resolveTask = async (taskId: string) => {
if (taskId === "FN-001") {
return { id: "FN-001", column: "done" as const };
}
if (taskId === "FN-002") {
return { id: "FN-002", column: "in-review" as const };
}
return null;
};
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
dependencies: ["FN-001", "FN-002"],
}, { resolveTask }))
.resolves.toBeUndefined();
});
it("returns a reason when a dependency is in-progress", async () => {
const resolveTask = async (taskId: string) => {
if (taskId === "FN-001") {
return { id: "FN-001", column: "in-progress" as const };
}
return null;
};
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
dependencies: ["FN-001"],
}, { resolveTask }))
.resolves.toBe("task has unresolved dependencies: FN-001");
});
it("returns a reason when a dependency is in triage", async () => {
const resolveTask = async (taskId: string) => {
if (taskId === "FN-001") {
return { id: "FN-001", column: "triage" as const };
}
return null;
};
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
dependencies: ["FN-001"],
}, { resolveTask }))
.resolves.toBe("task has unresolved dependencies: FN-001");
});
it("returns a reason when a dependency is in todo", async () => {
const resolveTask = async (taskId: string) => {
if (taskId === "FN-001") {
return { id: "FN-001", column: "todo" as const };
}
return null;
};
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
dependencies: ["FN-001"],
}, { resolveTask }))
.resolves.toBe("task has unresolved dependencies: FN-001");
});
it("returns a reason when a dependency task does not exist", async () => {
const resolveTask = async (_taskId: string) => null;
await expect(getTaskCompletionBlocker({
...baseCompletionTask,
dependencies: ["FN-999"],
}, { resolveTask }))
.resolves.toBe("task has unresolved dependencies: FN-999");
});
});

View File

@@ -100,7 +100,7 @@ export async function getTaskCompletionBlocker(
for (const dependencyId of dependencies) {
const dependency = await options.resolveTask(dependencyId);
if (!dependency || (dependency.column !== "done" && dependency.column !== "archived")) {
if (!dependency || (dependency.column !== "done" && dependency.column !== "in-review" && dependency.column !== "archived")) {
unresolvedDependencies.push(dependencyId);
}
}

View File

@@ -982,6 +982,9 @@ export interface ProjectSettings {
/** Maximum number of concurrent AI agents across all activity types
* (triage specification, task execution, and merge operations). */
maxConcurrent: number;
/** Maximum number of concurrent triage/specification agents. When undefined,
* falls back to maxConcurrent. */
maxTriageConcurrent?: number;
/** System-wide maximum concurrent agents across ALL projects.
* When multiple projects are active, the sum of their in-flight agents
* will not exceed this limit. Applies to triage, execution, and merge.