forgexa-cli 1.86.4__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.86.4 → forgexa_cli-1.87.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/agent_core.py +103 -14
  4. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/daemon.py +71 -23
  5. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/README.md +0 -0
  8. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/setup.cfg +0 -0
  25. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.86.4 → 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.86.4
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.86.4"
2
+ __version__ = "1.87.1"
@@ -782,7 +782,48 @@ def lock_provenance_projection(path: str, source: str) -> str | None:
782
782
  }, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
783
783
 
784
784
 
785
- def implementation_delivery_contract(node_type: str, task_input: dict, *, supported: bool, graph_type: str = "execution") -> dict | None:
785
+ EXECUTION_DELIVERY_WARNING_CODES = frozenset({
786
+ "partial_delivery", "dependency_authorization", "dependency_network", "dependency_unavailable",
787
+ "environment_unavailable", "verification_pending", "source_evidence_limited", "qa_handoff_ready",
788
+ })
789
+
790
+
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:
825
+ if type(protocol_version) is not int or protocol_version not in (1, 2):
826
+ return None
786
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"}:
787
828
  return None
788
829
  if task_input.get("verification_work_item") or task_input.get("execution_actor") == "human":
@@ -796,9 +837,11 @@ def implementation_delivery_contract(node_type: str, task_input: dict, *, suppor
796
837
  task_id = (baseline.get("task") or {}).get("id")
797
838
  if not isinstance(task_id, str) or not task_id:
798
839
  return None
799
- contract = {"version": 1, "mode": "execution_and_artifacts", "node_type": node_type, "graph_type": graph_type,
840
+ contract = {"version": protocol_version, "mode": "execution_and_artifacts", "node_type": node_type, "graph_type": graph_type,
800
841
  "task_id": task_id, "baseline_fingerprint": baseline.get("fingerprint"),
801
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
802
845
  contract["fingerprint"] = hashlib.sha256(json.dumps(contract, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
803
846
  return contract
804
847
 
@@ -807,27 +850,73 @@ def uses_controlled_delivery(task_input: dict) -> bool:
807
850
  contract = task_input.get("implementation_delivery")
808
851
  if not isinstance(contract, dict):
809
852
  return False
810
- expected = implementation_delivery_contract(contract.get("node_type"), task_input, supported=True, graph_type=contract.get("graph_type"))
853
+ if not uses_necessary_execution_checks(contract.get("node_type"), task_input):
854
+ return False
855
+ expected = implementation_delivery_contract(contract.get("node_type"), task_input, supported=True,
856
+ graph_type=contract.get("graph_type"), protocol_version=contract.get("version"),
857
+ qa_handoff_supported=contract.get("qa_handoff") is True)
811
858
  return expected is not None and contract == expected
812
859
 
813
860
 
814
861
  def assess_execution_delivery(contract: dict, report: object) -> dict:
815
- if not isinstance(report, dict) or type(report.get("version")) is not int or report["version"] != 1:
862
+ if not isinstance(report, dict) or type(report.get("version")) is not int or report["version"] not in (1, 2):
816
863
  return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration is missing or invalid"]}
817
- if set(report) != {"version", "contract_fingerprint", "status"} or report.get("contract_fingerprint") != contract.get("fingerprint") or not isinstance(report.get("status"), str) or report["status"] not in {"complete", "partial", "blocked"}:
864
+ if report["version"] != contract.get("version") or report.get("contract_fingerprint") != contract.get("fingerprint") or redact_execution_delivery(report) is None:
818
865
  return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration does not match the current contract"]}
819
- 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
872
+
873
+
874
+ def execution_delivery_allows_progress(contract: dict, assessment: dict) -> bool:
875
+ return assessment.get("report_valid") is True and (
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
+ )
879
+ )
880
+
881
+
882
+ def execution_delivery_warnings(contract: dict, report: object) -> list[str]:
883
+ if not isinstance(report, dict) or contract.get("version") != 2 or not assess_execution_delivery(contract, report)["report_valid"]:
884
+ return []
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
820
891
 
821
892
 
822
893
  def redact_execution_delivery(report: object) -> dict | None:
823
- if not isinstance(report, dict):
894
+ if not isinstance(report, dict) or type(report.get("version")) is not int or report["version"] not in (1, 2):
895
+ return None
896
+ required = {"version", "contract_fingerprint", "status"}
897
+ allowed = required | ({"warnings", "handoff"} if report["version"] == 2 else set())
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"]):
899
+ return None
900
+ if not isinstance(report.get("status"), str) or report["status"] not in {"complete", "partial", "blocked"}:
824
901
  return None
825
- return {"version": report.get("version") if type(report.get("version")) is int else 0,
826
- "contract_fingerprint": report.get("contract_fingerprint") if isinstance(report.get("contract_fingerprint"), str) and re.fullmatch(r"[0-9a-f]{64}", report["contract_fingerprint"]) else "",
827
- "status": report.get("status") if report.get("status") in ("complete", "partial", "blocked") else "unknown"}
902
+ warnings = report.get("warnings", [])
903
+ if not isinstance(warnings, list) or len(warnings) > len(EXECUTION_DELIVERY_WARNING_CODES) or any(
904
+ not isinstance(code, str) or code not in EXECUTION_DELIVERY_WARNING_CODES for code in warnings
905
+ ) or len(set(warnings)) != len(warnings):
906
+ return None
907
+ if "qa_handoff_ready" in warnings and "handoff" not in report:
908
+ return None
909
+ redacted = {key: report[key] for key in ("version", "contract_fingerprint", "status")}
910
+ if "warnings" in report:
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"])
916
+ return redacted
828
917
 
829
918
 
830
- 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:
831
920
  baseline = task_input.get("approved_plan_baseline")
832
921
  reasons = []
833
922
  if policy_supported is not True:
@@ -863,8 +952,8 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
863
952
  if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
864
953
  reasons.append("scope_unknown")
865
954
  decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
866
- if uses_controlled_delivery(task_input):
867
- 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")
868
957
  decision["fingerprint"] = hashlib.sha256(json.dumps({
869
958
  **decision, "baseline_fingerprint": fingerprint,
870
959
  "qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
@@ -874,7 +963,7 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
874
963
 
875
964
 
876
965
  def review_blocks_completion(policy: dict, assessment: dict) -> bool:
877
- if policy.get("decision_scope") == "controlled_delivery":
966
+ if policy.get("decision_scope") in {"controlled_delivery", "necessary_execution"}:
878
967
  return False
879
968
  return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
880
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.86.4"
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": 1},
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": 1},
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,16 +14649,23 @@ 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))
14643
14657
  delivery = agent_core.assess_execution_delivery(input_data["implementation_delivery"], report)
14644
14658
  result.metrics["execution_delivery"] = agent_core.redact_execution_delivery(report)
14645
14659
  result.metrics["execution_delivery_valid"] = delivery["report_valid"]
14646
- if not delivery["report_valid"]:
14660
+ result.metrics.pop("completion_warnings", None)
14661
+ if not delivery["report_valid"] and (report is not None or not necessary_checks):
14647
14662
  issues.extend(_OutputValidationIssue("validation_unavailable", message) for message in delivery["issues"])
14648
- elif delivery["status"] != "complete":
14663
+ elif delivery["report_valid"] and not agent_core.execution_delivery_allows_progress(input_data["implementation_delivery"], delivery):
14649
14664
  issues.append(_OutputValidationIssue("planned_task_incomplete", "Agent explicitly reported incomplete execution delivery"))
14665
+ else:
14666
+ warnings = agent_core.execution_delivery_warnings(input_data["implementation_delivery"], report)
14667
+ if warnings:
14668
+ result.metrics["completion_warnings"] = warnings
14650
14669
  fix_context = input_data.get("fix_context")
14651
14670
  if node_type == "testing" or (
14652
14671
  node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
@@ -14682,7 +14701,7 @@ class RuntimeDaemon:
14682
14701
  if failure_domain is not None and not controlled_delivery:
14683
14702
  result.metrics["failure_domain"] = failure_domain
14684
14703
  result.metrics["implementation_audit_node_complete"] = bool(
14685
- controlled_delivery or audit_result["report_valid"] and audit_result["node_complete"]
14704
+ audit_result["report_valid"] and audit_result["node_complete"]
14686
14705
  )
14687
14706
  result.metrics["implementation_audit_subject_blocked"] = bool(
14688
14707
  not controlled_delivery and audit_result["report_valid"] and not audit_result["node_complete"]
@@ -14698,7 +14717,7 @@ class RuntimeDaemon:
14698
14717
  f"{request['task_id']}: {request['reason']}" for request in requests[:3]
14699
14718
  )
14700
14719
  issues.append(_OutputValidationIssue(result.failure_code, result.error))
14701
- 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"]):
14702
14721
  issues.extend(_OutputValidationIssue("implementation_audit_incomplete", issue) for issue in (
14703
14722
  audit_result["issues"] or audit_result.get("blocking_reasons") or [
14704
14723
  "The selected approved task still has implementation gaps",
@@ -15239,8 +15258,12 @@ class RuntimeDaemon:
15239
15258
  ac for ac in ac_list
15240
15259
  if ac["status"] != "covered" and ac["id"] not in pending
15241
15260
  ]
15242
- if uncovered and controlled_delivery:
15261
+ if uncovered and (controlled_delivery or necessary_checks):
15243
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
15244
15267
  elif uncovered:
15245
15268
  ids = ", ".join(ac["id"] for ac in uncovered[:20])
15246
15269
  if len(uncovered) > 20:
@@ -15273,7 +15296,9 @@ class RuntimeDaemon:
15273
15296
  if not report_path.exists():
15274
15297
  issues.append(f"test-report.md not found in {doc_dir or 'workspace root'}")
15275
15298
 
15276
- 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)
15277
15302
 
15278
15303
  def _merge_job_index_path(self) -> Path:
15279
15304
  return Path(self.workspaces_root) / "deploy-merge-jobs.json"
@@ -16693,13 +16718,24 @@ class RuntimeDaemon:
16693
16718
  before_sha=before_sha,
16694
16719
  preserved_paths=preserved_paths,
16695
16720
  )
16696
- 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))
16697
16722
  if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16698
16723
  _record_output_validation_issues(result, issues)
16699
16724
  logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
16700
16725
  return result
16701
16726
  if require_source_inventory:
16702
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
16703
16739
  advisory_issues = [issue for issue in issues if issue.severity == "warning"]
16704
16740
  issues = [issue for issue in issues if issue.severity != "warning"]
16705
16741
  if not issues:
@@ -16719,7 +16755,8 @@ class RuntimeDaemon:
16719
16755
  if (
16720
16756
  agent_core.uses_controlled_delivery(task.input_data or {})
16721
16757
  and result.metrics.get("execution_delivery_valid") is True
16722
- and (result.metrics.get("execution_delivery") or {}).get("status") in {"partial", "blocked"}
16758
+ and not agent_core.execution_delivery_allows_progress(task.input_data["implementation_delivery"],
16759
+ agent_core.assess_execution_delivery(task.input_data["implementation_delivery"], result.metrics.get("execution_delivery")))
16723
16760
  ):
16724
16761
  result.status = "failed"
16725
16762
  result.failure_code = "planned_task_incomplete"
@@ -16858,13 +16895,24 @@ class RuntimeDaemon:
16858
16895
  before_sha=before_sha,
16859
16896
  preserved_paths=preserved_paths,
16860
16897
  )
16861
- 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))
16862
16899
  if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16863
16900
  _record_output_validation_issues(result, remaining)
16864
16901
  logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
16865
16902
  return result
16866
16903
  if require_source_inventory:
16867
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
16868
16916
  if remaining:
16869
16917
  if getattr(task, "workspace_resolution", None) == "local_bind":
16870
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.86.4
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.86.4"
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