forgexa-cli 1.17.3__tar.gz → 1.17.4__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.17.3 → forgexa_cli-1.17.4}/PKG-INFO +1 -1
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli/daemon.py +100 -28
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/pyproject.toml +1 -1
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/README.md +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/setup.cfg +0 -0
- {forgexa_cli-1.17.3 → forgexa_cli-1.17.4}/tests/test_auth_and_runtime_commands.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.17.
|
|
2
|
+
__version__ = "1.17.4"
|
|
@@ -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.
|
|
526
|
+
DAEMON_VERSION = "1.17.4"
|
|
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
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
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
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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,
|
|
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.
|
|
8377
|
-
#
|
|
8378
|
-
#
|
|
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.
|
|
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",
|
|
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
|