forgexa-cli 1.57.6__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.6 → forgexa_cli-1.58.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/agent_core.py +16 -0
  4. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/daemon.py +185 -31
  5. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/README.md +0 -0
  8. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.6 → forgexa_cli-1.58.0}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.6 → 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.6
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.6"
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.6"
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).
@@ -5507,6 +5517,10 @@ class _TaskAttachmentDownloadError(RuntimeError):
5507
5517
  """Raised when a required task attachment cannot be staged safely."""
5508
5518
 
5509
5519
 
5520
+ class _TaskAttachmentUnavailableError(_TaskAttachmentDownloadError):
5521
+ """Raised when the attachment source is unavailable but the task can proceed."""
5522
+
5523
+
5510
5524
  def _workspace_has_recent_activity(
5511
5525
  workspace_path: "Path", since_monotonic: float, max_depth: int = 4
5512
5526
  ) -> bool:
@@ -5627,6 +5641,8 @@ class ProcessManager:
5627
5641
  "authentication required",
5628
5642
  "authenticate with the github",
5629
5643
  "gh auth login",
5644
+ "your github token may be invalid",
5645
+ "rejected the cli github credential",
5630
5646
  ]
5631
5647
 
5632
5648
  def __init__(self):
@@ -6693,6 +6709,7 @@ class ProcessManager:
6693
6709
  data_home=tmp_data_root,
6694
6710
  require_windows_shell=True,
6695
6711
  )
6712
+ env.update(TMPDIR=tmp_data_root, BUN_TMPDIR=tmp_data_root)
6696
6713
  except RuntimeError as exc:
6697
6714
  return TaskResult(
6698
6715
  status="failed",
@@ -6786,6 +6803,7 @@ class ProcessManager:
6786
6803
  prompt_file=prompt_file,
6787
6804
  )
6788
6805
  env = agent_core.mimo_base_env(os.environ.copy())
6806
+ env.update(TMPDIR=tmp_root, BUN_TMPDIR=tmp_root)
6789
6807
  result = await self._run_cli(
6790
6808
  cmd, cwd, timeout, task_id, on_chunk=on_chunk, env=env,
6791
6809
  )
@@ -7640,6 +7658,59 @@ class ProcessManager:
7640
7658
  return retry_result
7641
7659
  _auth_state["persist"] = False
7642
7660
 
7661
+ auth_detail = " ".join(
7662
+ [*structured_errors, stderr]
7663
+ ).lower()
7664
+ token_auth_error = (
7665
+ "your github token may be invalid" in auth_detail
7666
+ or "rejected the cli github credential" in auth_detail
7667
+ )
7668
+ if token_auth_error and not (copilot_exit == 0 and returncode == 0):
7669
+ _auth_state["persist"] = False
7670
+ if (
7671
+ token_auth_error
7672
+ and not (copilot_exit == 0 and returncode == 0)
7673
+ and not signals["has_meaningful_content"]
7674
+ and not self._copilot_called_any_tools(stdout)
7675
+ ):
7676
+ recovery_home = (
7677
+ Path(_master_copilot_home)
7678
+ if auth_env_var and _master_copilot_home
7679
+ else agent_core.copilot_recovery_home(_isolated_copilot_home)
7680
+ )
7681
+ if (
7682
+ not _retry_without_token_env
7683
+ and recovery_home
7684
+ and (recovery_home / "config.json").is_file()
7685
+ ):
7686
+ restored = await asyncio.to_thread(
7687
+ agent_core.replace_copilot_auth_state,
7688
+ recovery_home,
7689
+ _isolated_copilot_home,
7690
+ )
7691
+ can_retry_with_login = bool(auth_env_var or restored)
7692
+ else:
7693
+ can_retry_with_login = False
7694
+ if can_retry_with_login:
7695
+ logger.warning(
7696
+ "Copilot token authentication failed before work for task %s; "
7697
+ "retrying once with stored login and token environment variables removed",
7698
+ task_id,
7699
+ )
7700
+ retry_result = await self._run_copilot(
7701
+ agent, prompt, cwd, timeout, task_id, on_chunk,
7702
+ stream_stderr=stream_stderr,
7703
+ disable_builtin_mcps=disable_builtin_mcps,
7704
+ _retry_without_effort=_retry_without_effort,
7705
+ _retry_without_token_env=True,
7706
+ _isolated_copilot_home=_isolated_copilot_home,
7707
+ _auth_state=_auth_state,
7708
+ )
7709
+ _auth_state["persist"] = retry_result.status == "success"
7710
+ retry_result.metrics["copilot_auth_recovery"] = True
7711
+ return retry_result
7712
+ _auth_state["persist"] = False
7713
+
7643
7714
  if copilot_exit == 0 and returncode == 0:
