forgexa-cli 1.57.5__tar.gz → 1.57.7__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.5 → forgexa_cli-1.57.7}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/daemon.py +222 -40
  4. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/pyproject.toml +1 -1
  6. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/README.md +0 -0
  7. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/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.5
3
+ Version: 1.57.7
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.5"
2
+ __version__ = "1.57.7"
@@ -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.5"
1153
+ DAEMON_VERSION = "1.57.7"
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(
@@ -5503,6 +5507,10 @@ class _TaskAttachmentDownloadError(RuntimeError):
5503
5507
  """Raised when a required task attachment cannot be staged safely."""
5504
5508
 
5505
5509
 
5510
+ class _TaskAttachmentUnavailableError(_TaskAttachmentDownloadError):
5511
+ """Raised when the attachment source is unavailable but the task can proceed."""
5512
+
5513
+
5506
5514
  def _workspace_has_recent_activity(
5507
5515
  workspace_path: "Path", since_monotonic: float, max_depth: int = 4
5508
5516
  ) -> bool:
@@ -5623,6 +5631,8 @@ class ProcessManager:
5623
5631
  "authentication required",
5624
5632
  "authenticate with the github",
5625
5633
  "gh auth login",
5634
+ "your github token may be invalid",
5635
+ "rejected the cli github credential",
5626
5636
  ]
5627
5637
 
5628
5638
  def __init__(self):
@@ -5726,7 +5736,9 @@ class ProcessManager:
5726
5736
  @staticmethod
5727
5737
  def agent_failure_kind(result: "TaskResult") -> str | None:
5728
5738
  """Classify a fallback-eligible failure without conflating its cause."""
5729
- if result.failure_code in {"all_agents_rate_limited", "all_agents_unavailable"}:
5739
+ if result.failure_code in {
5740
+ "all_agents_rate_limited", "all_agents_unavailable", "acceptance_criteria_uncovered",
5741
+ }:
5730
5742
  return None
5731
5743
  if result.failure_code == "agent_platform_unsupported":
5732
5744
  return "agent_unavailable"
@@ -7634,6 +7646,59 @@ class ProcessManager:
7634
7646
  return retry_result
7635
7647
  _auth_state["persist"] = False
7636
7648
 
7649
+ auth_detail = " ".join(
7650
+ [*structured_errors, stderr]
7651
+ ).lower()
7652
+ token_auth_error = (
7653
+ "your github token may be invalid" in auth_detail
7654
+ or "rejected the cli github credential" in auth_detail
7655
+ )
7656
+ if token_auth_error and not (copilot_exit == 0 and returncode == 0):
7657
+ _auth_state["persist"] = False
7658
+ if (
7659
+ token_auth_error
7660
+ and not (copilot_exit == 0 and returncode == 0)
7661
+ and not signals["has_meaningful_content"]
7662
+ and not self._copilot_called_any_tools(stdout)
7663
+ ):
7664
+ recovery_home = (
7665
+ Path(_master_copilot_home)
7666
+ if auth_env_var and _master_copilot_home
7667
+ else agent_core.copilot_recovery_home(_isolated_copilot_home)
7668
+ )
7669
+ if (
7670
+ not _retry_without_token_env
7671
+ and recovery_home
7672
+ and (recovery_home / "config.json").is_file()
7673
+ ):
7674
+ restored = await asyncio.to_thread(
7675
+ agent_core.replace_copilot_auth_state,
7676
+ recovery_home,
7677
+ _isolated_copilot_home,
7678
+ )
7679
+ can_retry_with_login = bool(auth_env_var or restored)
7680
+ else:
7681
+ can_retry_with_login = False
7682
+ if can_retry_with_login:
7683
+ logger.warning(
7684
+ "Copilot token authentication failed before work for task %s; "
7685
+ "retrying once with stored login and token environment variables removed",
7686
+ task_id,
7687
+ )
7688
+ retry_result = await self._run_copilot(
7689
+ agent, prompt, cwd, timeout, task_id, on_chunk,
7690
+ stream_stderr=stream_stderr,
7691
+ disable_builtin_mcps=disable_builtin_mcps,
7692
+ _retry_without_effort=_retry_without_effort,
7693
+ _retry_without_token_env=True,
7694
+ _isolated_copilot_home=_isolated_copilot_home,
7695
+ _auth_state=_auth_state,
7696
+ )
7697
+ _auth_state["persist"] = retry_result.status == "success"
7698
+ retry_result.metrics["copilot_auth_recovery"] = True
7699
+ return retry_result
7700
+ _auth_state["persist"] = False
7701
+
7637
7702
  if copilot_exit == 0 and returncode == 0:
