ctrlrelay 0.2.1__tar.gz → 0.3.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.3.0}/.github/workflows/pages.yml +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.gitignore +3 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/CHANGELOG.md +98 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/PKG-INFO +7 -4
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/README.md +6 -3
- ctrlrelay-0.3.0/config/orchestrator.yaml.example +141 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/architecture.md +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/cli.md +79 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/configuration.md +156 -2
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/development.md +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/getting-started.md +8 -3
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/index.md +3 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/operations.md +56 -34
- ctrlrelay-0.3.0/docs/personalization.md +343 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/pyproject.toml +1 -1
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/cli.py +353 -1
- ctrlrelay-0.3.0/src/ctrlrelay/core/config.py +712 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/github.py +59 -5
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/poller.py +364 -12
- ctrlrelay-0.3.0/src/ctrlrelay/install.py +192 -0
- ctrlrelay-0.3.0/src/ctrlrelay/personalization/__init__.py +26 -0
- ctrlrelay-0.3.0/src/ctrlrelay/personalization/manager.py +1176 -0
- ctrlrelay-0.3.0/src/ctrlrelay/personalization/paths.py +143 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/__init__.py +0 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/global-CLAUDE.md.snippet +61 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/launchd/__init__.py +0 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/launchd/bridge.plist.template +37 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/launchd/poller.plist.template +44 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/systemd/__init__.py +0 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/systemd/bridge.service.template +20 -0
- ctrlrelay-0.3.0/src/ctrlrelay/templates/systemd/poller.service.template +25 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_config.py +210 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dev_integration.py +249 -1
- ctrlrelay-0.3.0/tests/test_install.py +294 -0
- ctrlrelay-0.3.0/tests/test_personalization.py +1981 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_poller.py +709 -1
- 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.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/dependabot.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/build.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/cla.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/publish.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/test.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/CONTRIBUTING.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/LICENSE +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/SECURITY.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/Gemfile +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/_config.yml +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/bridge.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/feedback-loop.md +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/__main__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/protocol.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/server.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/audit.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/checkpoint.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/dispatcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/obs.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/scheduler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/state.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/worktree.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/dashboard/client.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/base.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/dev.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/secops.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/task.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/base.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/file_mock.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/socket_client.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/__init__.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/conftest.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_audit.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_bridge_protocol.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_bridge_server.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_checkpoint.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_ci_wait.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_dev.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_repos.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_secops.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_start.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_version.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dashboard_client.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dev_pipeline.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dispatcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_docs_site.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_github.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_obs.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_pipeline_base.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_post_merge.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_pr_verifier.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_pr_watcher.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_scheduler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_secops_integration.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_secops_pipeline.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_state.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_task_pipeline.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_telegram_handler.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_transport.py +0 -0
- {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_worktree.py +0 -0
|
@@ -7,6 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-05-08
|
|
11
|
+
|
|
12
|
+
Minor release. Three additive features: cross-machine **personalization
|
|
13
|
+
sync** of operator state through a private GitHub repo, **portability
|
|
14
|
+
fixes** that let one `orchestrator.yaml` work unmodified across machines,
|
|
15
|
+
and **label-driven issue matching** for the dev pipeline. One install
|
|
16
|
+
fix (`PYTHONUNBUFFERED`) and a docs page covering the new
|
|
17
|
+
personalization flow.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Personalization sync** (#123, #124). New `personalization:` config
|
|
22
|
+
block + `ctrlrelay personalization init/status/push/pull`
|
|
23
|
+
subcommands sync the operator's Claude Code state — global config,
|
|
24
|
+
per-project memory, spec/superpower outputs — across machines
|
|
25
|
+
through a separate (typically private) GitHub repo. Per-machine
|
|
26
|
+
branches (`personalization/<node_id>`) rebase onto `main` and FF
|
|
27
|
+
the integration branch, so two machines pushing concurrently never
|
|
28
|
+
overwrite each other; `--force-with-lease` is reserved for the
|
|
29
|
+
per-machine branch, never used on `main`. Source/target paths
|
|
30
|
+
support `${HOME}`, `${PROJECT}` (slug `<owner>--<repo>`),
|
|
31
|
+
`${PROJECT_ENCODED}` (matches Claude's path encoding),
|
|
32
|
+
`${PROJECT_LOCAL}`, and `${PROJECT_PARENT}` placeholders. An
|
|
33
|
+
allowlist limits commits to declared entries — random files in the
|
|
34
|
+
checkout aren't staged. **Adopt-flow** is on by default: `init`
|
|
35
|
+
moves pre-existing real targets (e.g. `~/.claude/CLAUDE.md` that
|
|
36
|
+
predates the sync setup) into the synced repo and lays a symlink in
|
|
37
|
+
their place; `--no-adopt` opts out. Both-real-content collisions
|
|
38
|
+
surface as `skipped-conflict-both-exist` for manual reconciliation.
|
|
39
|
+
See the new [Personalization sync](docs/personalization.md) page.
|
|
40
|
+
- **Auto-pull on cron** (#124). New optional
|
|
41
|
+
`schedules.personalization_cron` runs `personalization pull` on the
|
|
42
|
+
poller daemon, with two safety rails: skip-on-dirty (never rebases
|
|
43
|
+
under uncommitted operator edits) and `adopt=False` on the re-wire
|
|
44
|
+
(a background sync never silently moves files; adoption stays
|
|
45
|
+
init-only). Auto-push is intentionally not scheduled — daemon-side
|
|
46
|
+
commits surprise people. Dispatched via `asyncio.to_thread` so a
|
|
47
|
+
slow remote can't stall the poller's event loop (Telegram dispatch,
|
|
48
|
+
pending-resume sweeper, secops cron).
|
|
49
|
+
- **`paths.repo_root` + `paths.owner_aliases`** (#121). When set,
|
|
50
|
+
`repos[].local_path` is derived as
|
|
51
|
+
`${repo_root}/${owner_aliases.get(owner, owner)}/${repo}`. Per-repo
|
|
52
|
+
`local_path` still wins as an override. Without `repo_root`, the
|
|
53
|
+
legacy "local_path required per repo" behaviour is preserved.
|
|
54
|
+
Collapses 69 explicit `local_path` values to 20 in the maintainer's
|
|
55
|
+
config.
|
|
56
|
+
- **`node_id` defaults to hostname** (#121). Falls back to
|
|
57
|
+
`socket.gethostname()` when missing, null, or blank. Heartbeats and
|
|
58
|
+
session logs still get a meaningful per-node label without forcing
|
|
59
|
+
every operator to edit the file.
|
|
60
|
+
- **`ctrlrelay install launchd|systemd`** (#121). Renders
|
|
61
|
+
bridge/poller service unit files from in-package templates,
|
|
62
|
+
substituting `USER`, `HOME`, `CTRLRELAY_BIN`, `WORKDIR`,
|
|
63
|
+
`LABEL_PREFIX`, `POLLER_INTERVAL`, and (when set)
|
|
64
|
+
`CTRLRELAY_TELEGRAM_TOKEN`. Writes to the conventional locations
|
|
65
|
+
(`~/Library/LaunchAgents` on macOS, `~/.config/systemd/user` on
|
|
66
|
+
Linux) and refuses to clobber existing files unless `--force`.
|
|
67
|
+
Replaces the docs.operations.md copy-paste flow where every
|
|
68
|
+
operator hand-edited `/Users/$ME/...` strings — a tax on
|
|
69
|
+
portability and a common source of broken plists.
|
|
70
|
+
- **Label-driven issue matching** (#115, closes #80). Two new
|
|
71
|
+
per-repo lists govern which issues the poller hands to the dev
|
|
72
|
+
pipeline:
|
|
73
|
+
- `repos[].automation.exclude_labels` (default `["manual",
|
|
74
|
+
"operator", "instruction"]`) — issues carrying any of these
|
|
75
|
+
labels are skipped, marked seen, logged as
|
|
76
|
+
`poll.issue.excluded_by_label`, and never trigger code changes.
|
|
77
|
+
For operator tasks and pure instruction issues.
|
|
78
|
+
- `repos[].automation.include_labels` (default `[]`) — when
|
|
79
|
+
non-empty, issues carrying any of these labels opt **in** to the
|
|
80
|
+
dev pipeline regardless of who is (or isn't) assigned. For repos
|
|
81
|
+
that drive the pipeline by triage label rather than assignment.
|
|
82
|
+
Matching is case-insensitive. Trust model documented in
|
|
83
|
+
[configuration.md](docs/configuration.md#repos-automation): anyone
|
|
84
|
+
with triage permission on a repo can apply a label, which matches
|
|
85
|
+
ctrlrelay's existing trust model.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **`PYTHONUNBUFFERED=1` in launchd plists and systemd units** (#122).
|
|
90
|
+
Without it, daemon stdout/stderr buffered up to 4–8 KB before
|
|
91
|
+
flushing to the log file — so `tail -f` on a poller log looked
|
|
92
|
+
frozen for minutes during quiet periods, and crash diagnostics were
|
|
93
|
+
clipped at the last buffer boundary instead of the actual failure
|
|
94
|
+
point. Templates now set the env var and `ctrlrelay install
|
|
95
|
+
launchd|systemd` re-emits both unit files on next run.
|
|
96
|
+
|
|
97
|
+
### Docs
|
|
98
|
+
|
|
99
|
+
- **[Personalization sync](docs/personalization.md)** (#125). New
|
|
100
|
+
page (nav order 8) covering setup, the dotclaude repo layout, the
|
|
101
|
+
init/status/push/pull lifecycle, `--no-adopt`, auto-pull cron,
|
|
102
|
+
multi-machine bootstrap, and the gotchas (worktrees, edit-through-
|
|
103
|
+
symlink semantics, allowlist enforcement, conflict handling, strict
|
|
104
|
+
origin URL match). Pairs with new `schedules` and `personalization`
|
|
105
|
+
sections in [configuration.md](docs/configuration.md) and a new
|
|
106
|
+
`ctrlrelay personalization` block in [cli.md](docs/cli.md).
|
|
107
|
+
|
|
10
108
|
## [0.2.1] - 2026-04-28
|
|
11
109
|
|
|
12
110
|
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.3.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"]
|
|
@@ -232,6 +232,85 @@ ctrlrelay poller status [-c PATH]
|
|
|
232
232
|
|
|
233
233
|
Reports running / not-running.
|
|
234
234
|
|
|
235
|
+
## `ctrlrelay personalization`
|
|
236
|
+
|
|
237
|
+
See [Personalization sync]({{ '/personalization/' | relative_url }})
|
|
238
|
+
for the conceptual walkthrough. All subcommands require a
|
|
239
|
+
`personalization:` block in `orchestrator.yaml` and operate on the
|
|
240
|
+
checkout at `personalization.checkout_path` (default
|
|
241
|
+
`~/.ctrlrelay/personalization`).
|
|
242
|
+
|
|
243
|
+
### `personalization init`
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
ctrlrelay personalization init [-c PATH] [--no-adopt]
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Clones the personalization repo into `checkout_path`, creates the
|
|
250
|
+
per-machine working branch (`personalization/<node_id>`), and lays
|
|
251
|
+
down the symlinks declared in `personalization.paths`.
|
|
252
|
+
|
|
253
|
+
By default, **adopt-flow** is on: a target that exists as a real file
|
|
254
|
+
or directory while its corresponding source slot in the repo is empty
|
|
255
|
+
is moved into the repo and replaced with a symlink. Your existing
|
|
256
|
+
content is preserved and immediately under sync. Run `personalization
|
|
257
|
+
push` after `init` to send the adopted content to GitHub.
|
|
258
|
+
|
|
259
|
+
Pass `--no-adopt` to opt out of adoption — pre-existing real targets
|
|
260
|
+
surface as `skipped-real-file-at-target` instead, and you back them up
|
|
261
|
+
+ remove them manually before re-running.
|
|
262
|
+
|
|
263
|
+
If both the repo source and the on-disk target have real content,
|
|
264
|
+
`init` refuses with `skipped-conflict-both-exist` regardless of
|
|
265
|
+
`--no-adopt`. Reconcile manually before re-running.
|
|
266
|
+
|
|
267
|
+
### `personalization status`
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
ctrlrelay personalization status [-c PATH]
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Prints the working branch, repo URL, ahead/behind counts vs. origin,
|
|
274
|
+
and per-symlink state — `correct`, `wrong-target`, `missing`,
|
|
275
|
+
`source-missing`, etc. Read-only; doesn't touch the filesystem.
|
|
276
|
+
|
|
277
|
+
### `personalization push`
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
ctrlrelay personalization push [-c PATH] -m "MESSAGE"
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Stages the entries declared in `paths` (allowlist — random files in
|
|
284
|
+
the checkout are not committed), commits with `MESSAGE`, rebases the
|
|
285
|
+
per-machine branch onto `origin/<main_branch>`, and pushes. Retries
|
|
286
|
+
the fast-forward of `origin/main` up to three times if another
|
|
287
|
+
machine's push lands between fetch and push. Uses
|
|
288
|
+
`--force-with-lease` on the per-machine branch only when the local
|
|
289
|
+
working branch has diverged from the remote (after a rebase) — never
|
|
290
|
+
on `main`.
|
|
291
|
+
|
|
292
|
+
Conflicts during the rebase abort and list the unmerged files for you
|
|
293
|
+
to resolve.
|
|
294
|
+
|
|
295
|
+
### `personalization pull`
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
ctrlrelay personalization pull [-c PATH]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Fetches, rebases the per-machine branch onto `origin/<main_branch>`,
|
|
302
|
+
fast-forwards local `main` if it's an ancestor of the remote, and
|
|
303
|
+
re-wires symlinks (the config-as-code shipped in the repo may have
|
|
304
|
+
changed). Uses `adopt=True` like `init` so newly-declared paths can
|
|
305
|
+
adopt local content during a `pull`. Conflicts abort cleanly and list
|
|
306
|
+
the unmerged files.
|
|
307
|
+
|
|
308
|
+
The daemon-driven auto-pull (under
|
|
309
|
+
[`schedules.personalization_cron`]({{ '/configuration/#schedules' | relative_url }}))
|
|
310
|
+
calls the same code with two extra rails: skip when the working tree
|
|
311
|
+
is dirty, and re-wire with `adopt=False` so a background sync never
|
|
312
|
+
silently moves files.
|
|
313
|
+
|
|
235
314
|
## `ctrlrelay status`
|
|
236
315
|
|
|
237
316
|
```bash
|
|
@@ -32,13 +32,15 @@ repos: [ ... ]
|
|
|
32
32
|
| Key | Type | Required | Default | Description |
|
|
33
33
|
|---|---|---|---|---|
|
|
34
34
|
| `version` | string | no | `"1"` | Config schema version. Currently always `"1"`. |
|
|
35
|
-
| `node_id` | string |
|
|
35
|
+
| `node_id` | string | no | `socket.gethostname()` | Free-form identifier for this machine. Surfaces in dashboard heartbeats and session logs. Defaults to the OS hostname when omitted, null, or blank — set explicitly only if the hostname is meaningless (CI runners, ephemeral containers). |
|
|
36
36
|
| `timezone` | string | no | `"UTC"` | IANA timezone (e.g. `America/Santiago`). Used for scheduling. |
|
|
37
37
|
| `paths` | object | **yes** | — | See [paths](#paths). |
|
|
38
38
|
| `claude` | object | no | (defaults) | See [claude](#claude). |
|
|
39
39
|
| `transport` | object | **yes** | — | See [transport](#transport). |
|
|
40
40
|
| `dashboard` | object | no | (defaults) | See [dashboard](#dashboard). |
|
|
41
41
|
| `repos` | list | no | `[]` | See [repos](#repos). |
|
|
42
|
+
| `schedules` | object | no | (defaults) | See [schedules](#schedules). |
|
|
43
|
+
| `personalization` | object | no | unset | Cross-machine sync of Claude state. See [personalization](#personalization) and [Personalization sync]({{ '/personalization/' | relative_url }}). |
|
|
42
44
|
|
|
43
45
|
## paths
|
|
44
46
|
|
|
@@ -51,6 +53,11 @@ paths:
|
|
|
51
53
|
bare_repos: "~/.ctrlrelay/repos"
|
|
52
54
|
contexts: "~/.ctrlrelay/contexts"
|
|
53
55
|
skills: "~/.claude/skills"
|
|
56
|
+
# Optional convention for repos[].local_path:
|
|
57
|
+
repo_root: "~/Projects"
|
|
58
|
+
owner_aliases:
|
|
59
|
+
AInvirion: AINVIRION # GitHub owner -> on-disk folder name
|
|
60
|
+
SemClone: SEMCL.ONE
|
|
54
61
|
```
|
|
55
62
|
|
|
56
63
|
| Key | Type | Required | Description |
|
|
@@ -60,6 +67,8 @@ paths:
|
|
|
60
67
|
| `bare_repos` | path | **yes** | Where ctrlrelay clones bare mirrors of each configured repo. |
|
|
61
68
|
| `contexts` | path | **yes** | Per-repo context directory (looked up as `<contexts>/<owner-repo>/CLAUDE.md`). If a `CLAUDE.md` exists, it is symlinked into the worktree at session start. |
|
|
62
69
|
| `skills` | path | **yes** | Claude Code skills directory used by `ctrlrelay skills audit` and `ctrlrelay skills list`. |
|
|
70
|
+
| `repo_root` | path | no | Convention root for repo clones. When set, `repos[].local_path` may be omitted and is derived as `${repo_root}/${owner_aliases.get(owner, owner)}/${repo}`. Without `repo_root`, every repo entry must declare its own `local_path` (legacy behaviour). |
|
|
71
|
+
| `owner_aliases` | object | no | Map of GitHub owner -> on-disk folder name. Lets the convention work when local folders use a vanity name (`SemClone` repos under `~/Projects/SEMCL.ONE/`). Lookup falls through to the literal owner if not present. |
|
|
63
72
|
|
|
64
73
|
## claude
|
|
65
74
|
|
|
@@ -172,7 +181,7 @@ repos:
|
|
|
172
181
|
| Key | Type | Required | Default | Description |
|
|
173
182
|
|---|---|---|---|---|
|
|
174
183
|
| `name` | string | **yes** | — | GitHub `owner/repo` slug. Used for `gh` calls and bare-repo / worktree naming. |
|
|
175
|
-
| `local_path` | path |
|
|
184
|
+
| `local_path` | path | conditional | derived | Where the repo is checked out on disk for human use. Optional when `paths.repo_root` is set (then derived as `${repo_root}/${owner_aliases.get(owner, owner)}/${repo}`); required otherwise. An explicit value always wins as override. ctrlrelay itself uses bare mirrors under `paths.bare_repos`. |
|
|
176
185
|
| `dev_branch_template` | string | no | `"fix/issue-{n}"` | Branch-name template for dev-pipeline runs. `{n}` is replaced by the issue number. |
|
|
177
186
|
| `automation` | object | no | (defaults) | See [automation](#repos-automation). |
|
|
178
187
|
| `code_review` | object | no | (defaults) | Reserved for code-review policy. Currently unused by the bundled pipelines. |
|
|
@@ -193,6 +202,7 @@ and ask the operator), or `never` (skip).
|
|
|
193
202
|
| `deploy_after_merge` | `auto` | Whether to deploy after a merged PR. |
|
|
194
203
|
| `accept_foreign_assignments` | `false` | When `true`, the poller also picks up issues assigned to you by someone else. Default (`false`) runs the dev pipeline only on issues you self-assigned. |
|
|
195
204
|
| `exclude_labels` | `["manual", "operator", "instruction"]` | Issue labels that tell the poller "this isn't for the agent". See [exclude_labels](#reposautomationexclude_labels) below. |
|
|
205
|
+
| `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. |
|
|
196
206
|
|
|
197
207
|
The current secops and dev pipelines read these settings to bias their prompts
|
|
198
208
|
to Claude — they're not enforced by hard-coded checks.
|
|
@@ -228,6 +238,150 @@ label on GitHub **and** delete the issue number from
|
|
|
228
238
|
other mechanism — the poller treats "seen" as sticky per design, so operator
|
|
229
239
|
input is the source of truth).
|
|
230
240
|
|
|
241
|
+
### repos[].automation.include_labels
|
|
242
|
+
|
|
243
|
+
Out of the box, an issue enters the dev pipeline only when it's assigned to
|
|
244
|
+
the configured GitHub user (and, with the pre-#79 self-assignment filter, only
|
|
245
|
+
when *you* were the one who assigned it). That works for a personal to-do
|
|
246
|
+
list; it doesn't cover the "team-coordinated" workflow where a teammate without
|
|
247
|
+
rights on your account wants to say "this issue is safe for the agent to take
|
|
248
|
+
a shot at."
|
|
249
|
+
|
|
250
|
+
`include_labels` is the opt-in complement to `exclude_labels`. Any issue
|
|
251
|
+
carrying one of the configured labels is handed to the dev pipeline,
|
|
252
|
+
**regardless of assignment**. The label itself is the trust signal — you opt
|
|
253
|
+
in by configuring the label; anyone with triage permission on the repo can
|
|
254
|
+
then flag an issue for the bot.
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
repos:
|
|
258
|
+
- name: "your-org/your-repo"
|
|
259
|
+
local_path: "~/Projects/your-repo"
|
|
260
|
+
automation:
|
|
261
|
+
include_labels: ["ctrlrelay:auto"]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
- Default: `[]`. An empty list preserves the pre-#80 assignment-only trigger
|
|
265
|
+
— no behavior change for operators who haven't opted in.
|
|
266
|
+
- Matching is **case-insensitive** (`CtrlRelay:Auto` matches `ctrlrelay:auto`).
|
|
267
|
+
- An issue is accepted when **either** (a) it's assigned to the configured
|
|
268
|
+
user (subject to the self-assignment filter from #79 and
|
|
269
|
+
`accept_foreign_assignments`) **or** (b) it carries any label in
|
|
270
|
+
`include_labels`. A label match **skips** the self-assignment check — the
|
|
271
|
+
operator's config choice is the trust boundary.
|
|
272
|
+
- Dedup: an issue that is **both** labeled and assigned is picked up exactly
|
|
273
|
+
once per poll cycle — no duplicate entries in `seen_issues` and no double
|
|
274
|
+
pipeline spawn.
|
|
275
|
+
- `exclude_labels` always wins over `include_labels` on the same issue: an
|
|
276
|
+
explicit "not for the agent" opt-OUT beats the generic label opt-IN.
|
|
277
|
+
- When a repo configures `include_labels`, the poller runs **targeted**
|
|
278
|
+
queries per cycle: the existing `gh issue list --assignee <user>` plus
|
|
279
|
+
one `gh issue list --label <L>` call per configured label. Results merge
|
|
280
|
+
by issue number. This keeps the label path scale-safe on busy repos
|
|
281
|
+
where an unfiltered fetch would silently cap at gh's `--limit` and miss
|
|
282
|
+
labeled issues on later pages. Repos without `include_labels` run only
|
|
283
|
+
the cheap `--assignee` query, so enabling the feature on one repo does
|
|
284
|
+
not add API calls on the others.
|
|
285
|
+
- The event log entry for a label-triggered acceptance is
|
|
286
|
+
`poll.issue.included_by_label`, alongside the existing
|
|
287
|
+
`poll.issue.excluded_by_label` for exclusions.
|
|
288
|
+
- **Interaction with `task_labels`**: `include_labels` opts an issue
|
|
289
|
+
into the poller's consideration set. Once surfaced, the usual
|
|
290
|
+
routing still applies — if the same issue also carries a
|
|
291
|
+
`task_labels` label, it runs through the **task** pipeline (report-
|
|
292
|
+
only, no PR), not the dev pipeline. If you want label-triggered
|
|
293
|
+
issues to always run dev, make sure `include_labels` and
|
|
294
|
+
`task_labels` are disjoint (e.g. label opt-ins with
|
|
295
|
+
`ctrlrelay:auto` and task runs with `task:<topic>`).
|
|
296
|
+
- **Upgrade path**: enabling `include_labels` on a repo that was
|
|
297
|
+
already running the poller does NOT retroactively re-evaluate
|
|
298
|
+
issues already in `poller_state.json`. Any foreign-assigned issue
|
|
299
|
+
that pre-dates the config change won't be picked up via a later
|
|
300
|
+
label addition. Only brand-new issues (after the config change) or
|
|
301
|
+
issues you re-open will go through the label trigger. If you need
|
|
302
|
+
to re-evaluate pre-existing issues on a specific repo, stop the
|
|
303
|
+
poller, remove that repo's entry from `poller_state.json` under
|
|
304
|
+
`seen_issues`, and restart. (A fully automatic migration would
|
|
305
|
+
risk re-running pipelines for issues the bot had already handled.)
|
|
306
|
+
|
|
307
|
+
Trust model: anyone with triage permission on a repo can apply a label. That
|
|
308
|
+
matches the trust model ctrlrelay already uses — the operator configures which
|
|
309
|
+
repos and which labels trigger the pipeline; a hostile collaborator with
|
|
310
|
+
triage access was already able to push branches and trigger CI, so allowing
|
|
311
|
+
them to opt an issue into the dev pipeline is a narrower extension, not a new
|
|
312
|
+
vector.
|
|
313
|
+
|
|
314
|
+
## schedules
|
|
315
|
+
|
|
316
|
+
In-process cron jobs run by the poller daemon. All expressions are
|
|
317
|
+
standard 5-field (minute hour dom month dow) and evaluate in the
|
|
318
|
+
top-level `timezone`. Malformed expressions fail at config load — they
|
|
319
|
+
won't silently disable the job at runtime.
|
|
320
|
+
|
|
321
|
+
```yaml
|
|
322
|
+
schedules:
|
|
323
|
+
secops_cron: "0 6 * * *" # daily 06:00 sweep across repos
|
|
324
|
+
personalization_cron: "*/15 * * * *" # optional auto-pull
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
| Key | Type | Required | Default | Description |
|
|
328
|
+
|---|---|---|---|---|
|
|
329
|
+
| `secops_cron` | string | no | `"0 6 * * *"` | When to run the secops sweep (`ctrlrelay secops run` equivalent) across all configured repos. Set to e.g. `"0 6 * * 1"` for weekly Mondays. |
|
|
330
|
+
| `personalization_cron` | string | no | unset | When to auto-pull the personalization repo on this machine. Only effective when [`personalization`](#personalization) is also set. Skip-on-dirty: never rebases under uncommitted operator edits. Adoption is never performed by auto-pull — that stays an init-time concern. |
|
|
331
|
+
|
|
332
|
+
## personalization
|
|
333
|
+
|
|
334
|
+
Optional. Configures cross-machine sync of operator state (Claude
|
|
335
|
+
config, per-project memory, spec/superpower outputs) through a
|
|
336
|
+
separate (typically private) GitHub repo. See [Personalization
|
|
337
|
+
sync]({{ '/personalization/' | relative_url }}) for the full
|
|
338
|
+
walkthrough; this section documents the schema.
|
|
339
|
+
|
|
340
|
+
```yaml
|
|
341
|
+
personalization:
|
|
342
|
+
repo: "your-handle/dotclaude"
|
|
343
|
+
# checkout_path: "~/.ctrlrelay/personalization" # default
|
|
344
|
+
# main_branch: "main" # default
|
|
345
|
+
# node_id: "studio-mac" # default: top-level node_id
|
|
346
|
+
paths:
|
|
347
|
+
- source: "global/CLAUDE.md"
|
|
348
|
+
target: "~/.claude/CLAUDE.md"
|
|
349
|
+
- source: "global/skills/"
|
|
350
|
+
target: "~/.claude/skills/"
|
|
351
|
+
- source: "claude-memory/${PROJECT}/"
|
|
352
|
+
target: "~/.claude/projects/${PROJECT_ENCODED}/memory/"
|
|
353
|
+
project_scoped: true
|
|
354
|
+
- source: "specs/${PROJECT}/"
|
|
355
|
+
target: "${PROJECT_PARENT}/specs/${PROJECT}/"
|
|
356
|
+
project_scoped: true
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
| Key | Type | Required | Default | Description |
|
|
360
|
+
|---|---|---|---|---|
|
|
361
|
+
| `repo` | string | **yes** | — | `<owner>/<repo>` slug for the personalization repo on github.com. |
|
|
362
|
+
| `checkout_path` | string | no | `~/.ctrlrelay/personalization` | Where ctrlrelay clones the repo on this machine. |
|
|
363
|
+
| `main_branch` | string | no | `"main"` | Long-lived integration branch. Per-machine branches rebase onto this. |
|
|
364
|
+
| `node_id` | string | no | top-level `node_id` | Identifies this machine's working branch (`personalization/<node_id>`). Override only if the top-level value isn't safe as a git branch component. |
|
|
365
|
+
| `paths` | list | **yes** | — | One entry per file/dir to sync. See [personalization.paths](#personalizationpaths). |
|
|
366
|
+
|
|
367
|
+
### personalization.paths
|
|
368
|
+
|
|
369
|
+
Each entry declares one source-to-target wiring. Trailing slashes
|
|
370
|
+
distinguish files from directories — `source: "global/CLAUDE.md"` is
|
|
371
|
+
a file, `source: "global/skills/"` is a directory; the target must
|
|
372
|
+
agree.
|
|
373
|
+
|
|
374
|
+
| Key | Type | Required | Description |
|
|
375
|
+
|---|---|---|---|
|
|
376
|
+
| `source` | string | **yes** | Path inside the personalization repo. Trailing slash means directory. |
|
|
377
|
+
| `target` | string | **yes** | On-disk path the symlink will be created at. Supports `${HOME}` and (when `project_scoped`) `${PROJECT}`, `${PROJECT_ENCODED}`, `${PROJECT_LOCAL}`, `${PROJECT_PARENT}`. |
|
|
378
|
+
| `project_scoped` | bool | no (default `false`) | When `true`, the entry expands once per repo in `repos:`, with the `${PROJECT_*}` placeholders resolved against that repo's `local_path`. |
|
|
379
|
+
|
|
380
|
+
The `${PROJECT}` slug uses **`<owner>--<repo>`** with a double
|
|
381
|
+
hyphen so `a-b/c` and `a/b-c` produce different slugs. `${PROJECT_ENCODED}`
|
|
382
|
+
matches Claude Code's own path encoding rule (`/Users/foo/Projects/bar`
|
|
383
|
+
→ `-Users-foo-Projects-bar`).
|
|
384
|
+
|
|
231
385
|
## Example: telegram-enabled config
|
|
232
386
|
|
|
233
387
|
```yaml
|
|
@@ -57,16 +57,20 @@ cp config/orchestrator.yaml.example config/orchestrator.yaml
|
|
|
57
57
|
|
|
58
58
|
Open `config/orchestrator.yaml` and edit at least:
|
|
59
59
|
|
|
60
|
-
- `node_id` — a label for this machine (free-form string).
|
|
61
60
|
- `timezone` — your local IANA timezone.
|
|
62
61
|
- `repos[].name` — the `owner/repo` slug of a repository you can push to.
|
|
63
62
|
- `repos[].local_path` — where the local clone lives (or will live) on disk.
|
|
63
|
+
*Or* set `paths.repo_root` to a workspace root and let the path be
|
|
64
|
+
derived as `${repo_root}/${owner}/${repo}` (recommended for >1 repo).
|
|
65
|
+
|
|
66
|
+
`node_id` is optional — when omitted it defaults to your machine's
|
|
67
|
+
hostname (`socket.gethostname()`). Set it explicitly only if the
|
|
68
|
+
hostname is meaningless (CI runners, containers).
|
|
64
69
|
|
|
65
70
|
A minimal working config:
|
|
66
71
|
|
|
67
72
|
```yaml
|
|
68
73
|
version: "1"
|
|
69
|
-
node_id: "my-laptop"
|
|
70
74
|
timezone: "America/New_York"
|
|
71
75
|
|
|
72
76
|
paths:
|
|
@@ -102,7 +106,7 @@ You should see something like:
|
|
|
102
106
|
|
|
103
107
|
```
|
|
104
108
|
✓ Config valid: config/orchestrator.yaml
|
|
105
|
-
Node ID:
|
|
109
|
+
Node ID: your-hostname.local
|
|
106
110
|
Timezone: America/New_York
|
|
107
111
|
Transport: file_mock
|
|
108
112
|
Repos: 1
|
|
@@ -166,3 +170,4 @@ To run the poller as a long-lived background service, see [Operations]({{ '/oper
|
|
|
166
170
|
- [Feedback loop]({{ '/feedback-loop/' | relative_url }}) — what `BLOCKED_NEEDS_INPUT` actually does end-to-end.
|
|
167
171
|
- [CLI reference]({{ '/cli/' | relative_url }}) — every subcommand and flag.
|
|
168
172
|
- [Operations]({{ '/operations/' | relative_url }}) — running ctrlrelay as a service.
|
|
173
|
+
- [Personalization sync]({{ '/personalization/' | relative_url }}) — sync your Claude config, per-project memory, and spec outputs across machines.
|
|
@@ -36,6 +36,9 @@ isolated git worktree, opens a PR, and asks you on Telegram when it gets stuck.
|
|
|
36
36
|
- **[Operations]({{ '/operations/' | relative_url }})** — running the bridge
|
|
37
37
|
and poller under launchd (macOS) or systemd (Linux), tailing logs, reading
|
|
38
38
|
the state DB.
|
|
39
|
+
- **[Personalization sync]({{ '/personalization/' | relative_url }})** —
|
|
40
|
+
cross-machine sync of Claude config, per-project memory, and spec
|
|
41
|
+
outputs through a private GitHub repo.
|
|
39
42
|
|
|
40
43
|
## Build on it
|
|
41
44
|
|