generated from coulomb/repo-seed
Updated repo onboarding
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <remote-url> /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 <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>
|
||||
<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 <domain> /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 <slug></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 <slug></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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user