hyperloop 0.8.1__tar.gz → 0.9.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 (83) hide show
  1. {hyperloop-0.8.1 → hyperloop-0.9.0}/CHANGELOG.md +3 -0
  2. {hyperloop-0.8.1 → hyperloop-0.9.0}/PKG-INFO +1 -1
  3. {hyperloop-0.8.1 → hyperloop-0.9.0}/pyproject.toml +1 -1
  4. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/matrix_setup.py +68 -24
  5. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/config.py +3 -3
  6. {hyperloop-0.8.1 → hyperloop-0.9.0}/uv.lock +1 -1
  7. {hyperloop-0.8.1 → hyperloop-0.9.0}/.github/workflows/ci.yaml +0 -0
  8. {hyperloop-0.8.1 → hyperloop-0.9.0}/.github/workflows/release.yaml +0 -0
  9. {hyperloop-0.8.1 → hyperloop-0.9.0}/.gitignore +0 -0
  10. {hyperloop-0.8.1 → hyperloop-0.9.0}/.pre-commit-config.yaml +0 -0
  11. {hyperloop-0.8.1 → hyperloop-0.9.0}/.python-version +0 -0
  12. {hyperloop-0.8.1 → hyperloop-0.9.0}/CLAUDE.md +0 -0
  13. {hyperloop-0.8.1 → hyperloop-0.9.0}/LICENSE +0 -0
  14. {hyperloop-0.8.1 → hyperloop-0.9.0}/README.md +0 -0
  15. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/implementer.yaml +0 -0
  16. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/kustomization.yaml +0 -0
  17. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/pm.yaml +0 -0
  18. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/process-improver.yaml +0 -0
  19. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/process.yaml +0 -0
  20. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/rebase-resolver.yaml +0 -0
  21. {hyperloop-0.8.1 → hyperloop-0.9.0}/base/verifier.yaml +0 -0
  22. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/observability.md +0 -0
  23. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/prompts/checklist.md +0 -0
  24. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/prompts/checks/check_result_file.sh +0 -0
  25. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/prompts/rules.md +0 -0
  26. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/spec.md +0 -0
  27. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-001.md +0 -0
  28. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-002.md +0 -0
  29. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-003.md +0 -0
  30. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-004.md +0 -0
  31. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-005.md +0 -0
  32. {hyperloop-0.8.1 → hyperloop-0.9.0}/specs/tasks/task-006.md +0 -0
  33. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/__init__.py +0 -0
  34. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/__main__.py +0 -0
  35. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/__init__.py +0 -0
  36. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/git_state.py +0 -0
  37. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/local.py +0 -0
  38. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/matrix_probe.py +0 -0
  39. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/probe.py +0 -0
  40. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/serial.py +0 -0
  41. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/adapters/structlog_probe.py +0 -0
  42. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/cli.py +0 -0
  43. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/compose.py +0 -0
  44. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/__init__.py +0 -0
  45. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/decide.py +0 -0
  46. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/deps.py +0 -0
  47. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/model.py +0 -0
  48. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/domain/pipeline.py +0 -0
  49. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/logging.py +0 -0
  50. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/loop.py +0 -0
  51. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/__init__.py +0 -0
  52. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/pr.py +0 -0
  53. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/probe.py +0 -0
  54. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/runtime.py +0 -0
  55. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/serial.py +0 -0
  56. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/ports/state.py +0 -0
  57. {hyperloop-0.8.1 → hyperloop-0.9.0}/src/hyperloop/pr.py +0 -0
  58. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/__init__.py +0 -0
  59. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/__init__.py +0 -0
  60. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/pr.py +0 -0
  61. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/probe.py +0 -0
  62. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/runtime.py +0 -0
  63. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/serial.py +0 -0
  64. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/fakes/state.py +0 -0
  65. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_cli.py +0 -0
  66. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_compose.py +0 -0
  67. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_config.py +0 -0
  68. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_decide.py +0 -0
  69. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_deps.py +0 -0
  70. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_e2e.py +0 -0
  71. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_fakes.py +0 -0
  72. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_git_state.py +0 -0
  73. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_local_runtime.py +0 -0
  74. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_loop.py +0 -0
  75. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_matrix_probe.py +0 -0
  76. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_model.py +0 -0
  77. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_pipeline.py +0 -0
  78. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_pr.py +0 -0
  79. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_probe.py +0 -0
  80. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_serial_agents.py +0 -0
  81. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_smoke.py +0 -0
  82. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_state_contract.py +0 -0
  83. {hyperloop-0.8.1 → hyperloop-0.9.0}/tests/test_structlog_probe.py +0 -0
