You are working on multi-target output support — the system that lets aspens generate documentation for Claude Code, Codex CLI, or both simultaneously.
Key Concepts
- Target vs Backend: Target = where output goes (claude →
.claude/skills/, codex →.agents/skills/+ directory-scopedAGENTS.md). Backend = which LLM CLI generates the content (claude -porcodex exec). - Target definitions:
TARGETS.claude(centralized) andTARGETS.codex(directory-scoped). Each defines paths and capability flags:supportsHooks,supportsSettings,supportsGraph,supportsSkills,needsActivationSection,needsCodeMapEmbed,supportsMCP. Codex also hasmaxInstructionsBytes(32 KiB) anduserSkillsDir. Codex'sneedsCodeMapEmbedisfalse— condensed cluster/framework data goes into the synthetic.agents/skills/architecture/skill instead of the root AGENTS.md. - Canonical generation: Generation always produces Claude-canonical format first. Prompts always receive
CANONICAL_VARS(hardcoded Claude paths fromdoc-init.js). Transforms run after generation to produce other target formats. - Content transform:
transformForTarget()remaps paths and content. For Codex: base skill → rootAGENTS.md, domain skills → both.agents/skills/{domain}/SKILL.mdand source directoryAGENTS.md.generateCodexSkillReferences()creates.agents/skills/architecture/with code-map data. - Skills section completeness:
collectSkillsForList()(internal) reads every skill from disk undersourceTarget.skillsDirand overlays pending in-flight changes (filespassed to the transform) so the root instructions file's## Skillssection always lists every on-disk skill — not just the subset that changed in this sync. Pending changes win for descriptions; on-disk content survives for unchanged skills. - Instructions file disk fallback:
transformToDirectoryScopedloadsinstructionsFilefrom disk viarepoPathcontext parameter when it's not in the canonical files array (e.g., duringdoc init --strategy skip-existingor incrementaldoc sync). Uses a singlereadFileSyncfromfswrapped in try/catch (no separateexistsSynccheck). - Content sanitization:
sanitizeCodexInstructions()andsanitizeCodexSkill()strip Claude-specific references (hooks, skill-rules.json, Claude Code mentions) from Codex output. sanitizePublishedContent(content, filePath)— Single-chokepoint sanitizer invoked byskill-writer.json every disk write. Always strips## Activationblocks and## Key Filesblocks. Outsidecode-map.md, also strips count-bearing blocks:**Hub files…**,**Domain clusters:**,**High-churn hotspots:**,**Framework entry points…**. Defense in depth — upstream leaks can't reach the user.- Skills-variant stripping:
syncSkillsSection()removes LLM-emitted Skill-section variants (## Skills Reference,## Skills Overview, etc.) before injecting the canonical## Skillslist. Doc-init and doc-sync prompts also forbid such headings. ensureRootKeyFilesSection(content, graphSerialized)— Post-processes root instructions file to guarantee a## Key Filessection with top hub files from the graph.mergeConfiguredTargets(existing, next)— Merges target arrays to avoid dropping previously configured targets during narrower runs. Validates againstTARGETSkeys, deduplicates.getAllowedPaths(targets)— Returns{ dirPrefixes, exactFiles }union across all active targets.- Backend detection:
detectAvailableBackends()checks ifclaudeandcodexCLIs are installed.resolveBackend()picks best match: explicit flag > target match > fallback. - Config persistence:
.aspens.jsonat repo root stores{ targets, backend, version, saveTokens? }.readConfig()returnsnullif missing or if the config is structurally invalid.isValidConfig()validates targets, backend, version, andsaveTokens(viaisValidSaveTokensConfig()). loadConfig(repoPath, { persist })— Reads.aspens.jsonand, if missing, recovers viainferConfig()from on-disk artifacts. Returns{ config, recovered }. Persists inferred config to disk by default unlesspersist: falseis passed.- Feature config (
saveTokens): Optional object in.aspens.jsonvalidated byisValidSaveTokensConfig()— checksenabled(boolean),warnAtTokens/compactAtTokens(positive integers, compact > warn unless either isMAX_SAFE_INTEGER),saveHandoff/sessionRotation(booleans), optionalclaude/codexsub-objects withenabledandmode. writeConfigpreserves feature config:writeConfig()reads existing config and merges —saveTokenspreserved unless explicitly set tonull(intentional removal) orundefined(keep existing). Targets and backend also merge with existing.- Multi-target publish:
doc-syncusespublishFilesForTargets()to generate output for all configured targets from a single LLM run.repoPathis passed through to the transform context. - Codex inference tightened:
inferConfig()only adds'codex'to inferred targets when.codex/config dir or.agents/skills/dir exists. - Conditional architecture ref: Codex
buildCodexSkillRefs()only includes the architecture skill reference when a graph was actually serialized. - Architecture skill is codex-only synthetic: The codex
architectureskill is generated from graph data and has no Claude counterpart by design.logicalKeyForFile()returnsnullfor codexarchitecturepaths soassertTargetParity()won't raise a parity violation for the missing Claude side.
Critical Rules
- Generation always targets Claude canonical format first — transforms run after, never during. Prompts always receive
CANONICAL_VARS. - Split write logic:
writeSkillFiles()handles direct-write files.writeTransformedFiles()handles directory-scopedAGENTS.mdwith an explicit path allowlist and warn-and-skip policy. Both writers run their payloads throughsanitizePublishedContentbefore touching disk. - Path safety:
validateTransformedFiles()rejects absolute paths, traversal, and unexpected filenames.writeTransformedFiles()enforces the same checks. - Codex-only restrictions:
add agent/command/hookandcustomize agentsthrowCliErrorfor Codex-only repos.add skillworks for both targets. - Graph/hooks are Claude-only —
persistGraphArtifacts()returns data without writing files whentarget.supportsGraph === false. Hook installation skipped whensupportsHooks === false. - Config validation is defensive —
readConfig()treats malformed but parseable JSON (e.g., wrong types fortargets/backend/version/saveTokens) as invalid and returnsnull, same as missing config. repoPathcontext is required for disk fallback — callers oftransformForTargetmust passrepoPathin the context object forinstructionsFileto load from disk when not in canonical files, and forcollectSkillsForListto enumerate on-disk skills.
References
- Patterns: See
src/lib/target.jsfor all target property definitions
Last Updated: 2026-05-11