forgexa-cli 1.87.0__tar.gz → 1.87.1__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 (34) hide show
  1. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/agent_core.py +67 -11
  4. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/daemon.py +64 -22
  5. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/README.md +0 -0
  8. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/setup.cfg +0 -0
  25. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.87.0 → forgexa_cli-1.87.1}/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.87.0
3
+ Version: 1.87.1
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.87.0"
2
+ __version__ = "1.87.1"
@@ -784,11 +784,44 @@ def lock_provenance_projection(path: str, source: str) -> str | None:
784
784
 
785
785
  EXECUTION_DELIVERY_WARNING_CODES = frozenset({
786
786
  "partial_delivery", "dependency_authorization", "dependency_network", "dependency_unavailable",
787
- "environment_unavailable", "verification_pending", "source_evidence_limited",
787
+ "environment_unavailable", "verification_pending", "source_evidence_limited", "qa_handoff_ready",
788
788
  })
789
789
 
790
790
 
791
- def implementation_delivery_contract(node_type: str, task_input: dict, *, supported: bool, graph_type: str = "execution", protocol_version: int = 1) -> dict | None:
791
+ def is_frozen_analysis_path(value: str) -> bool:
792
+ path = PurePosixPath(value.replace("\\", "/"))
793
+ if path.is_absolute() or ".." in path.parts:
794
+ return False
795
+ parts = path.parts
796
+ if len(parts) < 4 or parts[0] != "docs":
797
+ return False
798
+ if parts[1] == "requirements":
799
+ return len(parts) == 4 or parts[3] == "analysis"
800
+ return parts[1] == "workitems" and parts[3] == "analysis"
801
+
802
+
803
+ def uses_necessary_execution_checks(node_type: str, task_input: dict, graph_type: str | None = None) -> bool:
804
+ actual_graph_type = graph_type if graph_type is not None else task_input.get("graph_type") or "execution"
805
+ return (
806
+ isinstance(node_type, str) and node_type in {"design", "coding", "testing", "review", "fix"}
807
+ and isinstance(actual_graph_type, str) and actual_graph_type in {"execution", "implementation", "fix"}
808
+ and not task_input.get("verification_work_item")
809
+ and task_input.get("execution_actor") != "human"
810
+ )
811
+
812
+
813
+ def implementation_validation_issue_blocks(node_type: str, task_input: dict, code: str) -> bool:
814
+ if not uses_necessary_execution_checks(node_type, task_input):
815
+ return True
816
+ return code in {
817
+ "approved_plan_baseline_modified", "verification_work_item_scope_violation", "remediation_scope_violation",
818
+ "qa_policy.source_test_level_disabled", "qa_policy.configuration_invalid", "qa_policy.validation_unavailable",
819
+ "qa_policy.source_change_inventory_unavailable",
820
+ "uncommitted_workspace_changes", "forbidden_branch_merge",
821
+ }
822
+
823
+
824
+ def implementation_delivery_contract(node_type: str, task_input: dict, *, supported: bool, graph_type: str = "execution", protocol_version: int = 1, qa_handoff_supported: bool = False) -> dict | None:
792
825
  if type(protocol_version) is not int or protocol_version not in (1, 2):
793
826
  return None
794
827
  if supported is not True or not isinstance(node_type, str) or not isinstance(graph_type, str) or node_type not in {"design", "coding", "testing", "review", "fix"} or graph_type not in {"execution", "implementation", "fix"}:
@@ -807,6 +840,8 @@ def implementation_delivery_contract(node_type: str, task_input: dict, *, suppor
807
840
  contract = {"version": protocol_version, "mode": "execution_and_artifacts", "node_type": node_type, "graph_type": graph_type,
808
841
  "task_id": task_id, "baseline_fingerprint": baseline.get("fingerprint"),
809
842
  "qa_policy": task_input.get("qa_test_level_policy_snapshot"), "change_scope": task_input.get("change_scope")}
843
+ if protocol_version == 2 and qa_handoff_supported is True and node_type in {"testing", "review"}:
844
+ contract["qa_handoff"] = True
810
845
  contract["fingerprint"] = hashlib.sha256(json.dumps(contract, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
811
846
  return contract
812
847
 
@@ -815,8 +850,11 @@ def uses_controlled_delivery(task_input: dict) -> bool:
815
850
  contract = task_input.get("implementation_delivery")
816
851
  if not isinstance(contract, dict):
817
852
  return False
853
+ if not uses_necessary_execution_checks(contract.get("node_type"), task_input):
854
+ return False
818
855
  expected = implementation_delivery_contract(contract.get("node_type"), task_input, supported=True,
819
- graph_type=contract.get("graph_type"), protocol_version=contract.get("version"))
856
+ graph_type=contract.get("graph_type"), protocol_version=contract.get("version"),
857
+ qa_handoff_supported=contract.get("qa_handoff") is True)
820
858
  return expected is not None and contract == expected
821
859
 
822
860
 
@@ -825,26 +863,38 @@ def assess_execution_delivery(contract: dict, report: object) -> dict:
825
863
  return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration is missing or invalid"]}
826
864
  if report["version"] != contract.get("version") or report.get("contract_fingerprint") != contract.get("fingerprint") or redact_execution_delivery(report) is None:
827
865
  return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration does not match the current contract"]}
828
- return {"report_valid": True, "status": report["status"], "issues": []}
866
+ assessment = {"report_valid": True, "status": report["status"], "issues": []}
867
+ if "handoff" in report:
868
+ if contract.get("qa_handoff") is not True or contract.get("node_type") not in {"testing", "review"}:
869
+ return {"report_valid": False, "status": "unknown", "issues": ["QA handoff is not enabled for this execution contract"]}
870
+ assessment["handoff_ready"] = True
871
+ return assessment
829
872
 
830
873
 
831
874
  def execution_delivery_allows_progress(contract: dict, assessment: dict) -> bool:
832
875
  return assessment.get("report_valid") is True and (
833
- assessment.get("status") == "complete" or contract.get("version") == 2 and assessment.get("status") == "partial"
876
+ assessment.get("status") == "complete" or contract.get("version") == 2 and (
877
+ assessment.get("status") == "partial" or contract.get("qa_handoff") is True and assessment.get("handoff_ready") is True
878
+ )
834
879
  )
835
880
 
836
881
 
837
882
  def execution_delivery_warnings(contract: dict, report: object) -> list[str]:
838
883
  if not isinstance(report, dict) or contract.get("version") != 2 or not assess_execution_delivery(contract, report)["report_valid"]:
839
884
  return []
840
- return list(report.get("warnings") or (["partial_delivery"] if report["status"] == "partial" else []))
885
+ warnings = list(report.get("warnings") or (["partial_delivery"] if report["status"] == "partial" else []))
886
+ if "handoff" in report and "verification_pending" not in warnings:
887
+ warnings.append("verification_pending")
888
+ if "handoff" in report and "qa_handoff_ready" not in warnings:
889
+ warnings.append("qa_handoff_ready")
890
+ return warnings
841
891
 
842
892
 
843
893
  def redact_execution_delivery(report: object) -> dict | None:
844
894
  if not isinstance(report, dict) or type(report.get("version")) is not int or report["version"] not in (1, 2):
845
895
  return None
846
896
  required = {"version", "contract_fingerprint", "status"}
847
- allowed = required | ({"warnings"} if report["version"] == 2 else set())
897
+ allowed = required | ({"warnings", "handoff"} if report["version"] == 2 else set())
848
898
  if not required <= set(report) <= allowed or not isinstance(report.get("contract_fingerprint"), str) or not re.fullmatch(r"[0-9a-f]{64}", report["contract_fingerprint"]):
849
899
  return None
850
900
  if not isinstance(report.get("status"), str) or report["status"] not in {"complete", "partial", "blocked"}:
@@ -854,13 +904,19 @@ def redact_execution_delivery(report: object) -> dict | None:
854
904
  not isinstance(code, str) or code not in EXECUTION_DELIVERY_WARNING_CODES for code in warnings
855
905
  ) or len(set(warnings)) != len(warnings):
856
906
  return None
907
+ if "qa_handoff_ready" in warnings and "handoff" not in report:
908
+ return None
857
909
  redacted = {key: report[key] for key in ("version", "contract_fingerprint", "status")}
858
910
  if "warnings" in report:
859
911
  redacted["warnings"] = list(warnings)
912
+ if "handoff" in report:
913
+ if not isinstance(report["handoff"], dict) or report["handoff"] != {"target": "human_qa", "status": "ready"}:
914
+ return None
915
+ redacted["handoff"] = dict(report["handoff"])
860
916
  return redacted
861
917
 
862
918
 
863
- def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None, policy_supported: bool = True) -> dict:
919
+ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None, policy_supported: bool = True, graph_type: str | None = None) -> dict:
864
920
  baseline = task_input.get("approved_plan_baseline")
