{"record":{"id":"79d07ee35ab7303a","repo":"conductor-oss/conductor","slug":"circular-skill-reference-detected-refname-is","errorCode":null,"errorMessage":"Circular skill reference detected: '${refName}' is already being normalized. Stack: ${stack}","messagePattern":"Circular skill reference detected: '(.+?)' is already being normalized\\. Stack: (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/normalizer/SkillNormalizer.java","lineNumber":253,"sourceCode":"\n            log.debug(\n                    \"Skill '{}': created read_skill_file tool with {} resources + {} sections\",\n                    name,\n                    resourceFiles.size(),\n                    skillSections.size());\n        }\n\n        // Step 7: Build per-run workspace tools\n        if (!workspaceRoots.isEmpty()) {\n            addWorkspaceTools(tools, name, workspaceRoots);\n        }\n\n        // Step 8: Wire cross-skill references\n        Set<String> stack = normalizingStack.get();\n        for (Map.Entry<String, Object> entry : crossSkillRefs.entrySet()) {\n            String refName = entry.getKey();\n            if (stack.contains(refName)) {\n                throw new IllegalArgumentException(\n                        \"Circular skill reference detected: '\"\n                                + refName\n                                + \"' is already being normalized. Stack: \"\n                                + stack);\n            }\n            stack.add(refName);\n            try {\n                Map<String, Object> refConfig = (Map<String, Object>) entry.getValue();\n                if (!workspaceRoots.isEmpty() && !refConfig.containsKey(\"workspace\")) {\n                    refConfig = new LinkedHashMap<>(refConfig);\n                    refConfig.put(\"workspace\", workspace);\n                }\n                AgentConfig refAgent = this.normalize(refConfig);\n\n                Map<String, Object> refToolConfig = new LinkedHashMap<>();\n                refToolConfig.put(\"agentConfig\", refAgent);\n\n                // inputSchema must include \"request\" property — same as sub-agent","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/normalizer/SkillNormalizer.java#L235-L271","documentation":"Thrown by SkillNormalizer during cross-skill reference wiring when a skill being normalized is already on the normalization stack — i.e., it references itself directly or transitively. The normalizer uses a ThreadLocal Set (normalizingStack) to track the current normalization chain and detect cycles. The error includes the full stack so the developer can trace the circular path.","triggerScenarios":"A skill (framework='skill') whose cross-skill references form a cycle: skill A references skill B which references skill A, or skill A references itself directly. The normalizer detects this when refName is already in the ThreadLocal normalizingStack during Step 8 (cross-skill reference wiring).","commonSituations":"Two skills that reference each other for composition (A includes B, B includes A), a skill that accidentally references itself (e.g., a 'utils' skill that lists itself as a dependency), or a longer transitive chain (A→B→C→A). Common when skills are auto-generated from a dependency graph that wasn't checked for cycles.","solutions":["Break the circular reference: identify the cycle from the error's stack trace and remove one link.","If skill A needs skill B's tools, make the dependency unidirectional (B should not reference A).","Extract shared functionality into a third skill that both A and B reference without creating a cycle.","For self-referencing skills, remove the skill's own name from its cross-skill references."],"exampleFix":"// before: circular A→B→A\n// skill A references skill B\n// skill B references skill A\n//\n// after: extract shared into skill C\n// skill A references skill C (no cycle)\n// skill B references skill C (no cycle)\n// skill C has no cross-skill references","handlingStrategy":"validation","validationCode":"// Pre-check for cycles in the skill reference graph before normalizing\nboolean hasCycle(Map<String, Set<String>> graph, String start) {\n    Set<String> visited = new HashSet<>();\n    Set<String> stack = new HashSet<>();\n    return dfs(graph, start, visited, stack);\n}\n\nboolean dfs(Map<String, Set<String>> graph, String node,\n            Set<String> visited, Set<String> stack) {\n    if (stack.contains(node)) return true;  // cycle\n    if (visited.contains(node)) return false;\n    visited.add(node);\n    stack.add(node);\n    for (String dep : graph.getOrDefault(node, Set.of())) {\n        if (dfs(graph, dep, visited, stack)) return true;\n    }\n    stack.remove(node);\n    return false;\n}","typeGuard":"static boolean isAcyclic(Map<String, Set<String>> skillGraph) {\n    for (String node : skillGraph.keySet()) {\n        if (hasCycle(skillGraph, node)) return false;\n    }\n    return true;\n}","tryCatchPattern":"try {\n    AgentConfig normalized = skillNormalizer.normalize(rawConfig);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"Circular skill reference\")) {\n        // parse the stack from the message, find the cycle, break one link\n    }\n    throw e;\n}","preventionTips":["Maintain a dependency graph of skills and check for cycles before normalization.","Skills should form a DAG — if A depends on B, B must not depend on A.","Log cross-skill references during skill construction to catch cycles early.","Run a topological sort on skills to detect cycles at design time."],"tags":["skill-normalizer","circular-reference","cycle-detection","agentspan"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}