forgexa-cli 1.68.1__tar.gz → 1.68.2__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.
Files changed (31) hide show
  1. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/daemon.py +131 -4
  4. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/pyproject.toml +1 -1
  6. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/README.md +0 -0
  7. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/py.typed +0 -0
  15. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/runtime_evidence.py +0 -0
  16. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli/zcode_protocol.py +0 -0
  17. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  18. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  19. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  20. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli.egg-info/requires.txt +0 -0
  21. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  22. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/setup.cfg +0 -0
  23. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_auth_and_runtime_commands.py +0 -0
  24. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_autoupgrade.py +0 -0
  25. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_check_command.py +0 -0
  26. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  27. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_local_bind_commands.py +0 -0
  28. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_runtime_credentials.py +0 -0
  29. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_session_credentials.py +0 -0
  30. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_silent_install.py +0 -0
  31. {forgexa_cli-1.68.1 → forgexa_cli-1.68.2}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.68.1
3
+ Version: 1.68.2
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-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.68.1"
2
+ __version__ = "1.68.2"
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
1234
1234
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1235
1235
  # Kept in sync with pyproject.toml version via bump-version.sh.
1236
1236
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1237
- DAEMON_VERSION = "1.68.1"
1237
+ DAEMON_VERSION = "1.68.2"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -3655,6 +3655,8 @@ class WorkspaceManager:
3655
3655
  Validates the daemon-private mapping entry (binding_id -> local_path),
3656
3656
  the directory's git remote identity, branch, and tracked changes.
3657
3657
  Pre-existing untracked and ignored files remain user-owned and local.
3658
+ Uncommitted task-owned requirement artifacts are backed up locally
3659
+ before restoring their committed baseline. User code is never reset.
3658
3660
  Ordinary local-bind tasks perform NO destructive git operation
3659
3661
  (no reset --hard, clean, branch deletion, or implicit branch switch)
3660
3662
  on the user's real directory. The only exception is an explicitly
@@ -3747,9 +3749,14 @@ class WorkspaceManager:
3747
3749
  "status", "--porcelain=v1", "--untracked-files=no", "-z", cwd=local_path,
3748
3750
  )
3749
3751
  dirty_paths = _git_status_paths(status_output)
3750
- if dirty_paths:
3751
- shown = ", ".join(dirty_paths[:20])
3752
- more = f" (+{len(dirty_paths) - 20} more)" if len(dirty_paths) > 20 else ""
3752
+ recoverable_paths = self._local_requirement_artifact_paths(task, local_path)
3753
+ user_dirty_paths = [
3754
+ path for path in dirty_paths
3755
+ if path not in recoverable_paths or not path.startswith("docs/requirements/")
3756
+ ]
3757
+ if user_dirty_paths:
3758
+ shown = ", ".join(user_dirty_paths[:20])
3759
+ more = f" (+{len(user_dirty_paths) - 20} more)" if len(user_dirty_paths) > 20 else ""
3753
3760
  raise RuntimeError(
3754
3761
  "local_binding_unavailable: local directory has uncommitted tracked changes, "
3755
3762
  f"refusing to overwrite user work: {shown}{more}. Commit or stash these "
@@ -3796,6 +3803,7 @@ class WorkspaceManager:
3796
3803
  remote_head = (
3797
3804
  await self._git("rev-parse", f"origin/{target_branch}", cwd=local_path)
3798
3805
  ).strip()
3806
+ await self._recover_local_requirement_artifacts(local_path, task, recoverable_paths)
3799
3807
  if local_head != remote_head:
3800
3808
  await self._check_local_untracked_overwrites(local_path, remote_head)
3801
3809
  try:
@@ -3841,6 +3849,7 @@ class WorkspaceManager:
3841
3849
  f"local branch is not at origin/{default_branch}; refusing to bootstrap from "
3842
3850
  "unpushed or stale history"
3843
3851
  )
3852
+ await self._recover_local_requirement_artifacts(local_path, task, recoverable_paths)
3844
3853
 
3845
3854
  if (
3846
3855
  task.node_type == "analysis"
@@ -3881,8 +3890,126 @@ class WorkspaceManager:
3881
3890
  f"HEAD after fetch — cross-phase handoff incomplete: {', '.join(missing_shas)}"
3882
3891
  )
3883
3892
 
3893
+ task.local_untracked_paths = {
3894
+ path for path in (await self._git(
3895
+ "ls-files", "--others", "-z", cwd=local_path,
3896
+ )).split("\0") if path
3897
+ }
3884
3898
  return local_path
3885
3899
 
