forgeo-cli 0.12.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.12.0 → forgeo_cli-1.12.1}/CHANGELOG.md +13 -1
  2. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/PKG-INFO +4 -3
  3. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/README.md +3 -2
  4. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/backlog.md +8 -8
  5. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/cli-reference.md +16 -5
  6. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/configuration.md +42 -5
  7. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/getting-started.md +15 -7
  8. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/web-console-api.md +2 -1
  9. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/install.sh +1 -1
  10. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/pyproject.toml +1 -1
  11. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/__init__.py +1 -1
  12. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/agent.py +12 -4
  13. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_github.py +7 -23
  14. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_gitlab.py +7 -23
  15. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_issue_base.py +25 -0
  16. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/cli.py +125 -68
  17. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/config.py +49 -1
  18. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/models.py +4 -11
  19. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/oauth_github.py +18 -7
  20. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/oauth_gitlab.py +18 -6
  21. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/oauth_jira.py +49 -27
  22. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/setup.py +63 -6
  23. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/conftest.py +37 -0
  24. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_agent.py +22 -1
  25. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_backlog_github.py +27 -0
  26. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_backlog_gitlab.py +27 -0
  27. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_cli.py +110 -24
  28. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_daemon.py +69 -64
  29. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_install.py +14 -3
  30. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_instances.py +2 -0
  31. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_models.py +44 -1
  32. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_oauth.py +29 -1
  33. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_setup.py +1 -1
  34. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_web.py +10 -3
  35. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_web_common.py +2 -0
  36. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_web_lock.py +8 -0
  37. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/www/404.html +4 -1
  38. forgeo_cli-1.12.1/www/favicon.ico +0 -0
  39. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/www/index.html +4 -1
  40. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/.github/workflows/ci.yml +0 -0
  41. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/.gitignore +0 -0
  42. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/CONTRIBUTING.md +0 -0
  43. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/LICENSE +0 -0
  44. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/config/nginx-forgeo.conf +0 -0
  45. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/agent-contract.md +0 -0
  46. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/img/console.png +0 -0
  47. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/img/demo.gif +0 -0
  48. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/img/logo.png +0 -0
  49. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/img/og.png +0 -0
  50. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/img/title.svg +0 -0
  51. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/docs/index.md +0 -0
  52. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/forgeo.spec +0 -0
  53. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/mkdocs.yml +0 -0
  54. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/scripts/__init__.py +0 -0
  55. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/scripts/render_homebrew_formula.py +0 -0
  56. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/scripts/test-github-backlog-e2e.sh +0 -0
  57. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/__main__.py +0 -0
  58. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/backlog.py +0 -0
  59. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_http.py +0 -0
  60. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/backlog_jira.py +0 -0
  61. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/central.py +0 -0
  62. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/daemon.py +0 -0
  63. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/daemon_control.py +0 -0
  64. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/forgeo.py +0 -0
  65. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/git.py +0 -0
  66. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/instances.py +0 -0
  67. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/io.py +0 -0
  68. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/notify.py +0 -0
  69. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/oauth.py +0 -0
  70. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/oauth_common.py +0 -0
  71. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/paths.py +0 -0
  72. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/runs.py +0 -0
  73. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/update.py +0 -0
  74. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/validate.py +0 -0
  75. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/central.css +0 -0
  76. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/central.js +0 -0
  77. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/index.html +0 -0
  78. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/instance.html +0 -0
  79. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web/central/login.html +0 -0
  80. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web/style.css +0 -0
  81. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/src/forgeo/web_common.py +0 -0
  82. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_backlog.py +0 -0
  83. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_backlog_http.py +0 -0
  84. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_backlog_jira.py +0 -0
  85. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_factory.py +0 -0
  86. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_git.py +0 -0
  87. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_io.py +0 -0
  88. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_paths.py +0 -0
  89. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_remote_backlog_cycle.py +0 -0
  90. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_render_homebrew.py +0 -0
  91. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_runs.py +0 -0
  92. {forgeo_cli-0.12.0 → forgeo_cli-1.12.1}/tests/test_update.py +0 -0
@@ -7,6 +7,17 @@ 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
+
10
21
  ## [0.12.0] - 2026-09-03
11
22
 
12
23
  ### Added
