hyperloop 0.1.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.
hyperloop/pr.py ADDED
@@ -0,0 +1,212 @@
1
+ """PR manager — draft PR lifecycle: creation, labeling, gate polling, merge.
2
+
3
+ Uses the `gh` CLI for all GitHub operations. The interface is matched by
4
+ FakePRManager (tests/fakes/pr.py) for testing without a real GitHub repo.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import re
12
+ import subprocess
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class PRManager:
18
+ """Manages draft PR lifecycle: creation, labeling, gate polling, merge."""
19
+
20
+ def __init__(
21
+ self,
22
+ repo: str,
23
+ merge_strategy: str = "squash",
24
+ delete_branch: bool = True,
25
+ ) -> None:
26
+ self.repo = repo
27
+ self.merge_strategy = merge_strategy
28
+ self.delete_branch = delete_branch
29
+
30
+ def create_draft(self, task_id: str, branch: str, title: str, spec_ref: str) -> str:
31
+ """Create a draft PR. Returns PR URL. Adds spec/task labels."""
32
+ result = subprocess.run(
33
+ [
34
+ "gh",
35
+ "pr",
36
+ "create",
37
+ "--draft",
38
+ "--head",
39
+ branch,
40
+ "--title",
41
+ title,
42
+ "--body",
43
+ "",
44
+ "--repo",
45
+ self.repo,
46
+ ],
47
+ capture_output=True,
48
+ text=True,
49
+ check=True,
50
+ )
51
+ pr_url = result.stdout.strip()
52
+
53
+ spec_name = _spec_name_from_ref(spec_ref)
54
+ subprocess.run(
55
+ [
56
+ "gh",
57
+ "pr",
58
+ "edit",
59
+ pr_url,
60
+ "--add-label",
61
+ f"task/{task_id}",
62
+ "--add-label",
63
+ f"spec/{spec_name}",
64
+ "--repo",
65
+ self.repo,
66
+ ],
67
+ capture_output=True,
68
+ text=True,
69
+ check=True,
70
+ )
71
+
72
+ logger.info("Created draft PR %s for task %s", pr_url, task_id)
73
+ return pr_url
74
+
75
+ def check_gate(self, pr_url: str, gate: str) -> bool:
76
+ """Check if a gate signal is present. v1: checks for 'lgtm' label.
77
+
78
+ Returns True if gate is cleared. Removes the label to prevent re-triggering.
79
+ """
80
+ result = subprocess.run(
81
+ [
82
+ "gh",
83
+ "pr",
84
+ "view",
85
+ pr_url,
86
+ "--json",
87
+ "labels",
88
+ "--repo",
89
+ self.repo,
90
+ ],
91
+ capture_output=True,
92
+ text=True,
93
+ check=True,
94
+ )
95
+ data = json.loads(result.stdout)
96
+ label_names = {label["name"] for label in data.get("labels", [])}
97
+
98
+ if "lgtm" in label_names:
99
+ subprocess.run(
100
+ [
101
+ "gh",
102
+ "pr",
103
+ "edit",
104
+ pr_url,
105
+ "--remove-label",
106
+ "lgtm",
107
+ "--repo",
108
+ self.repo,
109
+ ],
110
+ capture_output=True,
111
+ text=True,
112
+ check=True,
113
+ )
114
+ logger.info("Gate '%s' cleared for PR %s (lgtm label found and removed)", gate, pr_url)
115
+ return True
116
+
117
+ return False
118
+
119
+ def mark_ready(self, pr_url: str) -> None:
120
+ """Mark a draft PR as ready for review."""
121
+ subprocess.run(
122
+ [
123
+ "gh",
124
+ "pr",
125
+ "ready",
126
+ pr_url,
127
+ "--repo",
128
+ self.repo,
129
+ ],
130
+ capture_output=True,
131
+ text=True,
132
+ check=True,
133
+ )
134
+ logger.info("Marked PR %s as ready for review", pr_url)
135
+
136
+ def merge(self, pr_url: str, task_id: str, spec_ref: str) -> bool:
137
+ """Squash-merge a PR, preserving trailers. Returns True on success.
138
+
139
+ If merge conflict, returns False (caller handles NEEDS_REBASE).
140
+ """
141
+ delete_flag = "--delete-branch" if self.delete_branch else ""
142
+ body = f"Spec-Ref: {spec_ref}\nTask-Ref: {task_id}"
143
+
144
+ cmd = [
145
+ "gh",
146
+ "pr",
147
+ "merge",
148
+ pr_url,
149
+ f"--{self.merge_strategy}",
150
+ "--body",
151
+ body,
152
+ "--repo",
153
+ self.repo,
154
+ ]
155
+ if delete_flag:
156
+ cmd.append(delete_flag)
157
+
158
+ result = subprocess.run(
159
+ cmd,
160
+ capture_output=True,
161
+ text=True,
162
+ )
163
+
164
+ if result.returncode != 0:
165
+ logger.warning("Merge failed for PR %s: %s", pr_url, result.stderr.strip())
166
+ return False
167
+
168
+ logger.info("Merged PR %s for task %s", pr_url, task_id)
169
+ return True
170
+
171
+ def rebase_branch(self, branch: str, base_branch: str) -> bool:
172
+ """Rebase a branch onto base. Returns True if clean, False if conflicts."""
173
+ # Checkout the branch
174
+ checkout = subprocess.run(
175
+ ["git", "checkout", branch],
176
+ capture_output=True,
177
+ text=True,
178
+ )
179
+ if checkout.returncode != 0:
180
+ logger.warning("Failed to checkout branch %s: %s", branch, checkout.stderr.strip())
181
+ return False
182
+
183
+ # Attempt rebase
184
+ rebase = subprocess.run(
185
+ ["git", "rebase", base_branch],
186
+ capture_output=True,
187
+ text=True,
188
+ )
189
+ if rebase.returncode != 0:
190
+ # Abort the rebase on conflict
191
+ subprocess.run(
192
+ ["git", "rebase", "--abort"],
193
+ capture_output=True,
194
+ text=True,
195
+ )
196
+ logger.warning("Rebase conflict on branch %s onto %s", branch, base_branch)
197
+ return False
198
+
199
+ logger.info("Rebased branch %s onto %s", branch, base_branch)
200
+ return True
201
+
202
+
203
+ def _spec_name_from_ref(spec_ref: str) -> str:
204
+ """Derive a spec label name from a spec_ref path.
205
+
206
+ 'specs/persistence.md' -> 'persistence'
207
+ 'specs/sub/feature.md' -> 'sub/feature'
208
+ """
209
+ name = spec_ref
210
+ name = re.sub(r"^specs/", "", name)
211
+ name = re.sub(r"\.md$", "", name)
212
+ return name
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperloop
3
+ Version: 0.1.0
4
+ Summary: Orchestrator that walks tasks through composable process pipelines using AI agents
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: pyyaml>=6.0.3
7
+ Requires-Dist: rich>=15.0.0
8
+ Requires-Dist: typer>=0.24.1
9
+ Provides-Extra: dev
10
+ Requires-Dist: pre-commit>=4.0; extra == 'dev'
11
+ Requires-Dist: pyright>=1.1; extra == 'dev'
12
+ Requires-Dist: pytest>=8.0; extra == 'dev'
13
+ Requires-Dist: ruff>=0.8; extra == 'dev'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # hyperloop
17
+
18
+ Walks tasks through composable process pipelines using AI agents. You write specs, it creates tasks, implements them, verifies the work, and merges PRs.
19
+
20
+ ## Prerequisites
21
+
22
+ - Python 3.12+
23
+ - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
24
+ - `gh` CLI (authenticated, for PR management)
25
+ - `git`
26
+
27
+ ## Install
28
+
29
+ From PyPI (once published):
30
+
31
+ ```bash
32
+ pip install hyperloop
33
+ ```
34
+
35
+ From source (for development or testing):
36
+
37
+ ```bash
38
+ git clone git@github.com:jsell-rh/hyperloop.git
39
+ cd hyperloop
40
+ uv sync --all-extras
41
+ ```
42
+
43
+ ## Quickstart
44
+
45
+ 1. Create a repo with a spec:
46
+
47
+ ```bash
48
+ mkdir -p my-project/specs
49
+ cd my-project
50
+ git init && git commit --allow-empty -m "init"
51
+ ```
52
+
53
+ 2. Write a spec. This is what you want built. Be specific about acceptance criteria:
54
+
55
+ ```markdown
56
+ <!-- specs/auth.md -->
57
+ # User Authentication
58
+
59
+ Implement JWT-based authentication for the API.
60
+
61
+ ## Acceptance Criteria
62
+
63
+ - POST /auth/login accepts email + password, returns JWT
64
+ - POST /auth/register creates a new user account
65
+ - GET /auth/me returns the current user (requires valid JWT)
66
+ - Passwords are hashed with bcrypt, never stored in plaintext
67
+ - JWTs expire after 24 hours
68
+ ```
69
+
70
+ 3. Run:
71
+
72
+ ```bash
73
+ # From source:
74
+ uv run hyperloop run --repo owner/repo --branch main
75
+
76
+ # Or if installed:
77
+ hyperloop run --repo owner/repo --branch main
78
+ ```
79
+
80
+ 4. See what it would do without executing:
81
+
82
+ ```bash
83
+ hyperloop run --repo owner/repo --dry-run
84
+ ```
85
+
86
+ The orchestrator reads your specs, has the PM create tasks in `specs/tasks/`, then walks each task through the default pipeline: implement, verify, merge.
87
+
88
+ ## Configuration
89
+
90
+ Create `.hyperloop.yaml` in your repo root:
91
+
92
+ ```yaml
93
+ target:
94
+ base_branch: main
95
+
96
+ runtime:
97
+ max_workers: 4
98
+
99
+ merge:
100
+ auto_merge: true
101
+ strategy: squash
102
+ ```
103
+
104
+ Then just run from the repo directory:
105
+
106
+ ```bash
107
+ hyperloop run
108
+ ```
109
+
110
+ The repo is inferred from your git remote. All settings have sensible defaults.
111
+
112
+ ## Customizing Agent Behavior
113
+
114
+ Hyperloop ships with base agent definitions (implementer, verifier, etc.) that work out of the box. To customize them for your project, overlay with patches.
115
+
116
+ ### In-repo overlay
117
+
118
+ For single-repo projects. Agent patches live in the repo itself:
119
+
120
+ ```yaml
121
+ # .hyperloop.yaml
122
+ overlay: .hyperloop/agents/
123
+ ```
124
+
125
+ ```
126
+ your-repo/
127
+ ├── .hyperloop.yaml
128
+ ├── .hyperloop/
129
+ │ └── agents/
130
+ │ ├── implementer-patch.yaml
131
+ │ └── process-patch.yaml
132
+ └── specs/
133
+ ```
134
+
135
+ An implementer patch injects your project's persona:
136
+
137
+ ```yaml
138
+ # .hyperloop/agents/implementer-patch.yaml
139
+ kind: Agent
140
+ name: implementer
141
+ annotations:
142
+ ambient.io/persona: |
143
+ You work on a Go API service.
144
+ Build: make build. Test: make test. Lint: make lint.
145
+ Follow Clean Architecture. Use dependency injection.
146
+ ```
147
+
148
+ ### Shared overlay via gitops repo
149
+
150
+ For teams with multiple repos sharing agent definitions. The overlay lives in a central gitops repo and references the hyperloop base as a kustomize remote resource:
151
+
152
+ ```yaml
153
+ # .hyperloop.yaml
154
+ overlay: git@github.com:your-org/agent-gitops//overlays/api
155
+ ```
156
+
157
+ ```yaml
158
+ # your-org/agent-gitops/overlays/api/kustomization.yaml
159
+ resources:
160
+ - github.com/org/hyperloop//base?ref=v1.0.0
161
+
162
+ patches:
163
+ - path: implementer-patch.yaml
164
+ target:
165
+ kind: Agent
166
+ name: implementer
167
+ - path: process-patch.yaml
168
+ target:
169
+ kind: Process
170
+ name: default
171
+ ```
172
+
173
+ This pins the base version and lets you upgrade across all repos by bumping the ref.
174
+
175
+ ## Custom Processes
176
+
177
+ The default pipeline is: implement, verify, merge. Override it by patching the process:
178
+
179
+ ```yaml
180
+ # process-patch.yaml
181
+ kind: Process
182
+ name: default
183
+
184
+ pipeline:
185
+ - loop:
186
+ - loop:
187
+ - role: implementer
188
+ - role: verifier
189
+ - role: security-reviewer
190
+ - gate: human-pr-approval
191
+ - action: merge-pr
192
+ ```
193
+
194
+ Four primitives:
195
+
196
+ | Primitive | What it does |
197
+ |---|---|
198
+ | `role: X` | Spawn an agent. Fail restarts the enclosing loop. |
199
+ | `gate: X` | Block until external signal (v1: `lgtm` label on PR). |
200
+ | `loop` | Wrap steps. Retry from top on failure. |
201
+ | `action: X` | Terminal operation (`merge-pr`, `mark-pr-ready`). |
202
+
203
+ Loops nest. Inner loops retry independently of outer loops.
204
+
205
+ ## What it creates in your repo
206
+
207
+ The orchestrator writes to `specs/` in your repo:
208
+
209
+ ```
210
+ specs/
211
+ ├── tasks/ # task files with status, findings, spec references
212
+ ├── reviews/ # review artifacts from verifier (on branches)
213
+ └── prompts/ # process improvements (learned over time)
214
+ ```
215
+
216
+ All task state is tracked in git. Every commit includes `Spec-Ref` and `Task-Ref` trailers for traceability. PRs are created as drafts and labeled by spec and task.
217
+
218
+ ## Configuration Reference
219
+
220
+ ```yaml
221
+ # .hyperloop.yaml
222
+
223
+ overlay: .hyperloop/agents/ # local path or git URL to kustomization dir
224
+
225
+ target:
226
+ repo: owner/repo # GitHub repo (default: inferred from git remote)
227
+ base_branch: main # trunk branch
228
+ specs_dir: specs # where specs live
229
+
230
+ runtime:
231
+ default: local # local (v1) | ambient (planned)
232
+ max_workers: 6 # max parallel task workers
233
+
234
+ merge:
235
+ auto_merge: true # squash-merge on review pass
236
+ strategy: squash # squash | merge
237
+ delete_branch: true # delete worker branch after merge
238
+
239
+ poll_interval: 30 # seconds between orchestrator cycles
240
+ max_rounds: 50 # max retry rounds per task before failure
241
+ max_rebase_attempts: 3 # max rebase retries before full loop retry
242
+ ```
243
+
244
+ ## Development
245
+
246
+ ```bash
247
+ uv sync --all-extras
248
+ uv run pytest # run tests (280 tests)
249
+ uv run ruff check . # lint
250
+ uv run ruff format --check . # format check
251
+ uv run pyright # type check
252
+ uv run hyperloop --help # CLI help
253
+ ```
@@ -0,0 +1,23 @@
1
+ hyperloop/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ hyperloop/__main__.py,sha256=BSNHOzDGZgtLbVpUCzfZF85qaTEnASWqVun0CejFvaA,85
3
+ hyperloop/cli.py,sha256=GXBxZFJb8q3kkh0oR_7yCtmcrmiII0wcedwWDrCDvrs,5076
4
+ hyperloop/compose.py,sha256=rK1uWMzLVPlOwfqmmpJsZddFeAsw6eCiqWpPZlIdg0Q,4482
5
+ hyperloop/config.py,sha256=s9v1CbGvfby2C9Hd_rtVDYj3LfmHtfJUnFMGue0KNeE,5303
6
+ hyperloop/loop.py,sha256=bH8J1SbVbfSDe_4RPSRmXyYcciazdB_z4G5yvMXhmZ8,19937
7
+ hyperloop/pr.py,sha256=2Q5cnpnLLOdn41ba_zkmCuFdJEhuUABYPX6yGlWJQIM,6071
8
+ hyperloop/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ hyperloop/adapters/git_state.py,sha256=8Da6lSMLwf3qP1Rn9WakAJ-88YQc8no36aoZbrfLCpQ,9003
10
+ hyperloop/adapters/local.py,sha256=I-YCR76pa_bY5seac-29QRJWSI5DKhL0psNgrcIQfps,9262
11
+ hyperloop/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ hyperloop/domain/decide.py,sha256=c9RbT1zYw3_neeswM1-fQPvIC_mEbQ98SvlaAXekShs,3996
13
+ hyperloop/domain/deps.py,sha256=wMgGWpxxqmGdrobtVCRf32GMTs5DBaGIY0Yzx9rz9bg,2039
14
+ hyperloop/domain/model.py,sha256=4QBJY-U2CCIUfNoIl6zhoDQfKzZg21yyVuXyE0O4cik,4926
15
+ hyperloop/domain/pipeline.py,sha256=sIR9_gaNf-XHoYkrJYkuTO5sZ92UMmfvTJH8CSCzq0M,10712
16
+ hyperloop/ports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ hyperloop/ports/pr.py,sha256=giSAGDk-vl4ycMJUdewSRk2ASu80_r_t9wJFxo1gGMA,1066
18
+ hyperloop/ports/runtime.py,sha256=KeZ4BZozRpv_5MrqEjlYd47AgEzyQ81f79GaTIEDzk0,1229
19
+ hyperloop/ports/state.py,sha256=WT_JSaYo8t5nlgqwd64I5JB214iRm0pI25wU7jaw4OE,1814
20
+ hyperloop-0.1.0.dist-info/METADATA,sha256=XlcOvCQZEKm_GOWw2HIX5QS0-vkbRorVxKSHFQZ10jc,6327
21
+ hyperloop-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
22
+ hyperloop-0.1.0.dist-info/entry_points.txt,sha256=bP63nmW_9q_pSa8GOaP1svkdkVfX_MWd-QuKSYsPis8,48
23
+ hyperloop-0.1.0.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
+ hyperloop = hyperloop.cli:app