feat(onboarding): redesign repo integration journey

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 08:42:30 +01:00
parent 8a9314ded6
commit fe6704b9d0
6 changed files with 423 additions and 75 deletions

View File

@@ -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 = """\
<!--
CUSTODIAN SUGGESTION — generated by `custodian register-project`
This file was written by the Custodian State Hub registration workflow.
It contains the standard State Hub integration block for this repository.
A Claude Code agent in this repo should:
1. Read this file and the existing CLAUDE.md side by side
2. Integrate the sections below into CLAUDE.md — preserve all existing
project-specific conventions; add or update the hub integration block
3. Delete this file once integration is complete and commit the result
Do NOT add this file to .gitignore. It is a temporary artefact that signals
pending integration work to the repo agent.
-->
"""
_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:

View File

@@ -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" },

View File

@@ -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`<div class="kpi-infobox" style="position:relative">…</div>`;
withDocHelp(_card, "/docs/my-page");
```

View File

@@ -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 <remote-url> /path/to/repo
```
### Step 2 — Register from the repo root
```bash
cd /path/to/repo
custodian register-project --domain <slug>
```
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("<domain>")`, 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: `<repo-slug>`** 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=<slug> SCAN=1 REPO_PATH=<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/<DOMAIN>-WP-0001-<slug>.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("<domain>")` shows active workstreams,
blocking decisions, and recent progress — the standard orientation
- **Ecosystem todos:** tasks with `[repo:<slug>]` 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.

View File

@@ -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 <slug>
```
## 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> SLUG=<repo-slug> NAME="Display Name" PATH=/absolute/path
# Ingest SBOM (auto-detects lockfile at repo root)
make ingest-sbom REPO=<slug> REPO_PATH=/absolute/path
# Multi-ecosystem repo — scan all lockfiles recursively

View File

@@ -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`<div class="kpi-row">
<h3>Domains</h3>
<p class="big-num">${new Set(repoRows.map(r => r._domSlug)).size}</p>
</div>
<div class="card ${integratingCount > 0 ? 'card-integrating' : ''}">
<h3>Integrating</h3>
<p class="big-num">${integratingCount}</p>
<small>${integratingCount === 0 ? "✓ All repos integrated" : `${integratingCount} onboarding`}</small>
</div>
<div class="card ${coveredCount < repoRows.length ? '' : ''}">
<h3>SBOM Ingested</h3>
<p class="big-num">${coveredCount} / ${repoRows.length}</p>
@@ -160,13 +176,17 @@ if (domainBlocks.length === 0) {
<table class="repo-table">
<thead><tr>
<th>Repo</th>
<th>Status</th>
<th>SBOM</th>
<th>Packages</th>
<th>Local path</th>
</tr></thead>
<tbody>
${rows.map(r => html`<tr class="${r._hasSbom ? '' : 'row-gap'}">
${rows.map(r => html`<tr class="${r._integrating ? 'row-integrating' : r._hasSbom ? '' : 'row-gap'}">
<td class="repo-cell"><code>${r.repo}</code></td>
<td>${r._integrating
? html`<span class="chip chip-integrating">⚙ integrating</span>`
: html`<span class="chip chip-ok">ready</span>`}</td>
<td class="${r._hasSbom ? 'sbom-ok' : 'sbom-warn'}">${r.sbom}</td>
<td>${r.pkgs}</td>
<td class="path-cell">${r.path}</td>
@@ -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`<div class="howto">
<h4>Register a new repo</h4>
<pre>cd ~/the-custodian/state-hub
make add-repo DOMAIN=&lt;slug&gt; SLUG=&lt;repo-slug&gt; NAME="Display Name" PATH=/absolute/path</pre>
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"); }
```
<h4>Ingest SBOM (single ecosystem, auto-detect lockfile at root)</h4>
<pre>make ingest-sbom REPO=&lt;slug&gt; REPO_PATH=/absolute/path</pre>
<h4>Ingest SBOM (multi-ecosystem repo — scans all lockfiles recursively)</h4>
<pre>make ingest-sbom REPO=&lt;slug&gt; SCAN=1 REPO_PATH=/absolute/path</pre>
<h4>Infra-only repos (Ansible/shell — no lockfile)</h4>
<p>Register the repo for inventory purposes. SBOM gap is expected and intentional.
Terraform providers are tracked via <code>.terraform.lock.hcl</code> (auto-detected by <code>--scan</code>).</p>
```js
display(html`<div class="onboard-panel">
<div class="onboard-step">
<span class="onboard-num">1</span>
<div>
<strong>Clone the repo locally</strong>
<pre>git clone &lt;remote-url&gt; /path/to/repo</pre>
</div>
</div>
<div class="onboard-step">
<span class="onboard-num">2</span>
<div>
<strong>Register from the repo root</strong>
<pre>cd /path/to/repo
custodian register-project --domain &lt;slug&gt;</pre>
<p class="onboard-note">The custodian writes <code>CLAUDE.custodian.md</code>, registers the repo, and creates 4 onboarding tasks in the domain's topic.</p>
</div>
</div>
<div class="onboard-step">
<span class="onboard-num">3</span>
<div>
<strong>Open the repo in Claude Code</strong>
<pre>cd /path/to/repo && claude</pre>
<p class="onboard-note">The repo agent sees the Repo Integration workstream at session start and integrates autonomously — no manual interaction needed.</p>
</div>
</div>
<div class="onboard-step">
<span class="onboard-num">4</span>
<div>
<strong>Monitor here</strong>
<p class="onboard-note">The <strong>⚙ integrating</strong> badge clears when the repo agent completes all 4 onboarding tasks and closes the workstream.</p>
</div>
</div>
</div>`);
```
@@ -253,9 +298,15 @@ make add-repo DOMAIN=&lt;slug&gt; SLUG=&lt;repo-slug&gt; 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; }
</style>