ctrlrelay 0.2.1__tar.gz → 0.4.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.2.1 → ctrlrelay-0.4.0}/.github/workflows/pages.yml +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.gitignore +14 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/CHANGELOG.md +151 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/PKG-INFO +7 -4
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/README.md +6 -3
- ctrlrelay-0.4.0/config/orchestrator.yaml.example +141 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/architecture.md +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/cli.md +124 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/configuration.md +153 -2
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/development.md +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/getting-started.md +46 -56
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/index.md +3 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/operations.md +56 -34
- ctrlrelay-0.4.0/docs/personalization.md +343 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/pyproject.toml +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/cli.py +690 -19
- ctrlrelay-0.4.0/src/ctrlrelay/core/config.py +732 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/github.py +59 -5
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/poller.py +364 -12
- ctrlrelay-0.4.0/src/ctrlrelay/install.py +192 -0
- ctrlrelay-0.4.0/src/ctrlrelay/personalization/__init__.py +26 -0
- ctrlrelay-0.4.0/src/ctrlrelay/personalization/manager.py +1176 -0
- ctrlrelay-0.4.0/src/ctrlrelay/personalization/paths.py +143 -0
- ctrlrelay-0.4.0/src/ctrlrelay/setup.py +427 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/__init__.py +0 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/global-CLAUDE.md.snippet +61 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/launchd/bridge.plist.template +37 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/launchd/poller.plist.template +44 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/systemd/bridge.service.template +20 -0
- ctrlrelay-0.4.0/src/ctrlrelay/templates/systemd/poller.service.template +25 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_repos.py +13 -7
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_config.py +226 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dev_integration.py +249 -1
- ctrlrelay-0.4.0/tests/test_install.py +294 -0
- ctrlrelay-0.4.0/tests/test_personalization.py +1981 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_poller.py +709 -1
- ctrlrelay-0.4.0/tests/test_setup.py +531 -0
- ctrlrelay-0.2.1/config/orchestrator.yaml.example +0 -60
- ctrlrelay-0.2.1/src/ctrlrelay/core/config.py +0 -377
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/LICENSE +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/SECURITY.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/_config.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/bridge.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/dispatcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/state.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/dev.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/secops.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/task.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_obs.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_worktree.py +0 -0
|
@@ -37,3 +37,17 @@ repos.manifest
|
|
|
37
37
|
|
|
38
38
|
# Lock file — not committed per AInvirion Python-SDK convention.
|
|
39
39
|
uv.lock
|
|
40
|
+
|
|
41
|
+
# Operator-private notes (runbooks, personal cheatsheets).
|
|
42
|
+
notes/
|
|
43
|
+
|
|
44
|
+
# Rendered service unit files. `ctrlrelay install launchd|systemd`
|
|
45
|
+
# substitutes CTRLRELAY_TELEGRAM_TOKEN into a copy of the in-package
|
|
46
|
+
# template and writes it to the system path (~/Library/LaunchAgents on
|
|
47
|
+
# macOS, ~/.config/systemd/user on Linux). The default install command
|
|
48
|
+
# never lands a rendered file inside the repo, but a future flag — or
|
|
49
|
+
# an operator running it with --target-dir pointed at the working tree —
|
|
50
|
+
# would. Defense-in-depth so a `git add .` after such a slip can't
|
|
51
|
+
# scoop the literal token into a commit.
|
|
52
|
+
*.plist
|
|
53
|
+
*.service
|
|
@@ -7,6 +7,157 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-05-08
|
|
11
|
+
|
|
12
|
+
Minor release. One new feature plus one schema simplification:
|
|
13
|
+
|
|
14
|
+
- **`ctrlrelay setup`** — first-run onboarding command. Detects every
|
|
15
|
+
GitHub org you belong to, enumerates non-fork non-archived repos in
|
|
16
|
+
each, writes a fresh `~/.config/ctrlrelay/orchestrator.yaml`, clones
|
|
17
|
+
every repo to `~/Projects/<owner.lower()>/<repo>`, optionally
|
|
18
|
+
configures the personalization sync block, and optionally renders
|
|
19
|
+
launchd/systemd unit files. Replaces the multi-step manual playbook
|
|
20
|
+
operators previously had to follow on a new machine. Interactive by
|
|
21
|
+
default; `--yes` and `--owner` flags make it scriptable.
|
|
22
|
+
- **`paths.owner_aliases` deprecated; lowercase-org-folder convention.**
|
|
23
|
+
The path resolver now always derives `local_path` as
|
|
24
|
+
`${repo_root}/${owner.lower()}/${repo}` (closes #128). The previous
|
|
25
|
+
`owner_aliases` indirection caused `clone-all`/`pull-all`/`status`
|
|
26
|
+
to disagree with the dev pipeline on where a given repo lived.
|
|
27
|
+
Parsing of `owner_aliases` is retained so 0.3.x configs still load;
|
|
28
|
+
a `DeprecationWarning` fires when the block is non-empty.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`ctrlrelay setup`** (closes the onboarding gap reported during the
|
|
33
|
+
v0.3.0 reinstall flow). Composes existing primitives (gh discovery,
|
|
34
|
+
config generation, `git clone`, `personalization init`, `install
|
|
35
|
+
launchd|systemd`) into a single command. Reads
|
|
36
|
+
`$CTRLRELAY_TELEGRAM_TOKEN` so the rendered plist isn't a
|
|
37
|
+
placeholder. Refuses to overwrite an existing `orchestrator.yaml`
|
|
38
|
+
without `--force`.
|
|
39
|
+
- **`repos clone-all` / `pull-all` / `status` accept DEST as
|
|
40
|
+
optional**. When omitted, each command operates on the
|
|
41
|
+
config-resolved `local_path` of every repo, so the same path
|
|
42
|
+
resolution serves the bulk commands and the dev pipeline. Pass DEST
|
|
43
|
+
to override (lands at `DEST/<owner.lower()>/<repo>`).
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **Path resolver: `owner.lower()` is the folder, always.** Closes #128.
|
|
48
|
+
Affects every command that touches `repo.local_path`. Operators on
|
|
49
|
+
v0.3.0 with mixed-case folders (e.g. `~/Projects/AInvirion/...`)
|
|
50
|
+
must either rename the folder, set per-repo `local_path` overrides,
|
|
51
|
+
or run `ctrlrelay setup --force` to land everything at the new
|
|
52
|
+
lowercase paths.
|
|
53
|
+
|
|
54
|
+
### Migration from 0.3.0
|
|
55
|
+
|
|
56
|
+
- Drop `paths.owner_aliases` from `orchestrator.yaml` (or ignore the
|
|
57
|
+
deprecation warning until you next regenerate the config).
|
|
58
|
+
- Rename existing on-disk folders to lowercase (e.g.
|
|
59
|
+
`mv ~/Projects/AInvirion ~/Projects/ainvirion`) — or, easier, run
|
|
60
|
+
`ctrlrelay setup --force` to clone everything fresh under the new
|
|
61
|
+
convention.
|
|
62
|
+
|
|
63
|
+
## [0.3.0] - 2026-05-08
|
|
64
|
+
|
|
65
|
+
Minor release. Three additive features: cross-machine **personalization
|
|
66
|
+
sync** of operator state through a private GitHub repo, **portability
|
|
67
|
+
fixes** that let one `orchestrator.yaml` work unmodified across machines,
|
|
68
|
+
and **label-driven issue matching** for the dev pipeline. One install
|
|
69
|
+
fix (`PYTHONUNBUFFERED`) and a docs page covering the new
|
|
70
|
+
personalization flow.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **Personalization sync** (#123, #124). New `personalization:` config
|
|
75
|
+
block + `ctrlrelay personalization init/status/push/pull`
|
|
76
|
+
subcommands sync the operator's Claude Code state — global config,
|
|
77
|
+
per-project memory, spec/superpower outputs — across machines
|
|
78
|
+
through a separate (typically private) GitHub repo. Per-machine
|
|
79
|
+
branches (`personalization/<node_id>`) rebase onto `main` and FF
|
|
80
|
+
the integration branch, so two machines pushing concurrently never
|
|
81
|
+
overwrite each other; `--force-with-lease` is reserved for the
|
|
82
|
+
per-machine branch, never used on `main`. Source/target paths
|
|
83
|
+
support `${HOME}`, `${PROJECT}` (slug `<owner>--<repo>`),
|
|
84
|
+
`${PROJECT_ENCODED}` (matches Claude's path encoding),
|
|
85
|
+
`${PROJECT_LOCAL}`, and `${PROJECT_PARENT}` placeholders. An
|
|
86
|
+
allowlist limits commits to declared entries — random files in the
|
|
87
|
+
checkout aren't staged. **Adopt-flow** is on by default: `init`
|
|
88
|
+
moves pre-existing real targets (e.g. `~/.claude/CLAUDE.md` that
|
|
89
|
+
predates the sync setup) into the synced repo and lays a symlink in
|
|
90
|
+
their place; `--no-adopt` opts out. Both-real-content collisions
|
|
91
|
+
surface as `skipped-conflict-both-exist` for manual reconciliation.
|
|
92
|
+
See the new [Personalization sync](docs/personalization.md) page.
|
|
93
|
+
- **Auto-pull on cron** (#124). New optional
|
|
94
|
+
`schedules.personalization_cron` runs `personalization pull` on the
|
|
95
|
+
poller daemon, with two safety rails: skip-on-dirty (never rebases
|
|
96
|
+
under uncommitted operator edits) and `adopt=False` on the re-wire
|
|
97
|
+
(a background sync never silently moves files; adoption stays
|
|
98
|
+
init-only). Auto-push is intentionally not scheduled — daemon-side
|
|
99
|
+
commits surprise people. Dispatched via `asyncio.to_thread` so a
|
|
100
|
+
slow remote can't stall the poller's event loop (Telegram dispatch,
|
|
101
|
+
pending-resume sweeper, secops cron).
|
|
102
|
+
- **`paths.repo_root` + `paths.owner_aliases`** (#121). When set,
|
|
103
|
+
`repos[].local_path` is derived as
|
|
104
|
+
`${repo_root}/${owner_aliases.get(owner, owner)}/${repo}`. Per-repo
|
|
105
|
+
`local_path` still wins as an override. Without `repo_root`, the
|
|
106
|
+
legacy "local_path required per repo" behaviour is preserved.
|
|
107
|
+
Collapses 69 explicit `local_path` values to 20 in the maintainer's
|
|
108
|
+
config.
|
|
109
|
+
- **`node_id` defaults to hostname** (#121). Falls back to
|
|
110
|
+
`socket.gethostname()` when missing, null, or blank. Heartbeats and
|
|
111
|
+
session logs still get a meaningful per-node label without forcing
|
|
112
|
+
every operator to edit the file.
|
|
113
|
+
- **`ctrlrelay install launchd|systemd`** (#121). Renders
|
|
114
|
+
bridge/poller service unit files from in-package templates,
|
|
115
|
+
substituting `USER`, `HOME`, `CTRLRELAY_BIN`, `WORKDIR`,
|
|
116
|
+
`LABEL_PREFIX`, `POLLER_INTERVAL`, and (when set)
|
|
117
|
+
`CTRLRELAY_TELEGRAM_TOKEN`. Writes to the conventional locations
|
|
118
|
+
(`~/Library/LaunchAgents` on macOS, `~/.config/systemd/user` on
|
|
119
|
+
Linux) and refuses to clobber existing files unless `--force`.
|
|
120
|
+
Replaces the docs.operations.md copy-paste flow where every
|
|
121
|
+
operator hand-edited `/Users/$ME/...` strings — a tax on
|
|
122
|
+
portability and a common source of broken plists.
|
|
123
|
+
- **Label-driven issue matching** (#115, closes #80). Two new
|
|
124
|
+
per-repo lists govern which issues the poller hands to the dev
|
|
125
|
+
pipeline:
|
|
126
|
+
- `repos[].automation.exclude_labels` (default `["manual",
|
|
127
|
+
"operator", "instruction"]`) — issues carrying any of these
|
|
128
|
+
labels are skipped, marked seen, logged as
|
|
129
|
+
`poll.issue.excluded_by_label`, and never trigger code changes.
|
|
130
|
+
For operator tasks and pure instruction issues.
|
|
131
|
+
- `repos[].automation.include_labels` (default `[]`) — when
|
|
132
|
+
non-empty, issues carrying any of these labels opt **in** to the
|
|
133
|
+
dev pipeline regardless of who is (or isn't) assigned. For repos
|
|
134
|
+
that drive the pipeline by triage label rather than assignment.
|
|
135
|
+
Matching is case-insensitive. Trust model documented in
|
|
136
|
+
[configuration.md](docs/configuration.md#repos-automation): anyone
|
|
137
|
+
with triage permission on a repo can apply a label, which matches
|
|
138
|
+
ctrlrelay's existing trust model.
|
|
139
|
+
|
|
140
|
+
### Fixed
|
|
141
|
+
|
|
142
|
+
- **`PYTHONUNBUFFERED=1` in launchd plists and systemd units** (#122).
|
|
143
|
+
Without it, daemon stdout/stderr buffered up to 4–8 KB before
|
|
144
|
+
flushing to the log file — so `tail -f` on a poller log looked
|
|
145
|
+
frozen for minutes during quiet periods, and crash diagnostics were
|
|
146
|
+
clipped at the last buffer boundary instead of the actual failure
|
|
147
|
+
point. Templates now set the env var and `ctrlrelay install
|
|
148
|
+
launchd|systemd` re-emits both unit files on next run.
|
|
149
|
+
|
|
150
|
+
### Docs
|
|
151
|
+
|
|
152
|
+
- **[Personalization sync](docs/personalization.md)** (#125). New
|
|
153
|
+
page (nav order 8) covering setup, the dotclaude repo layout, the
|
|
154
|
+
init/status/push/pull lifecycle, `--no-adopt`, auto-pull cron,
|
|
155
|
+
multi-machine bootstrap, and the gotchas (worktrees, edit-through-
|
|
156
|
+
symlink semantics, allowlist enforcement, conflict handling, strict
|
|
157
|
+
origin URL match). Pairs with new `schedules` and `personalization`
|
|
158
|
+
sections in [configuration.md](docs/configuration.md) and a new
|
|
159
|
+
`ctrlrelay personalization` block in [cli.md](docs/cli.md).
|
|
160
|
+
|
|
10
161
|
## [0.2.1] - 2026-04-28
|
|
11
162
|
|
|
12
163
|
Patch release. Fixes a long-standing UX bug where `ctrlrelay` could
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ctrlrelay
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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/
|
|
@@ -84,9 +84,12 @@ are on the roadmap (see [Roadmap](#roadmap)).
|
|
|
84
84
|
|
|
85
85
|
## Features
|
|
86
86
|
|
|
87
|
-
- **Issue poller.** Detects issues
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
- **Issue poller.** Detects issues across every configured repo
|
|
88
|
+
(either assigned to you, or carrying a configurable opt-in label like
|
|
89
|
+
`ctrlrelay:auto`), spawns a dev session in a dedicated git worktree,
|
|
90
|
+
and opens a PR. Label triggers let a teammate without rights on your
|
|
91
|
+
account flag an issue as safe for the bot to pick up —
|
|
92
|
+
see [`include_labels`][docs-config].
|
|
90
93
|
- **Telegram bridge.** When a session hits a blocking question, the
|
|
91
94
|
bridge relays it to you as a DM and resumes the session once you
|
|
92
95
|
reply.
|
|
@@ -45,9 +45,12 @@ are on the roadmap (see [Roadmap](#roadmap)).
|
|
|
45
45
|
|
|
46
46
|
## Features
|
|
47
47
|
|
|
48
|
-
- **Issue poller.** Detects issues
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
- **Issue poller.** Detects issues across every configured repo
|
|
49
|
+
(either assigned to you, or carrying a configurable opt-in label like
|
|
50
|
+
`ctrlrelay:auto`), spawns a dev session in a dedicated git worktree,
|
|
51
|
+
and opens a PR. Label triggers let a teammate without rights on your
|
|
52
|
+
account flag an issue as safe for the bot to pick up —
|
|
53
|
+
see [`include_labels`][docs-config].
|
|
51
54
|
- **Telegram bridge.** When a session hits a blocking question, the
|
|
52
55
|
bridge relays it to you as a DM and resumes the session once you
|
|
53
56
|
reply.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# ctrlrelay orchestrator configuration
|
|
2
|
+
# Copy to orchestrator.yaml and customize
|
|
3
|
+
|
|
4
|
+
version: "1"
|
|
5
|
+
# node_id defaults to socket.gethostname() when omitted. Set explicitly
|
|
6
|
+
# only if the hostname is meaningless (CI runners, containers).
|
|
7
|
+
# node_id: "studio-mac"
|
|
8
|
+
timezone: "America/Santiago"
|
|
9
|
+
|
|
10
|
+
paths:
|
|
11
|
+
state_db: "~/.ctrlrelay/state.db"
|
|
12
|
+
worktrees: "~/.ctrlrelay/worktrees"
|
|
13
|
+
bare_repos: "~/.ctrlrelay/repos"
|
|
14
|
+
contexts: "~/.ctrlrelay/contexts"
|
|
15
|
+
skills: "~/.ctrlrelay/claude-config/skills"
|
|
16
|
+
# Optional. When set, repo entries below can omit local_path and have
|
|
17
|
+
# it derived as ${repo_root}/${owner_aliases.get(owner, owner)}/${repo}.
|
|
18
|
+
# Per-repo local_path always wins as override.
|
|
19
|
+
# repo_root: "~/Projects"
|
|
20
|
+
# owner_aliases:
|
|
21
|
+
# AInvirion: AINVIRION # GitHub owner -> on-disk folder name
|
|
22
|
+
# SemClone: SEMCL.ONE
|
|
23
|
+
|
|
24
|
+
# Headless coding agent. `type` selects the adapter (currently only
|
|
25
|
+
# "claude" is implemented; other backends — codex, opencode, hermes,
|
|
26
|
+
# kiro — are planned). Legacy `claude:` key is still accepted as an
|
|
27
|
+
# alias for backwards compatibility but is deprecated.
|
|
28
|
+
agent:
|
|
29
|
+
type: "claude"
|
|
30
|
+
binary: "claude"
|
|
31
|
+
default_timeout_seconds: 1800
|
|
32
|
+
output_format: "json"
|
|
33
|
+
|
|
34
|
+
transport:
|
|
35
|
+
type: "file_mock" # Use "telegram" for production
|
|
36
|
+
telegram:
|
|
37
|
+
bot_token_env: "CTRLRELAY_TELEGRAM_TOKEN"
|
|
38
|
+
chat_id: 123456789
|
|
39
|
+
socket_path: "~/.ctrlrelay/ctrlrelay.sock"
|
|
40
|
+
file_mock:
|
|
41
|
+
inbox: "~/.ctrlrelay/inbox.txt"
|
|
42
|
+
outbox: "~/.ctrlrelay/outbox.txt"
|
|
43
|
+
|
|
44
|
+
dashboard:
|
|
45
|
+
enabled: false
|
|
46
|
+
url: "https://ctrlrelay-dashboard.example.com"
|
|
47
|
+
auth_token_env: "CTRLRELAY_DASHBOARD_TOKEN"
|
|
48
|
+
|
|
49
|
+
# Scheduled jobs run in-process by the poller daemon. Cron expressions are
|
|
50
|
+
# standard 5-field (minute hour dom month dow) and evaluate in the `timezone`
|
|
51
|
+
# set above. Change `secops_cron` to e.g. "0 6 * * 1" for weekly (Mondays).
|
|
52
|
+
schedules:
|
|
53
|
+
secops_cron: "0 6 * * *"
|
|
54
|
+
# Optional. When personalization is configured AND this is set, the
|
|
55
|
+
# poller daemon runs `ctrlrelay personalization pull` on this cron
|
|
56
|
+
# so machines converge without manual sync. Skip-on-dirty: never
|
|
57
|
+
# rebases under uncommitted operator edits. Defaults to None
|
|
58
|
+
# (manual sync only).
|
|
59
|
+
# personalization_cron: "*/15 * * * *"
|
|
60
|
+
|
|
61
|
+
# Optional: cross-machine personalization sync.
|
|
62
|
+
#
|
|
63
|
+
# A separate (typically PRIVATE) GitHub repo holds the operator's Claude
|
|
64
|
+
# config, per-project memory, spec/superpower outputs, and workspace
|
|
65
|
+
# planning notes — anything that survives across sessions and computers
|
|
66
|
+
# but doesn't belong inside any project's source tree. ctrlrelay clones
|
|
67
|
+
# the repo to `checkout_path` and wires the symlinks in `paths`.
|
|
68
|
+
#
|
|
69
|
+
# Per-machine work happens on a `personalization/<node_id>` branch; push
|
|
70
|
+
# rebases onto `main` (no force-push, ever), so two machines pushing
|
|
71
|
+
# concurrently never overwrite each other's deltas.
|
|
72
|
+
#
|
|
73
|
+
# Slice 1: manual `ctrlrelay personalization init/status/push/pull`.
|
|
74
|
+
# Daemon scheduling, Telegram conflict escalation, and adopt-flow for
|
|
75
|
+
# pre-existing target files are deferred to Slice 2+.
|
|
76
|
+
#
|
|
77
|
+
# Placeholders allowed in `source` and `target`:
|
|
78
|
+
# ${HOME} -- current user's home
|
|
79
|
+
# ${PROJECT} -- <owner>--<repo> flat slug (project_scoped only;
|
|
80
|
+
# double hyphen avoids collisions like a-b/c vs a/b-c)
|
|
81
|
+
# ${PROJECT_ENCODED} -- Claude's path encoding (project_scoped only)
|
|
82
|
+
# ${PROJECT_LOCAL} -- absolute path of the repo's local checkout
|
|
83
|
+
# ${PROJECT_PARENT} -- parent dir of ${PROJECT_LOCAL}
|
|
84
|
+
#
|
|
85
|
+
# personalization:
|
|
86
|
+
# repo: "oscarvalenzuelab/dotclaude"
|
|
87
|
+
# # checkout_path: "~/.ctrlrelay/personalization" # default
|
|
88
|
+
# # main_branch: "main" # default
|
|
89
|
+
# # node_id: "macbook" # default: top-level node_id (i.e. hostname)
|
|
90
|
+
# paths:
|
|
91
|
+
# # Global Claude config — fixed paths, one entry each so individual
|
|
92
|
+
# # files can come and go without ripping a whole `~/.claude/` symlink.
|
|
93
|
+
# - source: "global/CLAUDE.md"
|
|
94
|
+
# target: "~/.claude/CLAUDE.md"
|
|
95
|
+
# - source: "global/skills/"
|
|
96
|
+
# target: "~/.claude/skills/"
|
|
97
|
+
# - source: "global/agents/"
|
|
98
|
+
# target: "~/.claude/agents/"
|
|
99
|
+
# - source: "global/commands/"
|
|
100
|
+
# target: "~/.claude/commands/"
|
|
101
|
+
# - source: "global/keybindings.json"
|
|
102
|
+
# target: "~/.claude/keybindings.json"
|
|
103
|
+
#
|
|
104
|
+
# # Per-project Claude memory. ${PROJECT_ENCODED} resolves at
|
|
105
|
+
# # link-time from the repo's local_path, so the same config works
|
|
106
|
+
# # on machines with different home directories or repo layouts.
|
|
107
|
+
# - source: "claude-memory/${PROJECT}/"
|
|
108
|
+
# target: "~/.claude/projects/${PROJECT_ENCODED}/memory/"
|
|
109
|
+
# project_scoped: true
|
|
110
|
+
#
|
|
111
|
+
# # Spec / superpower outputs Claude writes per the convention
|
|
112
|
+
# # documented in templates/global-CLAUDE.md.snippet. Lives next to
|
|
113
|
+
# # the repo (NOT inside it) so the project's own git history stays
|
|
114
|
+
# # uncontaminated.
|
|
115
|
+
# - source: "specs/${PROJECT}/"
|
|
116
|
+
# target: "${PROJECT_PARENT}/specs/${PROJECT}/"
|
|
117
|
+
# project_scoped: true
|
|
118
|
+
#
|
|
119
|
+
# # Workspace-level planning docs (multi-repo workspace, e.g. a
|
|
120
|
+
# # research project that spans 3 repos under a single parent dir).
|
|
121
|
+
# # Use one entry per workspace; if you accumulate many, this will
|
|
122
|
+
# # be promoted to a first-class `workspaces:` config block.
|
|
123
|
+
# # - source: "workspaces/RESEARCH-DMP/"
|
|
124
|
+
# # target: "~/Projects/RESEARCH/DMP/notes/"
|
|
125
|
+
|
|
126
|
+
repos: []
|
|
127
|
+
# Example repo configuration:
|
|
128
|
+
# - name: "owner/repo"
|
|
129
|
+
# # Optional when paths.repo_root is set; otherwise required.
|
|
130
|
+
# local_path: "~/Projects/repo"
|
|
131
|
+
# automation:
|
|
132
|
+
# dependabot_patch: auto
|
|
133
|
+
# dependabot_minor: ask
|
|
134
|
+
# dependabot_major: never
|
|
135
|
+
# # Issues carrying any of these labels are skipped by the poller:
|
|
136
|
+
# # marked seen, logged as poll.issue.excluded_by_label, and never
|
|
137
|
+
# # handed to the dev pipeline. Intended for operator tasks and
|
|
138
|
+
# # pure instruction issues that should not trigger code changes.
|
|
139
|
+
# # Matching is case-insensitive. Defaults to ["manual", "operator",
|
|
140
|
+
# # "instruction"]; override with an empty list to disable.
|
|
141
|
+
# exclude_labels: ["manual", "operator", "instruction"]
|
|
@@ -41,6 +41,51 @@ ctrlrelay [--version] <command> ...
|
|
|
41
41
|
Every command that reads config accepts `--config` / `-c` to point at a
|
|
42
42
|
non-default `orchestrator.yaml` (default: `config/orchestrator.yaml`).
|
|
43
43
|
|
|
44
|
+
## `ctrlrelay setup`
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ctrlrelay setup [OPTIONS]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
First-run onboarding: detect GitHub orgs, enumerate their non-fork
|
|
51
|
+
non-archived repos, write a fresh `~/.config/ctrlrelay/orchestrator.yaml`,
|
|
52
|
+
clone every repo to `~/Projects/<owner.lower()>/<repo>`, and (optionally)
|
|
53
|
+
configure the personalization sync block plus install launchd/systemd unit
|
|
54
|
+
files. Composes the building blocks operators previously had to wire by hand.
|
|
55
|
+
|
|
56
|
+
The interactive flow asks one question at a time. `--yes` skips prompts and
|
|
57
|
+
accepts every default. Repeat `--owner` to lock the owner list non-interactively.
|
|
58
|
+
|
|
59
|
+
Key flags:
|
|
60
|
+
|
|
61
|
+
| Flag | Default | Notes |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `--owner OWNER` | (interactive prompt) | Repeatable. When omitted, prompts to pick from accessible owners. |
|
|
64
|
+
| `--repo-root PATH` | `~/Projects` | Where repos land — `~/Projects/<owner.lower()>/<repo>`. |
|
|
65
|
+
| `--config-out PATH` | `~/.config/ctrlrelay/orchestrator.yaml` | Where to write the generated config. |
|
|
66
|
+
| `--timezone TZ` | `UTC` | IANA zone for cron schedules. |
|
|
67
|
+
| `--transport TRANSPORT` | `file_mock` | `telegram` or `file_mock`. |
|
|
68
|
+
| `--telegram-chat-id ID` | none | Required for `--transport=telegram`. |
|
|
69
|
+
| `--personalization-repo OWNER/REPO` | none | Adds a personalization block. |
|
|
70
|
+
| `--no-personalization` | off | Skip the personalization prompt entirely. |
|
|
71
|
+
| `--install-daemons` | (prompted) | Render and write launchd/systemd unit files. Reads `$CTRLRELAY_TELEGRAM_TOKEN` so the rendered plist isn't a placeholder. |
|
|
72
|
+
| `--skip-archived/--include-archived` | skip | gh repo list filter. |
|
|
73
|
+
| `--skip-forks/--include-forks` | skip | gh repo list filter. |
|
|
74
|
+
| `--yes` / `-y` | off | Accept every default; never prompt. |
|
|
75
|
+
| `--force` | off | Overwrite an existing `orchestrator.yaml` (and existing daemon plists when `--install-daemons`). |
|
|
76
|
+
|
|
77
|
+
Refuses to overwrite an existing config or daemon plist without `--force`.
|
|
78
|
+
Refuses to proceed if `gh auth status` fails — run `gh auth login` first.
|
|
79
|
+
|
|
80
|
+
After setup completes, the next manual steps are:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ctrlrelay.ctrlrelay-poller.plist
|
|
84
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ctrlrelay.ctrlrelay-bridge.plist
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
(Or the systemd equivalents on Linux.)
|
|
88
|
+
|
|
44
89
|
## `ctrlrelay config`
|
|
45
90
|
|
|
46
91
|
### `config validate`
|
|
@@ -232,6 +277,85 @@ ctrlrelay poller status [-c PATH]
|
|
|
232
277
|
|
|
233
278
|
Reports running / not-running.
|
|
234
279
|
|
|
280
|
+
## `ctrlrelay personalization`
|
|
281
|
+
|
|
282
|
+
See [Personalization sync]({{ '/personalization/' | relative_url }})
|
|
283
|
+
for the conceptual walkthrough. All subcommands require a
|
|
284
|
+
`personalization:` block in `orchestrator.yaml` and operate on the
|
|
285
|
+
checkout at `personalization.checkout_path` (default
|
|
286
|
+
`~/.ctrlrelay/personalization`).
|
|
287
|
+
|
|
288
|
+
### `personalization init`
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
ctrlrelay personalization init [-c PATH] [--no-adopt]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Clones the personalization repo into `checkout_path`, creates the
|
|
295
|
+
per-machine working branch (`personalization/<node_id>`), and lays
|
|
296
|
+
down the symlinks declared in `personalization.paths`.
|
|
297
|
+
|
|
298
|
+
By default, **adopt-flow** is on: a target that exists as a real file
|
|
299
|
+
or directory while its corresponding source slot in the repo is empty
|
|
300
|
+
is moved into the repo and replaced with a symlink. Your existing
|
|
301
|
+
content is preserved and immediately under sync. Run `personalization
|
|
302
|
+
push` after `init` to send the adopted content to GitHub.
|
|
303
|
+
|
|
304
|
+
Pass `--no-adopt` to opt out of adoption — pre-existing real targets
|
|
305
|
+
surface as `skipped-real-file-at-target` instead, and you back them up
|
|
306
|
+
+ remove them manually before re-running.
|
|
307
|
+
|
|
308
|
+
If both the repo source and the on-disk target have real content,
|
|
309
|
+
`init` refuses with `skipped-conflict-both-exist` regardless of
|
|
310
|
+
`--no-adopt`. Reconcile manually before re-running.
|
|
311
|
+
|
|
312
|
+
### `personalization status`
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
ctrlrelay personalization status [-c PATH]
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Prints the working branch, repo URL, ahead/behind counts vs. origin,
|
|
319
|
+
and per-symlink state — `correct`, `wrong-target`, `missing`,
|
|
320
|
+
`source-missing`, etc. Read-only; doesn't touch the filesystem.
|
|
321
|
+
|
|
322
|
+
### `personalization push`
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
ctrlrelay personalization push [-c PATH] -m "MESSAGE"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Stages the entries declared in `paths` (allowlist — random files in
|
|
329
|
+
the checkout are not committed), commits with `MESSAGE`, rebases the
|
|
330
|
+
per-machine branch onto `origin/<main_branch>`, and pushes. Retries
|
|
331
|
+
the fast-forward of `origin/main` up to three times if another
|
|
332
|
+
machine's push lands between fetch and push. Uses
|
|
333
|
+
`--force-with-lease` on the per-machine branch only when the local
|
|
334
|
+
working branch has diverged from the remote (after a rebase) — never
|
|
335
|
+
on `main`.
|
|
336
|
+
|
|
337
|
+
Conflicts during the rebase abort and list the unmerged files for you
|
|
338
|
+
to resolve.
|
|
339
|
+
|
|
340
|
+
### `personalization pull`
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
ctrlrelay personalization pull [-c PATH]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Fetches, rebases the per-machine branch onto `origin/<main_branch>`,
|
|
347
|
+
fast-forwards local `main` if it's an ancestor of the remote, and
|
|
348
|
+
re-wires symlinks (the config-as-code shipped in the repo may have
|
|
349
|
+
changed). Uses `adopt=True` like `init` so newly-declared paths can
|
|
350
|
+
adopt local content during a `pull`. Conflicts abort cleanly and list
|
|
351
|
+
the unmerged files.
|
|
352
|
+
|
|
353
|
+
The daemon-driven auto-pull (under
|
|
354
|
+
[`schedules.personalization_cron`]({{ '/configuration/#schedules' | relative_url }}))
|
|
355
|
+
calls the same code with two extra rails: skip when the working tree
|
|
356
|
+
is dirty, and re-wire with `adopt=False` so a background sync never
|
|
357
|
+
silently moves files.
|
|
358
|
+
|
|
235
359
|
## `ctrlrelay status`
|
|
236
360
|
|
|
237
361
|
```bash
|