7638
7703
  return TaskResult(
7639
7704
  status="success",
@@ -7731,6 +7796,12 @@ class ProcessManager:
7731
7796
  failure_error = f"Copilot exited with code {effective_rc}: {stderr[-500:].strip()}"
7732
7797
  else:
7733
7798
  failure_error = f"Copilot exited with code {effective_rc}"
7799
+ if token_auth_error:
7800
+ auth_source = auth_env_var or "stored Copilot login"
7801
+ failure_error = (
7802
+ f"Copilot authentication failed using {auth_source}. "
7803
+ f"{failure_error}"
7804
+ )
7734
7805
  logger.error(
7735
7806
  "Copilot task %s failed (process_rc=%s, result_exit=%s): %s",
7736
7807
  task_id,
@@ -10568,9 +10639,22 @@ class RuntimeDaemon:
10568
10639
  timeout=httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0),
10569
10640
  ) as response:
10570
10641
  if not 200 <= response.status_code < 300:
10571
- raise _TaskAttachmentDownloadError(
10572
- f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
10573
- )
10642
+ error = f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
10643
+ if response.status_code in {409, 502}:
10644
+ await response.aread()
10645
+ try:
10646
+ detail = response.json()
10647
+ except ValueError:
10648
+ detail = None
10649
+ if isinstance(detail, dict) and detail.get("detail") == "Task attachment integrity check failed":
10650
+ raise _TaskAttachmentDownloadError(error)
10651
+ if response.status_code == 409 and isinstance(detail, dict) and detail.get("detail") == "Task attachment source no longer matches stored checksum":
10652
+ raise _TaskAttachmentUnavailableError(
10653
+ f"Attachment '{attachment_name}' is unavailable: {detail['detail']}"
10654
+ )
10655
+ if response.status_code in {404, 502, 503, 504}:
10656
+ raise _TaskAttachmentUnavailableError(error)
10657
+ raise _TaskAttachmentDownloadError(error)
10574
10658
  content_length = response.headers.get("content-length")
10575
10659
  if content_length:
10576
10660
  try:
@@ -10604,6 +10688,10 @@ class RuntimeDaemon:
10604
10688
  temporary_path.replace(destination)
10605
10689
  except _TaskAttachmentDownloadError:
10606
10690
  raise
10691
+ except httpx.RequestError as exc:
10692
+ raise _TaskAttachmentUnavailableError(
10693
+ f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
10694
+ ) from exc
10607
10695
  except httpx.HTTPError as exc:
10608
10696
  raise _TaskAttachmentDownloadError(
10609
10697
  f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
@@ -10619,7 +10707,7 @@ class RuntimeDaemon:
10619
10707
  timeout=timeout_seconds,
10620
10708
  )
10621
10709
  except asyncio.TimeoutError as exc:
