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