generated from coulomb/repo-seed
Implement registration UX wishlist W1–W6 (260224)
W1: Document user-scope MCP config location in ~/.claude/CLAUDE.md —
adds verification and re-registration commands, warns against
settings.json (saves ~12K tokens per registration session).
W2: scripts/register_project.sh + make register-project —
5-step automation: API health → topic lookup → MCP check →
CLAUDE.md from template → progress event.
W3: state-hub/scripts/project_claude_md.template —
parameterised CLAUDE.md with {PROJECT_NAME}/{DOMAIN}/{TOPIC_ID}
placeholders; used by register_project.sh.
W4: Add custodian_topic_id + domain to all 6 canon project charters —
lets agents grep for topic IDs without touching the API.
W5: state-hub/mcp_server/TOOLS.md — compact 30-line tool reference
card; replaces reading the full server.py (~350 lines).
W6: Switch .mcp.json to absolute path + PYTHONPATH env so cwd is not
required; add scripts/patch_mcp_cwd.py for post-registration fix.
Update ~/.claude.json to match (cwd kept for belt-and-suspenders).
W7 (SessionStart hook) deferred: no SessionStart hook type in Claude
Code; PreToolUse with empty matcher fires before every tool call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
8
Makefile
8
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
|
COMPOSE = docker compose -f infra/docker-compose.yml --env-file .env
|
||||||
|
|
||||||
@@ -31,5 +31,11 @@ start: db
|
|||||||
$(MAKE) migrate
|
$(MAKE) migrate
|
||||||
$(MAKE) api
|
$(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=<domain> PROJECT_PATH=<path>"; exit 1)
|
||||||
|
@test -n "$(PROJECT_PATH)" || (echo "ERROR: PROJECT_PATH is required."; exit 1)
|
||||||
|
scripts/register_project.sh "$(DOMAIN)" "$(PROJECT_PATH)"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(COMPOSE) down -v
|
$(COMPOSE) down -v
|
||||||
|
|||||||
57
mcp_server/TOOLS.md
Normal file
57
mcp_server/TOOLS.md
Normal file
@@ -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="<uuid>",
|
||||||
|
workstream_id="<uuid>", # optional
|
||||||
|
detail={"key": "value"}, # optional structured data
|
||||||
|
)
|
||||||
|
```
|
||||||
42
scripts/patch_mcp_cwd.py
Normal file
42
scripts/patch_mcp_cwd.py
Normal file
@@ -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()
|
||||||
32
scripts/project_claude_md.template
Normal file
32
scripts/project_claude_md.template
Normal file
@@ -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.
|
||||||
145
scripts/register_project.sh
Executable file
145
scripts/register_project.sh
Executable file
@@ -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> <project_path>
|
||||||
|
# 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 <domain> <project_path>"
|
||||||
|
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."
|
||||||
Reference in New Issue
Block a user