Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions .claude/skills/base/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ Node.js (ESM) | Commander | Vitest | es-module-lexer | @clack/prompts | picocolo
- `npm test` — Run vitest suite
- `npm start` / `node bin/cli.js` — Run CLI
- `aspens scan [path]` — Deterministic repo analysis (no LLM)
- `aspens doc init [path]` — Generate skills via Claude CLI
- `aspens doc init [path]` — Generate skills + hooks + CLAUDE.md
- `aspens doc sync [path]` — Incremental skill updates from git diffs
- `aspens doc graph [path]` — Rebuild import graph cache (`.claude/graph.json`)
- `aspens add <type> [name]` — Install templates (agents, commands, hooks)
- `aspens customize agents` — Inject project context into installed agents

Expand All @@ -28,11 +29,14 @@ CLI entry (`bin/cli.js`) → command handlers (`src/commands/`) → lib modules

- `src/lib/scanner.js` — Deterministic repo scanner (languages, frameworks, domains, structure)
- `src/lib/graph-builder.js` — Static import analysis via es-module-lexer (hub files, clusters, priority)
- `src/lib/graph-persistence.js` — Graph serialization, subgraph extraction, code-map + index generation
- `src/lib/runner.js` — Claude CLI wrapper (`claude -p --output-format stream-json`)
- `src/lib/context-builder.js` — Assembles repo files into prompt-friendly context
- `src/lib/skill-writer.js` — Writes parsed `<file>` output to disk
- `src/lib/skill-writer.js` — Writes skill files, generates skill-rules.json, merges settings
- `src/lib/skill-reader.js` — Parses skill files, frontmatter, activation patterns, keywords
- `src/lib/errors.js` — `CliError` class (structured errors caught by CLI top-level handler)
- `src/prompts/` — Prompt templates with `{{partial}}` and `{{variable}}` substitution
- `src/templates/` — Bundled agents, commands, and hooks for `aspens add`
- `src/templates/` — Bundled agents, commands, hooks, and settings for `aspens add` / `doc init`

## Critical Conventions
- **Pure ESM** — `"type": "module"` throughout; use `import`/`export`, never `require()`
Expand All @@ -41,14 +45,15 @@ CLI entry (`bin/cli.js`) → command handlers (`src/commands/`) → lib modules
- **Path sanitization** — `parseFileOutput()` restricts writes to `.claude/` and `CLAUDE.md` only; no absolute paths or `..` traversal
- **Prompt partials** — `{{name}}` in prompt files resolves to `src/prompts/partials/name.md` first, then falls back to template variables
- **Scanner is deterministic** — no LLM calls; pure filesystem analysis
- **CliError pattern** — command handlers throw `CliError` instead of calling `process.exit()`; caught at top level in `bin/cli.js`

## Structure
- `bin/` — CLI entry point (commander setup)
- `src/commands/` — Command handlers (scan, doc-init, doc-sync, add, customize)
- `bin/` — CLI entry point (commander setup, CliError handler)
- `src/commands/` — Command handlers (scan, doc-init, doc-sync, doc-graph, add, customize)
- `src/lib/` — Core library modules
- `src/prompts/` — Prompt templates + partials
- `src/templates/` — Installable agents, commands, hooks
- `src/templates/` — Installable agents, commands, hooks, settings
- `tests/` — Vitest test files

---
**Last Updated:** 2026-03-21
**Last Updated:** 2026-03-24
27 changes: 18 additions & 9 deletions .claude/skills/claude-runner/skill.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
---
name: claude-runner
description: Claude CLI execution layer — prompt loading, stream-json parsing, file output extraction, path sanitization, and skill file writing
description: Claude CLI execution layer — prompt loading, stream-json parsing, file output extraction, path sanitization, skill file writing, and skill rule generation
---

## Activation

