devops-bot-sdk 1.4.27__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.27 → devops_bot_sdk-1.4.31}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/client.py +28 -1
  6. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/ipc/handlers.py +28 -14
  7. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/run_auto.py +122 -16
  8. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/README.md +0 -0
  9. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/collectors/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/collectors/files.py +0 -0
  16. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/collectors/process.py +0 -0
  17. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/collectors/screenshot.py +0 -0
  18. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/config.py +0 -0
  19. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/crucial.py +0 -0
  20. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/exceptions.py +0 -0
  21. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/git_ops.py +0 -0
  22. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/hooks/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/hooks/crucial_guard.py +0 -0
  25. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/local_exec.py +0 -0
  28. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/models/__init__.py +0 -0
  29. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/models/envelope.py +0 -0
  30. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/models/requests.py +0 -0
  31. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/models/responses.py +0 -0
  32. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/models/snapshots.py +0 -0
  33. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/py.typed +0 -0
  34. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.27 → devops_bot_sdk-1.4.31}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.27 → 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.27
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.27
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.27"
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.27
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.27"
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.27"
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
 
@@ -1845,31 +1845,45 @@ async def handle_orchestrate_auto_local_run(
1845
1845
  "current_step": f"{jira_key}: awaiting approval"},
1846
1846
  ).model_dump())
1847
1847
 
1848
- # Safety net for the gate-1 WhatsApp. The backend sends it
1849
- # when planning opens the gate; if that send failed the
1850
- # approver would get nothing and the ticket would silently
1851
- # time out to Backlog. Re-send it ourselves once, only after
1852
- # the grace window, so a healthy backend doesn't duplicate.
1853
1848
  _wa_summary = task.get("summary") or jira_key or "task"
1854
1849
 
1855
- async def _first_gate_wa_fallback(
1850
+ async def _send_first_gate_wa(
1856
1851
  tid=tid, jira_key=jira_key, _wa_summary=_wa_summary,
1857
- ) -> None:
1858
- await send(Envelope(
1859
- type="step_update", thread_id=thread_id,
1860
- data={"current_step": f"{jira_key}: no approval reply yet "
1861
- "re-sending WhatsApp approval request"},
1862
- ).model_dump())
1863
- await client.notify_whatsapp(
1852
+ ) -> bool:
1853
+ # approver_role defaults to TEAM_LEAD (matches the ML
1854
+ # backend's /approvals-ml routing that is known to work).
1855
+ # Log the outcome to the daemon log + activity DB so a
1856
+ # failed send is visible instead of silently swallowed.
1857
+ ok = await client.notify_whatsapp(
1864
1858
  tid,
1865
1859
  f"Approval needed for {jira_key or tid}: {_wa_summary} — "
1866
1860
  "reply on WhatsApp to approve or reject so the work can start.",
1867
1861
  summary=_wa_summary,
1868
1862
  )
1863
+ note = ("WhatsApp approval request sent" if ok
1864
+ else "WhatsApp approval request FAILED to send "
1865
+ "(check /approvals-ml + approver phone on file)")
1866
+ await send(Envelope(
1867
+ type="step_update", thread_id=thread_id,
1868
+ data={"current_step": f"{jira_key}: {note}"},
1869
+ ).model_dump())
1870
+ try:
1871
+ await client.log_activity(f"Task {tid}: {note}")
1872
+ except Exception: # noqa: BLE001
1873
+ pass
1874
+ return ok
1875
+
1876
+ # Send the gate-1 WhatsApp NOW via /approvals-ml (the ML
1877
+ # backend actually delivers it). Owning the send here makes
1878
+ # it reliable regardless of whether the AI backend's own
1879
+ # M5.2 send fired — the previous "no WhatsApp at all" bug.
1880
+ await _send_first_gate_wa()
1869
1881
 
1882
+ # Re-send once, after the grace window, only if still unanswered
1883
+ # (covers a transient failure of the send above).
1870
1884
  decision = await _poll_approval(
1871
1885
  client, tid, should_stop,
1872
- fallback_notify=_first_gate_wa_fallback,
1886
+ fallback_notify=_send_first_gate_wa,
1873
1887
  fallback_after_s=_FIRST_GATE_WA_FALLBACK_S,
1874
1888
  )
1875
1889
 
@@ -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
@@ -134,6 +160,14 @@ def _daemonize() -> bool:
134
160
  os.close(devnull)
135
161
  os.close(logfd)
136
162
 
163
+ # CRITICAL: when stdout is a file (not a tty) Python block-buffers it, so
164
+ # progress printed via _send() would sit in the buffer and never reach the
165
+ # log until the process exits — making the daemon look idle. Replace the
166
+ # streams with line-buffered writers so every progress line is flushed live,
167
+ # exactly like the foreground run.
168
+ sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
169
+ sys.stderr = sys.stdout
170
+
137
171
  _PID_FILE.write_text(str(os.getpid()))
138
172
  return True
139
173
 
@@ -149,9 +183,24 @@ def _parse_workspace_root(argv: list[str]) -> str | None:
149
183
  return None
150
184
 
151
185
 
186
+ _current_task: str | None = None
187
+
188
+
152
189
  async def _send(env: dict) -> None:
153
190
  etype = env.get("type", "")
154
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
+
155
204
  if etype == "pipeline_started":
156
205
  print(f"\n▶ orchestrate-auto (local) — {data.get('task_count', 0)} task(s)\n")
157
206
  elif etype == "step_update":
@@ -205,12 +254,14 @@ def _run_loop(workspace_root: str | None) -> None:
205
254
  print("\n⏸ interrupted — stop flag set. run `agentos-auto resume` to continue.")
206
255
  finally:
207
256
  # Whether the loop finished, paused, or crashed, drop our PID file so
208
- # `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).
209
259
  if _read_pid() == os.getpid():
210
260
  try:
211
261
  _PID_FILE.unlink()
212
262
  except FileNotFoundError:
213
263
  pass
264
+ _clear_current()
214
265
 
215
266
 
216
267
  def main() -> None:
@@ -225,13 +276,63 @@ def main() -> None:
225
276
  return
226
277
 
227
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).
228
282
  _set_stop()
229
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.
230
289
  if pid:
231
- print(f"stop requested — the background loop (pid {pid}) will pause after "
232
- "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}")
233
318
  else:
234
- 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
235
336
  return
236
337
 
237
338
  if cmd == "status":
@@ -239,12 +340,24 @@ def main() -> None:
239
340
  if pid:
240
341
  print(f"background loop: running (pid {pid})")
241
342
  print(f"logs: {_LOG_FILE}")
343
+ cur = _read_current()
344
+ if cur:
345
+ print(f"current ticket: {cur}")
242
346
  else:
243
347
  print("background loop: not running")
244
348
  print("stop pending: yes" if _stop_requested() else "stop pending: no")
245
349
  return
246
350
 
247
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
+
248
361
  # Refuse to start a second loop — one background daemon owns the To Do queue.
249
362
  pid = _daemon_running()
250
363
  if pid:
@@ -253,13 +366,6 @@ def main() -> None:
253
366
  print(" stop: agentos-auto stop")
254
367
  return
255
368
 
256
- # Keep non-technical users current: self-update, then re-run on the new
257
- # code. Runs in the FOREGROUND (before detaching) so the user sees update
258
- # output. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
259
- if "--no-update" not in argv:
260
- from sdk.updater import auto_update_and_reexec
261
- auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
262
-
263
369
  workspace_root = _parse_workspace_root(argv)
264
370
 
265
371
  # Background by default: detach so closing the terminal (SIGHUP) can't kill