@@ -2,6 +2,9 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v0.9.0 (2026-04-15)
6
+
7
+
5
8
  ## v0.8.1 (2026-04-15)
6
9
 
7
10
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperloop
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Orchestrator that walks tasks through composable process pipelines using AI agents
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperloop"
3
- version = "0.8.1"
3
+ version = "0.9.0"
4
4
  description = "Orchestrator that walks tasks through composable process pipelines using AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -182,15 +182,18 @@ def _create_room(
182
182
  homeserver: str,
183
183
  access_token: str,
184
184
  room_name: str,
185
+ invite_user: str = "",
185
186
  ) -> str:
186
- """Create a private Matrix room. Returns the room_id."""
187
+ """Create a private Matrix room and optionally invite a user. Returns the room_id."""
187
188
  url = f"{homeserver}/_matrix/client/v3/createRoom"
188
- body = {
189
+ body: dict[str, object] = {
189
190
  "name": room_name,
190
191
  "topic": "hyperloop orchestrator notifications",
191
192
  "visibility": "private",
192
193
  "preset": "private_chat",
193
194
  }
195
+ if invite_user:
196
+ body["invite"] = [invite_user]
194
197
 
195
198
  resp = client.post(
196
199
  url,
@@ -213,15 +216,15 @@ def ensure_matrix_ready(
213
216
  ) -> tuple[str, str]:
214
217
  """Ensure Matrix credentials and room are available.
215
218
 
216
- Resolution order for **access_token**:
217
- 1. Explicit ``token_env`` env var
218
- 2. Cached token from ``.hyperloop/matrix-state.json``
219
- 3. Auto-register via ``registration_token_env``
219
+ **Access token** resolution:
220
+ 1. Explicit ``token_env`` env var → use directly.
221
+ 2. ``registration_token_env`` register a fresh disposable bot
222
+ (``hyperloop-{repo}-{random}``), deactivate the previous one.
220
223
 
221
- Resolution order for **room_id**:
222
- 1. Explicit ``room_id`` in config
223
- 2. Cached room_id
224
- 3. Auto-create via Matrix API
224
+ **Room ID** resolution:
225
+ 1. Explicit ``room_id`` in config.
226
+ 2. Cached room_id from ``.hyperloop/matrix-state.json``.
227
+ 3. Auto-create via Matrix API (invites ``invite_user``).
225
228
 
226
229
  Returns:
227
230
  (access_token, room_id) tuple. Either or both may be empty string
@@ -234,17 +237,15 @@ def ensure_matrix_ready(
234
237
 
235
238
  # --- Resolve access token ---
236
239
  explicit_token = os.environ.get(config.token_env) if config.token_env else ""
237
- cached_token = cache.get("access_token", "") if cache else ""
238
- access_token = explicit_token or cached_token
240
+ access_token = explicit_token
239
241
 
240
242
  if not access_token:
241
- # Try auto-registration
242
243
  registration_token = (
243
244
  os.environ.get(config.registration_token_env) if config.registration_token_env else ""
244
245
  )
245
246
  if registration_token:
246
247
  try:
247
- access_token = _auto_register(
248
+ access_token = _register_disposable_bot(
248
249
  config, repo_path, homeserver, registration_token, cache
249
250
  )
250
251
  except Exception:
@@ -267,36 +268,53 @@ def ensure_matrix_ready(
267
268
  _log.exception("Matrix room creation failed")
268
269
  return access_token, ""
269
270
 
271
+ # Ensure the bot has joined the room (it's a new user each run)
272
+ if access_token != explicit_token:
273
+ try:
274
+ _join_room(httpx.Client(timeout=10.0), homeserver, access_token, room_id)
275
+ except Exception:
276
+ _log.exception("Matrix room join failed")
277
+
270
278
  return access_token, room_id
271
279
 
272
280
 
273
- def _auto_register(
281
+ def _register_disposable_bot(
274
282
  config: MatrixConfig,
275
283
  repo_path: Path,
276
284
  homeserver: str,
277
285
  registration_token: str,
278
286
  cache: dict[str, str] | None,
279
287
  ) -> str:
280
- """Register a bot user and cache credentials. Returns access_token."""
281
- repo_name = repo_path.name
282
- username = config.bot_username or f"hyperloop-{repo_name}"
288
+ """Register a fresh disposable bot user. Returns access_token.
283
289
 
284
- password = cache.get("password", "") if cache else ""
285
- if not password:
286
- password = secrets.token_urlsafe(32)
290
+ Each run gets a new identity (``hyperloop-{repo}-{random}``). The
291
+ previous bot is deactivated best-effort. Only the room_id is cached.
292
+ """
293
+ repo_name = repo_path.name
294
+ suffix = secrets.token_hex(4)
295
+ username = f"hyperloop-{repo_name}-{suffix}"
296
+ password = secrets.token_urlsafe(32)
287
297
 
288
298
  client = httpx.Client(timeout=30.0)
289
299
  try:
300
+ # Deactivate previous bot (best-effort)
301
+ prev_token = cache.get("access_token", "") if cache else ""
302
+ if prev_token:
303
+ _deactivate_user(client, homeserver, prev_token)
304
+
305
+ # Register new bot
290
306
  user_id, access_token = _register_bot(
291
307
  client, homeserver, registration_token, username, password
292
308
  )
293
309
 
310
+ # Cache room_id + new bot credentials
311
+ cached_room = cache.get("room_id", "") if cache else ""
294
312
  _save_cache(
295
313
  repo_path,
296
314
  homeserver=homeserver,
297
315
  user_id=user_id,
298
316
  access_token=access_token,
299
- room_id=config.room_id,
317
+ room_id=config.room_id or cached_room,
300
318
  password=password,
301
319
  )
302
320
 
@@ -306,17 +324,43 @@ def _auto_register(
306
324
  client.close()
307
325
 
308
326
 
327
+ def _deactivate_user(client: httpx.Client, homeserver: str, access_token: str) -> None:
328
+ """Deactivate the user associated with the given access token. Best-effort."""
329
+ import contextlib
330
+
331
+ url = f"{homeserver}/_matrix/client/v3/account/deactivate"
332
+ with contextlib.suppress(Exception):
333
+ client.post(
334
+ url,
335
+ json={"auth": {"type": "m.login.password"}},
336
+ headers={"Authorization": f"Bearer {access_token}"},
337
+ )
338
+
339
+
340
+ def _join_room(client: httpx.Client, homeserver: str, access_token: str, room_id: str) -> None:
341
+ """Join a room by ID."""
342
+ url = f"{homeserver}/_matrix/client/v3/join/{room_id}"
343
+ resp = client.post(url, json={}, headers={"Authorization": f"Bearer {access_token}"})
344
+ resp.raise_for_status()
345
+
346
+
309
347
  def _auto_create_room(
310
348
  config: MatrixConfig,
311
349
  repo_path: Path,
312
350
  homeserver: str,
313
351
  access_token: str,
314
352
  ) -> str:
315
- """Create a room and update the cache. Returns room_id."""
353
+ """Create a room, invite the configured user, and cache room_id. Returns room_id."""
316
354
  repo_name = repo_path.name
317
355
  client = httpx.Client(timeout=30.0)
318
356
  try:
319
- room_id = _create_room(client, homeserver, access_token, f"hyperloop-{repo_name}")
357
+ room_id = _create_room(
358
+ client,
359
+ homeserver,
360
+ access_token,
361
+ f"hyperloop-{repo_name}",
362
+ invite_user=config.invite_user,
363
+ )
320
364
 
321
365
  # Update cache with the new room_id
322
366
  cache = _load_cache(repo_path, homeserver) or {}
@@ -31,7 +31,7 @@ class MatrixConfig:
31
31
  token_env: str
32
32
  verbose: bool
33
33
  registration_token_env: str # env var holding the registration token
34
- bot_username: str
34
+ invite_user: str # Matrix user ID to invite to auto-created rooms
35
35
 
36
36
 
37
37
  @dataclass(frozen=True)
@@ -144,7 +144,7 @@ def _flatten_yaml(raw: dict[str, object]) -> dict[str, object]:
144
144
  flat["matrix_token_env"] = mx.get("token_env", "")
145
145
  flat["matrix_verbose"] = mx.get("verbose", False)
146
146
  flat["matrix_registration_token_env"] = mx.get("registration_token_env", "")
147
- flat["matrix_bot_username"] = mx.get("bot_username", "")
147
+ flat["matrix_invite_user"] = mx.get("invite_user", "")
148
148
 
149
149
  return flat
150
150
 
@@ -208,7 +208,7 @@ def load_config(
208
208
  token_env=token_env,
209
209
  verbose=bool(values.get("matrix_verbose", False)),
210
210
  registration_token_env=registration_token_env,
211
- bot_username=str(values.get("matrix_bot_username", "")),
211
+ invite_user=str(values.get("matrix_invite_user", "")),
212
212
  )
213
213
 
214
214
  obs_cfg = ObservabilityConfig(
@@ -120,7 +120,7 @@ wheels = [
120
120
 
121
121
  [[package]]
122
122
  name = "hyperloop"
123
- version = "0.8.0"
123
+ version = "0.8.1"
124
124
  source = { editable = "." }
125
125
  dependencies = [
126
126
  { name = "httpx" },
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