forgeo-cli 0.8.0__tar.gz → 0.10.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 (87) hide show
  1. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/CHANGELOG.md +50 -1
  2. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/PKG-INFO +26 -8
  3. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/README.md +25 -7
  4. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/backlog.md +96 -7
  5. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/cli-reference.md +2 -0
  6. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/configuration.md +54 -3
  7. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/getting-started.md +32 -16
  8. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/web-console-api.md +30 -9
  9. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/install.sh +1 -1
  10. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/pyproject.toml +1 -1
  11. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/__init__.py +1 -1
  12. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/agent.py +25 -1
  13. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/backlog.py +148 -178
  14. forgeo_cli-0.10.0/src/forgeo/backlog_github.py +639 -0
  15. forgeo_cli-0.10.0/src/forgeo/backlog_gitlab.py +617 -0
  16. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_http.py +2 -2
  17. forgeo_cli-0.10.0/src/forgeo/backlog_issue_base.py +183 -0
  18. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_jira.py +34 -151
  19. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/central.py +101 -4
  20. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/daemon_control.py +1 -1
  21. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/forgeo.py +1 -1
  22. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/models.py +220 -5
  23. forgeo_cli-0.10.0/src/forgeo/setup.py +462 -0
  24. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/validate.py +28 -27
  25. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/central.css +92 -0
  26. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/central.js +82 -2
  27. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/instance.html +16 -1
  28. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/conftest.py +20 -7
  29. forgeo_cli-0.10.0/tests/test_backlog_github.py +259 -0
  30. forgeo_cli-0.10.0/tests/test_backlog_gitlab.py +237 -0
  31. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_cli.py +7 -32
  32. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_setup.py +54 -3
  33. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_web.py +1 -1
  34. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_web_lock.py +1 -1
  35. forgeo_cli-0.8.0/src/forgeo/setup.py +0 -230
  36. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/.github/workflows/ci.yml +0 -0
  37. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/.gitignore +0 -0
  38. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/CONTRIBUTING.md +0 -0
  39. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/LICENSE +0 -0
  40. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/config/nginx-forgeo.conf +0 -0
  41. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/agent-contract.md +0 -0
  42. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/img/console.png +0 -0
  43. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/img/demo.gif +0 -0
  44. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/img/logo.png +0 -0
  45. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/img/og.png +0 -0
  46. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/img/title.svg +0 -0
  47. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/docs/index.md +0 -0
  48. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/forgeo.spec +0 -0
  49. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/mkdocs.yml +0 -0
  50. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/scripts/__init__.py +0 -0
  51. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/scripts/render_homebrew_formula.py +0 -0
  52. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/__main__.py +0 -0
  53. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/cli.py +0 -0
  54. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/config.py +0 -0
  55. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/daemon.py +0 -0
  56. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/git.py +0 -0
  57. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/instances.py +0 -0
  58. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/io.py +0 -0
  59. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/notify.py +0 -0
  60. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/oauth.py +0 -0
  61. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/paths.py +0 -0
  62. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/runs.py +0 -0
  63. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/update.py +0 -0
  64. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/index.html +0 -0
  65. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/login.html +0 -0
  66. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web/style.css +0 -0
  67. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/src/forgeo/web_common.py +0 -0
  68. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_agent.py +0 -0
  69. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_backlog.py +0 -0
  70. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_backlog_http.py +0 -0
  71. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_backlog_jira.py +0 -0
  72. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_daemon.py +0 -0
  73. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_factory.py +0 -0
  74. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_git.py +0 -0
  75. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_install.py +0 -0
  76. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_instances.py +0 -0
  77. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_io.py +0 -0
  78. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_models.py +0 -0
  79. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_oauth.py +0 -0
  80. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_paths.py +0 -0
  81. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_remote_backlog_cycle.py +0 -0
  82. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_render_homebrew.py +0 -0
  83. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_runs.py +0 -0
  84. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_update.py +0 -0
  85. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/tests/test_web_common.py +0 -0
  86. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/www/404.html +0 -0
  87. {forgeo_cli-0.8.0 → forgeo_cli-0.10.0}/www/index.html +0 -0
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0] - 2026-08-24
11
+
12
+ ### Added
13
+
14
+ - `forgeo init` now asks for **backlog provider** (`file`/`github`/`gitlab`/`jira`/`http`). For `github` it auto-detects `owner/repo` from `git remote origin`, prompts for `token_env` (`GITHUB_TOKEN`) and can persist a pasted classic PAT (`ghp_...`, scope `repo`) to `~/.config/forgeo/github_token_env.sh` (600, wired to `~/.bashrc`). Same for `gitlab` (`GITLAB_TOKEN`, base URL) and `jira`/`http`. The wizard writes `backlog: https://api.github.com`, `backlog_provider: github`, `github: {repo, auth: {token_env}}` and `state_dir: .forgeo` automatically.
15
+ - Web console now treats `jira`/`github`/`gitlab` as a **read-mostly mirror** instead of a replacement: the home page cards show `Open in Jira/GitHub/GitLab ↗` (via `external_board_url`/`external_board_label`), the instance page shows a top banner linking to the native board, and each task card/modal links to the native issue (`external_url`). Document backlogs (`file`/`http`) keep the existing primary-editor behaviour. Creating/editing still works through the dashboard, but triage is expected in the native tracker where Forgeo-specific state (BLOCKED/FAILED reasons, `agent_response`, retry budget) is now surfaced on the mirror.
16
+ - Central API now exposes `backlog_provider`, `backlog`, `backlog_is_issue_provider`, `external_board_url`/`external_board_label` on `GET /api/instances` and `GET /api/instances/<name>/status`, and `external_url` per task on `GET /api/instances/<name>/tasks` (+ single-task) for issue providers. Covers `https://api.github.com` → `https://github.com` and `https://…/api/v3` → web base mapping for GitHub Enterprise, and `https://jira…/issues/?jql=` / `https://gitlab…/{repo}/-/issues` board links.
17
+
18
+ ### Fixed
19
+
20
+ - GitHub provider now encodes `owner/repo` as two path segments (`quote` per segment, keeping `/`) instead of `quote(repo, safe='')` which produced `owner%2Frepo` and 404 on every `GET /repos/{owner%2Frepo}/issues`.
21
+
22
+ ### Changed
23
+
24
+ - `README`, `docs/backlog.md`, `docs/getting-started.md` and `docs/web-console-api.md` document the mirror vs editor split and the new `external_*` API fields.
25
+ - `src/forgeo/central.py` deduplicates provider metadata via `_backlog_meta()` and centralises GitHub web-base handling; `instance-card` is now a `div[role=link]` to allow nested external links without invalid HTML.
26
+ - `README`, `docs/getting-started.md` and `docs/cli-reference.md` document the new backlog-provider step in `forgeo init` and the `GITHUB_TOKEN` PAT setup (`https://github.com/settings/tokens/new`, scope `repo`).
27
+
28
+ ## [0.9.0] - 2026-08-23
29
+
30
+ ### Added
31
+
32
+ - GitHub Issues and GitLab Issues task providers. Set `backlog_provider: github`
33
+ or `gitlab` and point `backlog:` at the API base URL
34
+ (`https://api.github.com` or `https://github.example.com/api/v3` for
35
+ Enterprise; `https://gitlab.com` or a self-hosted root for GitLab). Issue
36
+ numbers/`iid`s become Forgeo task ids, `open`/`opened` vs `closed` maps to
37
+ `OPEN`/`COMPLETED`, and `forgeo-running`/`forgeo-blocked`/`forgeo-failed`
38
+ labels (configurable via `label_prefix`) represent the remaining states.
39
+ Engine state — blocker and failure reasons, retry counters, claim time,
40
+ dependencies, and bounded agent output — is stored in a hidden
41
+ `<!-- forgeo: {...} -->` block inside the issue body/description, so the
42
+ visible text stays human-readable and no custom fields or issue properties
43
+ are required.
44
+ - `github` and `gitlab` config blocks: `repo` (`owner/repo` or project path/id),
45
+ `token_env` (PAT from an environment variable, never stored in the file),
46
+ `label_prefix`/`property_key`, pagination, timeouts, stale-claim recovery
47
+ via `claim_timeout_seconds`, and optional workflow/field mappings mirroring
48
+ the Jira provider.
49
+ - Shared issue-provider helpers extracted to `backlog_issue_base` and a new
50
+ `DocumentBacklogStore` / `IssueBacklogBase` split in `backlog.py`,
51
+ unifying claim, label, and engine-state handling across Jira, GitHub, and
52
+ GitLab. `forgeo validate` now checks any remote backlog with a
53
+ provider-specific message, and `config/forgeo.yaml`, the README, and the
54
+ backlog/configuration docs list and document all five providers
55
+ (`file`, `http`, `jira`, `github`, `gitlab`).
56
+
10
57
  ## [0.8.0] - 2026-08-21
11
58
 
12
59
  ### Added
@@ -360,7 +407,9 @@ Initial release of the scheduled, agent-driven software forgeo.
360
407
  overlapping-run skipping.
361
408
  - Dogfooding docs removed; local configs kept out of the repository.
362
409
 
363
- [Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v0.8.0...HEAD
410
+ [Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v0.10.0...HEAD
411
+ [0.10.0]: https://github.com/lucaGazzola/forgeo/compare/v0.9.0...v0.10.0
412
+ [0.9.0]: https://github.com/lucaGazzola/forgeo/compare/v0.8.0...v0.9.0
364
413
  [0.8.0]: https://github.com/lucaGazzola/forgeo/compare/v0.7.3...v0.8.0
365
414
  [0.7.3]: https://github.com/lucaGazzola/forgeo/compare/v0.7.2...v0.7.3
366
415
  [0.7.2]: https://github.com/lucaGazzola/forgeo/compare/v0.7.1...v0.7.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forgeo-cli
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: A scheduled software forgeo: executes backlog tasks on main, refactors when idle, and writes BLOCKER.md when it needs human input.
5
5
  Project-URL: Homepage, https://forgeo.org
6
6
  Project-URL: Documentation, https://forgeo.org
@@ -110,6 +110,10 @@ forgeo init
110
110
 
111
111
  Guided wizard, run from your project root. Writes `forgeo.yaml` (the
112
112
  config) and a `.forgeo/` folder for the backlog, logs and blocker files.
113
+ The wizard asks for the backlog provider (`file` for a local JSON file, or
114
+ `github`/`gitlab`/`jira`/`http` for an external tracker — for `github` it
115
+ auto-detects `owner/repo` from `git remote` and can persist a pasted
116
+ `GITHUB_TOKEN` to `~/.config/forgeo/github_token_env.sh`).
113
117
 
114
118
  The base flow is then three steps: fill the backlog, check the
115
119
  configuration, start the daemon.
@@ -121,8 +125,10 @@ configuration, start the daemon.
121
125
  # Backlog format), created on first use:
122
126
  # .forgeo/backlog.json
123
127
 
124
- # Or: configure `backlog_provider: jira` and set `backlog` to a Jira base URL
125
- # (see the Jira backlog documentation).
128
+ # Or: pick github/gitlab/jira in forgeo init (or configure forgeo.yaml
129
+ # manually): set backlog_provider: github and backlog: https://api.github.com
130
+ # with github.repo + token_env, then export GITHUB_TOKEN and run
131
+ # forgeo validate. See backlog docs for Jira/GitHub/GitLab.
126
132
 
127
133
  # Or: add tasks from the web console once your forgeo is registered
128
134
  # (first `forgeo start` registers it automatically):
@@ -211,15 +217,27 @@ and get one aggregate overview with the central dashboard, `forgeo web`.
211
217
  | Web dashboard & HTTP API | [Web console & HTTP API](docs/web-console-api.md) |
212
218
 
213
219
  Everything is stored in plain files: the local backlog, `forgeo.log`, and
214
- `BLOCKER.md` whenever a decision is pending. The backlog can also live in
215
- another application behind an `http(s)` URL, or in Jira. HTTP backlogs
216
- exchange the complete task document; Jira issues are read and transitioned
217
- individually with workflow state and engine metadata stored on the issue (see
218
- [Backlog format](docs/backlog.md)). A *file* backlog is snapshotted (rotating
220
+ `BLOCKER.md` whenever a decision is pending. Backlog providers (see
221
+ [Backlog format](docs/backlog.md)):
222
+
223
+ - [JSON file](docs/backlog.md#a-json-file-backlog)
224
+ - [HTTP endpoint](docs/backlog.md#a-backlog-over-http)
225
+ - [Jira](docs/backlog.md#a-jira-backlog)
226
+ - [GitHub](docs/backlog.md#a-github-backlog)
227
+ - [GitLab](docs/backlog.md#a-gitlab-backlog)
228
+
229
+ File and HTTP backlogs exchange the complete task document; Jira/GitHub/GitLab issues are read and transitioned
230
+ individually with workflow state and engine metadata stored on the issue. A *file* backlog is snapshotted (rotating
219
231
  `backlog.json.bak` files) before every agent run and on daemon startup, and
220
232
  restored automatically if it is ever found corrupt — a bad write never loses
221
233
  your tasks.
222
234
 
235
+ The central dashboard (`forgeo web`) mirrors every instance: for `file`/`http` it is the primary editor; for
236
+ `jira`/`github`/`gitlab` it is a read-mostly mirror of the native tracker — each task card and its detail modal link
237
+ to the native issue (`Open in Jira/GitHub/GitLab ↗`), a top banner links to the native board, and the board surfaces
238
+ Forgeo-specific state the native UI does not (BLOCKED/FAILED reasons, `agent_response`, retry budget) — triage stays in
239
+ the tracker (see [Web console & HTTP API](docs/web-console-api.md)).
240
+
223
241
  ## Develop
224
242
 
225
243
  ```bash
@@ -56,6 +56,10 @@ forgeo init
56
56
 
57
57
  Guided wizard, run from your project root. Writes `forgeo.yaml` (the
58
58
  config) and a `.forgeo/` folder for the backlog, logs and blocker files.
59
+ The wizard asks for the backlog provider (`file` for a local JSON file, or
60
+ `github`/`gitlab`/`jira`/`http` for an external tracker — for `github` it
61
+ auto-detects `owner/repo` from `git remote` and can persist a pasted
62
+ `GITHUB_TOKEN` to `~/.config/forgeo/github_token_env.sh`).
59
63
 
60
64
  The base flow is then three steps: fill the backlog, check the
61
65
  configuration, start the daemon.
@@ -67,8 +71,10 @@ configuration, start the daemon.
67
71
  # Backlog format), created on first use:
68
72
  # .forgeo/backlog.json
69
73
 
70
- # Or: configure `backlog_provider: jira` and set `backlog` to a Jira base URL
71
- # (see the Jira backlog documentation).
74
+ # Or: pick github/gitlab/jira in forgeo init (or configure forgeo.yaml
75
+ # manually): set backlog_provider: github and backlog: https://api.github.com
76
+ # with github.repo + token_env, then export GITHUB_TOKEN and run
77
+ # forgeo validate. See backlog docs for Jira/GitHub/GitLab.
72
78
 
73
79
  # Or: add tasks from the web console once your forgeo is registered
74
80
  # (first `forgeo start` registers it automatically):
@@ -157,15 +163,27 @@ and get one aggregate overview with the central dashboard, `forgeo web`.
157
163
  | Web dashboard & HTTP API | [Web console & HTTP API](docs/web-console-api.md) |
158
164
 
159
165
  Everything is stored in plain files: the local backlog, `forgeo.log`, and
160
- `BLOCKER.md` whenever a decision is pending. The backlog can also live in
161
- another application behind an `http(s)` URL, or in Jira. HTTP backlogs
162
- exchange the complete task document; Jira issues are read and transitioned
163
- individually with workflow state and engine metadata stored on the issue (see
164
- [Backlog format](docs/backlog.md)). A *file* backlog is snapshotted (rotating
166
+ `BLOCKER.md` whenever a decision is pending. Backlog providers (see
167
+ [Backlog format](docs/backlog.md)):
168
+
169
+ - [JSON file](docs/backlog.md#a-json-file-backlog)
170
+ - [HTTP endpoint](docs/backlog.md#a-backlog-over-http)
171
+ - [Jira](docs/backlog.md#a-jira-backlog)
172
+ - [GitHub](docs/backlog.md#a-github-backlog)
173
+ - [GitLab](docs/backlog.md#a-gitlab-backlog)
174
+
175
+ File and HTTP backlogs exchange the complete task document; Jira/GitHub/GitLab issues are read and transitioned
176
+ individually with workflow state and engine metadata stored on the issue. A *file* backlog is snapshotted (rotating
165
177
  `backlog.json.bak` files) before every agent run and on daemon startup, and
166
178
  restored automatically if it is ever found corrupt — a bad write never loses
167
179
  your tasks.
168
180
 
181
+ The central dashboard (`forgeo web`) mirrors every instance: for `file`/`http` it is the primary editor; for
182
+ `jira`/`github`/`gitlab` it is a read-mostly mirror of the native tracker — each task card and its detail modal link
183
+ to the native issue (`Open in Jira/GitHub/GitLab ↗`), a top banner links to the native board, and the board surfaces
184
+ Forgeo-specific state the native UI does not (BLOCKED/FAILED reasons, `agent_response`, retry budget) — triage stays in
185
+ the tracker (see [Web console & HTTP API](docs/web-console-api.md)).
186
+
169
187
  ## Develop
170
188
 
171
189
  ```bash
@@ -1,12 +1,24 @@
1
1
  # Backlog format
2
2
 
3
- The backlog is a **plain JSON document**. By default it is a file you edit by
4
- hand, living wherever `backlog:` points in [forgeo.yaml](configuration.md) —
5
- `backlog.json` at the project root, or `.forgeo/backlog.json` when generated by
6
- `forgeo init`. Keep it outside the repository if you can so the agent never
7
- touches it. It can also be [served over HTTP](#a-backlog-over-http) by another
8
- application, in which case the document below is exactly what that endpoint
9
- exchanges with Forgeo, or it can be sourced directly from [Jira](#a-jira-backlog).
3
+ - [JSON file](#a-json-file-backlog)
4
+ - [HTTP endpoint](#a-backlog-over-http)
5
+ - [Jira](#a-jira-backlog)
6
+ - [GitHub](#a-github-backlog)
7
+ - [GitLab](#a-gitlab-backlog)
8
+
9
+ The backlog is a **plain JSON document** — a list of tasks Forgeo works through
10
+ one by one.
11
+
12
+ ## A JSON file backlog
13
+
14
+ By default the backlog is a file you edit by hand, living wherever `backlog:`
15
+ points in [forgeo.yaml](configuration.md) — `backlog.json` at the project root,
16
+ or `.forgeo/backlog.json` when generated by `forgeo init`. Keep it outside the
17
+ repository if you can so the agent never touches it. It can also be
18
+ [served over HTTP](#a-backlog-over-http) by another application, in which case
19
+ the document below is exactly what that endpoint exchanges with Forgeo, or it
20
+ can be sourced directly from [Jira](#a-jira-backlog),
21
+ [GitHub](#a-github-backlog) or [GitLab](#a-gitlab-backlog).
10
22
 
11
23
  ```json
12
24
  {
@@ -132,6 +144,11 @@ You add, remove, or reopen tasks by editing the file directly — or use the
132
144
  the task detail modal's **Edit** button updates an existing task's fields
133
145
  (`PATCH /api/instances/<name>/tasks/<id>`), while its **Delete** button
134
146
  removes an `OPEN` or `BLOCKED` task (`DELETE /api/instances/<name>/tasks/<id>`).
147
+ For `file`/`http` backlogs this is the primary editor; for `jira`/`github`/`gitlab` the web console is a
148
+ read-mostly **mirror** of the native tracker — a top banner links to the external board, each task card/modal
149
+ links to the native issue (`Open in Jira/GitHub/GitLab ↗`), and the board surfaces Forgeo-specific state
150
+ (`blocker_reason`, `failure_reason`, `agent_response`, retry budget) that the tracker does not — triage stays in
151
+ Jira/GitHub/GitLab, Forgeo reflects it (see [Web console & HTTP API](web-console-api.md)).
135
152
 
136
153
  ### Resolving a blocked task
137
154
 
@@ -424,3 +441,75 @@ If a process dies while holding a claim, a later cycle releases claims older
424
441
  than `claim_timeout_seconds` and returns them to the configured open status.
425
442
  An unavailable Jira endpoint fails the cycle; it is never treated as an empty
426
443
  backlog.
444
+
445
+ ## A GitHub backlog
446
+
447
+ Set `backlog_provider: github` and point `backlog:` at the GitHub API base URL:
448
+
449
+ ```yaml
450
+ backlog_provider: github
451
+ backlog: https://api.github.com
452
+
453
+ github:
454
+ repo: owner/repo
455
+ token_env: GITHUB_TOKEN
456
+ label_prefix: forgeo
457
+ ```
458
+
459
+ Use `https://api.github.com` for github.com or `https://github.example.com/api/v3` for Enterprise.
460
+
461
+ ### Mapping and lifecycle
462
+
463
+ - GitHub issue numbers are Forgeo task ids.
464
+ - `title`, `body` (visible part), `created_at` and `updated_at` map to task fields.
465
+ - `state` `open` maps to `OPEN`; `closed` maps to `COMPLETED`.
466
+ - Labels `forgeo-running`, `forgeo-blocked`, `forgeo-failed` (prefix configurable via `label_prefix`) represent running, blocked, and failed tasks. An open issue carrying `forgeo-running` is considered claimed and filtered from picking.
467
+ - Closing an issue completes its task; reopening it moves the task back to `OPEN`.
468
+
469
+ Forgeo stores blocker reasons, failure reasons, retry counters, claim time, dependencies, and bounded agent output in a hidden JSON block inside the issue body: `<!-- forgeo: {...} -->`. The visible body remains human-readable; the hidden block is stripped on read and merged on write. No GitHub issue property or custom field is required. Set `github.property_key` only for symmetry; the marker key is `forgeo` by default.
470
+
471
+ Dependencies are persisted via the hidden block's `dependencies` list; no GitHub issue links are required.
472
+
473
+ ### Authentication
474
+
475
+ GitHub credentials are never stored in `forgeo.yaml`:
476
+
477
+ - `token_env` names the environment variable holding a personal-access token (classic or fine-grained). The token is sent as `Authorization: Bearer <token>`.
478
+
479
+ ### Runtime behavior
480
+
481
+ The daemon lists GitHub issues with paginated `GET /repos/{owner}/{repo}/issues?state=all`. A task is claimed by adding the `forgeo-running` label and persisting `claimed_at` in the hidden block. If a process dies while holding a claim, a later cycle releases claims older than `claim_timeout_seconds` and removes the running label. An unavailable GitHub endpoint fails the cycle.
482
+
483
+ ## A GitLab backlog
484
+
485
+ Set `backlog_provider: gitlab` and point `backlog:` at the GitLab base URL:
486
+
487
+ ```yaml
488
+ backlog_provider: gitlab
489
+ backlog: https://gitlab.example.com
490
+
491
+ gitlab:
492
+ repo: group/project # or numeric project id
493
+ token_env: GITLAB_TOKEN
494
+ label_prefix: forgeo
495
+ ```
496
+
497
+ GitLab base URL is the instance root (e.g. `https://gitlab.com`); the client appends `/api/v4`.
498
+
499
+ ### Mapping and lifecycle
500
+
501
+ - GitLab issue `iid`s are Forgeo task ids.
502
+ - `title`, `description` (visible part), `created_at` and `updated_at` map to task fields.
503
+ - `state` `opened` maps to `OPEN`; `closed` maps to `COMPLETED`.
504
+ - Labels `forgeo-running`, `forgeo-blocked`, `forgeo-failed` represent running, blocked, and failed tasks, like GitHub. An `opened` issue with `forgeo-running` is filtered as claimed.
505
+ - Closing/reopening via `state_event` transitions the task to `COMPLETED`/`OPEN`.
506
+
507
+ Forgeo stores engine state the same way as GitHub: a hidden `<!-- forgeo: {...} -->` block inside `description`. Dependencies and other task attributes are kept there; no GitLab custom fields are required.
508
+
509
+ ### Authentication
510
+
511
+ - `token_env` names the environment variable holding a personal-access token. Sent as `PRIVATE-TOKEN` and `Authorization: Bearer`.
512
+
513
+ ### Runtime behavior
514
+
515
+ Paginated `GET /api/v4/projects/:id/issues?state=all`. Claiming adds `forgeo-running` and `claimed_at`; stale claims older than `claim_timeout_seconds` are released. An unavailable GitLab endpoint fails the cycle.
@@ -23,6 +23,8 @@ Guided first-time setup: interactively write a `forgeo.yaml`.
23
23
  | <span style="white-space: nowrap">`--config <file>`</span> | Where to write the config (default `forgeo.yaml`). |
24
24
  | <span style="white-space: nowrap">`--force`</span> | Overwrite an existing config file. |
25
25
 
26
+ The wizard asks for: Forgeo folder, backlog provider (`file`/`github`/`gitlab`/`jira`/`http` — for `github` it auto-detects `owner/repo` from `git remote` and can persist `GITHUB_TOKEN`), coding agent command, and refactor prompt.
27
+
26
28
  Exit codes:
27
29
 
28
30
  - `0` — config written.
@@ -22,11 +22,13 @@ paths), so `forgeo restart` is still used for those.
22
22
  | <span style="white-space: nowrap">`interval_minutes`</span> | `60` | How often Forgeo runs (≥ 1). |
23
23
  | <span style="white-space: nowrap">`branch`</span> | `main` | The single branch everything is committed to. |
24
24
  | <span style="white-space: nowrap">`remote`</span> | — | Remote to push to (e.g. `origin`); omit to only commit locally. |
25
- | <span style="white-space: nowrap">`backlog`</span> | `backlog.json` | The task backlog: the path of a JSON file, an HTTP endpoint serving the same document, or a Jira base URL when `backlog_provider: jira`. |
26
- | <span style="white-space: nowrap">`backlog_provider`</span> | `auto` | `auto` infers file/HTTP from `backlog`, or Jira when a `jira` block is present; explicitly choose `file`, `http`, or `jira` when preferred. |
25
+ | <span style="white-space: nowrap">`backlog`</span> | `backlog.json` | The task backlog: the path of a JSON file, an HTTP endpoint serving the same document, or a base URL for `jira`/`github`/`gitlab` providers. |
26
+ | <span style="white-space: nowrap">`backlog_provider`</span> | `auto` | `auto` infers file/HTTP from `backlog`, or `jira`/`github`/`gitlab` when the corresponding block is present; explicitly choose `file`, `http`, `jira`, `github`, or `gitlab`. |
27
27
  | <span style="white-space: nowrap">`state_dir`</span> | — | Directory for Forgeo's runtime files (locks, run history, daemon state). Remote backlogs default this to the directory of `forgeo.yaml`. |
28
- | <span style="white-space: nowrap">`backlog_auth`</span> | — | OAuth2 client credentials for a backlog URL that requires them (see [below](#backlog_auth)). |
28
+ | <span style="white-space: nowrap">`backlog_auth`</span> | — | OAuth2 client credentials for a backlog URL that requires them (see [below](#backlog_auth)). Only for `http` provider. |
29
29
  | <span style="white-space: nowrap">`jira`</span> | — | Jira REST, workflow, authentication, and custom-field settings. Required when `backlog_provider: jira`. |
30
+ | <span style="white-space: nowrap">`github`</span> | — | GitHub REST settings. Required when `backlog_provider: github`. |
31
+ | <span style="white-space: nowrap">`gitlab`</span> | — | GitLab REST settings. Required when `backlog_provider: gitlab`. |
30
32
  | <span style="white-space: nowrap">`blocker_file`</span> | `BLOCKER.md` | Where `BLOCKER.md` is written. Keep it outside the repo so it is never committed. |
31
33
  | <span style="white-space: nowrap">`agent_command`</span> | — | The coding agent: any shell command (string) or argv list. **Required.** |
32
34
  | <span style="white-space: nowrap">`agent_timeout_seconds`</span> | — | Optional: kill the agent after this many seconds (`null` = never). |
@@ -140,6 +142,55 @@ truth for human changes.
140
142
  | `jira.workflow` | defaults | Status ids or names for open, running, blocked, completed, and failed transitions. |
141
143
  | `jira.fields` | — | Optional custom-field ids for task attributes such as acceptance criteria and dependencies. |
142
144
 
145
+ ### GitHub backlog
146
+
147
+ Forgeo can read and update GitHub issues directly. Set `backlog_provider: github` and make `backlog` the GitHub API base URL. Issue numbers become task ids. Labels and a hidden JSON block in the issue body hold Forgeo's engine state.
148
+
149
+ ```yaml
150
+ backlog_provider: github
151
+ backlog: https://api.github.com
152
+ github:
153
+ repo: owner/repo
154
+ token_env: GITHUB_TOKEN
155
+ label_prefix: forgeo
156
+ ```
157
+
158
+ | Key | Default | Meaning |
159
+ | --- | --- | --- |
160
+ | `github.repo` | — | Required owner/repo. |
161
+ | `github.auth` | — | Required PAT env var `token_env`. |
162
+ | `github.label_prefix` | `forgeo` | Prefix for running/blocked/failed labels. |
163
+ | `github.property_key` | `forgeo` | Marker key for hidden body block (symmetry). |
164
+ | `github.page_size` | `30` | Issues per page. |
165
+ | `github.max_issues` | `1000` | Max issues read. |
166
+ | `github.timeout_seconds` | `30` | HTTP timeout. |
167
+ | `github.claim_timeout_seconds` | `86400` | Stale claim timeout. |
168
+ | `github.workflow` | defaults | State/label mapping. |
169
+ | `github.fields` | — | Optional field mappings. |
170
+
171
+ ### GitLab backlog
172
+
173
+ ```yaml
174
+ backlog_provider: gitlab
175
+ backlog: https://gitlab.example.com
176
+ gitlab:
177
+ repo: group/project
178
+ token_env: GITLAB_TOKEN
179
+ ```
180
+
181
+ | Key | Default | Meaning |
182
+ | --- | --- | --- |
183
+ | `gitlab.repo` | — | Required project path or numeric id. |
184
+ | `gitlab.auth` | — | Required PAT env var `token_env`. |
185
+ | `gitlab.label_prefix` | `forgeo` | Prefix for labels. |
186
+ | `gitlab.property_key` | `forgeo` | Marker key. |
187
+ | `gitlab.page_size` | `30` | Issues per page. |
188
+ | `gitlab.max_issues` | `1000` | Max issues. |
189
+ | `gitlab.timeout_seconds` | `30` | HTTP timeout. |
190
+ | `gitlab.claim_timeout_seconds` | `86400` | Stale claim timeout. |
191
+ | `gitlab.workflow` | defaults | State mapping. |
192
+ | `gitlab.fields` | — | Optional field mappings. |
193
+
143
194
  ## Key details
144
195
 
145
196
  ### `backlog_auth`
@@ -56,20 +56,29 @@ Run the guided wizard from your project root:
56
56
  forgeo init
57
57
  ```
58
58
 
59
- The wizard asks for three things:
59
+ The wizard asks for:
60
60
 
61
61
  1. **Forgeo folder** — where the backlog, `BLOCKER.md` and the log live
62
62
  (default `.forgeo`). It is gitignored by default.
63
- 2. **Coding agent command** — the bare command that launches your coding
63
+ 2. **Backlog provider** — where tasks live: `file` (local `.forgeo/backlog.json`),
64
+ `github` / `gitlab` / `jira` / `http`. For `github` it auto-detects
65
+ `owner/repo` from `git remote origin`, asks for `token_env` (default
66
+ `GITHUB_TOKEN`) and can persist a pasted classic PAT (`ghp_...`, scope `repo`)
67
+ to `~/.config/forgeo/github_token_env.sh` (600, wired to `~/.bashrc`). Same
68
+ for `gitlab` (`GITLAB_TOKEN`, base URL) and `jira`/`http`.
69
+ 3. **Coding agent command** — the bare command that launches your coding
64
70
  agent (default `opencode run --auto`). Forgeo appends the standard task
65
71
  prompt (which ends in `$FORGEO_TASK`) automatically, so you never type
66
72
  it. Enter a command that already references `$FORGEO_TASK` and it is
67
73
  kept verbatim.
68
- 3. **Refactor prompt** — the instruction used when the backlog is empty; the
74
+ 4. **Refactor prompt** — the instruction used when the backlog is empty; the
69
75
  default is offered, or you can paste a custom one.
70
76
 
71
77
  `forgeo init` writes `forgeo.yaml`, creates Forgeo folder, and appends
72
- `<folder>/` to `.gitignore` (unless you opt out).
78
+ `<folder>/` to `.gitignore` (unless you opt out). For `github`/`gitlab`/`jira`
79
+ set `backlog_provider` + `backlog` URL + provider block is written automatically
80
+ and `state_dir` is set to the Forgeo folder so runtime files stay beside the
81
+ config.
73
82
 
74
83
  ```bash
75
84
  forgeo init --force # overwrite an existing forgeo.yaml
@@ -77,18 +86,25 @@ forgeo init --force # overwrite an existing forgeo.yaml
77
86
 
78
87
  ## 3. Create your first backlog
79
88
 
80
- The backlog is a plain JSON file (see [Backlog format](backlog.md)). Create
81
- the file configured as `backlog:` in your `forgeo.yaml` — by default
82
- `.forgeo/backlog.json`. Once Forgeo is running you can also add tasks
83
- from the [web console](web-console-api.md) — no file editing needed. (If your
84
- tasks already live in another application, `backlog:` also accepts an
85
- [HTTP endpoint](backlog.md#a-backlog-over-http) or a [Jira source](backlog.md#a-jira-backlog)
86
- instead of a file.)
87
-
88
- For Jira, set `backlog_provider: jira`, point `backlog:` at the Jira base URL,
89
- configure `jira.jql` and the workflow mappings, export the credentials named in
90
- `jira.auth`, and run `forgeo validate` before starting the daemon. See [Jira
91
- backlogs](backlog.md#a-jira-backlog) for the complete configuration.
89
+ If you chose `file` in the wizard, the backlog is a plain JSON file (see
90
+ [Backlog format](backlog.md)) — by default `.forgeo/backlog.json`. Once Forgeo
91
+ is running you can also add tasks from the [web console](web-console-api.md) — no
92
+ file editing needed.
93
+
94
+ If you chose `github`/`gitlab`/`jira`/`http` in the wizard, your `forgeo.yaml`
95
+ already points at the provider (`backlog: https://api.github.com` etc.).
96
+ For `github` create a classic PAT at `https://github.com/settings/tokens/new`
97
+ (scope `repo`), `export GITHUB_TOKEN=ghp_...` (or let the wizard persist it),
98
+ then `forgeo validate` before `forgeo start`. Same for `gitlab` (`GITLAB_TOKEN`)
99
+ and `jira`. See [Backlog format](backlog.md) for provider details.
100
+
101
+ For `jira`/`github`/`gitlab`, set `backlog_provider:` to the provider, point `backlog:` at its base URL
102
+ (`https://jira.example.com`, `https://api.github.com` / `https://github.example.com/api/v3`,
103
+ `https://gitlab.example.com`), configure the provider block (`jira.jql` / `github.repo` / `gitlab.repo` and auth),
104
+ export the credentials named in `*_auth.token_env`, and run `forgeo validate` before starting the daemon.
105
+ The dashboard for these providers is a read-mostly mirror: a banner links to the native board, each card links to
106
+ the native issue, and Forgeo-specific state (BLOCKED/FAILED reasons, `agent_response`) is surfaced on the board — triage
107
+ stays in Jira/GitHub/GitLab. See [Backlog: Jira/GitHub/GitLab](backlog.md) for the complete configuration.
92
108
 
93
109
  ```json
94
110
  {
@@ -66,11 +66,20 @@ open-by-default behavior.
66
66
 
67
67
  - `GET /` — home page listing every registered instance: name, repository,
68
68
  daemon state (lock held), last outcome, next run, and per-status backlog
69
- counts, each linking to its instance page.
69
+ counts, each linking to its instance page. For issue-backed instances
70
+ (`jira`/`github`/`gitlab`) the card also shows an **Open in Jira/GitHub/GitLab ↗**
71
+ link to the native board.
70
72
  - `GET /instances/<name>/` — one instance's page: a kanban backlog, a
71
73
  **Create** tab with a form to add tasks (including an optional *Run at*
72
74
  date/time input for a one-shot schedule), plus tabs for **logs**, **history**,
73
- **blocker** and **config**. The header carries a **DAEMON** section with the
75
+ **blocker** and **config**. For document backlogs (`file`/`http`) this board
76
+ is the primary editor; for issue providers (`jira`/`github`/`gitlab`) it is a
77
+ read-mostly **mirror** of the native tracker: a banner at the top links to
78
+ the external board (`Jira`/`GitHub`/`GitLab`), each task card and its detail
79
+ modal carry an **Open in external ↗** link to the native issue, and the board
80
+ surfaces Forgeo-specific state that the native UI does not (BLOCKED/FAILED
81
+ reasons, `agent_response`, retry budget). Creating/editing still works via
82
+ the dashboard, but triage is expected in the native tool. The header carries a **DAEMON** section with the
74
83
  daemon status tag (`running`/`stopped`) and **Start**/**Stop**/**Restart**
75
84
  buttons that call `POST /api/instances/<name>/start|stop|restart`; the
76
85
  buttons reflect the current state (Start is disabled while running, Stop
@@ -152,15 +161,19 @@ outcome, next run, and backlog counts.
152
161
  curl http://127.0.0.1:8790/api/instances
153
162
  ```
154
163
 
155
- Each row also carries `backlog_error`: `null` normally, and the reason when
156
- that instance's remote backlog could not be read. Its counts are zero in that
157
- case — the row reports a backlog it could not reach, not an empty one. One
158
- unreachable provider never fails the whole listing.
164
+ Each row also carries `backlog_provider` (`file`/`http`/`jira`/`github`/`gitlab`),
165
+ `backlog` (path or base URL), `backlog_is_issue_provider`, and for issue
166
+ providers `external_board_url`/`external_board_label` (the native board link
167
+ shown on the home cards). It also carries `backlog_error`: `null` normally,
168
+ and the reason when that instance's remote backlog could not be read. Its
169
+ counts are zero in that case — the row reports a backlog it could not reach,
170
+ not an empty one. One unreachable provider never fails the whole listing.
159
171
 
160
172
  ### `GET /api/instances/<name>/tasks`
161
173
 
162
174
  List every task in that instance's backlog, in creation order. Each task
163
- carries extra `unsatisfied_dependencies` and retry fields (see below).
175
+ carries extra `unsatisfied_dependencies`, retry fields (see below), and for
176
+ issue providers `external_url` (link to the native Jira/GitHub/GitLab issue).
164
177
 
165
178
  ```bash
166
179
  curl http://127.0.0.1:8790/api/instances/my-repo/tasks
@@ -400,7 +413,10 @@ Errors:
400
413
  ### `GET /api/instances/<name>/status`
401
414
 
402
415
  Daemon status: name, repo, interval, `daemon_running` (whether the instance's
403
- lock is held), the recorded PID, `last_outcome`, and the `next_run_at`.
416
+ lock is held), the recorded PID, `last_outcome`, and the `next_run_at`. Also
417
+ carries `backlog_provider`, `backlog`, `backlog_is_issue_provider`,
418
+ `external_board_url`/`external_board_label` so the instance page can render the
419
+ issue-provider banner and per-task external links.
404
420
 
405
421
  ```bash
406
422
  curl http://127.0.0.1:8790/api/instances/my-repo/status
@@ -414,7 +430,12 @@ curl http://127.0.0.1:8790/api/instances/my-repo/status
414
430
  "daemon_running": true,
415
431
  "pid": 4242,
416
432
  "last_outcome": "task",
417
- "next_run_at": "2026-08-01T12:00:00+00:00"
433
+ "next_run_at": "2026-08-01T12:00:00+00:00",
434
+ "backlog_provider": "github",
435
+ "backlog": "https://api.github.com",
436
+ "backlog_is_issue_provider": true,
437
+ "external_board_url": "https://github.com/owner/repo/issues",
438
+ "external_board_label": "GitHub"
418
439
  }
419
440
  ```
420
441
 
@@ -14,7 +14,7 @@ set -eu
14
14
 
15
15
  REPO_OWNER="lucaGazzola"
16
16
  REPO_NAME="forgeo"
17
- DEFAULT_VERSION="0.8.0"
17
+ DEFAULT_VERSION="0.10.0"
18
18
  MIN_PYTHON="3.11"
19
19
  PYPI_PACKAGE="forgeo-cli"
20
20
  PREFIX="${FORGEO_PREFIX:-${HOME:-}/.local}"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "forgeo-cli"
7
- version = "0.8.0"
7
+ version = "0.10.0"
8
8
  description = "A scheduled software forgeo: executes backlog tasks on main, refactors when idle, and writes BLOCKER.md when it needs human input."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,4 +7,4 @@ try:
7
7
 
8
8
  __version__ = _pkg_version("forgeo-cli")
9
9
  except PackageNotFoundError: # standalone binary: no installed package metadata
10
- __version__ = "0.8.0"
10
+ __version__ = "0.10.0"
@@ -206,7 +206,26 @@ class ShellAgent(BaseAgent):
206
206
  except TimeoutError:
207
207
  timed_out = True
208
208
  _kill_process_group(proc)
209
- await proc.wait()
209
+ # ``proc.wait()`` waits for both process exit *and* pipe closure
210
+ # (see ``BaseSubprocessTransport._try_finish``). A grandchild that
211
+ # called ``setsid()`` and holds the write end of the pipe keeps it
212
+ # open forever, so the wait would hang past the drain deadline.
213
+ # Bound it by ``drain_timeout_seconds`` and force-close the
214
+ # transport if it still hangs.
215
+ try:
216
+ await asyncio.wait_for(proc.wait(), timeout=self.drain_timeout_seconds)
217
+ except TimeoutError:
218
+ try:
219
+ proc._transport.close() # type: ignore[attr-defined]
220
+ except Exception: # noqa: BLE001, S110 - close must not fail the task
221
+ pass
222
+ try:
223
+ await asyncio.wait_for(proc.wait(), timeout=1.0)
224
+ except TimeoutError:
225
+ logs.append(
226
+ f"[{self.name}] Process did not exit within "
227
+ f"{self.drain_timeout_seconds:g}s after kill; proceeding."
228
+ )
210
229
  # Always finish draining so lines already written (and any residual
211
230
  # after kill) are captured before we build the result. Bounded: a
212
231
  # grandchild that escaped the process group (daemonized agent, docker
@@ -215,6 +234,11 @@ class ShellAgent(BaseAgent):
215
234
  try:
216
235
  await asyncio.wait_for(readers, timeout=self.drain_timeout_seconds)
217
236
  except TimeoutError:
237
+ readers.cancel()
238
+ try:
239
+ await readers
240
+ except asyncio.CancelledError:
241
+ pass
218
242
  logs.append(
219
243
  f"[{self.name}] Output streams stayed open beyond "
220
244
  f"{self.drain_timeout_seconds:g}s; proceeding without them."