doc-sync
The doc-sync skill updater analyzes git diffs to identify changed files within a project, selectively regenerates affected skill documentation using Claude or Codex, and publishes transformed outputs across all configured deployment targets. Use this command to automatically keep skill definitions synchronized with code changes via incremental updates or full refresh modes, optionally triggering through git post-commit hooks for continuous documentation maintenance.
git clone --depth 1 https://github.com/aspenkit/aspens /tmp/doc-sync && cp -r /tmp/doc-sync/.claude/skills/doc-sync ~/.claude/skills/doc-syncskill.md
You are working on **doc-sync**, the incremental skill update command (`aspens doc sync`).
## Key Concepts
- **Monorepo-aware:** `getGitRoot(repoPath)` resolves the actual git root. `projectPrefix` (`toGitRelative`) computes the subdirectory offset. `scopeProjectFiles()` filters changed files to the project subdirectory. Diffs are fetched from `gitRoot` but file paths are project-relative.
- **Multi-target publish:** `configuredTargets()` reads `.aspens.json` for all configured targets. `chooseSyncSourceTarget()` picks the best source (prefers Claude if both exist). LLM generates for the source target; `publishFilesForTargets()` transforms output for all other configured targets. `graphSerialized` and `repoPath` are passed through to the transform context for conditional architecture references and disk-based instructions file loading.
- **Backend routing:** `runLLM()` from `runner.js` dispatches to `runClaude()` or `runCodex()` based on `config.backend` (defaults to source target's id).
- **Diff-based flow:** Gets `git diff HEAD~N..HEAD` from git root, scopes changed files to project prefix, then feeds diff plus existing skill contents and graph context to the selected backend.
- **Changetype filter (Phase 1):** `isNoOpDiff()` from `diff-classifier.js` skips the LLM call entirely on lockfile-only diffs and diffs touching zero code-bearing files. `LOCK_FILES` and `CODE_BEARING_EXTS` are the source of truth — extend them here, not at call sites.
- **Prompt path variables:** Passes `{ skillsDir, skillFilename, instructionsFile, configDir }` from source target to `loadPrompt()` for path substitution in prompts.
- **Refresh mode (`--refresh`):** Skips diff entirely. Reviews every skill against the current codebase. Base skill refreshed first, then domain skills in parallel batches of `PARALLEL_LIMIT` (3). Also refreshes instructions file and reports uncovered domains. Refresh mode runs `ensureRootKeyFilesSection` (legacy stripper) before `syncSkillsSection` so any leftover `## Key Files` blocks from old docs are removed.
- **Deterministic section repair:** `repairDeterministicSections()` runs a no-LLM pass that re-injects `## Skills` and `## Behavior` into the root instructions file from on-disk state and strips any legacy `## Key Files` block via `ensureRootKeyFilesSection`. Called from the no-op / "up to date" sync paths so missing-section drift is fixed every invocation. The normal sync flow also runs the same Skills + Behavior + legacy-strip block on the canonical instructions file after the LLM step.
- **Graph rebuild on every sync:** Calls `buildRepoGraph` + `persistGraphArtifacts` (with source target) to keep graph fresh. `graphSerialized` return value is captured and forwarded to `publishFilesForTargets` for conditional Codex architecture refs. Graph failure is non-fatal.
- **Legacy v0.7 hub-block cleanup:** `notifyLegacyHubBlockIfPresent()` surfaces a one-line notice on the first sync after upgrade when `CLAUDE.md`/`AGENTS.md` still carries the legacy `## Key Files` hub-counts block, so the diff that strips it isn't alarming. `regenerateStaleCodeMap()` force-rebuilds `.claude/code-map.md` on no-op syncs when it still carries the legacy `**Hub files**` block.
- **Graceful response handling:** After LLM returns, if output has content but no `<file>` tags, treats it as "no updates needed" with a verbose-only warning. The prompt explicitly requests an empty response when nothing needs updating.
- **Graph-aware skill mapping:** `mapChangesToSkills()` checks direct file matches via `fileMatchesActivation()` (from `skill-reader.js`) and also whether changed files are imported by files matching a skill's activation block.
- **Interactive file picker:** When diff exceeds 80k chars and TTY is available, offers multiselect with skill-relevant files pre-selected.
- **Prioritized diff:** `buildPrioritizedDiff()` gives skill-relevant files 60k char budget, everything else 20k (80k total). Cuts at `diff --git` boundaries.
- **Token optimization:** Affected skills sent in full; non-affected skills send only path + description line.
- **Split writes:** Direct-write files (`.claude/`, `CLAUDE.md`, root `AGENTS.md`) use `writeSkillFiles()`. Directory-scoped `AGENTS.md` files (e.g. `src/AGENTS.md`) use `writeTransformedFiles()`.
- **Skill-rules regeneration:** After writing, regenerates `skill-rules.json` via `extractRulesFromSkills()` — only for targets with `supportsHooks: true` (Claude). Uses `hookTarget` from publish targets list.
- **`findExistingSkills` is target-aware:** Uses `target.skillsDir` and `target.skillFilename` to locate skills for any target.
- **Git hook (monorepo-aware):** `installGitHook()` installs at the git root with per-project scoping. Hook uses `PROJECT_PATH` derived from project-relative offset. Each subproject gets its own labeled hook block (`# >>> aspens doc-sync hook (label) >>>`) with a unique function name (`__aspens_doc_sync_<slug>`). Multiple subprojects can coexist in one post-commit hook. Hook skips aspens-only commits scoped to the project prefix. 5-minute per-project cooldown via `/tmp/aspens-sync-<hash>.lock`; logs to `/tmp/aspens-sync-<hash>.log` (truncated to last 100 lines past 200). Unlabeled v0.6-era blocks are auto-upgraded on re-install.
- **Force writes:** doc-sync always calls `writeSkillFiles` with `force: true`.
## Critical Rules
- `runLLM` is called with `allowedTools: ['Read', 'Glob', 'Grep']` — doc-sync must never grant write tools.
- `parseOutput` restricts paths based on `getAllowedPaths([sourceTarget])` — paths outside the allowed set are silently dropped.
- **Unparseable output is a soft warning** — if LLM returns text without any `<file>` tags, doc-sync logs a verbose warning and treats it as "no updates needed" instead of throwing.
- `getGitDiff` gracefully falls back from N commits to 1 if fewer available. `actualCommits` tracks what was used.
- The command exits early with `CliError` if the source target's skills directory doesn't exist.LLM-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
Multi-target output system — target abstraction, backend routing, content transforms for Codex CLI and future targets
Context health analysis — freshness, domain coverage, hub surfacing, drift detection, LLM-powered interpretation, and auto-repair for generated agent context
Static import analysis that builds dependency graphs, domain clusters, hub files, git churn hotspots, and file priority rankings