#!/usr/bin/env bash # register_project.sh — register a project/repo with the Custodian State Hub # # Usage: scripts/register_project.sh [flags] # domain: slug of an active domain (e.g. custodian, railiance) # project_path: absolute path to the project directory # --additional: add a second repo to an existing domain; skip agent file generation # --codex: generate AGENTS.md (HTTP API) instead of CLAUDE.md + .claude/rules/ # # Examples: # scripts/register_project.sh railiance /home/worsch/railiance # scripts/register_project.sh railiance /home/worsch/railiance-infra --additional # scripts/register_project.sh capabilities /home/worsch/my-repo --codex # # What it does: # 1. Verify the API is reachable # 2. Verify the domain exists via GET /domains/{slug}/ # 3. Look up the topic ID for the domain (first active topic) # 4. Check that state-hub is in ~/.claude.json (skipped with --codex) # 5a. [default] Write CLAUDE.md + .claude/rules/*.md (modular @-import structure) # 5b. [--codex] Write AGENTS.md from agents-codex.template + SCOPE.md # 6. POST to /repos/ to register the repo # 7. POST /repos/{slug}/paths/ to register this machine's local path (host_paths) # 8. POST a progress event recording the registration set -euo pipefail DOMAIN="${1:-}" PROJECT_PATH="${2:-}" ADDITIONAL=false CODEX_MODE=false for arg in "${@:3}"; do case "$arg" in --additional) ADDITIONAL=true ;; --codex) CODEX_MODE=true ;; esac done SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" STATE_HUB_DIR="$(dirname "$SCRIPT_DIR")" RULES_TEMPLATES_DIR="$SCRIPT_DIR/project_rules" API_BASE="${API_BASE:-http://127.0.0.1:8000}" # ── Validate args ────────────────────────────────────────────────────────────── if [[ -z "$DOMAIN" || -z "$PROJECT_PATH" ]]; then echo "Usage: $0 [--additional]" echo " domain: slug of an active domain in the State Hub" echo " project_path: absolute path to project directory" echo " --additional: register a second repo; skip CLAUDE.md generation" exit 1 fi if [[ ! -d "$PROJECT_PATH" ]]; then echo "ERROR: project_path does not exist: $PROJECT_PATH" exit 1 fi PROJECT_NAME="$(basename "$PROJECT_PATH")" REPO_SLUG="$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-\|-$//g')" # Derive a workplan prefix: uppercase first token + -WP (e.g. marki-docx → MARKI-WP) FIRST_TOKEN="$(echo "$REPO_SLUG" | cut -d'-' -f1 | tr '[:lower:]' '[:upper:]')" WP_PREFIX="${FIRST_TOKEN}-WP" # ── Step 1: API health check ─────────────────────────────────────────────────── echo "==> Checking API at $API_BASE ..." if ! curl -sf "$API_BASE/state/health" > /dev/null; then echo "ERROR: State Hub API is not reachable." echo " Start it: cd $STATE_HUB_DIR && make api" echo " (requires postgres: make db first)" exit 1 fi echo " API OK" # ── Step 2: Verify domain exists ─────────────────────────────────────────────── echo "==> Verifying domain '$DOMAIN' ..." DOMAIN_JSON="$(curl -sf "$API_BASE/domains/$DOMAIN" 2>/dev/null || echo 'NOT_FOUND')" if [[ "$DOMAIN_JSON" == "NOT_FOUND" ]] || ! echo "$DOMAIN_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get('slug') else 1)" 2>/dev/null; then echo "ERROR: Domain '$DOMAIN' not found in the State Hub." echo " To create: make add-domain DOMAIN=$DOMAIN NAME=\"\"" echo " To list available: curl -s $API_BASE/domains/ | python3 -m json.tool" exit 1 fi echo " Domain OK" # ── Step 3: Look up topic ID ─────────────────────────────────────────────────── echo "==> Looking up topic for domain '$DOMAIN' ..." TOPICS_JSON="$(curl -sf "$API_BASE/topics/?status=active")" TOPIC_ID="$(echo "$TOPICS_JSON" | python3 -c " import json, sys topics = json.load(sys.stdin) match = next((t for t in topics if t.get('domain_slug') == sys.argv[1]), None) print(match['id'] if match else 'NOT_FOUND') " "$DOMAIN")" if [[ "$TOPIC_ID" == "NOT_FOUND" ]]; then echo "WARNING: No active topic found for domain '$DOMAIN'. repo-identity.md will omit topic_id." TOPIC_ID="(none)" else echo " topic_id: $TOPIC_ID" fi # ── Step 4: Check MCP registration ──────────────────────────────────────────── if [[ "$CODEX_MODE" == "true" ]]; then echo "==> Codex mode: skipping MCP registration check (Claude Code-specific)" else echo "==> Checking MCP server registration ..." MCP_OK="$(python3 -c " import json from pathlib import Path f = Path.home() / '.claude.json' if not f.exists(): print('MISSING_FILE') else: d = json.loads(f.read_text()) print('OK' if 'state-hub' in d.get('mcpServers', {}) else 'NOT_REGISTERED') ")" case "$MCP_OK" in MISSING_FILE) echo "WARNING: ~/.claude.json not found. MCP server not registered." ;; NOT_REGISTERED) echo "WARNING: 'state-hub' not in ~/.claude.json. See global CLAUDE.md §MCP Server Registration." ;; *) echo " MCP OK" ;; esac fi # ── Helper: render a template with variable substitution ────────────────────── render_template() { local tmpl="$1" sed \ -e "s|{PROJECT_NAME}|$PROJECT_NAME|g" \ -e "s|{PROJECT_DESCRIPTION}|$PROJECT_NAME — (fill in purpose)|g" \ -e "s|{DOMAIN}|$DOMAIN|g" \ -e "s|{TOPIC_ID}|$TOPIC_ID|g" \ -e "s|{REPO_SLUG}|$REPO_SLUG|g" \ -e "s|{WP_PREFIX}|$WP_PREFIX|g" \ "$tmpl" } # ── Step 5: Write agent instruction files ───────────────────────────────────── if [[ "$ADDITIONAL" != "true" ]]; then SCOPE_MD="$PROJECT_PATH/SCOPE.md" if [[ ! -f "$SCOPE_MD" ]]; then echo "==> Writing SCOPE.md stub ..." render_template "$RULES_TEMPLATES_DIR/scope.template" > "$SCOPE_MD" echo " SCOPE.md written (stub — fill in purpose and boundaries)." else echo "==> SCOPE.md already exists — skipping." fi if [[ "$CODEX_MODE" == "true" ]]; then # ── 5b: Codex — write AGENTS.md from HTTP-API template ──────────────── AGENTS_MD="$PROJECT_PATH/AGENTS.md" CODEX_TMPL="$RULES_TEMPLATES_DIR/agents-codex.template" if [[ ! -f "$CODEX_TMPL" ]]; then echo "ERROR: agents-codex.template not found at $CODEX_TMPL" exit 1 fi if [[ -f "$AGENTS_MD" ]]; then echo "==> AGENTS.md already exists — skipping (manual merge may be needed)." else echo "==> Writing AGENTS.md (Codex / HTTP API mode) ..." render_template "$CODEX_TMPL" > "$AGENTS_MD" echo " AGENTS.md written." fi echo "" echo "Files needing manual content:" echo " AGENTS.md — fill in purpose description and stack/commands sections" echo " SCOPE.md — fill in scope boundaries" else # ── 5a: Claude Code — write .claude/rules/ + CLAUDE.md ──────────────── RULES_DIR="$PROJECT_PATH/.claude/rules" CLAUDE_MD="$PROJECT_PATH/CLAUDE.md" echo "==> Writing .claude/rules/ ..." mkdir -p "$RULES_DIR" for rule in repo-identity session-protocol first-session workplan-convention \ stack-and-commands architecture repo-boundary agents; do tmpl="$RULES_TEMPLATES_DIR/${rule}.template" out="$RULES_DIR/${rule}.md" if [[ -f "$tmpl" ]]; then render_template "$tmpl" > "$out" echo " .claude/rules/${rule}.md" else echo "WARNING: template missing: $tmpl" fi done if [[ -f "$CLAUDE_MD" ]]; then echo "" echo "==> CLAUDE.md already exists — appending @-import suggestion." cat >> "$CLAUDE_MD" << 'SUGGESTION' SUGGESTION echo " Suggestion appended to existing CLAUDE.md." else echo "==> Writing CLAUDE.md ..." render_template "$RULES_TEMPLATES_DIR/claude-md.template" > "$CLAUDE_MD" echo " CLAUDE.md written." fi echo "" echo "Rule files needing manual content:" echo " .claude/rules/repo-identity.md — update purpose description" echo " .claude/rules/stack-and-commands.md — language, deps, dev commands" echo " .claude/rules/architecture.md — design overview" echo " .claude/rules/repo-boundary.md — what this repo does NOT own" echo " .claude/rules/workplan-convention.md — verify WP prefix: $WP_PREFIX" fi fi # ── Step 6: Register repo in State Hub ──────────────────────────────────────── echo "" echo "==> Registering repo '$PROJECT_NAME' under domain '$DOMAIN' ..." REPO_PAYLOAD="$(python3 -c " import json print(json.dumps({ 'domain_slug': '$DOMAIN', 'slug': '$REPO_SLUG', 'name': '$PROJECT_NAME', 'local_path': '$PROJECT_PATH', })) ")" REPO_RESULT="$(curl -sf -X POST "$API_BASE/repos/" \ -H "Content-Type: application/json" \ -d "$REPO_PAYLOAD" 2>/dev/null || echo 'CONFLICT')" if [[ "$REPO_RESULT" == "CONFLICT" ]]; then echo " Repo '$REPO_SLUG' already registered — skipping POST." else echo " Repo registered: $REPO_SLUG" fi # ── Step 7: Register this machine's local path ──────────────────────────────── echo "==> Registering host path for $(hostname) ..." curl -sf -X POST "$API_BASE/repos/$REPO_SLUG/paths" \ -H "Content-Type: application/json" \ -d "{\"host\": \"$(hostname)\", \"path\": \"$PROJECT_PATH\"}" > /dev/null \ && echo " host_paths[$(hostname)] = $PROJECT_PATH" # ── Step 8: Record progress event ───────────────────────────────────────────── echo "==> Recording registration event ..." python3 - < Run SBOM ingest now? [y/N] " INGEST_NOW