merge: integrate origin/dev into modular-enforcement branch

Resolves all merge conflicts, preserving our split module structure
while integrating all dev changes:
- Custom agent summaries support (parseRegisteredAgentSummaries)
- Background notification queue (enqueueNotificationForParent)
- Atlas shared git-worktree module (collectGitDiffStats, formatFileChanges)
- Ralph-loop withTimeout + DEFAULT_API_TIMEOUT=5000
- Session recovery assistant_prefill_unsupported error type
- Atlas agentOverrides forwarding
- Config handler plan model demotion (buildPlanDemoteConfig)
- Delegate-task agentOverrides, promptSyncWithModelSuggestionRetry, variant
- LSP init timeout + stale init detection
- isPlanFamily function + task-continuation-enforcer hook
- Handoff command
This commit is contained in:
YeonGyu-Kim
2026-02-08 17:34:47 +09:00
74 changed files with 4016 additions and 654 deletions
+7 -1
View File
@@ -13,6 +13,7 @@ import { loadProjectAgents, loadUserAgents } from "../features/claude-code-agent
import type { PluginComponents } from "./plugin-components-loader";
import { reorderAgentsByPriority } from "./agent-priority-order";
import { buildPrometheusAgentConfig } from "./prometheus-agent-config-builder";
import { buildPlanDemoteConfig } from "./plan-model-inheritance";
type AgentConfigRecord = Record<string, Record<string, unknown> | undefined> & {
build?: Record<string, unknown>;
@@ -152,7 +153,12 @@ export async function applyAgentConfig(params: {
? migrateAgentConfig(configAgent.build as Record<string, unknown>)
: {};
const planDemoteConfig = shouldDemotePlan ? { mode: "subagent" as const } : undefined;
const planDemoteConfig = shouldDemotePlan
? buildPlanDemoteConfig(
agentConfig["prometheus"] as Record<string, unknown> | undefined,
params.pluginConfig.agents?.plan as Record<string, unknown> | undefined,
)
: undefined;
params.config.agent = {
...agentConfig,
+286
View File
@@ -600,6 +600,187 @@ describe("Prometheus direct override priority over category", () => {
})
})
describe("Plan agent model inheritance from prometheus", () => {
test("plan agent inherits all model-related settings from resolved prometheus config", async () => {
//#given - prometheus resolves to claude-opus-4-6 with model settings
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {
plan: {
name: "plan",
mode: "primary",
prompt: "original plan prompt",
},
},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan inherits model and variant from prometheus, but NOT prompt
const agents = config.agent as Record<string, { mode?: string; model?: string; variant?: string; prompt?: string }>
expect(agents.plan).toBeDefined()
expect(agents.plan.mode).toBe("subagent")
expect(agents.plan.model).toBe("anthropic/claude-opus-4-6")
expect(agents.plan.variant).toBe("max")
expect(agents.plan.prompt).toBeUndefined()
})
test("plan agent inherits temperature, reasoningEffort, and other model settings from prometheus", async () => {
//#given - prometheus configured with category that has temperature and reasoningEffort
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "openai/gpt-5.2",
provenance: "override",
variant: "high",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
agents: {
prometheus: {
model: "openai/gpt-5.2",
variant: "high",
temperature: 0.3,
top_p: 0.9,
maxTokens: 16000,
reasoningEffort: "high",
textVerbosity: "medium",
thinking: { type: "enabled", budgetTokens: 8000 },
},
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan inherits ALL model-related settings from resolved prometheus
const agents = config.agent as Record<string, Record<string, unknown>>
expect(agents.plan).toBeDefined()
expect(agents.plan.mode).toBe("subagent")
expect(agents.plan.model).toBe("openai/gpt-5.2")
expect(agents.plan.variant).toBe("high")
expect(agents.plan.temperature).toBe(0.3)
expect(agents.plan.top_p).toBe(0.9)
expect(agents.plan.maxTokens).toBe(16000)
expect(agents.plan.reasoningEffort).toBe("high")
expect(agents.plan.textVerbosity).toBe("medium")
expect(agents.plan.thinking).toEqual({ type: "enabled", budgetTokens: 8000 })
})
test("plan agent user override takes priority over prometheus inherited settings", async () => {
//#given - prometheus resolves to opus, but user has plan override for gpt-5.2
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
agents: {
plan: {
model: "openai/gpt-5.2",
variant: "high",
temperature: 0.5,
},
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan uses its own override, not prometheus settings
const agents = config.agent as Record<string, Record<string, unknown>>
expect(agents.plan.model).toBe("openai/gpt-5.2")
expect(agents.plan.variant).toBe("high")
expect(agents.plan.temperature).toBe(0.5)
})
test("plan agent does NOT inherit prompt, description, or color from prometheus", async () => {
//#given
spyOn(shared, "resolveModelPipeline" as any).mockReturnValue({
model: "anthropic/claude-opus-4-6",
provenance: "provider-fallback",
variant: "max",
})
const pluginConfig: OhMyOpenCodeConfig = {
sisyphus_agent: {
planner_enabled: true,
replace_plan: true,
},
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then - plan has model settings but NOT prompt/description/color
const agents = config.agent as Record<string, Record<string, unknown>>
expect(agents.plan.model).toBe("anthropic/claude-opus-4-6")
expect(agents.plan.prompt).toBeUndefined()
expect(agents.plan.description).toBeUndefined()
expect(agents.plan.color).toBeUndefined()
})
})
describe("Deadlock prevention - fetchAvailableModels must not receive client", () => {
test("fetchAvailableModels should be called with undefined client to prevent deadlock during plugin init", async () => {
// given - This test ensures we don't regress on issue #1301
@@ -762,3 +943,108 @@ describe("config-handler plugin loading error boundary (#1559)", () => {
expect(commands["test-cmd"]).toBeDefined()
})
})
describe("per-agent todowrite/todoread deny when task_system enabled", () => {
const PRIMARY_AGENTS = ["sisyphus", "hephaestus", "atlas", "prometheus", "sisyphus-junior"]
test("denies todowrite and todoread for primary agents when task_system is enabled", async () => {
//#given
spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
atlas: { name: "atlas", prompt: "test", mode: "primary" },
prometheus: { name: "prometheus", prompt: "test", mode: "primary" },
"sisyphus-junior": { name: "sisyphus-junior", prompt: "test", mode: "subagent" },
oracle: { name: "oracle", prompt: "test", mode: "subagent" },
})
const pluginConfig: OhMyOpenCodeConfig = {
experimental: { task_system: true },
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then
const agentResult = config.agent as Record<string, { permission?: Record<string, unknown> }>
for (const agentName of PRIMARY_AGENTS) {
expect(agentResult[agentName]?.permission?.todowrite).toBe("deny")
expect(agentResult[agentName]?.permission?.todoread).toBe("deny")
}
})
test("does not deny todowrite/todoread when task_system is disabled", async () => {
//#given
spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" },
})
const pluginConfig: OhMyOpenCodeConfig = {
experimental: { task_system: false },
}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then
const agentResult = config.agent as Record<string, { permission?: Record<string, unknown> }>
expect(agentResult.sisyphus?.permission?.todowrite).toBeUndefined()
expect(agentResult.sisyphus?.permission?.todoread).toBeUndefined()
expect(agentResult.hephaestus?.permission?.todowrite).toBeUndefined()
expect(agentResult.hephaestus?.permission?.todoread).toBeUndefined()
})
test("does not deny todowrite/todoread when task_system is undefined", async () => {
//#given
spyOn(agents, "createBuiltinAgents" as any).mockResolvedValue({
sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" },
})
const pluginConfig: OhMyOpenCodeConfig = {}
const config: Record<string, unknown> = {
model: "anthropic/claude-opus-4-6",
agent: {},
}
const handler = createConfigHandler({
ctx: { directory: "/tmp" },
pluginConfig,
modelCacheState: {
anthropicContext1MEnabled: false,
modelContextLimitsCache: new Map(),
},
})
//#when
await handler(config)
//#then
const agentResult = config.agent as Record<string, { permission?: Record<string, unknown> }>
expect(agentResult.sisyphus?.permission?.todowrite).toBeUndefined()
expect(agentResult.sisyphus?.permission?.todoread).toBeUndefined()
})
})
@@ -0,0 +1,118 @@
import { describe, test, expect } from "bun:test"
import { buildPlanDemoteConfig } from "./plan-model-inheritance"
describe("buildPlanDemoteConfig", () => {
test("returns only mode when prometheus and plan override are both undefined", () => {
//#given
const prometheusConfig = undefined
const planOverride = undefined
//#when
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
//#then
expect(result).toEqual({ mode: "subagent" })
})
test("extracts all model settings from prometheus config", () => {
//#given
const prometheusConfig = {
name: "prometheus",
model: "anthropic/claude-opus-4-6",
variant: "max",
mode: "all",
prompt: "You are Prometheus...",
permission: { edit: "allow" },
description: "Plan agent (Prometheus)",
color: "#FF5722",
temperature: 0.1,
top_p: 0.95,
maxTokens: 32000,
thinking: { type: "enabled", budgetTokens: 10000 },
reasoningEffort: "high",
textVerbosity: "medium",
providerOptions: { key: "value" },
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, undefined)
//#then - picks model settings, NOT prompt/permission/description/color/name/mode
expect(result.mode).toBe("subagent")
expect(result.model).toBe("anthropic/claude-opus-4-6")
expect(result.variant).toBe("max")
expect(result.temperature).toBe(0.1)
expect(result.top_p).toBe(0.95)
expect(result.maxTokens).toBe(32000)
expect(result.thinking).toEqual({ type: "enabled", budgetTokens: 10000 })
expect(result.reasoningEffort).toBe("high")
expect(result.textVerbosity).toBe("medium")
expect(result.providerOptions).toEqual({ key: "value" })
expect(result.prompt).toBeUndefined()
expect(result.permission).toBeUndefined()
expect(result.description).toBeUndefined()
expect(result.color).toBeUndefined()
expect(result.name).toBeUndefined()
})
test("plan override takes priority over prometheus for all model settings", () => {
//#given
const prometheusConfig = {
model: "anthropic/claude-opus-4-6",
variant: "max",
temperature: 0.1,
reasoningEffort: "high",
}
const planOverride = {
model: "openai/gpt-5.2",
variant: "high",
temperature: 0.5,
reasoningEffort: "low",
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
//#then
expect(result.model).toBe("openai/gpt-5.2")
expect(result.variant).toBe("high")
expect(result.temperature).toBe(0.5)
expect(result.reasoningEffort).toBe("low")
})
test("falls back to prometheus when plan override has partial settings", () => {
//#given
const prometheusConfig = {
model: "anthropic/claude-opus-4-6",
variant: "max",
temperature: 0.1,
reasoningEffort: "high",
}
const planOverride = {
model: "openai/gpt-5.2",
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, planOverride)
//#then - plan model wins, rest inherits from prometheus
expect(result.model).toBe("openai/gpt-5.2")
expect(result.variant).toBe("max")
expect(result.temperature).toBe(0.1)
expect(result.reasoningEffort).toBe("high")
})
test("skips undefined values from both sources", () => {
//#given
const prometheusConfig = {
model: "anthropic/claude-opus-4-6",
}
//#when
const result = buildPlanDemoteConfig(prometheusConfig, undefined)
//#then
expect(result).toEqual({ mode: "subagent", model: "anthropic/claude-opus-4-6" })
expect(Object.keys(result)).toEqual(["mode", "model"])
})
})
@@ -0,0 +1,27 @@
const MODEL_SETTINGS_KEYS = [
"model",
"variant",
"temperature",
"top_p",
"maxTokens",
"thinking",
"reasoningEffort",
"textVerbosity",
"providerOptions",
] as const
export function buildPlanDemoteConfig(
prometheusConfig: Record<string, unknown> | undefined,
planOverride: Record<string, unknown> | undefined,
): Record<string, unknown> {
const modelSettings: Record<string, unknown> = {}
for (const key of MODEL_SETTINGS_KEYS) {
const value = planOverride?.[key] ?? prometheusConfig?.[key]
if (value !== undefined) {
modelSettings[key] = value
}
}
return { mode: "subagent" as const, ...modelSettings }
}
@@ -7,6 +7,10 @@ export function applyToolConfig(params: {
pluginConfig: OhMyOpenCodeConfig;
agentResult: Record<string, unknown>;
}): void {
const denyTodoTools = params.pluginConfig.experimental?.task_system
? { todowrite: "deny", todoread: "deny" }
: {}
params.config.tools = {
...(params.config.tools as Record<string, unknown>),
"grep_app_*": false,
@@ -39,6 +43,7 @@ export function applyToolConfig(params: {
call_omo_agent: "deny",
"task_*": "allow",
teammate: "allow",
...denyTodoTools,
};
}
if (params.agentResult.sisyphus) {
@@ -50,6 +55,7 @@ export function applyToolConfig(params: {
question: questionPermission,
"task_*": "allow",
teammate: "allow",
...denyTodoTools,
};
}
if (params.agentResult.hephaestus) {
@@ -59,6 +65,7 @@ export function applyToolConfig(params: {
call_omo_agent: "deny",
task: "allow",
question: questionPermission,
...denyTodoTools,
};
}
if (params.agentResult["prometheus"]) {
@@ -70,6 +77,7 @@ export function applyToolConfig(params: {
question: questionPermission,
"task_*": "allow",
teammate: "allow",
...denyTodoTools,
};
}
if (params.agentResult["sisyphus-junior"]) {
@@ -79,6 +87,7 @@ export function applyToolConfig(params: {
task: "allow",
"task_*": "allow",
teammate: "allow",
...denyTodoTools,
};
}