Updated repo onboarding

This commit is contained in:
2026-05-04 19:18:10 +02:00
parent bfed370a6e
commit 30f7ac8fea
4 changed files with 213 additions and 12 deletions

View File

@@ -1,5 +1,7 @@
import asyncio
import json
import os
import re
import socket
import subprocess
import sys
@@ -34,6 +36,8 @@ from api.schemas.managed_repo import (
PendingInterfaceChange,
RepoCreate,
RepoDispatch,
RepoOnboardRequest,
RepoOnboardResult,
RepoPathRegister,
RepoRead,
RepoScopeHealth,
@@ -90,6 +94,77 @@ async def register_repo(
return repo
@router.post("/onboard", response_model=RepoOnboardResult)
async def onboard_repo(body: RepoOnboardRequest) -> RepoOnboardResult:
"""Run the local repo onboarding script for an accessible working copy.
The dashboard uses this for the "Add Repo" action. The path must be visible
from the State Hub host, either as a local checkout or through an ops-bridge
mounted/exposed working copy. Keep the API agent-profile based so future
native coding agents can gain their own profiles without changing callers.
"""
project_path = Path(body.project_path).expanduser()
if not project_path.exists() or not project_path.is_dir():
raise HTTPException(
status_code=400,
detail=f"project_path is not an accessible directory: {body.project_path}",
)
if not (project_path / ".git").exists():
raise HTTPException(
status_code=400,
detail=f"project_path does not look like a git working copy: {body.project_path}",
)
script = Path(__file__).parent.parent.parent / "scripts" / "register_project.sh"
cmd = ["bash", str(script), body.domain_slug, str(project_path)]
if body.agent_profile == "codex":
cmd.append("--codex")
if body.additional:
cmd.append("--additional")
env = {
**os.environ,
"API_BASE": settings.api_base,
"CUSTODIAN_SKIP_SBOM_PROMPT": "true",
}
result = await asyncio.to_thread(
subprocess.run,
cmd,
cwd=str(script.parent.parent),
env=env,
stdin=subprocess.DEVNULL,
capture_output=True,
text=True,
timeout=180,
)
stdout = result.stdout or ""
stderr = result.stderr or ""
if result.returncode != 0:
raise HTTPException(
status_code=500,
detail={
"message": "Repo onboarding failed.",
"command": cmd,
"stdout": stdout,
"stderr": stderr,
},
)
repo_slug = None
match = re.search(r"Repo slug:\s+([a-z0-9][a-z0-9-]*)", stdout)
if match:
repo_slug = match.group(1)
return RepoOnboardResult(
ok=True,
repo_slug=repo_slug,
agent_profile=body.agent_profile,
command=cmd,
stdout=stdout,
stderr=stderr,
)
@router.get("/by-fingerprint", response_model=list[RepoRead])
async def get_repo_by_fingerprint(
hash: str,

View File

@@ -1,6 +1,6 @@
import uuid
from datetime import date, datetime
from typing import Any
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
@@ -32,6 +32,23 @@ class RepoPathRegister(BaseModel):
path: str
class RepoOnboardRequest(BaseModel):
"""Start scripted onboarding for a working copy that is visible to State Hub."""
domain_slug: str
project_path: str
agent_profile: Literal["claude-code", "codex"] = "codex"
additional: bool = False
class RepoOnboardResult(BaseModel):
ok: bool
repo_slug: str | None = None
agent_profile: str
command: list[str]
stdout: str = ""
stderr: str = ""
class RepoRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID

View File

@@ -303,37 +303,125 @@ const _h2onboard = [...document.querySelectorAll("#observablehq-main h2")]
if (_h2onboard) { _h2onboard.style.position = "relative"; withDocHelp(_h2onboard, "/docs/repo-integration"); }
```
```js
const onboardId = `onboard-${Math.random().toString(36).slice(2)}`;
const onboardDomainOptions = [...domains]
.sort((a, b) => a.slug.localeCompare(b.slug))
.map(d => html`<option value=${d.slug}>${d.slug}</option>`);
const onboardForm = html`<div class="onboard-action" id=${onboardId}>
<div class="onboard-action-head">
<div>
<h3>Add Repo</h3>
<p>Register an accessible git working copy and write the starter files for the selected agent profile.</p>
</div>
<button type="button" class="onboard-primary">Add Repo</button>
</div>
<div class="onboard-grid">
<label>
<span>Domain</span>
<select class="onboard-domain">${onboardDomainOptions}</select>
</label>
<label>
<span>Agent profile</span>
<select class="onboard-agent">
<option value="codex">Codex (AGENTS.md)</option>
<option value="claude-code">Claude Code (CLAUDE.md + rules)</option>
</select>
</label>
<label class="onboard-path-label">
<span>Repo path visible to State Hub</span>
<input class="onboard-path" type="text" placeholder="/home/worsch/example-repo" />
</label>
<label class="onboard-check">
<input class="onboard-additional" type="checkbox" />
<span>Additional repo for an existing domain</span>
</label>
</div>
<p class="onboard-path-note">Paste the absolute path when the checkout is on the State Hub host or exposed via ops-bridge.</p>
<pre class="onboard-status" aria-live="polite"></pre>
</div>`;
display(onboardForm);
{
const button = onboardForm.querySelector(".onboard-primary");
const status = onboardForm.querySelector(".onboard-status");
const pathInput = onboardForm.querySelector(".onboard-path");
button.addEventListener("click", async () => {
const body = {
domain_slug: onboardForm.querySelector(".onboard-domain").value,
project_path: pathInput.value.trim(),
agent_profile: onboardForm.querySelector(".onboard-agent").value,
additional: onboardForm.querySelector(".onboard-additional").checked,
};
if (!body.project_path) {
status.textContent = "Enter an absolute path that the State Hub API can access.";
pathInput.focus();
return;
}
button.disabled = true;
status.textContent = "Onboarding started...";
try {
const response = await fetch(`${API}/repos/onboard`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const detail = payload.detail ?? payload;
throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail, null, 2));
}
status.textContent = [
`Onboarding complete${payload.repo_slug ? `: ${payload.repo_slug}` : ""}`,
"",
payload.stdout?.trim() ?? "",
payload.stderr?.trim() ? `\nWarnings:\n${payload.stderr.trim()}` : "",
].join("\n").trim();
} catch (error) {
status.textContent = `Onboarding failed:\n${error.message}`;
} finally {
button.disabled = false;
}
});
}
```
```js
display(html`<div class="onboard-panel">
<div class="onboard-step">
<span class="onboard-num">1</span>
<div>
<strong>Clone the repo locally</strong>
<strong>Make the working copy accessible</strong>
<pre>git clone &lt;remote-url&gt; /path/to/repo</pre>
<p class="onboard-note">The path must be visible from the State Hub API host, either as a local checkout or through an ops-bridge-exposed path.</p>
</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>
<strong>Choose an agent profile</strong>
<pre>Codex -> AGENTS.md + SCOPE.md
Claude Code -> CLAUDE.md + .claude/rules/</pre>
<p class="onboard-note">The API keeps this as an agent profile so future native coding agents can get their own onboarding templates without changing the repo model.</p>
</div>
</div>
<div class="onboard-step">
<span class="onboard-num">3</span>
<div>
<strong>Open the repo in Claude Code and run /init</strong>
<pre>cd /path/to/repo && claude</pre>
<p class="onboard-note">Once Claude starts, run <code>/init</code> to trigger the integration. The repo agent reads <code>CLAUDE.custodian.md</code>, picks up the onboarding tasks, and integrates autonomously.</p>
<strong>Run onboarding</strong>
<pre>scripts/register_project.sh &lt;domain&gt; /path/to/repo --codex</pre>
<p class="onboard-note">Use the Add Repo form above for the automatic path. The script verifies the domain, writes agent instructions, registers the repo, records host_paths for this machine, and logs a progress event.</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>
<strong>Start the agent and monitor here</strong>
<p class="onboard-note">Open the repo in the chosen coding agent, complete the generated TODO stubs, then use the Repositories and DoI views to track integration gaps.</p>
</div>
</div>
</div>`);
@@ -372,6 +460,18 @@ custodian register-project --domain &lt;slug&gt;</pre>
.chip-integrating { background: #ede9fe; color: #5b21b6; }
.row-integrating { background: #faf5ff; }
.onboard-action { margin-top: 0.5rem; margin-bottom: 1rem; border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 8px; padding: 1rem; background: var(--theme-background); }
.onboard-action-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 0.9rem; }
.onboard-action h3 { margin: 0 0 0.2rem; font-size: 1rem; }
.onboard-action p { margin: 0; color: #6b7280; font-size: 0.85rem; }
.onboard-grid { display: grid; grid-template-columns: minmax(120px, 0.8fr) minmax(180px, 1fr) minmax(260px, 2fr); gap: 0.8rem; align-items: end; }
.onboard-grid label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.78rem; font-weight: 600; color: #4b5563; }
.onboard-grid input[type="text"], .onboard-grid select { min-height: 2.1rem; border: 1px solid var(--theme-foreground-faint, #d1d5db); border-radius: 4px; padding: 0.35rem 0.5rem; background: var(--theme-background); color: var(--theme-foreground); font: inherit; }
.onboard-check { flex-direction: row !important; align-items: center; grid-column: 1 / -1; font-weight: 500 !important; }
.onboard-primary { border: 0; border-radius: 4px; padding: 0.45rem 0.8rem; background: var(--theme-foreground-focus, #111827); color: var(--theme-background, white); font-weight: 700; cursor: pointer; white-space: nowrap; }
.onboard-primary:disabled { opacity: 0.6; cursor: progress; }
.onboard-path-note { margin-top: 0.65rem !important; font-size: 0.82rem !important; }
.onboard-status { min-height: 0; max-height: 18rem; overflow: auto; margin: 0.75rem 0 0; padding: 0.5rem 0.65rem; background: var(--theme-background-alt); border-radius: 4px; font-size: 0.78rem; white-space: pre-wrap; }
.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; }
@@ -387,4 +487,9 @@ custodian register-project --domain &lt;slug&gt;</pre>
vertical-align: middle;
}
@keyframes doi-spin { to { transform: rotate(360deg); } }
@media (max-width: 760px) {
.onboard-action-head { flex-direction: column; }
.onboard-primary { width: 100%; }
.onboard-grid { grid-template-columns: 1fr; }
}
</style>

View File

@@ -306,7 +306,11 @@ fi
# ── Optional: SBOM ingest ─────────────────────────────────────────────────────
if [[ "$ADDITIONAL" != "true" ]]; then
echo ""
read -r -p "==> Run SBOM ingest now? [y/N] " INGEST_NOW </dev/tty || INGEST_NOW="N"
if [[ "${CUSTODIAN_SKIP_SBOM_PROMPT:-}" == "true" ]]; then
INGEST_NOW="N"
else
read -r -p "==> Run SBOM ingest now? [y/N] " INGEST_NOW </dev/tty || INGEST_NOW="N"
fi
if [[ "$INGEST_NOW" =~ ^[Yy]$ ]]; then
INGEST_PY="$STATE_HUB_DIR/.venv/bin/python"
if [[ -x "$INGEST_PY" ]]; then