forgexa-cli 1.13.1__tar.gz → 1.13.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.
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/PKG-INFO +1 -1
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli/daemon.py +33 -1
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli/main.py +31 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/pyproject.toml +1 -1
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/tests/test_auth_and_runtime_commands.py +36 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/README.md +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.13.1 → forgexa_cli-1.13.2}/setup.cfg +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.13.
|
|
2
|
+
__version__ = "1.13.2"
|
|
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
523
523
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
524
524
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
525
525
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
526
|
-
DAEMON_VERSION = "1.13.
|
|
526
|
+
DAEMON_VERSION = "1.13.2"
|
|
527
527
|
|
|
528
528
|
|
|
529
529
|
def _detect_client_type() -> str:
|
|
@@ -2792,8 +2792,11 @@ class ProcessManager:
|
|
|
2792
2792
|
msg_data = data.get("data") or {}
|
|
2793
2793
|
if isinstance(msg_data.get("content"), str) and msg_data["content"].strip():
|
|
2794
2794
|
has_result = True
|
|
2795
|
+
elif ev_type == "assistant.message_start":
|
|
2796
|
+
has_assistant_events = True
|
|
2795
2797
|
elif ev_type == "assistant.message_delta":
|
|
2796
2798
|
has_meaningful_content = True
|
|
2799
|
+
has_assistant_events = True
|
|
2797
2800
|
# ── Generic / Claude "result" event ────────────────────────────
|
|
2798
2801
|
elif ev_type == "result":
|
|
2799
2802
|
# Copilot result format: {"type":"result","exitCode":0,"usage":{...}}
|
|
@@ -2916,6 +2919,19 @@ class ProcessManager:
|
|
|
2916
2919
|
or signals["has_meaningful_content"]
|
|
2917
2920
|
)
|
|
2918
2921
|
|
|
2922
|
+
@staticmethod
|
|
2923
|
+
def _copilot_completed_without_result(stdout: str) -> bool:
|
|
2924
|
+
"""Detect Copilot runs that completed a turn but omitted the final result event."""
|
|
2925
|
+
signals = ProcessManager._extract_output_signals(stdout)
|
|
2926
|
+
return bool(
|
|
2927
|
+
signals["has_turn_completed"]
|
|
2928
|
+
and signals["has_assistant_events"]
|
|
2929
|
+
and signals["has_meaningful_content"]
|
|
2930
|
+
and not signals["has_result"]
|
|
2931
|
+
and not signals["has_turn_failed"]
|
|
2932
|
+
and not signals["error_messages"]
|
|
2933
|
+
)
|
|
2934
|
+
|
|
2919
2935
|
@staticmethod
|
|
2920
2936
|
def is_rate_limited(result: "TaskResult") -> bool:
|
|
2921
2937
|
"""Check if an agent failure warrants trying a different agent.
|
|
@@ -3930,6 +3946,7 @@ class ProcessManager:
|
|
|
3930
3946
|
# Copilot always exits 0 on normal completion; check result.exitCode
|
|
3931
3947
|
# from the JSONL "result" event for a true success signal.
|
|
3932
3948
|
copilot_exit = self._extract_copilot_exit_code(stdout)
|
|
3949
|
+
completed_without_result = self._copilot_completed_without_result(stdout)
|
|
3933
3950
|
effective_rc = copilot_exit if copilot_exit is not None else returncode
|
|
3934
3951
|
|
|
3935
3952
|
if effective_rc == 0 and returncode == 0:
|
|
@@ -3940,6 +3957,21 @@ class ProcessManager:
|
|
|
3940
3957
|
stderr=stderr[-10000:],
|
|
3941
3958
|
metrics=metrics,
|
|
3942
3959
|
)
|
|
3960
|
+
elif copilot_exit is None and returncode != 0 and completed_without_result:
|
|
3961
|
+
logger.warning(
|
|
3962
|
+
"Copilot exited with return code %s for task %s despite a completed assistant turn and no result event; recovering to validation path",
|
|
3963
|
+
returncode,
|
|
3964
|
+
task_id,
|
|
3965
|
+
)
|
|
3966
|
+
metrics["recovered_missing_result_event"] = True
|
|
3967
|
+
metrics["recovered_process_returncode"] = returncode
|
|
3968
|
+
return TaskResult(
|
|
3969
|
+
status="success",
|
|
3970
|
+
exit_code=0,
|
|
3971
|
+
stdout=stdout[-settings.AGENT_MAX_OUTPUT_SIZE:],
|
|
3972
|
+
stderr=stderr[-10000:],
|
|
3973
|
+
metrics=metrics,
|
|
3974
|
+
)
|
|
3943
3975
|
else:
|
|
3944
3976
|
return TaskResult(
|
|
3945
3977
|
status="failed",
|
|
@@ -247,6 +247,31 @@ def _is_virtualenv() -> bool:
|
|
|
247
247
|
)
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
def _is_pep668_environment() -> bool:
|
|
251
|
+
"""Return True if the active Python is governed by PEP 668 (externally-managed).
|
|
252
|
+
|
|
253
|
+
Debian 12, Ubuntu 22.10+/24.04+, Fedora 38+ and other modern distros place
|
|
254
|
+
an EXTERNALLY-MANAGED marker in their stdlib directory. pip refuses both
|
|
255
|
+
user-site and system-wide installs on these environments unless
|
|
256
|
+
--break-system-packages is explicitly passed — even ``pip install --user``
|
|
257
|
+
is blocked. We detect the marker up-front so ``forgexa upgrade`` can add
|
|
258
|
+
the flag automatically rather than failing and asking the user to do it.
|
|
259
|
+
"""
|
|
260
|
+
try:
|
|
261
|
+
import sysconfig as _sysconfig
|
|
262
|
+
stdlib = _sysconfig.get_path("stdlib")
|
|
263
|
+
if stdlib and (Path(stdlib) / "EXTERNALLY-MANAGED").exists():
|
|
264
|
+
return True
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
try:
|
|
268
|
+
if (Path(sys.prefix) / "EXTERNALLY-MANAGED").exists():
|
|
269
|
+
return True
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
|
|
250
275
|
def _is_user_site_install(dist_root: Path | None) -> bool:
|
|
251
276
|
if dist_root is None:
|
|
252
277
|
return False
|
|
@@ -355,6 +380,12 @@ def _build_upgrade_plan(target_version: str | None) -> UpgradePlan:
|
|
|
355
380
|
command = [python, "-m", "pip", "install", "--upgrade"]
|
|
356
381
|
if _is_user_site_install(dist_root) and not _is_virtualenv():
|
|
357
382
|
command.append("--user")
|
|
383
|
+
if _is_pep668_environment():
|
|
384
|
+
# PEP 668 (Ubuntu 24.04+, Debian 12+, Fedora 38+, …) blocks pip
|
|
385
|
+
# installs — including --user — unless this flag is passed. The
|
|
386
|
+
# original installation on this machine must have used it too, so
|
|
387
|
+
# adding it here is consistent and safe.
|
|
388
|
+
command.append("--break-system-packages")
|
|
358
389
|
command.append(_package_spec_for_version(target_version))
|
|
359
390
|
return UpgradePlan(
|
|
360
391
|
installer="pip",
|
|
@@ -244,6 +244,7 @@ def test_build_upgrade_plan_for_pip_user_site(monkeypatch: pytest.MonkeyPatch) -
|
|
|
244
244
|
monkeypatch.setattr(main.importlib.util, "find_spec", lambda name: object() if name == "pip" else None)
|
|
245
245
|
monkeypatch.setattr(main, "_is_user_site_install", lambda root: True)
|
|
246
246
|
monkeypatch.setattr(main, "_is_virtualenv", lambda: False)
|
|
247
|
+
monkeypatch.setattr(main, "_is_pep668_environment", lambda: False)
|
|
247
248
|
monkeypatch.setattr(main.sys, "executable", "/usr/bin/python3")
|
|
248
249
|
|
|
249
250
|
plan = main._build_upgrade_plan("1.12.3")
|
|
@@ -260,6 +261,41 @@ def test_build_upgrade_plan_for_pip_user_site(monkeypatch: pytest.MonkeyPatch) -
|
|
|
260
261
|
]
|
|
261
262
|
|
|
262
263
|
|
|
264
|
+
def test_build_upgrade_plan_adds_break_system_packages_on_pep668(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
265
|
+
monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.13.1"))
|
|
266
|
+
monkeypatch.setattr(main, "_distribution_root", lambda dist: Path("/usr/lib/python3/dist-packages"))
|
|
267
|
+
monkeypatch.setattr(main, "_running_from_distribution_root", lambda root: True)
|
|
268
|
+
monkeypatch.setattr(main, "_direct_url_metadata", lambda dist: None)
|
|
269
|
+
monkeypatch.setattr(main, "_looks_like_pipx_environment", lambda python=None: False)
|
|
270
|
+
monkeypatch.setattr(main.importlib.util, "find_spec", lambda name: object() if name == "pip" else None)
|
|
271
|
+
monkeypatch.setattr(main, "_is_user_site_install", lambda root: True)
|
|
272
|
+
monkeypatch.setattr(main, "_is_virtualenv", lambda: False)
|
|
273
|
+
monkeypatch.setattr(main, "_is_pep668_environment", lambda: True)
|
|
274
|
+
monkeypatch.setattr(main.sys, "executable", "/usr/bin/python3")
|
|
275
|
+
|
|
276
|
+
plan = main._build_upgrade_plan(None)
|
|
277
|
+
|
|
278
|
+
assert "--break-system-packages" in plan.command
|
|
279
|
+
assert "--user" in plan.command
|
|
280
|
+
assert plan.command[-1] == "forgexa-cli"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_pep668_detection_returns_false_in_normal_venv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
284
|
+
monkeypatch.setattr(main.sys, "prefix", str(tmp_path))
|
|
285
|
+
import sysconfig as _sc
|
|
286
|
+
monkeypatch.setattr(_sc, "get_path", lambda name, **kw: str(tmp_path))
|
|
287
|
+
|
|
288
|
+
assert main._is_pep668_environment() is False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_pep668_detection_returns_true_when_marker_present(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
292
|
+
(tmp_path / "EXTERNALLY-MANAGED").write_text("[externally-managed]\n")
|
|
293
|
+
import sysconfig as _sc
|
|
294
|
+
monkeypatch.setattr(_sc, "get_path", lambda name, **kw: str(tmp_path))
|
|
295
|
+
|
|
296
|
+
assert main._is_pep668_environment() is True
|
|
297
|
+
|
|
298
|
+
|
|
263
299
|
def test_build_upgrade_plan_rejects_editable_install(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
264
300
|
monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.12.2"))
|
|
265
301
|
monkeypatch.setattr(main, "_distribution_root", lambda dist: Path("/installed/site-packages"))
|
|
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
|