sbxloop 0.5.1__tar.gz → 0.5.2__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 (43) hide show
  1. {sbxloop-0.5.1 → sbxloop-0.5.2}/PKG-INFO +1 -1
  2. sbxloop-0.5.1/src/sbxloop/_vendor/sbxloop_worker-0.5.1-py3-none-any.whl → sbxloop-0.5.2/src/sbxloop/_vendor/sbxloop_worker-0.5.2-py3-none-any.whl +0 -0
  3. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/_version.py +2 -2
  4. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/engine.py +20 -4
  5. sbxloop-0.5.2/src/sbxloop/gh/reporter.py +99 -0
  6. sbxloop-0.5.1/src/sbxloop/gh/reporter.py +0 -76
  7. {sbxloop-0.5.1 → sbxloop-0.5.2}/.gitignore +0 -0
  8. {sbxloop-0.5.1 → sbxloop-0.5.2}/README.md +0 -0
  9. {sbxloop-0.5.1 → sbxloop-0.5.2}/hatch_build.py +0 -0
  10. {sbxloop-0.5.1 → sbxloop-0.5.2}/pyproject.toml +0 -0
  11. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/__init__.py +0 -0
  12. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/cli/__init__.py +0 -0
  13. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/cli/app.py +0 -0
  14. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/cli/doctor.py +0 -0
  15. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/cli/tui.py +0 -0
  16. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/config.py +0 -0
  17. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/deliver.py +0 -0
  18. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/__init__.py +0 -0
  19. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/model.py +0 -0
  20. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/phases.py +0 -0
  21. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/prompts/__init__.py +0 -0
  22. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/prompts/decompose.md +0 -0
  23. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/prompts/execute.md +0 -0
  24. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/prompts/plan.md +0 -0
  25. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
  26. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/prompts/validate.md +0 -0
  27. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/engine/store.py +0 -0
  28. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/errors.py +0 -0
  29. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/events.py +0 -0
  30. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/gh/__init__.py +0 -0
  31. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/gh/ops.py +0 -0
  32. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/ids.py +0 -0
  33. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/py.typed +0 -0
  34. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/__init__.py +0 -0
  35. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/cli.py +0 -0
  36. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/models.py +0 -0
  37. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/pair.py +0 -0
  38. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/parse.py +0 -0
  39. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/provision.py +0 -0
  40. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/sbx/sandbox.py +0 -0
  41. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/worker/__init__.py +0 -0
  42. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/worker/client.py +0 -0
  43. {sbxloop-0.5.1 → sbxloop-0.5.2}/src/sbxloop/worker/wheel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sbxloop
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Agentic loop orchestration on Docker Sandboxes (sbx) with isolated credential domains
5
5
  Project-URL: Homepage, https://github.com/brettbergin/sbxloop
6
6
  Project-URL: Repository, https://github.com/brettbergin/sbxloop
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.5.1'
22
- __version_tuple__ = version_tuple = (0, 5, 1)
21
+ __version__ = version = '0.5.2'
22
+ __version_tuple__ = version_tuple = (0, 5, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -139,12 +139,17 @@ class LoopEngine:
139
139
  agent.install(extras="copilot", ensure_dev_tools=True)
140
140
  if github is not None:
141
141
  github.install(extras="")
142
- detach = self._attach_reporter(github, run_id)
142
+ reporter, detach = self._attach_reporter(github, run_id, outcome)
143
143
  try:
144
144
  phases = PhaseRunner(
145
145
  agent, self.config, run_id, outcome, workdir=pair.agent_workdir
146
146
  )
147
147
  state = self._run_phases(run_id, phases, deadline, pair)
148
+ # Summary must post while the github sandbox is alive;
149
+ # on an infra exception the run is resumable and the
150
+ # resumed run reopens the same tracking issue.
151
+ if reporter is not None:
152
+ reporter.close_run(run_id, state)
148
153
  finally:
149
154
  detach()
150
155
  # Harvest even when a phase raised: the sandbox is still
@@ -167,13 +172,24 @@ class LoopEngine:
167
172
  mounted=pair.mounted,
168
173
  )
169
174
 