865
921
  reasons = []
866
922
  if policy_supported is not True:
@@ -896,8 +952,8 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
896
952
  if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
897
953
  reasons.append("scope_unknown")
898
954
  decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
899
- if uses_controlled_delivery(task_input):
900
- decision.update(mode="advisory", decision_scope="controlled_delivery", delivery_contract=task_input["implementation_delivery"]["fingerprint"])
955
+ if uses_necessary_execution_checks(node_type, task_input, graph_type):
956
+ decision.update(mode="advisory", decision_scope="necessary_execution")
901
957
  decision["fingerprint"] = hashlib.sha256(json.dumps({
902
958
  **decision, "baseline_fingerprint": fingerprint,
903
959
  "qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
@@ -907,7 +963,7 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
907
963
 
908
964
 
909
965
  def review_blocks_completion(policy: dict, assessment: dict) -> bool:
910
- if policy.get("decision_scope") == "controlled_delivery":
966
+ if policy.get("decision_scope") in {"controlled_delivery", "necessary_execution"}:
911
967
  return False
912
968
  return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
913
969
  policy.get("mode") != "advisory" or assessment.get("status") == "deviation"
@@ -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.87.0"
1237
+ DAEMON_VERSION = "1.87.1"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -1927,6 +1927,22 @@ def _output_validation_issue(issue: str) -> _OutputValidationIssue:
1927
1927
  return _OutputValidationIssue("output_validation_failed", issue)
1928
1928
 
1929
1929
 
1930
+ def _implementation_validation_issues(task: "TaskInfo", result: "TaskResult", issues: list[str]) -> list[_OutputValidationIssue]:
1931
+ normalized = [_output_validation_issue(issue) for issue in issues]
1932
+ if not agent_core.uses_necessary_execution_checks(task.node_type, task.input_data or {}):
1933
+ return normalized
1934
+ normalized = [issue if agent_core.implementation_validation_issue_blocks(task.node_type, task.input_data or {}, issue.code)
1935
+ else _OutputValidationIssue(issue.code, str(issue), "warning") for issue in normalized]
1936
+ if any(issue.severity == "warning" for issue in normalized):
1937
+ warnings = list(result.metrics.get("completion_warnings") or [])
1938
+ if "partial_delivery" not in warnings:
1939
+ warnings.append("partial_delivery")
1940
+ result.metrics["completion_warnings"] = warnings
1941
+ if result.status in {"success", "partial"} and all(issue.severity == "warning" for issue in normalized):
1942
+ result.metrics.pop("failure_domain", None)
1943
+ return normalized
1944
+
1945
+
1930
1946
  def _record_output_validation_issues(result: "TaskResult", issues: list[str]) -> None:
1931
1947
  result.metrics["validation_issues"] = [str(issue) for issue in issues]
1932
1948
  result.metrics["validation_issue_details"] = [
@@ -2601,14 +2617,8 @@ def _get_analysis_outputs_for_type(req_type: str, change_scope: str = "standard"
2601
2617
 
2602
2618
 
2603
2619
  def _is_requirement_analysis_path(path: str) -> bool:
2604
- """Recognize v1 and v2 requirement analysis paths without backend imports."""
2605
- normalized = str(path or "").replace("\\", "/").lstrip("./")
2606
- return (
2607
- normalized.startswith("docs/requirements/")
2608
- and "/implement/" not in normalized
2609
- and "/bugfix/" not in normalized
2610
- and "/delivery/" not in normalized
2611
- )
2620
+ """Recognize frozen analysis assets in both supported layouts."""
2621
+ return agent_core.is_frozen_analysis_path(str(path or ""))
2612
2622
 
2613
2623
 
2614
2624
  # Mirrors type_workflow_profiles.py — maps req type to the expected design-phase output filename.
@@ -6489,7 +6499,7 @@ class ProcessManager:
6489
6499
  stderr = result.stderr or ""
6490
6500
  error = result.error or ""
6491
6501
 
6492
- if result.status != "success":
6502
+ if result.status not in {"success", "partial"}:
6493
6503
  if result.exit_code == 0:
6494
6504
  return "\n".join(part for part in (stderr, error) if part)
6495
6505
  return "\n".join(part for part in (stderr, error, stdout[-3000:]) if part)
@@ -10223,7 +10233,8 @@ class ServerConnection:
10223
10233
  "task_delivery": {"protocol_version": 1},
10224
10234
  "verification_recovery": {"protocol_version": 1},
10225
10235
  "task_review_policy": {"protocol_version": 1},
10226
- "implementation_delivery": {"protocol_version": 2},
10236
+ "implementation_delivery": {"protocol_version": 2, "qa_handoff_version": 1},
10237
+ "execution_validation": {"protocol_version": 1, "mode": "necessary"},
10227
10238
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10228
10239
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10229
10240
  },
@@ -10652,7 +10663,8 @@ class ServerConnection:
10652
10663
  "task_delivery": {"protocol_version": 1},
10653
10664
  "verification_recovery": {"protocol_version": 1},
10654
10665
  "task_review_policy": {"protocol_version": 1},
10655
- "implementation_delivery": {"protocol_version": 2},
10666
+ "implementation_delivery": {"protocol_version": 2, "qa_handoff_version": 1},
10667
+ "execution_validation": {"protocol_version": 1, "mode": "necessary"},
10656
10668
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10657
10669
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10658
10670
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
@@ -13814,7 +13826,7 @@ class RuntimeDaemon:
13814
13826
  logger.exception("Final QA source-test validation error for task %s", task.task_id)
13815
13827
  result.metrics["validation_retries_used"] = previous_validation_retries
13816
13828
 
13817
- if result.failure_code == "validation_retry_exhausted" and not is_local_bind and any(
13829
+ if result.failure_code in {"validation_retry_exhausted", "qa_policy.source_test_level_disabled", "qa_policy.source_change_inventory_unavailable", "qa_policy.validation_unavailable"} and not is_local_bind and any(
13818
13830
  str(issue).startswith((
13819
13831
  "qa_policy.source_test_level_disabled",
13820
13832
  "qa_policy.source_change_inventory_unavailable",
@@ -13966,7 +13978,7 @@ class RuntimeDaemon:
13966
13978
 
13967
13979
  review_policy = agent_core.task_review_policy(task.node_type, task.input_data or {}, changed_files=result.files_changed)
13968
13980
  result.metrics["task_review_policy"] = review_policy
13969
- if result.status == "success" and (task.critic_review_required or review_policy["mode"] == "required" and isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13981
+ if result.status == "success" and not agent_core.uses_necessary_execution_checks(task.node_type, task.input_data or {}) and (task.critic_review_required or review_policy["mode"] == "required" and isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13970
13982
  await self._collect_task_delivery_git(
13971
13983
  task, workspace_path, result, attempt_before_sha=node_before_sha,
13972
13984
  )
@@ -14637,6 +14649,8 @@ class RuntimeDaemon:
14637
14649
  issues: list[str] = []
14638
14650
  node_type = task.node_type
14639
14651
  input_data = task.input_data or {}
14652
+ necessary_checks = agent_core.uses_necessary_execution_checks(node_type, input_data)
14653
+ original_execution = (result.status, result.failure_code, result.error)
14640
14654
  controlled_delivery = agent_core.uses_controlled_delivery(input_data)
14641
14655
  if controlled_delivery:
14642
14656
  report = agent_core.extract_execution_delivery(_daemon_extract_agent_output(str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or ""), result.stdout))
@@ -14644,9 +14658,9 @@ class RuntimeDaemon:
14644
14658
  result.metrics["execution_delivery"] = agent_core.redact_execution_delivery(report)
14645
14659
  result.metrics["execution_delivery_valid"] = delivery["report_valid"]
14646
14660
  result.metrics.pop("completion_warnings", None)
14647
- if not delivery["report_valid"]:
14661
+ if not delivery["report_valid"] and (report is not None or not necessary_checks):
14648
14662
  issues.extend(_OutputValidationIssue("validation_unavailable", message) for message in delivery["issues"])
14649
- elif not agent_core.execution_delivery_allows_progress(input_data["implementation_delivery"], delivery):
14663
+ elif delivery["report_valid"] and not agent_core.execution_delivery_allows_progress(input_data["implementation_delivery"], delivery):
14650
14664
  issues.append(_OutputValidationIssue("planned_task_incomplete", "Agent explicitly reported incomplete execution delivery"))
14651
14665
  else:
14652
14666
  warnings = agent_core.execution_delivery_warnings(input_data["implementation_delivery"], report)
@@ -14687,7 +14701,7 @@ class RuntimeDaemon:
14687
14701
  if failure_domain is not None and not controlled_delivery:
14688
14702
  result.metrics["failure_domain"] = failure_domain
14689
14703
  result.metrics["implementation_audit_node_complete"] = bool(
14690
- controlled_delivery or audit_result["report_valid"] and audit_result["node_complete"]
14704
+ audit_result["report_valid"] and audit_result["node_complete"]
14691
14705
  )
14692
14706
  result.metrics["implementation_audit_subject_blocked"] = bool(
14693
14707
  not controlled_delivery and audit_result["report_valid"] and not audit_result["node_complete"]
@@ -14703,7 +14717,7 @@ class RuntimeDaemon:
14703
14717
  f"{request['task_id']}: {request['reason']}" for request in requests[:3]
14704
14718
  )
14705
14719
  issues.append(_OutputValidationIssue(result.failure_code, result.error))
14706
- elif not controlled_delivery and (not audit_result["report_valid"] or not audit_result["node_complete"]):
14720
+ elif not controlled_delivery and (audit_report is not None or not necessary_checks) and (not audit_result["report_valid"] or not audit_result["node_complete"]):
14707
14721
  issues.extend(_OutputValidationIssue("implementation_audit_incomplete", issue) for issue in (
14708
14722
  audit_result["issues"] or audit_result.get("blocking_reasons") or [
14709
14723
  "The selected approved task still has implementation gaps",
@@ -15244,8 +15258,12 @@ class RuntimeDaemon:
15244
15258
  ac for ac in ac_list
15245
15259
  if ac["status"] != "covered" and ac["id"] not in pending
15246
15260
  ]
15247
- if uncovered and controlled_delivery:
15261
+ if uncovered and (controlled_delivery or necessary_checks):
15248
15262
  result.metrics["business_acceptance"] = {"status": "pending_human_qa", "criteria": [ac["id"] for ac in uncovered]}
15263
+ warnings = list(result.metrics.get("completion_warnings") or [])
15264
+ if "verification_pending" not in warnings:
15265
+ warnings.append("verification_pending")
15266
+ result.metrics["completion_warnings"] = warnings
15249
15267
  elif uncovered:
15250
15268
  ids = ", ".join(ac["id"] for ac in uncovered[:20])
15251
15269
  if len(uncovered) > 20:
@@ -15278,7 +15296,9 @@ class RuntimeDaemon:
15278
15296
  if not report_path.exists():
15279
15297
  issues.append(f"test-report.md not found in {doc_dir or 'workspace root'}")
15280
15298
 
15281
- return issues
15299
+ if necessary_checks and result.failure_code == "implementation_plan_revision_required" and original_execution[0] in {"success", "partial"}:
15300
+ result.status, result.failure_code, result.error = original_execution
15301
+ return _implementation_validation_issues(task, result, issues)
15282
15302
 
15283
15303
  def _merge_job_index_path(self) -> Path:
15284
15304
  return Path(self.workspaces_root) / "deploy-merge-jobs.json"
@@ -16698,13 +16718,24 @@ class RuntimeDaemon:
16698
16718
  before_sha=before_sha,
16699
16719
  preserved_paths=preserved_paths,
16700
16720
  )
16701
- issues = [_output_validation_issue(issue) for issue in self._validate_outputs(workspace_path, task, result)]
16721
+ issues = _implementation_validation_issues(task, result, self._validate_outputs(workspace_path, task, result))
16702
16722
  if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16703
16723
  _record_output_validation_issues(result, issues)
16704
16724
  logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
16705
16725
  return result
16706
16726
  if require_source_inventory:
16707
16727
  issues.extend(_output_validation_issue(issue) for issue in inventory_issues)
16728
+ if agent_core.uses_necessary_execution_checks(task.node_type, task.input_data or {}):
16729
+ issues = _implementation_validation_issues(task, result, issues)
16730
+ blocking = [issue for issue in issues if issue.severity != "warning"]
16731
+ if blocking and result.status in {"success", "partial"}:
16732
+ result.status = "failed"
16733
+ result.failure_code = blocking[0].code
16734
+ result.error = "; ".join(blocking[:3])
16735
+ _record_output_validation_issues(result, issues)
16736
+ result.metrics["execution_validation_mode"] = "necessary"
16737
+ self._record_testing_telemetry(workspace_path, task, result, retries_used=attempt)
16738
+ return result
16708
16739
  advisory_issues = [issue for issue in issues if issue.severity == "warning"]
16709
16740
  issues = [issue for issue in issues if issue.severity != "warning"]
16710
16741
  if not issues:
@@ -16864,13 +16895,24 @@ class RuntimeDaemon:
16864
16895
  before_sha=before_sha,
16865
16896
  preserved_paths=preserved_paths,
16866
16897
  )
16867
- remaining = [_output_validation_issue(issue) for issue in self._validate_outputs(workspace_path, task, result)]
16898
+ remaining = _implementation_validation_issues(task, result, self._validate_outputs(workspace_path, task, result))
16868
16899
  if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16869
16900
  _record_output_validation_issues(result, remaining)
16870
16901
  logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
16871
16902
  return result
16872
16903
  if require_source_inventory:
16873
16904
  remaining.extend(_output_validation_issue(issue) for issue in inventory_issues)
16905
+ if agent_core.uses_necessary_execution_checks(task.node_type, task.input_data or {}):
16906
+ remaining = _implementation_validation_issues(task, result, remaining)
16907
+ blocking = [issue for issue in remaining if issue.severity != "warning"]
16908
+ if blocking and result.status in {"success", "partial"}:
16909
+ result.status = "failed"
16910
+ result.failure_code = blocking[0].code
16911
+ result.error = "; ".join(blocking[:3])
16912
+ _record_output_validation_issues(result, remaining)
16913
+ result.metrics["execution_validation_mode"] = "necessary"
16914
+ self._record_testing_telemetry(workspace_path, task, result, retries_used=max_retries)
16915
+ return result
16874
16916
  if remaining:
16875
16917
  if getattr(task, "workspace_resolution", None) == "local_bind":
16876
16918
  _append_local_bind_output_log(task.task_id, [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.87.0
3
+ Version: 1.87.1
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.87.0"
3
+ version = "1.87.1"
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