forgexa-cli 1.8.6__tar.gz → 1.8.7__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.8.6
3
+ Version: 1.8.7
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.8.6"
2
+ __version__ = "1.8.7"
@@ -392,7 +392,7 @@ except (ImportError, ModuleNotFoundError):
392
392
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
393
393
  # Kept in sync with pyproject.toml version via bump-version.sh.
394
394
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
395
- DAEMON_VERSION = "1.8.6"
395
+ DAEMON_VERSION = "1.8.7"
396
396
 
397
397
 
398
398
  def _detect_client_type() -> str:
@@ -648,6 +648,10 @@ _ANALYSIS_OUTPUTS_BY_TYPE: dict[str, list[str]] = {
648
648
  "documentation": ["outline.md", "analysis.json"],
649
649
  "improvement": ["improvement-spec.md", "TASKS.md", "analysis.json", "test-intent.json"],
650
650
  "task": ["task-plan.md", "analysis.json"],
651
+ # Research / feasibility study — no PRD/SDD/TASKS, only a research plan and metadata
652
+ "spike": ["research.md", "analysis.json"],
653
+ # Customer support Q&A — lightweight answer doc + metadata only
654
+ "faq": ["faq-answer.md", "analysis.json"],
651
655
  }
652
656
 
653
657
 
@@ -1123,17 +1127,34 @@ class WorkspaceManager:
1123
1127
  )
1124
1128
 
1125
1129
  if repo_url:
1130
+ # For non-fresh (refine/continuation) nodes, expand expect_branch to
1131
+ # cover any node that is part of a real requirement workflow AND is not
1132
+ # the initial analysis. This ensures a hard error (and workspace
1133
+ # re-clone) when the branch sync fails, rather than silently proceeding
1134
+ # with a stale workspace that will cause a non-fast-forward push later.
1135
+ expect_branch = bool(task.analysis_branch) or (
1136
+ bool(task.requirement_key) and not is_fresh_start and task.node_type != "analysis"
1137
+ )
1126
1138
  ws_path = await self._create_worktree(
1127
1139
  project_dir, repo_url, default_branch, workspace_key, branch_name,
1128
1140
  fresh_start=is_fresh_start,
1129
1141
  project_key=project_key,
1130
- expect_branch=bool(task.analysis_branch),
1142
+ expect_branch=expect_branch,
1131
1143
  )
1132
- # Refine mode: ensure we're on the analysis branch with its history
1133
- # (not reset to default_branch)
1134
- if analysis_mode == "refine" and task.node_type == "analysis":
1144
+ # After workspace creation, perform a final branch-specific fetch + reset
1145
+ # to ensure the working tree is at the absolute latest remote state.
1146
+ # This is critical in two scenarios:
1147
+ # 1. Analysis refine mode: must be on the analysis branch history.
1148
+ # 2. All continuation nodes (design/coding/testing): another runtime
1149
+ # may have pushed commits while this runtime's agent was executing.
1150
+ # A final sync here keeps the workspace current so the agent works
1151
+ # on the latest codebase and avoids non-fast-forward push failures.
1152
+ if not is_fresh_start:
1135
1153
  try:
1136
- await self._git("fetch", "origin", cwd=ws_path, project_key=project_key)
1154
+ await self._git(
1155
+ "fetch", "origin", branch_name,
1156
+ cwd=ws_path, project_key=project_key,
1157
+ )
1137
1158
  except RuntimeError:
1138
1159
  pass
1139
1160
  try:
@@ -1141,15 +1162,28 @@ class WorkspaceManager:
1141
1162
  await self._git("checkout", branch_name, cwd=ws_path)
1142
1163
  except RuntimeError:
1143
1164
  pass
1144
- # Pull latest from remote branch if it exists (preserves prior commits)
1165
+ # Use --ff-only to keep only fast-forward changes; if the branch has
1166
+ # diverged (force-pushed by prior phase), reset --hard is used below.
1167
+ pulled = False
1145
1168
  try:
1146
1169
  await self._git(
1147
1170
  "pull", "--ff-only", "origin", branch_name,
1148
1171
  cwd=ws_path, project_key=project_key,
1149
1172
  )
1173
+ pulled = True
1150
1174
  except RuntimeError:
1151
- # Remote branch might not exist yet or has diverged; that's OK
1152
1175
  pass