This skill triggers when editing claude-runner files:
- `src/lib/runner.js`
- `src/lib/skill-writer.js`
- `src/lib/skill-reader.js`
- `src/prompts/**/*.md`
- `tests/*extract*`, `tests/*parse*`, `tests/*prompt*`, `tests/*skill-writer*`

---

You are working on the **Claude CLI execution layer** — the bridge between assembled prompts and the `claude -p` CLI.
You are working on the **Claude CLI execution layer** — the bridge between assembled prompts and the `claude -p` CLI, plus skill file I/O.

## Key Files
- `src/lib/runner.js` — `runClaude()`, `loadPrompt()`, `parseFileOutput()`, `extractResultFromStream()`
- `src/lib/skill-writer.js` — `writeSkillFiles()` with dryRun/force/skip semantics
- `src/lib/runner.js` — `runClaude()`, `loadPrompt()`, `parseFileOutput()`, `validateSkillFiles()`, `extractResultFromStream()`
- `src/lib/skill-writer.js` — `writeSkillFiles()`, `extractRulesFromSkills()`, `generateDomainPatterns()`, `mergeSettings()`
- `src/lib/skill-reader.js` — `findSkillFiles()`, `parseFrontmatter()`, `parseActivationPatterns()`, `parseKeywords()`
- `src/prompts/` — Markdown prompt templates; `partials/` subdir holds reusable fragments

## Key Concepts
- **Stream-JSON protocol:** `runClaude()` always passes `--verbose --output-format stream-json` (both flags required together with `-p`). Output is NDJSON: `type: 'result'` has final text + cumulative usage; `type: 'assistant'` has text blocks and tool_use blocks; `type: 'user'` has tool_result blocks.
- **Prompt templating:** `loadPrompt(name, vars)` resolves `{{partial-name}}` from `src/prompts/partials/` first, then substitutes `{{varName}}` from `vars`. Partials use lowercase-kebab-case; unresolved partials that aren't in `vars` trigger a console warning.
- **File output parsing:** Primary format is `<file path="...">content</file>` XML tags. Fallback: `<!-- file: path -->` comment markers. Only paths under `.claude/` or exactly `CLAUDE.md` are allowed.
- **Stream-JSON protocol:** `runClaude()` always passes `--verbose --output-format stream-json`. Output is NDJSON: `type: 'result'` has final text + usage; `type: 'assistant'` has text/tool_use blocks; `type: 'user'` has tool_result blocks.
- **Prompt templating:** `loadPrompt(name, vars)` resolves `{{partial-name}}` from `src/prompts/partials/` first, then substitutes `{{varName}}` from `vars`.
- **File output parsing:** Primary: `<file path="...">content</file>` XML tags. Fallback: `<!-- file: path -->` comment markers. Handles code fences correctly.
- **Validation:** `validateSkillFiles()` checks for truncation (XML tag collisions), missing frontmatter, missing sections, bad file path references.
- **Skill rules generation:** `extractRulesFromSkills()` reads all skills via `skill-reader.js`, produces `skill-rules.json` (v2.0) with file patterns, keywords, and intent patterns.
- **Domain patterns:** `generateDomainPatterns()` converts file patterns to bash `detect_skill_domain()` function using `BEGIN/END` markers.
- **Settings merge:** `mergeSettings()` merges aspens hook config into existing `settings.json`, detecting aspens-managed hooks by command path markers.

## Critical Rules
- **Both `--verbose` and `--output-format stream-json` are required** — omitting either breaks stream parsing.
- **Path sanitization is non-negotiable** — `sanitizePath()` blocks `..` traversal, absolute paths, and any path not under `.claude/` or exactly `CLAUDE.md`.
- **Prompt partials resolve before variables** — `{{skill-format}}` resolves to `partials/skill-format.md` first. If a partial file doesn't exist, it falls through to variable substitution.
- **Prompt partials resolve before variables** — `{{skill-format}}` resolves to `partials/skill-format.md` first. If no file, falls through to variable substitution.
- **Timeout auto-scales** — small: 120s, medium: 300s, large: 600s, very-large: 900s. User `--timeout` overrides.
- **`writeSkillFiles` respects force/skip** — without `--force`, existing files are skipped. Dry-run writes nothing.
- **`mergeSettings` preserves non-aspens hooks** — identifies aspens hooks by `ASPENS_HOOK_MARKERS` (`skill-activation-prompt`, `post-tool-use-tracker`), replaces matching entries, preserves everything else.

