diff --git a/.mcp.json b/.mcp.json index 5383f6b..7ca4866 100644 --- a/.mcp.json +++ b/.mcp.json @@ -3,9 +3,9 @@ "state-hub": { "type": "stdio", "command": "/home/worsch/the-custodian/state-hub/.venv/bin/python", - "args": ["-m", "mcp_server.server"], - "cwd": "/home/worsch/the-custodian/state-hub", + "args": ["/home/worsch/the-custodian/state-hub/mcp_server/server.py"], "env": { + "PYTHONPATH": "/home/worsch/the-custodian/state-hub", "API_BASE": "http://127.0.0.1:8000" } } diff --git a/canon/projects/coulomb.social/project_charter_v0.1.md b/canon/projects/coulomb.social/project_charter_v0.1.md index fe9b160..2625d79 100644 --- a/canon/projects/coulomb.social/project_charter_v0.1.md +++ b/canon/projects/coulomb.social/project_charter_v0.1.md @@ -10,6 +10,8 @@ scope: domains: ["Coulomb.social"] sensitivity: internal tags: ["cocreation", "marketplace", "governance", "agents"] +custodian_topic_id: "36c7421b-c537-4723-bf75-42a3ebc6a1dc" +domain: coulomb_social --- # Coulomb.social — Project Charter v0.1 diff --git a/canon/projects/custodian/project_charter_v0.1.md b/canon/projects/custodian/project_charter_v0.1.md index d7670b7..1a739e1 100644 --- a/canon/projects/custodian/project_charter_v0.1.md +++ b/canon/projects/custodian/project_charter_v0.1.md @@ -10,6 +10,8 @@ scope: domains: ["Custodian"] sensitivity: internal tags: ["agent", "canon", "continuity", "sovereignty", "family"] +custodian_topic_id: "cee7bedf-2b48-46ef-8601-006474f2ad7a" +domain: custodian --- # Custodian — Project Charter v0.1 diff --git a/canon/projects/foerster-capabilities/project_charter_v0.1.md b/canon/projects/foerster-capabilities/project_charter_v0.1.md index 36e4fd2..4ce1c16 100644 --- a/canon/projects/foerster-capabilities/project_charter_v0.1.md +++ b/canon/projects/foerster-capabilities/project_charter_v0.1.md @@ -10,6 +10,8 @@ scope: domains: ["Foerster Capabilities"] sensitivity: internal tags: ["taxonomy", "systems-theory", "agency", "governance"] +custodian_topic_id: "64418556-3206-457a-ba29-6884b5b12cf3" +domain: foerster_capabilities --- # Foerster Capabilities — Project Charter v0.1 diff --git a/canon/projects/markitect/project_charter_v0.1.md b/canon/projects/markitect/project_charter_v0.1.md index 3aeed78..0a4531c 100644 --- a/canon/projects/markitect/project_charter_v0.1.md +++ b/canon/projects/markitect/project_charter_v0.1.md @@ -10,6 +10,8 @@ scope: domains: ["Markitect"] sensitivity: internal tags: ["information-management", "canon", "schemas", "knowledge"] +custodian_topic_id: "5571d954-0d30-4950-980d-7bcaaad8e3e2" +domain: markitect --- # Markitect — Project Charter v0.1 diff --git a/canon/projects/personhood/project_charter_v0.1.md b/canon/projects/personhood/project_charter_v0.1.md index 35b95e3..9dafc39 100644 --- a/canon/projects/personhood/project_charter_v0.1.md +++ b/canon/projects/personhood/project_charter_v0.1.md @@ -10,6 +10,8 @@ scope: domains: ["Personhood"] sensitivity: internal tags: ["law", "rights", "obligations", "agents", "jurisdictions"] +custodian_topic_id: "084430ab-c630-48dc-9e1d-d07d1e8fce3c" +domain: personhood --- # Personhood — Project Charter v0.1 diff --git a/canon/projects/railiance/project_charter_v0.1.md b/canon/projects/railiance/project_charter_v0.1.md index 7228340..fcf8637 100644 --- a/canon/projects/railiance/project_charter_v0.1.md +++ b/canon/projects/railiance/project_charter_v0.1.md @@ -10,6 +10,8 @@ scope: domains: ["Railiance"] sensitivity: internal tags: ["devops", "reliability", "automation", "sovereignty"] +custodian_topic_id: "ca369340-a64e-442e-98f1-a4fa7dc74a38" +domain: railiance --- # Railiance — Project Charter v0.1 diff --git a/state-hub/Makefile b/state-hub/Makefile index 3ac7d45..76e2555 100644 --- a/state-hub/Makefile +++ b/state-hub/Makefile @@ -1,4 +1,4 @@ -.PHONY: install db db-tools migrate seed api dashboard check start clean +.PHONY: install db db-tools migrate seed api dashboard check start clean register-project COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env @@ -31,5 +31,11 @@ start: db $(MAKE) migrate $(MAKE) api +## Register a project: make register-project DOMAIN=railiance PROJECT_PATH=/home/worsch/railiance +register-project: + @test -n "$(DOMAIN)" || (echo "ERROR: DOMAIN is required. Usage: make register-project DOMAIN= PROJECT_PATH="; exit 1) + @test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1) + scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)" + clean: $(COMPOSE) down -v diff --git a/state-hub/mcp_server/TOOLS.md b/state-hub/mcp_server/TOOLS.md new file mode 100644 index 0000000..eca80f8 --- /dev/null +++ b/state-hub/mcp_server/TOOLS.md @@ -0,0 +1,57 @@ +# State Hub MCP — Tool Reference Card + +Quick reference for all 11 tools and 5 resources. Read this instead of `server.py`. + +## Query Tools (read-only) + +| Tool | Key Args | When to use | +|------|----------|-------------| +| `get_state_summary()` | — | **Session start.** Full snapshot: totals, blocking decisions, blocked tasks, open workstreams, last 20 events. | +| `get_topic(slug)` | `slug`: e.g. `"markitect"` | Deep-dive on one topic + its workstreams + recent events. | +| `list_blocked_tasks(workstream_id?)` | optional filter | Surface all impediments, optionally scoped to one workstream. | +| `list_pending_decisions(topic_id?)` | optional filter | Decisions holding up work, sorted by deadline. | +| `get_recent_progress(limit, since?)` | `limit` default 20; `since` ISO datetime | Reconstruct recent session history. | + +## Mutate Tools (each auto-emits a progress_event) + +| Tool | Key Args | Notes | +|------|----------|-------| +| `create_task(workstream_id, title, ...)` | `priority`: low/medium/high/critical; `assignee?`; `due_date?` | Creates task under a workstream. | +| `update_task_status(task_id, status, ...)` | `status`: todo/in_progress/blocked/done/cancelled; `blocking_reason` required when blocked | | +| `record_decision(title, ...)` | `decision_type`: made/pending; `topic_id?`; `workstream_id?`; `deadline?` | Financial/legal + pending → auto-escalated per constitution §4. At least one of topic_id/workstream_id required. | +| `resolve_decision(decision_id, rationale, decided_by)` | all required | Marks decision resolved and records who decided. | +| `add_progress_event(summary, ...)` | `event_type`: note/milestone/blocker/insight; `topic_id?`; `workstream_id?`; `task_id?`; `detail?` | Append-only log entry. **Use at session end.** | +| `update_workstream_status(workstream_id, status)` | `status`: active/blocked/completed/archived | | + +## Resources (URI-addressable, read-only) + +| URI | Returns | +|-----|---------| +| `state://summary` | Full StateSummary JSON | +| `state://topics` | Active topics list | +| `state://workstreams/{topic_slug}` | Workstreams for a topic (by slug) | +| `state://decisions/blocking` | All pending decisions | +| `state://tasks/blocked` | All blocked tasks | + +## Domain Slugs + +`custodian` · `railiance` · `markitect` · `coulomb-social` · `personhood` · `foerster-capabilities` + +## Common Patterns + +```python +# New workstream (via API — no MCP tool yet): +# POST /workstreams/ {"topic_id": "...", "slug": "...", "title": "...", "status": "active", "owner": "..."} + +# Session start ritual: +get_state_summary() + +# Session end ritual: +add_progress_event( + summary="...", + event_type="note", # or milestone / insight / blocker + topic_id="", + workstream_id="", # optional + detail={"key": "value"}, # optional structured data +) +``` diff --git a/state-hub/scripts/patch_mcp_cwd.py b/state-hub/scripts/patch_mcp_cwd.py new file mode 100644 index 0000000..8724f89 --- /dev/null +++ b/state-hub/scripts/patch_mcp_cwd.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +""" +Patch ~/.claude.json to add the cwd field to the state-hub MCP entry. + +claude mcp add-json silently drops the cwd field. Run this script after +any claude mcp add-json call to restore it. + +Usage: python3 scripts/patch_mcp_cwd.py +""" +import json +import os +from pathlib import Path + +CLAUDE_JSON = Path.home() / ".claude.json" +STATE_HUB_DIR = Path(__file__).resolve().parent.parent # state-hub/ + +def main() -> None: + if not CLAUDE_JSON.exists(): + print(f"ERROR: {CLAUDE_JSON} not found. Run 'claude mcp add-json' first.") + raise SystemExit(1) + + config = json.loads(CLAUDE_JSON.read_text()) + servers = config.setdefault("mcpServers", {}) + + if "state-hub" not in servers: + print("ERROR: 'state-hub' not found in ~/.claude.json. Run 'claude mcp add-json' first.") + raise SystemExit(1) + + entry = servers["state-hub"] + cwd_str = str(STATE_HUB_DIR) + + if entry.get("cwd") == cwd_str: + print(f"OK: cwd already set to {cwd_str}") + return + + entry["cwd"] = cwd_str + CLAUDE_JSON.write_text(json.dumps(config, indent=2) + "\n") + print(f"Patched: ~/.claude.json state-hub.cwd = {cwd_str}") + + +if __name__ == "__main__": + main() diff --git a/state-hub/scripts/project_claude_md.template b/state-hub/scripts/project_claude_md.template new file mode 100644 index 0000000..2ad0da2 --- /dev/null +++ b/state-hub/scripts/project_claude_md.template @@ -0,0 +1,32 @@ +# {PROJECT_NAME} — Claude Code Instructions + +## Custodian State Hub Integration + +This project is tracked as the **{DOMAIN}** domain in the Custodian State Hub. +Hub topic ID: `{TOPIC_ID}` + +The State Hub runs locally at http://127.0.0.1:8000. The MCP server (`state-hub`) +exposes tools for reading and writing state without touching the API directly. + +### Session Protocol + +**At the start of every session:** +1. Call `get_state_summary()` — orients you to active workstreams, blocking decisions, + and recent progress. If it fails, the API is likely offline: + ``` + cd ~/the-custodian/state-hub && make api + ``` +2. Review any `blocking_decisions` entries for this project before starting work. + +**During work:** +- Use `create_task()` / `update_task_status()` to track concrete deliverables. +- Use `record_decision()` for any decision that affects direction or dependencies. +- Use `add_progress_event()` for notable events (milestones, blockers, insights). + +**At the end of every session:** +- Call `add_progress_event()` with a summary of what was accomplished or decided. + Include `topic_id: {TOPIC_ID}` and the relevant `workstream_id`. + +### Quick Reference + +See `~/the-custodian/state-hub/mcp_server/TOOLS.md` for a compact tool reference. diff --git a/state-hub/scripts/register_project.sh b/state-hub/scripts/register_project.sh new file mode 100755 index 0000000..4c80feb --- /dev/null +++ b/state-hub/scripts/register_project.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# register_project.sh — register a new project with the Custodian State Hub +# +# Usage: scripts/register_project.sh +# domain: one of custodian|railiance|markitect|coulomb_social|personhood|foerster_capabilities +# project_path: absolute path to the project directory +# +# Example: +# scripts/register_project.sh railiance /home/worsch/railiance +# +# What it does: +# 1. Verify the API is reachable +# 2. Look up the topic ID for the domain +# 3. Check that state-hub is in ~/.claude.json; warn if missing +# 4. Write $project_path/CLAUDE.md from the template (skip if exists) +# 5. POST a progress event recording the registration + +set -euo pipefail + +DOMAIN="${1:-}" +PROJECT_PATH="${2:-}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +STATE_HUB_DIR="$(dirname "$SCRIPT_DIR")" +API_BASE="${API_BASE:-http://127.0.0.1:8000}" + +# ── Validate args ────────────────────────────────────────────────────────────── +if [[ -z "$DOMAIN" || -z "$PROJECT_PATH" ]]; then + echo "Usage: $0 " + echo " domain: custodian|railiance|markitect|coulomb_social|personhood|foerster_capabilities" + echo " project_path: absolute path to project directory" + 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")" + +# ── 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: 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') == sys.argv[1]), None) +if not match: + print('NOT_FOUND') +else: + print(match['id']) +" "$DOMAIN")" + +if [[ "$TOPIC_ID" == "NOT_FOUND" ]]; then + echo "ERROR: No active topic found for domain '$DOMAIN'." + echo " Known domains: custodian railiance markitect coulomb_social personhood foerster_capabilities" + exit 1 +fi +echo " topic_id: $TOPIC_ID" + +# ── Step 3: Check MCP registration ──────────────────────────────────────────── +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()) + servers = d.get('mcpServers', {}) + print('OK' if 'state-hub' in servers else 'NOT_REGISTERED') +")" + +if [[ "$MCP_OK" == "MISSING_FILE" ]]; then + echo "WARNING: ~/.claude.json not found. MCP server is not registered." + echo " To register:" + echo " MСPCFG=\$(cat $STATE_HUB_DIR/../.mcp.json | python3 -c \"import json,sys; print(json.dumps(json.load(sys.stdin)['mcpServers']['state-hub']))\")" + echo " claude mcp add-json -s user state-hub \"\$MCPCFG\"" + echo " python3 $SCRIPT_DIR/patch_mcp_cwd.py" +elif [[ "$MCP_OK" == "NOT_REGISTERED" ]]; then + echo "WARNING: 'state-hub' not found in ~/.claude.json." + echo " To register, see CLAUDE.md MCP Server Registration section." +else + echo " MCP OK" +fi + +# ── Step 4: Write CLAUDE.md ──────────────────────────────────────────────────── +CLAUDE_MD="$PROJECT_PATH/CLAUDE.md" +TEMPLATE="$SCRIPT_DIR/project_claude_md.template" + +if [[ -f "$CLAUDE_MD" ]]; then + echo "==> CLAUDE.md already exists at $CLAUDE_MD — skipping." +else + echo "==> Writing CLAUDE.md to $CLAUDE_MD ..." + sed \ + -e "s|{PROJECT_NAME}|$PROJECT_NAME|g" \ + -e "s|{DOMAIN}|$DOMAIN|g" \ + -e "s|{TOPIC_ID}|$TOPIC_ID|g" \ + "$TEMPLATE" > "$CLAUDE_MD" + echo " Written." +fi + +# ── Step 5: Record progress event ───────────────────────────────────────────── +echo "==> Recording registration event ..." +EVENT_JSON="$(python3 -c " +import json +payload = { + 'topic_id': '$TOPIC_ID', + 'event_type': 'milestone', + 'summary': 'Project registered with State Hub: $PROJECT_NAME ($DOMAIN)', + 'author': 'custodian', + 'detail': { + 'project_path': '$PROJECT_PATH', + 'claude_md': '$CLAUDE_MD', + 'domain': '$DOMAIN', + }, +} +print(json.dumps(payload)) +")" + +curl -sf -X POST "$API_BASE/progress/" \ + -H "Content-Type: application/json" \ + -d "$EVENT_JSON" > /dev/null + +echo " Event recorded." +echo "" +echo "Registration complete!" +echo " Project: $PROJECT_NAME" +echo " Domain: $DOMAIN" +echo " Topic ID: $TOPIC_ID" +echo " CLAUDE.md: $CLAUDE_MD" +echo "" +echo "Next: restart Claude Code for the MCP server to be available in this project."