From fe6704b9d0f5a09e25d54220a311193fc5c67964 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 2 Mar 2026 08:42:30 +0100 Subject: [PATCH] feat(onboarding): redesign repo integration journey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit custodian_cli.py: - register-project now writes CLAUDE.custodian.md (suggestion) instead of overwriting CLAUDE.md; includes preamble with integration instructions - registers repo via POST /repos/ - creates a "Repo Integration: {slug}" workstream in the domain's topic with 4 onboarding tasks (integrate CLAUDE.md, first workplan, SBOM, EPs/TDs); checks for existing workstream to be idempotent - fixes {REPO_SLUG} template substitution (previously missing) dashboard: - repos.md: fetches workstreams; detects active repo-integration-* slugs; adds "Integrating" KPI card; shows ⚙ integrating badge per repo in coverage map and table; replaces "How to Ingest a Repo" with "Onboard a New Repo" 4-step panel with doc help button - docs/repo-integration.md (new): full collaboration model doc — custodian as coach, repo agent as executor; journey, generated tasks, first session protocol, ongoing relationship - docs/repos.md: links to new repo-integration doc; updates "What is a managed repo?" section; adds onboarding quick reference - docs/reference.md: fix latent build error — code examples were in ```js fences (executed by OF); changed to ```javascript (display only) - observablehq.config.js: adds "Repo Integration" to Reference nav Co-Authored-By: Claude Sonnet 4.6 --- custodian_cli.py | 163 ++++++++++++++++++++---- dashboard/observablehq.config.js | 1 + dashboard/src/docs/reference.md | 6 +- dashboard/src/docs/repo-integration.md | 167 +++++++++++++++++++++++++ dashboard/src/docs/repos.md | 26 ++-- dashboard/src/repos.md | 135 +++++++++++++------- 6 files changed, 423 insertions(+), 75 deletions(-) create mode 100644 dashboard/src/docs/repo-integration.md diff --git a/custodian_cli.py b/custodian_cli.py index 4a9dd38..af26550 100644 --- a/custodian_cli.py +++ b/custodian_cli.py @@ -26,6 +26,54 @@ API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000") TEMPLATE = STATE_HUB_DIR / "scripts" / "project_claude_md.template" PATCH_CWD = STATE_HUB_DIR / "scripts" / "patch_mcp_cwd.py" +_SUGGESTION_PREAMBLE = """\ + + +""" + +_ONBOARDING_TASKS = [ + ( + "Integrate CLAUDE.custodian.md → CLAUDE.md", + "high", + "A CLAUDE.custodian.md suggestion file was written by the custodian registration workflow. " + "Read both files, merge the hub integration block into the existing CLAUDE.md " + "(preserve all project-specific conventions), then delete CLAUDE.custodian.md and commit.", + ), + ( + "Write first workplan and initialise workplans/", + "high", + "Create a workplans/ directory and write the first workplan file following ADR-001 " + "(~/the-custodian/canon/architecture/adr-001-workplans-as-repo-artefacts.md). " + "Cover the repo's primary near-term work strand. Register the workstream in the state hub via MCP.", + ), + ( + "Ingest SBOM", + "medium", + # path substituted at call time + "", + ), + ( + "Register known EPs and TDs", + "low", + "Catalogue any known extension points (future enhancement hooks) and technical debt items " + "using the register_extension_point() and register_technical_debt() MCP tools.", + ), +] + # ── Helpers ──────────────────────────────────────────────────────────────────── def _api_get(path: str) -> object: @@ -68,12 +116,14 @@ def _check_mcp() -> bool: # ── Subcommands ──────────────────────────────────────────────────────────────── def cmd_register(args: argparse.Namespace) -> None: + """Register a project/repo with the State Hub and generate onboarding tasks.""" project_path = Path(args.path).resolve() if not project_path.is_dir(): print(f"ERROR: {project_path} is not a directory.") sys.exit(1) project_name = project_path.name + repo_slug = re.sub(r"-+", "-", re.sub(r"[^a-z0-9]", "-", project_name.lower())).strip("-") # ── Step 1: API health ───────────────────────────────────────────────────── print(f"==> Checking API at {API_BASE} ...") @@ -89,7 +139,7 @@ def cmd_register(args: argparse.Namespace) -> None: if domain: print(f" Detected: {domain}") else: - print(f"ERROR: Could not auto-detect domain. Pass --domain explicitly.") + print("ERROR: Could not auto-detect domain. Pass --domain explicitly.") print(f" Valid: {', '.join(valid_domains)}") sys.exit(1) @@ -103,10 +153,10 @@ def cmd_register(args: argparse.Namespace) -> None: match = next((t for t in topics if t.get("domain_slug") == domain), None) if not match: print(f" No topic found — creating one for domain '{domain}' ...") - slug = re.sub(r"[^a-z0-9]+", "-", domain.lower()).strip("-") + t_slug = re.sub(r"[^a-z0-9]+", "-", domain.lower()).strip("-") try: match = _api_post("/topics/", { - "slug": slug, + "slug": t_slug, "title": project_name, "domain": domain, "status": "active", @@ -124,33 +174,98 @@ def cmd_register(args: argparse.Namespace) -> None: print(" MCP OK") else: print("WARNING: 'state-hub' not in ~/.claude.json.") - print(f" See ~/.claude/CLAUDE.md → MCP Server Registration section.") + print(" See ~/.claude/CLAUDE.md → MCP Server Registration section.") - # ── Step 5: CLAUDE.md ────────────────────────────────────────────────────── - claude_md = project_path / "CLAUDE.md" - if claude_md.exists(): - print(f"==> CLAUDE.md already exists at {claude_md} — skipping.") + # ── Step 5: Write CLAUDE.custodian.md ───────────────────────────────────── + suggestion_file = project_path / "CLAUDE.custodian.md" + print(f"==> Writing custodian suggestion to {suggestion_file} ...") + content = ( + _SUGGESTION_PREAMBLE + + TEMPLATE.read_text() + .replace("{PROJECT_NAME}", project_name) + .replace("{DOMAIN}", domain) + .replace("{TOPIC_ID}", topic_id) + .replace("{REPO_SLUG}", repo_slug) + ) + suggestion_file.write_text(content) + print(" Written. The repo agent integrates it into CLAUDE.md then deletes it.") + + # ── Step 6: Register repo ───────────────────────────────────────────────── + print(f"==> Registering repo '{repo_slug}' under domain '{domain}' ...") + try: + _api_post("/repos/", { + "domain_slug": domain, + "slug": repo_slug, + "name": project_name, + "local_path": str(project_path), + }) + print(" Registered.") + except Exception as e: + print(f" NOTE: {e} — repo may already be registered, continuing.") + + # ── Step 7: Onboarding workstream + tasks ───────────────────────────────── + ws_slug = f"repo-integration-{repo_slug}" + print(f"==> Creating onboarding workstream '{ws_slug}' ...") + # Check if it already exists + existing_ws = next( + (w for w in _api_get("/workstreams/") if w.get("slug") == ws_slug and w.get("status") == "active"), + None, + ) + if existing_ws: + print(" Onboarding workstream already exists — skipping task creation.") else: - print(f"==> Writing CLAUDE.md to {claude_md} ...") - content = TEMPLATE.read_text() - content = content.replace("{PROJECT_NAME}", project_name) - content = content.replace("{DOMAIN}", domain) - content = content.replace("{TOPIC_ID}", topic_id) - claude_md.write_text(content) - print(" Written.") + try: + ws = _api_post("/workstreams/", { + "topic_id": topic_id, + "title": f"Repo Integration: {repo_slug}", + "slug": ws_slug, + "description": ( + f"Bootstrapping workstream created by the custodian during registration of " + f"'{repo_slug}'. Contains onboarding tasks for the repo agent to execute. " + f"ADR-001 exception: this workstream is DB-first because the repo has no " + f"workplans/ directory yet. Task T2 produces the first workplan file." + ), + "owner": domain, + "status": "active", + }) + ws_id = ws["id"] + sbom_desc = ( + f"Capture the repo's dependency snapshot. From state-hub dir: " + f"make ingest-sbom REPO={repo_slug} SCAN=1 REPO_PATH={project_path}" + ) + tasks = [ + (_ONBOARDING_TASKS[0][0], _ONBOARDING_TASKS[0][1], _ONBOARDING_TASKS[0][2]), + (_ONBOARDING_TASKS[1][0], _ONBOARDING_TASKS[1][1], _ONBOARDING_TASKS[1][2]), + (_ONBOARDING_TASKS[2][0], _ONBOARDING_TASKS[2][1], sbom_desc), + (_ONBOARDING_TASKS[3][0], _ONBOARDING_TASKS[3][1], _ONBOARDING_TASKS[3][2]), + ] + for title, priority, description in tasks: + _api_post("/tasks/", { + "workstream_id": ws_id, + "title": title, + "priority": priority, + "description": description, + }) + print(f" Created with {len(tasks)} onboarding tasks.") + print(f" The {domain} repo agent will see these at next session start.") + except Exception as e: + print(f" WARNING: Could not create onboarding tasks: {e}") + ws_id = None - # ── Step 6: Progress event ───────────────────────────────────────────────── + # ── Step 8: Progress event ───────────────────────────────────────────────── print("==> Recording registration event ...") try: _api_post("/progress/", { "topic_id": topic_id, "event_type": "milestone", - "summary": f"Project registered with State Hub: {project_name} ({domain})", + "summary": f"Repo registered: {project_name} ({domain}) — onboarding tasks created", "author": "custodian", "detail": { "project_path": str(project_path), - "claude_md": str(claude_md), + "suggestion_file": str(suggestion_file), + "repo_slug": repo_slug, "domain": domain, + "onboarding_workstream_slug": ws_slug, }, }) print(" Event recorded.") @@ -159,12 +274,14 @@ def cmd_register(args: argparse.Namespace) -> None: print() print("Registration complete!") - print(f" Project: {project_name}") - print(f" Domain: {domain}") - print(f" Topic ID: {topic_id}") - print(f" CLAUDE.md: {claude_md}") + print(f" Project: {project_name}") + print(f" Domain: {domain}") + print(f" Repo slug: {repo_slug}") + print(f" Topic ID: {topic_id}") + print(f" Suggestion: {suggestion_file}") print() - print("Next: restart Claude Code for the MCP server to be active in this project.") + print("Next: open the repo in Claude Code.") + print(" The repo agent will pick up 4 onboarding tasks and integrate autonomously.") def cmd_create_workstream(args: argparse.Namespace) -> None: diff --git a/dashboard/observablehq.config.js b/dashboard/observablehq.config.js index 91cf8c8..3fc6003 100644 --- a/dashboard/observablehq.config.js +++ b/dashboard/observablehq.config.js @@ -43,6 +43,7 @@ export default { { name: "Overview", path: "/docs/overview" }, { name: "Progress Log", path: "/docs/progress-log" }, { name: "Reference & Context Help", path: "/docs/reference" }, + { name: "Repo Integration", path: "/docs/repo-integration" }, { name: "Repos", path: "/docs/repos" }, { name: "SBOM", path: "/docs/sbom" }, { name: "Tasks", path: "/docs/tasks" }, diff --git a/dashboard/src/docs/reference.md b/dashboard/src/docs/reference.md index 951c844..483eb5e 100644 --- a/dashboard/src/docs/reference.md +++ b/dashboard/src/docs/reference.md @@ -56,20 +56,20 @@ first automatically. The helper is exported from `src/components/doc-overlay.js`: -```js +```javascript import {withDocHelp} from "./components/doc-overlay.js"; ``` **On a page h1:** -```js +```javascript const _h1 = document.querySelector("#observablehq-main h1"); if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/my-page"); } ``` **On a sidebar card or other element** (must have `position: relative`): -```js +```javascript const _card = html`
`; withDocHelp(_card, "/docs/my-page"); ``` diff --git a/dashboard/src/docs/repo-integration.md b/dashboard/src/docs/repo-integration.md new file mode 100644 index 0000000..499f019 --- /dev/null +++ b/dashboard/src/docs/repo-integration.md @@ -0,0 +1,167 @@ +--- +title: Repo Integration — Reference +--- + +# Repo Integration + +This page describes how a new repository is onboarded into the Custodian +ecosystem and how the custodian and the repo's own Claude agent collaborate +during and after integration. + +--- + +## The Collaboration Model + +The custodian acts as a **coach**: it registers the repo, writes an +integration suggestion, and generates a structured set of onboarding tasks. +The repo's own Claude agent acts as the **executor**: it reads those tasks, +makes all changes to the repo, and closes out the onboarding workstream. + +| Role | Responsibility | +|------|---------------| +| **Custodian** | Registers the repo, generates `CLAUDE.custodian.md`, creates the onboarding workstream and tasks, monitors integration status via the dashboard | +| **Repo agent** | Integrates `CLAUDE.custodian.md` → `CLAUDE.md`, writes the first workplan, ingests the SBOM, catalogues EPs/TDs, closes the onboarding workstream | + +The custodian never writes files into another repo directly. All changes to +the target repo are made from inside that repo by its own agent. This upholds +the [repo boundary rule](/docs/inter-repo-communication). + +--- + +## The Registration Journey + +### Step 1 — Clone the repo locally + +```bash +git clone /path/to/repo +``` + +### Step 2 — Register from the repo root + +```bash +cd /path/to/repo +custodian register-project --domain +``` + +The `--domain` flag is required. Run `custodian status` to list valid domain +slugs. The command takes about five seconds and produces no interactive +prompts. + +What happens automatically: +1. The API is health-checked +2. The domain is validated; the domain's topic ID is resolved +3. `CLAUDE.custodian.md` is written to the repo root — the integration suggestion +4. The repo is registered in the State Hub (`POST /repos/`) +5. A **Repo Integration** workstream is created in the domain's topic with 4 + onboarding tasks +6. A progress event is logged + +### Step 3 — Open the repo in Claude Code + +```bash +cd /path/to/repo +claude +``` + +The repo agent starts, calls `get_domain_summary("")`, and sees the +Repo Integration workstream. It works through the 4 onboarding tasks +autonomously. No human interaction is needed unless the agent has a question. + +### Step 4 — Monitor on the Repos page + +The [Repos](/repos) page shows each repo's integration status. An **integrating** +badge appears on repos with an active Repo Integration workstream. The badge +clears when the workstream is marked completed. + +--- + +## What the Registration Creates + +### `CLAUDE.custodian.md` + +A suggestion file written to the repo root. It contains the full State Hub +session protocol, First Session Protocol, workplan convention, contribution +tracking instructions, and SBOM guidance — pre-filled with the repo's domain, +topic ID, and slug. + +The repo agent integrates this content into the existing `CLAUDE.md` (or +creates a new one) and deletes the suggestion file. It is not meant to persist. + +### Repo Integration workstream + +A workstream titled **Repo Integration: ``** is created in the +target domain's topic. It is visible via `get_domain_summary()` at the repo +agent's next session start. + +> **ADR-001 note:** This workstream is a DB-first bootstrapping exception. +> The file-first principle does not apply here because the repo has no +> `workplans/` directory yet. Writing the first workplan file is task T2. + +### 4 Onboarding tasks + +| # | Title | Priority | What it means | +|---|-------|----------|---------------| +| T1 | Integrate `CLAUDE.custodian.md` → `CLAUDE.md` | high | Merge the suggestion into the existing CLAUDE.md; delete the suggestion file; commit | +| T2 | Write first workplan and initialise `workplans/` | high | Create `workplans/` and write the first workplan file per ADR-001; register the workstream in the hub | +| T3 | Ingest SBOM | medium | Run `make ingest-sbom REPO= SCAN=1 REPO_PATH=` from the state-hub dir | +| T4 | Register known EPs and TDs | low | Catalogue extension points and technical debt using the MCP tools | + +--- + +## Repo Agent: First Session Protocol + +When `get_domain_summary()` returns a **Repo Integration** workstream, the +repo agent should: + +1. Read `CLAUDE.custodian.md` alongside the existing `CLAUDE.md` +2. Execute T1 first — merge and delete the suggestion file, commit +3. Execute T2 — create `workplans/-WP-0001-.md` covering the + primary near-term work; register the workstream in the hub via MCP +4. Execute T3 — ingest the SBOM so the repo appears green on the Repos page +5. Execute T4 — a quick scan for obvious EPs/TDs; defer if nothing obvious +6. Mark each task `done` in the hub as completed +7. Mark the Repo Integration workstream `completed` +8. Log a progress event summarising the integration + +The agent should resolve each task independently and in order. It does not +need human approval for any of these steps unless it encounters an ambiguous +merge conflict in CLAUDE.md. + +--- + +## After Integration + +Once the onboarding workstream is closed, the repo participates in the full +custodian ecosystem: + +- **Session start:** `get_domain_summary("")` shows active workstreams, + blocking decisions, and recent progress — the standard orientation +- **Ecosystem todos:** tasks with `[repo:]` in their title created by + other agents appear in the domain summary and signal cross-repo work +- **Contributions:** outbound upstream work is tracked in `contrib/` and + registered via `register_contribution()` +- **SBOM:** re-ingest after dependency updates with `make ingest-sbom` +- **Session close:** `add_progress_event()` keeps the hub's episodic memory + current + +See the [Inter-Repo Communication](/docs/inter-repo-communication) reference +for task routing conventions. + +--- + +## Troubleshooting + +**`custodian: command not found`** +The CLI is installed via `make install-cli` from the state-hub directory. +Ensure `~/.local/bin` is on your `PATH`. + +**`ERROR: Cannot reach API`** +The state hub API must be running: `cd ~/the-custodian/state-hub && make api` + +**`CLAUDE.custodian.md` already exists** +Re-running `custodian register-project` overwrites it with a fresh +suggestion. The repo agent should integrate and delete it. + +**Repo already registered (slug conflict)** +The command is idempotent for the repo row. Onboarding tasks are re-created +only if no active Repo Integration workstream already exists. diff --git a/dashboard/src/docs/repos.md b/dashboard/src/docs/repos.md index 5105af8..a4d5390 100644 --- a/dashboard/src/docs/repos.md +++ b/dashboard/src/docs/repos.md @@ -12,9 +12,13 @@ their SBOM ingestion status, and a domain-grouped coverage map. ## What is a managed repo? A managed repo is a git repository that has been registered with the state hub -via `make add-repo` or `register_repo()`. Registration records the repo's slug, -domain, local path, and optional remote URL. Once registered, the repo can -receive SBOM ingestion and is eligible for the ADR-001 workplan validator. +via `custodian register-project` or `register_repo()`. Registration records the +repo's slug, domain, local path, and optional remote URL. Once registered, the +repo receives a `CLAUDE.custodian.md` integration suggestion, an onboarding +workstream with 4 tasks for the repo agent, and is eligible for SBOM ingestion +and the ADR-001 workplan validator. + +For the full onboarding journey see **[Repo Integration](/docs/repo-integration)**. --- @@ -52,14 +56,22 @@ Rows with no SBOM are highlighted in amber. --- +## Onboarding a new repo + +See **[Repo Integration](/docs/repo-integration)** for the full journey. + +Quick reference: + +```bash +# From the repo root — registers, writes CLAUDE.custodian.md, creates onboarding tasks +custodian register-project --domain +``` + ## Ingesting a repo's SBOM ```bash -# Register a new repo +# Auto-detects lockfile at repo root cd ~/the-custodian/state-hub -make add-repo DOMAIN= SLUG= NAME="Display Name" PATH=/absolute/path - -# Ingest SBOM (auto-detects lockfile at repo root) make ingest-sbom REPO= REPO_PATH=/absolute/path # Multi-ecosystem repo — scan all lockfiles recursively diff --git a/dashboard/src/repos.md b/dashboard/src/repos.md index 4713661..e8a7086 100644 --- a/dashboard/src/repos.md +++ b/dashboard/src/repos.md @@ -7,31 +7,38 @@ const API = "http://127.0.0.1:8000"; ``` ```js -let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _contribs = []; +let _repos = [], _domains = [], _sbom = [], _eps = [], _tds = [], _workstreams = []; try { - [_repos, _domains, _sbom, _eps, _tds, _contribs] = await Promise.all([ + [_repos, _domains, _sbom, _eps, _tds, _workstreams] = await Promise.all([ fetch(`${API}/repos/`).then(r => r.ok ? r.json() : []), fetch(`${API}/domains/`).then(r => r.ok ? r.json() : []), fetch(`${API}/sbom/`).then(r => r.ok ? r.json() : []), fetch(`${API}/extension-points/`).then(r => r.ok ? r.json() : []), fetch(`${API}/technical-debt/`).then(r => r.ok ? r.json() : []), - fetch(`${API}/contributions/`).then(r => r.ok ? r.json() : []), + fetch(`${API}/workstreams/`).then(r => r.ok ? r.json() : []), ]); } catch {} ``` ```js -const repos = _repos ?? []; -const domains = _domains ?? []; -const sbom = _sbom ?? []; -const eps = _eps ?? []; -const tds = _tds ?? []; -const contribs = _contribs ?? []; +const repos = _repos ?? []; +const domains = _domains ?? []; +const sbom = _sbom ?? []; +const eps = _eps ?? []; +const tds = _tds ?? []; +const workstreams = _workstreams ?? []; // Lookups const domainById = Object.fromEntries(domains.map(d => [d.id, d])); const domainBySlug = Object.fromEntries(domains.map(d => [d.slug, d])); +// Active "repo-integration-{slug}" workstreams — signals onboarding in progress +const integratingBySlug = Object.fromEntries( + workstreams + .filter(w => w.status === "active" && w.slug?.startsWith("repo-integration-")) + .map(w => [w.slug.replace("repo-integration-", ""), w]) +); + // Per-repo SBOM stats (from sbom entries) const sbomByRepo = {}; for (const e of sbom) { @@ -71,23 +78,27 @@ const repoRows = repos const lastScan = r.last_sbom_at ? new Date(r.last_sbom_at).toLocaleDateString() : (sbomData?.snapshot_at ? new Date(sbomData.snapshot_at).toLocaleDateString() : null); + const integrating = !!integratingBySlug[r.slug]; return { - _id: r.id, - _domSlug: domSlug, - _hasSbom: hasSbom, - repo: r.slug, - domain: domName, - path: r.local_path ?? "—", - sbom: hasSbom ? `✓ ${lastScan}` : "⚠ not ingested", - pkgs: pkgCount || (hasSbom ? "—" : 0), - eps: epByDomain[domSlug] ?? 0, - tds: tdByDomain[domSlug] ?? 0, + _id: r.id, + _domSlug: domSlug, + _hasSbom: hasSbom, + _integrating: integrating, + repo: r.slug, + domain: domName, + status: integrating ? "⚙ integrating" : "ready", + path: r.local_path ?? "—", + sbom: hasSbom ? `✓ ${lastScan}` : "⚠ not ingested", + pkgs: pkgCount || (hasSbom ? "—" : 0), + eps: epByDomain[domSlug] ?? 0, + tds: tdByDomain[domSlug] ?? 0, }; }) .sort((a, b) => a._domSlug.localeCompare(b._domSlug) || a.repo.localeCompare(b.repo)); -const gapCount = repoRows.filter(r => !r._hasSbom).length; -const coveredCount = repoRows.filter(r => r._hasSbom).length; +const gapCount = repoRows.filter(r => !r._hasSbom).length; +const coveredCount = repoRows.filter(r => r._hasSbom).length; +const integratingCount = repoRows.filter(r => r._integrating).length; ``` # Repos @@ -109,6 +120,11 @@ display(html`

Domains

${new Set(repoRows.map(r => r._domSlug)).size}

+
+

Integrating

+

${integratingCount}

+ ${integratingCount === 0 ? "✓ All repos integrated" : `⚙ ${integratingCount} onboarding`} +

SBOM Ingested

${coveredCount} / ${repoRows.length}

@@ -160,13 +176,17 @@ if (domainBlocks.length === 0) { + - ${rows.map(r => html` + ${rows.map(r => html` + @@ -197,31 +217,56 @@ const filteredRows = repoRows.filter(r => display(Inputs.table(filteredRows.map(r => ({ Repo: r.repo, Domain: r.domain, + Status: r.status, SBOM: r.sbom, Pkgs: r.pkgs, "EPs (domain)": r.eps || "—", "TDs (domain)": r.tds || "—", Path: r.path, -})), {maxWidth: 1000})); +})), {maxWidth: 1100})); ``` -## How to Ingest a Repo +## Onboard a New Repo ```js -display(html`
-

Register a new repo

-
cd ~/the-custodian/state-hub
-make add-repo DOMAIN=<slug> SLUG=<repo-slug> NAME="Display Name" PATH=/absolute/path
+const _h2onboard = [...document.querySelectorAll("#observablehq-main h2")] + .find(h => h.textContent.includes("Onboard a New Repo")); +if (_h2onboard) { _h2onboard.style.position = "relative"; withDocHelp(_h2onboard, "/docs/repo-integration"); } +``` -

Ingest SBOM (single ecosystem, auto-detect lockfile at root)

-
make ingest-sbom REPO=<slug> REPO_PATH=/absolute/path
- -

Ingest SBOM (multi-ecosystem repo — scans all lockfiles recursively)

-
make ingest-sbom REPO=<slug> SCAN=1 REPO_PATH=/absolute/path
- -

Infra-only repos (Ansible/shell — no lockfile)

-

Register the repo for inventory purposes. SBOM gap is expected and intentional. - Terraform providers are tracked via .terraform.lock.hcl (auto-detected by --scan).

+```js +display(html`
+
+ 1 +
+ Clone the repo locally +
git clone <remote-url> /path/to/repo
+
+
+
+ 2 +
+ Register from the repo root +
cd /path/to/repo
+custodian register-project --domain <slug>
+

The custodian writes CLAUDE.custodian.md, registers the repo, and creates 4 onboarding tasks in the domain's topic.

+
+
+
+ 3 +
+ Open the repo in Claude Code +
cd /path/to/repo && claude
+

The repo agent sees the Repo Integration workstream at session start and integrates autonomously — no manual interaction needed.

+
+
+
+ 4 +
+ Monitor here +

The ⚙ integrating badge clears when the repo agent completes all 4 onboarding tasks and closes the workstream.

+
+
`); ``` @@ -253,9 +298,15 @@ make add-repo DOMAIN=<slug> SLUG=<repo-slug> NAME="Display Name" PAT .sbom-warn { color: #856404; font-weight: 600; } .path-cell { font-family: monospace; font-size: 0.78rem; color: gray; } -.howto { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem 1.25rem; margin-top: 0.5rem; } -.howto h4 { margin: 0.75rem 0 0.3rem; font-size: 0.9rem; } -.howto h4:first-child { margin-top: 0; } -.howto pre { background: var(--theme-background); border-radius: 4px; padding: 0.5rem 0.75rem; font-size: 0.82rem; overflow-x: auto; margin: 0 0 0.5rem; } -.howto p { font-size: 0.85rem; color: gray; margin: 0 0 0.5rem; } +.card-integrating { border: 2px solid #7c3aed; } +.chip-integrating { background: #ede9fe; color: #5b21b6; } +.row-integrating { background: #faf5ff; } + +.onboard-panel { display: flex; flex-direction: column; gap: 0; margin-top: 0.5rem; background: var(--theme-background-alt); border-radius: 8px; overflow: hidden; } +.onboard-step { display: flex; gap: 1rem; align-items: flex-start; padding: 0.9rem 1.1rem; border-bottom: 1px solid var(--theme-foreground-faint, #eee); } +.onboard-step:last-child { border-bottom: none; } +.onboard-num { flex-shrink: 0; width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--theme-foreground-focus, #1a1a1a); color: var(--theme-background, white); font-size: 0.8rem; font-weight: 700; display: flex; align-items: center; justify-content: center; margin-top: 0.1rem; } +.onboard-step strong { font-size: 0.9rem; display: block; margin-bottom: 0.3rem; } +.onboard-step pre { background: var(--theme-background); border-radius: 4px; padding: 0.4rem 0.7rem; font-size: 0.8rem; overflow-x: auto; margin: 0 0 0.35rem; } +.onboard-note { font-size: 0.82rem; color: var(--theme-foreground-muted, gray); margin: 0; line-height: 1.45; }
RepoStatus SBOM Packages Local path
${r.repo}${r._integrating + ? html`⚙ integrating` + : html`ready`} ${r.sbom} ${r.pkgs} ${r.path}