forgexa-cli 1.83.1__tar.gz → 1.84.0__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.83.1 → forgexa_cli-1.84.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/agent_core.py +49 -3
  4. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/daemon.py +138 -69
  5. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/README.md +0 -0
  8. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/setup.cfg +0 -0
  25. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.83.1 → forgexa_cli-1.84.0}/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.83.1
3
+ Version: 1.84.0
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.83.1"
2
+ __version__ = "1.84.0"
@@ -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({**contract, "version": 1}, {
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
- if contract.get("version") == 2:
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.83.1"
1237
+ DAEMON_VERSION = "1.84.0"
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
- result.metrics.pop("validation_issues", None)
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": 3},
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": 3},
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 git info if validation triggered retries
13375
- if result.metrics.get("validation_issues") is not None or True:
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
- if _qa_source_test_validation_required(
13432
+ _mark_output_validation_unavailable(
13433
+ result,
13379
13434
  (task.input_data or {}).get("qa_test_level_policy_snapshot")
13380
- ):
13381
- _mark_qa_source_validation_unavailable(result)
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 (proceeding anyway)",
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(audit_result["issues"] or audit_result.get("blocking_reasons") or [
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
- if not fpath.exists():
14435
- continue
14436
- if f.endswith(".py"):
14437
- try:
14496
+ try:
14497
+ if not fpath.exists():
14498
+ continue
14499
+ if f.endswith(".py"):
14438
14500
  import ast
14439
- ast.parse(fpath.read_text(encoding="utf-8"), filename=f)
14440
- except SyntaxError as e:
14441
- issues.append(f"Python syntax error in {f}: {e.msg} (line {e.lineno})")
14442
- elif f.endswith((".js", ".ts", ".jsx", ".tsx")):
14443
- # Basic bracket/brace balance check
14444
- content = fpath.read_text(encoding="utf-8")
14445
- opens = content.count("{") + content.count("(") + content.count("[")
14446
- closes = content.count("}") + content.count(")") + content.count("]")
14447
- if abs(opens - closes) > 2:
14448
- issues.append(
14449
- f"Possible syntax issue in {f}: "
14450
- f"bracket imbalance (open={opens}, close={closes})"
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.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
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.metrics["validation_issues"] = issues
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.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
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.metrics["validation_issues"] = remaining
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.startswith("Verification work item modified requirement analysis assets:")
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
- iss for iss in remaining
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.startswith("Planned task file has no implementation evidence:")
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.metrics["validation_issues"] = remaining
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.83.1
3
+ Version: 1.84.0
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.83.1"
3
+ version = "1.84.0"
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