feat(kvb-agent-optimize): index repositories as a code graph for agents
The new category agent holds skills that act on an agent's own setup. kvb-agent-optimize sets up the general code index codebase-memory-mcp (MIT, fully local) for every repository in scope and tightens its agent instructions: agents.py reports whether AGENTS.md exists, whether CLAUDE.md and GEMINI.md point to it, dead relative links, whether it names the index, whether the binary is installed and the git state of the shared snapshot; the operator installs the index through kvb-org-operator; the agent indexes with index_repository and check_index_coverage, asks whether the snapshot is shared, sends code questions to the graph first, audits the instructions with prompt-audit and confirms every change. A domain graph the generic index cannot see goes to the line that owns the repository. kvb-optimize runs it as step 9, so kvb-dream does too. test_naming lists the category, test_dream the optimizer, and test_agent_optimize covers the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
269590a93d
commit
43b0949f6b
7 files changed
+255
-4
No files matched your search
@@ -9,7 +9,7 @@ Compatible with Claude Code, Codex, Gemini CLI, Cursor, Copilot, Windsurf, Cline
|
|||||||
|
|
||||||
## Naming 🏷️
|
## Naming 🏷️
|
||||||
|
|
||||||
Every first-party skill is named `kvb-<category>-<name>` 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-<category>-<name>` 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 🏛️
|
## Organigram 🏛️
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ flowchart LR
|
|||||||
kvb --> git[git]
|
kvb --> git[git]
|
||||||
kvb --> check[check]
|
kvb --> check[check]
|
||||||
kvb --> org[org]
|
kvb --> org[org]
|
||||||
|
kvb --> agent[agent]
|
||||||
kvb --> skill[skill]
|
kvb --> skill[skill]
|
||||||
kvb --> coach[coach]
|
kvb --> coach[coach]
|
||||||
kvb --> none[no category]
|
kvb --> none[no category]
|
||||||
@@ -72,6 +73,7 @@ flowchart LR
|
|||||||
org --> org_robot[kvb-org-robot]
|
org --> org_robot[kvb-org-robot]
|
||||||
org --> org_operator[kvb-org-operator]
|
org --> org_operator[kvb-org-operator]
|
||||||
org --> org_priority[kvb-org-priority]
|
org --> org_priority[kvb-org-priority]
|
||||||
|
agent --> agent_optimize[kvb-agent-optimize]
|
||||||
skill --> skill_tune[kvb-skill-tune]
|
skill --> skill_tune[kvb-skill-tune]
|
||||||
skill --> skill_auto[kvb-skill-auto]
|
skill --> skill_auto[kvb-skill-auto]
|
||||||
skill --> skill_essence[kvb-skill-essence]
|
skill --> skill_essence[kvb-skill-essence]
|
||||||
@@ -160,6 +162,9 @@ flowchart LR
|
|||||||
operator[kvb-org-operator]
|
operator[kvb-org-operator]
|
||||||
priority[kvb-org-priority]
|
priority[kvb-org-priority]
|
||||||
end
|
end
|
||||||
|
subgraph cat_agent [agent]
|
||||||
|
agent_optimize[kvb-agent-optimize]
|
||||||
|
end
|
||||||
subgraph cat_skill [skill]
|
subgraph cat_skill [skill]
|
||||||
autotune[kvb-skill-tune]
|
autotune[kvb-skill-tune]
|
||||||
autoskill[kvb-skill-auto]
|
autoskill[kvb-skill-auto]
|
||||||
@@ -272,7 +277,10 @@ flowchart LR
|
|||||||
optimize --> essence
|
optimize --> essence
|
||||||
optimize --> refactor
|
optimize --> refactor
|
||||||
optimize --> tune
|
optimize --> tune
|
||||||
|
optimize --> agent_optimize
|
||||||
optimize --> gaps
|
optimize --> gaps
|
||||||
|
agent_optimize -. install .-> operator
|
||||||
|
agent_optimize -. domain graphs .-> rasci
|
||||||
coach --> active_listening
|
coach --> active_listening
|
||||||
coach --> operator
|
coach --> operator
|
||||||
coach --> rasci
|
coach --> rasci
|
||||||
|
|||||||
@@ -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 <dir>` 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 |
|
||||||
Executable
+85
@@ -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())
|
||||||
@@ -26,8 +26,10 @@ inventory is skipped with one line.
|
|||||||
7. `kvb-code-refactor`: bring the uncommitted changes up to the project's
|
7. `kvb-code-refactor`: bring the uncommitted changes up to the project's
|
||||||
standards.
|
standards.
|
||||||
8. `kvb-code-tune`: optimize the hot paths the evidence measured.
|
8. `kvb-code-tune`: optimize the hot paths the evidence measured.
|
||||||
9. `kvb-think-gaps`: find the missing work and hand each pick to
|
9. `kvb-agent-optimize`: index every repository in scope as a code graph
|
||||||
`kvb-create-requirement`.
|
and tighten its agent instructions.
|
||||||
|
10. `kvb-think-gaps`: find the missing work and hand each pick to
|
||||||
|
`kvb-create-requirement`.
|
||||||
|
|
||||||
## Questions
|
## Questions
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -38,6 +38,7 @@ class TestDreamSkill(unittest.TestCase):
|
|||||||
"kvb-skill-essence",
|
"kvb-skill-essence",
|
||||||
"kvb-code-refactor",
|
"kvb-code-refactor",
|
||||||
"kvb-code-tune",
|
"kvb-code-tune",
|
||||||
|
"kvb-agent-optimize",
|
||||||
"kvb-think-gaps",
|
"kvb-think-gaps",
|
||||||
):
|
):
|
||||||
self.assertIn(f"`{skill}`", text)
|
self.assertIn(f"`{skill}`", text)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pathlib import Path
|
|||||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
SKILLS = REPO_ROOT / "skills"
|
SKILLS = REPO_ROOT / "skills"
|
||||||
README = REPO_ROOT / "README.md"
|
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(
|
UNCATEGORIZED = frozenset(
|
||||||
{
|
{
|
||||||
"kvb-cleanup",
|
"kvb-cleanup",
|
||||||
|
|||||||
Reference in new issue
Block a user