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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user