FN-6962: ignore hidden overlap paths by default
Hidden dot paths no longer create file-overlap blockers unless operators opt back into legacy behavior. - Add the ignoreHiddenOverlapPaths project setting, schema default, API sanitization, and Settings UI toggle. - Filter hidden dot segments from scheduler, gridlock, and self-healing overlap checks by default while preserving explicit ignore paths. - Cover default and opt-out behavior across core parity, dashboard settings routes, scheduler, gridlock, and self-healing tests. - Document the setting and add a patch changeset for the published CLI package. Files changed: .changeset/fn-6962-hidden-overlap-paths.md | 7 ++ docs/settings-reference.md | 1 + .../core/src/__tests__/settings-parity.test.ts | 7 ++ packages/core/src/settings-schema.ts | 1 + packages/core/src/types.ts | 7 ++ .../dashboard/app/components/SettingsModal.tsx | 4 +- .../components/__tests__/SettingsModal.test.tsx | 69 ++++++++++++++ .../settings/sections/SchedulingSection.tsx | 10 ++ .../src/__tests__/routes-settings.test.ts | 30 ++++++ packages/dashboard/src/routes.ts | 9 ++ .../src/routes/register-settings-memory-routes.ts | 6 +- .../engine/src/__tests__/gridlock-detector.test.ts | 32 +++++++ .../__tests__/scheduler-overlap-starvation.test.ts | 102 ++++++++++++++++++++- packages/engine/src/__tests__/self-healing.test.ts | 24 +++++ packages/engine/src/gridlock-detector.ts | 5 +- packages/engine/src/scheduler.ts | 40 +++++--- packages/engine/src/self-healing.ts | 2 +- packages/i18n/locales/en/app.json | 2 + packages/i18n/locales/es/app.json | 11 ++- packages/i18n/locales/fr/app.json | 11 ++- packages/i18n/locales/ko/app.json | 11 ++- packages/i18n/locales/zh-CN/app.json | 11 ++- packages/i18n/locales/zh-TW/app.json | 11 ++- packages/i18n/src/resources.d.ts | 2 + 24 files changed, 379 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-6962 Fusion-Task-Lineage: c79120d3-fd9e-439d-b2cf-439f9e2e6952
This commit is contained in:
7
.changeset/fn-6962-hidden-overlap-paths.md
Normal file
7
.changeset/fn-6962-hidden-overlap-paths.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Ignore hidden dot paths in overlap scheduling by default with a Settings toggle.
|
||||||
|
category: fix
|
||||||
|
dev: Adds project setting `ignoreHiddenOverlapPaths` and keeps `overlapIgnorePaths` as an additional explicit filter.
|
||||||
@@ -328,6 +328,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
|||||||
|
|
||||||
| `groupOverlappingFiles` | `boolean` | `true` | Serialize execution when file scopes overlap. |
|
| `groupOverlappingFiles` | `boolean` | `true` | Serialize execution when file scopes overlap. |
|
||||||
| `pluginTrustPolicy` | `"off" | "warn" | "enforce"` | `"warn"` | Plugin provenance enforcement mode: `off` records verification metadata only, `warn` blocks only `invalid` signatures, `enforce` allows only `verified-trusted` or `trusted-local`. |
|
| `pluginTrustPolicy` | `"off" | "warn" | "enforce"` | `"warn"` | Plugin provenance enforcement mode: `off` records verification metadata only, `warn` blocks only `invalid` signatures, `enforce` allows only `verified-trusted` or `trusted-local`. |
|
||||||
|
| `ignoreHiddenOverlapPaths` | `boolean` | `true` | Exclude hidden dot paths from overlap serialization by default. A hidden path is any normalized project-relative path with a segment beginning with `.`, such as `.fusion/tasks/FN-1/PROMPT.md`, `.changeset/fix.md`, `.github/workflows/ci.yml`, `.env`, or `packages/.cache/out.js`. Set to `false` to restore legacy strict counting of dot paths. Explicit `overlapIgnorePaths` entries still apply in addition to this default filter, and still apply when hidden-path filtering is disabled. |
|
||||||
| `overlapIgnorePaths` | `string[]` | `[]` | Optional project-relative file or directory paths to exclude from overlap blocking (for example `docs` or `generated/openapi.json`). Entries are trimmed, deduplicated, and must not be absolute or contain `..` traversal. |
|
| `overlapIgnorePaths` | `string[]` | `[]` | Optional project-relative file or directory paths to exclude from overlap blocking (for example `docs` or `generated/openapi.json`). Entries are trimmed, deduplicated, and must not be absolute or contain `..` traversal. |
|
||||||
| `autoMerge` | `boolean` | `true` | Auto-finalize tasks from `in-review`. Tasks can override this per-task (including at create time in New Task modal via **Auto-merge** = Default/Enabled/Disabled); explicit overrides are tagged with `autoMergeProvenance: "user"`, while tasks left at **Default** keep following the live global setting and do not snapshot it when entering review. Legacy pre-FN-6245 in-review rows that were stamped `autoMerge: true` are marked `autoMergeProvenance: "legacy-stamp"` on startup and can be inspected/cleared with Settings → Merge → **Legacy auto-merge stamp cleanup**, `fn pr automerge-cleanup [--apply] [--json]`, or `reconcileLegacyAutoMergeStamps({ apply: true })` after operator review. For grouped branch flows, per-task `autoMerge` governs member→group-integration landing while group `autoMerge` governs group→default-branch promotion eligibility. |
|
| `autoMerge` | `boolean` | `true` | Auto-finalize tasks from `in-review`. Tasks can override this per-task (including at create time in New Task modal via **Auto-merge** = Default/Enabled/Disabled); explicit overrides are tagged with `autoMergeProvenance: "user"`, while tasks left at **Default** keep following the live global setting and do not snapshot it when entering review. Legacy pre-FN-6245 in-review rows that were stamped `autoMerge: true` are marked `autoMergeProvenance: "legacy-stamp"` on startup and can be inspected/cleared with Settings → Merge → **Legacy auto-merge stamp cleanup**, `fn pr automerge-cleanup [--apply] [--json]`, or `reconcileLegacyAutoMergeStamps({ apply: true })` after operator review. For grouped branch flows, per-task `autoMerge` governs member→group-integration landing while group `autoMerge` governs group→default-branch promotion eligibility. |
|
||||||
| `maxAutoMergeRetries` | `number` | `3` | Project-scoped positive-integer cap for auto-merge conflict-resolution retries before Fusion parks or bounces a task for human/recovery handling. Unset, non-finite, zero, or negative values fall back to `3` to preserve historical behavior. |
|
| `maxAutoMergeRetries` | `number` | `3` | Project-scoped positive-integer cap for auto-merge conflict-resolution retries before Fusion parks or bounces a task for human/recovery handling. Unset, non-finite, zero, or negative values fall back to `3` to preserve historical behavior. |
|
||||||
|
|||||||
@@ -91,6 +91,13 @@ describe("settings key parity", () => {
|
|||||||
expect(DEFAULT_PROJECT_SETTINGS.heartbeatMultiplier).toBe(1);
|
expect(DEFAULT_PROJECT_SETTINGS.heartbeatMultiplier).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("defaults hidden overlap path filtering on and keeps it project-scoped", () => {
|
||||||
|
expect(DEFAULT_PROJECT_SETTINGS.ignoreHiddenOverlapPaths).toBe(true);
|
||||||
|
expect(isProjectSettingsKey("ignoreHiddenOverlapPaths")).toBe(true);
|
||||||
|
expect(isGlobalSettingsKey("ignoreHiddenOverlapPaths")).toBe(false);
|
||||||
|
expect(PROJECT_SETTINGS_KEYS).toContain("ignoreHiddenOverlapPaths");
|
||||||
|
});
|
||||||
|
|
||||||
it("defaults autoClaimCandidatesInPrompt to 5 and keeps it project-scoped", () => {
|
it("defaults autoClaimCandidatesInPrompt to 5 and keeps it project-scoped", () => {
|
||||||
expect(DEFAULT_PROJECT_SETTINGS.autoClaimCandidatesInPrompt).toBe(5);
|
expect(DEFAULT_PROJECT_SETTINGS.autoClaimCandidatesInPrompt).toBe(5);
|
||||||
expect(isProjectSettingsKey("autoClaimCandidatesInPrompt")).toBe(true);
|
expect(isProjectSettingsKey("autoClaimCandidatesInPrompt")).toBe(true);
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
|||||||
heartbeatScopeDiscipline: "strict",
|
heartbeatScopeDiscipline: "strict",
|
||||||
heartbeatPromptTemplate: "default",
|
heartbeatPromptTemplate: "default",
|
||||||
groupOverlappingFiles: true,
|
groupOverlappingFiles: true,
|
||||||
|
ignoreHiddenOverlapPaths: true,
|
||||||
overlapIgnorePaths: [],
|
overlapIgnorePaths: [],
|
||||||
autoMerge: true,
|
autoMerge: true,
|
||||||
// U18 (R15): the Review-response loop is default-on. Independent of `autoMerge` —
|
// U18 (R15): the Review-response loop is default-on. Independent of `autoMerge` —
|
||||||
|
|||||||
@@ -3650,6 +3650,13 @@ export interface ProjectSettings {
|
|||||||
*/
|
*/
|
||||||
heartbeatPromptTemplate?: HeartbeatPromptTemplate;
|
heartbeatPromptTemplate?: HeartbeatPromptTemplate;
|
||||||
groupOverlappingFiles: boolean;
|
groupOverlappingFiles: boolean;
|
||||||
|
/**
|
||||||
|
* When true (default), file-overlap serialization ignores project-relative paths
|
||||||
|
* containing any hidden dot segment (for example `.fusion/`, `.changeset/`,
|
||||||
|
* `.github/`, `.env`, or `packages/.cache/out.js`). Set false to restore the
|
||||||
|
* legacy behavior that counts hidden paths as overlap blockers.
|
||||||
|
*/
|
||||||
|
ignoreHiddenOverlapPaths?: boolean;
|
||||||
/** File/directory paths to ignore when evaluating overlap serialization.
|
/** File/directory paths to ignore when evaluating overlap serialization.
|
||||||
* Entries are project-relative paths (for example: `docs/README.md`, `docs/`, `generated/*`).
|
* Entries are project-relative paths (for example: `docs/README.md`, `docs/`, `generated/*`).
|
||||||
* Absolute paths and `..` traversal are not allowed.
|
* Absolute paths and `..` traversal are not allowed.
|
||||||
|
|||||||
@@ -711,6 +711,7 @@ export function SettingsModal({
|
|||||||
pollIntervalMs: 15000,
|
pollIntervalMs: 15000,
|
||||||
heartbeatMultiplier: 1,
|
heartbeatMultiplier: 1,
|
||||||
groupOverlappingFiles: true,
|
groupOverlappingFiles: true,
|
||||||
|
ignoreHiddenOverlapPaths: true,
|
||||||
overlapIgnorePaths: [],
|
overlapIgnorePaths: [],
|
||||||
autoMerge: true,
|
autoMerge: true,
|
||||||
mergeStrategy: "direct",
|
mergeStrategy: "direct",
|
||||||
@@ -986,6 +987,7 @@ export function SettingsModal({
|
|||||||
.then(([s, scoped]) => {
|
.then(([s, scoped]) => {
|
||||||
const normalizedSettings = {
|
const normalizedSettings = {
|
||||||
...s,
|
...s,
|
||||||
|
ignoreHiddenOverlapPaths: s.ignoreHiddenOverlapPaths ?? true,
|
||||||
mergeIntegrationWorktree: normalizeMergeIntegrationWorktreeMode(s.mergeIntegrationWorktree),
|
mergeIntegrationWorktree: normalizeMergeIntegrationWorktreeMode(s.mergeIntegrationWorktree),
|
||||||
mergeAdvanceAutoSync: normalizeMergeAdvanceAutoSyncMode(s.mergeAdvanceAutoSync),
|
mergeAdvanceAutoSync: normalizeMergeAdvanceAutoSyncMode(s.mergeAdvanceAutoSync),
|
||||||
maxAutoMergeRetries: resolveMaxAutoMergeRetriesForSettingsForm(s),
|
maxAutoMergeRetries: resolveMaxAutoMergeRetriesForSettingsForm(s),
|
||||||
@@ -2025,7 +2027,7 @@ export function SettingsModal({
|
|||||||
setImportFile(null);
|
setImportFile(null);
|
||||||
// Refresh settings to show imported values
|
// Refresh settings to show imported values
|
||||||
const refreshed = await fetchSettings(projectId);
|
const refreshed = await fetchSettings(projectId);
|
||||||
setForm(refreshed);
|
setForm({ ...refreshed, ignoreHiddenOverlapPaths: refreshed.ignoreHiddenOverlapPaths ?? true });
|
||||||
} else {
|
} else {
|
||||||
addToast(result.error || t("settings.importExport.importFailed", "Import failed"), "error");
|
addToast(result.error || t("settings.importExport.importFailed", "Import failed"), "error");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ const defaultSettings = {
|
|||||||
maxWorktrees: 4,
|
maxWorktrees: 4,
|
||||||
pollIntervalMs: 15000,
|
pollIntervalMs: 15000,
|
||||||
groupOverlappingFiles: true,
|
groupOverlappingFiles: true,
|
||||||
|
ignoreHiddenOverlapPaths: true,
|
||||||
overlapIgnorePaths: [],
|
overlapIgnorePaths: [],
|
||||||
autoMerge: true,
|
autoMerge: true,
|
||||||
mergeStrategy: "direct",
|
mergeStrategy: "direct",
|
||||||
@@ -2873,6 +2874,74 @@ describe("SettingsModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Scheduling overlap ignore paths", () => {
|
describe("Scheduling overlap ignore paths", () => {
|
||||||
|
it("defaults hidden overlap path filtering checked when settings omit the key", async () => {
|
||||||
|
const { ignoreHiddenOverlapPaths: _omitted, ...settingsWithoutHiddenDefault } = defaultSettings;
|
||||||
|
mockFetchSettings.mockResolvedValue(settingsWithoutHiddenDefault);
|
||||||
|
mockFetchSettingsByScope.mockResolvedValue({ global: settingsWithoutHiddenDefault, project: {} });
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/ignore hidden dot paths in overlap checks/i)).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders saved false for hidden overlap path filtering", async () => {
|
||||||
|
mockFetchSettings.mockResolvedValue({
|
||||||
|
...defaultSettings,
|
||||||
|
ignoreHiddenOverlapPaths: false,
|
||||||
|
});
|
||||||
|
mockFetchSettingsByScope.mockResolvedValue({ global: defaultSettings, project: { ignoreHiddenOverlapPaths: false } });
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/ignore hidden dot paths in overlap checks/i)).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends hidden overlap filtering false without disrupting explicit ignore paths", async () => {
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByLabelText(/ignore hidden dot paths in overlap checks/i));
|
||||||
|
await userEvent.type(screen.getByPlaceholderText("docs/"), "generated/*");
|
||||||
|
await userEvent.click(screen.getByText("Save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = mockUpdateSettings.mock.calls[0][0];
|
||||||
|
expect(payload.ignoreHiddenOverlapPaths).toBe(false);
|
||||||
|
expect(payload.overlapIgnorePaths).toEqual(["generated/*"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends hidden overlap filtering true after toggling saved false", async () => {
|
||||||
|
mockFetchSettings.mockResolvedValue({
|
||||||
|
...defaultSettings,
|
||||||
|
ignoreHiddenOverlapPaths: false,
|
||||||
|
});
|
||||||
|
mockFetchSettingsByScope.mockResolvedValue({ global: defaultSettings, project: { ignoreHiddenOverlapPaths: false } });
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
await waitFor(() => expect(mockFetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Scheduling & Capacity"));
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByLabelText(/ignore hidden dot paths in overlap checks/i));
|
||||||
|
await userEvent.click(screen.getByText("Save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockUpdateSettings).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
expect(mockUpdateSettings.mock.calls[0][0].ignoreHiddenOverlapPaths).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("renders existing overlap ignore paths from settings", async () => {
|
it("renders existing overlap ignore paths from settings", async () => {
|
||||||
mockFetchSettings.mockResolvedValue({
|
mockFetchSettings.mockResolvedValue({
|
||||||
...defaultSettings,
|
...defaultSettings,
|
||||||
|
|||||||
@@ -162,6 +162,16 @@ export function SchedulingSection({ scopeBanner, form, setForm, globalMaxConcurr
|
|||||||
<small>{t("settings.scheduling.whenEnabledTasksThatModifyTheSameFiles", "When enabled, tasks that modify the same files are queued serially to avoid merge conflicts")}</small>
|
<small>{t("settings.scheduling.whenEnabledTasksThatModifyTheSameFiles", "When enabled, tasks that modify the same files are queued serially to avoid merge conflicts")}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/**
|
||||||
|
* FNXC:SettingsScheduling 2026-06-23-13:22:
|
||||||
|
* Operators need a Scheduling toggle that defaults on for ignoring hidden dot paths in overlap checks while preserving the selected value independently of overlap serialization being enabled.
|
||||||
|
*/}
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="ignoreHiddenOverlapPaths" className="checkbox-label">
|
||||||
|
<input id="ignoreHiddenOverlapPaths" type="checkbox" checked={form.ignoreHiddenOverlapPaths !== false} onChange={(e) => setForm((f) => ({ ...f, ignoreHiddenOverlapPaths: e.target.checked }))}/>{t("settings.scheduling.ignoreHiddenDotPathsInOverlapChecks", " Ignore hidden dot paths in overlap checks ")}</label>
|
||||||
|
<small>{t("settings.scheduling.ignoreHiddenDotPathsHelp", "When enabled, overlap checks ignore hidden path segments such as .fusion/, .changeset/, .github/, .env, and nested .cache/ directories. Uncheck to restore legacy counting for stricter serialization.")}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group settings-overlap-ignore-group">
|
<div className="form-group settings-overlap-ignore-group">
|
||||||
<label>{t("settings.scheduling.ignoredOverlapPaths", "Ignored overlap paths")}</label>
|
<label>{t("settings.scheduling.ignoredOverlapPaths", "Ignored overlap paths")}</label>
|
||||||
<small>{t("settings.scheduling.optionalFileOrDirectoryPathsToIgnoreWhen", " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ")}<code>docs/</code>{t("settings.scheduling.or", " or ")}<code>generated/*</code>{t("settings.scheduling.closeParenPeriod", ").")}
|
<small>{t("settings.scheduling.optionalFileOrDirectoryPathsToIgnoreWhen", " Optional file or directory paths to ignore when overlap serialization is enabled. Paths are project-relative (for example ")}<code>docs/</code>{t("settings.scheduling.or", " or ")}<code>generated/*</code>{t("settings.scheduling.closeParenPeriod", ").")}
|
||||||
|
|||||||
@@ -773,6 +773,36 @@ describe("PUT /settings", () => {
|
|||||||
expect(store.updateSettings).toHaveBeenCalledWith({ maxConcurrent: 8, autoMerge: false });
|
expect(store.updateSettings).toHaveBeenCalledWith({ maxConcurrent: 8, autoMerge: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts boolean hidden-overlap filtering values", async () => {
|
||||||
|
const updatedSettings = { ...DEFAULT_SETTINGS, ignoreHiddenOverlapPaths: false };
|
||||||
|
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const res = await REQUEST(
|
||||||
|
buildApp(),
|
||||||
|
"PUT",
|
||||||
|
"/api/settings",
|
||||||
|
JSON.stringify({ ignoreHiddenOverlapPaths: false }),
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(store.updateSettings).toHaveBeenCalledWith({ ignoreHiddenOverlapPaths: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-boolean hidden-overlap filtering values", async () => {
|
||||||
|
const res = await REQUEST(
|
||||||
|
buildApp(),
|
||||||
|
"PUT",
|
||||||
|
"/api/settings",
|
||||||
|
JSON.stringify({ ignoreHiddenOverlapPaths: "false" }),
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body.error).toContain("ignoreHiddenOverlapPaths must be a boolean");
|
||||||
|
expect(store.updateSettings).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts valid nested evalSettings payload", async () => {
|
it("accepts valid nested evalSettings payload", async () => {
|
||||||
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
(store.updateSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
...DEFAULT_SETTINGS,
|
...DEFAULT_SETTINGS,
|
||||||
|
|||||||
@@ -532,6 +532,14 @@ function validateModelPresets(value: unknown): ModelPreset[] | undefined {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeBooleanSetting(name: string, value: unknown): boolean | undefined {
|
||||||
|
if (value === undefined || value === null) return undefined;
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw badRequest(`${name} must be a boolean`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeOverlapIgnorePaths(value: unknown): string[] | undefined {
|
function sanitizeOverlapIgnorePaths(value: unknown): string[] | undefined {
|
||||||
if (value === undefined || value === null) return undefined;
|
if (value === undefined || value === null) return undefined;
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
@@ -1049,6 +1057,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
|||||||
registerSettingsMemoryRoutes(routeContext, {
|
registerSettingsMemoryRoutes(routeContext, {
|
||||||
githubToken,
|
githubToken,
|
||||||
validateModelPresets,
|
validateModelPresets,
|
||||||
|
sanitizeBooleanSetting,
|
||||||
sanitizeOverlapIgnorePaths,
|
sanitizeOverlapIgnorePaths,
|
||||||
discoverDashboardPiExtensions,
|
discoverDashboardPiExtensions,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function __resetCreateFnAgentForInsights(): void {
|
|||||||
interface SettingsMemoryRouteDeps {
|
interface SettingsMemoryRouteDeps {
|
||||||
githubToken?: string;
|
githubToken?: string;
|
||||||
validateModelPresets: (input: unknown) => ModelPreset[] | undefined;
|
validateModelPresets: (input: unknown) => ModelPreset[] | undefined;
|
||||||
|
sanitizeBooleanSetting: (name: string, input: unknown) => boolean | undefined;
|
||||||
sanitizeOverlapIgnorePaths: (input: unknown) => string[] | undefined;
|
sanitizeOverlapIgnorePaths: (input: unknown) => string[] | undefined;
|
||||||
discoverDashboardPiExtensions: (cwd: string) => Promise<PiExtensionSettings>;
|
discoverDashboardPiExtensions: (cwd: string) => Promise<PiExtensionSettings>;
|
||||||
}
|
}
|
||||||
@@ -196,7 +197,7 @@ export function validateCloudflaredManifest(input: unknown): { ok: true } | { ok
|
|||||||
|
|
||||||
export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: SettingsMemoryRouteDeps): void {
|
export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: SettingsMemoryRouteDeps): void {
|
||||||
const { router, options, store, runtimeLogger, getProjectContext, rethrowAsApiError } = ctx;
|
const { router, options, store, runtimeLogger, getProjectContext, rethrowAsApiError } = ctx;
|
||||||
const { githubToken, validateModelPresets, sanitizeOverlapIgnorePaths, discoverDashboardPiExtensions } = deps;
|
const { githubToken, validateModelPresets, sanitizeBooleanSetting, sanitizeOverlapIgnorePaths, discoverDashboardPiExtensions } = deps;
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
// Query the local tailscaled for this node's tailnet DNS name and any
|
// Query the local tailscaled for this node's tailnet DNS name and any
|
||||||
@@ -603,6 +604,9 @@ export function registerSettingsMemoryRoutes(ctx: ApiRoutesContext, deps: Settin
|
|||||||
if (Object.prototype.hasOwnProperty.call(clientSettings, "modelPresets")) {
|
if (Object.prototype.hasOwnProperty.call(clientSettings, "modelPresets")) {
|
||||||
clientSettings.modelPresets = validateModelPresets(clientSettings.modelPresets);
|
clientSettings.modelPresets = validateModelPresets(clientSettings.modelPresets);
|
||||||
}
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(clientSettings, "ignoreHiddenOverlapPaths")) {
|
||||||
|
clientSettings.ignoreHiddenOverlapPaths = sanitizeBooleanSetting("ignoreHiddenOverlapPaths", clientSettings.ignoreHiddenOverlapPaths);
|
||||||
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(clientSettings, "overlapIgnorePaths")) {
|
if (Object.prototype.hasOwnProperty.call(clientSettings, "overlapIgnorePaths")) {
|
||||||
clientSettings.overlapIgnorePaths = sanitizeOverlapIgnorePaths(clientSettings.overlapIgnorePaths);
|
clientSettings.overlapIgnorePaths = sanitizeOverlapIgnorePaths(clientSettings.overlapIgnorePaths);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,38 @@ describe("GridlockDetector", () => {
|
|||||||
expect(event).toBeNull();
|
expect(event).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not report gridlock for hidden-only overlaps by default", async () => {
|
||||||
|
tasks = [
|
||||||
|
createTask("FN-1", { column: "todo" }),
|
||||||
|
createTask("FN-2", { column: "in-progress" }),
|
||||||
|
];
|
||||||
|
scopes = {
|
||||||
|
"FN-1": [".fusion/tasks/FN-1/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
"FN-2": [".fusion/tasks/FN-1/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = await detector.detectGridlock();
|
||||||
|
expect(event).toBeNull();
|
||||||
|
expect(onGridlock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports gridlock for hidden-only overlaps when legacy counting is restored", async () => {
|
||||||
|
settings = createSettings({ ignoreHiddenOverlapPaths: false });
|
||||||
|
tasks = [
|
||||||
|
createTask("FN-1", { column: "todo" }),
|
||||||
|
createTask("FN-2", { column: "in-progress" }),
|
||||||
|
];
|
||||||
|
scopes = {
|
||||||
|
"FN-1": [".fusion/tasks/FN-1/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
"FN-2": [".fusion/tasks/FN-1/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const event = await detector.detectGridlock();
|
||||||
|
expect(event?.blockedTaskIds).toEqual(["FN-1"]);
|
||||||
|
expect(event?.reasons).toEqual({ "FN-1": "overlap" });
|
||||||
|
expect(event?.blockingTaskIds).toEqual(["FN-2"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("respects overlap ignore paths from settings", async () => {
|
it("respects overlap ignore paths from settings", async () => {
|
||||||
settings = createSettings({ overlapIgnorePaths: ["docs/"] });
|
settings = createSettings({ overlapIgnorePaths: ["docs/"] });
|
||||||
tasks = [
|
tasks = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { Scheduler } from "../scheduler.js";
|
import { filterPathsByIgnoreList, Scheduler } from "../scheduler.js";
|
||||||
import type { Agent, AgentStore, Task, TaskStore } from "@fusion/core";
|
import type { Agent, AgentStore, Settings, Task, TaskStore } from "@fusion/core";
|
||||||
|
|
||||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||||
return {
|
return {
|
||||||
@@ -35,7 +35,7 @@ function createAgentStore(agents: Agent[]): AgentStore {
|
|||||||
} as unknown as AgentStore;
|
} as unknown as AgentStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStore(tasks: Task[], scopes: Record<string, string[]>): TaskStore {
|
function createStore(tasks: Task[], scopes: Record<string, string[]>, settings: Partial<Settings> = {}): TaskStore {
|
||||||
const updateTask = vi.fn(async (id: string, patch: Partial<Task>) => {
|
const updateTask = vi.fn(async (id: string, patch: Partial<Task>) => {
|
||||||
const task = tasks.find((candidate) => candidate.id === id);
|
const task = tasks.find((candidate) => candidate.id === id);
|
||||||
if (task) Object.assign(task, patch);
|
if (task) Object.assign(task, patch);
|
||||||
@@ -49,7 +49,7 @@ function createStore(tasks: Task[], scopes: Record<string, string[]>): TaskStore
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
listTasks: vi.fn(async () => tasks),
|
listTasks: vi.fn(async () => tasks),
|
||||||
getSettings: vi.fn(async () => ({ maxConcurrent: 10, maxWorktrees: 10, groupOverlappingFiles: true })),
|
getSettings: vi.fn(async () => ({ maxConcurrent: 10, maxWorktrees: 10, groupOverlappingFiles: true, ...settings })),
|
||||||
parseFileScopeFromPrompt: vi.fn(async (id: string) => scopes[id] ?? []),
|
parseFileScopeFromPrompt: vi.fn(async (id: string) => scopes[id] ?? []),
|
||||||
updateTask,
|
updateTask,
|
||||||
moveTask,
|
moveTask,
|
||||||
@@ -63,6 +63,57 @@ function createStore(tasks: Task[], scopes: Record<string, string[]>): TaskStore
|
|||||||
} as unknown as TaskStore;
|
} as unknown as TaskStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("filterPathsByIgnoreList", () => {
|
||||||
|
it("ignores hidden top-level files and directories by default", () => {
|
||||||
|
expect(filterPathsByIgnoreList([
|
||||||
|
".env",
|
||||||
|
".fusion/tasks/FN-1/PROMPT.md",
|
||||||
|
".changeset/fn-6962.md",
|
||||||
|
".github/workflows/ci.yml",
|
||||||
|
"src/foo.ts",
|
||||||
|
])).toEqual(["src/foo.ts"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores nested hidden directories and Windows separators by default", () => {
|
||||||
|
expect(filterPathsByIgnoreList([
|
||||||
|
"packages/.cache/out.js",
|
||||||
|
"packages\\.vite\\manifest.json",
|
||||||
|
"docs/readme.md",
|
||||||
|
])).toEqual(["docs/readme.md"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves hidden paths when legacy counting is explicitly restored", () => {
|
||||||
|
expect(filterPathsByIgnoreList([
|
||||||
|
".env",
|
||||||
|
"packages/.cache/out.js",
|
||||||
|
"src/foo.ts",
|
||||||
|
], undefined, { ignoreHiddenOverlapPaths: false })).toEqual([".env", "packages/.cache/out.js", "src/foo.ts"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies explicit ignores when hidden filtering is enabled", () => {
|
||||||
|
expect(filterPathsByIgnoreList([
|
||||||
|
".fusion/tasks/FN-1/PROMPT.md",
|
||||||
|
"docs/readme.md",
|
||||||
|
"generated/out.js",
|
||||||
|
"src/foo.ts",
|
||||||
|
], ["docs/", "generated/*"])).toEqual(["src/foo.ts"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies explicit ignores when hidden filtering is disabled", () => {
|
||||||
|
expect(filterPathsByIgnoreList([
|
||||||
|
".fusion/tasks/FN-1/PROMPT.md",
|
||||||
|
"docs/readme.md",
|
||||||
|
"generated/out.js",
|
||||||
|
"src/foo.ts",
|
||||||
|
], ["docs/", "generated/*"], { ignoreHiddenOverlapPaths: false })).toEqual([".fusion/tasks/FN-1/PROMPT.md", "src/foo.ts"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps visible paths for empty and blank explicit ignore lists", () => {
|
||||||
|
expect(filterPathsByIgnoreList(["src/foo.ts", "docs/readme.md"], [])).toEqual(["src/foo.ts", "docs/readme.md"]);
|
||||||
|
expect(filterPathsByIgnoreList(["src/foo.ts", "docs/readme.md"], ["", " "])).toEqual(["src/foo.ts", "docs/readme.md"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("scheduler overlap starvation regression (FN-057)", () => {
|
describe("scheduler overlap starvation regression (FN-057)", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
@@ -138,6 +189,49 @@ describe("scheduler overlap starvation regression (FN-057)", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores hidden-only overlap leases by default when scheduling todo work", async () => {
|
||||||
|
const tasks = [
|
||||||
|
makeTask({ id: "FN-039", column: "in-progress", priority: "normal" }),
|
||||||
|
makeTask({ id: "FN-030", column: "todo", priority: "urgent" }),
|
||||||
|
];
|
||||||
|
const store = createStore(tasks, {
|
||||||
|
"FN-039": [".fusion/tasks/FN-039/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
"FN-030": [".fusion/tasks/FN-039/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduler = new Scheduler(store);
|
||||||
|
(scheduler as any).running = true;
|
||||||
|
await scheduler.schedule();
|
||||||
|
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-030", "in-progress", expect.objectContaining({ allocateWorktree: expect.any(Function) }));
|
||||||
|
expect(store.updateTask).not.toHaveBeenCalledWith(
|
||||||
|
"FN-030",
|
||||||
|
expect.objectContaining({ status: "queued", overlapBlockedBy: "FN-039" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("counts hidden-only overlap leases when legacy counting is restored", async () => {
|
||||||
|
const tasks = [
|
||||||
|
makeTask({ id: "FN-039", column: "in-progress", priority: "normal" }),
|
||||||
|
makeTask({ id: "FN-030", column: "todo", priority: "urgent" }),
|
||||||
|
];
|
||||||
|
const store = createStore(tasks, {
|
||||||
|
"FN-039": [".fusion/tasks/FN-039/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
"FN-030": [".fusion/tasks/FN-039/PROMPT.md", "packages/.cache/out.js"],
|
||||||
|
}, { ignoreHiddenOverlapPaths: false });
|
||||||
|
|
||||||
|
const scheduler = new Scheduler(store);
|
||||||
|
(scheduler as any).running = true;
|
||||||
|
await scheduler.schedule();
|
||||||
|
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-030", {
|
||||||
|
status: "queued",
|
||||||
|
blockedBy: null,
|
||||||
|
overlapBlockedBy: "FN-039",
|
||||||
|
});
|
||||||
|
expect(store.moveTask).not.toHaveBeenCalledWith("FN-030", "in-progress", expect.anything());
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps active file-scope leases bounded while non-overlapping ready work proceeds", async () => {
|
it("keeps active file-scope leases bounded while non-overlapping ready work proceeds", async () => {
|
||||||
const tasks = [
|
const tasks = [
|
||||||
makeTask({ id: "FN-039", column: "in-progress", priority: "normal" }),
|
makeTask({ id: "FN-039", column: "in-progress", priority: "normal" }),
|
||||||
|
|||||||
@@ -9430,6 +9430,30 @@ describe("FN-5335 triple-proof no-action unit coverage", () => {
|
|||||||
manager.stop();
|
manager.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores hidden-only unmet dependency overlap when the setting is absent", async () => {
|
||||||
|
const { store, manager } = setup([
|
||||||
|
makeTask({ id: "FN-H", column: "in-progress", dependencies: ["FN-D"] }),
|
||||||
|
makeTask({ id: "FN-D", column: "todo" }),
|
||||||
|
], { "FN-H": [".fusion/tasks/FN-H/PROMPT.md"], "FN-D": [".fusion/tasks/FN-H/PROMPT.md"] });
|
||||||
|
vi.spyOn(manager as any, "evaluateBackwardMoveTripleProof").mockResolvedValue({ ok: true, metadata: {} });
|
||||||
|
|
||||||
|
await expect(manager.reconcileDependencyBlockingLeases()).resolves.toBe(0);
|
||||||
|
expect(store.moveTask).not.toHaveBeenCalled();
|
||||||
|
manager.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rebounds hidden-only unmet dependency overlap when legacy counting is restored", async () => {
|
||||||
|
const { store, manager } = setup([
|
||||||
|
makeTask({ id: "FN-H", column: "in-progress", dependencies: ["FN-D"] }),
|
||||||
|
makeTask({ id: "FN-D", column: "todo" }),
|
||||||
|
], { "FN-H": [".fusion/tasks/FN-H/PROMPT.md"], "FN-D": [".fusion/tasks/FN-H/PROMPT.md"] }, { ignoreHiddenOverlapPaths: false });
|
||||||
|
vi.spyOn(manager as any, "evaluateBackwardMoveTripleProof").mockResolvedValue({ ok: true, stalenessMs: 10_000, reason: "test", metadata: {} });
|
||||||
|
|
||||||
|
await expect(manager.reconcileDependencyBlockingLeases()).resolves.toBe(1);
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-H", "todo", expect.objectContaining({ moveSource: "engine", recoveryRehome: true }));
|
||||||
|
manager.stop();
|
||||||
|
});
|
||||||
|
|
||||||
it.each([{ userPaused: true }, { paused: true }])("does not rebound operator-paused holders: %o", async (pauseState) => {
|
it.each([{ userPaused: true }, { paused: true }])("does not rebound operator-paused holders: %o", async (pauseState) => {
|
||||||
const { store, manager } = setup([
|
const { store, manager } = setup([
|
||||||
makeTask({ id: "FN-H", column: "in-progress", dependencies: ["FN-D"], ...pauseState }),
|
makeTask({ id: "FN-H", column: "in-progress", dependencies: ["FN-D"], ...pauseState }),
|
||||||
|
|||||||
@@ -79,10 +79,11 @@ export class GridlockDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
|
const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
|
||||||
|
const filterOptions = { ignoreHiddenOverlapPaths: settings.ignoreHiddenOverlapPaths };
|
||||||
const activeScopes = new Map<string, string[]>();
|
const activeScopes = new Map<string, string[]>();
|
||||||
if (settings.groupOverlappingFiles) {
|
if (settings.groupOverlappingFiles) {
|
||||||
for (const task of active) {
|
for (const task of active) {
|
||||||
const scope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths);
|
const scope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths, filterOptions);
|
||||||
if (scope.length > 0) {
|
if (scope.length > 0) {
|
||||||
activeScopes.set(task.id, scope);
|
activeScopes.set(task.id, scope);
|
||||||
}
|
}
|
||||||
@@ -106,7 +107,7 @@ export class GridlockDetector {
|
|||||||
|
|
||||||
if (!settings.groupOverlappingFiles) continue;
|
if (!settings.groupOverlappingFiles) continue;
|
||||||
|
|
||||||
const taskScope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths);
|
const taskScope = filterPathsByIgnoreList(await this.store.parseFileScopeFromPrompt(task.id), overlapIgnorePaths, filterOptions);
|
||||||
if (taskScope.length === 0) continue;
|
if (taskScope.length === 0) continue;
|
||||||
|
|
||||||
for (const [activeId, activeScope] of activeScopes) {
|
for (const [activeId, activeScope] of activeScopes) {
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ function normalizeOverlapPath(path: string): string {
|
|||||||
return path.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
return path.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasHiddenOverlapPathSegment(path: string): boolean {
|
||||||
|
const normalizedPath = normalizeOverlapPath(path);
|
||||||
|
if (!normalizedPath) return false;
|
||||||
|
return normalizedPath.split("/").some((segment) => segment.startsWith("."));
|
||||||
|
}
|
||||||
|
|
||||||
function isIgnoredOverlapPath(path: string, ignorePath: string): boolean {
|
function isIgnoredOverlapPath(path: string, ignorePath: string): boolean {
|
||||||
const normalizedPath = normalizeOverlapPath(path);
|
const normalizedPath = normalizeOverlapPath(path);
|
||||||
const normalizedIgnore = normalizeOverlapPath(ignorePath);
|
const normalizedIgnore = normalizeOverlapPath(ignorePath);
|
||||||
@@ -114,22 +120,32 @@ function computeAutoClaimFingerprint(task: Task): string {
|
|||||||
].join("|");
|
].join("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FilterOverlapPathsOptions {
|
||||||
|
ignoreHiddenOverlapPaths?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove scope entries that match configured overlap-ignore paths.
|
* Remove scope entries that should not participate in file-overlap serialization.
|
||||||
* Used by scheduler overlap gating so shared safe paths (docs/generated/etc.)
|
*
|
||||||
* can bypass serialization while keeping overlap protection enabled globally.
|
* FNXC:OverlapScheduling 2026-06-23-13:09:
|
||||||
|
* Hidden dot paths are ignored by default for overlap blockers because task artifacts and hidden/generated metadata such as `.fusion/`, `.changeset/`, `.github/`, `.env`, and nested `.cache/` directories caused false serialization. Operators can set `ignoreHiddenOverlapPaths=false` to restore legacy counting, while explicit `overlapIgnorePaths` keep their exact/directory/glob-prefix semantics in either mode.
|
||||||
*/
|
*/
|
||||||
export function filterPathsByIgnoreList(paths: string[], ignorePaths?: string[]): string[] {
|
export function filterPathsByIgnoreList(
|
||||||
if (!ignorePaths || ignorePaths.length === 0) {
|
paths: string[],
|
||||||
|
ignorePaths?: string[],
|
||||||
|
options: FilterOverlapPathsOptions = {},
|
||||||
|
): string[] {
|
||||||
|
const shouldIgnoreHidden = options.ignoreHiddenOverlapPaths ?? true;
|
||||||
|
const normalizedIgnorePaths = (ignorePaths ?? []).map(normalizeOverlapPath).filter(Boolean);
|
||||||
|
|
||||||
|
if (!shouldIgnoreHidden && normalizedIgnorePaths.length === 0) {
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedIgnorePaths = ignorePaths.map(normalizeOverlapPath).filter(Boolean);
|
return paths.filter((path) => {
|
||||||
if (normalizedIgnorePaths.length === 0) {
|
if (shouldIgnoreHidden && hasHiddenOverlapPathSegment(path)) return false;
|
||||||
return paths;
|
return !normalizedIgnorePaths.some((ignore) => isIgnoredOverlapPath(path, ignore));
|
||||||
}
|
});
|
||||||
|
|
||||||
return paths.filter((path) => !normalizedIgnorePaths.some((ignore) => isIgnoredOverlapPath(path, ignore)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuedOverlapCandidate {
|
export interface QueuedOverlapCandidate {
|
||||||
@@ -1471,7 +1487,7 @@ export class Scheduler {
|
|||||||
const cached = filteredScopeByTaskId.get(taskId);
|
const cached = filteredScopeByTaskId.get(taskId);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
const scope = await this.store.parseFileScopeFromPrompt(taskId);
|
const scope = await this.store.parseFileScopeFromPrompt(taskId);
|
||||||
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
|
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths, { ignoreHiddenOverlapPaths: settings.ignoreHiddenOverlapPaths });
|
||||||
filteredScopeByTaskId.set(taskId, filteredScope);
|
filteredScopeByTaskId.set(taskId, filteredScope);
|
||||||
return filteredScope;
|
return filteredScope;
|
||||||
};
|
};
|
||||||
@@ -2147,7 +2163,7 @@ export class Scheduler {
|
|||||||
const cached = filteredScopeByTaskId.get(taskId);
|
const cached = filteredScopeByTaskId.get(taskId);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
const scope = await this.store.parseFileScopeFromPrompt(taskId);
|
const scope = await this.store.parseFileScopeFromPrompt(taskId);
|
||||||
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
|
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths, { ignoreHiddenOverlapPaths: settings.ignoreHiddenOverlapPaths });
|
||||||
filteredScopeByTaskId.set(taskId, filteredScope);
|
filteredScopeByTaskId.set(taskId, filteredScope);
|
||||||
return filteredScope;
|
return filteredScope;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5033,7 +5033,7 @@ export class SelfHealingManager {
|
|||||||
const cached = filteredScopeByTaskId.get(taskId);
|
const cached = filteredScopeByTaskId.get(taskId);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
const scope = await this.store.parseFileScopeFromPrompt(taskId);
|
const scope = await this.store.parseFileScopeFromPrompt(taskId);
|
||||||
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
|
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths, { ignoreHiddenOverlapPaths: settings.ignoreHiddenOverlapPaths });
|
||||||
filteredScopeByTaskId.set(taskId, filteredScope);
|
filteredScopeByTaskId.set(taskId, filteredScope);
|
||||||
return filteredScope;
|
return filteredScope;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6396,6 +6396,8 @@
|
|||||||
"fullAgentLog": "Full agent log",
|
"fullAgentLog": "Full agent log",
|
||||||
"globalMaxConcurrent": "Global Max Concurrent",
|
"globalMaxConcurrent": "Global Max Concurrent",
|
||||||
"heartbeatScopeDiscipline": "Heartbeat Scope Discipline",
|
"heartbeatScopeDiscipline": "Heartbeat Scope Discipline",
|
||||||
|
"ignoreHiddenDotPathsHelp": "When enabled, overlap checks ignore hidden path segments such as .fusion/, .changeset/, .github/, .env, and nested .cache/ directories. Uncheck to restore legacy counting for stricter serialization.",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": " Ignore hidden dot paths in overlap checks ",
|
||||||
"ignoredOverlapPaths": "Ignored overlap paths",
|
"ignoredOverlapPaths": "Ignored overlap paths",
|
||||||
"letEngineerAgentsAutoClaimBacklogTasks": " Let engineer agents auto-claim backlog tasks ",
|
"letEngineerAgentsAutoClaimBacklogTasks": " Let engineer agents auto-claim backlog tasks ",
|
||||||
"lite": "Lite",
|
"lite": "Lite",
|
||||||
|
|||||||
@@ -1932,7 +1932,8 @@
|
|||||||
"loadingMessage": "Cargando panel de Fusion",
|
"loadingMessage": "Cargando panel de Fusion",
|
||||||
"loadingProgress": "Progreso de carga del panel",
|
"loadingProgress": "Progreso de carga del panel",
|
||||||
"updatingMessage": "Actualizando panel de Fusion",
|
"updatingMessage": "Actualizando panel de Fusion",
|
||||||
"updatingVersion": "Actualizando a una nueva versión de frontend..."
|
"updatingVersion": "Actualizando a una nueva versión de frontend...",
|
||||||
|
"title": ""
|
||||||
},
|
},
|
||||||
"dbBanner": {
|
"dbBanner": {
|
||||||
"body": "La verificación de integridad de SQLite de Fusion en segundo plano informó de corrupción. Revise los objetos con error a continuación antes de continuar con operaciones críticas.",
|
"body": "La verificación de integridad de SQLite de Fusion en segundo plano informó de corrupción. Revise los objetos con error a continuación antes de continuar con operaciones críticas.",
|
||||||
@@ -6427,7 +6428,9 @@
|
|||||||
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
"whenTheStuckDetectorKillsAndReQueues": "",
|
"whenTheStuckDetectorKillsAndReQueues": "",
|
||||||
"browseWorkspacePath": "",
|
"browseWorkspacePath": "",
|
||||||
"overlapPickerNote": ""
|
"overlapPickerNote": "",
|
||||||
|
"ignoreHiddenDotPathsHelp": "",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.",
|
"globalBanner": "Estos ajustes se comparten entre todos tus proyectos de Fusion.",
|
||||||
@@ -8542,7 +8545,9 @@
|
|||||||
"todo": "Todo",
|
"todo": "Todo",
|
||||||
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
||||||
"editWorkflow": "Edit workflow",
|
"editWorkflow": "Edit workflow",
|
||||||
"newWorkflow": "New workflow"
|
"newWorkflow": "New workflow",
|
||||||
|
"merging": "",
|
||||||
|
"mergingTitle": ""
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"projectRoot": "Raíz del proyecto",
|
"projectRoot": "Raíz del proyecto",
|
||||||
|
|||||||
@@ -1932,7 +1932,8 @@
|
|||||||
"loadingMessage": "Chargement du tableau de bord Fusion",
|
"loadingMessage": "Chargement du tableau de bord Fusion",
|
||||||
"loadingProgress": "Progression du chargement du tableau de bord",
|
"loadingProgress": "Progression du chargement du tableau de bord",
|
||||||
"updatingMessage": "Mise à jour du tableau de bord Fusion",
|
"updatingMessage": "Mise à jour du tableau de bord Fusion",
|
||||||
"updatingVersion": "Mise à jour vers une nouvelle version de l'interface..."
|
"updatingVersion": "Mise à jour vers une nouvelle version de l'interface...",
|
||||||
|
"title": ""
|
||||||
},
|
},
|
||||||
"dbBanner": {
|
"dbBanner": {
|
||||||
"body": "La vérification d'intégrité SQLite en arrière-plan de Fusion a signalé une corruption. Veuillez examiner les objets défaillants ci-dessous avant de continuer les opérations critiques.",
|
"body": "La vérification d'intégrité SQLite en arrière-plan de Fusion a signalé une corruption. Veuillez examiner les objets défaillants ci-dessous avant de continuer les opérations critiques.",
|
||||||
@@ -6427,7 +6428,9 @@
|
|||||||
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
"whenTheStuckDetectorKillsAndReQueues": "",
|
"whenTheStuckDetectorKillsAndReQueues": "",
|
||||||
"browseWorkspacePath": "",
|
"browseWorkspacePath": "",
|
||||||
"overlapPickerNote": ""
|
"overlapPickerNote": "",
|
||||||
|
"ignoreHiddenDotPathsHelp": "",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "Ces paramètres sont partagés entre tous vos projets Fusion.",
|
"globalBanner": "Ces paramètres sont partagés entre tous vos projets Fusion.",
|
||||||
@@ -8542,7 +8545,9 @@
|
|||||||
"todo": "Todo",
|
"todo": "Todo",
|
||||||
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
||||||
"editWorkflow": "Edit workflow",
|
"editWorkflow": "Edit workflow",
|
||||||
"newWorkflow": "New workflow"
|
"newWorkflow": "New workflow",
|
||||||
|
"merging": "",
|
||||||
|
"mergingTitle": ""
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"projectRoot": "Racine du projet",
|
"projectRoot": "Racine du projet",
|
||||||
|
|||||||
@@ -1932,7 +1932,8 @@
|
|||||||
"loadingMessage": "Fusion 대시보드 로드 중",
|
"loadingMessage": "Fusion 대시보드 로드 중",
|
||||||
"loadingProgress": "대시보드 로딩 진행 상황",
|
"loadingProgress": "대시보드 로딩 진행 상황",
|
||||||
"updatingMessage": "Fusion 대시보드 업데이트 중",
|
"updatingMessage": "Fusion 대시보드 업데이트 중",
|
||||||
"updatingVersion": "새 프런트엔드 버전으로 업데이트 중..."
|
"updatingVersion": "새 프런트엔드 버전으로 업데이트 중...",
|
||||||
|
"title": ""
|
||||||
},
|
},
|
||||||
"dbBanner": {
|
"dbBanner": {
|
||||||
"body": "Fusion의 백그라운드 SQLite 무결성 검사에서 손상이 발견되었습니다. 중요한 작업을 계속하기 전에 아래의 실패한 개체를 검토하세요.",
|
"body": "Fusion의 백그라운드 SQLite 무결성 검사에서 손상이 발견되었습니다. 중요한 작업을 계속하기 전에 아래의 실패한 개체를 검토하세요.",
|
||||||
@@ -6427,7 +6428,9 @@
|
|||||||
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
"whenTheStuckDetectorKillsAndReQueues": "",
|
"whenTheStuckDetectorKillsAndReQueues": "",
|
||||||
"browseWorkspacePath": "",
|
"browseWorkspacePath": "",
|
||||||
"overlapPickerNote": ""
|
"overlapPickerNote": "",
|
||||||
|
"ignoreHiddenDotPathsHelp": "",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.",
|
"globalBanner": "이 설정은 모든 Fusion 프로젝트에서 공유됩니다.",
|
||||||
@@ -8542,7 +8545,9 @@
|
|||||||
"todo": "Todo",
|
"todo": "Todo",
|
||||||
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
||||||
"editWorkflow": "Edit workflow",
|
"editWorkflow": "Edit workflow",
|
||||||
"newWorkflow": "New workflow"
|
"newWorkflow": "New workflow",
|
||||||
|
"merging": "",
|
||||||
|
"mergingTitle": ""
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"projectRoot": "프로젝트 루트",
|
"projectRoot": "프로젝트 루트",
|
||||||
|
|||||||
@@ -1932,7 +1932,8 @@
|
|||||||
"loadingMessage": "加载 Fusion 仪表板",
|
"loadingMessage": "加载 Fusion 仪表板",
|
||||||
"loadingProgress": "仪表板加载进度",
|
"loadingProgress": "仪表板加载进度",
|
||||||
"updatingMessage": "更新 Fusion 仪表板",
|
"updatingMessage": "更新 Fusion 仪表板",
|
||||||
"updatingVersion": "更新到新的前端版本..."
|
"updatingVersion": "更新到新的前端版本...",
|
||||||
|
"title": ""
|
||||||
},
|
},
|
||||||
"dbBanner": {
|
"dbBanner": {
|
||||||
"body": "Fusion 的后台 SQLite 完整性检查报告有损坏。继续关键操作之前,请查看下面的失败对象。",
|
"body": "Fusion 的后台 SQLite 完整性检查报告有损坏。继续关键操作之前,请查看下面的失败对象。",
|
||||||
@@ -6427,7 +6428,9 @@
|
|||||||
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
"whenTheStuckDetectorKillsAndReQueues": "",
|
"whenTheStuckDetectorKillsAndReQueues": "",
|
||||||
"browseWorkspacePath": "",
|
"browseWorkspacePath": "",
|
||||||
"overlapPickerNote": ""
|
"overlapPickerNote": "",
|
||||||
|
"ignoreHiddenDotPathsHelp": "",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "这些设置在所有 Fusion 项目中共享。",
|
"globalBanner": "这些设置在所有 Fusion 项目中共享。",
|
||||||
@@ -8542,7 +8545,9 @@
|
|||||||
"todo": "Todo",
|
"todo": "Todo",
|
||||||
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
||||||
"editWorkflow": "Edit workflow",
|
"editWorkflow": "Edit workflow",
|
||||||
"newWorkflow": "New workflow"
|
"newWorkflow": "New workflow",
|
||||||
|
"merging": "",
|
||||||
|
"mergingTitle": ""
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"projectRoot": "项目根目录",
|
"projectRoot": "项目根目录",
|
||||||
|
|||||||
@@ -1932,7 +1932,8 @@
|
|||||||
"loadingMessage": "載入 Fusion 儀表板",
|
"loadingMessage": "載入 Fusion 儀表板",
|
||||||
"loadingProgress": "儀表板加載進度",
|
"loadingProgress": "儀表板加載進度",
|
||||||
"updatingMessage": "更新 Fusion 儀表板",
|
"updatingMessage": "更新 Fusion 儀表板",
|
||||||
"updatingVersion": "更新到新的前端版本..."
|
"updatingVersion": "更新到新的前端版本...",
|
||||||
|
"title": ""
|
||||||
},
|
},
|
||||||
"dbBanner": {
|
"dbBanner": {
|
||||||
"body": "Fusion 的背景 SQLite 完整性檢查報告有損毀。繼續重要操作之前,請查看下面的失敗物件。",
|
"body": "Fusion 的背景 SQLite 完整性檢查報告有損毀。繼續重要操作之前,請查看下面的失敗物件。",
|
||||||
@@ -6427,7 +6428,9 @@
|
|||||||
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
"whenEnabledTasksWithStalePlansPROMPTMd": "",
|
||||||
"whenTheStuckDetectorKillsAndReQueues": "",
|
"whenTheStuckDetectorKillsAndReQueues": "",
|
||||||
"browseWorkspacePath": "",
|
"browseWorkspacePath": "",
|
||||||
"overlapPickerNote": ""
|
"overlapPickerNote": "",
|
||||||
|
"ignoreHiddenDotPathsHelp": "",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": ""
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"globalBanner": "這些設定在所有 Fusion 專案中共用。",
|
"globalBanner": "這些設定在所有 Fusion 專案中共用。",
|
||||||
@@ -8542,7 +8545,9 @@
|
|||||||
"todo": "Todo",
|
"todo": "Todo",
|
||||||
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
"triggerAria": "Select workflow. Current workflow: {{name}}",
|
||||||
"editWorkflow": "Edit workflow",
|
"editWorkflow": "Edit workflow",
|
||||||
"newWorkflow": "New workflow"
|
"newWorkflow": "New workflow",
|
||||||
|
"merging": "",
|
||||||
|
"mergingTitle": ""
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"projectRoot": "項目根目錄",
|
"projectRoot": "項目根目錄",
|
||||||
|
|||||||
2
packages/i18n/src/resources.d.ts
vendored
2
packages/i18n/src/resources.d.ts
vendored
@@ -6403,6 +6403,8 @@ export default interface Resources {
|
|||||||
"fullAgentLog": "Full agent log",
|
"fullAgentLog": "Full agent log",
|
||||||
"globalMaxConcurrent": "Global Max Concurrent",
|
"globalMaxConcurrent": "Global Max Concurrent",
|
||||||
"heartbeatScopeDiscipline": "Heartbeat Scope Discipline",
|
"heartbeatScopeDiscipline": "Heartbeat Scope Discipline",
|
||||||
|
"ignoreHiddenDotPathsHelp": "When enabled, overlap checks ignore hidden path segments such as .fusion/, .changeset/, .github/, .env, and nested .cache/ directories. Uncheck to restore legacy counting for stricter serialization.",
|
||||||
|
"ignoreHiddenDotPathsInOverlapChecks": " Ignore hidden dot paths in overlap checks ",
|
||||||
"ignoredOverlapPaths": "Ignored overlap paths",
|
"ignoredOverlapPaths": "Ignored overlap paths",
|
||||||
"letEngineerAgentsAutoClaimBacklogTasks": " Let engineer agents auto-claim backlog tasks ",
|
"letEngineerAgentsAutoClaimBacklogTasks": " Let engineer agents auto-claim backlog tasks ",
|
||||||
"lite": "Lite",
|
"lite": "Lite",
|
||||||
|
|||||||
Reference in New Issue
Block a user