forgexa-cli 1.37.4__tar.gz → 1.38.0__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.37.4 → forgexa_cli-1.38.0}/PKG-INFO +1 -1
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/daemon.py +70 -12
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/pyproject.toml +1 -1
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/README.md +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/setup.cfg +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.37.4 → forgexa_cli-1.38.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.38.0"
|
|
@@ -910,7 +910,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
910
910
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
911
911
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
912
912
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
913
|
-
DAEMON_VERSION = "1.
|
|
913
|
+
DAEMON_VERSION = "1.38.0"
|
|
914
914
|
|
|
915
915
|
|
|
916
916
|
def _detect_client_type() -> str:
|
|
@@ -1245,6 +1245,8 @@ class TaskResult:
|
|
|
1245
1245
|
observations: list[dict] = field(default_factory=list)
|
|
1246
1246
|
metrics: dict = field(default_factory=dict)
|
|
1247
1247
|
git: dict = field(default_factory=dict)
|
|
1248
|
+
# Set from the complete agent stream before stdout is retained as a bounded tail.
|
|
1249
|
+
has_verified_completion: bool = False
|
|
1248
1250
|
|
|
1249
1251
|
|
|
1250
1252
|
def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
@@ -2833,24 +2835,68 @@ class WorkspaceManager:
|
|
|
2833
2835
|
)
|
|
2834
2836
|
|
|
2835
2837
|
try:
|
|
2836
|
-
await self._git(
|
|
2837
|
-
"
|
|
2838
|
+
remote_branch = await self._git(
|
|
2839
|
+
"ls-remote", "--heads", "origin", f"refs/heads/{target_branch}",
|
|
2838
2840
|
cwd=local_path,
|
|
2839
2841
|
)
|
|
2840
2842
|
except RuntimeError as exc:
|
|
2841
2843
|
raise RuntimeError(
|
|
2842
|
-
f"local_binding_unavailable: failed to
|
|
2844
|
+
f"local_binding_unavailable: failed to inspect '{target_branch}' on origin: {exc}"
|
|
2843
2845
|
) from exc
|
|
2844
2846
|
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2847
|
+
if remote_branch.strip():
|
|
2848
|
+
try:
|
|
2849
|
+
await self._git(
|
|
2850
|
+
"fetch", "origin", f"{target_branch}:refs/remotes/origin/{target_branch}",
|
|
2851
|
+
cwd=local_path,
|
|
2852
|
+
)
|
|
2853
|
+
except RuntimeError as exc:
|
|
2854
|
+
raise RuntimeError(
|
|
2855
|
+
f"local_binding_unavailable: failed to fetch '{target_branch}' from origin: {exc}"
|
|
2856
|
+
) from exc
|
|
2857
|
+
|
|
2858
|
+
ahead_output = await self._git(
|
|
2859
|
+
"rev-list", f"origin/{target_branch}..HEAD", "--count", cwd=local_path,
|
|
2860
|
+
)
|
|
2861
|
+
if ahead_output.strip() not in ("", "0"):
|
|
2862
|
+
raise RuntimeError(
|
|
2863
|
+
f"local_binding_unavailable: local branch '{target_branch}' has "
|
|
2864
|
+
f"{ahead_output.strip()} unpushed commit(s) ahead of origin — push them "
|
|
2865
|
+
"yourself first, Forgexa will not force-sync your real directory."
|
|
2866
|
+
)
|
|
2867
|
+
else:
|
|
2868
|
+
can_bootstrap_branch = (
|
|
2869
|
+
task.node_type == "analysis"
|
|
2870
|
+
and (task.input_data or {}).get("analysis_mode") == "fresh"
|
|
2871
|
+
and not task.required_commit_shas
|
|
2853
2872
|
)
|
|
2873
|
+
if not can_bootstrap_branch:
|
|
2874
|
+
raise RuntimeError(
|
|
2875
|
+
f"local_binding_unavailable: remote branch '{target_branch}' does not exist; "
|
|
2876
|
+
"only a fresh initial analysis may bootstrap it"
|
|
2877
|
+
)
|
|
2878
|
+
|
|
2879
|
+
default_branch = str((project or {}).get("default_branch") or "main").strip() or "main"
|
|
2880
|
+
try:
|
|
2881
|
+
await self._git(
|
|
2882
|
+
"fetch", "origin", f"{default_branch}:refs/remotes/origin/{default_branch}",
|
|
2883
|
+
cwd=local_path,
|
|
2884
|
+
)
|
|
2885
|
+
local_head = (await self._git("rev-parse", "HEAD", cwd=local_path)).strip()
|
|
2886
|
+
default_head = (
|
|
2887
|
+
await self._git("rev-parse", f"origin/{default_branch}", cwd=local_path)
|
|
2888
|
+
).strip()
|
|
2889
|
+
except RuntimeError as exc:
|
|
2890
|
+
raise RuntimeError(
|
|
2891
|
+
"local_binding_unavailable: failed to verify initial branch against "
|
|
2892
|
+
f"origin/{default_branch}: {exc}"
|
|
2893
|
+
) from exc
|
|
2894
|
+
if local_head != default_head:
|
|
2895
|
+
raise RuntimeError(
|
|
2896
|
+
f"local_binding_unavailable: remote branch '{target_branch}' does not exist and "
|
|
2897
|
+
f"local branch is not at origin/{default_branch}; refusing to bootstrap from "
|
|
2898
|
+
"unpushed or stale history"
|
|
2899
|
+
)
|
|
2854
2900
|
|
|
2855
2901
|
# Cross-phase handoff verification (docs/designs/forgexa-ide-
|
|
2856
2902
|
# design.md §6.7): every direct predecessor's commit SHA must
|
|
@@ -4745,6 +4791,8 @@ class ProcessManager:
|
|
|
4745
4791
|
@staticmethod
|
|
4746
4792
|
def has_verified_agent_completion(result: "TaskResult") -> bool:
|
|
4747
4793
|
"""Return True when structured output proves the agent completed a real turn."""
|
|
4794
|
+
if result.has_verified_completion:
|
|
4795
|
+
return True
|
|
4748
4796
|
combined = "\n".join(part for part in (result.stdout, result.stderr) if part)
|
|
4749
4797
|
signals = ProcessManager._extract_output_signals(combined)
|
|
4750
4798
|
return bool(
|
|
@@ -5927,6 +5975,13 @@ class ProcessManager:
|
|
|
5927
5975
|
and not signals["has_turn_failed"]
|
|
5928
5976
|
and not signals["error_messages"]
|
|
5929
5977
|
)
|
|
5978
|
+
has_verified_completion = bool(
|
|
5979
|
+
signals["has_assistant_events"]
|
|
5980
|
+
and signals["has_meaningful_content"]
|
|
5981
|
+
and (signals["has_result"] or signals["has_turn_completed"])
|
|
5982
|
+
and not signals["has_turn_failed"]
|
|
5983
|
+
and not signals["error_messages"]
|
|
5984
|
+
)
|
|
5930
5985
|
structured_errors = [
|
|
5931
5986
|
str(msg).strip()
|
|
5932
5987
|
for msg in (signals.get("error_messages") or [])
|
|
@@ -5948,6 +6003,7 @@ class ProcessManager:
|
|
|
5948
6003
|
stdout=stdout[-settings.AGENT_MAX_OUTPUT_SIZE:],
|
|
5949
6004
|
stderr=stderr[-10000:],
|
|
5950
6005
|
metrics=metrics,
|
|
6006
|
+
has_verified_completion=has_verified_completion,
|
|
5951
6007
|
)
|
|
5952
6008
|
elif not copilot_specific_error and copilot_exit is None and completed_without_result:
|
|
5953
6009
|
if returncode != 0:
|
|
@@ -5964,6 +6020,7 @@ class ProcessManager:
|
|
|
5964
6020
|
stdout=stdout[-settings.AGENT_MAX_OUTPUT_SIZE:],
|
|
5965
6021
|
stderr=stderr[-10000:],
|
|
5966
6022
|
metrics=metrics,
|
|
6023
|
+
has_verified_completion=has_verified_completion,
|
|
5967
6024
|
)
|
|
5968
6025
|
else:
|
|
5969
6026
|
# --- Immediate-failure retry ladder ---
|
|
@@ -6486,6 +6543,7 @@ class ProgressReporter:
|
|
|
6486
6543
|
"exit_code": result.exit_code,
|
|
6487
6544
|
"stdout_tail": result.stdout[-20000:] if result.stdout else "",
|
|
6488
6545
|
"stderr_tail": result.stderr[-5000:] if result.stderr else "",
|
|
6546
|
+
"verified_completion": result.has_verified_completion,
|
|
6489
6547
|
"error": result.error,
|
|
6490
6548
|
"failure_code": result.failure_code,
|
|
6491
6549
|
"preflight": result.preflight,
|
|
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
|