forgexa-cli 1.57.7__tar.gz → 1.58.1__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 (29) hide show
  1. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/agent_core.py +43 -0
  4. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/daemon.py +58 -11
  5. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/README.md +0 -0
  8. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.7 → forgexa_cli-1.58.1}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.7
3
+ Version: 1.58.1
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.57.7"
2
+ __version__ = "1.58.1"
@@ -816,6 +816,30 @@ class AgentDiscovery:
816
816
  os.environ["PATH"] = os.pathsep.join(additions) + os.pathsep + current
817
817
  logger.debug("Expanded PATH with: %s", ", ".join(additions))
818
818
 
819
+ @staticmethod
820
+ def _wsl_windows_executable(path: str) -> bool:
821
+ """Reject a Windows-side agent before its version probe can run via WSL interop."""
822
+ if sys.platform != "linux" or not os.environ.get("WSL_DISTRO_NAME"):
823
+ return False
824
+ resolved = Path(path).resolve()
825
+ if resolved.suffix.lower() in {".exe", ".cmd", ".bat"}:
826
+ return True
827
+ parts = resolved.parts
828
+ if len(parts) >= 3 and parts[1] == "mnt" and len(parts[2]) == 1 and parts[2].isalpha():
829
+ return True
830
+ # A custom DrvFs mount need not live under /mnt/<drive>.
831
+ try:
832
+ mounts = Path("/proc/mounts").read_text(encoding="utf-8")
833
+ except OSError:
834
+ return False
835
+ for line in mounts.splitlines():
836
+ fields = line.split()
837
+ if len(fields) >= 3 and fields[2] in {"9p", "drvfs"}:
838
+ mount = Path(fields[1].replace("\\040", " "))
839
+ if resolved == mount or mount in resolved.parents:
840
+ return True
841
+ return False
842
+
819
843
  async def discover(self) -> list[DiscoveredAgent]:
820
844
  self._expand_path()
821
845
  # Probe bwrap support once at discovery time and log a clear warning
@@ -827,6 +851,9 @@ class AgentDiscovery:
827
851
  cmd = custom_path or spec["commands"][0]
828
852
  resolved = shutil.which(cmd)
829
853
  if resolved:
854
+ if self._wsl_windows_executable(resolved):
855
+ logger.warning("Agent %s resolved only to a Windows executable inside WSL; skipping", agent_id)
856
+ continue
830
857
  version = await self._get_version(spec["detect"], resolved)
831
858
  if version is None and "detect_alt" in spec:
832
859
  # Primary detect command failed; try the alternative.
@@ -877,6 +904,8 @@ class AgentDiscovery:
877
904
  "binary found but not functional".
