diff --git a/README.md b/README.md index f7a7144..7ba0e21 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline ## Naming 🏷️ -Every first-party skill is named `kvb--` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `skill`, `coach`: `think` thinks without producing a file, `create` produces an artifact, `code` works on code, `ci` on pipelines, `git` on history and forges, `check` analyses and reports findings, `org` coordinates agents, `skill` works on skills themselves, `coach` serves the operator's wellbeing. Only `kvb-cleanup`, `kvb-coach`, `kvb-help` and the collectors `kvb-dream`, `kvb-optimize` and `kvb-transcend` stand without a category. Names use `a-z`, `0-9` and single hyphens, at most 64 characters, and equal their directory name. `tests/test_naming.py` enforces all of it, including that the organigram below lists every skill under its category. +Every first-party skill is named `kvb--` with one of these categories: `think`, `create`, `code`, `ci`, `git`, `check`, `org`, `agent`, `skill`, `coach`: `think` thinks without producing a file, `create` produces an artifact, `code` works on code, `ci` on pipelines, `git` on history and forges, `check` analyses and reports findings, `org` coordinates agents, `agent` makes a repository easy for agents (code index, agent instructions), `skill` works on skills themselves, `coach` serves the operator's wellbeing. Only `kvb-cleanup`, `kvb-coach`, `kvb-help` and the collectors `kvb-dream`, `kvb-optimize` and `kvb-transcend` stand without a category. Names use `a-z`, `0-9` and single hyphens, at most 64 characters, and equal their directory name. `tests/test_naming.py` enforces all of it, including that the organigram below lists every skill under its category. ## Organigram 🏛️ @@ -25,6 +25,7 @@ flowchart LR kvb --> git[git] kvb --> check[check] kvb --> org[org] + kvb --> agent[agent] kvb --> skill[skill] kvb --> coach[coach] kvb --> none[no category] @@ -72,6 +73,7 @@ flowchart LR org --> org_robot[kvb-org-robot] org --> org_operator[kvb-org-operator] org --> org_priority[kvb-org-priority] + agent --> agent_optimize[kvb-agent-optimize] skill --> skill_tune[kvb-skill-tune] skill --> skill_auto[kvb-skill-auto] skill --> skill_essence[kvb-skill-essence] @@ -160,6 +162,9 @@ flowchart LR operator[kvb-org-operator] priority[kvb-org-priority] end + subgraph cat_agent [agent] + agent_optimize[kvb-agent-optimize] + end subgraph cat_skill [skill] autotune[kvb-skill-tune] autoskill[kvb-skill-auto] @@ -272,7 +277,10 @@ flowchart LR optimize --> essence optimize --> refactor optimize --> tune + optimize --> agent_optimize optimize --> gaps + agent_optimize -. install .-> operator + agent_optimize -. domain graphs .-> rasci coach --> active_listening coach --> operator coach --> rasci diff --git a/skills/kvb-agent-optimize/SKILL.md b/skills/kvb-agent-optimize/SKILL.md new file mode 100644 index 0000000..3a53e4c --- /dev/null +++ b/skills/kvb-agent-optimize/SKILL.md @@ -0,0 +1,66 @@ +--- +name: kvb-agent-optimize +description: > + Make a repository easy for agents: a code graph index (codebase-memory-mcp) + and lean agent instructions. Trigger on /kvb-agent-optimize; kvb-optimize + runs it. +--- + +An agent that queries a graph reads a fraction of what an agent exploring +file by file reads, and short, current, consistent instructions get followed. +This skill sets up the general code index, +[codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) (MIT, +fully local), and tightens the agent instructions of every repository in +scope: the current one, or those `kvb-optimize` hands over. + +## Step 1: inventory + +`${CLAUDE_SKILL_DIR}/scripts/agents.py --repo ` prints one line per +check: AGENTS.md, whether CLAUDE.md and GEMINI.md point to it, dead relative +links in it, whether it names the index, whether the index binary is +installed, and the state of the shared snapshot +`.codebase-memory/graph.db.zst`. + +## Step 2: index + +- **Binary missing**: installing registers an MCP server in the agent + configuration, so it is the operator's step; hand these to the + `kvb-org-operator` skill and continue after the restart: + + ```bash + yay -S codebase-memory-mcp-bin + claude plugin marketplace add DeusData/codebase-memory-mcp + claude plugin install codebase-memory@codebase-memory + ``` + +- **Index** with the MCP tool `index_repository` on the repository path, then + `check_index_coverage`; every file kind it leaves out is a finding. +- **Snapshot**: ask whether the repository shares its graph. Shared: run + `index_repository` with `persistence: true` and commit + `.codebase-memory/graph.db.zst` at a fixed cadence (every commit of it adds + a full blob; Git LFS avoids that). Not shared: add `.codebase-memory/` to + `.gitignore`, and every agent indexes on its own. +- **Domain graphs** the generic index cannot see, such as dependencies a + project declares in its own metadata, belong to the line that owns the + repository: record them as its task in the `kvb-org-rasci` register, never + build them here. + +## Step 3: instructions + +- AGENTS.md is the single source; CLAUDE.md and GEMINI.md only point to it + (`@AGENTS.md`). +- AGENTS.md sends code questions to the graph first (`search_graph`, + `trace_path`, `get_architecture`) and to grep only after it. +- `/claude-api prompt-audit` over AGENTS.md and the files it references finds + stale, duplicate and contradicting rules; the dead links of step 1 join its + findings. + +## Step 4: confirm + +One `AskUserQuestion` per improvement, `multiSelect: false`: the change, its +evidence, and `Apply now`, `Record as a task`, `Skip`. No repository file +changes before the answer, and nothing is committed. + +## Output + +| Repository | Check | Before | After | Action | diff --git a/skills/kvb-agent-optimize/scripts/agents.py b/skills/kvb-agent-optimize/scripts/agents.py new file mode 100755 index 0000000..511aa57 --- /dev/null +++ b/skills/kvb-agent-optimize/scripts/agents.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Report how ready a repository is for agents: instructions, pointers, code index. + +Args: + --repo: path of the repository. + +Output: + one tab-separated line per check: check, state, detail. States are ok, + missing, drift, or for the snapshot tracked, ignored or untracked. + +Exit codes: + 0 report printed, 2 usage error. +""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + +POINTERS = ("CLAUDE.md", "GEMINI.md") +INDEX = "codebase-memory-mcp" +SNAPSHOT = ".codebase-memory/graph.db.zst" +LINK = re.compile(r"\]\(([^)\s#]+)") + + +def fail(message: str) -> None: + print(f"agents: {message}", file=sys.stderr) + sys.exit(2) + + +def git_ok(repo: Path, *args: str) -> bool: + return subprocess.run(["git", *args], cwd=repo, capture_output=True, check=False).returncode == 0 + + +def snapshot_state(repo: Path) -> str: + if not (repo / SNAPSHOT).exists(): + return "missing" + if git_ok(repo, "ls-files", "--error-unmatch", SNAPSHOT): + return "tracked" + if git_ok(repo, "check-ignore", "-q", SNAPSHOT): + return "ignored" + return "untracked" + + +def checks(repo: Path) -> list[tuple[str, str, str]]: + agents = repo / "AGENTS.md" + text = agents.read_text(encoding="utf-8") if agents.is_file() else "" + rows = [("AGENTS.md", "ok" if text else "missing", "single source of the agent instructions")] + for name in POINTERS: + pointer = repo / name + if pointer.is_file(): + points = "AGENTS.md" in pointer.read_text(encoding="utf-8") + rows.append((name, "ok" if points else "drift", f"{'points' if points else 'does not point'} to AGENTS.md")) + dead = sorted( + { + target + for target in LINK.findall(text) + if ":" not in target and not (repo / target).exists() + } + ) + rows.append(("links", "drift" if dead else "ok", ", ".join(dead) or "every relative link resolves")) + rows.append(("index-pointer", "ok" if INDEX in text else "missing", f"AGENTS.md names {INDEX}")) + rows.append(("index-binary", "ok" if shutil.which(INDEX) else "missing", f"{INDEX} on PATH")) + rows.append(("snapshot", snapshot_state(repo), SNAPSHOT)) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--repo", required=True) + args = parser.parse_args() + repo = Path(args.repo) + if not repo.is_dir(): + fail(f"{repo} is not a directory") + for row in checks(repo): + print("\t".join(row)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/kvb-optimize/SKILL.md b/skills/kvb-optimize/SKILL.md index db61b29..a11bbc4 100644 --- a/skills/kvb-optimize/SKILL.md +++ b/skills/kvb-optimize/SKILL.md @@ -26,8 +26,10 @@ inventory is skipped with one line. 7. `kvb-code-refactor`: bring the uncommitted changes up to the project's standards. 8. `kvb-code-tune`: optimize the hot paths the evidence measured. -9. `kvb-think-gaps`: find the missing work and hand each pick to - `kvb-create-requirement`. +9. `kvb-agent-optimize`: index every repository in scope as a code graph + and tighten its agent instructions. +10. `kvb-think-gaps`: find the missing work and hand each pick to + `kvb-create-requirement`. ## Questions diff --git a/tests/test_agent_optimize.py b/tests/test_agent_optimize.py new file mode 100644 index 0000000..668babe --- /dev/null +++ b/tests/test_agent_optimize.py @@ -0,0 +1,89 @@ +"""Exercise agents.py: instruction pointers, dead links, index pointer, binary and snapshot state.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = REPO_ROOT / "skills" / "kvb-agent-optimize" / "scripts" / "agents.py" +GIT = shutil.which("git") + + +class TestAgents(unittest.TestCase): + def setUp(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.tmp = Path(tmp.name) + self.bin = self.tmp / "bin" + self.bin.mkdir() + os.symlink(GIT, self.bin / "git") + + def _run(self, repo: Path) -> subprocess.CompletedProcess: + return subprocess.run( + [sys.executable, str(SCRIPT), "--repo", str(repo)], + capture_output=True, + text=True, + env={"PATH": str(self.bin), "HOME": str(self.tmp)}, + check=False, + ) + + def _report(self, repo: Path) -> dict[str, tuple[str, str]]: + result = self._run(repo) + self.assertEqual(result.returncode, 0, result.stderr) + return {check: (state, detail) for check, state, detail in (line.split("\t") for line in result.stdout.splitlines())} + + def test_ready_repository_with_drift(self): + repo = self.tmp / "repo" + (repo / "docs").mkdir(parents=True) + (repo / "docs" / "a.md").write_text("a", encoding="utf-8") + (repo / "AGENTS.md").write_text( + "Query codebase-memory-mcp first. [a](docs/a.md) [b](docs/b.md#x) [web](https://x.org) [top](#top)\n", + encoding="utf-8", + ) + (repo / "CLAUDE.md").write_text("@AGENTS.md\n", encoding="utf-8") + (repo / "GEMINI.md").write_text("own rules\n", encoding="utf-8") + (repo / ".codebase-memory").mkdir() + (repo / ".codebase-memory" / "graph.db.zst").write_bytes(b"x") + (repo / ".gitignore").write_text(".codebase-memory/\n", encoding="utf-8") + subprocess.run([GIT, "init", "-q"], cwd=repo, check=True) + executable = self.bin / "codebase-memory-mcp" + executable.write_text("#!/bin/sh\n", encoding="utf-8") + executable.chmod(0o755) + self.assertEqual( + self._report(repo), + { + "AGENTS.md": ("ok", "single source of the agent instructions"), + "CLAUDE.md": ("ok", "points to AGENTS.md"), + "GEMINI.md": ("drift", "does not point to AGENTS.md"), + "links": ("drift", "docs/b.md"), + "index-pointer": ("ok", "AGENTS.md names codebase-memory-mcp"), + "index-binary": ("ok", "codebase-memory-mcp on PATH"), + "snapshot": ("ignored", ".codebase-memory/graph.db.zst"), + }, + ) + + def test_bare_directory(self): + report = self._report(self.tmp) + self.assertEqual( + {check: state for check, (state, _) in report.items()}, + { + "AGENTS.md": "missing", + "links": "ok", + "index-pointer": "missing", + "index-binary": "missing", + "snapshot": "missing", + }, + ) + + def test_missing_repository_exits_2(self): + self.assertEqual(self._run(self.tmp / "none").returncode, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dream.py b/tests/test_dream.py index 900c24f..f56c531 100644 --- a/tests/test_dream.py +++ b/tests/test_dream.py @@ -38,6 +38,7 @@ class TestDreamSkill(unittest.TestCase): "kvb-skill-essence", "kvb-code-refactor", "kvb-code-tune", + "kvb-agent-optimize", "kvb-think-gaps", ): self.assertIn(f"`{skill}`", text) diff --git a/tests/test_naming.py b/tests/test_naming.py index a4ef450..f4e31a4 100644 --- a/tests/test_naming.py +++ b/tests/test_naming.py @@ -9,7 +9,7 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] SKILLS = REPO_ROOT / "skills" README = REPO_ROOT / "README.md" -CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "skill", "coach") +CATEGORIES = ("think", "create", "code", "ci", "git", "check", "org", "agent", "skill", "coach") UNCATEGORIZED = frozenset( { "kvb-cleanup",