ctrlrelay 0.1.12__tar.gz → 0.2.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 (97) hide show
  1. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/CHANGELOG.md +126 -0
  2. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/PKG-INFO +1 -1
  3. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/operations.md +13 -8
  4. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/pyproject.toml +1 -1
  5. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/cli.py +572 -80
  6. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/config.py +74 -0
  7. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/github.py +35 -6
  8. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/state.py +150 -0
  9. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/worktree.py +176 -12
  10. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/pipelines/dev.py +457 -51
  11. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/pipelines/post_merge.py +150 -18
  12. ctrlrelay-0.2.1/tests/test_cli_repos.py +268 -0
  13. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_config.py +73 -1
  14. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_dev_integration.py +148 -7
  15. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_dev_pipeline.py +574 -11
  16. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_github.py +39 -0
  17. ctrlrelay-0.2.1/tests/test_post_merge.py +1312 -0
  18. ctrlrelay-0.2.1/tests/test_state.py +594 -0
  19. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_worktree.py +485 -12
  20. ctrlrelay-0.1.12/tests/test_post_merge.py +0 -666
  21. ctrlrelay-0.1.12/tests/test_state.py +0 -271
  22. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/dependabot.yml +0 -0
  26. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/workflows/build.yml +0 -0
  27. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/workflows/cla.yml +0 -0
  28. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/workflows/pages.yml +0 -0
  29. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/workflows/publish.yml +0 -0
  30. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.github/workflows/test.yml +0 -0
  31. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/.gitignore +0 -0
  32. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/CODE_OF_CONDUCT.md +0 -0
  33. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/CONTRIBUTING.md +0 -0
  34. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/LICENSE +0 -0
  35. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/README.md +0 -0
  36. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/SECURITY.md +0 -0
  37. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/config/orchestrator.yaml.example +0 -0
  38. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/Gemfile +0 -0
  39. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/_config.yml +0 -0
  40. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/architecture.md +0 -0
  41. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/bridge.md +0 -0
  42. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/cli.md +0 -0
  43. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/configuration.md +0 -0
  44. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/development.md +0 -0
  45. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/feedback-loop.md +0 -0
  46. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/getting-started.md +0 -0
  47. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/docs/index.md +0 -0
  48. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/__init__.py +0 -0
  49. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/bridge/__init__.py +0 -0
  50. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/bridge/__main__.py +0 -0
  51. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/bridge/protocol.py +0 -0
  52. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/bridge/server.py +0 -0
  53. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  54. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/__init__.py +0 -0
  55. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/audit.py +0 -0
  56. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/checkpoint.py +0 -0
  57. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/dispatcher.py +0 -0
  58. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/obs.py +0 -0
  59. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/poller.py +0 -0
  60. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/pr_verifier.py +0 -0
  61. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/pr_watcher.py +0 -0
  62. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/core/scheduler.py +0 -0
  63. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/dashboard/__init__.py +0 -0
  64. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/dashboard/client.py +0 -0
  65. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/pipelines/__init__.py +0 -0
  66. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/pipelines/base.py +0 -0
  67. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/pipelines/secops.py +0 -0
  68. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/pipelines/task.py +0 -0
  69. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/transports/__init__.py +0 -0
  70. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/transports/base.py +0 -0
  71. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/transports/file_mock.py +0 -0
  72. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/src/ctrlrelay/transports/socket_client.py +0 -0
  73. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/__init__.py +0 -0
  74. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/conftest.py +0 -0
  75. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_audit.py +0 -0
  76. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_bridge_protocol.py +0 -0
  77. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_bridge_server.py +0 -0
  78. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_checkpoint.py +0 -0
  79. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_cli_ci_wait.py +0 -0
  80. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_cli_dev.py +0 -0
  81. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_cli_secops.py +0 -0
  82. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_cli_start.py +0 -0
  83. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_cli_version.py +0 -0
  84. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_dashboard_client.py +0 -0
  85. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_dispatcher.py +0 -0
  86. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_docs_site.py +0 -0
  87. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_obs.py +0 -0
  88. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_pipeline_base.py +0 -0
  89. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_poller.py +0 -0
  90. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_pr_verifier.py +0 -0
  91. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_pr_watcher.py +0 -0
  92. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_scheduler.py +0 -0
  93. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_secops_integration.py +0 -0
  94. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_secops_pipeline.py +0 -0
  95. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_task_pipeline.py +0 -0
  96. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_telegram_handler.py +0 -0
  97. {ctrlrelay-0.1.12 → ctrlrelay-0.2.1}/tests/test_transport.py +0 -0