170
- def _attach_reporter(self, github: WorkerClient | None, run_id: str) -> Callable[[], None]:
175
+ def _attach_reporter(
176
+ self, github: WorkerClient | None, run_id: str, outcome: str
177
+ ) -> tuple[GithubReporterHook | None, Callable[[], None]]:
178
+ """Attach progress reporting; opens the tracking issue immediately.
179
+
180
+ Run start/end go through explicit ``open_run``/``close_run`` calls
181
+ rather than bus events: RUN_START is emitted before the github
182
+ sandbox exists and RUN_END after it is gone, so the hook could never
183
+ observe them (#58).
184
+ """
171
185
  gh = self.config.github
172
186
  if not gh.report or github is None:
173
- return lambda: None
187
+ return None, lambda: None
174
188
  assert gh.repo is not None # report=True without a repo cannot provision a github worker
175
189
  hook = GithubReporterHook(GithubOps(github, run_id), gh.repo)
176
- return self.bus.attach_hook(hook)
190
+ detach = self.bus.attach_hook(hook)
191
+ hook.open_run(run_id, outcome)
192
+ return hook, detach
177
193
 
178
194
  def _harvest(self, run_id: str, pair: SandboxPair) -> None:
179
195
  """Copy the in-VM work dir out to the host (unmounted runs only).
@@ -0,0 +1,99 @@
1
+ """GithubReporterHook: mirrors run progress to a GitHub tracking issue.
2
+
3
+ The default consumer of the github-ops sandbox. When the GitHub integration
4
+ is configured (``[github].repo``) and reporting is enabled (``report = true``
5
+ or ``--report``), the engine opens a tracking issue right after the github
6
+ sandbox is ready, comments as tasks finish, and posts a final summary while
7
+ the sandbox is still alive.
8
+
9
+ Run start/end are **explicit calls** (``open_run``/``close_run``) rather
10
+ than bus events: the run lifecycle events are emitted outside the window in
11
+ which the hook is attached and the github sandbox exists, so subscribing to
12
+ them can never work — see #58. Only per-task progress arrives via the bus.
13
+
14
+ Every entry point is guarded: a reporting failure must not fail a run — and
15
+ the EventBus isolates ``on_event`` anyway.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+
22
+ from sbxloop.events import Event, HostEventTypes
23
+ from sbxloop.gh.ops import GithubOps, IssueRef
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class GithubReporterHook:
29
+ def __init__(self, ops: GithubOps, repo: str) -> None:
30
+ self.ops = ops
31
+ self.repo = repo
32
+ self.issue: IssueRef | None = None
33
+ self._task_lines: list[str] = []
34
+
35
+ # -- explicit lifecycle (called by the engine, sandbox guaranteed alive) --
36
+
37
+ def open_run(self, run_id: str, outcome: str) -> None:
38
+ """Open (or on resume, re-find) the run's tracking issue."""
39
+ try:
40
+ self.issue = self._find_existing(run_id) or self._create(run_id, outcome)
41
+ except Exception:
42
+ logger.warning("github reporting: opening tracking issue failed", exc_info=True)
43
+
44
+ def close_run(self, run_id: str, state: str) -> None:
45
+ """Post the final summary comment; call before sandbox teardown."""
46
+ if self.issue is None:
47
+ return
48
+ summary = "\n".join(self._task_lines) or "_no tasks were executed_"
49
+ try:
50
+ self.ops.issue_comment(
51
+ self.repo,
52
+ self.issue.number,
53
+ f"Run `{run_id}` finished: **{state}**\n\n{summary}",
54
+ )
55
+ except Exception:
56
+ logger.warning("github reporting: final summary failed", exc_info=True)
57
+
58
+ # -- Hook protocol (task progress only) ----------------------------------
59
+
60
+ def on_event(self, event: Event) -> None:
61
+ try:
62
+ if event.type == HostEventTypes.TASK_END:
63
+ self._on_task_end(event)
64
+ except Exception:
65
+ logger.warning("github reporting failed for %s", event.type, exc_info=True)
66
+
67
+ # -- internals -----------------------------------------------------------
68
+
69
+ def _find_existing(self, run_id: str) -> IssueRef | None:
70
+ """A resumed run reuses its issue instead of opening a duplicate."""
71
+ query = f'repo:{self.repo} is:issue in:title "sbxloop run {run_id}"'
72
+ for item in self.ops.search_issues(query, per_page=5):
73
+ if item.get("title") == f"sbxloop run {run_id}" and item.get("number"):
74
+ return IssueRef(number=int(item["number"]), url=str(item.get("html_url", "")))
75
+ return None
76
+
77
+ def _create(self, run_id: str, outcome: str) -> IssueRef:
78
+ body = (
79
+ f"sbxloop run `{run_id}` started.\n\n"
80
+ f"**Outcome:**\n\n> {outcome}\n\n"
81
+ "Progress is reported as comments on this issue."
82
+ )
83
+ return self.ops.issue_create(
84
+ self.repo,
85
+ title=f"sbxloop run {run_id}",
86
+ body=body,
87
+ labels=["sbxloop"],
88
+ )
89
+
90
+ def _on_task_end(self, event: Event) -> None:
91
+ if self.issue is None:
92
+ return
93
+ task_id = event.data.get("task_id", "?")
94
+ title = event.data.get("title", "")
95
+ state = event.data.get("state", "?")
96
+ marker = "✅" if state == "done" else "❌"
97
+ line = f"{marker} `{task_id}` {title} — **{state}**"
98
+ self._task_lines.append(line)
99
+ self.ops.issue_comment(self.repo, self.issue.number, line)
@@ -1,76 +0,0 @@
1
- """GithubReporterHook: mirrors run progress to a GitHub tracking issue.
2
-
3
- The default consumer of the github-ops sandbox. When the GitHub integration
4
- is configured (``[github].repo``) and reporting is enabled (``report = true``
5
- or ``--report``), the hook opens a tracking issue at run start, comments as
6
- tasks finish, and posts a final summary at run end. It never raises — a
7
- reporting failure must not fail a run — and the EventBus isolates it anyway.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import logging
13
-
14
- from sbxloop.events import Event, HostEventTypes
15
- from sbxloop.gh.ops import GithubOps, IssueRef
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class GithubReporterHook:
21
- def __init__(self, ops: GithubOps, repo: str) -> None:
22
- self.ops = ops
23
- self.repo = repo
24
- self.issue: IssueRef | None = None
25
- self._task_lines: list[str] = []
26
-
27
- # -- Hook protocol -----------------------------------------------------
28
-
29
- def on_event(self, event: Event) -> None:
30
- try:
31
- if event.type == HostEventTypes.RUN_START:
32
- self._on_run_start(event)
33
- elif event.type == HostEventTypes.TASK_END:
34
- self._on_task_end(event)
35
- elif event.type == HostEventTypes.RUN_END:
36
- self._on_run_end(event)
37
- except Exception:
38
- logger.warning("github reporting failed for %s", event.type, exc_info=True)
39
-
40
- # -- handlers ----------------------------------------------------------
41
-
42
- def _on_run_start(self, event: Event) -> None:
43
- outcome = str(event.data.get("outcome", ""))
44
- body = (
45
- f"sbxloop run `{event.run_id}` started.\n\n"
46
- f"**Outcome:**\n\n> {outcome}\n\n"
47
- "Progress is reported as comments on this issue."
48
- )
49
- self.issue = self.ops.issue_create(
50
- self.repo,
51
- title=f"sbxloop run {event.run_id}",
52
- body=body,
53
- labels=["sbxloop"],
54
- )
55
-
56
- def _on_task_end(self, event: Event) -> None:
57
- if self.issue is None:
58
- return
59
- task_id = event.data.get("task_id", "?")
60
- title = event.data.get("title", "")
61
- state = event.data.get("state", "?")
62
- marker = "✅" if state == "done" else "❌"
63
- line = f"{marker} `{task_id}` {title} — **{state}**"
64
- self._task_lines.append(line)
65
- self.ops.issue_comment(self.repo, self.issue.number, line)
66
-
67
- def _on_run_end(self, event: Event) -> None:
68
- if self.issue is None:
69
- return
70
- state = event.data.get("state", "?")
71
- summary = "\n".join(self._task_lines) or "_no tasks were executed_"
72
- self.ops.issue_comment(
73
- self.repo,
74
- self.issue.number,
75
- f"Run `{event.run_id}` finished: **{state}**\n\n{summary}",
76
- )
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