forgexa-cli 1.17.3__tar.gz → 1.17.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.17.3
3
+ Version: 1.17.5
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.17.3"
2
+ __version__ = "1.17.5"
@@ -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.17.3"
526
+ DAEMON_VERSION = "1.17.5"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -2017,31 +2017,36 @@ class WorkspaceManager:
2017
2017
  # A final sync here keeps the workspace current so the agent works
2018
2018
  # on the latest codebase and avoids non-fast-forward push failures.
2019
2019
  if not is_fresh_start:
2020
+ sync_success = False
2021
+ sync_error = ""
2020
2022
  try:
2021
2023
  await self._git(
2022
2024
  "fetch", "origin",
2023
2025
  f"{branch_name}:refs/remotes/origin/{branch_name}",
2024
2026
  cwd=ws_path, project_key=project_key,
2025
2027
  )
2026
- except RuntimeError:
2027
- pass
2028
- try:
2029
- # Ensure we're on the correct branch
2030
- await self._git("checkout", branch_name, cwd=ws_path)
2031
- except RuntimeError:
2032
- pass
2033
- # Use --ff-only to keep only fast-forward changes; if the branch has
2034
- # diverged (force-pushed by prior phase), reset --hard is used below.
2035
- pulled = False
2036
- try:
2037
- await self._git(
2038
- "pull", "--ff-only", "origin", branch_name,
2039
- cwd=ws_path, project_key=project_key,
2040
- )
2041
- pulled = True
2042
- except RuntimeError:
2043
- pass
2044
- if not pulled:
2028
+ except RuntimeError as exc:
2029
+ sync_error = f"fetch branch ref failed: {exc}"
2030
+ else:
2031
+ try:
2032
+ # Ensure we're on the correct branch
2033
+ await self._git("checkout", branch_name, cwd=ws_path)
2034
+ except RuntimeError as exc:
2035
+ sync_error = f"checkout branch failed: {exc}"
2036
+ else:
2037
+ # Use --ff-only to keep only fast-forward changes; if the
2038
+ # branch has diverged (force-pushed by prior phase),
2039
+ # reset --hard is used below.
2040
+ try:
2041
+ await self._git(
2042
+ "pull", "--ff-only", "origin", branch_name,
2043
+ cwd=ws_path, project_key=project_key,
2044
+ )
2045
+ sync_success = True
2046
+ except RuntimeError as exc:
2047
+ sync_error = f"pull --ff-only failed: {exc}"
2048
+
2049
+ if not sync_success:
2045
2050
  # ff-only failed (diverged or remote not yet created) — try
2046
2051
  # reset --hard to force-sync to whatever the remote has.
2047
2052
  try:
@@ -2049,9 +2054,35 @@ class WorkspaceManager:
2049
2054
  "reset", "--hard", f"origin/{branch_name}",
2050
2055
  cwd=ws_path,
2051
2056
  )
2052
- except RuntimeError:
2053
- # Remote branch may not exist yet (first analysis on fresh repo)
2054
- pass
2057
+ sync_success = True
2058
+ except RuntimeError as exc:
2059
+ if sync_error:
2060
+ sync_error = f"{sync_error}; reset failed: {exc}"
2061
+ else:
2062
+ sync_error = f"reset --hard failed: {exc}"
2063
+
2064
+ if expect_branch and not sync_success:
2065
+ main_repo = project_dir / "_main"
2066
+ logger.warning(
2067
+ "Final branch sync failed for expected remote branch %s in %s: %s. "
2068
+ "Discarding worktree to avoid running on the wrong base.",
2069
+ branch_name,
2070
+ ws_path,
2071
+ sync_error or "unknown sync error",
2072
+ )
2073
+ try:
2074
+ await self._remove_broken_worktree(main_repo, ws_path, workspace_key)
2075
+ except Exception as cleanup_exc:
2076
+ logger.warning(
2077
+ "Failed to discard unsafe worktree %s after sync failure: %s",
2078
+ ws_path,
2079
+ cleanup_exc,
2080
+ )
2081
+ raise RuntimeError(
2082
+ f"Failed to sync expected remote branch '{branch_name}' before execution "
2083
+ f"({sync_error or 'unknown sync error'}). Refusing to continue on a "
2084
+ "workspace that may be based on the default branch."
2085
+ )
2055
2086
  return ws_path
2056
2087
  else:
2057
2088
  # No repo — create a directory with git init for change tracking
