diff --git a/CLAUDE.md b/CLAUDE.md index 3abafaf..f90c93c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,11 @@ This repo **is** the dev-team Claude Code plugin and its marketplace. Do not sca ## Layout - `agents/` + `skills/` — the six roles (orchestrator is skill-only, no agent); each agent is a thin launcher — preloads its skill via `skills:`, sets effort and memory, pins no tools or model (skills are the single source of truth for role definitions) -- `commands/` — `/dev-team:sync` seed-sync command; `/dev-team:tickets` ticket-system setup/tuning; `/dev-team:checkins` Orca check-in setup -- `bin/` — check-in trigger scripts and the coordinator lock, run by Orca automations (copied into each project's git dir by the hook); `peer`, the Orca coordinator's dispatch / tell / watch helper -- `templates/`, `workflow/` — framework reference shipped with the plugin, referenced via `${CLAUDE_PLUGIN_ROOT}`; `workflow/handoffs.md` is the canonical home of gate-validity rules, `workflow/adoption.md` of existing-project adoption rules, `workflow/orca.md` of the optional Orca runtime, `workflow/tickets.md` of ticket-system rules +- `commands/` — `/dev-team:sync` seed-sync command; `/dev-team:tickets` ticket-system setup/tuning; `/dev-team:checkins` Orca check-in setup; `/dev-team:board` Owner board setup +- `bin/` — check-in trigger scripts and the coordinator lock, run by Orca automations (copied into each project's git dir by the hook); `peer`, the Orca coordinator's dispatch / tell / watch helper; `board-drift`, the Owner board's PR check +- `templates/`, `workflow/` — framework reference shipped with the plugin, referenced via `${CLAUDE_PLUGIN_ROOT}`; `workflow/handoffs.md` is the canonical home of gate-validity rules, `workflow/adoption.md` of existing-project adoption rules, `workflow/orca.md` of the optional Orca runtime, `workflow/tickets.md` of ticket-system rules, `workflow/board.md` of the Owner board - `seeds/` — files the `bootstrap` skill copies into consuming projects (standards, stack, status, backlog index, project CLAUDE.md); project-owned after copy -- `examples/workspace/` — worked example (one feature folder plus its ADR) referenced by the skills +- `examples/workspace/` — worked example (one feature folder plus its ADR) referenced by the skills; `examples/board/` — Owner board sample data and README screenshots (made-up data only) - `hooks/` — tiny SessionStart announcement; refreshes check-in scripts - `.claude-plugin/` — `plugin.json` (no version field: every commit is an update) + `marketplace.json` diff --git a/README.md b/README.md index 9c79261..5b73bd3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,15 @@ Projects driven from Orca can opt into a second runtime ([`workflow/orca.md`](wo **Check-ins** (`/dev-team:checkins`) are scheduled Orca automations defined in the workspace's `standards/checkins.md` — e.g. keep work moving every 30 minutes, catch stalled or failed work hourly, have the product-manager groom tickets assigned to you each weekday. Each starts with a free trigger check, so an idle project costs nothing; each reuses its session so it keeps its bearings (a run still re-sends that session, so check-in sessions are kept small). Optional extras: mirror `status.md` onto Orca's worktree board (with an unread flag when you're needed), and Orca's built-in browser for UI checks in place of Playwright. +## Owner board (optional) + +With several roles running at once, the orchestrator's conversation moves too fast to follow, and the workspace documents are written for agents. The **Owner board** ([`workflow/board.md`](workflow/board.md)) is a live, private claude.ai page for you: questions and approvals waiting on you first, then who is on what, then every piece of work in flight with its PR, state, and next step. The orchestrator updates it in the same turn as `status.md`, and removes a question the moment you answer it; a token-free script (`bin/board-drift`) catches PRs that merged, failed, or went ready on their own. Works with either runtime. Set it up with `/dev-team:board`. + + + + Owner board with sample data: two questions waiting on the Owner, seven roles and their current tasks, and work grouped as Reporting, Mobile and Platform with ticket, PR, state and next step. + + ## Ticket systems (optional) Work can come from Linear, Jira, GitHub Issues and Projects, or any tracker enabled through MCP — or through Orca's built-in Linear client or the `gh` CLI, which need no MCP server — configured per project in the workspace's `standards/tickets.md` (off by default; rules in [`workflow/tickets.md`](workflow/tickets.md)). The tracker is the **source of work and the record of outcomes**; agents still work and talk through the workspace, which is cheaper than MCP round-trips. A ticket is read once at intake and its ID carried in the PRD, fix record, and status row; phase changes sync its status; and only what the config's **Elevation** allowlist names goes back out — a release comment, high-priority bugs — never findings, questions, or task notes. Only the orchestrator and product-manager write to the tracker. @@ -117,9 +126,10 @@ Validate with `claude plugin validate .` | Path | What | Updatable | |---|---|---| | `agents/`, `skills/` | The six roles (orchestrator is skill-only) + bootstrap skill | ✅ via plugin update | -| `commands/` | `/dev-team:sync` — pull missing/updated seeds into a project; `/dev-team:tickets` — ticket-system setup/tuning; `/dev-team:checkins` — Orca check-ins | ✅ via plugin update | +| `commands/` | `/dev-team:sync` — pull missing/updated seeds into a project; `/dev-team:tickets` — ticket-system setup/tuning; `/dev-team:checkins` — Orca check-ins; `/dev-team:board` — Owner board | ✅ via plugin update | | `templates/`, `workflow/` | Document templates, pipeline/handoff/communication rules | ✅ via plugin update | -| `bin/` | Check-in trigger scripts and the coordinator lock (Orca runtime) | ✅ via plugin update | +| `bin/` | Check-in trigger scripts, the coordinator lock and `peer` (Orca runtime); `board-drift` (Owner board) | ✅ via plugin update | | `hooks/` | Session-start announcement + agent-memory size sentinel | ✅ via plugin update | | `seeds/` | Copied into projects by bootstrap/sync | ❌ project-owned after copy | | `examples/workspace/` | Worked example of the full pipeline, in the workspace layout | ✅ via plugin update | +| `examples/board/` | Owner board sample data and screenshots | ✅ via plugin update | diff --git a/bin/board-drift b/bin/board-drift new file mode 100755 index 0000000..ca19f60 --- /dev/null +++ b/bin/board-drift @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""List Owner-board work rows whose state no longer matches their GitHub PR (workflow/board.md). + +Usage: board-drift [owner/repo] + holds the board's `work` rows as JSON files, saved by an ArtifactData `list` of collection `work` with +`out_dir`, so reading the board costs almost nothing. A row's own `repo` wins; otherwise the argument, otherwise +this checkout's GitHub repo. Prints one line per row to fix (exit 1), or "board in sync" (exit 0). Uses `gh`. +""" +import glob, json, os, subprocess, sys + +FAIL = ("FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ERROR", "ACTION_REQUIRED") + + +def gh(*a): + try: + return subprocess.run(["gh", *a], capture_output=True, text=True, timeout=60).stdout + except (OSError, subprocess.TimeoutExpired): + return "" + + +if len(sys.argv) < 2: + sys.exit(__doc__) +default_repo = sys.argv[2] if len(sys.argv) > 2 else gh("repo", "view", "--json", "nameWithOwner", + "-q", ".nameWithOwner").strip() +drift = [] +for f in sorted(glob.glob(os.path.join(sys.argv[1], "**", "*.json"), recursive=True)): + d = json.load(open(f)) + row, doc = d.get("data", d), os.path.basename(f)[:-5] + state, pr = row.get("state"), row.get("pr") + if not pr or state == "done": + continue + try: + p = json.loads(gh("pr", "view", str(pr), "-R", row.get("repo") or default_repo, + "--json", "state,isDraft,statusCheckRollup") or "{}") + except ValueError: + p = {} + if not p: + drift.append(f"{doc}: PR #{pr} not found — check the row") + continue + failed = any((c.get("conclusion") or c.get("state") or "").upper() in FAIL for c in p.get("statusCheckRollup") or []) + want = None + if p["state"] in ("MERGED", "CLOSED"): + want = f"done (PR {p['state'].lower()})" + elif failed and state not in ("fixing", "owner", "blocked"): + want = "fixing (a check failed" + (", draft)" if p["isDraft"] else ")") + elif p["isDraft"] and state == "ready" and not failed: + want = "building or review (the PR is a draft)" + elif not p["isDraft"] and state in ("building", "fixing", "planning", "queued") and not failed: + want = "ready or review (the PR is ready and no check failed)" + if want: + drift.append(f"{doc}: board says {state}, should be {want} [#{pr}]") +print("\n".join(drift) if drift else "board in sync") +sys.exit(1 if drift else 0) diff --git a/commands/board.md b/commands/board.md new file mode 100644 index 0000000..f86a65a --- /dev/null +++ b/commands/board.md @@ -0,0 +1,30 @@ +--- +description: Set up or refresh the Owner board — a live claude.ai page of what needs the Owner, who is on what, and work in flight. +argument-hint: "[change, e.g. 'link tickets to Jira']" +--- + +`` below is the workspace root: `source "${CLAUDE_PLUGIN_ROOT}/bin/_workspace.sh" . && echo $WS`. + +Set up the project's Owner board. Rules: `${CLAUDE_PLUGIN_ROOT}/workflow/board.md`. + +Request: $ARGUMENTS + +1. **Preconditions.** This session has the `Artifact` and `ArtifactData` tools (claude.ai artifacts). Otherwise stop and say so; under the Orca runtime, the worktree-card board (`workflow/orca.md` § Board) is the lighter fallback. + +2. **New or refresh.** The dev-team section (`CLAUDE.md`, or `CLAUDE.local.md` in guest mode) has an `**Owner board:** ` line → refresh: read that artifact (`Artifact` `read`) and republish the current template to the same URL in step 4, omitting `capabilities` so its data and rules carry over. Otherwise create a new board. + +3. **Config** — ask once, offering defaults, and keep current values on a refresh unless the request changes them: + - **Project name** for the title (default: the repository's folder name). + - **How the board names the Owner** in "Waiting on …" (default `you`). + - **Ticket link base**, with the ticket ID appended: from `/standards/tickets.md` § Scope when a system is configured — Linear `https://linear.app//issue/`, GitHub `https://github.com//issues/`, Jira `https:///browse/` — else none (IDs show as plain text). + - **Repository** for PR links: `gh repo view --json nameWithOwner -q .nameWithOwner`. + +4. **Publish.** Copy `${CLAUDE_PLUGIN_ROOT}/templates/board.html` to the scratchpad, replace both `{{PROJECT}}` with the project name, and publish it with the `Artifact` tool: icon `board`, a one-line description, and on a new board `capabilities: {"db": {"rules": [{"path": "", "read": "view", "write": "owner"}]}, "user": {}}` — viewers read, only the Owner (and the coordinator writing as them) changes data. + +5. **Seed** with one `ArtifactData` `batch`: `meta/config` (`owner`, `ticketUrl`, `repo` — omit empty ones), `meta/board.updated`, one `work` row per `/status.md` row, one `asks` row per pending ✋ gate or feature blocked on the Owner, and `peers` rows (`peer list` under Orca). A new board with nothing in flight starts empty. + +6. **Verify.** `ArtifactData` `list` each collection and confirm the rows match `/status.md`. + +7. **Record.** New board: add `- **Owner board:** — the orchestrator keeps it current (dev-team workflow/board.md).` to the dev-team section. Show the diff; write on approval. + +8. Give the Owner the link and offer to pin it (`Artifact` `pin`) — it is a page they will keep reopening. diff --git a/examples/board/board-dark.png b/examples/board/board-dark.png new file mode 100644 index 0000000..456e17d Binary files /dev/null and b/examples/board/board-dark.png differ diff --git a/examples/board/board-light.png b/examples/board/board-light.png new file mode 100644 index 0000000..051bcb8 Binary files /dev/null and b/examples/board/board-light.png differ diff --git a/examples/board/sample.json b/examples/board/sample.json new file mode 100644 index 0000000..b738ee0 --- /dev/null +++ b/examples/board/sample.json @@ -0,0 +1,27 @@ +{ + "meta/config": { "ticketUrl": "https://example.com/tickets/", "repo": "acme/app" }, + "asks": [ + { "id": "a1", "order": 1, "ticket": "APP-214", "question": "Approve the saved-searches PRD? It adds email alerts, which the ticket did not ask for." }, + { "id": "a2", "order": 2, "ticket": "APP-231", "question": "May dt-dev-2 record 10 seconds of real microphone audio in the emulator? Fake audio passes; real audio is the open question." } + ], + "peers": [ + { "id": "dt-product", "order": 1, "name": "dt-product", "role": "product-manager", "doing": "APP-214 saved searches: PRD waiting on you" }, + { "id": "dt-designer", "order": 2, "name": "dt-designer", "role": "designer", "doing": "APP-219 onboarding checklist: mockups" }, + { "id": "dt-architect", "order": 3, "name": "dt-architect", "role": "principal-engineer", "doing": "Idle" }, + { "id": "dt-dev-1", "order": 4, "name": "dt-dev-1", "role": "senior-developer", "doing": "APP-207 CSV export: T03 of 4" }, + { "id": "dt-dev-2", "order": 5, "name": "dt-dev-2", "role": "senior-developer", "doing": "APP-231 voice notes: waiting on the mic go" }, + { "id": "dt-dev-3", "order": 6, "name": "dt-dev-3", "role": "senior-developer", "doing": "APP-226 fixing review findings on #412" }, + { "id": "dt-reviewer", "order": 7, "name": "dt-reviewer", "role": "reviewer", "doing": "Reviewing #418, APP-228 rate limits" } + ], + "work": [ + { "id": "APP-207", "group": "Reporting", "group_order": 1, "ticket": "APP-207", "title": "CSV export for invoices", "peer": "dt-dev-1", "pr": 409, "state": "building", "next": "T03 streaming writer, then T04" }, + { "id": "APP-214", "group": "Reporting", "group_order": 1, "ticket": "APP-214", "title": "Saved searches", "peer": "dt-product", "state": "owner", "next": "PRD approval (Needs you)" }, + { "id": "APP-219", "group": "Reporting", "group_order": 1, "ticket": "APP-219", "title": "Onboarding checklist", "peer": "dt-designer", "state": "planning", "next": "Mockups to the PM for approval" }, + { "id": "APP-231", "group": "Mobile", "group_order": 2, "ticket": "APP-231", "title": "Voice notes on Android", "peer": "dt-dev-2", "pr": 415, "state": "owner", "next": "Recording go (Needs you)" }, + { "id": "APP-226", "group": "Mobile", "group_order": 2, "ticket": "APP-226", "title": "Offline drafts", "peer": "dt-dev-3", "pr": 412, "state": "fixing", "next": "Two review findings, then a delta review" }, + { "id": "APP-222", "group": "Mobile", "group_order": 2, "ticket": "APP-222", "title": "Notification settings", "pr": 405, "state": "ready", "next": "Reviewed and green: merge when you like" }, + { "id": "APP-228", "group": "Platform", "group_order": 3, "ticket": "APP-228", "title": "Rate limits per API key", "peer": "dt-reviewer", "pr": 418, "state": "review", "next": "Security checks first, then limits" }, + { "id": "APP-233", "group": "Platform", "group_order": 3, "ticket": "APP-233", "title": "Audit log retention", "state": "queued", "next": "Starts after APP-228" }, + { "id": "APP-224", "group": "Platform", "group_order": 3, "ticket": "APP-224", "title": "Upgrade Node to 22", "pr": 401, "state": "done", "next": "Merged" } + ] +} diff --git a/skills/bootstrap/SKILL.md b/skills/bootstrap/SKILL.md index 5aa7584..5949231 100644 --- a/skills/bootstrap/SKILL.md +++ b/skills/bootstrap/SKILL.md @@ -50,6 +50,7 @@ git log --oneline -1 2>/dev/null; ls docs CLAUDE.md CLAUDE.local.md 2>/dev/null - **Autonomy:** `supervised`, `standard` (default), or `autonomous`, plus any gates to keep (`${CLAUDE_PLUGIN_ROOT}/workflow/handoffs.md` § Autonomy). - **Ticket system:** Linear, Jira, GitHub, or another tracker? Yes → run `${CLAUDE_PLUGIN_ROOT}/commands/tickets.md`. No → `/dev-team:tickets` enables it later. - **Orca:** does the Owner run Claude from Orca? Yes → add the Runtime line (`${CLAUDE_PLUGIN_ROOT}/workflow/orca.md` § Opt in) and offer `/dev-team:checkins`. + - **Owner board:** a live page of what needs the Owner, who is on what, and work in flight — recommended when several roles run at once. Yes → run `${CLAUDE_PLUGIN_ROOT}/commands/board.md`. No → `/dev-team:board` sets it up later. - **Design tool:** Figma, Penpot, or another tool with an MCP server? Yes → fill `/standards/design.md` § Design tool. No → mockups are HTML in each feature's `mockups/`. 6. **Existing projects — harvest, then draw the line** (`workflow/adoption.md`): diff --git a/skills/orchestrator/SKILL.md b/skills/orchestrator/SKILL.md index 131f4b8..946e9e2 100644 --- a/skills/orchestrator/SKILL.md +++ b/skills/orchestrator/SKILL.md @@ -11,7 +11,7 @@ Paths: `` is the workspace root, `` the current feature's folder Traffic control: decide who acts next and where the work runs, verify prerequisites, keep `/status.md` accurate. Coordination only — this role owns no phase artifact. Runs in the main conversation; it is a set of rules, not an agent. ## Boundaries -- **Owns** `/status.md` and `/status-done.md` — one row per feature in flight: phase, current role, blockers, approvals, path. The only files this role writes; with a ticket system enabled, it also makes the intake, status-sync, and release-comment writes `/standards/tickets.md` allows (`${CLAUDE_PLUGIN_ROOT}/workflow/tickets.md`). +- **Owns** `/status.md` and `/status-done.md` — one row per feature in flight: phase, current role, blockers, approvals, path. The only files this role writes; with a ticket system enabled, it also makes the intake, status-sync, and release-comment writes `/standards/tickets.md` allows (`${CLAUDE_PLUGIN_ROOT}/workflow/tickets.md`). With an **Owner board** line in the project, it keeps the board current in the same turn as `/status.md` (`${CLAUDE_PLUGIN_ROOT}/workflow/board.md`). - **Never:** write code or phase artifacts, answer another role's domain questions, approve anything on the Project Owner's behalf (sole exception: the release an *autonomous* level delegates to it), route past a missing gate. - **Escalate:** missing approval → the gate's approver (Owner or delegate); question open past its Required By date → Project Owner; ambiguous pipeline state → Project Owner, never guess. diff --git a/templates/board.html b/templates/board.html new file mode 100644 index 0000000..6debec6 --- /dev/null +++ b/templates/board.html @@ -0,0 +1,199 @@ +{{PROJECT}} Board + + + + +
+
+

