FN-6575: make CE stage gating opt-out
Make Compound Engineering stage launch gating opt-out so registered stages like debug remain launchable by default. - Replace enabledStages settings schema with disabledStages defaults and docs. - Derive launchable stages from the live registry minus explicit disabled opt-outs. - Update orchestrator gating, exports, tests, and plugin docs for the new setting. - Add a patch changeset for the Compound Engineering plugin behavior fix. Files changed: .changeset/fn-6575-ce-stage-optout.md | 5 ++++ docs/plugins/compound-engineering.md | 4 +-- .../fusion-plugin-compound-engineering/README.md | 9 ++++-- .../manifest.json | 8 +++--- .../src/__tests__/manifest.test.ts | 2 +- .../src/__tests__/settings.test.ts | 33 +++++++++++++--------- .../src/__tests__/skill-wiring.test.ts | 21 +++++++++++++- .../src/index.ts | 1 + .../src/session/orchestrator.ts | 9 ++++-- .../src/settings.ts | 33 ++++++++++++++-------- 10 files changed, 87 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-6575 Fusion-Task-Lineage: 903732d7-4ffb-4c8f-ba3e-e517d88bc644
This commit is contained in:
5
.changeset/fn-6575-ce-stage-optout.md
Normal file
5
.changeset/fn-6575-ce-stage-optout.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Compound Engineering now treats stage launch settings as an explicit `disabledStages` opt-out list so newly bundled stages, including `ce-debug`, remain launchable on existing installs with stale settings snapshots.
|
||||||
@@ -123,8 +123,8 @@ Settings render under **Settings → Plugins → Compound Engineering**.
|
|||||||
the host default. Consumed by the orchestrator's factory call.
|
the host default. Consumed by the orchestrator's factory call.
|
||||||
- `defaultModelId` (string) — model within the provider; blank uses the host
|
- `defaultModelId` (string) — model within the provider; blank uses the host
|
||||||
default. Consumed by the orchestrator's factory call.
|
default. Consumed by the orchestrator's factory call.
|
||||||
- `enabledStages` (string[], default = full registry) — only these stage IDs may
|
- `disabledStages` (string[], default `[]`) — explicit opt-out list. Registered
|
||||||
be launched; the orchestrator rejects others.
|
stages launch by default; the orchestrator rejects only IDs listed here.
|
||||||
|
|
||||||
**Sync**
|
**Sync**
|
||||||
- `reconcileOnHooks` (boolean, default `true`) — auto-fire the reconcile sweep
|
- `reconcileOnHooks` (boolean, default `true`) — auto-fire the reconcile sweep
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ grouped as follows. Every setting has a real consumption point in the plugin.
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Default Session Provider** (`defaultProvider`) | string | _(host default)_ | Passed to the interactive-session factory as `defaultProvider`. Blank → host picks. |
|
| **Default Session Provider** (`defaultProvider`) | string | _(host default)_ | Passed to the interactive-session factory as `defaultProvider`. Blank → host picks. |
|
||||||
| **Default Session Model** (`defaultModelId`) | string | _(host default)_ | Passed to the factory as `defaultModelId`. Blank → host picks. |
|
| **Default Session Model** (`defaultModelId`) | string | _(host default)_ | Passed to the factory as `defaultModelId`. Blank → host picks. |
|
||||||
| **Enabled Stages** (`enabledStages`) | string[] | full registry | Only these stage IDs may be launched; the orchestrator rejects others. |
|
| **Disabled Stages** (`disabledStages`) | string[] | `[]` | Explicit opt-out list. Registered stages launch by default, and the orchestrator rejects only IDs listed here. |
|
||||||
|
|
||||||
### Sync
|
### Sync
|
||||||
|
|
||||||
@@ -200,5 +200,8 @@ grouped as follows. Every setting has a real consumption point in the plugin.
|
|||||||
| **Reconcile Cadence (minutes)** (`reconcileIntervalMinutes`) | number | `15` | Cadence hint for an on-demand refresh surface. Not a continuous poll loop. |
|
| **Reconcile Cadence (minutes)** (`reconcileIntervalMinutes`) | number | `15` | Cadence hint for an on-demand refresh surface. Not a continuous poll loop. |
|
||||||
|
|
||||||
Getters live in `src/settings.ts` (`getDefaultProvider`, `getDefaultModelId`,
|
Getters live in `src/settings.ts` (`getDefaultProvider`, `getDefaultModelId`,
|
||||||
`getEnabledStages`, `getReconcileOnHooks`, `getReconcileIntervalMinutes`), each
|
`getDisabledStages`, `getEnabledStages`, `getReconcileOnHooks`,
|
||||||
returning its default when the setting is absent.
|
`getReconcileIntervalMinutes`), each returning its default when the setting is
|
||||||
|
absent. `getEnabledStages` remains a derived helper for the live registry minus
|
||||||
|
explicit `disabledStages` opt-outs; stale persisted `enabledStages` snapshots are
|
||||||
|
ignored.
|
||||||
|
|||||||
@@ -30,13 +30,13 @@
|
|||||||
"group": "Sessions",
|
"group": "Sessions",
|
||||||
"defaultValue": ""
|
"defaultValue": ""
|
||||||
},
|
},
|
||||||
"enabledStages": {
|
"disabledStages": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"itemType": "string",
|
"itemType": "string",
|
||||||
"label": "Enabled Stages",
|
"label": "Disabled Stages",
|
||||||
"description": "Stage IDs that may be launched from the Compound Engineering view (for example strategy, ideate, brainstorm, plan, work, debug).",
|
"description": "Stage IDs hidden from launch in the Compound Engineering view. Empty means all registered stages are launchable.",
|
||||||
"group": "Sessions",
|
"group": "Sessions",
|
||||||
"defaultValue": ["strategy", "ideate", "brainstorm", "plan", "work", "debug"]
|
"defaultValue": []
|
||||||
},
|
},
|
||||||
"reconcileOnHooks": {
|
"reconcileOnHooks": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ describe("compound engineering plugin manifest", () => {
|
|||||||
const expectedKeys = [
|
const expectedKeys = [
|
||||||
"defaultProvider",
|
"defaultProvider",
|
||||||
"defaultModelId",
|
"defaultModelId",
|
||||||
"enabledStages",
|
"disabledStages",
|
||||||
"reconcileOnHooks",
|
"reconcileOnHooks",
|
||||||
"reconcileIntervalMinutes",
|
"reconcileIntervalMinutes",
|
||||||
].sort();
|
].sort();
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { describe, expect, it } from "vitest";
|
|||||||
import type { PluginSettingType } from "@fusion/plugin-sdk";
|
import type { PluginSettingType } from "@fusion/plugin-sdk";
|
||||||
import { listStages } from "../session/stage-registry.js";
|
import { listStages } from "../session/stage-registry.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ENABLED_STAGES,
|
DEFAULT_DISABLED_STAGES,
|
||||||
DEFAULT_MODEL_ID,
|
DEFAULT_MODEL_ID,
|
||||||
DEFAULT_PROVIDER,
|
DEFAULT_PROVIDER,
|
||||||
DEFAULT_RECONCILE_INTERVAL_MINUTES,
|
DEFAULT_RECONCILE_INTERVAL_MINUTES,
|
||||||
DEFAULT_RECONCILE_ON_HOOKS,
|
DEFAULT_RECONCILE_ON_HOOKS,
|
||||||
getDefaultModelId,
|
getDefaultModelId,
|
||||||
getDefaultProvider,
|
getDefaultProvider,
|
||||||
|
getDisabledStages,
|
||||||
getEnabledStages,
|
getEnabledStages,
|
||||||
getReconcileIntervalMinutes,
|
getReconcileIntervalMinutes,
|
||||||
getReconcileOnHooks,
|
getReconcileOnHooks,
|
||||||
@@ -42,21 +43,21 @@ describe("compound engineering plugin settings schema", () => {
|
|||||||
[
|
[
|
||||||
"defaultModelId",
|
"defaultModelId",
|
||||||
"defaultProvider",
|
"defaultProvider",
|
||||||
"enabledStages",
|
"disabledStages",
|
||||||
"reconcileIntervalMinutes",
|
"reconcileIntervalMinutes",
|
||||||
"reconcileOnHooks",
|
"reconcileOnHooks",
|
||||||
].sort(),
|
].sort(),
|
||||||
);
|
);
|
||||||
expect(settingsSchema.defaultProvider.group).toBe("Sessions");
|
expect(settingsSchema.defaultProvider.group).toBe("Sessions");
|
||||||
expect(settingsSchema.defaultModelId.group).toBe("Sessions");
|
expect(settingsSchema.defaultModelId.group).toBe("Sessions");
|
||||||
expect(settingsSchema.enabledStages.group).toBe("Sessions");
|
expect(settingsSchema.disabledStages.group).toBe("Sessions");
|
||||||
expect(settingsSchema.reconcileOnHooks.group).toBe("Sync");
|
expect(settingsSchema.reconcileOnHooks.group).toBe("Sync");
|
||||||
expect(settingsSchema.reconcileIntervalMinutes.group).toBe("Sync");
|
expect(settingsSchema.reconcileIntervalMinutes.group).toBe("Sync");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults enabledStages to the full stage registry", () => {
|
it("defaults disabledStages to no explicit opt-outs", () => {
|
||||||
expect(DEFAULT_ENABLED_STAGES).toEqual(listStages().map((s) => s.stageId));
|
expect(DEFAULT_DISABLED_STAGES).toEqual([]);
|
||||||
expect(settingsSchema.enabledStages.defaultValue).toEqual(DEFAULT_ENABLED_STAGES);
|
expect(settingsSchema.disabledStages.defaultValue).toEqual(DEFAULT_DISABLED_STAGES);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses documented literal defaults", () => {
|
it("uses documented literal defaults", () => {
|
||||||
@@ -72,9 +73,6 @@ describe("compound engineering plugin settings schema", () => {
|
|||||||
expect(DEFAULT_PROVIDER).toBe("");
|
expect(DEFAULT_PROVIDER).toBe("");
|
||||||
expect(getDefaultModelId(empty)).toBeUndefined();
|
expect(getDefaultModelId(empty)).toBeUndefined();
|
||||||
expect(DEFAULT_MODEL_ID).toBe("");
|
expect(DEFAULT_MODEL_ID).toBe("");
|
||||||
// getEnabledStages re-reads the LIVE registry default (so runtime-registered
|
|
||||||
// stages are launchable); DEFAULT_ENABLED_STAGES is the import-time snapshot
|
|
||||||
// used for the schema/manifest literal.
|
|
||||||
expect(getEnabledStages(empty)).toEqual(listStages().map((s) => s.stageId));
|
expect(getEnabledStages(empty)).toEqual(listStages().map((s) => s.stageId));
|
||||||
expect(getReconcileOnHooks(empty)).toBe(DEFAULT_RECONCILE_ON_HOOKS);
|
expect(getReconcileOnHooks(empty)).toBe(DEFAULT_RECONCILE_ON_HOOKS);
|
||||||
expect(getReconcileIntervalMinutes(empty)).toBe(DEFAULT_RECONCILE_INTERVAL_MINUTES);
|
expect(getReconcileIntervalMinutes(empty)).toBe(DEFAULT_RECONCILE_INTERVAL_MINUTES);
|
||||||
@@ -84,14 +82,15 @@ describe("compound engineering plugin settings schema", () => {
|
|||||||
const populated = {
|
const populated = {
|
||||||
defaultProvider: "anthropic",
|
defaultProvider: "anthropic",
|
||||||
defaultModelId: "claude-opus",
|
defaultModelId: "claude-opus",
|
||||||
enabledStages: ["strategy", "plan"],
|
disabledStages: ["plan"],
|
||||||
reconcileOnHooks: false,
|
reconcileOnHooks: false,
|
||||||
reconcileIntervalMinutes: 30,
|
reconcileIntervalMinutes: 30,
|
||||||
} satisfies Record<string, unknown>;
|
} satisfies Record<string, unknown>;
|
||||||
|
|
||||||
expect(getDefaultProvider(populated)).toBe("anthropic");
|
expect(getDefaultProvider(populated)).toBe("anthropic");
|
||||||
expect(getDefaultModelId(populated)).toBe("claude-opus");
|
expect(getDefaultModelId(populated)).toBe("claude-opus");
|
||||||
expect(getEnabledStages(populated)).toEqual(["strategy", "plan"]);
|
expect(getDisabledStages(populated)).toEqual(["plan"]);
|
||||||
|
expect(getEnabledStages(populated)).toEqual(listStages().map((s) => s.stageId).filter((id) => id !== "plan"));
|
||||||
expect(getReconcileOnHooks(populated)).toBe(false);
|
expect(getReconcileOnHooks(populated)).toBe(false);
|
||||||
expect(getReconcileIntervalMinutes(populated)).toBe(30);
|
expect(getReconcileIntervalMinutes(populated)).toBe(30);
|
||||||
});
|
});
|
||||||
@@ -102,10 +101,18 @@ describe("compound engineering plugin settings schema", () => {
|
|||||||
expect(getReconcileIntervalMinutes({ reconcileIntervalMinutes: 7.9 })).toBe(7);
|
expect(getReconcileIntervalMinutes({ reconcileIntervalMinutes: 7.9 })).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores stale enabledStages snapshots when deriving launchable stages", () => {
|
||||||
|
expect(getEnabledStages({ enabledStages: ["strategy", "ideate", "brainstorm", "plan", "work"] })).toEqual(
|
||||||
|
listStages().map((s) => s.stageId),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("falls back to defaults for malformed values", () => {
|
it("falls back to defaults for malformed values", () => {
|
||||||
const liveDefault = listStages().map((s) => s.stageId);
|
const liveDefault = listStages().map((s) => s.stageId);
|
||||||
expect(getEnabledStages({ enabledStages: "not-an-array" })).toEqual(liveDefault);
|
expect(getDisabledStages({ disabledStages: "not-an-array" })).toEqual([]);
|
||||||
expect(getEnabledStages({ enabledStages: [] })).toEqual(liveDefault);
|
expect(getDisabledStages({ disabledStages: [] })).toEqual([]);
|
||||||
|
expect(getEnabledStages({ disabledStages: "not-an-array" })).toEqual(liveDefault);
|
||||||
|
expect(getEnabledStages({ disabledStages: [] })).toEqual(liveDefault);
|
||||||
expect(getDefaultProvider({ defaultProvider: " " })).toBeUndefined();
|
expect(getDefaultProvider({ defaultProvider: " " })).toBeUndefined();
|
||||||
expect(getReconcileOnHooks({ reconcileOnHooks: "yes" })).toBe(DEFAULT_RECONCILE_ON_HOOKS);
|
expect(getReconcileOnHooks({ reconcileOnHooks: "yes" })).toBe(DEFAULT_RECONCILE_ON_HOOKS);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe("session skill wiring", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it("rejects debug launch cleanly when the stage is disabled", async () => {
|
it("ignores stale enabledStages snapshots so registered stages remain launchable", async () => {
|
||||||
h.ctx.settings = { enabledStages: ["strategy", "ideate", "brainstorm", "plan", "work"] };
|
h.ctx.settings = { enabledStages: ["strategy", "ideate", "brainstorm", "plan", "work"] };
|
||||||
const factory = vi.fn(async () => ({
|
const factory = vi.fn(async () => ({
|
||||||
session: makeScriptedSession([{ type: "complete", data: { artifact: "# done" } }]),
|
session: makeScriptedSession([{ type: "complete", data: { artifact: "# done" } }]),
|
||||||
@@ -74,6 +74,25 @@ describe("session skill wiring", () => {
|
|||||||
turnTimeoutMs: 5000,
|
turnTimeoutMs: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const stageId of ["strategy", "work", "debug"]) {
|
||||||
|
await orch.start(stageId, { openingMessage: `launch ${stageId}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(factory).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects debug launch cleanly when the stage is disabled", async () => {
|
||||||
|
h.ctx.settings = { disabledStages: ["debug"] };
|
||||||
|
const factory = vi.fn(async () => ({
|
||||||
|
session: makeScriptedSession([{ type: "complete", data: { artifact: "# done" } }]),
|
||||||
|
}));
|
||||||
|
const orch = new CeOrchestrator({
|
||||||
|
ctx: h.ctx,
|
||||||
|
createInteractiveAiSession: factory,
|
||||||
|
projectRoot: h.projectRoot,
|
||||||
|
turnTimeoutMs: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
await expect(orch.start("debug", { openingMessage: "investigate" })).rejects.toThrow(
|
await expect(orch.start("debug", { openingMessage: "investigate" })).rejects.toThrow(
|
||||||
"CE stage is not enabled: debug",
|
"CE stage is not enabled: debug",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export {
|
|||||||
settingsSchema,
|
settingsSchema,
|
||||||
getDefaultProvider,
|
getDefaultProvider,
|
||||||
getDefaultModelId,
|
getDefaultModelId,
|
||||||
|
getDisabledStages,
|
||||||
getEnabledStages,
|
getEnabledStages,
|
||||||
getReconcileOnHooks,
|
getReconcileOnHooks,
|
||||||
getReconcileIntervalMinutes,
|
getReconcileIntervalMinutes,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
import { resolveDefaultInstallTargetRoot } from "../skill-installation.js";
|
import { resolveDefaultInstallTargetRoot } from "../skill-installation.js";
|
||||||
import { getCePipelineStore, type CePipelineStore } from "../sync/pipeline-store.js";
|
import { getCePipelineStore, type CePipelineStore } from "../sync/pipeline-store.js";
|
||||||
import { createCeTaskWithLink } from "../sync/ce-task.js";
|
import { createCeTaskWithLink } from "../sync/ce-task.js";
|
||||||
import { getDefaultModelId, getDefaultProvider, getEnabledStages } from "../settings.js";
|
import { getDefaultModelId, getDefaultProvider, getDisabledStages } from "../settings.js";
|
||||||
import type { CeActivityTurn, CeSession, CeSessionStore } from "./session-store.js";
|
import type { CeActivityTurn, CeSession, CeSessionStore } from "./session-store.js";
|
||||||
import { getCeSessionStore } from "./session-store.js";
|
import { getCeSessionStore } from "./session-store.js";
|
||||||
import { getStage, type CeStageDefinition } from "./stage-registry.js";
|
import { getStage, type CeStageDefinition } from "./stage-registry.js";
|
||||||
@@ -383,8 +383,11 @@ export class CeOrchestrator {
|
|||||||
async start(stageId: string, opts: StartStageOptions): Promise<CeStepResult> {
|
async start(stageId: string, opts: StartStageOptions): Promise<CeStepResult> {
|
||||||
const stage = getStage(stageId);
|
const stage = getStage(stageId);
|
||||||
if (!stage) throw new Error(`Unknown CE stage: ${stageId}`);
|
if (!stage) throw new Error(`Unknown CE stage: ${stageId}`);
|
||||||
// Setting-gated launch (U9): only stages the operator enabled may launch.
|
/*
|
||||||
if (!getEnabledStages(this.ctx.settings).includes(stageId)) {
|
* FNXC:CompoundEngineering 2026-06-17-08:09:
|
||||||
|
* Stage launch gating is opt-out: a registered CE stage launches unless operators explicitly list it in disabledStages. This keeps existing installs from rejecting newly appended stages because of stale enabledStages snapshots.
|
||||||
|
*/
|
||||||
|
if (getDisabledStages(this.ctx.settings).includes(stageId)) {
|
||||||
throw new Error(`CE stage is not enabled: ${stageId}`);
|
throw new Error(`CE stage is not enabled: ${stageId}`);
|
||||||
}
|
}
|
||||||
if (!this.factory) {
|
if (!this.factory) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { listStages } from "./session/stage-registry.js";
|
|||||||
* consumption point in the existing plugin code:
|
* consumption point in the existing plugin code:
|
||||||
* - Sessions group → the orchestrator's interactive-session factory call
|
* - Sessions group → the orchestrator's interactive-session factory call
|
||||||
* (`defaultProvider`/`defaultModelId`) and the launch
|
* (`defaultProvider`/`defaultModelId`) and the launch
|
||||||
* guard (`enabledStages`).
|
* guard (`disabledStages`).
|
||||||
* - Sync group → the reconciler trigger surface (auto-drain on hooks +
|
* - Sync group → the reconciler trigger surface (auto-drain on hooks +
|
||||||
* the cadence hint a refresh surface reads).
|
* the cadence hint a refresh surface reads).
|
||||||
*
|
*
|
||||||
@@ -20,8 +20,8 @@ import { listStages } from "./session/stage-registry.js";
|
|||||||
export const DEFAULT_PROVIDER = "";
|
export const DEFAULT_PROVIDER = "";
|
||||||
export const DEFAULT_MODEL_ID = "";
|
export const DEFAULT_MODEL_ID = "";
|
||||||
|
|
||||||
/** Sessions: which pipeline stages are launchable. Defaults to the full registry. */
|
/** Sessions: stage launch opt-outs. Empty means every registered stage is launchable. */
|
||||||
export const DEFAULT_ENABLED_STAGES: string[] = listStages().map((s) => s.stageId);
|
export const DEFAULT_DISABLED_STAGES: string[] = [];
|
||||||
|
|
||||||
/** Sync: whether the board→pipeline reconcile sweep auto-fires after lifecycle hooks. */
|
/** Sync: whether the board→pipeline reconcile sweep auto-fires after lifecycle hooks. */
|
||||||
export const DEFAULT_RECONCILE_ON_HOOKS = true;
|
export const DEFAULT_RECONCILE_ON_HOOKS = true;
|
||||||
@@ -48,13 +48,13 @@ export const settingsSchema: Record<string, PluginSettingSchema> = {
|
|||||||
group: "Sessions",
|
group: "Sessions",
|
||||||
defaultValue: DEFAULT_MODEL_ID,
|
defaultValue: DEFAULT_MODEL_ID,
|
||||||
},
|
},
|
||||||
enabledStages: {
|
disabledStages: {
|
||||||
type: "array",
|
type: "array",
|
||||||
itemType: "string",
|
itemType: "string",
|
||||||
label: "Enabled Stages",
|
label: "Disabled Stages",
|
||||||
description: "Stage IDs that may be launched from the Compound Engineering view (for example strategy, ideate, brainstorm, plan, work).",
|
description: "Stage IDs hidden from launch in the Compound Engineering view. Empty means all registered stages are launchable.",
|
||||||
group: "Sessions",
|
group: "Sessions",
|
||||||
defaultValue: DEFAULT_ENABLED_STAGES,
|
defaultValue: DEFAULT_DISABLED_STAGES,
|
||||||
},
|
},
|
||||||
|
|
||||||
reconcileOnHooks: {
|
reconcileOnHooks: {
|
||||||
@@ -112,12 +112,23 @@ export function getDefaultModelId(settings: Record<string, unknown>): string | u
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage IDs that may be launched. When unset, defaults to the LIVE registry
|
* Stage IDs explicitly disabled by the operator. Malformed or empty values mean
|
||||||
* (re-read here, not the import-time snapshot) so a stage registered at runtime
|
* no opt-outs, so all registered stages remain launchable.
|
||||||
* is launchable by default — disabling is an explicit opt-out, not opt-in.
|
*/
|
||||||
|
export function getDisabledStages(settings: Record<string, unknown>): string[] {
|
||||||
|
return asStringArray(settings, "disabledStages", DEFAULT_DISABLED_STAGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stage IDs that may be launched. This is the LIVE registry minus explicit
|
||||||
|
* disabled-stage opt-outs; stale persisted `enabledStages` snapshots are ignored.
|
||||||
|
*
|
||||||
|
* FNXC:CompoundEngineering 2026-06-17-08:06:
|
||||||
|
* The previous enabledStages allow-list was snapshotted into plugin settings at first install, so later appended stages such as debug were silently un-launchable on existing installs. Use disabledStages as an explicit opt-out so every registered stage is launchable by default as documented.
|
||||||
*/
|
*/
|
||||||
export function getEnabledStages(settings: Record<string, unknown>): string[] {
|
export function getEnabledStages(settings: Record<string, unknown>): string[] {
|
||||||
return asStringArray(settings, "enabledStages", listStages().map((s) => s.stageId));
|
const disabled = new Set(getDisabledStages(settings));
|
||||||
|
return listStages().map((s) => s.stageId).filter((stageId) => !disabled.has(stageId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the reconcile sweep auto-fires after lifecycle hooks. */
|
/** Whether the reconcile sweep auto-fires after lifecycle hooks. */
|
||||||
|
|||||||
Reference in New Issue
Block a user