878
905
  """
879
906
  import re
907
+ probe_dir = None
908
+ proc = None
880
909
  try:
881
910
  parts = detect_cmd.split()
882
911
  if executable:
@@ -886,11 +915,17 @@ class AgentDiscovery:
886
915
  if resolved:
887
916
  parts[0] = resolved
888
917
  parts = _prepare_agent_command(parts, preserve_argv=False)
918
+ env = None
919
+ if detect_cmd in ("opencode --version", "mimo --version"):
920
+ probe_dir = tempfile.TemporaryDirectory(prefix=f"forgexa-{detect_cmd.split()[0]}-probe-")
921
+ env = os.environ.copy()
922
+ env.update(TMPDIR=probe_dir.name, BUN_TMPDIR=probe_dir.name)
889
923
  proc = await asyncio.create_subprocess_exec(
890
924
  *parts,
891
925
  stdin=asyncio.subprocess.DEVNULL,
892
926
  stdout=asyncio.subprocess.PIPE,
893
927
  stderr=asyncio.subprocess.PIPE,
928
+ env=env,
894
929
  **_agent_process_group_kwargs(),
895
930
  )
896
931
  stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10)
@@ -918,11 +953,19 @@ class AgentDiscovery:
918
953
  )
919
954
  return None
920
955
  return output
956
+ except asyncio.CancelledError:
957
+ raise
921
958
  except Exception as exc:
922
959
  logger.warning(
923
960
  "Version check for %r raised %s: %s", detect_cmd, type(exc).__name__, exc,
924
961
  )
925
962
  return None
963
+ finally:
964
+ if proc is not None and proc.returncode is None:
965
+ _kill_proc(proc)
966
+ await proc.wait()
967
+ if probe_dir is not None:
968
+ probe_dir.cleanup()
926
969
 
927
970
  @staticmethod
928
971
  async def _probe_bwrap_support() -> None:
@@ -1150,7 +1150,7 @@ except (ImportError, ModuleNotFoundError):
1150
1150
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1151
1151
  # Kept in sync with pyproject.toml version via bump-version.sh.
1152
1152
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1153
- DAEMON_VERSION = "1.57.7"
1153
+ DAEMON_VERSION = "1.58.1"
1154
1154
 
1155
1155
 
1156
1156
  def _detect_client_type() -> str:
@@ -2899,11 +2899,21 @@ class WorkspaceManager:
2899
2899
  self._ssh_keys[project_key] = ssh_private_key
2900
2900
 
2901
2901
  @staticmethod
2902
- def _summarize_git_error(error: Exception) -> str:
2902
+ def _summarize_git_error(error: Exception, *, tail: bool = False) -> str:
2903
2903
  """Return a compact Git error without exposing embedded HTTPS credentials."""
2904
2904
  detail = " ".join(str(error).split())
2905
2905
  detail = re.sub(r"(https?://)[^/@\s]+@", r"\1***@", detail)
2906
- return detail[:500]
2906
+ return detail[-500:] if tail else detail[:500]
2907
+
2908
+ @staticmethod
2909
+ def _git_push_diagnostic(error: Exception) -> str:
2910
+ detail = str(error).replace("\r\n", "\n").replace("\r", "\n")
2911
+ detail = re.sub(r"(https?://)[^/@\s]+@", r"\1***@", detail)
2912
+ if len(detail) <= 16_000:
2913
+ return detail
2914
+ beginning, ending = detail[:1000], detail[-14_800:]
2915
+ omitted = len(detail) - len(beginning) - len(ending)
2916
+ return f"{beginning}\n... [{omitted} earlier characters omitted] ...\n{ending}"
2907
2917
 
2908
2918
  def _migrate_legacy_project_dirs(self) -> None:
2909
2919
  """Move project directories from root/ to root/projects/ (one-time migration).
@@ -6699,6 +6709,7 @@ class ProcessManager:
6699
6709
  data_home=tmp_data_root,
6700
6710
  require_windows_shell=True,
6701
6711
  )
6712
+ env.update(TMPDIR=tmp_data_root, BUN_TMPDIR=tmp_data_root)
6702
6713
  except RuntimeError as exc:
6703
6714
  return TaskResult(
6704
6715
  status="failed",
@@ -6792,6 +6803,7 @@ class ProcessManager:
6792
6803
  prompt_file=prompt_file,
6793
6804
  )
6794
6805
  env = agent_core.mimo_base_env(os.environ.copy())
6806
+ env.update(TMPDIR=tmp_root, BUN_TMPDIR=tmp_root)
6795
6807
  result = await self._run_cli(
6796
6808
  cmd, cwd, timeout, task_id, on_chunk=on_chunk, env=env,
6797
6809
  )
@@ -7900,6 +7912,7 @@ class ProcessManager:
7900
7912
  env: dict | None = None,
7901
7913
  stream_stderr: bool = False,
7902
7914
  ) -> TaskResult:
7915
+ proc = None
7903
7916
  try:
7904
7917
  cmd = _prepare_agent_command(cmd)
7905
7918
  proc = await asyncio.create_subprocess_exec(
@@ -7938,7 +7951,8 @@ class ProcessManager:
7938
7951
  failure_code="agent_launch_not_found",
7939
7952
  )
7940
7953
  except asyncio.TimeoutError as exc:
7941
- _kill_proc(self.active_processes.pop(task_id, None) or proc)
7954
+ if proc is not None:
7955
+ _kill_proc(proc)
7942
7956
  _err = (
7943
7957
  f"Agent produced no stdout, stderr, or workspace changes for "
7944
7958
  f"{exc.idle_seconds:.0f}s — process terminated. Inspect retained "
@@ -7954,16 +7968,14 @@ class ProcessManager:
7954
7968
  )
7955
7969
  except Exception as exc:
7956
7970
  logger.exception("CLI stream error for task %s", task_id)
7957
- if task_id in self.active_processes:
7958
- try:
7959
- self.active_processes[task_id].kill()
7960
- except Exception:
7961
- pass
7962
7971
  return TaskResult(
7963
7972
  status="failed", exit_code=-1, stdout="", stderr="",
7964
7973
  error=f"Stream processing error: {exc}",
7965
7974
  )
7966
7975
  finally:
7976
+ if proc is not None and proc.returncode is None:
7977
+ _kill_proc(proc)
7978
+ await proc.wait()
7967
7979
  self.active_processes.pop(task_id, None)
7968
7980
 
7969
7981
  def _parse_claude_output(self, stdout: str) -> dict:
@@ -17715,6 +17727,41 @@ class RuntimeDaemon:
17715
17727
 
17716
17728
  if remote_ahead:
17717
17729
  remote_count = len(remote_ahead.splitlines())
17730
+ if (
17731
+ task is not None
17732
+ and task.workspace_resolution == "cloud_worktree"
17733
+ and task.node_type == "analysis"
17734
+ and task.input_data.get("analysis_mode") == "fresh"
17735
+ and task.requirement_key
17736
+ and branch == f"feature/{task.requirement_key}"
17737
+ ):
17738
+ default_branch = task.project.get("default_branch") or "main"
17739
+ try:
17740
+ await git(
17741
+ "merge-base", "--is-ancestor", f"origin/{default_branch}",
17742
+ "HEAD", cwd=workspace_path,
17743
+ )
17744
+ if await self.workspace_manager._branch_has_impl_commits(
17745
+ workspace_path, branch, default_branch,
17746
+ ):
17747
+ return (
17748
+ f"Push blocked: remote branch '{branch}' contains "
17749
+ "implementation commits; fresh analysis cannot replace it."
17750
+ )
17751
+ remote_sha = (await git(
17752
+ "rev-parse", "--verify", f"refs/remotes/origin/{branch}",
17753
+ cwd=workspace_path,
17754
+ )).strip()
17755
+ await git(
17756
+ "push", f"--force-with-lease=refs/heads/{branch}:{remote_sha}",
17757
+ "-u", "origin", branch, cwd=workspace_path,
17758
+ project_key=project_key, timeout=settings.GIT_PUSH_TIMEOUT,
17759
+ )
17760
+ logger.info("Pushed fresh analysis branch %s with exact lease", branch)
17761
+ return None
17762
+ except RuntimeError as exc:
17763
+ detail = self.workspace_manager._git_push_diagnostic(exc)
17764
+ return f"Fresh analysis push blocked for branch '{branch}': {detail}"
17718
17765
  # Divergence detected — but this is expected after rebase.
17719
17766
  # Check if the remote commits are already incorporated
17720
17767
  # (i.e., their patches are empty against our branch, meaning
@@ -17922,8 +17969,8 @@ class RuntimeDaemon:
17922
17969
  f"Push refused: remote branch '{branch}' has {remote_count} "
17923
17970
  f"commit(s) not in local history, rebase (-X theirs) failed, "
17924
17971
  f"and force-with-lease fallback also failed. "
17925
- f"Rebase error: {rebase_exc_str[:200]} | "
17926
- f"Force error: {force_exc_str[:200]}"
17972
+ f"Rebase error:\n{self.workspace_manager._git_push_diagnostic(rebase_exc)}\n"
17973
+ f"Force error:\n{self.workspace_manager._git_push_diagnostic(force_exc)}"
17927
17974
  )
17928
17975
 
17929
17976
  logger.info("Found unpushed commits on %s, pushing...", branch)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.7
3
+ Version: 1.58.1
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.57.7"
3
+ version = "1.58.1"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes