forgexa-cli 1.57.4__tar.gz → 1.57.6__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.57.4 → forgexa_cli-1.57.6}/PKG-INFO +1 -1
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/daemon.py +121 -35
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/pyproject.toml +1 -1
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/README.md +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/setup.cfg +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.57.
|
|
2
|
+
__version__ = "1.57.6"
|
|
@@ -1150,7 +1150,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1150
1150
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1151
1151
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1152
1152
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1153
|
-
DAEMON_VERSION = "1.57.
|
|
1153
|
+
DAEMON_VERSION = "1.57.6"
|
|
1154
1154
|
|
|
1155
1155
|
|
|
1156
1156
|
def _detect_client_type() -> str:
|
|
@@ -1736,6 +1736,7 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
|
1736
1736
|
result.stdout = ""
|
|
1737
1737
|
result.stderr = ""
|
|
1738
1738
|
result.observations = []
|
|
1739
|
+
result.metrics.pop("validation_issues", None)
|
|
1739
1740
|
critic_review = result.critic_review
|
|
1740
1741
|
if isinstance(critic_review, dict) and isinstance(critic_review.get("result"), dict):
|
|
1741
1742
|
result.critic_review = {
|
|
@@ -1755,8 +1756,11 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
|
1755
1756
|
}
|
|
1756
1757
|
if result.error:
|
|
1757
1758
|
result.error = (
|
|
1758
|
-
"
|
|
1759
|
-
"
|
|
1759
|
+
"Task verification is incomplete: acceptance criteria remain uncovered. "
|
|
1760
|
+
"Review the validation log in Forgexa Desktop; resolve the criteria and "
|
|
1761
|
+
"commit or stash local changes before retrying."
|
|
1762
|
+
if result.failure_code == "acceptance_criteria_uncovered"
|
|
1763
|
+
else "Local task failed. Review this task in Forgexa Desktop on the running device."
|
|
1760
1764
|
)
|
|
1761
1765
|
|
|
1762
1766
|
def _prepare_ai_job_output(
|
|
@@ -5726,7 +5730,9 @@ class ProcessManager:
|
|
|
5726
5730
|
@staticmethod
|
|
5727
5731
|
def agent_failure_kind(result: "TaskResult") -> str | None:
|
|
5728
5732
|
"""Classify a fallback-eligible failure without conflating its cause."""
|
|
5729
|
-
if result.failure_code in {
|
|
5733
|
+
if result.failure_code in {
|
|
5734
|
+
"all_agents_rate_limited", "all_agents_unavailable", "acceptance_criteria_uncovered",
|
|
5735
|
+
}:
|
|
5730
5736
|
return None
|
|
5731
5737
|
if result.failure_code == "agent_platform_unsupported":
|
|
5732
5738
|
return "agent_unavailable"
|
|
@@ -9520,7 +9526,7 @@ class ServerConnection:
|
|
|
9520
9526
|
self._publish_local_runtime_registration()
|
|
9521
9527
|
self._warn_runtime_credential_expiring()
|
|
9522
9528
|
except Exception as e:
|
|
9523
|
-
logger.error("[%s] Failed to register with server: %s", self.label, e)
|
|
9529
|
+
logger.error("[%s] Failed to register with server: %s", self.label, _format_exc(e))
|
|
9524
9530
|
raise
|
|
9525
9531
|
|
|
9526
9532
|
def start_services(
|
|
@@ -11089,10 +11095,11 @@ class RuntimeDaemon:
|
|
|
11089
11095
|
# initial heartbeat already carries a real status snapshot).
|
|
11090
11096
|
await self.preflight_monitor.start()
|
|
11091
11097
|
|
|
11092
|
-
# 2. Register with all servers
|
|
11093
11098
|
# 2. Register with all servers (with retry on transient failures)
|
|
11094
|
-
|
|
11095
|
-
|
|
11099
|
+
attempt = 0
|
|
11100
|
+
while not self._shutdown:
|
|
11101
|
+
attempt += 1
|
|
11102
|
+
rejected_explicit_tokens = 0
|
|
11096
11103
|
for url in self.server_urls:
|
|
11097
11104
|
if any(c.server_url == url.rstrip("/") for c in self.connections):
|
|
11098
11105
|
continue # Already connected to this server
|
|
@@ -11136,24 +11143,34 @@ class RuntimeDaemon:
|
|
|
11136
11143
|
)
|
|
11137
11144
|
raise SystemExit(1)
|
|
11138
11145
|
except Exception as e:
|
|
11139
|
-
logger.error("[%s] Failed to connect: %s — skipping this server", conn.label, e)
|
|
11146
|
+
logger.error("[%s] Failed to connect: %s — skipping this server", conn.label, _format_exc(e))
|
|
11140
11147
|
await conn.client.aclose()
|
|
11148
|
+
if (
|
|
11149
|
+
self._has_explicit_api_token
|
|
11150
|
+
and isinstance(e, httpx.HTTPStatusError)
|
|
11151
|
+
and e.response.status_code in (401, 403)
|
|
11152
|
+
):
|
|
11153
|
+
rejected_explicit_tokens += 1
|
|
11141
11154
|
|
|
11142
11155
|
if self.connections:
|
|
11143
11156
|
break # At least one server connected
|
|
11157
|
+
if rejected_explicit_tokens == len(self.server_urls):
|
|
11158
|
+
logger.error("Explicit daemon API token rejected by all servers; update DAEMON_API_TOKEN or FORGEXA_TOKEN")
|
|
11159
|
+
raise SystemExit(1)
|
|
11144
11160
|
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
await asyncio.
|
|
11161
|
+
wait = min(attempt * 5, 60)
|
|
11162
|
+
logger.warning(
|
|
11163
|
+
"No servers reachable (attempt %d). Retrying in %ds...",
|
|
11164
|
+
attempt, wait,
|
|
11165
|
+
)
|
|
11166
|
+
try:
|
|
11167
|
+
await asyncio.wait_for(self._shutdown_event.wait(), timeout=wait)
|
|
11168
|
+
except asyncio.TimeoutError:
|
|
11169
|
+
pass
|
|
11152
11170
|
|
|
11153
11171
|
if not self.connections:
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
raise SystemExit(1)
|
|
11172
|
+
await self._shutdown_gracefully()
|
|
11173
|
+
return
|
|
11157
11174
|
|
|
11158
11175
|
logger.info("Daemon ready. Connected to %d server(s). Polling for tasks...",
|
|
11159
11176
|
len(self.connections))
|
|
@@ -13041,7 +13058,7 @@ class RuntimeDaemon:
|
|
|
13041
13058
|
# are *optional*: if they exist they are validated, but their
|
|
13042
13059
|
# absence is not an error (the agent only writes regression
|
|
13043
13060
|
# tests + test-report.md).
|
|
13044
|
-
_skip_test_artifacts =
|
|
13061
|
+
_skip_test_artifacts = req_type == "task"
|
|
13045
13062
|
_requires_structured_artifacts = False
|
|
13046
13063
|
# Scope-adaptive pipeline (Phase 1): a content_only change
|
|
13047
13064
|
# scope (analysis-predicted, machine-verified server-side)
|
|
@@ -13063,19 +13080,28 @@ class RuntimeDaemon:
|
|
|
13063
13080
|
# importable (standalone vendored daemon execution).
|
|
13064
13081
|
_skip_test_artifacts = True
|
|
13065
13082
|
|
|
13066
|
-
#
|
|
13067
|
-
#
|
|
13068
|
-
#
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13083
|
+
# Keep e2e evidence validation active for any requirement type.
|
|
13084
|
+
# An enforced policy requires artifacts only when the analysis
|
|
13085
|
+
# contract expects test intent or the analysis supplied one.
|
|
13086
|
+
_analysis_dir = (
|
|
13087
|
+
input_data.get("analysis_output_dir")
|
|
13088
|
+
or (input_data.get("context") or {}).get("analysis_output_dir")
|
|
13089
|
+
or ""
|
|
13090
|
+
)
|
|
13091
|
+
_has_test_intent = bool(_analysis_dir) and (
|
|
13092
|
+
workspace_path / str(_analysis_dir) / "test-intent.json"
|
|
13093
|
+
).is_file()
|
|
13074
13094
|
if (
|
|
13075
13095
|
_skip_test_artifacts
|
|
13076
13096
|
and _change_scope != "content_only"
|
|
13077
13097
|
and (
|
|
13078
|
-
(
|
|
13098
|
+
(
|
|
13099
|
+
(qa_policy or {}).get("enforcement") == "enforce"
|
|
13100
|
+
and (
|
|
13101
|
+
"test-intent.json" in _get_analysis_outputs_for_type(req_type, _change_scope)
|
|
13102
|
+
or _has_test_intent
|
|
13103
|
+
)
|
|
13104
|
+
)
|
|
13079
13105
|
or _testing_intents_have_e2e(workspace_path, input_data)
|
|
13080
13106
|
)
|
|
13081
13107
|
):
|
|
@@ -13250,15 +13276,30 @@ class RuntimeDaemon:
|
|
|
13250
13276
|
))
|
|
13251
13277
|
|
|
13252
13278
|
# --- coverage-matrix.json validation ---
|
|
13279
|
+
if req_type == "task":
|
|
13280
|
+
result.metrics.pop("acceptance_criteria_manual_review_required", None)
|
|
13253
13281
|
cm_path = base / "coverage-matrix.json"
|
|
13254
13282
|
if cm_path.exists():
|
|
13255
13283
|
try:
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13284
|
+
cm_content = cm_path.read_text(encoding="utf-8")
|
|
13285
|
+
cm_data = _json.loads(cm_content)
|
|
13286
|
+
if req_type == "task":
|
|
13287
|
+
coverage = runtime_evidence.parse_acceptance_criteria_coverage_artifacts(
|
|
13288
|
+
_json.dumps(_tc_data) if _tc_data is not None else None,
|
|
13289
|
+
cm_content,
|
|
13290
|
+
)
|
|
13291
|
+
links = coverage.get("coverage_links") or []
|
|
13292
|
+
if coverage.get("collection_status") != "completed" or not links:
|
|
13293
|
+
reason = coverage.get("failure_reason") or "coverage_matrix_empty"
|
|
13294
|
+
issues.append(f"coverage-matrix.json has invalid acceptance criteria: {reason}")
|
|
13295
|
+
else:
|
|
13296
|
+
result.metrics["acceptance_criteria_manual_review_required"] = True
|
|
13297
|
+
else:
|
|
13298
|
+
ac_list = cm_data.get("acceptance_criteria", [])
|
|
13299
|
+
uncovered = [ac for ac in ac_list if ac.get("status") != "covered"]
|
|
13300
|
+
if uncovered:
|
|
13301
|
+
ids = ", ".join(ac.get("id", "?") for ac in uncovered[:5])
|
|
13302
|
+
issues.append(f"Uncovered acceptance criteria in coverage-matrix.json: {ids}")
|
|
13262
13303
|
except (_json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
13263
13304
|
issues.append(f"coverage-matrix.json is not valid JSON: {e}")
|
|
13264
13305
|
elif _requires_structured_artifacts or _requires_e2e_artifacts:
|
|
@@ -14682,6 +14723,29 @@ class RuntimeDaemon:
|
|
|
14682
14723
|
)
|
|
14683
14724
|
return result # All validations passed
|
|
14684
14725
|
|
|
14726
|
+
if getattr(task, "workspace_resolution", None) == "local_bind":
|
|
14727
|
+
_append_local_bind_output_log(task.task_id, [
|
|
14728
|
+
"[daemon] Output validation found these issues:",
|
|
14729
|
+
*[f"[daemon] {issue}" for issue in issues],
|
|
14730
|
+
])
|
|
14731
|
+
|
|
14732
|
+
if (
|
|
14733
|
+
task.node_type == "testing"
|
|
14734
|
+
and (task.input_data or {}).get("requirement_type") == "task"
|
|
14735
|
+
and all(
|
|
14736
|
+
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
14737
|
+
for issue in issues
|
|
14738
|
+
)
|
|
14739
|
+
):
|
|
14740
|
+
result.status = "failed"
|
|
14741
|
+
result.failure_code = "acceptance_criteria_uncovered"
|
|
14742
|
+
result.error = f"Task verification is incomplete: {issues[0]}"
|
|
14743
|
+
result.metrics["validation_issues"] = issues
|
|
14744
|
+
self._record_testing_telemetry(
|
|
14745
|
+
workspace_path, task, result, retries_used=attempt
|
|
14746
|
+
)
|
|
14747
|
+
return result
|
|
14748
|
+
|
|
14685
14749
|
issues_text = "\n".join(f"- {iss}" for iss in issues)
|
|
14686
14750
|
logger.warning(
|
|
14687
14751
|
"Validation gate: %d issues found for task %s (attempt %d/%d):\n%s",
|
|
@@ -14775,6 +14839,27 @@ class RuntimeDaemon:
|
|
|
14775
14839
|
)
|
|
14776
14840
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
14777
14841
|
if remaining:
|
|
14842
|
+
if getattr(task, "workspace_resolution", None) == "local_bind":
|
|
14843
|
+
_append_local_bind_output_log(task.task_id, [
|
|
14844
|
+
"[daemon] Output validation remains incomplete:",
|
|
14845
|
+
*[f"[daemon] {issue}" for issue in remaining],
|
|
14846
|
+
])
|
|
14847
|
+
if (
|
|
14848
|
+
task.node_type == "testing"
|
|
14849
|
+
and (task.input_data or {}).get("requirement_type") == "task"
|
|
14850
|
+
and all(
|
|
14851
|
+
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
14852
|
+
for issue in remaining
|
|
14853
|
+
)
|
|
14854
|
+
):
|
|
14855
|
+
result.status = "failed"
|
|
14856
|
+
result.failure_code = "acceptance_criteria_uncovered"
|
|
14857
|
+
result.error = f"Task verification is incomplete: {remaining[0]}"
|
|
14858
|
+
result.metrics["validation_issues"] = remaining
|
|
14859
|
+
self._record_testing_telemetry(
|
|
14860
|
+
workspace_path, task, result, retries_used=max_retries
|
|
14861
|
+
)
|
|
14862
|
+
return result
|
|
14778
14863
|
# Distinguish blocking artifact-contract failures from advisory syntax
|
|
14779
14864
|
# warnings. Evidence coverage and references are release-blocking even
|
|
14780
14865
|
# when the file itself exists.
|
|
@@ -14788,6 +14873,7 @@ class RuntimeDaemon:
|
|
|
14788
14873
|
"not found",
|
|
14789
14874
|
"is empty",
|
|
14790
14875
|
"not valid json",
|
|
14876
|
+
"invalid acceptance criteria",
|
|
14791
14877
|
"case_evidence",
|
|
14792
14878
|
"test-evidence.json has no",
|
|
14793
14879
|
"evidence item",
|
|
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
|