codebase-explorer 2.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. cbe/__init__.py +9 -0
  2. cbe/__main__.py +4 -0
  3. cbe/aggregation.py +183 -0
  4. cbe/api_completion.py +410 -0
  5. cbe/batching.py +283 -0
  6. cbe/catalog_query.py +376 -0
  7. cbe/checks.py +390 -0
  8. cbe/cli.py +434 -0
  9. cbe/concrete_facts.py +321 -0
  10. cbe/data/LICENSE.tiktoken +30 -0
  11. cbe/data/o200k_base.tiktoken +199998 -0
  12. cbe/declarations.py +537 -0
  13. cbe/docs.py +929 -0
  14. cbe/generate.py +316 -0
  15. cbe/grammars.py +741 -0
  16. cbe/graph.py +857 -0
  17. cbe/graph_backends/__init__.py +43 -0
  18. cbe/graph_backends/_calls.py +262 -0
  19. cbe/graph_backends/_paths.py +42 -0
  20. cbe/graph_backends/identity.py +93 -0
  21. cbe/graph_backends/imports.py +256 -0
  22. cbe/graph_backends/joern.py +1021 -0
  23. cbe/graph_backends/resolution.py +288 -0
  24. cbe/graph_backends/toolchain.py +396 -0
  25. cbe/grounding.py +57 -0
  26. cbe/host_run.py +1752 -0
  27. cbe/model_settings.py +80 -0
  28. cbe/projection.py +305 -0
  29. cbe/prompts.py +316 -0
  30. cbe/records.py +200 -0
  31. cbe/skeleton.py +474 -0
  32. cbe/source_context.py +709 -0
  33. cbe/symbols.py +136 -0
  34. cbe/token_budget.py +68 -0
  35. cbe/treename.py +398 -0
  36. cbe/usage.py +300 -0
  37. cbe_install/__init__.py +11 -0
  38. cbe_install/cli.py +434 -0
  39. cbe_install/resources/agents/cbe-writer.md +16 -0
  40. cbe_install/resources/codebase-explorer/SKILL.md +101 -0
  41. cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
  42. cbe_install/resources/codebase-explorer/references/codex.md +37 -0
  43. codebase_explorer-2.2.0.dist-info/METADATA +233 -0
  44. codebase_explorer-2.2.0.dist-info/RECORD +47 -0
  45. codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
  46. codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
  47. codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
