forgexa-cli 1.83.1__tar.gz → 1.84.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.83.1 → forgexa_cli-1.84.1}/PKG-INFO +1 -1
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/agent_core.py +49 -3
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/daemon.py +138 -69
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/pyproject.toml +1 -1
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/README.md +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/setup.cfg +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.83.1 → forgexa_cli-1.84.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.84.1"
|
|
@@ -67,6 +67,12 @@ except ImportError:
|
|
|
67
67
|
|
|
68
68
|
logger = logging.getLogger("agent_core")
|
|
69
69
|
|
|
70
|
+
IMPLEMENTATION_AUDIT_PROTOCOL_VERSION = 3
|
|
71
|
+
LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS = frozenset({
|
|
72
|
+
"validation_issues", "validation_issue_details", "timeout_recovery_validation_issues",
|
|
73
|
+
"timeout_recovery_original_failure", "task_delivery_issue",
|
|
74
|
+
})
|
|
75
|
+
|
|
70
76
|
|
|
71
77
|
def assess_approved_plan_conformance(baseline: dict, report: object) -> dict:
|
|
72
78
|
result = {
|
|
@@ -271,8 +277,41 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
271
277
|
"files": [], "evidence": row.get("evidence") or [],
|
|
272
278
|
} for task_id in pending_verification_ids)
|
|
273
279
|
implementation_statuses = {row["task_id"]: row["status"] for row in checks}
|
|
280
|
+
deferred_contract = contract.get("deferred_acceptance_verification")
|
|
281
|
+
reserved_indexes = set()
|
|
282
|
+
if (
|
|
283
|
+
isinstance(deferred_contract, dict) and type(deferred_contract.get("version")) is int
|
|
284
|
+
and deferred_contract["version"] in (1, 2, 3)
|
|
285
|
+
and deferred_contract.get("execution_phase") == "verification_final"
|
|
286
|
+
and isinstance(deferred_contract.get("workflow_id"), str) and deferred_contract["workflow_id"]
|
|
287
|
+
and isinstance(deferred_contract.get("analysis_revision_id"), str) and deferred_contract["analysis_revision_id"]
|
|
288
|
+
and re.fullmatch(r"[0-9a-f]{64}", str(deferred_contract.get("qa_test_level_policy_fingerprint", "")))
|
|
289
|
+
):
|
|
290
|
+
reserved_ids = deferred_contract.get("acceptance_criteria_ids")
|
|
291
|
+
criteria = deferred_contract.get("criteria")
|
|
292
|
+
if (
|
|
293
|
+
isinstance(reserved_ids, list) and reserved_ids and all(isinstance(value, str) for value in reserved_ids)
|
|
294
|
+
and len(set(reserved_ids)) == len(reserved_ids) and isinstance(criteria, list)
|
|
295
|
+
and len(criteria) == len(reserved_ids)
|
|
296
|
+
and reserved_ids == [criterion.get("id") for criterion in criteria if isinstance(criterion, dict)]
|
|
297
|
+
):
|
|
298
|
+
for criterion_id in reserved_ids:
|
|
299
|
+
match = re.fullmatch(r"AC-([0-9]+)", criterion_id)
|
|
300
|
+
if match is None or not 1 <= int(match.group(1)) <= len(contract.get("acceptance_criteria") or []):
|
|
301
|
+
reserved_indexes.clear()
|
|
302
|
+
break
|
|
303
|
+
reserved_indexes.add(int(match.group(1)))
|
|
304
|
+
if len(reserved_indexes) != len(reserved_ids):
|
|
305
|
+
reserved_indexes.clear()
|
|
274
306
|
acceptance_checks = []
|
|
307
|
+
deferred_acceptance = []
|
|
275
308
|
for row in report["acceptance_checks"]:
|
|
309
|
+
if (
|
|
310
|
+
row["index"] in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
|
|
311
|
+
and all(implementation_statuses.get(task_id) == "covered" for task_id in row["task_ids"])
|
|
312
|
+
):
|
|
313
|
+
deferred_acceptance.append(row["index"])
|
|
314
|
+
continue
|
|
276
315
|
if (
|
|
277
316
|
pending_verification_ids and row["status"] in ("blocked", "unverified")
|
|
278
317
|
and row.get("plan_revision") is None
|
|
@@ -285,7 +324,9 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
285
324
|
"files": row.get("files") or [], "evidence": row["evidence"],
|
|
286
325
|
} for task_id in pending_verification_ids)
|
|
287
326
|
acceptance_checks.append(row)
|
|
288
|
-
result = assess_implementation_audit({
|
|
327
|
+
result = assess_implementation_audit({
|
|
328
|
+
**contract, "version": 1, "final_verification": contract.get("final_verification") and not deferred_acceptance,
|
|
329
|
+
}, {
|
|
289
330
|
**report, "task_checks": checks, "acceptance_checks": acceptance_checks,
|
|
290
331
|
})
|
|
291
332
|
result["report"] = report
|
|
@@ -293,6 +334,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
293
334
|
result["report_valid"] = not result["issues"]
|
|
294
335
|
result["node_complete"] = result["node_complete"] and not issues
|
|
295
336
|
result["verification_pending"] = sorted({item["task_id"] for item in pending})
|
|
337
|
+
result["deferred_acceptance"] = sorted(deferred_acceptance)
|
|
296
338
|
result["findings"].extend(pending)
|
|
297
339
|
for request in original["revision_requests"]:
|
|
298
340
|
if request not in result["revision_requests"]:
|
|
@@ -302,7 +344,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
302
344
|
if contract.get("final_verification") or request["task_id"] == contract.get("subject_task_id")
|
|
303
345
|
]
|
|
304
346
|
result["followup_task_ids"] = sorted({finding["task_id"] for finding in result["findings"]})
|
|
305
|
-
if issues or pending or result["revision_requests"]:
|
|
347
|
+
if issues or pending or result["revision_requests"] or deferred_acceptance:
|
|
306
348
|
result["status"] = "incomplete"
|
|
307
349
|
if result["report_valid"] and result["blocking_revision_requests"]:
|
|
308
350
|
result["status"] = "plan_revision_required"
|
|
@@ -312,7 +354,8 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
312
354
|
|
|
313
355
|
def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
314
356
|
"""Validate implementation coverage independently of later runtime acceptance."""
|
|
315
|
-
|
|
357
|
+
version = contract.get("version", 1)
|
|
358
|
+
if type(version) is int and version == 2:
|
|
316
359
|
return _assess_implementation_contributions(contract, report)
|
|
317
360
|
issues: list[str] = []
|
|
318
361
|
findings: list[dict] = []
|
|
@@ -322,6 +365,9 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
322
365
|
"findings": findings, "followup_task_ids": [], "report": report,
|
|
323
366
|
"revision_requests": [], "blocking_reasons": [],
|
|
324
367
|
}
|
|
368
|
+
if type(version) is not int or version not in (1, 2):
|
|
369
|
+
issues.append("Implementation audit contract version is unsupported")
|
|
370
|
+
return result
|
|
325
371
|
if (
|
|
326
372
|
not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
|
|
327
373
|
or not isinstance(report, dict) or report.get("audit_id") != contract["audit_id"]
|
|
@@ -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.84.1"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -1885,6 +1885,56 @@ def _format_reflection_findings(findings: object) -> str:
|
|
|
1885
1885
|
return "\n".join(lines)
|
|
1886
1886
|
|
|
1887
1887
|
|
|
1888
|
+
class _OutputValidationIssue(str):
|
|
1889
|
+
code: str
|
|
1890
|
+
severity: str
|
|
1891
|
+
|
|
1892
|
+
def __new__(cls, code: str, message: str, severity: str = "error") -> "_OutputValidationIssue":
|
|
1893
|
+
issue = super().__new__(cls, message)
|
|
1894
|
+
issue.code = code
|
|
1895
|
+
issue.severity = severity
|
|
1896
|
+
return issue
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
def _output_validation_issue(issue: str) -> _OutputValidationIssue:
|
|
1900
|
+
if isinstance(issue, _OutputValidationIssue):
|
|
1901
|
+
return issue
|
|
1902
|
+
for prefix, code in (
|
|
1903
|
+
("Planned task file has no implementation evidence:", "planned_task_incomplete"),
|
|
1904
|
+
("Verification work item modified requirement analysis assets:", "verification_work_item_scope_violation"),
|
|
1905
|
+
("Uncovered acceptance criteria in coverage-matrix.json:", "acceptance_criteria_uncovered"),
|
|
1906
|
+
):
|
|
1907
|
+
if issue.startswith(prefix):
|
|
1908
|
+
return _OutputValidationIssue(code, issue)
|
|
1909
|
+
if issue.startswith("qa_policy."):
|
|
1910
|
+
return _OutputValidationIssue(issue.partition(":")[0], issue)
|
|
1911
|
+
if issue.startswith("Possible syntax issue in "):
|
|
1912
|
+
return _OutputValidationIssue("syntax_advisory", issue, "warning")
|
|
1913
|
+
return _OutputValidationIssue("output_validation_failed", issue)
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
def _record_output_validation_issues(result: "TaskResult", issues: list[str]) -> None:
|
|
1917
|
+
result.metrics["validation_issues"] = [str(issue) for issue in issues]
|
|
1918
|
+
result.metrics["validation_issue_details"] = [
|
|
1919
|
+
{"code": normalized.code, "severity": normalized.severity, "message": str(normalized)}
|
|
1920
|
+
for issue in issues for normalized in [_output_validation_issue(issue)]
|
|
1921
|
+
]
|
|
1922
|
+
|
|
1923
|
+
|
|
1924
|
+
def _mark_output_validation_unavailable(result: "TaskResult", snapshot: object = None) -> None:
|
|
1925
|
+
if result.status == "failed" and result.failure_code:
|
|
1926
|
+
return
|
|
1927
|
+
if _qa_source_test_validation_required(snapshot):
|
|
1928
|
+
_mark_qa_source_validation_unavailable(result)
|
|
1929
|
+
return
|
|
1930
|
+
result.status = "failed"
|
|
1931
|
+
result.failure_code = "validation_unavailable"
|
|
1932
|
+
result.error = "Output validation could not complete; inspect the runtime validation log before retrying."
|
|
1933
|
+
_record_output_validation_issues(result, [
|
|
1934
|
+
_OutputValidationIssue("validation_unavailable", result.error),
|
|
1935
|
+
])
|
|
1936
|
+
|
|
1937
|
+
|
|
1888
1938
|
def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
1889
1939
|
"""Strip raw local-environment output from ordinary completion fields.
|
|
1890
1940
|
|
|
@@ -1898,7 +1948,8 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
|
1898
1948
|
result.stdout = ""
|
|
1899
1949
|
result.stderr = ""
|
|
1900
1950
|
result.observations = []
|
|
1901
|
-
|
|
1951
|
+
for key in agent_core.LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS:
|
|
1952
|
+
result.metrics.pop(key, None)
|
|
1902
1953
|
critic_review = result.critic_review
|
|
1903
1954
|
if isinstance(critic_review, dict) and isinstance(critic_review.get("result"), dict):
|
|
1904
1955
|
result.critic_review = {
|
|
@@ -6698,6 +6749,7 @@ class ProcessManager:
|
|
|
6698
6749
|
|
|
6699
6750
|
elapsed = time.monotonic() - start_time
|
|
6700
6751
|
result.metrics["duration_seconds"] = round(elapsed, 2)
|
|
6752
|
+
result.metrics["actual_agent"] = agent.agent_id
|
|
6701
6753
|
|
|
6702
6754
|
# Detect agent-level failures despite exit code 0
|
|
6703
6755
|
output_failure = None if agent_core.canonical_agent_id(agent.agent_id) in {"deepseek", "minimax"} else self._detect_agent_output_failure(result, agent.agent_id)
|
|
@@ -10028,7 +10080,7 @@ class ServerConnection:
|
|
|
10028
10080
|
"acceptance_verification": {
|
|
10029
10081
|
"protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
|
|
10030
10082
|
},
|
|
10031
|
-
"implementation_audit": {"protocol_version":
|
|
10083
|
+
"implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
|
|
10032
10084
|
"task_delivery": {"protocol_version": 1},
|
|
10033
10085
|
"recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
|
|
10034
10086
|
"task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
|
|
@@ -10454,7 +10506,7 @@ class ServerConnection:
|
|
|
10454
10506
|
"acceptance_verification": {
|
|
10455
10507
|
"protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
|
|
10456
10508
|
},
|
|
10457
|
-
"implementation_audit": {"protocol_version":
|
|
10509
|
+
"implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
|
|
10458
10510
|
"task_delivery": {"protocol_version": 1},
|
|
10459
10511
|
"recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
|
|
10460
10512
|
"local_evidence": {"protocol_version": 1, "structured_summary": True},
|
|
@@ -12486,6 +12538,9 @@ class RuntimeDaemon:
|
|
|
12486
12538
|
):
|
|
12487
12539
|
return False
|
|
12488
12540
|
|
|
12541
|
+
result.metrics["timeout_recovery_original_failure"] = {
|
|
12542
|
+
"status": result.status, "failure_code": result.failure_code, "error": result.error,
|
|
12543
|
+
}
|
|
12489
12544
|
validation_issues = self._validate_outputs(workspace_path, task, result)
|
|
12490
12545
|
if validation_issues:
|
|
12491
12546
|
result.metrics["timeout_recovery_validation_issues"] = validation_issues
|
|
@@ -13371,17 +13426,14 @@ class RuntimeDaemon:
|
|
|
13371
13426
|
reporter, on_output_chunk, tried_agents=tried_agents,
|
|
13372
13427
|
default_branch=default_branch, before_sha=delivery_before_sha,
|
|
13373
13428
|
)
|
|
13374
|
-
# Re-collect
|
|
13375
|
-
|
|
13376
|
-
pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
|
|
13429
|
+
# Re-collect after validation, including a successful repair.
|
|
13430
|
+
pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
|
|
13377
13431
|
except Exception:
|
|
13378
|
-
|
|
13432
|
+
_mark_output_validation_unavailable(
|
|
13433
|
+
result,
|
|
13379
13434
|
(task.input_data or {}).get("qa_test_level_policy_snapshot")
|
|
13380
|
-
)
|
|
13381
|
-
|
|
13382
|
-
logger.exception("Validation gate error for task %s (refusing to proceed)", task.task_id)
|
|
13383
|
-
else:
|
|
13384
|
-
logger.exception("Validation gate error for task %s (proceeding anyway)", task.task_id)
|
|
13435
|
+
)
|
|
13436
|
+
logger.exception("Validation gate error for task %s (refusing to proceed)", task.task_id)
|
|
13385
13437
|
|
|
13386
13438
|
if result.failure_code == "verification_work_item_scope_violation" and not is_local_bind:
|
|
13387
13439
|
await self._discard_verification_scope_changes(
|
|
@@ -13455,8 +13507,11 @@ class RuntimeDaemon:
|
|
|
13455
13507
|
)
|
|
13456
13508
|
pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
|
|
13457
13509
|
except Exception:
|
|
13510
|
+
_mark_output_validation_unavailable(
|
|
13511
|
+
result, (task.input_data or {}).get("qa_test_level_policy_snapshot"),
|
|
13512
|
+
)
|
|
13458
13513
|
logger.exception(
|
|
13459
|
-
"Post-validation gate error for task %s (
|
|
13514
|
+
"Post-validation gate error for task %s (refusing to proceed)",
|
|
13460
13515
|
task.task_id,
|
|
13461
13516
|
)
|
|
13462
13517
|
break
|
|
@@ -14216,9 +14271,10 @@ class RuntimeDaemon:
|
|
|
14216
14271
|
result.status = "failed"
|
|
14217
14272
|
result.failure_code = "approved_plan_baseline_modified"
|
|
14218
14273
|
result.error = "Implementation modified frozen analysis assets: " + ", ".join(forbidden_paths[:5])
|
|
14219
|
-
issues.append(result.error)
|
|
14274
|
+
issues.append(_OutputValidationIssue(result.failure_code, result.error))
|
|
14220
14275
|
|
|
14221
14276
|
audit_contract = input_data.get("implementation_audit_contract")
|
|
14277
|
+
result.metrics.pop("implementation_audit_node_complete", None)
|
|
14222
14278
|
if isinstance(audit_contract, dict):
|
|
14223
14279
|
agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
|
|
14224
14280
|
audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
|
|
@@ -14229,6 +14285,9 @@ class RuntimeDaemon:
|
|
|
14229
14285
|
if isinstance(audit_report, dict):
|
|
14230
14286
|
result.observations.append({"type": "implementation_audit", "data": audit_report})
|
|
14231
14287
|
audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
|
|
14288
|
+
result.metrics["implementation_audit_node_complete"] = bool(
|
|
14289
|
+
audit_result["report_valid"] and audit_result["node_complete"]
|
|
14290
|
+
)
|
|
14232
14291
|
if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
|
|
14233
14292
|
result.status = "failed"
|
|
14234
14293
|
result.failure_code = "implementation_plan_revision_required"
|
|
@@ -14236,11 +14295,12 @@ class RuntimeDaemon:
|
|
|
14236
14295
|
result.error = "Approved analysis and task plan require revision before implementation can continue: " + "; ".join(
|
|
14237
14296
|
f"{request['task_id']}: {request['reason']}" for request in requests[:3]
|
|
14238
14297
|
)
|
|
14239
|
-
issues.append(result.error)
|
|
14298
|
+
issues.append(_OutputValidationIssue(result.failure_code, result.error))
|
|
14240
14299
|
elif not audit_result["report_valid"] or not audit_result["node_complete"]:
|
|
14241
|
-
issues.extend(
|
|
14300
|
+
issues.extend(_OutputValidationIssue("implementation_audit_incomplete", issue) for issue in (
|
|
14301
|
+
audit_result["issues"] or audit_result.get("blocking_reasons") or [
|
|
14242
14302
|
"The selected approved task still has implementation gaps",
|
|
14243
|
-
])
|
|
14303
|
+
]))
|
|
14244
14304
|
|
|
14245
14305
|
if node_type == "coding" or (
|
|
14246
14306
|
node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
|
|
@@ -14248,11 +14308,12 @@ class RuntimeDaemon:
|
|
|
14248
14308
|
delivered_files = list(result.files_changed)
|
|
14249
14309
|
delivered_files.extend(input_data.get("planned_task_predecessor_files") or [])
|
|
14250
14310
|
for missing_path in agent_core.missing_planned_task_files(input_data, delivered_files):
|
|
14251
|
-
issues.append(
|
|
14311
|
+
issues.append(_OutputValidationIssue(
|
|
14312
|
+
"planned_task_incomplete",
|
|
14252
14313
|
f"Planned task file has no implementation evidence: {missing_path}. "
|
|
14253
14314
|
"Complete the planned change or report the task as blocked; "
|
|
14254
14315
|
"documentation-only changes do not complete a source-code task."
|
|
14255
|
-
)
|
|
14316
|
+
))
|
|
14256
14317
|
|
|
14257
14318
|
qa_policy, qa_policy_issues = _parse_frozen_qa_test_level_policy(
|
|
14258
14319
|
input_data.get("qa_test_level_policy_snapshot")
|
|
@@ -14272,10 +14333,11 @@ class RuntimeDaemon:
|
|
|
14272
14333
|
if _is_requirement_analysis_path(path)
|
|
14273
14334
|
)
|
|
14274
14335
|
if forbidden_paths:
|
|
14275
|
-
issues.append(
|
|
14336
|
+
issues.append(_OutputValidationIssue(
|
|
14337
|
+
"verification_work_item_scope_violation",
|
|
14276
14338
|
"Verification work item modified requirement analysis assets: "
|
|
14277
14339
|
+ ", ".join(forbidden_paths[:5])
|
|
14278
|
-
)
|
|
14340
|
+
))
|
|
14279
14341
|
|
|
14280
14342
|
if verification_work_item and node_type == "analysis":
|
|
14281
14343
|
analysis_doc_path = str(input_data.get("verification_analysis_doc_path", "") or "")
|
|
@@ -14431,24 +14493,29 @@ class RuntimeDaemon:
|
|
|
14431
14493
|
# Syntax-check modified Python and JS/TS files
|
|
14432
14494
|
for f in result.files_changed:
|
|
14433
14495
|
fpath = workspace_path / f
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14496
|
+
try:
|
|
14497
|
+
if not fpath.exists():
|
|
14498
|
+
continue
|
|
14499
|
+
if f.endswith(".py"):
|
|
14438
14500
|
import ast
|
|
14439
|
-
ast.parse(fpath.
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
)
|
|
14501
|
+
ast.parse(fpath.read_bytes(), filename=f)
|
|
14502
|
+
elif f.endswith((".js", ".ts", ".jsx", ".tsx")):
|
|
14503
|
+
content = fpath.read_text(encoding="utf-8")
|
|
14504
|
+
opens = content.count("{") + content.count("(") + content.count("[")
|
|
14505
|
+
closes = content.count("}") + content.count(")") + content.count("]")
|
|
14506
|
+
if abs(opens - closes) > 2:
|
|
14507
|
+
issues.append(_OutputValidationIssue(
|
|
14508
|
+
"syntax_advisory", f"Possible syntax issue in {f}: "
|
|
14509
|
+
f"bracket imbalance (open={opens}, close={closes})", "warning",
|
|
14510
|
+
))
|
|
14511
|
+
except SyntaxError as error:
|
|
14512
|
+
issues.append(_OutputValidationIssue(
|
|
14513
|
+
"source_syntax_invalid", f"Python syntax error in {f}: {error.msg} (line {error.lineno})",
|
|
14514
|
+
))
|
|
14515
|
+
except (OSError, UnicodeError):
|
|
14516
|
+
issues.append(_OutputValidationIssue(
|
|
14517
|
+
"validation_unavailable", f"Output validation could not read source file: {f}",
|
|
14518
|
+
))
|
|
14452
14519
|
|
|
14453
14520
|
# Task 6 (P6 fix): warning-level phase_handoffs check for non-analysis
|
|
14454
14521
|
# nodes. These nodes SHOULD append a phase_handoffs entry to analysis.json
|
|
@@ -14773,10 +14840,11 @@ class RuntimeDaemon:
|
|
|
14773
14840
|
ids = ", ".join(ac["id"] for ac in uncovered[:20])
|
|
14774
14841
|
if len(uncovered) > 20:
|
|
14775
14842
|
ids += f" (+{len(uncovered) - 20} more)"
|
|
14776
|
-
issues.append(
|
|
14843
|
+
issues.append(_OutputValidationIssue(
|
|
14844
|
+
"acceptance_criteria_uncovered",
|
|
14777
14845
|
"Uncovered acceptance criteria in "
|
|
14778
14846
|
f"coverage-matrix.json: {ids}"
|
|
14779
|
-
)
|
|
14847
|
+
))
|
|
14780
14848
|
except (_json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
14781
14849
|
issues.append(f"coverage-matrix.json is not valid JSON: {e}")
|
|
14782
14850
|
elif (
|
|
@@ -16220,12 +16288,18 @@ class RuntimeDaemon:
|
|
|
16220
16288
|
before_sha=before_sha,
|
|
16221
16289
|
preserved_paths=preserved_paths,
|
|
16222
16290
|
)
|
|
16223
|
-
issues = self._validate_outputs(workspace_path, task, result)
|
|
16291
|
+
issues = [_output_validation_issue(issue) for issue in self._validate_outputs(workspace_path, task, result)]
|
|
16224
16292
|
if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
|
|
16293
|
+
_record_output_validation_issues(result, issues)
|
|
16294
|
+
logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
|
|
16225
16295
|
return result
|
|
16226
16296
|
if require_source_inventory:
|
|
16227
|
-
issues.extend(inventory_issues)
|
|
16297
|
+
issues.extend(_output_validation_issue(issue) for issue in inventory_issues)
|
|
16298
|
+
advisory_issues = [issue for issue in issues if issue.severity == "warning"]
|
|
16299
|
+
issues = [issue for issue in issues if issue.severity != "warning"]
|
|
16228
16300
|
if not issues:
|
|
16301
|
+
if advisory_issues:
|
|
16302
|
+
_record_output_validation_issues(result, advisory_issues)
|
|
16229
16303
|
self._record_testing_telemetry(
|
|
16230
16304
|
workspace_path, task, result, retries_used=attempt
|
|
16231
16305
|
)
|
|
@@ -16240,7 +16314,7 @@ class RuntimeDaemon:
|
|
|
16240
16314
|
if (
|
|
16241
16315
|
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16242
16316
|
and all(
|
|
16243
|
-
issue.
|
|
16317
|
+
issue.code == "acceptance_criteria_uncovered"
|
|
16244
16318
|
for issue in issues
|
|
16245
16319
|
)
|
|
16246
16320
|
):
|
|
@@ -16251,7 +16325,7 @@ class RuntimeDaemon:
|
|
|
16251
16325
|
"Resolve the unmet criteria or missing test prerequisites, "
|
|
16252
16326
|
"then rerun verification."
|
|
16253
16327
|
)
|
|
16254
|
-
result
|
|
16328
|
+
_record_output_validation_issues(result, issues)
|
|
16255
16329
|
self._record_testing_telemetry(
|
|
16256
16330
|
workspace_path, task, result, retries_used=attempt
|
|
16257
16331
|
)
|
|
@@ -16349,11 +16423,13 @@ class RuntimeDaemon:
|
|
|
16349
16423
|
before_sha=before_sha,
|
|
16350
16424
|
preserved_paths=preserved_paths,
|
|
16351
16425
|
)
|
|
16352
|
-
remaining = self._validate_outputs(workspace_path, task, result)
|
|
16426
|
+
remaining = [_output_validation_issue(issue) for issue in self._validate_outputs(workspace_path, task, result)]
|
|
16353
16427
|
if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
|
|
16428
|
+
_record_output_validation_issues(result, remaining)
|
|
16429
|
+
logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
|
|
16354
16430
|
return result
|
|
16355
16431
|
if require_source_inventory:
|
|
16356
|
-
remaining.extend(inventory_issues)
|
|
16432
|
+
remaining.extend(_output_validation_issue(issue) for issue in inventory_issues)
|
|
16357
16433
|
if remaining:
|
|
16358
16434
|
if getattr(task, "workspace_resolution", None) == "local_bind":
|
|
16359
16435
|
_append_local_bind_output_log(task.task_id, [
|
|
@@ -16363,7 +16439,7 @@ class RuntimeDaemon:
|
|
|
16363
16439
|
if (
|
|
16364
16440
|
agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
|
|
16365
16441
|
and all(
|
|
16366
|
-
issue.
|
|
16442
|
+
issue.code == "acceptance_criteria_uncovered"
|
|
16367
16443
|
for issue in remaining
|
|
16368
16444
|
)
|
|
16369
16445
|
):
|
|
@@ -16374,7 +16450,7 @@ class RuntimeDaemon:
|
|
|
16374
16450
|
"Resolve the unmet criteria or missing test prerequisites, "
|
|
16375
16451
|
"then rerun verification."
|
|
16376
16452
|
)
|
|
16377
|
-
result
|
|
16453
|
+
_record_output_validation_issues(result, remaining)
|
|
16378
16454
|
self._record_testing_telemetry(
|
|
16379
16455
|
workspace_path, task, result, retries_used=max_retries
|
|
16380
16456
|
)
|
|
@@ -16385,35 +16461,22 @@ class RuntimeDaemon:
|
|
|
16385
16461
|
scope_violations = [
|
|
16386
16462
|
issue
|
|
16387
16463
|
for issue in remaining
|
|
16388
|
-
if issue.
|
|
16464
|
+
if issue.code == "verification_work_item_scope_violation"
|
|
16389
16465
|
]
|
|
16390
|
-
critical_patterns = (
|
|
16391
|
-
"missing",
|
|
16392
|
-
"not found",
|
|
16393
|
-
"is empty",
|
|
16394
|
-
"not valid json",
|
|
16395
|
-
"invalid acceptance criteria",
|
|
16396
|
-
"case_evidence",
|
|
16397
|
-
"test-evidence.json has no",
|
|
16398
|
-
"evidence item",
|
|
16399
|
-
"anchor not found",
|
|
16400
|
-
"uncovered acceptance criteria",
|
|
16401
|
-
"no p0 priority test cases",
|
|
16402
|
-
"contains no test cases",
|
|
16403
|
-
)
|
|
16404
16466
|
critical_issues = [
|
|
16405
|
-
|
|
16406
|
-
if any(p in iss.lower() for p in critical_patterns)
|
|
16467
|
+
issue for issue in remaining if issue.severity != "warning"
|
|
16407
16468
|
]
|
|
16408
16469
|
qa_policy_violations = [
|
|
16409
|
-
issue for issue in remaining if issue.startswith("qa_policy.")
|
|
16470
|
+
issue for issue in remaining if issue.code.startswith("qa_policy.")
|
|
16410
16471
|
]
|
|
16411
16472
|
planned_delivery_issues = [
|
|
16412
16473
|
issue for issue in remaining
|
|
16413
|
-
if issue.
|
|
16474
|
+
if issue.code == "planned_task_incomplete"
|
|
16414
16475
|
]
|
|
16476
|
+
audit_incomplete = result.metrics.get("implementation_audit_node_complete") is False
|
|
16415
16477
|
blocking_issues = list(dict.fromkeys(
|
|
16416
16478
|
critical_issues + scope_violations + qa_policy_violations + planned_delivery_issues
|
|
16479
|
+
+ (remaining if audit_incomplete else [])
|
|
16417
16480
|
))
|
|
16418
16481
|
if blocking_issues:
|
|
16419
16482
|
# Agent didn't produce required output — mark as failed
|
|
@@ -16430,6 +16493,12 @@ class RuntimeDaemon:
|
|
|
16430
16493
|
elif planned_delivery_issues:
|
|
16431
16494
|
result.failure_code = "planned_task_incomplete"
|
|
16432
16495
|
result.error = "; ".join(planned_delivery_issues[:3])
|
|
16496
|
+
elif any(issue.code == "validation_unavailable" for issue in remaining):
|
|
16497
|
+
result.failure_code = "validation_unavailable"
|
|
16498
|
+
result.error = "; ".join(critical_issues[:3])
|
|
16499
|
+
elif audit_incomplete:
|
|
16500
|
+
result.failure_code = "implementation_audit_incomplete"
|
|
16501
|
+
result.error = "; ".join(remaining[:3])
|
|
16433
16502
|
else:
|
|
16434
16503
|
result.failure_code = "validation_retry_exhausted"
|
|
16435
16504
|
result.error = (
|
|
@@ -16443,7 +16512,7 @@ class RuntimeDaemon:
|
|
|
16443
16512
|
"(proceeding anyway)",
|
|
16444
16513
|
len(remaining), max_retries, task.task_id,
|
|
16445
16514
|
)
|
|
16446
|
-
result
|
|
16515
|
+
_record_output_validation_issues(result, remaining)
|
|
16447
16516
|
self._record_testing_telemetry(workspace_path, task, result, retries_used=max_retries)
|
|
16448
16517
|
return result
|
|
16449
16518
|
|
|
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
|