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.
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/CHANGELOG.md +24 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/PKG-INFO +4 -3
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/README.md +3 -2
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/backlog.md +22 -7
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/cli-reference.md +34 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/configuration.md +79 -7
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/getting-started.md +16 -4
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/web-console-api.md +2 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/install.sh +1 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/pyproject.toml +4 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/__init__.py +1 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/agent.py +12 -4
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_github.py +78 -43
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_gitlab.py +71 -40
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_issue_base.py +25 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_jira.py +105 -26
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/cli.py +698 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/config.py +49 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/models.py +136 -20
- forgeo_cli-1.12.1/src/forgeo/oauth_common.py +163 -0
- forgeo_cli-1.12.1/src/forgeo/oauth_github.py +497 -0
- forgeo_cli-1.12.1/src/forgeo/oauth_gitlab.py +421 -0
- forgeo_cli-1.12.1/src/forgeo/oauth_jira.py +410 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/setup.py +325 -16
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/conftest.py +37 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_agent.py +22 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_github.py +31 -4
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_gitlab.py +27 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_cli.py +110 -24
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_daemon.py +69 -64
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_install.py +16 -5
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_instances.py +2 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_models.py +44 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_oauth.py +29 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_setup.py +1 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_web.py +10 -3
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_web_common.py +2 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_web_lock.py +8 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/www/404.html +4 -1
- forgeo_cli-1.12.1/www/favicon.ico +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/www/index.html +4 -1
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/.github/workflows/ci.yml +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/.gitignore +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/CONTRIBUTING.md +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/LICENSE +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/config/nginx-forgeo.conf +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/agent-contract.md +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/console.png +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/demo.gif +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/logo.png +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/og.png +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/img/title.svg +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/docs/index.md +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/forgeo.spec +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/mkdocs.yml +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/scripts/__init__.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/scripts/render_homebrew_formula.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/scripts/test-github-backlog-e2e.sh +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/__main__.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_http.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/central.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/daemon.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/daemon_control.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/forgeo.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/git.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/instances.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/io.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/notify.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/oauth.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/paths.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/runs.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/update.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/validate.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/central.css +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/central.js +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/index.html +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/instance.html +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/login.html +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web/style.css +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/src/forgeo/web_common.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_http.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_backlog_jira.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_factory.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_git.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_io.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_paths.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_remote_backlog_cycle.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_render_homebrew.py +0 -0
- {forgeo_cli-0.11.0 → forgeo_cli-1.12.1}/tests/test_runs.py +0 -0
- {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/
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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`
|
|
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
|
-
|
|
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`
|
|
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.
|
|
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
|
-
|
|
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.*`)
|
|
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
|
|
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.
|
|
61
|
+
**Remote providers** — `forgeo.yaml` already points at the provider. Authenticate with a PAT **or** browser OAuth:
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
|
|
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
|
|
9
|
+
forgeo web status
|
|
10
|
+
forgeo web stop
|
|
10
11
|
forgeo web --token # bearer auth for /api/*
|
|
11
12
|
```
|
|
12
13
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "forgeo-cli"
|
|
7
|
-
version = "
|
|
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"
|
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
body =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|