generated from coulomb/repo-seed
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:
163
custodian_cli.py
163
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 = """\
|
||||
<!--
|
||||
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:
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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");
|
||||
```
|
||||
|
||||
167
dashboard/src/docs/repo-integration.md
Normal file
167
dashboard/src/docs/repo-integration.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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=<slug> SLUG=<repo-slug> 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=<slug> REPO_PATH=/absolute/path</pre>
|
||||
|
||||
<h4>Ingest SBOM (multi-ecosystem repo — scans all lockfiles recursively)</h4>
|
||||
<pre>make ingest-sbom REPO=<slug> 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 <remote-url> /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 <slug></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=<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; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user