---
**Last Updated:** 2026-03-24
34 changes: 19 additions & 15 deletions .claude/skills/doc-sync/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,31 @@ This skill triggers when editing doc-sync-related files:
You are working on **doc-sync**, the incremental skill update command (`aspens doc sync`).

## Key Files
- `src/commands/doc-sync.js` — Main command: git diff → skill mapping → Claude update → write
- `src/commands/doc-sync.js` — Main command: git diff → graph rebuild → skill mapping → Claude update → write
- `src/prompts/doc-sync.md` — System prompt sent to Claude (uses `{{skill-format}}` partial)
- `src/lib/runner.js` — `runClaude()`, `loadPrompt()`, `parseFileOutput()` shared across commands
- `src/lib/skill-writer.js` — `writeSkillFiles()` writes `{ path, content }[]` to disk
- `src/lib/scanner.js` — `scanRepo()` used to detect domains for skill mapping
- `src/lib/graph-persistence.js` — `loadGraph()`, `extractSubgraph()`, `formatNavigationContext()` for graph context

## Key Concepts
- **Diff-based flow:** Gets `git diff HEAD~N..HEAD` and `git log`, feeds them plus existing skill contents to Claude via `runClaude()` with read-only tools (`Read`, `Glob`, `Grep`).
- **Skill mapping:** `mapChangesToSkills()` matches changed file names and meaningful path segments against each skill's `## Activation` section. Generic segments (`src`, `lib`, `components`, etc.) are excluded via `GENERIC_PATH_SEGMENTS`. Base skill is flagged only when structural files change (`package.json`, `Dockerfile`, etc.).
- **Token optimization:** Affected skills are sent in full; non-affected skills send only path + description line.
- **Diff truncation:** `truncateDiff()` caps at 15,000 chars, cutting at the last `diff --git` boundary. CLAUDE.md is capped at 5,000 chars.
- **Output parsing:** Claude returns `<file path="...">` XML tags; `parseFileOutput()` in runner.js handles parsing and path sanitization (blocks `..`, absolute paths, only allows `.claude/` and `CLAUDE.md`).
- **Git hook:** `--install-hook` installs a `post-commit` hook with a 5-minute cooldown lock file (`/tmp/aspens-sync-*.lock`). Runs `npx aspens doc sync --commits 1` in background. Appends to existing hooks if present.
- **Force writes:** doc-sync always calls `writeSkillFiles` with `force: true` — it overwrites existing skills without prompting.
- **Diff-based flow:** Gets `git diff HEAD~N..HEAD` and `git log`, feeds them plus existing skill contents and graph context to Claude.
- **Graph rebuild on every sync:** Calls `buildRepoGraph` + `persistGraphArtifacts` to keep `.claude/graph.json` fresh. Graph failure is non-fatal.
- **Graph-aware skill mapping:** `mapChangesToSkills()` checks not just direct file matches but 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:** Skill-relevant files get 60k char budget, everything else gets 20k (80k total). Cuts at `diff --git` boundaries.
- **Skill mapping:** Matches changed file names and meaningful path segments against `## Activation` sections. Generic segments excluded via `GENERIC_PATH_SEGMENTS`.
- **Token optimization:** Affected skills sent in full; non-affected skills send only path + description line.
- **Diff truncation:** `truncateDiff()` caps at configurable limit, cutting at the last `diff --git` boundary. CLAUDE.md capped at 5,000 chars.
- **Git hook:** `installGitHook()` creates a `post-commit` hook with 5-minute cooldown lock file. `removeGitHook()` removes via `>>>` / `<<<` markers.
- **Force writes:** doc-sync always calls `writeSkillFiles` with `force: true`.

