agent-control-plane 0.3.0 → 0.6.0

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 (106) hide show
  1. package/README.md +141 -28
  2. package/assets/workflow-catalog.json +1 -1
  3. package/bin/pr-risk.sh +22 -7
  4. package/bin/sync-pr-labels.sh +1 -1
  5. package/hooks/heartbeat-hooks.sh +125 -12
  6. package/hooks/issue-reconcile-hooks.sh +1 -1
  7. package/hooks/pr-reconcile-hooks.sh +1 -1
  8. package/npm/bin/agent-control-plane.js +257 -59
  9. package/package.json +39 -32
  10. package/tools/bin/debug-session.sh +106 -0
  11. package/tools/bin/flow-config-lib.sh +1203 -60
  12. package/tools/bin/flow-runtime-doctor-linux.sh +136 -0
  13. package/tools/bin/flow-runtime-doctor.sh +5 -1
  14. package/tools/bin/flow-shell-lib.sh +32 -0
  15. package/tools/bin/github-core-rate-limit-state.sh +77 -0
  16. package/tools/bin/github-write-outbox.sh +470 -0
  17. package/tools/bin/heartbeat-loop-scheduling-lib.sh +7 -7
  18. package/tools/bin/heartbeat-safe-auto.sh +42 -0
  19. package/tools/bin/install-project-launchd.sh +17 -2
  20. package/tools/bin/install-project-systemd.sh +255 -0
  21. package/tools/bin/project-init.sh +21 -1
  22. package/tools/bin/project-launchd-bootstrap.sh +5 -1
  23. package/tools/bin/project-runtimectl.sh +91 -2
  24. package/tools/bin/project-systemd-bootstrap.sh +74 -0
  25. package/tools/bin/scaffold-profile.sh +61 -3
  26. package/tools/bin/uninstall-project-systemd.sh +87 -0
  27. package/tools/dashboard/app.js +228 -6
  28. package/tools/dashboard/dashboard_snapshot.py +55 -0
  29. package/tools/dashboard/issue_queue_state.py +101 -0
  30. package/tools/dashboard/server.py +123 -1
  31. package/tools/dashboard/styles.css +526 -455
  32. package/tools/templates/pr-fix-template.md +3 -1
  33. package/tools/templates/pr-merge-repair-template.md +2 -1
  34. package/references/architecture.md +0 -217
  35. package/references/commands.md +0 -128
  36. package/references/control-plane-map.md +0 -124
  37. package/references/docs-map.md +0 -73
  38. package/references/release-checklist.md +0 -65
  39. package/references/repo-map.md +0 -36
  40. package/tools/bin/agent-cleanup-worktree +0 -247
  41. package/tools/bin/agent-github-update-labels +0 -71
  42. package/tools/bin/agent-init-worktree +0 -216
  43. package/tools/bin/agent-project-archive-run +0 -52
  44. package/tools/bin/agent-project-capture-worker +0 -46
  45. package/tools/bin/agent-project-catch-up-issue-pr-links +0 -118
  46. package/tools/bin/agent-project-catch-up-merged-prs +0 -194
  47. package/tools/bin/agent-project-catch-up-scheduled-issue-retries +0 -123
  48. package/tools/bin/agent-project-cleanup-session +0 -513
  49. package/tools/bin/agent-project-detached-launch +0 -127
  50. package/tools/bin/agent-project-heartbeat-loop +0 -1029
  51. package/tools/bin/agent-project-open-issue-worktree +0 -89
  52. package/tools/bin/agent-project-open-pr-worktree +0 -80
  53. package/tools/bin/agent-project-publish-issue-pr +0 -465
  54. package/tools/bin/agent-project-reconcile-issue-session +0 -1398
  55. package/tools/bin/agent-project-reconcile-pr-session +0 -1230
  56. package/tools/bin/agent-project-retry-state +0 -147
  57. package/tools/bin/agent-project-run-claude-session +0 -805
  58. package/tools/bin/agent-project-run-codex-resilient +0 -955
  59. package/tools/bin/agent-project-run-codex-session +0 -435
  60. package/tools/bin/agent-project-run-kilo-session +0 -369
  61. package/tools/bin/agent-project-run-ollama-session +0 -658
  62. package/tools/bin/agent-project-run-openclaw-session +0 -1309
  63. package/tools/bin/agent-project-run-opencode-session +0 -377
  64. package/tools/bin/agent-project-run-pi-session +0 -479
  65. package/tools/bin/agent-project-sync-anchor-repo +0 -139
  66. package/tools/bin/agent-project-worker-status +0 -188
  67. package/tools/bin/branch-verification-guard.sh +0 -364
  68. package/tools/bin/capture-worker.sh +0 -18
  69. package/tools/bin/cleanup-worktree.sh +0 -52
  70. package/tools/bin/codex-quota +0 -31
  71. package/tools/bin/create-follow-up-issue.sh +0 -114
  72. package/tools/bin/dashboard-launchd-bootstrap.sh +0 -50
  73. package/tools/bin/issue-publish-localization-guard.sh +0 -142
  74. package/tools/bin/issue-publish-scope-guard.sh +0 -242
  75. package/tools/bin/issue-requires-local-workspace-install.sh +0 -31
  76. package/tools/bin/issue-resource-class.sh +0 -12
  77. package/tools/bin/kick-scheduler.sh +0 -75
  78. package/tools/bin/label-follow-up-issues.sh +0 -14
  79. package/tools/bin/new-pr-worktree.sh +0 -50
  80. package/tools/bin/new-worktree.sh +0 -49
  81. package/tools/bin/pr-risk.sh +0 -12
  82. package/tools/bin/prepare-worktree.sh +0 -142
  83. package/tools/bin/provider-cooldown-state.sh +0 -204
  84. package/tools/bin/publish-issue-worker.sh +0 -31
  85. package/tools/bin/reconcile-bootstrap-lib.sh +0 -113
  86. package/tools/bin/reconcile-issue-worker.sh +0 -34
  87. package/tools/bin/reconcile-pr-worker.sh +0 -34
  88. package/tools/bin/record-verification.sh +0 -71
  89. package/tools/bin/render-flow-config.sh +0 -98
  90. package/tools/bin/resident-issue-controller-lib.sh +0 -448
  91. package/tools/bin/resident-issue-queue-status.py +0 -35
  92. package/tools/bin/retry-state.sh +0 -31
  93. package/tools/bin/reuse-issue-worktree.sh +0 -121
  94. package/tools/bin/run-codex-bypass.sh +0 -3
  95. package/tools/bin/run-codex-safe.sh +0 -3
  96. package/tools/bin/run-codex-task.sh +0 -280
  97. package/tools/bin/serve-dashboard.sh +0 -5
  98. package/tools/bin/split-retained-slice.sh +0 -124
  99. package/tools/bin/start-issue-worker.sh +0 -943
  100. package/tools/bin/start-pr-fix-worker.sh +0 -491
  101. package/tools/bin/start-pr-merge-repair-worker.sh +0 -8
  102. package/tools/bin/start-pr-review-worker.sh +0 -261
  103. package/tools/bin/start-resident-issue-loop.sh +0 -499
  104. package/tools/bin/update-github-labels.sh +0 -14
  105. package/tools/bin/worker-status.sh +0 -19
  106. package/tools/bin/workflow-catalog.sh +0 -77
