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) {
| Repo |
+ Status |
SBOM |
Packages |
Local path |
- ${rows.map(r => html`
+ ${rows.map(r => html`
${r.repo} |
+ ${r._integrating
+ ? html`⚙ integrating`
+ : html`ready`} |
${r.sbom} |
${r.pkgs} |
${r.path} |
@@ -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; }