diff --git a/src/features/opencode-skill-loader/async-loader.ts b/src/features/opencode-skill-loader/async-loader.ts index 141d5aa6f..ee866696b 100644 --- a/src/features/opencode-skill-loader/async-loader.ts +++ b/src/features/opencode-skill-loader/async-loader.ts @@ -87,7 +87,7 @@ export async function loadSkillFromPathAsync( const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp - const baseName = data.name || defaultName + const baseName = String(data.name || defaultName) const skillName = namePrefix ? `${namePrefix}/${baseName}` : baseName const originalDescription = data.description || "" const isOpencodeSource = scope === "opencode" || scope === "opencode-project" diff --git a/src/features/opencode-skill-loader/loaded-skill-from-path.ts b/src/features/opencode-skill-loader/loaded-skill-from-path.ts index c94855828..4097f6917 100644 --- a/src/features/opencode-skill-loader/loaded-skill-from-path.ts +++ b/src/features/opencode-skill-loader/loaded-skill-from-path.ts @@ -25,7 +25,7 @@ export async function loadSkillFromPath(options: { const mcpJsonMcp = await loadMcpJsonFromDir(options.resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp - const baseName = data.name || options.defaultName + const baseName = String(data.name || options.defaultName) const skillName = namePrefix ? `${namePrefix}/${baseName}` : baseName const originalDescription = data.description || "" const isOpencodeSource = options.scope === "opencode" || options.scope === "opencode-project" diff --git a/src/hooks/compaction-context-injector/recovery-prompt-config.ts b/src/hooks/compaction-context-injector/recovery-prompt-config.ts index f0f8480c8..64c8dcfab 100644 --- a/src/hooks/compaction-context-injector/recovery-prompt-config.ts +++ b/src/hooks/compaction-context-injector/recovery-prompt-config.ts @@ -1,3 +1,4 @@ +import { stripInvisibleAgentCharacters } from "../../shared/agent-display-names" import type { CompactionAgentConfigCheckpoint } from "../../shared/compaction-agent-config-checkpoint" export type RecoveryPromptConfig = CompactionAgentConfigCheckpoint & { @@ -66,7 +67,7 @@ export function isPromptConfigRecovered( const agentMatches = typeof actualAgent === "string" && !isCompactionAgent(actualAgent) && - actualAgent.toLowerCase() === expectedPromptConfig.agent.toLowerCase() + stripInvisibleAgentCharacters(actualAgent).toLowerCase() === stripInvisibleAgentCharacters(expectedPromptConfig.agent).toLowerCase() return ( agentMatches && diff --git a/src/hooks/keyword-detector/ultrawork/default.ts b/src/hooks/keyword-detector/ultrawork/default.ts index 639ae6b61..37790d9ea 100644 --- a/src/hooks/keyword-detector/ultrawork/default.ts +++ b/src/hooks/keyword-detector/ultrawork/default.ts @@ -104,7 +104,7 @@ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST. | Architecture decision needed | MUST call plan agent | \`\`\` -task(subagent_type="plan", load_skills=[], prompt="") +task(subagent_type="plan", load_skills=[], run_in_background=false, prompt="") \`\`\` **WHY PLAN AGENT IS MANDATORY:** @@ -119,9 +119,9 @@ task(subagent_type="plan", load_skills=[], prompt=" - Prefer tools over internal knowledge for fresh or user-specific data diff --git a/src/hooks/keyword-detector/ultrawork/planner.ts b/src/hooks/keyword-detector/ultrawork/planner.ts index ee29771c3..c6cec77c6 100644 --- a/src/hooks/keyword-detector/ultrawork/planner.ts +++ b/src/hooks/keyword-detector/ultrawork/planner.ts @@ -117,7 +117,7 @@ Each TODO item MUST include: | Wave | Tasks | Dispatch Command | |------|-------|------------------| -| 1 | 1, 4 | \`task(category="...", load_skills=[...], run_in_background=false)\` × 2 | +| 1 | 1, 4 | \`task(category="...", load_skills=[...], run_in_background=true)\` × 2 | | 2 | 2, 3, 5 | \`task(...)\` × 3 after Wave 1 completes | | 3 | 6 | \`task(...)\` final integration | diff --git a/src/hooks/ralph-loop/oracle-verification-detector.ts b/src/hooks/ralph-loop/oracle-verification-detector.ts index 304a38809..9d360282d 100644 --- a/src/hooks/ralph-loop/oracle-verification-detector.ts +++ b/src/hooks/ralph-loop/oracle-verification-detector.ts @@ -1,3 +1,4 @@ +import { stripInvisibleAgentCharacters } from "../../shared/agent-display-names" import { ULTRAWORK_VERIFICATION_PROMISE } from "./constants" export interface OracleVerificationEvidence { @@ -54,7 +55,7 @@ export function isOracleVerified(text: string): boolean { return false } - const isOracleAgent = evidence.agent.toLowerCase() === "oracle" + const isOracleAgent = stripInvisibleAgentCharacters(evidence.agent).toLowerCase() === "oracle" const isVerifiedPromise = evidence.promise === ULTRAWORK_VERIFICATION_PROMISE return isOracleAgent && isVerifiedPromise @@ -62,7 +63,7 @@ export function isOracleVerified(text: string): boolean { export function extractOracleSessionID(text: string): string | undefined { const evidence = parseOracleVerificationEvidence(text) - if (!evidence || evidence.agent.toLowerCase() !== "oracle") { + if (!evidence || stripInvisibleAgentCharacters(evidence.agent).toLowerCase() !== "oracle") { return undefined } diff --git a/src/plugin/tool-execute-after.ts b/src/plugin/tool-execute-after.ts index fb1be3aa4..b0c31bb47 100644 --- a/src/plugin/tool-execute-after.ts +++ b/src/plugin/tool-execute-after.ts @@ -1,6 +1,7 @@ import { consumeToolMetadata } from "../features/tool-metadata-store" import type { CreatedHooks } from "../create-hooks" import { log } from "../shared" +import { stripInvisibleAgentCharacters } from "../shared/agent-display-names" import type { PluginContext } from "./types" import { readState, writeState } from "../hooks/ralph-loop/storage" @@ -60,7 +61,7 @@ export function createToolExecuteAfterHandler(args: { const verificationAttemptId = prompt?.match(VERIFICATION_ATTEMPT_PATTERN)?.[1]?.trim() const loopState = directory ? readState(directory) : null const isVerificationContext = - agent === "oracle" + (agent ? stripInvisibleAgentCharacters(agent) : agent) === "oracle" && !!sessionId && !!directory && loopState?.active === true diff --git a/src/plugin/tool-execute-before.ts b/src/plugin/tool-execute-before.ts index 3649720b9..68b1545ea 100644 --- a/src/plugin/tool-execute-before.ts +++ b/src/plugin/tool-execute-before.ts @@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto" import { getMainSessionID } from "../features/claude-code-session-state" import { clearBoulderState } from "../features/boulder-state" import { log } from "../shared" +import { stripInvisibleAgentCharacters } from "../shared/agent-display-names" import { resolveSessionAgent } from "./session-agent-resolver" import { parseRalphLoopArguments } from "../hooks/ralph-loop/command-arguments" import { ULTRAWORK_VERIFICATION_PROMISE } from "../hooks/ralph-loop/constants" @@ -109,7 +110,7 @@ export function createToolExecuteBeforeHandler(args: { } const normalizedSubagentType = - typeof argsObject.subagent_type === "string" ? argsObject.subagent_type : undefined + typeof argsObject.subagent_type === "string" ? stripInvisibleAgentCharacters(argsObject.subagent_type) : undefined const prompt = typeof argsObject.prompt === "string" ? argsObject.prompt : "" const loopState = typeof ctx.directory === "string" ? readState(ctx.directory) : null const shouldInjectOracleVerification = diff --git a/src/shared/agent-tool-restrictions.ts b/src/shared/agent-tool-restrictions.ts index 865251d6f..255f31600 100644 --- a/src/shared/agent-tool-restrictions.ts +++ b/src/shared/agent-tool-restrictions.ts @@ -1,3 +1,5 @@ +import { stripInvisibleAgentCharacters } from "./agent-display-names" + /** * Agent tool restrictions for session.prompt calls. * OpenCode SDK's session.prompt `tools` parameter expects boolean values. @@ -45,13 +47,15 @@ const AGENT_RESTRICTIONS: Record> = { } export function getAgentToolRestrictions(agentName: string): Record { - return AGENT_RESTRICTIONS[agentName] - ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1] + const stripped = stripInvisibleAgentCharacters(agentName) + return AGENT_RESTRICTIONS[stripped] + ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === stripped.toLowerCase())?.[1] ?? {} } export function hasAgentToolRestrictions(agentName: string): boolean { - const restrictions = AGENT_RESTRICTIONS[agentName] - ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1] + const stripped = stripInvisibleAgentCharacters(agentName) + const restrictions = AGENT_RESTRICTIONS[stripped] + ?? Object.entries(AGENT_RESTRICTIONS).find(([key]) => key.toLowerCase() === stripped.toLowerCase())?.[1] return restrictions !== undefined && Object.keys(restrictions).length > 0 } diff --git a/src/shared/agent-variant.ts b/src/shared/agent-variant.ts index 4551e3d3a..3120d07dc 100644 --- a/src/shared/agent-variant.ts +++ b/src/shared/agent-variant.ts @@ -1,4 +1,5 @@ import type { OhMyOpenCodeConfig } from "../config" +import { stripInvisibleAgentCharacters } from "./agent-display-names" import { AGENT_MODEL_REQUIREMENTS, CATEGORY_MODEL_REQUIREMENTS } from "./model-requirements" export function resolveAgentVariant( @@ -9,12 +10,13 @@ export function resolveAgentVariant( return undefined } + const stripped = stripInvisibleAgentCharacters(agentName) const agentOverrides = config.agents as | Record | undefined const agentOverride = agentOverrides - ? agentOverrides[agentName] - ?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1] + ? agentOverrides[stripped] + ?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === stripped.toLowerCase())?.[1] : undefined if (!agentOverride) { return undefined @@ -37,18 +39,19 @@ export function resolveVariantForModel( agentName: string, currentModel: { providerID: string; modelID: string }, ): string | undefined { + const stripped = stripInvisibleAgentCharacters(agentName) const agentOverrides = config.agents as | Record | undefined const agentOverride = agentOverrides - ? agentOverrides[agentName] - ?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1] + ? agentOverrides[stripped] + ?? Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === stripped.toLowerCase())?.[1] : undefined if (agentOverride?.variant) { return agentOverride.variant } - const agentRequirement = AGENT_MODEL_REQUIREMENTS[agentName] + const agentRequirement = AGENT_MODEL_REQUIREMENTS[stripped] if (agentRequirement) { return findVariantInChain(agentRequirement.fallbackChain, currentModel) } diff --git a/src/tools/call-omo-agent/tools.ts b/src/tools/call-omo-agent/tools.ts index d3e12f14d..cca3711f6 100644 --- a/src/tools/call-omo-agent/tools.ts +++ b/src/tools/call-omo-agent/tools.ts @@ -6,7 +6,7 @@ import type { CategoriesConfig, AgentOverrides } from "../../config/schema" import type { DelegatedModelConfig } from "../../shared/model-resolution-types" import type { FallbackEntry } from "../../shared/model-requirements" import { AGENT_MODEL_REQUIREMENTS } from "../../shared/model-requirements" -import { getAgentConfigKey } from "../../shared/agent-display-names" +import { getAgentConfigKey, stripInvisibleAgentCharacters } from "../../shared/agent-display-names" import { normalizeFallbackModels } from "../../shared/model-resolver" import { buildFallbackChainFromModels } from "../../shared/fallback-chain-from-models" import { log } from "../../shared" @@ -104,20 +104,21 @@ export function createCallOmoAgent( const toolCtx = toolContext as ToolContextWithMetadata log(`[call_omo_agent] Starting with agent: ${args.subagent_type}, background: ${args.run_in_background}`) - // Case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc. + // Strip ZWSP and case-insensitive agent validation - allows "Explore", "EXPLORE", "explore" etc. + const strippedAgentType = stripInvisibleAgentCharacters(args.subagent_type) if ( !ALLOWED_AGENTS.some( - (name) => name.toLowerCase() === args.subagent_type.toLowerCase(), + (name) => name.toLowerCase() === strippedAgentType.toLowerCase(), ) ) { return `Error: Invalid agent type "${args.subagent_type}". Only ${ALLOWED_AGENTS.join(", ")} are allowed.` } - const normalizedAgent = args.subagent_type.toLowerCase() as AllowedAgentType + const normalizedAgent = strippedAgentType.toLowerCase() as AllowedAgentType args = { ...args, subagent_type: normalizedAgent } // Check if agent is disabled - if (disabledAgents.some((disabled) => disabled.toLowerCase() === normalizedAgent)) { + if (disabledAgents.some((disabled) => stripInvisibleAgentCharacters(disabled).toLowerCase() === normalizedAgent)) { return `Error: Agent "${normalizedAgent}" is disabled via disabled_agents configuration. Remove it from disabled_agents in your ${CONFIG_BASENAME}.json to use it.` } diff --git a/src/tools/delegate-task/sync-prompt-sender.ts b/src/tools/delegate-task/sync-prompt-sender.ts index bd38830e5..1f8ad22a5 100644 --- a/src/tools/delegate-task/sync-prompt-sender.ts +++ b/src/tools/delegate-task/sync-prompt-sender.ts @@ -8,6 +8,7 @@ import { } from "../../shared/model-suggestion-retry" import { formatDetailedError } from "./error-formatting" import { getAgentToolRestrictions } from "../../shared/agent-tool-restrictions" +import { stripInvisibleAgentCharacters } from "../../shared/agent-display-names" import { applySessionPromptParams } from "../../shared/session-prompt-params-helpers" import { setSessionTools } from "../../shared/session-tools-store" import { createInternalAgentTextPart } from "../../shared/internal-initiator-marker" @@ -41,7 +42,7 @@ function buildPromptGenerationParams(model: DelegatedModelConfig | undefined): R } function isOracleAgent(agentToUse: string): boolean { - return agentToUse.toLowerCase() === "oracle" + return stripInvisibleAgentCharacters(agentToUse).toLowerCase() === "oracle" } function isUnexpectedEofError(error: unknown): boolean { @@ -80,7 +81,7 @@ export async function sendSyncPrompt( const promptArgs = { path: { id: input.sessionID }, body: { - agent: input.agentToUse.replace(/^\u200B+/, ""), + agent: stripInvisibleAgentCharacters(input.agentToUse), system: input.systemContent, tools, parts: [createInternalAgentTextPart(effectivePrompt)],