7644
7715
  return TaskResult(
7645
7716
  status="success",
@@ -7737,6 +7808,12 @@ class ProcessManager:
7737
7808
  failure_error = f"Copilot exited with code {effective_rc}: {stderr[-500:].strip()}"
7738
7809
  else:
7739
7810
  failure_error = f"Copilot exited with code {effective_rc}"
7811
+ if token_auth_error:
7812
+ auth_source = auth_env_var or "stored Copilot login"
7813
+ failure_error = (
7814
+ f"Copilot authentication failed using {auth_source}. "
7815
+ f"{failure_error}"
7816
+ )
7740
7817
  logger.error(
7741
7818
  "Copilot task %s failed (process_rc=%s, result_exit=%s): %s",
7742
7819
  task_id,
@@ -7835,6 +7912,7 @@ class ProcessManager:
7835
7912
  env: dict | None = None,
7836
7913
  stream_stderr: bool = False,
7837
7914
  ) -> TaskResult:
7915
+ proc = None
7838
7916
  try:
7839
7917
  cmd = _prepare_agent_command(cmd)
7840
7918
  proc = await asyncio.create_subprocess_exec(
@@ -7873,7 +7951,8 @@ class ProcessManager:
7873
7951
  failure_code="agent_launch_not_found",
7874
7952
  )
7875
7953
  except asyncio.TimeoutError as exc:
7876
- _kill_proc(self.active_processes.pop(task_id, None) or proc)
7954
+ if proc is not None:
7955
+ _kill_proc(proc)
7877
7956
  _err = (
7878
7957
  f"Agent produced no stdout, stderr, or workspace changes for "
7879
7958
  f"{exc.idle_seconds:.0f}s — process terminated. Inspect retained "
@@ -7889,16 +7968,14 @@ class ProcessManager:
7889
7968
  )
7890
7969
  except Exception as exc:
7891
7970
  logger.exception("CLI stream error for task %s", task_id)
7892
- if task_id in self.active_processes:
7893
- try:
7894
- self.active_processes[task_id].kill()
7895
- except Exception:
7896
- pass
7897
7971
  return TaskResult(
7898
7972
  status="failed", exit_code=-1, stdout="", stderr="",
7899
7973
  error=f"Stream processing error: {exc}",
7900
7974
  )
7901
7975
  finally:
7976
+ if proc is not None and proc.returncode is None:
7977
+ _kill_proc(proc)
7978
+ await proc.wait()
7902
7979
  self.active_processes.pop(task_id, None)
7903
7980
 
7904
7981
  def _parse_claude_output(self, stdout: str) -> dict:
@@ -10574,9 +10651,22 @@ class RuntimeDaemon:
10574
10651
  timeout=httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0),
10575
10652
  ) as response:
10576
10653
  if not 200 <= response.status_code < 300:
10577
- raise _TaskAttachmentDownloadError(
10578
- f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
10579
- )
10654
+ error = f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
10655
+ if response.status_code in {409, 502}:
10656
+ await response.aread()
10657
+ try:
10658
+ detail = response.json()
10659
+ except ValueError:
10660
+ detail = None
10661
+ if isinstance(detail, dict) and detail.get("detail") == "Task attachment integrity check failed":
10662
+ raise _TaskAttachmentDownloadError(error)
10663
+ if response.status_code == 409 and isinstance(detail, dict) and detail.get("detail") == "Task attachment source no longer matches stored checksum":
10664
+ raise _TaskAttachmentUnavailableError(
10665
+ f"Attachment '{attachment_name}' is unavailable: {detail['detail']}"
10666
+ )
10667
+ if response.status_code in {404, 502, 503, 504}:
10668
+ raise _TaskAttachmentUnavailableError(error)
10669
+ raise _TaskAttachmentDownloadError(error)
10580
10670
  content_length = response.headers.get("content-length")
10581
10671
  if content_length:
10582
10672
  try:
@@ -10610,6 +10700,10 @@ class RuntimeDaemon:
10610
10700
  temporary_path.replace(destination)
10611
10701
  except _TaskAttachmentDownloadError:
10612
10702
  raise
10703
+ except httpx.RequestError as exc:
10704
+ raise _TaskAttachmentUnavailableError(
10705
+ f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
10706
+ ) from exc
10613
10707
  except httpx.HTTPError as exc:
10614
10708
  raise _TaskAttachmentDownloadError(
10615
10709
  f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
@@ -10625,7 +10719,7 @@ class RuntimeDaemon:
10625
10719
  timeout=timeout_seconds,
10626
10720
  )
10627
10721
  except asyncio.TimeoutError as exc:
