diff --git a/state-hub/custodian_cli.py b/state-hub/custodian_cli.py index 0b0ba46..49ef9eb 100644 --- a/state-hub/custodian_cli.py +++ b/state-hub/custodian_cli.py @@ -95,6 +95,24 @@ def _api_post(path: str, body: dict) -> object: return json.loads(r.read()) +def _api_patch(path: str, body: dict) -> object: + url = API_BASE.rstrip("/") + path + data = json.dumps({k: v for k, v in body.items() if v is not None}).encode() + req = urllib.request.Request( + url, + data=data, + headers={"Content-Type": "application/json"}, + method="PATCH", + ) + with urllib.request.urlopen(req, timeout=10) as r: + return json.loads(r.read()) + + +def _find_repo_by_slug(repo_slug: str) -> dict | None: + repos = _api_get("/repos/") + return next((r for r in repos if r.get("slug") == repo_slug), None) + + def _detect_domain(project_path: Path) -> str | None: """Try to read domain from project charter frontmatter.""" for charter in project_path.rglob("project_charter_v*.md"): @@ -192,16 +210,30 @@ def cmd_register(args: argparse.Namespace) -> None: # ── Step 6: Register repo ───────────────────────────────────────────────── print(f"==> Registering repo '{repo_slug}' under domain '{domain}' ...") + repo = None try: - _api_post("/repos/", { + repo = _api_post("/repos/", { "domain_slug": domain, "slug": repo_slug, "name": project_name, "local_path": str(project_path), }) print(" Registered.") + except urllib.error.HTTPError as e: + if e.code != 409: + print(f" NOTE: {e} — repo registration failed, continuing.") + else: + print(" Repo already registered, reusing existing record.") + repo = _find_repo_by_slug(repo_slug) except Exception as e: print(f" NOTE: {e} — repo may already be registered, continuing.") + repo = _find_repo_by_slug(repo_slug) + + repo_id = repo.get("id") if isinstance(repo, dict) else None + if repo_id: + print(f" repo_id: {repo_id}") + else: + print(" WARNING: Could not resolve repo_id; onboarding workstream will remain domain-level.") # ── Step 7: Onboarding workstream + tasks ───────────────────────────────── ws_slug = f"repo-integration-{repo_slug}" @@ -213,6 +245,18 @@ def cmd_register(args: argparse.Namespace) -> None: ) if existing_ws: print(" Onboarding workstream already exists — skipping task creation.") + if repo_id and not existing_ws.get("repo_id"): + existing_owner = existing_ws.get("owner") + _api_patch(f"/workstreams/{existing_ws['id']}/", { + "repo_id": repo_id, + "owner": repo_slug if existing_owner in (None, domain) else existing_owner, + }) + print(" Attached existing onboarding workstream to repo.") + elif repo_id and existing_ws.get("repo_id") != repo_id: + print( + " WARNING: Existing onboarding workstream is attached to a different repo_id; " + "leaving it unchanged." + ) else: try: ws = _api_post("/workstreams/", { @@ -225,8 +269,9 @@ def cmd_register(args: argparse.Namespace) -> None: 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, + "owner": repo_slug, "status": "active", + "repo_id": repo_id, }) ws_id = ws["id"] sbom_desc = (