codex-support
Multi-target output system — target abstraction, backend routing, content transforms for Codex CLI and future targets
git clone --depth 1 https://github.com/aspenkit/aspens /tmp/codex-support && cp -r /tmp/codex-support/.claude/skills/codex-support ~/.claude/skills/codex-supportskill.md
---
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-scoped `AGENTS.md`). Backend = which LLM CLI generates the content (`claude -p` or `codex exec`).
- **Target definitions:** `TARGETS.claude` (centralized) and `TARGETS.codex` (directory-scoped). Each defines paths and capability flags: `supportsHooks`, `supportsSettings`, `supportsGraph`, `supportsSkills`, `needsActivationSection`, `needsCodeMapEmbed`, `supportsMCP`. Codex also has `maxInstructionsBytes` (32 KiB) and `userSkillsDir`. Codex's `needsCodeMapEmbed` is `false` — 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 from `doc-init.js`). Transforms run **after** generation to produce other target formats.
- **Content transform:** `transformForTarget()` remaps paths and content. For Codex: base skill → root `AGENTS.md`, domain skills → both `.agents/skills/{domain}/SKILL.md` and source directory `AGENTS.md`. `generateCodexSkillReferences()` creates `.agents/skills/architecture/` with code-map data.
- **Skills section completeness:** `collectSkillsForList()` (internal) reads every skill from disk under `sourceTarget.skillsDir` and overlays pending in-flight changes (`files` passed to the transform) so the root instructions file's `## Skills` section 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:** `transformToDirectoryScoped` loads `instructionsFile` from disk via `repoPath` context parameter when it's not in the canonical files array (e.g., during `doc init --strategy skip-existing` or incremental `doc sync`). Uses a single `readFileSync` from `fs` wrapped in try/catch (no separate `existsSync` check).
- **Content sanitization:** `sanitizeCodexInstructions()` and `sanitizeCodexSkill()` strip Claude-specific references (hooks, skill-rules.json, Claude Code mentions) from Codex output.
- **`sanitizePublishedContent(content, filePath)`** — Single-chokepoint sanitizer invoked by `skill-writer.js` on every disk write. Always strips `## Activation` blocks and `## Key Files` blocks. Outside `code-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 `## Skills` list. Doc-init and doc-sync prompts also forbid such headings.
- **`ensureRootKeyFilesSection(content)`** — Backwards-compat shim only. Strips legacy `## Key Files` hub blocks left in older docs and collapses extra blank lines. **Does not insert anything** — hub rankings live in `code-map.md` / `graph.json` exclusively.
- **`mergeConfiguredTargets(existing, next)`** — Merges target arrays to avoid dropping previously configured targets during narrower runs. Validates against `TARGETS` keys, deduplicates.
- **`getAllowedPaths(targets)`** — Returns `{ dirPrefixes, exactFiles }` union across all active targets.
- **Backend detection:** `detectAvailableBackends()` checks if `claude` and `codex` CLIs are installed. `resolveBackend()` picks best match: explicit flag > target match > fallback.
- **Config persistence:** `.aspens.json` at repo root stores `{ targets, backend, version, saveTokens? }`. `readConfig()` returns `null` if missing **or if the config is structurally invalid**. `isValidConfig()` validates targets, backend, version, and `saveTokens` (via `isValidSaveTokensConfig()`).
- **`loadConfig(repoPath, { persist })`** — Reads `.aspens.json` and, if missing, recovers via `inferConfig()` from on-disk artifacts. Returns `{ config, recovered }`. Persists inferred config to disk by default unless `persist: false` is passed.
- **Feature config (`saveTokens`):** Optional object in `.aspens.json` validated by `isValidSaveTokensConfig()` — checks `enabled` (boolean), `warnAtTokens`/`compactAtTokens` (positive integers, compact > warn unless either is `MAX_SAFE_INTEGER`), `saveHandoff`/`sessionRotation` (booleans), optional `claude`/`codex` sub-objects with `enabled` and `mode`.
- **`writeConfig` preserves feature config:** `writeConfig()` reads existing config and merges — `saveTokens` preserved unless explicitly set to `null` (intentional removal) or `undefined` (keep existing). Targets and backend also merge with existing.
- **Multi-target publish:** `doc-sync` uses `publishFilesForTargets()` to generate output for all configured targets from a single LLM run. `repoPath` is 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 `architecture` skill is generated from graph data and has no Claude counterpart by design. `logicalKeyForFile()` returns `null` for codex `architecture` paths so `assertTargetParity()` 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. `writeTransfoLLM-powered injection of project context into installed agent templates via `aspens customize agents`
>
Core conventions, tech stack, and project structure for aspens
Claude/Codex CLI execution layer — prompt loading, stream-json parsing, file output extraction, path sanitization, skill file writing, and skill rule generation
Top-level Commander wiring, welcome screen, missing-hook warning, CliError exit handling, and the public programmatic API surface
Context health analysis — freshness, domain coverage, hub surfacing, drift detection, LLM-powered interpretation, and auto-repair for generated agent context
Incremental skill updater that maps git diffs to affected skills and optionally auto-syncs via a post-commit hook
Static import analysis that builds dependency graphs, domain clusters, hub files, git churn hotspots, and file priority rankings