ctrlrelay 0.7.0__tar.gz → 0.8.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.
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/CHANGELOG.md +17 -1
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/PKG-INFO +1 -1
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/config/orchestrator.yaml.example +6 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/configuration.md +26 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/pyproject.toml +1 -1
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/cli.py +2 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/config.py +7 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/pipelines/dev.py +4 -1
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_config.py +64 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_dev_pipeline.py +59 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/workflows/pages.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/.gitignore +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/LICENSE +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/README.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/SECURITY.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/_config.yml +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/architecture.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/bridge.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/cli.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/development.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/getting-started.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/index.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/operations.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/docs/personalization.md +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/dispatcher.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/github.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/poller.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/state.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/gh_protocol.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/install.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/personalization/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/personalization/manager.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/personalization/paths.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/pipelines/secops.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/pipelines/task.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/setup.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/global-CLAUDE.md.snippet +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/launchd/bridge.plist.template +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/launchd/poller.plist.template +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/systemd/bridge.service.template +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/templates/systemd/poller.service.template +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_dev_integration.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_gh_protocol.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_install.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_obs.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_personalization.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_poller.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_setup.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.7.0 → ctrlrelay-0.8.0}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`ci_wait_timeout_seconds`: per-repo cap on the dev pipeline's
|
|
15
|
+
`ctrlrelay ci wait` call.** The dev pipeline prompt tells Claude to run
|
|
16
|
+
`ctrlrelay ci wait --pr <PR> --repo <repo> --timeout 600` before
|
|
17
|
+
signaling DONE — a single blocking call, hardcoded to 10 minutes, that
|
|
18
|
+
generates zero tokens while it polls GitHub. Measured directly on a real
|
|
19
|
+
session: two `ci wait` calls accounted for 12 of a 26-minute run, versus
|
|
20
|
+
under 35K output tokens generated the whole time — the wall-clock cost
|
|
21
|
+
is CI runtime, not model work. `ci_wait_timeout_seconds` (default `600`,
|
|
22
|
+
unchanged behavior) lets a repo with a faster CI suite cap that single
|
|
23
|
+
call lower instead of always budgeting the full 10 minutes.
|
|
24
|
+
|
|
10
25
|
## [0.7.0] - 2026-09-04
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -1232,7 +1247,8 @@ pipeline).
|
|
|
1232
1247
|
per-phase implementation plans (Phase 0 through Phase 4).
|
|
1233
1248
|
- `docs/Claude_Code_Project_Guide.md` — project development guide.
|
|
1234
1249
|
|
|
1235
|
-
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.
|
|
1250
|
+
[Unreleased]: https://github.com/AInvirion/ctrlrelay/compare/v0.8.0...HEAD
|
|
1251
|
+
[0.8.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.7.0...v0.8.0
|
|
1236
1252
|
[0.7.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.6.0...v0.7.0
|
|
1237
1253
|
[0.6.0]: https://github.com/AInvirion/ctrlrelay/compare/v0.5.0...v0.6.0
|
|
1238
1254
|
[0.1.3]: https://github.com/AInvirion/ctrlrelay/releases/tag/v0.1.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
|
|
5
5
|
Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
|
|
6
6
|
Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
|
|
@@ -146,3 +146,9 @@ repos: []
|
|
|
146
146
|
# # Matching is case-insensitive. Defaults to [] (assignment alone
|
|
147
147
|
# # is sufficient, same as today).
|
|
148
148
|
# require_labels: []
|
|
149
|
+
# # Hard cap, in seconds, on the single `ctrlrelay ci wait` call the
|
|
150
|
+
# # dev pipeline prompt runs before signaling DONE. Lower this to
|
|
151
|
+
# # match how long this repo's CI actually takes — no tokens are
|
|
152
|
+
# # generated while blocked in this call, so a slow default here
|
|
153
|
+
# # just adds idle wall-clock time. Defaults to 600 (10 minutes).
|
|
154
|
+
# ci_wait_timeout_seconds: 600
|
|
@@ -201,6 +201,7 @@ and ask the operator), or `never` (skip).
|
|
|
201
201
|
| `exclude_labels` | `["manual", "operator", "instruction"]` | Issue labels that tell the poller "this isn't for the agent". See [exclude_labels](#reposautomationexclude_labels) below. |
|
|
202
202
|
| `include_labels` | `[]` | Issue labels that opt an issue **into** the dev pipeline regardless of who is (or isn't) assigned. See [include_labels](#reposautomationinclude_labels) below. |
|
|
203
203
|
| `require_labels` | `[]` | Issue labels that gate the plain-assignment trigger: when set, assignment alone is no longer enough — the issue must also carry one of these labels. See [require_labels](#reposautomationrequire_labels) below. |
|
|
204
|
+
| `ci_wait_timeout_seconds` | `600` | Hard cap, in seconds, on the single `ctrlrelay ci wait` call the dev pipeline prompt tells Claude to run before signaling DONE. See [ci_wait_timeout_seconds](#reposautomationci_wait_timeout_seconds) below. |
|
|
204
205
|
|
|
205
206
|
The current secops and dev pipelines read these settings to bias their prompts
|
|
206
207
|
to Claude — they're not enforced by hard-coded checks.
|
|
@@ -345,6 +346,31 @@ repos:
|
|
|
345
346
|
picked up yet.
|
|
346
347
|
- `exclude_labels` is still checked first, same precedence as always.
|
|
347
348
|
|
|
349
|
+
### repos[].automation.ci_wait_timeout_seconds
|
|
350
|
+
|
|
351
|
+
The dev pipeline prompt tells Claude to run `ctrlrelay ci wait --pr <PR>
|
|
352
|
+
--repo <repo> --timeout <N>` before signaling DONE — a single blocking call
|
|
353
|
+
that polls GitHub every 15s until CI finishes, fails, or the timeout hits.
|
|
354
|
+
No tokens get generated while it's blocked; on a repo with a slow CI suite,
|
|
355
|
+
that one call can eat most of a session's wall-clock time.
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
repos:
|
|
359
|
+
- name: "your-org/your-repo"
|
|
360
|
+
local_path: "~/Projects/your-repo"
|
|
361
|
+
automation:
|
|
362
|
+
ci_wait_timeout_seconds: 300
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
- Default: `600` (10 minutes).
|
|
366
|
+
- Lower it to match how long the repo's CI actually takes — a repo with a
|
|
367
|
+
2-minute CI suite gains nothing from a 10-minute cap.
|
|
368
|
+
- A timeout here isn't a failure: `ctrlrelay ci wait` exits 2 (not 1) when
|
|
369
|
+
the deadline hits with checks still pending, and the prompt tells Claude
|
|
370
|
+
to treat that as acceptable and hand off rather than loop.
|
|
371
|
+
- This does not change the overall session timeout (`agent.default_timeout_seconds`,
|
|
372
|
+
default 1800s) — it only bounds the one `ci wait` call.
|
|
373
|
+
|
|
348
374
|
## schedules
|
|
349
375
|
|
|
350
376
|
In-process cron jobs run by the poller daemon. All expressions are
|
|
@@ -748,6 +748,7 @@ def run_dev(
|
|
|
748
748
|
state_db=db,
|
|
749
749
|
transport=None,
|
|
750
750
|
contexts_dir=config.paths.contexts,
|
|
751
|
+
ci_wait_timeout_seconds=repo_config.automation.ci_wait_timeout_seconds,
|
|
751
752
|
)
|
|
752
753
|
|
|
753
754
|
try:
|
|
@@ -1273,6 +1274,7 @@ def poller_start(
|
|
|
1273
1274
|
state_db=state_db,
|
|
1274
1275
|
transport=connected_transport,
|
|
1275
1276
|
contexts_dir=config.paths.contexts,
|
|
1277
|
+
ci_wait_timeout_seconds=repo_config.automation.ci_wait_timeout_seconds,
|
|
1276
1278
|
)
|
|
1277
1279
|
|
|
1278
1280
|
# Lock-conflict retry hook. The poller marks issues seen
|
|
@@ -201,6 +201,13 @@ class AutomationConfig(BaseModel):
|
|
|
201
201
|
# precedence so "not for the agent at all" overrides "agent does
|
|
202
202
|
# this but differently".
|
|
203
203
|
task_labels: list[str] = Field(default_factory=lambda: ["task"])
|
|
204
|
+
# Hard cap, in seconds, on a single `ctrlrelay ci wait` call the dev
|
|
205
|
+
# pipeline prompt tells Claude to run before signaling DONE. A repo
|
|
206
|
+
# with slow CI can eat most of a session's wall-clock time sitting
|
|
207
|
+
# in this one blocking call generating zero tokens; a fast-CI repo
|
|
208
|
+
# doesn't need anywhere near the 600s default. Lower this per-repo
|
|
209
|
+
# to match how long that repo's CI actually takes.
|
|
210
|
+
ci_wait_timeout_seconds: int = 600
|
|
204
211
|
|
|
205
212
|
|
|
206
213
|
_REPO_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$")
|
|
@@ -330,6 +330,7 @@ class DevPipeline:
|
|
|
330
330
|
issue_title = extra.get("issue_title", "")
|
|
331
331
|
issue_body = extra.get("issue_body", "")
|
|
332
332
|
branch_name = extra.get("branch_name", "")
|
|
333
|
+
ci_wait_timeout_seconds = extra.get("ci_wait_timeout_seconds", 600)
|
|
333
334
|
state_file_path = str(state_file) if state_file else "/tmp/state.json"
|
|
334
335
|
|
|
335
336
|
return f"""You are working on issue #{issue_number} in repository {repo}.
|
|
@@ -349,7 +350,7 @@ Execute the following workflow:
|
|
|
349
350
|
4. Push the branch and open a PR that references the issue
|
|
350
351
|
5. Before signaling DONE, verify the PR is mergeable:
|
|
351
352
|
- Wait for CI by running EXACTLY this command (do NOT improvise bash):
|
|
352
|
-
`ctrlrelay ci wait --pr <PR> --repo {repo} --timeout
|
|
353
|
+
`ctrlrelay ci wait --pr <PR> --repo {repo} --timeout {ci_wait_timeout_seconds}`
|
|
353
354
|
Exit codes: 0 = all checks passed, 1 = a check failed (investigate,
|
|
354
355
|
fix, push, then re-run the wait), 2 = hard timeout while CI is still
|
|
355
356
|
pending (treat as acceptable — hand off and let the orchestrator
|
|
@@ -608,6 +609,7 @@ async def run_dev_issue(
|
|
|
608
609
|
max_fix_attempts: int = DEFAULT_MAX_FIX_ATTEMPTS,
|
|
609
610
|
max_blocked_rounds: int = DEFAULT_MAX_BLOCKED_ROUNDS,
|
|
610
611
|
pr_verifier: PRVerifier | None = None,
|
|
612
|
+
ci_wait_timeout_seconds: int = 600,
|
|
611
613
|
) -> PipelineResult:
|
|
612
614
|
"""Run dev pipeline for a single issue."""
|
|
613
615
|
session_id = f"dev-{repo.replace('/', '-')}-{issue_number}-{uuid.uuid4().hex[:8]}"
|
|
@@ -715,6 +717,7 @@ async def run_dev_issue(
|
|
|
715
717
|
"issue_title": issue.get("title", ""),
|
|
716
718
|
"issue_body": issue.get("body", ""),
|
|
717
719
|
"branch_name": branch_name,
|
|
720
|
+
"ci_wait_timeout_seconds": ci_wait_timeout_seconds,
|
|
718
721
|
},
|
|
719
722
|
)
|
|
720
723
|
|
|
@@ -640,3 +640,67 @@ class TestAutomationRequireLabels:
|
|
|
640
640
|
|
|
641
641
|
assert config.repos[0].automation.require_labels == ["ctrlrelay:auto"]
|
|
642
642
|
assert config.repos[1].automation.require_labels == []
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
class TestAutomationCiWaitTimeout:
|
|
646
|
+
"""ci_wait_timeout_seconds caps the single `ctrlrelay ci wait` call the
|
|
647
|
+
dev pipeline prompt tells Claude to run before signaling DONE. Default
|
|
648
|
+
600 preserves today's behavior."""
|
|
649
|
+
|
|
650
|
+
def test_default_ci_wait_timeout_is_600(self) -> None:
|
|
651
|
+
auto = AutomationConfig()
|
|
652
|
+
assert auto.ci_wait_timeout_seconds == 600
|
|
653
|
+
|
|
654
|
+
def test_ci_wait_timeout_override_accepts_int(self) -> None:
|
|
655
|
+
auto = AutomationConfig(ci_wait_timeout_seconds=300)
|
|
656
|
+
assert auto.ci_wait_timeout_seconds == 300
|
|
657
|
+
|
|
658
|
+
def test_config_without_ci_wait_timeout_key_defaults_to_600(
|
|
659
|
+
self, sample_config_dict: dict, tmp_path: Path
|
|
660
|
+
) -> None:
|
|
661
|
+
sample_config_dict["repos"] = [
|
|
662
|
+
{
|
|
663
|
+
"name": "owner/repo",
|
|
664
|
+
"local_path": "~/Projects/repo",
|
|
665
|
+
"automation": {"dependabot_patch": "auto"},
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
cfg_path = tmp_path / "cfg.yaml"
|
|
669
|
+
cfg_path.write_text(yaml.dump(sample_config_dict))
|
|
670
|
+
|
|
671
|
+
config = load_config(cfg_path)
|
|
672
|
+
|
|
673
|
+
assert config.repos[0].automation.ci_wait_timeout_seconds == 600
|
|
674
|
+
|
|
675
|
+
def test_ci_wait_timeout_from_yaml(
|
|
676
|
+
self, sample_config_dict: dict, tmp_path: Path
|
|
677
|
+
) -> None:
|
|
678
|
+
sample_config_dict["repos"] = [
|
|
679
|
+
{
|
|
680
|
+
"name": "owner/repo",
|
|
681
|
+
"local_path": "~/Projects/repo",
|
|
682
|
+
"automation": {"ci_wait_timeout_seconds": 300},
|
|
683
|
+
}
|
|
684
|
+
]
|
|
685
|
+
cfg_path = tmp_path / "cfg.yaml"
|
|
686
|
+
cfg_path.write_text(yaml.dump(sample_config_dict))
|
|
687
|
+
|
|
688
|
+
config = load_config(cfg_path)
|
|
689
|
+
|
|
690
|
+
assert config.repos[0].automation.ci_wait_timeout_seconds == 300
|
|
691
|
+
|
|
692
|
+
def test_ci_wait_timeout_rejects_non_int(
|
|
693
|
+
self, sample_config_dict: dict, tmp_path: Path
|
|
694
|
+
) -> None:
|
|
695
|
+
sample_config_dict["repos"] = [
|
|
696
|
+
{
|
|
697
|
+
"name": "owner/repo",
|
|
698
|
+
"local_path": "~/Projects/repo",
|
|
699
|
+
"automation": {"ci_wait_timeout_seconds": "fast"},
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
cfg_path = tmp_path / "cfg.yaml"
|
|
703
|
+
cfg_path.write_text(yaml.dump(sample_config_dict))
|
|
704
|
+
|
|
705
|
+
with pytest.raises(ConfigError, match="ci_wait_timeout_seconds"):
|
|
706
|
+
load_config(cfg_path)
|
|
@@ -61,6 +61,65 @@ class TestDevPipeline:
|
|
|
61
61
|
"prompt should explicitly ban `until`/`while` bash CI-wait loops"
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
+
def test_prompt_uses_default_ci_wait_timeout_when_not_configured(self) -> None:
|
|
65
|
+
"""No ci_wait_timeout_seconds in extra: falls back to 600, matching
|
|
66
|
+
AutomationConfig's default so an unconfigured repo behaves as before."""
|
|
67
|
+
from ctrlrelay.pipelines.dev import DevPipeline
|
|
68
|
+
|
|
69
|
+
pipeline = DevPipeline(
|
|
70
|
+
dispatcher=MagicMock(),
|
|
71
|
+
github=MagicMock(),
|
|
72
|
+
worktree=MagicMock(),
|
|
73
|
+
dashboard=None,
|
|
74
|
+
state_db=MagicMock(),
|
|
75
|
+
transport=None,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
prompt = pipeline._build_prompt(
|
|
79
|
+
repo="owner/repo",
|
|
80
|
+
issue_number=42,
|
|
81
|
+
extra={
|
|
82
|
+
"issue_title": "t",
|
|
83
|
+
"issue_body": "b",
|
|
84
|
+
"branch_name": "fix/issue-42",
|
|
85
|
+
},
|
|
86
|
+
session_id="dev-42",
|
|
87
|
+
state_file=Path("/tmp/state.json"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
assert "ctrlrelay ci wait --pr <PR> --repo owner/repo --timeout 600" in prompt
|
|
91
|
+
|
|
92
|
+
def test_prompt_uses_configured_ci_wait_timeout(self) -> None:
|
|
93
|
+
"""A repo with a slow (or fast) CI can override the 600s default so
|
|
94
|
+
the dev pipeline doesn't spend most of a session blocked in one
|
|
95
|
+
`ctrlrelay ci wait` call."""
|
|
96
|
+
from ctrlrelay.pipelines.dev import DevPipeline
|
|
97
|
+
|
|
98
|
+
pipeline = DevPipeline(
|
|
99
|
+
dispatcher=MagicMock(),
|
|
100
|
+
github=MagicMock(),
|
|
101
|
+
worktree=MagicMock(),
|
|
102
|
+
dashboard=None,
|
|
103
|
+
state_db=MagicMock(),
|
|
104
|
+
transport=None,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
prompt = pipeline._build_prompt(
|
|
108
|
+
repo="owner/repo",
|
|
109
|
+
issue_number=42,
|
|
110
|
+
extra={
|
|
111
|
+
"issue_title": "t",
|
|
112
|
+
"issue_body": "b",
|
|
113
|
+
"branch_name": "fix/issue-42",
|
|
114
|
+
"ci_wait_timeout_seconds": 300,
|
|
115
|
+
},
|
|
116
|
+
session_id="dev-42",
|
|
117
|
+
state_file=Path("/tmp/state.json"),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assert "ctrlrelay ci wait --pr <PR> --repo owner/repo --timeout 300" in prompt
|
|
121
|
+
assert "--timeout 600" not in prompt
|
|
122
|
+
|
|
64
123
|
@pytest.mark.asyncio
|
|
65
124
|
async def test_run_dispatches_claude_session(self, tmp_path: Path) -> None:
|
|
66
125
|
"""Should dispatch Claude session with issue context."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|