hyperloop 0.1.0__tar.gz → 0.2.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.
- {hyperloop-0.1.0 → hyperloop-0.2.0}/.github/workflows/release.yaml +5 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/CHANGELOG.md +6 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/PKG-INFO +16 -15
- {hyperloop-0.1.0 → hyperloop-0.2.0}/README.md +15 -14
- {hyperloop-0.1.0 → hyperloop-0.2.0}/pyproject.toml +1 -1
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/cli.py +16 -8
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/loop.py +42 -26
- {hyperloop-0.1.0 → hyperloop-0.2.0}/.github/workflows/ci.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/.gitignore +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/.pre-commit-config.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/.python-version +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/CLAUDE.md +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/base/implementer.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/base/pm.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/base/process-improver.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/base/process.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/base/rebase-resolver.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/base/verifier.yaml +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/specs/spec.md +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/__init__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/__main__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/adapters/__init__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/adapters/git_state.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/adapters/local.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/compose.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/config.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/domain/__init__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/domain/decide.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/domain/deps.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/domain/model.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/domain/pipeline.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/ports/__init__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/ports/pr.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/ports/runtime.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/ports/state.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/src/hyperloop/pr.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/__init__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/fakes/__init__.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/fakes/pr.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/fakes/runtime.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/fakes/state.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_cli.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_compose.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_config.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_decide.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_deps.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_fakes.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_git_state.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_local_runtime.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_loop.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_model.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_pipeline.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_pr.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_smoke.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/tests/test_state_contract.py +0 -0
- {hyperloop-0.1.0 → hyperloop-0.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperloop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Orchestrator that walks tasks through composable process pipelines using AI agents
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: pyyaml>=6.0.3
|
|
@@ -15,14 +15,14 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
|
|
16
16
|
# hyperloop
|
|
17
17
|
|
|
18
|
-
Walks tasks through composable process pipelines using AI agents. You write specs, it creates tasks, implements them, verifies the work, and merges
|
|
18
|
+
Walks tasks through composable process pipelines using AI agents. You write specs, it creates tasks, implements them, verifies the work, and merges the results.
|
|
19
19
|
|
|
20
20
|
## Prerequisites
|
|
21
21
|
|
|
22
22
|
- Python 3.12+
|
|
23
23
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
|
|
24
|
-
- `gh` CLI (authenticated, for PR management)
|
|
25
24
|
- `git`
|
|
25
|
+
- `gh` CLI (optional, for GitHub PR operations)
|
|
26
26
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
@@ -67,23 +67,23 @@ Implement JWT-based authentication for the API.
|
|
|
67
67
|
- JWTs expire after 24 hours
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
3. Run:
|
|
70
|
+
3. Run against the local repo:
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
# From
|
|
74
|
-
uv run hyperloop run
|
|
73
|
+
# From inside the repo
|
|
74
|
+
uv run hyperloop run
|
|
75
75
|
|
|
76
|
-
# Or
|
|
77
|
-
hyperloop run --
|
|
78
|
-
```
|
|
76
|
+
# Or point to a repo elsewhere
|
|
77
|
+
uv run hyperloop run --path ~/code/my-project
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
# With GitHub PR support (draft PRs, lgtm gates, squash-merge)
|
|
80
|
+
uv run hyperloop run --repo owner/repo
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
# Dry run (show config, don't execute)
|
|
83
|
+
uv run hyperloop run --dry-run
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
When `--repo` is not set, completed work is merged locally into the base branch via `git merge`. When `--repo` is set, the orchestrator creates draft PRs, polls for `lgtm` labels on gates, and squash-merges via GitHub.
|
|
87
87
|
|
|
88
88
|
## Configuration
|
|
89
89
|
|
|
@@ -101,13 +101,14 @@ merge:
|
|
|
101
101
|
strategy: squash
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Then
|
|
104
|
+
Then run from the repo directory (or use `--path`):
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
107
|
hyperloop run
|
|
108
|
+
hyperloop run --path ~/code/my-project
|
|
108
109
|
```
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
All settings have sensible defaults. `--repo` is only needed for GitHub PR operations.
|
|
111
112
|
|
|
112
113
|
## Customizing Agent Behavior
|
|
113
114
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# hyperloop
|
|
2
2
|
|
|
3
|
-
Walks tasks through composable process pipelines using AI agents. You write specs, it creates tasks, implements them, verifies the work, and merges
|
|
3
|
+
Walks tasks through composable process pipelines using AI agents. You write specs, it creates tasks, implements them, verifies the work, and merges the results.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
7
|
- Python 3.12+
|
|
8
8
|
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) (`claude` on PATH)
|
|
9
|
-
- `gh` CLI (authenticated, for PR management)
|
|
10
9
|
- `git`
|
|
10
|
+
- `gh` CLI (optional, for GitHub PR operations)
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -52,23 +52,23 @@ Implement JWT-based authentication for the API.
|
|
|
52
52
|
- JWTs expire after 24 hours
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
3. Run:
|
|
55
|
+
3. Run against the local repo:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
# From
|
|
59
|
-
uv run hyperloop run
|
|
58
|
+
# From inside the repo
|
|
59
|
+
uv run hyperloop run
|
|
60
60
|
|
|
61
|
-
# Or
|
|
62
|
-
hyperloop run --
|
|
63
|
-
```
|
|
61
|
+
# Or point to a repo elsewhere
|
|
62
|
+
uv run hyperloop run --path ~/code/my-project
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
# With GitHub PR support (draft PRs, lgtm gates, squash-merge)
|
|
65
|
+
uv run hyperloop run --repo owner/repo
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# Dry run (show config, don't execute)
|
|
68
|
+
uv run hyperloop run --dry-run
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
When `--repo` is not set, completed work is merged locally into the base branch via `git merge`. When `--repo` is set, the orchestrator creates draft PRs, polls for `lgtm` labels on gates, and squash-merges via GitHub.
|
|
72
72
|
|
|
73
73
|
## Configuration
|
|
74
74
|
|
|
@@ -86,13 +86,14 @@ merge:
|
|
|
86
86
|
strategy: squash
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
Then
|
|
89
|
+
Then run from the repo directory (or use `--path`):
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
hyperloop run
|
|
93
|
+
hyperloop run --path ~/code/my-project
|
|
93
94
|
```
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
All settings have sensible defaults. `--repo` is only needed for GitHub PR operations.
|
|
96
97
|
|
|
97
98
|
## Customizing Agent Behavior
|
|
98
99
|
|
|
@@ -52,6 +52,10 @@ def _config_table(cfg: Config) -> Table:
|
|
|
52
52
|
|
|
53
53
|
@app.command()
|
|
54
54
|
def run(
|
|
55
|
+
path: Path = typer.Option(
|
|
56
|
+
Path.cwd(),
|
|
57
|
+
help="Path to the target repo. Default: current directory.",
|
|
58
|
+
),
|
|
55
59
|
repo: str | None = typer.Option(
|
|
56
60
|
None,
|
|
57
61
|
help="GitHub repo (owner/repo). Inferred from git remote if not set.",
|
|
@@ -64,7 +68,7 @@ def run(
|
|
|
64
68
|
None,
|
|
65
69
|
"--config",
|
|
66
70
|
"-c",
|
|
67
|
-
help="Config file path. Default: .hyperloop.yaml",
|
|
71
|
+
help="Config file path. Default: .hyperloop.yaml in the target repo.",
|
|
68
72
|
),
|
|
69
73
|
max_workers: int | None = typer.Option(
|
|
70
74
|
None,
|
|
@@ -78,7 +82,8 @@ def run(
|
|
|
78
82
|
) -> None:
|
|
79
83
|
"""Run the orchestrator loop."""
|
|
80
84
|
# 1. Load config (file + CLI overrides)
|
|
81
|
-
|
|
85
|
+
repo_path = path.resolve()
|
|
86
|
+
config_path = config_file or (repo_path / ".hyperloop.yaml")
|
|
82
87
|
try:
|
|
83
88
|
cfg = load_config(
|
|
84
89
|
config_path,
|
|
@@ -99,7 +104,9 @@ def run(
|
|
|
99
104
|
style="blue",
|
|
100
105
|
)
|
|
101
106
|
)
|
|
102
|
-
|
|
107
|
+
table = _config_table(cfg)
|
|
108
|
+
table.add_row("path", str(repo_path))
|
|
109
|
+
console.print(table)
|
|
103
110
|
console.print()
|
|
104
111
|
|
|
105
112
|
# 3. Dry run — show config and exit
|
|
@@ -107,13 +114,15 @@ def run(
|
|
|
107
114
|
console.print("[bold yellow]Dry run[/bold yellow] -- exiting without executing.")
|
|
108
115
|
return
|
|
109
116
|
|
|
110
|
-
# 4. Validate repo
|
|
117
|
+
# 4. Validate repo path exists and is a git repo
|
|
118
|
+
if not (repo_path / ".git").exists():
|
|
119
|
+
console.print(f"[bold red]Error:[/bold red] {repo_path} is not a git repository.")
|
|
120
|
+
raise typer.Exit(code=1)
|
|
121
|
+
|
|
111
122
|
if cfg.repo is None:
|
|
112
123
|
console.print(
|
|
113
|
-
"[
|
|
114
|
-
"Use --repo owner/repo or set target.repo in .hyperloop.yaml"
|
|
124
|
+
"[dim]No --repo set. PR operations (draft, merge, gate) will be skipped.[/dim]"
|
|
115
125
|
)
|
|
116
|
-
raise typer.Exit(code=1)
|
|
117
126
|
|
|
118
127
|
# 5. Construct runtime and state store, run loop
|
|
119
128
|
from hyperloop.adapters.git_state import GitStateStore
|
|
@@ -136,7 +145,6 @@ def run(
|
|
|
136
145
|
),
|
|
137
146
|
)
|
|
138
147
|
|
|
139
|
-
repo_path = Path.cwd()
|
|
140
148
|
state = GitStateStore(repo_path, specs_dir=cfg.specs_dir)
|
|
141
149
|
runtime = LocalRuntime(repo_path=str(repo_path))
|
|
142
150
|
|
|
@@ -340,45 +340,61 @@ class Orchestrator:
|
|
|
340
340
|
self._state.clear_findings(task.id)
|
|
341
341
|
|
|
342
342
|
def _merge_ready_prs(self) -> None:
|
|
343
|
-
"""Merge
|
|
343
|
+
"""Merge branches for tasks at the merge-pr action step.
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
With a PRManager: rebase + squash-merge the PR.
|
|
346
|
+
Without a PRManager: local git merge of worker branch into base branch.
|
|
347
|
+
On conflict, transitions to NEEDS_REBASE.
|
|
346
348
|
"""
|
|
347
|
-
if self._pr_manager is None:
|
|
348
|
-
logger.debug("merge: no PRManager — skipping merge")
|
|
349
|
-
return
|
|
350
|
-
|
|
351
349
|
all_tasks = self._state.get_world().tasks
|
|
352
350
|
for task in all_tasks.values():
|
|
353
351
|
if task.status != TaskStatus.IN_PROGRESS:
|
|
354
352
|
continue
|
|
355
353
|
if task.phase != Phase("merge-pr"):
|
|
356
354
|
continue
|
|
357
|
-
if task.pr is None:
|
|
358
|
-
continue
|
|
359
355
|
|
|
360
356
|
branch = task.branch or f"worker/{task.id}"
|
|
361
357
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
self.
|
|
366
|
-
task.id, TaskStatus.NEEDS_REBASE, phase=Phase("merge-pr")
|
|
367
|
-
)
|
|
368
|
-
continue
|
|
358
|
+
if self._pr_manager is not None and task.pr is not None:
|
|
359
|
+
self._merge_via_pr(task.id, task.pr, task.spec_ref, branch)
|
|
360
|
+
else:
|
|
361
|
+
self._merge_local(task.id, branch)
|
|
369
362
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
self._state.transition_task(
|
|
374
|
-
task.id, TaskStatus.NEEDS_REBASE, phase=Phase("merge-pr")
|
|
375
|
-
)
|
|
376
|
-
continue
|
|
363
|
+
def _merge_via_pr(self, task_id: str, pr_url: str, spec_ref: str, branch: str) -> None:
|
|
364
|
+
"""Merge via GitHub PR: rebase, then squash-merge."""
|
|
365
|
+
assert self._pr_manager is not None
|
|
377
366
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
self._state.
|
|
381
|
-
|
|
367
|
+
if not self._pr_manager.rebase_branch(branch, "main"):
|
|
368
|
+
logger.warning("Rebase conflict for task %s, marking NEEDS_REBASE", task_id)
|
|
369
|
+
self._state.transition_task(task_id, TaskStatus.NEEDS_REBASE, phase=Phase("merge-pr"))
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
if not self._pr_manager.merge(pr_url, task_id, spec_ref):
|
|
373
|
+
logger.warning("Merge conflict for task %s, marking NEEDS_REBASE", task_id)
|
|
374
|
+
self._state.transition_task(task_id, TaskStatus.NEEDS_REBASE, phase=Phase("merge-pr"))
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
self._state.transition_task(task_id, TaskStatus.COMPLETE, phase=None)
|
|
378
|
+
self._state.clear_findings(task_id)
|
|
379
|
+
logger.info("Merged PR for task %s", task_id)
|
|
380
|
+
|
|
381
|
+
def _merge_local(self, task_id: str, branch: str) -> None:
|
|
382
|
+
"""Merge worker branch into base branch locally (no PR)."""
|
|
383
|
+
import subprocess
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
subprocess.run(
|
|
387
|
+
["git", "merge", branch, "--no-edit", "-m", f"merge: {task_id}"],
|
|
388
|
+
check=True,
|
|
389
|
+
capture_output=True,
|
|
390
|
+
)
|
|
391
|
+
self._state.transition_task(task_id, TaskStatus.COMPLETE, phase=None)
|
|
392
|
+
self._state.clear_findings(task_id)
|
|
393
|
+
logger.info("Local merge of %s into base branch", task_id)
|
|
394
|
+
except subprocess.CalledProcessError:
|
|
395
|
+
logger.warning("Local merge conflict for task %s, marking NEEDS_REBASE", task_id)
|
|
396
|
+
subprocess.run(["git", "merge", "--abort"], capture_output=True, check=False)
|
|
397
|
+
self._state.transition_task(task_id, TaskStatus.NEEDS_REBASE, phase=Phase("merge-pr"))
|
|
382
398
|
|
|
383
399
|
# -----------------------------------------------------------------------
|
|
384
400
|
# World building
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|