@@ -439,7 +450,8 @@ Initial release of the scheduled, agent-driven software forgeo.
439
450
  overlapping-run skipping.
440
451
  - Dogfooding docs removed; local configs kept out of the repository.
441
452
 
442
- [Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v0.12.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
443
455
  [0.12.0]: https://github.com/lucaGazzola/forgeo/compare/v0.11.0...v0.12.0
444
456
  [0.11.0]: https://github.com/lucaGazzola/forgeo/compare/v0.10.0...v0.11.0
445
457
  [0.10.0]: https://github.com/lucaGazzola/forgeo/compare/v0.9.0...v0.10.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: forgeo-cli
3
- Version: 0.12.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`/`gitlab`/`jira` it auto-detects `owner/repo` and offers `PAT` (`GITHUB_TOKEN` → `~/.config/forgeo/`) or browser OAuth (`forgeo auth login` → `~/.config/forgeo/tokens/`).
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`/`gitlab`/`jira` it auto-detects `owner/repo` and offers `PAT` (`GITHUB_TOKEN` → `~/.config/forgeo/`) or browser OAuth (`forgeo auth login` → `~/.config/forgeo/tokens/`).
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.
@@ -178,7 +178,7 @@ jira:
178
178
  # jira:
179
179
  # jql: 'project = APP AND labels = forgeo'
180
180
  # auth:
181
- # oauth: {client_id: xxxx, client_secret_env: JIRA_CLIENT_SECRET, scope: "offline_access read:jira-user read:jira-work"}
181
+ # oauth: {client_id: xxxx, client_secret_env: JIRA_CLIENT_SECRET, scope: "offline_access read:jira-user read:jira-work", flow: browser}
182
182
  # # then: forgeo auth login --provider jira --client-id xxxx
183
183
  ```
184
184
 
@@ -186,7 +186,7 @@ jira:
186
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).
187
187
  - Engine state (`blocker_reason`, `retry_count`, `agent_response`, etc.) in Jira issue property `forgeo`.
188
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.
189
- - 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.
190
190
  - Daemon paginates JQL; stale claims released after `claim_timeout_seconds`. Unavailable Jira fails the cycle.
191
191
 
192
192
  ## A GitHub backlog
@@ -196,14 +196,14 @@ jira:
196
196
  backlog_provider: github
197
197
  backlog: https://api.github.com # or https://github.example.com/api/v3
198
198
  github: {repo: owner/repo, auth: {token_env: GITHUB_TOKEN}}
199
- # OAuth (browser/device):
199
+ # OAuth (device or browser):
200
200
  # github: {repo: owner/repo, auth: {oauth: {client_id: Iv1.xxxx, flow: device, scope: repo}}}
201
201
  # # then: forgeo auth login --provider github --client-id Iv1.xxxx
202
202
  ```
203
203
 
204
204
  - Issue numbers → task IDs; `title`/`body`/`created_at`/`updated_at` → task fields.
205
- - `open` → `OPEN`, `closed` → `COMPLETED`; labels `forgeo-running`/`blocked`/`failed` for the rest.
206
- - 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.
207
207
  - Paginated `GET /repos/{owner}/{repo}/issues?state=all`; claiming adds `forgeo-running` + `claimed_at`.
208
208
 
209
209
  Test live with `scripts/test-github-backlog-e2e.sh`:
@@ -219,12 +219,12 @@ GITHUB_TOKEN=... ./scripts/test-github-backlog-e2e.sh
219
219
  backlog_provider: gitlab
220
220
  backlog: https://gitlab.example.com # instance root, /api/v4 appended
221
221
  gitlab: {repo: group/project, auth: {token_env: GITLAB_TOKEN}}
222
- # OAuth (browser PKCE):
222
+ # OAuth (browser PKCE, device if enabled by the instance):
223
223
  # gitlab: {repo: group/project, auth: {oauth: {client_id: abc, flow: browser, scope: api}}}
224
224
  # # then: forgeo auth login --provider gitlab --client-id abc
225
225
  ```
226
226
 
227
- - `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).
228
228
  - Paginated `GET /api/v4/projects/:id/issues?state=all`.
229
229
 
230
230
  ## Managing tasks