10628
- raise _TaskAttachmentDownloadError(
10722
+ raise _TaskAttachmentUnavailableError(
10629
10723
  f"Attachment '{attachment_name}' download timed out after "
10630
10724
  f"{timeout_seconds:.0f}s"
10631
10725
  ) from exc
@@ -10648,6 +10742,7 @@ class RuntimeDaemon:
10648
10742
 
10649
10743
  staging_dir = Path(tempfile.mkdtemp(prefix=f"forgexa-attachments-{task.task_id[:8]}-"))
10650
10744
  staged: list[tuple[str, list[str], str]] = []
10745
+ unavailable: list[tuple[str, list[str], str]] = []
10651
10746
  deadline = time.monotonic() + self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS
10652
10747
  remaining_bytes = self._TASK_ATTACHMENT_MAX_TOTAL_BYTES
10653
10748
  try:
@@ -10676,12 +10771,6 @@ class RuntimeDaemon:
10676
10771
  raise _TaskAttachmentDownloadError(
10677
10772
  f"Attachment '{file_name}' cannot be staged securely because it has no artifact ID"
10678
10773
  )
10679
- remaining_seconds = deadline - time.monotonic()
10680
- if remaining_seconds <= 0:
10681
- raise _TaskAttachmentDownloadError(
10682
- f"Task attachment staging timed out after "
10683
- f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
10684
- )
10685
10774
  if not conn.runtime_id:
10686
10775
  raise _TaskAttachmentDownloadError(
10687
10776
  f"Attachment '{file_name}' cannot be staged because this runtime is not registered"
@@ -10690,16 +10779,27 @@ class RuntimeDaemon:
10690
10779
  f"{conn.server_url.rstrip('/')}/api/v1/runtimes/{conn.runtime_id}/"
10691
10780
  f"tasks/{task.task_id}/attachments/{artifact_id}"
10692
10781
  )
10693
- await self._download_task_attachment(
10694
- conn.client,
10695
- source_url,
10696
- destination,
10697
- attachment_name=file_name,
10698
- expected_size=expected_size or None,
10699
- expected_checksum=str(attachment.get("checksum") or ""),
10700
- max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
10701
- timeout_seconds=remaining_seconds,
10702
- )
10782
+ try:
10783
+ remaining_seconds = deadline - time.monotonic()
10784
+ if remaining_seconds <= 0:
10785
+ raise _TaskAttachmentUnavailableError(
10786
+ f"Task attachment staging timed out after "
10787
+ f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
10788
+ )
10789
+ await self._download_task_attachment(
10790
+ conn.client,
10791
+ source_url,
10792
+ destination,
10793
+ attachment_name=file_name,
10794
+ expected_size=expected_size or None,
10795
+ expected_checksum=str(attachment.get("checksum") or ""),
10796
+ max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
10797
+ timeout_seconds=remaining_seconds,
10798
+ )
10799
+ except _TaskAttachmentUnavailableError as exc:
10800
+ logger.warning("Task %s attachment unavailable: %s", task.task_id, exc)
10801
+ unavailable.append((file_name, download_url_aliases, str(exc)))
10802
+ continue
10703
10803
  remaining_bytes -= destination.stat().st_size
10704
10804
  staged.append((file_name, download_url_aliases, str(destination)))
10705
10805
  except Exception:
@@ -10707,10 +10807,29 @@ class RuntimeDaemon:
10707
10807
  raise
10708
10808
 
10709
10809
  task.input_prompt = self._rewrite_staged_attachment_references(task.input_prompt, staged)
10810
+ if unavailable:
10811
+ notices = []
10812
+ for file_name, download_urls, error in unavailable:
10813
+ notices.append(f"- `{file_name}`: {error}")
10814
+ for download_url in download_urls:
10815
+ command_pattern = r"`curl -L '" + re.escape(download_url) + r"' -o /tmp/[^`]+`"
10816
+ task.input_prompt = re.sub(command_pattern, "[attachment unavailable]", task.input_prompt)
10817
+ task.input_prompt = task.input_prompt.replace(f"Download: {download_url}", "Attachment unavailable")
10818
+ task.input_prompt = task.input_prompt.replace(download_url, "[attachment unavailable]")
10819
+ task.input_prompt = "\n".join([
10820
+ "## Unavailable Attachments",
10821
+ "These files could not be retrieved. Continue with available inputs and report any missing evidence; do not retry their download URLs.",
10822
+ *notices,
10823
+ "",
10824
+ task.input_prompt,
10825
+ ])
10710
10826
  input_data = dict(task.input_data or {})
10711
10827
  input_data["prompt"] = task.input_prompt
10712
10828
  task.input_data = input_data
10713
10829
  logger.info("Prepared %d attachment(s) for task %s", len(staged), task.task_id)
10830
+ if not staged:
10831
+ shutil.rmtree(staging_dir, ignore_errors=True)
10832
+ return None
10714
10833
  return staging_dir
10715
10834
 
10716
10835
  @staticmethod
@@ -17608,6 +17727,41 @@ class RuntimeDaemon:
17608
17727
 
17609
17728
  if remote_ahead:
17610
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}"
17611
17765
  # Divergence detected — but this is expected after rebase.
17612
17766
  # Check if the remote commits are already incorporated
17613
17767
  # (i.e., their patches are empty against our branch, meaning
@@ -17815,8 +17969,8 @@ class RuntimeDaemon:
17815
17969
  f"Push refused: remote branch '{branch}' has {remote_count} "
17816
17970
  f"commit(s) not in local history, rebase (-X theirs) failed, "
17817
17971
  f"and force-with-lease fallback also failed. "
17818
- f"Rebase error: {rebase_exc_str[:200]} | "
17819
- 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)}"
17820
17974
  )
17821
17975
 
17822
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.6
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.6"
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