diff --git a/AGENTS.md b/AGENTS.md index f3745e7..43c0374 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,74 +1,162 @@ -# AGENTS.md +# State Hub — Agent Instructions -This repository is the standalone home for the Custodian State Hub service. +## Repo Identity -## Session Start +**Purpose:** Standalone State Hub service repository extracted from the-custodian/state-hub. Owns the FastAPI API, MCP server, dashboard, migrations, consistency tooling, and operational docs. -1. Read this file and `SCOPE.md`. -2. Read `.custodian-brief.md` if present. -3. If the State Hub API is reachable, query the local hub for orientation: - - `GET http://127.0.0.1:8000/state/summary` - - `GET http://127.0.0.1:8000/messages/?to_agent=hub&unread_only=true` -4. Mark relevant inbox messages read after acting on them. -5. Check `git status --short` before editing. +**Domain:** custodian +**Repo slug:** state-hub +**Topic ID:** `cee7bedf-2b48-46ef-8601-006474f2ad7a` +**Workplan prefix:** `STATE-WP-` -If the API is not reachable, continue from local files. The repo must remain -usable offline. +--- -## Repository Boundary +## State Hub Integration -State Hub owns: +The Custodian State Hub tracks work across all domains. Interact via HTTP REST — +there is no MCP server for Codex agents. -- FastAPI app, models, schemas, routers, migrations -- MCP server and tool reference -- Observable dashboard -- consistency, registration, SBOM, token, image, and repo-sync scripts -- task-flow engine and flow definitions -- State Hub operational docs, tests, policies, prompts, and infra +| Context | URL | +|---------|-----| +| Local workstation | `http://127.0.0.1:8000` | +| Remote via tunnel | `http://127.0.0.1:18000` | -The Custodian governance repo owns: - -- canon, constitution, values, memory, and broad cross-domain governance -- bridge workplans that coordinate extraction from the old embedded layout - -Do not write governance canon directly from this repo. - -## Build And Test - -After the implementation move, the expected command surface is: +### Orient at session start ```bash -make install -make db -make migrate -make test -make api -make mcp-http -make dashboard -make dashboard-check +# Offline brief — works without hub connection +cat .custodian-brief.md + +# Active workstreams for this domain +curl -s "http://127.0.0.1:8000/workstreams/?topic_id=cee7bedf-2b48-46ef-8601-006474f2ad7a&status=active" \ + | python3 -m json.tool + +# Check inbox +curl -s "http://127.0.0.1:8000/messages/?to_agent=state-hub&unread_only=true" \ + | python3 -m json.tool ``` -When API routers, models, migrations, or consistency logic change, run the -relevant tests before closing the session. Prefer `make test` when the database -test prerequisites are available. `make test` includes an Observable dashboard -build smoke check so dashboard startup regressions are not missed. - -## Workplans - -Use `workplans/` for State Hub-local workplans. New workplans should use: - -```text -SHUB-WP-0001 +Mark a message read: +```bash +curl -s -X PATCH "http://127.0.0.1:8000/messages//read" \ + -H "Content-Type: application/json" -d '{}' ``` -For migrated Custodian-hosted plans, preserve existing `state_hub_workstream_id` -and task IDs when safe. Never call `create_workstream()` or `create_task()` -manually for a file-backed workplan before the file exists in this repo. +### Log progress (required at session close) -## Session Close +```bash +curl -s -X POST http://127.0.0.1:8000/progress/ \ + -H "Content-Type: application/json" \ + -d '{ + "summary": "what was done", + "event_type": "note", + "author": "codex", + "workstream_id": "", + "task_id": "" + }' +``` -1. Add a progress event through State Hub if the API is reachable. -2. Run consistency sync for this repo once it is registered. -3. Record any decisions that change repo ownership, state model, API contracts, - or deployment topology. -4. Leave the worktree clear or explicitly report remaining uncommitted changes. +Omit `workstream_id` / `task_id` when not applicable. + +### Update task status + +```bash +curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ + -H "Content-Type: application/json" \ + -d '{"status": "in_progress"}' +# values: todo | in_progress | done | blocked +``` + +### Flag a task for human review + +```bash +curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ + -H "Content-Type: application/json" \ + -d '{"needs_human": true, "intervention_note": "reason"}' +``` + +--- + +## Session Protocol + +**Start:** +1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe) +2. Check inbox: `GET /messages/?to_agent=state-hub&unread_only=true`; mark read +3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks +4. Check blocked tasks: `GET /tasks/?needs_human=true` + +**During work:** +- Update task statuses in workplan files as tasks progress +- Record significant decisions via `POST /decisions/` + +**Close:** +1. Update workplan file task statuses to reflect progress +2. Log: `POST /progress/` with a summary of what changed +3. Note for the custodian operator: after workplan file changes, run from + `~/state-hub`: + ```bash + make fix-consistency REPO=state-hub + ``` + This syncs task status from files into the hub DB. + +--- + +## Workplan Convention (ADR-001) + +Work items originate as files in this repo — not in the hub. The hub is a +read/cache/index layer that rebuilds from files. + +**File location:** `workplans/STATE-WP-NNNN-.md` + +**Archived location:** finished workplans may move to +`workplans/archived/YYMMDD-STATE-WP-NNNN-.md`. The `YYMMDD` prefix is +the completion/archive date; the frontmatter `id` does not change. + +**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use +`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use +this only for low-risk work completed directly; create a normal workplan for +anything needing analysis, design, approval, dependencies, or multiple phases. + +**Frontmatter:** + +```yaml +--- +id: STATE-WP-NNNN +type: workplan +title: "..." +domain: custodian +repo: state-hub +status: proposed | ready | active | blocked | backlog | finished | archived +owner: codex +topic_slug: ... +created: "YYYY-MM-DD" +updated: "YYYY-MM-DD" +state_hub_workstream_id: "" # written by fix-consistency — do not edit +--- +``` + +Use `proposed` for a new draft, `ready` after review against current repo +state, and `finished` after implementation. `stalled` and `needs_review` are +derived health labels, not frontmatter statuses. + +**Task block format** (one per `##` section): + +``` +## Task Title + +` ` `task +id: STATE-WP-NNNN-T01 +status: todo | in_progress | done | blocked +priority: high | medium | low +state_hub_task_id: "" # written by fix-consistency — do not edit +` ` ` + +Task description text. +``` + +Status progression: `todo` → `in_progress` → `done` (or `blocked`) + +To create a new workplan: +1. Write the file following the format above +2. Notify the custodian operator to run `make fix-consistency REPO=state-hub` + (or send a message to the hub agent via `POST /messages/`) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5c72245 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ +# State Hub — Claude Code Instructions + +@SCOPE.md +@.claude/rules/repo-identity.md +@.claude/rules/session-protocol.md +@.claude/rules/first-session.md +@.claude/rules/workplan-convention.md +@.claude/rules/stack-and-commands.md +@.claude/rules/architecture.md +@.claude/rules/repo-boundary.md +@.claude/rules/agents.md diff --git a/scripts/project_rules/session-protocol.template b/scripts/project_rules/session-protocol.template index a471ad7..316021f 100644 --- a/scripts/project_rules/session-protocol.template +++ b/scripts/project_rules/session-protocol.template @@ -8,19 +8,32 @@ Read the offline-safe brief first — it works without a live hub connection: ```bash cat .custodian-brief.md ``` -Then call the MCP tool for richer cross-domain context (skip if unreachable): +Then call the MCP tool for richer cross-domain context when MCP tools are exposed: ``` get_domain_summary("{DOMAIN}") ``` +If MCP tools are unavailable in the current agent session, use the REST API: +```bash +curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool +``` If the hub is offline: `cd ~/state-hub && make api` **Step 2 — Check inbox** +With MCP tools: ``` get_messages(to_agent="{REPO_SLUG}", unread_only=True) ``` Mark read with `mark_message_read(message_id)`. Reply or act on coordination requests before proceeding. +Without MCP tools: +```bash +curl -s "http://127.0.0.1:8000/messages/?to_agent={REPO_SLUG}&unread_only=true" \ + | python3 -m json.tool +curl -s -X PATCH "http://127.0.0.1:8000/messages//read" \ + -H "Content-Type: application/json" -d '{}' +``` + **Step 3 — Scan workplans** ```bash ls workplans/ @@ -46,9 +59,16 @@ If no workstreams: follow First Session Protocol (`first-session.md`). > are First Session Protocol only. Work structure belongs in repo files (ADR-001). **Session close:** +With MCP tools: ``` add_progress_event(summary="...", topic_id="{TOPIC_ID}", workstream_id="") ``` +Without MCP tools: +```bash +curl -s -X POST http://127.0.0.1:8000/progress/ \ + -H "Content-Type: application/json" \ + -d '{"topic_id":"{TOPIC_ID}","workstream_id":"","event_type":"note","summary":"what changed","author":"codex"}' +``` If workplan files were modified, ensure the local copy is up to date first: ```bash git -C pull --ff-only diff --git a/scripts/update_agent_instruction_files.py b/scripts/update_agent_instruction_files.py new file mode 100644 index 0000000..9ec1fce --- /dev/null +++ b/scripts/update_agent_instruction_files.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import json +import re +import urllib.request +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +TEMPLATE_DIR = ROOT / "scripts" / "project_rules" +API_BASE = "http://127.0.0.1:8000" + + +def fetch(path: str): + with urllib.request.urlopen(f"{API_BASE}{path}") as response: + return json.load(response) + + +def render(template: str, values: dict[str, str]) -> str: + for key, value in values.items(): + template = template.replace("{" + key + "}", value) + return template + + +def repo_topic_id(repo: dict, topics: list[dict]) -> str: + if repo.get("topic_id"): + return repo["topic_id"] + match = next((t for t in topics if t.get("domain_slug") == repo.get("domain_slug")), None) + return match["id"] if match else "(none)" + + +def wp_prefix(repo_slug: str) -> str: + first = repo_slug.split("-", 1)[0].upper() + return f"{first}-WP" + + +def brief_domain(path: Path) -> str | None: + brief = path / ".custodian-brief.md" + if not brief.exists(): + return None + match = re.search(r"^\*\*Domain:\*\*\s+(\S+)\s*$", brief.read_text(encoding="utf-8"), re.MULTILINE) + return match.group(1) if match else None + + +def choose_repos(repos: list[dict]) -> list[dict]: + by_path: dict[str, list[dict]] = {} + for repo in repos: + local_path = repo.get("local_path") or "" + path = Path(local_path) + if not local_path.startswith("/home/worsch/") or not path.exists(): + continue + by_path.setdefault(str(path), []).append(repo) + + chosen: list[dict] = [] + for local_path, candidates in sorted(by_path.items()): + path = Path(local_path) + domain = brief_domain(path) + if domain: + domain_matches = [r for r in candidates if r.get("domain_slug") == domain] + if domain_matches: + candidates = domain_matches + active = [r for r in candidates if r.get("status") == "active"] + chosen.append(active[0] if active else candidates[0]) + return chosen + + +def main() -> None: + repos = fetch("/repos/") + topics = fetch("/topics/?status=active") + + agents_template = (TEMPLATE_DIR / "agents-codex.template").read_text(encoding="utf-8") + claude_template = (TEMPLATE_DIR / "claude-md.template").read_text(encoding="utf-8") + scope_template = (TEMPLATE_DIR / "scope.template").read_text(encoding="utf-8") + rule_names = [ + "repo-identity", + "session-protocol", + "first-session", + "workplan-convention", + "stack-and-commands", + "architecture", + "repo-boundary", + "agents", + ] + rule_templates = { + name: (TEMPLATE_DIR / f"{name}.template").read_text(encoding="utf-8") + for name in rule_names + } + + updated: list[str] = [] + for repo in choose_repos(repos): + path = Path(repo["local_path"]) + repo_slug = repo["slug"] + project_name = repo.get("name") or path.name + description = repo.get("description") or f"{project_name} - (fill in purpose)" + values = { + "PROJECT_NAME": project_name, + "PROJECT_DESCRIPTION": description, + "DOMAIN": repo.get("domain_slug") or "", + "TOPIC_ID": repo_topic_id(repo, topics), + "REPO_SLUG": repo_slug, + "WP_PREFIX": wp_prefix(repo_slug), + } + + (path / "AGENTS.md").write_text(render(agents_template, values), encoding="utf-8") + (path / "CLAUDE.md").write_text(render(claude_template, values), encoding="utf-8") + scope_path = path / "SCOPE.md" + if not scope_path.exists(): + scope_path.write_text(render(scope_template, values), encoding="utf-8") + + rules_dir = path / ".claude" / "rules" + rules_dir.mkdir(parents=True, exist_ok=True) + for name, template in rule_templates.items(): + (rules_dir / f"{name}.md").write_text(render(template, values), encoding="utf-8") + + updated.append(f"{repo_slug}\t{path}") + + print(f"Updated {len(updated)} local repo(s):") + for line in updated: + print(line) + + +if __name__ == "__main__": + main()