1176
+ if not pulled:
1177
+ # ff-only failed (diverged or remote not yet created) — try
1178
+ # reset --hard to force-sync to whatever the remote has.
1179
+ try:
1180
+ await self._git(
1181
+ "reset", "--hard", f"origin/{branch_name}",
1182
+ cwd=ws_path,
1183
+ )
1184
+ except RuntimeError:
1185
+ # Remote branch may not exist yet (first analysis on fresh repo)
1186
+ pass
1153
1187
  return ws_path
1154
1188
  else:
1155
1189
  # No repo — create a directory with git init for change tracking
@@ -1292,18 +1326,29 @@ class WorkspaceManager:
1292
1326
  # only fetches the default branch. Explicitly fetch the
1293
1327
  # feature branch with a full refspec so that
1294
1328
  # origin/{branch_name} is available for checkout/reset.
1329
+ _last_sync_err: str = ""
1295
1330
  try:
1296
1331
  await self._git("fetch", "origin", cwd=ws_path, project_key=project_key)
1297
- except RuntimeError:
1298
- pass
1332
+ except RuntimeError as _pre_fe:
1333
+ logger.warning(
1334
+ "fetch origin failed for worktree %s: %s "
1335
+ "(likely auth/SSH issue — will retry in sync loop)",
1336
+ ws_path, _pre_fe,
1337
+ )
1338
+ _last_sync_err = str(_pre_fe)[:300]
1299
1339
  try:
1300
1340
  await self._git(
1301
1341
  "fetch", "origin",
1302
1342
  f"{branch_name}:refs/remotes/origin/{branch_name}",
1303
1343
  cwd=ws_path, project_key=project_key,
1304
1344
  )
1305
- except RuntimeError:
1306
- pass # Branch may not exist on remote yet
1345
+ except RuntimeError as _pre_fe2:
1346
+ logger.warning(
1347
+ "fetch branch %s failed for worktree %s: %s "
1348
+ "(likely auth/SSH issue — will retry in sync loop)",
1349
+ branch_name, ws_path, _pre_fe2,
1350
+ )
1351
+ _last_sync_err = str(_pre_fe2)[:300]
1307
1352
 
1308
1353
  if fresh_start:
1309
1354
  # Safety check: if the branch already exists on remote with
@@ -1392,8 +1437,12 @@ class WorkspaceManager:
1392
1437
  cwd=ws_path,
1393
1438
  project_key=project_key,
1394
1439
  )
1395
- except RuntimeError:
1396
- pass
1440
+ except RuntimeError as _sf:
1441
+ logger.warning(
1442
+ "Re-fetch %s failed (attempt %d): %s",
1443
+ branch_name, _sync_attempt + 1, _sf,
1444
+ )
1445
+ _last_sync_err = str(_sf)[:300]
1397
1446
  continue
1398
1447
  else:
1399
1448
  logger.warning("Failed to checkout %s after retries: %s", branch_name, exc)
@@ -1419,8 +1468,12 @@ class WorkspaceManager:
1419
1468
  cwd=ws_path,
1420
1469
  project_key=project_key,
1421
1470
  )
1422
- except RuntimeError:
1423
- pass
1471
+ except RuntimeError as _sf2:
1472
+ logger.warning(
1473
+ "Re-fetch %s failed (attempt %d): %s",
1474
+ branch_name, _sync_attempt + 1, _sf2,
1475
+ )
1476
+ _last_sync_err = str(_sf2)[:300]
1424
1477
  else:
1425
1478
  logger.warning(
1426
1479
  "Could not reset to origin/%s after retries: %s — "
@@ -1448,10 +1501,28 @@ class WorkspaceManager:
1448
1501
  f"Stale local clone discarded. "
1449
1502
  f"The task will be retried with a fresh clone."
1450
1503
  )
