diff --git a/.agents/skills/architecture/references/code-map.md b/.agents/skills/architecture/references/code-map.md index 5e68dea..c2fbb48 100644 --- a/.agents/skills/architecture/references/code-map.md +++ b/.agents/skills/architecture/references/code-map.md @@ -13,10 +13,10 @@ | Domain | Files | Top entries | |--------|-------|-------------| -| src | 44 | `src/commands/doc-init.js`, `src/lib/runner.js`, `src/lib/target.js` | +| src | 45 | `src/commands/doc-init.js`, `src/lib/runner.js`, `src/lib/target.js` | **High-churn hotspots:** -- `src/commands/doc-init.js` - 34 changes -- `src/commands/doc-sync.js` - 20 changes +- `src/commands/doc-init.js` - 35 changes +- `src/commands/doc-sync.js` - 21 changes - `src/lib/runner.js` - 17 changes diff --git a/.agents/skills/codex-support/SKILL.md b/.agents/skills/codex-support/SKILL.md index 0c19f40..7ba45da 100644 --- a/.agents/skills/codex-support/SKILL.md +++ b/.agents/skills/codex-support/SKILL.md @@ -29,6 +29,7 @@ You are working on **multi-target output support** — the system that lets aspe - **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`. - **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. +- **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 `existsSync`/`readFileSync` from `fs`. - **Content sanitization:** `sanitizeCodexInstructions()` and `sanitizeCodexSkill()` strip Claude-specific references (hooks, skill-rules.json, Claude Code mentions) from Codex output. - **`ensureRootKeyFilesSection(content, graphSerialized)`** — Post-processes root instructions file to guarantee a `## Key Files` section with top hub files from the graph. - **`mergeConfiguredTargets(existing, next)`** — Merges target arrays to avoid dropping previously configured targets during narrower runs. Validates against `TARGETS` keys, deduplicates. @@ -37,7 +38,7 @@ You are working on **multi-target output support** — the system that lets aspe - **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()`). - **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. +- **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. @@ -48,9 +49,10 @@ You are working on **multi-target output support** — the system that lets aspe - **Codex-only restrictions:** `add agent/command/hook` and `customize agents` throw `CliError` for Codex-only repos. `add skill` works for both targets. - **Graph/hooks are Claude-only** — `persistGraphArtifacts()` returns data without writing files when `target.supportsGraph === false`. Hook installation skipped when `supportsHooks === false`. - **Config validation is defensive** — `readConfig()` treats malformed but parseable JSON (e.g., wrong types for `targets`/`backend`/`version`/`saveTokens`) as invalid and returns `null`, same as missing config. +- **`repoPath` context is required for disk fallback** — callers of `transformForTarget` must pass `repoPath` in the context object for `instructionsFile` to load from disk when not in canonical files. ## References - **Patterns:** See `src/lib/target.js` for all target property definitions --- -**Last Updated:** 2026-04-09 +**Last Updated:** 2026-04-25 diff --git a/.agents/skills/doc-sync/SKILL.md b/.agents/skills/doc-sync/SKILL.md index 0b22073..9a4f04e 100644 --- a/.agents/skills/doc-sync/SKILL.md +++ b/.agents/skills/doc-sync/SKILL.md @@ -33,13 +33,13 @@ You are working on **doc-sync**, the incremental skill update command (`aspens d ## 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` is passed through to control conditional architecture references. +- **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. - **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. - **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. -- **Unparseable response detection:** After LLM returns, if output has content but no `` tags at all, throws `CliError` instead of silently treating it as "no updates needed". +- **Graceful response handling:** After LLM returns, if output has content but no `` 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. @@ -53,7 +53,7 @@ You are working on **doc-sync**, the incremental skill update command (`aspens d ## 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 an error** — if LLM returns text without any `` tags, doc-sync throws `CliError` rather than silently proceeding with zero files. +- **Unparseable output is a soft warning** — if LLM returns text without any `` 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. - `checkMissingHooks()` in `bin/cli.js` only checks for Claude skills (not Codex — Codex doesn't use hooks). @@ -64,4 +64,4 @@ You are working on **doc-sync**, the incremental skill update command (`aspens d - **Patterns:** `src/lib/skill-reader.js` — `GENERIC_PATH_SEGMENTS`, `fileMatchesActivation()`, `getActivationBlock()` --- -**Last Updated:** 2026-04-08 +**Last Updated:** 2026-04-25 diff --git a/.claude/skills/codex-support/skill.md b/.claude/skills/codex-support/skill.md index 0c19f40..7ba45da 100644 --- a/.claude/skills/codex-support/skill.md +++ b/.claude/skills/codex-support/skill.md @@ -29,6 +29,7 @@ You are working on **multi-target output support** — the system that lets aspe - **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`. - **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. +- **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 `existsSync`/`readFileSync` from `fs`. - **Content sanitization:** `sanitizeCodexInstructions()` and `sanitizeCodexSkill()` strip Claude-specific references (hooks, skill-rules.json, Claude Code mentions) from Codex output. - **`ensureRootKeyFilesSection(content, graphSerialized)`** — Post-processes root instructions file to guarantee a `## Key Files` section with top hub files from the graph. - **`mergeConfiguredTargets(existing, next)`** — Merges target arrays to avoid dropping previously configured targets during narrower runs. Validates against `TARGETS` keys, deduplicates. @@ -37,7 +38,7 @@ You are working on **multi-target output support** — the system that lets aspe - **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()`). - **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. +- **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. @@ -48,9 +49,10 @@ You are working on **multi-target output support** — the system that lets aspe - **Codex-only restrictions:** `add agent/command/hook` and `customize agents` throw `CliError` for Codex-only repos. `add skill` works for both targets. - **Graph/hooks are Claude-only** — `persistGraphArtifacts()` returns data without writing files when `target.supportsGraph === false`. Hook installation skipped when `supportsHooks === false`. - **Config validation is defensive** — `readConfig()` treats malformed but parseable JSON (e.g., wrong types for `targets`/`backend`/`version`/`saveTokens`) as invalid and returns `null`, same as missing config. +- **`repoPath` context is required for disk fallback** — callers of `transformForTarget` must pass `repoPath` in the context object for `instructionsFile` to load from disk when not in canonical files. ## References - **Patterns:** See `src/lib/target.js` for all target property definitions --- -**Last Updated:** 2026-04-09 +**Last Updated:** 2026-04-25 diff --git a/.claude/skills/doc-sync/skill.md b/.claude/skills/doc-sync/skill.md index 6ab1b4f..0242bc8 100644 --- a/.claude/skills/doc-sync/skill.md +++ b/.claude/skills/doc-sync/skill.md @@ -33,13 +33,13 @@ You are working on **doc-sync**, the incremental skill update command (`aspens d ## 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` is passed through to control conditional architecture references. +- **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. - **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. - **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. -- **Unparseable response detection:** After LLM returns, if output has content but no `` tags at all, throws `CliError` instead of silently treating it as "no updates needed". +- **Graceful response handling:** After LLM returns, if output has content but no `` 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. @@ -53,7 +53,7 @@ You are working on **doc-sync**, the incremental skill update command (`aspens d ## 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 an error** — if LLM returns text without any `` tags, doc-sync throws `CliError` rather than silently proceeding with zero files. +- **Unparseable output is a soft warning** — if LLM returns text without any `` 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. - `checkMissingHooks()` in `bin/cli.js` only checks for Claude skills (not Codex — Codex doesn't use hooks). @@ -64,4 +64,4 @@ You are working on **doc-sync**, the incremental skill update command (`aspens d - **Patterns:** `src/lib/skill-reader.js` — `GENERIC_PATH_SEGMENTS`, `fileMatchesActivation()`, `getActivationBlock()` --- -**Last Updated:** 2026-04-08 +**Last Updated:** 2026-04-25 diff --git a/AGENTS.md b/AGENTS.md index 145a909..fe6fde2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,41 @@ +# aspens + +## Skills + +- `.agents/skills/doc-sync/SKILL.md` — Incremental skill updater that maps git diffs to affected skills and optionally auto-syncs via a post-commit hook +- `.agents/skills/codex-support/SKILL.md` — Multi-target output system — target abstraction, backend routing, content transforms for Codex CLI and future targets +- `.agents/skills/architecture/SKILL.md` — Import graph and code-map reference for structural changes. + +## Commands + +- `npm test` — run Vitest (`vitest run`) +- `npm start` — run the CLI (`node bin/cli.js`) +- `npm run lint` — no-op check (`echo 'No linter configured yet' && exit 0`) +- `aspens scan [path]` — deterministic repo scan +- `aspens doc init [path]` — generate skills, hooks, and instructions file (`--target claude|codex|all`, `--recommended` for full recommended setup including save-tokens, agents, and doc-sync hook) +- `aspens doc impact [path]` — show freshness, coverage, drift, and LLM interpretation of generated context (interactive apply for repairs) +- `aspens doc sync [path]` — update docs from recent diffs +- `aspens doc graph [path]` — rebuild `.agents/skills/architecture/references/code-map.md` +- `aspens add [name]` — install bundled templates +- `aspens save-tokens [path]` — install token-saving session settings (`--recommended`, `--remove`) + +## Release + +- Release workflow: `/Users/MV/aspenkit/dev/release.md` + +## Conventions + +- ESM only: use `import`/`export`; never `require()`. +- Prefer `CliError` from command handlers; top-level handling lives in `bin/cli.js`. +- `es-module-lexer` must be initialized before `parse()`. +- Keep target/backend semantics straight: target is output format/location; backend is the generating CLI. Persist config in `.aspens.json`. +- Do not duplicate base-skill guidance here; consult `.agents/skills/base/SKILL.md` for deeper repo context. + +## Behavior + +- **Verify before claiming** — Never state that something is configured, running, scheduled, or complete without confirming it first. If you haven't verified it in this session, say so rather than assuming. +- **Make sure code is running** — If you suggest code changes, ensure the code is running and tested before claiming the task is done. + ## Key Files **Hub files (most depended-on):** @@ -11,10 +49,10 @@ | Domain | Files | Top entries | |--------|-------|-------------| -| src | 44 | `src/commands/doc-init.js`, `src/lib/runner.js`, `src/lib/target.js` | +| src | 45 | `src/commands/doc-init.js`, `src/lib/runner.js`, `src/lib/target.js` | **High-churn hotspots:** -- `src/commands/doc-init.js` - 34 changes -- `src/commands/doc-sync.js` - 20 changes +- `src/commands/doc-init.js` - 35 changes +- `src/commands/doc-sync.js` - 21 changes - `src/lib/runner.js` - 17 changes diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb02f3..bd7aa90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## [Unreleased] +## [0.7.3] - 2026-04-25 + +### Fixed +- **`doc sync` crash on unstructured LLM response** — when the LLM responds with explanatory text instead of `` tags, `doc sync` now treats it as "no updates needed" instead of throwing. Also tightened the prompt to request an empty response when nothing needs updating. +- **Codex `AGENTS.md` missing most content** — the codex transform produced only hub files and domain clusters (~17 lines) instead of the full instructions derived from `CLAUDE.md`. The transform now loads `CLAUDE.md` from disk when it's not in the canonical files (e.g., during `doc init --strategy skip-existing` or incremental `doc sync`). + ## [0.7.2] - 2026-04-16 diff --git a/README.md b/README.md index 58fa0ac..dc60549 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ **Your CLAUDE.md stopped working. Here's why.** [![npm version](https://img.shields.io/npm/v/aspens.svg)](https://www.npmjs.com/package/aspens) -[![npm downloads](https://img.shields.io/npm/dm/aspens.svg)](https://www.npmjs.com/package/aspens) +[![npm downloads](https://img.shields.io/npm/dy/aspens.svg)](https://www.npmjs.com/package/aspens) [![GitHub stars](https://img.shields.io/github/stars/aspenkit/aspens)](https://github.com/aspenkit/aspens) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -19,14 +19,28 @@ aspens replaces the monolith with scoped skill files (~35 lines each) generated Works with Claude Code, Codex, or both. +## Install + ```bash -npx aspens doc init --recommended +npm install -g aspens +``` + +Then in your project: + +```bash +aspens doc init --recommended ``` -Then verify what it generated: +Verify what it generated: ```bash -npx aspens doc impact +aspens doc impact +``` + +Or run without installing: + +```bash +npx aspens doc init --recommended ``` ![aspens demo](demo/demo-full.gif) diff --git a/package-lock.json b/package-lock.json index 3156e16..0096ad1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aspens", - "version": "0.7.2", + "version": "0.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aspens", - "version": "0.7.2", + "version": "0.7.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6782f18..e20a11a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aspens", - "version": "0.7.2", + "version": "0.7.3", "description": "Keep coding-agent context accurate as your codebase changes", "type": "module", "bin": { diff --git a/src/commands/doc-init.js b/src/commands/doc-init.js index c796e72..1e3a618 100644 --- a/src/commands/doc-init.js +++ b/src/commands/doc-init.js @@ -137,7 +137,7 @@ function validateGeneratedChunk(files, repoPath) { return files; } -function buildOutputFilesForTargets(canonicalFiles, targets, scan, graphSerialized) { +function buildOutputFilesForTargets(canonicalFiles, targets, scan, graphSerialized, repoPath) { let outputFiles = [...canonicalFiles]; const nonClaudeTargets = targets.filter(target => target.id !== 'claude'); @@ -146,6 +146,7 @@ function buildOutputFilesForTargets(canonicalFiles, targets, scan, graphSerializ const transformed = transformForTarget(canonicalFiles, TARGETS.claude, target, { scanResult: scan, graphSerialized, + repoPath, }); const transformValidation = validateTransformedFiles(transformed); @@ -716,7 +717,7 @@ export async function docInitCommand(path, options) { // For --target all: keep canonical + add transformed for each non-Claude target. const canonicalFiles = [...allFiles]; // preserve originals if (!shouldWriteIncrementally) { - allFiles = buildOutputFilesForTargets(canonicalFiles, targets, scan, graphSerialized); + allFiles = buildOutputFilesForTargets(canonicalFiles, targets, scan, graphSerialized, repoPath); } let results = []; @@ -1546,7 +1547,7 @@ async function generateChunked(repoPath, scan, repoGraph, domains, baseOnly, tim if (writeIncrementally && files.length > 0) { writeIncrementalOutputs( repoPath, - buildOutputFilesForTargets(allFiles, targets, scan, graphSerialized), + buildOutputFilesForTargets(allFiles, targets, scan, graphSerialized, repoPath), shouldForce, writeState ); @@ -1628,7 +1629,7 @@ async function generateChunked(repoPath, scan, repoGraph, domains, baseOnly, tim if (writeIncrementally) { writeIncrementalOutputs( repoPath, - buildOutputFilesForTargets(allFiles, targets, scan, graphSerialized), + buildOutputFilesForTargets(allFiles, targets, scan, graphSerialized, repoPath), shouldForce, writeState ); @@ -1711,7 +1712,7 @@ async function generateChunked(repoPath, scan, repoGraph, domains, baseOnly, tim if (writeIncrementally && files.length > 0) { writeIncrementalOutputs( repoPath, - buildOutputFilesForTargets(allFiles, targets, scan, graphSerialized), + buildOutputFilesForTargets(allFiles, targets, scan, graphSerialized, repoPath), shouldForce, writeState ); diff --git a/src/commands/doc-sync.js b/src/commands/doc-sync.js index f02a4c7..011b644 100644 --- a/src/commands/doc-sync.js +++ b/src/commands/doc-sync.js @@ -62,7 +62,7 @@ function chooseSyncSourceTarget(repoPath, targets) { return targets[0] || TARGETS.claude; } -function publishFilesForTargets(baseFiles, sourceTarget, publishTargets, scan, graphSerialized = null) { +function publishFilesForTargets(baseFiles, sourceTarget, publishTargets, scan, graphSerialized = null, repoPath = null) { const published = []; for (const target of publishTargets) { @@ -74,6 +74,7 @@ function publishFilesForTargets(baseFiles, sourceTarget, publishTargets, scan, g const transformed = transformForTarget(baseFiles, sourceTarget, target, { scanResult: scan, graphSerialized, + repoPath, }); published.push(...transformed); } @@ -308,12 +309,12 @@ ${truncate(instructionsContent, 5000)} // Step 6: Parse output const baseFiles = parseOutput(result.text, allowedPaths); - const hasFileTags = / 0 && !hasFileTags) { - syncSpinner.stop(pc.red('Unparseable response')); - throw new CliError('LLM returned content without tags. Aborting instead of treating it as "no updates needed".'); + if (baseFiles.length === 0 && result.text.trim().length > 0 && !/ tags — treating as no updates needed.'); + } } - const files = publishFilesForTargets(baseFiles, sourceTarget, publishTargets, scan, graphSerialized); + const files = publishFilesForTargets(baseFiles, sourceTarget, publishTargets, scan, graphSerialized, repoPath); if (files.length === 0) { syncSpinner.stop('No updates needed'); @@ -646,7 +647,7 @@ async function refreshAllSkills(repoPath, options, sourceTarget, publishTargets return; } - const filesToWrite = publishFilesForTargets(allUpdatedFiles, sourceTarget, publishTargets, scan, graphSerialized); + const filesToWrite = publishFilesForTargets(allUpdatedFiles, sourceTarget, publishTargets, scan, graphSerialized, repoPath); const directWriteFiles = filesToWrite.filter(f => !(f.path.endsWith('/AGENTS.md') && f.path !== 'AGENTS.md')); const dirScopedFiles = filesToWrite.filter(f => f.path.endsWith('/AGENTS.md') && f.path !== 'AGENTS.md'); const results = [ diff --git a/src/lib/target-transform.js b/src/lib/target-transform.js index e95045a..194ecef 100644 --- a/src/lib/target-transform.js +++ b/src/lib/target-transform.js @@ -6,6 +6,7 @@ */ import { join } from 'path'; +import { readFileSync } from 'fs'; export function transformForTarget(files, sourceTarget, destTarget, context) { if (sourceTarget.id === destTarget.id) return files; @@ -44,11 +45,19 @@ function remapCentralizedPath(filePath, sourceTarget, destTarget) { function transformToDirectoryScoped(files, sourceTarget, destTarget, context) { const scanResult = context?.scanResult; const graphSerialized = context?.graphSerialized; + const repoPath = context?.repoPath; const result = []; const baseSkillPrefix = sourceTarget.skillsDir + '/base/'; const baseSkill = files.find(file => file.path.startsWith(baseSkillPrefix)); - const instructionsFile = files.find(file => file.path === sourceTarget.instructionsFile); + let instructionsFile = files.find(file => file.path === sourceTarget.instructionsFile); + + if (!instructionsFile && repoPath && sourceTarget.instructionsFile) { + try { + const content = readFileSync(join(repoPath, sourceTarget.instructionsFile), 'utf8'); + instructionsFile = { path: sourceTarget.instructionsFile, content }; + } catch {} + } const domainSkills = files.filter(file => file !== baseSkill && file !== instructionsFile && diff --git a/src/prompts/doc-sync.md b/src/prompts/doc-sync.md index d1fc4f6..4235d38 100644 --- a/src/prompts/doc-sync.md +++ b/src/prompts/doc-sync.md @@ -22,7 +22,7 @@ Return ONLY the files that need updating, wrapped in XML tags: - Only output files that actually changed. If a skill doesn't need updates, don't include it. - Output the COMPLETE file content, not a diff or patch. The file will be written as-is. - Use `` and `` tags exactly as shown. -- If nothing needs updating (cosmetic changes, test-only changes, docs-only changes), output nothing. +- If nothing needs updating (cosmetic changes, test-only changes, docs-only changes), output an empty response — no text at all, not even an explanation. ## Rules