devops-bot-sdk 1.4.29__tar.gz → 1.4.31__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 (38) hide show
  1. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/client.py +28 -1
  6. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/run_auto.py +114 -16
  7. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/README.md +0 -0
  8. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  9. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/__init__.py +0 -0
  14. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/files.py +0 -0
  15. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/process.py +0 -0
  16. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/screenshot.py +0 -0
  17. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/config.py +0 -0
  18. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/crucial.py +0 -0
  19. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/exceptions.py +0 -0
  20. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/git_ops.py +0 -0
  21. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/graphify.py +0 -0
  22. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/hooks/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/hooks/crucial_guard.py +0 -0
  24. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/ipc/__init__.py +0 -0
  25. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/ipc/electron_bridge.py +0 -0
  26. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/ipc/handlers.py +0 -0
  27. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/local_exec.py +0 -0
  28. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/__init__.py +0 -0
  29. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/envelope.py +0 -0
  30. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/requests.py +0 -0
  31. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/responses.py +0 -0
  32. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/snapshots.py +0 -0
  33. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/py.typed +0 -0
  34. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.29
3
+ Version: 1.4.31
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.29
3
+ Version: 1.4.31
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.29"
7
+ version = "1.4.31"
8
8
  description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.29
3
+ Version: 1.4.31
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.29"
33
+ __version__ = "1.4.31"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -36,7 +36,7 @@ from sdk.sse import _check_status, stream_with_reconnect
36
36
 
37
37
  logger = logging.getLogger(__name__)
38
38
 
39
- SDK_VERSION = "1.4.29"
39
+ SDK_VERSION = "1.4.31"
40
40
  _POLL_INTERVAL = 3.0
41
41
  _POLL_TIMEOUT = 600.0
42
42
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -691,6 +691,33 @@ class BackendClient:
691
691
  logger.warning("client.move_to_backlog error task=%s err=%s", task_id, exc)
692
692
  return False
693
693
 
694
+ async def move_to_todo(self, task_id: str) -> bool:
695
+ """PUT {BE}/tasks-ml/{id} status="To Do" (best-effort).
696
+
697
+ Used by `agentos-auto stop` to return the in-flight ticket to the unstarted
698
+ queue so a later run re-picks it up from scratch (rather than leaving it
699
+ In Progress). Returns True on success; logs and returns False otherwise.
700
+ """
701
+ from sdk.config import BE_DEFAULT_URL
702
+
703
+ url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}"
704
+ headers = {
705
+ k: v for k, v in self._headers.items() if k.lower() != "content-type"
706
+ }
707
+ headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
708
+ try:
709
+ async with httpx.AsyncClient(timeout=20.0) as client:
710
+ resp = await client.put(url, files={"status": (None, "To Do")}, headers=headers)
711
+ if resp.status_code >= 400:
712
+ logger.warning("client.move_to_todo failed task=%s status=%s body=%s",
713
+ task_id, resp.status_code, resp.text[:200])
714
+ return False
715
+ await self.log_activity(f"Task {task_id}: status → To Do")
716
+ return True
717
+ except Exception as exc: # noqa: BLE001
718
+ logger.warning("client.move_to_todo error task=%s err=%s", task_id, exc)
719
+ return False
720
+
694
721
  async def mark_in_progress(self, task_id: str) -> bool:
695
722
  """PUT {BE}/tasks-ml/{id} status=In Progress (best-effort).
696
723
 
@@ -28,11 +28,11 @@ Auto-update: on `run`/`resume`, the SDK checks for a newer version and upgrades
28
28
  itself (then re-runs on the new code) BEFORE detaching, so non-technical users
29
29
  never reinstall.
30
30
 
31
- Stop/resume model: the loop marks each task In Progress/Done on the backend as
32
- it goes, so the backend task status IS the saved state. `stop` writes a flag the
33
- running loop checks between tasks (so a stop takes effect after the in-flight
34
- task finishes); `resume` (or just re-running) clears the flag and continues with
35
- whatever is still To Do.
31
+ Stop model: `stop` is immediate it kills the loop's whole process group (the
32
+ daemon and any child `claude`) and returns the in-flight ticket to "To Do" so a
33
+ later run restarts it cleanly, rather than pausing after the current task. The
34
+ tracked in-flight ticket id is kept in ~/.agentos/auto.current. `resume` (or just
35
+ re-running) starts fresh over whatever is still To Do.
36
36
  """
