forgeo-cli 0.9.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/CHANGELOG.md +20 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/PKG-INFO +15 -3
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/README.md +14 -2
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/backlog.md +5 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/cli-reference.md +2 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/getting-started.md +32 -16
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/web-console-api.md +30 -9
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/install.sh +1 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/pyproject.toml +1 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/__init__.py +1 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/agent.py +25 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_github.py +11 -12
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/central.py +101 -4
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/daemon_control.py +1 -1
- forgeo_cli-0.10.0/src/forgeo/setup.py +462 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/central.css +92 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/central.js +82 -2
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/instance.html +16 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/conftest.py +20 -7
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_cli.py +7 -32
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_setup.py +54 -3
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_web.py +1 -1
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_web_lock.py +1 -1
- forgeo_cli-0.9.0/src/forgeo/setup.py +0 -230
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/.github/workflows/ci.yml +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/.gitignore +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/CONTRIBUTING.md +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/LICENSE +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/config/nginx-forgeo.conf +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/agent-contract.md +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/configuration.md +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/img/console.png +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/img/demo.gif +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/img/logo.png +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/img/og.png +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/img/title.svg +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/docs/index.md +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/forgeo.spec +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/mkdocs.yml +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/scripts/__init__.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/scripts/render_homebrew_formula.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/__main__.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/backlog.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_gitlab.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_http.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_issue_base.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/backlog_jira.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/cli.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/config.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/daemon.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/forgeo.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/git.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/instances.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/io.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/models.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/notify.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/oauth.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/paths.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/runs.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/update.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/validate.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/index.html +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web/central/login.html +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web/style.css +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/src/forgeo/web_common.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_agent.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_backlog.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_backlog_github.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_backlog_gitlab.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_backlog_http.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_backlog_jira.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_daemon.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_factory.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_git.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_install.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_instances.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_io.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_models.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_oauth.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_paths.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_remote_backlog_cycle.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_render_homebrew.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_runs.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_update.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/tests/test_web_common.py +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/www/404.html +0 -0
- {forgeo_cli-0.9.0 → forgeo_cli-0.10.0}/www/index.html +0 -0
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-08-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `forgeo init` now asks for **backlog provider** (`file`/`github`/`gitlab`/`jira`/`http`). For `github` it auto-detects `owner/repo` from `git remote origin`, prompts for `token_env` (`GITHUB_TOKEN`) and can persist a pasted classic PAT (`ghp_...`, scope `repo`) to `~/.config/forgeo/github_token_env.sh` (600, wired to `~/.bashrc`). Same for `gitlab` (`GITLAB_TOKEN`, base URL) and `jira`/`http`. The wizard writes `backlog: https://api.github.com`, `backlog_provider: github`, `github: {repo, auth: {token_env}}` and `state_dir: .forgeo` automatically.
|
|
15
|
+
- Web console now treats `jira`/`github`/`gitlab` as a **read-mostly mirror** instead of a replacement: the home page cards show `Open in Jira/GitHub/GitLab ↗` (via `external_board_url`/`external_board_label`), the instance page shows a top banner linking to the native board, and each task card/modal links to the native issue (`external_url`). Document backlogs (`file`/`http`) keep the existing primary-editor behaviour. Creating/editing still works through the dashboard, but triage is expected in the native tracker where Forgeo-specific state (BLOCKED/FAILED reasons, `agent_response`, retry budget) is now surfaced on the mirror.
|
|
16
|
+
- Central API now exposes `backlog_provider`, `backlog`, `backlog_is_issue_provider`, `external_board_url`/`external_board_label` on `GET /api/instances` and `GET /api/instances/<name>/status`, and `external_url` per task on `GET /api/instances/<name>/tasks` (+ single-task) for issue providers. Covers `https://api.github.com` → `https://github.com` and `https://…/api/v3` → web base mapping for GitHub Enterprise, and `https://jira…/issues/?jql=` / `https://gitlab…/{repo}/-/issues` board links.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- GitHub provider now encodes `owner/repo` as two path segments (`quote` per segment, keeping `/`) instead of `quote(repo, safe='')` which produced `owner%2Frepo` and 404 on every `GET /repos/{owner%2Frepo}/issues`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `README`, `docs/backlog.md`, `docs/getting-started.md` and `docs/web-console-api.md` document the mirror vs editor split and the new `external_*` API fields.
|
|
25
|
+
- `src/forgeo/central.py` deduplicates provider metadata via `_backlog_meta()` and centralises GitHub web-base handling; `instance-card` is now a `div[role=link]` to allow nested external links without invalid HTML.
|
|
26
|
+
- `README`, `docs/getting-started.md` and `docs/cli-reference.md` document the new backlog-provider step in `forgeo init` and the `GITHUB_TOKEN` PAT setup (`https://github.com/settings/tokens/new`, scope `repo`).
|
|
27
|
+
|
|
10
28
|
## [0.9.0] - 2026-08-23
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -389,7 +407,8 @@ Initial release of the scheduled, agent-driven software forgeo.
|
|
|
389
407
|
overlapping-run skipping.
|
|
390
408
|
- Dogfooding docs removed; local configs kept out of the repository.
|
|
391
409
|
|
|
392
|
-
[Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v0.
|
|
410
|
+
[Unreleased]: https://github.com/lucaGazzola/forgeo/compare/v0.10.0...HEAD
|
|
411
|
+
[0.10.0]: https://github.com/lucaGazzola/forgeo/compare/v0.9.0...v0.10.0
|
|
393
412
|
[0.9.0]: https://github.com/lucaGazzola/forgeo/compare/v0.8.0...v0.9.0
|
|
394
413
|
[0.8.0]: https://github.com/lucaGazzola/forgeo/compare/v0.7.3...v0.8.0
|
|
395
414
|
[0.7.3]: https://github.com/lucaGazzola/forgeo/compare/v0.7.2...v0.7.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: forgeo-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: A scheduled software forgeo: executes backlog tasks on main, refactors when idle, and writes BLOCKER.md when it needs human input.
|
|
5
5
|
Project-URL: Homepage, https://forgeo.org
|
|
6
6
|
Project-URL: Documentation, https://forgeo.org
|
|
@@ -110,6 +110,10 @@ forgeo init
|
|
|
110
110
|
|
|
111
111
|
Guided wizard, run from your project root. Writes `forgeo.yaml` (the
|
|
112
112
|
config) and a `.forgeo/` folder for the backlog, logs and blocker files.
|
|
113
|
+
The wizard asks for the backlog provider (`file` for a local JSON file, or
|
|
114
|
+
`github`/`gitlab`/`jira`/`http` for an external tracker — for `github` it
|
|
115
|
+
auto-detects `owner/repo` from `git remote` and can persist a pasted
|
|
116
|
+
`GITHUB_TOKEN` to `~/.config/forgeo/github_token_env.sh`).
|
|
113
117
|
|
|
114
118
|
The base flow is then three steps: fill the backlog, check the
|
|
115
119
|
configuration, start the daemon.
|
|
@@ -121,8 +125,10 @@ configuration, start the daemon.
|
|
|
121
125
|
# Backlog format), created on first use:
|
|
122
126
|
# .forgeo/backlog.json
|
|
123
127
|
|
|
124
|
-
# Or:
|
|
125
|
-
#
|
|
128
|
+
# Or: pick github/gitlab/jira in forgeo init (or configure forgeo.yaml
|
|
129
|
+
# manually): set backlog_provider: github and backlog: https://api.github.com
|
|
130
|
+
# with github.repo + token_env, then export GITHUB_TOKEN and run
|
|
131
|
+
# forgeo validate. See backlog docs for Jira/GitHub/GitLab.
|
|
126
132
|
|
|
127
133
|
# Or: add tasks from the web console once your forgeo is registered
|
|
128
134
|
# (first `forgeo start` registers it automatically):
|
|
@@ -226,6 +232,12 @@ individually with workflow state and engine metadata stored on the issue. A *fil
|
|
|
226
232
|
restored automatically if it is ever found corrupt — a bad write never loses
|
|
227
233
|
your tasks.
|
|
228
234
|
|
|
235
|
+
The central dashboard (`forgeo web`) mirrors every instance: for `file`/`http` it is the primary editor; for
|
|
236
|
+
`jira`/`github`/`gitlab` it is a read-mostly mirror of the native tracker — each task card and its detail modal link
|
|
237
|
+
to the native issue (`Open in Jira/GitHub/GitLab ↗`), a top banner links to the native board, and the board surfaces
|
|
238
|
+
Forgeo-specific state the native UI does not (BLOCKED/FAILED reasons, `agent_response`, retry budget) — triage stays in
|
|
239
|
+
the tracker (see [Web console & HTTP API](docs/web-console-api.md)).
|
|
240
|
+
|
|
229
241
|
## Develop
|
|
230
242
|
|
|
231
243
|
```bash
|
|
@@ -56,6 +56,10 @@ forgeo init
|
|
|
56
56
|
|
|
57
57
|
Guided wizard, run from your project root. Writes `forgeo.yaml` (the
|
|
58
58
|
config) and a `.forgeo/` folder for the backlog, logs and blocker files.
|
|
59
|
+
The wizard asks for the backlog provider (`file` for a local JSON file, or
|
|
60
|
+
`github`/`gitlab`/`jira`/`http` for an external tracker — for `github` it
|
|
61
|
+
auto-detects `owner/repo` from `git remote` and can persist a pasted
|
|
62
|
+
`GITHUB_TOKEN` to `~/.config/forgeo/github_token_env.sh`).
|
|
59
63
|
|
|
60
64
|
The base flow is then three steps: fill the backlog, check the
|
|
61
65
|
configuration, start the daemon.
|
|
@@ -67,8 +71,10 @@ configuration, start the daemon.
|
|
|
67
71
|
# Backlog format), created on first use:
|
|
68
72
|
# .forgeo/backlog.json
|
|
69
73
|
|
|
70
|
-
# Or:
|
|
71
|
-
#
|
|
74
|
+
# Or: pick github/gitlab/jira in forgeo init (or configure forgeo.yaml
|
|
75
|
+
# manually): set backlog_provider: github and backlog: https://api.github.com
|
|
76
|
+
# with github.repo + token_env, then export GITHUB_TOKEN and run
|
|
77
|
+
# forgeo validate. See backlog docs for Jira/GitHub/GitLab.
|
|
72
78
|
|
|
73
79
|
# Or: add tasks from the web console once your forgeo is registered
|
|
74
80
|
# (first `forgeo start` registers it automatically):
|
|
@@ -172,6 +178,12 @@ individually with workflow state and engine metadata stored on the issue. A *fil
|
|
|
172
178
|
restored automatically if it is ever found corrupt — a bad write never loses
|
|
173
179
|
your tasks.
|
|
174
180
|
|
|
181
|
+
The central dashboard (`forgeo web`) mirrors every instance: for `file`/`http` it is the primary editor; for
|
|
182
|
+
`jira`/`github`/`gitlab` it is a read-mostly mirror of the native tracker — each task card and its detail modal link
|
|
183
|
+
to the native issue (`Open in Jira/GitHub/GitLab ↗`), a top banner links to the native board, and the board surfaces
|
|
184
|
+
Forgeo-specific state the native UI does not (BLOCKED/FAILED reasons, `agent_response`, retry budget) — triage stays in
|
|
185
|
+
the tracker (see [Web console & HTTP API](docs/web-console-api.md)).
|
|
186
|
+
|
|
175
187
|
## Develop
|
|
176
188
|
|
|
177
189
|
```bash
|
|
@@ -144,6 +144,11 @@ You add, remove, or reopen tasks by editing the file directly — or use the
|
|
|
144
144
|
the task detail modal's **Edit** button updates an existing task's fields
|
|
145
145
|
(`PATCH /api/instances/<name>/tasks/<id>`), while its **Delete** button
|
|
146
146
|
removes an `OPEN` or `BLOCKED` task (`DELETE /api/instances/<name>/tasks/<id>`).
|
|
147
|
+
For `file`/`http` backlogs this is the primary editor; for `jira`/`github`/`gitlab` the web console is a
|
|
148
|
+
read-mostly **mirror** of the native tracker — a top banner links to the external board, each task card/modal
|
|
149
|
+
links to the native issue (`Open in Jira/GitHub/GitLab ↗`), and the board surfaces Forgeo-specific state
|
|
150
|
+
(`blocker_reason`, `failure_reason`, `agent_response`, retry budget) that the tracker does not — triage stays in
|
|
151
|
+
Jira/GitHub/GitLab, Forgeo reflects it (see [Web console & HTTP API](web-console-api.md)).
|
|
147
152
|
|
|
148
153
|
### Resolving a blocked task
|
|
149
154
|
|
|
@@ -23,6 +23,8 @@ Guided first-time setup: interactively write a `forgeo.yaml`.
|
|
|
23
23
|
| <span style="white-space: nowrap">`--config <file>`</span> | Where to write the config (default `forgeo.yaml`). |
|
|
24
24
|
| <span style="white-space: nowrap">`--force`</span> | Overwrite an existing config file. |
|
|
25
25
|
|
|
26
|
+
The wizard asks for: Forgeo folder, backlog provider (`file`/`github`/`gitlab`/`jira`/`http` — for `github` it auto-detects `owner/repo` from `git remote` and can persist `GITHUB_TOKEN`), coding agent command, and refactor prompt.
|
|
27
|
+
|
|
26
28
|
Exit codes:
|
|
27
29
|
|
|
28
30
|
- `0` — config written.
|
|
@@ -56,20 +56,29 @@ Run the guided wizard from your project root:
|
|
|
56
56
|
forgeo init
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
The wizard asks for
|
|
59
|
+
The wizard asks for:
|
|
60
60
|
|
|
61
61
|
1. **Forgeo folder** — where the backlog, `BLOCKER.md` and the log live
|
|
62
62
|
(default `.forgeo`). It is gitignored by default.
|
|
63
|
-
2. **
|
|
63
|
+
2. **Backlog provider** — where tasks live: `file` (local `.forgeo/backlog.json`),
|
|
64
|
+
`github` / `gitlab` / `jira` / `http`. For `github` it auto-detects
|
|
65
|
+
`owner/repo` from `git remote origin`, asks for `token_env` (default
|
|
66
|
+
`GITHUB_TOKEN`) and can persist a pasted classic PAT (`ghp_...`, scope `repo`)
|
|
67
|
+
to `~/.config/forgeo/github_token_env.sh` (600, wired to `~/.bashrc`). Same
|
|
68
|
+
for `gitlab` (`GITLAB_TOKEN`, base URL) and `jira`/`http`.
|
|
69
|
+
3. **Coding agent command** — the bare command that launches your coding
|
|
64
70
|
agent (default `opencode run --auto`). Forgeo appends the standard task
|
|
65
71
|
prompt (which ends in `$FORGEO_TASK`) automatically, so you never type
|
|
66
72
|
it. Enter a command that already references `$FORGEO_TASK` and it is
|
|
67
73
|
kept verbatim.
|
|
68
|
-
|
|
74
|
+
4. **Refactor prompt** — the instruction used when the backlog is empty; the
|
|
69
75
|
default is offered, or you can paste a custom one.
|
|
70
76
|
|
|
71
77
|
`forgeo init` writes `forgeo.yaml`, creates Forgeo folder, and appends
|
|
72
|
-
`<folder>/` to `.gitignore` (unless you opt out).
|
|
78
|
+
`<folder>/` to `.gitignore` (unless you opt out). For `github`/`gitlab`/`jira`
|
|
79
|
+
set `backlog_provider` + `backlog` URL + provider block is written automatically
|
|
80
|
+
and `state_dir` is set to the Forgeo folder so runtime files stay beside the
|
|
81
|
+
config.
|
|
73
82
|
|
|
74
83
|
```bash
|
|
75
84
|
forgeo init --force # overwrite an existing forgeo.yaml
|
|
@@ -77,18 +86,25 @@ forgeo init --force # overwrite an existing forgeo.yaml
|
|
|
77
86
|
|
|
78
87
|
## 3. Create your first backlog
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`jira
|
|
91
|
-
|
|
89
|
+
If you chose `file` in the wizard, the backlog is a plain JSON file (see
|
|
90
|
+
[Backlog format](backlog.md)) — by default `.forgeo/backlog.json`. Once Forgeo
|
|
91
|
+
is running you can also add tasks from the [web console](web-console-api.md) — no
|
|
92
|
+
file editing needed.
|
|
93
|
+
|
|
94
|
+
If you chose `github`/`gitlab`/`jira`/`http` in the wizard, your `forgeo.yaml`
|
|
95
|
+
already points at the provider (`backlog: https://api.github.com` etc.).
|
|
96
|
+
For `github` create a classic PAT at `https://github.com/settings/tokens/new`
|
|
97
|
+
(scope `repo`), `export GITHUB_TOKEN=ghp_...` (or let the wizard persist it),
|
|
98
|
+
then `forgeo validate` before `forgeo start`. Same for `gitlab` (`GITLAB_TOKEN`)
|
|
99
|
+
and `jira`. See [Backlog format](backlog.md) for provider details.
|
|
100
|
+
|
|
101
|
+
For `jira`/`github`/`gitlab`, set `backlog_provider:` to the provider, point `backlog:` at its base URL
|
|
102
|
+
(`https://jira.example.com`, `https://api.github.com` / `https://github.example.com/api/v3`,
|
|
103
|
+
`https://gitlab.example.com`), configure the provider block (`jira.jql` / `github.repo` / `gitlab.repo` and auth),
|
|
104
|
+
export the credentials named in `*_auth.token_env`, and run `forgeo validate` before starting the daemon.
|
|
105
|
+
The dashboard for these providers is a read-mostly mirror: a banner links to the native board, each card links to
|
|
106
|
+
the native issue, and Forgeo-specific state (BLOCKED/FAILED reasons, `agent_response`) is surfaced on the board — triage
|
|
107
|
+
stays in Jira/GitHub/GitLab. See [Backlog: Jira/GitHub/GitLab](backlog.md) for the complete configuration.
|
|
92
108
|
|
|
93
109
|
```json
|
|
94
110
|
{
|
|
@@ -66,11 +66,20 @@ open-by-default behavior.
|
|
|
66
66
|
|
|
67
67
|
- `GET /` — home page listing every registered instance: name, repository,
|
|
68
68
|
daemon state (lock held), last outcome, next run, and per-status backlog
|
|
69
|
-
counts, each linking to its instance page.
|
|
69
|
+
counts, each linking to its instance page. For issue-backed instances
|
|
70
|
+
(`jira`/`github`/`gitlab`) the card also shows an **Open in Jira/GitHub/GitLab ↗**
|
|
71
|
+
link to the native board.
|
|
70
72
|
- `GET /instances/<name>/` — one instance's page: a kanban backlog, a
|
|
71
73
|
**Create** tab with a form to add tasks (including an optional *Run at*
|
|
72
74
|
date/time input for a one-shot schedule), plus tabs for **logs**, **history**,
|
|
73
|
-
**blocker** and **config**.
|
|
75
|
+
**blocker** and **config**. For document backlogs (`file`/`http`) this board
|
|
76
|
+
is the primary editor; for issue providers (`jira`/`github`/`gitlab`) it is a
|
|
77
|
+
read-mostly **mirror** of the native tracker: a banner at the top links to
|
|
78
|
+
the external board (`Jira`/`GitHub`/`GitLab`), each task card and its detail
|
|
79
|
+
modal carry an **Open in external ↗** link to the native issue, and the board
|
|
80
|
+
surfaces Forgeo-specific state that the native UI does not (BLOCKED/FAILED
|
|
81
|
+
reasons, `agent_response`, retry budget). Creating/editing still works via
|
|
82
|
+
the dashboard, but triage is expected in the native tool. The header carries a **DAEMON** section with the
|
|
74
83
|
daemon status tag (`running`/`stopped`) and **Start**/**Stop**/**Restart**
|
|
75
84
|
buttons that call `POST /api/instances/<name>/start|stop|restart`; the
|
|
76
85
|
buttons reflect the current state (Start is disabled while running, Stop
|
|
@@ -152,15 +161,19 @@ outcome, next run, and backlog counts.
|
|
|
152
161
|
curl http://127.0.0.1:8790/api/instances
|
|
153
162
|
```
|
|
154
163
|
|
|
155
|
-
Each row also carries `
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
Each row also carries `backlog_provider` (`file`/`http`/`jira`/`github`/`gitlab`),
|
|
165
|
+
`backlog` (path or base URL), `backlog_is_issue_provider`, and for issue
|
|
166
|
+
providers `external_board_url`/`external_board_label` (the native board link
|
|
167
|
+
shown on the home cards). It also carries `backlog_error`: `null` normally,
|
|
168
|
+
and the reason when that instance's remote backlog could not be read. Its
|
|
169
|
+
counts are zero in that case — the row reports a backlog it could not reach,
|
|
170
|
+
not an empty one. One unreachable provider never fails the whole listing.
|
|
159
171
|
|
|
160
172
|
### `GET /api/instances/<name>/tasks`
|
|
161
173
|
|
|
162
174
|
List every task in that instance's backlog, in creation order. Each task
|
|
163
|
-
carries extra `unsatisfied_dependencies
|
|
175
|
+
carries extra `unsatisfied_dependencies`, retry fields (see below), and for
|
|
176
|
+
issue providers `external_url` (link to the native Jira/GitHub/GitLab issue).
|
|
164
177
|
|
|
165
178
|
```bash
|
|
166
179
|
curl http://127.0.0.1:8790/api/instances/my-repo/tasks
|
|
@@ -400,7 +413,10 @@ Errors:
|
|
|
400
413
|
### `GET /api/instances/<name>/status`
|
|
401
414
|
|
|
402
415
|
Daemon status: name, repo, interval, `daemon_running` (whether the instance's
|
|
403
|
-
lock is held), the recorded PID, `last_outcome`, and the `next_run_at`.
|
|
416
|
+
lock is held), the recorded PID, `last_outcome`, and the `next_run_at`. Also
|
|
417
|
+
carries `backlog_provider`, `backlog`, `backlog_is_issue_provider`,
|
|
418
|
+
`external_board_url`/`external_board_label` so the instance page can render the
|
|
419
|
+
issue-provider banner and per-task external links.
|
|
404
420
|
|
|
405
421
|
```bash
|
|
406
422
|
curl http://127.0.0.1:8790/api/instances/my-repo/status
|
|
@@ -414,7 +430,12 @@ curl http://127.0.0.1:8790/api/instances/my-repo/status
|
|
|
414
430
|
"daemon_running": true,
|
|
415
431
|
"pid": 4242,
|
|
416
432
|
"last_outcome": "task",
|
|
417
|
-
"next_run_at": "2026-08-01T12:00:00+00:00"
|
|
433
|
+
"next_run_at": "2026-08-01T12:00:00+00:00",
|
|
434
|
+
"backlog_provider": "github",
|
|
435
|
+
"backlog": "https://api.github.com",
|
|
436
|
+
"backlog_is_issue_provider": true,
|
|
437
|
+
"external_board_url": "https://github.com/owner/repo/issues",
|
|
438
|
+
"external_board_label": "GitHub"
|
|
418
439
|
}
|
|
419
440
|
```
|
|
420
441
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "forgeo-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0"
|
|
8
8
|
description = "A scheduled software forgeo: executes backlog tasks on main, refactors when idle, and writes BLOCKER.md when it needs human input."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -206,7 +206,26 @@ class ShellAgent(BaseAgent):
|
|
|
206
206
|
except TimeoutError:
|
|
207
207
|
timed_out = True
|
|
208
208
|
_kill_process_group(proc)
|
|
209
|
-
|
|
209
|
+
# ``proc.wait()`` waits for both process exit *and* pipe closure
|
|
210
|
+
# (see ``BaseSubprocessTransport._try_finish``). A grandchild that
|
|
211
|
+
# called ``setsid()`` and holds the write end of the pipe keeps it
|
|
212
|
+
# open forever, so the wait would hang past the drain deadline.
|
|
213
|
+
# Bound it by ``drain_timeout_seconds`` and force-close the
|
|
214
|
+
# transport if it still hangs.
|
|
215
|
+
try:
|
|
216
|
+
await asyncio.wait_for(proc.wait(), timeout=self.drain_timeout_seconds)
|
|
217
|
+
except TimeoutError:
|
|
218
|
+
try:
|
|
219
|
+
proc._transport.close() # type: ignore[attr-defined]
|
|
220
|
+
except Exception: # noqa: BLE001, S110 - close must not fail the task
|
|
221
|
+
pass
|
|
222
|
+
try:
|
|
223
|
+
await asyncio.wait_for(proc.wait(), timeout=1.0)
|
|
224
|
+
except TimeoutError:
|
|
225
|
+
logs.append(
|
|
226
|
+
f"[{self.name}] Process did not exit within "
|
|
227
|
+
f"{self.drain_timeout_seconds:g}s after kill; proceeding."
|
|
228
|
+
)
|
|
210
229
|
# Always finish draining so lines already written (and any residual
|
|
211
230
|
# after kill) are captured before we build the result. Bounded: a
|
|
212
231
|
# grandchild that escaped the process group (daemonized agent, docker
|
|
@@ -215,6 +234,11 @@ class ShellAgent(BaseAgent):
|
|
|
215
234
|
try:
|
|
216
235
|
await asyncio.wait_for(readers, timeout=self.drain_timeout_seconds)
|
|
217
236
|
except TimeoutError:
|
|
237
|
+
readers.cancel()
|
|
238
|
+
try:
|
|
239
|
+
await readers
|
|
240
|
+
except asyncio.CancelledError:
|
|
241
|
+
pass
|
|
218
242
|
logs.append(
|
|
219
243
|
f"[{self.name}] Output streams stayed open beyond "
|
|
220
244
|
f"{self.drain_timeout_seconds:g}s; proceeding without them."
|
|
@@ -111,6 +111,11 @@ class GithubClient:
|
|
|
111
111
|
) from exc
|
|
112
112
|
return data
|
|
113
113
|
|
|
114
|
+
def _repo_path(self) -> str:
|
|
115
|
+
# GitHub API expects owner/repo as two separate path segments;
|
|
116
|
+
# encode each segment but keep the slash between them.
|
|
117
|
+
return "/".join(quote(part, safe="") for part in self.config.repo.split("/"))
|
|
118
|
+
|
|
114
119
|
def search_issues(
|
|
115
120
|
self,
|
|
116
121
|
*,
|
|
@@ -118,8 +123,7 @@ class GithubClient:
|
|
|
118
123
|
per_page: int = 30,
|
|
119
124
|
state: str = "all",
|
|
120
125
|
) -> list[dict[str, Any]]:
|
|
121
|
-
|
|
122
|
-
path = f"/repos/{quote(repo, safe='')}/issues"
|
|
126
|
+
path = f"/repos/{self._repo_path()}/issues"
|
|
123
127
|
query: dict[str, Any] = {"state": state, "per_page": per_page, "page": page}
|
|
124
128
|
data = self._request("GET", path, query=query)
|
|
125
129
|
if isinstance(data, list):
|
|
@@ -127,28 +131,23 @@ class GithubClient:
|
|
|
127
131
|
return []
|
|
128
132
|
|
|
129
133
|
def get_issue(self, issue_number: int) -> dict[str, Any]:
|
|
130
|
-
|
|
131
|
-
path = f"/repos/{quote(repo, safe='')}/issues/{issue_number}"
|
|
134
|
+
path = f"/repos/{self._repo_path()}/issues/{issue_number}"
|
|
132
135
|
return self._request("GET", path) # type: ignore[no-any-return]
|
|
133
136
|
|
|
134
137
|
def create_issue(self, fields: dict[str, Any]) -> dict[str, Any]:
|
|
135
|
-
|
|
136
|
-
path = f"/repos/{quote(repo, safe='')}/issues"
|
|
138
|
+
path = f"/repos/{self._repo_path()}/issues"
|
|
137
139
|
return self._request("POST", path, payload=fields) # type: ignore[no-any-return]
|
|
138
140
|
|
|
139
141
|
def update_issue(self, issue_number: int, fields: dict[str, Any]) -> dict[str, Any]:
|
|
140
|
-
|
|
141
|
-
path = f"/repos/{quote(repo, safe='')}/issues/{issue_number}"
|
|
142
|
+
path = f"/repos/{self._repo_path()}/issues/{issue_number}"
|
|
142
143
|
return self._request("PATCH", path, payload=fields) # type: ignore[no-any-return]
|
|
143
144
|
|
|
144
145
|
def add_comment(self, issue_number: int, body: str) -> None:
|
|
145
|
-
|
|
146
|
-
path = f"/repos/{quote(repo, safe='')}/issues/{issue_number}/comments"
|
|
146
|
+
path = f"/repos/{self._repo_path()}/issues/{issue_number}/comments"
|
|
147
147
|
self._request("POST", path, payload={"body": body})
|
|
148
148
|
|
|
149
149
|
def delete_issue(self, issue_number: int) -> None:
|
|
150
|
-
|
|
151
|
-
path = f"/repos/{quote(repo, safe='')}/issues/{issue_number}"
|
|
150
|
+
path = f"/repos/{self._repo_path()}/issues/{issue_number}"
|
|
152
151
|
self._request("PATCH", path, payload={"state": "closed"})
|
|
153
152
|
|
|
154
153
|
|
|
@@ -68,7 +68,7 @@ from datetime import datetime, timedelta
|
|
|
68
68
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
69
69
|
from pathlib import Path
|
|
70
70
|
from typing import Any
|
|
71
|
-
from urllib.parse import parse_qs, unquote, urlparse
|
|
71
|
+
from urllib.parse import parse_qs, quote, unquote, urlparse
|
|
72
72
|
|
|
73
73
|
from pydantic import ValidationError
|
|
74
74
|
from rich.console import Console
|
|
@@ -88,7 +88,7 @@ from forgeo.instances import (
|
|
|
88
88
|
list_instances,
|
|
89
89
|
registry_path,
|
|
90
90
|
)
|
|
91
|
-
from forgeo.models import ForgeoConfig, Task, TaskStatus
|
|
91
|
+
from forgeo.models import ISSUE_PROVIDERS, ForgeoConfig, Task, TaskStatus
|
|
92
92
|
from forgeo.paths import daemon_state_path, lock_path, runs_path
|
|
93
93
|
from forgeo.runs import RunRecorder
|
|
94
94
|
from forgeo.web_common import (
|
|
@@ -119,6 +119,71 @@ DEFAULT_FORGEO_CONFIG_DIR = Path.home() / ".config" / "forgeo"
|
|
|
119
119
|
_HOME_PAGE = "/central/index.html"
|
|
120
120
|
_INSTANCE_PAGE = "/central/instance.html"
|
|
121
121
|
|
|
122
|
+
_ISSUE_PROVIDER_LABELS: dict[str, str] = {
|
|
123
|
+
"jira": "Jira",
|
|
124
|
+
"github": "GitHub",
|
|
125
|
+
"gitlab": "GitLab",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _github_web_base(api_base: str) -> str:
|
|
130
|
+
"""Derive the web base from a GitHub API base URL.
|
|
131
|
+
|
|
132
|
+
``https://api.github.com`` → ``https://github.com``,
|
|
133
|
+
``https://github.example.com/api/v3`` → ``https://github.example.com``,
|
|
134
|
+
otherwise the base itself when already a web URL.
|
|
135
|
+
"""
|
|
136
|
+
base = api_base.rstrip("/")
|
|
137
|
+
if base.endswith("/api/v3"):
|
|
138
|
+
return base[:-7].rstrip("/")
|
|
139
|
+
if "api.github.com" in base:
|
|
140
|
+
return base.replace("api.github.com", "github.com")
|
|
141
|
+
return base
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _external_board_url(config: ForgeoConfig | None) -> str | None:
|
|
145
|
+
"""The native backlog URL for an issue provider, or ``None`` for documents."""
|
|
146
|
+
if config is None or not isinstance(config.backlog, str):
|
|
147
|
+
return None
|
|
148
|
+
provider = config.effective_backlog_provider
|
|
149
|
+
base = config.backlog.rstrip("/")
|
|
150
|
+
if provider == "jira":
|
|
151
|
+
if config.jira is None:
|
|
152
|
+
return base
|
|
153
|
+
return f"{base}/issues/?jql={quote(config.jira.jql, safe='')}"
|
|
154
|
+
if provider == "github" and config.github is not None:
|
|
155
|
+
web_base = _github_web_base(base)
|
|
156
|
+
repo = config.github.repo.strip("/")
|
|
157
|
+
return f"{web_base}/{repo}/issues"
|
|
158
|
+
if provider == "gitlab" and config.gitlab is not None:
|
|
159
|
+
repo = config.gitlab.repo.strip("/")
|
|
160
|
+
if repo.isdigit():
|
|
161
|
+
return f"{base}/-/issues"
|
|
162
|
+
return f"{base}/{repo}/-/issues"
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _external_issue_url(config: ForgeoConfig | None, task_id: str) -> str | None:
|
|
167
|
+
"""The native issue URL for one task, or ``None`` for document providers."""
|
|
168
|
+
if config is None or not isinstance(config.backlog, str) or not task_id:
|
|
169
|
+
return None
|
|
170
|
+
provider = config.effective_backlog_provider
|
|
171
|
+
base = config.backlog.rstrip("/")
|
|
172
|
+
if provider == "jira":
|
|
173
|
+
return f"{base}/browse/{quote(task_id, safe='')}"
|
|
174
|
+
if provider == "github" and config.github is not None:
|
|
175
|
+
web_base = _github_web_base(base)
|
|
176
|
+
repo = config.github.repo.strip("/")
|
|
177
|
+
# GitHub ids are numeric; a stale WEB-### prefix is stripped for the URL.
|
|
178
|
+
issue_num = task_id.split("-")[-1] if "-" in task_id and task_id.rsplit("-", 1)[-1].isdigit() else task_id
|
|
179
|
+
return f"{web_base}/{repo}/issues/{issue_num}"
|
|
180
|
+
if provider == "gitlab" and config.gitlab is not None:
|
|
181
|
+
repo = config.gitlab.repo.strip("/")
|
|
182
|
+
if repo.isdigit():
|
|
183
|
+
return f"{base}/-/issues/{task_id}"
|
|
184
|
+
return f"{base}/{repo}/-/issues/{task_id}"
|
|
185
|
+
return None
|
|
186
|
+
|
|
122
187
|
_WEB_TASK_ID_RE = re.compile(r"^WEB-(\d+)$")
|
|
123
188
|
|
|
124
189
|
|
|
@@ -412,7 +477,7 @@ def _task_payload(
|
|
|
412
477
|
tasks: list[Task],
|
|
413
478
|
config: ForgeoConfig | None = None,
|
|
414
479
|
) -> dict[str, Any]:
|
|
415
|
-
"""Serialize a task for the API, annotating
|
|
480
|
+
"""Serialize a task for the API, annotating dependencies and external links.
|
|
416
481
|
|
|
417
482
|
The extra ``unsatisfied_dependencies`` field lists every dependency id
|
|
418
483
|
that is not ``COMPLETED`` yet (with its current status, or ``missing``
|
|
@@ -422,6 +487,8 @@ def _task_payload(
|
|
|
422
487
|
(``retry_budget``, the per-task ``retries_left`` override falling back to
|
|
423
488
|
the config's ``failed_retry_max``) and ``retries_remaining`` so the
|
|
424
489
|
console can show whether a FAILED task will be retried automatically.
|
|
490
|
+
For issue providers the payload also carries ``external_url`` pointing at
|
|
491
|
+
the native issue page.
|
|
425
492
|
"""
|
|
426
493
|
payload = task.model_dump(mode="json")
|
|
427
494
|
payload["unsatisfied_dependencies"] = unsatisfied_dependencies(tasks, task)
|
|
@@ -429,6 +496,9 @@ def _task_payload(
|
|
|
429
496
|
budget = task.retries_left if task.retries_left is not None else config.failed_retry_max
|
|
430
497
|
payload["retry_budget"] = budget
|
|
431
498
|
payload["retries_remaining"] = max(0, budget - task.retry_count)
|
|
499
|
+
external = _external_issue_url(config, task.id)
|
|
500
|
+
if external is not None:
|
|
501
|
+
payload["external_url"] = external
|
|
432
502
|
return payload
|
|
433
503
|
|
|
434
504
|
|
|
@@ -481,6 +551,29 @@ def _next_run(info: InstanceInfo, config: ForgeoConfig | None) -> str | None:
|
|
|
481
551
|
return iso(estimate)
|
|
482
552
|
|
|
483
553
|
|
|
554
|
+
def _backlog_meta(config: ForgeoConfig | None) -> dict[str, Any]:
|
|
555
|
+
"""Provider and external URL for a config, or document defaults."""
|
|
556
|
+
if config is None:
|
|
557
|
+
return {
|
|
558
|
+
"backlog_provider": None,
|
|
559
|
+
"backlog": None,
|
|
560
|
+
"backlog_is_issue_provider": False,
|
|
561
|
+
"external_board_url": None,
|
|
562
|
+
"external_board_label": None,
|
|
563
|
+
}
|
|
564
|
+
provider = config.effective_backlog_provider
|
|
565
|
+
is_issue = provider in ISSUE_PROVIDERS
|
|
566
|
+
label = _ISSUE_PROVIDER_LABELS.get(provider) if is_issue else None
|
|
567
|
+
backlog_value = str(config.backlog)
|
|
568
|
+
return {
|
|
569
|
+
"backlog_provider": provider,
|
|
570
|
+
"backlog": backlog_value,
|
|
571
|
+
"backlog_is_issue_provider": is_issue,
|
|
572
|
+
"external_board_url": _external_board_url(config),
|
|
573
|
+
"external_board_label": label,
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
484
577
|
def _status_payload(info: InstanceInfo) -> dict[str, Any]:
|
|
485
578
|
"""The per-instance status payload."""
|
|
486
579
|
config = info.config
|
|
@@ -493,6 +586,7 @@ def _status_payload(info: InstanceInfo) -> dict[str, Any]:
|
|
|
493
586
|
"pid": None,
|
|
494
587
|
"last_outcome": None,
|
|
495
588
|
"next_run_at": None,
|
|
589
|
+
**_backlog_meta(None),
|
|
496
590
|
}
|
|
497
591
|
state = _daemon_state(config)
|
|
498
592
|
pid: int | None = read_lock_pid(lock_path(config))
|
|
@@ -508,6 +602,7 @@ def _status_payload(info: InstanceInfo) -> dict[str, Any]:
|
|
|
508
602
|
"pid": pid,
|
|
509
603
|
"last_outcome": _last_outcome(config),
|
|
510
604
|
"next_run_at": _next_run(info, config),
|
|
605
|
+
**_backlog_meta(config),
|
|
511
606
|
}
|
|
512
607
|
|
|
513
608
|
|
|
@@ -524,6 +619,7 @@ def _summary(info: InstanceInfo) -> dict[str, Any]:
|
|
|
524
619
|
"next_run_at": None,
|
|
525
620
|
"backlog_counts": {status.value: 0 for status in TaskStatus},
|
|
526
621
|
"backlog_error": None,
|
|
622
|
+
**_backlog_meta(None),
|
|
527
623
|
}
|
|
528
624
|
tasks, backlog_error = _read_tasks_or_error(config)
|
|
529
625
|
return {
|
|
@@ -535,6 +631,7 @@ def _summary(info: InstanceInfo) -> dict[str, Any]:
|
|
|
535
631
|
"next_run_at": _next_run(info, config),
|
|
536
632
|
"backlog_counts": backlog_status_counts(tasks),
|
|
537
633
|
"backlog_error": backlog_error,
|
|
634
|
+
**_backlog_meta(config),
|
|
538
635
|
}
|
|
539
636
|
|
|
540
637
|
|
|
@@ -1294,7 +1391,7 @@ class CentralWebServer:
|
|
|
1294
1391
|
return False
|
|
1295
1392
|
self._httpd = httpd
|
|
1296
1393
|
thread = threading.Thread(
|
|
1297
|
-
target=httpd.serve_forever,
|
|
1394
|
+
target=lambda: httpd.serve_forever(poll_interval=0.05),
|
|
1298
1395
|
name="forgeo-central-web",
|
|
1299
1396
|
daemon=True,
|
|
1300
1397
|
)
|