forgexa-cli 1.14.8__tar.gz → 1.14.9__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.8
3
+ Version: 1.14.9
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.8"
2
+ __version__ = "1.14.9"
@@ -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.8"
526
+ DAEMON_VERSION = "1.14.9"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -1892,8 +1892,16 @@ class WorkspaceManager:
1892
1892
  # the initial analysis. This ensures a hard error (and workspace
1893
1893
  # re-clone) when the branch sync fails, rather than silently proceeding
1894
1894
  # with a stale workspace that will cause a non-fast-forward push later.
1895
- expect_branch = bool(task.analysis_branch) or (
1896
- bool(task.requirement_key) and not is_fresh_start and task.node_type != "analysis"
1895
+ #
1896
+ # IMPORTANT: Fresh analysis tasks MUST NOT set expect_branch even when
1897
+ # task.analysis_branch is populated from workflow metadata. That value
1898
+ # reflects the branch from a *previous* analysis run (e.g. a prior Copilot
1899
+ # analysis) which may have been deleted as part of the fresh restart.
1900
+ # Only refine/continuation tasks require the branch to already exist.
1901
+ expect_branch = (not is_fresh_start) and (
1902
+ bool(task.analysis_branch) or (
1903
+ bool(task.requirement_key) and task.node_type != "analysis"
1904
+ )
1897
1905
  )
1898
1906
  ws_path = await self._create_worktree(
1899
1907
  project_dir, repo_url, default_branch, workspace_key, branch_name,
@@ -2394,16 +2402,41 @@ class WorkspaceManager:
2394
2402
  _branch_fetch_ok = False
2395
2403
 
2396
2404
  if fresh_start and not _branch_fetch_ok:
2397
- # Cannot determine remote branch state prohibit reset to avoid
2398
- # silently destroying implementation commits that exist on the remote
2399
- # but could not be fetched (auth/network failure). Conservative
2400
- # stance: treat unverified state as "has implementation work".
2401
- logger.warning(
2402
- "Fresh start blocked for %s: branch fetch failed (%s) — "
2403
- "remote state unknown, refusing to reset to avoid data loss",
2404
- branch_name, _last_sync_err[:200],
2405
+ # Distinguish "branch not found" from "auth/network failure".
2406
+ # When the branch simply doesn't exist on the remote, that is the
2407
+ # *expected* state for a fresh analysis restart (the branch was
2408
+ # deleted by a prior fresh run or was never pushed). In that case
2409
+ # keep fresh_start=True so the worktree is correctly reset to the
2410
+ # default branch.
2411
+ # Only block fresh_start when the fetch failed for reasons other
2412
+ # than "not found" (e.g. auth/SSH/network issues) where we genuinely
2413
+ # cannot determine whether implementation commits exist on the remote.
2414
+ _err_lower = _last_sync_err.lower()
2415
+ _branch_not_found = (
2416
+ "couldn't find remote ref" in _err_lower
2417
+ or "无法找到远程引用" in _err_lower # Chinese git output
2418
+ or "remote ref does not exist" in _err_lower
2419
+ or "not found" in _err_lower
2405
2420
  )
2406
- fresh_start = False
2421
+ if _branch_not_found:
2422
+ # Branch is gone from remote — ideal state for a fresh restart.
2423
+ logger.info(
2424
+ "Fresh start for %s: remote branch not found — expected for a "
2425
+ "fresh analysis restart (branch was deleted or never existed). "
2426
+ "Proceeding with clean worktree reset to origin/%s",
2427
+ branch_name, default_branch,
2428
+ )
2429
+ # fresh_start remains True
2430
+ else:
2431
+ # Cannot determine remote branch state (auth/network failure) —
2432
+ # prohibit reset to avoid silently destroying implementation commits
2433
+ # that may exist on the remote but could not be fetched.
2434
+ logger.warning(
2435
+ "Fresh start blocked for %s: branch fetch failed (%s) — "
2436
+ "remote state unknown, refusing to reset to avoid data loss",
2437
+ branch_name, _last_sync_err[:200],
2438
+ )
2439
+ fresh_start = False
2407
2440
 
2408
2441
  if fresh_start:
2409
2442
  # Safety check: if the branch already has IMPLEMENTATION commits
@@ -4533,6 +4566,17 @@ class ProcessManager:
4533
4566
 
4534
4567
  Mirror the user-facing ~/.copilot state into a writable daemon-owned
4535
4568
  directory, then point COPILOT_HOME there for non-interactive runs.
4569
+
4570
+ Stale-auth cleanup: Before the per-task isolation fix, the daemon
4571
+ pointed COPILOT_HOME directly at this master directory, so Copilot
4572
+ wrote its own OAuth token (plain-text on Linux systems without a
4573
+ credential store) here. Those files are NOT present in ~/.copilot/
4574
+ (the user's interactive home), so the normal mirror loop never
4575
+ overwrites them — they persist across restarts and poison isolated
4576
+ temp dirs. We clean up any file/dir in the master home that has no
4577
+ counterpart in ~/.copilot/ (excluding session-state and logs which
4578
+ are deliberately retained) to ensure the master home stays a clean
4579
+ mirror of the user-facing directory.
4536
4580
  """
4537
4581
  source_root = Path.home() / ".copilot"
4538
4582
  target_root = Path.home() / ".forgexa" / "copilot-home"
@@ -4541,6 +4585,26 @@ class ProcessManager:
4541
4585
  if not source_root.exists():
4542
4586
  return str(target_root)
4543
4587
 
4588
+ # Collect names present in source for stale-file detection.
4589
+ source_names: set[str] = {child.name for child in source_root.iterdir()}
4590
+
4591
+ # Remove master-home entries that are absent from ~/.copilot/.
4592
+ # These are typically old Copilot OAuth token files written when
4593
+ # COPILOT_HOME pointed here directly (pre-isolation-fix era).
4594
+ _keep_always = {"session-state", "logs"}
4595
+ for target_child in list(target_root.iterdir()):
4596
+ if target_child.name in _keep_always:
4597
+ continue
4598
+ if target_child.name not in source_names:
4599
+ try:
4600
+ if target_child.is_dir():
4601
+ shutil.rmtree(str(target_child), ignore_errors=True)
4602
+ else:
4603
+ target_child.unlink(missing_ok=True)
4604
+ logger.debug("Copilot home: removed stale %s", target_child.name)
4605
+ except Exception as exc:
4606
+ logger.debug("Copilot home: could not remove stale %s: %s", target_child.name, exc)
4607
+
4544
4608
  for child in source_root.iterdir():
4545
4609
  if child.name in {"logs", "session-state"}:
4546
4610
  continue
@@ -4628,6 +4692,37 @@ class ProcessManager:
4628
4692
  env["TERM"] = "dumb" # suppress TTY-detection that suspends the process
4629
4693
  env["COPILOT_HOME"] = _isolated_copilot_home
4630
4694
 
4695
+ # Inject GH_TOKEN from the gh CLI for headless/automation auth.
4696
+ # Per `copilot login --help`, env-var auth is the recommended method
4697
+ # for automation: COPILOT_GITHUB_TOKEN > GH_TOKEN > GITHUB_TOKEN.
4698
+ # GH_TOKEN accepts OAuth tokens from the GitHub CLI (gh) app, which
4699
+ # is exactly what `gh auth token` returns.
4700
+ #
4701
+ # This bypasses any stale Copilot-specific OAuth token file that may
4702
+ # be cached in COPILOT_HOME from an earlier run (e.g., after a
4703
+ # password change when `gh auth login` refreshes gh's token but does
4704
+ # NOT update Copilot's own stored token). After `gh auth login` the
4705
+ # daemon works again without requiring `copilot login` or manual
4706
+ # deletion of ~/.forgexa/copilot-home/.
4707
+ if not any(env.get(k) for k in ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN")):
4708
+ try:
4709
+ _gh_proc = await asyncio.create_subprocess_exec(
4710
+ "gh", "auth", "token",
4711
+ stdout=asyncio.subprocess.PIPE,
4712
+ stderr=asyncio.subprocess.DEVNULL,
4713
+ stdin=asyncio.subprocess.DEVNULL,
4714
+ )
4715
+ _gh_token_out, _ = await asyncio.wait_for(_gh_proc.communicate(), timeout=10.0)
4716
+ _gh_token = (_gh_token_out or b"").decode().strip()
4717
+ if _gh_token:
4718
+ env["GH_TOKEN"] = _gh_token
4719
+ logger.debug("Copilot task %s: injected GH_TOKEN from gh CLI", task_id)
4720
+ except Exception as _gh_exc:
4721
+ logger.debug(
4722
+ "Copilot task %s: could not inject GH_TOKEN (%s); falling back to stored auth",
4723
+ task_id, _gh_exc,
4724
+ )
4725
+
4631
4726
  model_override = os.environ.get("FACTORY_COPILOT_MODEL")
4632
4727
  # FACTORY_COPILOT_REASONING is opt-in: only pass --effort when the env
4633
4728
  # var is explicitly set to a non-empty string. The default (unset / "")
@@ -4768,12 +4863,27 @@ class ProcessManager:
4768
4863
  if effective_rc == 1 and _no_tools and not copilot_specific_error:
4769
4864
  logger.error(
4770
4865
  "Copilot exitCode=1 with no tool calls for task %s after retries. "
4771
- "Likely causes: (1) GitHub auth expired — run `gh auth status` and "
4772
- "`gh auth login --scopes copilot`; (2) Copilot subscription inactive; "
4866
+ "Likely causes: (1) GitHub auth expired — run `gh auth status`; if "
4867
+ "expired, run `copilot login` (Copilot's own auth) and `gh auth login`; "
4868
+ "(2) Copilot subscription inactive; "
4773
4869
  "(3) --allow-all flag not supported by this CLI version (%s). "
4774
4870
  "To reproduce manually: %s",
4775
4871
  task_id, agent.version, _cmd_display,
4776
4872
  )
4873
+ # Detect auth-specific error for targeted diagnostics.
4874
+ _is_auth_error = any(
4875
+ "gh auth login" in str(msg) or "authenticate with the GitHub" in str(msg)
4876
+ for msg in structured_errors
4877
+ )
4878
+ if _is_auth_error:
4879
+ logger.error(
4880
+ "Copilot auth error for task %s: %s. "
4881
+ "Fix: (1) run 'gh auth login' to refresh gh credentials; "
4882
+ "(2) run 'copilot login' to refresh Copilot's own OAuth token; "
4883
+ "(3) if the issue persists, delete ~/.forgexa/copilot-home/ and retry.",
4884
+ task_id,
4885
+ structured_errors[-1] if structured_errors else "auth failure",
4886
+ )
4777
4887
  if copilot_specific_error:
4778
4888
  failure_error = copilot_specific_error[:1500]
4779
4889
  elif stderr.strip():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.14.8
3
+ Version: 1.14.9
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.8"
3
+ version = "1.14.9"
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