37
37
  from __future__ import annotations
38
38
 
@@ -46,6 +46,9 @@ _AGENTOS_DIR = Path.home() / ".agentos"
46
46
  _STOP_FILE = _AGENTOS_DIR / "auto.stop"
47
47
  _PID_FILE = _AGENTOS_DIR / "auto.pid"
48
48
  _LOG_FILE = _AGENTOS_DIR / "auto.log"
49
+ # Backend id (UUID) of the ticket currently being processed. `stop` reads it to
50
+ # return the in-flight ticket to "To Do" before killing the loop.
51
+ _CURRENT_FILE = _AGENTOS_DIR / "auto.current"
49
52
 
50
53
 
51
54
  def _stop_requested() -> bool:
@@ -64,6 +67,29 @@ def _clear_stop() -> None:
64
67
  pass
65
68
 
66
69
 
70
+ def _write_current(task_id: str) -> None:
71
+ try:
72
+ _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
73
+ _CURRENT_FILE.write_text(task_id)
74
+ except Exception: # noqa: BLE001
75
+ pass
76
+
77
+
78
+ def _read_current() -> str | None:
79
+ try:
80
+ val = _CURRENT_FILE.read_text().strip()
81
+ return val or None
82
+ except FileNotFoundError:
83
+ return None
84
+
85
+
86
+ def _clear_current() -> None:
87
+ try:
88
+ _CURRENT_FILE.unlink()
89
+ except FileNotFoundError:
90
+ pass
91
+
92
+
67
93
  # ── Background-daemon support ─────────────────────────────────────────────
68
94
  # `agentos-auto` must keep running after the terminal is closed and stop ONLY on
69
95
  # `agentos-auto stop`. We fully detach from the controlling terminal (double-fork
@@ -157,9 +183,24 @@ def _parse_workspace_root(argv: list[str]) -> str | None:
157
183
  return None
158
184
 
159
185
 
186
+ _current_task: str | None = None
187
+
188
+
160
189
  async def _send(env: dict) -> None:
161
190
  etype = env.get("type", "")
162
191
  data = env.get("data", {}) or {}
192
+
193
+ # Track the in-flight ticket (backend UUID) so `stop` can return it to To Do.
194
+ # The handler tags per-task envelopes with jira_task_id / task_id (both = the
195
+ # backend UUID here). Persist only on change to avoid churning the file.
196
+ global _current_task
197
+ tid = data.get("jira_task_id") or data.get("task_id")
198
+ if tid and tid != _current_task:
199
+ _current_task = tid
200
+ _write_current(tid)
201
+ if etype == "done":
202
+ _clear_current()
203
+
163
204
  if etype == "pipeline_started":
164
205
  print(f"\n▶ orchestrate-auto (local) — {data.get('task_count', 0)} task(s)\n")
165
206
  elif etype == "step_update":
@@ -213,12 +254,14 @@ def _run_loop(workspace_root: str | None) -> None:
213
254
  print("\n⏸ interrupted — stop flag set. run `agentos-auto resume` to continue.")
214
255
  finally:
215
256
  # Whether the loop finished, paused, or crashed, drop our PID file so
216
- # `status` doesn't report a dead daemon as running.
257
+ # `status` doesn't report a dead daemon as running, and clear the
258
+ # in-flight marker (the queue is drained on a clean finish).
217
259
  if _read_pid() == os.getpid():
218
260
  try:
219
261
  _PID_FILE.unlink()
220
262
  except FileNotFoundError:
221
263
  pass
264
+ _clear_current()
222
265
 
223
266
 
224
267
  def main() -> None:
@@ -233,13 +276,63 @@ def main() -> None:
233
276
  return
234
277
 
235
278
  if cmd == "stop":
279
+ # Hard stop: halt the current task immediately, return it to To Do so it
280
+ # restarts cleanly next run, and kill the whole loop (not a cooperative
281
+ # pause after the current task).
236
282
  _set_stop()
