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:
|
||||
|
||||
Reference in New Issue
Block a user