ctrlrelay 0.1.5__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.
@@ -0,0 +1,47 @@
1
+ """Transport protocol definition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol, runtime_checkable
6
+
7
+
8
+ class TransportError(Exception):
9
+ """Raised when transport operations fail."""
10
+
11
+
12
+ @runtime_checkable
13
+ class Transport(Protocol):
14
+ """Protocol for orchestrator-to-human communication.
15
+
16
+ Implementations accept optional correlation kwargs (``session_id``,
17
+ ``repo``, ``issue_number``) used solely for structured logging — they are
18
+ never part of the wire payload.
19
+ """
20
+
21
+ async def send(
22
+ self,
23
+ message: str,
24
+ *,
25
+ session_id: str | None = None,
26
+ repo: str | None = None,
27
+ issue_number: int | None = None,
28
+ ) -> None:
29
+ """Send a one-way message (no response expected)."""
30
+ ...
31
+
32
+ async def ask(
33
+ self,
34
+ question: str,
35
+ options: list[str] | None = None,
36
+ timeout: int = 300,
37
+ *,
38
+ session_id: str | None = None,
39
+ repo: str | None = None,
40
+ issue_number: int | None = None,
41
+ ) -> str:
42
+ """Ask a question and wait for response."""
43
+ ...
44
+
45
+ async def close(self) -> None:
46
+ """Close the transport connection."""
47
+ ...
@@ -0,0 +1,94 @@
1
+ """File-based mock transport for testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ from ctrlrelay.core.obs import get_logger, hash_text, log_event
11
+ from ctrlrelay.transports.base import TransportError
12
+
13
+ _logger = get_logger("transport.file_mock")
14
+
15
+
16
+ class FileMockTransport:
17
+ """Transport that reads/writes to files for testing."""
18
+
19
+ def __init__(self, inbox: Path, outbox: Path) -> None:
20
+ self.inbox = inbox
21
+ self.outbox = outbox
22
+
23
+ async def send(
24
+ self,
25
+ message: str,
26
+ *,
27
+ session_id: str | None = None,
28
+ repo: str | None = None,
29
+ issue_number: int | None = None,
30
+ ) -> None:
31
+ """Write message to outbox file."""
32
+ timestamp = datetime.now(timezone.utc).isoformat()
33
+ with self.outbox.open("a") as f:
34
+ f.write(f"[{timestamp}] {message}\n")
35
+
36
+ async def ask(
37
+ self,
38
+ question: str,
39
+ options: list[str] | None = None,
40
+ timeout: int = 300,
41
+ *,
42
+ session_id: str | None = None,
43
+ repo: str | None = None,
44
+ issue_number: int | None = None,
45
+ ) -> str:
46
+ """Write question to outbox and poll inbox for answer."""
47
+ timestamp = datetime.now(timezone.utc).isoformat()
48
+ opts = f" [{'/'.join(options)}]" if options else ""
49
+ with self.outbox.open("a") as f:
50
+ f.write(f"[{timestamp}] QUESTION: {question}{opts}\n")
51
+
52
+ log_event(
53
+ _logger,
54
+ "dev.question.posted",
55
+ session_id=session_id,
56
+ repo=repo,
57
+ issue_number=issue_number,
58
+ transport="file_mock",
59
+ destination=str(self.outbox),
60
+ question=question,
61
+ question_length=len(question),
62
+ question_hash=hash_text(question),
63
+ options=options,
64
+ )
65
+
66
+ sent_at = time.monotonic()
67
+ start = asyncio.get_event_loop().time()
68
+ while True:
69
+ content = self.inbox.read_text().strip()
70
+ if content:
71
+ self.inbox.write_text("")
72
+ answer = content.split("\n")[0].strip()
73
+ log_event(
74
+ _logger,
75
+ "dev.answer.received",
76
+ session_id=session_id,
77
+ repo=repo,
78
+ issue_number=issue_number,
79
+ transport="file_mock",
80
+ answer=answer,
81
+ answer_length=len(answer),
82
+ answer_hash=hash_text(answer),
83
+ elapsed_ms=int((time.monotonic() - sent_at) * 1000),
84
+ )
85
+ return answer
86
+
87
+ if asyncio.get_event_loop().time() - start > timeout:
88
+ raise TransportError(f"ask() timeout after {timeout}s")
89
+
90
+ await asyncio.sleep(0.5)
91
+
92
+ async def close(self) -> None:
93
+ """No-op for file transport."""
94
+ pass
@@ -0,0 +1,180 @@
1
+ """Unix socket transport client for bridge communication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ from ctrlrelay.bridge.protocol import (
11
+ BridgeMessage,
12
+ BridgeOp,
13
+ ProtocolError,
14
+ parse_message,
15
+ serialize_message,
16
+ )
17
+ from ctrlrelay.core.obs import get_logger, hash_text, log_event
18
+ from ctrlrelay.transports.base import TransportError
19
+
20
+ _logger = get_logger("transport.socket")
21
+
22
+
23
+ class SocketTransport:
24
+ """Transport that connects to bridge via Unix socket."""
25
+
26
+ def __init__(self, socket_path: Path) -> None:
27
+ self.socket_path = socket_path
28
+ self._reader: asyncio.StreamReader | None = None
29
+ self._writer: asyncio.StreamWriter | None = None
30
+ self._pending: dict[str, asyncio.Future[BridgeMessage]] = {}
31
+ self._receive_task: asyncio.Task | None = None
32
+
33
+ @property
34
+ def connected(self) -> bool:
35
+ return self._writer is not None and not self._writer.is_closing()
36
+
37
+ async def connect(self) -> None:
38
+ """Connect to bridge socket."""
39
+ try:
40
+ self._reader, self._writer = await asyncio.open_unix_connection(
41
+ str(self.socket_path)
42
+ )
43
+ self._receive_task = asyncio.create_task(self._receive_loop())
44
+ except (OSError, ConnectionRefusedError) as e:
45
+ raise TransportError(f"Failed to connect to bridge: {e}") from e
46
+
47
+ async def _receive_loop(self) -> None:
48
+ """Background task to receive messages from bridge."""
49
+ assert self._reader is not None
50
+ try:
51
+ while True:
52
+ line = await self._reader.readline()
53
+ if not line:
54
+ break
55
+ try:
56
+ msg = parse_message(line.decode())
57
+ if msg.request_id and msg.request_id in self._pending:
58
+ self._pending[msg.request_id].set_result(msg)
59
+ except ProtocolError:
60
+ pass
61
+ except asyncio.CancelledError:
62
+ pass
63
+
64
+ async def _send_message(self, msg: BridgeMessage) -> None:
65
+ """Send message to bridge."""
66
+ if not self.connected:
67
+ raise TransportError("Transport not connected")
68
+ assert self._writer is not None
69
+ data = serialize_message(msg).encode()
70
+ self._writer.write(data)
71
+ await self._writer.drain()
72
+
73
+ async def _send_and_wait(self, msg: BridgeMessage, timeout: int) -> BridgeMessage:
74
+ """Send message and wait for response."""
75
+ assert msg.request_id is not None
76
+ future: asyncio.Future[BridgeMessage] = asyncio.get_event_loop().create_future()
77
+ self._pending[msg.request_id] = future
78
+
79
+ try:
80
+ await self._send_message(msg)
81
+ return await asyncio.wait_for(future, timeout=timeout)
82
+ except asyncio.TimeoutError as e:
83
+ raise TransportError("Timeout waiting for response") from e
84
+ finally:
85
+ self._pending.pop(msg.request_id, None)
86
+
87
+ async def send(
88
+ self,
89
+ message: str,
90
+ *,
91
+ session_id: str | None = None,
92
+ repo: str | None = None,
93
+ issue_number: int | None = None,
94
+ ) -> None:
95
+ """Send a one-way message."""
96
+ request_id = f"r-{uuid.uuid4().hex[:8]}"
97
+ msg = BridgeMessage(
98
+ op=BridgeOp.SEND,
99
+ request_id=request_id,
100
+ text=message,
101
+ session_id=session_id,
102
+ repo=repo,
103
+ issue_number=issue_number,
104
+ )
105
+ await self._send_message(msg)
106
+
107
+ async def ask(
108
+ self,
109
+ question: str,
110
+ options: list[str] | None = None,
111
+ timeout: int = 300,
112
+ *,
113
+ session_id: str | None = None,
114
+ repo: str | None = None,
115
+ issue_number: int | None = None,
116
+ ) -> str:
117
+ """Ask a question and wait for response."""
118
+ request_id = f"r-{uuid.uuid4().hex[:8]}"
119
+ msg = BridgeMessage(
120
+ op=BridgeOp.ASK,
121
+ request_id=request_id,
122
+ question=question,
123
+ options=options,
124
+ timeout=timeout,
125
+ session_id=session_id,
126
+ repo=repo,
127
+ issue_number=issue_number,
128
+ )
129
+
130
+ log_event(
131
+ _logger,
132
+ "dev.question.posted",
133
+ session_id=session_id,
134
+ repo=repo,
135
+ issue_number=issue_number,
136
+ transport="socket",
137
+ destination=str(self.socket_path),
138
+ request_id=request_id,
139
+ question=question,
140
+ question_length=len(question),
141
+ question_hash=hash_text(question),
142
+ options=options,
143
+ )
144
+
145
+ sent_at = time.monotonic()
146
+ response = await self._send_and_wait(msg, timeout)
147
+
148
+ if response.op == BridgeOp.ERROR:
149
+ raise TransportError(f"Bridge error: {response.message}")
150
+ if response.op == BridgeOp.ANSWER and response.answer:
151
+ log_event(
152
+ _logger,
153
+ "dev.answer.received",
154
+ session_id=session_id,
155
+ repo=repo,
156
+ issue_number=issue_number,
157
+ transport="socket",
158
+ request_id=request_id,
159
+ answer=response.answer,
160
+ answer_length=len(response.answer),
161
+ answer_hash=hash_text(response.answer),
162
+ elapsed_ms=int((time.monotonic() - sent_at) * 1000),
163
+ )
164
+ return response.answer
165
+
166
+ raise TransportError(f"Unexpected response: {response.op}")
167
+
168
+ async def close(self) -> None:
169
+ """Close the connection."""
170
+ if self._receive_task:
171
+ self._receive_task.cancel()
172
+ try:
173
+ await self._receive_task
174
+ except asyncio.CancelledError:
175
+ pass
176
+ if self._writer:
177
+ self._writer.close()
178
+ await self._writer.wait_closed()
179
+ self._reader = None
180
+ self._writer = None
@@ -0,0 +1,251 @@
1
+ Metadata-Version: 2.4
2
+ Name: ctrlrelay
3
+ Version: 0.1.5
4
+ Summary: Local-first orchestrator for headless coding agents across multiple GitHub repos
5
+ Project-URL: Homepage, https://github.com/AInvirion/ctrlrelay
6
+ Project-URL: Documentation, https://ainvirion.github.io/ctrlrelay/
7
+ Project-URL: Repository, https://github.com/AInvirion/ctrlrelay
8
+ Project-URL: Issues, https://github.com/AInvirion/ctrlrelay/issues
9
+ Project-URL: Changelog, https://github.com/AInvirion/ctrlrelay/blob/main/CHANGELOG.md
10
+ Author-email: Oscar Valenzuela <oscar.valenzuela.b@gmail.com>
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ Keywords: automation,claude,github,orchestrator
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Classifier: Topic :: Software Development :: Version Control :: Git
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Requires-Python: >=3.12
27
+ Requires-Dist: apscheduler>=3.10.0
28
+ Requires-Dist: pydantic>=2.0.0
29
+ Requires-Dist: python-telegram-bot>=21.0
30
+ Requires-Dist: pyyaml>=6.0.0
31
+ Requires-Dist: rich>=13.0.0
32
+ Requires-Dist: typer>=0.12.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
35
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
36
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
37
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # ctrlrelay
41
+
42
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
43
+ [![Tests and lint](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml/badge.svg)](https://github.com/AInvirion/ctrlrelay/actions/workflows/test.yml)
44
+ [![Build](https://github.com/AInvirion/ctrlrelay/actions/workflows/build.yml/badge.svg)](https://github.com/AInvirion/ctrlrelay/actions/workflows/build.yml)
45
+ [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/ctrlrelay/)
46
+ [![GitHub Issues](https://img.shields.io/github/issues/AInvirion/ctrlrelay.svg)](https://github.com/AInvirion/ctrlrelay/issues)
47
+ [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/AInvirion/ctrlrelay.svg)](https://github.com/AInvirion/ctrlrelay/pulls)
48
+
49
+ > Local-first orchestrator for headless coding agents across your GitHub
50
+ > repos. Watches for assigned issues, runs a dev pipeline in an isolated
51
+ > git worktree, opens a PR, and asks you on Telegram when it gets stuck.
52
+
53
+ ## Table of Contents
54
+
55
+ - [About](#about)
56
+ - [Features](#features)
57
+ - [How it works](#how-it-works)
58
+ - [Getting started](#getting-started)
59
+ - [Prerequisites](#prerequisites)
60
+ - [Installation](#installation)
61
+ - [Quick start](#quick-start)
62
+ - [Documentation](#documentation)
63
+ - [Roadmap](#roadmap)
64
+ - [Contributing](#contributing)
65
+ - [Security](#security)
66
+ - [License](#license)
67
+
68
+ ## About
69
+
70
+ Headless coding agents are great interactively. Running them across half
71
+ a dozen repos without staring at a terminal is a different problem: who
72
+ schedules the runs, who watches for new work, who hands you the "I'm
73
+ blocked, what do you want?" question, who tracks the PR until it merges.
74
+
75
+ `ctrlrelay` is a small daemon that does all of that on your laptop. It
76
+ is local-first on purpose: no server, no queue, no multi-tenant anything.
77
+ Your agent credentials, your GitHub credentials, your repos, your
78
+ machine.
79
+
80
+ Today `ctrlrelay` ships with a Claude Code (`claude -p`) backend. The
81
+ orchestrator layer — worktrees, state DB, scheduler, Telegram bridge —
82
+ is agent-agnostic, and plug-in backends for other headless coding agents
83
+ are on the roadmap (see [Roadmap](#roadmap)).
84
+
85
+ ## Features
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.
90
+ - **Telegram bridge.** When a session hits a blocking question, the
91
+ bridge relays it to you as a DM and resumes the session once you
92
+ reply.
93
+ - **PR watcher.** Tracks the opened PR to merge and closes the loop
94
+ with a Telegram notification.
95
+ - **In-process scheduler** (APScheduler). Runs periodic jobs inside
96
+ the poller daemon. Ships with a `secops` job that reviews Dependabot
97
+ alerts and PRs daily at 6am; cron expressions follow standard Vixie
98
+ 5-field semantics (Sun=0, DOM-OR-DOW).
99
+ - **Checkpoint protocol.** The agent writes a structured state file
100
+ at the end of every session so the orchestrator knows whether it
101
+ succeeded, failed, or is blocked on input. Agent backends implement
102
+ this protocol to integrate.
103
+ - **Cross-platform supervision.** launchd (macOS) and systemd (Linux)
104
+ examples in `docs/operations.md`. One codebase, identical behavior.
105
+
106
+ ## How it works
107
+
108
+ ```
109
+ ┌───────────────┐
110
+ │ GitHub API │
111
+ └───────┬───────┘
112
+ │ (poll: issues assigned to me)
113
+ ┌───────▼───────┐ ┌──────────────┐
114
+ │ poller daemon │◄────────┤ APScheduler│
115
+ │ (launchd / │ │ (secops cron)│
116
+ │ systemd) │ └──────────────┘
117
+ └───┬───────┬───┘
118
+ new issue │ │ blocked session
119
+ │ │
120
+ ┌──────────▼──┐ ┌─▼───────────────────┐
121
+ │ dev pipeline│ │ Telegram bridge │
122
+ │ in worktree │ │ (socket ↔ bot API) │
123
+ │ agent CLI │ └──────────┬──────────┘
124
+ └──────┬──────┘ │
125
+ │ PR opened │ DM you
126
+ ▼ ▼
127
+ ┌────────────┐ ┌─────────┐
128
+ │ PR watcher │ │ You │
129
+ └────────────┘ └─────────┘
130
+ ```
131
+
132
+ Under the hood it's Python + `asyncio` + `sqlite` for state. The agent
133
+ is invoked as a subprocess (today: `claude -p`), and GitHub is accessed
134
+ via the `gh` CLI. No web server, no queue, no database dependency — just
135
+ a launchd/systemd-supervised daemon.
136
+
137
+ ## Getting started
138
+
139
+ ### Prerequisites
140
+
141
+ - **Python 3.12+**
142
+ - **git 2.20+**
143
+ - The **[`gh` CLI][gh-cli]**, authenticated (`gh auth login`) — used for
144
+ all GitHub API calls.
145
+ - A **headless coding agent backend.** Today that means the
146
+ **[`claude` CLI][claude-cli]**, authenticated (`claude auth login`).
147
+ Future backends will document their own setup.
148
+ - *(Optional, for the `secops` pipeline)* the **[`codex` CLI][codex-cli]**,
149
+ authenticated. The secops pipeline invokes `codex review` as an
150
+ independent reviewer for the agent's output; you can disable it by
151
+ setting `code_review.method: "none"` in your config if you prefer to
152
+ skip the review step.
153
+ - *(Optional)* a Telegram bot token if you want the bridge — see
154
+ [Telegram bridge docs][docs-bridge].
155
+
156
+ ### Installation
157
+
158
+ **From PyPI** (once published):
159
+
160
+ ```bash
161
+ pip install ctrlrelay
162
+ # or: uv pip install ctrlrelay
163
+ ```
164
+
165
+ **From source** (current path while in alpha):
166
+
167
+ ```bash
168
+ git clone https://github.com/AInvirion/ctrlrelay.git
169
+ cd ctrlrelay
170
+ uv pip install -e . # or: pip install -e .
171
+ ```
172
+
173
+ ### Quick start
174
+
175
+ ```bash
176
+ # Copy and edit the example config:
177
+ cp config/orchestrator.yaml.example config/orchestrator.yaml
178
+
179
+ # Validate it:
180
+ ctrlrelay config validate
181
+
182
+ # Run the dev pipeline against an issue you're assigned:
183
+ ctrlrelay run dev --issue 42 --repo your-org/your-repo
184
+
185
+ # Or start the poller to auto-process newly assigned issues + run the
186
+ # scheduled secops sweep daily at 6am:
187
+ ctrlrelay poller start # daemonizes; returns the terminal
188
+ ctrlrelay poller status # verify it's running
189
+ ```
190
+
191
+ Run as a supervised daemon (launchd on macOS / systemd on Linux) — see
192
+ [Operations][ops-docs].
193
+
194
+ ## Documentation
195
+
196
+ - [Getting started][docs-start]
197
+ - [Configuration][docs-config]
198
+ - [Telegram bridge][docs-bridge]
199
+ - [Feedback loop][docs-feedback]
200
+ - [CLI reference][docs-cli]
201
+ - [Operations (launchd / systemd / scheduled jobs)][ops-docs]
202
+ - [Architecture][docs-arch]
203
+ - [Development][docs-dev]
204
+
205
+ ## Roadmap
206
+
207
+ - **Multi-agent backend support.** The agent dispatcher is the seam we
208
+ intend to widen so `ctrlrelay` can drive alternative headless coding
209
+ agents (e.g. OpenAI Codex CLI, OpenCode, Hermes) alongside Claude
210
+ Code. Each backend will implement the same checkpoint protocol and
211
+ be selectable per repo via config.
212
+ - **Additional scheduled jobs.** The in-process scheduler already has
213
+ `secops`; follow-ups include a weekly activity summary and a stale-
214
+ session reaper.
215
+ - **Dashboard mode.** An optional, opt-in heartbeat push to a hosted
216
+ dashboard for operators running the daemon across many machines.
217
+
218
+ ## Contributing
219
+
220
+ We welcome contributions from the community! Please read our
221
+ [Contributing Guidelines](CONTRIBUTING.md) before submitting a pull
222
+ request, and abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
223
+
224
+ First-time contributors will be prompted by the CLA Assistant bot to
225
+ sign the Contributor Assignment Agreement in-PR — it's a one-time,
226
+ one-comment step.
227
+
228
+ ## Security
229
+
230
+ If you discover a security vulnerability, please follow our
231
+ [Security Policy](SECURITY.md). Please do not file public GitHub issues
232
+ for security reports — open a private advisory instead.
233
+
234
+ ## License
235
+
236
+ This project is licensed under the Apache License 2.0 — see the
237
+ [LICENSE](LICENSE) file for details.
238
+
239
+ Copyright (c) 2026 AInvirion LLC. All Rights Reserved.
240
+
241
+ [claude-cli]: https://docs.anthropic.com/claude/docs/claude-cli
242
+ [gh-cli]: https://cli.github.com/
243
+ [codex-cli]: https://github.com/openai/codex
244
+ [docs-start]: https://ainvirion.github.io/ctrlrelay/getting-started/
245
+ [docs-config]: https://ainvirion.github.io/ctrlrelay/configuration/
246
+ [docs-bridge]: https://ainvirion.github.io/ctrlrelay/bridge/
247
+ [docs-feedback]: https://ainvirion.github.io/ctrlrelay/feedback-loop/
248
+ [docs-cli]: https://ainvirion.github.io/ctrlrelay/cli/
249
+ [ops-docs]: https://ainvirion.github.io/ctrlrelay/operations/
250
+ [docs-arch]: https://ainvirion.github.io/ctrlrelay/architecture/
251
+ [docs-dev]: https://ainvirion.github.io/ctrlrelay/development/
@@ -0,0 +1,36 @@
1
+ ctrlrelay/__init__.py,sha256=fFGk3vk282vfddBtZCBXvCV9bc5cEWu3QMgXEfkq16c,175
2
+ ctrlrelay/cli.py,sha256=EWpqSiAfcfqcWVSqefDfph5uk301aSYUU3LTQFOgXmo,51108
3
+ ctrlrelay/bridge/__init__.py,sha256=_xSaxU_IGHMBmdl6h3VX5PRgsI6DcGu-9COdIF_6LX0,459
4
+ ctrlrelay/bridge/__main__.py,sha256=87DXn2Z6Wud6aA1-7NnlMolgZ99yKtWAjGJzl4kqR3w,1833
5
+ ctrlrelay/bridge/protocol.py,sha256=9zOZp6DjIgke4JJRxc3YVDR725mbHA5-S3rGaQ33A9s,1695
6
+ ctrlrelay/bridge/server.py,sha256=2-JCl-btyAxubPtkbHt2mJVbdNYa_CaKyMMMX6T1T_g,10211
7
+ ctrlrelay/bridge/telegram_handler.py,sha256=YgcTO8osz5_BpzLm2JU6rneVHjpo1eSRLUm6pzpU__k,4121
8
+ ctrlrelay/core/__init__.py,sha256=aTEJzxTqZnwuTcZotlx0HbzBOh3SqoRyeVa3r5Sa8wY,1193
9
+ ctrlrelay/core/audit.py,sha256=Sa0QCZFgJ9bd1uSZKDHfpy_6B63SxreWDlc3iYijn8o,7068
10
+ ctrlrelay/core/checkpoint.py,sha256=jN-UHq6pboe-LkUN20X-VCNnbuHtSnmkYjASVag87Cc,4512
11
+ ctrlrelay/core/config.py,sha256=6Lnuy99x-RLtTlEZXDy7AmPmU8G5K3404OyO1eMup0c,9228
12
+ ctrlrelay/core/dispatcher.py,sha256=ggDOArLnh7pfYFsSh85tvgJ0jKSMfDpysAiQFdZcm-E,6482
13
+ ctrlrelay/core/github.py,sha256=Nf0n1elhqCoJjjYCcknnW6oGxalq6yhih0j0A9-BDFA,8370
14
+ ctrlrelay/core/obs.py,sha256=HuqqFXe-ngxg68bMrvY4GpWTCucOjr7Qz0kpGCrpVmE,3361
15
+ ctrlrelay/core/poller.py,sha256=DWjHMKeMeyjKgJ_MrQ1K1GgNyDIFqzBAnnQJzz0kpZk,13037
16
+ ctrlrelay/core/pr_verifier.py,sha256=SlOMXCrwHfzmJCeisKbm4_q4P9FhdcOpRsWUEtsSOI8,7351
17
+ ctrlrelay/core/pr_watcher.py,sha256=dsVSZ-7wnLnIyH5-sRpBkz7ExSqZn7spSGJKb90N0vE,4482
18
+ ctrlrelay/core/scheduler.py,sha256=AoUPQ9u4YpWTD0FsROCpP65qtgu4mxiiItJtTmArgt4,13530
19
+ ctrlrelay/core/state.py,sha256=PNcp5iIKwnkQyKMO4mjj-cVys0qmABr93lJTzj1RT7k,4681
20
+ ctrlrelay/core/worktree.py,sha256=Ovn3aP4qVY7DT_4s199VHZhzGl0ZBTS1SKZYUDlL1nw,27495
21
+ ctrlrelay/dashboard/__init__.py,sha256=aJtDFoW-aW7kggFfC5DCydk9QFLB2GPjaKDfcN9upBM,193
22
+ ctrlrelay/dashboard/client.py,sha256=b7_jKy_hXTlhECvo-ZVnOwLs-6Pg73zzFLUO-3DzOmg,5015
23
+ ctrlrelay/pipelines/__init__.py,sha256=0SwQ9d_XPTODMiiJAd2fq9B5Ya0RvSZt_iQZjHAuFUY,419
24
+ ctrlrelay/pipelines/base.py,sha256=PJZCrA4M6sErGp5iud4K4nts3hmyU-B2ddoutXUmHU0,1144
25
+ ctrlrelay/pipelines/dev.py,sha256=5wGHZjYaXiu0OrL1G4bi75H0kI0cexHDYXNWQMGRmq0,20981
26
+ ctrlrelay/pipelines/post_merge.py,sha256=Ws97Tz-y7B0pX6JXq0pBmm2mX3q0-jJH0RZNQTwQ5f0,10917
27
+ ctrlrelay/pipelines/secops.py,sha256=wp2nLRg42LYETt7BvEqpEILjBEjqCQG9EV0FQ7Cn5tQ,14124
28
+ ctrlrelay/transports/__init__.py,sha256=W_1GeL8LzKbyK6qLOIHSc_aw2Iohj8nGlTCyxOWXz8Q,1020
29
+ ctrlrelay/transports/base.py,sha256=cjbh1UhBXjBEbnKwlS3zJXi2R3HOx0W00CH2y1tHOoM,1199
30
+ ctrlrelay/transports/file_mock.py,sha256=FME-k7DyszSto6IpjuPC5tsCA_bJiejQ0lSY_8yOyr4,2960
31
+ ctrlrelay/transports/socket_client.py,sha256=YvBaXJt-Pjkcj3ayHU8NFTyUap50sKdVUwqrGwt2G-s,5975
32
+ ctrlrelay-0.1.5.dist-info/METADATA,sha256=hRERHF5v14gfjUDlDBlVCUqMmpM9Fq7DFsMH3Lfr5Jc,10682
33
+ ctrlrelay-0.1.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
34
+ ctrlrelay-0.1.5.dist-info/entry_points.txt,sha256=OfNfO9p6j82rKledRFMzTntWWhgIbARE23OEd6UCDdU,48
35
+ ctrlrelay-0.1.5.dist-info/licenses/LICENSE,sha256=rUu0YtwfchA4Zv96xvsR0cGxmARpqaeJOWQae5a24oA,11339
36
+ ctrlrelay-0.1.5.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ctrlrelay = ctrlrelay.cli:app