Merge pull request #3436 from code-yeongyu/fix/bug-batch-1

fix: numeric skill names, ZWSP agent lookups, ultrawork run_in_background
This commit is contained in:
YeonGyu-Kim
2026-04-15 10:53:44 +09:00
committed by GitHub
14 changed files with 61 additions and 48 deletions
@@ -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"
@@ -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"
@@ -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 &&
+13 -13
View File
@@ -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="<gathered context + user request>")
task(subagent_type="plan", load_skills=[], run_in_background=false, prompt="<gathered context + user request>")
\`\`\`
**WHY PLAN AGENT IS MANDATORY:**
@@ -119,9 +119,9 @@ task(subagent_type="plan", load_skills=[], prompt="<gathered context + user requ
| Scenario | Action |
|----------|--------|
| Plan agent asks clarifying questions | \`task(session_id="{returned_session_id}", load_skills=[], prompt="<your answer>")\` |
| Need to refine the plan | \`task(session_id="{returned_session_id}", load_skills=[], prompt="Please adjust: <feedback>")\` |
| Plan needs more detail | \`task(session_id="{returned_session_id}", load_skills=[], prompt="Add more detail to Task N")\` |
| Plan agent asks clarifying questions | \`task(session_id="{returned_session_id}", load_skills=[], run_in_background=false, prompt="<your answer>")\` |
| Need to refine the plan | \`task(session_id="{returned_session_id}", load_skills=[], run_in_background=false, prompt="Please adjust: <feedback>")\` |
| Plan needs more detail | \`task(session_id="{returned_session_id}", load_skills=[], run_in_background=false, prompt="Add more detail to Task N")\` |
**WHY SESSION_ID IS CRITICAL:**
- Plan agent retains FULL conversation context
@@ -131,10 +131,10 @@ task(subagent_type="plan", load_skills=[], prompt="<gathered context + user requ
\`\`\`
// WRONG: Starting fresh loses all context
task(subagent_type="plan", load_skills=[], prompt="Here's more info...")
task(subagent_type="plan", load_skills=[], run_in_background=false, prompt="Here's more info...")
// CORRECT: Resume preserves everything
task(session_id="ses_abc123", load_skills=[], prompt="Here's my answer to your question: ...")
task(session_id="ses_abc123", load_skills=[], run_in_background=false, prompt="Here's my answer to your question: ...")
\`\`\`
**FAILURE TO CALL PLAN AGENT = INCOMPLETE WORK.**
@@ -149,21 +149,21 @@ task(session_id="ses_abc123", load_skills=[], prompt="Here's my answer to your q
|-----------|--------|-----|
| Codebase exploration | task(subagent_type="explore", load_skills=[], run_in_background=true) | Parallel, context-efficient |
| Documentation lookup | task(subagent_type="librarian", load_skills=[], run_in_background=true) | Specialized knowledge |
| Planning | task(subagent_type="plan", load_skills=[]) | Parallel task graph + structured TODO list |
| Hard problem (conventional) | task(subagent_type="oracle", load_skills=[]) | Architecture, debugging, complex logic |
| Hard problem (non-conventional) | task(category="artistry", load_skills=[...]) | Different approach needed |
| Implementation | task(category="...", load_skills=[...]) | Domain-optimized models |
| Planning | task(subagent_type="plan", load_skills=[], run_in_background=false) | Parallel task graph + structured TODO list |
| Hard problem (conventional) | task(subagent_type="oracle", load_skills=[], run_in_background=false) | Architecture, debugging, complex logic |
| Hard problem (non-conventional) | task(category="artistry", load_skills=[...], run_in_background=true) | Different approach needed |
| Implementation | task(category="...", load_skills=[...], run_in_background=true) | Domain-optimized models |
**CATEGORY + SKILL DELEGATION:**
\`\`\`
// Frontend work
task(category="visual-engineering", load_skills=["frontend-ui-ux"])
task(category="visual-engineering", load_skills=["frontend-ui-ux"], run_in_background=true)
// Complex logic
task(category="ultrabrain", load_skills=["typescript-programmer"])
task(category="ultrabrain", load_skills=["typescript-programmer"], run_in_background=true)
// Quick fixes
task(category="quick", load_skills=["git-master"])
task(category="quick", load_skills=["git-master"], run_in_background=true)
\`\`\`
**YOU SHOULD ONLY DO IT YOURSELF WHEN:**
@@ -156,7 +156,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="<gathered context + user request>")
task(subagent_type="plan", load_skills=[], run_in_background=false, prompt="<gathered context + user request>")
\`\`\`
### SESSION CONTINUITY WITH PLAN AGENT (CRITICAL)
@@ -165,9 +165,9 @@ task(subagent_type="plan", load_skills=[], prompt="<gathered context + user requ
| Scenario | Action |
|----------|--------|
| Plan agent asks clarifying questions | \`task(session_id="{returned_session_id}", load_skills=[], prompt="<your answer>")\` |
| Need to refine the plan | \`task(session_id="{returned_session_id}", load_skills=[], prompt="Please adjust: <feedback>")\` |
| Plan needs more detail | \`task(session_id="{returned_session_id}", load_skills=[], prompt="Add more detail to Task N")\` |
| Plan agent asks clarifying questions | \`task(session_id="{returned_session_id}", load_skills=[], run_in_background=false, prompt="<your answer>")\` |
| Need to refine the plan | \`task(session_id="{returned_session_id}", load_skills=[], run_in_background=false, prompt="Please adjust: <feedback>")\` |
| Plan needs more detail | \`task(session_id="{returned_session_id}", load_skills=[], run_in_background=false, prompt="Add more detail to Task N")\` |
**FAILURE TO CALL PLAN AGENT = INCOMPLETE WORK.**
@@ -183,10 +183,10 @@ task(subagent_type="plan", load_skills=[], prompt="<gathered context + user requ
|-----------|--------|-----|
| Codebase exploration | task(subagent_type="explore", load_skills=[], run_in_background=true) | Parallel, context-efficient |
| Documentation lookup | task(subagent_type="librarian", load_skills=[], run_in_background=true) | Specialized knowledge |
| Planning | task(subagent_type="plan", load_skills=[]) | Parallel task graph + structured TODO list |
| Hard problem (conventional) | task(subagent_type="oracle", load_skills=[]) | Architecture, debugging, complex logic |
| Hard problem (non-conventional) | task(category="artistry", load_skills=[...]) | Different approach needed |
| Implementation | task(category="...", load_skills=[...]) | Domain-optimized models |
| Planning | task(subagent_type="plan", load_skills=[], run_in_background=false) | Parallel task graph + structured TODO list |
| Hard problem (conventional) | task(subagent_type="oracle", load_skills=[], run_in_background=false) | Architecture, debugging, complex logic |
| Hard problem (non-conventional) | task(category="artistry", load_skills=[...], run_in_background=true) | Different approach needed |
| Implementation | task(category="...", load_skills=[...], run_in_background=true) | Domain-optimized models |
**YOU SHOULD ONLY DO IT YOURSELF WHEN:**
- Task is trivially simple (1-2 lines, obvious change)
+3 -3
View File
@@ -71,9 +71,9 @@ Use these when they provide clear value based on the decision framework above:
|----------|-------------|------------|
| explore agent | Need codebase patterns you don't have | \`task(subagent_type="explore", load_skills=[], run_in_background=true, ...)\` |
| librarian agent | External library docs, OSS examples | \`task(subagent_type="librarian", load_skills=[], run_in_background=true, ...)\` |
| oracle agent | Stuck on architecture/debugging after 2+ attempts | \`task(subagent_type="oracle", load_skills=[], ...)\` |
| plan agent | Complex multi-step with dependencies (5+ steps) | \`task(subagent_type="plan", load_skills=[], ...)\` |
| task category | Specialized work matching a category | \`task(category="...", load_skills=[...])\` |
| oracle agent | Stuck on architecture/debugging after 2+ attempts | \`task(subagent_type="oracle", load_skills=[], run_in_background=false, ...)\` |
| plan agent | Complex multi-step with dependencies (5+ steps) | \`task(subagent_type="plan", load_skills=[], run_in_background=false, ...)\` |
| task category | Specialized work matching a category | \`task(category="...", load_skills=[...], run_in_background=true)\` |
<tool_usage_rules>
- Prefer tools over internal knowledge for fresh or user-specific data
@@ -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 |
@@ -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
}
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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 =
+8 -4
View File
@@ -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<string, Record<string, boolean>> = {
}
export function getAgentToolRestrictions(agentName: string): Record<string, boolean> {
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
}
+8 -5
View File
@@ -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<string, { variant?: string; category?: string }>
| 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<string, { variant?: string; category?: string }>
| 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)
}
+6 -5
View File
@@ -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.`
}
@@ -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)],