feat(agents): add Hephaestus - autonomous deep worker agent (#1287)
* refactor(keyword-detector): split constants into domain-specific modules * feat(shared): add requiresAnyModel and isAnyFallbackModelAvailable * feat(config): add hephaestus to agent schemas * feat(agents): add Hephaestus autonomous deep worker * feat(cli): update model-fallback for hephaestus support * feat(plugin): add hephaestus to config handler with ordering * test(delegate-task): update tests for hephaestus agent * docs: update AGENTS.md files for hephaestus * docs: add hephaestus to READMEs * chore: regenerate config schema * fix(delegate-task): bypass requiresModel check when user provides explicit config * docs(hephaestus): add 4-part context structure for explore/librarian prompts * docs: fix review comments from cubic (non-breaking changes) - Move Hephaestus from Primary Agents to Subagents (uses own fallback chain) - Fix Hephaestus fallback chain documentation (claude-opus-4-5 → gemini-3-pro) - Add settings.local.json to claude-code-hooks config sources - Fix delegate_task parameters in ultrawork prompt (agent→subagent_type, background→run_in_background, add load_skills) - Update line counts in AGENTS.md (index.ts: 788, manager.ts: 1440) * docs: fix additional documentation inconsistencies from oracle review - Fix delegate_task parameters in Background Agents example (docs/features.md) - Fix Hephaestus fallback chain in root AGENTS.md to match model-requirements.ts * docs: clarify Hephaestus has no fallback (requires gpt-5.2-codex only) Hephaestus uses requiresModel constraint - it only activates when gpt-5.2-codex is available. The fallback chain in code is unreachable, so documentation should not mention fallbacks. * fix(hephaestus): remove unreachable fallback chain entries Hephaestus has requiresModel: gpt-5.2-codex which means the agent only activates when that specific model is available. The fallback entries (claude-opus-4-5, gemini-3-pro) were unreachable and misleading. --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
This commit is contained in:
@@ -106,6 +106,45 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe("Plan agent demote behavior", () => {
|
||||
test("orders core agents as sisyphus -> hephaestus -> prometheus -> atlas", async () => {
|
||||
// #given
|
||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||
}
|
||||
createBuiltinAgentsMock.mockResolvedValue({
|
||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||
oracle: { name: "oracle", prompt: "test", mode: "subagent" },
|
||||
atlas: { name: "atlas", prompt: "test", mode: "primary" },
|
||||
})
|
||||
const pluginConfig: OhMyOpenCodeConfig = {
|
||||
sisyphus_agent: {
|
||||
planner_enabled: true,
|
||||
},
|
||||
}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
agent: {},
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then
|
||||
const keys = Object.keys(config.agent as Record<string, unknown>)
|
||||
const coreAgents = ["sisyphus", "hephaestus", "prometheus", "atlas"]
|
||||
const ordered = keys.filter((key) => coreAgents.includes(key))
|
||||
expect(ordered).toEqual(coreAgents)
|
||||
})
|
||||
|
||||
test("plan agent should be demoted to subagent mode when replacePlan is true", async () => {
|
||||
// given
|
||||
const pluginConfig: OhMyOpenCodeConfig = {
|
||||
@@ -173,6 +212,41 @@ describe("Plan agent demote behavior", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Agent permission defaults", () => {
|
||||
test("hephaestus should allow delegate_task", async () => {
|
||||
// #given
|
||||
const createBuiltinAgentsMock = agents.createBuiltinAgents as unknown as {
|
||||
mockResolvedValue: (value: Record<string, unknown>) => void
|
||||
}
|
||||
createBuiltinAgentsMock.mockResolvedValue({
|
||||
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
|
||||
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
|
||||
oracle: { name: "oracle", prompt: "test", mode: "subagent" },
|
||||
})
|
||||
const pluginConfig: OhMyOpenCodeConfig = {}
|
||||
const config: Record<string, unknown> = {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
agent: {},
|
||||
}
|
||||
const handler = createConfigHandler({
|
||||
ctx: { directory: "/tmp" },
|
||||
pluginConfig,
|
||||
modelCacheState: {
|
||||
anthropicContext1MEnabled: false,
|
||||
modelContextLimitsCache: new Map(),
|
||||
},
|
||||
})
|
||||
|
||||
// #when
|
||||
await handler(config)
|
||||
|
||||
// #then
|
||||
const agentConfig = config.agent as Record<string, { permission?: Record<string, string> }>
|
||||
expect(agentConfig.hephaestus).toBeDefined()
|
||||
expect(agentConfig.hephaestus.permission?.delegate_task).toBe("allow")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Prometheus category config resolution", () => {
|
||||
test("resolves ultrabrain category config", () => {
|
||||
// given
|
||||
|
||||
@@ -48,6 +48,28 @@ export function resolveCategoryConfig(
|
||||
return userCategories?.[categoryName] ?? DEFAULT_CATEGORIES[categoryName];
|
||||
}
|
||||
|
||||
const CORE_AGENT_ORDER = ["sisyphus", "hephaestus", "prometheus", "atlas"] as const;
|
||||
|
||||
function reorderAgentsByPriority(agents: Record<string, unknown>): Record<string, unknown> {
|
||||
const ordered: Record<string, unknown> = {};
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const key of CORE_AGENT_ORDER) {
|
||||
if (Object.prototype.hasOwnProperty.call(agents, key)) {
|
||||
ordered[key] = agents[key];
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(agents)) {
|
||||
if (!seen.has(key)) {
|
||||
ordered[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
const { ctx, pluginConfig, modelCacheState } = deps;
|
||||
|
||||
@@ -287,7 +309,7 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
prompt: PROMETHEUS_SYSTEM_PROMPT,
|
||||
permission: PROMETHEUS_PERMISSION,
|
||||
description: `${configAgent?.plan?.description ?? "Plan agent"} (Prometheus - OhMyOpenCode)`,
|
||||
color: (configAgent?.plan?.color as string) ?? "#FF6347",
|
||||
color: (configAgent?.plan?.color as string) ?? "#9D4EDD", // Amethyst Purple - wisdom/foresight
|
||||
...(temperatureToUse !== undefined ? { temperature: temperatureToUse } : {}),
|
||||
...(topPToUse !== undefined ? { top_p: topPToUse } : {}),
|
||||
...(maxTokensToUse !== undefined ? { maxTokens: maxTokensToUse } : {}),
|
||||
@@ -369,6 +391,10 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
};
|
||||
}
|
||||
|
||||
if (config.agent) {
|
||||
config.agent = reorderAgentsByPriority(config.agent as Record<string, unknown>);
|
||||
}
|
||||
|
||||
const agentResult = config.agent as AgentConfig;
|
||||
|
||||
config.tools = {
|
||||
@@ -397,6 +423,10 @@ export function createConfigHandler(deps: ConfigHandlerDeps) {
|
||||
const agent = agentResult.sisyphus as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, call_omo_agent: "deny", delegate_task: "allow", question: "allow" };
|
||||
}
|
||||
if (agentResult.hephaestus) {
|
||||
const agent = agentResult.hephaestus as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, call_omo_agent: "deny", delegate_task: "allow", question: "allow" };
|
||||
}
|
||||
if (agentResult["prometheus"]) {
|
||||
const agent = agentResult["prometheus"] as AgentWithPermission;
|
||||
agent.permission = { ...agent.permission, call_omo_agent: "deny", delegate_task: "allow", question: "allow" };
|
||||
|
||||
Reference in New Issue
Block a user