1504
+ # Destroy the stale worktree before raising so the
1505
+ # next retry can re-create it fresh from origin.
1506
+ # Without this, every retry hits the same broken state.
1507
+ try:
1508
+ await self._remove_broken_worktree(
1509
+ main_repo, ws_path, workspace_key
1510
+ )
1511
+ logger.info(
1512
+ "Removed stale worktree %s — retry will re-clone from origin",
1513
+ ws_path,
1514
+ )
1515
+ except Exception as _rm_exc:
1516
+ logger.warning("Could not remove stale worktree %s: %s", ws_path, _rm_exc)
1517
+ _err_detail = (
1518
+ f"Git error: {_last_sync_err}" if _last_sync_err
1519
+ else "fetch timed out or credentials missing/invalid"
1520
+ )
1451
1521
  raise RuntimeError(
1452
- f"Failed to sync branch '{branch_name}' from remote after 3 attempts. "
1522
+ f"Failed to sync branch '{branch_name}' from remote after 3 attempts "
1523
+ f"({_err_detail}). "
1453
1524
  f"The branch should exist (pushed by prior analysis/design phase). "
1454
- f"This task will be retried by the orchestrator."
1525
+ f"Stale local workspace discarded — this task will be retried by the orchestrator."
1455
1526
  )
1456
1527
  else:
1457
1528
  logger.warning(
@@ -1680,12 +1751,23 @@ class WorkspaceManager:
1680
1751
  # interprets backslashes as escape sequences, corrupting the
1681
1752
  # path (e.g. C:\Users → C:Users).
1682
1753
  key_path_safe = key_path.replace("\\", "/") if sys.platform == "win32" else key_path
1754
+ # RC1 (Windows): os.chmod(S_IRUSR) does not set proper NTFS ACLs.
1755
+ # Windows OpenSSH rejects keys that aren't exclusively owner-readable
1756
+ # ("UNPROTECTED PRIVATE KEY FILE"). StrictModes=no bypasses this.
1757
+ # RC2 (Windows): /dev/null doesn't exist on Windows native OpenSSH
1758
+ # (C:\Windows\System32\OpenSSH\ssh.exe). Use NUL instead.
1759
+ if sys.platform == "win32":
1760
+ _known_hosts_null = "NUL"
1761
+ _strict_modes_opt = " -o StrictModes=no"
1762
+ else:
1763
+ _known_hosts_null = "/dev/null"
1764
+ _strict_modes_opt = ""
1683
1765
  env = {
1684
1766
  **os.environ,
1685
1767
  "GIT_SSH_COMMAND": (
1686
1768
  f'ssh -i "{key_path_safe}"'
1687
1769
  f" -o StrictHostKeyChecking=accept-new"
1688
- f" -o UserKnownHostsFile=/dev/null"
1770
+ f" -o UserKnownHostsFile={_known_hosts_null}"
1689
1771
  f" -o IdentitiesOnly=yes"
1690
1772
  # Detect a stalled TCP connection (server accepts but
1691
1773
  # never sends the git protocol banner). After 30 s of
@@ -1695,6 +1777,7 @@ class WorkspaceManager:
1695
1777
  f" -o ConnectTimeout=30"
1696
1778
  f" -o ServerAliveInterval=30"
1697
1779
  f" -o ServerAliveCountMax=3"
1780
+ f"{_strict_modes_opt}"
1698
1781
  ),
1699
1782
  }
1700
1783
  except Exception:
@@ -5784,6 +5867,24 @@ class RuntimeDaemon:
5784
5867
  branch = (await git("rev-parse", "--abbrev-ref", "HEAD", cwd=workspace_path)).strip()
5785
5868
 
5786
5869
  if branch and branch != "HEAD":
5870
+ # Always refresh the remote tracking ref before any divergence
5871
+ # checks. Without this, origin/{branch} may be stale if another
5872
+ # runtime pushed commits while our agent was executing, causing
5873
+ # the remote_ahead check to return empty and the naive push to
5874
+ # fail with "non-fast-forward". This is the single most reliable
5875
+ # guard for cross-runtime / cross-machine collaboration scenarios.
5876
+ try:
5877
+ await git(
5878
+ "fetch", "origin", branch,
5879
+ cwd=workspace_path, project_key=project_key,
5880
+ )
5881
+ except RuntimeError as _pre_push_fetch_exc:
5882
+ logger.warning(
5883
+ "Pre-push fetch of branch '%s' failed: %s — "
5884
+ "divergence check will use possibly stale tracking ref",
5885
+ branch, _pre_push_fetch_exc,
5886
+ )
5887
+
5787
5888
  # Check if there are unpushed commits
5788
5889
  try:
5789
5890
  unpushed = (await git(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.8.6
3
+ Version: 1.8.7
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.8.6"
3
+ version = "1.8.7"
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