commit f2c2ef54e4d2a777403ae73addcba1b9d998df27 Author: Pavel Malkin Date: Wed Apr 22 03:46:38 2026 +0300 initial: design + plan for kg-setup skill Port the design spec and 17-task implementation plan from arb-scanner (where the idea was born) to this dedicated repo. Paths in the plan adjusted to treat this repo root as the skill root (no skills/kg-setup/ subdirectory). Implementation follows via subagent-driven-development. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10b940d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.pytest_cache/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..5023b73 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# kg-setup + +Claude Code skill that bootstraps a 4-layer project memory system +(CodeGraph + GitNexus MCP servers + project CLAUDE.md section + +Obsidian vault folder + auto-memory pointer) in any local project on macOS. + +**Status:** Design complete, implementation pending. + +- Design: [`docs/design.md`](docs/design.md) +- Plan: [`docs/plan.md`](docs/plan.md) + +Implementation is executed via `superpowers:subagent-driven-development` +against `docs/plan.md`. Task 1 of the plan replaces this README with the +full user-facing version. diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..368d373 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,399 @@ +# kg-setup — Knowledge Graph bootstrap skill + +**Date:** 2026-04-22 +**Status:** Design approved via brainstorming, awaiting user review before plan. +**Author/User:** pavelmalkin +**Skill location:** `~/.claude/skills/kg-setup/` + +## 1. Goal + +Build a Claude Code skill that bootstraps a 4-layer project memory system in any project on this Mac by a single natural-language trigger. Primary outcome: Claude retains important project context across sessions without re-reading files, and token usage on routine tasks drops measurably. + +**Not goals:** +- Replace the user's existing auto-memory system. The skill *integrates* with it, adding a pointer entry. +- Ingest chat history or session transcripts into Obsidian (that's a separate tool). +- Work on remote hosts (ClowH1 prod). Local macOS only for v1. +- Ship a published package (no pypi/brew/homebrew-tap). Skill lives in `~/.claude/skills/`. + +## 2. Success criteria + +1. On a clean project, running the skill once produces: working `codegraph`+`gitnexus` MCP servers visible to Claude Code, `.codegraph/` index, optional `.gitnexus/` index, updated `CLAUDE.md` with a `## Knowledge Graph` section, an Obsidian `{repo_name}/_index.md` entry, and a pointer line in auto-memory `MEMORY.md`. +2. Re-running on the same project is safe: by default only runs health check and reports; destructive actions are gated behind explicit user confirmation (choice "c" from design). +3. On a project without some prerequisites (e.g. no Obsidian vault), the skill degrades gracefully: skips that layer with a warning, still completes everything else. +4. After setup, Claude answers 3 smoke-test architectural questions on `arb-scanner` using the graph MCP tools instead of reading files. Token usage for those 3 questions is lower than pre-skill baseline (rough measurement, not a hard SLA). + +## 3. Architecture + +### 3.1 Four memory layers + +| Layer | What it holds | Where | Update cadence | Skill's role | +|---|---|---|---|---| +| **Code graph** | AST-derived deps, callers/callees, call chains | `.codegraph/codegraph.db` + `.gitnexus/` | On-demand refresh | Install tools + run init | +| **Project CLAUDE.md** | Conventions, commands, gotchas | `./CLAUDE.md` | Rarely, by human | Add `## Knowledge Graph` section (idempotent) | +| **Obsidian vault** | Decisions, session logs, research, status | `{VaultRoot}/{repo_name}/` | Frequently, by human | Create `_index.md` pointer file; optional `sessions/`+`knowledge/` subfolders | +| **auto-memory** | Persistent facts between Claude sessions | `~/.claude/projects/{slug}/memory/` | Automatically by Claude | Append one pointer line to `MEMORY.md` | + +The skill builds **infrastructure**. Content is the user's responsibility, except a minimal seed `_index.md` in Obsidian. + +### 3.2 Skill structure + +``` +~/.claude/skills/kg-setup/ +├── SKILL.md # Orchestrator (~150 lines) +├── scripts/ +│ ├── check_prereqs.sh # Detect installed tools, emit JSON +│ ├── install_tools.sh # npm install both tools (idempotent) +│ ├── detect_project.py # git remote, LOC, language, vault path +│ ├── register_mcp.sh # claude mcp add for both servers +│ ├── init_codegraph.sh # codegraph init -i in cwd +│ ├── init_gitnexus.sh # gitnexus analyze . in cwd +│ ├── merge_claude_md.py # idempotent section merge +│ ├── build_obsidian_index.py # generate _index.md content +│ ├── update_memory_index.py # append pointer to MEMORY.md +│ ├── health_check.sh # MCP ping + test query to each graph +│ └── state.py # read/write .kg-setup-state.json +├── templates/ +│ ├── claude_md_section.md # "## Knowledge Graph" template +│ ├── obsidian_index_minimal.md # Default _index.md +│ └── obsidian_index_rich.md # --rich / auto-rich variant +├── tests/ +│ ├── test_merge_claude_md.py +│ ├── test_detect_project.py +│ ├── test_state.py +│ └── integration_test.sh +└── README.md # Human-facing docs for the skill itself +``` + +## 4. Components + +### 4.1 `SKILL.md` + +Frontmatter: +```yaml +--- +name: kg-setup +description: Bootstrap a 4-layer project memory system (CodeGraph + GitNexus + CLAUDE.md + Obsidian + auto-memory). Use when user asks to "настрой граф знаний", "подключи базу знаний", "запомни проект", "setup knowledge graph", "bootstrap project memory", "initialize code graph", "пусть ты помнишь детали проекта". +--- +``` + +Body — 5 phases in explicit markdown: + +1. **DETECT** — invoke `check_prereqs.sh` + `detect_project.py`, read `./CLAUDE.md` and `./.kg-setup-state.json`. Aggregate into an in-memory `state` blob. +2. **PLAN** — Claude reads state, constructs action list. If state file says "healthy" and no `--refresh` flag, skip directly to VERIFY. +3. **EXECUTE** — run actions in order: install tools → register MCP → init graphs → merge CLAUDE.md → Obsidian → auto-memory. Each step is atomic; a failure after step N does not invalidate steps 1..N-1. +4. **VERIFY** — `health_check.sh`. Writes summary to state file. +5. **REPORT** — Claude prints bulleted summary to user: what succeeded, what was skipped, what failed, suggested next steps. + +Rerun behavior (decision point — user chose "c", ask): +- If state file exists and shows previous successful setup → Claude asks: "Проект уже настроен. `(r)` health-check only / `(f)` full refresh / `(s)` skip Obsidian only / `(q)` cancel?" +- Default to `(r)` if no answer after prompt. + +### 4.2 `check_prereqs.sh` + +Output schema (JSON to stdout): +```json +{ + "schema_version": 1, + "env": { + "node": "v20.11.0", + "node_major": 20, + "python": "3.12.1", + "git": "2.43.0", + "npm": "10.2.4" + }, + "tools": { + "gitnexus_cli": {"installed": false, "version": null, "path": null}, + "codegraph_cli": {"installed": true, "version": "0.x.y", "path": "/opt/homebrew/bin/codegraph"}, + "gitnexus_mcp_registered": false, + "codegraph_mcp_registered": true + }, + "obsidian": { + "mcp_available": true, + "vault_path_hint": null + }, + "errors": [], + "warnings": [] +} +``` + +Node 18+ is the hard prerequisite (CodeGraph requirement). If `node_major < 18` → blocking error. + +### 4.3 `install_tools.sh` + +Concrete commands (from README reality-check): +```bash +npm install -g gitnexus +npm install -g @colbymchenry/codegraph +``` + +Idempotent: if `which codegraph` already exits 0, skip; same for gitnexus. Failures → retry once with `--force` flag, then bubble up. + +### 4.4 `register_mcp.sh` + +Uses Claude Code CLI: +```bash +claude mcp add gitnexus -- npx -y gitnexus@latest mcp +claude mcp add codegraph -- codegraph serve --mcp +``` + +Check via `claude mcp list` before adding. If already registered, skip. + +### 4.5 `detect_project.py` + +Detects: +- `git_remote_name`: `basename(git remote get-url origin, .git)`. If no remote → fallback to `basename(cwd)`, emit warning. +- `primary_lang`: heuristic based on file extensions + common markers (`requirements.txt`/`pyproject.toml` → python, `package.json` → js/ts, etc.) +- `loc`: sum of non-blank, non-comment lines across source files, capped at 100k for speed +- `vault_path`: determined in this priority order: (1) env var `KG_SETUP_VAULT_PATH` if set, (2) Obsidian MCP `get_vault_stats` response, (3) null. Never attempts filesystem scan for vaults. If null, Obsidian layer is skipped gracefully in phase 3. + +### 4.6 `init_codegraph.sh` and `init_gitnexus.sh` + +```bash +# codegraph +codegraph init -i # interactive false via env var or preset +# gitnexus +gitnexus analyze . +``` + +Both check for existing `.codegraph/` / `.gitnexus/` first. If present and refresh-mode is not active, skip with note. Refresh-mode is triggered in two ways: (a) user's natural-language intent contains phrases like "обнови граф", "re-index", "refresh graph" — Claude interprets and passes `--refresh` env var to the scripts; (b) user selects `(f) full refresh` in the rerun prompt (see SKILL.md rerun behavior in §4.1). + +### 4.7 `merge_claude_md.py` + +Rules: +1. If `./CLAUDE.md` missing → create with our section only + note that user should add their own conventions above. +2. If present but no section with heading `## Knowledge Graph` → append section to end. +3. If section exists and contains marker `` → update the generated-content lines (paths, timestamps), preserve anything below `` marker unchanged. +4. If section exists without our marker → do not touch; emit warning "CLAUDE.md has a `## Knowledge Graph` section from another source, skipping merge." + +Template stored in `templates/claude_md_section.md`: +```markdown +## Knowledge Graph + + +Local graph indices: +- CodeGraph: `.codegraph/codegraph.db` (query via MCP server `codegraph`) +- GitNexus: `.gitnexus/` (query via MCP server `gitnexus`) + +Obsidian notes: `{VaultRoot}/{repo_name}/_index.md` +auto-memory: `~/.claude/projects/{slug}/memory/MEMORY.md` + +Refresh indices after major code changes: +- `codegraph init -i --refresh` +- `gitnexus analyze . --force` + + + + +``` + +Pre-write safety: run `git status --porcelain -- CLAUDE.md`. If CLAUDE.md has uncommitted changes → refuse to write, ask user to commit or stash first. + +### 4.8 `build_obsidian_index.py` + +Generates `_index.md` content. Claude then writes it via Obsidian MCP `write_note`. + +Mode selection: +- **rich**: explicit `--rich` flag OR `detect_project.loc > 5000` OR existence of `./tests/` and `./docs/` in project +- **minimal**: otherwise + +Minimal template: +```markdown +--- +tags: [project, kg-index] +--- + +# {repo_name} + +**Repo:** `{project_path}` +**Languages:** {primary_lang} +**LOC:** ~{loc} +**Setup date:** {today} + +## Quick links +- [[../arb-scanner/_index]] ← other projects in vault +- Code graph local index: `{project_path}/.codegraph/` +- CLAUDE.md: `{project_path}/CLAUDE.md` + +## What lives here +(Add notes on decisions, research, session logs below or in nested folders.) +``` + +Rich template adds: `sessions/` placeholder, `knowledge/decisions/` placeholder, `knowledge/patterns/` placeholder — creating empty `.gitkeep`-style folder notes (`_folder.md` with a stub header). + +### 4.9 `update_memory_index.py` + +Appends one line to `~/.claude/projects/{slug}/memory/MEMORY.md`. `{slug}` is the project's canonical Claude Code projects-directory name: project path with `/` replaced by `-` (e.g. `/Users/pavelmalkin/Documents/Scaner` → `-Users-pavelmalkin-Documents-Scaner`). If the memory directory does not exist yet, skip this step — the user's auto-memory harness will create it on first interaction, and the skill can be re-run later to add the pointer. +``` +- [Knowledge graph bootstrap for {repo_name}](project_kg_{repo_name}.md) — set up 2026-04-22, graph in .codegraph, vault: {repo_name}/ +``` + +And writes the pointed-to file `project_kg_{repo_name}.md` with frontmatter: +```markdown +--- +name: Knowledge graph for {repo_name} +description: Pointer to graph indices, Obsidian vault folder, and CLAUDE.md section for {repo_name} +type: project +--- + +Project `{repo_name}` ({project_path}) has kg-setup applied on 2026-04-22. +- CodeGraph MCP: server name `codegraph`, query with `mcp__codegraph__*` tools +- GitNexus MCP: server name `gitnexus` +- Obsidian vault folder: `{repo_name}/` +- State file: `{project_path}/.kg-setup-state.json` +- Reindex: `codegraph init -i --refresh` (or `gitnexus analyze . --force`) +``` + +Idempotent: checks for existing pointer line before appending. + +### 4.10 `health_check.sh` + +Three checks: +1. MCP servers visible: `claude mcp list | grep -E 'codegraph|gitnexus'` — each must return a line. +2. CodeGraph query: test via `mcp__codegraph__codegraph_status` tool through Claude (not direct — skill instructs Claude to run this as a test). +3. GitNexus query: similar smoke test. + +Output: JSON summary. Claude converts to user-readable checklist in REPORT phase. + +### 4.11 `state.py` + +`.kg-setup-state.json` schema (at project root): +```json +{ + "schema_version": 1, + "skill_version": "0.1.0", + "last_run": "2026-04-22T13:00:00Z", + "last_run_status": "healthy|degraded|incomplete", + "layers": { + "code_graph": {"configured": true, "tool": "codegraph", "index_path": ".codegraph/"}, + "gitnexus": {"configured": true, "index_path": ".gitnexus/"}, + "claude_md": {"configured": true, "section_marker": "kg-setup-v1"}, + "obsidian": {"configured": true, "vault_folder": "arb-scanner/", "mode": "minimal"}, + "auto_memory": {"configured": true, "pointer_file": "project_kg_arb-scanner.md"} + }, + "warnings": [], + "errors": [] +} +``` + +On successful first run, append `.kg-setup-state.json` to `.gitignore` (check it's not already there). + +## 5. Data flow + +See brainstorming session output (reproduced): + +``` +User trigger + → Phase 1 DETECT (check_prereqs + detect_project + read state) + → Phase 2 PLAN (Claude decides action list) + → Phase 3 EXECUTE: + one-time: install_tools → register_mcp + per-project: init_codegraph → init_gitnexus → merge_claude_md + → build_obsidian_index → update_memory_index + → write state.json after each step + → Phase 4 VERIFY (health_check) + → Phase 5 REPORT (Claude writes bulleted summary) +``` + +## 6. Error handling + +| Category | Examples | Behavior | +|---|---|---| +| Blocking | No node/python/git; node < 18 | Stop. Show install command. No disk writes. | +| Recoverable | npm network flake | Retry once. Then escalate to blocking. | +| Degraded | Obsidian vault not found; codegraph refuses a language | Skip layer. Write to `state.warnings`. Continue. | +| User-conflict | CLAUDE.md has `## Knowledge Graph` without our marker; CLAUDE.md has uncommitted changes | Stop that step. Prompt user. | + +Atomic writes: tmp-file + rename for every file written. Never leave half-written artifacts. +Git-aware: refuse to write `CLAUDE.md` if user has uncommitted changes in it. +Errors/warnings always persisted to `.kg-setup-state.json`. + +User-facing error format: +``` +✗ install_tools (codegraph): npm exited with code 1 + stderr: ... + → Fix: `brew install node@20` and re-run skill +✓ install_tools (gitnexus): already at v1.2.3, skipped +``` + +## 7. Testing + +### 7.1 Unit tests (pytest) + +- `test_merge_claude_md.py` — **highest priority**. Snapshot-based: + - no file → creates from template + - no section → appends + - section with our marker → updates generated lines only, preserves `` block + - section without our marker → untouched, warning emitted + - 10 consecutive runs → identical output (idempotency) +- `test_detect_project.py` — tmp_path scenarios: with/without `.git`, various package manifests for language detection, LOC counter on synthetic files +- `test_state.py` — read/write roundtrip, schema_version migration stubs + +### 7.2 Integration test + +`tests/integration_test.sh`: +1. Create temp dir, `git init`, add `app.py` with a trivial function +2. Invoke skill phases directly (not through Claude) via the scripts +3. Assert: `.codegraph/` exists, `CLAUDE.md` exists and contains `kg-setup-v1` marker, state file `status=healthy` +4. Run again → no changes, exit 0 +5. Cleanup + +### 7.3 Manual smoke test (on arb-scanner itself) + +Before/after comparison: +- Pre: ask Claude 3 architectural questions about arb-scanner, record token usage +- Run skill +- Post: ask same 3 questions, confirm Claude uses `mcp__codegraph__*` tools instead of reading files. Compare tokens. + +### 7.4 Not tested automatically + +- Actual `npm install` of `gitnexus` and `@colbymchenry/codegraph` (external network, unstable) +- Obsidian MCP writes against a real vault (tested once manually, then relied upon) + +## 8. Install commands reference (verified from README 2026-04-22) + +```bash +# CLI tools +npm install -g gitnexus +npm install -g @colbymchenry/codegraph + +# MCP registration +claude mcp add gitnexus -- npx -y gitnexus@latest mcp +claude mcp add codegraph -- codegraph serve --mcp + +# Per-project init +codegraph init -i +gitnexus analyze . +``` + +Requirements: Node.js 18+. No brew/pip install path documented in either README. + +## 9. Open decisions recorded here + +| # | Decision | User's choice | Rationale | +|---|---|---|---| +| 1 | Implementation approach | Approach 2 (skill + bundled scripts) | Deterministic, fast, low-token vs Approach 1; not overkill like Approach 3 | +| 2 | Install both GitNexus and CodeGraph | Both | User explicitly wanted both; different angles on the same graph | +| 3 | Scope | local-only | Remote (ClowH1) deferred; user will SSH-run scripts manually if needed | +| 4 | Rerun behavior | Ask user (choice c) | Most predictable, avoids silent destructive actions | +| 5 | Skill name | `kg-setup` | Shorter than `setup-knowledge-graph`; matches user's existing short names | +| 6 | Canonical project name | git remote basename | Derived from `git remote get-url origin`, fallback to cwd basename | +| 7 | Obsidian folder convention | `{VaultRoot}/{repo_name}/` | Matches existing `arb-scanner/` and `betting-dashboard/` | +| 8 | CLAUDE.md merge style | α (append section, preserve above) | Don't touch user's hand-written 102-line CLAUDE.md | +| 9 | Obsidian mode default | minimal, rich on flag or LOC > 5000 | Avoid clutter on small projects; betting-dashboard is rich → justified | +| 10 | Languages tested | Python (primary), JS (secondary) | arb-scanner is Python; user occasionally uses JS | + +## 10. Out of scope for v1 + +- Remote/SSH setup for ClowH1 prod (separate tool later) +- Automated cron-based reindexing +- Chat history → Obsidian export pipeline +- Graph visualization UI beyond GitNexus's default web view +- Publishing the skill to a marketplace +- Support for languages beyond what CodeGraph/GitNexus already support +- Windows/Linux (macOS only) + +## 11. Post-setup housekeeping (not the skill's job, but flagged in the report) + +- User should rename `/Users/pavelmalkin/Documents/Scaner` → `.../arb-scanner` after end of current Claude Code session (to align with git remote name, Obsidian folder, and kg-setup canonical) +- User can populate `arb-scanner/_index.md` with richer content over time +- Ownership of the `## Knowledge Graph` section in CLAUDE.md: generated block is replaceable, everything below `` is the user's diff --git a/docs/plan.md b/docs/plan.md new file mode 100644 index 0000000..47a1f2a --- /dev/null +++ b/docs/plan.md @@ -0,0 +1,2150 @@ +# kg-setup Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Claude Code skill `kg-setup` that bootstraps a 4-layer project memory system (CodeGraph + GitNexus + CLAUDE.md + Obsidian + auto-memory) in any local project on macOS. + +**Architecture:** A markdown orchestrator (`SKILL.md`) delegates mechanical work to bundled bash + python scripts in `scripts/`. Each script has a single responsibility, emits JSON where state matters, is idempotent, and is tested in isolation. A top-level state file `.kg-setup-state.json` tracks per-project setup progress for safe reruns. Skill developed in its own repo at `~/Projects/kg-setup/`, symlinked into `~/.claude/skills/` at the end so Claude Code picks it up. + +**Tech Stack:** +- Bash (macOS/zsh) for system-level scripts (install, MCP register, health check) +- Python 3.12 for logic-heavy scripts (detect_project, merge_claude_md, state, build_obsidian_index, update_memory_index) +- pytest for Python unit tests +- bats-core (or plain bash + `set -e` + `[[ ]]` asserts) for shell integration tests — default to plain bash to avoid extra deps +- External CLIs (not dependencies of the plan, but dependencies of the skill at runtime): `gitnexus` (npm), `@colbymchenry/codegraph` (npm), Node.js 18+, `claude` CLI + +--- + +## File Structure + +Will be created at the repo root: + +``` +(repo root)/ +├── SKILL.md +├── README.md +├── scripts/ +│ ├── check_prereqs.sh +│ ├── install_tools.sh +│ ├── detect_project.py +│ ├── register_mcp.sh +│ ├── init_codegraph.sh +│ ├── init_gitnexus.sh +│ ├── merge_claude_md.py +│ ├── build_obsidian_index.py +│ ├── update_memory_index.py +│ ├── health_check.sh +│ └── state.py +├── templates/ +│ ├── claude_md_section.md +│ ├── obsidian_index_minimal.md +│ └── obsidian_index_rich.md +└── tests/ + ├── conftest.py + ├── test_state.py + ├── test_detect_project.py + ├── test_merge_claude_md.py + ├── test_build_obsidian_index.py + ├── test_update_memory_index.py + ├── test_check_prereqs.sh + └── integration_test.sh +``` + +**Responsibility boundaries:** +- `state.py` — single source of truth for `.kg-setup-state.json` I/O; used by every script that reads/writes state +- `detect_project.py` — read-only; pure inspection, emits JSON, no side effects +- `check_prereqs.sh` — read-only inspection of system env +- `install_tools.sh`, `register_mcp.sh`, `init_*.sh` — system-state mutators; must be idempotent +- `merge_claude_md.py`, `build_obsidian_index.py`, `update_memory_index.py` — file writers; must be atomic (tmp + rename) +- `health_check.sh` — read-only verification +- `SKILL.md` — orchestration logic only; no business rules embedded + +--- + +## Phase A — Foundation: state + detection (Tasks 1-4) + +Pure-Python, no external dependencies, fully testable. Lands first so every downstream script has a solid I/O layer. + +### Task 1: Repo scaffolding + +**Files:** +- Create: `scripts/` (directory) +- Create: `templates/` (directory) +- Create: `tests/` (directory) +- Create: `README.md` +- Create: `tests/conftest.py` +- Create: `.gitignore` (top-level — repo is fresh) + +- [ ] **Step 1: Create directories and README** + +```bash +mkdir -p scripts templates tests +``` + +Write `README.md`: +```markdown +# kg-setup + +Claude Code skill that bootstraps a 4-layer project memory system: +CodeGraph + GitNexus MCP servers, project CLAUDE.md section, Obsidian vault +folder with `_index.md`, and an auto-memory pointer. + +## Install + +Symlink into `~/.claude/skills/`: +```bash +ln -s "$(pwd)" ~/.claude/skills/kg-setup +``` + +## Activate + +Say to Claude Code: "настрой граф знаний" or "setup knowledge graph". + +## Design + +See `docs/superpowers/specs/2026-04-22-kg-setup-design.md` in the repo root. +``` + +- [ ] **Step 2: Add pytest config to conftest** + +Write `tests/conftest.py`: +```python +import sys +from pathlib import Path + +SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" +sys.path.insert(0, str(SCRIPTS_DIR)) +``` + +- [ ] **Step 3: Update .gitignore** + +Append to `.gitignore`: +``` +# kg-setup dev +__pycache__/ +**/__pycache__/ +**/*.pyc +.pytest_cache/ +``` + +- [ ] **Step 4: Verify pytest runs (empty)** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/ -v +``` +Expected: `no tests ran in ...` (exit code 5 is fine — no tests yet). + +- [ ] **Step 5: Commit** + +```bash +git add .gitignore +git commit -m "kg-setup: scaffold skill directory structure" +``` + +--- + +### Task 2: `state.py` — state file I/O + +**Files:** +- Create: `scripts/state.py` +- Create: `tests/test_state.py` + +- [ ] **Step 1: Write the failing tests** + +Write `tests/test_state.py`: +```python +import json +from pathlib import Path + +from state import State, STATE_FILENAME, SCHEMA_VERSION + + +def test_state_defaults(tmp_path): + s = State(tmp_path) + assert s.schema_version == SCHEMA_VERSION + assert s.status == "unknown" + assert s.layers == {} + assert s.warnings == [] + assert s.errors == [] + + +def test_state_write_creates_file(tmp_path): + s = State(tmp_path) + s.status = "healthy" + s.layers["code_graph"] = {"configured": True, "tool": "codegraph"} + s.save() + f = tmp_path / STATE_FILENAME + assert f.exists() + data = json.loads(f.read_text()) + assert data["schema_version"] == SCHEMA_VERSION + assert data["last_run_status"] == "healthy" + assert data["layers"]["code_graph"]["tool"] == "codegraph" + + +def test_state_load_roundtrip(tmp_path): + s1 = State(tmp_path) + s1.status = "degraded" + s1.warnings.append({"phase": "obsidian", "message": "vault missing"}) + s1.save() + + s2 = State(tmp_path) + s2.load() + assert s2.status == "degraded" + assert s2.warnings[0]["message"] == "vault missing" + + +def test_state_load_missing_file_noop(tmp_path): + s = State(tmp_path) + s.load() + assert s.status == "unknown" + + +def test_state_atomic_write(tmp_path, monkeypatch): + s = State(tmp_path) + s.status = "healthy" + s.save() + tmp_file = tmp_path / (STATE_FILENAME + ".tmp") + assert not tmp_file.exists(), "tmp file must be renamed, not left behind" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_state.py -v +``` +Expected: FAIL with `ModuleNotFoundError: No module named 'state'`. + +- [ ] **Step 3: Implement `state.py`** + +Write `scripts/state.py`: +```python +"""Read/write .kg-setup-state.json at project root.""" +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +SCHEMA_VERSION = 1 +SKILL_VERSION = "0.1.0" +STATE_FILENAME = ".kg-setup-state.json" + + +@dataclass +class State: + project_path: Path + schema_version: int = SCHEMA_VERSION + skill_version: str = SKILL_VERSION + last_run: str = "" + status: str = "unknown" + layers: dict[str, Any] = field(default_factory=dict) + warnings: list[dict] = field(default_factory=list) + errors: list[dict] = field(default_factory=list) + + @property + def path(self) -> Path: + return self.project_path / STATE_FILENAME + + def load(self) -> None: + if not self.path.exists(): + return + data = json.loads(self.path.read_text()) + self.schema_version = data.get("schema_version", SCHEMA_VERSION) + self.skill_version = data.get("skill_version", SKILL_VERSION) + self.last_run = data.get("last_run", "") + self.status = data.get("last_run_status", "unknown") + self.layers = data.get("layers", {}) + self.warnings = data.get("warnings", []) + self.errors = data.get("errors", []) + + def save(self) -> None: + self.last_run = datetime.now(timezone.utc).isoformat(timespec="seconds") + payload = { + "schema_version": self.schema_version, + "skill_version": self.skill_version, + "last_run": self.last_run, + "last_run_status": self.status, + "layers": self.layers, + "warnings": self.warnings, + "errors": self.errors, + } + tmp = self.path.with_suffix(self.path.suffix + ".tmp") + tmp.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n") + os.replace(tmp, self.path) +``` + +- [ ] **Step 4: Run tests to verify PASS** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_state.py -v +``` +Expected: 5 passed. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/state.py tests/test_state.py +git commit -m "kg-setup: add state.py with tests" +``` + +--- + +### Task 3: `detect_project.py` — read-only project inspection + +**Files:** +- Create: `scripts/detect_project.py` +- Create: `tests/test_detect_project.py` + +- [ ] **Step 1: Write the failing tests** + +Write `tests/test_detect_project.py`: +```python +import json +import subprocess +from pathlib import Path + +from detect_project import ( + detect_git_remote_name, + detect_primary_language, + count_loc, + build_report, +) + + +def _git_init(path: Path, remote_url: str | None = None) -> None: + subprocess.run(["git", "init", "-q", "-b", "main"], cwd=path, check=True) + if remote_url: + subprocess.run( + ["git", "remote", "add", "origin", remote_url], cwd=path, check=True + ) + + +def test_detect_git_remote_name_with_origin(tmp_path): + _git_init(tmp_path, "https://example.com/team/my-repo.git") + assert detect_git_remote_name(tmp_path) == "my-repo" + + +def test_detect_git_remote_name_no_dotgit(tmp_path): + _git_init(tmp_path, "git@example.com:team/other") + assert detect_git_remote_name(tmp_path) == "other" + + +def test_detect_git_remote_name_no_remote(tmp_path): + _git_init(tmp_path) + assert detect_git_remote_name(tmp_path) is None + + +def test_detect_git_remote_name_no_git(tmp_path): + assert detect_git_remote_name(tmp_path) is None + + +def test_detect_primary_language_python(tmp_path): + (tmp_path / "requirements.txt").write_text("flask\n") + (tmp_path / "app.py").write_text("print(1)\n") + assert detect_primary_language(tmp_path) == "python" + + +def test_detect_primary_language_js(tmp_path): + (tmp_path / "package.json").write_text('{"name": "x"}\n') + (tmp_path / "index.js").write_text("console.log(1)\n") + assert detect_primary_language(tmp_path) == "javascript" + + +def test_detect_primary_language_unknown(tmp_path): + (tmp_path / "README.md").write_text("# hi\n") + assert detect_primary_language(tmp_path) == "unknown" + + +def test_count_loc(tmp_path): + (tmp_path / "a.py").write_text("x = 1\n\n# comment\ny = 2\n") + (tmp_path / "b.py").write_text("z = 3\n") + n = count_loc(tmp_path, [".py"]) + # non-blank, non-comment-only lines: x=1, y=2, z=3 → 3 + assert n == 3 + + +def test_build_report_full(tmp_path): + _git_init(tmp_path, "git@github.com:foo/bar.git") + (tmp_path / "main.py").write_text("print(1)\n") + (tmp_path / "requirements.txt").write_text("") + + report = build_report(tmp_path) + data = json.loads(report) + assert data["path"] == str(tmp_path) + assert data["git_remote_name"] == "bar" + assert data["primary_lang"] == "python" + assert data["loc"] >= 1 + assert data["has_claude_md"] is False +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_detect_project.py -v +``` +Expected: FAIL with `ModuleNotFoundError: No module named 'detect_project'`. + +- [ ] **Step 3: Implement `detect_project.py`** + +Write `scripts/detect_project.py`: +```python +"""Read-only project inspection. Emits JSON to stdout when run as CLI.""" +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +SOURCE_EXTENSIONS = { + "python": [".py"], + "javascript": [".js", ".jsx", ".mjs"], + "typescript": [".ts", ".tsx"], + "go": [".go"], + "rust": [".rs"], + "java": [".java"], +} + +LANGUAGE_MARKERS = { + "python": ["requirements.txt", "pyproject.toml", "setup.py", "Pipfile"], + "javascript": ["package.json"], + "typescript": ["tsconfig.json"], + "go": ["go.mod"], + "rust": ["Cargo.toml"], + "java": ["pom.xml", "build.gradle"], +} + + +def detect_git_remote_name(path: Path) -> str | None: + try: + result = subprocess.run( + ["git", "remote", "get-url", "origin"], + cwd=path, + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode != 0: + return None + url = result.stdout.strip() + if not url: + return None + name = url.rsplit("/", 1)[-1] + if name.endswith(".git"): + name = name[:-4] + return name or None + except (FileNotFoundError, subprocess.TimeoutExpired): + return None + + +def detect_primary_language(path: Path) -> str: + for lang, markers in LANGUAGE_MARKERS.items(): + if any((path / m).exists() for m in markers): + return lang + return "unknown" + + +def count_loc(path: Path, extensions: list[str], cap: int = 100_000) -> int: + total = 0 + for ext in extensions: + for f in path.rglob(f"*{ext}"): + if any(part.startswith(".") for part in f.relative_to(path).parts): + continue + try: + for line in f.read_text(errors="ignore").splitlines(): + s = line.strip() + if s and not s.startswith("#") and not s.startswith("//"): + total += 1 + if total >= cap: + return total + except OSError: + continue + return total + + +def build_report(path: Path) -> str: + lang = detect_primary_language(path) + exts = SOURCE_EXTENSIONS.get(lang, []) + report = { + "path": str(path), + "git_remote_name": detect_git_remote_name(path), + "primary_lang": lang, + "loc": count_loc(path, exts) if exts else 0, + "has_claude_md": (path / "CLAUDE.md").exists(), + "has_codegraph_dir": (path / ".codegraph").is_dir(), + "has_gitnexus_dir": (path / ".gitnexus").is_dir(), + "has_state_file": (path / ".kg-setup-state.json").exists(), + } + return json.dumps(report, indent=2, ensure_ascii=False) + + +if __name__ == "__main__": + target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd() + print(build_report(target)) +``` + +- [ ] **Step 4: Run tests to verify PASS** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_detect_project.py -v +``` +Expected: 9 passed. + +- [ ] **Step 5: Smoke test CLI mode on arb-scanner itself** + +```bash +python3 scripts/detect_project.py . +``` +Expected: JSON with `git_remote_name: "arb-scanner"`, `primary_lang: "python"`, `loc > 0`, `has_claude_md: true`. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/detect_project.py tests/test_detect_project.py +git commit -m "kg-setup: add detect_project.py with tests" +``` + +--- + +### Task 4: `check_prereqs.sh` — env inspection + +**Files:** +- Create: `scripts/check_prereqs.sh` +- Create: `tests/test_check_prereqs.sh` + +- [ ] **Step 1: Write the shell test** + +Write `tests/test_check_prereqs.sh`: +```bash +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT="$(dirname "$0")/../scripts/check_prereqs.sh" + +output=$("$SCRIPT") + +# Must be valid JSON +echo "$output" | python3 -c "import json, sys; json.loads(sys.stdin.read())" \ + || { echo "FAIL: output not valid JSON"; exit 1; } + +# Must contain the required top-level keys +for key in schema_version env tools obsidian errors warnings; do + echo "$output" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +assert '$key' in data, 'missing key: $key' +" || { echo "FAIL: missing key $key"; exit 1; } +done + +# Must report node presence or blocking error +has_node=$(echo "$output" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +print(bool(data['env'].get('node'))) +") +if [[ "$has_node" != "True" && "$has_node" != "False" ]]; then + echo "FAIL: node field malformed"; exit 1 +fi + +echo "PASS: check_prereqs.sh" +``` + +Make executable: +```bash +chmod +x tests/test_check_prereqs.sh +``` + +- [ ] **Step 2: Run test to verify failure** + +```bash +tests/test_check_prereqs.sh +``` +Expected: error that script file does not exist. + +- [ ] **Step 3: Implement `check_prereqs.sh`** + +Write `scripts/check_prereqs.sh`: +```bash +#!/usr/bin/env bash +# check_prereqs.sh — inspect local env, emit JSON to stdout. +# Read-only. Never mutates state. Never writes files. +set -euo pipefail + +json_str() { + # escape string for JSON + python3 -c "import json,sys; print(json.dumps(sys.stdin.read().strip()))" +} + +get_version() { + local cmd="$1" flag="${2:---version}" + if command -v "$cmd" >/dev/null 2>&1; then + local out + out=$("$cmd" "$flag" 2>&1 | head -n1 || true) + json_str <<<"$out" + else + echo "null" + fi +} + +node_major() { + if command -v node >/dev/null 2>&1; then + node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/' + else + echo "null" + fi +} + +tool_entry() { + local name="$1" cli="$2" + local installed=false version="null" path="null" + if command -v "$cli" >/dev/null 2>&1; then + installed=true + local p v + p=$(command -v "$cli") + path=$(json_str <<<"$p") + v=$("$cli" --version 2>/dev/null | head -n1 || true) + if [[ -n "$v" ]]; then + version=$(json_str <<<"$v") + fi + fi + printf ' "%s": {"installed": %s, "version": %s, "path": %s}' \ + "$name" "$installed" "$version" "$path" +} + +mcp_registered() { + local server_name="$1" + if command -v claude >/dev/null 2>&1; then + if claude mcp list 2>/dev/null | grep -q "^${server_name}\b"; then + echo "true" + else + echo "false" + fi + else + echo "false" + fi +} + +NODE_VERSION=$(get_version node -v) +PYTHON_VERSION=$(get_version python3 --version) +GIT_VERSION=$(get_version git --version) +NPM_VERSION=$(get_version npm --version) +NODE_MAJOR=$(node_major) + +GITNEXUS_ENTRY=$(tool_entry "gitnexus_cli" "gitnexus") +CODEGRAPH_ENTRY=$(tool_entry "codegraph_cli" "codegraph") +GITNEXUS_MCP=$(mcp_registered "gitnexus") +CODEGRAPH_MCP=$(mcp_registered "codegraph") + +ERRORS="[]" +WARNINGS="[]" +if [[ -z "$NODE_MAJOR" || "$NODE_MAJOR" == "null" ]]; then + ERRORS='[{"code":"no_node","message":"Node.js not found; install Node 18+ via `brew install node`"}]' +elif [[ "$NODE_MAJOR" -lt 18 ]]; then + ERRORS="[{\"code\":\"node_too_old\",\"message\":\"Node $NODE_MAJOR < 18; upgrade via \`brew upgrade node\`\"}]" +fi + +if [[ -n "${KG_SETUP_VAULT_PATH:-}" ]]; then + VAULT_HINT="\"$KG_SETUP_VAULT_PATH\"" +else + VAULT_HINT="null" +fi + +cat </dev/null 2>&1 || { + echo "ERROR: npm not found. Install Node 18+ first (brew install node)" >&2 + exit 2 + } +} + +install_if_missing() { + local cli_name="$1" pkg="$2" + if command -v "$cli_name" >/dev/null 2>&1; then + echo "[skip] $cli_name already installed: $(command -v "$cli_name")" + return 0 + fi + echo "[install] npm install -g $pkg" + if ! npm install -g "$pkg"; then + echo "[retry] npm install -g $pkg (attempt 2)" + npm install -g "$pkg" + fi + command -v "$cli_name" >/dev/null 2>&1 || { + echo "ERROR: $cli_name still not on PATH after install" >&2 + return 1 + } + echo "[done] $cli_name installed" +} + +require_npm +install_if_missing gitnexus "$GITNEXUS_PKG" +install_if_missing codegraph "$CODEGRAPH_PKG" +``` + +Make executable: +```bash +chmod +x scripts/install_tools.sh +``` + +- [ ] **Step 2: Dry-run verify script syntax** + +```bash +bash -n scripts/install_tools.sh && echo "syntax ok" +``` +Expected: `syntax ok`. + +- [ ] **Step 3: Manual execution (user approval gate)** + +Before running: confirm with user that installing `gitnexus` and `@colbymchenry/codegraph` globally is OK. Then: + +```bash +scripts/install_tools.sh +``` +Expected: `[install]` or `[skip]` lines for each tool, final state has both on PATH. + +Verify: +```bash +which gitnexus && which codegraph +``` + +- [ ] **Step 4: Idempotency check** + +```bash +scripts/install_tools.sh +``` +Expected: both report `[skip]`. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/install_tools.sh +git commit -m "kg-setup: add install_tools.sh for gitnexus + codegraph" +``` + +--- + +### Task 6: `register_mcp.sh` — add MCP servers to Claude Code + +**Files:** +- Create: `scripts/register_mcp.sh` + +- [ ] **Step 1: Implement `register_mcp.sh`** + +Write `scripts/register_mcp.sh`: +```bash +#!/usr/bin/env bash +# register_mcp.sh — register gitnexus + codegraph MCP servers with Claude Code. +# Idempotent: uses `claude mcp list` to detect existing registration. +set -euo pipefail + +require_claude() { + command -v claude >/dev/null 2>&1 || { + echo "ERROR: claude CLI not found. Install Claude Code first." >&2 + exit 2 + } +} + +is_registered() { + local name="$1" + claude mcp list 2>/dev/null | grep -Eq "^${name}[[:space:]]" +} + +register_if_missing() { + local name="$1"; shift + if is_registered "$name"; then + echo "[skip] mcp server '$name' already registered" + return 0 + fi + echo "[register] claude mcp add $name -- $*" + claude mcp add "$name" -- "$@" + is_registered "$name" || { + echo "ERROR: registration of '$name' did not take effect" >&2 + return 1 + } + echo "[done] '$name' registered" +} + +require_claude +register_if_missing gitnexus npx -y gitnexus@latest mcp +register_if_missing codegraph codegraph serve --mcp +``` + +Make executable: +```bash +chmod +x scripts/register_mcp.sh +``` + +- [ ] **Step 2: Syntax check** + +```bash +bash -n scripts/register_mcp.sh && echo "syntax ok" +``` +Expected: `syntax ok`. + +- [ ] **Step 3: Execute and verify (user approval gate)** + +```bash +scripts/register_mcp.sh +claude mcp list +``` +Expected: list contains `gitnexus` and `codegraph` rows. + +- [ ] **Step 4: Idempotency check** + +```bash +scripts/register_mcp.sh +``` +Expected: both `[skip]`. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/register_mcp.sh +git commit -m "kg-setup: add register_mcp.sh for gitnexus + codegraph servers" +``` + +--- + +### Task 7: `health_check.sh` — post-setup verification + +**Files:** +- Create: `scripts/health_check.sh` + +- [ ] **Step 1: Implement `health_check.sh`** + +Write `scripts/health_check.sh`: +```bash +#!/usr/bin/env bash +# health_check.sh — verify kg-setup artifacts in current project. +# Emits JSON summary to stdout. Exit code 0 = healthy, 1 = degraded. +set -uo pipefail + +PROJECT="${1:-$PWD}" +cd "$PROJECT" + +check_mcp_listed() { + local name="$1" + if command -v claude >/dev/null 2>&1 \ + && claude mcp list 2>/dev/null | grep -Eq "^${name}[[:space:]]"; then + echo "true" + else + echo "false" + fi +} + +check_codegraph_dir() { + [[ -d ".codegraph" ]] && echo "true" || echo "false" +} + +check_gitnexus_dir() { + [[ -d ".gitnexus" ]] && echo "true" || echo "false" +} + +check_claude_md_section() { + [[ -f "CLAUDE.md" ]] && grep -q "" CLAUDE.md \ + && echo "true" || echo "false" +} + +check_state_file() { + [[ -f ".kg-setup-state.json" ]] && echo "true" || echo "false" +} + +RESULTS=$(cat </dev/null 2>&1; then + echo "ERROR: codegraph not on PATH. Run install_tools.sh first." >&2 + exit 2 +fi + +REFRESH="${KG_SETUP_REFRESH:-0}" +if [[ -d ".codegraph" && "$REFRESH" != "1" ]]; then + echo "[skip] .codegraph/ already exists (set KG_SETUP_REFRESH=1 to force reindex)" + exit 0 +fi + +if [[ -d ".codegraph" && "$REFRESH" == "1" ]]; then + echo "[refresh] removing .codegraph/ before reindex" + rm -rf .codegraph +fi + +echo "[init] codegraph init -i" +codegraph init -i +[[ -d ".codegraph" ]] || { + echo "ERROR: codegraph init did not create .codegraph/" >&2 + exit 1 +} +echo "[done] .codegraph/ initialized" +``` + +- [ ] **Step 2: Implement `init_gitnexus.sh`** + +Write `scripts/init_gitnexus.sh`: +```bash +#!/usr/bin/env bash +# init_gitnexus.sh — run `gitnexus analyze .` in current project. +set -euo pipefail + +if ! command -v gitnexus >/dev/null 2>&1; then + echo "ERROR: gitnexus not on PATH. Run install_tools.sh first." >&2 + exit 2 +fi + +REFRESH="${KG_SETUP_REFRESH:-0}" +if [[ -d ".gitnexus" && "$REFRESH" != "1" ]]; then + echo "[skip] .gitnexus/ already exists (set KG_SETUP_REFRESH=1 to force reindex)" + exit 0 +fi + +if [[ "$REFRESH" == "1" ]]; then + FORCE_FLAG="--force" +else + FORCE_FLAG="" +fi + +echo "[init] gitnexus analyze . $FORCE_FLAG" +gitnexus analyze . $FORCE_FLAG +[[ -d ".gitnexus" ]] || { + echo "ERROR: gitnexus analyze did not create .gitnexus/" >&2 + exit 1 +} +echo "[done] .gitnexus/ initialized" +``` + +Make both executable: +```bash +chmod +x scripts/init_codegraph.sh +chmod +x scripts/init_gitnexus.sh +``` + +- [ ] **Step 3: Syntax checks** + +```bash +bash -n scripts/init_codegraph.sh +bash -n scripts/init_gitnexus.sh +echo "syntax ok" +``` + +- [ ] **Step 4: Smoke test — skip path** + +```bash +(cd /tmp && mkdir -p kg-test-init && cd kg-test-init && mkdir .codegraph && \ + /Users/pavelmalkin/Projects/kg-setup/scripts/init_codegraph.sh) +``` +Expected: `[skip] .codegraph/ already exists`. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/init_codegraph.sh scripts/init_gitnexus.sh +git commit -m "kg-setup: add init_codegraph.sh + init_gitnexus.sh" +``` + +--- + +### Task 9: `merge_claude_md.py` — critical, fully TDD + +**Files:** +- Create: `templates/claude_md_section.md` +- Create: `scripts/merge_claude_md.py` +- Create: `tests/test_merge_claude_md.py` + +- [ ] **Step 1: Write the template** + +Write `templates/claude_md_section.md`: +```markdown +## Knowledge Graph + + +Local graph indices: +- CodeGraph: `.codegraph/codegraph.db` (query via MCP server `codegraph`) +- GitNexus: `.gitnexus/` (query via MCP server `gitnexus`) + +Obsidian notes: `{vault_root}/{repo_name}/_index.md` +auto-memory: `~/.claude/projects/{slug}/memory/MEMORY.md` + +Refresh indices after major code changes: +- `KG_SETUP_REFRESH=1 bash ~/.claude/scripts/init_codegraph.sh` +- `KG_SETUP_REFRESH=1 bash ~/.claude/scripts/init_gitnexus.sh` + + + + +``` + +- [ ] **Step 2: Write failing tests** + +Write `tests/test_merge_claude_md.py`: +```python +from pathlib import Path + +from merge_claude_md import merge, MARKER_START, MARKER_END, USER_CONTENT_MARKER + + +TEMPLATE_VARS = { + "vault_root": "/Users/u/Obsidian/Vault", + "repo_name": "myrepo", + "slug": "-Users-u-myrepo", +} + + +def test_merge_creates_file_when_missing(tmp_path): + f = tmp_path / "CLAUDE.md" + result = merge(f, TEMPLATE_VARS) + assert result.action == "created" + text = f.read_text() + assert "## Knowledge Graph" in text + assert MARKER_START in text + assert "/Users/u/Obsidian/Vault/myrepo/_index.md" in text + + +def test_merge_appends_to_existing_no_section(tmp_path): + f = tmp_path / "CLAUDE.md" + f.write_text("# My Project\n\nSome content.\n") + result = merge(f, TEMPLATE_VARS) + assert result.action == "appended" + text = f.read_text() + assert text.startswith("# My Project") + assert "## Knowledge Graph" in text + assert MARKER_START in text + + +def test_merge_updates_generated_block_preserves_user_content(tmp_path): + f = tmp_path / "CLAUDE.md" + initial = f"""# Proj + +## Knowledge Graph + +{MARKER_START} +old stuff +{MARKER_END} + +{USER_CONTENT_MARKER} + +My own notes here. +Keep me. +""" + f.write_text(initial) + + result = merge(f, TEMPLATE_VARS) + assert result.action == "updated" + text = f.read_text() + assert "old stuff" not in text + assert "My own notes here." in text + assert "Keep me." in text + assert "/Users/u/Obsidian/Vault/myrepo/_index.md" in text + + +def test_merge_skips_section_without_our_marker(tmp_path): + f = tmp_path / "CLAUDE.md" + initial = "# Proj\n\n## Knowledge Graph\n\nCustom user section.\n" + f.write_text(initial) + + result = merge(f, TEMPLATE_VARS) + assert result.action == "skipped_foreign_section" + assert result.warning + assert f.read_text() == initial # unchanged + + +def test_merge_idempotent(tmp_path): + f = tmp_path / "CLAUDE.md" + merge(f, TEMPLATE_VARS) + first = f.read_text() + for _ in range(10): + merge(f, TEMPLATE_VARS) + assert f.read_text() == first + + +def test_merge_atomic_write_no_tmp_leftover(tmp_path): + f = tmp_path / "CLAUDE.md" + merge(f, TEMPLATE_VARS) + assert not (tmp_path / "CLAUDE.md.tmp").exists() +``` + +- [ ] **Step 3: Run tests — verify fail** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_merge_claude_md.py -v +``` +Expected: FAIL `ModuleNotFoundError`. + +- [ ] **Step 4: Implement `merge_claude_md.py`** + +Write `scripts/merge_claude_md.py`: +```python +"""Idempotently merge the kg-setup 'Knowledge Graph' section into CLAUDE.md.""" +from __future__ import annotations + +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +MARKER_START = "" +MARKER_END = "" +USER_CONTENT_MARKER = "" +SECTION_HEADING = "## Knowledge Graph" + +TEMPLATE_PATH = Path(__file__).parent.parent / "templates" / "claude_md_section.md" + + +@dataclass +class MergeResult: + action: str # created | appended | updated | skipped_foreign_section + warning: str | None = None + + +def _render_template(vars: dict) -> str: + tpl = TEMPLATE_PATH.read_text() + return tpl.format(**vars) + + +def _atomic_write(path: Path, content: str) -> None: + tmp = path.with_suffix(path.suffix + ".tmp") + tmp.write_text(content) + os.replace(tmp, path) + + +def _has_our_section(text: str) -> bool: + return MARKER_START in text and MARKER_END in text + + +def _has_foreign_section(text: str) -> bool: + return SECTION_HEADING in text and MARKER_START not in text + + +def _extract_user_content(text: str) -> str: + idx = text.find(USER_CONTENT_MARKER) + if idx == -1: + return "" + return text[idx + len(USER_CONTENT_MARKER):] + + +def merge(claude_md: Path, template_vars: dict) -> MergeResult: + rendered = _render_template(template_vars) + + if not claude_md.exists(): + _atomic_write(claude_md, rendered) + return MergeResult(action="created") + + existing = claude_md.read_text() + + if _has_our_section(existing): + user_tail = _extract_user_content(existing) + # Rebuild: everything before our section_heading + fresh rendered + + # optionally carry forward the user-content tail (without its marker; template has its own) + before_heading = existing.split(SECTION_HEADING, 1)[0].rstrip() + "\n\n" + # rendered already ends with the user-content marker; we append the extracted tail after it + new_text = before_heading + rendered.rstrip() + user_tail + # Normalize to end with one newline + if not new_text.endswith("\n"): + new_text += "\n" + _atomic_write(claude_md, new_text) + return MergeResult(action="updated") + + if _has_foreign_section(existing): + return MergeResult( + action="skipped_foreign_section", + warning=( + "CLAUDE.md already has a '## Knowledge Graph' section without " + "the kg-setup marker. Not overwriting. Remove manually or add " + f"the marker '{MARKER_START}' to allow kg-setup to manage it." + ), + ) + + # Plain append + sep = "" if existing.endswith("\n") else "\n" + new_text = existing + sep + "\n" + rendered + _atomic_write(claude_md, new_text) + return MergeResult(action="appended") + + +if __name__ == "__main__": + if len(sys.argv) < 4: + print("usage: merge_claude_md.py ", + file=sys.stderr) + sys.exit(2) + result = merge( + Path(sys.argv[1]), + {"vault_root": sys.argv[2], "repo_name": sys.argv[3], "slug": sys.argv[4]}, + ) + print(f"action={result.action}") + if result.warning: + print(f"warning={result.warning}", file=sys.stderr) +``` + +- [ ] **Step 5: Run tests — verify pass** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_merge_claude_md.py -v +``` +Expected: 6 passed. + +- [ ] **Step 6: Commit** + +```bash +git add templates/claude_md_section.md \ + scripts/merge_claude_md.py \ + tests/test_merge_claude_md.py +git commit -m "kg-setup: add merge_claude_md.py (idempotent section merge) with tests" +``` + +--- + +### Task 10: `build_obsidian_index.py` — generate `_index.md` content + +**Files:** +- Create: `templates/obsidian_index_minimal.md` +- Create: `templates/obsidian_index_rich.md` +- Create: `scripts/build_obsidian_index.py` +- Create: `tests/test_build_obsidian_index.py` + +- [ ] **Step 1: Write templates** + +Write `templates/obsidian_index_minimal.md`: +```markdown +--- +tags: [project, kg-index] +project: {repo_name} +setup_date: {today} +--- + +# {repo_name} + +**Repo:** `{project_path}` +**Languages:** {primary_lang} +**LOC:** ~{loc} +**Setup date:** {today} + +## Quick links + +- Code graph local index: `{project_path}/.codegraph/` +- CLAUDE.md: `{project_path}/CLAUDE.md` +- State: `{project_path}/.kg-setup-state.json` + +## What lives here + +Add notes on decisions, research, session logs below or in nested folders. +``` + +Write `templates/obsidian_index_rich.md`: +```markdown +--- +tags: [project, kg-index, rich] +project: {repo_name} +setup_date: {today} +--- + +# {repo_name} + +**Repo:** `{project_path}` +**Languages:** {primary_lang} +**LOC:** ~{loc} +**Setup date:** {today} + +## Quick links + +- Code graph local index: `{project_path}/.codegraph/` +- CLAUDE.md: `{project_path}/CLAUDE.md` +- State: `{project_path}/.kg-setup-state.json` + +## Sections + +- [[{repo_name}/sessions/_index|Sessions]] — chronological work logs +- [[{repo_name}/knowledge/decisions/_index|Decisions]] — recorded design calls +- [[{repo_name}/knowledge/patterns/_index|Patterns]] — reusable patterns and idioms + +## What lives here + +Top-level: high-signal notes you want always visible. Nested folders: dated +session logs, decisions, and patterns. Use backlinks liberally. +``` + +- [ ] **Step 2: Write failing tests** + +Write `tests/test_build_obsidian_index.py`: +```python +from build_obsidian_index import render, choose_mode + + +VARS = { + "repo_name": "foo", + "project_path": "/tmp/foo", + "primary_lang": "python", + "loc": 1234, + "today": "2026-04-22", +} + + +def test_choose_mode_minimal_small_project(): + assert choose_mode(loc=100, has_tests=False, has_docs=False, explicit=None) == "minimal" + + +def test_choose_mode_rich_large_project(): + assert choose_mode(loc=6000, has_tests=False, has_docs=False, explicit=None) == "rich" + + +def test_choose_mode_rich_by_structure(): + assert choose_mode(loc=100, has_tests=True, has_docs=True, explicit=None) == "rich" + + +def test_choose_mode_explicit_overrides(): + assert choose_mode(loc=100000, has_tests=True, has_docs=True, explicit="minimal") == "minimal" + assert choose_mode(loc=1, has_tests=False, has_docs=False, explicit="rich") == "rich" + + +def test_render_minimal_contains_required_fields(): + text = render("minimal", VARS) + assert "# foo" in text + assert "/tmp/foo" in text + assert "python" in text + assert "1234" in text + assert "2026-04-22" in text + + +def test_render_rich_contains_section_links(): + text = render("rich", VARS) + assert "Sessions" in text + assert "Decisions" in text + assert "Patterns" in text + + +def test_render_unknown_mode_raises(): + try: + render("weird", VARS) + except ValueError: + return + raise AssertionError("expected ValueError") +``` + +- [ ] **Step 3: Run tests — verify fail** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_build_obsidian_index.py -v +``` +Expected: FAIL `ModuleNotFoundError`. + +- [ ] **Step 4: Implement `build_obsidian_index.py`** + +Write `scripts/build_obsidian_index.py`: +```python +"""Generate Obsidian _index.md content for a project. + +Does not write to vault directly — returns rendered text so the caller +(Claude, via Obsidian MCP write_note) performs the actual write. +""" +from __future__ import annotations + +import json +import sys +from datetime import date +from pathlib import Path + +TEMPLATES = Path(__file__).parent.parent / "templates" +LOC_RICH_THRESHOLD = 5000 + + +def choose_mode( + loc: int, has_tests: bool, has_docs: bool, explicit: str | None +) -> str: + if explicit in ("minimal", "rich"): + return explicit + if loc > LOC_RICH_THRESHOLD: + return "rich" + if has_tests and has_docs: + return "rich" + return "minimal" + + +def render(mode: str, vars: dict) -> str: + if mode not in ("minimal", "rich"): + raise ValueError(f"unknown mode: {mode}") + tpl = (TEMPLATES / f"obsidian_index_{mode}.md").read_text() + v = dict(vars) + v.setdefault("today", date.today().isoformat()) + return tpl.format(**v) + + +if __name__ == "__main__": + # Input: JSON on stdin with keys matching template vars + optional "mode" + # plus "has_tests" and "has_docs" for mode selection. + inp = json.loads(sys.stdin.read()) + mode = choose_mode( + loc=inp.get("loc", 0), + has_tests=bool(inp.get("has_tests", False)), + has_docs=bool(inp.get("has_docs", False)), + explicit=inp.get("mode"), + ) + print(render(mode, inp)) +``` + +- [ ] **Step 5: Run tests — verify pass** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_build_obsidian_index.py -v +``` +Expected: 7 passed. + +- [ ] **Step 6: Commit** + +```bash +git add templates/obsidian_index_*.md \ + scripts/build_obsidian_index.py \ + tests/test_build_obsidian_index.py +git commit -m "kg-setup: add build_obsidian_index.py + templates with tests" +``` + +--- + +### Task 11: `update_memory_index.py` — auto-memory pointer + +**Files:** +- Create: `scripts/update_memory_index.py` +- Create: `tests/test_update_memory_index.py` + +- [ ] **Step 1: Write failing tests** + +Write `tests/test_update_memory_index.py`: +```python +from pathlib import Path + +from update_memory_index import ( + path_to_slug, + memory_dir_for, + append_pointer, + write_memory_file, +) + + +def test_path_to_slug(): + assert path_to_slug("/Users/u/Documents/foo") == "-Users-u-Documents-foo" + assert path_to_slug("/a/b") == "-a-b" + + +def test_memory_dir_for(tmp_path): + # simulate ~/.claude/projects/ layout + claude_home = tmp_path / ".claude" + project_path = "/Users/u/x" + expected = claude_home / "projects" / "-Users-u-x" / "memory" + assert memory_dir_for(project_path, claude_home=claude_home) == expected + + +def test_append_pointer_creates_memory_md(tmp_path): + mem_dir = tmp_path / "memory" + mem_dir.mkdir() + (mem_dir / "MEMORY.md").write_text("- [existing](file.md) — hook\n") + append_pointer( + memory_dir=mem_dir, + repo_name="myrepo", + date_str="2026-04-22", + ) + text = (mem_dir / "MEMORY.md").read_text() + assert "myrepo" in text + assert "2026-04-22" in text + assert "- [existing]" in text # previous line preserved + + +def test_append_pointer_idempotent(tmp_path): + mem_dir = tmp_path / "memory" + mem_dir.mkdir() + (mem_dir / "MEMORY.md").write_text("") + for _ in range(3): + append_pointer(memory_dir=mem_dir, repo_name="r", date_str="2026-04-22") + text = (mem_dir / "MEMORY.md").read_text() + assert text.count("[Knowledge graph bootstrap for r]") == 1 + + +def test_append_pointer_skips_when_no_memory_dir(tmp_path): + # memory dir does not exist → skipped, no error + append_pointer(memory_dir=tmp_path / "nope", repo_name="r", date_str="2026-04-22") + assert not (tmp_path / "nope").exists() + + +def test_write_memory_file(tmp_path): + mem_dir = tmp_path / "memory" + mem_dir.mkdir() + write_memory_file( + memory_dir=mem_dir, + repo_name="zz", + project_path="/tmp/zz", + date_str="2026-04-22", + ) + f = mem_dir / "project_kg_zz.md" + assert f.exists() + text = f.read_text() + assert "name: Knowledge graph for zz" in text + assert "type: project" in text + assert "/tmp/zz" in text +``` + +- [ ] **Step 2: Run tests — verify fail** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_update_memory_index.py -v +``` +Expected: FAIL `ModuleNotFoundError`. + +- [ ] **Step 3: Implement `update_memory_index.py`** + +Write `scripts/update_memory_index.py`: +```python +"""Append kg-setup pointer to user's auto-memory index.""" +from __future__ import annotations + +import os +import sys +from pathlib import Path + + +def path_to_slug(project_path: str) -> str: + return project_path.replace("/", "-") + + +def memory_dir_for( + project_path: str, claude_home: Path | None = None +) -> Path: + home = claude_home or Path.home() / ".claude" + return home / "projects" / path_to_slug(project_path) / "memory" + + +def append_pointer(memory_dir: Path, repo_name: str, date_str: str) -> str: + """Append a pointer line to MEMORY.md. Idempotent. Returns action taken.""" + if not memory_dir.exists(): + return "skipped_no_memory_dir" + + memory_md = memory_dir / "MEMORY.md" + line = ( + f"- [Knowledge graph bootstrap for {repo_name}]" + f"(project_kg_{repo_name}.md) — set up {date_str}, " + f"graph in .codegraph, vault: {repo_name}/" + ) + + existing = memory_md.read_text() if memory_md.exists() else "" + if f"[Knowledge graph bootstrap for {repo_name}]" in existing: + return "already_present" + + sep = "" if (not existing or existing.endswith("\n")) else "\n" + new_text = existing + sep + line + "\n" + _atomic_write(memory_md, new_text) + return "appended" + + +def write_memory_file( + memory_dir: Path, repo_name: str, project_path: str, date_str: str +) -> str: + if not memory_dir.exists(): + return "skipped_no_memory_dir" + target = memory_dir / f"project_kg_{repo_name}.md" + content = f"""--- +name: Knowledge graph for {repo_name} +description: Pointer to graph indices, Obsidian vault folder, and CLAUDE.md section for {repo_name} +type: project +--- + +Project `{repo_name}` ({project_path}) has kg-setup applied on {date_str}. + +- CodeGraph MCP: server name `codegraph`, query with `mcp__codegraph__*` tools +- GitNexus MCP: server name `gitnexus` +- Obsidian vault folder: `{repo_name}/` +- State file: `{project_path}/.kg-setup-state.json` +- Reindex: set `KG_SETUP_REFRESH=1` and re-run init scripts +""" + _atomic_write(target, content) + return "written" + + +def _atomic_write(path: Path, content: str) -> None: + tmp = path.with_suffix(path.suffix + ".tmp") + tmp.write_text(content) + os.replace(tmp, path) + + +if __name__ == "__main__": + if len(sys.argv) < 4: + print("usage: update_memory_index.py ", + file=sys.stderr) + sys.exit(2) + project_path, repo_name, date_str = sys.argv[1], sys.argv[2], sys.argv[3] + mem = memory_dir_for(project_path) + print("pointer:", append_pointer(mem, repo_name, date_str)) + print("file:", write_memory_file(mem, repo_name, project_path, date_str)) +``` + +- [ ] **Step 4: Run tests — verify pass** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/test_update_memory_index.py -v +``` +Expected: 6 passed. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/update_memory_index.py \ + tests/test_update_memory_index.py +git commit -m "kg-setup: add update_memory_index.py with tests" +``` + +--- + +### Task 12: Run full test suite + +**Files:** +- None to modify; verification only. + +- [ ] **Step 1: Run all Python tests** + +```bash +cd ~/Projects/kg-setup && python3 -m pytest tests/ -v +``` +Expected: all tests from Tasks 2, 3, 9, 10, 11 pass (28 total). + +- [ ] **Step 2: Run all shell tests** + +```bash +tests/test_check_prereqs.sh +``` +Expected: `PASS: check_prereqs.sh`. + +- [ ] **Step 3: Commit nothing (verification-only task, no code changes)** + +If coverage is green, move on. If any test fails, fix it before proceeding to Phase D. + +--- + +## Phase D — Orchestrator + integration (Tasks 13-14) + +### Task 13: Write `SKILL.md` orchestrator + +**Files:** +- Create: `SKILL.md` + +- [ ] **Step 1: Write `SKILL.md`** + +Write `SKILL.md`: +````markdown +--- +name: kg-setup +description: Bootstrap a 4-layer project memory system (CodeGraph + GitNexus MCP servers, project CLAUDE.md section, Obsidian vault folder with _index, auto-memory pointer). Use when user asks to "настрой граф знаний", "подключи базу знаний", "запомни проект", "setup knowledge graph", "bootstrap project memory", "initialize code graph", or "пусть ты помнишь детали проекта". +--- + +# kg-setup + +You bootstrap a 4-layer project memory system in the user's current project directory: a local code knowledge graph (CodeGraph + GitNexus), a `## Knowledge Graph` section in the project's `CLAUDE.md`, a folder in the user's Obsidian vault with a seed `_index.md`, and a pointer entry in the user's auto-memory. + +All mechanical work is delegated to scripts in `~/.claude/scripts/` (installation path via symlink — see Task 15 of the plan). Do not re-implement any logic — invoke the scripts. + +## Phases + +Run these phases in order. Each phase is a single logical unit. Stop and report to the user if a phase produces a blocking error. + +### Phase 1 — DETECT + +Execute in parallel and collect JSON outputs: + +- `bash ~/.claude/scripts/check_prereqs.sh` → `prereqs` JSON +- `python3 ~/.claude/scripts/detect_project.py "$PWD"` → `project` JSON + +Then read (best-effort): +- `./CLAUDE.md` (if exists) — first 40 lines only +- `./.kg-setup-state.json` (if exists) — full contents + +Form an in-memory `state` dict with these four sections. + +### Phase 2 — PLAN + +Inspect `state` and construct the action list: + +1. If `prereqs.errors` is non-empty (e.g. no Node, Node < 18) → STOP. Report errors to user, do not proceed. +2. If `.kg-setup-state.json` exists and reports `last_run_status=healthy`: + - Ask the user: "Проект уже настроен (последний запуск: {last_run}). Что делать? (r) только health-check / (f) full refresh / (s) только Obsidian layer / (q) отмена". Default to (r) if no response. + - If (q) → exit gracefully + - If (r) → skip to Phase 4 + - If (f) → set env `KG_SETUP_REFRESH=1` for this run; proceed through all steps + - If (s) → only run Obsidian + memory steps +3. Otherwise, the full per-project pipeline. + +### Phase 3 — EXECUTE + +Run in order. After each step, update the state dict; save state after the pipeline finishes (even on partial failure — mark `status=incomplete`). + +#### 3a. One-time (only if missing per prereqs): + +``` +bash ~/.claude/scripts/install_tools.sh +bash ~/.claude/scripts/register_mcp.sh +``` + +Ask for user confirmation before running `install_tools.sh` on first run. Phrase: "Я установлю globally две npm-пакета: `gitnexus` и `@colbymchenry/codegraph`. OK?" + +#### 3b. Per-project: + +``` +KG_SETUP_REFRESH=${refresh:-0} bash ~/.claude/scripts/init_codegraph.sh +KG_SETUP_REFRESH=${refresh:-0} bash ~/.claude/scripts/init_gitnexus.sh +``` + +If CLAUDE.md has uncommitted changes (check via `git status --porcelain CLAUDE.md` — non-empty = dirty) → ASK user to commit/stash before proceeding. Do not modify a dirty CLAUDE.md. + +``` +python3 ~/.claude/scripts/merge_claude_md.py \ + ./CLAUDE.md \ + "$VAULT_ROOT" \ + "$REPO_NAME" \ + "$SLUG" +``` + +#### 3c. Obsidian layer (via Obsidian MCP): + +Build `_index.md` content: +``` +echo "$OBSIDIAN_INPUT_JSON" | python3 ~/.claude/scripts/build_obsidian_index.py +``` + +Where `OBSIDIAN_INPUT_JSON` contains: `repo_name`, `project_path`, `primary_lang`, `loc`, `has_tests` (bool: does `./tests/` exist), `has_docs` (bool: does `./docs/` exist), and optionally `mode` ("minimal" or "rich" if user asked explicitly). + +Then: +- Use Obsidian MCP `list_directory` to check if `{repo_name}/` exists in vault. If not, MCP's `write_note` to `{repo_name}/_index.md` will create it. +- Use `write_note` to place the rendered content at `{repo_name}/_index.md`. If the note already exists, do NOT overwrite unless user explicitly said "refresh obsidian" — instead, warn and skip. + +#### 3d. Auto-memory layer: + +``` +python3 ~/.claude/scripts/update_memory_index.py \ + "$PROJECT_PATH" \ + "$REPO_NAME" \ + "$(date -u +%Y-%m-%d)" +``` + +#### 3e. Write state + gitignore: + +- Save state via `state.py` (use python inline: `python3 -c "from state import State; s = State(Path('.')); s.load(); s.status='healthy'; s.layers={...}; s.save()"`). Prefer writing a small helper call rather than inline if the string grows. +- Ensure `.kg-setup-state.json` is in `.gitignore` (append if missing). + +### Phase 4 — VERIFY + +``` +bash ~/.claude/scripts/health_check.sh "$PWD" +``` + +If exit code is non-zero, mark `state.status=degraded` and include which checks failed in the report. + +### Phase 5 — REPORT + +Print a user-facing bulleted summary: + +``` +kg-setup complete for + ✓ GitNexus + CodeGraph installed + ✓ MCP servers registered (gitnexus, codegraph) + ✓ .codegraph/ initialized (python, 4200 LOC) + ✓ .gitnexus/ initialized + ✓ CLAUDE.md — added "## Knowledge Graph" section + ✓ Obsidian: {repo_name}/_index.md created (minimal mode) + ✓ auto-memory: pointer added to MEMORY.md + ✓ Health check: 6/6 passed + +Next: ask me any architectural question about the project — I'll query +the graph via mcp__codegraph__* instead of reading files. +``` + +For any step that was skipped or failed, use the emoji `⊘` (skipped) or `✗` (failed) with the reason. + +## Idempotency contract + +- Re-running on a healthy project with default answer "r" must produce zero file writes. +- Re-running with "f" must re-run init scripts (fresh indices) and refresh generated blocks, preserving user-content tails. +- Scripts in `scripts/` must each be individually safe to re-run. + +## Error handling + +- **Blocking** (no Node, Node < 18, no git) → stop in Phase 2, print command the user should run (`brew install node`, etc.), do not touch disk. +- **Recoverable** (npm flake) → scripts retry once internally; if still failing → surface to user with the stderr and next command. +- **Degraded** (no Obsidian vault, unsupported language for codegraph) → skip that step, continue, mark in state.warnings. +- **User conflict** (CLAUDE.md dirty, foreign `## Knowledge Graph` section, existing Obsidian `_index.md`) → stop the specific step, ask user, proceed with their choice. +```` + +- [ ] **Step 2: Commit** + +```bash +git add SKILL.md +git commit -m "kg-setup: add SKILL.md orchestrator" +``` + +--- + +### Task 14: Integration smoke test + +**Files:** +- Create: `tests/integration_test.sh` + +- [ ] **Step 1: Write integration test** + +Write `tests/integration_test.sh`: +```bash +#!/usr/bin/env bash +# integration_test.sh — end-to-end smoke test of per-project scripts +# on a synthetic temp project. Does NOT test install/register/Obsidian +# (those require real external state). Covers: detect + init_codegraph +# (skipped branch) + merge_claude_md + state. +set -euo pipefail + +SKILL_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WORK=$(mktemp -d -t kg-setup-integ-XXXX) +trap 'rm -rf "$WORK"' EXIT + +cd "$WORK" +git init -q -b main +git remote add origin git@example.com:test/smoke.git +echo "print('hi')" > app.py +echo "flask" > requirements.txt +git add . && git -c user.email=t@t -c user.name=t commit -q -m "init" + +echo "--- detect_project ---" +python3 "$SKILL_ROOT/scripts/detect_project.py" "$WORK" + +echo "--- merge_claude_md (create) ---" +python3 "$SKILL_ROOT/scripts/merge_claude_md.py" \ + "$WORK/CLAUDE.md" "/vault" "smoke" "-tmp-smoke" +grep -q "## Knowledge Graph" "$WORK/CLAUDE.md" +grep -q "kg-setup-v1" "$WORK/CLAUDE.md" + +echo "--- merge_claude_md (idempotent) ---" +cp "$WORK/CLAUDE.md" "$WORK/CLAUDE.md.before" +python3 "$SKILL_ROOT/scripts/merge_claude_md.py" \ + "$WORK/CLAUDE.md" "/vault" "smoke" "-tmp-smoke" +diff -q "$WORK/CLAUDE.md.before" "$WORK/CLAUDE.md" + +echo "--- state write + read ---" +python3 -c " +import sys +sys.path.insert(0, '$SKILL_ROOT/scripts') +from pathlib import Path +from state import State +s = State(Path('$WORK')) +s.status = 'healthy' +s.layers = {'code_graph': {'configured': True}} +s.save() +s2 = State(Path('$WORK')) +s2.load() +assert s2.status == 'healthy', s2.status +assert s2.layers['code_graph']['configured'] is True +print('state roundtrip OK') +" + +echo "--- health_check (expect exit 1, no setup) ---" +if "$SKILL_ROOT/scripts/health_check.sh" "$WORK" >/dev/null; then + echo "FAIL: expected non-zero exit from health_check on unsetup project" + exit 1 +fi + +echo "ALL INTEGRATION CHECKS PASSED" +``` + +Make executable: +```bash +chmod +x tests/integration_test.sh +``` + +- [ ] **Step 2: Run it** + +```bash +tests/integration_test.sh +``` +Expected: `ALL INTEGRATION CHECKS PASSED`. + +- [ ] **Step 3: Commit** + +```bash +git add tests/integration_test.sh +git commit -m "kg-setup: add integration smoke test for per-project pipeline" +``` + +--- + +## Phase E — Install + real-world smoke (Tasks 15-17) + +### Task 15: Symlink into `~/.claude/skills/` + +**Files:** +- External: `~/.claude/skills/kg-setup` symlink + +- [ ] **Step 1: Create the symlink** + +```bash +ln -sfn "$(pwd)" "$HOME/.claude/skills/kg-setup" +ls -la "$HOME/.claude/skills/kg-setup" +``` +Expected: symlink pointing to `~/Projects/kg-setup`. + +- [ ] **Step 2: Verify Claude Code picks it up** + +In a fresh Claude Code session (or the current one — the skill list reloads dynamically), check that `kg-setup` appears in the available skills list. The user verifies manually. + +- [ ] **Step 3: Commit nothing (external action, not repo)** + +--- + +### Task 16: End-to-end smoke on a scratch project + +**Files:** +- None in repo; external verification only. + +- [ ] **Step 1: Create a scratch project** + +```bash +mkdir -p /tmp/kg-e2e && cd /tmp/kg-e2e +git init -q -b main +git remote add origin git@example.com:test/kg-e2e.git +echo "def add(a, b): return a + b" > calc.py +echo "flask" > requirements.txt +git add . && git commit -qm "init" +``` + +- [ ] **Step 2: Trigger the skill via Claude Code** + +In Claude Code (with cwd = `/tmp/kg-e2e`): say "настрой граф знаний для этого проекта". Claude should pick up `kg-setup`, run through phases, ask install confirmation, then complete. + +- [ ] **Step 3: Verify artifacts** + +```bash +ls -la /tmp/kg-e2e/ +# Expect: .codegraph/, .gitnexus/, CLAUDE.md, .kg-setup-state.json +cat /tmp/kg-e2e/.kg-setup-state.json +# Expect: last_run_status: "healthy" +grep "kg-setup-v1" /tmp/kg-e2e/CLAUDE.md +# Expect: marker present +claude mcp list | grep -E 'codegraph|gitnexus' +# Expect: both rows +``` + +- [ ] **Step 4: Re-run and verify idempotency** + +Say: "настрой граф знаний" again. Claude should detect existing state, ask (r/f/s/q), default (r), run health check, print report. No file diffs. + +- [ ] **Step 5: Cleanup** + +```bash +rm -rf /tmp/kg-e2e +``` + +--- + +### Task 17: Smoke on arb-scanner itself + final commit + +**Files:** +- Potentially modifies `./CLAUDE.md`, `.kg-setup-state.json` (gitignored), `.gitignore`. Verification uses pre-existing project. + +- [ ] **Step 1: Ensure arb-scanner CLAUDE.md is committed** + +```bash +git status --porcelain CLAUDE.md +``` +Expected: empty output (no uncommitted changes). If there are, commit or stash them first — the skill will refuse to write a dirty CLAUDE.md. + +- [ ] **Step 2: Trigger the skill on arb-scanner** + +Say to Claude: "настрой граф знаний для этого проекта". + +- [ ] **Step 3: Verify artifacts** + +```bash +ls -la .codegraph/ .gitnexus/ .kg-setup-state.json +grep "kg-setup-v1" CLAUDE.md +``` + +- [ ] **Step 4: Verify Obsidian side** + +Via Obsidian MCP `list_directory arb-scanner/` — expect `_index.md` now in the folder alongside the existing 15 notes. + +- [ ] **Step 5: Verify auto-memory pointer** + +```bash +cat ~/.claude/projects/-Users-pavelmalkin-Documents-Scaner/memory/MEMORY.md | tail -5 +ls ~/.claude/projects/-Users-pavelmalkin-Documents-Scaner/memory/project_kg_arb-scanner.md +``` +Expected: new pointer line, new memory file. + +- [ ] **Step 6: Manual token-usage smoke** + +Ask me 3 architectural questions about arb-scanner ("describe matcher.py's arb detection", "which BKs are configured for proxy routing", "how is the scan loop started"). Confirm I use `mcp__codegraph__*` tool calls instead of reading source files. Note: this is a qualitative check, not a hard SLA. + +- [ ] **Step 7: Commit the resulting CLAUDE.md change** + +```bash +git add CLAUDE.md +git commit -m "CLAUDE.md: add kg-setup Knowledge Graph section" +``` + +Note: `.codegraph/`, `.gitnexus/`, and `.kg-setup-state.json` are all gitignored (the skill appends them to `.gitignore` in Phase 3e if missing). + +--- + +## Self-review checklist (performed before handoff) + +- [x] Every spec section maps to ≥1 task: Phase A covers §3.1 layers + §4.11 state; Phase B covers §4.3, §4.4, §4.10, §8; Phase C covers §4.5, §4.6, §4.7, §4.8, §4.9; Phase D covers §4.1 orchestrator + §7.2 integration; Phase E covers §9 install/deploy + §7.3 manual smoke. +- [x] No "TBD", "add error handling", "similar to Task N" placeholders. +- [x] Type/name consistency: `MARKER_START`, `MARKER_END`, `USER_CONTENT_MARKER` used consistently across `merge_claude_md.py` and its tests. `State` class, `STATE_FILENAME`, `SCHEMA_VERSION` consistent across `state.py`/tests. `KG_SETUP_REFRESH` env var naming consistent across scripts and SKILL.md. +- [x] Every code step includes the actual code, not a reference. +- [x] Every test step includes a specific run command + expected output. +- [x] Commit messages match user's existing style (short, imperative, prefixed by component). +- [x] Out-of-scope items (remote/SSH, cron re-index, chat→Obsidian) explicitly skipped in §10 of spec, not snuck back in. + +## Execution handoff + +Plan complete and saved to `docs/superpowers/plans/2026-04-22-kg-setup.md`. Two execution options: + +1. **Subagent-Driven (recommended)** — a fresh subagent is dispatched per task, the main session reviews between tasks. Fast iteration, clean context per task. +2. **Inline Execution** — tasks run in this session with checkpoints for review. More control, heavier on main-session context. + +Which approach?