package/README.md CHANGED
@@ -12,6 +12,8 @@
12
12
  `agent-control-plane` (ACP) keeps your coding agents running reliably without
13
13
  you having to stare at them all day.
14
14
 
15
+ **✅ ROADMAP COMPLETE (v0.6.0)** - All planned features delivered!
16
+
15
17
  It is the operator layer for coding agents that need to keep running after the
16
18
  novelty wears off — and the responsible adult in the room that stops them from
17
19
  going completely off the rails.
@@ -33,7 +35,7 @@ however, and suddenly that "not smart enough" free model is grinding through
33
35
  your issue backlog like a junior developer who is weirdly enthusiastic about
34
36
  reading CI logs.
35
37
 
36
- That is what ACP does. It turns a GitHub repo into a managed runtime: a
38
+ That is what ACP does. It turns a forge-backed repo into a managed runtime: a
37
39
  repeatable setup, a stable home for state, a heartbeat that keeps agents
38
40
  scheduled and supervised, and a dashboard you can actually glance at without
39
41
  spelunking through temp folders, worktrees, or half-remembered `tmux` sessions.
@@ -73,7 +75,7 @@ monthly API budget in a long weekend, or enter a retry loop that only stops when
73
75
  the credit card does.
74
76
 
75
77
  ACP is the person standing next to the fuse. It enforces launch limits,
76
- reconciles outcomes before touching GitHub, validates before it publishes, and
78
+ reconciles outcomes before touching your forge, validates before it publishes, and
77
79
  respects cooldowns instead of hammering a provider at full throttle. The agent
78
80
  gets to be smart and fast. ACP makes sure "smart and fast" does not also mean
79
81
  "unattended and irreversible."
@@ -98,6 +100,27 @@ it had a supervisor."
98
100
  | Run reproducible agent research cheaply | Cost-controlled execution harness for studying agent behavior, output quality, or prompting strategies |
99
101
  | Enforce safety by architecture, not by hope | Launch limits, reconcile gates, and cooldowns that are built into the runtime, not left to chance |
100
102
 
103
+ ## Why Gitea Local-First
104
+
105
+ ACP started GitHub-first, but a local-first Gitea loop is often a better daily
106
+ working setup:
107
+
108
+ - It reduces dependence on GitHub API rate limits for routine issue and PR work.
109
+ - It lets agents collaborate against a local forge while you keep GitHub as the
110
+ public mirror or release boundary.
111
+ - It gives you a safer place to let ACP iterate quickly, because local Gitea is
112
+ cheaper to reset, inspect, and isolate than a live hosted repo.
113
+ - It matches how ACP now works best operationally: local runtime state,
114
+ local worktrees, local dashboard, and a forge that can live on the same
115
+ machine.
116
+
117
+ The intended model is:
118
+
119
+ - `Gitea main` is the working mainline ACP automates day to day.
120
+ - Your local source checkout auto-syncs from that forge mainline.
121
+ - GitHub becomes the publish/release mirror once the codebase is stable enough
122
+ to push outward.
123
+
101
124
  ## Use Cases
102
125
 
103
126
  Teams and solo builders usually reach for ACP when one of these starts to feel
@@ -120,7 +143,7 @@ familiar:
120
143
  ## Roadmap
121
144
 
