devops-bot-sdk 1.4.19__tar.gz → 1.4.22__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.
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/client.py +56 -1
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/ipc/handlers.py +102 -5
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/local_exec.py +14 -2
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/README.md +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.22"
|
|
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.
|
|
3
|
+
Version: 1.4.22
|
|
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.
|
|
33
|
+
__version__ = "1.4.22"
|
|
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.
|
|
39
|
+
SDK_VERSION = "1.4.22"
|
|
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
|
|
@@ -685,11 +685,66 @@ class BackendClient:
|
|
|
685
685
|
logger.warning("client.move_to_backlog failed task=%s status=%s body=%s",
|
|
686
686
|
task_id, resp.status_code, resp.text[:200])
|
|
687
687
|
return False
|
|
688
|
+
await self.log_activity(f"Task {task_id}: status → Backlog")
|
|
688
689
|
return True
|
|
689
690
|
except Exception as exc: # noqa: BLE001
|
|
690
691
|
logger.warning("client.move_to_backlog error task=%s err=%s", task_id, exc)
|
|
691
692
|
return False
|
|
692
693
|
|
|
694
|
+
async def mark_in_progress(self, task_id: str) -> bool:
|
|
695
|
+
"""PUT {BE}/tasks-ml/{id} status=In Progress (best-effort).
|
|
696
|
+
|
|
697
|
+
Called at the start of processing to move the ticket from To Do to
|
|
698
|
+
In Progress. Returns True on success; logs and returns False otherwise.
|
|
699
|
+
"""
|
|
700
|
+
from sdk.config import BE_DEFAULT_URL
|
|
701
|
+
|
|
702
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}"
|
|
703
|
+
headers = {
|
|
704
|
+
k: v for k, v in self._headers.items() if k.lower() != "content-type"
|
|
705
|
+
}
|
|
706
|
+
headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
|
|
707
|
+
try:
|
|
708
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
709
|
+
resp = await client.put(url, files={"status": (None, "In Progress")}, headers=headers)
|
|
710
|
+
if resp.status_code >= 400:
|
|
711
|
+
logger.warning("client.mark_in_progress failed task=%s status=%s body=%s",
|
|
712
|
+
task_id, resp.status_code, resp.text[:200])
|
|
713
|
+
return False
|
|
714
|
+
await self.log_activity(f"Task {task_id}: status → In Progress")
|
|
715
|
+
return True
|
|
716
|
+
except Exception as exc: # noqa: BLE001
|
|
717
|
+
logger.warning("client.mark_in_progress error task=%s err=%s", task_id, exc)
|
|
718
|
+
return False
|
|
719
|
+
|
|
720
|
+
async def log_activity(self, activity: str) -> bool:
|
|
721
|
+
"""POST /ml-api/agent-activity — emit an agent activity event (best-effort).
|
|
722
|
+
|
|
723
|
+
Called on status transitions (In Progress, Backlog), every 4 files written
|
|
724
|
+
during local execution, and on errors. Never raises so a log failure can't
|
|
725
|
+
break a run.
|
|
726
|
+
"""
|
|
727
|
+
from sdk.config import BE_DEFAULT_URL
|
|
728
|
+
|
|
729
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/ml-api/agent-activity"
|
|
730
|
+
headers = {
|
|
731
|
+
**self._headers,
|
|
732
|
+
"accept": "*/*",
|
|
733
|
+
"developer-name": "null",
|
|
734
|
+
"product": "",
|
|
735
|
+
}
|
|
736
|
+
try:
|
|
737
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
738
|
+
resp = await client.post(url, json={"activity": str(activity)}, headers=headers)
|
|
739
|
+
if resp.status_code >= 400:
|
|
740
|
+
logger.warning("client.log_activity failed status=%s body=%s",
|
|
741
|
+
resp.status_code, resp.text[:200])
|
|
742
|
+
return False
|
|
743
|
+
return True
|
|
744
|
+
except Exception as exc: # noqa: BLE001
|
|
745
|
+
logger.warning("client.log_activity error err=%s", exc)
|
|
746
|
+
return False
|
|
747
|
+
|
|
693
748
|
async def notify_whatsapp(
|
|
694
749
|
self,
|
|
695
750
|
task_id: str,
|
|
@@ -438,7 +438,10 @@ async def _code_one_repo(
|
|
|
438
438
|
# 0. Make sure the repo is present (clone from repo_url only if missing/empty).
|
|
439
439
|
await git_ops.ensure_repo(repo_path, repo_url, gh_token)
|
|
440
440
|
|
|
441
|
+
_write_count = 0
|
|
442
|
+
|
|
441
443
|
async def _on_event(event: dict) -> None:
|
|
444
|
+
nonlocal _write_count
|
|
442
445
|
if event.get("type") == "assistant":
|
|
443
446
|
for c in event.get("message", {}).get("content", []):
|
|
444
447
|
if c.get("type") == "tool_use":
|
|
@@ -450,6 +453,14 @@ async def _code_one_repo(
|
|
|
450
453
|
data={"status": "running", "current_step": label},
|
|
451
454
|
).model_dump())
|
|
452
455
|
if tname in ("Write", "Edit", "MultiEdit"):
|
|
456
|
+
_write_count += 1
|
|
457
|
+
if _write_count % 4 == 0:
|
|
458
|
+
try:
|
|
459
|
+
await client.log_activity(
|
|
460
|
+
f"Task {task_id} [{name}]: {_write_count} files written"
|
|
461
|
+
)
|
|
462
|
+
except Exception: # noqa: BLE001
|
|
463
|
+
pass
|
|
453
464
|
try:
|
|
454
465
|
from sdk import graphify
|
|
455
466
|
graphify.schedule_update(repo_path)
|
|
@@ -659,6 +670,11 @@ async def handle_pipeline_approve(
|
|
|
659
670
|
return
|
|
660
671
|
|
|
661
672
|
# ── Approved: run the local claude CLI on EACH repo the ticket names ──
|
|
673
|
+
# Mark back to In Progress — covers both direct approval in the auto-loop
|
|
674
|
+
# and a resume that arrives after the ticket sat in Backlog waiting for
|
|
675
|
+
# the human's WhatsApp reply or a usage-limit window reset.
|
|
676
|
+
await client.mark_in_progress(task_id)
|
|
677
|
+
|
|
662
678
|
# The prompt is built per-repo inside _code_one_repo so its framing matches
|
|
663
679
|
# whether that target is a git repo (branch + PR) or a plain local folder.
|
|
664
680
|
agent_mode = bool(pending.get("agent_mode", True))
|
|
@@ -765,10 +781,13 @@ async def handle_pipeline_approve(
|
|
|
765
781
|
await client.notify_whatsapp(task_id, note, summary="AgentOS local run alert")
|
|
766
782
|
except Exception: # noqa: BLE001
|
|
767
783
|
pass
|
|
784
|
+
await client.log_activity(f"Task {task_id}: local execution failed — {err[:300]}")
|
|
785
|
+
await client.move_to_backlog(task_id)
|
|
768
786
|
await send(Envelope(
|
|
769
787
|
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
770
788
|
data={"code": "LOCAL_EXEC_FAILED", "message": err[:300], "task_id": task_id},
|
|
771
789
|
).model_dump())
|
|
790
|
+
return
|
|
772
791
|
|
|
773
792
|
# ── Finalize on the server once per task (validation + Jira/cost) ─────
|
|
774
793
|
await send(Envelope(
|
|
@@ -786,6 +805,8 @@ async def handle_pipeline_approve(
|
|
|
786
805
|
},
|
|
787
806
|
)
|
|
788
807
|
except Exception as exc:
|
|
808
|
+
await client.log_activity(f"Task {task_id}: server finalization failed — {str(exc)[:200]}")
|
|
809
|
+
await client.move_to_backlog(task_id)
|
|
789
810
|
await send(_error_envelope(thread_id, exc).model_dump())
|
|
790
811
|
return
|
|
791
812
|
|
|
@@ -1500,9 +1521,11 @@ def _resolve_local_project_path(task: dict, payload: dict) -> str:
|
|
|
1500
1521
|
return str(Path(root).expanduser() / key)
|
|
1501
1522
|
|
|
1502
1523
|
|
|
1503
|
-
# Approval polling cadence for the first-approval gate (agentMode=true).
|
|
1504
|
-
#
|
|
1524
|
+
# Approval polling cadence for the first-approval gate (agentMode=true).
|
|
1525
|
+
# _FIRST_GATE_TIMEOUT_S: ticket moves to Backlog after 15 min with no reply.
|
|
1526
|
+
# _APPROVAL_POLL_TIMEOUT_S: longer timeout kept for mid-run crucial-decision gates.
|
|
1505
1527
|
_APPROVAL_POLL_INTERVAL_S = 15.0
|
|
1528
|
+
_FIRST_GATE_TIMEOUT_S = 900.0
|
|
1506
1529
|
_APPROVAL_POLL_TIMEOUT_S = 1200.0
|
|
1507
1530
|
# Max times a single ticket's session is resumed after a context overflow.
|
|
1508
1531
|
_MAX_CONTEXT_RESUMES = 3
|
|
@@ -1551,9 +1574,10 @@ async def _poll_approval(
|
|
|
1551
1574
|
|
|
1552
1575
|
Returns 'approved' | 'rejected' | 'stopped' | 'timeout'. The decision is the
|
|
1553
1576
|
human's WhatsApp reply, recorded backend-side under approval_log:{task_id}.
|
|
1577
|
+
Times out after _FIRST_GATE_TIMEOUT_S (15 min) so the ticket can move to Backlog.
|
|
1554
1578
|
"""
|
|
1555
1579
|
waited = 0.0
|
|
1556
|
-
while waited <
|
|
1580
|
+
while waited < _FIRST_GATE_TIMEOUT_S:
|
|
1557
1581
|
if should_stop is not None and should_stop():
|
|
1558
1582
|
return "stopped"
|
|
1559
1583
|
status = await client.approval_status(task_id)
|
|
@@ -1589,6 +1613,53 @@ async def _poll_fresh_approval(
|
|
|
1589
1613
|
return "timeout"
|
|
1590
1614
|
|
|
1591
1615
|
|
|
1616
|
+
async def _background_approval_watcher(
|
|
1617
|
+
client: BackendClient,
|
|
1618
|
+
session_id: str,
|
|
1619
|
+
task_id: str,
|
|
1620
|
+
jira_key: str,
|
|
1621
|
+
thread_id: str,
|
|
1622
|
+
send: Callable,
|
|
1623
|
+
should_stop: Callable[[], bool] | None,
|
|
1624
|
+
) -> None:
|
|
1625
|
+
"""Poll indefinitely for a late approval on a timed-out gate, then resume.
|
|
1626
|
+
|
|
1627
|
+
Spawned as a fire-and-forget task when the first-gate 15-min timeout fires.
|
|
1628
|
+
When the user approves (via WhatsApp), calls handle_pipeline_approve so the
|
|
1629
|
+
ticket resumes from exactly where it paused. handle_pipeline_approve already
|
|
1630
|
+
calls mark_in_progress, so the Jira status flips In Progress → Backlog →
|
|
1631
|
+
In Progress automatically. Cleans up _PENDING_LOCAL on any terminal outcome.
|
|
1632
|
+
"""
|
|
1633
|
+
try:
|
|
1634
|
+
while True:
|
|
1635
|
+
if should_stop is not None and should_stop():
|
|
1636
|
+
return
|
|
1637
|
+
status = await client.approval_status(task_id)
|
|
1638
|
+
if status == "approved":
|
|
1639
|
+
break
|
|
1640
|
+
if status == "rejected":
|
|
1641
|
+
await send(Envelope(
|
|
1642
|
+
type="step_update", thread_id=thread_id,
|
|
1643
|
+
data={"current_step": f"{jira_key}: rejected (late approval) — skipped"},
|
|
1644
|
+
).model_dump())
|
|
1645
|
+
return
|
|
1646
|
+
await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
|
|
1647
|
+
# Approval arrived — move back to In Progress and resume execution.
|
|
1648
|
+
await send(Envelope(
|
|
1649
|
+
type="step_update", thread_id=thread_id,
|
|
1650
|
+
data={"current_step": f"{jira_key}: late approval received — resuming from Backlog"},
|
|
1651
|
+
).model_dump())
|
|
1652
|
+
await handle_pipeline_approve(
|
|
1653
|
+
client,
|
|
1654
|
+
{"session_id": session_id, "decision": "approved", "thread_id": thread_id},
|
|
1655
|
+
send,
|
|
1656
|
+
)
|
|
1657
|
+
except Exception as exc: # noqa: BLE001
|
|
1658
|
+
logger.warning("background_approval_watcher.failed task=%s err=%s", task_id, exc)
|
|
1659
|
+
finally:
|
|
1660
|
+
_PENDING_LOCAL.pop(session_id, None)
|
|
1661
|
+
|
|
1662
|
+
|
|
1592
1663
|
async def handle_orchestrate_auto_local_run(
|
|
1593
1664
|
client: BackendClient, payload: dict, send: Callable,
|
|
1594
1665
|
should_stop: Callable[[], bool] | None = None,
|
|
@@ -1680,6 +1751,9 @@ async def handle_orchestrate_auto_local_run(
|
|
|
1680
1751
|
attempt = 0
|
|
1681
1752
|
while True:
|
|
1682
1753
|
attempt += 1
|
|
1754
|
+
# Mark In Progress before any work begins so Jira reflects
|
|
1755
|
+
# the real state even if the run fails partway through.
|
|
1756
|
+
await client.mark_in_progress(tid)
|
|
1683
1757
|
# Phase 1 — server plans + gates. NO local code/bash runs yet.
|
|
1684
1758
|
await _run_local_pipeline(client, task_payload, send)
|
|
1685
1759
|
session_id = next(
|
|
@@ -1711,6 +1785,20 @@ async def handle_orchestrate_auto_local_run(
|
|
|
1711
1785
|
"processed": processed, "task_count": total},
|
|
1712
1786
|
).model_dump())
|
|
1713
1787
|
return
|
|
1788
|
+
if decision == "timeout":
|
|
1789
|
+
# 15 min elapsed — move to Backlog but keep _PENDING_LOCAL
|
|
1790
|
+
# alive, then spawn a background watcher that polls until
|
|
1791
|
+
# the user approves and resumes execution automatically.
|
|
1792
|
+
await client.move_to_backlog(tid)
|
|
1793
|
+
await send(Envelope(
|
|
1794
|
+
type="step_update", thread_id=thread_id,
|
|
1795
|
+
data={"current_step": f"{jira_key}: no approval in 15m — moved to Backlog"
|
|
1796
|
+
" (background watcher will auto-resume on approval)"},
|
|
1797
|
+
).model_dump())
|
|
1798
|
+
asyncio.ensure_future(_background_approval_watcher(
|
|
1799
|
+
client, session_id, tid, jira_key, thread_id, send, should_stop,
|
|
1800
|
+
))
|
|
1801
|
+
break
|
|
1714
1802
|
if decision != "approved":
|
|
1715
1803
|
_PENDING_LOCAL.pop(session_id, None)
|
|
1716
1804
|
await send(Envelope(
|
|
@@ -1755,10 +1843,19 @@ async def handle_orchestrate_auto_local_run(
|
|
|
1755
1843
|
processed += 1
|
|
1756
1844
|
break
|
|
1757
1845
|
except Exception as exc: # noqa: BLE001 — isolate per-ticket failures
|
|
1758
|
-
|
|
1846
|
+
# str(exc) is EMPTY for some exceptions (notably TimeoutError /
|
|
1847
|
+
# asyncio.TimeoutError / CancelledError), which used to render as a
|
|
1848
|
+
# blank "failed — (continuing)" with no clue to the cause. Always
|
|
1849
|
+
# prefix the exception type so the real reason is visible; log the
|
|
1850
|
+
# full repr for after-the-fact debugging.
|
|
1851
|
+
detail = f"{type(exc).__name__}: {exc}".rstrip(": ")
|
|
1852
|
+
logger.warning("orchestrate_auto.task_failed",
|
|
1853
|
+
extra={"task_id": tid, "error": repr(exc)})
|
|
1854
|
+
await client.log_activity(f"Task {tid}: unexpected error — {detail[:200]}")
|
|
1855
|
+
await client.move_to_backlog(tid)
|
|
1759
1856
|
await send(Envelope(
|
|
1760
1857
|
type="step_update", thread_id=thread_id,
|
|
1761
|
-
data={"current_step": f"{jira_key or tid}: failed — {
|
|
1858
|
+
data={"current_step": f"{jira_key or tid}: failed — {detail[:160]} (continuing)"},
|
|
1762
1859
|
).model_dump())
|
|
1763
1860
|
continue
|
|
1764
1861
|
|
|
@@ -22,6 +22,7 @@ import json
|
|
|
22
22
|
import os
|
|
23
23
|
import re
|
|
24
24
|
import shutil
|
|
25
|
+
import sys
|
|
25
26
|
from datetime import datetime
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
from typing import Any, Awaitable, Callable
|
|
@@ -242,7 +243,6 @@ async def run_claude_local(
|
|
|
242
243
|
# destructive tool calls are blocked until a human approves (the guard reads
|
|
243
244
|
# AGENTOS_APPROVAL_DIR set below). Merges with any global/project hooks.
|
|
244
245
|
if approval_dir:
|
|
245
|
-
import sys
|
|
246
246
|
from sdk import crucial
|
|
247
247
|
settings_path = os.path.join(approval_dir, "claude_settings.json")
|
|
248
248
|
try:
|
|
@@ -271,8 +271,20 @@ async def run_claude_local(
|
|
|
271
271
|
if approval_dir:
|
|
272
272
|
env["AGENTOS_APPROVAL_DIR"] = approval_dir
|
|
273
273
|
|
|
274
|
+
# On Windows, npm/nvm install the CLI as a `.cmd`/`.bat` batch shim (e.g.
|
|
275
|
+
# claude.CMD). `create_subprocess_exec` calls CreateProcess directly — with NO
|
|
276
|
+
# command processor — and a batch shim launched that way can fail to bootstrap
|
|
277
|
+
# on some setups (notably nvm-for-Windows, where node lives behind a junction):
|
|
278
|
+
# it starts, emits nothing, and exits, surfacing here as "no result event".
|
|
279
|
+
# Route the shim through %COMSPEC% so it runs in a real cmd.exe exactly as an
|
|
280
|
+
# interactive shell would. Native `.exe` installs and POSIX paths spawn directly.
|
|
281
|
+
spawn_cmd = cmd
|
|
282
|
+
if sys.platform == "win32" and cli.lower().endswith((".cmd", ".bat")):
|
|
283
|
+
comspec = os.environ.get("COMSPEC", "cmd.exe")
|
|
284
|
+
spawn_cmd = [comspec, "/c", *cmd]
|
|
285
|
+
|
|
274
286
|
proc = await asyncio.create_subprocess_exec(
|
|
275
|
-
*
|
|
287
|
+
*spawn_cmd,
|
|
276
288
|
cwd=str(path),
|
|
277
289
|
stdin=asyncio.subprocess.PIPE,
|
|
278
290
|
stdout=asyncio.subprocess.PIPE,
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.19 → devops_bot_sdk-1.4.22}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
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
|