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.
Files changed (29) hide show
  1. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/daemon.py +121 -35
  4. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/pyproject.toml +1 -1
  6. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/README.md +0 -0
  7. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.4 → forgexa_cli-1.57.6}/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.57.4
3
+ Version: 1.57.6
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.57.4"
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.4"
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
- "Local task failed. Review this task in Forgexa Desktop on the "
1759
- "running device."
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 {"all_agents_rate_limited", "all_agents_unavailable"}:
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
- max_registration_attempts = 5
11095
- for attempt in range(max_registration_attempts):
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
- if attempt < max_registration_attempts - 1:
11146
- wait = (attempt + 1) * 5 # 5s, 10s, 15s, 20s
11147
- logger.warning(
11148
- "No servers reachable (attempt %d/%d). Retrying in %ds...",
11149
- attempt + 1, max_registration_attempts, wait,
11150
- )
11151
- await asyncio.sleep(wait)
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
- logger.error("Failed to connect to any server after %d attempts. Exiting.",
11155
- max_registration_attempts)
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 = False
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
- # The type-profile test_coverage skip (e.g. "task") is a
13067
- # lightweight-verify default — NOT an exemption from the e2e
13068
- # grounding chain or from a frozen enforce-mode artifact
13069
- # contract that was injected into the prompt. Both apply to
13070
- # every requirement type: SI-1616 (task verify) failed the e2e
13071
- # evidence chain because the skip semantics diverged between
13072
- # server daemons (profile skip applied) and standalone desktop
13073
- # daemons (profile import fails → nothing skipped).
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
- (qa_policy or {}).get("enforcement") == "enforce"
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
- cm_data = _json.loads(cm_path.read_text(encoding="utf-8"))
13257
- ac_list = cm_data.get("acceptance_criteria", [])
13258
- uncovered = [ac for ac in ac_list if ac.get("status") != "covered"]
13259
- if uncovered:
13260
- ids = ", ".join(ac.get("id", "?") for ac in uncovered[:5])
13261
- issues.append(f"Uncovered acceptance criteria in coverage-matrix.json: {ids}")
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.4
3
+ Version: 1.57.6
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.57.4"
3
+ version = "1.57.6"
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