ctrlrelay 0.10.0__py3-none-any.whl

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 (53) hide show
  1. ctrlrelay/__init__.py +33 -0
  2. ctrlrelay/bridge/__init__.py +21 -0
  3. ctrlrelay/bridge/__main__.py +89 -0
  4. ctrlrelay/bridge/protocol.py +75 -0
  5. ctrlrelay/bridge/server.py +740 -0
  6. ctrlrelay/bridge/telegram_handler.py +136 -0
  7. ctrlrelay/cli.py +3138 -0
  8. ctrlrelay/core/__init__.py +54 -0
  9. ctrlrelay/core/audit.py +257 -0
  10. ctrlrelay/core/checkpoint.py +155 -0
  11. ctrlrelay/core/config.py +778 -0
  12. ctrlrelay/core/dispatcher.py +372 -0
  13. ctrlrelay/core/github.py +432 -0
  14. ctrlrelay/core/network.py +277 -0
  15. ctrlrelay/core/obs.py +118 -0
  16. ctrlrelay/core/poller.py +920 -0
  17. ctrlrelay/core/pr_verifier.py +230 -0
  18. ctrlrelay/core/pr_watcher.py +121 -0
  19. ctrlrelay/core/question_expiry.py +193 -0
  20. ctrlrelay/core/scheduler.py +337 -0
  21. ctrlrelay/core/state.py +642 -0
  22. ctrlrelay/core/worktree.py +893 -0
  23. ctrlrelay/dashboard/__init__.py +5 -0
  24. ctrlrelay/dashboard/client.py +159 -0
  25. ctrlrelay/gh_protocol.py +47 -0
  26. ctrlrelay/install.py +203 -0
  27. ctrlrelay/personalization/__init__.py +26 -0
  28. ctrlrelay/personalization/manager.py +1183 -0
  29. ctrlrelay/personalization/paths.py +143 -0
  30. ctrlrelay/pipelines/__init__.py +15 -0
  31. ctrlrelay/pipelines/base.py +50 -0
  32. ctrlrelay/pipelines/dev.py +1367 -0
  33. ctrlrelay/pipelines/post_merge.py +411 -0
  34. ctrlrelay/pipelines/secops.py +1018 -0
  35. ctrlrelay/pipelines/task.py +713 -0
  36. ctrlrelay/setup.py +617 -0
  37. ctrlrelay/templates/__init__.py +0 -0
  38. ctrlrelay/templates/global-CLAUDE.md.snippet +61 -0
  39. ctrlrelay/templates/launchd/__init__.py +0 -0
  40. ctrlrelay/templates/launchd/bridge.plist.template +37 -0
  41. ctrlrelay/templates/launchd/poller.plist.template +44 -0
  42. ctrlrelay/templates/systemd/__init__.py +0 -0
  43. ctrlrelay/templates/systemd/bridge.service.template +20 -0
  44. ctrlrelay/templates/systemd/poller.service.template +25 -0
  45. ctrlrelay/transports/__init__.py +34 -0
  46. ctrlrelay/transports/base.py +74 -0
  47. ctrlrelay/transports/file_mock.py +103 -0
  48. ctrlrelay/transports/socket_client.py +298 -0
  49. ctrlrelay-0.10.0.dist-info/METADATA +254 -0
  50. ctrlrelay-0.10.0.dist-info/RECORD +53 -0
  51. ctrlrelay-0.10.0.dist-info/WHEEL +4 -0
  52. ctrlrelay-0.10.0.dist-info/entry_points.txt +2 -0
  53. ctrlrelay-0.10.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,143 @@
