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.
Files changed (114) hide show
  1. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/pages.yml +1 -1
  2. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.gitignore +14 -0
  3. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/CHANGELOG.md +151 -0
  4. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/PKG-INFO +7 -4
  5. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/README.md +6 -3
  6. ctrlrelay-0.4.0/config/orchestrator.yaml.example +141 -0
  7. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/architecture.md +1 -1
  8. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/cli.md +124 -0
  9. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/configuration.md +153 -2
  10. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/development.md +1 -1
  11. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/getting-started.md +46 -56
  12. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/index.md +3 -0
  13. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/operations.md +56 -34
  14. ctrlrelay-0.4.0/docs/personalization.md +343 -0
  15. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/pyproject.toml +1 -1
  16. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/cli.py +690 -19
  17. ctrlrelay-0.4.0/src/ctrlrelay/core/config.py +732 -0
  18. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/github.py +59 -5
  19. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/poller.py +364 -12
  20. ctrlrelay-0.4.0/src/ctrlrelay/install.py +192 -0
  21. ctrlrelay-0.4.0/src/ctrlrelay/personalization/__init__.py +26 -0
  22. ctrlrelay-0.4.0/src/ctrlrelay/personalization/manager.py +1176 -0
  23. ctrlrelay-0.4.0/src/ctrlrelay/personalization/paths.py +143 -0
  24. ctrlrelay-0.4.0/src/ctrlrelay/setup.py +427 -0
  25. ctrlrelay-0.4.0/src/ctrlrelay/templates/__init__.py +0 -0
  26. ctrlrelay-0.4.0/src/ctrlrelay/templates/global-CLAUDE.md.snippet +61 -0
  27. ctrlrelay-0.4.0/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  28. ctrlrelay-0.4.0/src/ctrlrelay/templates/launchd/bridge.plist.template +37 -0
  29. ctrlrelay-0.4.0/src/ctrlrelay/templates/launchd/poller.plist.template +44 -0
  30. ctrlrelay-0.4.0/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  31. ctrlrelay-0.4.0/src/ctrlrelay/templates/systemd/bridge.service.template +20 -0
  32. ctrlrelay-0.4.0/src/ctrlrelay/templates/systemd/poller.service.template +25 -0
  33. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_repos.py +13 -7
  34. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_config.py +226 -0
  35. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dev_integration.py +249 -1
  36. ctrlrelay-0.4.0/tests/test_install.py +294 -0
  37. ctrlrelay-0.4.0/tests/test_personalization.py +1981 -0
  38. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_poller.py +709 -1
  39. ctrlrelay-0.4.0/tests/test_setup.py +531 -0
  40. ctrlrelay-0.2.1/config/orchestrator.yaml.example +0 -60
  41. ctrlrelay-0.2.1/src/ctrlrelay/core/config.py +0 -377
  42. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/dependabot.yml +0 -0
  46. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/build.yml +0 -0
  47. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/cla.yml +0 -0
  48. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/publish.yml +0 -0
  49. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/.github/workflows/test.yml +0 -0
  50. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  51. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/CONTRIBUTING.md +0 -0
  52. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/LICENSE +0 -0
  53. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/SECURITY.md +0 -0
  54. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/Gemfile +0 -0
  55. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/_config.yml +0 -0
  56. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/bridge.md +0 -0
  57. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/docs/feedback-loop.md +0 -0
  58. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/__init__.py +0 -0
  59. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  60. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/__main__.py +0 -0
  61. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  62. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/server.py +0 -0
  63. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  64. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/__init__.py +0 -0
  65. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/audit.py +0 -0
  66. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/checkpoint.py +0 -0
  67. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/dispatcher.py +0 -0
  68. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/obs.py +0 -0
  69. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  70. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  71. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/scheduler.py +0 -0
  72. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/state.py +0 -0
  73. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/core/worktree.py +0 -0
  74. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  75. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/dashboard/client.py +0 -0
  76. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  77. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/base.py +0 -0
  78. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/dev.py +0 -0
  79. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  80. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/secops.py +0 -0
  81. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/pipelines/task.py +0 -0
  82. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/__init__.py +0 -0
  83. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/base.py +0 -0
  84. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/file_mock.py +0 -0
  85. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/src/ctrlrelay/transports/socket_client.py +0 -0
  86. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/__init__.py +0 -0
  87. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/conftest.py +0 -0
  88. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_audit.py +0 -0
  89. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_bridge_protocol.py +0 -0
  90. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_bridge_server.py +0 -0
  91. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_checkpoint.py +0 -0
  92. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_ci_wait.py +0 -0
  93. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_dev.py +0 -0
  94. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_secops.py +0 -0
  95. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_start.py +0 -0
  96. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_cli_version.py +0 -0
  97. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dashboard_client.py +0 -0
  98. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dev_pipeline.py +0 -0
  99. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_dispatcher.py +0 -0
  100. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_docs_site.py +0 -0
  101. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_github.py +0 -0
  102. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_obs.py +0 -0
  103. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_pipeline_base.py +0 -0
  104. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_post_merge.py +0 -0
  105. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_pr_verifier.py +0 -0
  106. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_pr_watcher.py +0 -0
  107. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_scheduler.py +0 -0
  108. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_secops_integration.py +0 -0
  109. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_secops_pipeline.py +0 -0
  110. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_state.py +0 -0
  111. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_task_pipeline.py +0 -0
  112. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_telegram_handler.py +0 -0
  113. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_transport.py +0 -0
  114. {ctrlrelay-0.2.1 → ctrlrelay-0.4.0}/tests/test_worktree.py +0 -0
@@ -47,7 +47,7 @@ jobs:
47
47
  --destination ./_site
48
48
 
49
49
  - name: Upload artifact
50
- uses: actions/upload-pages-artifact@v3
50
+ uses: actions/upload-pages-artifact@v5
51
51
  with:
52
52
  path: docs/_site
53
53
 
@@ -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.2.1
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 assigned to you across every
88
- configured repo, spawns a dev session in a dedicated git worktree,
89
- and opens a PR.
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 assigned to you across every
49
- configured repo, spawns a dev session in a dedicated git worktree,
50
- and opens a PR.
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"]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Architecture
3
3
  layout: default
4
- nav_order: 8
4
+ nav_order: 9
5
5
  description: "Layer diagram, dispatcher / Claude interaction, state-DB shape, and worktree lifecycle for contributors."
6
6
  permalink: /architecture/
7
7
  ---
@@ -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