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.
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/PKG-INFO +1 -1
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli/daemon.py +79 -23
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/pyproject.toml +1 -1
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/README.md +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/setup.cfg +0 -0
- {forgexa_cli-1.14.4 → forgexa_cli-1.14.5}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.14.
|
|
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.
|
|
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(
|
|
5110
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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"):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|