@@ -3060,6 +3091,14 @@ class WorkspaceManager:
3060
3091
  # Treat as if branch doesn't exist to force fresh worktree
3061
3092
  branch_exists_remote = False
3062
3093
 
3094
+ if expect_branch and not fresh_start and not branch_exists_remote:
3095
+ self._safe_rmtree(ws_path)
3096
+ raise RuntimeError(
3097
+ f"Expected remote branch '{branch_name}' to exist, but it could not be "
3098
+ "fetched or resolved. Refusing to create a continuation workspace from "
3099
+ f"origin/{default_branch} because that risks overwriting requirement-scoped history."
3100
+ )
3101
+
3063
3102
  if branch_exists_remote and not fresh_start:
3064
3103
  # Branch exists on remote and we want to preserve it (refine mode)
3065
3104
  # First, update local branch ref to match remote (in case it's stale)
@@ -3106,6 +3145,13 @@ class WorkspaceManager:
3106
3145
  ws_path,
3107
3146
  )
3108
3147
  return existing_branch_worktree
3148
+ if expect_branch:
3149
+ self._safe_rmtree(ws_path)
3150
+ raise RuntimeError(
3151
+ f"Failed to attach local worktree for existing remote branch '{branch_name}'. "
3152
+ "Refusing to fall back to the default branch because that would risk "
3153
+ "pushing unrelated commits into the requirement branch."
3154
+ )
3109
3155
  # Fallback: create from default_branch
3110
3156
  try:
3111
3157
  await self._git(
@@ -8198,7 +8244,7 @@ class RuntimeDaemon:
8198
8244
  """Execute an AIJob in daemon workspace and report results back.
8199
8245
 
8200
8246
  Uses WorkspaceManager for branch-based isolation, runs the agent CLI
8201
- with the job's prompt, auto-commits results, and reports back.
8247
+ with the job's prompt, snapshots git state, and reports back.
8202
8248
  """
8203
8249
  job_id = aj.get("job_id", "")
8204
8250
  task_type = aj.get("task_type", "unknown")
@@ -8373,12 +8419,15 @@ class RuntimeDaemon:
8373
8419
  )
8374
8420
  await _flush_output_to_server()
8375
8421
 
8376
- # 4. Auto-commit if successful (always call _auto_commit on success:
8377
- # _auto_commit handles both uncommitted changes AND internally-committed
8378
- # changes that just need to be pushed — same as _execute_task).
8422
+ # 4. Capture branch/commit metadata without mutating the repository.
8423
+ # Workspace-mode AIJobs are prompt-output tasks: scenario generation,
8424
+ # test-script generation/repair, and deliverable generation all return
8425
+ # their result via stdout / API payloads rather than repository commits.
8426
+ # Pushing here is dangerous because it can propagate stale local branch
8427
+ # state even when the AIJob produced no repo changes at all.
8379
8428
  git_info = {}
8380
8429
  if result.status == "success":
8381
- git_info = await self._auto_commit(workspace_path, fake_task)
8430
+ git_info = await self._collect_workspace_git_info(workspace_path)
8382
8431
 
8383
8432
  # 5. Report completion
8384
8433
  # For deliverables: allow up to 200K chars (full document); others: last 20K
@@ -8444,6 +8493,29 @@ class RuntimeDaemon:
8444
8493
  finally:
8445
8494
  project_lock.release()
8446
8495
 
8496
+ async def _collect_workspace_git_info(self, workspace_path: Path) -> dict:
8497
+ """Return the current branch and commit SHA without mutating remote state."""
8498
+ git = self.workspace_manager._git
8499
+ info: dict[str, str] = {}
8500
+
8501
+ try:
8502
+ branch = (await git(
8503
+ "rev-parse", "--abbrev-ref", "HEAD", cwd=workspace_path,
8504
+ )).strip()
8505
+ if branch and branch != "HEAD":
8506
+ info["branch"] = branch
8507
+ except RuntimeError:
8508
+ pass
8509
+
8510
+ try:
8511
+ commit_sha = (await git("rev-parse", "HEAD", cwd=workspace_path)).strip()
8512
+ if commit_sha:
8513
+ info["commit_sha"] = commit_sha
8514
+ except RuntimeError:
8515
+ pass
8516
+
8517
+ return info
8518
+
8447
8519
  async def _validate_and_retry(
8448
8520
  self,
8449
8521
  agent: "DiscoveredAgent",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.17.3
3
+ Version: 1.17.5
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.17.3"
3
+ version = "1.17.5"
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