forgexa-cli 1.87.0__tar.gz → 1.87.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/PKG-INFO +1 -1
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/agent_core.py +67 -11
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/daemon.py +64 -22
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/pyproject.toml +1 -1
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/README.md +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/setup.cfg +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.87.0 → forgexa_cli-1.87.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.87.
|
|
2
|
+
__version__ = "1.87.2"
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
900
|
-
decision.update(mode="advisory", decision_scope="
|
|
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")
|
|
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.
|
|
1237
|
+
DAEMON_VERSION = "1.87.2"
|
|
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
|
|
2605
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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, [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|