122
145
  ACP is moving toward a true multi-backend control plane. The goal is one runtime
123
- and one dashboard for many coding-agent backends, across macOS, Linux, and
146
+ and one dashboard for many coding-agent backends, across macOS, Linux, and Windows (WSL2)
124
147
  Windows.
125
148
 
126
149
  ### Backend Status
@@ -129,14 +152,41 @@ Windows.
129
152
  | --- | --- | --- |
130
153
  | `codex` | Production-ready | Full ACP workflow support today. |
131
154
  | `claude` | Production-ready | Full ACP workflow support today. |
132
- | `openclaw` | Production-ready | Full ACP workflow support, including resident-style runs. |
133
- | `ollama` | Experimental | Working adapter with Node.js agentic loop. Runs any model served by a local Ollama instance. Output quality depends on model size 7–9B models handle exploration well but struggle with complex multi-step tasks. |
134
- | `pi` | Experimental | Working adapter using the [pi CLI](https://github.com/mariozechner/pi) in `--print --no-session` mode. Connects to any OpenRouter-compatible model. Useful as a lightweight alternative to openclaw for free-tier model testing. |
135
- | `opencode` | Experimental | Working adapter for [Crush](https://github.com/charmbracelet/crush) (formerly opencode). Non-interactive `crush run` with full tool execution. |
136
- | `kilo` | Experimental | Working adapter for [Kilo Code](https://github.com/Kilo-Org/kilocode). Non-interactive `kilo run --auto` with JSON event stream. |
137
- | `gemini-cli` | Roadmap | Strong future candidate; not wired into ACP yet. |
138
- | `nanoclaw` | Exploratory | Ecosystem reference for containerized and WSL2-friendly workflows. |
139
- | `picoclaw` | Exploratory | Ecosystem reference for lightweight Linux and edge-style runtimes. |
155
+ | `openclaw` | Production-ready | Full ACP workflow, including resident-style runs. |
156
+ | `ollama` | **Hardening** | Working adapter with Node.js agentic loop. **v0.4.9+: Added health-check + context detection.** Moved toward production-ready. |
157
+ | `pi` | Experimental | Working adapter using the pi CLI. **Health-check + API key validation.** |
158
+ | `opencode` | Experimental | Working adapter for Crush. **Health-check (verify `crush` binary).** |
159
+ | `kilo` | Experimental | Working adapter for Kilo Code. **Health-check + JSON stream validation.** |
160
+ | `gemini-cli` | **Integrated** | Google's official terminal agent (v0.39.1+). Full ACP adapter with health-check, API key validation, and streaming JSON output. |
161
+ | `nanoclaw` | Not integrable | Standalone agent system (like ACP), not a CLI backend. Reference for container patterns. |
162
+ | `picoclaw` | Not integrable | Standalone agent system (Go-based). Runs on $10 hardware. Not a CLI backend. |
163
+
164
+ ### Adapter Pattern
165
+
166
+ ACP uses a **standardized adapter interface** to support multiple backends. Every adapter implements these functions:
167
+
168
+ | Function | Purpose |
169
+ | --- | --- |
170
+ | `adapter_info()` | Print adapter metadata (id, name, type, version, model) |
171
+ | `adapter_health_check()` | Verify backend is available (exit 0 = healthy) |
172
+ | `adapter_run()` | Execute a task (params: MODE SESSION WORKTREE PROMPT_FILE) |
173
+ | `adapter_status()` | Get run status for a session |
174
+
175
+ **Available adapters:**
176
+ - `tools/bin/ollama-adapter.sh` - Ollama local models (implements health-check)
177
+ - All existing backends (`codex`, `claude`, `pi`, `opencode`, `kilo`) use the same interface via `run-codex-task.sh`, `run-claude-task.sh`, etc.
178
+
179
+ **Using adapters:**
180
+
181
+ ```bash
182
+ # Print adapter info
183
+ bash tools/bin/ollama-adapter.sh
184
+
185
+ # Run a task with generic runner
186
+ bash tools/bin/run-with-adapter.sh tools/bin/ollama-adapter.sh safe my-session /path/to/worktree /path/to/prompt.txt
187
+ ```
188
+
189
+ See `tools/bin/adapter-interface.sh` for the full interface specification.
140
190
 
141
191
  If you are trying ACP on a real repo right now, start with `codex`, `claude`,
142
192
  or `openclaw`. Use `ollama` to run local models — useful for research, offline
@@ -211,7 +261,7 @@ flowchart LR
211
261
  Scheduler --> Workers["issue / PR worker launchers"]
212
262
  Workers --> Backends["codex / claude / openclaw / ollama / pi / opencode / kilo"]
213
263
  Backends --> Reconcile["reconcile issue / PR session"]
214
- Reconcile --> GitHub["issues / PRs / labels / comments"]
264
+ Reconcile --> Forge["issues / PRs / labels / comments"]
215
265
  Scheduler --> State["runs + state + history"]
216
266
  State --> Dashboard["dashboard snapshot + UI"]
217
267
  ```
@@ -227,7 +277,7 @@ sequenceDiagram
227
277
  participant Heartbeat
228
278
  participant Worker
229
279
  participant Reconcile
230
- participant GitHub
280
+ participant Forge
231
281
 
232
282
  Operator->>RuntimeCtl: runtime start --profile-id <id>
233
283
  RuntimeCtl->>Supervisor: keep runtime alive
@@ -236,7 +286,7 @@ sequenceDiagram
236
286
  Bootstrap->>Heartbeat: invoke published heartbeat
237
287
  Heartbeat->>Worker: launch eligible issue/PR flow
238
288
  Worker->>Reconcile: emit result artifacts
239
- Reconcile->>GitHub: labels, comments, PR actions
289
+ Reconcile->>Forge: labels, comments, PR actions
240
290
  end
241
291
  ```
242
292
 
@@ -267,9 +317,9 @@ system.
267
317
  | --- | --- | --- | --- |
268
318
  | Node.js `>= 18` | yes | Runs the npm package entrypoint and `npx` wrapper. | CI runs on Node `22`. Node `20` or `22` both work fine. |
269
319
  | `bash` | yes | All runtime, profile, and worker orchestration scripts are Bash. | Your login shell can be `zsh`; `bash` just needs to be on `PATH`. |
270
- | `git` | yes | Manages worktrees, checks branch state, and coordinates repo automation. | Required even if you interact only through GitHub issues and PRs. |
271
- | `gh` | yes | GitHub CLI auth and API access for issues, PRs, labels, and metadata. | Run `gh auth login` before first use. |
272
- | `jq` | yes | Parses JSON from `gh` output and worker metadata throughout. | Missing `jq` will break GitHub-heavy and runtime flows. |
320
+ | `git` | yes | Manages worktrees, checks branch state, and coordinates repo automation. | Required even if you interact only through forge issues and PRs. |
321
+ | `gh` | for GitHub-first setups | GitHub CLI auth and API access for issues, PRs, labels, and metadata. | Run `gh auth login` before first use when `--forge-provider github`. |
322
+ | `jq` | yes | Parses JSON from `gh` output and worker metadata throughout. | Missing `jq` will break GitHub-heavy and Gitea-heavy runtime flows. |
273
323
  | `python3` | yes | Powers the dashboard server, snapshot renderer, and config helpers. | Required for both dashboard use and several internal scripts. |
274
324
  | `tmux` | yes | Runs long-lived worker sessions and captures their status. | Missing `tmux` means background worker workflows will not launch. |
275
325
  | Worker CLI (backend-specific) | depends on backend | The coding agent for a profile. Supported: `codex`, `claude`, `openclaw` (production); `ollama`, `pi`, `opencode`, `kilo` (experimental). | Install and authenticate your chosen backend before starting background runs. |
@@ -279,8 +329,10 @@ system.
279
329
  | `kilo` CLI | for `kilo` backend | TypeScript coding agent ([kilocode/cli](https://github.com/Kilo-Org/kilocode)). | Install via `npm i -g @kilocode/cli`. |
280
330
  | Bundled `codex-quota` + ACP quota manager | automatic for Codex | Quota-aware failover and health signals for Codex profiles. | Bundled by default. Override with `ACP_CODEX_QUOTA_BIN` only if you have a custom setup. |
281
331
 
282
- Make sure `gh` and your chosen worker backend are both authenticated for the
283
- same OS user before starting any background runtime.
332
+ Make sure your chosen worker backend is authenticated for the same OS user
333
+ before starting any background runtime. For GitHub-first setups, authenticate
334
+ `gh`. For Gitea local-first setups, provide `--gitea-base-url` plus a token or
335
+ username/password during setup so ACP can write issues, PR comments, and labels.
284
336
 
285
337
  ## Install
286
338
 
@@ -313,7 +365,7 @@ npx agent-control-plane@latest setup
313
365
  The wizard walks you through the full setup in one pass:
314
366
 
315
367
  1. Detects the current repo and suggests sane defaults
316
- 2. Installs missing dependencies and authenticates `gh`
368
+ 2. Captures forge mode (`github` or `gitea`) and the auth/settings that mode needs
317
369
  3. Checks backend readiness (API keys for openclaw/pi, local server for ollama)
318
370
  4. Scaffolds the profile, runs health checks, starts the runtime
319
371
  5. Launches the monitoring dashboard in the background
@@ -343,16 +395,41 @@ npx agent-control-plane@latest setup \
343
395
  With `--json`, ACP emits a single structured object on `stdout` and sends
344
396
  progress logs to `stderr`, which keeps parsing stable.
345
397
 
398
+ Example: local-first Gitea setup
399
+
400
+ ```bash
401
+ npx agent-control-plane@latest setup \
402
+ --forge-provider gitea \
403
+ --repo-slug acp-admin/my-repo \
404
+ --gitea-base-url http://127.0.0.1:3000 \
405
+ --gitea-token <token> \
406
+ --start-runtime \
407
+ --start-dashboard
408
+ ```
409
+
410
+ This writes the forge settings into the profile `runtime.env`, so later
411
+ heartbeat, reconcile, and publish steps keep talking to the same Gitea
412
+ instance without extra shell exports.
413
+
346
414
  ### Option B — Manual setup
347
415
 
348
416
  If you prefer explicit control over each step:
349
417
 
350
- **1. Authenticate GitHub**
418
+ **1. Authenticate the working forge**
351
419
 
352
420
  ```bash
353
421
  gh auth login
354
422
  ```
355
423
 
424
+ For Gitea local-first, skip `gh auth login` and pass Gitea settings directly to
425
+ `setup` or `init`:
426
+
427
+ ```bash
428
+ --forge-provider gitea \
429
+ --gitea-base-url http://127.0.0.1:3000 \
430
+ --gitea-token <token>
431
+ ```
432
+
356
433
  **2. Install the packaged runtime**
357
434
 
358
435
  ```bash
@@ -368,6 +445,7 @@ re-run after upgrades.
368
445
  npx agent-control-plane@latest init \
369
446
  --profile-id my-repo \
370
447
  --repo-slug owner/my-repo \
448
+ --forge-provider github \
371
449
  --repo-root ~/src/my-repo \
372
450
  --agent-root ~/.agent-runtime/projects/my-repo \
373
451
  --worktree-root ~/src/my-repo-worktrees \
@@ -377,7 +455,9 @@ npx agent-control-plane@latest init \
377
455
  | Flag | Purpose |
378
456
  | --- | --- |
379
457
  | `--profile-id` | Short name used in all ACP commands |
380
- | `--repo-slug` | GitHub repo ACP should track |
458
+ | `--repo-slug` | Forge repo ACP should track |
459
+ | `--forge-provider` | Which forge ACP should automate (`github` or `gitea`) |
460
+ | `--gitea-base-url` | Base URL when `--forge-provider gitea` |
381
461
  | `--repo-root` | Path to your local checkout |
382
462
  | `--agent-root` | Where ACP keeps per-project runtime state |
383
463
  | `--worktree-root` | Where ACP places repo worktrees |
@@ -407,9 +487,8 @@ grouped and inspectable without digging through scattered temp files.
407
487
  ## Starter Issues
408
488
 
409
489
  The setup wizard can create a set of recurring `agent-keep-open` issues on your
410
- repo so ACP starts working immediately after installation. Each issue carries the
411
- `agent-ready` and `agent-keep-open` labels, and ACP picks them up on its next
412
- heartbeat cycle.
490
+ repo so ACP starts working immediately after installation. ACP picks them up on
491
+ its next heartbeat cycle without requiring a separate readiness label.
413
492
 
414
493
  Built-in templates:
415
494
 
@@ -421,9 +500,9 @@ Built-in templates:
421
500
  | Dependency audit | Fix vulnerabilities and update safe patches |
422
501
  | Refactoring sweep | Reduce complexity and duplication |
423
502
 
424
- You can also create your own recurring issues just add the `agent-ready` and
425
- `agent-keep-open` labels to any GitHub issue and ACP will work on it
426
- continuously.
503
+ You can also create your own recurring issues by adding the
504
+ `agent-keep-open` label to any GitHub issue. ACP will keep revisiting that
505
+ issue continuously unless it is blocked or already claimed by an open agent PR.
427
506
 
428
507
  To skip this step during setup, pass `--no-create-starter-issues`.
429
508
 
@@ -472,6 +551,40 @@ npx agent-control-plane@latest launchd-uninstall --profile-id my-repo
472
551
 
473
552
  These commands are macOS-only and manage per-user `launchd` agents.
474
553
 
554
+ ## Windows (WSL2) Autostart
555
+
556
+ ACP runs inside WSL2 (Windows Subsystem for Linux) where it uses systemd for service management. This requires WSL2 with systemd enabled (Windows 11 22H2+).
557
+
558
+ **Prerequisites:**
559
+ 1. Install WSL2 with Ubuntu: `wsl --install -d Ubuntu` (in PowerShell Admin)
560
+ 2. Enable systemd in WSL2 (create `/etc/wsl.conf` with `[boot] systemd=true`)
561
+ 3. Run `wsl --shutdown` from PowerShell, then restart WSL2
562
+
563
+ **Install project service in WSL2:**
564
+
565
+ ```bash
566
+ # Inside WSL2 Ubuntu terminal
567
+ cd /mnt/c/Users/You/Projects/your-repo
568
+
569
+ # Bootstrap systemd service (same as Linux)
570
+ agent-control-plane project systemd-bootstrap \
571
+ --project-dir . \
572
+ --repo-url https://github.com/your-org/your-repo.git \
573
+ --worker-type claude \
574
+ --schedule "*/30 * * * *" \
575
+ --issues "1,2,3"
576
+ ```
577
+
578
+ **Manage the service:**
579
+
580
+ ```bash
581
+ systemctl --user daemon-reload
582
+ systemctl --user enable --now agent-control-plane@$(basename $(pwd)).timer
583
+ systemctl --user status agent-control-plane@$(basename $(pwd)).timer
584
+ ```
585
+
586
+ See [WSL2_SETUP.md](docs/WSL2_SETUP.md) for full setup guide, Docker in WSL2, and troubleshooting.
587
+
475
588
  ## Update
476
589
 
477
590
  After upgrading the package, refresh the runtime and verify health:
@@ -7,7 +7,7 @@
7
7
  {
8
8
  "id": "issue-implementation",
9
9
  "kind": "issue",
10
- "trigger": "Open issue with agent-ready and without agent-running/agent-blocked",
10
+ "trigger": "Open issue without agent-running, without an open agent PR claim, and not blocked by retry policy",
11
11
  "entrypoint": "tools/bin/start-issue-worker.sh",
12
12
  "summary": "Primary implementation loop for focused issues that should end in a PR or a blocked report."
13
13
  },
package/bin/pr-risk.sh CHANGED
@@ -26,10 +26,10 @@ fi
26
26
  gh_api_json_matching_or_fallback() {
27
27
  local fallback="${1:?fallback required}"
28
28
  local jq_filter="${2:?jq filter required}"
29
- shift 2
29
+ local route="${3:?route required}"
30
30
  local output=""
31
31
 
32
- output="$(gh api "$@" 2>/dev/null || true)"
32
+ output="$(flow_github_api_repo "${REPO_SLUG}" "${route}" 2>/dev/null || true)"
33
33
  if jq -e "${jq_filter}" >/dev/null 2>&1 <<<"${output}"; then
34
34
  printf '%s\n' "${output}"
35
35
  return 0
@@ -38,17 +38,32 @@ gh_api_json_matching_or_fallback() {
38
38
  printf '%s\n' "${fallback}"
39
39
  }
40
40
 
41
- PR_JSON="$(gh pr view "$PR_NUMBER" -R "$REPO_SLUG" --json number,title,url,body,isDraft,headRefName,headRefOid,baseRefName,labels,files,mergeStateStatus,reviewDecision,reviewRequests,statusCheckRollup,comments 2>/dev/null)" \
42
- || { printf 'pr-risk: gh pr view failed for PR %s (repo: %s)\n' "$PR_NUMBER" "$REPO_SLUG" >&2; exit 1; }
41
+ PR_JSON="$(flow_github_pr_view_json "$REPO_SLUG" "$PR_NUMBER" 2>/dev/null || true)"
42
+ if ! jq -e '.number? != null' >/dev/null 2>&1 <<<"${PR_JSON:-}"; then
43
+ if flow_using_gitea; then
44
+ printf 'pr-risk: forge PR view failed for PR %s (repo: %s)\n' "$PR_NUMBER" "$REPO_SLUG" >&2
45
+ exit 1
46
+ fi
47
+ PR_JSON="$(gh pr view "$PR_NUMBER" -R "$REPO_SLUG" --json number,title,url,body,isDraft,headRefName,headRefOid,baseRefName,labels,files,mergeStateStatus,reviewDecision,reviewRequests,statusCheckRollup,comments 2>/dev/null)" \
48
+ || { printf 'pr-risk: forge PR view failed for PR %s (repo: %s)\n' "$PR_NUMBER" "$REPO_SLUG" >&2; exit 1; }
49
+ fi
43
50
  PR_HEAD_SHA="$(jq -r '.headRefOid // ""' <<<"$PR_JSON")"
44
51
  PR_HEAD_COMMITTED_AT=""
45
52
  if [[ -n "${PR_HEAD_SHA}" ]]; then
46
- PR_HEAD_COMMITTED_AT="$(gh api "repos/${REPO_SLUG}/commits/${PR_HEAD_SHA}" --jq .commit.committer.date 2>/dev/null || true)"
53
+ PR_HEAD_COMMITTED_AT="$(
54
+ flow_github_api_repo "${REPO_SLUG}" "commits/${PR_HEAD_SHA}" 2>/dev/null \
55
+ | jq -r '.commit.committer.date // .commit.author.date // .created // .timestamp // ""' 2>/dev/null \
56
+ || true
57
+ )"
58
+ fi
59
+ if flow_using_gitea; then
60
+ REVIEW_COMMENTS_JSON='[]'
61
+ else
62
+ REVIEW_COMMENTS_JSON="$(gh_api_json_matching_or_fallback '[]' 'type == "array"' "pulls/${PR_NUMBER}/comments")"
47
63
  fi
48
- REVIEW_COMMENTS_JSON="$(gh_api_json_matching_or_fallback '[]' 'type == "array"' "repos/${REPO_SLUG}/pulls/${PR_NUMBER}/comments")"
49
64
  CHECK_RUNS_JSON='{"check_runs":[]}'
50
65
  if [[ -n "${PR_HEAD_SHA}" ]]; then
51
- CHECK_RUNS_JSON="$(gh_api_json_matching_or_fallback '{"check_runs":[]}' 'type == "object" and ((.check_runs // []) | type == "array")' "repos/${REPO_SLUG}/commits/${PR_HEAD_SHA}/check-runs")"
66
+ CHECK_RUNS_JSON="$(gh_api_json_matching_or_fallback '{"check_runs":[]}' 'type == "object" and ((.check_runs // []) | type == "array")' "commits/${PR_HEAD_SHA}/check-runs")"
52
67
  fi
53
68
 
54
69
  PR_JSON="$PR_JSON" PR_HEAD_SHA="$PR_HEAD_SHA" PR_HEAD_COMMITTED_AT="$PR_HEAD_COMMITTED_AT" REVIEW_COMMENTS_JSON="$REVIEW_COMMENTS_JSON" CHECK_RUNS_JSON="$CHECK_RUNS_JSON" PR_LANE_OVERRIDE="${PR_LANE_OVERRIDE:-}" MANAGED_PR_PREFIXES_JSON="$MANAGED_PR_PREFIXES_JSON" MANAGED_PR_ISSUE_CAPTURE_REGEX="$MANAGED_PR_ISSUE_CAPTURE_REGEX" ALLOW_INFRA_CI_BYPASS="$ALLOW_INFRA_CI_BYPASS" LOCAL_FIRST_PR_POLICY="$LOCAL_FIRST_PR_POLICY" node <<'EOF'
@@ -100,7 +100,7 @@ fi
100
100
  bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$PR_NUMBER" "${args[@]}" >/dev/null
101
101
 
102
102
  if [[ -n "$linked_issue_id" ]]; then
103
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$linked_issue_id" --remove agent-ready --remove agent-running >/dev/null || true
103
+ bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$linked_issue_id" --remove agent-running >/dev/null || true
104
104
  fi
105
105
 
106
106
  printf 'PR_NUMBER=%s\n' "$PR_NUMBER"
@@ -36,13 +36,115 @@ HEARTBEAT_SNAPSHOT_CACHE_DIR="${TMPDIR:-/tmp}/heartbeat-snapshot.$$"
36
36
  # pipes), so in-memory variables would be lost immediately. We rely exclusively
37
37
  # on the PID-scoped disk cache under HEARTBEAT_SNAPSHOT_CACHE_DIR.
38
38
 
39
+ heartbeat_github_mirror_dir() {
40
+ [[ -n "${STATE_ROOT:-}" ]] || return 1
41
+ printf '%s/github-mirror/heartbeat\n' "${STATE_ROOT}"
42
+ }
43
+
44
+ heartbeat_github_mirror_file() {
45
+ local mirror_key="${1:?mirror key required}"
46
+ local mirror_dir=""
47
+ mirror_dir="$(heartbeat_github_mirror_dir)" || return 1
48
+ printf '%s/%s.json\n' "${mirror_dir}" "${mirror_key}"
49
+ }
50
+
51
+ heartbeat_json_matches_kind() {
52
+ local payload="${1:-}"
53
+ local expected_kind="${2:-}"
54
+
55
+ [[ -n "${payload}" && -n "${expected_kind}" ]] || return 1
56
+ jq -e --arg kind "${expected_kind}" 'type == $kind' >/dev/null <<<"${payload}"
57
+ }
58
+
59
+ heartbeat_write_json_mirror() {
60
+ local mirror_key="${1:?mirror key required}"
61
+ local payload="${2:-}"
62
+ local expected_kind="${3:?expected kind required}"
63
+ local source_mode="${4:-live}"
64
+ local mirror_file=""
65
+ local mirror_dir=""
66
+ local meta_file=""
67
+ local tmp_file=""
68
+ local meta_tmp_file=""
69
+ local updated_at=""
70
+
71
+ heartbeat_json_matches_kind "${payload}" "${expected_kind}" || return 1
72
+ mirror_dir="$(heartbeat_github_mirror_dir)" || return 1
73
+ mirror_file="${mirror_dir}/${mirror_key}.json"
74
+ meta_file="${mirror_dir}/${mirror_key}.env"
75
+ mkdir -p "${mirror_dir}"
76
+ updated_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
77
+ tmp_file="${mirror_file}.tmp.$$"
78
+ meta_tmp_file="${meta_file}.tmp.$$"
79
+ printf '%s' "${payload}" >"${tmp_file}"
80
+ {
81
+ printf 'MIRROR_KEY=%s\n' "${mirror_key}"
82
+ printf 'JSON_KIND=%s\n' "${expected_kind}"
83
+ printf 'SOURCE=%s\n' "${source_mode}"
84
+ printf 'UPDATED_AT=%s\n' "${updated_at}"
85
+ } >"${meta_tmp_file}"
86
+ mv "${tmp_file}" "${mirror_file}"
87
+ mv "${meta_tmp_file}" "${meta_file}"
88
+ }
89
+
90
+ heartbeat_read_json_mirror() {
91
+ local mirror_key="${1:?mirror key required}"
92
+ local expected_kind="${2:?expected kind required}"
93
+ local default_json="${3:-}"
94
+ local mirror_file=""
95
+ local payload=""
96
+
97
+ mirror_file="$(heartbeat_github_mirror_file "${mirror_key}" 2>/dev/null || true)"
98
+ if [[ -n "${mirror_file}" && -f "${mirror_file}" ]]; then
99
+ payload="$(cat "${mirror_file}" 2>/dev/null || true)"
100
+ if heartbeat_json_matches_kind "${payload}" "${expected_kind}"; then
101
+ printf '%s\n' "${payload}"
102
+ return 0
103
+ fi
104
+ fi
105
+
106
+ printf '%s\n' "${default_json}"
107
+ }
108
+
109
+ heartbeat_cached_json_with_local_mirror() {
110
+ local cache_file="${1:?cache file required}"
111
+ local mirror_key="${2:?mirror key required}"
112
+ local expected_kind="${3:?expected kind required}"
113
+ local default_json="${4:-}"
114
+ local live_fetch_fn="${5:?live fetch function required}"
115
+ local payload=""
116
+
117
+ shift 5
118
+
119
+ if [[ -f "${cache_file}" ]]; then
120
+ cat "${cache_file}"
121
+ return 0
122
+ fi
123
+
124
+ if payload="$("${live_fetch_fn}" "$@" 2>/dev/null)" && heartbeat_json_matches_kind "${payload}" "${expected_kind}"; then
125
+ heartbeat_write_json_mirror "${mirror_key}" "${payload}" "${expected_kind}" "live" || true
126
+ else
127
+ payload="$(heartbeat_read_json_mirror "${mirror_key}" "${expected_kind}" "${default_json}")"
128
+ fi
129
+
130
+ printf '%s' "${payload}" >"${cache_file}"
131
+ printf '%s\n' "${payload}"
132
+ }
133
+
39
134
  heartbeat_cached_issue_list_json() {
40
135
  mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
41
136
  local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/issues.json"
42
137
  if [[ ! -f "${cache_file}" ]]; then
43
- local snapshot
44
- snapshot="$(flow_github_issue_list_json "$REPO_SLUG" open 100 2>/dev/null || true)"
45
- printf '%s' "${snapshot}" >"${cache_file}"
138
+ heartbeat_cached_json_with_local_mirror \
139
+ "${cache_file}" \
140
+ "issues-open-100" \
141
+ "array" \
142
+ '[]' \
143
+ flow_github_issue_list_json_live \
144
+ "$REPO_SLUG" \
145
+ open \
146
+ 100
147
+ return 0
46
148
  fi
47
149
  cat "${cache_file}"
48
150
  }
@@ -51,9 +153,16 @@ heartbeat_cached_pr_list_json() {
51
153
  mkdir -p "${HEARTBEAT_SNAPSHOT_CACHE_DIR}"
52
154
  local cache_file="${HEARTBEAT_SNAPSHOT_CACHE_DIR}/prs.json"
53
155
  if [[ ! -f "${cache_file}" ]]; then
54
- local snapshot
55
- snapshot="$(flow_github_pr_list_json "$REPO_SLUG" open 100 2>/dev/null || true)"
56
- printf '%s' "${snapshot}" >"${cache_file}"
156
+ heartbeat_cached_json_with_local_mirror \
157
+ "${cache_file}" \
158
+ "prs-open-100" \
159
+ "array" \
160
+ '[]' \
161
+ flow_github_pr_list_json_live \
162
+ "$REPO_SLUG" \
163
+ open \
164
+ 100
165
+ return 0
57
166
  fi
58
167
  cat "${cache_file}"
59
168
  }
@@ -111,7 +220,6 @@ heartbeat_retry_reason_is_baseline_blocked() {
111
220
  heartbeat_issue_json_cached() {
112
221
  local issue_id="${1:?issue id required}"
113
222
  local cache_file=""
114
- local issue_json=""
115
223
 
116
224
  if [[ ! -d "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}" ]]; then
117
225
  mkdir -p "${HEARTBEAT_ISSUE_JSON_CACHE_DIR}"
@@ -123,9 +231,14 @@ heartbeat_issue_json_cached() {
123
231
  return 0
124
232
  fi
125
233
 
126
- issue_json="$(flow_github_issue_view_json "$REPO_SLUG" "$issue_id" 2>/dev/null || true)"
127
- printf '%s' "${issue_json}" >"${cache_file}"
128
- printf '%s\n' "${issue_json}"
234
+ heartbeat_cached_json_with_local_mirror \
235
+ "${cache_file}" \
236
+ "issue-${issue_id}" \
237
+ "object" \
238
+ '{}' \
239
+ flow_github_issue_view_json_live \
240
+ "$REPO_SLUG" \
241
+ "$issue_id"
129
242
  }
130
243
 
131
244
  heartbeat_open_agent_pr_issue_ids() {
@@ -483,9 +596,9 @@ heartbeat_mark_issue_running() {
483
596
  local cached_json
484
597
  cached_json="$(heartbeat_issue_json_cached "$issue_id" 2>/dev/null || true)"
485
598
  if [[ "$is_heavy" == "yes" ]]; then
486
- ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null || true
599
+ ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-blocked --add agent-running --add agent-e2e-heavy >/dev/null || true
487
600
  else
488
- ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-ready --remove agent-blocked --add agent-running >/dev/null || true
601
+ ACP_CACHED_ISSUE_JSON="${cached_json}" bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "$REPO_SLUG" --number "$issue_id" --remove agent-blocked --add agent-running >/dev/null || true
489
602
  fi
490
603
  }
491
604
 
@@ -218,7 +218,7 @@ issue_publish_extra_args() {
218
218
  }
219
219
 
220
220
  issue_remove_running() {
221
- bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "${REPO_SLUG}" --number "$ISSUE_ID" --remove agent-ready --remove agent-running --remove agent-blocked >/dev/null || true
221
+ bash "${FLOW_TOOLS_DIR}/agent-github-update-labels" --repo-slug "${REPO_SLUG}" --number "$ISSUE_ID" --remove agent-running --remove agent-blocked >/dev/null || true
222
222
  }
223
223
 
224
224
  issue_mark_blocked() {
@@ -75,7 +75,7 @@ pr_cleanup_linked_issue_session() {
75
75
 
76
76
  local should_close
77
77
  should_close="$(pr_linked_issue_should_close "$issue_id")"
78
- update_args=(--remove agent-ready --remove agent-running --remove agent-blocked --remove agent-e2e-heavy --remove agent-automerge --remove agent-exclusive)
78
+ update_args=(--remove agent-running --remove agent-blocked --remove agent-e2e-heavy --remove agent-automerge --remove agent-exclusive)
79
79
  pr_best_effort_update_labels --repo-slug "${REPO_SLUG}" --number "$issue_id" "${update_args[@]}"
80
80
 
81
81
  local issue_session="${ISSUE_SESSION_PREFIX}${issue_id}"