FN-6233: add typed triage policy settings
Render triage planning thresholds from typed workflow settings instead of hard-coded prompt constants. - Add typed triage threshold/default workflow settings, migration coverage, exports, and docs. - Render built-in triage prompt placeholders before engine prompt execution and tests. - Remove the duplicate standard triage prompt from engine in favor of the core workflow IR source. - Add regression coverage for default and customized triage policy rendering. Files changed: .changeset/FN-6233-triage-threshold-settings.md | 7 + docs/settings-reference.md | 19 ++ docs/workflow-steps.md | 2 +- packages/core/src/__tests__/agent-prompts.test.ts | 16 +- .../builtin-workflow-settings-triage.test.ts | 63 ++++ .../src/__tests__/settings-consistency.test.ts | 24 +- .../core/src/__tests__/settings-migration.test.ts | 12 +- .../src/__tests__/workflow-ir-settings.test.ts | 11 +- packages/core/src/agent-prompts.ts | 20 +- packages/core/src/builtin-workflow-settings.ts | 154 ++++++++- packages/core/src/index.ts | 7 +- packages/core/src/moved-settings.ts | 13 +- .../triage-planning-prompt-single-source.test.ts | 9 +- .../__tests__/triage-threshold-settings.test.ts | 84 +++++ packages/engine/src/__tests__/triage.test.ts | 17 +- packages/engine/src/triage.ts | 343 +-------------------- packages/engine/vitest.config.ts | 5 +- 17 files changed, 423 insertions(+), 383 deletions(-) Fusion-Task-Id: FN-6233 Fusion-Task-Lineage: b2a986df-fa37-4297-a359-84d8463b0867
This commit is contained in:
7
.changeset/FN-6233-triage-threshold-settings.md
Normal file
7
.changeset/FN-6233-triage-threshold-settings.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add workflow-native typed settings for triage/spec policy thresholds and routing defaults. The built-in defaults preserve current behavior: size bands remain S <2h, M 2-4h, L 4-8h; subtask signals use the canonical planning-prompt values of step threshold 7 and packages/modules threshold 3; file-scope/remediation thresholds remain 20 and 30.
|
||||||
|
|
||||||
|
These triage policy settings are new workflow settings, not moved project settings, so they are excluded from the U4 `MOVED_SETTINGS_KEYS` tombstone while still resolving through workflow effective settings.
|
||||||
@@ -236,6 +236,25 @@ These groups moved out of project settings and into workflow settings (built-in
|
|||||||
| **Review / approval** | `requirePrApproval`, `requirePlanApproval`, `reviewHandoffPolicy`, `maxReviewerContextRetries`, `maxReviewerFallbackRetries` |
|
| **Review / approval** | `requirePrApproval`, `requirePlanApproval`, `reviewHandoffPolicy`, `maxReviewerContextRetries`, `maxReviewerFallbackRetries` |
|
||||||
| **Per-phase model lanes** | `executionProvider`/`executionModelId`, `planningProvider`/`planningModelId` (+ fallbacks), `validatorProvider`/`validatorModelId` (+ fallbacks) |
|
| **Per-phase model lanes** | `executionProvider`/`executionModelId`, `planningProvider`/`planningModelId` (+ fallbacks), `validatorProvider`/`validatorModelId` (+ fallbacks) |
|
||||||
|
|
||||||
|
### Workflow-native triage policy settings
|
||||||
|
|
||||||
|
The built-in workflows also declare triage/spec policy settings that were **not** moved from project settings. They are workflow-native declarations: they never lived in `DEFAULT_PROJECT_SETTINGS`, are not `MOVED_SETTINGS_KEYS`, and resolve only through the workflow effective-settings path.
|
||||||
|
|
||||||
|
| Setting | Default | Purpose |
|
||||||
|
|---|---:|---|
|
||||||
|
| `triageSizeSmallMaxHours` | `2` | Size S upper hour boundary (`S (<2h)`). |
|
||||||
|
| `triageSizeMediumMaxHours` | `4` | Size M upper hour boundary (`M (2-4h)`). |
|
||||||
|
| `triageSizeLargeMaxHours` | `8` | Size L upper hour boundary; XL starts at `8h+`. |
|
||||||
|
| `triageSubtaskStepThreshold` | `7` | Canonical “MORE THAN 7 implementation steps” split-consideration threshold. |
|
||||||
|
| `triageSubtaskLargeStepSignal` | `9` | Broad-scope signal for large tasks whose plan reaches 9+ steps. |
|
||||||
|
| `triageSubtaskAdditiveStepSignal` | `12` | Additive partitioning signal for 12+ implementation steps. |
|
||||||
|
| `triageSubtaskPackageThreshold` | `3` | Canonical package/module breadth threshold (“MORE THAN 3 different packages/modules”). |
|
||||||
|
| `triageSubtaskFileScopeThreshold` | `20` | File Scope entry count that signals broad work. |
|
||||||
|
| `triageSubtaskRemediationBatchThreshold` | `30` | Large remediation batch threshold. |
|
||||||
|
| `triageNoCommitsDecisionVerbs` | all seven built-ins | Decision-only verbs: Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report. |
|
||||||
|
| `triageDecisionOnlyWorkflowId` | `builtin:quick-fix` | Preferred workflow for decision-only/no-commit tasks. |
|
||||||
|
| `triageDefaultWorkflowId` | `builtin:coding` | Default workflow for standard coding tasks. |
|
||||||
|
|
||||||
In the dashboard Settings modal, Project Models now exposes Plan/Triage, Executor,
|
In the dashboard Settings modal, Project Models now exposes Plan/Triage, Executor,
|
||||||
and Reviewer dropdown controls for the default workflow. The modal's primary
|
and Reviewer dropdown controls for the default workflow. The modal's primary
|
||||||
**Save** action persists pending default-workflow model lane overrides; there is no
|
**Save** action persists pending default-workflow model lane overrides; there is no
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ The default built-in catalog entry `builtin:coding` is backed by the canonical `
|
|||||||
|
|
||||||
`builtin:stepwise-coding` is a separate graph variant backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review/rework as authored graph structure.
|
`builtin:stepwise-coding` is a separate graph variant backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review/rework as authored graph structure.
|
||||||
|
|
||||||
During triage/planning sessions, agents can call `fn_workflow_list` to discover available built-in and custom workflows and read their descriptions before routing work. They can call `fn_workflow_select` to select a workflow for the task being specified, or pass `workflow_id` when creating child tasks with `fn_task_create`; decision-only or investigation tasks can also set `noCommitsExpected` / `**No commits expected:** true` when no code changes are expected.
|
During triage/planning sessions, agents can call `fn_workflow_list` to discover available built-in and custom workflows and read their descriptions before routing work. They can call `fn_workflow_select` to select a workflow for the task being specified, or pass `workflow_id` when creating child tasks with `fn_task_create`; decision-only or investigation tasks can also set `noCommitsExpected` / `**No commits expected:** true` when no code changes are expected. The built-in triage thresholds, decision-only verb list, and default routing IDs are workflow-native typed settings resolved from the selected workflow.
|
||||||
|
|
||||||
#### Runtime invariant criterion
|
#### Runtime invariant criterion
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getTemplatesForRole,
|
getTemplatesForRole,
|
||||||
} from "../agent-prompts.js";
|
} from "../agent-prompts.js";
|
||||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
||||||
|
import { renderTriagePolicyPlaceholders } from "../builtin-workflow-settings.js";
|
||||||
import { resolvePlanningPromptFromIr } from "../workflow-ir-resolver.js";
|
import { resolvePlanningPromptFromIr } from "../workflow-ir-resolver.js";
|
||||||
import type { AgentPromptsConfig, AgentPromptTemplate } from "../types.js";
|
import type { AgentPromptsConfig, AgentPromptTemplate } from "../types.js";
|
||||||
import type { WorkflowIr } from "../workflow-ir-types.js";
|
import type { WorkflowIr } from "../workflow-ir-types.js";
|
||||||
@@ -272,10 +273,17 @@ describe("resolveAgentPrompt", () => {
|
|||||||
expect(triageSource).not.toMatch(/export const (?!FAST_TRIAGE_SYSTEM_PROMPT)[A-Z_]*TRIAGE[A-Z_]*SYSTEM_PROMPT\s*=/);
|
expect(triageSource).not.toMatch(/export const (?!FAST_TRIAGE_SYSTEM_PROMPT)[A-Z_]*TRIAGE[A-Z_]*SYSTEM_PROMPT\s*=/);
|
||||||
expect(planningPrompt).toBe(corePrompt);
|
expect(planningPrompt).toBe(corePrompt);
|
||||||
expect(corePrompt).toContain("**Broad-scope decomposition signals:**");
|
expect(corePrompt).toContain("**Broad-scope decomposition signals:**");
|
||||||
expect(corePrompt).toContain("step count would reach 9 or more");
|
expect(corePrompt).toContain("step count would reach {{triageSubtaskLargeStepSignal}} or more");
|
||||||
expect(corePrompt).toContain("would reach 12 or more");
|
expect(corePrompt).toContain("would reach {{triageSubtaskAdditiveStepSignal}} or more");
|
||||||
expect(corePrompt).toContain("20 or more entries");
|
expect(corePrompt).toContain("{{triageSubtaskFileScopeThreshold}} or more entries");
|
||||||
expect(corePrompt).toContain("at or above 30 items");
|
expect(corePrompt).toContain("at or above {{triageSubtaskRemediationBatchThreshold}} items");
|
||||||
|
|
||||||
|
const renderedPrompt = renderTriagePolicyPlaceholders(corePrompt, {});
|
||||||
|
expect(renderedPrompt).toContain("step count would reach 9 or more");
|
||||||
|
expect(renderedPrompt).toContain("would reach 12 or more");
|
||||||
|
expect(renderedPrompt).toContain("20 or more entries");
|
||||||
|
expect(renderedPrompt).toContain("at or above 30 items");
|
||||||
|
expect(renderedPrompt).not.toContain("{{");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves custom planning prompts and ignores IRs without planning prompts", () => {
|
it("resolves custom planning prompts and ignores IRs without planning prompts", () => {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
BUILTIN_MOVED_WORKFLOW_SETTINGS,
|
||||||
|
BUILTIN_TRIAGE_POLICY_SETTINGS,
|
||||||
|
BUILTIN_WORKFLOW_SETTINGS,
|
||||||
|
renderTriagePolicyPlaceholders,
|
||||||
|
} from "../builtin-workflow-settings.js";
|
||||||
|
|
||||||
|
const expectedDefaults: Record<string, { type: string; default: unknown }> = {
|
||||||
|
triageSizeSmallMaxHours: { type: "number", default: 2 },
|
||||||
|
triageSizeMediumMaxHours: { type: "number", default: 4 },
|
||||||
|
triageSizeLargeMaxHours: { type: "number", default: 8 },
|
||||||
|
triageSubtaskStepThreshold: { type: "number", default: 7 },
|
||||||
|
triageSubtaskLargeStepSignal: { type: "number", default: 9 },
|
||||||
|
triageSubtaskAdditiveStepSignal: { type: "number", default: 12 },
|
||||||
|
triageSubtaskPackageThreshold: { type: "number", default: 3 },
|
||||||
|
triageSubtaskFileScopeThreshold: { type: "number", default: 20 },
|
||||||
|
triageSubtaskRemediationBatchThreshold: { type: "number", default: 30 },
|
||||||
|
triageNoCommitsDecisionVerbs: {
|
||||||
|
type: "multi-enum",
|
||||||
|
default: ["Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", "Investigate and report"],
|
||||||
|
},
|
||||||
|
triageDecisionOnlyWorkflowId: { type: "enum", default: "builtin:quick-fix" },
|
||||||
|
triageDefaultWorkflowId: { type: "enum", default: "builtin:coding" },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("workflow-native triage policy settings", () => {
|
||||||
|
it("declares behavior-equivalent typed defaults outside the moved-key catalog", () => {
|
||||||
|
const triageById = new Map(BUILTIN_TRIAGE_POLICY_SETTINGS.map((setting) => [setting.id, setting]));
|
||||||
|
const fullIds = new Set(BUILTIN_WORKFLOW_SETTINGS.map((setting) => setting.id));
|
||||||
|
const movedIds = new Set(BUILTIN_MOVED_WORKFLOW_SETTINGS.map((setting) => setting.id));
|
||||||
|
|
||||||
|
expect(BUILTIN_TRIAGE_POLICY_SETTINGS).toHaveLength(Object.keys(expectedDefaults).length);
|
||||||
|
for (const [id, expected] of Object.entries(expectedDefaults)) {
|
||||||
|
const setting = triageById.get(id);
|
||||||
|
expect(setting, `${id} should be declared`).toBeDefined();
|
||||||
|
expect(setting?.type).toBe(expected.type);
|
||||||
|
expect(setting?.default).toStrictEqual(expected.default);
|
||||||
|
expect(fullIds.has(id), `${id} should be in the full built-in catalog`).toBe(true);
|
||||||
|
expect(movedIds.has(id), `${id} should not be in the moved-key catalog`).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders placeholders from resolved settings and rejects dangling tokens", () => {
|
||||||
|
const prompt = [
|
||||||
|
"Size S (<{{triageSizeSmallMaxHours}}h)",
|
||||||
|
"MORE THAN {{triageSubtaskStepThreshold}} implementation steps",
|
||||||
|
"verbs: {{triageNoCommitsDecisionVerbs}}",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
const rendered = renderTriagePolicyPlaceholders(prompt, {
|
||||||
|
triageSizeSmallMaxHours: 1,
|
||||||
|
triageSubtaskStepThreshold: 5,
|
||||||
|
triageNoCommitsDecisionVerbs: ["Audit", "Confirm"],
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(rendered).toContain("Size S (<1h)");
|
||||||
|
expect(rendered).toContain("MORE THAN 5 implementation steps");
|
||||||
|
expect(rendered).toContain("verbs: Audit, Confirm");
|
||||||
|
expect(rendered).not.toContain("{{");
|
||||||
|
expect(() => renderTriagePolicyPlaceholders("{{unknownTriageToken}}", {})).toThrow(/Unresolved triage policy placeholder/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { MOVED_SETTINGS_KEYS } from "../moved-settings.js";
|
import { MOVED_SETTINGS_KEYS } from "../moved-settings.js";
|
||||||
import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js";
|
import {
|
||||||
|
BUILTIN_TRIAGE_POLICY_SETTINGS,
|
||||||
|
BUILTIN_WORKFLOW_SETTINGS,
|
||||||
|
} from "../builtin-workflow-settings.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_GLOBAL_SETTINGS,
|
DEFAULT_GLOBAL_SETTINGS,
|
||||||
DEFAULT_PROJECT_SETTINGS,
|
DEFAULT_PROJECT_SETTINGS,
|
||||||
@@ -39,18 +42,29 @@ describe("settings consistency (U5)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("(b) MOVED_SETTINGS_KEYS and BUILTIN_WORKFLOW_SETTINGS declaration ids are exactly equal sets", () => {
|
it("(b) every built-in declaration is either moved or workflow-native triage policy", () => {
|
||||||
const declIds = new Set(BUILTIN_WORKFLOW_SETTINGS.map((s) => s.id));
|
const declIds = new Set(BUILTIN_WORKFLOW_SETTINGS.map((s) => s.id));
|
||||||
const moved = new Set(movedKeys);
|
const moved = new Set(movedKeys);
|
||||||
|
const native = new Set(BUILTIN_TRIAGE_POLICY_SETTINGS.map((s) => s.id));
|
||||||
// Every moved key has a declaration.
|
// Every moved key has a declaration.
|
||||||
for (const key of moved) {
|
for (const key of moved) {
|
||||||
expect(declIds.has(key), `moved key '${key}' has no BUILTIN_WORKFLOW_SETTINGS declaration`).toBe(true);
|
expect(declIds.has(key), `moved key '${key}' has no BUILTIN_WORKFLOW_SETTINGS declaration`).toBe(true);
|
||||||
}
|
}
|
||||||
// Every declaration is a moved key.
|
// Every declaration is either a moved key or an explicitly workflow-native triage setting.
|
||||||
for (const id of declIds) {
|
for (const id of declIds) {
|
||||||
expect(moved.has(id), `declaration '${id}' is missing from MOVED_SETTINGS_KEYS`).toBe(true);
|
expect(
|
||||||
|
moved.has(id) || native.has(id),
|
||||||
|
`declaration '${id}' must be in MOVED_SETTINGS_KEYS or BUILTIN_TRIAGE_POLICY_SETTINGS`,
|
||||||
|
).toBe(true);
|
||||||
}
|
}
|
||||||
expect(moved.size).toBe(declIds.size);
|
for (const id of native) {
|
||||||
|
expect(moved.has(id), `native triage setting '${id}' must not be in MOVED_SETTINGS_KEYS`).toBe(false);
|
||||||
|
expect(PROJECT_SETTINGS_KEYS as readonly string[], `native triage setting '${id}' must not be project schema key`).not.toContain(id);
|
||||||
|
expect(GLOBAL_SETTINGS_KEYS as readonly string[], `native triage setting '${id}' must not be global schema key`).not.toContain(id);
|
||||||
|
expect(Object.keys(DEFAULT_PROJECT_SETTINGS), `native triage setting '${id}' must not be project default`).not.toContain(id);
|
||||||
|
expect(Object.keys(DEFAULT_GLOBAL_SETTINGS), `native triage setting '${id}' must not be global default`).not.toContain(id);
|
||||||
|
}
|
||||||
|
expect(declIds.size).toBe(moved.size + native.size);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("(c) every moved key is absent from GLOBAL_SETTINGS_KEYS / PROJECT_SETTINGS_KEYS and their predicates", () => {
|
it("(c) every moved key is absent from GLOBAL_SETTINGS_KEYS / PROJECT_SETTINGS_KEYS and their predicates", () => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
SETTINGS_MIGRATION_VERSION,
|
SETTINGS_MIGRATION_VERSION,
|
||||||
SETTINGS_MIGRATION_MARKER_KEY,
|
SETTINGS_MIGRATION_MARKER_KEY,
|
||||||
} from "../moved-settings.js";
|
} from "../moved-settings.js";
|
||||||
|
import { BUILTIN_TRIAGE_POLICY_SETTINGS } from "../builtin-workflow-settings.js";
|
||||||
import { resolveEffectiveSettingsById, type WorkflowSettingsResolverStore } from "../workflow-settings-resolver.js";
|
import { resolveEffectiveSettingsById, type WorkflowSettingsResolverStore } from "../workflow-settings-resolver.js";
|
||||||
import { DEFAULT_PROJECT_SETTINGS, PROJECT_SETTINGS_KEYS } from "../settings-schema.js";
|
import { DEFAULT_PROJECT_SETTINGS, PROJECT_SETTINGS_KEYS } from "../settings-schema.js";
|
||||||
|
|
||||||
@@ -155,7 +156,16 @@ describe("settings hard-move migration (U4)", () => {
|
|||||||
expect(DEFAULT_PROJECT_SETTINGS).toHaveProperty("titleSummarizerModelId", undefined);
|
expect(DEFAULT_PROJECT_SETTINGS).toHaveProperty("titleSummarizerModelId", undefined);
|
||||||
expect(DEFAULT_PROJECT_SETTINGS).toHaveProperty("titleSummarizerFallbackProvider", undefined);
|
expect(DEFAULT_PROJECT_SETTINGS).toHaveProperty("titleSummarizerFallbackProvider", undefined);
|
||||||
expect(DEFAULT_PROJECT_SETTINGS).toHaveProperty("titleSummarizerFallbackModelId", undefined);
|
expect(DEFAULT_PROJECT_SETTINGS).toHaveProperty("titleSummarizerFallbackModelId", undefined);
|
||||||
// 26 keys after removing buildTimeoutMs plus the summarizer lane from the catalog.
|
// 26 keys after removing buildTimeoutMs plus the summarizer lane from the moved catalog.
|
||||||
|
expect(MOVED_SETTINGS_KEYS.length).toBe(26);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("workflow-native triage policy settings are excluded from moved/project schemas", () => {
|
||||||
|
for (const setting of BUILTIN_TRIAGE_POLICY_SETTINGS) {
|
||||||
|
expect(MOVED_SETTINGS_KEYS, `${setting.id} is workflow-native, not a moved key`).not.toContain(setting.id);
|
||||||
|
expect(PROJECT_SETTINGS_KEYS, `${setting.id} must not be a project schema key`).not.toContain(setting.id);
|
||||||
|
expect(DEFAULT_PROJECT_SETTINGS as Record<string, unknown>).not.toHaveProperty(setting.id);
|
||||||
|
}
|
||||||
expect(MOVED_SETTINGS_KEYS.length).toBe(26);
|
expect(MOVED_SETTINGS_KEYS.length).toBe(26);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import {
|
|||||||
} from "../workflow-ir.js";
|
} from "../workflow-ir.js";
|
||||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
||||||
import { getBuiltinWorkflow } from "../builtin-workflows.js";
|
import { getBuiltinWorkflow } from "../builtin-workflows.js";
|
||||||
import { BUILTIN_WORKFLOW_SETTINGS } from "../builtin-workflow-settings.js";
|
import {
|
||||||
|
BUILTIN_MOVED_WORKFLOW_SETTINGS,
|
||||||
|
BUILTIN_WORKFLOW_SETTINGS,
|
||||||
|
} from "../builtin-workflow-settings.js";
|
||||||
import { DEFAULT_PROJECT_SETTINGS } from "../types.js";
|
import { DEFAULT_PROJECT_SETTINGS } from "../types.js";
|
||||||
import type {
|
import type {
|
||||||
WorkflowIrV2,
|
WorkflowIrV2,
|
||||||
@@ -208,7 +211,7 @@ describe("parseWorkflowIr — workflow settings declarations (U1)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("built-in workflow settings parity anchor (U1, R4)", () => {
|
describe("built-in workflow settings parity anchor (U1, R4)", () => {
|
||||||
it("the built-in coding workflow declares the full moved-key catalog", () => {
|
it("the built-in coding workflow declares the full workflow settings catalog", () => {
|
||||||
const builtin = BUILTIN_CODING_WORKFLOW_IR as WorkflowIrV2;
|
const builtin = BUILTIN_CODING_WORKFLOW_IR as WorkflowIrV2;
|
||||||
const declaredIds = new Set((builtin.settings ?? []).map((s) => s.id));
|
const declaredIds = new Set((builtin.settings ?? []).map((s) => s.id));
|
||||||
for (const setting of BUILTIN_WORKFLOW_SETTINGS) {
|
for (const setting of BUILTIN_WORKFLOW_SETTINGS) {
|
||||||
@@ -224,7 +227,7 @@ describe("built-in workflow settings parity anchor (U1, R4)", () => {
|
|||||||
// Post-U4 hard-move: every catalog key has been REMOVED from
|
// Post-U4 hard-move: every catalog key has been REMOVED from
|
||||||
// DEFAULT_PROJECT_SETTINGS (the type-vs-schema split keeps the type field but
|
// DEFAULT_PROJECT_SETTINGS (the type-vs-schema split keeps the type field but
|
||||||
// drops the default literal), so the legacy object no longer carries them.
|
// drops the default literal), so the legacy object no longer carries them.
|
||||||
for (const setting of BUILTIN_WORKFLOW_SETTINGS) {
|
for (const setting of BUILTIN_MOVED_WORKFLOW_SETTINGS) {
|
||||||
expect(Object.prototype.hasOwnProperty.call(legacy, setting.id)).toBe(false);
|
expect(Object.prototype.hasOwnProperty.call(legacy, setting.id)).toBe(false);
|
||||||
}
|
}
|
||||||
// The declaration defaults are now the single source of truth; pin the legacy
|
// The declaration defaults are now the single source of truth; pin the legacy
|
||||||
@@ -249,7 +252,7 @@ describe("built-in workflow settings parity anchor (U1, R4)", () => {
|
|||||||
reflectionEnabled: false,
|
reflectionEnabled: false,
|
||||||
// Per-phase model lanes have undefined legacy defaults → declaration omits default.
|
// Per-phase model lanes have undefined legacy defaults → declaration omits default.
|
||||||
};
|
};
|
||||||
for (const setting of BUILTIN_WORKFLOW_SETTINGS) {
|
for (const setting of BUILTIN_MOVED_WORKFLOW_SETTINGS) {
|
||||||
if (Object.prototype.hasOwnProperty.call(expectedDefaults, setting.id)) {
|
if (Object.prototype.hasOwnProperty.call(expectedDefaults, setting.id)) {
|
||||||
expect(setting.default).toStrictEqual(expectedDefaults[setting.id]);
|
expect(setting.default).toStrictEqual(expectedDefaults[setting.id]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -402,8 +402,8 @@ When the task includes \`breakIntoSubtasks: true\`, first decide whether it shou
|
|||||||
For tasks you assess as Size M or L, consider whether splitting into 2-5 child tasks would improve execution quality. Default to keeping the task whole; only split when the work is genuinely large or has clearly independent deliverables.
|
For tasks you assess as Size M or L, consider whether splitting into 2-5 child tasks would improve execution quality. Default to keeping the task whole; only split when the work is genuinely large or has clearly independent deliverables.
|
||||||
|
|
||||||
**Consider splitting when ANY of these apply:**
|
**Consider splitting when ANY of these apply:**
|
||||||
- The task will require MORE THAN 7 implementation steps
|
- The task will require MORE THAN {{triageSubtaskStepThreshold}} implementation steps
|
||||||
- The task affects MORE THAN 3 different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change)
|
- The task affects MORE THAN {{triageSubtaskPackageThreshold}} different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change)
|
||||||
- Any single step would take more than 1-2 hours to complete
|
- Any single step would take more than 1-2 hours to complete
|
||||||
- The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people
|
- The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people
|
||||||
|
|
||||||
@@ -416,10 +416,10 @@ For tasks you assess as Size M or L, consider whether splitting into 2-5 child t
|
|||||||
- If you decide not to split an M/L task, proceed with a normal PROMPT.md specification
|
- If you decide not to split an M/L task, proceed with a normal PROMPT.md specification
|
||||||
|
|
||||||
**Broad-scope decomposition signals:**
|
**Broad-scope decomposition signals:**
|
||||||
- Size L tasks, especially when the planned step count would reach 9 or more.
|
- Size L tasks, especially when the planned step count would reach {{triageSubtaskLargeStepSignal}} or more.
|
||||||
- Plans whose implementation-step count would reach 12 or more (additive signal — counts even when the surrounding "more than 7/10 steps" threshold above has not yet fired).
|
- Plans whose implementation-step count would reach {{triageSubtaskAdditiveStepSignal}} or more (additive signal — counts even when the surrounding step-count threshold above has not yet fired).
|
||||||
- Tasks whose declared \`## File Scope\` would list 20 or more entries.
|
- Tasks whose declared \`## File Scope\` would list {{triageSubtaskFileScopeThreshold}} or more entries.
|
||||||
- Descriptions that quantify large remediation batches (for example "47 failing tests", "30+ broken files") at or above 30 items — treat as a strong signal that the work should be partitioned by subsystem or file group before specifying.
|
- Descriptions that quantify large remediation batches (for example "47 failing tests", "30+ broken files") at or above {{triageSubtaskRemediationBatchThreshold}} items — treat as a strong signal that the work should be partitioned by subsystem or file group before specifying.
|
||||||
- When two or more of the signals above fire together, default to splitting via \`fn_task_create\`. If you still choose to keep the task as a single unit, justify the decision explicitly in the PROMPT.md \`## Mission\` paragraph.
|
- When two or more of the signals above fire together, default to splitting via \`fn_task_create\`. If you still choose to keep the task as a single unit, justify the decision explicitly in the PROMPT.md \`## Mission\` paragraph.
|
||||||
|
|
||||||
## Triage tools
|
## Triage tools
|
||||||
@@ -445,7 +445,7 @@ When ALL of the following are true, include this metadata line in the header blo
|
|||||||
- Add this exact line: **No commits expected:** true
|
- Add this exact line: **No commits expected:** true
|
||||||
|
|
||||||
Set it only when all of these conditions hold:
|
Set it only when all of these conditions hold:
|
||||||
- Title/mission starts with decision verbs like "Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", or "Investigate and report"
|
- Title/mission starts with decision verbs like {{triageNoCommitsDecisionVerbs}}
|
||||||
- Acceptance criteria are strictly observational (record findings, log a decision, update task log/docs) with no required code/config/file mutations
|
- Acceptance criteria are strictly observational (record findings, log a decision, update task log/docs) with no required code/config/file mutations
|
||||||
- Task description explicitly says things like "no code changes expected" or "the deliverable is the recorded decision"
|
- Task description explicitly says things like "no code changes expected" or "the deliverable is the recorded decision"
|
||||||
|
|
||||||
@@ -463,7 +463,7 @@ Anti-heuristics (bias to false-negative when ambiguous):
|
|||||||
- Always include a testing step and a documentation step
|
- Always include a testing step and a documentation step
|
||||||
- For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`fn_task_document_write\`
|
- For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`fn_task_document_write\`
|
||||||
- Include a "Do NOT" section with project-appropriate guardrails
|
- Include a "Do NOT" section with project-appropriate guardrails
|
||||||
- Size assessment: S (<2h), M (2-4h), L (4-8h). Split if XL (8h+)
|
- Size assessment: S (<{{triageSizeSmallMaxHours}}h), M ({{triageSizeSmallMaxHours}}-{{triageSizeMediumMaxHours}}h), L ({{triageSizeMediumMaxHours}}-{{triageSizeLargeMaxHours}}h). Split if XL ({{triageSizeLargeMaxHours}}h+)
|
||||||
- Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
|
- Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
|
||||||
- 0-1 → Level 0, 2-3 → Level 1, 4-5 → Level 2, 6-8 → Level 3
|
- 0-1 → Level 0, 2-3 → Level 1, 4-5 → Level 2, 6-8 → Level 3
|
||||||
|
|
||||||
@@ -476,8 +476,8 @@ package.json when explicit commands are provided.
|
|||||||
## Workflow Routing
|
## Workflow Routing
|
||||||
- Call \`fn_workflow_list\` to discover available workflows before selecting a routing path, and read each workflow description as the routing signal.
|
- Call \`fn_workflow_list\` to discover available workflows before selecting a routing path, and read each workflow description as the routing signal.
|
||||||
- For investigation, audit, research, or decision-only tasks that produce no code changes, set \`**No commits expected:** true\` in the PROMPT.md header when the no-commits criteria above are met, then select an appropriate lightweight workflow.
|
- For investigation, audit, research, or decision-only tasks that produce no code changes, set \`**No commits expected:** true\` in the PROMPT.md header when the no-commits criteria above are met, then select an appropriate lightweight workflow.
|
||||||
- For decision-only tasks (Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report), prefer \`builtin:quick-fix\` or a custom investigation workflow when one is available.
|
- For decision-only tasks ({{triageNoCommitsDecisionVerbs}}), prefer \`{{triageDecisionOnlyWorkflowId}}\` or a custom investigation workflow when one is available.
|
||||||
- For standard coding tasks, \`builtin:coding\` is the default and is usually appropriate.
|
- For standard coding tasks, \`{{triageDefaultWorkflowId}}\` is the default and is usually appropriate.
|
||||||
- Use \`fn_workflow_select\` to set the workflow on the current task, or pass \`workflow_id\` to \`fn_task_create\` when creating subtasks.
|
- Use \`fn_workflow_select\` to set the workflow on the current task, or pass \`workflow_id\` to \`fn_task_create\` when creating subtasks.
|
||||||
- Match the task nature to the workflow description; descriptions are authoritative for routing decisions.
|
- Match the task nature to the workflow description; descriptions are authoritative for routing decisions.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
|
import type { Settings } from "./types.js";
|
||||||
import type { WorkflowSettingDefinition } from "./workflow-ir-types.js";
|
import type { WorkflowSettingDefinition } from "./workflow-ir-types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in workflow settings catalog.
|
||||||
|
*
|
||||||
|
* `BUILTIN_MOVED_WORKFLOW_SETTINGS` is the U4 moved-key catalog: keys that
|
||||||
|
* formerly lived in `DEFAULT_PROJECT_SETTINGS` and are tombstoned by
|
||||||
|
* `MOVED_SETTINGS_KEYS`. Keep those defaults byte-equal to the legacy literals.
|
||||||
|
*
|
||||||
|
* `BUILTIN_TRIAGE_POLICY_SETTINGS` is workflow-native triage/spec policy. These
|
||||||
|
* keys never lived in `DEFAULT_PROJECT_SETTINGS`, are NOT part of the U4
|
||||||
|
* hard-move migration, must never be added to `MOVED_SETTINGS_KEYS`, and must
|
||||||
|
* not appear in project/global settings schemas. Canonical values are inherited
|
||||||
|
* from the post-FN-6232 planning prompt: subtask step threshold `7` (not the
|
||||||
|
* older engine copy) and packages/modules threshold `3`.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The moved-key catalog declared as workflow settings (U1, R4).
|
* The moved-key catalog declared as workflow settings (U1, R4).
|
||||||
*
|
*
|
||||||
@@ -23,7 +39,7 @@ import type { WorkflowSettingDefinition } from "./workflow-ir-types.js";
|
|||||||
* in project settings.
|
* in project settings.
|
||||||
* - merge-cluster keys + `maxConcurrent` — owned by the columns/traits track.
|
* - merge-cluster keys + `maxConcurrent` — owned by the columns/traits track.
|
||||||
*/
|
*/
|
||||||
export const BUILTIN_WORKFLOW_SETTINGS: WorkflowSettingDefinition[] = [
|
export const BUILTIN_MOVED_WORKFLOW_SETTINGS: WorkflowSettingDefinition[] = [
|
||||||
// ── Step execution ─────────────────────────────────────────────────────
|
// ── Step execution ─────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: "workflowStepTimeoutMs",
|
id: "workflowStepTimeoutMs",
|
||||||
@@ -230,3 +246,139 @@ export const BUILTIN_WORKFLOW_SETTINGS: WorkflowSettingDefinition[] = [
|
|||||||
description: "Fallback model id for the validation phase.",
|
description: "Fallback model id for the validation phase.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_TRIAGE_POLICY_SETTINGS: WorkflowSettingDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "triageSizeSmallMaxHours",
|
||||||
|
name: "Triage size S max hours",
|
||||||
|
type: "number",
|
||||||
|
default: 2,
|
||||||
|
description: "Upper hour boundary for Size S triage guidance (S is below this value).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSizeMediumMaxHours",
|
||||||
|
name: "Triage size M max hours",
|
||||||
|
type: "number",
|
||||||
|
default: 4,
|
||||||
|
description: "Upper hour boundary for Size M triage guidance.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSizeLargeMaxHours",
|
||||||
|
name: "Triage size L max hours",
|
||||||
|
type: "number",
|
||||||
|
default: 8,
|
||||||
|
description: "Upper hour boundary for Size L triage guidance; larger work should split as XL.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSubtaskStepThreshold",
|
||||||
|
name: "Triage subtask step threshold",
|
||||||
|
type: "number",
|
||||||
|
default: 7,
|
||||||
|
description: "Implementation-step count above which triage should consider splitting an M/L task.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSubtaskLargeStepSignal",
|
||||||
|
name: "Triage large-step signal",
|
||||||
|
type: "number",
|
||||||
|
default: 9,
|
||||||
|
description: "Planned step count that is a broad-scope decomposition signal for Size L tasks.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSubtaskAdditiveStepSignal",
|
||||||
|
name: "Triage additive step signal",
|
||||||
|
type: "number",
|
||||||
|
default: 12,
|
||||||
|
description: "Implementation-step count that independently signals possible partitioning.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSubtaskPackageThreshold",
|
||||||
|
name: "Triage package/module threshold",
|
||||||
|
type: "number",
|
||||||
|
default: 3,
|
||||||
|
description: "Distinct package/module count above which triage should consider splitting coherent M/L work.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSubtaskFileScopeThreshold",
|
||||||
|
name: "Triage file-scope threshold",
|
||||||
|
type: "number",
|
||||||
|
default: 20,
|
||||||
|
description: "File Scope entry count that signals broad work likely needing partitioning.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageSubtaskRemediationBatchThreshold",
|
||||||
|
name: "Triage remediation batch threshold",
|
||||||
|
type: "number",
|
||||||
|
default: 30,
|
||||||
|
description: "Quantified remediation batch size that strongly signals subsystem partitioning.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageNoCommitsDecisionVerbs",
|
||||||
|
name: "Triage no-commits decision verbs",
|
||||||
|
type: "multi-enum",
|
||||||
|
default: ["Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", "Investigate and report"],
|
||||||
|
options: [
|
||||||
|
{ value: "Decide", label: "Decide" },
|
||||||
|
{ value: "Evaluate", label: "Evaluate" },
|
||||||
|
{ value: "Verify", label: "Verify" },
|
||||||
|
{ value: "Confirm", label: "Confirm" },
|
||||||
|
{ value: "Audit", label: "Audit" },
|
||||||
|
{ value: "Review whether", label: "Review whether" },
|
||||||
|
{ value: "Investigate and report", label: "Investigate and report" },
|
||||||
|
],
|
||||||
|
description: "Decision-only title/mission verbs used when deciding whether a task expects no commits.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageDecisionOnlyWorkflowId",
|
||||||
|
name: "Triage decision-only workflow",
|
||||||
|
type: "enum",
|
||||||
|
default: "builtin:quick-fix",
|
||||||
|
options: [
|
||||||
|
{ value: "builtin:quick-fix", label: "Quick fix" },
|
||||||
|
{ value: "builtin:coding", label: "Coding" },
|
||||||
|
],
|
||||||
|
description: "Preferred workflow id for decision-only or investigation tasks that expect no code changes.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "triageDefaultWorkflowId",
|
||||||
|
name: "Triage default workflow",
|
||||||
|
type: "enum",
|
||||||
|
default: "builtin:coding",
|
||||||
|
options: [
|
||||||
|
{ value: "builtin:coding", label: "Coding" },
|
||||||
|
{ value: "builtin:quick-fix", label: "Quick fix" },
|
||||||
|
],
|
||||||
|
description: "Default workflow id for standard coding tasks.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BUILTIN_WORKFLOW_SETTINGS: WorkflowSettingDefinition[] = [
|
||||||
|
...BUILTIN_MOVED_WORKFLOW_SETTINGS,
|
||||||
|
...BUILTIN_TRIAGE_POLICY_SETTINGS,
|
||||||
|
];
|
||||||
|
|
||||||
|
const TRIAGE_POLICY_DEFAULTS = new Map(
|
||||||
|
BUILTIN_TRIAGE_POLICY_SETTINGS.map((setting) => [setting.id, setting.default]),
|
||||||
|
);
|
||||||
|
|
||||||
|
function formatTriagePolicyValue(id: string, value: unknown): string {
|
||||||
|
if (id === "triageNoCommitsDecisionVerbs") {
|
||||||
|
const verbs = Array.isArray(value) ? value : TRIAGE_POLICY_DEFAULTS.get(id);
|
||||||
|
return (Array.isArray(verbs) ? verbs : []).map((verb) => String(verb)).join(", ");
|
||||||
|
}
|
||||||
|
return String(value ?? TRIAGE_POLICY_DEFAULTS.get(id) ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderTriagePolicyPlaceholders(prompt: string, settings: Partial<Settings>): string {
|
||||||
|
let rendered = prompt;
|
||||||
|
const values = settings as Record<string, unknown>;
|
||||||
|
for (const setting of BUILTIN_TRIAGE_POLICY_SETTINGS) {
|
||||||
|
const token = new RegExp(`\\{\\{${setting.id}\\}\\}`, "g");
|
||||||
|
rendered = rendered.replace(token, formatTriagePolicyValue(setting.id, values[setting.id] ?? setting.default));
|
||||||
|
}
|
||||||
|
const leftover = rendered.match(/\{\{[^}]+\}\}/);
|
||||||
|
if (leftover) {
|
||||||
|
throw new Error(`Unresolved triage policy placeholder: ${leftover[0]}`);
|
||||||
|
}
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,12 @@ export type {
|
|||||||
export { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
|
export { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
|
||||||
export { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
|
export { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js";
|
||||||
export { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
|
export { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
|
||||||
export { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
|
export {
|
||||||
|
BUILTIN_WORKFLOW_SETTINGS,
|
||||||
|
BUILTIN_MOVED_WORKFLOW_SETTINGS,
|
||||||
|
BUILTIN_TRIAGE_POLICY_SETTINGS,
|
||||||
|
renderTriagePolicyPlaceholders,
|
||||||
|
} from "./builtin-workflow-settings.js";
|
||||||
export {
|
export {
|
||||||
MOVED_SETTINGS_KEYS,
|
MOVED_SETTINGS_KEYS,
|
||||||
SETTINGS_MIGRATION_VERSION,
|
SETTINGS_MIGRATION_VERSION,
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
* `MOVED_SETTINGS_KEYS` is the single, authoritative record of the settings keys
|
* `MOVED_SETTINGS_KEYS` is the single, authoritative record of the settings keys
|
||||||
* that left `DEFAULT_PROJECT_SETTINGS` and now live exclusively as **workflow
|
* that left `DEFAULT_PROJECT_SETTINGS` and now live exclusively as **workflow
|
||||||
* setting values** per `(workflowId, projectId)`. It is derived directly from the
|
* setting values** per `(workflowId, projectId)`. It is derived directly from the
|
||||||
* built-in workflow declaration catalog (`BUILTIN_WORKFLOW_SETTINGS`) so the move
|
* moved workflow declaration catalog (`BUILTIN_MOVED_WORKFLOW_SETTINGS`) so the move
|
||||||
* has exactly one source of truth — a key is "moved" iff a built-in workflow
|
* has exactly one source of truth. Workflow-native declarations (for example
|
||||||
* declares it. Adding/removing a key from the catalog automatically reflows the
|
* triage policy thresholds) are deliberately excluded from this tombstone.
|
||||||
|
* Adding/removing a key from the moved catalog automatically reflows the
|
||||||
* tombstone list, the migration write target, and the stale-writer guard.
|
* tombstone list, the migration write target, and the stale-writer guard.
|
||||||
*
|
*
|
||||||
* What the tombstone shields (KTD-5, R8):
|
* What the tombstone shields (KTD-5, R8):
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
* setting and is intentionally ABSENT from this list.
|
* setting and is intentionally ABSENT from this list.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
|
import { BUILTIN_MOVED_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the per-project settings hard-move migration. Persisted per
|
* The version of the per-project settings hard-move migration. Persisted per
|
||||||
@@ -49,11 +50,11 @@ export const SETTINGS_MIGRATION_VERSION = 1;
|
|||||||
export const SETTINGS_MIGRATION_MARKER_KEY = "settingsMigrationVersion";
|
export const SETTINGS_MIGRATION_MARKER_KEY = "settingsMigrationVersion";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The definitive moved-key catalog — derived from the built-in workflow
|
* The definitive moved-key catalog — derived from the moved workflow
|
||||||
* declarations so it cannot drift from them. Frozen so callers cannot mutate it.
|
* declarations so it cannot drift from them. Frozen so callers cannot mutate it.
|
||||||
*/
|
*/
|
||||||
export const MOVED_SETTINGS_KEYS: readonly string[] = Object.freeze(
|
export const MOVED_SETTINGS_KEYS: readonly string[] = Object.freeze(
|
||||||
BUILTIN_WORKFLOW_SETTINGS.map((s) => s.id),
|
BUILTIN_MOVED_WORKFLOW_SETTINGS.map((s) => s.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Set form for O(1) membership checks on the hot write path. */
|
/** Set form for O(1) membership checks on the hot write path. */
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||||||
import type { Settings, Task, TaskDetail, TaskStore, WorkflowIr } from "@fusion/core";
|
import type { Settings, Task, TaskDetail, TaskStore, WorkflowIr } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
BUILTIN_CODING_WORKFLOW_IR,
|
BUILTIN_CODING_WORKFLOW_IR,
|
||||||
|
renderTriagePolicyPlaceholders,
|
||||||
resolveAgentPrompt,
|
resolveAgentPrompt,
|
||||||
resolvePlanningPromptFromIr,
|
resolvePlanningPromptFromIr,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
@@ -107,6 +108,8 @@ async function captureBasePrompt(task: Task, store: TaskStore): Promise<string>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canonicalPlanningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR)!;
|
const canonicalPlanningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR)!;
|
||||||
|
const renderedCanonicalPlanningPrompt = renderTriagePolicyPlaceholders(canonicalPlanningPrompt, {});
|
||||||
|
const renderedDefaultTriagePrompt = renderTriagePolicyPlaceholders(resolveAgentPrompt("triage"), {});
|
||||||
|
|
||||||
describe("triage planning prompt single source", () => {
|
describe("triage planning prompt single source", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -119,14 +122,14 @@ describe("triage planning prompt single source", () => {
|
|||||||
getTaskWorkflowSelection: vi.fn().mockReturnValue({ workflowId: "builtin:coding", stepIds: [] }),
|
getTaskWorkflowSelection: vi.fn().mockReturnValue({ workflowId: "builtin:coding", stepIds: [] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(captureBasePrompt(task, store)).resolves.toBe(canonicalPlanningPrompt);
|
await expect(captureBasePrompt(task, store)).resolves.toBe(renderedCanonicalPlanningPrompt);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the built-in workflow IR planning prompt when no workflow is selected", async () => {
|
it("uses the built-in workflow IR planning prompt when no workflow is selected", async () => {
|
||||||
const task = createTask({ id: "FN-6232-NO-SELECTION", executionMode: "standard" });
|
const task = createTask({ id: "FN-6232-NO-SELECTION", executionMode: "standard" });
|
||||||
const store = createStore(task);
|
const store = createStore(task);
|
||||||
|
|
||||||
await expect(captureBasePrompt(task, store)).resolves.toBe(canonicalPlanningPrompt);
|
await expect(captureBasePrompt(task, store)).resolves.toBe(renderedCanonicalPlanningPrompt);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves user triage prompt override precedence", async () => {
|
it("preserves user triage prompt override precedence", async () => {
|
||||||
@@ -174,6 +177,6 @@ describe("triage planning prompt single source", () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(captureBasePrompt(task, store)).resolves.toBe(resolveAgentPrompt("triage"));
|
await expect(captureBasePrompt(task, store)).resolves.toBe(renderedDefaultTriagePrompt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, expect, it, afterEach } from "vitest";
|
||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import {
|
||||||
|
BUILTIN_CODING_WORKFLOW_IR,
|
||||||
|
renderTriagePolicyPlaceholders,
|
||||||
|
resolveEffectiveSettingsById,
|
||||||
|
resolvePlanningPromptFromIr,
|
||||||
|
TaskStore,
|
||||||
|
} from "@fusion/core";
|
||||||
|
|
||||||
|
const cleanupDirs: string[] = [];
|
||||||
|
|
||||||
|
function makeTempDir(prefix: string): string {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), prefix));
|
||||||
|
cleanupDirs.push(dir);
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
while (cleanupDirs.length) {
|
||||||
|
rmSync(cleanupDirs.pop()!, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function builtinPlanningPrompt(): string {
|
||||||
|
const prompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR);
|
||||||
|
if (!prompt) throw new Error("builtin:coding planning prompt missing");
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("triage threshold workflow settings", () => {
|
||||||
|
it("renders behavior-equivalent defaults into the built-in planning prompt", () => {
|
||||||
|
const rendered = renderTriagePolicyPlaceholders(builtinPlanningPrompt(), {});
|
||||||
|
|
||||||
|
expect(rendered).toContain("MORE THAN 7 implementation steps");
|
||||||
|
expect(rendered).toContain("MORE THAN 3 different packages/modules");
|
||||||
|
expect(rendered).toContain("9 or more");
|
||||||
|
expect(rendered).toContain("12 or more");
|
||||||
|
expect(rendered).toContain("20 or more entries");
|
||||||
|
expect(rendered).toContain("at or above 30 items");
|
||||||
|
expect(rendered).toContain("S (<2h), M (2-4h), L (4-8h). Split if XL (8h+)");
|
||||||
|
expect(rendered).toContain("Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report");
|
||||||
|
expect(rendered).toContain("prefer `builtin:quick-fix`");
|
||||||
|
expect(rendered).toContain("`builtin:coding` is the default");
|
||||||
|
expect(rendered).not.toContain("{{");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reflects stored workflow overrides in effective settings and rendered prompt", async () => {
|
||||||
|
const rootDir = makeTempDir("fn-6233-triage-root-");
|
||||||
|
const globalDir = makeTempDir("fn-6233-triage-global-");
|
||||||
|
const store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
|
||||||
|
await store.init();
|
||||||
|
try {
|
||||||
|
const projectId = store.getWorkflowSettingsProjectId();
|
||||||
|
await store.updateWorkflowSettingValues("builtin:coding", projectId, { triageSubtaskStepThreshold: 3 });
|
||||||
|
|
||||||
|
const effective = await resolveEffectiveSettingsById(store, "builtin:coding", projectId);
|
||||||
|
expect(effective.triageSubtaskStepThreshold).toBe(3);
|
||||||
|
|
||||||
|
const rendered = renderTriagePolicyPlaceholders(builtinPlanningPrompt(), effective);
|
||||||
|
expect(rendered).toContain("MORE THAN 3 implementation steps");
|
||||||
|
expect(rendered).not.toContain("MORE THAN 7 implementation steps");
|
||||||
|
expect(rendered).not.toContain("{{");
|
||||||
|
} finally {
|
||||||
|
store.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps migrated threshold numbers out of the triage prompt assembly code path", async () => {
|
||||||
|
const source = await readFile(new URL("../triage.ts", import.meta.url), "utf8");
|
||||||
|
const promptAssembly = source.slice(
|
||||||
|
source.indexOf("const workflowPlanningPrompt"),
|
||||||
|
source.indexOf("const triageSystemPromptFinal"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(promptAssembly).toContain("renderTriagePolicyPlaceholders");
|
||||||
|
expect(promptAssembly).not.toMatch(/\b(?:7|9|12|20|30)\b/);
|
||||||
|
expect(promptAssembly).not.toMatch(/builtin:quick-fix|builtin:coding/);
|
||||||
|
expect(promptAssembly).not.toMatch(/Decide|Evaluate|Verify|Confirm|Audit|Review whether|Investigate and report/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core";
|
import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core";
|
||||||
import { resolveAgentPrompt } from "@fusion/core";
|
import { renderTriagePolicyPlaceholders, resolveAgentPrompt } from "@fusion/core";
|
||||||
import {
|
import {
|
||||||
TriageProcessor,
|
TriageProcessor,
|
||||||
TRIAGE_SYSTEM_PROMPT,
|
|
||||||
FAST_TRIAGE_SYSTEM_PROMPT,
|
FAST_TRIAGE_SYSTEM_PROMPT,
|
||||||
buildSpecificationPrompt,
|
buildSpecificationPrompt,
|
||||||
readAttachmentContents,
|
readAttachmentContents,
|
||||||
@@ -23,6 +22,7 @@ const { mockReviewStep, mockCreateFnAgent } = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage");
|
const TRIAGE_POLICY_PROMPT = resolveAgentPrompt("triage");
|
||||||
|
const RENDERED_TRIAGE_POLICY_PROMPT = renderTriagePolicyPlaceholders(TRIAGE_POLICY_PROMPT, {});
|
||||||
|
|
||||||
vi.mock("../reviewer.js", () => ({
|
vi.mock("../reviewer.js", () => ({
|
||||||
reviewStep: mockReviewStep,
|
reviewStep: mockReviewStep,
|
||||||
@@ -635,11 +635,12 @@ describe("canonical triage policy prompt", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes explicit subtask breakdown thresholds", () => {
|
it("includes explicit rendered subtask breakdown thresholds", () => {
|
||||||
expect(TRIAGE_POLICY_PROMPT).toContain("MORE THAN 7 implementation steps");
|
expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain("MORE THAN 7 implementation steps");
|
||||||
expect(TRIAGE_POLICY_PROMPT).toContain(
|
expect(RENDERED_TRIAGE_POLICY_PROMPT).toContain(
|
||||||
"MORE THAN 3 different packages/modules",
|
"MORE THAN 3 different packages/modules",
|
||||||
);
|
);
|
||||||
|
expect(TRIAGE_POLICY_PROMPT).toContain("MORE THAN {{triageSubtaskStepThreshold}} implementation steps");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("biases toward keeping tasks whole and acknowledges coordination overhead", () => {
|
it("biases toward keeping tasks whole and acknowledges coordination overhead", () => {
|
||||||
@@ -676,7 +677,7 @@ describe("FN-5893 invariant regression wording", () => {
|
|||||||
const missingSectionRevisePattern =
|
const missingSectionRevisePattern =
|
||||||
/For bug fixes and UI-affordance add\/remove tasks, the spec MUST include a `## Surface Enumeration` section\. During self-review via `fn_review_spec\(\)`, treat a missing section on a bug-fix or UI-affordance add\/remove spec as a blocking REVISE\./;
|
/For bug fixes and UI-affordance add\/remove tasks, the spec MUST include a `## Surface Enumeration` section\. During self-review via `fn_review_spec\(\)`, treat a missing section on a bug-fix or UI-affordance add\/remove spec as a blocking REVISE\./;
|
||||||
|
|
||||||
for (const prompt of [TRIAGE_POLICY_PROMPT, TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
|
for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
|
||||||
expect(prompt).toContain("## Surface Enumeration");
|
expect(prompt).toContain("## Surface Enumeration");
|
||||||
expect(prompt).toMatch(missingSectionRevisePattern);
|
expect(prompt).toMatch(missingSectionRevisePattern);
|
||||||
expect(prompt).toContain("docs/testing.md");
|
expect(prompt).toContain("docs/testing.md");
|
||||||
@@ -709,7 +710,7 @@ describe("FN-5893 invariant regression wording", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("defines the FN-6229 Symptom Verification contract in standard and fast prompts", () => {
|
it("defines the FN-6229 Symptom Verification contract in standard and fast prompts", () => {
|
||||||
for (const prompt of [TRIAGE_POLICY_PROMPT, TRIAGE_SYSTEM_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
|
for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
|
||||||
expect(prompt).toContain("## Symptom Verification");
|
expect(prompt).toContain("## Symptom Verification");
|
||||||
expect(prompt).toContain("Use the exact heading `## Symptom Verification`");
|
expect(prompt).toContain("Use the exact heading `## Symptom Verification`");
|
||||||
expect(prompt).toContain("**Original symptom** — what the user/issue reported was broken");
|
expect(prompt).toContain("**Original symptom** — what the user/issue reported was broken");
|
||||||
@@ -759,7 +760,7 @@ describe("fast-mode triage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("documents workflow routing in standard and fast prompts", () => {
|
it("documents workflow routing in standard and fast prompts", () => {
|
||||||
for (const prompt of [TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
|
for (const prompt of [RENDERED_TRIAGE_POLICY_PROMPT, FAST_TRIAGE_SYSTEM_PROMPT]) {
|
||||||
expect(prompt).toContain("## Workflow Routing");
|
expect(prompt).toContain("## Workflow Routing");
|
||||||
expect(prompt).toContain("fn_workflow_list");
|
expect(prompt).toContain("fn_workflow_list");
|
||||||
expect(prompt).toContain("fn_workflow_select");
|
expect(prompt).toContain("fn_workflow_select");
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
getTaskDuplicateLineage,
|
getTaskDuplicateLineage,
|
||||||
parseExplicitDuplicateMarker,
|
parseExplicitDuplicateMarker,
|
||||||
resolveAgentPrompt,
|
resolveAgentPrompt,
|
||||||
|
renderTriagePolicyPlaceholders,
|
||||||
resolveTaskPlanningPrompt,
|
resolveTaskPlanningPrompt,
|
||||||
resolvePersistAgentThinkingLog,
|
resolvePersistAgentThinkingLog,
|
||||||
compareTaskPriority,
|
compareTaskPriority,
|
||||||
@@ -88,339 +89,6 @@ import { archiveAsGhostBug } from "./self-healing.js";
|
|||||||
import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js";
|
import { createRunAuditor, generateSyntheticRunId } from "./run-audit.js";
|
||||||
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js";
|
import { resolveAndEmitGoalContext } from "./goal-injection-diagnostics.js";
|
||||||
|
|
||||||
export const TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "fn", an AI-orchestrated task board.
|
|
||||||
|
|
||||||
## Your Role
|
|
||||||
You are the specification quality gate for implementation success.
|
|
||||||
Your job: take a rough task description and produce a fully specified PROMPT.md that another AI agent can execute autonomously in a fresh context with zero memory of this conversation.
|
|
||||||
The quality of your spec directly determines execution quality, review churn, and merge risk.
|
|
||||||
|
|
||||||
## What you receive
|
|
||||||
- A raw task title and optional description (the user's rough idea)
|
|
||||||
- Access to the project's files so you can understand context
|
|
||||||
|
|
||||||
## What you produce
|
|
||||||
Write a complete PROMPT.md specification to the given path using the write tool.
|
|
||||||
|
|
||||||
## PROMPT.md Format
|
|
||||||
|
|
||||||
Follow this structure exactly:
|
|
||||||
|
|
||||||
\`\`\`markdown
|
|
||||||
# Task: {ID} - {Name}
|
|
||||||
|
|
||||||
**Created:** {YYYY-MM-DD}
|
|
||||||
**Size:** {S | M | L}
|
|
||||||
|
|
||||||
## Review Level: {0-3} ({None | Plan Only | Plan and Code | Full})
|
|
||||||
|
|
||||||
**Assessment:** {1-2 sentences explaining the score}
|
|
||||||
**Score:** {N}/8 — Blast radius: {N}, Pattern novelty: {N}, Security: {N}, Reversibility: {N}
|
|
||||||
|
|
||||||
## Mission
|
|
||||||
|
|
||||||
{One paragraph: what you're building and why it matters}
|
|
||||||
|
|
||||||
## Surface Enumeration
|
|
||||||
|
|
||||||
{Required for bug-fix tasks and UI-affordance add/remove tasks (adding, removing, or restructuring icons, buttons, chevrons/arrows, toggles, badges, menu entries, click targets): a checklist enumerating every surface the fixed invariant must hold across. Include every provider/bridge for streaming and agent paths; desktop AND mobile breakpoints; empty/undefined/duplicate/populated data states; and every hook/component/module that shares the affected logic. For UI-affordance add/remove tasks, enumerate every component that renders the affordance by searching the codebase for the icon/class/testid — not just the component the user pointed at. Explicitly check for leftover shells after removal (empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels) across both desktop and mobile breakpoints. Use the canonical checklist in docs/testing.md as the starting point.}
|
|
||||||
|
|
||||||
## Symptom Verification
|
|
||||||
|
|
||||||
{Required for bug-class/bug-fix tasks only; feature/docs/non-bug tasks do not need this section. Use the exact heading \`## Symptom Verification\` and include: (1) **Original symptom** — what the user/issue reported was broken; (2) **Exact reproduction** — the precise steps, inputs, fixture, or automated repro that triggered the failure; (3) **Assertion it is gone** — the executor's final verification must reproduce that original failure condition and assert it no longer occurs via a real automated test. Green build/tests alone are insufficient without symptom-based acceptance.}
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **None**
|
|
||||||
{OR}
|
|
||||||
- **Task:** {ID} ({what must be complete})
|
|
||||||
|
|
||||||
## Context to Read First
|
|
||||||
|
|
||||||
{List specific files the worker should read before starting — only what's needed}
|
|
||||||
|
|
||||||
## File Scope
|
|
||||||
|
|
||||||
{List files/directories the task will create or modify — be specific}
|
|
||||||
|
|
||||||
- \`path/to/file.ext\`
|
|
||||||
- \`path/to/directory/*\`
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
> Optional: a step heading may carry a \`(depends: N,M)\` annotation listing the 1-indexed
|
|
||||||
> step numbers it depends on — e.g. \`### Step 3 (depends: 1): Title\`. Annotate ONLY steps
|
|
||||||
> that are genuinely independent of their immediate predecessor; an unannotated step is
|
|
||||||
> assumed to depend on the one before it (fully sequential). Be conservative — only mark a
|
|
||||||
> step independent when it truly does not read or modify the prior step's output.
|
|
||||||
|
|
||||||
### Step 0: Preflight
|
|
||||||
|
|
||||||
- [ ] Required files and paths exist
|
|
||||||
- [ ] Dependencies satisfied
|
|
||||||
|
|
||||||
### Step 1: {Name}
|
|
||||||
|
|
||||||
- [ ] {Specific, verifiable outcome}
|
|
||||||
- [ ] {Specific, verifiable outcome}
|
|
||||||
- [ ] Run targeted tests for changed files, asserting the invariant across all known surfaces (enumerate every provider/bridge, desktop + mobile breakpoints, and empty/undefined/populated data states)
|
|
||||||
|
|
||||||
For bug-fix and UI-affordance add/remove tasks, paste and fill in this checklist in the \`## Surface Enumeration\` section:
|
|
||||||
- [ ] Providers / bridges / execution paths touched by the invariant
|
|
||||||
- [ ] Desktop + mobile breakpoints / platforms that exercise the behavior
|
|
||||||
- [ ] Empty / undefined / duplicate / populated data states
|
|
||||||
- [ ] Shared hooks / components / modules / helpers reusing the logic
|
|
||||||
- [ ] Every component that renders the affordance (search the codebase for the icon/class/testid, not just the one the user pointed at)
|
|
||||||
- [ ] Leftover shells after removal — empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels — are explicitly checked and fixed/hidden
|
|
||||||
|
|
||||||
For bug-class/bug-fix tasks, add and fill in the exact \`## Symptom Verification\` section:
|
|
||||||
- [ ] **Original symptom** — what the user/issue reported was broken
|
|
||||||
- [ ] **Exact reproduction** — the precise steps, inputs, fixture, or automated repro that triggered the failure
|
|
||||||
- [ ] **Assertion it is gone** — final verification reproduces the original failure condition and asserts it no longer occurs via a real automated test; green build/tests alone are insufficient
|
|
||||||
|
|
||||||
**Artifacts:**
|
|
||||||
- \`path/to/file\` (new | modified)
|
|
||||||
|
|
||||||
### Step {N-1}: Testing & Verification
|
|
||||||
|
|
||||||
> ZERO failures allowed for checks required by this task's quality gates. Run impacted/package-scoped verification first; run workspace-wide suites only when the task or workflow explicitly requires them, or during final integration after impacted checks pass.
|
|
||||||
> If keeping lint/tests/build/typecheck green requires edits outside the initial File Scope, make those fixes as part of this task.
|
|
||||||
|
|
||||||
- [ ] Run lint check (\`pnpm lint\`)
|
|
||||||
- [ ] Run impacted tests
|
|
||||||
- [ ] Run project typecheck if available
|
|
||||||
- [ ] Fix all failures
|
|
||||||
- [ ] Build passes
|
|
||||||
|
|
||||||
### Step {N}: Documentation & Delivery
|
|
||||||
|
|
||||||
- [ ] Update relevant documentation
|
|
||||||
- [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...)
|
|
||||||
- [ ] Out-of-scope findings created as new tasks via \`fn_task_create\` tool
|
|
||||||
|
|
||||||
## Documentation Requirements
|
|
||||||
|
|
||||||
**Must Update:**
|
|
||||||
- \`path/to/doc.md\` — {what to add/change}
|
|
||||||
|
|
||||||
**Check If Affected:**
|
|
||||||
- \`path/to/doc.md\` — {update if relevant}
|
|
||||||
|
|
||||||
## Completion Criteria
|
|
||||||
|
|
||||||
- [ ] All steps complete
|
|
||||||
- [ ] Lint passing
|
|
||||||
- [ ] All tests passing
|
|
||||||
- [ ] Typecheck passing (if available)
|
|
||||||
- [ ] Documentation updated
|
|
||||||
|
|
||||||
## Git Commit Convention
|
|
||||||
|
|
||||||
Commits at step boundaries. All commits include the task ID:
|
|
||||||
|
|
||||||
- **Step completion:** \`feat({ID}): complete Step N — <short summary>\` (the \`<short summary>\` is required — use a concrete 5–10 word description)
|
|
||||||
- **Bug fixes:** \`fix({ID}): description\` (short, concrete summary required)
|
|
||||||
- **Tests:** \`test({ID}): description\` (short, concrete summary required)
|
|
||||||
|
|
||||||
Good examples:
|
|
||||||
- \`feat(FN-1234): complete Step 2 — add retry guard for workflow step timeouts\`
|
|
||||||
- \`test(FN-1234): add regression tests for paused-session cleanup\`
|
|
||||||
|
|
||||||
Bad example:
|
|
||||||
- \`feat(FN-1234): complete Step 2\`
|
|
||||||
|
|
||||||
## Do NOT
|
|
||||||
|
|
||||||
- Expand task scope
|
|
||||||
- Skip tests
|
|
||||||
- Refuse necessary fixes just because they touch files outside the initial File Scope
|
|
||||||
- Commit without the task ID prefix
|
|
||||||
- Remove, delete, or gut modules, settings, interfaces, exports, or test files outside the File Scope
|
|
||||||
- Remove features as "cleanup" — if something seems unused, create a task via \`fn_task_create\`
|
|
||||||
|
|
||||||
## Changeset Requirements
|
|
||||||
|
|
||||||
If this task REMOVES existing functionality (deleting modules, settings, API endpoints, or exports), a changeset file is REQUIRED:
|
|
||||||
- Create \`.changeset/{task-id}-removal.md\` explaining what was removed and why
|
|
||||||
- This is mandatory for any net-negative change (more deletions than additions to existing files)
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Testing requirements
|
|
||||||
|
|
||||||
The Testing & Verification step MUST require REAL automated tests — actual test
|
|
||||||
files with assertions that run via a test runner. Typechecks and builds are NOT
|
|
||||||
tests. Manual verification is NOT a test.
|
|
||||||
|
|
||||||
- Each implementation step should include writing tests for the code being changed
|
|
||||||
- For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. During self-review via \`fn_review_spec()\`, treat a missing section on a bug-fix or UI-affordance add/remove spec as a blocking REVISE.
|
|
||||||
- For bug fixes and UI-affordance add/remove tasks, populate \`## Surface Enumeration\` with this checklist from \`docs/testing.md\`: providers/bridges/execution paths; desktop + mobile breakpoints/platforms; empty/undefined/duplicate/populated data states; shared hooks/components/modules/helpers; every component that renders the affordance; leftover shells after removal.
|
|
||||||
- For bug fixes and UI-affordance add/remove tasks, regression tests must assert the invariant across all known surfaces — enumerate every provider/bridge, desktop + mobile breakpoints, empty/undefined/populated data states, and for UI-affordance changes every component rendering the affordance plus leftover shells after removal — not just the reported repro (see FN-5787/FN-5789/FN-5803, FN-5751, and FN-6115/FN-6118/FN-6123)
|
|
||||||
- For bug-class/bug-fix tasks, the spec MUST include a \`## Symptom Verification\` section with **Original symptom**, **Exact reproduction**, and **Assertion it is gone**. The final verification step must perform symptom-based acceptance: reproduce the original failure and prove it is gone with a real automated test. Green build/tests alone are insufficient. Feature/docs/non-bug tasks are not required to carry \`## Symptom Verification\`.
|
|
||||||
- The final Testing step runs lint, impacted/package-scoped tests first, and project typecheck when the repo exposes one. Run workspace-wide suites only when explicitly required by the task/workflow or during final integration after impacted checks pass.
|
|
||||||
- Specs must instruct executors to fix lint failures and quality-gate failures directly, even when the required edits extend beyond the original File Scope
|
|
||||||
- If the project has no test framework, the Testing step must include setting one up
|
|
||||||
as part of this task (not just skipping tests)
|
|
||||||
|
|
||||||
## Duplicate check
|
|
||||||
Before writing a spec, first call \`fn_task_list\` to see active tasks, then call \`fn_task_search\` with 2-4 distinct keyword phrases from the task title and description (for example file paths, error symptoms, and symbol names).
|
|
||||||
For any likely match in \`done\` or \`archived\`, call \`fn_task_get\` to inspect details before deciding.
|
|
||||||
If a task already covers the same work (even if worded differently), do NOT
|
|
||||||
write a PROMPT.md. Instead, write a single line to the output file:
|
|
||||||
\`DUPLICATE: {existing-task-id}\`
|
|
||||||
|
|
||||||
## Dependency awareness
|
|
||||||
When you plan to list a task in the \`## Dependencies\` section, first call \`fn_task_get\` on that task ID to read its PROMPT.md.
|
|
||||||
Use what you learn — file scope, APIs, patterns, completion criteria — to make the new spec accurate: reference the right paths, avoid conflicting assumptions, and describe what the dependency must deliver before this task starts.
|
|
||||||
If the dependency task has no PROMPT.md yet (not yet specified), note that in the Dependencies section.
|
|
||||||
|
|
||||||
## Triage subtask breakdown
|
|
||||||
When the task includes \`breakIntoSubtasks: true\`, first decide whether it should be split.
|
|
||||||
|
|
||||||
- Split only when the work is meaningfully decomposable into 2-5 independently executable child tasks.
|
|
||||||
- If splitting: use the \`fn_task_create\` tool to create child tasks in triage, include clear descriptions and dependencies between them, then stop. Do NOT write a PROMPT.md for the parent task.
|
|
||||||
- **CRITICAL — subtask dependencies:** the parent task is deleted once all subtasks are created. \`dependencies\` on a new subtask may ONLY reference sibling subtasks you have created earlier in this same split (or unrelated existing tasks). **Never depend on the parent task's id.** If a child conceptually "waits for the parent's remaining work", create a sibling subtask that does that work and depend on the sibling instead. The \`fn_task_create\` tool will reject parent-id dependencies with an error.
|
|
||||||
- If not splitting: proceed with a normal PROMPT.md specification.
|
|
||||||
|
|
||||||
## Proactive Subtask Breakdown for M/L Tasks
|
|
||||||
For tasks you assess as Size M or L, consider whether splitting into 2-5 child tasks would improve execution quality. Default to keeping the task whole; only split when the work is genuinely large or has clearly independent deliverables.
|
|
||||||
|
|
||||||
**Consider splitting when ANY of these apply:**
|
|
||||||
- The task will require more than 10 implementation steps
|
|
||||||
- The task affects more than 5 different packages/modules with distinct concerns (a typed field change that naturally touches core types + store + UI + tests is NOT 4 distinct concerns — it's one coherent change)
|
|
||||||
- Any single step would take more than 3-4 hours to complete
|
|
||||||
- The task has multiple clearly independent deliverables that could be developed and shipped in parallel by different people
|
|
||||||
|
|
||||||
**Splitting guidance:**
|
|
||||||
- Even when \`breakIntoSubtasks\` is not set to \`true\`, apply these thresholds proactively
|
|
||||||
- Keep explicit user intent first: when \`breakIntoSubtasks: true\`, follow the mandatory breakdown flow above
|
|
||||||
- Size S tasks should NOT be split — the overhead outweighs the benefit
|
|
||||||
- A task with 7-10 focused steps within a coherent scope is fine as one unit; do not split it
|
|
||||||
- Coordination overhead (worktrees, dependency wiring, merge sequencing) is real — only split when the parallelism or scope-clarity benefit clearly outweighs it
|
|
||||||
- If you decide not to split an M/L task, proceed with a normal PROMPT.md specification
|
|
||||||
|
|
||||||
**Broad-scope decomposition signals:**
|
|
||||||
- Size L tasks, especially when the planned step count would reach 9 or more.
|
|
||||||
- Plans whose implementation-step count would reach 12 or more (additive signal — counts even when the surrounding "more than 7/10 steps" threshold above has not yet fired).
|
|
||||||
- Tasks whose declared \`## File Scope\` would list 20 or more entries.
|
|
||||||
- Descriptions that quantify large remediation batches (for example "47 failing tests", "30+ broken files") at or above 30 items — treat as a strong signal that the work should be partitioned by subsystem or file group before specifying.
|
|
||||||
- When two or more of the signals above fire together, default to splitting via \`fn_task_create\`. If you still choose to keep the task as a single unit, justify the decision explicitly in the PROMPT.md \`## Mission\` paragraph.
|
|
||||||
|
|
||||||
## Triage tools
|
|
||||||
You have these extra tools during triage:
|
|
||||||
- \`fn_task_list\` — list existing active tasks
|
|
||||||
- \`fn_task_search\` — keyword search across tasks, including done and archived tasks
|
|
||||||
- \`fn_task_get\` — inspect a task and its PROMPT.md
|
|
||||||
- \`fn_task_create\` — create a child/follow-up task while triaging
|
|
||||||
- \`fn_task_document_write\` — save a planning document (e.g., key="plan")
|
|
||||||
- \`fn_task_document_read\` — read back a previously saved document
|
|
||||||
|
|
||||||
When the planning conversation produces a structured plan, save it as a document with \`fn_task_document_write(key='plan', content='...')\` so the executor can reference it during implementation.
|
|
||||||
|
|
||||||
## Step Design Principles
|
|
||||||
- Each implementation step should produce a testable artifact or observable outcome
|
|
||||||
- Order steps by dependency (foundation before integration, implementation before final validation)
|
|
||||||
- Testing & Verification must run before Documentation & Delivery
|
|
||||||
- Avoid giant catch-all steps; split outcomes so execution can be verified incrementally
|
|
||||||
|
|
||||||
## Decision-only task flag (noCommitsExpected)
|
|
||||||
When ALL of the following are true, include this metadata line in the header block after Size/Review Level:
|
|
||||||
|
|
||||||
- Add this exact line: **No commits expected:** true
|
|
||||||
|
|
||||||
Set it only when all of these conditions hold:
|
|
||||||
- Title/mission starts with decision verbs like "Decide", "Evaluate", "Verify", "Confirm", "Audit", "Review whether", or "Investigate and report"
|
|
||||||
- Acceptance criteria are strictly observational (record findings, log a decision, update task log/docs) with no required code/config/file mutations
|
|
||||||
- Task description explicitly says things like "no code changes expected" or "the deliverable is the recorded decision"
|
|
||||||
|
|
||||||
Anti-heuristics (bias to false-negative when ambiguous):
|
|
||||||
- SET: Decide whether FN-XYZ needs a fix
|
|
||||||
- LEAVE UNSET: Investigate FN-XYZ
|
|
||||||
- LEAVE UNSET: Investigate FN-XYZ and fix if needed
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
- Read the project structure and relevant source files to understand context BEFORE writing
|
|
||||||
- Check package.json/scripts and explicit project commands to align real lint/test/build/typecheck commands
|
|
||||||
- Look for similar completed tasks and existing code patterns before inventing spec structure
|
|
||||||
- Be specific — name actual files, functions, and patterns from the codebase
|
|
||||||
- Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step)
|
|
||||||
- Always include a testing step and a documentation step
|
|
||||||
- For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`fn_task_document_write\`
|
|
||||||
- Include a "Do NOT" section with project-appropriate guardrails
|
|
||||||
- Size assessment: S (<2h), M (2-4h), L (4-8h). Split if XL (8h+)
|
|
||||||
- Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
|
|
||||||
- 0-1 → Level 0, 2-3 → Level 1, 4-5 → Level 2, 6-8 → Level 3
|
|
||||||
|
|
||||||
## Project commands
|
|
||||||
When the user prompt includes a "Project Commands" section with test and/or build
|
|
||||||
commands, use those EXACT commands in the testing/verification steps and anywhere
|
|
||||||
the spec references running tests or builds. Do NOT guess or infer commands from
|
|
||||||
package.json when explicit commands are provided.
|
|
||||||
|
|
||||||
## Workflow Routing
|
|
||||||
- Call \`fn_workflow_list\` to discover available workflows before selecting a routing path, and read each workflow description as the routing signal.
|
|
||||||
- For investigation, audit, research, or decision-only tasks that produce no code changes, set \`**No commits expected:** true\` in the PROMPT.md header when the no-commits criteria above are met, then select an appropriate lightweight workflow.
|
|
||||||
- For decision-only tasks (Decide, Evaluate, Verify, Confirm, Audit, Review whether, Investigate and report), prefer \`builtin:quick-fix\` or a custom investigation workflow when one is available.
|
|
||||||
- For standard coding tasks, \`builtin:coding\` is the default and is usually appropriate.
|
|
||||||
- Use \`fn_workflow_select\` to set the workflow on the current task, or pass \`workflow_id\` to \`fn_task_create\` when creating subtasks.
|
|
||||||
- Match the task nature to the workflow description; descriptions are authoritative for routing decisions.
|
|
||||||
|
|
||||||
## Spec Review
|
|
||||||
|
|
||||||
After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review.
|
|
||||||
|
|
||||||
- **APPROVE** → your spec is accepted, you're done
|
|
||||||
- **REVISE** → fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`fn_review_spec()\` again. Repeat until approved.
|
|
||||||
- **RETHINK** → your approach was fundamentally rejected. The conversation will rewind. Read the feedback carefully and take a completely different approach. Do NOT repeat the rejected strategy.
|
|
||||||
|
|
||||||
You MUST call \`fn_review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
|
|
||||||
|
|
||||||
## PROMPT.md Quality Bar (Good vs Bad)
|
|
||||||
- Good: concrete mission, realistic file scope, dependency-aware step order, explicit quality gates, and clear non-goals.
|
|
||||||
- Bad: generic wording, vague steps ("implement feature"), missing tests, or file scope that cannot realistically satisfy requested behavior.
|
|
||||||
- Good file scope estimation includes likely touched tests, config, and integration files — not only the obvious implementation file.
|
|
||||||
|
|
||||||
Never reference a \`.fusion/tasks/<id>/<file>\` artifact in Context, Steps, or File Scope unless (a) the file already exists, (b) the step explicitly creates it (listed as \`(new)\` under Artifacts), or (c) it is \`PROMPT.md\` / \`task.json\` / \`attachments/*\` for a sibling task. Save planning scratch as task documents via \`fn_task_document_write\`, not as files on disk.
|
|
||||||
|
|
||||||
## Output
|
|
||||||
Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review.
|
|
||||||
|
|
||||||
## Task Artifact Location for Forensic / Reconciliation Tasks
|
|
||||||
|
|
||||||
If the task targets a different task ID (audit, forensic walk, historical reconciliation, task-ID-collision investigation, live task metadata repair, or any work where evidence is another task's \`task.json\` / \`PROMPT.md\` / DB row), include this guidance in the generated PROMPT.md \`## Context to Read First\` and \`## File Scope\`:
|
|
||||||
- Authoritative target-task artifacts live at the **project root**: \`<rootDir>/.fusion/tasks/{TARGET_ID}/\` (\`task.json\`, \`PROMPT.md\`, \`attachments/\`, agent logs).
|
|
||||||
- Authoritative task DB rows live at the **project root** SQLite file: \`<rootDir>/.fusion/fusion.db\` (WAL mode). Read via \`TaskStore\` APIs; do not instruct direct SQL surgery.
|
|
||||||
- \`.fusion/\` is gitignored, so a fresh worktree from \`main\` does **not** include \`.fusion/tasks/{TARGET_ID}/\` or \`.fusion/fusion.db\`. The running worktree's own \`.fusion/\` (if present) is scratch/session state for the running task only, not source of truth.
|
|
||||||
- Prefer \`fn_task_get\` / \`fn_task_list\` when the target task ID is known; fall back to project-root filesystem reads only when tools cannot provide needed evidence.
|
|
||||||
|
|
||||||
## Frontend UX Criteria Injection
|
|
||||||
|
|
||||||
<!-- UX criteria mirror the "frontend-ux-design" reviewer persona in packages/core/src/types.ts — keep them aligned. -->
|
|
||||||
|
|
||||||
If the derived **File Scope** touches any of the following paths:
|
|
||||||
- \`packages/dashboard/**\`
|
|
||||||
- \`packages/*/app/components/**\`
|
|
||||||
- \`packages/*/app/hooks/**\`
|
|
||||||
- Any \`*.css\` or \`*.tsx\` file inside a dashboard-like package
|
|
||||||
|
|
||||||
…then **PREPEND** a \`## Frontend UX Criteria\` section to the generated PROMPT.md, placed immediately after the \`## Mission\` section.
|
|
||||||
|
|
||||||
Use this exact checklist (keep it verbatim — do not expand or reorder):
|
|
||||||
|
|
||||||
\`\`\`markdown
|
|
||||||
## Frontend UX Criteria
|
|
||||||
|
|
||||||
- [ ] **Design tokens only** — no hardcoded \`px\` values except \`0\`, no hardcoded hex/rgb colors; use CSS custom properties (\`--color-*\`, \`--spacing-*\`, etc.)
|
|
||||||
- [ ] **Icon sizing** — match the surrounding component's icon size convention (default lucide size unless the local pattern already uses an explicit \`size={N}\`)
|
|
||||||
- [ ] **Semantic color tokens for status** — use \`--color-error\` for stderr/error states, \`--color-warning\` for starting/pending states; never hardcode status colors
|
|
||||||
- [ ] **Component reuse** — reach for existing classes (\`.btn\`, \`.btn-icon\`, \`.card\`, \`.input\`) before writing one-off styles
|
|
||||||
- [ ] **Responsive scaffolding** — add \`@media (max-width: 768px)\` overrides for any new layout; verify mobile usability
|
|
||||||
- [ ] **Single canonical nav destination** — each route must appear in exactly one of: Header primary nav, Header overflow menu, or MobileNavBar More; no duplicates across all three
|
|
||||||
- [ ] **Status-indicator dot convention** — use the existing \`.status-dot\` pattern (size, border, animation) rather than custom dot styling
|
|
||||||
- [ ] **Visual hierarchy preserved** — new elements must not disrupt heading levels, content flow, or information architecture established in the surrounding page
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Only inject this section when the task genuinely touches frontend UI. Omit it for backend-only, config-only, or documentation-only tasks.`;
|
|
||||||
|
|
||||||
export const FAST_TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "fn", an AI-orchestrated task board. This task is running in **fast mode** — produce a lean, executable PROMPT.md without heavyweight review scoring or subtask analysis.
|
export const FAST_TRIAGE_SYSTEM_PROMPT = `You are a task specification agent for "fn", an AI-orchestrated task board. This task is running in **fast mode** — produce a lean, executable PROMPT.md without heavyweight review scoring or subtask analysis.
|
||||||
|
|
||||||
## Your Role
|
## Your Role
|
||||||
@@ -1318,9 +986,14 @@ export class TriageProcessor {
|
|||||||
? resolveAgentPrompt("triage", settings.agentPrompts)
|
? resolveAgentPrompt("triage", settings.agentPrompts)
|
||||||
: "";
|
: "";
|
||||||
const defaultTriagePrompt = resolveAgentPrompt("triage");
|
const defaultTriagePrompt = resolveAgentPrompt("triage");
|
||||||
|
const resolvedBasePrompt = userTriagePrompt
|
||||||
|
|| (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : (workflowPlanningPrompt || defaultTriagePrompt));
|
||||||
|
// Apply the workflow-native triage policy renderer to both standard and
|
||||||
|
// fast prompts. Fast mode currently has no policy placeholders, making
|
||||||
|
// this a no-op there while still guaranteeing no dangling token leaks.
|
||||||
|
const renderedBasePrompt = renderTriagePolicyPlaceholders(resolvedBasePrompt, settings);
|
||||||
const triageLayers = buildPromptLayers({
|
const triageLayers = buildPromptLayers({
|
||||||
basePrompt: userTriagePrompt
|
basePrompt: renderedBasePrompt,
|
||||||
|| (isFast ? FAST_TRIAGE_SYSTEM_PROMPT : (workflowPlanningPrompt || defaultTriagePrompt)),
|
|
||||||
goalContext: triageGoalResolution.goalContext,
|
goalContext: triageGoalResolution.goalContext,
|
||||||
agentInstructions: [
|
agentInstructions: [
|
||||||
triageIdentitySection,
|
triageIdentitySection,
|
||||||
|
|||||||
@@ -117,10 +117,7 @@ export default defineConfig({
|
|||||||
extends: true,
|
extends: true,
|
||||||
test: {
|
test: {
|
||||||
name: "engine-reliability",
|
name: "engine-reliability",
|
||||||
include: [
|
include: ["src/__tests__/reliability-interactions/**/*.test.ts"],
|
||||||
"src/__tests__/reliability-interactions/**/*.test.ts",
|
|
||||||
"src/__tests__/merger-ai-cleanup.test.ts",
|
|
||||||
],
|
|
||||||
// Mirror the engine-default exclusion so reliability slow tests
|
// Mirror the engine-default exclusion so reliability slow tests
|
||||||
// also tier into engine-slow.
|
// also tier into engine-slow.
|
||||||
exclude: ["src/**/*.slow.test.ts"],
|
exclude: ["src/**/*.slow.test.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user