Refresh agent instruction templates

This commit is contained in:
2026-05-18 16:55:53 +02:00
parent febad058e6
commit 6186a99bbd
4 changed files with 301 additions and 59 deletions

204
AGENTS.md
View File

@@ -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/<id>/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": "<uuid>",
"task_id": "<uuid>"
}'
```
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/<task_id>" \
-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/<task_id>" \
-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-<slug>.md`
**Archived location:** finished workplans may move to
`workplans/archived/YYMMDD-STATE-WP-NNNN-<slug>.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: "<uuid>" # 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: "<uuid>" # 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/`)

11
CLAUDE.md Normal file
View File

@@ -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

View File

@@ -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/<id>/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="<uuid>")
```
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":"<uuid>","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 <repo_path> pull --ff-only

View File

@@ -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()