cbm-onboard
Register maintained or ephemeral Git checkouts with codebase-memory-mcp, keep long-lived indexes current through non-clobbering Git hooks, and tear down ephemeral indexes by exact deterministic identity. Use when asked to index, onboard, register, remove, or keep a repository cur
Install
npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/tools/cbm-onboard
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
git clone https://github.com/ConnorGriffin/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole connorgriffin/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Onboard a repository to Codebase Memory
Use the bundled scripts for either a long-lived maintained checkout or a short-lived checkout with an explicit onboard/teardown lifecycle. Never index a directory containing several repositories.
Requirements
- Install
codebase-memory-mcpand make its executable available onPATH, or setCODEBASE_MEMORY_BINto the executable path. - Ephemeral onboarding and teardown require Codebase Memory MCP v0.10.8 or newer. Maintained-checkout onboarding keeps its existing compatibility.
- Resolve the installed
cbm-onboardskill directory.
Workflow
Resolve the target Git repository. Default to the current repository.
For a long-lived maintained checkout, run:
<cbm-onboard-skill-directory>/scripts/cbm-onboard.sh <repo-path>The script resolves linked worktrees to the maintained checkout, reconciles a marked baseline inside
.cbmignore, preserves custom exclusions, and adds a managed reindex block to Git's configuredpost-commit,post-merge, andpost-checkouthooks without deleting an existing shell hook. Thepost-checkoutblock carries a[ "$3" = "1" ] || exit 0guard so it only fires on branch checkouts, not per-file checkouts. A symlinked hook is followed to the file it resolves to, which is what a dotfiles-managed hooks directory needs, and the write lands in whatever repo owns that file. A symlink with no regular-file target is refused, and a.cbmignoresymlink is refused outright. A non-shell foreign hook is left unchanged with a warning because composing it would be unsafe.Linked worktrees share the control checkout's Git hooks; they cannot have independent hooks. Pass
--this-checkoutonly to select the supplied checkout's.cbmignoreand initial index. Hook installation still resolves through Git's shared hooks directory. When one of those shared hooks fires from a linked worktree, it refreshes that worktree in fast mode under the same deterministic identity used by the ephemeral lifecycle. It never falls back to a path-derived project. Failed checkout classification, a missing binary or detached launcher, an unsupported version, or an identity that cannot be derived prints one reason and lets the Git operation continue.Re-run onboarding from the installed skill directory to repair an enrollment whose managed hook points at a stale or foreign installation. Onboarding replaces both its current fenced block and the older unfenced
codebase-memory-mcp: reindexmarker, preserves every line it does not own, and writes one managed block pointing at the installation that invoked it.For an ephemeral checkout, run the complete lifecycle:
<cbm-onboard-skill-directory>/scripts/cbm-onboard.sh \ --no-hooks --this-checkout <worktree-path> # Use the worktree, then: <cbm-onboard-skill-directory>/scripts/cbm-teardown.sh <worktree-path>--no-hooksreconciles only the selected checkout's.cbmignore, performs a full index under a deterministic name derived from its canonical physical path, and never enters hook resolution or installation.--this-checkoutis load-bearing for a linked worktree: without it, onboarding deliberately resolves to the main checkout, matching maintained-checkout behavior.Teardown recomputes that exact identity and calls
delete_projectdirectly; it never scans projects, edits repository files or Git configuration, or removes hooks or worktrees. Both a successful deletion and an exact already-missing response are success, so teardown is safe to repeat.For an automated workflow that must bind one checkout to its exact project without touching that checkout, run:
python3 <cbm-onboard-skill-directory>/scripts/cbm-lifecycle.py ensure <checkout-path>ensureis the machine interface: it resolves the supplied checkout as given, canonicalizes it physically, derives the same deterministic name the rest of the lifecycle uses, and makes exactly that project ready. It asksindex_statusfor the computed name only, indexes solely on the exact not-found response, and re-asksindex_statusafterwards becauseindex_repositorydoes not echo the root it indexed. It never chooses amonglist_projects, and it writes nothing to the repository, its.cbmignore, its Git configuration, or its hooks.It prints one object on success,
{"root_path", "project", "status"}, where status isreadyfor a project that was already indexed andindexedfor one it just built. A missing, unsupported, or unable-to-respond CLI prints{"status": "unavailable"}and exits 2, with a bounded actionable reason on stderr; it never exposes raw CLI output, environment values, or private source. Missing and unsupported binaries remain distinguishable. An active-generation conflict says to wait and retry the same checkout, not that a sandbox denied it, and never authorizes closing unrelated sessions. Every malformed response or wrong identity remains fail-closed with exit 1 and nothing on stdout.In a workspace-write sandbox, try
ensurenormally first. If it exits 2 with{"status": "unavailable"}, confirm thatcodebase-memory-mcpis present and reports a supported version. A usable binary can still be unavailable only inside the sandbox because its local CLI must secure and write~/.cache/codebase-memory-mcpand coordinate through a Unix socket under/private/tmp. Retry the sameensurecommand with escalated permissions and make those local-only destinations explicit in the approval rationale; state that no repository data is sent to a network destination. Do not request a generic permission to “index a private repository,” which hides the actual boundary and can be correctly rejected when the destination is unspecified. If the diagnostic names an active-generation conflict, wait for it and retry; do not frame it as a sandbox denial or close another session.Verify the project through the available Codebase Memory MCP interface with
index_statusorlist_projects.Report node and edge counts when available.
Tell the user that
.cbmignoreis tracked and should be committed. The Git hooks are clone-local, so rerun onboarding after a fresh clone.
The initial index uses full mode. Post-commit/post-merge/post-checkout indexing uses fast mode in a detached process and always exits 0, so a broken index never fails a commit, merge, or checkout. Maintained checkouts keep their derived project name; linked worktrees use their deterministic lifecycle identity. Re-running onboarding is idempotent.
Repositories dominated by YAML, prose, or shell may produce a thin graph; say so rather than refusing to index them.
Files (skills)
-
agents
-
openai.yaml 209 B
interface: display_name: "Codebase Memory Onboard" short_description: "Index and keep a repository graph current" default_prompt: "Use $cbm-onboard to register this repository with codebase-memory-mcp."
-
-
scripts
-
cbm-lifecycle.py 9.4 KB
#!/usr/bin/env python3 """Shared contract checks for ephemeral Codebase Memory lifecycle scripts.""" from __future__ import annotations import hashlib import json import os import re import shutil import subprocess import sys from pathlib import Path MINIMUM_VERSION = (0, 10, 8) VERSION_PATTERN = re.compile(rb"codebase-memory-mcp (0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\n?\Z") # The exact response Codebase Memory gives for a project it does not hold. Only # this signal permits indexing; anything else is a failure rather than a miss. MISSING_PROJECT_ERROR = "project not found or not indexed" def fail(message: str) -> "NoReturn": print(message, file=sys.stderr) raise SystemExit(1) def physical_identity(target: str, *, main_checkout: bool = False) -> tuple[str, str]: git_arguments = ( ["worktree", "list", "--porcelain", "-z"] if main_checkout else ["rev-parse", "--show-toplevel"] ) result = subprocess.run( ["git", "-C", target, *git_arguments], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False, ) if result.returncode != 0: fail(f"not a Git repository: {target}") if main_checkout: fields = result.stdout.split(b"\0") reported = next( (field[len(b"worktree ") :] for field in fields if field.startswith(b"worktree ")), b"", ) else: if not result.stdout.endswith(b"\n"): fail(f"not a Git repository: {target}") reported = result.stdout[:-1] if not reported: fail(f"not a Git repository: {target}") if b"\n" in reported: fail("checkout paths containing newline bytes are unsupported") root = os.path.realpath(os.fsdecode(reported)) root_bytes = os.fsencode(root) if b"\n" in root_bytes: fail("checkout paths containing newline bytes are unsupported") project = "cbm-onboard-v1-" + hashlib.sha256(root_bytes).hexdigest() return root, project def identity(target: str, *, main_checkout: bool = False) -> None: root, project = physical_identity(target, main_checkout=main_checkout) json.dump({"root": root, "project": project}, sys.stdout) sys.stdout.write("\n") def parsed_version(banner: bytes) -> "tuple[int, ...] | None": match = VERSION_PATTERN.fullmatch(banner) return tuple(int(component) for component in match.groups()) if match else None def validate_version(path: str) -> None: version = parsed_version(Path(path).read_bytes()) if version is None: fail("unsupported codebase-memory-mcp version banner") if version < MINIMUM_VERSION: fail("codebase-memory-mcp 0.10.8 or newer is required") def read_envelope(raw: str) -> tuple[dict, object]: """Split one Codebase Memory response into its structured body and error flag.""" try: envelope = json.loads(raw) structured = envelope["structuredContent"] except (json.JSONDecodeError, KeyError, TypeError): fail("invalid Codebase Memory JSON response") if not isinstance(structured, dict): fail("invalid Codebase Memory JSON response") return structured, envelope.get("isError") def validate_response(project: str, status: str, is_error: str, path: str) -> None: try: raw = Path(path).read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): fail("invalid Codebase Memory JSON response") structured, reported_error = read_envelope(raw) if ( structured.get("project") != project or structured.get("status") != status or reported_error is not (is_error == "true") ): fail("Codebase Memory response did not match the requested project and status") def unavailable(reason: str) -> "NoReturn": """Report the visible degraded mode without exposing tool output.""" json.dump({"status": "unavailable"}, sys.stdout) sys.stdout.write("\n") print(reason, file=sys.stderr) raise SystemExit(2) def unavailable_reason(diagnostic: str) -> str: """Turn a bounded tool failure class into safe operator guidance.""" if ( "active generation" in diagnostic.lower() or "active-generation" in diagnostic.lower() or "generation is active" in diagnostic.lower() ): return ( "Codebase Memory has an active-generation conflict; wait for that generation " "to finish, then retry this checkout. Do not terminate unrelated sessions." ) return ( "Codebase Memory could not return a response. Verify its supported version; " "in a workspace-write sandbox, retry this same command with the documented " "local-only permission rationale." ) def envelope_or_unavailable(code: int, raw: str, diagnostic: str) -> tuple[dict, object]: """Read one tool response, distinguishing "produced nothing" from "answered wrongly". A nonzero exit paired with empty stdout means the binary could not operate at all here (for example a sandbox that blocks the daemon endpoint it needs) — the same degraded mode as a missing or too-old binary, reported as `unavailable`. A zero exit with empty stdout is still a protocol violation: the tool claimed success and said nothing, which is not a case where "no graph is available" is a safe conclusion. Non-empty stdout that fails to parse is always a protocol violation regardless of exit code. """ if code != 0 and raw == "": unavailable(unavailable_reason(diagnostic)) return read_envelope(raw) def usable_binary() -> str: configured = os.environ.get("CODEBASE_MEMORY_BIN") or shutil.which("codebase-memory-mcp") if not configured or not os.access(configured, os.X_OK): unavailable("codebase-memory-mcp was not found or is not executable.") banner = subprocess.run( [configured, "--version"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False, ) version = parsed_version(banner.stdout) if banner.returncode == 0 else None if version is None or version < MINIMUM_VERSION: unavailable("codebase-memory-mcp does not report a supported version (0.10.8 or newer).") return configured def call_tool(binary: str, arguments: list[str]) -> tuple[int, str, str]: result = subprocess.run( [binary, "cli", "--json", *arguments], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) return ( result.returncode, result.stdout.decode("utf-8", "replace"), result.stderr.decode("utf-8", "replace"), ) def ready_for(structured: dict, reported_error: object, project: str, root: str) -> bool: return ( reported_error is False and structured.get("project") == project and structured.get("status") == "ready" and structured.get("root_path") == root ) def ensure(target: str) -> None: """Make the exact project for one physical checkout ready, and name it.""" binary = usable_binary() root, project = physical_identity(target) code, raw, diagnostic = call_tool(binary, ["index_status", "--project", project]) structured, reported_error = envelope_or_unavailable(code, raw, diagnostic) if code == 0: if not ready_for(structured, reported_error, project, root): fail(f"Codebase Memory did not report {project} ready for {root}") json.dump({"root_path": root, "project": project, "status": "ready"}, sys.stdout) sys.stdout.write("\n") return if code != 1 or reported_error is not True or structured.get("error") != MISSING_PROJECT_ERROR: fail(f"Codebase Memory index_status failed for {project}") code, raw, diagnostic = call_tool( binary, ["index_repository", "--repo-path", root, "--mode", "full", "--name", project], ) structured, reported_error = envelope_or_unavailable(code, raw, diagnostic) if ( code != 0 or reported_error is not False or structured.get("project") != project or structured.get("status") != "indexed" ): fail(f"Codebase Memory failed to index {root} as {project}") # index_repository never echoes the root it indexed, so the binding is only # proven by asking for the project's own root afterwards. code, raw, diagnostic = call_tool(binary, ["index_status", "--project", project]) structured, reported_error = envelope_or_unavailable(code, raw, diagnostic) if code != 0 or not ready_for(structured, reported_error, project, root): fail(f"Codebase Memory did not report {project} ready for {root} after indexing") json.dump({"root_path": root, "project": project, "status": "indexed"}, sys.stdout) sys.stdout.write("\n") def main() -> None: if len(sys.argv) == 3 and sys.argv[1] == "identity": identity(sys.argv[2]) return if len(sys.argv) == 4 and sys.argv[1:3] == ["identity", "--main"]: identity(sys.argv[3], main_checkout=True) return if len(sys.argv) == 3 and sys.argv[1] == "ensure": ensure(sys.argv[2]) return if len(sys.argv) == 3 and sys.argv[1] == "version": validate_version(sys.argv[2]) return if len(sys.argv) == 6 and sys.argv[1] == "response": validate_response(*sys.argv[2:]) return fail( "usage: cbm-lifecycle.py identity [--main] PATH | ensure PATH" " | version FILE | response PROJECT STATUS BOOL FILE" ) if __name__ == "__main__": main() -
cbm-onboard.sh 8.5 KB
#!/bin/sh # Register a repository with codebase-memory-mcp without clobbering local rules. set -eu BIN="${CODEBASE_MEMORY_BIN:-$(command -v codebase-memory-mcp 2>/dev/null || true)}" [ -n "$BIN" ] && [ -x "$BIN" ] || { printf '%s\n' "codebase-memory-mcp is not executable; install it or set CODEBASE_MEMORY_BIN" >&2 exit 1 } SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" REINDEX="$SCRIPT_DIR/cbm-reindex.sh" LIFECYCLE="$SCRIPT_DIR/cbm-lifecycle.py" THIS_CHECKOUT=0 NO_HOOKS=0 TARGET="." TARGET_SET=0 while [ "$#" -gt 0 ]; do case "$1" in --this-checkout) THIS_CHECKOUT=1 ;; --no-hooks) NO_HOOKS=1 ;; --*) printf 'unknown option: %s\n' "$1" >&2 exit 1 ;; *) [ "$TARGET_SET" -eq 0 ] || { printf '%s\n' "expected at most one repository path" >&2 exit 1 } TARGET="$1" TARGET_SET=1 ;; esac shift done if [ "$NO_HOOKS" -eq 1 ] && [ "${CBM_SKIP_INDEX:-0}" = "1" ]; then printf '%s\n' "CBM_SKIP_INDEX=1 is not supported with --no-hooks" >&2 exit 1 fi IDENTITY_TMP="$(mktemp)" VERSION_TMP="$(mktemp)" RESPONSE_TMP="$(mktemp)" trap 'rm -f "$IDENTITY_TMP" "$VERSION_TMP" "$RESPONSE_TMP"' EXIT if [ "$NO_HOOKS" -eq 1 ]; then if [ "$THIS_CHECKOUT" -eq 1 ]; then python3 "$LIFECYCLE" identity "$TARGET" >"$IDENTITY_TMP" else python3 "$LIFECYCLE" identity --main "$TARGET" >"$IDENTITY_TMP" fi ROOT="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["root"])' "$IDENTITY_TMP")" PROJECT="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["project"])' "$IDENTITY_TMP")" "$BIN" --version >"$VERSION_TMP" python3 "$LIFECYCLE" version "$VERSION_TMP" elif [ "$THIS_CHECKOUT" -eq 1 ]; then ROOT="$(git -C "$TARGET" rev-parse --show-toplevel 2>/dev/null || true)" else ROOT="$(git -C "$TARGET" worktree list --porcelain 2>/dev/null | awk '/^worktree / { print substr($0, 10); exit }')" fi [ -n "$ROOT" ] || { printf 'not a Git repository: %s\n' "$TARGET" >&2 exit 1 } IGNORE="$ROOT/.cbmignore" [ ! -L "$IGNORE" ] || { printf 'refusing symlink target: %s\n' "$IGNORE" >&2 exit 1 } BEGIN_IGNORE="# >>> cbm-onboard managed baseline — do not edit inside this block >>>" END_IGNORE="# <<< cbm-onboard managed baseline <<<" BEGIN_HOOK="# >>> cbm-onboard managed reindex >>>" END_HOOK="# <<< cbm-onboard managed reindex <<<" MANAGED_TMP="$(mktemp)" OUTPUT_TMP="$(mktemp)" HOOK_TMP="$(mktemp)" trap 'rm -f "$IDENTITY_TMP" "$VERSION_TMP" "$RESPONSE_TMP" "$MANAGED_TMP" "$OUTPUT_TMP" "$HOOK_TMP"' EXIT { printf '%s\n' "$BEGIN_IGNORE" printf '%s\n' \ ".venv/" "venv/" "env/" "__pycache__/" "*.pyc" \ ".pytest_cache/" ".mypy_cache/" ".ruff_cache/" ".tox/" \ "node_modules/" "dist/" "build/" "*.egg-info/" \ ".agentflow/" ".claude/worktrees/" ".codex/worktrees/" ".impeccable/" \ "*.db" "*.sqlite" "*.sqlite3" "*.csv" "*.parquet" \ ".env" ".env.*" "*.pem" "*.key" "*.p12" "*.pfx" \ ".aws/" ".ssh/" ".secrets/" "secrets/" "credentials/" "*.log" printf '%s\n' "$END_IGNORE" } >"$MANAGED_TMP" EXISTING="/dev/null" [ -e "$IGNORE" ] && EXISTING="$IGNORE" awk -v begin="$BEGIN_IGNORE" -v end="$END_IGNORE" ' function rtrim(s) { sub(/[ \t]+$/, "", s); return s } FNR == NR { managed[FNR] = $0; managed_set[$0] = 1; managed_n = FNR; next } { lines[++n] = $0 } END { for (i = 1; i <= managed_n; i++) print managed[i] custom_n = 0 i = 1 while (i <= n) { if (rtrim(lines[i]) == begin) { found = 0 for (j = i + 1; j <= n; j++) { if (rtrim(lines[j]) == begin) { break } if (rtrim(lines[j]) == end) { found = j; break } } if (found) { i = found + 1; continue } custom[++custom_n] = lines[i] i++; continue } if (rtrim(lines[i]) == end) { custom[++custom_n] = lines[i] i++; continue } if (lines[i] in managed_set) { i++; continue } custom[++custom_n] = lines[i] i++ } first = 1 last = custom_n while (first <= last && custom[first] == "") first++ while (last >= first && custom[last] == "") last-- if (last >= first) { print "" for (i = first; i <= last; i++) print custom[i] } } ' "$MANAGED_TMP" "$EXISTING" >"$OUTPUT_TMP" if [ ! -e "$IGNORE" ] || ! cmp -s "$OUTPUT_TMP" "$IGNORE"; then cp "$OUTPUT_TMP" "$IGNORE" printf '%s\n' "updated $IGNORE" else printf '%s\n' "$IGNORE is already current" fi if [ "$NO_HOOKS" -eq 0 ]; then COMMON_DIR="$(git -C "$ROOT" rev-parse --git-common-dir)" case "$COMMON_DIR" in /*) : ;; *) COMMON_DIR="$ROOT/$COMMON_DIR" ;; esac # Honor an explicit core.hooksPath (e.g. a dotfiles-managed dispatcher): # install alongside it rather than shadowing it under .git/hooks. Fall back # to the repo-local, worktree-safe hooks dir when unset. HOOKS_PATH="$(git -C "$ROOT" config --get core.hooksPath 2>/dev/null || true)" if [ -n "$HOOKS_PATH" ]; then # git expands a leading tilde in a path-valued config; a global hooksPath is # commonly written that way, and joining it to the repo would create a literal # "~" directory inside the checkout. case "$HOOKS_PATH" in "~") HOOKS_PATH="$HOME" ;; "~/"*) HOOKS_PATH="$HOME/${HOOKS_PATH#\~/}" ;; esac case "$HOOKS_PATH" in /*) HOOKS_DIR="$HOOKS_PATH" ;; *) HOOKS_DIR="$ROOT/$HOOKS_PATH" ;; esac else HOOKS_DIR="$COMMON_DIR/hooks" fi HOOK_SYMLINK_REFUSED=0 for HOOK_NAME in post-commit post-merge post-checkout; do HOOK="$HOOKS_DIR/$HOOK_NAME" HOOK_FILE="$HOOK" # A dotfiles-managed hooks dir is usually a farm of symlinks into the dotfiles # checkout. Edit the file the link resolves to: shadowing the link would be # clobbered by the next dotfiles run, and refusing leaves no hook at all. if [ -L "$HOOK" ]; then HOOK_FILE="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$HOOK")" if [ ! -f "$HOOK_FILE" ]; then printf 'refusing symlink without a regular-file target: %s\n' "$HOOK" >&2 HOOK_SYMLINK_REFUSED=1 continue fi printf 'following symlink %s to %s\n' "$HOOK" "$HOOK_FILE" fi : >"$HOOK_TMP" mkdir -p "$(dirname "$HOOK_FILE")" INSTALL_HOOK=1 if [ -e "$HOOK_FILE" ]; then FIRST_LINE="$(sed -n '1p' "$HOOK_FILE")" if ! printf '%s\n' "$FIRST_LINE" | grep -Eq '^#!.*[/[:space:]](ba|da|k|z)?sh([[:space:]]|$)'; then printf 'SKIP hook installation: existing hook is not a shell script; left unchanged: %s\n' \ "$HOOK_FILE" >&2 INSTALL_HOOK=0 fi else printf '%s\n' "#!/bin/sh" >"$HOOK_TMP" fi [ "$INSTALL_HOOK" -eq 1 ] || continue if [ -e "$HOOK_FILE" ]; then awk -v begin="$BEGIN_HOOK" -v end="$END_HOOK" ' { lines[++n] = $0 } END { i = 1 while (i <= n) { if (lines[i] ~ /^# codebase-memory-mcp: reindex on .* \(managed by cbm-onboard/) { i++ if (i <= n && lines[i] ~ /^"[^"]*\/cbm-reindex\.sh"[ \t]*$/) i++ continue } if (lines[i] == begin) { found = 0 for (j = i + 1; j <= n; j++) { if (lines[j] == begin) { break } if (lines[j] == end) { found = j; break } } if (found) { if (out_n > 0 && output[out_n] == "") out_n-- i = found + 1 continue } } output[++out_n] = lines[i] i++ } for (i = 1; i <= out_n; i++) print output[i] } ' "$HOOK_FILE" >"$HOOK_TMP" fi { printf '\n%s\n' "$BEGIN_HOOK" if [ "$HOOK_NAME" = "post-checkout" ]; then printf '%s\n' '[ "$3" = "1" ] || exit 0' fi printf '"%s"\n' "$REINDEX" printf '%s\n' "$END_HOOK" } >>"$HOOK_TMP" cp "$HOOK_TMP" "$HOOK_FILE" chmod +x "$HOOK_FILE" printf '%s\n' "installed managed reindex command in $HOOK_FILE" done [ "$HOOK_SYMLINK_REFUSED" -eq 0 ] || exit 1 fi if [ "${CBM_SKIP_INDEX:-0}" = "1" ]; then printf '%s\n' "skipped initial index (CBM_SKIP_INDEX=1): $ROOT" elif [ "$NO_HOOKS" -eq 1 ]; then INDEX_EXIT=0 "$BIN" cli --json index_repository --repo-path "$ROOT" --mode full --name "$PROJECT" \ >"$RESPONSE_TMP" || INDEX_EXIT=$? python3 "$LIFECYCLE" response "$PROJECT" indexed false "$RESPONSE_TMP" || exit 1 [ "$INDEX_EXIT" -eq 0 ] || { printf 'Codebase Memory index failed with exit %s\n' "$INDEX_EXIT" >&2 exit 1 } printf '%s\n' "indexed $ROOT as $PROJECT" else PAYLOAD="$(python3 -c \ 'import json,sys; print(json.dumps({"repo_path": sys.argv[1], "mode": "full"}))' \ "$ROOT")" "$BIN" cli index_repository "$PAYLOAD" printf '%s\n' "indexed $ROOT" fi -
cbm-reindex.sh 2.3 KB
#!/bin/sh # Reindex the checkout that fired a managed Git hook. set -u BIN="${CODEBASE_MEMORY_BIN:-$(command -v codebase-memory-mcp 2>/dev/null || true)}" [ -n "$BIN" ] && [ -x "$BIN" ] || { printf '%s\n' "Codebase Memory refresh skipped: binary is not executable" >&2 exit 0 } LAUNCHER="$(command -v nohup 2>/dev/null || true)" [ -n "$LAUNCHER" ] && [ -x "$LAUNCHER" ] || { printf '%s\n' "Codebase Memory refresh skipped: detached launcher is unavailable" >&2 exit 0 } SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" LIFECYCLE="$SCRIPT_DIR/cbm-lifecycle.py" ROOT="$(unset GIT_DIR GIT_WORK_TREE; cd "$PWD" && git rev-parse --show-toplevel 2>/dev/null || true)" [ -n "$ROOT" ] || exit 0 GIT_DIR_ABS="$(unset GIT_DIR GIT_WORK_TREE; cd "$PWD" && git rev-parse --absolute-git-dir 2>/dev/null || true)" COMMON_DIR="$(unset GIT_DIR GIT_WORK_TREE; cd "$PWD" && git rev-parse --git-common-dir 2>/dev/null || true)" case "$COMMON_DIR" in /*) : ;; *) COMMON_DIR="$(unset GIT_DIR GIT_WORK_TREE; cd "$PWD/$COMMON_DIR" 2>/dev/null && pwd -P || true)" ;; esac [ -n "$GIT_DIR_ABS" ] && [ -n "$COMMON_DIR" ] || { printf '%s\n' "Codebase Memory refresh skipped: checkout classification failed" >&2 exit 0 } if [ "$GIT_DIR_ABS" != "$COMMON_DIR" ]; then VERSION_TMP="$(mktemp 2>/dev/null || true)" if [ -z "$VERSION_TMP" ] || ! "$BIN" --version >"$VERSION_TMP" 2>/dev/null || ! python3 "$LIFECYCLE" version "$VERSION_TMP" >/dev/null 2>&1; then [ -z "$VERSION_TMP" ] || rm -f "$VERSION_TMP" printf '%s\n' "Codebase Memory refresh skipped: binary version is unsupported" >&2 exit 0 fi rm -f "$VERSION_TMP" IDENTITY="$(unset GIT_DIR GIT_WORK_TREE; python3 "$LIFECYCLE" identity "$ROOT" 2>/dev/null || true)" PROJECT="$(printf '%s' "$IDENTITY" | python3 -c \ 'import json,sys; print(json.load(sys.stdin)["project"])' 2>/dev/null || true)" [ -n "$PROJECT" ] || { printf '%s\n' "Codebase Memory refresh skipped: worktree identity could not be derived" >&2 exit 0 } "$LAUNCHER" "$BIN" cli --json index_repository \ --repo-path "$ROOT" --mode fast --name "$PROJECT" \ >/dev/null 2>&1 </dev/null & exit 0 fi PAYLOAD="$(python3 -c \ 'import json,sys; print(json.dumps({"repo_path": sys.argv[1], "mode": "fast"}))' \ "$ROOT")" || exit 0 "$LAUNCHER" "$BIN" cli index_repository "$PAYLOAD" \ >/dev/null 2>&1 </dev/null & exit 0 -
cbm-teardown.sh 1.6 KB
#!/bin/sh # Remove one ephemeral checkout's deterministic Codebase Memory project. set -eu BIN="${CODEBASE_MEMORY_BIN:-$(command -v codebase-memory-mcp 2>/dev/null || true)}" [ -n "$BIN" ] && [ -x "$BIN" ] || { printf '%s\n' "codebase-memory-mcp is not executable; install it or set CODEBASE_MEMORY_BIN" >&2 exit 1 } TARGET="." if [ "$#" -gt 1 ]; then printf '%s\n' "expected at most one repository path" >&2 exit 1 fi if [ "$#" -eq 1 ]; then case "$1" in --*) printf 'unknown option: %s\n' "$1" >&2 exit 1 ;; *) TARGET="$1" ;; esac fi SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" LIFECYCLE="$SCRIPT_DIR/cbm-lifecycle.py" IDENTITY_TMP="$(mktemp)" VERSION_TMP="$(mktemp)" RESPONSE_TMP="$(mktemp)" trap 'rm -f "$IDENTITY_TMP" "$VERSION_TMP" "$RESPONSE_TMP"' EXIT python3 "$LIFECYCLE" identity "$TARGET" >"$IDENTITY_TMP" PROJECT="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["project"])' "$IDENTITY_TMP")" "$BIN" --version >"$VERSION_TMP" python3 "$LIFECYCLE" version "$VERSION_TMP" DELETE_EXIT=0 "$BIN" cli --json delete_project --project "$PROJECT" >"$RESPONSE_TMP" || DELETE_EXIT=$? case "$DELETE_EXIT" in 0) python3 "$LIFECYCLE" response "$PROJECT" deleted false "$RESPONSE_TMP" printf '%s\n' "deleted Codebase Memory project $PROJECT" ;; 1) python3 "$LIFECYCLE" response "$PROJECT" not_found true "$RESPONSE_TMP" printf '%s\n' "Codebase Memory project $PROJECT was not found" ;; *) printf 'Codebase Memory delete failed with exit %s\n' "$DELETE_EXIT" >&2 exit 1 ;; esac
-
-
SKILL.md 7.2 KB
--- name: cbm-onboard description: Register maintained or ephemeral Git checkouts with codebase-memory-mcp, keep long-lived indexes current through non-clobbering Git hooks, and tear down ephemeral indexes by exact deterministic identity. Use when asked to index, onboard, register, remove, or keep a repository current in Codebase Memory, or to resolve which Codebase Memory project belongs to a checkout and make it ready. --- # Onboard a repository to Codebase Memory Use the bundled scripts for either a long-lived maintained checkout or a short-lived checkout with an explicit onboard/teardown lifecycle. Never index a directory containing several repositories. ## Requirements - Install `codebase-memory-mcp` and make its executable available on `PATH`, or set `CODEBASE_MEMORY_BIN` to the executable path. - Ephemeral onboarding and teardown require Codebase Memory MCP v0.10.8 or newer. Maintained-checkout onboarding keeps its existing compatibility. - Resolve the installed `cbm-onboard` skill directory. ## Workflow 1. Resolve the target Git repository. Default to the current repository. 2. For a long-lived maintained checkout, run: ```sh <cbm-onboard-skill-directory>/scripts/cbm-onboard.sh <repo-path> ``` The script resolves linked worktrees to the maintained checkout, reconciles a marked baseline inside `.cbmignore`, preserves custom exclusions, and adds a managed reindex block to Git's configured `post-commit`, `post-merge`, and `post-checkout` hooks without deleting an existing shell hook. The `post-checkout` block carries a `[ "$3" = "1" ] || exit 0` guard so it only fires on branch checkouts, not per-file checkouts. A symlinked hook is followed to the file it resolves to, which is what a dotfiles-managed hooks directory needs, and the write lands in whatever repo owns that file. A symlink with no regular-file target is refused, and a `.cbmignore` symlink is refused outright. A non-shell foreign hook is left unchanged with a warning because composing it would be unsafe. Linked worktrees share the control checkout's Git hooks; they cannot have independent hooks. Pass `--this-checkout` only to select the supplied checkout's `.cbmignore` and initial index. Hook installation still resolves through Git's shared hooks directory. When one of those shared hooks fires from a linked worktree, it refreshes that worktree in fast mode under the same deterministic identity used by the ephemeral lifecycle. It never falls back to a path-derived project. Failed checkout classification, a missing binary or detached launcher, an unsupported version, or an identity that cannot be derived prints one reason and lets the Git operation continue. Re-run onboarding from the installed skill directory to repair an enrollment whose managed hook points at a stale or foreign installation. Onboarding replaces both its current fenced block and the older unfenced `codebase-memory-mcp: reindex` marker, preserves every line it does not own, and writes one managed block pointing at the installation that invoked it. 3. For an ephemeral checkout, run the complete lifecycle: ```sh <cbm-onboard-skill-directory>/scripts/cbm-onboard.sh \ --no-hooks --this-checkout <worktree-path> # Use the worktree, then: <cbm-onboard-skill-directory>/scripts/cbm-teardown.sh <worktree-path> ``` `--no-hooks` reconciles only the selected checkout's `.cbmignore`, performs a full index under a deterministic name derived from its canonical physical path, and never enters hook resolution or installation. `--this-checkout` is load-bearing for a linked worktree: without it, onboarding deliberately resolves to the main checkout, matching maintained-checkout behavior. Teardown recomputes that exact identity and calls `delete_project` directly; it never scans projects, edits repository files or Git configuration, or removes hooks or worktrees. Both a successful deletion and an exact already-missing response are success, so teardown is safe to repeat. 4. For an automated workflow that must bind one checkout to its exact project without touching that checkout, run: ```sh python3 <cbm-onboard-skill-directory>/scripts/cbm-lifecycle.py ensure <checkout-path> ``` `ensure` is the machine interface: it resolves the supplied checkout as given, canonicalizes it physically, derives the same deterministic name the rest of the lifecycle uses, and makes exactly that project ready. It asks `index_status` for the computed name only, indexes solely on the exact not-found response, and re-asks `index_status` afterwards because `index_repository` does not echo the root it indexed. It never chooses among `list_projects`, and it writes nothing to the repository, its `.cbmignore`, its Git configuration, or its hooks. It prints one object on success, `{"root_path", "project", "status"}`, where status is `ready` for a project that was already indexed and `indexed` for one it just built. A missing, unsupported, or unable-to-respond CLI prints `{"status": "unavailable"}` and exits 2, with a bounded actionable reason on stderr; it never exposes raw CLI output, environment values, or private source. Missing and unsupported binaries remain distinguishable. An active-generation conflict says to wait and retry the same checkout, not that a sandbox denied it, and never authorizes closing unrelated sessions. Every malformed response or wrong identity remains fail-closed with exit 1 and nothing on stdout. In a workspace-write sandbox, try `ensure` normally first. If it exits 2 with `{"status": "unavailable"}`, confirm that `codebase-memory-mcp` is present and reports a supported version. A usable binary can still be unavailable only inside the sandbox because its local CLI must secure and write `~/.cache/codebase-memory-mcp` and coordinate through a Unix socket under `/private/tmp`. Retry the same `ensure` command with escalated permissions and make those local-only destinations explicit in the approval rationale; state that no repository data is sent to a network destination. Do not request a generic permission to “index a private repository,” which hides the actual boundary and can be correctly rejected when the destination is unspecified. If the diagnostic names an active-generation conflict, wait for it and retry; do not frame it as a sandbox denial or close another session. 5. Verify the project through the available Codebase Memory MCP interface with `index_status` or `list_projects`. 6. Report node and edge counts when available. 7. Tell the user that `.cbmignore` is tracked and should be committed. The Git hooks are clone-local, so rerun onboarding after a fresh clone. The initial index uses full mode. Post-commit/post-merge/post-checkout indexing uses fast mode in a detached process and always exits 0, so a broken index never fails a commit, merge, or checkout. Maintained checkouts keep their derived project name; linked worktrees use their deterministic lifecycle identity. Re-running onboarding is idempotent. Repositories dominated by YAML, prose, or shell may produce a thin graph; say so rather than refusing to index them.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.