forgexa-cli 1.57.7__tar.gz → 1.58.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.
Files changed (29) hide show
  1. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/agent_core.py +16 -0
  4. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/daemon.py +58 -11
  5. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/README.md +0 -0
  8. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.7 → forgexa_cli-1.58.0}/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.0
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.0"
@@ -877,6 +877,8 @@ class AgentDiscovery:
877
877
  "binary found but not functional".
878
878
  """
879
879
  import re
880
+ probe_dir = None
881
+ proc = None
880
882
  try:
881
883
  parts = detect_cmd.split()
882
884
  if executable:
@@ -886,11 +888,17 @@ class AgentDiscovery:
886
888
  if resolved:
887
889
  parts[0] = resolved
888
890
  parts = _prepare_agent_command(parts, preserve_argv=False)
891
+ env = None
892
+ if detect_cmd in ("opencode --version", "mimo --version"):
893
+ probe_dir = tempfile.TemporaryDirectory(prefix=f"forgexa-{detect_cmd.split()[0]}-probe-")
894
+ env = os.environ.copy()
895
+ env.update(TMPDIR=probe_dir.name, BUN_TMPDIR=probe_dir.name)
889
896
  proc = await asyncio.create_subprocess_exec(
890
897
  *parts,
891
898
  stdin=asyncio.subprocess.DEVNULL,
892
899
  stdout=asyncio.subprocess.PIPE,
893
900
  stderr=asyncio.subprocess.PIPE,
901
+ env=env,
894
902
  **_agent_process_group_kwargs(),
895
903
  )
896
904
  stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=10)
@@ -918,11 +926,19 @@ class AgentDiscovery:
918
926
  )
919
927
  return None
920
928
  return output
929
+ except asyncio.CancelledError:
930
+ raise
921
931
  except Exception as exc:
922
932
  logger.warning(
923
933
  "Version check for %r raised %s: %s", detect_cmd, type(exc).__name__, exc,
924
934
  )
925
935
  return None
936
+ finally:
937
+ if proc is not None and proc.returncode is None:
938
+ _kill_proc(proc)
939
+ await proc.wait()
940
+ if probe_dir is not None:
941
+ probe_dir.cleanup()
926
942
 
927
943
  @staticmethod
928
944
  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.0"
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.0
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.0"
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