forgeo-cli 0.11.0__tar.gz → 1.12.1__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 (92) hide show
  1. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/CHANGELOG.md +24 -1
  2. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/PKG-INFO +4 -3
  3. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/README.md +3 -2
  4. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/backlog.md +22 -7
  5. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/cli-reference.md +34 -0
  6. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/configuration.md +79 -7
  7. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/getting-started.md +16 -4
  8. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/web-console-api.md +2 -1
  9. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/install.sh +1 -1
  10. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/pyproject.toml +4 -1
  11. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/__init__.py +1 -1
  12. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/agent.py +12 -4
  13. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_github.py +78 -43
  14. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_gitlab.py +71 -40
  15. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_issue_base.py +25 -0
  16. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_jira.py +105 -26
  17. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/cli.py +698 -0
  18. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/config.py +49 -1
  19. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/models.py +136 -20
  20. forgeo_cli-1.12.1/src/forgeo/oauth_common.py +163 -0
  21. forgeo_cli-1.12.1/src/forgeo/oauth_github.py +497 -0
  22. forgeo_cli-1.12.1/src/forgeo/oauth_gitlab.py +421 -0
  23. forgeo_cli-1.12.1/src/forgeo/oauth_jira.py +410 -0
  24. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/setup.py +325 -16
  25. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/conftest.py +37 -0
  26. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_agent.py +22 -1
  27. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_github.py +31 -4
  28. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_gitlab.py +27 -0
  29. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_cli.py +110 -24
  30. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_daemon.py +69 -64
  31. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_install.py +16 -5
  32. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_instances.py +2 -0
  33. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_models.py +44 -1
  34. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_oauth.py +29 -1
  35. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_setup.py +1 -1
  36. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_web.py +10 -3
  37. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_web_common.py +2 -0
  38. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_web_lock.py +8 -0
  39. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/www/404.html +4 -1
  40. forgeo_cli-1.12.1/www/favicon.ico +0 -0
  41. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/www/index.html +4 -1
  42. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/.github/workflows/ci.yml +0 -0
  43. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/.gitignore +0 -0
  44. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/CONTRIBUTING.md +0 -0
  45. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/LICENSE +0 -0
  46. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/config/nginx-forgeo.conf +0 -0
  47. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/agent-contract.md +0 -0
  48. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/console.png +0 -0
  49. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/demo.gif +0 -0
  50. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/logo.png +0 -0
  51. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/og.png +0 -0
  52. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/title.svg +0 -0
  53. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/index.md +0 -0
  54. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/forgeo.spec +0 -0
  55. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/mkdocs.yml +0 -0
  56. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/scripts/__init__.py +0 -0
  57. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/scripts/render_homebrew_formula.py +0 -0
  58. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/scripts/test-github-backlog-e2e.sh +0 -0
  59. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/__main__.py +0 -0
  60. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog.py +0 -0
  61. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_http.py +0 -0
  62. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/central.py +0 -0
  63. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/daemon.py +0 -0
  64. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/daemon_control.py +0 -0
  65. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/forgeo.py +0 -0
  66. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/git.py +0 -0
  67. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/instances.py +0 -0
  68. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/io.py +0 -0
  69. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/notify.py +0 -0
  70. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/oauth.py +0 -0
  71. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/paths.py +0 -0
  72. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/runs.py +0 -0
  73. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/update.py +0 -0
  74. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/validate.py +0 -0
  75. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/central.css +0 -0
  76. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/central.js +0 -0
  77. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/index.html +0 -0
  78. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/instance.html +0 -0
  79. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/login.html +0 -0
  80. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/style.css +0 -0
  81. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web_common.py +0 -0
  82. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog.py +0 -0
  83. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_http.py +0 -0
  84. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_jira.py +0 -0
  85. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_factory.py +0 -0
  86. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_git.py +0 -0
  87. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_io.py +0 -0
  88. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_paths.py +0 -0
  89. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_remote_backlog_cycle.py +0 -0
  90. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_render_homebrew.py +0 -0
  91. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_runs.py +0 -0
  92. {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_update.py +0 -0
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.12.1] - 2026-09-09
11
+
12
+ ### Fixed
13
+
14
+ - GitHub/GitLab backlogs preserve per-task customization when a task is created.
15
+ - OAuth issue-backlog login supports browser URLs, callback ports, and config-relative tokens.
16
+
17
+ ### Removed
18
+
19
+ - Unused `github.fields`/`gitlab.fields` field mapping (GitHub/GitLab issues have no custom fields).
20
+
21
+ ## [0.12.0] - 2026-09-03
22
+
23
+ ### Added
24
+
25
+ - Browser-based OAuth login for GitHub, GitLab, and Jira, with cached tokens and setup guidance.
26
+
27
+ ### Fixed
28
+
29
+ - Stabilized the CI lint configuration.
30
+
10
31
  ## [0.11.0] - 2026-08-31
11
32
 
12
33
  ### Added
@@ -429,7 +450,9 @@ Initial release of the scheduled, agent-driven software forgeo.
429
450
  overlapping-run skipping.
430
451
  - Dogfooding docs removed; local configs kept out of the repository.
431
452
 
432
- [Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v0.11.0...HEAD
453
+ [Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v1.12.1...HEAD
454
+ [1.12.1]: https://github.com/lucaGazzola/forgeo/compare/v0.12.0...v1.12.1
455
+ [0.12.0]: https://github.com/lucaGazzola/forgeo/compare/v0.11.0...v0.12.0
433
456
  [0.11.0]: https://github.com/lucaGazzola/forgeo/compare/v0.10.0...v0.11.0
434
457
  [0.10.0]: https://github.com/lucaGazzola/forgeo/compare/v0.9.0...v0.10.0
435
458
  [0.9.0]: https://github.com/lucaGazzola/forgeo/compare/v0.8.0...v0.9.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forgeo-cli
3
- Version: 0.11.0
3
+ Version: 1.12.1
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
@@ -99,7 +99,7 @@ pipx install forgeo-cli # Python 3.11+
99
99
  forgeo init
100
100
  ```
101
101
 
102
- Wizard in your project root. Creates `forgeo.yaml` and `.forgeo/` (backlog, logs, blockers). Prompts for backlog provider (`file` / `github` / `gitlab` / `jira` / `http`) and agent command. For `github` it auto-detects `owner/repo` and can persist `GITHUB_TOKEN` to `~/.config/forgeo/`.
102
+ Wizard in your project root. Creates `forgeo.yaml` and `.forgeo/` (backlog, logs, blockers). Prompts for backlog provider (`file` / `github` / `gitlab` / `jira` / `http`) and agent command. For issue providers it offers a PAT or OAuth login (`forgeo auth login` → `~/.config/forgeo/tokens/`); GitHub repository detection is automatic when possible, while Jira asks for its URL and JQL.
103
103
 
104
104
  ### 3. Fill the backlog
105
105
 
@@ -129,7 +129,8 @@ Each cycle: pick task → run agent → commit/push (or `REVIEW` branch when `re
129
129
  forgeo status # config, counts, next task, daemon state, last outcome
130
130
  forgeo once # one cycle in foreground, no daemon
131
131
  forgeo run --task TASK-012 # run a specific OPEN task now
132
- forgeo stop / restart # stop or restart daemon
132
+ forgeo stop # stop daemon
133
+ forgeo restart # restart daemon
133
134
  forgeo web # dashboard for all instances
134
135
  ```
135
136
 
@@ -45,7 +45,7 @@ pipx install forgeo-cli # Python 3.11+
45
45
  forgeo init
46
46
  ```
47
47
 
48
- Wizard in your project root. Creates `forgeo.yaml` and `.forgeo/` (backlog, logs, blockers). Prompts for backlog provider (`file` / `github` / `gitlab` / `jira` / `http`) and agent command. For `github` it auto-detects `owner/repo` and can persist `GITHUB_TOKEN` to `~/.config/forgeo/`.
48
+ Wizard in your project root. Creates `forgeo.yaml` and `.forgeo/` (backlog, logs, blockers). Prompts for backlog provider (`file` / `github` / `gitlab` / `jira` / `http`) and agent command. For issue providers it offers a PAT or OAuth login (`forgeo auth login` → `~/.config/forgeo/tokens/`); GitHub repository detection is automatic when possible, while Jira asks for its URL and JQL.
49
49
 
50
50
  ### 3. Fill the backlog
51
51
 
@@ -75,7 +75,8 @@ Each cycle: pick task → run agent → commit/push (or `REVIEW` branch when `re
75
75
  forgeo status # config, counts, next task, daemon state, last outcome
76
76
  forgeo once # one cycle in foreground, no daemon
77
77
  forgeo run --task TASK-012 # run a specific OPEN task now
78
- forgeo stop / restart # stop or restart daemon
78
+ forgeo stop # stop daemon
79
+ forgeo restart # restart daemon
79
80
  forgeo web # dashboard for all instances
80
81
  ```
81
82
 
@@ -150,7 +150,7 @@ backlog: https://api.example.com/backlog
150
150
  | Every read | `GET <url>` returns `{"tasks": [...]}` |
151
151
  | Every write | `POST <url>` sends the full document |
152
152
 
153
- Add `backlog_auth` for OAuth2. Same schema and ordering. Endpoint must:
153
+ Add `backlog_auth` for OAuth2 client-credentials access to an HTTP backlog. This is a service-account flow, not the human browser login described for issue providers below. Same schema and ordering. Endpoint must:
154
154
 
155
155
  - Return `{"tasks": [...]}`; non-object or non-list → empty.
156
156
  - Replace, not append — POST body is the complete list.
@@ -166,6 +166,7 @@ Runtime files (`backlog.lock`, `runs.jsonl`, etc.) go in `state_dir` (defaults t
166
166
  ## A Jira backlog
167
167
 
168
168
  ```yaml
169
+ # PAT:
169
170
  backlog_provider: jira
170
171
  backlog: https://jira.example.com
171
172
  jira:
@@ -173,26 +174,36 @@ jira:
173
174
  project_key: APP
174
175
  auth: {scheme: basic, username_env: JIRA_USER, token_env: JIRA_TOKEN}
175
176
  workflow: {open_statuses: ["10000", "10001"], open_status: "10000", running_status: "3", completed_status: "10002"}
177
+ # OAuth 3LO (Cloud):
178
+ # jira:
179
+ # jql: 'project = APP AND labels = forgeo'
180
+ # auth:
181
+ # oauth: {client_id: xxxx, client_secret_env: JIRA_CLIENT_SECRET, scope: "offline_access read:jira-user read:jira-work", flow: browser}
182
+ # # then: forgeo auth login --provider jira --client-id xxxx
176
183
  ```
177
184
 
178
185
  - Jira keys → task IDs; `summary`/`description`/`created`/`updated` → task fields.
179
186
  - `open_statuses` = pickable; `running_status` = claimed before agent runs; `completed_status`/`blocked_status`/`failed_status` = terminal (blocked/failed optional — labels `forgeo-blocked`/`forgeo-failed` always applied).
180
187
  - Engine state (`blocker_reason`, `retry_count`, `agent_response`, etc.) in Jira issue property `forgeo`.
181
188
  - Optional `jira.fields` maps custom fields for `acceptance_criteria`, `dependencies`, etc. Without `run_at` mapping, Jira `duedate` is used at midnight UTC. Dependencies also inferred from `blocks` issue links.
182
- - Auth from env vars: `basic` (username + token) or `bearer` (PAT). Uses REST API v3 (`/search/jql` + cursor) by default; set `api_version: 2` for older servers.
189
+ - Auth is exactly one of PAT from environment variables (`basic` username + token or `bearer` token) or Jira Cloud OAuth 3LO (`oauth`). Uses REST API v3 (`/search/jql` + cursor) by default; set `api_version: 2` for older servers. OAuth browser login stores the token outside the config and discovers `cloud_id`; keep the configured client-secret environment variable available for refresh.
183
190
  - Daemon paginates JQL; stale claims released after `claim_timeout_seconds`. Unavailable Jira fails the cycle.
184
191
 
185
192
  ## A GitHub backlog
186
193
 
187
194
  ```yaml
195
+ # PAT:
188
196
  backlog_provider: github
189
197
  backlog: https://api.github.com # or https://github.example.com/api/v3
190
- github: {repo: owner/repo, token_env: GITHUB_TOKEN}
198
+ github: {repo: owner/repo, auth: {token_env: GITHUB_TOKEN}}
199
+ # OAuth (device or browser):
200
+ # github: {repo: owner/repo, auth: {oauth: {client_id: Iv1.xxxx, flow: device, scope: repo}}}
201
+ # # then: forgeo auth login --provider github --client-id Iv1.xxxx
191
202
  ```
192
203
 
193
204
  - Issue numbers → task IDs; `title`/`body`/`created_at`/`updated_at` → task fields.
194
- - `open` → `OPEN`, `closed` → `COMPLETED`; labels `forgeo-running`/`blocked`/`failed` for the rest.
195
- - Engine state in hidden `<!-- forgeo: {...} -->` block inside the body.
205
+ - `open` → `OPEN`, `closed` → `COMPLETED`; labels `forgeo-running`/`forgeo-blocked`/`forgeo-failed` for the rest.
206
+ - Per-task customization (`agent_command`, `agent_timeout_seconds`, `acceptance_criteria`, `dependencies`, `files_to_modify`, `run_at`, `retries_left`, `review_required`) and engine state both live in a hidden `<!-- forgeo: {...} -->` block inside the body. There is no `github.fields` custom-field mapping — GitHub issues expose no custom fields on the REST issues API.
196
207
  - Paginated `GET /repos/{owner}/{repo}/issues?state=all`; claiming adds `forgeo-running` + `claimed_at`.
197
208
 
198
209
  Test live with `scripts/test-github-backlog-e2e.sh`:
@@ -204,12 +215,16 @@ GITHUB_TOKEN=... ./scripts/test-github-backlog-e2e.sh
204
215
  ## A GitLab backlog
205
216
 
206
217
  ```yaml
218
+ # PAT:
207
219
  backlog_provider: gitlab
208
220
  backlog: https://gitlab.example.com # instance root, /api/v4 appended
209
- gitlab: {repo: group/project, token_env: GITLAB_TOKEN}
221
+ gitlab: {repo: group/project, auth: {token_env: GITLAB_TOKEN}}
222
+ # OAuth (browser PKCE, device if enabled by the instance):
223
+ # gitlab: {repo: group/project, auth: {oauth: {client_id: abc, flow: browser, scope: api}}}
224
+ # # then: forgeo auth login --provider gitlab --client-id abc
210
225
  ```
211
226
 
212
- - `iid` → task ID; `opened`/`closed` → `OPEN`/`COMPLETED`; same hidden-block and label mechanics as GitHub.
227
+ - `iid` → task ID; `opened`/`closed` → `OPEN`/`COMPLETED`; labels `forgeo-running`/`forgeo-blocked`/`forgeo-failed`; same hidden-block mechanics as GitHub (per-task customization and engine state in a hidden `<!-- forgeo: {...} -->` block in the description). OAuth is an alternative to `token_env` and supports browser PKCE (or device flow when enabled by the GitLab instance).
213
228
  - Paginated `GET /api/v4/projects/:id/issues?state=all`.
214
229
 
215
230
  ## Managing tasks
@@ -125,6 +125,40 @@ Unregister (never touches config/repo). Errors if unknown.
125
125
 
126
126
  Table of all instances: name, daemon state, last outcome (from `runs.jsonl`). Exits `0` with hint if none.
127
127
 
128
+ ## `forgeo auth`
129
+
130
+ Browser/OAuth login for `github`/`gitlab`/`jira` (alternative to a PAT in an environment variable). This is separate from `backlog_auth`, which uses OAuth2 client credentials for an HTTP backlog. Tokens are stored `0600` in `~/.config/forgeo/tokens/` and read by `forgeo validate`/`start`/`once`.
131
+
132
+ Create an OAuth application with the provider before logging in. GitHub defaults to the device flow, GitLab defaults to browser PKCE, and Jira Cloud supports browser PKCE only. Browser flows listen on `127.0.0.1`; the port is ephemeral unless `--callback-port` is supplied. If the provider requires an exact registered redirect URI, register `http://127.0.0.1:<port>/callback` and use that same port in the command.
133
+
134
+ ### `forgeo auth login --provider <github|gitlab|jira>`
135
+
136
+ | Flag | Description |
137
+ | --- | --- |
138
+ | `--provider` | `github` (default) / `gitlab` / `jira`. |
139
+ | `--config <file>` | `forgeo.yaml` to read `auth.oauth.client_id` (defaults to `./forgeo.yaml`). |
140
+ | `--client-id <id>` | OAuth client ID (overrides config; required if not in `forgeo.yaml`). |
141
+ | `--flow <device|browser>` | `device` for GitHub, `browser` for GitLab/Jira. GitLab device flow is available only when enabled by the GitLab instance; Jira is browser-only. |
142
+ | `--scope <scope>` | Scope (default `repo` / `api` / `offline_access read:jira-user read:jira-work`). |
143
+ | `--token-file <path>` | Where to store token (default per-provider per-host `~/.config/forgeo/tokens/<provider>.json`). |
144
+ | `--callback-port <port>` | Fixed `127.0.0.1` port for browser callbacks. Default is an ephemeral port; use this when the OAuth app requires an exact callback URI. |
145
+ | `--cloud-id <id>` | Jira Cloud ID (overrides `jira.auth.oauth.cloud_id`). |
146
+ | `--api-base <url>` | Provider API base (default from `forgeo.yaml` backlog or `https://api.github.com`/`https://gitlab.com`). |
147
+ | `--no-open-browser` | Print the authorization URL instead of opening it automatically. Applies to browser and device flows. |
148
+
149
+ Device flow: prints a verification URL and user code, then polls the provider token endpoint. Browser flow: opens (or prints) the provider authorization URL with a PKCE `code_challenge`, listens on loopback, and exchanges the callback code for an access token. Jira also discovers an Atlassian `cloud_id` and stores a refresh token when one is returned. If `client_secret_env` is configured for Jira, export that variable for token refreshes by the daemon. `forgeo init` with OAuth writes `auth.oauth` and offers to run login immediately.
150
+
151
+ ### `forgeo auth status --provider <p>` / `forgeo auth logout --provider <p>`
152
+
153
+ ```bash
154
+ forgeo auth status --provider github
155
+ forgeo auth status --provider gitlab
156
+ forgeo auth status --provider jira
157
+ forgeo auth logout --provider github
158
+ ```
159
+
160
+ `status` masks the token and shows `scope`/`expires_in`/`cloud_id` (Jira). `logout` removes the file. Both honor `--token-file`/`--api-base`/`--config` and use the project-local `forgeo.yaml` when it exists; pass `--config` for a config elsewhere.
161
+
128
162
  ## `forgeo web`
129
163
 
130
164
  Central dashboard aggregating every registered instance (reads files directly, works whether daemons are running).
@@ -70,6 +70,7 @@ Add `backlog_auth` for OAuth2-protected HTTP endpoints (see below). A failed HTT
70
70
  ### Jira
71
71
 
72
72
  ```yaml
73
+ # PAT / basic (Server/DC):
73
74
  backlog_provider: jira
74
75
  backlog: https://jira.example.com
75
76
  state_dir: .forgeo
@@ -86,12 +87,30 @@ jira:
86
87
  open_status: "10000"
87
88
  running_status: "3"
88
89
  completed_status: "10002"
90
+ # OAuth 3LO (Cloud, browser):
91
+ # jira:
92
+ # jql: 'project = APP AND labels = forgeo'
93
+ # auth:
94
+ # oauth:
95
+ # client_id: xxxx
96
+ # client_secret_env: JIRA_CLIENT_SECRET
97
+ # scope: offline_access read:jira-user read:jira-work
98
+ # token_file: ~/.config/forgeo/tokens/jira.json # optional
99
+ # callback_port: 8765 # optional fixed loopback port
100
+ # # then: forgeo auth login --provider jira --client-id xxxx
89
101
  ```
90
102
 
91
103
  | Key | Default | Description |
92
104
  | --- | --- | --- |
93
105
  | `jira.jql` | — | JQL scope — include all lifecycle states. |
94
- | `jira.auth` | — | `basic` (username + token) or `bearer` (token). |
106
+ | `jira.auth` | — | Exactly one of PAT (`basic`/`bearer` with `token_env`) or `oauth` (Jira Cloud browser PKCE). |
107
+ | `jira.auth.oauth.client_id` | — | Atlassian OAuth app client ID. Required for OAuth. |
108
+ | `jira.auth.oauth.client_secret_env` | — | Optional environment variable containing the OAuth client secret; required for refresh when the app is confidential. |
109
+ | `jira.auth.oauth.scope` | — | Requested scopes; `offline_access read:jira-user read:jira-work` by default. |
110
+ | `jira.auth.oauth.token_file` | per-host path | Token JSON file, mode `0600`; relative paths resolve from `forgeo.yaml`. |
111
+ | `jira.auth.oauth.cloud_id` | — | Atlassian site ID; auto-detected after login when omitted. |
112
+ | `jira.auth.oauth.flow` | `browser` | Browser PKCE only. |
113
+ | `jira.auth.oauth.callback_port` | ephemeral | Fixed loopback port for browser login when the app requires an exact callback URI. |
95
114
  | `jira.project_key` | — | Project for dashboard task creation. |
96
115
  | `jira.issue_type` | `Task` | Issue type for creation. |
97
116
  | `jira.api_version` | `3` | `3` = Cloud cursor pagination, `2` = offset. |
@@ -109,17 +128,36 @@ Status values can be names or IDs (IDs preferred). Jira keys become task IDs. En
109
128
  ### GitHub
110
129
 
111
130
  ```yaml
131
+ # PAT:
112
132
  backlog_provider: github
113
133
  backlog: https://api.github.com # or https://github.example.com/api/v3
114
134
  github:
115
135
  repo: owner/repo
116
- token_env: GITHUB_TOKEN
136
+ auth:
137
+ token_env: GITHUB_TOKEN
138
+ # OAuth / browser (device flow for CLI, browser PKCE):
139
+ # github:
140
+ # repo: owner/repo
141
+ # auth:
142
+ # oauth:
143
+ # client_id: Iv1.xxxx
144
+ # flow: device # device | browser
145
+ # scope: repo
146
+ # token_file: ~/.config/forgeo/tokens/github.json
147
+ # callback_port: 8765 # optional fixed loopback port for browser flow
148
+ # # then: forgeo auth login --provider github --client-id Iv1.xxxx
117
149
  ```
118
150
 
119
151
  | Key | Default | Description |
120
152
  | --- | --- | --- |
121
153
  | `github.repo` | — | `owner/repo`. |
122
- | `github.auth` | — | `token_env` for PAT. |
154
+ | `github.auth` | — | Exactly one of `token_env` (PAT) or `oauth`. |
155
+ | `github.auth.oauth.client_id` | — | OAuth app client ID. Required for OAuth. |
156
+ | `github.auth.oauth.flow` | `device` | `device` or `browser`. |
157
+ | `github.auth.oauth.scope` | `repo` | OAuth scope. |
158
+ | `github.auth.oauth.token_file` | per-host path | Token JSON file, mode `0600`; relative paths resolve from `forgeo.yaml`. |
159
+ | `github.auth.oauth.callback_port` | ephemeral | Fixed loopback port for browser login when the app requires an exact callback URI. |
160
+ | `github.auth.oauth.client_secret_env` | — | Optional environment variable containing a confidential browser-flow client secret. |
123
161
  | `github.label_prefix` | `forgeo` | Label prefix. |
124
162
  | `github.property_key` | `forgeo` | Marker key for hidden body block. |
125
163
  | `github.page_size` | `30` | Issues per page. |
@@ -127,21 +165,55 @@ github:
127
165
  | `github.timeout_seconds` | `30` | HTTP timeout. |
128
166
  | `github.claim_timeout_seconds` | `86400` | Stale claim timeout. |
129
167
  | `github.workflow` | defaults | State/label mapping. |
130
- | `github.fields` | — | Field mappings for `acceptance_criteria`, `dependencies`, `files_to_modify`, `agent_command`, `agent_timeout_seconds`, `run_at`, `retries_left`. |
131
168
 
132
- Issue numbers become task IDs; `open`/`closed` maps to `OPEN`/`COMPLETED`; `forgeo-running`/`blocked`/`failed` labels cover the rest. Engine state is stored in a hidden `<!-- forgeo: {...} -->` block in the issue body.
169
+ Issue numbers become task IDs; `open`/`closed` maps to `OPEN`/`COMPLETED`; `forgeo-running`/`blocked`/`failed` labels cover the rest. GitHub issues expose no custom-field mechanism on the REST issues API, so per-task customization (`agent_command`, `agent_timeout_seconds`, `acceptance_criteria`, `dependencies`, `files_to_modify`, `run_at`, `retries_left`, `review_required`) and engine state both live in a hidden `<!-- forgeo: {...} -->` block in the issue body. Engine-managed fields (`retry_count`, `agent_response`, `failure_reason`, ...) are written by Forgeo and not meant to be edited by hand.
133
170
 
134
171
  ### GitLab
135
172
 
136
173
  ```yaml
174
+ # PAT:
137
175
  backlog_provider: gitlab
138
176
  backlog: https://gitlab.example.com # instance root, /api/v4 appended
139
177
  gitlab:
140
178
  repo: group/project
141
- token_env: GITLAB_TOKEN
179
+ auth:
180
+ token_env: GITLAB_TOKEN
181
+ # OAuth (browser PKCE, device if enabled):
182
+ # gitlab:
183
+ # repo: group/project
184
+ # auth:
185
+ # oauth:
186
+ # client_id: abc123
187
+ # flow: browser # browser | device
188
+ # scope: api
189
+ # token_file: ~/.config/forgeo/tokens/gitlab.json
190
+ # callback_port: 8765 # optional fixed loopback port for browser flow
191
+ # # then: forgeo auth login --provider gitlab --client-id abc123
142
192
  ```
143
193
 
144
- Same keys as GitHub (`gitlab.*`), including `workflow` and `fields` for the same 7 mappings. Issue `iid` becomes task ID; `opened`/`closed` maps to `OPEN`/`COMPLETED`; hidden `<!-- forgeo: {...} -->` block for engine state.
194
+ Same backlog and task keys as GitHub (`gitlab.*`). Its auth must contain exactly one of `token_env` (PAT) or `oauth`; OAuth supports `client_id`, `flow`, `scope`, `token_file`, `callback_port`, and optional `client_secret_env`. Issue `iid` becomes task ID; `opened`/`closed` maps to `OPEN`/`COMPLETED`; per-task customization and engine state live in a hidden `<!-- forgeo: {...} -->` block in the issue description, as with GitHub.
195
+
196
+ ### Browser OAuth details
197
+
198
+ For GitHub and GitLab, OAuth is an alternative to the provider PAT. For Jira, OAuth is available for Jira Cloud and is browser-only. Do not configure both `token_env` and `oauth` in the same provider; validation requires exactly one.
199
+
200
+ The token file is outside `forgeo.yaml`, is written with mode `0600`, and defaults to a provider/host-specific file under `~/.config/forgeo/tokens/`. A relative `token_file` is resolved relative to the config file. A `client_secret_env` value is only an environment variable name; the secret itself must be exported before login and kept available to the daemon for refreshes.
201
+
202
+ Browser login uses a loopback callback at `http://127.0.0.1:<port>/callback`. The default port is ephemeral. If the provider requires a pre-registered exact URI, register a fixed port in the OAuth application and pass it to login:
203
+
204
+ ```bash
205
+ forgeo auth login --provider github --client-id Iv1.xxx --flow browser --callback-port 8765
206
+ forgeo auth login --provider gitlab --client-id abc123 --callback-port 8765
207
+ export JIRA_CLIENT_SECRET=...
208
+ forgeo auth login --provider jira --client-id xxxx --callback-port 8765
209
+ ```
210
+
211
+ Use `--no-open-browser` to print the authorization URL without opening it. After login, verify the provider-specific token and start the daemon:
212
+
213
+ ```bash
214
+ forgeo auth status --provider github # or gitlab / jira
215
+ forgeo validate
216
+ ```
145
217
 
146
218
  ## Key details
147
219
 
@@ -28,11 +28,11 @@ forgeo init
28
28
  The wizard asks for:
29
29
 
30
30
  1. **Forgeo folder** — where backlog/logs live (default `.forgeo`, gitignored).
31
- 2. **Backlog provider** — `file` (local JSON), `github`/`gitlab`/`jira`/`http`. For `github` it auto-detects `owner/repo` from `git remote` and can persist a pasted `GITHUB_TOKEN` to `~/.config/forgeo/github_token_env.sh`.
31
+ 2. **Backlog provider** — `file` (local JSON), `github`/`gitlab`/`jira`/`http`. For GitHub it auto-detects `owner/repo` from `git remote` and can persist a pasted `GITHUB_TOKEN` to `~/.config/forgeo/github_token_env.sh`.
32
32
  3. **Agent command** — bare command for your agent (default `opencode run --auto`). Forgeo appends the task prompt (`$FORGEO_TASK`); if your command already references `$FORGEO_TASK` it is kept verbatim.
33
33
  4. **Refactor prompt** — used when the backlog is empty.
34
34
 
35
- Writes `forgeo.yaml`, creates the folder, and appends it to `.gitignore` (opt-out available). With `github`/`gitlab`/`jira` it also sets `backlog_provider`, `backlog` URL, provider block, and `state_dir`.
35
+ Writes `forgeo.yaml`, creates the folder, and appends it to `.gitignore` (opt-out available). With `github`/`gitlab`/`jira` it also sets `backlog_provider`, `backlog` URL, provider block, and `state_dir`. For `github`/`gitlab`/`jira` you can pick `PAT` or OAuth. GitHub defaults to device flow; GitLab and Jira use browser PKCE. Browser tokens are stored in `~/.config/forgeo/tokens/`.
36
36
 
37
37
  ```bash
38
38
  forgeo init --force # overwrite existing config
@@ -58,13 +58,25 @@ forgeo init --force # overwrite existing config
58
58
 
59
59
  You can also add tasks from the dashboard once Forgeo is running — no file editing needed.
60
60
 
61
- **Remote providers** — `forgeo.yaml` already points at the provider. Export credentials and validate:
61
+ **Remote providers** — `forgeo.yaml` already points at the provider. Authenticate with a PAT **or** browser OAuth:
62
62
 
63
63
  ```bash
64
- export GITHUB_TOKEN=ghp_... # or GITLAB_TOKEN / JIRA_USER + JIRA_TOKEN
64
+ # PAT:
65
+ export GITHUB_TOKEN=ghp_... # or GITLAB_TOKEN / JIRA_TOKEN
66
+ # GitHub OAuth (device flow by default):
67
+ forgeo auth login --provider github --client-id Iv1.xxx
68
+ # GitLab OAuth (browser PKCE):
69
+ forgeo auth login --provider gitlab --client-id abc123
70
+ # Jira Cloud OAuth (browser PKCE; export the configured secret for refresh):
71
+ export JIRA_CLIENT_SECRET=...
72
+ forgeo auth login --provider jira --client-id xxxx
73
+ forgeo auth status --provider github # use gitlab or jira for those providers
74
+ forgeo auth logout --provider github
65
75
  forgeo validate
66
76
  ```
67
77
 
78
+ Browser login uses an ephemeral loopback port by default. If the OAuth application requires an exact registered callback URL, register `http://127.0.0.1:8765/callback` and add `--callback-port 8765` to the login command. Use `--no-open-browser` when you want to open the printed authorization URL yourself. For a config outside the current directory, add `--config /path/to/forgeo.yaml` to the auth command.
79
+
68
80
  See [Backlog format](backlog.md) for Jira/GitHub/GitLab details. The dashboard for issue providers is a read-mostly mirror — triage stays in the native tracker.
69
81
 
70
82
  !!! tip
@@ -6,7 +6,8 @@ The **central dashboard** (`forgeo web`) is the only web interface. Daemons bind
6
6
  forgeo web # 0.0.0.0:8790 foreground
7
7
  forgeo web --port 9000 --host 127.0.0.1
8
8
  forgeo web -d # background
9
- forgeo web status / stop
9
+ forgeo web status
10
+ forgeo web stop
10
11
  forgeo web --token # bearer auth for /api/*
11
12
  ```
12
13
 
@@ -14,7 +14,7 @@ set -eu
14
14
 
15
15
  REPO_OWNER="lucaGazzola"
16
16
  REPO_NAME="forgeo"
17
- DEFAULT_VERSION="0.11.0"
17
+ DEFAULT_VERSION="1.12.1"
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.11.0"
7
+ version = "1.12.1"
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"
@@ -61,6 +61,9 @@ asyncio_default_fixture_loop_scope = "function"
61
61
  line-length = 100
62
62
  target-version = "py311"
63
63
 
64
+ [tool.ruff.lint]
65
+ select = ["E4", "E7", "E9", "F"]
66
+
64
67
  [tool.mypy]
65
68
  python_version = "3.11"
66
69
  mypy_path = "src"
@@ -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.11.0"
10
+ __version__ = "1.12.1"
@@ -51,11 +51,19 @@ def _kill_process_group(proc: asyncio.subprocess.Process) -> None:
51
51
  own session (``start_new_session=True``), so ``proc.pid`` is its process
52
52
  group id and killing the group reaps the entire process tree. Falls back
53
53
  to killing just the direct child when the group is already gone.
54
+
55
+ Platforms without process groups (Windows) fall back to killing the
56
+ direct child only.
54
57
  """
55
- try:
56
- os.killpg(proc.pid, signal.SIGKILL)
57
- except (ProcessLookupError, PermissionError):
58
- proc.kill()
58
+ killpg = getattr(os, "killpg", None)
59
+ if killpg is not None:
60
+ sigkill = getattr(signal, "SIGKILL", signal.SIGTERM)
61
+ try:
62
+ killpg(proc.pid, sigkill)
63
+ except (ProcessLookupError, PermissionError):
64
+ proc.kill()
65
+ return
66
+ proc.kill()
59
67
 
60
68
 
61
69
  class SandboxUnavailableError(RuntimeError):
@@ -39,6 +39,7 @@ from forgeo.backlog_issue_base import (
39
39
  parse_numeric_issue_id,
40
40
  parse_optional_datetime,
41
41
  require_env_token,
42
+ task_engine_state,
42
43
  )
43
44
  from forgeo.models import ExecutionResult, GithubBacklogConfig, Task, TaskStatus
44
45
 
@@ -57,11 +58,43 @@ class GithubClient:
57
58
  def __init__(self, base_url: str, config: GithubBacklogConfig) -> None:
58
59
  self.base_url = base_url.rstrip("/")
59
60
  self.config = config
61
+ self._oauth_provider: Any | None = None # lazy GithubOAuthTokenProvider when oauth is used
62
+
63
+ def _oauth_token_provider(self) -> Any | None:
64
+ if self._oauth_provider is not None:
65
+ return self._oauth_provider
66
+ auth = self.config.auth
67
+ if auth.oauth is None:
68
+ return None
69
+ from forgeo.oauth_github import GithubOAuthTokenProvider, GithubTokenStore
70
+
71
+ token_file = auth.oauth.token_file
72
+ store = GithubTokenStore(path=token_file, api_base=self.base_url) if token_file is not None else GithubTokenStore(api_base=self.base_url)
73
+ provider = GithubOAuthTokenProvider(store)
74
+ self._oauth_provider = provider
75
+ return provider
60
76
 
61
77
  def _auth_header(self) -> str:
62
- token = require_env_token(self.config.auth.token_env, "GitHub", GithubRequestError)
63
- # GitHub accepts Bearer or token; use Bearer for consistency
64
- return f"Bearer {token}"
78
+ auth = self.config.auth
79
+ if auth.token_env is not None:
80
+ token = require_env_token(auth.token_env, "GitHub", GithubRequestError)
81
+ return f"Bearer {token}"
82
+ if auth.oauth is not None:
83
+ provider = self._oauth_token_provider()
84
+ assert provider is not None
85
+ try:
86
+ token = provider.token()
87
+ except Exception as exc:
88
+ # Wrap file errors as GithubRequestError so callers surface a clear message
89
+ if isinstance(exc, GithubRequestError):
90
+ raise
91
+ from forgeo.oauth_github import GithubOAuthError
92
+
93
+ if isinstance(exc, GithubOAuthError):
94
+ raise GithubRequestError(str(exc)) from exc
95
+ raise GithubRequestError(str(exc)) from exc
96
+ return f"Bearer {token}"
97
+ raise GithubRequestError("GitHub auth is not configured (token_env or oauth required)")
65
98
 
66
99
  def _api_url(self, path: str, query: dict[str, Any] | None = None) -> str:
67
100
  url = f"{self.base_url}{path}"
@@ -77,23 +110,42 @@ class GithubClient:
77
110
  query: dict[str, Any] | None = None,
78
111
  payload: dict[str, Any] | None = None,
79
112
  ) -> Any:
80
- body = None
81
- if payload is not None:
82
- body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
83
- request = urllib.request.Request(
84
- self._api_url(path, query),
85
- data=body,
86
- method=method,
87
- headers={
88
- "Accept": "application/vnd.github+json",
89
- "Authorization": self._auth_header(),
90
- "X-GitHub-Api-Version": "2022-11-28",
91
- **({"Content-Type": "application/json"} if body is not None else {}),
92
- },
93
- )
94
- return execute_json_request(
95
- request, self.config.timeout_seconds, GithubRequestError, method
96
- )
113
+ # Retry once when GitHub rejects an OAuth token (revoked/expired)
114
+ for attempt in (0, 1):
115
+ body = None
116
+ if payload is not None:
117
+ body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
118
+ auth_header = self._auth_header()
119
+ request = urllib.request.Request(
120
+ self._api_url(path, query),
121
+ data=body,
122
+ method=method,
123
+ headers={
124
+ "Accept": "application/vnd.github+json",
125
+ "Authorization": auth_header,
126
+ "X-GitHub-Api-Version": "2022-11-28",
127
+ **({"Content-Type": "application/json"} if body is not None else {}),
128
+ },
129
+ )
130
+ try:
131
+ return execute_json_request(
132
+ request, self.config.timeout_seconds, GithubRequestError, method
133
+ )
134
+ except GithubRequestError as exc:
135
+ # On 401/403 with OAuth, invalidate cache and retry once so a freshly
136
+ # written token (e.g. after `forgeo auth login`) is picked up.
137
+ if (
138
+ attempt == 0
139
+ and exc.status in (401, 403)
140
+ and self.config.auth.oauth is not None
141
+ and self._oauth_provider is not None
142
+ ):
143
+ try:
144
+ self._oauth_provider.invalidate() # noqa: BLE001
145
+ except Exception: # noqa: BLE001
146
+ pass
147
+ continue
148
+ raise
97
149
 
98
150
  def _repo_path(self) -> str:
99
151
  # GitHub API expects owner/repo as two separate path segments;
@@ -520,14 +572,8 @@ class GithubBacklog(IssueBacklogBase):
520
572
  return task
521
573
 
522
574
  async def create_task(self, task: Task) -> Task:
523
- engine: dict[str, Any] = {
524
- "state": TaskStatus.OPEN.value,
525
- "acceptance_criteria": task.acceptance_criteria,
526
- "dependencies": task.dependencies,
527
- "files_to_modify": task.files_to_modify,
528
- }
529
- if task.review_required is not None:
530
- engine["review_required"] = task.review_required
575
+ engine: dict[str, Any] = {"state": TaskStatus.OPEN.value}
576
+ engine.update(task_engine_state(task))
531
577
  fields: dict[str, Any] = {
532
578
  "title": task.title,
533
579
  "body": embed_engine_state(task.description, engine),
@@ -539,7 +585,9 @@ class GithubBacklog(IssueBacklogBase):
539
585
  if not isinstance(number, int):
540
586
  raise GithubRequestError("GitHub create response did not contain an issue number")
541
587
  issue_id = str(number)
542
- await self.put_engine_state(issue_id, {"state": TaskStatus.OPEN.value})
588
+ # The full state (including the author-set per-task customization) is
589
+ # embedded in the body at creation; do not re-write the marker here,
590
+ # or a bare {"state": "OPEN"} would clobber those fields.
543
591
  result = await self.get_task(issue_id)
544
592
  if result is None:
545
593
  raise GithubRequestError(f"Created GitHub issue {issue_id} could not be read back")
@@ -568,20 +616,7 @@ class GithubBacklog(IssueBacklogBase):
568
616
  fields["title"] = candidate.title
569
617
  if not ENGINE_STATE_FIELDS.isdisjoint(updates):
570
618
  state = await self.get_engine_state(task_id)
571
- state.update(
572
- {
573
- "acceptance_criteria": candidate.acceptance_criteria,
574
- "dependencies": candidate.dependencies,
575
- "files_to_modify": candidate.files_to_modify,
576
- "agent_command": candidate.agent_command,
577
- "agent_timeout_seconds": candidate.agent_timeout_seconds,
578
- "run_at": candidate.run_at.isoformat() if candidate.run_at else None,
579
- "retries_left": candidate.retries_left,
580
- "review_required": candidate.review_required,
581
- "review_branch": candidate.review_branch,
582
- "review_commit_sha": candidate.review_commit_sha,
583
- }
584
- )
619
+ state.update(task_engine_state(candidate))
585
620
  fields["body"] = embed_engine_state(candidate.description, state)
586
621
  if fields:
587
622
  await self._call(self.client.update_issue, number, fields)