forgexa-cli 1.65.0__tar.gz → 1.65.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.
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/PKG-INFO +1 -1
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/daemon.py +97 -20
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/pyproject.toml +1 -1
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/README.md +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/setup.cfg +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.65.0 → forgexa_cli-1.65.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.65.
|
|
2
|
+
__version__ = "1.65.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.65.
|
|
1237
|
+
DAEMON_VERSION = "1.65.2"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -8494,7 +8494,7 @@ class ProcessManager:
|
|
|
8494
8494
|
conventional `main` / `master` refs. Falls back to HEAD~1 if merge-base
|
|
8495
8495
|
detection fails.
|
|
8496
8496
|
"""
|
|
8497
|
-
info: dict[str, Any] = {}
|
|
8497
|
+
info: dict[str, Any] = {"files_changed_verified": False}
|
|
8498
8498
|
try:
|
|
8499
8499
|
# Prefer the exact pre-node HEAD when available so the diff is
|
|
8500
8500
|
# scoped to this node's work, not the whole requirement branch.
|
|
@@ -8531,8 +8531,11 @@ class ProcessManager:
|
|
|
8531
8531
|
**_agent_process_group_kwargs(),
|
|
8532
8532
|
)
|
|
8533
8533
|
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
|
|
8534
|
+
if proc.returncode != 0:
|
|
8535
|
+
return info
|
|
8534
8536
|
files = [f for f in stdout.decode().strip().split("\n") if f]
|
|
8535
8537
|
info["files_changed"] = files
|
|
8538
|
+
info["files_changed_verified"] = True
|
|
8536
8539
|
|
|
8537
8540
|
# Lines added/removed since branch point
|
|
8538
8541
|
proc2 = await _create_subprocess_exec(
|
|
@@ -10533,6 +10536,25 @@ def _parse_frozen_qa_test_level_policy(
|
|
|
10533
10536
|
}, []
|
|
10534
10537
|
|
|
10535
10538
|
|
|
10539
|
+
def _qa_source_test_validation_required(snapshot: object) -> bool:
|
|
10540
|
+
policy, issues = _parse_frozen_qa_test_level_policy(snapshot)
|
|
10541
|
+
return bool(issues or (
|
|
10542
|
+
policy is not None
|
|
10543
|
+
and policy["enforcement"] == "enforce"
|
|
10544
|
+
and any(not config["enabled"] for config in policy["levels"].values())
|
|
10545
|
+
))
|
|
10546
|
+
|
|
10547
|
+
|
|
10548
|
+
def _mark_qa_source_validation_unavailable(result: TaskResult) -> None:
|
|
10549
|
+
result.status = "failed"
|
|
10550
|
+
result.failure_code = "validation_retry_exhausted"
|
|
10551
|
+
result.error = (
|
|
10552
|
+
"qa_policy.validation_unavailable: source-test output validation could not be completed; "
|
|
10553
|
+
"refusing to commit or push unvalidated changes."
|
|
10554
|
+
)
|
|
10555
|
+
result.metrics["validation_issues"] = [result.error]
|
|
10556
|
+
|
|
10557
|
+
|
|
10536
10558
|
def _validate_enforced_qa_test_level_artifacts(
|
|
10537
10559
|
policy: dict[str, Any],
|
|
10538
10560
|
test_intent_data: object,
|
|
@@ -12799,19 +12821,18 @@ class RuntimeDaemon:
|
|
|
12799
12821
|
if result.metrics.get("validation_issues") is not None or True:
|
|
12800
12822
|
pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
|
|
12801
12823
|
except Exception:
|
|
12802
|
-
|
|
12824
|
+
if _qa_source_test_validation_required(
|
|
12825
|
+
(task.input_data or {}).get("qa_test_level_policy_snapshot")
|
|
12826
|
+
):
|
|
12827
|
+
_mark_qa_source_validation_unavailable(result)
|
|
12828
|
+
logger.exception("Validation gate error for task %s (refusing to proceed)", task.task_id)
|
|
12829
|
+
else:
|
|
12830
|
+
logger.exception("Validation gate error for task %s (proceeding anyway)", task.task_id)
|
|
12803
12831
|
|
|
12804
12832
|
if result.failure_code == "verification_work_item_scope_violation" and not is_local_bind:
|
|
12805
12833
|
await self._discard_verification_scope_changes(
|
|
12806
12834
|
workspace_path, task, result, node_before_sha,
|
|
12807
12835
|
)
|
|
12808
|
-
elif result.failure_code == "validation_retry_exhausted" and not is_local_bind and any(
|
|
12809
|
-
str(issue).startswith("qa_policy.source_test_level_disabled")
|
|
12810
|
-
for issue in (result.metrics.get("validation_issues") or [])
|
|
12811
|
-
):
|
|
12812
|
-
await self._discard_qa_policy_source_test_changes(
|
|
12813
|
-
workspace_path, task, result, node_before_sha,
|
|
12814
|
-
)
|
|
12815
12836
|
|
|
12816
12837
|
# 4.6 Post-validation rate-limit fallback.
|
|
12817
12838
|
# _validate_and_retry returns early (preserving the rate-limit error)
|
|
@@ -13005,6 +13026,32 @@ class RuntimeDaemon:
|
|
|
13005
13026
|
task.task_id, _aj_err,
|
|
13006
13027
|
)
|
|
13007
13028
|
|
|
13029
|
+
if result.status == "success" and _qa_source_test_validation_required(
|
|
13030
|
+
(task.input_data or {}).get("qa_test_level_policy_snapshot")
|
|
13031
|
+
):
|
|
13032
|
+
previous_validation_retries = result.metrics.get("validation_retries_used", 0)
|
|
13033
|
+
try:
|
|
13034
|
+
result = await self._validate_and_retry(
|
|
13035
|
+
agent, task, workspace_path, result, reporter, on_output_chunk,
|
|
13036
|
+
max_retries=0, default_branch=default_branch, before_sha=node_before_sha,
|
|
13037
|
+
)
|
|
13038
|
+
except Exception:
|
|
13039
|
+
_mark_qa_source_validation_unavailable(result)
|
|
13040
|
+
logger.exception("Final QA source-test validation error for task %s", task.task_id)
|
|
13041
|
+
result.metrics["validation_retries_used"] = previous_validation_retries
|
|
13042
|
+
|
|
13043
|
+
if result.failure_code == "validation_retry_exhausted" and not is_local_bind and any(
|
|
13044
|
+
str(issue).startswith((
|
|
13045
|
+
"qa_policy.source_test_level_disabled",
|
|
13046
|
+
"qa_policy.source_change_inventory_unavailable",
|
|
13047
|
+
"qa_policy.validation_unavailable",
|
|
13048
|
+
))
|
|
13049
|
+
for issue in (result.metrics.get("validation_issues") or [])
|
|
13050
|
+
):
|
|
13051
|
+
await self._discard_qa_policy_source_test_changes(
|
|
13052
|
+
workspace_path, task, result, node_before_sha,
|
|
13053
|
+
)
|
|
13054
|
+
|
|
13008
13055
|
# 5. Auto-commit and push if changes exist
|
|
13009
13056
|
if result.status == "success" and not is_local_bind:
|
|
13010
13057
|
workspace_health_error = await self._workspace_checkout_health_error(
|
|
@@ -13468,7 +13515,7 @@ class RuntimeDaemon:
|
|
|
13468
13515
|
input_data.get("qa_test_level_policy_snapshot")
|
|
13469
13516
|
)
|
|
13470
13517
|
issues.extend(qa_policy_issues)
|
|
13471
|
-
if qa_policy is not None
|
|
13518
|
+
if qa_policy is not None:
|
|
13472
13519
|
issues.extend(_validate_enforced_qa_source_test_paths(
|
|
13473
13520
|
qa_policy,
|
|
13474
13521
|
result.files_changed,
|
|
@@ -15353,14 +15400,19 @@ class RuntimeDaemon:
|
|
|
15353
15400
|
|
|
15354
15401
|
Returns the (possibly updated) TaskResult.
|
|
15355
15402
|
"""
|
|
15403
|
+
require_source_inventory = _qa_source_test_validation_required(
|
|
15404
|
+
(task.input_data or {}).get("qa_test_level_policy_snapshot")
|
|
15405
|
+
)
|
|
15356
15406
|
for attempt in range(max_retries):
|
|
15357
|
-
await self._refresh_validation_changed_paths(
|
|
15407
|
+
inventory_issues = await self._refresh_validation_changed_paths(
|
|
15358
15408
|
workspace_path,
|
|
15359
15409
|
result,
|
|
15360
15410
|
default_branch=default_branch,
|
|
15361
15411
|
before_sha=before_sha,
|
|
15362
15412
|
)
|
|
15363
15413
|
issues = self._validate_outputs(workspace_path, task, result)
|
|
15414
|
+
if require_source_inventory:
|
|
15415
|
+
issues.extend(inventory_issues)
|
|
15364
15416
|
if not issues:
|
|
15365
15417
|
self._record_testing_telemetry(
|
|
15366
15418
|
workspace_path, task, result, retries_used=attempt
|
|
@@ -15475,13 +15527,15 @@ class RuntimeDaemon:
|
|
|
15475
15527
|
return result
|
|
15476
15528
|
|
|
15477
15529
|
# Final check after all retries
|
|
15478
|
-
await self._refresh_validation_changed_paths(
|
|
15530
|
+
inventory_issues = await self._refresh_validation_changed_paths(
|
|
15479
15531
|
workspace_path,
|
|
15480
15532
|
result,
|
|
15481
15533
|
default_branch=default_branch,
|
|
15482
15534
|
before_sha=before_sha,
|
|
15483
15535
|
)
|
|
15484
15536
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
15537
|
+
if require_source_inventory:
|
|
15538
|
+
remaining.extend(inventory_issues)
|
|
15485
15539
|
if remaining:
|
|
15486
15540
|
if getattr(task, "workspace_resolution", None) == "local_bind":
|
|
15487
15541
|
_append_local_bind_output_log(task.task_id, [
|
|
@@ -15572,11 +15626,12 @@ class RuntimeDaemon:
|
|
|
15572
15626
|
*,
|
|
15573
15627
|
default_branch: str,
|
|
15574
15628
|
before_sha: str,
|
|
15575
|
-
) ->
|
|
15629
|
+
) -> list[str]:
|
|
15576
15630
|
"""Merge reported, working-tree, and already-committed task paths."""
|
|
15577
15631
|
reported_paths = ProcessManager._normalize_repo_paths(result.files_changed)
|
|
15578
15632
|
paths: set[str] = set()
|
|
15579
15633
|
collected_paths = False
|
|
15634
|
+
inventory_issues: list[str] = []
|
|
15580
15635
|
try:
|
|
15581
15636
|
status_output = await self.workspace_manager._git(
|
|
15582
15637
|
"status",
|
|
@@ -15590,6 +15645,9 @@ class RuntimeDaemon:
|
|
|
15590
15645
|
))
|
|
15591
15646
|
collected_paths = True
|
|
15592
15647
|
except Exception:
|
|
15648
|
+
inventory_issues.append(
|
|
15649
|
+
"qa_policy.source_change_inventory_unavailable: working-tree changed paths could not be verified."
|
|
15650
|
+
)
|
|
15593
15651
|
logger.debug(
|
|
15594
15652
|
"Could not collect working-tree paths for validation", exc_info=True,
|
|
15595
15653
|
)
|
|
@@ -15600,15 +15658,27 @@ class RuntimeDaemon:
|
|
|
15600
15658
|
default_branch=default_branch,
|
|
15601
15659
|
before_sha=before_sha,
|
|
15602
15660
|
)
|
|
15661
|
+
if committed_git.get("files_changed_verified") is False:
|
|
15662
|
+
inventory_issues.append(
|
|
15663
|
+
"qa_policy.source_change_inventory_unavailable: committed changed paths could not be verified."
|
|
15664
|
+
)
|
|
15603
15665
|
paths.update(ProcessManager._normalize_repo_paths(
|
|
15604
15666
|
committed_git.get("files_changed") or []
|
|
15605
15667
|
))
|
|
15606
15668
|
collected_paths = True
|
|
15607
15669
|
except Exception:
|
|
15670
|
+
inventory_issues.append(
|
|
15671
|
+
"qa_policy.source_change_inventory_unavailable: committed changed paths could not be verified."
|
|
15672
|
+
)
|
|
15608
15673
|
logger.debug(
|
|
15609
15674
|
"Could not collect committed paths for validation", exc_info=True,
|
|
15610
15675
|
)
|
|
15676
|
+
else:
|
|
15677
|
+
inventory_issues.append(
|
|
15678
|
+
"qa_policy.source_change_inventory_unavailable: the pre-node commit baseline is unavailable."
|
|
15679
|
+
)
|
|
15611
15680
|
result.files_changed = sorted(paths if collected_paths else reported_paths)
|
|
15681
|
+
return inventory_issues
|
|
15612
15682
|
|
|
15613
15683
|
def _record_testing_telemetry(
|
|
15614
15684
|
self, workspace_path: Path, task: TaskInfo, result: TaskResult, retries_used: int
|
|
@@ -15980,7 +16050,7 @@ class RuntimeDaemon:
|
|
|
15980
16050
|
before_sha: str,
|
|
15981
16051
|
) -> None:
|
|
15982
16052
|
"""Discard cloud-worktree changes that violate disabled test levels."""
|
|
15983
|
-
if
|
|
16053
|
+
if not before_sha or getattr(task, "workspace_resolution", None) == "local_bind":
|
|
15984
16054
|
return
|
|
15985
16055
|
|
|
15986
16056
|
policy, _ = _parse_frozen_qa_test_level_policy(
|
|
@@ -15997,18 +16067,25 @@ class RuntimeDaemon:
|
|
|
15997
16067
|
and not policy["levels"][level]["enabled"]
|
|
15998
16068
|
)
|
|
15999
16069
|
)
|
|
16000
|
-
|
|
16070
|
+
unverified_changes = any(
|
|
16071
|
+
str(issue).startswith((
|
|
16072
|
+
"qa_policy.source_change_inventory_unavailable", "qa_policy.validation_unavailable",
|
|
16073
|
+
))
|
|
16074
|
+
for issue in (result.metrics.get("validation_issues") or [])
|
|
16075
|
+
)
|
|
16076
|
+
if not forbidden_paths and not unverified_changes:
|
|
16001
16077
|
return
|
|
16002
16078
|
|
|
16003
16079
|
try:
|
|
16004
16080
|
await self.workspace_manager._git(
|
|
16005
16081
|
"reset", "--hard", before_sha, cwd=workspace_path,
|
|
16006
16082
|
)
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16083
|
+
if forbidden_paths:
|
|
16084
|
+
await self.workspace_manager._git(
|
|
16085
|
+
"clean", "-f", "--", *forbidden_paths, cwd=workspace_path,
|
|
16086
|
+
)
|
|
16010
16087
|
logger.warning(
|
|
16011
|
-
"
|
|
16088
|
+
"Rolled back task %s after QA source-test validation failure: %s",
|
|
16012
16089
|
task.task_id,
|
|
16013
16090
|
forbidden_paths,
|
|
16014
16091
|
)
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|