diff --git a/CHANGELOG.md b/CHANGELOG.md index 1341d14d..5b62c014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,25 +6,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Removed +- **Codebase Intelligence System** — Removed due to overengineering concerns + - Deleted `/gsd:analyze-codebase` command + - Deleted `/gsd:query-intel` command + - Removed SQLite graph database and sql.js dependency (21MB) + - Removed intel hooks (gsd-intel-index.js, gsd-intel-session.js, gsd-intel-prune.js) + - Removed entity file generation and templates + ## [1.9.0] - 2025-01-20 ### Added -- **Codebase Intelligence System** — Automatic semantic understanding of your codebase - - `/gsd:analyze-codebase` now generates semantic entities (modules, services, utils) from AST analysis - - `/gsd:query-intel` command for CLI access to dependency graph (`dependents`, `hotspots`) - - SQLite graph database (`.planning/intel/graph.db`) stores entity relationships - - SessionStart hook injects relevant codebase context into conversations - - PostToolUse hook maintains index incrementally as files change - - Stop hook prunes deleted files from index - **Model Profiles** — `/gsd:set-profile` for quality/balanced/budget agent configurations - **Workflow Settings** — `/gsd:settings` command for toggling workflow behaviors interactively -### Changed -- Subagent prompts now include codebase intelligence context when available -- New project initialization creates intel directory structure -- Install process registers intel hooks automatically -- Documentation updated: help.md, README.md with new commands and codebase intelligence features - ### Fixed - Orchestrators now inline file contents in Task prompts (fixes context issues with @ references) - Tech debt from milestone audit addressed diff --git a/GSD-STYLE.md b/GSD-STYLE.md index 23e6b177..c99e9d08 100644 --- a/GSD-STYLE.md +++ b/GSD-STYLE.md @@ -483,70 +483,6 @@ How to make tests pass --- -## Codebase Intelligence - -GSD includes an automatic codebase learning system that indexes code and detects patterns. - -### Files - -| File | Purpose | Updated By | -|------|---------|------------| -| `.planning/intel/index.json` | File exports/imports index | PostToolUse hook | -| `.planning/intel/conventions.json` | Detected naming/directory/suffix patterns | PostToolUse hook | -| `.planning/intel/summary.md` | Concise context for injection | PostToolUse hook | - -### Index Schema - -```json -{ - "version": 1, - "updated": 1234567890, - "files": { - "/absolute/path/to/file.ts": { - "exports": ["functionA", "ClassB", "default"], - "imports": ["react", "./utils", "@org/pkg"], - "indexed": 1234567890 - } - } -} -``` - -### Convention Detection - -**Naming conventions** (requires 5+ exports, 70%+ match rate): -- camelCase, PascalCase, snake_case, SCREAMING_SNAKE -- 'default' is skipped (keyword, not naming indicator) - -**Directory purposes** (lookup table): -- components, hooks, utils, lib, services, api, routes, types, models, tests, etc. - -**Suffix patterns** (requires 5+ files): -- .test.*, .spec.*, .service.*, .controller.*, etc. - -### Hook Patterns - -**PostToolUse hook (intel-index.js):** -- Triggers on Write/Edit of JS/TS files -- Incremental update (single file per invocation) -- Silent failure (never blocks Claude) -- Regenerates conventions.json and summary.md on every update - -**SessionStart hook (intel-session.js):** -- Triggers on startup/resume -- Reads index.json and conventions.json -- Outputs `` wrapped summary -- Silent failure if intel files missing - -### Commands - -**`/gsd:analyze-codebase`** — Bulk scan for brownfield projects: -- Creates .planning/intel/ directory -- Scans all JS/TS files (excludes node_modules, dist, build, .git, vendor, coverage) -- Uses same extraction logic as PostToolUse hook -- Works standalone (no /gsd:new-project required) - ---- - ## Summary: Core Meta-Patterns 1. **XML for semantic structure, Markdown for content** diff --git a/README.md b/README.md index 8212401f..c6e7888a 100644 --- a/README.md +++ b/README.md @@ -324,50 +324,6 @@ Use for: bug fixes, small features, config changes, one-off tasks. ## Why It Works -### Codebase Intelligence - -GSD learns your codebase patterns automatically. As Claude writes code, a PostToolUse hook indexes exports and imports, detects naming conventions, and builds a semantic understanding of your codebase. - -**How it works:** - -1. **Automatic learning** — Every time Claude writes or edits a JS/TS file, the hook extracts exports/imports and updates `.planning/intel/index.json` -2. **Convention detection** — Analyzes exports for naming patterns (camelCase, PascalCase, etc.), identifies directory purposes, detects file suffixes -3. **Graph database** — Stores entity relationships in SQLite for dependency analysis -4. **Context injection** — At session start, injects a summary into Claude's context so it knows your codebase structure and conventions - -**For existing codebases:** - -``` -/gsd:analyze-codebase -``` - -Performs a bulk scan of your codebase to bootstrap the intelligence layer. Works standalone — no `/gsd:new-project` required. After initial analysis, hooks continue incremental learning. - -**Query the graph:** - -``` -/gsd:query-intel dependents src/lib/db.ts # What depends on this file? -/gsd:query-intel hotspots # Most-depended-on files -``` - -**Files created:** - -| File | Purpose | -|------|---------| -| `.planning/intel/index.json` | File exports and imports index | -| `.planning/intel/conventions.json` | Detected patterns (naming, directories, suffixes) | -| `.planning/intel/graph.db` | SQLite database with entity relationships | -| `.planning/intel/summary.md` | Concise context for session injection | - -**Benefits:** - -- Claude follows your naming conventions automatically -- New files go in the right directories -- Consistency maintained across sessions -- Query blast radius before refactoring -- Identify high-impact hotspot files -- No manual documentation of patterns needed - ### Context Engineering Claude Code is incredibly powerful *if* you give it the context it needs. Most people don't. @@ -478,8 +434,6 @@ You're never locked in. The system adapts. | Command | What it does | |---------|--------------| | `/gsd:map-codebase` | Analyze existing codebase before new-project | -| `/gsd:analyze-codebase` | Bootstrap codebase intelligence for existing projects | -| `/gsd:query-intel ` | Query dependency graph (dependents, hotspots) | ### Phase Management diff --git a/agents/gsd-entity-generator.md b/agents/gsd-entity-generator.md deleted file mode 100644 index e55867ae..00000000 --- a/agents/gsd-entity-generator.md +++ /dev/null @@ -1,237 +0,0 @@ ---- -name: gsd-entity-generator -description: Generates semantic entity documentation for codebase files. Spawned by analyze-codebase with file list. Writes entities directly to disk. -tools: Read, Write, Bash -color: cyan ---- - - -You are a GSD entity generator. You create semantic documentation for source files that captures PURPOSE (what the code does and why it exists), not just syntax. - -You are spawned by `/gsd:analyze-codebase` with a list of file paths. - -Your job: Read each file, analyze its purpose, write entity markdown to `.planning/intel/entities/`, return statistics only. - - - -**Entities are consumed by the intelligence system:** - -**PostToolUse hook** syncs each entity to graph.db: -- Extracts frontmatter (path, type, status) -- Extracts [[wiki-links]] from Dependencies section -- Creates nodes and edges in the graph - -**Query interface** uses entities to answer: -- "What depends on this file?" -- "What does this file depend on?" -- "What are the most connected files?" - -**Summary generation** aggregates entities into `.planning/intel/summary.md`: -- Dependency hotspots -- Module statistics -- Connection patterns - -**What this means for your output:** - -1. **Frontmatter must be valid YAML** - Hook parses it to create graph nodes -2. **[[wiki-links]] must use correct slugs** - Hook extracts these for edges -3. **Purpose must be substantive** - "Handles authentication" not "Exports auth functions" -4. **Type must match heuristics** - Enables filtering by module type - - - - - -Extract file paths from your prompt. You'll receive: -- Total file count -- Output directory path -- Slug convention rules -- Entity template -- List of absolute file paths - -Parse file paths into a list for processing. Track progress counters: -- files_processed = 0 -- entities_created = 0 -- already_existed = 0 -- errors = 0 - - - -For each file path: - -1. **Read file content:** - Use the Read tool with the absolute file path. - -2. **Analyze the file:** - - What is its purpose? (Why does this file exist? What problem does it solve?) - - What does it export? (Functions, classes, types, constants) - - What does it import? (Dependencies and why they're needed) - - What type of module is it? (Use type heuristics table) - -3. **Generate slug:** - - Remove leading `/` - - Remove file extension - - Replace `/` and `.` with `-` - - Lowercase everything - - Example: `src/lib/db.ts` -> `src-lib-db` - - Example: `/Users/foo/project/src/auth/login.ts` -> `users-foo-project-src-auth-login` - - Use path relative to project root when possible for cleaner slugs - -4. **Check if entity exists:** - ```bash - ls .planning/intel/entities/{slug}.md 2>/dev/null - ``` - If exists, increment already_existed and skip to next file. - -5. **Build entity content using template:** - - Frontmatter with path, type, date, status - - Purpose section (1-3 substantive sentences) - - Exports section (signatures + descriptions) - - Dependencies section ([[wiki-links]] for internal, plain text for external) - - Used By: Always "TBD" (graph analysis fills this later) - - Notes: Optional (only if important context) - -6. **Write entity file:** - Write to `.planning/intel/entities/{slug}.md` - -7. **Track statistics:** - Increment files_processed and entities_created. - -8. **Handle errors:** - If file can't be read or analyzed, increment errors and continue. - -**Important:** PostToolUse hook automatically syncs each entity to graph.db after you write it. You don't need to touch the graph. - - - -After all files processed, return ONLY statistics. Do NOT include entity contents. - -Format: -``` -## ENTITY GENERATION COMPLETE - -**Files processed:** {files_processed} -**Entities created:** {entities_created} -**Already existed:** {already_existed} -**Errors:** {errors} - -Entities written to: .planning/intel/entities/ -``` - -If errors occurred, list the file paths that failed (not the error messages). - - - - - -Use this EXACT format for every entity: - -```markdown ---- -path: {absolute_path} -type: [module|component|util|config|api|hook|service|model|test] -updated: {YYYY-MM-DD} -status: active ---- - -# {filename} - -## Purpose - -[1-3 sentences: What does this file do? Why does it exist? What problem does it solve? Focus on the "why", not implementation details.] - -## Exports - -[List each export with signature and purpose:] -- `functionName(params): ReturnType` - Brief description of what it does -- `ClassName` - What this class represents -- `CONSTANT_NAME` - What this constant configures - -If no exports: "None" - -## Dependencies - -[Internal dependencies use [[wiki-links]], external use plain text:] -- [[internal-file-slug]] - Why this dependency is needed -- external-package - What functionality it provides - -If no dependencies: "None" - -## Used By - -TBD - -## Notes - -[Optional: Patterns, gotchas, important context. Omit section entirely if nothing notable.] -``` - - - -Determine entity type from file path and content: - -| Type | Indicators | -|------|-----------| -| api | In api/, routes/, endpoints/ directory, exports route handlers | -| component | In components/, exports React/Vue/Svelte components | -| util | In utils/, lib/, helpers/, exports utility functions | -| config | In config/, *.config.*, exports configuration objects | -| hook | In hooks/, exports use* functions (React hooks) | -| service | In services/, exports service classes/functions | -| model | In models/, types/, exports data models or TypeScript types | -| test | *.test.*, *.spec.*, contains test suites | -| module | Default if unclear, general-purpose module | - - - -**Internal dependencies** (files in the codebase): -- Convert import path to slug format -- Wrap in [[double brackets]] -- Example: Import from `../../lib/db.ts` -> Dependency: `[[src-lib-db]]` -- Example: Import from `@/services/auth` -> Dependency: `[[src-services-auth]]` - -**External dependencies** (npm/pip/cargo packages): -- Plain text, no brackets -- Include brief purpose -- Example: `import { z } from 'zod'` -> Dependency: `zod - Schema validation` - -**Identifying internal vs external:** -- Import path starts with `.` or `..` -> internal (wiki-link) -- Import path starts with `@/` or `~/` -> internal (wiki-link, resolve alias) -- Import path is package name (no path separator) -> external (plain text) -- Import path starts with `@org/` -> usually external (npm scoped package) - - - - -**WRITE ENTITIES DIRECTLY.** Do not return entity contents to orchestrator. The whole point is reducing context transfer. - -**USE EXACT TEMPLATE FORMAT.** The PostToolUse hook parses frontmatter and [[wiki-links]]. Wrong format = broken graph sync. - -**FRONTMATTER MUST BE VALID YAML.** No tabs, proper quoting for paths with special characters. - -**PURPOSE MUST BE SUBSTANTIVE.** Bad: "Exports database functions." Good: "Manages database connection pooling and query execution. Provides transaction support and connection health monitoring." - -**INTERNAL DEPS USE [[WIKI-LINKS]].** Hook extracts these to create graph edges. Plain text deps don't create edges. - -**RETURN ONLY STATISTICS.** Your response should be ~10 lines. Just confirm what was written. - -**DO NOT COMMIT.** The orchestrator handles git operations. - -**SKIP EXISTING ENTITIES.** Check if entity file exists before writing. Don't overwrite existing entities. - - - - -Entity generation complete when: - -- [ ] All file paths processed -- [ ] Each new entity written to `.planning/intel/entities/{slug}.md` -- [ ] Entity markdown follows template exactly -- [ ] Frontmatter is valid YAML -- [ ] Purpose section is substantive (not just "exports X") -- [ ] Internal dependencies use [[wiki-links]] -- [ ] External dependencies are plain text -- [ ] Statistics returned (not entity contents) -- [ ] Existing entities skipped (not overwritten) - diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 3f68c8a6..e65a96b9 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -52,20 +52,6 @@ git check-ignore -q .planning 2>/dev/null && COMMIT_PLANNING_DOCS=false Store `COMMIT_PLANNING_DOCS` for use in git operations. - -Check for codebase intelligence: - -```bash -cat .planning/intel/summary.md 2>/dev/null -``` - -If exists: -- Follow detected naming conventions when writing code -- Place new files in directories that match their purpose -- Use established patterns (camelCase, PascalCase, etc.) - -This context helps maintain codebase consistency during execution. - Read the plan file provided in your prompt context. diff --git a/agents/gsd-indexer.md b/agents/gsd-indexer.md deleted file mode 100644 index e7bce7d5..00000000 --- a/agents/gsd-indexer.md +++ /dev/null @@ -1,241 +0,0 @@ ---- -name: gsd-indexer -description: Indexes codebase files by extracting exports and imports. Spawned by analyze-codebase with file list. Writes index.json directly to disk. -tools: Read, Write, Bash -color: cyan ---- - - -You are a GSD indexer. You read source files and extract exports and imports to build a codebase index. - -You are spawned by `/gsd:analyze-codebase` with a list of file paths (obtained from Glob results). - -Your job: Read each file, extract exports/imports using regex, write complete index.json to `.planning/intel/index.json`, return statistics only. - - - -**index.json is consumed by multiple intelligence components:** - -**Convention detection (Step 4)** analyzes the index to detect: -- Naming patterns (camelCase, PascalCase, etc.) -- Directory organization patterns -- File suffix patterns - -**Entity generation (Step 9)** uses index to prioritize files: -- High-export files (3+ exports = likely core modules) -- Hub files (referenced by 5+ other files via imports) - -**PostToolUse hook** uses index for incremental updates: -- When files are edited, hook updates index entries -- Keeps index fresh without full rescan - -**What this means for your output:** - -1. **Use absolute paths as keys** - Enables O(1) lookup for file entries -2. **Extract accurately** - Wrong exports break convention detection -3. **Write directly** - Orchestrator MUST NOT load file contents (context exhaustion on 500+ files) -4. **Return statistics only** - ~10 lines, not index contents - - - - - -Extract from your prompt: -- Output path: `.planning/intel/index.json` -- List of absolute file paths (one per line) - -Initialize counters: -- files_processed = 0 -- exports_found = 0 -- imports_found = 0 -- errors = 0 - -Initialize index structure: -```javascript -{ - version: 1, - updated: Date.now(), - files: {} -} -``` - - - -For each file path in the list: - -**1. Read file content:** -Use the Read tool with the absolute file path. - -**2. Extract exports using these patterns:** - -Named exports: -```regex -export\s*\{([^}]+)\} -``` - -Declaration exports: -```regex -export\s+(?:const|let|var|function\*?|async\s+function|class)\s+(\w+) -``` - -Default exports: -```regex -export\s+default\s+(?:function\s*\*?\s*|class\s+)?(\w+)? -``` - -CommonJS object exports: -```regex -module\.exports\s*=\s*\{([^}]+)\} -``` - -CommonJS single exports: -```regex -module\.exports\s*=\s*(\w+)\s*[;\n] -``` - -TypeScript type/interface exports: -```regex -export\s+(?:type|interface)\s+(\w+) -``` - -For named exports and CommonJS object exports, split the captured group by commas to get individual names. - -**3. Extract imports using these patterns:** - -ES6 imports: -```regex -import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"] -``` - -Side-effect imports (not preceded by 'from'): -```regex -import\s+['"]([^'"]+)['"] -``` - -CommonJS require: -```regex -require\s*\(\s*['"]([^'"]+)['"]\s*\) -``` - -**4. Store in index:** -```javascript -index.files[absolutePath] = { - exports: [], // Array of export names (strings) - imports: [], // Array of import sources (strings) - indexed: Date.now() -} -``` - -**5. Track statistics:** -- Increment files_processed -- Add length of exports array to exports_found -- Add length of imports array to imports_found - -**6. Handle errors:** -If file can't be read: -- Increment errors counter -- Log the file path -- Continue to next file (don't stop processing) - - - -After all files processed, write complete index to disk: - -```javascript -{ - "version": 1, - "updated": Date.now(), - "files": { - "/absolute/path/to/file.js": { - "exports": ["functionA", "ClassB", "default"], - "imports": ["react", "./utils"], - "indexed": 1737360330000 - } - } -} -``` - -Write to `.planning/intel/index.json` using the Write tool. - -The index must: -- Use absolute paths as keys (not relative) -- Include `version: 1` for schema migrations -- Include `updated` timestamp at top level -- Include `indexed` timestamp per file - - - -After index written, return ONLY statistics. Do NOT include index contents. - -Format: -``` -## INDEXING COMPLETE - -**Files processed:** {files_processed} -**Exports found:** {exports_found} -**Imports found:** {imports_found} -**Errors:** {errors} - -Index written to: .planning/intel/index.json -``` - -If errors occurred, list the file paths that failed (not the error messages). - - - - - -Reference for all regex patterns (copied from analyze-codebase Step 3): - -**Export Patterns:** - -| Pattern Name | Regex | Captures | -|--------------|-------|----------| -| Named exports | `export\s*\{([^}]+)\}` | Names in braces | -| Declaration exports | `export\s+(?:const\|let\|var\|function\*?\|async\s+function\|class)\s+(\w+)` | Single name | -| Default exports | `export\s+default\s+(?:function\s*\*?\s*\|class\s+)?(\w+)?` | Name or empty | -| CommonJS object | `module\.exports\s*=\s*\{([^}]+)\}` | Names in braces | -| CommonJS single | `module\.exports\s*=\s*(\w+)\s*[;\n]` | Single name | -| TypeScript | `export\s+(?:type\|interface)\s+(\w+)` | Type/interface name | - -**Import Patterns:** - -| Pattern Name | Regex | Captures | -|--------------|-------|----------| -| ES6 imports | `import\s+(?:\{[^}]*\}\|\*\s+as\s+\w+\|\w+)\s+from\s+['"]([^'"]+)['"]` | Module path | -| Side-effect | `import\s+['"]([^'"]+)['"]` | Module path | -| CommonJS | `require\s*\(\s*['"]([^'"]+)['"]\s*\)` | Module path | - -**Processing notes:** -- For named exports, split captured group by comma and trim whitespace -- For default exports, record "default" if no identifier captured -- Deduplicate imports (same source may be required multiple times) - - - - -**WRITE INDEX.JSON DIRECTLY.** Do not return index contents to orchestrator. The whole point is reducing context transfer. - -**USE EXACT REGEX PATTERNS.** These patterns are validated and match what the PostToolUse hook expects. Wrong patterns = broken index. - -**ABSOLUTE PATHS AS KEYS.** The index uses absolute paths for O(1) lookup. Do not convert to relative paths. - -**HANDLE ERRORS GRACEFULLY.** If a file can't be read, log it and continue. Don't stop processing. - -**RETURN ONLY STATISTICS.** Your response should be ~10 lines. Just confirm what was written. - -**DO NOT COMMIT.** The orchestrator handles git operations. - - - - -Indexing complete when: - -- [ ] All file paths processed -- [ ] index.json written to `.planning/intel/index.json` -- [ ] Index has version, updated, and files properties -- [ ] Each file entry has exports, imports, and indexed timestamp -- [ ] Absolute paths used as keys (not relative) -- [ ] Statistics returned (not index contents) -- [ ] Errors logged but processing continued - diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index c158c1a8..1266897b 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -416,9 +416,6 @@ Output: [What artifacts will be created] @.planning/ROADMAP.md @.planning/STATE.md -# Codebase intelligence (if exists) -@.planning/intel/summary.md - # Only reference prior plan SUMMARYs if genuinely needed @path/to/relevant/source.ts @@ -1039,25 +1036,6 @@ If exists, load relevant documents based on phase type: | (default) | STACK.md, ARCHITECTURE.md | - -Check for codebase intelligence: - -```bash -cat .planning/intel/summary.md 2>/dev/null -``` - -If exists, this provides: -- File count and structure overview -- Detected naming conventions (use these when creating new files) -- Key directories and their purposes -- Export patterns - -**How to use:** -- Follow detected naming conventions when planning new exports -- Place new files in directories that match their purpose -- Reference existing patterns when describing implementation - - Check roadmap and existing phases: diff --git a/bin/install.js b/bin/install.js index 18942451..23e3e8ce 100755 --- a/bin/install.js +++ b/bin/install.js @@ -418,72 +418,6 @@ function install(isGlobal) { console.log(` ${green}✓${reset} Configured update check hook`); } - // Register intel hooks for codebase intelligence - const intelIndexCommand = isGlobal - ? 'node "$HOME/.claude/hooks/gsd-intel-index.js"' - : 'node .claude/hooks/gsd-intel-index.js'; - - const intelSessionCommand = isGlobal - ? 'node "$HOME/.claude/hooks/gsd-intel-session.js"' - : 'node .claude/hooks/gsd-intel-session.js'; - - // PostToolUse hook for indexing - if (!settings.hooks.PostToolUse) { - settings.hooks.PostToolUse = []; - } - - const hasIntelIndexHook = settings.hooks.PostToolUse.some(entry => - entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-intel-index')) - ); - - if (!hasIntelIndexHook) { - settings.hooks.PostToolUse.push({ - hooks: [{ - type: 'command', - command: intelIndexCommand - }] - }); - console.log(` ${green}✓${reset} Configured intel indexing hook`); - } - - // SessionStart hook for context injection - const hasIntelSessionHook = settings.hooks.SessionStart.some(entry => - entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-intel-session')) - ); - - if (!hasIntelSessionHook) { - settings.hooks.SessionStart.push({ - hooks: [{ - type: 'command', - command: intelSessionCommand - }] - }); - console.log(` ${green}✓${reset} Configured intel session hook`); - } - - // Stop hook for pruning deleted files - const intelPruneCommand = isGlobal - ? 'node "$HOME/.claude/hooks/gsd-intel-prune.js"' - : 'node .claude/hooks/gsd-intel-prune.js'; - - if (!settings.hooks.Stop) { - settings.hooks.Stop = []; - } - - const hasIntelPruneHook = settings.hooks.Stop.some(entry => - entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-intel-prune')) - ); - - if (!hasIntelPruneHook) { - settings.hooks.Stop.push({ - hooks: [{ - type: 'command', - command: intelPruneCommand - }] - }); - console.log(` ${green}✓${reset} Configured intel prune hook`); - } - return { settingsPath, settings, statuslineCommand }; } diff --git a/commands/gsd/analyze-codebase.md b/commands/gsd/analyze-codebase.md deleted file mode 100644 index 1833329c..00000000 --- a/commands/gsd/analyze-codebase.md +++ /dev/null @@ -1,476 +0,0 @@ ---- -name: gsd:analyze-codebase -description: Scan existing codebase and populate .planning/intel/ with file index, conventions, and semantic entity files -argument-hint: "" -allowed-tools: - - Read - - Bash - - Glob - - Write - - Task ---- - - -Scan codebase to populate .planning/intel/ with file index, conventions, and semantic entity files. - -Works standalone (without /gsd:new-project) for brownfield codebases. Creates summary.md for context injection at session start. Generates entity files that capture file PURPOSE (what it does, why it exists), not just syntax. - -Output: .planning/intel/index.json, conventions.json, summary.md, entities/*.md - - - -This command performs bulk codebase scanning to bootstrap the Codebase Intelligence system. - -**Use for:** -- Brownfield projects before /gsd:new-project -- Refreshing intel after major changes -- Standalone intel without full project setup - -After initial scan, the PostToolUse hook (hooks/intel-index.js) maintains incremental updates. - -**Execution model (Steps 2-3 - Indexing):** -- Orchestrator finds file paths via Glob (Step 2) -- Spawns `gsd-indexer` subagent with file paths only (Step 3) -- Subagent reads files in fresh 200k context, applies regex patterns -- Subagent writes index.json directly to disk -- Subagent returns statistics only (not file contents or index data) -- This prevents context exhaustion on large codebases (500+ files) - -**Execution model (Step 9 - Entity Generation):** -- Orchestrator selects files for entity generation (up to 50 based on priority) -- Spawns `gsd-entity-generator` subagent with file list (paths only, not contents) -- Subagent reads files in fresh 200k context, generates entities, writes to disk -- PostToolUse hook automatically syncs entities to graph.db -- Subagent returns statistics only (not entity contents) -- This preserves orchestrator context for large codebases (500+ files) -- Users can skip Step 9 if they only want the index (faster) - - - - -## Step 1: Create directory structure - -```bash -mkdir -p .planning/intel -``` - -## Step 2: Find all indexable files - -Use Glob tool with pattern: `**/*.{js,ts,jsx,tsx,mjs,cjs}` - -Exclude directories (skip any path containing): -- node_modules -- dist -- build -- .git -- vendor -- coverage -- .next -- __pycache__ - -Filter results to remove excluded paths. Store as `file_paths` array. - -**Output:** List of absolute file paths for indexing. Do NOT read file contents. - -## Step 3: Spawn indexer subagent - -Spawn `gsd-indexer` subagent with the file paths from Step 2. - -**Why subagent delegation:** -- Orchestrator would exhaust context reading 500+ files inline -- Subagent gets fresh 200k context for file reading -- Orchestrator only handles file paths (small) -- Subagent writes index.json directly (large) - -**Task tool invocation:** - -```python -file_list = "\n".join(file_paths) # From Step 2 Glob results - -Task( - prompt=f"""Index codebase files by extracting exports and imports. - -You are a GSD indexer. Read source files and extract exports/imports using regex patterns. - -**Parameters:** -- Files to process: {len(file_paths)} -- Output path: .planning/intel/index.json - -**Export patterns:** -- Named: export\\s*\\{{([^}}]+)\\}} -- Declaration: export\\s+(?:const|let|var|function\\*?|async\\s+function|class)\\s+(\\w+) -- Default: export\\s+default\\s+(?:function\\s*\\*?\\s*|class\\s+)?(\w+)? -- CommonJS object: module\\.exports\\s*=\\s*\\{{([^}}]+)\\}} -- CommonJS single: module\\.exports\\s*=\\s*(\\w+)\\s*[;\\n] -- TypeScript: export\\s+(?:type|interface)\\s+(\\w+) - -**Import patterns:** -- ES6: import\\s+(?:\\{{[^}}]*\\}}|\\*\\s+as\\s+\\w+|\\w+)\\s+from\\s+['\"]([^'\"]+)['\"] -- Side-effect: import\\s+['\"]([^'\"]+)['\"] -- CommonJS: require\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\) - -**Index schema:** -```json -{{ - "version": 1, - "updated": {{timestamp}}, - "files": {{ - "/absolute/path/file.js": {{ - "exports": ["name1", "name2"], - "imports": ["source1", "source2"], - "indexed": {{timestamp}} - }} - }} -}} -``` - -**Process:** -For each file path below: -1. Read file content using Read tool -2. Apply export regex patterns, collect export names -3. Apply import regex patterns, collect import sources -4. Store in index structure with absolute path as key - -**Files:** -{file_list} - -**Return format:** -When complete, return ONLY statistics: - -## INDEXING COMPLETE - -**Files processed:** {{N}} -**Exports found:** {{M}} -**Imports found:** {{K}} -**Errors:** {{E}} - -Index written to: .planning/intel/index.json - -Do NOT return index contents. -""", - subagent_type="gsd-indexer" -) -``` - -**Wait for completion:** Task() blocks until subagent finishes. - -**Verify index created:** -```bash -ls -la .planning/intel/index.json -``` - -## Step 4: Detect conventions - -Analyze the collected index for patterns. - -**Naming conventions** (require 5+ exports, 70%+ match rate): -- camelCase: `^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)+$` or single lowercase `^[a-z][a-z0-9]*$` -- PascalCase: `^[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)*$` or single `^[A-Z][a-z0-9]+$` -- snake_case: `^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$` -- SCREAMING_SNAKE: `^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$` or single `^[A-Z][A-Z0-9]*$` -- Skip 'default' when counting (it's a keyword, not naming convention) - -**Directory patterns** (use lookup table): -``` -components -> UI components -hooks -> React/custom hooks -utils, lib -> Utility functions -services -> Service layer -api, routes -> API endpoints -types -> TypeScript types -models -> Data models -tests, __tests__, test, spec -> Test files -controllers -> Controllers -middleware -> Middleware -config -> Configuration -constants -> Constants -pages -> Page components -views -> View templates -``` - -**Suffix patterns** (require 5+ occurrences): -``` -.test.*, .spec.* -> Test files -.service.* -> Service layer -.controller.* -> Controllers -.model.* -> Data models -.util.*, .utils.* -> Utility functions -.helper.*, .helpers.* -> Helper functions -.config.* -> Configuration -.types.*, .type.* -> TypeScript types -.hook.*, .hooks.* -> React/custom hooks -.context.* -> React context -.store.* -> State store -.slice.* -> Redux slice -.reducer.* -> Redux reducer -.action.*, .actions.* -> Redux actions -.api.* -> API layer -.route.*, .routes.* -> Route definitions -.middleware.* -> Middleware -.schema.* -> Schema definitions -.mock.*, .mocks.* -> Mock data -.fixture.*, .fixtures.* -> Test fixtures -``` - -## Step 5: Read index.json - -The `gsd-indexer` subagent wrote index.json in Step 3. Read it back for convention detection and statistics. - -```bash -cat .planning/intel/index.json -``` - -Expected schema: -```json -{ - "version": 1, - "updated": 1737360330000, - "files": { - "/absolute/path/to/file.js": { - "exports": ["functionA", "ClassB"], - "imports": ["react", "./utils"], - "indexed": 1737360330000 - } - } -} -``` - -## Step 6: Write conventions.json - -Write to `.planning/intel/conventions.json`: -```javascript -{ - "version": 1, - "updated": 1737360330000, - "naming": { - "exports": { - "dominant": "camelCase", - "count": 42, - "percentage": 85 - } - }, - "directories": { - "components": { "purpose": "UI components", "files": 15 }, - "hooks": { "purpose": "React/custom hooks", "files": 8 } - }, - "suffixes": { - ".test.js": { "purpose": "Test files", "count": 12 } - } -} -``` - -## Step 7: Generate summary.md - -Write to `.planning/intel/summary.md`: - -```markdown -# Codebase Intelligence Summary - -Last updated: [ISO timestamp] -Indexed files: [N] - -## Naming Conventions - -- Export naming: [case] ([percentage]% of [count] exports) - -## Key Directories - -- `[dir]/`: [purpose] ([N] files) -- ... (top 5) - -## File Patterns - -- `*[suffix]`: [purpose] ([count] files) -- ... (top 3) - -Total exports: [N] -``` - -Target: < 500 tokens. Keep concise for context injection. - -## Step 8: Report completion - -Display summary statistics: - -``` -Codebase Analysis Complete - -Files indexed: [N] -Exports found: [N] -Imports found: [N] - -Conventions detected: -- Naming: [dominant case] ([percentage]%) -- Directories: [list] -- Patterns: [list] - -Files created: -- .planning/intel/index.json -- .planning/intel/conventions.json -- .planning/intel/summary.md -``` - -## Step 9: Generate semantic entities (optional) - -Generate entity files that capture semantic understanding of key files. These provide PURPOSE, not just syntax. - -**Skip this step if:** User only wants the index, or codebase has < 10 files. - -### 9.1 Create entities directory - -```bash -mkdir -p .planning/intel/entities -``` - -### 9.2 Select files for entity generation - -Select up to 50 files based on these criteria (in priority order): - -1. **High-export files:** 3+ exports (likely core modules) -2. **Hub files:** Referenced by 5+ other files (via imports analysis) -3. **Key directories:** Entry points (index.js, main.js, app.js), config files -4. **Structural files:** Files matching convention patterns (services, controllers, models) - -From the index.json, identify candidates and limit to 50 files maximum per run. - -### 9.3 Spawn entity generator subagent - -Spawn `gsd-entity-generator` with the selected file list. - -**Pass to subagent:** -- Total file count -- Output directory: `.planning/intel/entities/` -- Slug convention: `src/lib/db.ts` -> `src-lib-db` (replace / with -, remove extension, lowercase) -- Entity template (include full template from agent definition) -- List of absolute file paths (one per line) - -**Task tool invocation:** - -```python -# Build file list (one absolute path per line) -file_list = "\n".join(selected_files) -today = date.today().isoformat() - -Task( - prompt=f"""Generate semantic entity documentation for key codebase files. - -You are a GSD entity generator. Read source files and create semantic documentation that captures PURPOSE (what/why), not just syntax. - -**Parameters:** -- Files to process: {len(selected_files)} -- Output directory: .planning/intel/entities/ -- Date: {today} - -**Slug convention:** -- Remove leading / -- Remove file extension -- Replace / and . with - -- Lowercase everything -- Example: src/lib/db.ts -> src-lib-db - -**Entity template:** -```markdown ---- -path: {{absolute_path}} -type: [module|component|util|config|api|hook|service|model|test] -updated: {today} -status: active ---- - -# {{filename}} - -## Purpose - -[1-3 sentences: What does this file do? Why does it exist? What problem does it solve?] - -## Exports - -- `functionName(params): ReturnType` - Brief description -- `ClassName` - What this class represents - -If no exports: "None" - -## Dependencies - -- [[internal-file-slug]] - Why needed (for internal deps) -- external-package - What it provides (for npm packages) - -If no dependencies: "None" - -## Used By - -TBD -``` - -**Process:** -For each file path below: -1. Read file content using Read tool -2. Analyze purpose, exports, dependencies -3. Check if entity already exists (skip if so) -4. Write entity to .planning/intel/entities/{{slug}}.md -5. PostToolUse hook syncs to graph.db automatically - -**Files:** -{file_list} - -**Return format:** -When complete, return ONLY statistics: - -## ENTITY GENERATION COMPLETE - -**Files processed:** {{N}} -**Entities created:** {{M}} -**Already existed:** {{K}} -**Errors:** {{E}} - -Entities written to: .planning/intel/entities/ - -Do NOT include entity contents in your response. -""", - subagent_type="gsd-entity-generator" -) -``` - -**Wait for completion:** Task() blocks until subagent finishes. - -**Parse result:** Extract entities_created count from response for final report. - -### 9.4 Verify entity generation - -Confirm entities were written: - -```bash -ls .planning/intel/entities/*.md 2>/dev/null | wc -l -``` - -### 9.5 Report entity statistics - -``` -Entity Generation Complete - -Entity files created: [N] (from subagent response) -Location: .planning/intel/entities/ -Graph database: Updated automatically via PostToolUse hook - -Next: Intel hooks will continue incremental updates as you code. -``` - - - - -- .planning/intel/index.json - File index with exports and imports -- .planning/intel/conventions.json - Detected naming and structural patterns -- .planning/intel/summary.md - Concise summary for context injection -- .planning/intel/entities/*.md - Semantic entity files (optional, Step 9) - - - -- [ ] .planning/intel/ directory created -- [ ] All JS/TS files scanned (excluding node_modules, dist, build, .git, vendor, coverage) -- [ ] index.json populated with exports and imports for each file -- [ ] conventions.json has detected patterns (naming, directories, suffixes) -- [ ] summary.md is concise (< 500 tokens) -- [ ] Statistics reported to user -- [ ] Entity files generated for key files (if Step 9 executed) -- [ ] Entity files contain Purpose section with semantic understanding - diff --git a/commands/gsd/help.md b/commands/gsd/help.md index 80c79652..669757ea 100644 --- a/commands/gsd/help.md +++ b/commands/gsd/help.md @@ -76,17 +76,6 @@ Map an existing codebase for brownfield projects. Usage: `/gsd:map-codebase` -**`/gsd:analyze-codebase`** -Bootstrap codebase intelligence for existing projects. - -- Scans all JS/TS files and extracts exports/imports -- Detects naming conventions, directory patterns, file suffixes -- Creates `.planning/intel/` with index, conventions, and summary -- Works standalone (no `/gsd:new-project` required) -- After initial scan, PostToolUse hook continues incremental learning - -Usage: `/gsd:analyze-codebase` - ### Phase Planning **`/gsd:discuss-phase `** @@ -339,19 +328,6 @@ Quick switch model profile for GSD agents. Usage: `/gsd:set-profile budget` -### Codebase Intelligence - -**`/gsd:query-intel [path]`** -Query the codebase intelligence graph database. - -- `dependents ` — What files depend on this? (blast radius) -- `hotspots` — Which files have the most dependents? - -Requires `/gsd:analyze-codebase` to build the graph first. - -Usage: `/gsd:query-intel dependents src/lib/db.ts` -Usage: `/gsd:query-intel hotspots` - ### Utility Commands **`/gsd:help`** @@ -389,10 +365,6 @@ Usage: `/gsd:update` │ └── done/ # Completed todos ├── debug/ # Active debug sessions │ └── resolved/ # Archived resolved issues -├── intel/ # Codebase intelligence (auto-populated) -│ ├── index.json # File exports and imports -│ ├── conventions.json # Detected patterns -│ └── summary.md # Context for session injection ├── codebase/ # Codebase map (brownfield projects) │ ├── STACK.md # Languages, frameworks, dependencies │ ├── ARCHITECTURE.md # Patterns, layers, data flow diff --git a/commands/gsd/new-project.md b/commands/gsd/new-project.md index b3db8d23..818c52d7 100644 --- a/commands/gsd/new-project.md +++ b/commands/gsd/new-project.md @@ -19,7 +19,6 @@ This is the most leveraged moment in any project. Deep questioning here means be - `.planning/PROJECT.md` — project context - `.planning/config.json` — workflow preferences - `.planning/research/` — domain research (optional) -- `.planning/intel/` — codebase intelligence (auto-populated by hooks) - `.planning/REQUIREMENTS.md` — scoped requirements - `.planning/ROADMAP.md` — phase structure - `.planning/STATE.md` — project memory @@ -58,14 +57,7 @@ This is the most leveraged moment in any project. Deep questioning here means be fi ``` -3. **Create intel directory for codebase intelligence:** - ```bash - mkdir -p .planning/intel - ``` - - This prepares the directory for the PostToolUse hook to populate with index.json, conventions.json, and summary.md as Claude writes code. - -4. **Detect existing code (brownfield detection):** +3. **Detect existing code (brownfield detection):** ```bash CODE_FILES=$(find . -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" 2>/dev/null | grep -v node_modules | grep -v .git | head -20) HAS_PACKAGE=$([ -f package.json ] || [ -f requirements.txt ] || [ -f Cargo.toml ] || [ -f go.mod ] || [ -f Package.swift ] && echo "yes") @@ -365,7 +357,7 @@ Create `.planning/config.json` with all settings: - Add `.planning/` to `.gitignore` (create if needed) **If commit_docs = Yes:** -- Add `.planning/intel/` to `.gitignore` (intel is always local — changes constantly, can be regenerated) +- No additional gitignore entries needed **Commit config.json:** @@ -981,7 +973,6 @@ Present completion with next steps: - `ARCHITECTURE.md` - `PITFALLS.md` - `SUMMARY.md` -- `.planning/intel/` (created empty, populated by hooks during coding) - `.planning/REQUIREMENTS.md` - `.planning/ROADMAP.md` - `.planning/STATE.md` diff --git a/commands/gsd/plan-phase.md b/commands/gsd/plan-phase.md index 025976bd..fcb4de90 100644 --- a/commands/gsd/plan-phase.md +++ b/commands/gsd/plan-phase.md @@ -249,9 +249,6 @@ RESEARCH_CONTENT=$(cat "${PHASE_DIR}"/*-RESEARCH.md 2>/dev/null) # Gap closure files (only if --gaps mode) VERIFICATION_CONTENT=$(cat "${PHASE_DIR}"/*-VERIFICATION.md 2>/dev/null) UAT_CONTENT=$(cat "${PHASE_DIR}"/*-UAT.md 2>/dev/null) - -# Codebase intelligence (if exists) -INTEL_CONTENT=$(cat .planning/intel/summary.md 2>/dev/null) ``` ## 8. Spawn gsd-planner Agent @@ -292,11 +289,6 @@ Fill prompt with inlined content and spawn: {verification_content} {uat_content} -**Codebase Intel (if exists):** - -{intel_content} - - diff --git a/commands/gsd/query-intel.md b/commands/gsd/query-intel.md deleted file mode 100644 index e25b12c0..00000000 --- a/commands/gsd/query-intel.md +++ /dev/null @@ -1,128 +0,0 @@ ---- -name: gsd:query-intel -description: Query codebase intelligence graph for dependencies and hotspots -argument-hint: " [file-path]" -allowed-tools: - - Bash ---- - - -Query the codebase intelligence graph database for relationship information. - -**Query types:** -- `dependents ` — What files depend on this file? (blast radius) -- `hotspots` — Which files have the most dependents? (change carefully) - -Output: Formatted query results from graph.db - - - -This command exposes the graph query capabilities built by Phase 4 (Semantic Intelligence). - -**Use for:** -- Checking blast radius before refactoring a core file -- Identifying high-impact files that need careful changes -- Understanding dependency relationships in the codebase - -**Requires:** `.planning/intel/graph.db` (created by `/gsd:analyze-codebase` with entity generation) - -If graph.db doesn't exist, the command will return an error suggesting to run analyze-codebase first. - - - - -## Step 1: Parse arguments - -Extract query type and optional file path from arguments. - -**Arguments:** $ARGUMENTS - -**Expected formats:** -- `dependents src/lib/db.ts` — query what depends on this file -- `hotspots` — query most-depended-on files -- `hotspots 10` — query top 10 hotspots (default: 5) - -## Step 2: Convert file path to entity ID - -For `dependents` queries, convert the file path to entity ID format: -- `src/lib/db.ts` → `src-lib-db` -- Replace `/` with `-`, remove extension - -```bash -# Example conversion -FILE_PATH="src/lib/db.ts" -ENTITY_ID=$(echo "$FILE_PATH" | sed 's/\.[^.]*$//' | tr '/' '-') -``` - -## Step 3: Execute query - -Run the appropriate query against the graph database: - -**For dependents:** -```bash -echo '{"action":"query","type":"dependents","target":"'$ENTITY_ID'","limit":20}' | node hooks/gsd-intel-index.js -``` - -**For hotspots:** -```bash -echo '{"action":"query","type":"hotspots","limit":5}' | node hooks/gsd-intel-index.js -``` - -## Step 4: Format and present results - -Parse the JSON response and present in readable format. - -**For dependents:** -``` -## Files that depend on {file-path} - -Found {count} dependents: - -1. src/api/users.ts -2. src/api/auth.ts -3. src/services/payment.ts -... - -**Blast radius:** {count} files would be affected by changes. -``` - -**For hotspots:** -``` -## Dependency Hotspots - -These files have the most dependents — change carefully: - -| Rank | File | Dependents | -|------|------|------------| -| 1 | src/lib/db.ts | 42 | -| 2 | src/types/user.ts | 35 | -| 3 | src/utils/format.ts | 28 | -``` - -## Step 5: Handle errors - -**If graph.db doesn't exist:** -``` -No graph database found at .planning/intel/graph.db - -Run /gsd:analyze-codebase first to build the dependency graph. -``` - -**If entity not found:** -``` -No entity found for: {file-path} - -The file may not be indexed yet. Try: -- /gsd:analyze-codebase to rebuild the index -- Check the file path is correct -``` - - - - -- [ ] Query type parsed from arguments -- [ ] File path converted to entity ID (for dependents) -- [ ] Query executed against graph.db -- [ ] Results formatted in readable markdown -- [ ] Errors handled gracefully with helpful messages - diff --git a/get-shit-done/templates/entity.md b/get-shit-done/templates/entity.md deleted file mode 100644 index 48b8262d..00000000 --- a/get-shit-done/templates/entity.md +++ /dev/null @@ -1,173 +0,0 @@ -# Entity Template - -Template for `.planning/codebase/{entity-slug}.md` - file-level intelligence documentation. - ---- - -## File Template - -```markdown ---- -path: {path} -type: {type} -updated: {updated} -status: {status} ---- - -# {filename} - -## Purpose - -{purpose} - -## Exports - -{exports} - -## Dependencies - -{dependencies} - -## Used By - -{used_by} - -## Notes - -{notes} -``` - ---- - -## Field Reference - -### Frontmatter - -| Field | Values | Description | -|-------|--------|-------------| -| `path` | Absolute path | Full path to the file | -| `type` | module, component, util, config, test, api, hook | Primary classification | -| `updated` | YYYY-MM-DD | Last time this entity was updated | -| `status` | active, deprecated, stub | Current state | - -### Sections - -**Purpose** (required) -1-3 sentences covering: -- What this file does -- Why it exists -- Who/what uses it (high-level) - -**Exports** (required for modules with exports) -List each export with signature and description: -```markdown -- `functionName(arg: Type): ReturnType` - What it does -- `ClassName` - What it represents -- `CONSTANT_NAME` - What value it holds -``` - -For files without exports (config, tests), write "None" or describe what the file defines. - -**Dependencies** (required) -Internal dependencies use wiki-links (slugified paths): -```markdown -- [[src-lib-db]] - Database client -- [[src-types-user]] - User type definitions -``` - -External dependencies use plain text: -```markdown -- react - Component framework -- jose - JWT handling -``` - -**Used By** (grows over time) -Files that import this one, using wiki-links: -```markdown -- [[src-app-api-auth-route]] -- [[src-components-dashboard]] -``` - -Initially may be empty or incomplete. Updated as Claude encounters imports. - -**Notes** (optional) -Patterns, gotchas, or context: -```markdown -- Uses singleton pattern for connection pooling -- WARNING: Must call `init()` before any other method -- Related: See [[src-lib-cache]] for caching layer -``` - ---- - -## Slug Convention - -Entity slugs are derived from file paths: -- `src/lib/db.ts` becomes `src-lib-db` -- `src/app/api/auth/route.ts` becomes `src-app-api-auth-route` - -Rule: Replace `/` and `.` with `-`, drop file extension. - ---- - -## Example - -```markdown ---- -path: /project/src/lib/auth.ts -type: util -updated: 2025-01-15 -status: active ---- - -# auth.ts - -## Purpose - -JWT token management using jose library. Handles token creation, verification, and refresh rotation. Used by all protected API routes via middleware. - -## Exports - -- `createAccessToken(userId: string): Promise` - Creates 15-min access token -- `createRefreshToken(userId: string): Promise` - Creates 7-day refresh token -- `verifyToken(token: string): Promise` - Validates and decodes token -- `rotateRefresh(oldToken: string): Promise` - Issues new token pair - -## Dependencies - -- [[src-lib-db]] - Stores refresh tokens for revocation -- [[src-types-auth]] - TokenPayload, TokenPair types -- jose - JWT signing and verification -- bcrypt - Password hashing - -## Used By - -- [[src-middleware]] -- [[src-app-api-auth-login-route]] -- [[src-app-api-auth-logout-route]] -- [[src-app-api-auth-refresh-route]] - -## Notes - -- Access tokens are stateless; refresh tokens stored in DB for revocation -- Uses RS256 algorithm with keys from environment -- WARNING: Never log token values, even in debug mode -``` - ---- - -## Guidelines - -**When to create/update:** -- After modifying a file during plan execution -- When encountering a file that lacks documentation -- When relationships change (new imports, exports) - -**Minimal viable entity:** -At minimum, an entity needs frontmatter + Purpose. Other sections can be "TBD" if unknown. - -**Accuracy over completeness:** -Better to have partial accurate info than complete guesses. Mark unknowns explicitly. - -**Link discovery:** -The hook that processes entities extracts all `[[wiki-links]]` to build the relationship graph. Ensure links use correct slug format. diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 18f05335..d21c978f 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -598,8 +598,7 @@ Execute each task in the prompt. **Deviations are normal** - handle them automat - Continue implementing, applying rules as needed - Run the verification - Confirm done criteria met - - **Update intel entities** for modified files (see `` below) - - **Commit the task** (see `` below) - includes entity files in commit + - **Commit the task** (see `` below) - Track task completion and commit hash for Summary documentation - Continue to next task @@ -928,148 +927,6 @@ None - plan executed exactly as written. - - -## Update Codebase Intelligence Entity - -**Trigger:** After each task's `` criteria met, BEFORE committing. - -This step documents your understanding of modified files. The knowledge base self-evolves as you work. - -**1. Get files from the just-completed task:** - -Check the task's `` list. If no `` attribute, use `git status --short` to identify modified files. - -**2. For each file, determine if significant:** - -**Skip these patterns (not worth indexing):** -```bash -# Build/generated -node_modules/*, .next/*, dist/*, build/*, .git/* - -# Tests (their targets are more valuable) -*.test.*, *.spec.*, __tests__/*, __mocks__/* - -# Config files (change rarely, low context value) -package.json, package-lock.json, tsconfig.json, *.config.*, *.config.js, *.config.ts - -# Environment and locks -.env*, *.lock, *.log, yarn.lock, pnpm-lock.yaml -``` - -If file matches skip pattern: continue to next file. - -**3. Derive entity path:** - -```bash -# Convert file path to entity filename -# src/lib/auth.ts → src-lib-auth.md -# app/api/users/route.ts → app-api-users-route.md - -FILE_PATH="$1" -ENTITY_NAME=$(echo "$FILE_PATH" | sed 's|^[./]*||' | tr '/' '-' | sed 's/\.[^.]*$//') -ENTITY_PATH=".planning/intel/entities/${ENTITY_NAME}.md" -``` - -**4. Create intel directory if needed:** - -```bash -mkdir -p .planning/intel/entities -``` - -**5. Check if entity exists:** - -```bash -if [ -f "$ENTITY_PATH" ]; then - ACTION="update" -else - ACTION="create" -fi -``` - -**6. Create or update entity:** - -Use template from `~/.claude/get-shit-done/templates/entity.md`. - -Fill fields based on what you just built: - -| Field | Source | -|-------|--------| -| `path` | Absolute path to file | -| `type` | Infer: module, component, util, config, api, hook | -| `updated` | Today's date (YYYY-MM-DD) | -| `status` | active (unless you're deprecating) | -| **Purpose** | What you understand this file does | -| **Exports** | Extract from the code you just wrote | -| **Dependencies** | `[[slugified-path]]` for internal, plain for external | -| **Used By** | Add callers if you know them from this session | -| **Notes** | Gotchas, patterns, warnings discovered | - -**If updating existing entity:** -- Read current content first -- Preserve Used By entries you didn't touch -- Update sections that changed -- Don't remove information unless it's wrong - -**7. Write entity file:** - -```bash -# Write the entity content -cat > "$ENTITY_PATH" << 'EOF' ---- -path: {path} -type: {type} -updated: {today} -status: active ---- - -# {filename} - -## Purpose - -{purpose} - -## Exports - -{exports} - -## Dependencies - -{dependencies} - -## Used By - -{used_by} - -## Notes - -{notes} -EOF -``` - -**8. Verify entity was created/updated:** - -```bash -head -10 "$ENTITY_PATH" -``` - -**9. Stage entity file:** - -Entity files are staged along with task code files in the task commit. - -```bash -git add "$ENTITY_PATH" -``` - -**Error handling:** -- If entity creation fails: Log warning, continue to task_commit -- Entity update is NOT blocking - a failed entity shouldn't stop code from being committed -- Log: `"Warning: Could not update entity for {file}: {reason}"` - -**Target size:** Keep entities concise (30-50 lines). Purpose and Exports are most valuable. - - - ## TDD Plan Execution diff --git a/hooks/gsd-intel-index.js b/hooks/gsd-intel-index.js deleted file mode 100755 index 8fc64419..00000000 --- a/hooks/gsd-intel-index.js +++ /dev/null @@ -1,1217 +0,0 @@ -#!/usr/bin/env node -// Codebase Intelligence - PostToolUse Indexing Hook -// Indexes file exports/imports when Claude writes or edits JS/TS files -// Also handles entity files for semantic codebase understanding - -/** - * CLI Query Interface - * - * In addition to PostToolUse indexing (Write/Edit actions), this hook supports - * direct graph queries via stdin: - * - * Query dependents (what uses this file?): - * echo '{"action":"query","type":"dependents","target":"src-lib-db"}' | node hooks/gsd-intel-index.js - * - * Query hotspots (most depended-on files): - * echo '{"action":"query","type":"hotspots","limit":5}' | node hooks/gsd-intel-index.js - * - * Options: - * - target: Entity ID (required for dependents, e.g., 'src-lib-db' for src/lib/db.ts) - * - limit: Max results (default: 10 for dependents, 5 for hotspots) - * - maxDepth: Traversal depth for dependents (default: 5) - * - * Output: JSON to stdout with query results - */ - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); -// Use ASM.js version for bundling (no WASM file dependency) -const initSqlJs = require('sql.js/dist/sql-asm.js'); - -// Graph database schema (simple-graph pattern) -const GRAPH_SCHEMA = ` -CREATE TABLE IF NOT EXISTS nodes ( - body TEXT, - id TEXT GENERATED ALWAYS AS (json_extract(body, '$.id')) VIRTUAL NOT NULL UNIQUE -); -CREATE INDEX IF NOT EXISTS id_idx ON nodes(id); - -CREATE TABLE IF NOT EXISTS edges ( - source TEXT NOT NULL, - target TEXT NOT NULL, - relationship TEXT DEFAULT 'depends_on', - UNIQUE(source, target, relationship) ON CONFLICT REPLACE -); -CREATE INDEX IF NOT EXISTS source_idx ON edges(source); -CREATE INDEX IF NOT EXISTS target_idx ON edges(target); -`; - -// Singleton SQL instance (async init, reuse across calls) -let sqlInstance = null; - -/** - * Get or initialize sql.js instance - * Caches the SQL constructor for reuse - */ -async function getSQL() { - if (!sqlInstance) { - sqlInstance = await initSqlJs(); - } - return sqlInstance; -} - -/** - * Load or create the graph database - * Returns { db, dbPath } for operations and persistence - */ -async function loadGraphDatabase() { - const SQL = await getSQL(); - const dbPath = path.join(process.cwd(), '.planning', 'intel', 'graph.db'); - - let db; - if (fs.existsSync(dbPath)) { - const buffer = fs.readFileSync(dbPath); - db = new SQL.Database(buffer); - } else { - db = new SQL.Database(); - db.run(GRAPH_SCHEMA); - } - - return { db, dbPath }; -} - -/** - * Persist database to disk - * Must call after every write operation - */ -function persistDatabase(db, dbPath) { - const data = db.export(); - const buffer = Buffer.from(data); - fs.writeFileSync(dbPath, buffer); -} - -/** - * Get dependency hotspots from graph - * Returns top N files by number of dependents - */ -function getHotspots(db, limit = 5) { - const results = db.exec(` - SELECT - e.target as id, - COUNT(*) as count, - json_extract(n.body, '$.path') as path, - json_extract(n.body, '$.type') as type - FROM edges e - LEFT JOIN nodes n ON e.target = n.id - GROUP BY e.target - ORDER BY count DESC - LIMIT ? - `, [limit]); - - if (!results[0]?.values) return []; - - return results[0].values.map(([id, count, path, type]) => ({ - id, - count, - path: path || id, - type: type || 'unknown' - })); -} - -/** - * Get nodes grouped by type - */ -function getNodesByType(db) { - const results = db.exec(` - SELECT - json_extract(body, '$.type') as type, - COUNT(*) as count - FROM nodes - GROUP BY type - ORDER BY count DESC - `); - - if (!results[0]?.values) return []; - - return results[0].values.map(([type, count]) => ({ - type: type || 'other', - count - })); -} - -/** - * Get all dependents of a file (transitive) - * Uses recursive CTE for graph traversal - */ -function getDependents(db, entityId, maxDepth = 5) { - const results = db.exec(` - WITH RECURSIVE dependents(id, depth) AS ( - SELECT ?, 0 - UNION - SELECT e.source, d.depth + 1 - FROM edges e - JOIN dependents d ON e.target = d.id - WHERE d.depth < ? - ) - SELECT DISTINCT - d.id, - d.depth, - json_extract(n.body, '$.path') as path - FROM dependents d - LEFT JOIN nodes n ON d.id = n.id - WHERE d.id != ? - ORDER BY d.depth, d.id - `, [entityId.toLowerCase(), maxDepth, entityId.toLowerCase()]); - - if (!results[0]?.values) return []; - - return results[0].values.map(([id, depth, path]) => ({ - id, - depth, - path: path || id - })); -} - -// JS/TS file extensions to index -const INDEXABLE_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']; - -// Entity file location -const ENTITY_DIR = '.planning/intel/entities'; - -// Convention detection thresholds -const MIN_SAMPLES = 5; -const MIN_MATCH_RATE = 0.70; - -// Well-known directory purposes -const DIRECTORY_PURPOSES = { - 'components': 'UI components', - 'hooks': 'React/custom hooks', - 'utils': 'Utility functions', - 'lib': 'Utility functions', - 'services': 'Service layer', - 'api': 'API endpoints', - 'routes': 'API endpoints', - 'types': 'TypeScript types', - 'models': 'Data models', - 'tests': 'Test files', - '__tests__': 'Test files', - 'test': 'Test files', - 'spec': 'Test files', - 'controllers': 'Controllers', - 'middleware': 'Middleware', - 'config': 'Configuration', - 'constants': 'Constants', - 'assets': 'Static assets', - 'styles': 'Stylesheets', - 'pages': 'Page components', - 'views': 'View templates' -}; - -// Suffix patterns and their purposes -const SUFFIX_PURPOSES = { - 'test': 'Test files', - 'spec': 'Test files', - 'service': 'Service layer', - 'controller': 'Controllers', - 'model': 'Data models', - 'util': 'Utility functions', - 'utils': 'Utility functions', - 'helper': 'Helper functions', - 'helpers': 'Helper functions', - 'config': 'Configuration', - 'types': 'TypeScript types', - 'type': 'TypeScript types', - 'interface': 'TypeScript interfaces', - 'interfaces': 'TypeScript interfaces', - 'constants': 'Constants', - 'constant': 'Constants', - 'hook': 'React/custom hooks', - 'hooks': 'React/custom hooks', - 'context': 'React context', - 'store': 'State store', - 'slice': 'Redux slice', - 'reducer': 'Redux reducer', - 'action': 'Redux action', - 'actions': 'Redux actions', - 'api': 'API layer', - 'route': 'Route definitions', - 'routes': 'Route definitions', - 'middleware': 'Middleware', - 'schema': 'Schema definitions', - 'styles': 'Stylesheets', - 'mock': 'Mock data', - 'mocks': 'Mock data', - 'fixture': 'Test fixtures', - 'fixtures': 'Test fixtures' -}; - -/** - * Extract import sources from file content - * Returns array of import source paths (e.g., 'react', './utils', '@org/pkg') - */ -function extractImports(content) { - const imports = new Set(); - - // ES6 imports: import { x } from 'y', import x from 'y', import * as x from 'y' - const es6Named = /import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g; - let match; - while ((match = es6Named.exec(content)) !== null) { - imports.add(match[1]); - } - - // ES6 side-effect imports: import 'y' - const es6SideEffect = /import\s+['"]([^'"]+)['"]/g; - while ((match = es6SideEffect.exec(content)) !== null) { - // Avoid matching 'from' part of previous pattern - if (!content.slice(Math.max(0, match.index - 10), match.index).includes('from')) { - imports.add(match[1]); - } - } - - // CommonJS: require('y') - const cjs = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; - while ((match = cjs.exec(content)) !== null) { - imports.add(match[1]); - } - - return Array.from(imports); -} - -/** - * Extract exported symbol names from file content - * Returns array of export names (e.g., 'functionA', 'ClassB', 'default') - */ -function extractExports(content) { - const exports = new Set(); - - // Named exports: export { x, y, z } - const namedExport = /export\s*\{([^}]+)\}/g; - let match; - while ((match = namedExport.exec(content)) !== null) { - const names = match[1].split(',').map(n => { - // Handle "x as y" syntax - export the alias - const parts = n.trim().split(/\s+as\s+/); - return parts[parts.length - 1].trim(); - }).filter(n => n); - names.forEach(n => exports.add(n)); - } - - // Declaration exports: export const|let|var|function|async function|class - const declExport = /export\s+(?:const|let|var|function\*?|async\s+function|class)\s+(\w+)/g; - while ((match = declExport.exec(content)) !== null) { - exports.add(match[1]); - } - - // Default export: export default (with optional identifier) - const defaultExport = /export\s+default\s+(?:function\s*\*?\s*|class\s+)?(\w+)?/g; - while ((match = defaultExport.exec(content)) !== null) { - exports.add('default'); - if (match[1]) { - exports.add(match[1]); - } - } - - // CommonJS: module.exports = { x, y } - const cjsExport = /module\.exports\s*=\s*\{([^}]+)\}/g; - while ((match = cjsExport.exec(content)) !== null) { - const names = match[1].split(',').map(n => { - // Handle "x: y" syntax - export the key - const parts = n.trim().split(/\s*:\s*/); - return parts[0].trim(); - }).filter(n => n && /^\w+$/.test(n)); - names.forEach(n => exports.add(n)); - } - - // CommonJS: module.exports = identifier - const cjsSingleExport = /module\.exports\s*=\s*(\w+)\s*[;\n]/g; - while ((match = cjsSingleExport.exec(content)) !== null) { - exports.add('default'); - exports.add(match[1]); - } - - // TypeScript: export type X, export interface X - const tsExport = /export\s+(?:type|interface)\s+(\w+)/g; - while ((match = tsExport.exec(content)) !== null) { - exports.add(match[1]); - } - - return Array.from(exports); -} - -/** - * Detect naming convention case type for a given name - * Returns: 'camelCase' | 'PascalCase' | 'snake_case' | 'SCREAMING_SNAKE' | 'kebab-case' | null - */ -function detectCase(name) { - if (!name || typeof name !== 'string') return null; - - // Skip 'default' as it's a keyword, not a naming convention indicator - if (name === 'default') return null; - - // Case detection patterns (order matters for specificity) - const patterns = [ - { name: 'SCREAMING_SNAKE', regex: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/ }, - { name: 'snake_case', regex: /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/ }, - { name: 'kebab-case', regex: /^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$/ }, - { name: 'PascalCase', regex: /^[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)*$/ }, - { name: 'camelCase', regex: /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)+$/ } - ]; - - for (const { name: caseName, regex } of patterns) { - if (regex.test(name)) { - return caseName; - } - } - - // Single lowercase word could be camelCase (e.g., 'main', 'app') - if (/^[a-z][a-z0-9]*$/.test(name)) { - return 'camelCase'; - } - - // Single PascalCase word (e.g., 'App', 'Main') - if (/^[A-Z][a-z0-9]+$/.test(name)) { - return 'PascalCase'; - } - - // Single SCREAMING word (e.g., 'DEBUG', 'API') - if (/^[A-Z][A-Z0-9]*$/.test(name)) { - return 'SCREAMING_SNAKE'; - } - - return null; -} - -/** - * Detect conventions from the index - * Analyzes exports, directories, and file suffixes - * Returns conventions object with detected patterns - */ -function detectConventions(index) { - const conventions = { - version: 1, - updated: Date.now(), - naming: {}, - directories: {}, - suffixes: {} - }; - - if (!index || !index.files) { - return conventions; - } - - // Collect all exports across all files for naming analysis - const caseCounts = {}; - let totalExports = 0; - - // Collect directory info - const directoryCounts = {}; - - // Collect suffix patterns - const suffixCounts = {}; - - for (const [filePath, fileData] of Object.entries(index.files)) { - // Analyze exports for naming conventions - if (fileData.exports && Array.isArray(fileData.exports)) { - for (const exportName of fileData.exports) { - const caseType = detectCase(exportName); - if (caseType) { - caseCounts[caseType] = (caseCounts[caseType] || 0) + 1; - totalExports++; - } - } - } - - // Analyze directory structure - const dirParts = filePath.split(path.sep); - for (const dirName of dirParts) { - const purpose = DIRECTORY_PURPOSES[dirName]; - if (purpose) { - const dirKey = dirName; - if (!directoryCounts[dirKey]) { - directoryCounts[dirKey] = { purpose, files: 0 }; - } - directoryCounts[dirKey].files++; - } - } - - // Analyze file suffix patterns - const suffixMatch = filePath.match(/\.([a-z]+)\.(js|ts|jsx|tsx|mjs|cjs)$/i); - if (suffixMatch) { - const suffix = suffixMatch[1].toLowerCase(); - const fullSuffix = `.${suffix}.${suffixMatch[2].toLowerCase()}`; - if (!suffixCounts[fullSuffix]) { - const purpose = SUFFIX_PURPOSES[suffix] || 'Unknown'; - suffixCounts[fullSuffix] = { purpose, count: 0 }; - } - suffixCounts[fullSuffix].count++; - } - } - - // Determine dominant naming convention for exports - if (totalExports >= MIN_SAMPLES) { - let dominant = null; - let maxCount = 0; - - for (const [caseType, count] of Object.entries(caseCounts)) { - if (count > maxCount) { - maxCount = count; - dominant = caseType; - } - } - - if (dominant && (maxCount / totalExports) >= MIN_MATCH_RATE) { - conventions.naming.exports = { - dominant, - count: maxCount, - percentage: Math.round((maxCount / totalExports) * 100) - }; - } - } - - // Include directories with known purposes - for (const [dirName, data] of Object.entries(directoryCounts)) { - conventions.directories[dirName] = { - purpose: data.purpose, - files: data.files - }; - } - - // Include suffix patterns with 5+ occurrences - for (const [suffix, data] of Object.entries(suffixCounts)) { - if (data.count >= MIN_SAMPLES) { - conventions.suffixes[suffix] = { - purpose: data.purpose, - count: data.count - }; - } - } - - return conventions; -} - -/** - * Generate summary.md content from index and conventions - * Target: < 500 tokens for context injection - */ -function generateSummary(index, conventions) { - const lines = []; - const fileCount = Object.keys(index.files || {}).length; - - lines.push('# Codebase Intelligence Summary'); - lines.push(''); - lines.push(`Last updated: ${new Date().toISOString()}`); - lines.push(`Indexed files: ${fileCount}`); - lines.push(''); - - // Naming conventions - if (conventions.naming?.exports?.dominant) { - const n = conventions.naming.exports; - lines.push('## Naming Conventions'); - lines.push(''); - lines.push(`- Export naming: ${n.dominant} (${n.percentage}% of ${n.count} exports)`); - lines.push(''); - } - - // Key directories (top 5) - const dirs = Object.entries(conventions.directories || {}); - if (dirs.length > 0) { - lines.push('## Key Directories'); - lines.push(''); - for (const [dir, info] of dirs.slice(0, 5)) { - lines.push(`- \`${dir}/\`: ${info.purpose} (${info.files} files)`); - } - lines.push(''); - } - - // Suffix patterns (top 3) - const suffixes = Object.entries(conventions.suffixes || {}); - if (suffixes.length > 0) { - lines.push('## File Patterns'); - lines.push(''); - for (const [suffix, info] of suffixes.slice(0, 3)) { - lines.push(`- \`*${suffix}\`: ${info.purpose} (${info.count} files)`); - } - lines.push(''); - } - - // Total exports count - let totalExports = 0; - for (const fileData of Object.values(index.files || {})) { - if (fileData.exports) { - totalExports += fileData.exports.filter(e => e !== 'default').length; - } - } - if (totalExports > 0) { - lines.push(`Total exports: ${totalExports}`); - } - - return lines.join('\n'); -} - -/** - * Check if a file path is an entity file - */ -function isEntityFile(filePath) { - return filePath.includes(ENTITY_DIR + '/') && - filePath.endsWith('.md'); -} - -/** - * Check if a file is a code file we should index - */ -function isCodeFile(filePath) { - const ext = path.extname(filePath).toLowerCase(); - return INDEXABLE_EXTENSIONS.includes(ext); -} - -/** - * Extract [[wiki-links]] from entity content - * Returns array of linked entity names (e.g., 'src-lib-db', 'src-api-auth') - */ -function extractWikiLinks(content) { - const links = []; - const regex = /\[\[([^\]]+)\]\]/g; - let match; - while ((match = regex.exec(content)) !== null) { - links.push(match[1]); - } - return links; -} - -/** - * Parse frontmatter from entity content - * Returns object with frontmatter fields - */ -function parseEntityFrontmatter(content) { - const frontmatter = {}; - const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (fmMatch) { - const fmLines = fmMatch[1].split('\n'); - for (const line of fmLines) { - const colonIdx = line.indexOf(':'); - if (colonIdx > 0) { - const key = line.slice(0, colonIdx).trim(); - const value = line.slice(colonIdx + 1).trim(); - frontmatter[key] = value; - } - } - } - return frontmatter; -} - -/** - * Extract purpose from entity content (first paragraph after ## Purpose) - */ -function extractPurpose(content) { - const purposeMatch = content.match(/## Purpose\s*\n+([^\n#]+)/); - if (purposeMatch) { - return purposeMatch[1].trim(); - } - return null; -} - -/** - * Generate entity slug from file path - * e.g., 'src/lib/db.ts' -> 'src-lib-db' - */ -function generateSlug(filePath) { - return filePath - .replace(/^\/+/, '') // Remove leading slashes - .replace(/\.[^.]+$/, '') // Remove extension - .replace(/[\/\\]/g, '-') // Replace path separators with hyphens - .replace(/[^a-zA-Z0-9-]/g, '-') // Replace non-alphanumeric with hyphens - .replace(/-+/g, '-') // Collapse multiple hyphens - .replace(/^-|-$/g, '') // Remove leading/trailing hyphens - .toLowerCase(); -} - -/** - * Check if signature (exports/imports) changed - * Returns true if entity should be regenerated - */ -function signatureChanged(prevEntry, exports, imports) { - if (!prevEntry) return true; // New file - - const prevExports = JSON.stringify(prevEntry.exports || []); - const prevImports = JSON.stringify(prevEntry.imports || []); - const newExports = JSON.stringify(exports); - const newImports = JSON.stringify(imports); - - return prevExports !== newExports || prevImports !== newImports; -} - -/** - * Generate semantic entity file using Claude - * Spawns `claude -p` to analyze file and generate entity markdown - * - * @param {string} filePath - Path to the code file - * @param {string} content - File content - * @param {Array} exports - Extracted exports - * @param {Array} imports - Extracted imports - */ -async function generateEntity(filePath, content, exports, imports) { - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - const entitiesDir = path.join(intelDir, 'entities'); - - // Ensure entities directory exists - if (!fs.existsSync(entitiesDir)) { - fs.mkdirSync(entitiesDir, { recursive: true }); - } - - const slug = generateSlug(filePath); - const entityPath = path.join(entitiesDir, `${slug}.md`); - const today = new Date().toISOString().split('T')[0]; - - // Build prompt for Claude - const prompt = `Analyze this code file and generate ONLY the entity markdown (no explanation, no code fences). - -Path: ${filePath} -Exports: ${exports.join(', ') || 'none'} -Imports: ${imports.join(', ') || 'none'} - -Content: -${content.slice(0, 3000)}${content.length > 3000 ? '\n... (truncated)' : ''} - -Output this EXACT format (fill in the brackets): - ---- -path: ${filePath} -type: [module|component|util|config|api|hook|service|model|test] -updated: ${today} -status: active ---- - -# ${path.basename(filePath)} - -## Purpose - -[1-2 sentences: What does this file do? Why does it exist?] - -## Exports - -[List each export with brief description, or "None" if no exports] - -## Dependencies - -[List internal deps as [[slug]] wiki-links, external as plain text, or "None"] - -## Used By - -TBD - -## Notes - -[Optional: any important patterns or gotchas, or remove this section]`; - - try { - // Spawn claude -p with timeout - const cmd = `claude -p "${prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`; - const result = execSync(cmd, { - encoding: 'utf8', - timeout: 30000, // 30 second timeout - maxBuffer: 1024 * 1024 // 1MB buffer - }); - - // Write entity file - fs.writeFileSync(entityPath, result.trim()); - - // Sync to graph - await syncEntityToGraph(entityPath); - - return entityPath; - } catch (e) { - // Silent failure - don't block on entity generation errors - return null; - } -} - -/** - * Generate semantic summary from graph database - * Target: < 500 tokens for context injection - */ -async function generateGraphSummary() { - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - const dbPath = path.join(intelDir, 'graph.db'); - const summaryPath = path.join(intelDir, 'summary.md'); - - if (!fs.existsSync(dbPath)) { - return null; - } - - try { - const { db } = await loadGraphDatabase(); - - const lines = []; - lines.push('# Codebase Intelligence'); - lines.push(''); - - const countResult = db.exec('SELECT COUNT(*) FROM nodes'); - const fileCount = countResult[0]?.values[0]?.[0] || 0; - lines.push(`**Indexed entities:** ${fileCount}`); - lines.push(`**Last updated:** ${new Date().toISOString().split('T')[0]}`); - lines.push(''); - - const hotspots = getHotspots(db, 5); - if (hotspots.length > 0) { - lines.push('## Dependency Hotspots'); - lines.push(''); - lines.push('Files with most dependents (change carefully):'); - for (const { path: filePath, count, type } of hotspots) { - const typeLabel = type !== 'unknown' ? ` [${type}]` : ''; - lines.push(`1. \`${filePath}\` (${count} dependents)${typeLabel}`); - } - lines.push(''); - } - - const byType = getNodesByType(db); - if (byType.length > 0) { - lines.push('## Module Types'); - lines.push(''); - for (const { type, count } of byType) { - const label = type.charAt(0).toUpperCase() + type.slice(1); - lines.push(`- **${label}**: ${count} files`); - } - lines.push(''); - } - - const edgeResult = db.exec('SELECT COUNT(*) FROM edges'); - const edgeCount = edgeResult[0]?.values[0]?.[0] || 0; - if (edgeCount > 0) { - lines.push(`**Relationships tracked:** ${edgeCount}`); - lines.push(''); - } - - db.close(); - - const summary = lines.join('\n'); - fs.writeFileSync(summaryPath, summary); - - return summary; - } catch (e) { - return null; - } -} - -/** - * Sync entity file to graph database - * Called when an entity .md file is written - * - * @param {string} entityPath - Path to entity file - */ -async function syncEntityToGraph(entityPath) { - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - - // Opt-in check (same as updateIndex) - if (!fs.existsSync(intelDir)) { - return; - } - - try { - const { db, dbPath } = await loadGraphDatabase(); - - // Read entity file - const content = fs.readFileSync(entityPath, 'utf8'); - const entityId = path.basename(entityPath, '.md').toLowerCase(); - const frontmatter = parseEntityFrontmatter(content); - const links = extractWikiLinks(content); - - // Build node JSON - const nodeBody = JSON.stringify({ - id: entityId, - path: frontmatter.path || entityPath, - type: frontmatter.type || 'unknown', - updated: frontmatter.updated || new Date().toISOString().split('T')[0], - status: frontmatter.status || 'active' - }); - - // Upsert node (ON CONFLICT handled by schema) - db.run( - `INSERT INTO nodes (body) VALUES (?) - ON CONFLICT(id) DO UPDATE SET body = excluded.body`, - [nodeBody] - ); - - // Delete old edges for this source, insert new ones - db.run('DELETE FROM edges WHERE source = ?', [entityId]); - - if (links.length > 0) { - const stmt = db.prepare('INSERT INTO edges (source, target) VALUES (?, ?)'); - for (const target of links) { - stmt.run([entityId, target.toLowerCase()]); - } - stmt.free(); - } - - // Persist to disk (critical - sql.js is in-memory) - persistDatabase(db, dbPath); - db.close(); - } catch (e) { - // Silent failure - never block Claude - // Graph sync is best-effort enhancement - } -} - -/** - * Regenerate summary.md from all entity files - * Creates a semantic overview of the codebase - * Prefers graph-backed summary when graph.db exists - */ -async function regenerateEntitySummary() { - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - const entitiesDir = path.join(intelDir, 'entities'); - const summaryPath = path.join(intelDir, 'summary.md'); - const dbPath = path.join(intelDir, 'graph.db'); - - // Try graph-backed summary first (preferred when graph.db exists) - if (fs.existsSync(dbPath)) { - const graphSummary = await generateGraphSummary(); - if (graphSummary) { - return; // Graph summary written successfully - } - } - - // Fallback: entity-file-based summary - // Check directories exist - if (!fs.existsSync(entitiesDir)) { - return; - } - - // Read all entity files - const entityFiles = fs.readdirSync(entitiesDir) - .filter(f => f.endsWith('.md')) - .map(f => path.join(entitiesDir, f)); - - if (entityFiles.length === 0) { - return; - } - - // Parse all entities - const entities = []; - const dependentCounts = {}; // Track how many things depend on each entity - - for (const filePath of entityFiles) { - try { - const content = fs.readFileSync(filePath, 'utf8'); - const frontmatter = parseEntityFrontmatter(content); - const purpose = extractPurpose(content); - const links = extractWikiLinks(content); - - // Count dependencies (things this entity links to) - for (const link of links) { - dependentCounts[link] = (dependentCounts[link] || 0) + 1; - } - - entities.push({ - name: path.basename(filePath, '.md'), - path: frontmatter.path || null, - type: frontmatter.type || 'unknown', - updated: frontmatter.updated || null, - status: frontmatter.status || 'unknown', - purpose, - links - }); - } catch (e) { - // Skip unreadable files - } - } - - // Generate summary - const lines = []; - lines.push('# Codebase Intelligence'); - lines.push(''); - lines.push(`**Files indexed:** ${entities.length}`); - lines.push(`**Last updated:** ${new Date().toISOString().split('T')[0]}`); - lines.push(''); - - // Group by type - const byType = {}; - for (const entity of entities) { - const type = entity.type || 'other'; - if (!byType[type]) { - byType[type] = []; - } - byType[type].push(entity); - } - - // Key Modules section - lines.push('## Key Modules'); - lines.push(''); - - const typeLabels = { - 'api': 'API Layer', - 'component': 'Components', - 'util': 'Utilities', - 'hook': 'Hooks', - 'service': 'Services', - 'model': 'Models', - 'config': 'Configuration', - 'other': 'Other' - }; - - for (const [type, typeEntities] of Object.entries(byType)) { - if (typeEntities.length === 0) continue; - - const label = typeLabels[type] || type.charAt(0).toUpperCase() + type.slice(1); - lines.push(`### ${label}`); - - // Show up to 5 entities per type, prioritize those with purposes - const withPurpose = typeEntities.filter(e => e.purpose); - const shown = withPurpose.slice(0, 5); - if (shown.length < 5) { - const remaining = typeEntities.filter(e => !e.purpose).slice(0, 5 - shown.length); - shown.push(...remaining); - } - - for (const entity of shown) { - const desc = entity.purpose || 'No description'; - lines.push(`- **${entity.path || entity.name}** - ${desc}`); - } - lines.push(''); - } - - // Dependency Hotspots (most depended-on files) - const hotspots = Object.entries(dependentCounts) - .sort((a, b) => b[1] - a[1]) - .slice(0, 5); - - if (hotspots.length > 0) { - lines.push('## Dependency Hotspots'); - lines.push(''); - lines.push('Files with most dependents (change carefully):'); - - for (const [entityName, count] of hotspots) { - const entity = entities.find(e => e.name === entityName); - const desc = entity?.purpose || ''; - const pathStr = entity?.path || entityName; - lines.push(`1. \`${pathStr}\` (${count} dependents)${desc ? ' - ' + desc : ''}`); - } - lines.push(''); - } - - // Recent Updates (last 5 by date) - const withDates = entities - .filter(e => e.updated) - .sort((a, b) => b.updated.localeCompare(a.updated)) - .slice(0, 5); - - if (withDates.length > 0) { - lines.push('## Recent Updates'); - lines.push(''); - for (const entity of withDates) { - const desc = entity.purpose ? ` - ${entity.purpose}` : ''; - lines.push(`- ${entity.name}.md (${entity.updated})${desc}`); - } - lines.push(''); - } - - // Write summary - fs.writeFileSync(summaryPath, lines.join('\n')); -} - -/** - * Update the index.json file with new file entry - * Uses read-modify-write pattern with synchronous operations - * - * IMPORTANT: Only runs if .planning/intel/ already exists (opt-in behavior). - * Directory is created by /gsd:new-project or /gsd:analyze-codebase. - */ -function updateIndex(filePath, exports, imports) { - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - const indexPath = path.join(intelDir, 'index.json'); - - // Opt-in check: only index if intel directory already exists - // This prevents polluting non-GSD projects - if (!fs.existsSync(intelDir)) { - return; - } - - // Read existing index or create new - let index = { version: 1, updated: null, files: {} }; - try { - const content = fs.readFileSync(indexPath, 'utf8'); - index = JSON.parse(content); - } catch (e) { - // File doesn't exist or invalid JSON - start fresh - } - - // Normalize file path to absolute - const normalizedPath = path.resolve(filePath); - - // Update single file entry (incremental) - index.files[normalizedPath] = { - exports, - imports, - indexed: Date.now() - }; - index.updated = Date.now(); - - // Write atomically (directory already exists per opt-in check above) - fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); - - // Detect and save conventions (regenerated on every index update) - const conventions = detectConventions(index); - const conventionsPath = path.join(process.cwd(), '.planning', 'intel', 'conventions.json'); - fs.writeFileSync(conventionsPath, JSON.stringify(conventions, null, 2)); - - // Generate and write summary.md for context injection - const summary = generateSummary(index, conventions); - const summaryPath = path.join(process.cwd(), '.planning', 'intel', 'summary.md'); - fs.writeFileSync(summaryPath, summary); -} - -/** - * Handle CLI query actions - * Routes to appropriate graph query function based on query type - * - * @param {Object} data - Query action data - * @param {string} data.action - Must be 'query' - * @param {string} data.type - Query type: 'dependents' | 'hotspots' - * @param {string} [data.target] - Entity ID for dependents query (e.g., 'src-lib-db') - * @param {number} [data.limit] - Max results (default: 10 for dependents, 5 for hotspots) - * @param {number} [data.maxDepth] - Max traversal depth for dependents (default: 5) - * @returns {Promise} Query results - */ -async function handleQuery(data) { - const { db, dbPath } = await loadGraphDatabase(); - - try { - switch (data.type) { - case 'dependents': { - if (!data.target) { - return { error: 'target is required for dependents query' }; - } - const results = getDependents(db, data.target, data.maxDepth || 5); - const limited = data.limit ? results.slice(0, data.limit) : results.slice(0, 10); - return { - query: 'dependents', - target: data.target, - count: results.length, - results: limited - }; - } - - case 'hotspots': { - const results = getHotspots(db, data.limit || 5); - return { - query: 'hotspots', - count: results.length, - results - }; - } - - default: - return { error: `Unknown query type: ${data.type}. Valid types: dependents, hotspots` }; - } - } finally { - db.close(); - } -} - -// Read JSON from stdin (standard hook pattern) -let input = ''; -process.stdin.setEncoding('utf8'); -process.stdin.on('data', chunk => input += chunk); -process.stdin.on('end', () => { - try { - const data = JSON.parse(input); - - // Handle query actions (graph queries) - if (data.action === 'query') { - handleQuery(data).then(result => { - console.log(JSON.stringify(result)); - process.exit(0); - }).catch(err => { - console.log(JSON.stringify({ error: err.message })); - process.exit(1); - }); - return; // Don't fall through to Write/Edit handling - } - - // Only process Write and Edit tools - if (!['Write', 'Edit'].includes(data.tool_name)) { - process.exit(0); - } - - const filePath = data.tool_input?.file_path; - if (!filePath) { - process.exit(0); - } - - // Handle entity file writes - sync to graph, regenerate summary - if (isEntityFile(filePath)) { - syncEntityToGraph(filePath).then(async () => { - await regenerateEntitySummary(); - process.exit(0); - }).catch(() => { - // Silent failure - process.exit(0); - }); - return; // Don't exit synchronously, wait for async - } - - // Handle code file writes - existing behavior - if (!isCodeFile(filePath)) { - process.exit(0); - } - - // Get file content - // Write tool provides content in tool_input - // Edit tool only provides old_string/new_string, so read from disk - let content = data.tool_input?.content; - if (!content) { - // Edit tool - read file from disk - const resolvedPath = path.resolve(filePath); - if (fs.existsSync(resolvedPath)) { - content = fs.readFileSync(resolvedPath, 'utf8'); - } else { - // File doesn't exist (shouldn't happen after Edit, but be safe) - process.exit(0); - } - } - - // Extract imports and exports - const exports = extractExports(content); - const imports = extractImports(content); - - // Check if signature changed (triggers entity regeneration) - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - const indexPath = path.join(intelDir, 'index.json'); - let prevEntry = null; - - if (fs.existsSync(indexPath)) { - try { - const indexContent = fs.readFileSync(indexPath, 'utf8'); - const index = JSON.parse(indexContent); - const normalizedPath = path.resolve(filePath); - prevEntry = index.files?.[normalizedPath]; - } catch (e) { - // Ignore read errors - } - } - - // Update index first (always) - updateIndex(filePath, exports, imports); - - // Generate entity if signature changed - if (fs.existsSync(intelDir) && signatureChanged(prevEntry, exports, imports)) { - generateEntity(filePath, content, exports, imports) - .then(async (entityPath) => { - if (entityPath) { - // Regenerate summary after new entity - await regenerateEntitySummary(); - } - process.exit(0); - }) - .catch(() => { - process.exit(0); - }); - return; // Wait for async - } - - process.exit(0); - } catch (error) { - // Silent failure - never block Claude - process.exit(0); - } -}); diff --git a/hooks/gsd-intel-prune.js b/hooks/gsd-intel-prune.js deleted file mode 100644 index aa2c58b0..00000000 --- a/hooks/gsd-intel-prune.js +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env node - -/** - * Intel Prune Hook (Stop event) - * - * Removes stale entries from index.json when files no longer exist. - * Runs after each Claude response to keep intel fresh. - * - * Fast: Only does fs.existsSync checks, no file reading. - * Silent: Never blocks or errors, always exits 0. - */ - -const fs = require('fs'); -const path = require('path'); - -function pruneIndex() { - const intelDir = path.join(process.cwd(), '.planning', 'intel'); - const indexPath = path.join(intelDir, 'index.json'); - - // Only run if intel directory exists (opt-in check) - if (!fs.existsSync(intelDir)) { - return { pruned: 0, total: 0 }; - } - - // Read existing index - let index; - try { - const content = fs.readFileSync(indexPath, 'utf8'); - index = JSON.parse(content); - } catch (e) { - // No index or invalid JSON - return { pruned: 0, total: 0 }; - } - - if (!index.files || typeof index.files !== 'object') { - return { pruned: 0, total: 0 }; - } - - // Check each file and collect deleted ones - const filePaths = Object.keys(index.files); - const deleted = filePaths.filter(filePath => !fs.existsSync(filePath)); - - if (deleted.length === 0) { - return { pruned: 0, total: filePaths.length }; - } - - // Remove deleted entries - for (const filePath of deleted) { - delete index.files[filePath]; - } - index.updated = Date.now(); - - // Write updated index - fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); - - // Regenerate conventions and summary after pruning - // Import detection logic from intel-index.js would be complex, - // so we just update the index. Conventions/summary stay until - // next PostToolUse or /gsd:analyze-codebase refresh. - - return { pruned: deleted.length, total: filePaths.length }; -} - -// Read JSON from stdin (standard hook pattern) -let input = ''; -process.stdin.setEncoding('utf8'); -process.stdin.on('data', chunk => input += chunk); -process.stdin.on('end', () => { - try { - // Stop hook receives session data, but we don't need it - // Just prune stale entries - pruneIndex(); - process.exit(0); - } catch (error) { - // Silent failure - never block Claude - process.exit(0); - } -}); diff --git a/hooks/gsd-intel-session.js b/hooks/gsd-intel-session.js deleted file mode 100644 index 2a65287f..00000000 --- a/hooks/gsd-intel-session.js +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env node -// Codebase Intelligence - SessionStart Context Injection Hook -// Reads pre-generated summary.md and injects into Claude's context - -const fs = require('fs'); -const path = require('path'); - -// Read JSON from stdin (standard hook pattern) -let input = ''; -process.stdin.setEncoding('utf8'); -process.stdin.on('data', chunk => input += chunk); -process.stdin.on('end', () => { - try { - const data = JSON.parse(input); - - // Only inject on startup or resume - if (!['startup', 'resume'].includes(data.source)) { - process.exit(0); - } - - // Read pre-generated summary (created by gsd-intel-index.js) - const summaryPath = path.join(process.cwd(), '.planning', 'intel', 'summary.md'); - - if (!fs.existsSync(summaryPath)) { - process.exit(0); // No intel, skip silently - } - - const summary = fs.readFileSync(summaryPath, 'utf8').trim(); - - if (summary) { - process.stdout.write(`\n${summary}\n`); - } - - process.exit(0); - } catch (error) { - // Silent failure - never block Claude - process.exit(0); - } -}); diff --git a/package.json b/package.json index 44f7c3db..53ac1d40 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,7 @@ }, "dependencies": {}, "devDependencies": { - "esbuild": "^0.24.0", - "sql.js": "^1.12.0" + "esbuild": "^0.24.0" }, "scripts": { "build:hooks": "node scripts/build-hooks.js", diff --git a/scripts/build-hooks.js b/scripts/build-hooks.js index 93cd2cf3..a8c47869 100644 --- a/scripts/build-hooks.js +++ b/scripts/build-hooks.js @@ -1,77 +1,27 @@ #!/usr/bin/env node /** - * Bundle GSD hooks with dependencies for zero-config installation. - * - * sql.js includes a WASM binary that must be inlined for portability. - * This script bundles hooks into self-contained files that work from any cwd. + * Copy GSD hooks to dist for installation. */ -const esbuild = require('esbuild'); const fs = require('fs'); const path = require('path'); const HOOKS_DIR = path.join(__dirname, '..', 'hooks'); const DIST_DIR = path.join(HOOKS_DIR, 'dist'); -// Hooks that need bundling (have npm dependencies) -const HOOKS_TO_BUNDLE = [ - 'gsd-intel-index.js' -]; - -// Hooks that are pure Node.js (just copy) +// Hooks to copy (pure Node.js, no bundling needed) const HOOKS_TO_COPY = [ - 'gsd-intel-session.js', - 'gsd-intel-prune.js', 'gsd-check-update.js', 'gsd-statusline.js' ]; -async function build() { +function build() { // Ensure dist directory exists if (!fs.existsSync(DIST_DIR)) { fs.mkdirSync(DIST_DIR, { recursive: true }); } - // Bundle hooks with dependencies - for (const hook of HOOKS_TO_BUNDLE) { - const entryPoint = path.join(HOOKS_DIR, hook); - const outfile = path.join(DIST_DIR, hook); - - if (!fs.existsSync(entryPoint)) { - console.warn(`Warning: ${hook} not found, skipping`); - continue; - } - - console.log(`Bundling ${hook}...`); - - await esbuild.build({ - entryPoints: [entryPoint], - bundle: true, - platform: 'node', - target: 'node18', - outfile, - format: 'cjs', - // Inline WASM as base64 for sql.js - loader: { - '.wasm': 'binary' - }, - // Don't externalize anything - bundle it all - external: [], - // Minify for smaller package size - minify: true, - // Keep function names for debugging - keepNames: true, - // Handle sql.js WASM loading - define: { - 'process.env.NODE_ENV': '"production"' - } - // Note: shebang preserved from source file by esbuild - }); - - console.log(` → ${outfile}`); - } - - // Copy pure Node.js hooks (no bundling needed) + // Copy hooks to dist for (const hook of HOOKS_TO_COPY) { const src = path.join(HOOKS_DIR, hook); const dest = path.join(DIST_DIR, hook); @@ -89,7 +39,4 @@ async function build() { console.log('\nBuild complete.'); } -build().catch(err => { - console.error('Build failed:', err); - process.exit(1); -}); +build();