3900
+ @staticmethod
3901
+ def _local_requirement_artifact_paths(task: TaskInfo, local_path: Path | None = None) -> set[str]:
3902
+ key = str(task.requirement_key or "")
3903
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", key):
3904
+ return set()
3905
+ prefix = f"docs/requirements/{key}/"
3906
+ data = task.input_data or {}
3907
+ context = data.get("context") or {}
3908
+ names = {
3909
+ name for outputs in _ANALYSIS_OUTPUTS_BY_TYPE.values() for name in outputs
3910
+ } | {
3911
+ "design.md", "test-cases.json", "coverage-matrix.json",
3912
+ "test-evidence.json", "test-report.md", "release-note.md",
3913
+ }
3914
+ names.update(
3915
+ _get_design_output_for_type(requirement_type)
3916
+ for requirement_type in _ANALYSIS_OUTPUTS_BY_TYPE
3917
+ )
3918
+ paths: set[str] = set()
3919
+ for values in (data, context):
3920
+ for field_name in ("analysis_output_dir", "output_dir"):
3921
+ directory = str(values.get(field_name) or "").replace("\\", "/").rstrip("/")
3922
+ if (
3923
+ directory.startswith(prefix)
3924
+ and not any(part in ("", ".", "..") for part in directory.split("/"))
3925
+ ):
3926
+ paths.update(f"{directory}/{name}" for name in names)
3927
+ if local_path is not None:
3928
+ for manifest_path in sorted(paths):
3929
+ if not manifest_path.endswith("/test-cases.json"):
3930
+ continue
3931
+ manifest_file = local_path / manifest_path
3932
+ if not manifest_file.resolve().is_relative_to(local_path.resolve()):
3933
+ continue
3934
+ try:
3935
+ manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
3936
+ except (OSError, ValueError, UnicodeError):
3937
+ continue
3938
+ cases = manifest.get("test_cases") if isinstance(manifest, dict) else None
3939
+ if not isinstance(cases, list):
3940
+ continue
3941
+ for case in cases:
3942
+ if not isinstance(case, dict):
3943
+ continue
3944
+ case_id = str(case.get("id") or "")
3945
+ script_path = str(case.get("script_path") or "").replace("\\", "/")
3946
+ parts = script_path.split("/")
3947
+ if (
3948
+ re.fullmatch(r"TC-\d+", case_id)
3949
+ and len(parts) == 3
3950
+ and parts[0] == "tests"
3951
+ and parts[1] in ("e2e", "unit", "integration")
3952
+ and parts[2].startswith((case_id + "-", case_id + "."))
3953
+ and parts[2].endswith((".spec.ts", ".spec.js", ".test.ts", ".test.js", ".py"))
3954
+ ):
3955
+ paths.add(script_path)
3956
+ return paths
3957
+
3958
+ async def _recover_local_requirement_artifacts(
3959
+ self, local_path: Path, task: TaskInfo, recoverable_paths: set[str],
3960
+ ) -> None:
3961
+ if not recoverable_paths:
3962
+ return
3963
+ git = self._git
3964
+ status_output = await git(
3965
+ "status", "--porcelain=v1", "--untracked-files=all", "-z", cwd=local_path,
3966
+ )
3967
+ paths = sorted({
3968
+ path for status, path in _git_status_entries(status_output)
3969
+ if path in recoverable_paths
3970
+ and (status == "??" or path.startswith("docs/requirements/"))
3971
+ })
3972
+ if not paths:
3973
+ return
3974
+ workspace_root = local_path.resolve()
3975
+ if any(not (local_path / path).resolve().is_relative_to(workspace_root) for path in paths):
3976
+ raise RuntimeError(
3977
+ "local_binding_unavailable: requirement artifact resolves outside the local "
3978
+ "workspace; refusing to recover files through an external symbolic link"
3979
+ )
3980
+ recovery_ref = f"refs/forgexa/local-artifact-recovery/{task.task_id}/{time.time_ns()}"
3981
+ await git("check-ref-format", recovery_ref, cwd=local_path)
3982
+ name, email = _resolve_git_author(task.project)
3983
+ try:
3984
+ await git(
3985
+ "-c", f"user.name={name}", "-c", f"user.email={email}",
3986
+ "stash", "push", "--include-untracked", "--message",
3987
+ f"Forgexa requirement artifact recovery for {task.task_id}", "--",
3988
+ *(f":(literal){path}" for path in paths), cwd=local_path,
3989
+ )
3990
+ recovery_sha = (await git("rev-parse", "refs/stash", cwd=local_path)).strip()
3991
+ await git("update-ref", recovery_ref, recovery_sha, cwd=local_path)
3992
+ except RuntimeError as exc:
3993
+ raise RuntimeError(
3994
+ "local_binding_unavailable: could not preserve uncommitted requirement "
3995
+ "artifacts; any created backup remains in the local Git stash. "
3996
+ "No user code was discarded."
3997
+ ) from exc
3998
+ _append_local_bind_output_log(task.task_id, [
3999
+ f"[daemon] Preserved {len(paths)} unfinished requirement artifact(s) in "
4000
+ f"{recovery_ref}; restored their committed baseline for this task. "
4001
+ f"Recover with git stash apply --index {recovery_ref}.",
4002
+ ])
4003
+ logger.info("Preserved local requirement artifacts for task %s in %s", task.task_id, recovery_ref)
4004
+ remaining_status = await git(
4005
+ "status", "--porcelain=v1", "--untracked-files=no", "-z", cwd=local_path,
4006
+ )
4007
+ if _git_status_paths(remaining_status):
4008
+ raise RuntimeError(
4009
+ "local_binding_unavailable: tracked changes remain after requirement artifact "
4010
+ f"recovery; no task was started. Preserved artifacts are in {recovery_ref}."
4011
+ )
4012
+
3886
4013
  async def _check_local_untracked_overwrites(self, local_path: Path, revision: str) -> None:
3887
4014
  untracked_paths = {
3888
4015
  path for path in (await self._git(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.68.1
3
+ Version: 1.68.2
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-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.68.1"
3
+ version = "1.68.2"
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 = "MIT"
File without changes
File without changes