10622
- raise _TaskAttachmentDownloadError(
10710
+ raise _TaskAttachmentUnavailableError(
10623
10711
  f"Attachment '{attachment_name}' download timed out after "
10624
10712
  f"{timeout_seconds:.0f}s"
10625
10713
  ) from exc
@@ -10642,6 +10730,7 @@ class RuntimeDaemon:
10642
10730
 
10643
10731
  staging_dir = Path(tempfile.mkdtemp(prefix=f"forgexa-attachments-{task.task_id[:8]}-"))
10644
10732
  staged: list[tuple[str, list[str], str]] = []
10733
+ unavailable: list[tuple[str, list[str], str]] = []
10645
10734
  deadline = time.monotonic() + self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS
10646
10735
  remaining_bytes = self._TASK_ATTACHMENT_MAX_TOTAL_BYTES
10647
10736
  try:
@@ -10670,12 +10759,6 @@ class RuntimeDaemon:
10670
10759
  raise _TaskAttachmentDownloadError(
10671
10760
  f"Attachment '{file_name}' cannot be staged securely because it has no artifact ID"
10672
10761
  )
10673
- remaining_seconds = deadline - time.monotonic()
10674
- if remaining_seconds <= 0:
10675
- raise _TaskAttachmentDownloadError(
10676
- f"Task attachment staging timed out after "
10677
- f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
10678
- )
10679
10762
  if not conn.runtime_id:
10680
10763
  raise _TaskAttachmentDownloadError(
10681
10764
  f"Attachment '{file_name}' cannot be staged because this runtime is not registered"
@@ -10684,16 +10767,27 @@ class RuntimeDaemon:
10684
10767
  f"{conn.server_url.rstrip('/')}/api/v1/runtimes/{conn.runtime_id}/"
10685
10768
  f"tasks/{task.task_id}/attachments/{artifact_id}"
10686
10769
  )
10687
- await self._download_task_attachment(
10688
- conn.client,
10689
- source_url,
10690
- destination,
10691
- attachment_name=file_name,
10692
- expected_size=expected_size or None,
10693
- expected_checksum=str(attachment.get("checksum") or ""),
10694
- max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
10695
- timeout_seconds=remaining_seconds,
10696
- )
10770
+ try:
10771
+ remaining_seconds = deadline - time.monotonic()
10772
+ if remaining_seconds <= 0:
10773
+ raise _TaskAttachmentUnavailableError(
10774
+ f"Task attachment staging timed out after "
10775
+ f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
10776
+ )
10777
+ await self._download_task_attachment(
10778
+ conn.client,
10779
+ source_url,
10780
+ destination,
10781
+ attachment_name=file_name,
10782
+ expected_size=expected_size or None,
10783
+ expected_checksum=str(attachment.get("checksum") or ""),
10784
+ max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
10785
+ timeout_seconds=remaining_seconds,
10786
+ )
10787
+ except _TaskAttachmentUnavailableError as exc:
10788
+ logger.warning("Task %s attachment unavailable: %s", task.task_id, exc)
10789
+ unavailable.append((file_name, download_url_aliases, str(exc)))
10790
+ continue
10697
10791
  remaining_bytes -= destination.stat().st_size
10698
10792
  staged.append((file_name, download_url_aliases, str(destination)))
10699
10793
  except Exception:
@@ -10701,10 +10795,29 @@ class RuntimeDaemon:
10701
10795
  raise
10702
10796
 
10703
10797
  task.input_prompt = self._rewrite_staged_attachment_references(task.input_prompt, staged)
10798
+ if unavailable:
10799
+ notices = []
10800
+ for file_name, download_urls, error in unavailable:
10801
+ notices.append(f"- `{file_name}`: {error}")
10802
+ for download_url in download_urls:
10803
+ command_pattern = r"`curl -L '" + re.escape(download_url) + r"' -o /tmp/[^`]+`"
10804
+ task.input_prompt = re.sub(command_pattern, "[attachment unavailable]", task.input_prompt)
10805
+ task.input_prompt = task.input_prompt.replace(f"Download: {download_url}", "Attachment unavailable")
10806
+ task.input_prompt = task.input_prompt.replace(download_url, "[attachment unavailable]")
10807
+ task.input_prompt = "\n".join([
10808
+ "## Unavailable Attachments",
10809
+ "These files could not be retrieved. Continue with available inputs and report any missing evidence; do not retry their download URLs.",
10810
+ *notices,
10811
+ "",
10812
+ task.input_prompt,
10813
+ ])
10704
10814
  input_data = dict(task.input_data or {})
10705
10815
  input_data["prompt"] = task.input_prompt
10706
10816
  task.input_data = input_data
10707
10817
  logger.info("Prepared %d attachment(s) for task %s", len(staged), task.task_id)
10818
+ if not staged:
10819
+ shutil.rmtree(staging_dir, ignore_errors=True)
10820
+ return None
10708
10821
  return staging_dir
10709
10822
 
10710
10823
  @staticmethod
@@ -13052,7 +13165,7 @@ class RuntimeDaemon:
13052
13165
  # are *optional*: if they exist they are validated, but their
13053
13166
  # absence is not an error (the agent only writes regression
13054
13167
  # tests + test-report.md).
13055
- _skip_test_artifacts = False
13168
+ _skip_test_artifacts = req_type == "task"
13056
13169
  _requires_structured_artifacts = False
13057
13170
  # Scope-adaptive pipeline (Phase 1): a content_only change
13058
13171
  # scope (analysis-predicted, machine-verified server-side)
@@ -13074,19 +13187,28 @@ class RuntimeDaemon:
13074
13187
  # importable (standalone vendored daemon execution).
13075
13188
  _skip_test_artifacts = True
13076
13189
 
13077
- # The type-profile test_coverage skip (e.g. "task") is a
13078
- # lightweight-verify default — NOT an exemption from the e2e
13079
- # grounding chain or from a frozen enforce-mode artifact
13080
- # contract that was injected into the prompt. Both apply to
13081
- # every requirement type: SI-1616 (task verify) failed the e2e
13082
- # evidence chain because the skip semantics diverged between
13083
- # server daemons (profile skip applied) and standalone desktop
13084
- # daemons (profile import fails → nothing skipped).
13190
+ # Keep e2e evidence validation active for any requirement type.
13191
+ # An enforced policy requires artifacts only when the analysis
13192
+ # contract expects test intent or the analysis supplied one.
13193
+ _analysis_dir = (
13194
+ input_data.get("analysis_output_dir")
13195
+ or (input_data.get("context") or {}).get("analysis_output_dir")
13196
+ or ""
13197
+ )
13198
+ _has_test_intent = bool(_analysis_dir) and (
13199
+ workspace_path / str(_analysis_dir) / "test-intent.json"
13200
+ ).is_file()
13085
13201
  if (
13086
13202
  _skip_test_artifacts
13087
13203
  and _change_scope != "content_only"
13088
13204
  and (
13089
- (qa_policy or {}).get("enforcement") == "enforce"
13205
+ (
13206
+ (qa_policy or {}).get("enforcement") == "enforce"
13207
+ and (
13208
+ "test-intent.json" in _get_analysis_outputs_for_type(req_type, _change_scope)
13209
+ or _has_test_intent
13210
+ )
13211
+ )
13090
13212
  or _testing_intents_have_e2e(workspace_path, input_data)
13091
13213
  )
13092
13214
  ):
@@ -13261,15 +13383,30 @@ class RuntimeDaemon:
13261
13383
  ))
