feat(FN-1423): add promptOverrides to project settings
- Add promptOverrides configuration to project settings with null-as-delete semantics - Add core prompt-key catalog with resolver primitives in packages/core/src/prompt-overrides.ts - Export prompt key catalog, resolution functions, and settings merge helpers from @fusion/core - Add parity tests for promptOverrides settings handling - Add settings export/import coverage for promptOverrides - Add settings export tests verifying promptOverrides round-trip - Update settings reference documentation with promptOverrides section - Add store methods for loading/exporting project settings with prompt overrides
This commit is contained in:
@@ -94,6 +94,7 @@ const PROJECT_KEYS: (keyof ProjectSettings)[] = [
|
||||
"missionMaxTaskRetries",
|
||||
"missionHealthCheckIntervalMs",
|
||||
"agentPrompts",
|
||||
"promptOverrides",
|
||||
"reflectionEnabled",
|
||||
"reflectionIntervalMs",
|
||||
"reflectionAfterTask",
|
||||
|
||||
@@ -7,6 +7,28 @@ export {
|
||||
getAvailableTemplates,
|
||||
getTemplatesForRole,
|
||||
} from "./agent-prompts.js";
|
||||
|
||||
// ── Prompt Overrides ─────────────────────────────────────────────────
|
||||
export {
|
||||
PROMPT_KEY_CATALOG,
|
||||
resolvePrompt,
|
||||
resolveRolePrompts,
|
||||
hasRoleOverrides,
|
||||
getOverriddenKeys,
|
||||
clearOverrides,
|
||||
getPromptKeyMetadata,
|
||||
getPromptKeysForRole,
|
||||
isValidPromptKey,
|
||||
isValidPromptOverrideMap,
|
||||
assertValidPromptOverrideMap,
|
||||
} from "./prompt-overrides.js";
|
||||
export type {
|
||||
PromptKey,
|
||||
PromptKeyMetadata,
|
||||
PromptKeyCatalog,
|
||||
PromptOverrideEntry,
|
||||
PromptOverrideMap,
|
||||
} from "./prompt-overrides.js";
|
||||
export {
|
||||
ROLE_DEFAULT_PERMISSIONS,
|
||||
normalizePermissions,
|
||||
|
||||
377
packages/core/src/prompt-overrides.test.ts
Normal file
377
packages/core/src/prompt-overrides.test.ts
Normal file
@@ -0,0 +1,377 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
PROMPT_KEY_CATALOG,
|
||||
PromptKey,
|
||||
PromptOverrideMap,
|
||||
resolvePrompt,
|
||||
resolveRolePrompts,
|
||||
hasRoleOverrides,
|
||||
getOverriddenKeys,
|
||||
clearOverrides,
|
||||
getPromptKeyMetadata,
|
||||
getPromptKeysForRole,
|
||||
isValidPromptKey,
|
||||
isValidPromptOverrideMap,
|
||||
assertValidPromptOverrideMap,
|
||||
} from "./prompt-overrides.js";
|
||||
|
||||
describe("prompt-overrides", () => {
|
||||
describe("PROMPT_KEY_CATALOG", () => {
|
||||
it("should contain all expected prompt keys", () => {
|
||||
const expectedKeys: PromptKey[] = [
|
||||
"executor-welcome",
|
||||
"executor-guardrails",
|
||||
"executor-spawning",
|
||||
"executor-completion",
|
||||
"triage-welcome",
|
||||
"triage-context",
|
||||
"reviewer-verdict",
|
||||
"merger-conflicts",
|
||||
];
|
||||
|
||||
for (const key of expectedKeys) {
|
||||
expect(PROMPT_KEY_CATALOG).toHaveProperty(key);
|
||||
expect(PROMPT_KEY_CATALOG[key].key).toBe(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("should have valid metadata for each key", () => {
|
||||
for (const [key, meta] of Object.entries(PROMPT_KEY_CATALOG)) {
|
||||
expect(meta.key).toBe(key);
|
||||
expect(typeof meta.name).toBe("string");
|
||||
expect(meta.name.length).toBeGreaterThan(0);
|
||||
expect(Array.isArray(meta.roles)).toBe(true);
|
||||
expect(meta.roles.length).toBeGreaterThan(0);
|
||||
expect(typeof meta.description).toBe("string");
|
||||
expect(typeof meta.defaultContent).toBe("string");
|
||||
expect(meta.defaultContent.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should have appropriate roles for each key", () => {
|
||||
// Executor keys should only be for executor role
|
||||
expect(PROMPT_KEY_CATALOG["executor-welcome"].roles).toContain("executor");
|
||||
expect(PROMPT_KEY_CATALOG["executor-guardrails"].roles).toContain("executor");
|
||||
expect(PROMPT_KEY_CATALOG["executor-spawning"].roles).toContain("executor");
|
||||
expect(PROMPT_KEY_CATALOG["executor-completion"].roles).toContain("executor");
|
||||
|
||||
// Triage keys should only be for triage role
|
||||
expect(PROMPT_KEY_CATALOG["triage-welcome"].roles).toContain("triage");
|
||||
expect(PROMPT_KEY_CATALOG["triage-context"].roles).toContain("triage");
|
||||
|
||||
// Reviewer and merger keys
|
||||
expect(PROMPT_KEY_CATALOG["reviewer-verdict"].roles).toContain("reviewer");
|
||||
expect(PROMPT_KEY_CATALOG["merger-conflicts"].roles).toContain("merger");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPromptKeyMetadata", () => {
|
||||
it("should return metadata for valid keys", () => {
|
||||
const meta = getPromptKeyMetadata("executor-welcome");
|
||||
expect(meta).toBeDefined();
|
||||
expect(meta?.key).toBe("executor-welcome");
|
||||
expect(meta?.name).toBe("Executor Welcome");
|
||||
});
|
||||
|
||||
it("should return undefined for invalid keys", () => {
|
||||
expect(getPromptKeyMetadata("invalid-key" as PromptKey)).toBeUndefined();
|
||||
expect(getPromptKeyMetadata("" as PromptKey)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPromptKeysForRole", () => {
|
||||
it("should return all keys for executor role", () => {
|
||||
const keys = getPromptKeysForRole("executor");
|
||||
expect(keys).toHaveLength(4);
|
||||
expect(keys.map((k) => k.key)).toContain("executor-welcome");
|
||||
expect(keys.map((k) => k.key)).toContain("executor-guardrails");
|
||||
expect(keys.map((k) => k.key)).toContain("executor-spawning");
|
||||
expect(keys.map((k) => k.key)).toContain("executor-completion");
|
||||
});
|
||||
|
||||
it("should return all keys for triage role", () => {
|
||||
const keys = getPromptKeysForRole("triage");
|
||||
expect(keys).toHaveLength(2);
|
||||
expect(keys.map((k) => k.key)).toContain("triage-welcome");
|
||||
expect(keys.map((k) => k.key)).toContain("triage-context");
|
||||
});
|
||||
|
||||
it("should return single key for reviewer role", () => {
|
||||
const keys = getPromptKeysForRole("reviewer");
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0].key).toBe("reviewer-verdict");
|
||||
});
|
||||
|
||||
it("should return single key for merger role", () => {
|
||||
const keys = getPromptKeysForRole("merger");
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0].key).toBe("merger-conflicts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolvePrompt", () => {
|
||||
it("should return override when present and non-empty", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom executor welcome",
|
||||
};
|
||||
|
||||
const result = resolvePrompt("executor-welcome", overrides);
|
||||
expect(result).toBe("Custom executor welcome");
|
||||
});
|
||||
|
||||
it("should return default when no override present", () => {
|
||||
const overrides: PromptOverrideMap = {};
|
||||
const defaultContent = PROMPT_KEY_CATALOG["executor-welcome"].defaultContent;
|
||||
|
||||
const result = resolvePrompt("executor-welcome", overrides);
|
||||
expect(result).toBe(defaultContent);
|
||||
});
|
||||
|
||||
it("should return default when override is empty string", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "",
|
||||
};
|
||||
const defaultContent = PROMPT_KEY_CATALOG["executor-welcome"].defaultContent;
|
||||
|
||||
const result = resolvePrompt("executor-welcome", overrides);
|
||||
expect(result).toBe(defaultContent);
|
||||
});
|
||||
|
||||
it("should return default when override is undefined", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": undefined,
|
||||
};
|
||||
const defaultContent = PROMPT_KEY_CATALOG["executor-welcome"].defaultContent;
|
||||
|
||||
const result = resolvePrompt("executor-welcome", overrides);
|
||||
expect(result).toBe(defaultContent);
|
||||
});
|
||||
|
||||
it("should return default when overrides is undefined", () => {
|
||||
const defaultContent = PROMPT_KEY_CATALOG["executor-welcome"].defaultContent;
|
||||
|
||||
const result = resolvePrompt("executor-welcome", undefined);
|
||||
expect(result).toBe(defaultContent);
|
||||
});
|
||||
|
||||
it("should return default for unrecognized key", () => {
|
||||
const result = resolvePrompt("invalid-key" as PromptKey, {});
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string for unrecognized key with no defaults", () => {
|
||||
const result = resolvePrompt("invalid-key" as PromptKey, undefined);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRolePrompts", () => {
|
||||
it("should resolve all prompts for executor role", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom welcome",
|
||||
};
|
||||
|
||||
const result = resolveRolePrompts("executor", overrides);
|
||||
|
||||
// Custom override
|
||||
expect(result["executor-welcome"]).toBe("Custom welcome");
|
||||
|
||||
// Defaults for others
|
||||
expect(result["executor-guardrails"]).toBe(PROMPT_KEY_CATALOG["executor-guardrails"].defaultContent);
|
||||
expect(result["executor-spawning"]).toBe(PROMPT_KEY_CATALOG["executor-spawning"].defaultContent);
|
||||
expect(result["executor-completion"]).toBe(PROMPT_KEY_CATALOG["executor-completion"].defaultContent);
|
||||
});
|
||||
|
||||
it("should resolve all prompts for triage role", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"triage-welcome": "Custom triage welcome",
|
||||
};
|
||||
|
||||
const result = resolveRolePrompts("triage", overrides);
|
||||
|
||||
expect(result["triage-welcome"]).toBe("Custom triage welcome");
|
||||
expect(result["triage-context"]).toBe(PROMPT_KEY_CATALOG["triage-context"].defaultContent);
|
||||
});
|
||||
|
||||
it("should return all defaults when no overrides", () => {
|
||||
const result = resolveRolePrompts("executor", undefined);
|
||||
|
||||
for (const [key, content] of Object.entries(result)) {
|
||||
expect(content).toBe(PROMPT_KEY_CATALOG[key as PromptKey].defaultContent);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return empty object for roles with no prompts", () => {
|
||||
// Scheduler and custom roles have no defined prompts
|
||||
const result = resolveRolePrompts("scheduler" as any, {});
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasRoleOverrides", () => {
|
||||
it("should return true when at least one override is set", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom",
|
||||
};
|
||||
|
||||
expect(hasRoleOverrides("executor", overrides)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when no overrides set", () => {
|
||||
expect(hasRoleOverrides("executor", {})).toBe(false);
|
||||
expect(hasRoleOverrides("executor", undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when overrides are empty strings", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "",
|
||||
};
|
||||
|
||||
expect(hasRoleOverrides("executor", overrides)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when only other role has overrides", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"triage-welcome": "Custom",
|
||||
};
|
||||
|
||||
expect(hasRoleOverrides("executor", overrides)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOverriddenKeys", () => {
|
||||
it("should return keys with non-empty overrides", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom",
|
||||
"triage-welcome": "Custom triage",
|
||||
"merger-conflicts": "",
|
||||
};
|
||||
|
||||
const result = getOverriddenKeys(overrides);
|
||||
expect(result).toContain("executor-welcome");
|
||||
expect(result).toContain("triage-welcome");
|
||||
expect(result).not.toContain("merger-conflicts");
|
||||
});
|
||||
|
||||
it("should return empty array for undefined", () => {
|
||||
expect(getOverriddenKeys(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return empty array for empty object", () => {
|
||||
expect(getOverriddenKeys({})).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearOverrides", () => {
|
||||
it("should remove specified keys", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom",
|
||||
"executor-guardrails": "Custom guardrails",
|
||||
};
|
||||
|
||||
const result = clearOverrides(overrides, ["executor-welcome"]);
|
||||
|
||||
expect(result).not.toHaveProperty("executor-welcome");
|
||||
expect(result?.["executor-guardrails"]).toBe("Custom guardrails");
|
||||
});
|
||||
|
||||
it("should return undefined when all keys are cleared", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom",
|
||||
};
|
||||
|
||||
const result = clearOverrides(overrides, ["executor-welcome"]);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle undefined input", () => {
|
||||
const result = clearOverrides(undefined, ["executor-welcome"]);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should preserve other keys when clearing", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Custom",
|
||||
"triage-welcome": "Custom triage",
|
||||
};
|
||||
|
||||
const result = clearOverrides(overrides, ["executor-welcome"]);
|
||||
|
||||
expect(result?.["triage-welcome"]).toBe("Custom triage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidPromptKey", () => {
|
||||
it("should return true for valid keys", () => {
|
||||
expect(isValidPromptKey("executor-welcome")).toBe(true);
|
||||
expect(isValidPromptKey("merger-conflicts")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for invalid keys", () => {
|
||||
expect(isValidPromptKey("invalid")).toBe(false);
|
||||
expect(isValidPromptKey("")).toBe(false);
|
||||
expect(isValidPromptKey(123 as any)).toBe(false);
|
||||
expect(isValidPromptKey(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidPromptOverrideMap", () => {
|
||||
it("should return true for valid maps", () => {
|
||||
expect(isValidPromptOverrideMap({})).toBe(true);
|
||||
expect(isValidPromptOverrideMap({ "executor-welcome": "Custom" })).toBe(true);
|
||||
expect(isValidPromptOverrideMap({ "executor-welcome": undefined })).toBe(true);
|
||||
expect(isValidPromptOverrideMap({ "executor-welcome": "", "triage-welcome": "Custom" })).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for invalid values", () => {
|
||||
expect(isValidPromptOverrideMap(null)).toBe(false);
|
||||
expect(isValidPromptOverrideMap("string")).toBe(false);
|
||||
expect(isValidPromptOverrideMap(123)).toBe(false);
|
||||
expect(isValidPromptOverrideMap({ "invalid-key": "value" })).toBe(false);
|
||||
expect(isValidPromptOverrideMap({ "executor-welcome": 123 as any })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertValidPromptOverrideMap", () => {
|
||||
it("should not throw for valid maps", () => {
|
||||
expect(() => assertValidPromptOverrideMap({})).not.toThrow();
|
||||
expect(() => assertValidPromptOverrideMap({ "executor-welcome": "Custom" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw for invalid maps", () => {
|
||||
expect(() => assertValidPromptOverrideMap(null)).toThrow();
|
||||
expect(() => assertValidPromptOverrideMap({ "invalid": "value" })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fallback behavior", () => {
|
||||
it("should always return a string (never undefined or throw)", () => {
|
||||
// All valid keys should return strings
|
||||
for (const key of Object.keys(PROMPT_KEY_CATALOG) as PromptKey[]) {
|
||||
expect(typeof resolvePrompt(key, undefined)).toBe("string");
|
||||
expect(typeof resolvePrompt(key, {})).toBe("string");
|
||||
expect(typeof resolvePrompt(key, { [key]: undefined })).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle partial overrides gracefully", () => {
|
||||
const overrides: PromptOverrideMap = {
|
||||
"executor-welcome": "Only this is overridden",
|
||||
// Other keys intentionally not set
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
const result = resolveRolePrompts("executor", overrides);
|
||||
|
||||
// Overridden key should have custom value
|
||||
expect(result["executor-welcome"]).toBe("Only this is overridden");
|
||||
|
||||
// Other keys should have defaults
|
||||
expect(result["executor-guardrails"]).toBe(PROMPT_KEY_CATALOG["executor-guardrails"].defaultContent);
|
||||
expect(result["executor-spawning"]).toBe(PROMPT_KEY_CATALOG["executor-spawning"].defaultContent);
|
||||
expect(result["executor-completion"]).toBe(PROMPT_KEY_CATALOG["executor-completion"].defaultContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
392
packages/core/src/prompt-overrides.ts
Normal file
392
packages/core/src/prompt-overrides.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Prompt customization foundation for runtime prompt overrides.
|
||||
*
|
||||
* This module provides:
|
||||
* - A typed catalog of stable prompt-key identifiers
|
||||
* - Metadata structures with default prompt content
|
||||
* - Resolver functions that return override text when present, otherwise fall back to defaults
|
||||
* - Deterministic fallback behavior for missing/invalid entries
|
||||
*
|
||||
* Runtime packages can use these APIs to resolve prompt text by stable keys,
|
||||
* enabling project-level customization of AI agent prompts without modifying
|
||||
* the core prompt templates in agent-prompts.ts.
|
||||
*
|
||||
* @module prompt-overrides
|
||||
*/
|
||||
|
||||
import type { AgentCapability } from "./types.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompt Key Catalog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Stable identifier for a customizable prompt segment.
|
||||
*
|
||||
* Each key represents a discrete portion of an agent's prompt that can be
|
||||
* independently overridden at the project level via settings.
|
||||
*/
|
||||
export type PromptKey =
|
||||
| "executor-welcome"
|
||||
| "executor-guardrails"
|
||||
| "executor-spawning"
|
||||
| "executor-completion"
|
||||
| "triage-welcome"
|
||||
| "triage-context"
|
||||
| "reviewer-verdict"
|
||||
| "merger-conflicts";
|
||||
|
||||
/**
|
||||
* Metadata describing a prompt key including its purpose and default content.
|
||||
*/
|
||||
export interface PromptKeyMetadata {
|
||||
/** Stable key identifier */
|
||||
key: PromptKey;
|
||||
/** Human-readable name for UI display */
|
||||
name: string;
|
||||
/** Which agent role(s) this prompt applies to */
|
||||
roles: AgentCapability[];
|
||||
/** Short description of what this prompt segment controls */
|
||||
description: string;
|
||||
/**
|
||||
* Default prompt content.
|
||||
* Runtime packages should use resolvePrompt() to get the effective content
|
||||
* (override if present in settings, otherwise this default).
|
||||
*/
|
||||
defaultContent: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of prompt key to its metadata.
|
||||
*/
|
||||
export type PromptKeyCatalog = Record<PromptKey, PromptKeyMetadata>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Built-in Prompt Key Metadata
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Built-in metadata catalog for all supported prompt keys.
|
||||
* Each entry describes a customizable prompt segment with its default content.
|
||||
*/
|
||||
export const PROMPT_KEY_CATALOG: PromptKeyCatalog = {
|
||||
"executor-welcome": {
|
||||
key: "executor-welcome",
|
||||
name: "Executor Welcome",
|
||||
roles: ["executor"],
|
||||
description: "Introductory section for the executor agent",
|
||||
defaultContent: `You are a task execution agent for "fn", an AI-orchestrated task board.
|
||||
|
||||
You are working in a git worktree isolated from the main branch. Your job is to implement the task described in the PROMPT.md specification you're given.`,
|
||||
},
|
||||
"executor-guardrails": {
|
||||
key: "executor-guardrails",
|
||||
name: "Executor Guardrails",
|
||||
roles: ["executor"],
|
||||
description: "Behavioral guardrails and constraints for the executor",
|
||||
defaultContent: `## Guardrails
|
||||
- Treat the File Scope in PROMPT.md as the expected starting scope, not a hard boundary when quality gates fail
|
||||
- Read "Context to Read First" files before starting
|
||||
- Follow the "Do NOT" section strictly
|
||||
- If tests, build, or typecheck fail and the fix requires touching code outside the declared File Scope, fix those failures directly and keep the repo green`,
|
||||
},
|
||||
"executor-spawning": {
|
||||
key: "executor-spawning",
|
||||
name: "Executor Spawning",
|
||||
roles: ["executor"],
|
||||
description: "Instructions for spawning child agents",
|
||||
defaultContent: `## Spawning Child Agents
|
||||
|
||||
You can spawn child agents to handle parallel work or specialized sub-tasks:
|
||||
|
||||
**When to use \`spawn_agent\`:**
|
||||
- Parallel work that can be divided into independent chunks
|
||||
- Specialized tasks requiring different expertise or tools
|
||||
- Delegation of sub-tasks to specialized agents
|
||||
|
||||
**How to spawn:**
|
||||
\`\`\`javascript
|
||||
spawn_agent({
|
||||
name: "researcher",
|
||||
role: "engineer",
|
||||
task: "Research best practices for authentication in React applications"
|
||||
})
|
||||
\`\`\``,
|
||||
},
|
||||
"executor-completion": {
|
||||
key: "executor-completion",
|
||||
name: "Executor Completion",
|
||||
roles: ["executor"],
|
||||
description: "Completion criteria and signaling for executor",
|
||||
defaultContent: `## Completion
|
||||
After all steps are done, tests pass, typecheck passes, and docs are updated:
|
||||
\`\`\`bash
|
||||
Call \`task_done()\` to signal completion.
|
||||
\`\`\``,
|
||||
},
|
||||
"triage-welcome": {
|
||||
key: "triage-welcome",
|
||||
name: "Triage Welcome",
|
||||
roles: ["triage"],
|
||||
description: "Introductory section for the triage/specification agent",
|
||||
defaultContent: `You are a task specification agent for "fn", an AI-orchestrated task board.
|
||||
|
||||
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.`,
|
||||
},
|
||||
"triage-context": {
|
||||
key: "triage-context",
|
||||
name: "Triage Context",
|
||||
roles: ["triage"],
|
||||
description: "Context-gathering instructions for triage",
|
||||
defaultContent: `## 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`,
|
||||
},
|
||||
"reviewer-verdict": {
|
||||
key: "reviewer-verdict",
|
||||
name: "Reviewer Verdict",
|
||||
roles: ["reviewer"],
|
||||
description: "Verdict criteria and format for code/review agent",
|
||||
defaultContent: `## Verdict Criteria
|
||||
|
||||
- **APPROVE** — Step will achieve its stated outcomes. Minor suggestions go in
|
||||
the Suggestions section but do NOT block progress. If your only findings are
|
||||
minor or suggestion-level, verdict is APPROVE.
|
||||
- **REVISE** — Step will fail, produce incorrect results, or miss a stated
|
||||
requirement without fixes. Use ONLY for issues that would cause the worker to
|
||||
redo work later.
|
||||
- **RETHINK** — Approach is fundamentally wrong. Explain why and suggest an
|
||||
alternative.`,
|
||||
},
|
||||
"merger-conflicts": {
|
||||
key: "merger-conflicts",
|
||||
name: "Merger Conflicts",
|
||||
roles: ["merger"],
|
||||
description: "Merge conflict resolution instructions for merger",
|
||||
defaultContent: `## Conflict resolution
|
||||
If there are merge conflicts:
|
||||
1. Run \`git diff --name-only --diff-filter=U\` to list conflicted files
|
||||
2. Read each conflicted file — look for the <<<<<<< / ======= / >>>>>>> markers
|
||||
3. Understand the intent of BOTH sides, then edit the file to produce the correct merged result
|
||||
4. Remove ALL conflict markers — the result must be clean, compilable code
|
||||
5. Run \`git add <file>\` for each resolved file
|
||||
6. Do NOT change anything beyond what's needed to resolve the conflict`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the metadata for a specific prompt key.
|
||||
* Returns undefined if the key is not recognized.
|
||||
*/
|
||||
export function getPromptKeyMetadata(key: PromptKey): PromptKeyMetadata | undefined {
|
||||
return PROMPT_KEY_CATALOG[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prompt keys for a specific agent role.
|
||||
*/
|
||||
export function getPromptKeysForRole(role: AgentCapability): PromptKeyMetadata[] {
|
||||
return Object.values(PROMPT_KEY_CATALOG).filter((meta) => meta.roles.includes(role));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Override Entry Type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A single prompt override entry stored in project settings.
|
||||
* The value is the custom prompt content; undefined means "use default".
|
||||
*/
|
||||
export type PromptOverrideEntry = string | undefined;
|
||||
|
||||
/**
|
||||
* Collection of prompt overrides keyed by PromptKey.
|
||||
* Stored in project settings as `promptOverrides: Record<PromptKey, string>`.
|
||||
*/
|
||||
export type PromptOverrideMap = Partial<Record<PromptKey, string>>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolver Functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Resolve the effective prompt content for a given key.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. If `overrides[key]` is a non-empty string, return the override
|
||||
* 2. Otherwise, return the default content from PROMPT_KEY_CATALOG
|
||||
*
|
||||
* @param key - The prompt key to resolve
|
||||
* @param overrides - The project-level overrides map (from settings)
|
||||
* @returns The effective prompt content (override or default)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const overrides: PromptOverrideMap = {
|
||||
* "executor-welcome": "Custom welcome message..."
|
||||
* };
|
||||
* const content = resolvePrompt("executor-welcome", overrides);
|
||||
* // Returns custom welcome if set, otherwise PROMPT_KEY_CATALOG["executor-welcome"].defaultContent
|
||||
* ```
|
||||
*/
|
||||
export function resolvePrompt(
|
||||
key: PromptKey,
|
||||
overrides?: PromptOverrideMap,
|
||||
): string {
|
||||
// Check for a valid override
|
||||
if (overrides && key in overrides) {
|
||||
const override = overrides[key];
|
||||
// Non-empty string is a valid override
|
||||
if (override !== undefined && override !== "") {
|
||||
return override;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default
|
||||
const metadata = PROMPT_KEY_CATALOG[key];
|
||||
if (metadata) {
|
||||
return metadata.defaultContent;
|
||||
}
|
||||
|
||||
// Key not found — return empty string (graceful degradation)
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all prompt overrides for a given role.
|
||||
*
|
||||
* Returns a map of prompt key → effective content (override or default)
|
||||
* for all keys applicable to the specified role.
|
||||
*
|
||||
* @param role - The agent role to get prompts for
|
||||
* @param overrides - The project-level overrides map (from settings)
|
||||
* @returns Record mapping prompt keys to their effective content
|
||||
*/
|
||||
export function resolveRolePrompts(
|
||||
role: AgentCapability,
|
||||
overrides?: PromptOverrideMap,
|
||||
): Record<PromptKey, string> {
|
||||
const result: Partial<Record<PromptKey, string>> = {};
|
||||
|
||||
for (const meta of getPromptKeysForRole(role)) {
|
||||
result[meta.key] = resolvePrompt(meta.key, overrides);
|
||||
}
|
||||
|
||||
return result as Record<PromptKey, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any overrides are set for a given role.
|
||||
*
|
||||
* @param role - The agent role to check
|
||||
* @param overrides - The project-level overrides map (from settings)
|
||||
* @returns True if at least one override is set for the role
|
||||
*/
|
||||
export function hasRoleOverrides(
|
||||
role: AgentCapability,
|
||||
overrides?: PromptOverrideMap,
|
||||
): boolean {
|
||||
if (!overrides) return false;
|
||||
|
||||
const roleKeys = getPromptKeysForRole(role);
|
||||
return roleKeys.some((meta) => {
|
||||
const override = overrides[meta.key];
|
||||
return override !== undefined && override !== "";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all overridden keys (keys with non-empty override values).
|
||||
*
|
||||
* @param overrides - The project-level overrides map
|
||||
* @returns Array of keys that have overrides set
|
||||
*/
|
||||
export function getOverriddenKeys(overrides?: PromptOverrideMap): PromptKey[] {
|
||||
if (!overrides) return [];
|
||||
|
||||
return (Object.keys(overrides) as PromptKey[]).filter(
|
||||
(key) => overrides[key] !== undefined && overrides[key] !== "",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear specific override keys by setting them to undefined.
|
||||
* Used by TaskStore.updateSettings() to implement null-as-delete semantics.
|
||||
*
|
||||
* @param overrides - Current overrides map
|
||||
* @param keysToClear - Keys to clear (set to undefined)
|
||||
* @returns New overrides map with specified keys cleared
|
||||
*/
|
||||
export function clearOverrides(
|
||||
overrides: PromptOverrideMap | undefined,
|
||||
keysToClear: PromptKey[],
|
||||
): PromptOverrideMap | undefined {
|
||||
if (!overrides && keysToClear.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: PromptOverrideMap = { ...overrides };
|
||||
|
||||
for (const key of keysToClear) {
|
||||
delete result[key];
|
||||
}
|
||||
|
||||
// Return undefined if map becomes empty (runtime uses defaults)
|
||||
if (Object.keys(result).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a value is a valid PromptKey.
|
||||
*
|
||||
* @param value - Value to check
|
||||
* @returns True if the value is a valid PromptKey
|
||||
*/
|
||||
export function isValidPromptKey(value: unknown): value is PromptKey {
|
||||
if (typeof value !== "string") return false;
|
||||
return value in PROMPT_KEY_CATALOG;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an object is a valid PromptOverrideMap.
|
||||
*
|
||||
* @param value - Value to check
|
||||
* @returns True if the value is a valid PromptOverrideMap
|
||||
*/
|
||||
export function isValidPromptOverrideMap(value: unknown): value is PromptOverrideMap {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>;
|
||||
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (!isValidPromptKey(key)) {
|
||||
return false;
|
||||
}
|
||||
// Values must be strings or undefined
|
||||
if (val !== undefined && typeof val !== "string") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to ensure a value is a valid PromptOverrideMap.
|
||||
* Throws if the value is not a valid PromptOverrideMap.
|
||||
*
|
||||
* @param value - Value to validate
|
||||
* @throws Error if the value is not a valid PromptOverrideMap
|
||||
*/
|
||||
export function assertValidPromptOverrideMap(value: unknown): asserts value is PromptOverrideMap {
|
||||
if (!isValidPromptOverrideMap(value)) {
|
||||
throw new Error(
|
||||
`Invalid prompt override map: expected Record<PromptKey, string | undefined>, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -388,6 +388,105 @@ describe("settings-export", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("promptOverrides export/import", () => {
|
||||
it("should export promptOverrides when set", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Custom welcome" },
|
||||
});
|
||||
|
||||
const result = await exportSettings(store, { scope: "project" });
|
||||
|
||||
expect(result.project?.promptOverrides).toEqual({ "executor-welcome": "Custom welcome" });
|
||||
});
|
||||
|
||||
it("should not export promptOverrides when not set", async () => {
|
||||
const result = await exportSettings(store, { scope: "project" });
|
||||
|
||||
expect(result.project?.promptOverrides).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should import promptOverrides in merge mode", async () => {
|
||||
const importData: SettingsExportData = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
project: {
|
||||
promptOverrides: { "executor-welcome": "Imported welcome" },
|
||||
},
|
||||
};
|
||||
|
||||
const result = await importSettings(store, importData, { scope: "project", merge: true });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.projectCount).toBe(1);
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({ "executor-welcome": "Imported welcome" });
|
||||
});
|
||||
|
||||
it("should merge promptOverrides with existing overrides", async () => {
|
||||
// Set initial overrides
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Original" },
|
||||
});
|
||||
|
||||
const importData: SettingsExportData = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
project: {
|
||||
promptOverrides: { "triage-welcome": "Imported triage" },
|
||||
},
|
||||
};
|
||||
|
||||
await importSettings(store, importData, { scope: "project", merge: true });
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({
|
||||
"executor-welcome": "Original",
|
||||
"triage-welcome": "Imported triage",
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear promptOverrides when importing null", async () => {
|
||||
// Set initial overrides
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Original", "triage-welcome": "Triage" },
|
||||
});
|
||||
|
||||
const importData: SettingsExportData = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
project: {
|
||||
promptOverrides: null as any,
|
||||
},
|
||||
};
|
||||
|
||||
await importSettings(store, importData, { scope: "project", merge: true });
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should clear specific promptOverride key when importing null value", async () => {
|
||||
// Set initial overrides
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Original", "triage-welcome": "Triage" },
|
||||
});
|
||||
|
||||
const importData: SettingsExportData = {
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
project: {
|
||||
promptOverrides: { "executor-welcome": null as unknown as string },
|
||||
},
|
||||
};
|
||||
|
||||
await importSettings(store, importData, { scope: "project", merge: true });
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({ "triage-welcome": "Triage" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("readExportFile", () => {
|
||||
it("should read and parse valid export file", async () => {
|
||||
const filePath = join(env.tempDir, "test-export.json");
|
||||
|
||||
@@ -872,6 +872,164 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Prompt Overrides Tests ─────────────────────────────────────────
|
||||
|
||||
describe("promptOverrides settings", () => {
|
||||
it("can set a single prompt override", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Custom executor welcome message" },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({ "executor-welcome": "Custom executor welcome message" });
|
||||
});
|
||||
|
||||
it("can set multiple prompt overrides", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: {
|
||||
"executor-welcome": "Custom welcome",
|
||||
"triage-welcome": "Custom triage welcome",
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({
|
||||
"executor-welcome": "Custom welcome",
|
||||
"triage-welcome": "Custom triage welcome",
|
||||
});
|
||||
});
|
||||
|
||||
it("promptOverrides is undefined by default", async () => {
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can merge new overrides with existing overrides", async () => {
|
||||
// Set initial overrides
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Initial welcome" },
|
||||
});
|
||||
|
||||
// Add more overrides
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "triage-welcome": "Custom triage" },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({
|
||||
"executor-welcome": "Initial welcome",
|
||||
"triage-welcome": "Custom triage",
|
||||
});
|
||||
});
|
||||
|
||||
it("can update an existing override", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Original" },
|
||||
});
|
||||
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Updated" },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({ "executor-welcome": "Updated" });
|
||||
});
|
||||
|
||||
it("can clear a specific override with null value", async () => {
|
||||
// Set initial overrides
|
||||
await store.updateSettings({
|
||||
promptOverrides: {
|
||||
"executor-welcome": "Welcome",
|
||||
"triage-welcome": "Triage",
|
||||
},
|
||||
});
|
||||
|
||||
// Clear only executor-welcome
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": null as unknown as string },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toEqual({ "triage-welcome": "Triage" });
|
||||
});
|
||||
|
||||
it("clears entire promptOverrides when all keys are cleared", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Welcome" },
|
||||
});
|
||||
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": null as unknown as string },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can clear entire promptOverrides with null", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Welcome", "triage-welcome": "Triage" },
|
||||
});
|
||||
|
||||
await store.updateSettings({
|
||||
promptOverrides: null as unknown as Record<string, string>,
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toBeUndefined();
|
||||
});
|
||||
|
||||
it("persists empty string overrides as cleared (not stored)", async () => {
|
||||
// Setting an empty string should be treated as "clear" and not persist
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "" },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.promptOverrides).toBeUndefined();
|
||||
});
|
||||
|
||||
it("handles promptOverrides in getSettingsByScope", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Scoped welcome" },
|
||||
});
|
||||
|
||||
const { project } = await store.getSettingsByScope();
|
||||
expect(project.promptOverrides).toEqual({ "executor-welcome": "Scoped welcome" });
|
||||
});
|
||||
|
||||
it("preserves other settings when updating promptOverrides", async () => {
|
||||
await store.updateSettings({
|
||||
maxConcurrent: 5,
|
||||
autoMerge: false,
|
||||
});
|
||||
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Welcome" },
|
||||
});
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.maxConcurrent).toBe(5);
|
||||
expect(settings.autoMerge).toBe(false);
|
||||
expect(settings.promptOverrides).toEqual({ "executor-welcome": "Welcome" });
|
||||
});
|
||||
|
||||
it("preserves promptOverrides when updating other settings", async () => {
|
||||
await store.updateSettings({
|
||||
promptOverrides: { "executor-welcome": "Welcome", "triage-welcome": "Triage" },
|
||||
});
|
||||
|
||||
await store.updateSettings({ maxConcurrent: 7 });
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.maxConcurrent).toBe(7);
|
||||
expect(settings.promptOverrides).toEqual({
|
||||
"executor-welcome": "Welcome",
|
||||
"triage-welcome": "Triage",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Concurrent stress test ───────────────────────────────────────
|
||||
|
||||
describe("concurrent stress", () => {
|
||||
|
||||
@@ -650,6 +650,46 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// Handle null values as "delete this key from settings"
|
||||
// This allows the frontend to explicitly clear a setting by sending null
|
||||
// (since JSON.stringify drops undefined keys, we use null as a sentinel)
|
||||
|
||||
// Handle special null-as-delete semantics for promptOverrides
|
||||
const incomingPromptOverrides = (projectPatch as Record<string, unknown>)["promptOverrides"];
|
||||
if (incomingPromptOverrides === null) {
|
||||
// promptOverrides: null → clear the entire promptOverrides object
|
||||
delete (config.settings as unknown as Record<string, unknown>)["promptOverrides"];
|
||||
delete (projectPatch as Record<string, unknown>)["promptOverrides"];
|
||||
} else if (
|
||||
incomingPromptOverrides !== undefined &&
|
||||
typeof incomingPromptOverrides === "object" &&
|
||||
incomingPromptOverrides !== null
|
||||
) {
|
||||
// promptOverrides: { key: value } → merge with existing, treating null values as delete
|
||||
const incomingMap = incomingPromptOverrides as Record<string, unknown>;
|
||||
const existingMap = ((config.settings as unknown as Record<string, unknown>)["promptOverrides"] as Record<string, string>) ?? {};
|
||||
const mergedMap: Record<string, string> = { ...existingMap };
|
||||
|
||||
for (const [key, value] of Object.entries(incomingMap)) {
|
||||
if (value === null) {
|
||||
// null → delete this specific key
|
||||
delete mergedMap[key];
|
||||
} else if (typeof value === "string" && value !== "") {
|
||||
// non-empty string → set this key
|
||||
// Empty strings are treated as "clear" and not stored
|
||||
mergedMap[key] = value;
|
||||
}
|
||||
// Empty strings are silently ignored (treated as "clear")
|
||||
}
|
||||
|
||||
// If merged map is empty, remove the entire promptOverrides
|
||||
if (Object.keys(mergedMap).length === 0) {
|
||||
delete (config.settings as unknown as Record<string, unknown>)["promptOverrides"];
|
||||
delete (projectPatch as Record<string, unknown>)["promptOverrides"];
|
||||
} else {
|
||||
(config.settings as unknown as Record<string, unknown>)["promptOverrides"] = mergedMap;
|
||||
(projectPatch as Record<string, unknown>)["promptOverrides"] = mergedMap;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle null values for other top-level keys (non-promptOverrides)
|
||||
for (const key of Object.keys(projectPatch)) {
|
||||
if ((projectPatch as Record<string, unknown>)[key] === null) {
|
||||
delete (config.settings as unknown as Record<string, unknown>)[key];
|
||||
|
||||
@@ -1105,6 +1105,20 @@ export interface ProjectSettings {
|
||||
* When set, allows per-project customization of system prompts
|
||||
* for different agent roles (executor, triage, reviewer, merger). */
|
||||
agentPrompts?: AgentPromptsConfig;
|
||||
/** Prompt segment overrides for fine-grained customization of agent prompts.
|
||||
* Each key maps to a customizable prompt segment (e.g., "executor-welcome",
|
||||
* "triage-context"). When a key is present with a non-empty value, that
|
||||
* override replaces the default prompt segment. Missing or empty values
|
||||
* fall back to the default prompt content.
|
||||
*
|
||||
* This is separate from `agentPrompts` which controls full role templates.
|
||||
* `promptOverrides` allows surgical customization of specific prompt segments
|
||||
* without replacing entire role prompts.
|
||||
*
|
||||
* Supported keys: "executor-welcome", "executor-guardrails", "executor-spawning",
|
||||
* "executor-completion", "triage-welcome", "triage-context", "reviewer-verdict",
|
||||
* "merger-conflicts". */
|
||||
promptOverrides?: Record<string, string>;
|
||||
/** Enable/disable agent self-reflection workflows. Default: false. */
|
||||
reflectionEnabled?: boolean;
|
||||
/** How often periodic reflections occur in milliseconds. Default: 3_600_000 (1 hour). */
|
||||
@@ -1217,6 +1231,7 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
|
||||
missionMaxTaskRetries: 3,
|
||||
missionHealthCheckIntervalMs: 300_000,
|
||||
agentPrompts: undefined,
|
||||
promptOverrides: undefined,
|
||||
reflectionEnabled: false,
|
||||
reflectionIntervalMs: 3_600_000,
|
||||
reflectionAfterTask: true,
|
||||
@@ -1321,6 +1336,7 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
|
||||
"missionMaxTaskRetries",
|
||||
"missionHealthCheckIntervalMs",
|
||||
"agentPrompts",
|
||||
"promptOverrides",
|
||||
"reflectionEnabled",
|
||||
"reflectionIntervalMs",
|
||||
"reflectionAfterTask",
|
||||
|
||||
Reference in New Issue
Block a user