cbe_install/cli.py ADDED
@@ -0,0 +1,434 @@
1
+ """Small, ownership-aware installer for the skills shipped inside this wheel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import hashlib
7
+ import importlib
8
+ import json
9
+ import os
10
+ import shlex
11
+ import shutil
12
+ import sys
13
+ import tempfile
14
+ from importlib import resources
15
+ from importlib.metadata import PackageNotFoundError, version
16
+ from pathlib import Path, PurePosixPath
17
+
18
+ from . import (AGENTS_RESOURCE, BUSINESS_MODULE, PRODUCT, RUNTIME_MODULES, RUNTIME_SKILLS, SKILLS,
19
+ SOURCE_AGENT_BASE, SOURCE_PARENT_LEVEL, SOURCE_SKILL_BASE)
20
+
21
+ MANIFEST = ".skill-install.json"
22
+ HOSTS = ("codex", "claude-code", "dsh", "opencode", "pi")
23
+ PROJECT_DIRS = {
24
+ "codex": (".agents", "skills"),
25
+ "claude-code": (".claude", "skills"),
26
+ "dsh": (".dsh", "skills"),
27
+ "opencode": (".opencode", "skills"),
28
+ "pi": (".pi", "skills"),
29
+ }
30
+ USER_DIRS = {
31
+ "codex": (".agents", "skills"),
32
+ "claude-code": (".claude", "skills"),
33
+ "dsh": (".dsh", "skills"),
34
+ "opencode": (".config", "opencode", "skills"),
35
+ "pi": (".pi", "agent", "skills"),
36
+ }
37
+
38
+
39
+ class InstallError(Exception):
40
+ """An actionable input, dependency, or ownership conflict."""
41
+
42
+
43
+ def _digest(data: bytes) -> str:
44
+ return hashlib.sha256(data).hexdigest()
45
+
46
+
47
+ def _runtime_python() -> Path:
48
+ return Path(sys.executable).absolute()
49
+
50
+
51
+ def _with_runtime(skill_bytes: bytes) -> bytes:
52
+ python = str(_runtime_python())
53
+ command = shlex.quote(python)
54
+ lines = [
55
+ "\n## Installed runtime (generated by cbe-install)",
56
+ f"Python executable: `{python}`. Use it as one argument, even when its path contains spaces.",
57
+ "Replace bare CLI names in the examples below with these absolute entry points; no PATH or source checkout is required:",
58
+ *(f"- `{module}`: `{command} -m {module}`" for module in RUNTIME_MODULES),
59
+ "Each entry point is several words, so write the whole command in every call. Do not store it in a shell variable: zsh does not split an unquoted variable into words. A shell function that runs it also works if you define it in the same call that uses it.",
60
+ "Re-run `cbe-install upgrade` after moving or reinstalling the tool environment.\n",
61
+ ]
62
+ note = ("\n".join(lines) + "\n").encode("utf-8")
63
+ closing = skill_bytes.find(b"\n---\n", 4) if skill_bytes.startswith(b"---\n") else -1
64
+ position = closing + len(b"\n---\n") if closing >= 0 else 0
65
+ return skill_bytes[:position] + note + skill_bytes[position:]
66
+
67
+
68
+ def _release_version() -> str:
69
+ try:
70
+ return version(PRODUCT)
71
+ except PackageNotFoundError:
72
+ return "source"
73
+
74
+
75
+ def _runtime_check() -> None:
76
+ try:
77
+ importlib.import_module(BUSINESS_MODULE)
78
+ except (ImportError, OSError) as exc:
79
+ missing = getattr(exc, "name", None) or BUSINESS_MODULE
80
+ raise InstallError(
81
+ f"runtime dependency {missing!r} is unavailable ({exc}); "
82
+ f"reinstall {PRODUCT} with dependencies using uv tool install --reinstall <package-or-wheel>"
83
+ ) from exc
84
+
85
+
86
+ def _checkout_root() -> Path | None:
87
+ """The author checkout this module runs from, or None inside an installed wheel."""
88
+ location = Path(__file__).resolve()
89
+ candidate = location.parents[SOURCE_PARENT_LEVEL]
90
+ if (
91
+ not {"site-packages", "dist-packages"}.intersection(location.parts)
92
+ and (candidate / "pyproject.toml").is_file()
93
+ ):
94
+ return candidate
95
+ return None
96
+
97
+
98
+ def _agent_files() -> dict[str, bytes]:
99
+ """The Claude Code subagent definitions that ship with the skill, by file name."""
100
+ root = resources.files(__package__).joinpath("resources", AGENTS_RESOURCE)
101
+ if not root.is_dir() and (checkout := _checkout_root()) is not None:
102
+ root = checkout / SOURCE_AGENT_BASE
103
+ if not root.is_dir():
104
+ raise InstallError(f"wheel is missing the agent resources; rebuild {PRODUCT} from its release source")
105
+ found = {child.name: child.read_bytes() for child in root.iterdir() if child.is_file()}
106
+ if not found:
107
+ raise InstallError(f"wheel has no agent definitions; rebuild {PRODUCT} from its release source")
108
+ return found
109
+
110
+
111
+ def _resource_files(skill: str) -> dict[str, bytes]:
112
+ root = resources.files(__package__).joinpath("resources", skill)
113
+ if not root.is_dir() and (checkout := _checkout_root()) is not None:
114
+ # Development checkouts use the same canonical files as the wheel build.
115
+ # A wheel without embedded resources never reaches into an author checkout.
116
+ root = checkout / SOURCE_SKILL_BASE / skill
117
+ if not root.is_dir():
118
+ raise InstallError(f"wheel is missing skill resource {skill!r}; rebuild {PRODUCT} from its release source")
119
+ found: dict[str, bytes] = {}
120
+
121
+ def collect(node, prefix: PurePosixPath) -> None:
122
+ for child in node.iterdir():
123
+ rel = prefix / child.name
124
+ if child.is_dir():
125
+ collect(child, rel)
126
+ elif child.is_file():
127
+ found[rel.as_posix()] = child.read_bytes()
128
+
129
+ collect(root, PurePosixPath())
130
+ if "SKILL.md" not in found:
131
+ raise InstallError(f"wheel skill {skill!r} has no SKILL.md; rebuild the release")
132
+ if skill in RUNTIME_SKILLS:
133
+ found["SKILL.md"] = _with_runtime(found["SKILL.md"])
134
+ return found
135
+
136
+
137
+ def _valid_file_list(files: object) -> bool:
138
+ return isinstance(files, dict) and not any(
139
+ not isinstance(name, str)
140
+ or not isinstance(digest, str)
141
+ or digest != digest.lower()
142
+ or len(digest) != 64
143
+ or not name
144
+ or "\\" in name
145
+ or PurePosixPath(name).is_absolute()
146
+ or ".." in PurePosixPath(name).parts
147
+ or PurePosixPath(name).as_posix() != name
148
+ or name == MANIFEST
149
+ for name, digest in files.items()
150
+ )
151
+
152
+
153
+ def _read_manifest(destination: Path, skill: str) -> dict | None:
154
+ path = destination / MANIFEST
155
+ if not path.exists():
156
+ return None
157
+ if path.is_symlink():
158
+ raise InstallError(f"refusing symlinked ownership manifest: {path}")
159
+ try:
160
+ manifest = json.loads(path.read_text(encoding="utf-8"))
161
+ except (OSError, ValueError) as exc:
162
+ raise InstallError(f"cannot read ownership manifest {path}: {exc}") from exc
163
+ if not isinstance(manifest, dict) or manifest.get("product") != PRODUCT or manifest.get("skill") != skill:
164
+ raise InstallError(f"{path} belongs to another product or is invalid; inspect it before changing this skill")
165
+ if not _valid_file_list(manifest.get("files")) or not _valid_file_list(manifest.get("agents", {})):
166
+ raise InstallError(f"invalid file list in {path}; inspect it before changing this skill")
167
+ return manifest
168
+
169
+
170
+ def _check_existing(base: Path, tracked: dict[str, str], *, allow_missing: bool = False,
171
+ guard_parents: bool = True) -> None:
172
+ """Every tracked file under ``base`` is the one this installer wrote.
173
+
174
+ A skill directory must not sit behind a symlink (``guard_parents``); the shared agents directory may."""
175
+ for rel, expected in tracked.items():
176
+ path = base / rel
177
+ if path.is_symlink() or (guard_parents and any(
178
+ parent.is_symlink() for parent in path.parents if parent != base.parent)):
179
+ raise InstallError(f"refusing symlinked installed path: {path}")
180
+ if not path.exists():
181
+ if allow_missing:
182
+ continue
183
+ raise InstallError(f"installed file was removed: {path}; restore it or resolve ownership manually")
184
+ if not path.is_file() or _digest(path.read_bytes()) != expected:
185
+ raise InstallError(f"user-modified installed file: {path}; save your edits before upgrading or uninstalling")
186
+
187
+
188
+ def _check_symlinks(destination: Path) -> None:
189
+ if destination.is_symlink():
190
+ raise InstallError(f"refusing symlinked skill directory: {destination}")
191
+ if destination.exists():
192
+ for path in destination.rglob("*"):
193
+ if path.is_symlink():
194
+ raise InstallError(f"refusing symlink within skill directory: {path}")
195
+
196
+
197
+ def _recover_backup(destination: Path) -> None:
198
+ backup = destination.with_name(f".{destination.name}.{PRODUCT}.backup")
199
+ if not backup.exists():
200
+ return
201
+ if destination.exists():
202
+ raise InstallError(
203
+ f"both {destination} and recovery backup {backup} exist; inspect them before retrying"
204
+ )
205
+ backup.rename(destination)
206
+
207
+
208
+ def _agents_dir(root: Path) -> Path | None:
209
+ """Claude Code reads subagent definitions from the ``agents`` directory beside its ``skills`` directory."""
210
+ return root.parent / "agents" if root.name == "skills" and root.parent.name == ".claude" else None
211
+
212
+
213
+ def _write_agent_files(agents_dir: Path, agents: dict[str, bytes]) -> list[str]:
214
+ agents_dir.mkdir(parents=True, exist_ok=True)
215
+ lines = []
216
+ for name, data in agents.items():
217
+ path = agents_dir / name
218
+ if path.is_file() and path.read_bytes() == data:
219
+ continue
220
+ staged = agents_dir / f".{name}.{PRODUCT}.stage"
221
+ staged.write_bytes(data)
222
+ os.replace(staged, path)
223
+ lines.append(f"installed {path}")
224
+ return lines
225
+
226
+
227
+ def _install_one(root: Path, skill: str) -> str:
228
+ root.mkdir(parents=True, exist_ok=True)
229
+ destination = root / skill
230
+ _recover_backup(destination)
231
+ _check_symlinks(destination)
232
+ desired = _resource_files(skill)
233
+ agents_dir = _agents_dir(root)
234
+ agents = _agent_files() if agents_dir else {}
235
+ prior = _read_manifest(destination, skill) if destination.exists() else None
236
+ if destination.exists() and prior is None:
237
+ raise InstallError(f"{destination} already exists without a {PRODUCT} ownership manifest; choose another directory")
238
+ tracked = prior["files"] if prior else {}
239
+ tracked_agents = prior.get("agents", {}) if prior else {}
240
+ if prior:
241
+ _check_existing(destination, tracked)
242
+ for rel in desired:
243
+ path = destination / rel
244
+ if path.exists() and rel not in tracked:
245
+ raise InstallError(f"unowned file conflicts with release: {path}; move it before installing")
246
+ if agents_dir:
247
+ _check_existing(agents_dir, tracked_agents, allow_missing=True, guard_parents=False)
248
+ for name in agents.keys() - tracked_agents.keys():
249
+ path = agents_dir / name
250
+ if path.exists() or path.is_symlink():
251
+ raise InstallError(f"unowned file conflicts with release: {path}; move it before installing")
252
+ hashes = {name: _digest(data) for name, data in desired.items()}
253
+ agent_hashes = {name: _digest(data) for name, data in agents.items()}
254
+ if (prior and tracked == hashes and tracked_agents == agent_hashes
255
+ and prior.get("version") == _release_version()
256
+ and all((agents_dir / name).is_file() for name in agents)):
257
+ return f"unchanged {destination}"
258
+
259
+ stage = Path(tempfile.mkdtemp(prefix=f".{skill}.stage-", dir=root))
260
+ backup = destination.with_name(f".{skill}.{PRODUCT}.backup")
261
+ try:
262
+ if destination.exists():
263
+ shutil.copytree(destination, stage, dirs_exist_ok=True)
264
+ for rel in tracked.keys() - hashes.keys():
265
+ (stage / rel).unlink()
266
+ for rel, data in desired.items():
267
+ target = stage / rel
268
+ target.parent.mkdir(parents=True, exist_ok=True)
269
+ target.write_bytes(data)
270
+ manifest = {
271
+ "product": PRODUCT,
272
+ "skill": skill,
273
+ "version": _release_version(),
274
+ "runtime_python": str(_runtime_python()),
275
+ "files": hashes,
276
+ **({"agents": agent_hashes} if agent_hashes else {}),
277
+ }
278
+ (stage / MANIFEST).write_text(json.dumps(manifest, sort_keys=True, indent=2) + "\n", encoding="utf-8")
279
+ if destination.exists():
280
+ destination.rename(backup)
281
+ try:
282
+ stage.rename(destination)
283
+ except OSError:
284
+ backup.rename(destination)
285
+ raise
286
+ shutil.rmtree(backup)
287
+ else:
288
+ stage.rename(destination)
289
+ finally:
290
+ if stage.exists():
291
+ shutil.rmtree(stage)
292
+ written = _write_agent_files(agents_dir, agents) if agents_dir else []
293
+ return "\n".join([f"installed {destination}", *written])
294
+
295
+
296
+ def _uninstall_one(root: Path, skill: str) -> str:
297
+ destination = root / skill
298
+ _recover_backup(destination)
299
+ if not destination.exists():
300
+ return f"absent {destination}"
301
+ _check_symlinks(destination)
302
+ prior = _read_manifest(destination, skill)
303
+ if prior is None:
304
+ raise InstallError(f"{destination} has no {PRODUCT} ownership manifest; refusing to remove it")
305
+ _check_existing(destination, prior["files"], allow_missing=True)
306
+ agents_dir = _agents_dir(root)
307
+ tracked_agents = prior.get("agents", {}) if agents_dir else {}
308
+ if agents_dir:
309
+ _check_existing(agents_dir, tracked_agents, allow_missing=True, guard_parents=False)
310
+ for rel in prior["files"]:
311
+ path = destination / rel
312
+ if path.exists():
313
+ path.unlink()
314
+ for name in tracked_agents:
315
+ (agents_dir / name).unlink(missing_ok=True)
316
+ (destination / MANIFEST).unlink()
317
+ for directory in sorted((p for p in destination.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True):
318
+ if not any(directory.iterdir()):
319
+ directory.rmdir()
320
+ if not any(destination.iterdir()):
321
+ destination.rmdir()
322
+ agents = f" and {len(tracked_agents)} agent file(s)" if tracked_agents else ""
323
+ return f"uninstalled {destination}{agents}; user files preserved"
324
+
325
+
326
+ def _targets(args: argparse.Namespace) -> list[Path]:
327
+ if args.dir:
328
+ return [Path(args.dir).expanduser().resolve()]
329
+ hosts = args.host or []
330
+ if not hosts:
331
+ raise InstallError("choose --host codex|claude-code|dsh|opencode|pi|all, or --dir PATH; use --interactive for prompts")
332
+ if "all" in hosts:
333
+ hosts = list(HOSTS)
334
+ hosts = list(dict.fromkeys(hosts))
335
+ if args.scope == "project":
336
+ project = Path(args.project or Path.cwd()).expanduser().resolve()
337
+ if not project.is_dir():
338
+ raise InstallError(f"project directory does not exist: {project}")
339
+ bases = [project.joinpath(*PROJECT_DIRS[host]) for host in hosts]
340
+ else:
341
+ bases = []
342
+ for host in hosts:
343
+ if host == "dsh" and os.environ.get("DSH_HOME"):
344
+ bases.append(Path(os.environ["DSH_HOME"]).expanduser() / "skills")
345
+ elif host == "opencode" and os.environ.get("XDG_CONFIG_HOME"):
346
+ bases.append(Path(os.environ["XDG_CONFIG_HOME"]).expanduser() / "opencode" / "skills")
347
+ else:
348
+ bases.append(Path.home().joinpath(*USER_DIRS[host]))
349
+ return list(dict.fromkeys(bases))
350
+
351
+
352
+ def _prompt(args: argparse.Namespace) -> None:
353
+ choice = input("Host (codex/claude-code/dsh/opencode/pi/all/custom): ").strip().lower()
354
+ if choice == "custom":
355
+ args.dir = input("Skill parent directory: ").strip()
356
+ if not args.dir:
357
+ raise InstallError("custom directory cannot be empty")
358
+ return
359
+ if choice not in (*HOSTS, "all"):
360
+ raise InstallError(f"unknown host {choice!r}")
361
+ args.host = [choice]
362
+ scope = input("Scope (project/user) [project]: ").strip().lower() or "project"
363
+ if scope not in ("project", "user"):
364
+ raise InstallError(f"unknown scope {scope!r}")
365
+ args.scope = scope
366
+ if scope == "project":
367
+ args.project = input(f"Project directory [{Path.cwd()}]: ").strip() or str(Path.cwd())
368
+
369
+
370
+ def _parser() -> argparse.ArgumentParser:
371
+ parser = argparse.ArgumentParser(
372
+ prog=f"{PRODUCT}-install",
373
+ description=f"Install {PRODUCT} wheel-bundled agent skills, and the Claude Code subagent beside .claude/skills, without changing host configuration",
374
+ )
375
+ sub = parser.add_subparsers(dest="command", required=True)
376
+ for command in ("install", "upgrade", "doctor", "uninstall"):
377
+ part = sub.add_parser(command)
378
+ part.add_argument("--host", action="append", choices=(*HOSTS, "all"), help="Repeat for multiple hosts")
379
+ part.add_argument("--scope", choices=("project", "user"), default="project")
380
+ part.add_argument("--project", help="Project root; used only with project scope")
381
+ part.add_argument("--dir", help="Explicit parent directory for skills; overrides host and scope. A directory named .claude/skills also gets the agents directory beside it")
382
+ part.add_argument("--interactive", action="store_true", help="Ask for host and path even when stdin is redirected")
383
+ return parser
384
+
385
+
386
+ def main(argv: list[str] | None = None) -> int:
387
+ args = _parser().parse_args(argv)
388
+ try:
389
+ if args.command in ("install", "upgrade", "doctor"):
390
+ _runtime_check()
391
+ for skill in SKILLS:
392
+ _resource_files(skill)
393
+ _agent_files()
394
+ if args.interactive or (args.command != "doctor" and not args.host and not args.dir and sys.stdin.isatty()):
395
+ _prompt(args)
396
+ if args.command == "doctor" and not args.host and not args.dir:
397
+ print(f"OK {PRODUCT} {_release_version()}: runtime, {len(SKILLS)} bundled skill(s), and {len(_agent_files())} Claude Code agent(s)")
398
+ return 0
399
+ for root in _targets(args):
400
+ for skill in SKILLS:
401
+ if args.command == "doctor":
402
+ destination = root / skill
403
+ _check_symlinks(destination)
404
+ manifest = _read_manifest(destination, skill)
405
+ if manifest is None:
406
+ raise InstallError(f"skill is not installed: {destination}; run {PRODUCT}-install install")
407
+ _check_existing(destination, manifest["files"])
408
+ runtime = manifest.get("runtime_python")
409
+ if isinstance(runtime, str) and runtime and not Path(runtime).is_file():
410
+ raise InstallError(f"installed runtime no longer exists: {runtime}; reinstall the tool and upgrade this skill")
411
+ print(f"OK {destination} ({manifest.get('version', 'unknown')})")
412
+ if agents_dir := _agents_dir(root):
413
+ tracked_agents = manifest.get("agents", {})
414
+ if tracked_agents.keys() != _agent_files().keys():
415
+ raise InstallError(f"agent files are not installed in {agents_dir}; run {PRODUCT}-install upgrade")
416
+ _check_existing(agents_dir, tracked_agents, guard_parents=False)
417
+ for name in tracked_agents:
418
+ print(f"OK {agents_dir / name}")
419
+ elif args.command == "uninstall":
420
+ print(_uninstall_one(root, skill))
421
+ else:
422
+ print(_install_one(root, skill))
423
+ if args.command in ("install", "upgrade"):
424
+ command = shlex.quote(str(_runtime_python()))
425
+ for module in RUNTIME_MODULES:
426
+ print(f"runtime {module}: {command} -m {module}")
427
+ return 0
428
+ except (InstallError, OSError, EOFError) as exc:
429
+ print(f"error: {exc}", file=sys.stderr)
430
+ return 2
431
+
432
+
433
+ if __name__ == "__main__":
434
+ raise SystemExit(main())
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: cbe-writer
3
+ description: Completes exactly one Codebase Explorer task from a CBE instruction file and writes the JSON result file. Use only for tasks handed out by `cbe host next`.
4
+ tools: Read, Write
5
+ ---
6
+
7
+ You complete one Codebase Explorer task. The message names an instruction file. Read it, follow it exactly, and write the one JSON object it asks for to the path it gives.
8
+
9
+ The task kind decides the result shape:
10
+
11
+ - A structured-detail batch lists its symbols as numbered lines, and either the source files with their line ranges or the source text itself. Read each listed file completely (page through long files). Return `{"units": [{"h": [12], "explain": "..."}, {"h": [20, 21], "title": "...", "explain": "..."}]}`, naming symbols only by their number (`h` with several numbers is a group). Leave out constants, imports and trivial accessors; group the tests that pin one behavior into one unit and say what they pin. Do not write ids, kinds, dispositions, line numbers or a source field: the program derives them. A re-ask lists only the gaps; return only the additional units.
12
+ - The responsibility-tree task lists the accepted detail units and no raw source. Return `{"nodes": [...], "depends_on": [...]}`; every unit id must appear in exactly one node.
13
+ - A review task shows one batch's units and its source. Return `{"decision": "accepted"|"needs_repair", "issues": [...]}`.
14
+ - The naming and overview tasks return the short JSON shapes their instruction file states.
15
+
16
+ Read only the source the instruction file lists. Do not run commands, edit other files, or add commentary to the JSON. Reply with the single line the instruction file specifies.
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: codebase-explorer
3
+ description: Generate a navigable, source-grounded documentation map (INDEX, a semantic responsibility tree, node pages with structured detail units and program-derived source anchors, and a symbol catalogue) for a code repository. Inside Claude Code, Codex, or another agent harness with subagents, the host agent runs `cbe host` and dispatches one subagent per reading task on the user's own subscription; with an OpenAI-compatible endpoint, run `cbe generate`. Also use it to query a generated map, resume an interrupted run, or report a run's token usage and cost.
4
+ ---
5
+
6
+ # Codebase Explorer
7
+
8
+ CBE splits documentation generation into deterministic CLI steps and model work. The CLI freezes one source snapshot (and validates an optional analyzed graph against it), builds the symbol inventory and reading batches, hands out tasks, checks every JSON result, builds the module tree from the call and import edges, renders node pages with program-derived source anchors, and builds the catalogue for `cbe find`. The model work is one fresh completion per task: one structured-detail payload per batch, the names of the modules in the tree (`tree_name_<k>` for the leaf modules, `tree_top` for the groups above them; the program decides the structure and the model cannot change it), optional reviews, and the system overview. Each detail unit has exactly one primary owner in the tree; the program, never the model, writes the published line ranges.
9
+
10
+ Pick the path by where you run:
11
+
12
+ | You are | Use | Model work runs on |
13
+ |---|---|---|
14
+ | Claude Code, Codex, or another harness that can start subagents | `cbe host ...` (below) | your subagents, on the user's subscription |
15
+ | A shell or service that reaches an OpenAI-compatible endpoint, with no harness login | `cbe generate` (end of this file) | that endpoint, one tool-free request per task |
16
+
17
+ ## Runtime
18
+
19
+ Use the absolute `cbe` command from the "Installed runtime" note if `cbe-install` added one above. Otherwise try `cbe --version`, then `"$(uv tool dir --bin)/cbe" --version`. If CBE is missing, ask the user before installing it.
20
+
21
+ The `cbe` CLI and this Skill are **one build**: the structured-detail, responsibility-tree, `cbe graph` and `cbe context` protocol above exists only in a build that ships with this Skill. An older `cbe` will plan a different run and reject new flags, so install a build matching this Skill, not a remembered version number.
22
+
23
+ - Published release: install the wheel or tag that ships with this Skill. Do not assume a tag name; use whatever the Skill's release page names.
24
+ - Development build from a checkout (use this while the build is unpublished). Point `uv` at the checkout so the CLI and this Skill come from the same commit:
25
+
26
+ ```sh
27
+ uv tool install --python 3.13 /path/to/codebase-explorer
28
+ ```
29
+
30
+ After installing, confirm `cbe --version` and that the newest surface is present, for example `cbe context --help` and `cbe graph capabilities --help`; a CLI without those commands predates this Skill.
31
+
32
+ ## Host-driven generation
33
+
34
+ 1. **Plan.** From the repository root:
35
+
36
+ ```sh
37
+ cbe host plan . --review sample --jobs 4 --host claude-code # or --host codex
38
+ cbe host plan . --graph /abs/path/graph.json # analyzed graph for the same source
39
+ ```
40
+
41
+ `.` is the repository root; the CLI turns it into an absolute path. `--run-dir`, `--repo`, and `--graph` need absolute paths. The command freezes `context.json` (the source snapshot), builds the symbol inventory and reading batches, and prints `run_dir`, `run_tag`, and the file, token, module, and task counts. `RUN` in the commands below is that `run_dir`, written out in full. Tell the user these counts before dispatching, and that the `tasks` count is only the planned tasks: each review finding queues one more repair task later, so the run can finish with more. `--review` is `none`, `sample` (default: one review of the largest batch), or `all` (one review per batch; each finding queues one repair). `--jobs` caps concurrent subagents; keep it within the user's subscription capacity. `--graph` is optional: without it the run records the graph as not provided; with it, CBE rejects a graph whose `source_manifest` does not match the frozen source.
42
+
43
+ 2. **Dispatch.** Run `cbe host next --run-dir RUN`. It leases every task it returns to you, including a re-ask of a task whose answer `submit` rejected. Start one fresh subagent for each returned task, re-asks included, whose whole prompt is the task's `dispatch_message`. Label it with the task's `description` (Claude Code) or `codex_task_name` (Codex), copied exactly; the label lets CBE find that subagent's exact usage later. Attempts at one task share its label, and `import-usage` adds them together. Start the subagents for one `next` batch concurrently. The host page says how you learn that a subagent has finished: in Claude Code its hand-back message starts your next turn, in Codex you wait with `wait_agent`. Do not poll the run directory or sleep in a loop while subagents run. The host-specific calls are in [claude-code.md](references/claude-code.md) and [codex.md](references/codex.md).
44
+
45
+ 3. **Submit.** When a subagent has finished, run `cbe host submit --run-dir RUN --task TASK`, adding the usage numbers the harness showed for that subagent when it shows any. Do not wait for usage that arrives later: a task submitted without usage still gets exact usage from `import-usage`. Submit every finished task before you call `next` again: `next` counts a task as running until `submit` settles it, and a free slot appears only then. The CLI reads the JSON file that the subagent wrote. Module and repair detail tasks allow 3 answers; other tasks allow 2. A detail answer names symbols by the short numbers in its instruction file. CBE keeps every usable unit and re-asks with only the gaps: functions, methods or classes with logic that no unit explains, and rejected units. After the last answer, symbols that are still unexplained are indexed without prose and flagged `unexplained` (STATUS, summary and RUN.md list them), and the task still completes. Other tasks go back to the queue with the exact error and a correction request; on their final invalid answer, usable content is normalized, accepted, and flagged, or the task is marked `failed`. If no answer of a detail task is a JSON object with a `units` list, the task fails and requires resume; the rest of the run continues.
46
+
47
+ `submit` prints one JSON object and exits 0 whether it accepts or rejects, so read `accepted` first. With `accepted: true`, `normalized` and `flags` say whether CBE corrected the answer; a repeated submit of a finished task returns `already: true` and neither key. With `accepted: false` there is no `normalized` or `flags`: `error` holds the checker's message, `attempts` counts the answers so far, and `task_status` is `pending` (a re-ask is queued and `next` returns it) or `failed` (the attempts are used up; see step 4). Both outcomes also add a line to `RUN/progress.log`. The `error` text and that log line are enough to tell the user why an answer was rejected; do not open the instruction files for it. The re-asked subagent finds the same error in its own instruction file.
48
+
49
+ 4. **Repeat** `next` and `submit` until `status` is `complete` or `partial`. Structured-detail batches come first. Reviews may run as their batches finish, alongside other detail work; findings queue repairs. When all detail and review has settled, the program freezes the module tree and the naming tasks appear (`tree_name_<k>` in parallel, then `tree_top`); an accepted repair or late batch makes the tree again. A node the model cannot name gets a program-made title and a note. The overview comes last. Follow `next` for the current dependency order. CBE renders the docs when the overview arrives or when nothing more can run. With `partial`, every finished batch is rendered, the pending work is named in the notes of `docs/RUN.md`, and failed parts stay pending. Report the failed tasks and their errors to the user (the `failed` list in `next`, `submit`, and `status` output names each task with its last error); after the cause is fixed, `cbe host resume` gives them fresh attempts.
50
+
51
+ 5. **Verify.** `cbe host verify --run-dir RUN` runs the deterministic structural and coverage checks: every task is done, the sources did not change, every file is in exactly one reading batch, every detail unit has exactly one primary owner, the responsibility tree is single-rooted and acyclic, every unit's published range is present and in bounds, relative links resolve, the catalogue covers every file, and symbol/unit queries resolve to their actual primary page anchors (or explicitly explain an index-only disposition). These checks do not judge whether an explanation is true. Truth of the prose stays with the separate `review_*` task (one fresh subagent) and the final independent verification, so a run that reports `complete` with passing checks is structurally sound, not thereby semantically reviewed. Report any failed check and say which of the three states applies: run finished, structure valid, semantic review done.
52
+
53
+ Rules for the host agent:
54
+ - Do not read the instruction files or the source yourself, and do not write or edit result JSON. The subagent does that work; the CLI checks it. This keeps your own context and cost small.
55
+ - Do not run more than `--jobs` subagents at once. `next` never hands out more.
56
+ - Never mark a task done by hand. Only `submit` accepts work.
57
+
58
+ ## Progress and resume
59
+
60
+ The run records where it is while it runs: `RUN/progress.log` (one line per claim, acceptance, re-ask, normalization, failure, retry, and render, with timings), `RUN/STATUS.md` (every task's state, attempts, seconds, and output path), and `RUN/host/state.json`. `cbe host status` prints the same table. Any host can continue from these records: `cbe host resume --run-dir RUN` (or `--repo /abs/path/to/repo`, which picks the repository's latest run) accepts results that subagents finished writing before an interruption, returns lost leases to the queue, gives failed tasks fresh attempts (`--keep-failed` skips that), and prints the next action. Then continue at step 2. If it reports changed sources, start a new plan: the old plan no longer matches the code.
61
+
62
+ ## Cost and usage
63
+
64
+ Report cost after every run. The rule: **use the harness's exact numbers when it records them; otherwise estimate, and say clearly that it is an estimate.**
65
+
66
+ 1. Run `cbe host import-usage --run-dir RUN --harness claude-code` (or `codex`). It reads the per-subagent usage that the harness already wrote to disk.
67
+ 2. Run `cbe host cost --run-dir RUN`. Rows with imported usage are `exact` and are priced by the model recorded in their transcript; the rest are `estimate`, computed from instruction, source, and result sizes. Add `--model MODEL` (for example `claude-sonnet-5-5`) only when `import-usage` listed `tasks_without_exact_usage`: it prices those estimated rows, which otherwise show `null` dollars. Token certainty and pricing are separate: unknown model prices produce `null` dollar values in JSON and `unknown` in the report. Supply current rates with `--price INPUT,OUTPUT,CACHE_WRITE,CACHE_READ` when needed; CBE does not silently substitute a comparison price. It writes `host/COST.md` and `host/cost.json`.
68
+ 3. Tell the user: the number of subagent runs, the wall time (from the `plan` line to the last `render` line in `RUN/progress.log`), the token totals and whether they are exact or estimated, and the API-list-price equivalent. Say that the dollar figure is not a bill: a subscription spends plan quota. Your own orchestration turns are not included. The harness shows their total with a command that only the user can type (`/usage` in Claude Code, `/status` in Codex); ask the user to run it if they want that total.
69
+
70
+ ## Read and query the result
71
+
72
+ Start at `RUN/docs/INDEX.md`: the overview, then the module tree, one line per module saying what it is responsible for. Pick the module that matches the maintenance question and open its node page; if the tree is not enough, use `cbe find --run-dir RUN --term WORD` to locate a symbol, `cbe query` to read one unit and `cbe source` for its frozen source. `DEPENDENCIES.md` holds the dependency view and `RUN.md` the graph coverage and generation notes. On a node page select a unit. The page keeps the explanation, necessary boundary notes, uncertainties, and compact relation links. Its footer shows how to find the selected title's ID and then query it.
73
+
74
+ The dependency view uses **graph-confirmed** for pairs found in both the tree and resolved call/import edges, **model-inferred** for tree-only pairs, and **graph-only** for graph-only pairs. The first two count edges between subtrees; graph-only counts direct node owners. **Containment** rows show ancestor direct units versus a descendant subtree as `→ N / ← M`, relative to the listed pair. **Within containment** rows list direct owner pairs separately. Both containment categories stay out of Mermaid and the three dependency totals. Graph-only and within containment each keep up to 40 pairs, ranked by edge count, with independent omitted counts. `summary.json` reports the full within containment count in `within_containment`. If the model supplies no key flows, the report shows the first five **Program candidates, not selected by the model**. Program labels and navigation use English; model prose keeps its original language.
75
+
76
+ `RUN` is the absolute run directory. Find an ID, then retrieve its complete explanation and evidence:
77
+
78
+ ```sh
79
+ cbe find --run-dir RUN --term SEARCH_TERM
80
+ cbe query --run-dir RUN --id ID
81
+ cbe query --run-dir RUN --id ID --json
82
+ cbe query --run-dir RUN --id ID --edges cfg,dataflow
83
+ cbe query --run-dir RUN --id ID --edges all --json
84
+ cbe source --run-dir RUN --id ID --context 2
85
+ ```
86
+
87
+ `ID` can be a unit ID, inventory symbol ID, or file path. `query` returns explanation text, full signatures, all source ranges, folded members, relation evidence, and incoming and outgoing call/import edges with their original statuses. CFG/dataflow edges are counted by default; use `--edges cfg,dataflow` or `--edges all` to expand original outgoing edges from the frozen graph. Dataflow is only in graphs built with `cbe graph build --with-dataflow`; on other graphs `--edges dataflow` says so and `internal_edge_counts` has no `dataflow` entry. Use `--json` for the machine-readable object. A grouped symbol retains its own source range; take `unit_id` from `query --json` and query that ID for the owning unit and all its ranges. A file query lists its symbols and associated units. An index-only symbol returns its recorded reason instead of invented prose.
88
+
89
+ `source` prints numbered code from `RUN/context.json`. It verifies each selected file's SHA-256 before printing any code. It reports a digest mismatch as an error. `--context N` adds N surrounding lines and must be nonnegative. Use this command to inspect the precise code behind an explanation.
90
+
91
+ `catalog.json` preserves the unit records and indexes graph edges. Complete graph evidence stays once in the frozen context and `query` expands it on demand. `summary.json` reports token ratios and their scope. Graph evidence describes static analysis; it does not prove runtime behavior. Confirm that the explanations answer the maintenance question before claiming semantic quality.
92
+
93
+ ## Script-driven generation (OpenAI-compatible API)
94
+
95
+ ```sh
96
+ cbe generate . --model gpt-oss-120b \
97
+ --base-url https://api.example.com/v1 --api-key-env MY_API_KEY
98
+ ```
99
+
100
+ `cbe generate` speaks the OpenAI-compatible `POST {base-url}/chat/completions` shape. The API key comes from the named environment variable (default `OPENAI_API_KEY`) or a standard config file, never from a flag, and CBE never writes it to the run log or any durable artifact. The command runs every completion itself on the same run state as `cbe host`, prints `run_dir`, `index`, status, token totals, and elapsed time, and records each physical call in `calls.csv`. It exits 0 when complete and 3 when the docs are rendered with pending parts; continue with `cbe generate . --resume`. Review modes are `none`, `sample`, and `all`.
101
+
@@ -0,0 +1,63 @@
1
+ # Claude Code host
2
+
3
+ ## Install
4
+
5
+ Install a CBE build that ships with this Skill (the CLI and Skill are one build; an older CLI lacks the `cbe graph`/`cbe context` surface), then install the skill. From an unpublished checkout:
6
+
7
+ ```sh
8
+ uv tool install --python 3.13 /path/to/codebase-explorer
9
+ "$(uv tool dir --bin)/cbe-install" install --host claude-code --scope project # .claude/skills/codebase-explorer and .claude/agents/cbe-writer.md
10
+ ```
11
+
12
+ Use `--scope user` for `~/.claude/skills` and `~/.claude/agents`. `cbe-install` installs the skill and the `cbe-writer` subagent (Read and Write tools only, so a writing subagent cannot run commands). It does not overwrite a `cbe-writer.md` or skill directory that it did not install; `cbe-install uninstall` removes both. Alternatively install the plugin, which carries the same two parts:
13
+
14
+ ```sh
15
+ claude plugin marketplace add zlxdengtianhei/codebase-explorer
16
+ claude plugin install codebase-explorer@codebase-explorer
17
+ ```
18
+
19
+ The plugin does not install the Python CLI; the skill's Runtime section covers that.
20
+
21
+ ## Dispatch with the Agent tool
22
+
23
+ For each task from `cbe host next`, call the Agent tool with:
24
+
25
+ - `description`: the task's `description` field, exactly (`cbe <run_tag> <task>`). `import-usage` matches transcripts by it.
26
+ - `prompt`: the task's `dispatch_message`, exactly.
27
+ - `subagent_type`: `cbe-writer` once `cbe-install` (or the plugin) has installed it; the plugin's copy may be listed as `codebase-explorer:cbe-writer`. If the Agent tool does not list it, use `general-purpose`.
28
+ - `model`: the model the user chose. If the user did not choose, `sonnet` is enough for module, naming, and overview tasks.
29
+
30
+ Issue one Agent call per task of a `next` batch, all in a single message, so the subagents run concurrently. `next` also returns a re-ask of a rejected task: start a new subagent for it too, with the same `description` and `prompt`.
31
+
32
+ ## Submit when a subagent hands back
33
+
34
+ Claude Code runs these Agent calls in the background: each call returns at once with `Async agent launched`, whether or not you asked for background mode. After you start the batch, end your turn. Each subagent finishes with one line, `DONE <task>`. It reaches you in a new message that starts your next turn. Depending on the Claude Code session, that message is a hand-back (sometimes wrapped as a message from another Claude session) or a task notification that also carries a usage block. Act on the first message that names the task:
35
+
36
+ 1. Submit that task at once, without the usage flags: `cbe host submit --run-dir RUN --task module_3`. `import-usage` below adds exact usage after the run.
37
+ 2. Call `next` and start the subagents it returns.
38
+ 3. End your turn again.
39
+
40
+ Do not poll `host/outbox/` or sleep in a loop while subagents run. The outbox file can exist before its subagent has ended, and a polling command asks the user for permission. Do not wait for a second message about a task you can already submit: in some sessions the later completion notification did not start a new turn, so a run that waited for it stopped there. If one does arrive after you submitted, it needs no action. Calling `next` before you submit a finished task leaves its slot taken.
41
+
42
+ If an Agent call returns the subagent's result instead (the result ends with a usage block, `<usage>` with `subagent_tokens`, `tool_uses`, `duration_ms`), submit from that result and you may add its usage:
43
+
44
+ ```sh
45
+ cbe host submit --run-dir RUN --task module_3 --usage-tokens 21480 --tool-uses 5 --duration-ms 48210 --usage-source claude-code-agent-result
46
+ ```
47
+
48
+ `subagent_tokens` is the size of that subagent's final context (input, cache, and output of its last turn), not the total it processed across turns. Keep it as a quick check only.
49
+
50
+ ## Permission prompts
51
+
52
+ The run commands in this skill (everything after the install step) do not use quotes, for example `cbe host plan . ...` instead of `cbe host plan "$PWD" ...`. Claude Code cannot check quoted text before a command runs, so a command with a quoted variable asks for confirmation even when `cbe` is on the allow list. Without quotes, the user can allow `cbe` as a command prefix and the run goes through without prompts. If a runtime note at the top of the skill gives the command as `/path/to/python -m cbe`, that whole prefix is what the user allows.
53
+
54
+ ## Exact usage
55
+
56
+ Claude Code writes every subagent transcript to `~/.claude/projects/<project>/<session>/subagents/agent-<id>.jsonl`, with a `usage` object on each model message (input, cache write, cache read, output tokens) and the model ID. The `.meta.json` next to it keeps the Agent `description`.
57
+
58
+ ```sh
59
+ cbe host import-usage --run-dir RUN --harness claude-code
60
+ cbe host cost --run-dir RUN
61
+ ```
62
+
63
+ `import-usage` sums every turn of every subagent whose description is `cbe <run_tag> <task>`, so the token counts are exact; the attempts at one task add up. `cost` prices them at the Anthropic list price of the model in each transcript. Add `--model MODEL` (for example `claude-sonnet-5-5`) only when `import-usage` lists `tasks_without_exact_usage`: it prices those estimated rows. For the whole session, including your orchestration turns, ask the user to run `/usage`: it is a command only the user can type (a subscription shows plan usage, not dollars).