@@ -127,7 +127,9 @@ Table of all instances: name, daemon state, last outcome (from `runs.jsonl`). Ex
127
127
 
128
128
  ## `forgeo auth`
129
129
 
130
- Browser/OAuth login for `github`/`gitlab`/`jira` (alternative to `*_TOKEN` PAT). Tokens stored `0600` in `~/.config/forgeo/tokens/` and read by `forgeo validate`/`start`/`once`.
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.
131
133
 
132
134
  ### `forgeo auth login --provider <github|gitlab|jira>`
133
135
 
@@ -136,17 +138,26 @@ Browser/OAuth login for `github`/`gitlab`/`jira` (alternative to `*_TOKEN` PAT).
136
138
  | `--provider` | `github` (default) / `gitlab` / `jira`. |
137
139
  | `--config <file>` | `forgeo.yaml` to read `auth.oauth.client_id` (defaults to `./forgeo.yaml`). |
138
140
  | `--client-id <id>` | OAuth client ID (overrides config; required if not in `forgeo.yaml`). |
139
- | `--flow <device|browser>` | `device` (GitHub CLI) / `browser` (PKCE loopback `127.0.0.1:0/callback`). Defaults: `device` for GitHub, `browser` for GitLab/Jira. |
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. |
140
142
  | `--scope <scope>` | Scope (default `repo` / `api` / `offline_access read:jira-user read:jira-work`). |
141
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`). |
142
146
  | `--api-base <url>` | Provider API base (default from `forgeo.yaml` backlog or `https://api.github.com`/`https://gitlab.com`). |
143
- | `--no-open-browser` | Print URL instead of `webbrowser.open()`. |
147
+ | `--no-open-browser` | Print the authorization URL instead of opening it automatically. Applies to browser and device flows. |
144
148
 
145
- Device flow: prints `https://github.com/login/device` + `user_code`, polls `…/login/oauth/access_token`. Browser flow: opens `…/oauth/authorize` + PKCE `code_challenge`, listens on loopback, exchanges `code` for `access_token` (`refresh_token` for Jira). `forgeo init` with `browser` writes `auth.oauth` and offers `Run browser login now?`.
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.
146
150
 
147
151
  ### `forgeo auth status --provider <p>` / `forgeo auth logout --provider <p>`
148
152
 
149
- `status` masks token (`ghp_…1234`), shows `scope`/`expires_in`/`cloud_id` (Jira). `logout` removes the file. Both honour `--token-file`/`--api-base`/`--config`.
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.
150
161
 
151
162
  ## `forgeo web`
152
163
 
@@ -96,13 +96,21 @@ jira:
96
96
  # client_secret_env: JIRA_CLIENT_SECRET
97
97
  # scope: offline_access read:jira-user read:jira-work
98
98
  # token_file: ~/.config/forgeo/tokens/jira.json # optional
99
+ # callback_port: 8765 # optional fixed loopback port
99
100
  # # then: forgeo auth login --provider jira --client-id xxxx
100
101
  ```
101
102
 
102
103
  | Key | Default | Description |
103
104
  | --- | --- | --- |
104
105
  | `jira.jql` | — | JQL scope — include all lifecycle states. |
105
- | `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. |
106
114
  | `jira.project_key` | — | Project for dashboard task creation. |
107
115
  | `jira.issue_type` | `Task` | Issue type for creation. |
108
116
  | `jira.api_version` | `3` | `3` = Cloud cursor pagination, `2` = offset. |
@@ -136,13 +144,20 @@ github:
136
144
  # flow: device # device | browser
137
145
  # scope: repo
138
146
  # token_file: ~/.config/forgeo/tokens/github.json
147
+ # callback_port: 8765 # optional fixed loopback port for browser flow
139
148
  # # then: forgeo auth login --provider github --client-id Iv1.xxxx
140
149
  ```
141
150
 
142
151
  | Key | Default | Description |
143
152
  | --- | --- | --- |
144
153
  | `github.repo` | — | `owner/repo`. |
145
- | `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. |
146
161
  | `github.label_prefix` | `forgeo` | Label prefix. |
147
162
  | `github.property_key` | `forgeo` | Marker key for hidden body block. |
148
163
  | `github.page_size` | `30` | Issues per page. |
