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.
Files changed (112) hide show
  1. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/pages.yml +1 -1
  2. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.gitignore +3 -0
  3. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/CHANGELOG.md +98 -0
  4. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/PKG-INFO +7 -4
  5. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/README.md +6 -3
  6. ctrlrelay-0.3.0/config/orchestrator.yaml.example +141 -0
  7. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/architecture.md +1 -1
  8. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/cli.md +79 -0
  9. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/configuration.md +156 -2
  10. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/development.md +1 -1
  11. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/getting-started.md +8 -3
  12. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/index.md +3 -0
  13. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/operations.md +56 -34
  14. ctrlrelay-0.3.0/docs/personalization.md +343 -0
  15. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/pyproject.toml +1 -1
  16. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/cli.py +353 -1
  17. ctrlrelay-0.3.0/src/ctrlrelay/core/config.py +712 -0
  18. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/github.py +59 -5
  19. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/poller.py +364 -12
  20. ctrlrelay-0.3.0/src/ctrlrelay/install.py +192 -0
  21. ctrlrelay-0.3.0/src/ctrlrelay/personalization/__init__.py +26 -0
  22. ctrlrelay-0.3.0/src/ctrlrelay/personalization/manager.py +1176 -0
  23. ctrlrelay-0.3.0/src/ctrlrelay/personalization/paths.py +143 -0
  24. ctrlrelay-0.3.0/src/ctrlrelay/templates/__init__.py +0 -0
  25. ctrlrelay-0.3.0/src/ctrlrelay/templates/global-CLAUDE.md.snippet +61 -0
  26. ctrlrelay-0.3.0/src/ctrlrelay/templates/launchd/__init__.py +0 -0
  27. ctrlrelay-0.3.0/src/ctrlrelay/templates/launchd/bridge.plist.template +37 -0
  28. ctrlrelay-0.3.0/src/ctrlrelay/templates/launchd/poller.plist.template +44 -0
  29. ctrlrelay-0.3.0/src/ctrlrelay/templates/systemd/__init__.py +0 -0
  30. ctrlrelay-0.3.0/src/ctrlrelay/templates/systemd/bridge.service.template +20 -0
  31. ctrlrelay-0.3.0/src/ctrlrelay/templates/systemd/poller.service.template +25 -0
  32. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_config.py +210 -0
  33. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dev_integration.py +249 -1
  34. ctrlrelay-0.3.0/tests/test_install.py +294 -0
  35. ctrlrelay-0.3.0/tests/test_personalization.py +1981 -0
  36. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_poller.py +709 -1
  37. ctrlrelay-0.2.1/config/orchestrator.yaml.example +0 -60
  38. ctrlrelay-0.2.1/src/ctrlrelay/core/config.py +0 -377
  39. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  40. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  41. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  42. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/dependabot.yml +0 -0
  43. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/build.yml +0 -0
  44. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/cla.yml +0 -0
  45. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/publish.yml +0 -0
  46. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/.github/workflows/test.yml +0 -0
  47. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  48. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/CONTRIBUTING.md +0 -0
  49. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/LICENSE +0 -0
  50. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/SECURITY.md +0 -0
  51. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/Gemfile +0 -0
  52. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/_config.yml +0 -0
  53. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/bridge.md +0 -0
  54. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/docs/feedback-loop.md +0 -0
  55. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/__init__.py +0 -0
  56. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/__init__.py +0 -0
  57. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/__main__.py +0 -0
  58. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/protocol.py +0 -0
  59. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/server.py +0 -0
  60. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/bridge/telegram_handler.py +0 -0
  61. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/__init__.py +0 -0
  62. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/audit.py +0 -0
  63. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/checkpoint.py +0 -0
  64. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/dispatcher.py +0 -0
  65. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/obs.py +0 -0
  66. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/pr_verifier.py +0 -0
  67. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/pr_watcher.py +0 -0
  68. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/scheduler.py +0 -0
  69. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/state.py +0 -0
  70. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/core/worktree.py +0 -0
  71. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/dashboard/__init__.py +0 -0
  72. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/dashboard/client.py +0 -0
  73. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/__init__.py +0 -0
  74. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/base.py +0 -0
  75. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/dev.py +0 -0
  76. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/post_merge.py +0 -0
  77. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/secops.py +0 -0
  78. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/pipelines/task.py +0 -0
  79. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/__init__.py +0 -0
  80. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/base.py +0 -0
  81. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/file_mock.py +0 -0
  82. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/src/ctrlrelay/transports/socket_client.py +0 -0
  83. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/__init__.py +0 -0
  84. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/conftest.py +0 -0
  85. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_audit.py +0 -0
  86. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_bridge_protocol.py +0 -0
  87. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_bridge_server.py +0 -0
  88. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_checkpoint.py +0 -0
  89. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_ci_wait.py +0 -0
  90. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_dev.py +0 -0
  91. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_repos.py +0 -0
  92. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_secops.py +0 -0
  93. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_start.py +0 -0
  94. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_cli_version.py +0 -0
  95. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dashboard_client.py +0 -0
  96. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dev_pipeline.py +0 -0
  97. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_dispatcher.py +0 -0
  98. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_docs_site.py +0 -0
  99. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_github.py +0 -0
  100. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_obs.py +0 -0
  101. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_pipeline_base.py +0 -0
  102. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_post_merge.py +0 -0
  103. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_pr_verifier.py +0 -0
  104. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_pr_watcher.py +0 -0
  105. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_scheduler.py +0 -0
  106. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_secops_integration.py +0 -0
  107. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_secops_pipeline.py +0 -0
  108. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_state.py +0 -0
  109. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_task_pipeline.py +0 -0
  110. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_telegram_handler.py +0 -0
  111. {ctrlrelay-0.2.1 → ctrlrelay-0.3.0}/tests/test_transport.py +0 -0
  112. {ctrlrelay-0.2.1 → ctrlrelay-0.3.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,6 @@ 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/
@@ -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.2.1
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 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
  ---
@@ -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 | **yes** | | Free-form identifier for this machine. Surfaces in dashboard heartbeats and session logs. |
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 | **yes** | | Where the repo is checked out on disk for human use. ctrlrelay itself uses bare mirrors under `paths.bare_repos`. |
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
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Development
3
3
  layout: default
4
- nav_order: 9
4
+ nav_order: 10
5
5
  description: "Local development setup, running tests, linting, and contributing."
6
6
  permalink: /development/
7
7
  ---
@@ -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: my-laptop
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