1
+ """Path encoding and template resolution for personalization sync.
2
+
3
+ Two responsibilities:
4
+
5
+ 1. ``encode_project_path`` — replicate Claude Code's directory-name
6
+ encoding for ``~/.claude/projects/<encoded>/``. Verified empirically
7
+ against ~/.claude/projects/ on a real install: every character that
8
+ is not ``[A-Za-z0-9-]`` is replaced by ``-``. No collapsing of runs;
9
+ ``/.ctrlrelay/`` becomes ``--ctrlrelay-`` (the slash + dot each
10
+ produce their own ``-``).
11
+
12
+ 2. ``resolve_template`` — substitute the placeholder set declared in
13
+ ``config.py`` (``${HOME}``, ``${PROJECT}``, ``${PROJECT_ENCODED}``,
14
+ ``${PROJECT_LOCAL}``, ``${PROJECT_PARENT}``) in source/target paths
15
+ from ``Personalization.paths``.
16
+
17
+ Kept dependency-free so the manager can use it during init/wire without
18
+ loading the rest of the package.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+ # Anything outside [A-Za-z0-9-] becomes '-'. Hyphens already in the
28
+ # original path stay as hyphens (idempotent under this rule).
29
+ _NON_SAFE_CHAR_RE = re.compile(r"[^A-Za-z0-9-]")
30
+
31
+
32
+ def encode_project_path(absolute_path: Path | str, *, resolve_symlinks: bool = False) -> str:
33
+ """Encode an absolute filesystem path the way Claude Code does for
34
+ its per-project directory under ``~/.claude/projects/``.
35
+
36
+ Args:
37
+ absolute_path: Absolute path or ``~``-prefixed path. Relative
38
+ paths are an error.
39
+ resolve_symlinks: When True, follow symlinks before encoding.
40
+ Default False — match the path as configured in
41
+ ``orchestrator.yaml`` (``RepoConfig.local_path``) which is
42
+ ``expanduser``'d but not resolved. Set True only if you
43
+ observe Claude using the resolved form on this machine.
44
+
45
+ Returns:
46
+ The encoded directory name (no leading slash; first character
47
+ is ``-`` because absolute paths start with ``/``).
48
+
49
+ Raises:
50
+ ValueError: If ``absolute_path`` is not absolute after
51
+ ``expanduser``.
52
+ """
53
+ p = Path(absolute_path).expanduser()
54
+ if not p.is_absolute():
55
+ raise ValueError(
56
+ f"encode_project_path requires an absolute path, got {absolute_path!r}"
57
+ )
58
+ if resolve_symlinks:
59
+ p = p.resolve()
60
+ return _NON_SAFE_CHAR_RE.sub("-", str(p))
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class TemplateContext:
65
+ """Inputs for ``resolve_template``.
66
+
67
+ For non-project-scoped sync entries leave both fields ``None``.
68
+ For project-scoped entries set both: ``project`` is the
69
+ ``<owner>-<repo>`` flat slug (avoids nested-owner-dir surprises and
70
+ name collisions across orgs); ``project_local`` is the
71
+ ``RepoConfig.local_path`` value (already ``expanduser``'d at config
72
+ load time).
73
+ """
74
+
75
+ project: str | None = None
76
+ project_local: Path | None = None
77
+
78
+
79
+ def resolve_template(template: str, ctx: TemplateContext) -> Path:
80
+ """Substitute placeholders in a source/target template and return a Path.
81
+
82
+ Recognized placeholders:
83
+ - ``${HOME}`` — current user's home directory.
84
+ - ``${PROJECT}`` — ``<owner>-<repo>`` flat slug.
85
+ - ``${PROJECT_ENCODED}`` — Claude's path encoding of
86
+ ``project_local`` (cf. ``encode_project_path``).
87
+ - ``${PROJECT_LOCAL}`` — absolute path to the project working tree.
88
+ - ``${PROJECT_PARENT}`` — parent directory of ``project_local``.
89
+
90
+ Project placeholders raise ``ValueError`` when the context lacks
91
+ project information; ``HOME`` always works. Trailing slashes on the
92
+ template are preserved on the returned ``Path`` only by virtue of
93
+ the caller — ``Path`` itself does not retain trailing slashes —
94
+ so callers that care about file-vs-directory distinction must
95
+ inspect the *template string* (e.g., ``template.endswith("/")``)
96
+ not the returned path.
97
+ """
98
+ result = template
99
+ if "${HOME}" in result:
100
+ result = result.replace("${HOME}", str(Path.home()))
101
+
102
+ needs_project = any(
103
+ marker in result
104
+ for marker in (
105
+ "${PROJECT}",
106
+ "${PROJECT_ENCODED}",
107
+ "${PROJECT_LOCAL}",
108
+ "${PROJECT_PARENT}",
109
+ )
110
+ )
111
+ if needs_project:
112
+ if ctx.project is None or ctx.project_local is None:
113
+ raise ValueError(
114
+ f"template {template!r} uses project placeholders but no "
115
+ "project context was provided"
116
+ )
117
+ # Replace longest matches first so ``${PROJECT_ENCODED}`` doesn't
118
+ # get partially-eaten by a ``${PROJECT}`` substitution.
119
+ result = result.replace("${PROJECT_ENCODED}", encode_project_path(ctx.project_local))
120
+ result = result.replace("${PROJECT_LOCAL}", str(ctx.project_local))
121
+ result = result.replace("${PROJECT_PARENT}", str(ctx.project_local.parent))
122
+ result = result.replace("${PROJECT}", ctx.project)
123
+
124
+ return Path(result).expanduser()
125
+
126
+
127
+ _PROJECT_SLUG_SEP = "--"
128
+
129
+
130
+ def project_slug(repo_name: str) -> str:
131
+ """Flatten an ``owner/repo`` string to ``owner--repo``.
132
+
133
+ Used both for the ``${PROJECT}`` placeholder value and for the
134
+ on-disk source-tree directory name inside the personalization
135
+ repo (e.g. ``claude-memory/AInvirion--ctrlrelay/``). Avoids
136
+ nested owner subdirectories and, importantly, avoids collisions
137
+ where ``a-b/c`` and ``a/b-c`` would both flatten to ``a-b-c``
138
+ under a single-hyphen scheme. Double-hyphen is collision-free
139
+ for valid GitHub owner/repo pairs because GitHub disallows
140
+ consecutive hyphens in owner names (Codex review pass 6 caught
141
+ this).
142
+ """
143
+ return repo_name.replace("/", _PROJECT_SLUG_SEP)
@@ -0,0 +1,15 @@
1
+ """Pipeline implementations for ctrlrelay."""
2
+
3
+ from ctrlrelay.pipelines.base import Pipeline, PipelineContext, PipelineResult
4
+ from ctrlrelay.pipelines.dev import DevPipeline, run_dev_issue
5
+ from ctrlrelay.pipelines.secops import SecopsPipeline, run_secops_all
6
+
7
+ __all__ = [
8
+ "Pipeline",
9
+ "PipelineContext",
10
+ "PipelineResult",
11
+ "SecopsPipeline",
12
+ "run_secops_all",
13
+ "DevPipeline",
14
+ "run_dev_issue",
15
+ ]
@@ -0,0 +1,50 @@
1
+ """Base protocol and types for pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Protocol, runtime_checkable
8
+
9
+
10
+ @dataclass
11
+ class PipelineContext:
12
+ """Context for a pipeline execution."""
13
+
14
+ session_id: str
15
+ repo: str
16
+ worktree_path: Path
17
+ context_path: Path
18
+ state_file: Path
19
+ issue_number: int | None = None
20
+ extra: dict[str, Any] = field(default_factory=dict)
21
+
22
+
23
+ @dataclass
24
+ class PipelineResult:
25
+ """Result of a pipeline execution."""
26
+
27
+ success: bool
28
+ session_id: str
29
+ summary: str
30
+ blocked: bool = False
31
+ question: str | None = None
32
+ error: str | None = None
33
+ outputs: dict[str, Any] = field(default_factory=dict)
34
+
35
+
36
+ @runtime_checkable
37
+ class Pipeline(Protocol):
38
+ """Protocol for pipeline implementations."""
39
+
40
+ name: str
41
+
42
+ async def run(self, ctx: PipelineContext) -> PipelineResult:
43
+ """Execute the pipeline."""
44
+ ...
45
+
46
+ async def resume(
47
+ self, ctx: PipelineContext, answer: str
48
+ ) -> PipelineResult:
49
+ """Resume a blocked pipeline with user answer."""
50
+ ...