@@ -150,9 +165,8 @@ github:
150
165
  | `github.timeout_seconds` | `30` | HTTP timeout. |
151
166
  | `github.claim_timeout_seconds` | `86400` | Stale claim timeout. |
152
167
  | `github.workflow` | defaults | State/label mapping. |
153
- | `github.fields` | — | Field mappings for `acceptance_criteria`, `dependencies`, `files_to_modify`, `agent_command`, `agent_timeout_seconds`, `run_at`, `retries_left`. |
154
168
 
155
- 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.
156
170
 
157
171
  ### GitLab
158
172
 
@@ -173,10 +187,33 @@ gitlab:
173
187
  # flow: browser # browser | device
174
188
  # scope: api
175
189
  # token_file: ~/.config/forgeo/tokens/gitlab.json
190
+ # callback_port: 8765 # optional fixed loopback port for browser flow
176
191
  # # then: forgeo auth login --provider gitlab --client-id abc123
177
192
  ```
178
193
 
179
- 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
+ ```
180
217
 
181
218
  ## Key details
182
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`. For `github`/`gitlab`/`jira` you can pick `PAT` or `browser` (OAuth) — browser stores a token in `~/.config/forgeo/tokens/`.
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,17 +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. Authenticate with PAT **or** browser:
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_USER + JIRA_TOKEN
66
- # Browser OAuth (device flow preferred for CLI):
67
- forgeo auth login --provider github --client-id Iv1.xxx # or gitlab/jira
68
- forgeo auth status # forgeo auth logout to clear
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
69
75
  forgeo validate
70
76
  ```
71
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
+
72
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.
73
81
 
74
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.12.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.12.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"
@@ -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.12.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
 
@@ -571,14 +572,8 @@ class GithubBacklog(IssueBacklogBase):
571
572
  return task
572
573
 
573
574
  async def create_task(self, task: Task) -> Task:
