Claude Skill

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

LLM Mart · 0 points · 17 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ConnorGriffin-skills-skills_tools_cbm-onboard-872be56.zip · 11 KB
Part of connorgriffin/skills — 25 skills

Install

skills CLI npx skills add https://github.com/ConnorGriffin/skills/tree/main/skills/tools/cbm-onboard
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install connorgriffin-skills@llmmart
Git 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-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:

    <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:

    <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:

    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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related