From 560d27e1263b141f071d22a21871bdce42738b10 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Sat, 30 May 2026 02:01:57 +0200 Subject: [PATCH 1/2] feat: add `init` command for AI agent adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit codebase-intelligence has the data, but AI agents default to grep/read instead of querying it. `init` closes that gap. `codebase-intelligence init [path]` writes an idempotent, marked instruction block ("query CI before grep/read") into each agent's repo file — AGENTS.md, CLAUDE.md, .cursor/rules/codebase-intelligence.mdc, .github/copilot-instructions.md, GEMINI.md, CONVENTIONS.md — and installs a portable skill to ~/.claude/skills/. Only content between the codebase-intelligence:start/:end markers is touched, so re-running is safe. - src/install: pure managed-block upsert engine + per-agent target registry + shared block/skill content (single source of truth) - skills/codebase-intelligence/SKILL.md: registry skill for ags / npx skills add - flags: --agents , --no-skill, --json - 18 new tests (real fs, temp dirs); drift-guard ties SKILL.md to renderSkill() - docs: README, cli-reference, architecture, llms.txt, llms-full.txt, CHANGELOG.md --- CHANGELOG.md | 40 +++ CLAUDE.md | 1 + README.md | 41 ++- docs/architecture.md | 1 + docs/cli-reference.md | 18 +- llms-full.txt | 14 +- llms.txt | 3 +- skills/codebase-intelligence/SKILL.md | 32 +++ .../active/2026-05-30-agent-adoption-init.md | 82 ++++++ src/cli.ts | 72 +++++- src/install/index.test.ts | 196 ++++++++++++++ src/install/index.ts | 243 ++++++++++++++++++ 12 files changed, 737 insertions(+), 6 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 skills/codebase-intelligence/SKILL.md create mode 100644 specs/active/2026-05-30-agent-adoption-init.md create mode 100644 src/install/index.test.ts create mode 100644 src/install/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a6483e8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- **`init` command** — agent adoption layer. `codebase-intelligence init [path]` writes + an idempotent, marked instruction block ("query CI before grep/read") into each + agent's repo file (`AGENTS.md`, `CLAUDE.md`, + `.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`, + `GEMINI.md`, `CONVENTIONS.md`) and installs a portable skill to + `~/.claude/skills/codebase-intelligence/SKILL.md`. + - `--agents ` to target a subset of agents (default: all). + - `--no-skill` to skip the global skill install. + - `--json` for machine-readable output. + - Writes are idempotent — only content between the + `codebase-intelligence:start`/`:end` markers is ever touched; existing user content + is preserved. +- **`src/install/` module** — managed-block upsert engine, per-agent target registry, + and shared block/skill content (single source of truth). +- **Registry skill** — `skills/codebase-intelligence/SKILL.md`, installable via + `ags install codebase-intelligence` or `npx skills add`. + +### Changed + +- Docs updated for the new command: `README.md`, `docs/cli-reference.md`, + `docs/architecture.md`, `llms.txt`, `llms-full.txt`. + +## [2.3.0] - 2026 + +Baseline for this changelog. For release history prior to and including 2.3.0, see the +[git tags](https://github.com/bntvllnt/codebase-intelligence/tags) and commit history. + +[Unreleased]: https://github.com/bntvllnt/codebase-intelligence/compare/v2.3.0...HEAD +[2.3.0]: https://github.com/bntvllnt/codebase-intelligence/releases/tag/v2.3.0 diff --git a/CLAUDE.md b/CLAUDE.md index 528e99c..3b3e262 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,7 @@ src/ process/index.ts <- Entry point detection + call chain tracing community/index.ts <- Louvain clustering persistence/index.ts <- Graph export/import to .code-visualizer/ + install/index.ts <- Agent adoption: managed-block engine + per-agent files + skill (init) cli.ts <- CLI entry point (commander) docs/ architecture.md <- Pipeline, module map, data flow, design decisions diff --git a/README.md b/README.md index 2fa9d81..f042487 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ claude mcp add -s user -t stdio codebase-intelligence -- npx -y codebase-intelli - [Features](#features) - [Installation](#installation) - [CLI Usage](#cli-usage) +- [Agent Adoption](#agent-adoption) - [MCP Integration (Secondary)](#mcp-integration-secondary) - [Metrics](#metrics) - [Architecture](#architecture) @@ -54,7 +55,7 @@ claude mcp add -s user -t stdio codebase-intelligence -- npx -y codebase-intelli ## Features -- **15 CLI commands** for architecture analysis, dependency impact, dead code detection, and search +- **16 CLI commands** for architecture analysis, dependency impact, dead code detection, search, and agent setup - **Machine-readable JSON output** (`--json`) for automation and CI pipelines - **Auto-cached index** in `.code-visualizer/` for fast repeat queries - **11 architectural metrics** — PageRank, betweenness, coupling, cohesion, tension, churn, complexity, blast radius, dead exports, test coverage, escape velocity @@ -62,6 +63,7 @@ claude mcp add -s user -t stdio codebase-intelligence -- npx -y codebase-intelli - **BM25 search** — ranked keyword search across files and symbols - **Process tracing** — detect entry points and execution flows through the call graph - **Community detection** — Louvain clustering for natural file groupings +- **Agent adoption** — `init` writes per-agent instruction files + installs a skill so AI agents query CI before grep/read - **MCP parity (secondary)** — same analysis available as 15 MCP tools, 2 prompts, and 3 resources ## Installation @@ -104,6 +106,7 @@ codebase-intelligence [options] | `rename` | Reference discovery for rename planning | | `processes` | Entry-point execution flow tracing | | `clusters` | Community-detected file clusters | +| `init` | Make AI agents use CI — writes per-agent instruction files + installs the skill | ### Useful flags @@ -116,6 +119,39 @@ codebase-intelligence [options] For full command details, see [docs/cli-reference.md](docs/cli-reference.md). +## Agent Adoption + +codebase-intelligence has the data — but AI agents only benefit if they actually +*query* it instead of defaulting to grep/read. `init` closes that gap. + +```bash +codebase-intelligence init # current repo, all agents + skill +codebase-intelligence init ./repo --agents claude,agents +codebase-intelligence init --no-skill +``` + +It writes an idempotent, marked instruction block ("query CI before grep/read") into +each agent's native file, and installs a portable skill: + +| Layer | Target | +|---|---| +| Repo instructions | `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, `.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` (Aider) | +| Portable skill | `~/.claude/skills/codebase-intelligence/SKILL.md` | + +Writes are idempotent — only content between the +`` / `:end` markers is ever touched, so re-running +is safe and your own edits are preserved. + +### Install the skill directly + +The skill is also published to the [skills.sh](https://www.skills.sh/) registry: + +```bash +ags install codebase-intelligence +# or +npx skills add github.com/bntvllnt/codebase-intelligence +``` + ## MCP Integration (Secondary) Running without a subcommand starts the MCP stdio server (backward compatible): @@ -208,7 +244,8 @@ codebase-intelligence ## Release -Publishing is automated through GitHub Actions. +Publishing is automated through GitHub Actions. See [CHANGELOG.md](CHANGELOG.md) for +release notes. ### Normal CI (before release) diff --git a/docs/architecture.md b/docs/architecture.md index eef5e9b..47ee643 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -42,6 +42,7 @@ src/ process/index.ts <- Entry point detection + call chain tracing community/index.ts <- Louvain clustering persistence/index.ts <- Graph export/import to .code-visualizer/ + install/index.ts <- Agent adoption: managed-block engine + per-agent file targets + skill server/graph-store.ts <- Global graph state (shared by CLI + MCP) cli.ts <- Entry point, CLI commands + MCP fallback ``` diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 764e21b..339291c 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -1,6 +1,6 @@ # CLI Reference -15 commands for terminal and CI use. Full parity with MCP tools. All commands auto-cache the index to `.code-visualizer/`. +16 commands for terminal and CI use. The 15 analysis commands have full parity with MCP tools and auto-cache the index to `.code-visualizer/`; `init` sets up agent adoption. ## Commands @@ -156,6 +156,20 @@ codebase-intelligence clusters [--min-files ] [--json] [--force] **Output:** clusters with files, file count, cohesion. +### init + +Make AI agents use codebase-intelligence: write a managed instruction block into each +agent's repo file (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, +`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md`) and install the +portable skill to `~/.claude/skills/`. Idempotent — only content between the +`codebase-intelligence:start`/`:end` markers is touched. + +```bash +codebase-intelligence init [path] [--agents ] [--no-skill] [--json] +``` + +**Output:** per-file actions (created / updated / unchanged) and skill install status. + ## Flags | Flag | Available On | Description | @@ -173,6 +187,8 @@ codebase-intelligence clusters [--min-files ] [--json] [--force] | `--entry ` | processes | Filter by entry point name | | `--min-files ` | clusters | Min files per cluster | | `--no-dry-run` | rename | Actually perform the rename (default: dry run) | +| `--agents ` | init | Comma-separated agents (default: all) | +| `--no-skill` | init | Skip installing the global Claude skill | ## Behavior diff --git a/llms-full.txt b/llms-full.txt index 0523749..49897ec 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -283,7 +283,7 @@ Community-detected file clusters. Input: `{ minFiles?: number }`. Returns: clust # CLI Reference -15 commands — full parity with MCP tools. +16 commands — 15 analysis commands (full parity with MCP tools) plus `init` for agent adoption. ## Commands @@ -377,6 +377,18 @@ codebase-intelligence clusters [--min-files ] [--json] [--force] ``` Community-detected file clusters (Louvain algorithm). +### init +```bash +codebase-intelligence init [path] [--agents ] [--no-skill] [--json] +``` +Make AI agents actually use codebase-intelligence. Writes an idempotent, marked +instruction block ("query CI before grep/read") into each agent's repo file — +`AGENTS.md`, `CLAUDE.md`, `.cursor/rules/codebase-intelligence.mdc`, +`.github/copilot-instructions.md`, `GEMINI.md`, `CONVENTIONS.md` — and installs the +portable skill to `~/.claude/skills/codebase-intelligence/SKILL.md`. Only content +between the `codebase-intelligence:start`/`:end` markers is ever touched, so re-running +is safe. `--agents` limits targets (default: all); `--no-skill` skips the skill. + ## Global Behavior - **Auto-caching**: First run parses and saves index to `.code-visualizer/`. Subsequent runs use cache if HEAD unchanged. diff --git a/llms.txt b/llms.txt index 90b2040..51ca681 100644 --- a/llms.txt +++ b/llms.txt @@ -17,7 +17,7 @@ MCP mode (AI agents): codebase-intelligence ./path/to/project ``` -CLI mode (humans/CI) — 15 commands, full MCP parity: +CLI mode (humans/CI) — 16 commands (15 analysis with MCP parity + `init` for agent adoption): ```bash codebase-intelligence overview ./src codebase-intelligence hotspots ./src --metric coupling @@ -34,6 +34,7 @@ codebase-intelligence impact ./src getUserById codebase-intelligence rename ./src oldName newName codebase-intelligence processes ./src --entry main codebase-intelligence clusters ./src --min-files 3 +codebase-intelligence init . # make AI agents use CI ``` ## Optional diff --git a/skills/codebase-intelligence/SKILL.md b/skills/codebase-intelligence/SKILL.md new file mode 100644 index 0000000..98485ce --- /dev/null +++ b/skills/codebase-intelligence/SKILL.md @@ -0,0 +1,32 @@ +--- +name: codebase-intelligence +description: Query the codebase-intelligence CLI to understand TypeScript architecture, dependencies, blast radius, and risk before reading files. Use for any "how is this structured", "what breaks if I change X", "where is the complexity" question. +--- + +# Codebase Intelligence + +`codebase-intelligence` turns a TypeScript codebase into a queryable graph of +architecture, dependencies, and risk metrics. **Prefer it over grep/read** when the +task is about structure, impact, or risk — it is faster and more accurate than +scanning files one by one. + +## When to use + +| Goal | Command | +|------|---------| +| First look / architecture | `codebase-intelligence overview ` | +| Risk & complexity ranking | `codebase-intelligence hotspots ` | +| Impact of changing a symbol | `codebase-intelligence impact ` | +| File-level blast radius | `codebase-intelligence dependents ` | +| Unused exports | `codebase-intelligence dead-exports ` | +| Keyword search | `codebase-intelligence search ` | +| Rename planning | `codebase-intelligence rename ` | +| Module structure | `codebase-intelligence modules ` | + +## Rules + +- Run `overview` first to orient, then drill down (hotspots → file/symbol → impact). +- Always pass `--json` in automation/subagents for structured output. +- Use `impact`/`dependents` BEFORE editing to gauge blast radius. +- No global install? Prefix any command with `npx codebase-intelligence@latest`. +- Full reference: `codebase-intelligence --help`. diff --git a/specs/active/2026-05-30-agent-adoption-init.md b/specs/active/2026-05-30-agent-adoption-init.md new file mode 100644 index 0000000..16759f0 --- /dev/null +++ b/specs/active/2026-05-30-agent-adoption-init.md @@ -0,0 +1,82 @@ +# Spec: Agent Adoption — `init` command + +## Problem + +codebase-intelligence has the data (architecture, impact, risk metrics) but AI coding +agents don't *use* it. They default to grep/read. Missing layer: durable, in-repo +instructions + a portable skill that tell agents "query CI first." + +This is the adoption/distribution layer — persistent agent instructions + an installable +skill — implemented natively in TypeScript. No new runtime deps, no LLM, no Python. + +## Goal + +One command — `codebase-intelligence init [path]` — that: +1. Writes a managed instruction block into per-agent repo files (6 agents). +2. Installs a portable Claude skill (`~/.claude/skills/`). +3. (Maintainer side) ship a registry-ready SKILL.md for skills.sh / `ags install`. + +## Design + +``` +init [path] + ├── installRepoFiles(repoRoot, targets) → Layer 1 (committed, team-wide) + └── installGlobalSkill(homeDir) → Layer 2 (per-dev Claude skill) + +renderBlock() ── single source of truth ──► upsertManagedBlock() ──► each target file +renderSkill() ── single source of truth ──► SKILL.md (global + registry) +``` + +### Managed-block engine (correctness core) + +`upsertManagedBlock(existing, block, markers) -> string` — pure, fs-free. + +- no markers in `existing` → append block (preserve original). +- markers present → replace between (preserve content before & after). +- empty `existing` → block only. +- idempotent: f(f(x)) == f(x). + +Markers (HTML comments, work in all markdown/.mdc): +``` + + +``` + +### Agent target table (single source, adapters differ by path/preamble) + +| id | repo file | preamble | +|----|-----------|----------| +| agents | `AGENTS.md` | — (cross-agent std; covers Codex) | +| claude | `CLAUDE.md` | — | +| cursor | `.cursor/rules/codebase-intelligence.mdc` | mdc frontmatter | +| copilot | `.github/copilot-instructions.md` | — | +| gemini | `GEMINI.md` | — | +| aider | `CONVENTIONS.md` | — | + +### Instruction block content + +Discovery-first mandate ("use BEFORE grep/read for architecture/impact/risk") + +command cheatsheet table + `--json` note + MCP pointer. + +### Global skill / registry + +- `installGlobalSkill` → `~/.claude/skills/codebase-intelligence/SKILL.md` (skill = Claude concept). +- Registry: commit `skills/codebase-intelligence/SKILL.md` + README `ags install` / `npx skills add` docs. + skills.sh directory submission = manual web/PR step (flagged, can't automate). + +## State Machine + +N/A — Stateless. Each `init` run is an idempotent upsert (input files → output files). +No transitions; re-runnable to convergence. + +## Test Plan (real fs, temp dirs — no mocks) + +- upsertManagedBlock: empty / append / replace / idempotent / preserve-outside. +- installRepoFiles: creates all targets, merges into pre-existing CLAUDE.md without clobber. +- renderBlock/renderSkill: contain mandate keywords + command names. + +## Out of Scope + +- Auto-indexing on init (block tells agent to run overview). +- Non-Claude global skill dirs (other agents covered by repo files + ags distribution). +- Visualization / narrative report / multimodal outputs (separate, future work). diff --git a/src/cli.ts b/src/cli.ts index 347c902..fe9f908 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -40,7 +40,14 @@ import { impactAnalysis, renameSymbol, } from "./core/index.js"; +import { + installRepoFiles, + installGlobalSkill, + isAgentId, + ALL_AGENT_IDS, +} from "./install/index.js"; import type { CodebaseGraph } from "./types/index.js"; +import type { AgentId } from "./install/index.js"; const INDEX_DIR_NAME = ".code-visualizer"; @@ -179,6 +186,12 @@ interface McpOptions { clean?: boolean; } +interface InitOptions { + agents?: string; + skill?: boolean; + json?: boolean; +} + const program = new Command(); program @@ -200,7 +213,8 @@ program " impact Symbol-level blast radius\n" + " rename Find references for rename\n" + " processes Entry point execution flows\n" + - " clusters Community-detected file clusters\n\n" + + " clusters Community-detected file clusters\n" + + " init [path] Make AI agents use CI (writes agent instruction files + skill)\n\n" + "MCP mode:\n" + " codebase-intelligence Start MCP stdio server\n\n" + "Try: codebase-intelligence overview ./src", @@ -935,6 +949,62 @@ program } }); +// ── Subcommand: init ─────────────────────────────────────── + +program + .command("init") + .description("Make AI agents use codebase-intelligence: write per-agent instruction files + install the skill") + .argument("[path]", "Repo root (default: current directory)", ".") + .option("--agents ", `Comma-separated agents to target (default: all). Available: ${ALL_AGENT_IDS.join(", ")}`) + .option("--no-skill", "Skip installing the global Claude skill") + .option("--json", "Output as JSON") + .action((targetPath: string, options: InitOptions) => { + const resolved = path.resolve(targetPath); + if (!fs.existsSync(resolved)) { + process.stderr.write(`Error: Path does not exist: ${targetPath}\n`); + process.exit(1); + } + + let agents: AgentId[] | undefined; + if (options.agents) { + const requested = options.agents + .split(",") + .map((a) => a.trim()) + .filter(Boolean); + const invalid = requested.filter((a) => !isAgentId(a)); + if (invalid.length > 0) { + process.stderr.write( + `Error: Unknown agents: ${invalid.join(", ")}. Available: ${ALL_AGENT_IDS.join(", ")}\n`, + ); + process.exit(2); + } + agents = requested.filter(isAgentId); + } + + const repoResults = installRepoFiles(resolved, { agents }); + const skillResult = options.skill === false ? undefined : installGlobalSkill(); + + if (options.json) { + outputJson({ repoFiles: repoResults, skill: skillResult ?? null }); + return; + } + + output(`Codebase Intelligence — agent adoption`); + output(`──────────────────────────────────────`); + output(`Repo instruction files (${resolved}):`); + for (const r of repoResults) { + output(` ${r.action.padEnd(9)} ${r.path}`); + } + if (skillResult) { + output(``); + output(`Global skill:`); + output(` ${skillResult.action.padEnd(9)} ${skillResult.path}`); + } + output(``); + output(`Done. Agents in this repo will now be told to query codebase-intelligence first.`); + output(`Re-run anytime — writes are idempotent (managed blocks only).`); + }); + // ── MCP fallback (backward compat) ────────────────────────── program diff --git a/src/install/index.test.ts b/src/install/index.test.ts new file mode 100644 index 0000000..c07eb22 --- /dev/null +++ b/src/install/index.test.ts @@ -0,0 +1,196 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import { fileURLToPath } from "node:url"; +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + upsertManagedBlock, + ensurePreamble, + renderBlock, + renderSkill, + installRepoFiles, + installGlobalSkill, + isAgentId, + AGENT_TARGETS, + ALL_AGENT_IDS, + DEFAULT_MARKERS, +} from "./index.js"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(here, "..", ".."); + +describe("upsertManagedBlock", () => { + it("wraps the block with markers for empty input", () => { + const out = upsertManagedBlock("", "BODY"); + expect(out).toContain(DEFAULT_MARKERS.start); + expect(out).toContain(DEFAULT_MARKERS.end); + expect(out).toContain("BODY"); + expect(out.endsWith("\n")).toBe(true); + }); + + it("appends to existing content without markers, preserving it", () => { + const existing = "# My Notes\n\nimportant user content\n"; + const out = upsertManagedBlock(existing, "BODY"); + expect(out).toContain("important user content"); + expect(out.indexOf("important user content")).toBeLessThan(out.indexOf(DEFAULT_MARKERS.start)); + expect(out).toContain("BODY"); + }); + + it("replaces only the managed region, preserving content before and after", () => { + const first = upsertManagedBlock("BEFORE\n", "OLD BODY"); + const withAfter = `${first}\nAFTER USER CONTENT\n`; + const out = upsertManagedBlock(withAfter, "NEW BODY"); + expect(out).toContain("BEFORE"); + expect(out).toContain("AFTER USER CONTENT"); + expect(out).toContain("NEW BODY"); + expect(out).not.toContain("OLD BODY"); + expect(out.match(new RegExp(DEFAULT_MARKERS.end.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"), "g"))?.length).toBe(1); + }); + + it("is idempotent", () => { + const once = upsertManagedBlock("seed\n", "BODY"); + const twice = upsertManagedBlock(once, "BODY"); + expect(twice).toBe(once); + }); +}); + +describe("ensurePreamble", () => { + it("returns preamble for empty content", () => { + expect(ensurePreamble("", "---\nx: 1\n---\n")).toBe("---\nx: 1\n---\n"); + }); + + it("leaves content that already has frontmatter untouched", () => { + const content = "---\nexisting: true\n---\nbody\n"; + expect(ensurePreamble(content, "---\nx: 1\n---\n")).toBe(content); + }); + + it("prepends preamble when content lacks frontmatter", () => { + const out = ensurePreamble("plain body\n", "---\nx: 1\n---\n"); + expect(out.startsWith("---\nx: 1\n---")).toBe(true); + expect(out).toContain("plain body"); + }); +}); + +describe("renderBlock / renderSkill content", () => { + it("block states the discovery-first mandate and key commands", () => { + const block = renderBlock(); + expect(block).toMatch(/BEFORE grep\/read/i); + for (const cmd of ["overview", "hotspots", "impact", "dependents", "dead-exports"]) { + expect(block).toContain(cmd); + } + expect(block).toContain("--json"); + }); + + it("skill has valid registry frontmatter and prefers CLI over grep", () => { + const skill = renderSkill(); + expect(skill.startsWith("---\n")).toBe(true); + expect(skill).toMatch(/name: codebase-intelligence/); + expect(skill).toMatch(/Prefer it over grep\/read/i); + }); +}); + +describe("registry skill file stays in sync with renderSkill()", () => { + it("committed skills/codebase-intelligence/SKILL.md equals renderSkill() output", () => { + const skillPath = path.join(repoRoot, "skills", "codebase-intelligence", "SKILL.md"); + const onDisk = fs.readFileSync(skillPath, "utf-8"); + expect(onDisk).toBe(renderSkill()); + }); +}); + +describe("isAgentId", () => { + it("accepts known ids and rejects unknown", () => { + expect(isAgentId("claude")).toBe(true); + expect(isAgentId("agents")).toBe(true); + expect(isAgentId("nope")).toBe(false); + }); +}); + +describe("installRepoFiles", () => { + let tmp: string; + + beforeEach(() => { + tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ci-init-repo-")); + }); + + afterEach(() => { + fs.rmSync(tmp, { recursive: true, force: true }); + }); + + it("creates every agent file with the managed block", () => { + const results = installRepoFiles(tmp); + expect(results).toHaveLength(AGENT_TARGETS.length); + for (const target of AGENT_TARGETS) { + const filePath = path.join(tmp, target.file); + expect(fs.existsSync(filePath)).toBe(true); + const content = fs.readFileSync(filePath, "utf-8"); + expect(content).toContain(DEFAULT_MARKERS.start); + expect(content).toContain("Codebase Intelligence"); + } + expect(results.every((r) => r.action === "created")).toBe(true); + }); + + it("adds frontmatter to the Cursor .mdc target", () => { + installRepoFiles(tmp, { agents: ["cursor"] }); + const cursorTarget = AGENT_TARGETS.find((t) => t.id === "cursor"); + if (!cursorTarget) throw new Error("cursor target missing from registry"); + const content = fs.readFileSync(path.join(tmp, cursorTarget.file), "utf-8"); + expect(content.startsWith("---\n")).toBe(true); + expect(content).toContain("alwaysApply: true"); + }); + + it("honors the agents subset", () => { + const results = installRepoFiles(tmp, { agents: ["claude"] }); + expect(results).toHaveLength(1); + expect(results[0].path).toBe("CLAUDE.md"); + expect(fs.existsSync(path.join(tmp, "AGENTS.md"))).toBe(false); + }); + + it("merges into a pre-existing file without clobbering user content", () => { + const claudePath = path.join(tmp, "CLAUDE.md"); + fs.writeFileSync(claudePath, "# Project Rules\n\nDo not delete me.\n", "utf-8"); + + installRepoFiles(tmp, { agents: ["claude"] }); + + const content = fs.readFileSync(claudePath, "utf-8"); + expect(content).toContain("Do not delete me."); + expect(content).toContain(DEFAULT_MARKERS.start); + }); + + it("is idempotent — a second run reports unchanged and content is stable", () => { + installRepoFiles(tmp); + const before = AGENT_TARGETS.map((t) => fs.readFileSync(path.join(tmp, t.file), "utf-8")); + + const second = installRepoFiles(tmp); + expect(second.every((r) => r.action === "unchanged")).toBe(true); + + const after = AGENT_TARGETS.map((t) => fs.readFileSync(path.join(tmp, t.file), "utf-8")); + expect(after).toEqual(before); + }); + + it("covers all registry ids in ALL_AGENT_IDS", () => { + expect([...ALL_AGENT_IDS].sort()).toEqual(AGENT_TARGETS.map((t) => t.id).sort()); + }); +}); + +describe("installGlobalSkill", () => { + let home: string; + + beforeEach(() => { + home = fs.mkdtempSync(path.join(os.tmpdir(), "ci-init-home-")); + }); + + afterEach(() => { + fs.rmSync(home, { recursive: true, force: true }); + }); + + it("writes the skill to ~/.claude/skills and is idempotent", () => { + const first = installGlobalSkill(home); + const skillPath = path.join(home, ".claude", "skills", "codebase-intelligence", "SKILL.md"); + expect(first.action).toBe("created"); + expect(first.path).toBe(skillPath); + expect(fs.readFileSync(skillPath, "utf-8")).toBe(renderSkill()); + + const second = installGlobalSkill(home); + expect(second.action).toBe("unchanged"); + }); +}); diff --git a/src/install/index.ts b/src/install/index.ts new file mode 100644 index 0000000..00ff831 --- /dev/null +++ b/src/install/index.ts @@ -0,0 +1,243 @@ +import fs from "fs"; +import path from "path"; +import os from "os"; + +// ── Managed-block engine ──────────────────────────────────── +// +// Idempotent upsert of an auto-generated block into a (possibly +// pre-existing, user-owned) text file. Content outside the markers is +// never touched, so re-running `init` converges instead of clobbering. + +export interface BlockMarkers { + start: string; + end: string; +} + +export const DEFAULT_MARKERS: BlockMarkers = { + start: "", + end: "", +}; + +/** + * Insert or replace a managed block in `existing` content. + * + * - Both markers present (in order) → replace the region between them. + * - Otherwise → append the block, preserving all existing content. + * + * Pure and filesystem-free. `upsert(upsert(x)) === upsert(x)`. + * + * @param existing - Current file content ("" if the file does not exist). + * @param block - Block body to manage (markers are added automatically). + * @param markers - Comment markers delimiting the managed region. + * @returns The new file content, always newline-terminated. + */ +export function upsertManagedBlock( + existing: string, + block: string, + markers: BlockMarkers = DEFAULT_MARKERS, +): string { + const wrapped = `${markers.start}\n${block.trim()}\n${markers.end}`; + + const startIdx = existing.indexOf(markers.start); + const endIdx = existing.indexOf(markers.end, startIdx + markers.start.length); + + if (startIdx !== -1 && endIdx !== -1) { + const before = existing.slice(0, startIdx); + const after = existing.slice(endIdx + markers.end.length); + return `${before}${wrapped}${after}`; + } + + if (existing.trim() === "") { + return `${wrapped}\n`; + } + + return `${existing.replace(/\s+$/, "")}\n\n${wrapped}\n`; +} + +/** + * Ensure `content` begins with a frontmatter preamble (e.g. Cursor `.mdc`). + * No-op when frontmatter (`---`) is already present. + */ +export function ensurePreamble(content: string, preamble: string): string { + if (content.trimStart().startsWith("---")) { + return content; + } + if (content.trim() === "") { + return preamble; + } + return `${preamble.replace(/\s+$/, "")}\n\n${content}`; +} + +// ── Agent target registry ─────────────────────────────────── +// +// Single source of truth for which files each agent reads. Adapters +// differ only by path / optional frontmatter — the block content is +// shared (see renderBlock). + +export type AgentId = "agents" | "claude" | "cursor" | "copilot" | "gemini" | "aider"; + +export interface AgentTarget { + id: AgentId; + label: string; + /** Repo-relative path to the instruction file. */ + file: string; + /** Frontmatter ensured at the top of the file when present. */ + preamble?: string; +} + +const CURSOR_PREAMBLE = `--- +description: Codebase Intelligence — query the CLI before grep/read for architecture, impact, and risk +alwaysApply: true +--- +`; + +export const AGENT_TARGETS: readonly AgentTarget[] = [ + { id: "agents", label: "AGENTS.md (cross-agent standard, incl. Codex)", file: "AGENTS.md" }, + { id: "claude", label: "Claude Code", file: "CLAUDE.md" }, + { + id: "cursor", + label: "Cursor", + file: path.join(".cursor", "rules", "codebase-intelligence.mdc"), + preamble: CURSOR_PREAMBLE, + }, + { id: "copilot", label: "GitHub Copilot", file: path.join(".github", "copilot-instructions.md") }, + { id: "gemini", label: "Gemini CLI", file: "GEMINI.md" }, + { id: "aider", label: "Aider", file: "CONVENTIONS.md" }, +]; + +export const ALL_AGENT_IDS: readonly AgentId[] = AGENT_TARGETS.map((t) => t.id); + +/** Runtime guard: is `value` a known agent id? */ +export function isAgentId(value: string): value is AgentId { + return (ALL_AGENT_IDS as readonly string[]).includes(value); +} + +// ── Content (single source of truth) ──────────────────────── + +/** + * The instruction block injected into every agent file. This is the + * product: it tells agents to query CI before grep/read. + */ +export function renderBlock(): string { + return `## Codebase Intelligence + +This repo is analyzed by \`codebase-intelligence\` — a static-analysis tool that maps +architecture, dependencies, and risk metrics for TypeScript code. + +**Use it BEFORE grep/read when you need to understand structure, impact, or risk.** +It is faster and more accurate than reading files one at a time. + +| When you need to… | Run | +|---|---| +| Understand the codebase | \`codebase-intelligence overview .\` | +| Find where complexity/risk lives | \`codebase-intelligence hotspots .\` | +| Blast radius before editing a symbol | \`codebase-intelligence impact . \` | +| Who depends on a file | \`codebase-intelligence dependents . \` | +| Find unused exports | \`codebase-intelligence dead-exports .\` | +| Keyword search | \`codebase-intelligence search . \` | +| Plan a rename | \`codebase-intelligence rename . \` | + +- Add \`--json\` for machine-readable output (use in automation/subagents). +- No global install? Prefix with \`npx codebase-intelligence@latest\`. +- Full command list: \`codebase-intelligence --help\`. +- MCP stdio server: \`codebase-intelligence .\``; +} + +/** + * The portable skill body (frontmatter + instructions). Installed to + * `~/.claude/skills/` and shipped to the skills.sh registry. + */ +export function renderSkill(): string { + return `--- +name: codebase-intelligence +description: Query the codebase-intelligence CLI to understand TypeScript architecture, dependencies, blast radius, and risk before reading files. Use for any "how is this structured", "what breaks if I change X", "where is the complexity" question. +--- + +# Codebase Intelligence + +\`codebase-intelligence\` turns a TypeScript codebase into a queryable graph of +architecture, dependencies, and risk metrics. **Prefer it over grep/read** when the +task is about structure, impact, or risk — it is faster and more accurate than +scanning files one by one. + +## When to use + +| Goal | Command | +|------|---------| +| First look / architecture | \`codebase-intelligence overview \` | +| Risk & complexity ranking | \`codebase-intelligence hotspots \` | +| Impact of changing a symbol | \`codebase-intelligence impact \` | +| File-level blast radius | \`codebase-intelligence dependents \` | +| Unused exports | \`codebase-intelligence dead-exports \` | +| Keyword search | \`codebase-intelligence search \` | +| Rename planning | \`codebase-intelligence rename \` | +| Module structure | \`codebase-intelligence modules \` | + +## Rules + +- Run \`overview\` first to orient, then drill down (hotspots → file/symbol → impact). +- Always pass \`--json\` in automation/subagents for structured output. +- Use \`impact\`/\`dependents\` BEFORE editing to gauge blast radius. +- No global install? Prefix any command with \`npx codebase-intelligence@latest\`. +- Full reference: \`codebase-intelligence --help\`. +`; +} + +// ── Install operations (filesystem) ───────────────────────── + +export interface InstallResult { + path: string; + action: "created" | "updated" | "unchanged"; +} + +export interface InstallRepoOptions { + /** Subset of agents to target. Defaults to all. */ + agents?: readonly AgentId[]; +} + +/** Write the instruction block into each selected agent's repo file. */ +export function installRepoFiles(repoRoot: string, options: InstallRepoOptions = {}): InstallResult[] { + const selected = options.agents ?? ALL_AGENT_IDS; + const block = renderBlock(); + const results: InstallResult[] = []; + + for (const target of AGENT_TARGETS) { + if (!selected.includes(target.id)) continue; + + const filePath = path.join(repoRoot, target.file); + const existedBefore = fs.existsSync(filePath); + const original = existedBefore ? fs.readFileSync(filePath, "utf-8") : ""; + + const base = target.preamble ? ensurePreamble(original, target.preamble) : original; + const next = upsertManagedBlock(base, block); + + if (existedBefore && original === next) { + results.push({ path: target.file, action: "unchanged" }); + continue; + } + + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, next, "utf-8"); + results.push({ path: target.file, action: existedBefore ? "updated" : "created" }); + } + + return results; +} + +/** Install the portable skill into the per-user Claude skills directory. */ +export function installGlobalSkill(homeDir: string = os.homedir()): InstallResult { + const dir = path.join(homeDir, ".claude", "skills", "codebase-intelligence"); + const filePath = path.join(dir, "SKILL.md"); + const skill = renderSkill(); + + const existedBefore = fs.existsSync(filePath); + const original = existedBefore ? fs.readFileSync(filePath, "utf-8") : ""; + + if (existedBefore && original === skill) { + return { path: filePath, action: "unchanged" }; + } + + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, skill, "utf-8"); + return { path: filePath, action: existedBefore ? "updated" : "created" }; +} From ac2ad41f7252815a38e745c34861430a1d691a62 Mon Sep 17 00:00:00 2001 From: bntvllnt <32437578+bntvllnt@users.noreply.github.com> Date: Sat, 30 May 2026 02:07:33 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(spec):=20ship=20agent-adoption-init?= =?UTF-8?q?=20=E2=80=94=20move=20active=20=E2=86=92=20shipped=20+=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/history.log | 1 + .../2026-05-30-feat-agent-adoption-init.md} | 0 2 files changed, 1 insertion(+) rename specs/{active/2026-05-30-agent-adoption-init.md => shipped/2026-05-30-feat-agent-adoption-init.md} (100%) diff --git a/specs/history.log b/specs/history.log index ae079fb..38699ac 100644 --- a/specs/history.log +++ b/specs/history.log @@ -4,3 +4,4 @@ 2026-03-11 | shipped | fix-error-handling | 1h→0.5h | 1d | Consistent impact_analysis error handling, LOC off-by-one fix, empty file guard. 17 regression tests. 2026-03-11 | shipped | fix-metrics-test-files | 2h→1.5h | 1d | Exclude test files from coverage/coupling metrics, isTestFile detection, coupling formula fix. 19 regression tests. 2026-03-11 | shipped | feat-metric-quality | 3h→2h | 1d | LEAF verdict for single-file modules, tension suppression for type hubs/entry points, file_context path normalization. 20 regression tests. +2026-05-30 | shipped | feat-agent-adoption-init | 3h→1h | 1d | `init` command: idempotent managed-block instructions for 6 agents + portable skill + registry SKILL.md. 18 tests, docs + CHANGELOG. PR #34. diff --git a/specs/active/2026-05-30-agent-adoption-init.md b/specs/shipped/2026-05-30-feat-agent-adoption-init.md similarity index 100% rename from specs/active/2026-05-30-agent-adoption-init.md rename to specs/shipped/2026-05-30-feat-agent-adoption-init.md