forgexa-cli 1.14.4__tar.gz → 1.14.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.14.4
3
+ Version: 1.14.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: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.14.4"
2
+ __version__ = "1.14.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.14.4"
526
+ DAEMON_VERSION = "1.14.5"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -5106,28 +5106,47 @@ class ProcessManager:
5106
5106
 
5107
5107
  return info
5108
5108
 
5109
- async def _collect_git_info_vs_parent(self, cwd: Path) -> dict:
5110
- """Collect git diff stats comparing HEAD vs merge-base with default branch.
5109
+ async def _collect_git_info_vs_parent(
5110
+ self,
5111
+ cwd: Path,
5112
+ *,
5113
+ default_branch: str = "",
5114
+ before_sha: str = "",
5115
+ ) -> dict:
5116
+ """Collect git diff stats comparing HEAD vs merge-base with the base branch.
5111
5117
 
5112
- Uses merge-base to capture ALL changes on the feature branch, not just
5118
+ Uses merge-base to capture ALL changes on the current branch, not just
5113
5119
  the last commit (agents like claude may create multiple commits).
5114
- Falls back to HEAD~1 if merge-base detection fails.
5120
+ Prefers the project's configured default branch, then falls back to the
5121
+ conventional `main` / `master` refs. Falls back to HEAD~1 if merge-base
5122
+ detection fails.
5115
5123
  """
5116
5124
  info: dict[str, Any] = {}
5117
5125
  try:
5118
- # Try to find merge-base with origin's default branch
5119
- base_ref = "HEAD~1"
5120
- for candidate in ("origin/master", "origin/main"):
5121
- proc_mb = await asyncio.create_subprocess_exec(
5122
- "git", "merge-base", candidate, "HEAD",
5123
- stdout=asyncio.subprocess.PIPE,
5124
- stderr=asyncio.subprocess.PIPE,
5125
- cwd=str(cwd),
5126
- )
5127
- mb_stdout, _ = await asyncio.wait_for(proc_mb.communicate(), timeout=10)
5128
- if proc_mb.returncode == 0 and mb_stdout.strip():
5129
- base_ref = mb_stdout.decode().strip()
5130
- break
5126
+ # Prefer the exact pre-node HEAD when available so the diff is
5127
+ # scoped to this node's work, not the whole requirement branch.
5128
+ base_ref = str(before_sha or "").strip() or "HEAD~1"
5129
+ if not str(before_sha or "").strip():
5130
+ candidate_refs: list[str] = []
5131
+ for candidate in (default_branch, "main", "master"):
5132
+ candidate = str(candidate or "").strip()
5133
+ if not candidate:
5134
+ continue
5135
+ ref = candidate if candidate.startswith("origin/") else f"origin/{candidate}"
5136
+ if ref not in candidate_refs:
5137
+ candidate_refs.append(ref)
5138
+
5139
+ for candidate in candidate_refs:
5140
+ proc_mb = await asyncio.create_subprocess_exec(
5141
+ "git", "merge-base", candidate, "HEAD",
5142
+ stdout=asyncio.subprocess.PIPE,
5143
+ stderr=asyncio.subprocess.PIPE,
5144
+ cwd=str(cwd),
5145
+ )
5146
+ mb_stdout, _ = await asyncio.wait_for(proc_mb.communicate(), timeout=10)
5147
+ if proc_mb.returncode == 0 and mb_stdout.strip():
5148
+ base_ref = mb_stdout.decode().strip()
5149
+ break
5131
5150
 
5132
5151
  # Changed files since branch point
5133
5152
  proc = await asyncio.create_subprocess_exec(
@@ -5173,6 +5192,8 @@ class ProcessManager:
5173
5192
  )
5174
5193
  stdout4, _ = await asyncio.wait_for(proc4.communicate(), timeout=10)
5175
5194
  info["commit_sha"] = stdout4.decode().strip()
5195
+ info["before_sha"] = base_ref
5196
+ info["after_sha"] = info["commit_sha"]
5176
5197
 
5177
5198
  except Exception as e:
5178
5199
  logger.debug("Git info (vs parent) collection failed: %s", e)
@@ -6364,6 +6385,8 @@ class RuntimeDaemon:
6364
6385
  async def _execute_task(self, task: TaskInfo, conn: ServerConnection):
6365
6386
  """Execute a single task, reporting to the originating server connection."""
6366
6387
  reporter = conn.reporter
6388
+ default_branch = str((task.project or {}).get("default_branch") or "").strip()
6389
+ node_before_sha = ""
6367
6390
  # Use workspace-level granularity (requirement_workflow_id or graph_id) so that
6368
6391
  # tasks for DIFFERENT requirements can run in parallel on the same daemon, while
6369
6392
  # tasks for the SAME requirement (e.g. analysis + retry) are still serialized to
@@ -6543,6 +6566,19 @@ class RuntimeDaemon:
6543
6566
  output_dir_norm, task.task_id,
6544
6567
  )
6545
6568
 
6569
+ try:
6570
+ node_before_sha = (
6571
+ await self.workspace_manager._git(
6572
+ "rev-parse", "HEAD", cwd=workspace_path, timeout=30,
6573
+ )
6574
+ ).strip()
6575
+ except Exception as _before_sha_err:
6576
+ logger.debug(
6577
+ "Could not resolve pre-node HEAD for task %s: %s",
6578
+ task.task_id,
6579
+ _before_sha_err,
6580
+ )
6581
+
6546
6582
  # 3. Run agent with real-time output streaming + periodic progress heartbeat
6547
6583
  await reporter.report_progress(task.task_id, 10, "running_agent")
6548
6584
 
@@ -6615,7 +6651,11 @@ class RuntimeDaemon:
6615
6651
  _skip_fallback = False
6616
6652
  if self.process_manager.is_rate_limited(result):
6617
6653
  _pre_fallback_git = await self.process_manager._collect_git_info(workspace_path)
6618
- _pre_fallback_committed = await self.process_manager._collect_git_info_vs_parent(workspace_path)
6654
+ _pre_fallback_committed = await self.process_manager._collect_git_info_vs_parent(
6655
+ workspace_path,
6656
+ default_branch=default_branch,
6657
+ before_sha=node_before_sha,
6658
+ )
6619
6659
  has_workspace_changes = (
6620
6660
  bool(_pre_fallback_git.get("files_changed"))
6621
6661
  or bool(_pre_fallback_committed.get("files_changed"))
@@ -6726,7 +6766,11 @@ class RuntimeDaemon:
6726
6766
  # (e.g., after rate-limit fallback, or API errors not caught by output parsing).
6727
6767
  if result.status == "success" and task.node_type in ("coding", "fix", "testing"):
6728
6768
  has_uncommitted = bool(pre_commit_git.get("files_changed"))
6729
- committed_git = await self.process_manager._collect_git_info_vs_parent(workspace_path)
6769
+ committed_git = await self.process_manager._collect_git_info_vs_parent(
6770
+ workspace_path,
6771
+ default_branch=default_branch,
6772
+ before_sha=node_before_sha,
6773
+ )
6730
6774
  has_committed = bool(committed_git.get("files_changed"))
6731
6775
  has_tokens = (
6732
6776
  int(result.metrics.get("token_input", 0) or 0)
@@ -6764,7 +6808,11 @@ class RuntimeDaemon:
6764
6808
  )
6765
6809
  if can_attempt_recovery:
6766
6810
  original_exit_code = result.exit_code
6767
- committed_git = await self.process_manager._collect_git_info_vs_parent(workspace_path)
6811
+ committed_git = await self.process_manager._collect_git_info_vs_parent(
6812
+ workspace_path,
6813
+ default_branch=default_branch,
6814
+ before_sha=node_before_sha,
6815
+ )
6768
6816
  has_committed_changes = bool(committed_git.get("files_changed"))
6769
6817
  has_uncommitted_changes = bool(pre_commit_git.get("files_changed"))
6770
6818
  has_no_uncommitted = not has_uncommitted_changes
@@ -6888,7 +6936,11 @@ class RuntimeDaemon:
6888
6936
  # 4.55 Analysis/design/fix nodes must update their deliverables in THIS run.
6889
6937
  # Existing files from a prior iteration are not sufficient evidence.
6890
6938
  if result.status == "success" and task.node_type in ("analysis", "design", "fix"):
6891
- committed_git = await self.process_manager._collect_git_info_vs_parent(workspace_path)
6939
+ committed_git = await self.process_manager._collect_git_info_vs_parent(
6940
+ workspace_path,
6941
+ default_branch=default_branch,
6942
+ before_sha=node_before_sha,
6943
+ )
6892
6944
  git_check_passed = self.process_manager._has_required_deliverable_updates(
6893
6945
  task,
6894
6946
  pre_commit_git.get("files_changed"),
@@ -7002,7 +7054,11 @@ class RuntimeDaemon:
7002
7054
  )
7003
7055
  result.error = f"Git push failed: {push_err}"
7004
7056
  # Re-collect git info after commit (compare with parent)
7005
- post_commit_git = await self.process_manager._collect_git_info_vs_parent(workspace_path)
7057
+ post_commit_git = await self.process_manager._collect_git_info_vs_parent(
7058
+ workspace_path,
7059
+ default_branch=default_branch,
7060
+ before_sha=node_before_sha,
7061
+ )
7006
7062
  # Merge: use the pre-commit file list if post-commit is empty
7007
7063
  result.git = post_commit_git
7008
7064
  if not result.git.get("files_changed") and pre_commit_git.get("files_changed"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.14.4
3
+ Version: 1.14.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: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.14.4"
3
+ version = "1.14.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 = { text = "MIT" }
File without changes
File without changes