{"record":{"id":"a2ff79b848b5943e","repo":"conductor-oss/conductor","slug":"circular-skill-reference-detected-key","errorCode":null,"errorMessage":"Circular skill reference detected: {key}","messagePattern":"Circular skill reference detected: (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/service/SkillRegistryService.java","lineNumber":690,"sourceCode":"\n    private Map<String, Object> mergeMetadata(\n            Map<String, Object> packageMetadata, Map<String, Object> manifestMetadata) {\n        if (packageMetadata.isEmpty()) {\n            return manifestMetadata;\n        }\n        if (manifestMetadata.isEmpty()) {\n            return packageMetadata;\n        }\n        Map<String, Object> merged = new LinkedHashMap<>(packageMetadata);\n        merged.putAll(manifestMetadata);\n        return merged;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private Map<String, Object> rawConfigForDetail(SkillDetail detail, Set<String> stack) {\n        String key = detail.getName() + \"@\" + detail.getVersion();\n        if (!stack.add(key)) {\n            throw new IllegalArgumentException(\"Circular skill reference detected: \" + key);\n        }\n        try {\n            Map<String, Object> rawConfig = deepCopy(detail.getRawConfig());\n            rawConfig.put(\n                    \"skillRef\",\n                    Map.of(\n                            \"name\", detail.getName(),\n                            \"version\", detail.getVersion(),\n                            \"checksum\", detail.getChecksum()));\n            Map<String, Object> pinnedRefs = toMap(rawConfig.get(\"crossSkillRefs\"));\n            if (!pinnedRefs.isEmpty()) {\n                rawConfig.put(\"crossSkillRefs\", hydratePinnedCrossSkills(pinnedRefs, stack));\n            } else if (!Boolean.TRUE.equals(rawConfig.get(\"crossSkillRefsPinned\"))) {\n                Object skillMd = rawConfig.get(\"skillMd\");\n                if (skillMd instanceof String md) {\n                    rawConfig.put(\"crossSkillRefs\", resolveRegisteredCrossSkills(md, stack));\n                }\n            }","sourceCodeStart":672,"sourceCodeEnd":708,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/agentspan/src/main/java/org/conductoross/conductor/ai/agentspan/runtime/service/SkillRegistryService.java#L672-L708","documentation":"Thrown by rawConfigForDetail when hydrating cross-skill references would recurse into a skill already on the current resolution stack — i.e. skill A's crossSkillRefs reference B, and B's reference A (directly or transitively). A `Set<String>` of `name@version` keys is threaded through the recursion; a duplicate add returns false and aborts. This prevents infinite recursion / stack overflow when building a SkillDetail tree.","triggerScenarios":"Two or more skill manifests declare pinned crossSkillRefs that form a cycle: A->B->A, or A->B->C->A. Calling any API that materializes a SkillDetail with hydrated refs (list-with-details, get detail) walks the graph and trips the guard.","commonSituations":"Author copy-pasted a manifest and forgot to update the ref list; two skills legitimately co-operate and were published referencing each other; a ref was bumped but the cycle was not noticed because individual publishes succeed.","solutions":["Inspect the `name@version` in the message — that is the skill re-entering its own resolution chain.","Open each manifest's `crossSkillRefs` and break the cycle: make the dependency one-directional, or move shared logic into a third skill both reference.","Re-publish the corrected skill(s); the registry does not auto-heal cycles."],"exampleFix":"// before: skill-a references skill-b, skill-b references skill-a\ncrossSkillRefs:\n  skill-b: \"1.0.0\"   # in skill-a manifest\n# in skill-b manifest\ncrossSkillRefs:\n  skill-a: \"1.0.0\"   # REMOVE — forms cycle\n// after: only skill-a references skill-b; skill-b references a shared helper\ncrossSkillRefs:\n  skill-helpers: \"1.0.0\"","handlingStrategy":"validation","validationCode":"// Detect cross-skill reference cycles before publishing.\nboolean hasCycle(Map<String, Set<String>> graph, String start) {\n    Set<String> visited = new HashSet<>(), stack = new HashSet<>();\n    java.util.function.BiPredicate<String, String> dfs = null;\n    dfs = (node, from) -> {\n        if (stack.contains(node)) return true;\n        if (!visited.add(node)) return false;\n        stack.add(node);\n        for (String next : graph.getOrDefault(node, Set.of())) if (dfs.test(next, node)) return true;\n        stack.remove(node);\n        return false;\n    };\n    return dfs.test(start, start);\n}","typeGuard":"// Build a name@version -> set of referenced name@version map from manifests, then run hasCycle above before publish.","tryCatchPattern":"try {\n    return skillRegistry.getDetail(name, version);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"Circular skill reference\")) {\n        return conflict(e.getMessage()); // 409, author must break the cycle\n    }\n    throw e;\n}","preventionTips":["Keep cross-skill dependencies acyclic — factor shared logic into a leaf skill.","Run a cycle check in CI over all published manifests.","When bumping a skill, audit its crossSkillRefs for new cycles."],"tags":["skill-registry","cross-skill-ref","cycle","dependency-graph"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}