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 fromgitRootbut file paths are project-relative. - Multi-target publish:
configuredTargets()reads.aspens.jsonfor 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.graphSerializedandrepoPathare passed through to the transform context for conditional architecture references and disk-based instructions file loading. - Backend routing:
runLLM()fromrunner.jsdispatches torunClaude()orrunCodex()based onconfig.backend(defaults to source target's id). - Diff-based flow: Gets
git diff HEAD~N..HEADfrom 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()fromdiff-classifier.jsskips the LLM call entirely on lockfile-only diffs and diffs touching zero code-bearing files.LOCK_FILESandCODE_BEARING_EXTSare the source of truth — extend them here, not at call sites. - Prompt path variables: Passes
{ skillsDir, skillFilename, instructionsFile, configDir }from source target toloadPrompt()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 ofPARALLEL_LIMIT(3). Also refreshes instructions file and reports uncovered domains. Refresh mode runsensureRootKeyFilesSectionbeforesyncSkillsSectionso the root file always carries a current Key Files block. - Deterministic section repair:
repairDeterministicSections()runs a no-LLM pass that re-injects## Skills,## Behavior, and## Key Filesinto the root instructions file from on-disk state. 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 + Key Files injection block on the canonical instructions file after the LLM step, so drift gets repaired whether or not the LLM produced an update. - Graph rebuild on every sync: Calls
buildRepoGraph+persistGraphArtifacts(with source target) to keep graph fresh.graphSerializedreturn value is captured and forwarded topublishFilesForTargetsfor 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 whenAGENTS.md/AGENTS.mdstill carries the legacy## Key Fileshub-counts block, so the diff that strips it isn't alarming.regenerateStaleCodeMap()force-rebuilds.claude/code-map.mdon 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 viafileMatchesActivation()(fromskill-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 atdiff --gitboundaries. - Token optimization: Affected skills sent in full; non-affected skills send only path + description line.
- Split writes: Direct-write files (
.claude/,AGENTS.md, rootAGENTS.md) usewriteSkillFiles(). Directory-scopedAGENTS.mdfiles (e.g.src/AGENTS.md) usewriteTransformedFiles(). - Skill-rules regeneration: After writing, regenerates
skill-rules.jsonviaextractRulesFromSkills()— only for targets withsupportsHooks: true(Claude). UseshookTargetfrom publish targets list. findExistingSkillsis target-aware: Usestarget.skillsDirandtarget.skillFilenameto locate skills for any target.- Git hook (monorepo-aware):
installGitHook()installs at the git root with per-project scoping. Hook usesPROJECT_PATHderived 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
writeSkillFileswithforce: true.
Critical Rules
runLLMis called withallowedTools: ['Read', 'Glob', 'Grep']— doc-sync must never grant write tools.parseOutputrestricts paths based ongetAllowedPaths([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. getGitDiffgracefully falls back from N commits to 1 if fewer available.actualCommitstracks what was used.- The command exits early with
CliErrorif the source target's skills directory doesn't exist. checkMissingHooks()inbin/cli.jsonly checks for Claude skills (not Codex — Codex doesn't use hooks).dedupeFiles()ensures no duplicate paths when publishing across multiple targets.- Git operations use
gitRoot— diffs, logs, and changed files are fetched from git root, notrepoPath. File paths are then scoped viaprojectPrefix. diff-classifier.jsis a leaf module —graph-builder.jsimportsLOCK_FILESfrom it; never import fromgraph-builderback into the classifier.
References
- Patterns:
src/lib/skill-reader.js—GENERIC_PATH_SEGMENTS,fileMatchesActivation(),getActivationBlock()
Last Updated: 2026-05-11