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:
YeonGyu-Kim
2026-02-01 19:26:57 +09:00
committed by GitHub
parent 5f053cd75b
commit 64825158a7
45 changed files with 2617 additions and 970 deletions
+5 -2
View File
@@ -1,7 +1,10 @@
# SHARED UTILITIES KNOWLEDGE BASE
## OVERVIEW
55 cross-cutting utilities: path resolution, token truncation, config parsing, model resolution.
55 cross-cutting utilities. Import via barrel pattern: `import { log, deepMerge } from "../../shared"`
**Categories**: Path resolution, Token truncation, Config parsing, Model resolution, System directives, Tool restrictions
## STRUCTURE
```
@@ -10,7 +13,7 @@ shared/
├── logger.ts # File-based logging (/tmp/oh-my-opencode.log)
├── dynamic-truncator.ts # Token-aware context window management (194 lines)
├── model-resolver.ts # 3-step resolution (Override → Fallback → Default)
├── model-requirements.ts # Agent/category model fallback chains (132 lines)
├── model-requirements.ts # Agent/category model fallback chains (162 lines)
├── model-availability.ts # Provider model fetching & fuzzy matching (154 lines)
├── jsonc-parser.ts # JSONC parsing with comment support
├── frontmatter.ts # YAML frontmatter extraction (JSON_SCHEMA only)
+8 -8
View File
@@ -95,22 +95,22 @@ describe("resolveVariantForModel", () => {
expect(variant).toBe("max")
})
test("returns correct variant for openai provider", () => {
// given
test("returns correct variant for openai provider (hephaestus agent)", () => {
// #given hephaestus has openai/gpt-5.2-codex with variant "medium" in its chain
const config = {} as OhMyOpenCodeConfig
const model = { providerID: "openai", modelID: "gpt-5.2" }
const model = { providerID: "openai", modelID: "gpt-5.2-codex" }
// when
const variant = resolveVariantForModel(config, "sisyphus", model)
// #when
const variant = resolveVariantForModel(config, "hephaestus", model)
// then
expect(variant).toBe("medium")
})
test("returns undefined for provider with no variant in chain", () => {
// given
test("returns undefined for provider not in sisyphus chain", () => {
// #given openai is not in sisyphus fallback chain anymore
const config = {} as OhMyOpenCodeConfig
const model = { providerID: "google", modelID: "gemini-3-pro" }
const model = { providerID: "openai", modelID: "gpt-5.2" }
// when
const variant = resolveVariantForModel(config, "sisyphus", model)
+20
View File
@@ -259,6 +259,26 @@ export async function fetchAvailableModels(
return modelSet
}
export function isAnyFallbackModelAvailable(
fallbackChain: Array<{ providers: string[]; model: string }>,
availableModels: Set<string>,
): boolean {
if (availableModels.size === 0) {
return false
}
for (const entry of fallbackChain) {
const hasAvailableProvider = entry.providers.some((provider) => {
return fuzzyMatchModel(entry.model, availableModels, [provider]) !== null
})
if (hasAvailableProvider) {
return true
}
}
log("[isAnyFallbackModelAvailable] no model available in chain", { chainLength: fallbackChain.length })
return false
}
export function __resetModelCache(): void {}
export function isModelCacheAvailable(): boolean {
+25 -9
View File
@@ -23,20 +23,25 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
expect(primary.variant).toBe("high")
})
test("sisyphus has valid fallbackChain with claude-opus-4-5 as primary", () => {
// given - sisyphus agent requirement
test("sisyphus has valid fallbackChain with claude-opus-4-5 as primary and requiresAnyModel", () => {
// #given - sisyphus agent requirement
const sisyphus = AGENT_MODEL_REQUIREMENTS["sisyphus"]
// when - accessing Sisyphus requirement
// then - fallbackChain exists with claude-opus-4-5 as first entry
// #when - accessing Sisyphus requirement
// #then - fallbackChain exists with claude-opus-4-5 as first entry, glm-4.7-free as last
expect(sisyphus).toBeDefined()
expect(sisyphus.fallbackChain).toBeArray()
expect(sisyphus.fallbackChain.length).toBeGreaterThan(0)
expect(sisyphus.fallbackChain).toHaveLength(5)
expect(sisyphus.requiresAnyModel).toBe(true)
const primary = sisyphus.fallbackChain[0]
expect(primary.providers[0]).toBe("anthropic")
expect(primary.model).toBe("claude-opus-4-5")
expect(primary.variant).toBe("max")
const last = sisyphus.fallbackChain[4]
expect(last.providers[0]).toBe("opencode")
expect(last.model).toBe("glm-4.7-free")
})
test("librarian has valid fallbackChain with glm-4.7 as primary", () => {
@@ -156,10 +161,21 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
expect(primary.providers[0]).toBe("kimi-for-coding")
})
test("all 9 builtin agents have valid fallbackChain arrays", () => {
// given - list of 9 agent names
test("hephaestus requires gpt-5.2-codex", () => {
// #given - hephaestus agent requirement
const hephaestus = AGENT_MODEL_REQUIREMENTS["hephaestus"]
// #when - accessing hephaestus requirement
// #then - requiresModel is set to gpt-5.2-codex
expect(hephaestus).toBeDefined()
expect(hephaestus.requiresModel).toBe("gpt-5.2-codex")
})
test("all 10 builtin agents have valid fallbackChain arrays", () => {
// #given - list of 10 agent names
const expectedAgents = [
"sisyphus",
"hephaestus",
"oracle",
"librarian",
"explore",
@@ -173,8 +189,8 @@ describe("AGENT_MODEL_REQUIREMENTS", () => {
// when - checking AGENT_MODEL_REQUIREMENTS
const definedAgents = Object.keys(AGENT_MODEL_REQUIREMENTS)
// then - all agents present with valid fallbackChain
expect(definedAgents).toHaveLength(9)
// #then - all agents present with valid fallbackChain
expect(definedAgents).toHaveLength(10)
for (const agent of expectedAgents) {
const requirement = AGENT_MODEL_REQUIREMENTS[agent]
expect(requirement).toBeDefined()
+9 -2
View File
@@ -8,6 +8,7 @@ export type ModelRequirement = {
fallbackChain: FallbackEntry[]
variant?: string // Default variant (used when entry doesn't specify one)
requiresModel?: string // If set, only activates when this model is available (fuzzy match)
requiresAnyModel?: boolean // If true, requires at least ONE model in fallbackChain to be available (or empty availability treated as unavailable)
}
export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
@@ -17,9 +18,15 @@ export const AGENT_MODEL_REQUIREMENTS: Record<string, ModelRequirement> = {
{ providers: ["kimi-for-coding"], model: "k2p5" },
{ providers: ["opencode"], model: "kimi-k2.5-free" },
{ providers: ["zai-coding-plan"], model: "glm-4.7" },
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
{ providers: ["google", "github-copilot", "opencode"], model: "gemini-3-pro" },
{ providers: ["opencode"], model: "glm-4.7-free" },
],
requiresAnyModel: true,
},
hephaestus: {
fallbackChain: [
{ providers: ["openai", "github-copilot", "opencode"], model: "gpt-5.2-codex", variant: "medium" },
],
requiresModel: "gpt-5.2-codex",
},
oracle: {
fallbackChain: [