574
- engine: dict[str, Any] = {
575
- "state": TaskStatus.OPEN.value,
576
- "acceptance_criteria": task.acceptance_criteria,
577
- "dependencies": task.dependencies,
578
- "files_to_modify": task.files_to_modify,
579
- }
580
- if task.review_required is not None:
581
- engine["review_required"] = task.review_required
575
+ engine: dict[str, Any] = {"state": TaskStatus.OPEN.value}
576
+ engine.update(task_engine_state(task))
582
577
  fields: dict[str, Any] = {
583
578
  "title": task.title,
584
579
  "body": embed_engine_state(task.description, engine),
@@ -590,7 +585,9 @@ class GithubBacklog(IssueBacklogBase):
590
585
  if not isinstance(number, int):
591
586
  raise GithubRequestError("GitHub create response did not contain an issue number")
592
587
  issue_id = str(number)
593
- 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.
594
591
  result = await self.get_task(issue_id)
595
592
  if result is None:
596
593
  raise GithubRequestError(f"Created GitHub issue {issue_id} could not be read back")
@@ -619,20 +616,7 @@ class GithubBacklog(IssueBacklogBase):
619
616
  fields["title"] = candidate.title
620
617
  if not ENGINE_STATE_FIELDS.isdisjoint(updates):
621
618
  state = await self.get_engine_state(task_id)
622
- state.update(
623
- {
624
- "acceptance_criteria": candidate.acceptance_criteria,
625
- "dependencies": candidate.dependencies,
626
- "files_to_modify": candidate.files_to_modify,
627
- "agent_command": candidate.agent_command,
628
- "agent_timeout_seconds": candidate.agent_timeout_seconds,
629
- "run_at": candidate.run_at.isoformat() if candidate.run_at else None,
630
- "retries_left": candidate.retries_left,
631
- "review_required": candidate.review_required,
632
- "review_branch": candidate.review_branch,
633
- "review_commit_sha": candidate.review_commit_sha,
634
- }
635
- )
619
+ state.update(task_engine_state(candidate))
636
620
  fields["body"] = embed_engine_state(candidate.description, state)
637
621
  if fields:
638
622
  await self._call(self.client.update_issue, number, fields)
@@ -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, GitlabBacklogConfig, Task, TaskStatus
44
45
 
@@ -550,14 +551,8 @@ class GitlabBacklog(IssueBacklogBase):
550
551
  return task
551
552
 
552
553
  async def create_task(self, task: Task) -> Task:
553
- engine: dict[str, Any] = {
554
- "state": TaskStatus.OPEN.value,
555
- "acceptance_criteria": task.acceptance_criteria,
556
- "dependencies": task.dependencies,
557
- "files_to_modify": task.files_to_modify,
558
- }
559
- if task.review_required is not None:
560
- engine["review_required"] = task.review_required
554
+ engine: dict[str, Any] = {"state": TaskStatus.OPEN.value}
555
+ engine.update(task_engine_state(task))
561
556
  fields: dict[str, Any] = {
562
557
  "title": task.title,
563
558
  "description": embed_engine_state(task.description, engine),
@@ -569,7 +564,9 @@ class GitlabBacklog(IssueBacklogBase):
569
564
  if not isinstance(iid, int):
570
565
  raise GitlabRequestError("GitLab create response did not contain an issue iid")
571
566
  issue_id = str(iid)
572
- await self.put_engine_state(issue_id, {"state": TaskStatus.OPEN.value})
567
+ # The full state (including the author-set per-task customization) is
568
+ # embedded in the description at creation; do not re-write the marker
569
+ # here, or a bare {"state": "OPEN"} would clobber those fields.
573
570
  result = await self.get_task(issue_id)
574
571
  if result is None:
575
572
  raise GitlabRequestError(f"Created GitLab issue {issue_id} could not be read back")
@@ -598,20 +595,7 @@ class GitlabBacklog(IssueBacklogBase):
598
595
  fields["title"] = candidate.title
599
596
  if not ENGINE_STATE_FIELDS.isdisjoint(updates):
600
597
  state = await self.get_engine_state(task_id)
601
- state.update(
602
- {
603
- "acceptance_criteria": candidate.acceptance_criteria,
604
- "dependencies": candidate.dependencies,
605
- "files_to_modify": candidate.files_to_modify,
606
- "agent_command": candidate.agent_command,
607
- "agent_timeout_seconds": candidate.agent_timeout_seconds,
608
- "run_at": candidate.run_at.isoformat() if candidate.run_at else None,
609
- "retries_left": candidate.retries_left,
610
- "review_required": candidate.review_required,
611
- "review_branch": candidate.review_branch,
612
- "review_commit_sha": candidate.review_commit_sha,
613
- }
614
- )
598
+ state.update(task_engine_state(candidate))
615
599
  fields["description"] = embed_engine_state(candidate.description, state)
616
600
  if fields:
617
601
  await self._call(self.client.update_issue, iid, fields)
@@ -300,6 +300,31 @@ ENGINE_STATE_FIELDS: frozenset[str] = frozenset(
300
300
  )
301
301
 
302
302
 
303
+ def task_engine_state(task: Any) -> dict[str, Any]:
304
+ """The author-controlled task fields stored in the hidden engine-state marker.
305
+
306
+ These are the per-task customization fields the human sets (as opposed to
307
+ engine-managed runtime state such as ``retry_count`` or ``agent_response``).
308
+ GitHub and GitLab issues expose no portable custom-field mechanism on their
309
+ REST issues API, so these fields travel in the hidden ``<!-- forgeo: ... -->``
310
+ marker; Jira maps them to custom fields instead. ``None`` values are kept
311
+ so a marker written by ``create_task`` and one refreshed by ``update_task``
312
+ stay identical, and absent values read back as ``None`` either way.
313
+ """
314
+ return {
315
+ "acceptance_criteria": task.acceptance_criteria,
316
+ "dependencies": task.dependencies,
317
+ "files_to_modify": task.files_to_modify,
318
+ "agent_command": task.agent_command,
319
+ "agent_timeout_seconds": task.agent_timeout_seconds,
320
+ "run_at": task.run_at.isoformat() if task.run_at is not None else None,
321
+ "retries_left": task.retries_left,
322
+ "review_required": task.review_required,
323
+ "review_branch": task.review_branch,
324
+ "review_commit_sha": task.review_commit_sha,
325
+ }
326
+
327
+
303
328
  # ------------------------------------------------------------------ #
304
329
  # Shared HTTP / auth helpers #
305
330
  # ------------------------------------------------------------------ #