13262
13384
 
13263
13385
  # --- coverage-matrix.json validation ---
13386
+ if req_type == "task":
13387
+ result.metrics.pop("acceptance_criteria_manual_review_required", None)
13264
13388
  cm_path = base / "coverage-matrix.json"
13265
13389
  if cm_path.exists():
13266
13390
  try:
13267
- cm_data = _json.loads(cm_path.read_text(encoding="utf-8"))
13268
- ac_list = cm_data.get("acceptance_criteria", [])
13269
- uncovered = [ac for ac in ac_list if ac.get("status") != "covered"]
13270
- if uncovered:
13271
- ids = ", ".join(ac.get("id", "?") for ac in uncovered[:5])
13272
- issues.append(f"Uncovered acceptance criteria in coverage-matrix.json: {ids}")
13391
+ cm_content = cm_path.read_text(encoding="utf-8")
13392
+ cm_data = _json.loads(cm_content)
13393
+ if req_type == "task":
13394
+ coverage = runtime_evidence.parse_acceptance_criteria_coverage_artifacts(
13395
+ _json.dumps(_tc_data) if _tc_data is not None else None,
13396
+ cm_content,
13397
+ )
13398
+ links = coverage.get("coverage_links") or []
13399
+ if coverage.get("collection_status") != "completed" or not links:
13400
+ reason = coverage.get("failure_reason") or "coverage_matrix_empty"
13401
+ issues.append(f"coverage-matrix.json has invalid acceptance criteria: {reason}")
13402
+ else:
13403
+ result.metrics["acceptance_criteria_manual_review_required"] = True
13404
+ else:
13405
+ ac_list = cm_data.get("acceptance_criteria", [])
13406
+ uncovered = [ac for ac in ac_list if ac.get("status") != "covered"]
13407
+ if uncovered:
13408
+ ids = ", ".join(ac.get("id", "?") for ac in uncovered[:5])
13409
+ issues.append(f"Uncovered acceptance criteria in coverage-matrix.json: {ids}")
13273
13410
  except (_json.JSONDecodeError, UnicodeDecodeError) as e:
