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.
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/PKG-INFO +1 -1
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/agent_core.py +103 -14
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/daemon.py +71 -23
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/pyproject.toml +1 -1
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/README.md +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/setup.cfg +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.86.4 → forgexa_cli-1.87.1}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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"]
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
867
|
-
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")
|
|
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")
|
|
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.
|
|
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
|
|
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": 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
|
|
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
|
-
|
|
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["
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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, [
|
|
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
|