237
283
  pid = _daemon_running()
284
+ cur = _read_current()
285
+
286
+ # 1. Kill the loop's ENTIRE process group first (daemon + any child
287
+ # `claude` it spawned) so nothing keeps mutating the ticket. The daemon
288
+ # is a session leader (setsid), so its PGID == its PID.
238
289
  if pid:
239
- print(f"stop requested — the background loop (pid {pid}) will pause after "
240
- "the current task, then exit.")
290
+ import signal
291
+ import time as _t
292
+ # Kill the whole process group ONLY when pid is its own group leader
293
+ # (the daemon, via setsid) — that also reaps the child `claude`. For a
294
+ # foreground run (shares the shell's group) fall back to a single kill
295
+ # so we don't take down the user's terminal.
296
+ try:
297
+ is_group_leader = os.getpgid(pid) == pid
298
+ except ProcessLookupError:
299
+ is_group_leader = False
300
+ killed = False
301
+ for sig in (signal.SIGTERM, signal.SIGKILL):
302
+ try:
303
+ if is_group_leader:
304
+ os.killpg(pid, sig)
305
+ else:
306
+ os.kill(pid, sig)
307
+ except ProcessLookupError:
308
+ killed = True
309
+ break
310
+ except Exception: # noqa: BLE001
311
+ pass
312
+ _t.sleep(1.5)
313
+ if not _pid_alive(pid):
314
+ killed = True
315
+ break
316
+ print(f"stopped background loop (pid {pid})" if killed
317
+ else f"sent stop signals to pid {pid}")
241
318
  else:
242
- print("stop requested a running `agentos-auto` will pause after the current task.")
319
+ print("no background loop running stop flag set for any foreground run.")
320
+
321
+ # 2. Now that the loop is dead, move the in-flight ticket back to To Do.
322
+ if cur:
323
+ try:
324
+ from sdk.client import BackendClient
325
+ ok = asyncio.run(BackendClient.from_config().move_to_todo(cur))
326
+ print(f"in-flight ticket → To Do ({'ok' if ok else 'failed'})")
327
+ except Exception as exc: # noqa: BLE001
328
+ print(f"warning: could not move in-flight ticket to To Do ({exc})")
329
+
330
+ # 3. Clean up state files.
331
+ for f in (_PID_FILE, _CURRENT_FILE):
332
+ try:
333
+ f.unlink()
334
+ except FileNotFoundError:
335
+ pass
243
336
  return
244
337
 
245
338
  if cmd == "status":
@@ -247,12 +340,24 @@ def main() -> None:
247
340
  if pid:
248
341
  print(f"background loop: running (pid {pid})")
249
342
  print(f"logs: {_LOG_FILE}")
343
+ cur = _read_current()
344
+ if cur:
345
+ print(f"current ticket: {cur}")
250
346
  else:
251
347
  print("background loop: not running")
252
348
  print("stop pending: yes" if _stop_requested() else "stop pending: no")
253
349
  return
254
350
 
255
351
  if cmd in ("run", "resume"):
352
+ # Self-update FIRST (before the already-running check) so the installed
353
+ # package is refreshed even when a daemon is already running — otherwise a
354
+ # long-lived old daemon would pin `stop`/`status` to a stale version. Runs
355
+ # in the FOREGROUND so the user sees update output; re-execs on the new
356
+ # code. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
357
+ if "--no-update" not in argv:
358
+ from sdk.updater import auto_update_and_reexec
359
+ auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
360
+
256
361
  # Refuse to start a second loop — one background daemon owns the To Do queue.
257
362
  pid = _daemon_running()
258
363
  if pid:
@@ -261,13 +366,6 @@ def main() -> None:
261
366
  print(" stop: agentos-auto stop")
262
367
  return
263
368
 
264
- # Keep non-technical users current: self-update, then re-run on the new
265
- # code. Runs in the FOREGROUND (before detaching) so the user sees update
266
- # output. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
267
- if "--no-update" not in argv:
268
- from sdk.updater import auto_update_and_reexec
269
- auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
270
-
271
369
  workspace_root = _parse_workspace_root(argv)
272
370
 
273
371
  # Background by default: detach so closing the terminal (SIGHUP) can't kill