13274
13411
  issues.append(f"coverage-matrix.json is not valid JSON: {e}")
13275
13412
  elif _requires_structured_artifacts or _requires_e2e_artifacts:
@@ -14693,6 +14830,29 @@ class RuntimeDaemon:
14693
14830
  )
14694
14831
  return result # All validations passed
14695
14832
 
14833
+ if getattr(task, "workspace_resolution", None) == "local_bind":
14834
+ _append_local_bind_output_log(task.task_id, [
14835
+ "[daemon] Output validation found these issues:",
14836
+ *[f"[daemon] {issue}" for issue in issues],
14837
+ ])
14838
+
14839
+ if (
14840
+ task.node_type == "testing"
14841
+ and (task.input_data or {}).get("requirement_type") == "task"
14842
+ and all(
14843
+ issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
14844
+ for issue in issues
14845
+ )
14846
+ ):
14847
+ result.status = "failed"
14848
+ result.failure_code = "acceptance_criteria_uncovered"
14849
+ result.error = f"Task verification is incomplete: {issues[0]}"
14850
+ result.metrics["validation_issues"] = issues
14851
+ self._record_testing_telemetry(
14852
+ workspace_path, task, result, retries_used=attempt
14853
+ )
14854
+ return result
14855
+
14696
14856
  issues_text = "\n".join(f"- {iss}" for iss in issues)
14697
14857
  logger.warning(
14698
14858
  "Validation gate: %d issues found for task %s (attempt %d/%d):\n%s",
@@ -14786,6 +14946,27 @@ class RuntimeDaemon:
14786
14946
  )
14787
14947
  remaining = self._validate_outputs(workspace_path, task, result)
14788
14948
  if remaining:
14949
+ if getattr(task, "workspace_resolution", None) == "local_bind":
14950
+ _append_local_bind_output_log(task.task_id, [
14951
+ "[daemon] Output validation remains incomplete:",
14952
+ *[f"[daemon] {issue}" for issue in remaining],
14953
+ ])
14954
+ if (
14955
+ task.node_type == "testing"
14956
+ and (task.input_data or {}).get("requirement_type") == "task"
14957
+ and all(
14958
+ issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
14959
+ for issue in remaining
14960
+ )
14961
+ ):
14962
+ result.status = "failed"
14963
+ result.failure_code = "acceptance_criteria_uncovered"
14964
+ result.error = f"Task verification is incomplete: {remaining[0]}"
14965
+ result.metrics["validation_issues"] = remaining
14966
+ self._record_testing_telemetry(
14967
+ workspace_path, task, result, retries_used=max_retries
14968
+ )
14969
+ return result
14789
14970
  # Distinguish blocking artifact-contract failures from advisory syntax
14790
14971
  # warnings. Evidence coverage and references are release-blocking even
14791
14972
  # when the file itself exists.
@@ -14799,6 +14980,7 @@ class RuntimeDaemon:
14799
14980
  "not found",
14800
14981
  "is empty",
14801
14982
  "not valid json",
14983
+ "invalid acceptance criteria",
14802
14984
  "case_evidence",
14803
14985
  "test-evidence.json has no",
14804
14986
  "evidence item",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.5
3
+ Version: 1.57.7
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.5"
3
+ version = "1.57.7"
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