## Critical Rules
- `runClaude` is called with `allowedTools: ['Read', 'Glob', 'Grep']` — doc-sync must never grant write tools to the inner Claude call.
- `parseFileOutput` restricts output paths to `.claude/` prefix and `CLAUDE.md` exactly — any other path is silently dropped. Do not change these guards.
- The `getGitDiff` function gracefully falls back from N commits to 1 if the repo has fewer commits than requested. `actualCommits` tracks what was actually used.
- The command exits early with an error if `.claude/skills/` doesn't exist — it requires `aspens doc init` to have been run first.
- The hook cooldown mechanism uses `/tmp` lock files keyed by repo path hash — don't change the naming scheme without updating cleanup logic.
- `runClaude` is called with `allowedTools: ['Read', 'Glob', 'Grep']` — doc-sync must never grant write tools.
- `parseFileOutput` restricts paths to `.claude/` prefix and `CLAUDE.md` exactly — any other path is silently dropped.
- `getGitDiff` gracefully falls back from N commits to 1 if fewer available. `actualCommits` tracks what was used.
- The command exits early with `CliError` if `.claude/skills/` doesn't exist.
- The hook cooldown uses `/tmp/aspens-sync-*.lock` keyed by repo path hash — don't change naming without updating cleanup.
- `checkMissingHooks()` in `bin/cli.js` warns when skills exist but hooks are missing (pre-0.2.2 installs).

---
**Last Updated:** 2026-03-21
**Last Updated:** 2026-03-24
58 changes: 26 additions & 32 deletions .claude/skills/import-graph/skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,42 @@ description: Static import analysis that builds dependency graphs, domain cluste

This skill triggers when editing import-graph-related files:
- `src/lib/graph-builder.js`
- `src/lib/graph-persistence.js`
- `src/commands/doc-graph.js`
- `tests/graph-builder.test.js`

---

You are working on the **import graph builder** — a static analysis module that parses JS/TS and Python source files to produce dependency graphs, hub rankings, domain clusters, and churn-based hotspots.
You are working on the **import graph system** — static analysis that parses JS/TS and Python source files to produce dependency graphs, plus persistence/query layers for runtime use.

## Key Files
- `src/lib/graph-builder.js` — All graph logic (691 lines, single file)
- `src/lib/graph-builder.js` — Core graph logic: walk, parse, metrics, ranking, clustering (691 lines)
- `src/lib/graph-persistence.js` — Serialize, persist, load, subgraph extraction, code-map, graph-index
- `src/commands/doc-graph.js` — Standalone `aspens doc graph` command
- `src/lib/scanner.js` — Provides `detectEntryPoints()`, only internal dependency of graph-builder
- `tests/graph-builder.test.js` — Tests using temp fixture directories
- `src/lib/scanner.js` — Provides `detectEntryPoints()`, the only internal dependency

## Key Concepts
`buildRepoGraph(repoPath, languages?)` is the sole public entry point. It runs a **9-step pipeline**:
1. Walk source files (skip `SKIP_DIRS`, vendored, generated)
2. Parse imports per file (es-module-lexer for JS/TS, regex for Python)
3. Populate `importedBy` reverse edges
4. Git churn analysis (6-month window via `git log`)
5. Compute per-file metrics (fanIn, fanOut, exportCount, churn, priority)
6. Rank files by priority descending
7. Identify hub files (top 20 by fanIn)
8. Domain clustering via BFS connected components
9. Identify hotspots (`churn > 3 && lines > 50`)
**graph-builder.js** — `buildRepoGraph(repoPath, languages?)` runs a 9-step pipeline:
1. Walk source files → 2. Parse imports → 3. Reverse edges → 4. Git churn → 5. Per-file metrics → 6. Priority ranking → 7. Hub detection → 8. Domain clustering → 9. Hotspots