@@ -7,6 +7,132 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.1] - 2026-04-28
11
+
12
+ Patch release. Fixes a long-standing UX bug where `ctrlrelay` could
13
+ only be invoked from a directory containing `config/orchestrator.yaml`.
14
+
15
+ ### Fixed
16
+
17
+ - **`--config` now auto-discovers `orchestrator.yaml`.** Every CLI
18
+ command previously hardcoded a relative `config/orchestrator.yaml`
19
+ default, so running `ctrlrelay status` (or any other subcommand)
20
+ from `/tmp`, `$HOME`, or anywhere outside the project root failed
21
+ with `Config file not found: config/orchestrator.yaml`. The CLI
22
+ now resolves the config in this order:
23
+
24
+ 1. The path passed to `--config` / `-c`, if any.
25
+ 2. `$CTRLRELAY_CONFIG` (a new environment variable).
26
+ 3. `./config/orchestrator.yaml`, walking up from the current
27
+ working directory to the filesystem root — matches how `git`
28
+ and `uv` find their config.
29
+ 4. `$XDG_CONFIG_HOME/ctrlrelay/orchestrator.yaml` (defaults to
30
+ `~/.config/ctrlrelay/orchestrator.yaml`).
31
+
32
+ When nothing matches, the error now lists every location searched
33
+ so it's clear where to drop the file or which env var to set.
34
+ Daemon spawn paths (`ctrlrelay poller start` re-exec under
35
+ `--foreground`) pass the *resolved* absolute path to the child so
36
+ the daemon doesn't break when launchd starts it from `/`.
37
+
38
+ ## [0.2.0] - 2026-04-27
39
+
40
+ Minor release. Adds bulk repo operations driven by
41
+ `config/orchestrator.yaml`, plus a batch of dev-pipeline correctness
42
+ fixes (worktree ownership, PR-CI lock contention, watcher
43
+ persistence) and a docs cleanup.
44
+
45
+ ### Added
46
+
47
+ - **`ctrlrelay repos clone-all/pull-all/status`** (closes #117).
48
+ Stand up an isolated workspace from the orchestrator manifest in
49
+ one command:
50
+
51
+ ```
52
+ ctrlrelay repos clone-all ~/code/myproject [--filter ORG] [--dry-run]
53
+ ctrlrelay repos pull-all ~/code/myproject [--filter ORG] [--dry-run]
54
+ ctrlrelay repos status ~/code/myproject [--filter ORG]
55
+ ```
56
+
57
+ Each repo lands at `DEST/<org>/<repo>` derived from the `name:`
58
+ field; remote is `git@github.com:{name}.git`. The configured
59
+ `local_path` is ignored when `DEST` is passed, so existing
60
+ `~/Projects/...` checkouts stay untouched. Replaces the legacy
61
+ `bkp/sync` shell scripts that broke when the manifest was
62
+ archived during the rewrite.
63
+ - **`RepoConfig.name` validator.** Repo names are now validated
64
+ against `^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$` at config load.
65
+ Rejects `..`, extra slashes, and shell metacharacters before they
66
+ reach a clone target — defense in depth for the new bulk
67
+ commands.
68
+
69
+ ### Fixed
70
+
71
+ - **Watchers persist across poller restarts** (closes #57). Adds
72
+ a `pr_watches` state-db table so in-flight merge watchers
73
+ survive launchd kickstart, crashes, and reboots. Before this,
74
+ any PR sitting in review across a poller restart silently lost
75
+ its post-merge automation (issue auto-close + Telegram
76
+ notification) for the rest of its 7-day window. The poller now
77
+ rehydrates surviving rows on startup and spawns one watcher
78
+ task per row.
79
+ - **Repo lock released during PR CI verification** (closes #29).
80
+ `run_dev_issue` used to hold the per-repo lock through the
81
+ `PRVerifier.wait_for_checks` polling window — a pure `gh` poll
82
+ that can run for up to 30 minutes — which made every peer
83
+ session targeting the same repo report "Repository locked by
84
+ another session" while no git work was in flight. The lock is
85
+ now released before `verify`, reacquired only if a `request_fix`
86
+ follow-up needs to spawn an agent against the worktree, and
87
+ released again on cleanup. `CancelledError` during the unlocked
88
+ window propagates without leaking a lock row.
89
+ - **Branch ownership signal survives delete+recreate** (closes #51).
90
+ `create_worktree_with_new_branch` now returns
91
+ `(path, created_fresh)` so the caller knows whether THIS session
92
+ created the branch (fresh from default, or via the stale-merged
93
+ delete+recreate path). Before #51, `run_dev_issue` snapshotted
94
+ `branch_preexisted` BEFORE the call; the snapshot went stale the
95
+ moment the helper detected a fully-merged local branch and
96
+ deleted+recreated it. A FAILED cleanup would then skip
97
+ `delete_branch` and leak partial commits into the next retry.
98
+ - **Refuse reuse when branch still backs an open PR** (closes #52).
99
+ `create_worktree_with_new_branch` now probes GitHub (via
100
+ `GitHubCLI.list_prs(head=...)`) before reusing an existing local
101
+ branch. If an open PR still backs it (prior DONE session whose
102
+ PR is unmerged, or any external source), raises `WorktreeError`
103
+ with the PR number and a concrete operator action instead of
104
+ silently hijacking the reviewer's already-reviewed branch or
105
+ tripping "A pull request already exists" at `gh pr create`.
106
+ - **`pull-all` checks subprocess return codes.** `git status`
107
+ failure no longer treats an empty stdout as "clean" and proceeds
108
+ to pull a corrupt repo. `git fetch` failure on a dirty tree is
109
+ now reported as `failed` instead of silently being counted as
110
+ `dirty — fetched only`.
111
+ - **`status` no longer crashes on edge cases.** `git rev-list`
112
+ parsing wrapped in a helper that returns 0 on any non-zero
113
+ return code or non-numeric output, instead of raising on
114
+ `int(ahead)`.
115
+
116
+ ### Changed
117
+
118
+ - **Docs use `com.example.*` placeholder for launchd labels**
119
+ (closes #23). The launchd plist examples and `launchctl`
120
+ commands no longer hard-code `com.ainvirion.ctrlrelay-*` as the
121
+ job label. Anyone copying the docs verbatim picked up that label
122
+ too, which is fine until two forks of the project share a
123
+ machine. Swapped to `com.example.ctrlrelay-*` with a one-line
124
+ note directing readers to use a reverse-DNS prefix they own.
125
+
126
+ ### Operator notes
127
+
128
+ - Upgrade via `uv tool install ctrlrelay@latest --force` and
129
+ restart poller + bridge.
130
+ No schema or config changes — the new `pr_watches` state-db
131
+ table is created idempotently on first start.
132
+ - New workflow: `ctrlrelay repos clone-all ~/code/myproject` to
133
+ stand up a fresh workspace, `repos pull-all` to refresh it.
134
+ Existing `~/Projects/...` checkouts are not touched.
135
+
10
136
  ## [0.1.12] - 2026-04-22
11
137
 
12
138
  Patch release. Closes #90 — three related polish items on the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctrlrelay
3
- Version: 0.1.12
3
+ Version: 0.2.1
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/
@@ -33,7 +33,12 @@ and restart on failure — the unit examples below already do this.
33
33
  Save plist files under `~/Library/LaunchAgents/`. Use the **absolute** path to
34
34
  `ctrlrelay` (run `which ctrlrelay` to find it) — launchd's PATH is minimal.
35
35
 
36
- `~/Library/LaunchAgents/com.ainvirion.ctrlrelay-bridge.plist`:
36
+ The job labels below use `com.example.ctrlrelay-*` as a placeholder. Pick a
37
+ reverse-DNS prefix you own (e.g. `com.yourname.ctrlrelay-*`) and use it
38
+ consistently across the filename, the `<Label>` value, and the `launchctl`
39
+ commands so `launchctl list` output is unambiguous.
40
+
41
+ `~/Library/LaunchAgents/com.example.ctrlrelay-bridge.plist`:
37
42
 
38
43
  {% raw %}
39
44
  ```xml
@@ -42,7 +47,7 @@ Save plist files under `~/Library/LaunchAgents/`. Use the **absolute** path to
42
47
  <plist version="1.0">
43
48
  <dict>
44
49
  <key>Label</key>
45
- <string>com.ainvirion.ctrlrelay-bridge</string>
50
+ <string>com.example.ctrlrelay-bridge</string>
46
51
  <key>ProgramArguments</key>
47
52
  <array>
48
53
  <string>/opt/homebrew/bin/ctrlrelay</string>
@@ -72,7 +77,7 @@ Save plist files under `~/Library/LaunchAgents/`. Use the **absolute** path to
72
77
  ```
73
78
  {% endraw %}
74
79
 
75
- `~/Library/LaunchAgents/com.ainvirion.ctrlrelay-poller.plist`:
80
+ `~/Library/LaunchAgents/com.example.ctrlrelay-poller.plist`:
76
81
 
77
82
  {% raw %}
78
83
  ```xml
@@ -81,7 +86,7 @@ Save plist files under `~/Library/LaunchAgents/`. Use the **absolute** path to
81
86
  <plist version="1.0">
82
87
  <dict>
83
88
  <key>Label</key>
84
- <string>com.ainvirion.ctrlrelay-poller</string>
89
+ <string>com.example.ctrlrelay-poller</string>
85
90
  <key>ProgramArguments</key>
86
91
  <array>
87
92
  <string>/opt/homebrew/bin/ctrlrelay</string>
@@ -122,16 +127,16 @@ Create the log directory and load the agents:
122
127
 
123
128
  ```bash
124
129
  mkdir -p ~/.ctrlrelay/logs
125
- launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ainvirion.ctrlrelay-bridge.plist
126
- launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ainvirion.ctrlrelay-poller.plist
130
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.ctrlrelay-bridge.plist
131
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.ctrlrelay-poller.plist
127
132
  ```
128
133
 
129
134
  Manage them:
130
135
 
131
136
  ```bash
132
137
  launchctl list | grep ctrlrelay # check loaded
133
- launchctl bootout gui/$(id -u)/com.ainvirion.ctrlrelay-poller # stop
134
- launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ainvirion.ctrlrelay-poller.plist # start
138
+ launchctl bootout gui/$(id -u)/com.example.ctrlrelay-poller # stop
139
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.ctrlrelay-poller.plist # start
135
140
  ```
136
141
 
137
142
  ### Linux — systemd
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ctrlrelay"
3
- version = "0.1.12"
3
+ version = "0.2.1"
4
4
  description = "Local-first orchestrator for headless coding agents across multiple GitHub repos"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"