{{PROJECT}} Board

+ Connecting… +
+ +
+

Needs you

+

Questions for you appear here.

+
+ +
+

Who's on what

+

Each role and its current task appear here.

+
+ +
+

Work

+

Work in flight appears here once the coordinator posts it.

+
+ +

Updated by the coordinator as work moves.

+
+ + diff --git a/tests/board-preview.py b/tests/board-preview.py new file mode 100755 index 0000000..9ef0f77 --- /dev/null +++ b/tests/board-preview.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Render templates/board.html with sample data, outside claude.ai: a stand-in for claude.use("db") serves it. + +Usage: tests/board-preview.py [light|dark] [sample.json] +Used by tests/board.sh and for the README screenshots (examples/board/). +""" +import json, sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +out, theme = sys.argv[1], (sys.argv[2] if len(sys.argv) > 2 else "light") +sample = json.loads(Path(sys.argv[3] if len(sys.argv) > 3 else ROOT / "examples/board/sample.json").read_text()) +page = (ROOT / "templates/board.html").read_text().replace("{{PROJECT}}", "Acme") + +stub = """""" % json.dumps(sample) + +Path(out).write_text(f'' + f'' + f"{stub}{page}") diff --git a/tests/board.sh b/tests/board.sh new file mode 100755 index 0000000..7908537 --- /dev/null +++ b/tests/board.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# Tests for the Owner board: bin/board-drift against a fake gh, and templates/board.html rendered with +# examples/board/sample.json in headless Chrome (skipped when no Chrome is found). Run: tests/board.sh +cd "$(dirname "$0")/.." || exit 1 +REPO=$PWD +T=$(mktemp -d); trap 'rm -rf "$T"' EXIT +fail=0 +ok() { if eval "$2"; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; } + +# --- board-drift -------------------------------------------------------------------------------------------------- +mkdir -p "$T/bin" "$T/rows/sub" +cat > "$T/bin/gh" <<'EOF' +#!/usr/bin/env python3 +import json, os, sys +a = sys.argv[1:] +open(os.environ["FAKE_GH_LOG"], "a").write(json.dumps(a) + "\n") +if a[:2] == ["repo", "view"]: + print("acme/app"); sys.exit(0) +prs = json.load(open(os.environ["FAKE_GH_PRS"])) +print(json.dumps(prs.get(a[a.index("-R") + 1] + "#" + a[2], {}))) +EOF +chmod +x "$T/bin/gh" +export PATH="$T/bin:$PATH" FAKE_GH_LOG="$T/gh.log" FAKE_GH_PRS="$T/prs.json" +cat > "$T/prs.json" <<'EOF' +{"acme/app#1": {"state": "MERGED", "isDraft": false, "statusCheckRollup": []}, + "acme/app#2": {"state": "OPEN", "isDraft": false, "statusCheckRollup": [{"conclusion": "FAILURE"}]}, + "acme/app#3": {"state": "OPEN", "isDraft": true, "statusCheckRollup": [{"conclusion": "SUCCESS"}]}, + "acme/app#4": {"state": "OPEN", "isDraft": false, "statusCheckRollup": [{"state": "PENDING"}]}, + "acme/app#5": {"state": "OPEN", "isDraft": false, "statusCheckRollup": []}, + "acme/app#6": {"state": "OPEN", "isDraft": true, "statusCheckRollup": [{"conclusion": "FAILURE"}]}, + "acme/mobile#7": {"state": "CLOSED", "isDraft": false, "statusCheckRollup": []}} +EOF +row() { printf '%s' "$2" > "$T/rows/$1.json"; } +row merged '{"data": {"pr": 1, "state": "building"}}' +row failed '{"data": {"pr": 2, "state": "review"}}' +row draft '{"data": {"pr": 3, "state": "ready"}}' +row ready '{"pr": 4, "state": "building"}' +row reviewing '{"data": {"pr": 5, "state": "review"}}' +row waiting '{"data": {"pr": 6, "state": "owner"}}' +row done '{"data": {"pr": 1, "state": "done"}}' +row nopr '{"data": {"state": "planning"}}' +row missing '{"data": {"pr": 8, "state": "review"}}' +row planned '{"data": {"pr": 4, "state": "planning"}}' +row queued '{"data": {"pr": 5, "state": "queued"}}' +printf '%s' '{"data": {"pr": 7, "repo": "acme/mobile", "state": "review"}}' > "$T/rows/sub/other.json" + +OUT=$("$REPO/bin/board-drift" "$T/rows"); RC=$? +ok "drift exits 1" '[ $RC = 1 ]' +ok "merged PR → done" '[[ $OUT == *"merged: board says building, should be done (PR merged)"* ]]' +ok "failed check → fixing" '[[ $OUT == *"failed: board says review, should be fixing (a check failed)"* ]]' +ok "draft PR is not ready" '[[ $OUT == *"draft: board says ready, should be building or review"* ]]' +ok "ready PR is not building" '[[ $OUT == *"ready: board says building, should be ready or review"* ]]' +ok "row repo wins, nested files read" '[[ $OUT == *"other: board says review, should be done (PR closed)"* ]] && grep -q "acme/mobile" "$T/gh.log"' +ok "missing PR is reported" '[[ $OUT == *"missing: PR #8 not found"* ]]' +ok "ready PR is not planning" '[[ $OUT == *"planned: board says planning, should be ready or review"* ]]' +ok "ready PR is not queued" '[[ $OUT == *"queued: board says queued, should be ready or review"* ]]' +ok "in-sync rows stay quiet" '[[ $OUT != *reviewing* && $OUT != *waiting* && $OUT != *nopr* ]]' +ok "done rows are not looked up" '[ "$(grep -c "\"1\"" "$T/gh.log")" = 1 ]' +mkdir -p "$T/sync"; printf '%s' '{"data": {"pr": 5, "state": "review"}}' > "$T/sync/a.json" +OUT=$("$REPO/bin/board-drift" "$T/sync"); RC=$? +ok "in sync exits 0" '[ $RC = 0 ] && [ "$OUT" = "board in sync" ]' + +# --- template render ---------------------------------------------------------------------------------------------- +CH=${CHROME:-} +for c in google-chrome google-chrome-stable chromium chromium-browser \ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \ + "$HOME"/Library/Caches/ms-playwright/chromium-*/chrome-mac*/"Google Chrome for Testing.app"/Contents/MacOS/"Google Chrome for Testing"; do + [ -n "$CH" ] && break + command -v "$c" >/dev/null 2>&1 && CH=$c +done +if [ -z "$CH" ]; then + echo "skip template render: no Chrome found (set CHROME=)" +else + dom() { python3 tests/board-preview.py "$T/p.html" light "$1" && "$CH" --headless --disable-gpu --no-sandbox \ + --virtual-time-budget=4000 --dump-dom "file://$T/p.html" 2>/dev/null; } + DOM=$(dom examples/board/sample.json) + count() { printf '%s' "$DOM" | grep -o "$1" | wc -l | tr -d ' '; } + ok "render: 2 asks" '[ "$(count "class=\"ask\"")" = 2 ]' + ok "render: 7 peers" '[ "$(count "class=\"peer[\" ]")" = 7 ]' + ok "render: 3 groups, open counts" '[ "$(count "class=\"group\"")" = 3 ] && [[ $DOM == *"2 open · 3 total"* ]]' + ok "render: owner state defaults to you" '[ "$(count ">Waiting on you<")" = 2 ]' + ok "render: ticket links from config" '[[ $DOM == *"href=\"https://example.com/tickets/APP-207\""* ]]' + ok "render: PR links from config repo" '[[ $DOM == *"href=\"https://github.com/acme/app/pull/409\""* ]]' + ok "render: updated stamp" '[[ $DOM == *"Updated 3 min ago"* ]]' + python3 -c 'import json,sys;s=json.load(open(sys.argv[1]));s["meta/config"]={"owner":"Sam"};s["work"][0]["repo"]="acme/mobile";json.dump(s,open(sys.argv[2],"w"))' \ + examples/board/sample.json "$T/s2.json" + DOM=$(dom "$T/s2.json") + ok "render: owner name from config" '[[ $DOM == *">Waiting on Sam<"* ]]' + ok "render: no ticket URL, plain IDs" '[[ $DOM != *"example.com/tickets"* ]] && [[ $DOM == *">APP-207<"* ]]' + ok "render: no repo, row repo still links" '[[ $DOM == *"https://github.com/acme/mobile/pull/409"* ]] && [[ $DOM != *"acme/app/pull"* ]]' +fi +exit $fail diff --git a/workflow/board.md b/workflow/board.md new file mode 100644 index 0000000..57be69e --- /dev/null +++ b/workflow/board.md @@ -0,0 +1,28 @@ +# Owner Board (optional) + +A live page for the Project Owner: what needs them, who is on what, and every piece of work in flight. Once several roles run at once, the coordinator's conversation moves too fast to follow and the workspace documents are written for agents, not people — the board is the Owner's view. `/status.md` stays the source of truth: the board is derived from it, and no role reads the board for state. + +Works under either runtime. It needs Claude Code's claude.ai artifacts (the `Artifact` and `ArtifactData` tools). Set up with `/dev-team:board`; it is on when the project's dev-team section has an `**Owner board:** ` line. This file is the canonical home of the rules below. + +## Data + +A private claude.ai artifact with its own database. The page (`${CLAUDE_PLUGIN_ROOT}/templates/board.html`) only reads; the orchestrator writes, with `ArtifactData`. + +| Path | One document per | Fields | +|---|---|---| +| `asks/` | question or approval waiting on the Owner | `ticket`, `question` — one plain sentence: the decision needed and why — `order` | +| `peers/` | role session: an Orca home, or under the default runtime the role now running | `name`, `role`, `doing` (current task, or `Idle`), `order` | +| `work/` | feature or ticket in flight; id = feature folder or ticket ID | `ticket`, `title`, `group`, `group_order`, `peer`, `pr`, `repo` (only when not the config's), `state`, `next` | +| `meta/board` | — | `updated` (ISO time of the last write) | +| `meta/config` | — | `owner` (how the board names the Owner; default "you"), `ticketUrl` (the ticket ID is appended), `repo` (`owner/name`) | + +`state` is one of: `planning` (Requirements through Technical Design), `building`, `review`, `fixing` (review findings or a failed check), `ready` (reviewed, checks green), `owner` (waiting on the Owner — it also has an `asks` row), `blocked`, `queued`, `done`. + +## Rules + +- **Same turn as `/status.md`.** Every status change, dispatch, settlement, and question routed to the Owner updates the board in the same turn — one `ArtifactData` `batch`, including `meta/board.updated`. The Owner trusts the board; a lagging one is worse than none. +- **Asks come and go at once.** A question or approval for the Owner becomes an `asks` row the moment it is routed to them, and its work row goes to `owner`. The row is deleted in the turn the answer arrives. +- **Done rows** stay `done` until the Owner has seen them, then are deleted — at the latest the next day. +- **Read cheaply, fix drift.** Read rows with an `ArtifactData` `list` using `out_dir` (files, not context). `${CLAUDE_PLUGIN_ROOT}/bin/board-drift ` prints the work rows whose PR moved on its own — merged, a failed check, marked ready — fix only those. Run it whenever the coordinator wakes (under Orca, after every `peer watch`) and before answering the Owner on status. +- **Only the orchestrator writes.** Roles report through their normal channels; peers never touch the board. +- **Pre-authorized.** Turning the board on approves these writes; they are not floor actions (`workflow/handoffs.md` § Autonomy). The board stays private unless the Owner shares it, and nothing secret goes on it. diff --git a/workflow/handoffs.md b/workflow/handoffs.md index d2be2a5..1aa77cd 100644 --- a/workflow/handoffs.md +++ b/workflow/handoffs.md @@ -46,7 +46,7 @@ How involved the Project Owner is, set per project by one line in the project `C | Requirement question | ✋ Owner (PM drafts the answer) | ✅ PM as proxy | ✅ PM as proxy | | Release (feature → Done) | ✋ Owner | ✋ Owner | ✅ Orchestrator, on *Approve* or *Approve with follow-ups* (follow-ups filed as backlog items) | -- **The floor stays with the Owner at every level**: anything irreversible or destructive (including data-model changes affecting existing data), security-sensitive surfaces (auth, permissions, secrets, PII), anything that spends money, and any action leaving the repository (deploy, publish, external messages). Release means *Done in `/status.md`*, never a deploy. Ticket-system writes that `/standards/tickets.md` allows are pre-authorized by that config (`workflow/tickets.md`). +- **The floor stays with the Owner at every level**: anything irreversible or destructive (including data-model changes affecting existing data), security-sensitive surfaces (auth, permissions, secrets, PII), anything that spends money, and any action leaving the repository (deploy, publish, external messages). Release means *Done in `/status.md`*, never a deploy. Ticket-system writes that `/standards/tickets.md` allows are pre-authorized by that config (`workflow/tickets.md`), and Owner-board writes by turning the board on (`workflow/board.md`). - **Overrides** follow the level: `Autonomy: autonomous; Owner keeps: PRD, release` holds named gates at ✋; `Owner waives: floor` delegates the floor too and must be written out explicitly. - **Record who decided**: an approval made because of the level reads `Approved by: (autonomy: )` in the artifact and `/status.md`, so the Owner can audit everything decided without them. - **Surface what was decided without the Owner.** When a feature reaches Done at any level below *supervised*, the Orchestrator's report lists, in a few lines, every decision the Owner did not sign for that feature: brief or PRD Assumptions, major decisions made by a role, and gates the level delegated. The audit trail is useless if nobody reads it. diff --git a/workflow/orca.md b/workflow/orca.md index 728ab14..c9e86d0 100644 --- a/workflow/orca.md +++ b/workflow/orca.md @@ -26,7 +26,7 @@ and deny the subagent tool in `.claude/settings.json` (`"permissions": { "deny": 1. **Dispatch** — `peer dispatch [--keep | --restart] [--worktree [--name --base-branch ]]`. It prepends the role line and the pointer to § Peers, launches a new home's peer with the role's model and effort (`workflow/overview.md` § Models per role), and records the home. The spec **points, never pastes**: the artifact or task file to work from (`/tasks/T03-….md`), its acceptance source, and the role's write boundary. The peer reads the docs itself. Independent tasks go to different homes at once; add a `dt-dev-N` home when every developer home is busy. 2. **Tell, never just send.** An idle peer does not read its mailbox, so a bare `orca orchestration send` can sit unread while the peer waits on you. `peer tell "" ""` sends to the peer's dispatch and types a nudge into its terminal until a turn starts; to answer an `ask`, add `--reply `. When it reports the nudge unconfirmed, look at the terminal. A peer whose dispatch has settled gets follow-up work as a dispatch (`--keep`), not a message. 3. **Wait** — `peer watch [minutes]`, run in the background: it costs no tokens while it waits. It returns on any peer message except heartbeats — peers do not reliably pick the right message type, and a go asked in a `status` must wake you as surely as `worker_done` — when a peer passes the size limit (`SIZE`), when a working peer's terminal has been quiet for 20 minutes (`STALL`), or at the time limit. -4. **Handle the whole batch, then acknowledge.** A request for a go is answered first, in the same turn. `worker_done` is the handoff signal; the artifact is the handoff: verify the gate per `workflow/handoffs.md` (headers only), update `/status.md`, and choose the home's next dispatch (below). Once every message has its next step recorded, `orca orchestration check --run --ack `. **The unacknowledged batch is the list of unhandled reports**: `peer watch` returns it at once until it is acknowledged, and `peer pending` shows it — run it before any status answer to the Owner. +4. **Handle the whole batch, then acknowledge.** A request for a go is answered first, in the same turn. `worker_done` is the handoff signal; the artifact is the handoff: verify the gate per `workflow/handoffs.md` (headers only), update `/status.md` (and the Owner board, when on), and choose the home's next dispatch (below). Once every message has its next step recorded, `orca orchestration check --run --ack `. **The unacknowledged batch is the list of unhandled reports**: `peer watch` returns it at once until it is acknowledged, and `peer pending` shows it — run it before any status answer to the Owner. 5. **Questions** stay question docs (`workflow/communication.md`). A blocked peer writes the doc, then uses its preamble's `ask` naming the path; the coordinator dispatches it to the owning role's home and answers with `peer tell --reply` once it is `Answered`. Questions for the Owner are answered in the coordinator session. ## Peers @@ -73,7 +73,7 @@ Scheduled Orca automations that keep work moving without the Owner prompting: de ## Board (optional) -Add `Board: on` to the project's Runtime line to mirror `/status.md` onto Orca's worktree board — `/status.md` stays the truth. At each phase change the coordinator runs `orca worktree set --worktree --workspace-status --comment " · · "`, with status `todo` (Requirements), `in-progress` (UX Design, Technical Design, Implementation), `in-review` (Review), `completed` (Done). When the Owner is needed — an approval, an Owner question, a session not responding — it adds `--unread`. Features sharing a worktree share its card; the comment names each. +The Owner board (`workflow/board.md`) is the fuller view and works with either runtime; this is the lighter, Orca-native one, and the two can run together. Add `Board: on` to the project's Runtime line to mirror `/status.md` onto Orca's worktree board — `/status.md` stays the truth. At each phase change the coordinator runs `orca worktree set --worktree --workspace-status --comment " · · "`, with status `todo` (Requirements), `in-progress` (UX Design, Technical Design, Implementation), `in-review` (Review), `completed` (Done). When the Owner is needed — an approval, an Owner question, a session not responding — it adds `--unread`. Features sharing a worktree share its card; the comment names each. ## Browser