opencode-agora 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. opencode_agora-0.1.0/LICENSE +21 -0
  2. opencode_agora-0.1.0/MANIFEST.in +7 -0
  3. opencode_agora-0.1.0/PKG-INFO +195 -0
  4. opencode_agora-0.1.0/README.md +185 -0
  5. opencode_agora-0.1.0/agora +244 -0
  6. opencode_agora-0.1.0/agora-plugin/cred.ts +112 -0
  7. opencode_agora-0.1.0/agora-plugin/envPath.ts +33 -0
  8. opencode_agora-0.1.0/agora-plugin/hook.ts +132 -0
  9. opencode_agora-0.1.0/agora-plugin/index.ts +160 -0
  10. opencode_agora-0.1.0/agora-plugin/inject.ts +49 -0
  11. opencode_agora-0.1.0/agora-plugin/lift.ts +161 -0
  12. opencode_agora-0.1.0/agora-plugin/scope.ts +36 -0
  13. opencode_agora-0.1.0/agora_mcp.py +240 -0
  14. opencode_agora-0.1.0/agorad/__init__.py +1 -0
  15. opencode_agora-0.1.0/agorad/access/__init__.py +1 -0
  16. opencode_agora-0.1.0/agorad/access/anchors.py +22 -0
  17. opencode_agora-0.1.0/agorad/access/gitx.py +206 -0
  18. opencode_agora-0.1.0/agorad/access/oc.py +124 -0
  19. opencode_agora-0.1.0/agorad/access/pages.py +25 -0
  20. opencode_agora-0.1.0/agorad/access/store.py +181 -0
  21. opencode_agora-0.1.0/agorad/core/__init__.py +1 -0
  22. opencode_agora-0.1.0/agorad/core/app.py +137 -0
  23. opencode_agora-0.1.0/agorad/core/auth.py +134 -0
  24. opencode_agora-0.1.0/agorad/core/gate.py +281 -0
  25. opencode_agora-0.1.0/agorad/core/health.py +182 -0
  26. opencode_agora-0.1.0/agorad/core/msg.py +92 -0
  27. opencode_agora-0.1.0/agorad/core/peer.py +90 -0
  28. opencode_agora-0.1.0/agorad/core/recover.py +83 -0
  29. opencode_agora-0.1.0/agorad/core/report.py +189 -0
  30. opencode_agora-0.1.0/agorad/core/state.py +183 -0
  31. opencode_agora-0.1.0/agorad/core/task.py +308 -0
  32. opencode_agora-0.1.0/agorad/installer.py +44 -0
  33. opencode_agora-0.1.0/agorad/main.py +181 -0
  34. opencode_agora-0.1.0/agorad/transport/__init__.py +1 -0
  35. opencode_agora-0.1.0/agorad/transport/httpd.py +316 -0
  36. opencode_agora-0.1.0/board_tui.py +649 -0
  37. opencode_agora-0.1.0/install.sh +222 -0
  38. opencode_agora-0.1.0/opencode_agora.egg-info/PKG-INFO +195 -0
  39. opencode_agora-0.1.0/opencode_agora.egg-info/SOURCES.txt +64 -0
  40. opencode_agora-0.1.0/opencode_agora.egg-info/dependency_links.txt +1 -0
  41. opencode_agora-0.1.0/opencode_agora.egg-info/entry_points.txt +5 -0
  42. opencode_agora-0.1.0/opencode_agora.egg-info/top_level.txt +3 -0
  43. opencode_agora-0.1.0/pyproject.toml +41 -0
  44. opencode_agora-0.1.0/setup.cfg +4 -0
  45. opencode_agora-0.1.0/skills/agora-dispatch/SKILL.md +95 -0
  46. opencode_agora-0.1.0/skills/agora-ops/SKILL.md +60 -0
  47. opencode_agora-0.1.0/tests/test_access_gitx.py +343 -0
  48. opencode_agora-0.1.0/tests/test_access_oc.py +69 -0
  49. opencode_agora-0.1.0/tests/test_access_pages.py +40 -0
  50. opencode_agora-0.1.0/tests/test_access_store.py +140 -0
  51. opencode_agora-0.1.0/tests/test_agora_wait.py +567 -0
  52. opencode_agora-0.1.0/tests/test_architecture.py +73 -0
  53. opencode_agora-0.1.0/tests/test_board_tui.py +757 -0
  54. opencode_agora-0.1.0/tests/test_core_auth.py +125 -0
  55. opencode_agora-0.1.0/tests/test_core_gate.py +684 -0
  56. opencode_agora-0.1.0/tests/test_core_health.py +550 -0
  57. opencode_agora-0.1.0/tests/test_core_msg.py +271 -0
  58. opencode_agora-0.1.0/tests/test_core_recover.py +194 -0
  59. opencode_agora-0.1.0/tests/test_core_report.py +208 -0
  60. opencode_agora-0.1.0/tests/test_core_state.py +173 -0
  61. opencode_agora-0.1.0/tests/test_core_task.py +543 -0
  62. opencode_agora-0.1.0/tests/test_main.py +109 -0
  63. opencode_agora-0.1.0/tests/test_mcp_shell.py +270 -0
  64. opencode_agora-0.1.0/tests/test_packaging.py +68 -0
  65. opencode_agora-0.1.0/tests/test_pointer.py +110 -0
  66. opencode_agora-0.1.0/tests/test_transport_httpd.py +970 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 guangbin79
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # F31 sdist 清单:与 pyproject 数据面同源(源码分发可离线重装)
2
+ include install.sh
3
+ include agora
4
+ recursive-include agora-plugin *.ts
5
+ exclude agora-plugin/*.test.ts
6
+ prune agora-plugin/tests
7
+ recursive-include skills *
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: opencode-agora
3
+ Version: 0.1.0
4
+ Summary: opencode 会话间任务派发与凭据治理:agorad 服务+opencode 插件+agora CLI
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # opencode-agora
12
+
13
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
14
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776ab.svg)](https://www.python.org)
15
+ [![made for opencode v2](https://img.shields.io/badge/made%20for-opencode%20v2-6e7bff.svg)](https://opencode.ai)
16
+
17
+ [English](README.md) | [简体中文](README.zh-CN.md)
18
+
19
+ Multi-agent delegation for [opencode](https://opencode.ai) v2. Installing it upgrades your main session into an **orchestration agent** — **the LLM plans, the machine verifies**. Each task runs in its own git worktree; only verified work gets merged.
20
+
21
+ ```text
22
+ You: "Fix the login bug in repoB. Run pytest -q, merge when green."
23
+ agora: └─ spawns a worker in an isolated git worktree
24
+ worker fixes → service runs pytest itself → review green → ff-only merge
25
+ You: "How's it going?" → board: ✅ merged
26
+ ```
27
+
28
+ The difference in one line: **the LLM holds the steering wheel; the machine holds the brakes.** Orchestration — what to run, in what order, when to replan — stays in your conversation, so mid-flight intent (*"now apply the same fix to repoB and repoC"*) becomes a new schedule on the spot. And agents don't grade their own homework here: the verify command runs under the service, not the worker — green means the tests actually passed on the exact commit being merged.
29
+
30
+ ## Quick Start
31
+
32
+ Prerequisites: [opencode](https://opencode.ai) v2 (`curl -fsSL https://opencode.ai/v2/install | bash`), [bun](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`), `git`, `python3` ≥ 3.10 (stdlib only — zero pip dependencies).
33
+
34
+ ```bash
35
+ git clone https://github.com/guangbin79/opencode-agora.git
36
+ cd opencode-agora && ./install.sh
37
+ ```
38
+
39
+ From PyPI instead — equivalent install, no clone needed (the git route above stays as the source-code form):
40
+
41
+ ```bash
42
+ pipx install opencode-agora
43
+ agora-install
44
+ ```
45
+
46
+ That's it. The installer is idempotent (safe to re-run); it registers the MCP server, copies the plugin + skills, and leaves a root credential at `$AGORA_HOME/.agora/root.token` (0600).
47
+
48
+ Now just talk to your opencode session as usual:
49
+
50
+ | You say | What happens |
51
+ |---|---|
52
+ | *"Have an agent fix X in repoY, verify with `pytest -q`"* | Task created, worker dispatched into its own worktree |
53
+ | *"How's it going?"* | Live board: five buckets, dependency chains, escalation feed |
54
+ | *"Stop that task"* | Cascade-cancel with confirmation |
55
+ | *"Bump the dependency in repoA, then apply the same fix in repoB and repoC"* | `after:` dependency chain — B and C wait for A, unrelated repos run in parallel |
56
+
57
+ Standalone dashboard: `agora-board` (vi-motion TUI). Update path: `git pull && ./install.sh`.
58
+
59
+ ## Where It Sits
60
+
61
+ | | LLM-led orchestration | Machine-run quality gate |
62
+ |---|:---:|:---:|
63
+ | Task-graph orchestrators (plan once, execute a static graph) | ✗ | ✓ |
64
+ | Agent-swarm frameworks | ✓ | ✗ (agents self-report) |
65
+ | **opencode-agora** | ✓ | ✓ |
66
+
67
+ Orchestration needs the model's flexibility — real intent surfaces mid-conversation, not in an upfront plan. Merge quality must not trust the agent — it has to run where the agent can't fake it. Most projects take one side; agora is built on the bet that you need both.
68
+
69
+ Making your session an orchestrator isn't unique — subagent trees do that too. The unique part, on an open-source and model-neutral stack, is the **control plane that keeps the orchestrator honest**: verify gate, role-separated credentials, and append-only audit running as an independent service the agent cannot rewrite. Native tools may absorb this tomorrow; today the gap is real.
70
+
71
+ Two capabilities this control plane buys in practice:
72
+
73
+ - **Single repo: enforced worktree writes.** Every agent edit is forced into the task's own worktree by the plugin gate + session tokens — your main checkout is never touched. Convention becomes mechanism.
74
+ - **Multiple repos: one root agent, sibling repos.** A single session at the parent directory orchestrates cross-repo chains with `after:` — the downstream task sees the upstream repo's merged result through the `../` sibling layout, while unrelated repos run in parallel.
75
+
76
+ **Honest trade-offs** (the same reasons you might not want this):
77
+
78
+ - A wrong `verify_cmd` at creation can stall a task — it fails safe: workers can't edit verify, so the worst case is a stuck task, never a bad merge
79
+ - Your main session's context is the orchestration bottleneck — that's why `agora-board` exists: five-bucket state at a glance without spending conversation window
80
+ - Every scheduling decision is a model call — if your work pre-decomposes into a clean static graph, a zero-token orchestrator is cheaper and more reproducible
81
+
82
+ ## How It Works
83
+
84
+ ```text
85
+ Human ──▶ Main session (skills: agora-dispatch / agora-ops)
86
+ │ MCP (13 tools)
87
+ ▼
88
+ agorad — Python stdlib service on localhost
89
+ state authority · verify gate · auto-merge · message routing
90
+ crash recovery · audit ledger
91
+ │
92
+ Per-task git worktrees (enforced isolation)
93
+ ```
94
+
95
+ Task lifecycle: `create → dispatch → worker codes → verify (service-run) → auto-merge (ff-only) → closed`
96
+
97
+ After install, your session gains two skills (agora-dispatch / agora-ops) and 13 MCP tools. There is no background planner: the model you talk to **is** the orchestrator — task creation, `after:` chains, cancels, escalation replies are all model decisions from conversation context. agorad stays the state authority: verify executor, merge gate, message router, audit ledger.
98
+
99
+ ## Two Quality Gates
100
+
101
+ **Verify gate** — the non-negotiable one. The verify command is registered at task creation (`task_create(verify_cmd="pytest -q")`, or `verify_pending: true` to defer). The **service runs it in the task worktree**, then trial-merges into a detached integration worktree and runs it again. Only two green runs advance the target branch; integration failures and test side effects never touch your main checkout. One hard requirement: your main checkout must be clean (no uncommitted or untracked changes) before the final push, otherwise the task reopens with a cleanup hint. Each run has a timeout (300 seconds by default). Red = the worker gets the output and retries. Three consecutive failures = blocked + escalation ping. The worker cannot skip it, fake it, or report results itself.
102
+
103
+ **Review gate** — an open standard, bring your own reviewer. The contract is one anchor file:
104
+
105
+ - If `.review-loop/` exists in the task worktree, the last-reviewed anchor is checked at `done` and **re-checked immediately before merge** (closes the "reviewed, then sneaked a commit" window).
106
+ - Anchor green + verify green = automatic ff-only merge.
107
+ - No `.review-loop/` directory? Everything still works — tasks complete at `done` and **you merge manually**. Autonomy is earned by both gates, not assumed.
108
+
109
+ Any reviewer fits — OpenCodeReview, CodeRabbit, a human checklist — agora only reads the anchor. A minimal reviewer is ~10 lines:
110
+
111
+ ```bash
112
+ #!/usr/bin/env bash
113
+ # review-loop.sh <worktree> <target_branch> — run after each commit; exit non-zero if not green
114
+ set -euo pipefail
115
+ cd "$1"
116
+ base=$(git merge-base HEAD "${2:?}"); sha=$(git rev-parse HEAD)
117
+ findings=$(your_reviewer "$(git diff "$base"..HEAD)") # OCR / CodeRabbit / human list
118
+ printf '%s\n' "$findings" | grep -qiE 'blocker|high' && { printf '%s\n' "$findings"; exit 1; }
119
+ mkdir -p .review-loop && printf '%s\n' "$sha" > .review-loop/.last-reviewed
120
+ ```
121
+
122
+ Full spec — contract details, worker protocol, threat model, idle auto-trigger design notes: [docs/review-loop.md](docs/review-loop.md) (Chinese).
123
+
124
+ ## Highlights
125
+
126
+ - **Verified autonomy** — auto-merge only after service-run tests pass and the review anchor is green, with the anchor re-verified at merge time
127
+ - **Enforced isolation** — plugin-level gate blocks git-repo writes without task credentials; workers can't touch your main checkout. Accepted risk: nested dispatch lets a worker create subtasks on any local repo path with a self-chosen verify — worker-created task targets are not path-clamped
128
+ - **Cross-repo and nested orchestration** — `after:` dependency chains; workers can create and manage their own subtasks, and a parent can finish only after child results return; hard limits on nesting (5) and concurrency (8)
129
+ - **Role-separated credentials** — root / dispatcher / worker session tokens; workers can cancel or register verify only for their own subtasks, never manage parents or other tasks, or read the board
130
+ - **Crash recovery** — `kill -9` the service at any moment; append-only state + worker re-injection with the same credentials on restart
131
+ - **Real-time accountability** — escalation pings to the main session, append-only JSONL audit for every state change, health patrol for dead dispatchers
132
+ - **PR-workflow friendly** — `task_create(target_branch="feature/x")` merges into a feature branch instead of main
133
+ - **Lightweight** — Python stdlib service, zero pip dependencies, localhost only, no telemetry
134
+
135
+ ## Comparison
136
+
137
+ | | Manual multi-session | Swarm frameworks | opencode-agora |
138
+ |---|---|---|---|
139
+ | Per-task isolation | you manage worktrees | varies | enforced (plugin gate + worktree per task) |
140
+ | "Tests pass" means | agent says so | agent says so | service ran the command itself |
141
+ | Cross-repo dependencies | manual sequencing | yes | `after:` chains, parallel when unrelated |
142
+ | Merge | manual | varies | automatic, only when both gates are green |
143
+ | Audit trail | none | partial | append-only JSONL, every state change |
144
+ | Install footprint | — | heavy runtimes | python3 stdlib + git + bun |
145
+
146
+ ## Configuration
147
+
148
+ | Option | Default | Description |
149
+ |--------|---------|-------------|
150
+ | `AGORA_HOME` | `~/plaza` | Ledger home (state, messages, audit, wiki) |
151
+ | `$AGORA_HOME/.agora/inactive-repos` | empty | Repos where agora is fully dormant (one path per line) |
152
+ | `AGORA_BOARD_INTERVAL` | `5` | Dashboard poll interval (seconds) |
153
+ | `AGORA_HEALTH_INTERVAL` | `300` | Dispatcher health patrol (seconds) |
154
+ | `AGORA_VERIFY_TIMEOUT` | `300` | Timeout per task/integration verify run (seconds); expiry kills the process group |
155
+ | `enforce` | on | Global gate switch (root only, audited) |
156
+ | `$AGORA_HOME/.agora/email.json` | off | Stuck-task email alerts (`{"to","command"}`) |
157
+
158
+ ## Safety Model
159
+
160
+ Honest by design — what's machine-checked vs what's trusted:
161
+
162
+ **Enforced** (cannot be bypassed):
163
+ - Git-repo writes only within task worktrees (plugin gate + session tokens)
164
+ - Verify must actually run and pass, executed by the service
165
+ - Role matrix: workers can't cancel, modify verify, or toggle enforce
166
+ - Every state change lands in the append-only audit ledger
167
+
168
+ **Accepted risks** (single-machine trust model):
169
+ - A worker using raw `bash` can bypass the write gate (audit is the backstop)
170
+ - Token theft within the same user account
171
+ - The review anchor is a process protocol, not an anti-cheat: the worker writes it, the service reads it. The verify gate is the real quality bar; review adds discipline, not security
172
+
173
+ ## FAQ
174
+
175
+ **How do I update?** `git pull && ./install.sh` — the plugin and service payloads are copies, so re-running the installer is required after pulling.
176
+
177
+ **How do I uninstall?**
178
+ ```bash
179
+ rm -rf ~/.config/opencode/plugins/agora ~/.config/opencode/skills/agora-dispatch \
180
+ ~/.config/opencode/skills/agora-ops ~/.local/bin/agora ~/.local/bin/agora-board \
181
+ ~/plaza ~/.agora-home
182
+ # then remove the "agora" entry from the mcp section of ~/.config/opencode/opencode.json
183
+ ```
184
+
185
+ (`~/plaza` is the default `AGORA_HOME` — if you configured a custom one, remove that directory instead.)
186
+
187
+ **Does it work without a review-loop?** Yes — verify still runs and gates everything; tasks finish at `done` and you merge manually. Implement the [open standard](docs/review-loop.md) to unlock auto-merge.
188
+
189
+ **Where does my code go?** Nowhere. agorad binds to localhost, workers are local opencode sessions, there is no telemetry, no cloud, no agora API keys. Whatever models your opencode uses, agora orchestrates them.
190
+
191
+ **Platforms?** Linux (verified). macOS: the code paths are in place (the installer falls back to `nohup` where `setsid` is absent) but not yet verified on a real Mac. Windows: run inside WSL.
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,185 @@
1
+ # opencode-agora
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776ab.svg)](https://www.python.org)
5
+ [![made for opencode v2](https://img.shields.io/badge/made%20for-opencode%20v2-6e7bff.svg)](https://opencode.ai)
6
+
7
+ [English](README.md) | [简体中文](README.zh-CN.md)
8
+
9
+ Multi-agent delegation for [opencode](https://opencode.ai) v2. Installing it upgrades your main session into an **orchestration agent** — **the LLM plans, the machine verifies**. Each task runs in its own git worktree; only verified work gets merged.
10
+
11
+ ```text
12
+ You: "Fix the login bug in repoB. Run pytest -q, merge when green."
13
+ agora: └─ spawns a worker in an isolated git worktree
14
+ worker fixes → service runs pytest itself → review green → ff-only merge
15
+ You: "How's it going?" → board: ✅ merged
16
+ ```
17
+
18
+ The difference in one line: **the LLM holds the steering wheel; the machine holds the brakes.** Orchestration — what to run, in what order, when to replan — stays in your conversation, so mid-flight intent (*"now apply the same fix to repoB and repoC"*) becomes a new schedule on the spot. And agents don't grade their own homework here: the verify command runs under the service, not the worker — green means the tests actually passed on the exact commit being merged.
19
+
20
+ ## Quick Start
21
+
22
+ Prerequisites: [opencode](https://opencode.ai) v2 (`curl -fsSL https://opencode.ai/v2/install | bash`), [bun](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`), `git`, `python3` ≥ 3.10 (stdlib only — zero pip dependencies).
23
+
24
+ ```bash
25
+ git clone https://github.com/guangbin79/opencode-agora.git
26
+ cd opencode-agora && ./install.sh
27
+ ```
28
+
29
+ From PyPI instead — equivalent install, no clone needed (the git route above stays as the source-code form):
30
+
31
+ ```bash
32
+ pipx install opencode-agora
33
+ agora-install
34
+ ```
35
+
36
+ That's it. The installer is idempotent (safe to re-run); it registers the MCP server, copies the plugin + skills, and leaves a root credential at `$AGORA_HOME/.agora/root.token` (0600).
37
+
38
+ Now just talk to your opencode session as usual:
39
+
40
+ | You say | What happens |
41
+ |---|---|
42
+ | *"Have an agent fix X in repoY, verify with `pytest -q`"* | Task created, worker dispatched into its own worktree |
43
+ | *"How's it going?"* | Live board: five buckets, dependency chains, escalation feed |
44
+ | *"Stop that task"* | Cascade-cancel with confirmation |
45
+ | *"Bump the dependency in repoA, then apply the same fix in repoB and repoC"* | `after:` dependency chain — B and C wait for A, unrelated repos run in parallel |
46
+
47
+ Standalone dashboard: `agora-board` (vi-motion TUI). Update path: `git pull && ./install.sh`.
48
+
49
+ ## Where It Sits
50
+
51
+ | | LLM-led orchestration | Machine-run quality gate |
52
+ |---|:---:|:---:|
53
+ | Task-graph orchestrators (plan once, execute a static graph) | ✗ | ✓ |
54
+ | Agent-swarm frameworks | ✓ | ✗ (agents self-report) |
55
+ | **opencode-agora** | ✓ | ✓ |
56
+
57
+ Orchestration needs the model's flexibility — real intent surfaces mid-conversation, not in an upfront plan. Merge quality must not trust the agent — it has to run where the agent can't fake it. Most projects take one side; agora is built on the bet that you need both.
58
+
59
+ Making your session an orchestrator isn't unique — subagent trees do that too. The unique part, on an open-source and model-neutral stack, is the **control plane that keeps the orchestrator honest**: verify gate, role-separated credentials, and append-only audit running as an independent service the agent cannot rewrite. Native tools may absorb this tomorrow; today the gap is real.
60
+
61
+ Two capabilities this control plane buys in practice:
62
+
63
+ - **Single repo: enforced worktree writes.** Every agent edit is forced into the task's own worktree by the plugin gate + session tokens — your main checkout is never touched. Convention becomes mechanism.
64
+ - **Multiple repos: one root agent, sibling repos.** A single session at the parent directory orchestrates cross-repo chains with `after:` — the downstream task sees the upstream repo's merged result through the `../` sibling layout, while unrelated repos run in parallel.
65
+
66
+ **Honest trade-offs** (the same reasons you might not want this):
67
+
68
+ - A wrong `verify_cmd` at creation can stall a task — it fails safe: workers can't edit verify, so the worst case is a stuck task, never a bad merge
69
+ - Your main session's context is the orchestration bottleneck — that's why `agora-board` exists: five-bucket state at a glance without spending conversation window
70
+ - Every scheduling decision is a model call — if your work pre-decomposes into a clean static graph, a zero-token orchestrator is cheaper and more reproducible
71
+
72
+ ## How It Works
73
+
74
+ ```text
75
+ Human ──▶ Main session (skills: agora-dispatch / agora-ops)
76
+ │ MCP (13 tools)
77
+ ▼
78
+ agorad — Python stdlib service on localhost
79
+ state authority · verify gate · auto-merge · message routing
80
+ crash recovery · audit ledger
81
+ │
82
+ Per-task git worktrees (enforced isolation)
83
+ ```
84
+
85
+ Task lifecycle: `create → dispatch → worker codes → verify (service-run) → auto-merge (ff-only) → closed`
86
+
87
+ After install, your session gains two skills (agora-dispatch / agora-ops) and 13 MCP tools. There is no background planner: the model you talk to **is** the orchestrator — task creation, `after:` chains, cancels, escalation replies are all model decisions from conversation context. agorad stays the state authority: verify executor, merge gate, message router, audit ledger.
88
+
89
+ ## Two Quality Gates
90
+
91
+ **Verify gate** — the non-negotiable one. The verify command is registered at task creation (`task_create(verify_cmd="pytest -q")`, or `verify_pending: true` to defer). The **service runs it in the task worktree**, then trial-merges into a detached integration worktree and runs it again. Only two green runs advance the target branch; integration failures and test side effects never touch your main checkout. One hard requirement: your main checkout must be clean (no uncommitted or untracked changes) before the final push, otherwise the task reopens with a cleanup hint. Each run has a timeout (300 seconds by default). Red = the worker gets the output and retries. Three consecutive failures = blocked + escalation ping. The worker cannot skip it, fake it, or report results itself.
92
+
93
+ **Review gate** — an open standard, bring your own reviewer. The contract is one anchor file:
94
+
95
+ - If `.review-loop/` exists in the task worktree, the last-reviewed anchor is checked at `done` and **re-checked immediately before merge** (closes the "reviewed, then sneaked a commit" window).
96
+ - Anchor green + verify green = automatic ff-only merge.
97
+ - No `.review-loop/` directory? Everything still works — tasks complete at `done` and **you merge manually**. Autonomy is earned by both gates, not assumed.
98
+
99
+ Any reviewer fits — OpenCodeReview, CodeRabbit, a human checklist — agora only reads the anchor. A minimal reviewer is ~10 lines:
100
+
101
+ ```bash
102
+ #!/usr/bin/env bash
103
+ # review-loop.sh <worktree> <target_branch> — run after each commit; exit non-zero if not green
104
+ set -euo pipefail
105
+ cd "$1"
106
+ base=$(git merge-base HEAD "${2:?}"); sha=$(git rev-parse HEAD)
107
+ findings=$(your_reviewer "$(git diff "$base"..HEAD)") # OCR / CodeRabbit / human list
108
+ printf '%s\n' "$findings" | grep -qiE 'blocker|high' && { printf '%s\n' "$findings"; exit 1; }
109
+ mkdir -p .review-loop && printf '%s\n' "$sha" > .review-loop/.last-reviewed
110
+ ```
111
+
112
+ Full spec — contract details, worker protocol, threat model, idle auto-trigger design notes: [docs/review-loop.md](docs/review-loop.md) (Chinese).
113
+
114
+ ## Highlights
115
+
116
+ - **Verified autonomy** — auto-merge only after service-run tests pass and the review anchor is green, with the anchor re-verified at merge time
117
+ - **Enforced isolation** — plugin-level gate blocks git-repo writes without task credentials; workers can't touch your main checkout. Accepted risk: nested dispatch lets a worker create subtasks on any local repo path with a self-chosen verify — worker-created task targets are not path-clamped
118
+ - **Cross-repo and nested orchestration** — `after:` dependency chains; workers can create and manage their own subtasks, and a parent can finish only after child results return; hard limits on nesting (5) and concurrency (8)
119
+ - **Role-separated credentials** — root / dispatcher / worker session tokens; workers can cancel or register verify only for their own subtasks, never manage parents or other tasks, or read the board
120
+ - **Crash recovery** — `kill -9` the service at any moment; append-only state + worker re-injection with the same credentials on restart
121
+ - **Real-time accountability** — escalation pings to the main session, append-only JSONL audit for every state change, health patrol for dead dispatchers
122
+ - **PR-workflow friendly** — `task_create(target_branch="feature/x")` merges into a feature branch instead of main
123
+ - **Lightweight** — Python stdlib service, zero pip dependencies, localhost only, no telemetry
124
+
125
+ ## Comparison
126
+
127
+ | | Manual multi-session | Swarm frameworks | opencode-agora |
128
+ |---|---|---|---|
129
+ | Per-task isolation | you manage worktrees | varies | enforced (plugin gate + worktree per task) |
130
+ | "Tests pass" means | agent says so | agent says so | service ran the command itself |
131
+ | Cross-repo dependencies | manual sequencing | yes | `after:` chains, parallel when unrelated |
132
+ | Merge | manual | varies | automatic, only when both gates are green |
133
+ | Audit trail | none | partial | append-only JSONL, every state change |
134
+ | Install footprint | — | heavy runtimes | python3 stdlib + git + bun |
135
+
136
+ ## Configuration
137
+
138
+ | Option | Default | Description |
139
+ |--------|---------|-------------|
140
+ | `AGORA_HOME` | `~/plaza` | Ledger home (state, messages, audit, wiki) |
141
+ | `$AGORA_HOME/.agora/inactive-repos` | empty | Repos where agora is fully dormant (one path per line) |
142
+ | `AGORA_BOARD_INTERVAL` | `5` | Dashboard poll interval (seconds) |
143
+ | `AGORA_HEALTH_INTERVAL` | `300` | Dispatcher health patrol (seconds) |
144
+ | `AGORA_VERIFY_TIMEOUT` | `300` | Timeout per task/integration verify run (seconds); expiry kills the process group |
145
+ | `enforce` | on | Global gate switch (root only, audited) |
146
+ | `$AGORA_HOME/.agora/email.json` | off | Stuck-task email alerts (`{"to","command"}`) |
147
+
148
+ ## Safety Model
149
+
150
+ Honest by design — what's machine-checked vs what's trusted:
151
+
152
+ **Enforced** (cannot be bypassed):
153
+ - Git-repo writes only within task worktrees (plugin gate + session tokens)
154
+ - Verify must actually run and pass, executed by the service
155
+ - Role matrix: workers can't cancel, modify verify, or toggle enforce
156
+ - Every state change lands in the append-only audit ledger
157
+
158
+ **Accepted risks** (single-machine trust model):
159
+ - A worker using raw `bash` can bypass the write gate (audit is the backstop)
160
+ - Token theft within the same user account
161
+ - The review anchor is a process protocol, not an anti-cheat: the worker writes it, the service reads it. The verify gate is the real quality bar; review adds discipline, not security
162
+
163
+ ## FAQ
164
+
165
+ **How do I update?** `git pull && ./install.sh` — the plugin and service payloads are copies, so re-running the installer is required after pulling.
166
+
167
+ **How do I uninstall?**
168
+ ```bash
169
+ rm -rf ~/.config/opencode/plugins/agora ~/.config/opencode/skills/agora-dispatch \
170
+ ~/.config/opencode/skills/agora-ops ~/.local/bin/agora ~/.local/bin/agora-board \
171
+ ~/plaza ~/.agora-home
172
+ # then remove the "agora" entry from the mcp section of ~/.config/opencode/opencode.json
173
+ ```
174
+
175
+ (`~/plaza` is the default `AGORA_HOME` — if you configured a custom one, remove that directory instead.)
176
+
177
+ **Does it work without a review-loop?** Yes — verify still runs and gates everything; tasks finish at `done` and you merge manually. Implement the [open standard](docs/review-loop.md) to unlock auto-merge.
178
+
179
+ **Where does my code go?** Nowhere. agorad binds to localhost, workers are local opencode sessions, there is no telemetry, no cloud, no agora API keys. Whatever models your opencode uses, agora orchestrates them.
180
+
181
+ **Platforms?** Linux (verified). macOS: the code paths are in place (the installer falls back to `nohup` where `setsid` is absent) but not yet verified on a real Mac. Windows: run inside WSL.
182
+
183
+ ## License
184
+
185
+ MIT
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env python3
2
+ """agora CLI(F14):bash 可判定的只读条件谓词——看门狗规范条件等待。
3
+
4
+ wait 子命令阻塞等待任务终态(merged/closed/blocked/cancelled),对齐
5
+ docker wait / kubectl wait 约定。status 子命令(F28 防误杀)单查任务三态
6
+ 判读:数据全取既有 /board 单任务视图+/doctor(零服务端新端点),分类
7
+ 客户端推导。取数照抄 board_tui.py 形态:
8
+ $AGORA_HOME/.agora/{root.token,service.json},零直读状态文件、零格式逆向。
9
+ 非 MCP agora.wait(seconds)(那是 agent 回合内本地睡眠,两者不互替)。
10
+ """
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ import time
16
+ import urllib.error
17
+ import urllib.request
18
+ from pathlib import Path
19
+
20
+ TERMINAL = frozenset(("merged", "closed", "blocked", "cancelled"))
21
+ _INFLIGHT = ("todo", "dispatched", "running", "done", "verify_failed", "reopened")
22
+ _INTERVAL = 15 # 内部轮询间隔(秒)
23
+ _HTTP_TIMEOUT = 5 # 单次请求超时(board_tui 同值)
24
+ _TIMEOUT_MSG = "须为非负有限秒数"
25
+ _STALE_AFTER = 1200 # 心跳冷阈值(秒)——与 agorad report._STALE_AFTER 同口径(客户端推导锚)
26
+
27
+
28
+ class Fail(Exception):
29
+ """终局异常(不可重试)——exit 2。"""
30
+
31
+
32
+ class Transient(Exception):
33
+ """瞬时不可达(连接拒等 OSError 族)——阻塞形态内部重试吸收。"""
34
+
35
+
36
+ class Rejected(Transient):
37
+ """认牌失败(401/403)——牌换代窗口瞬态;不跟随换模时仍终局 Fail。"""
38
+
39
+
40
+ def _pointer_home():
41
+ """~/.agora-home 指针回退(gt-32c051 统一发现桥;board_tui 同形)。"""
42
+ try:
43
+ return ((Path.home() / ".agora-home").read_text(encoding="utf-8")
44
+ .split("\n", 1)[0].strip() or None)
45
+ except (OSError, ValueError): # ValueError=坏字节(UnicodeDecodeError 非 OSError 族)
46
+ return None
47
+
48
+
49
+ def discover(home=None):
50
+ """root 牌+端口:$AGORA_HOME/.agora/{root.token,service.json}(board_tui 同形)。"""
51
+ home = Path(home or os.environ.get("AGORA_HOME") or _pointer_home()
52
+ or Path.home() / "plaza")
53
+ sj = json.loads((home / ".agora" / "service.json").read_text(encoding="utf-8"))
54
+ port = int(sj["port"]) # 非数端口(半写形态)在此炸 ValueError/TypeError,不外溢 fetch
55
+ token = (home / ".agora" / "root.token").read_text(encoding="utf-8").strip()
56
+ if "\n" in token or "\r" in token: # 控制符会让 putheader 带牌面炸进 stderr/折叠改头
57
+ raise ValueError("root 牌撕裂(含换行/回车)")
58
+ return f"http://127.0.0.1:{port}", token
59
+
60
+
61
+ def _call(base, token, path, body):
62
+ """HTTP 取数缝(fetch/status 共用):POST <path> 带 root 牌回解析 JSON。
63
+
64
+ 401/403→Rejected(换模跟随窗按瞬态)、其余 HTTP 错→Fail(404 注任务未知)、
65
+ 网络错(OSError 族)→Transient(可重试)。
66
+ """
67
+ req = urllib.request.Request(
68
+ base + path, data=json.dumps(body).encode(),
69
+ headers={"Content-Type": "application/json", "X-Agora-Token": token},
70
+ method="POST")
71
+ try:
72
+ with urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT) as r:
73
+ return json.loads(r.read().decode())
74
+ except urllib.error.HTTPError as e:
75
+ if e.code in (401, 403): # 认牌失败:牌换代/撕裂对窗口,跟随形态按瞬态
76
+ raise Rejected(f"HTTP {e.code}: {path}") from None
77
+ note = "(任务未知)" if e.code == 404 and path == "/board" else ""
78
+ raise Fail(f"HTTP {e.code}" + note + f": {path}") from None
79
+ except OSError as e: # URLError(连接拒等)=瞬时,可重试
80
+ raise Transient(str(e)) from None
81
+
82
+
83
+ def fetch(base, token, tid):
84
+ """POST /board 单任务视图 → (state, commit)。报因尾附 tid(看门狗聚合 stderr 可归任务)。"""
85
+ try:
86
+ t = _call(base, token, "/board", {"task": tid})["task"]
87
+ except (Fail, Rejected) as e:
88
+ raise type(e)(f"{e}: {tid}") from None
89
+ return t["state"], t.get("commit")
90
+
91
+
92
+ def classify(state):
93
+ """状态分类缝(exit code 决策):终态→0,在飞(含未知态)→1。"""
94
+ return 0 if state in TERMINAL else 1
95
+
96
+
97
+ def wait(tid, base, token, timeout=1800, once=False, interval=_INTERVAL,
98
+ clock=time.monotonic, pause=time.sleep, reread=None):
99
+ """轮询至终态:返回 (exit, stdout 行);重试至超时仍不可达 raise Fail。
100
+
101
+ reread(run_wait 注入 discover)每轮轮询前重读指针——服务换模
102
+ (杀旧拉新、端口/牌换代)即跟随新端点续等,不对死端口空转到超时;
103
+ 跟随时认牌失败(401/403,撕裂对/换代窗口)按瞬态重试。
104
+ """
105
+ deadline = clock() + timeout
106
+ while True:
107
+ if reread:
108
+ try:
109
+ nb, nt = reread()
110
+ if nb and nt: # 半写空端点/空牌不采纳,保持旧端点下轮再读
111
+ base, token = nb, nt
112
+ except (OSError, ValueError, TypeError, KeyError):
113
+ pass # 指针半写/缺失(含非 dict JSON)保持旧端点,下轮再读
114
+ try:
115
+ state, commit = fetch(base, token, tid)
116
+ except Transient as e:
117
+ if isinstance(e, Rejected) and reread is None:
118
+ raise Fail(str(e)) from None # 不跟随换模:认牌失败维持终局
119
+ if once or clock() >= deadline:
120
+ head = ("认牌失败" if isinstance(e, Rejected)
121
+ else "服务不可达" if once else "服务不可达(重试至超时)")
122
+ raise Fail(f"{head}: {e}") from None
123
+ else:
124
+ if classify(state) == 0:
125
+ return 0, (f"{state} @{commit[:12]}" if commit else state)
126
+ if once or clock() >= deadline:
127
+ return 1, f"in-flight: {state}"
128
+ pause(min(interval, max(0.0, deadline - clock())))
129
+
130
+
131
+ def run_wait(a):
132
+ base, token = discover()
133
+ if not token:
134
+ raise Fail("root 牌缺失")
135
+ code, line = wait(a.task_id, base, token, timeout=a.timeout, once=a.once,
136
+ reread=discover)
137
+ print(line)
138
+ return code
139
+
140
+
141
+ def status_view(tid, task, stale_ids, now):
142
+ """F28 三态判读(客户端推导,零服务端新端点):数据=/board 单任务视图+/doctor。
143
+
144
+ fresh=心跳近(last_seen 距今≤_STALE_AFTER);busy_round=在飞+心跳冷+
145
+ 不在 doctor stale_candidates(基线含 env 活性,env 在写=深潜中);stale_candidate
146
+ =在 doctor 僵态候选单。终态或无 worker=无判读(class None)。
147
+ """
148
+ state = task["state"]
149
+ seen = task.get("last_seen")
150
+ ago = None if not seen else max(0, int(now - seen))
151
+ if state in TERMINAL or not task.get("worker_sid"):
152
+ cls = None
153
+ elif tid in stale_ids:
154
+ cls = "stale_candidate"
155
+ elif ago is None or ago > _STALE_AFTER:
156
+ cls = "busy_round"
157
+ else:
158
+ cls = "fresh"
159
+ return {"task": tid, "state": state, "last_seen_ago": ago, "class": cls}
160
+
161
+
162
+ def run_status(a):
163
+ base, token = discover()
164
+ if not token:
165
+ raise Fail("root 牌缺失")
166
+ now = time.time()
167
+ try:
168
+ task = _call(base, token, "/board", {"task": a.task_id})["task"]
169
+ doc = _call(base, token, "/doctor", {})
170
+ except Fail as e: # 报因尾附 tid(镜像 fetch:看门狗聚合 stderr 可归任务)
171
+ raise type(e)(f"{e}: {a.task_id}") from None
172
+ except Transient as e: # 单查不重试:判读要新鲜,旧数据不如报错
173
+ head = "认牌失败" if isinstance(e, Rejected) else "服务不可达"
174
+ raise Fail(f"{head}: {e}: {a.task_id}") from None
175
+ v = status_view(a.task_id, task,
176
+ {tid for tid, _ in doc.get("stale_candidates", [])},
177
+ now) # 旧 daemon 无该键→空集降级,不裸 KeyError
178
+ if a.json:
179
+ print(json.dumps(v, ensure_ascii=False))
180
+ else:
181
+ ago = "-" if v["last_seen_ago"] is None else f"{v['last_seen_ago']}s"
182
+ print(f"{v['task']} state={v['state']} last_seen={ago} "
183
+ f"class={v['class'] or '-'}")
184
+ return 0
185
+
186
+
187
+ def _timeout_arg(v):
188
+ """非负有限秒数;拒 nan/inf/负值(nan 会让 deadline 比较恒 False 变热轮询)。"""
189
+ try:
190
+ t = float(v)
191
+ except (ValueError, TypeError):
192
+ raise argparse.ArgumentTypeError(_TIMEOUT_MSG) from None
193
+ if not 0 <= t < float("inf"):
194
+ raise argparse.ArgumentTypeError(_TIMEOUT_MSG)
195
+ return t
196
+
197
+
198
+ def _parser():
199
+ ap = argparse.ArgumentParser(
200
+ prog="agora",
201
+ description="agora CLI——bash 可判定的只读条件谓词(F14)。")
202
+ sub = ap.add_subparsers(dest="cmd", required=True, metavar="command")
203
+ w = sub.add_parser(
204
+ "wait", help="阻塞等待任务终态(--once 单查)",
205
+ description="阻塞等待任务终态(--once 单查);非 MCP agora.wait(seconds)"
206
+ "(那是 agent 回合内本地睡眠)。",
207
+ epilog="退出码:0=终态到达(merged/closed/blocked/cancelled 任一)\n"
208
+ f"1=--timeout 到期仍在飞({'/'.join(_INFLIGHT)}),"
209
+ "或 --once 下当前在飞\n"
210
+ "2=异常(用法错/任务未知 404/root 牌缺失/认牌失败/重试至超时仍不可达)",
211
+ formatter_class=argparse.RawDescriptionHelpFormatter)
212
+ w.add_argument("task_id", metavar="task-id", help="任务 id(ag-xxxxxx 或历史 task-/gt-xxxxxx)")
213
+ w.add_argument("--timeout", type=_timeout_arg, default=1800,
214
+ help="到期仍在飞退出 1(秒);缺省 1800(防调用方永挂)")
215
+ w.add_argument("--once", action="store_true", help="单查形态,立即返回不阻塞")
216
+ w.set_defaults(fn=run_wait)
217
+ s = sub.add_parser(
218
+ "status", help="任务三态判读单查(F28 防误杀:cancel 前先看一眼)",
219
+ description="单查任务三态判读:state+last_seen 距今秒+分类——"
220
+ "fresh(心跳近)/busy_round(在飞+心跳冷+非僵态候选)/"
221
+ "stale_candidate(在 doctor 僵态候选单);数据取 /board+/doctor。",
222
+ epilog="退出码:0=取数成功(判读读输出,不作退出码)\n"
223
+ "2=异常(用法错/任务未知/不可达/认牌失败)",
224
+ formatter_class=argparse.RawDescriptionHelpFormatter)
225
+ s.add_argument("task_id", metavar="task-id", help="任务 id(ag-xxxxxx 或历史 task-/gt-xxxxxx)")
226
+ s.add_argument("--json", action="store_true", help="机器可读 JSON 形态")
227
+ s.set_defaults(fn=run_status)
228
+ return ap
229
+
230
+
231
+ def main(argv=None):
232
+ argv = sys.argv[1:] if argv is None else argv
233
+ try:
234
+ a = _parser().parse_args(argv)
235
+ return a.fn(a)
236
+ except KeyboardInterrupt:
237
+ return 130 # 打断≠终态:非零防看门狗误判 merged
238
+ except Exception as e:
239
+ print(f"agora: {e}", file=sys.stderr)
240
+ return 2
241
+
242
+
243
+ if __name__ == "__main__":
244
+ sys.exit(main())