**graph-persistence.js** — Persistence and query layer:
- `serializeGraph()` converts raw graph to indexed format (O(1) lookups, file→cluster mapping)
- `persistGraphArtifacts()` writes `.claude/graph.json` + `.claude/code-map.md` + `.claude/graph-index.json` + auto-gitignores them
- `extractSubgraph(graph, filePaths)` returns 1-hop neighborhood of mentioned files + relevant hubs/hotspots/clusters
- `formatNavigationContext(subgraph)` renders compact markdown (~50 line budget) for prompt injection
- `extractFileReferences(prompt, graph)` tiered extraction: explicit paths → bare filenames → cluster keywords
- `generateCodeMap()` standalone overview for graph hook consumption
- `generateGraphIndex()` tiny inverted index (export names → files, hub basenames, cluster labels)

## Critical Rules
- **`await init` before any `parseJsImports` call.** es-module-lexer requires WASM initialization. `buildRepoGraph` calls it at the top; standalone usage of `parseJsImports` must also await it.
- **Priority formula is load-bearing:** `fanIn * 3.0 + exportCount * 1.5 + (isEntry ? 10.0 : 0) + churn * 2.0 + (1/(depth+1)) * 1.0`. Downstream consumers (doc-init, scan commands) depend on this ranking.
- **All paths are repo-relative strings** (e.g. `src/lib/scanner.js`), never absolute. Resolution functions convert abs→relative before returning.
- **Import resolution tries extensions in order:** `.js, .ts, .tsx, .jsx, .mjs` then `/index` variants. Changing this order changes which file wins when ambiguous.
- **Python regex uses global flags** — `lastIndex` is reset before each exec loop. Forgetting this causes missed imports.
- **Errors are swallowed, not thrown:** parse failures, unreadable files, and missing git all return empty/null. The graph must always complete.

## Key Patterns
- **Internal vs external imports:** Relative/aliased imports that resolve to a file on disk → internal (edges). Everything else → `externalImports` array (no edges).
- **Path alias support:** Reads `tsconfig.json`/`jsconfig.json` from root + monorepo subdirs. Strips comments before JSON.parse.
- **Tests use `createFixture(name, files)`** to build temp directories under `tests/fixtures/graph-builder/`, cleaned up in `afterAll`.

## Exported API
- `buildRepoGraph(repoPath, languages?)` — main entry, returns `{ files, edges, ranked, hubs, clusters, hotspots, entryPoints, stats }`
- `parseJsImports(content, relPath)` — `{ imports: string[], exports: string[] }`
- `parsePyImports(content)` — `string[]` of raw specifiers
- `resolveRelativeImport(repoPath, fromFile, specifier)` — `string | null`
- `computeDomainClusters(files, edges)` — `{ components, coupling }`
- **`await init` before any `parseJsImports` call.** es-module-lexer requires WASM initialization.
- **Priority formula is load-bearing:** `fanIn * 3.0 + exportCount * 1.5 + (isEntry ? 10.0 : 0) + churn * 2.0 + (1/(depth+1)) * 1.0`. Downstream consumers depend on this ranking.
- **All paths are repo-relative strings**, never absolute. Resolution functions convert abs→relative.
- **Graph artifacts are gitignored** — `ensureGraphGitignore()` adds `.claude/graph.json`, `.claude/graph-index.json`, `.claude/code-map.md` to prevent commit loops.
- **Errors are swallowed, not thrown** in graph-builder — parse failures return empty/null. The graph must always complete.
- **`extractSubgraph` logic is mirrored** in `graph-context-prompt.mjs` (standalone hook, no imports). Keep both in sync.
- **doc-sync rebuilds graph on every sync** — calls `buildRepoGraph` + `persistGraphArtifacts` to keep it fresh.

---
**Last Updated:** 2026-03-21
**Last Updated:** 2026-03-24
Loading
Loading