forgexa-cli 1.76.3__tar.gz → 1.77.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/agent_core.py +94 -0
  4. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/daemon.py +45 -12
  5. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/runtime_evidence.py +80 -5
  6. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/pyproject.toml +1 -1
  8. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/README.md +0 -0
  9. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  14. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/main.py +0 -0
  15. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/py.typed +0 -0
  16. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli/zcode_protocol.py +0 -0
  17. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  18. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  19. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  20. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/requires.txt +0 -0
  21. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  22. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/setup.cfg +0 -0
  23. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_auth_and_runtime_commands.py +0 -0
  24. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_autoupgrade.py +0 -0
  25. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_check_command.py +0 -0
  26. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  27. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_local_bind_commands.py +0 -0
  28. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_runtime_credentials.py +0 -0
  29. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_session_credentials.py +0 -0
  30. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_silent_install.py +0 -0
  31. {forgexa_cli-1.76.3 → forgexa_cli-1.77.1}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.76.3
3
+ Version: 1.77.1
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.76.3"
2
+ __version__ = "1.77.1"
@@ -67,6 +67,15 @@ except ImportError:
67
67
  logger = logging.getLogger("agent_core")
68
68
 
69
69
 
70
+ def is_implementation_testing_task(node_type: str, input_data: dict) -> bool:
71
+ """Retain the testing responsibility of a phase-remediation wrapper."""
72
+ fix_context = input_data.get("fix_context")
73
+ return node_type == "testing" or (
74
+ node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
75
+ and isinstance(fix_context, dict) and fix_context.get("failed_node_type") == "testing"
76
+ )
77
+
78
+
70
79
  def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> list[str]:
71
80
  """Find planned files without delivery evidence, including grouped sub-tasks."""
72
81
  def normalize_path(value: object) -> str | None:
@@ -103,6 +112,61 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
103
112
  )
104
113
 
105
114
 
115
+ def extract_implementation_audit(text: str) -> dict | None:
116
+ """Read the last complete audit envelope from mixed assistant output."""
117
+ decoder = json.JSONDecoder()
118
+ offset = 0
119
+ report = None
120
+ report_end = 0
121
+ while offset < len(text):
122
+ start = text.find("{", offset)
123
+ if start < 0:
124
+ break
125
+ try:
126
+ value, end = decoder.raw_decode(text, start)
127
+ except json.JSONDecodeError:
128
+ offset = start + 1
129
+ continue
130
+ if isinstance(value, dict) and "implementation_audit" in value:
131
+ report = value["implementation_audit"] if isinstance(value["implementation_audit"], dict) else None
132
+ report_end = end
133
+ offset = end
134
+ if '"implementation_audit"' in text[report_end:]:
135
+ return None
136
+ return report
137
+
138
+
139
+ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
140
+ """Keep contract-owned status and hashed evidence without local free text."""
141
+ from hashlib import sha256
142
+
143
+ if not assess_implementation_audit(contract, report)["report_valid"]:
144
+ return None
145
+ summary = {"audit_id": contract["audit_id"], "task_checks": [], "acceptance_checks": []}
146
+ for key in ("task_checks", "acceptance_checks"):
147
+ for row in report[key]:
148
+ evidence = []
149
+ for reference in row["evidence"][:20]:
150
+ digest = reference.removeprefix("local-evidence-sha256:")
151
+ evidence.append(reference if digest != reference and len(digest) == 64
152
+ and all(character in "0123456789abcdef" for character in digest)
153
+ else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
154
+ check = {"status": row["status"], "evidence": evidence}
155
+ if key == "task_checks":
156
+ check["task_id"] = row["task_id"]
157
+ else:
158
+ check.update(index=row["index"], task_ids=row["task_ids"])
159
+ if row["status"] != "covered":
160
+ check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
161
+ if row.get("plan_revision"):
162
+ check["plan_revision"] = {
163
+ "reason": "Local evidence conflicts with the approved plan",
164
+ "proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
165
+ }
166
+ summary[key].append(check)
167
+ return summary
168
+
169
+
106
170
  def assess_implementation_audit(contract: dict, report: object) -> dict:
107
171
  """Validate implementation coverage independently of later runtime acceptance."""
108
172
  issues: list[str] = []
@@ -111,6 +175,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
111
175
  "audit_id": contract.get("audit_id"), "report_valid": False,
112
176
  "node_complete": False, "status": "incomplete", "issues": issues,
113
177
  "findings": findings, "followup_task_ids": [], "report": report,
178
+ "revision_requests": [], "blocking_reasons": [],
114
179
  }
115
180
  if (
116
181
  not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
@@ -156,6 +221,12 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
156
221
  else:
157
222
  index = row.get("index")
158
223
  related = row.get("task_ids")
224
+ if type(index) is int and not 1 <= index <= criterion_count:
225
+ issues.append(
226
+ f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
227
+ "report the baseline conflict instead of dropping or renumbering requirements"
228
+ )
229
+ continue
159
230
  if (
160
231
  type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
161
232
  or not isinstance(related, list) or not related
@@ -164,6 +235,10 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
164
235
  issues.append("Implementation audit has invalid acceptance criterion or task references")
165
236
  continue
166
237
  checked_criteria.add(index)
238
+ if len(related) > 1:
239
+ unresolved_contributors = [task_id for task_id in related if checked.get(task_id) != "covered"]
240
+ if unresolved_contributors:
241
+ related = unresolved_contributors
167
242
  if status != "covered":
168
243
  description = row.get("description")
169
244
  paths = row.get("files") or []
@@ -177,9 +252,22 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
177
252
  ):
178
253
  issues.append("Implementation gap paths must be repository-relative")
179
254
  continue
255
+ revision = row.get("plan_revision")
256
+ if revision is not None:
257
+ if not isinstance(revision, dict) or any(
258
+ not isinstance(revision.get(key), str) or not revision[key].strip()
259
+ for key in ("reason", "proposal")
260
+ ):
261
+ issues.append("Plan revision requests require evidence, reason and a concrete proposal")
262
+ continue
263
+ result["revision_requests"].extend({
264
+ "task_id": task_id, "reason": revision["reason"], "proposal": revision["proposal"],
265
+ "evidence": evidence, "files": paths,
266
+ } for task_id in related)
180
267
  findings.extend({
181
268
  "task_id": task_id, "status": status, "description": description,
182
269
  "files": paths, "evidence": evidence,
270
+ **({"plan_revision": revision} if revision is not None else {}),
183
271
  } for task_id in related)
184
272
  required = expected if contract.get("final_verification") else {subject}
185
273
  if required - checked.keys():
@@ -191,8 +279,14 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
191
279
  not issues and checked.get(subject) == "covered"
192
280
  and not any(finding["task_id"] == subject for finding in findings)
193
281
  )
282
+ result["blocking_reasons"] = list(dict.fromkeys(
283
+ f"Approved task '{subject}' is {finding['status']}: {finding['description']}"
284
+ for finding in findings if finding["task_id"] == subject
285
+ ))
194
286
  result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
195
287
  result["status"] = "complete" if not issues and not findings else "incomplete"
288
+ if result["report_valid"] and result["revision_requests"]:
289
+ result["status"] = "plan_revision_required"
196
290
  return result
197
291
 
198
292
 
@@ -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.76.3"
1237
+ DAEMON_VERSION = "1.77.1"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -2641,6 +2641,14 @@ def _read_local_bind_shared_output_snapshot(task_id: str) -> dict | None:
2641
2641
 
2642
2642
  def _prepare_local_bind_completion(task: "TaskInfo", result: "TaskResult") -> None:
2643
2643
  """Attach the automatic private snapshot before stripping normal local output."""
2644
+ result.metrics.pop("implementation_audit", None)
2645
+ audit_contract = (task.input_data or {}).get("implementation_audit_contract")
2646
+ if isinstance(audit_contract, dict):
2647
+ audit_report = next((item.get("data") for item in reversed(result.observations)
2648
+ if item.get("type") == "implementation_audit"), None)
2649
+ summary = agent_core.redact_implementation_audit(audit_contract, audit_report)
2650
+ if summary is not None:
2651
+ result.metrics["implementation_audit"] = summary
2644
2652
  if _MANAGED_WSL_POLICY is not None:
2645
2653
  result.shared_local_output = None
2646
2654
  if _local_bind_output_sharing_enabled(task.local_workspace_binding_id):
@@ -9904,6 +9912,9 @@ class ServerConnection:
9904
9912
  "local_fresh_branch_reset": {
9905
9913
  "protocol_version": _LOCAL_FRESH_BRANCH_RESET_PROTOCOL_VERSION,
9906
9914
  },
9915
+ "acceptance_verification": {
9916
+ "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
9917
+ },
9907
9918
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
9908
9919
  },
9909
9920
  "server_url": self.server_url,
@@ -10324,6 +10335,9 @@ class ServerConnection:
10324
10335
  "local_fresh_branch_reset": {
10325
10336
  "protocol_version": _LOCAL_FRESH_BRANCH_RESET_PROTOCOL_VERSION,
10326
10337
  },
10338
+ "acceptance_verification": {
10339
+ "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
10340
+ },
10327
10341
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10328
10342
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10329
10343
  },
@@ -13410,8 +13424,9 @@ class RuntimeDaemon:
13410
13424
  task.task_id, 94, "acceptance_pending_final_qa",
13411
13425
  execution_attempt_id=task.execution_attempt_id,
13412
13426
  output_lines=[
13413
- "[daemon] E2E test assets are prepared; acceptance remains pending "
13414
- "post-deployment final QA: " + ", ".join(acceptance["criteria"]),
13427
+ "[daemon] Verification assets are prepared; acceptance remains pending "
13428
+ "post-deployment automated or human verification: "
13429
+ + ", ".join(acceptance["criteria"]),
13415
13430
  "[daemon] No browser acceptance or release approval is claimed.",
13416
13431
  ],
13417
13432
  )
@@ -13860,14 +13875,15 @@ class RuntimeDaemon:
13860
13875
  issues: list[str] = []
13861
13876
  node_type = task.node_type
13862
13877
  input_data = task.input_data or {}
13878
+ if agent_core.is_implementation_testing_task(node_type, input_data):
13879
+ node_type = "testing"
13863
13880
  req_type = input_data.get("requirement_type", "feature")
13864
13881
  verification_work_item = bool(input_data.get("verification_work_item"))
13865
13882
 
13866
13883
  audit_contract = input_data.get("implementation_audit_contract")
13867
13884
  if isinstance(audit_contract, dict):
13868
13885
  agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
13869
- parsed = _daemon_extract_json(_daemon_extract_agent_output(agent_id, result.stdout))
13870
- audit_report = parsed.get("implementation_audit") if isinstance(parsed, dict) else None
13886
+ audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
13871
13887
  result.observations = [
13872
13888
  observation for observation in result.observations
13873
13889
  if observation.get("type") != "implementation_audit"
@@ -13875,8 +13891,15 @@ class RuntimeDaemon:
13875
13891
  if isinstance(audit_report, dict):
13876
13892
  result.observations.append({"type": "implementation_audit", "data": audit_report})
13877
13893
  audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
13878
- if not audit_result["report_valid"] or not audit_result["node_complete"]:
13879
- issues.extend(audit_result["issues"] or ["The selected approved task still has implementation gaps"])
13894
+ if audit_result["report_valid"] and audit_result["revision_requests"]:
13895
+ result.status = "failed"
13896
+ result.failure_code = "implementation_plan_revision_required"
13897
+ result.error = "Approved analysis and task plan require revision before implementation can continue"
13898
+ issues.append(result.error)
13899
+ elif not audit_result["report_valid"] or not audit_result["node_complete"]:
13900
+ issues.extend(audit_result["issues"] or audit_result["blocking_reasons"] or [
13901
+ "The selected approved task still has implementation gaps",
13902
+ ])
13880
13903
 
13881
13904
  if node_type == "coding" or (
13882
13905
  node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
@@ -14415,7 +14438,11 @@ class RuntimeDaemon:
14415
14438
  )
14416
14439
  except (_json.JSONDecodeError, UnicodeDecodeError) as e:
14417
14440
  issues.append(f"coverage-matrix.json is not valid JSON: {e}")
14418
- elif _requires_structured_artifacts or _requires_e2e_artifacts:
14441
+ elif (
14442
+ _requires_structured_artifacts
14443
+ or _requires_e2e_artifacts
14444
+ or _input.get("deferred_acceptance_verification") is not None
14445
+ ):
14419
14446
  issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
14420
14447
 
14421
14448
  # --- test-evidence.json validation (M1 evidence bundle) ---
@@ -15840,6 +15867,8 @@ class RuntimeDaemon:
15840
15867
  preserved_paths=preserved_paths,
15841
15868
  )
15842
15869
  issues = self._validate_outputs(workspace_path, task, result)
15870
+ if result.failure_code == "implementation_plan_revision_required":
15871
+ return result
15843
15872
  if require_source_inventory:
15844
15873
  issues.extend(inventory_issues)
15845
15874
  if not issues:
@@ -15855,7 +15884,7 @@ class RuntimeDaemon:
15855
15884
  ])
15856
15885
 
15857
15886
  if (
15858
- task.node_type == "testing"
15887
+ agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
15859
15888
  and all(
15860
15889
  issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
15861
15890
  for issue in issues
@@ -15967,6 +15996,8 @@ class RuntimeDaemon:
15967
15996
  preserved_paths=preserved_paths,
15968
15997
  )
15969
15998
  remaining = self._validate_outputs(workspace_path, task, result)
15999
+ if result.failure_code == "implementation_plan_revision_required":
16000
+ return result
15970
16001
  if require_source_inventory:
15971
16002
  remaining.extend(inventory_issues)
15972
16003
  if remaining:
@@ -15976,7 +16007,7 @@ class RuntimeDaemon:
15976
16007
  *[f"[daemon] {issue}" for issue in remaining],
15977
16008
  ])
15978
16009
  if (
15979
- task.node_type == "testing"
16010
+ agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
15980
16011
  and all(
15981
16012
  issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
15982
16013
  for issue in remaining
@@ -16135,7 +16166,7 @@ class RuntimeDaemon:
16135
16166
  result.metrics["validation_issue_categories"] = sorted(
16136
16167
  {_categorize_validation_issue(str(issue)) for issue in issues}
16137
16168
  )
16138
- if task.node_type != "testing":
16169
+ if not agent_core.is_implementation_testing_task(task.node_type, task.input_data or {}):
16139
16170
  return
16140
16171
  input_data = task.input_data or {}
16141
16172
  context = input_data.get("context") or {}
@@ -17589,7 +17620,9 @@ class RuntimeDaemon:
17589
17620
  before_sha: str,
17590
17621
  ) -> None:
17591
17622
  """Bind generated testing artifacts to the committed testing attempt."""
17592
- if task.workspace_resolution != "cloud_worktree" or task.node_type != "testing":
17623
+ if task.workspace_resolution != "cloud_worktree" or not agent_core.is_implementation_testing_task(
17624
+ task.node_type, task.input_data or {},
17625
+ ):
17593
17626
  return
17594
17627
 
17595
17628
  after_sha = str((result.git or {}).get("commit_sha") or "").strip()
@@ -15,6 +15,7 @@ from pathlib import Path
15
15
  from typing import Any
16
16
 
17
17
  RUNTIME_EVIDENCE_PLAN_VERSION = 1
18
+ DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION = 2
18
19
  TEST_COVERAGE_CHECK_KIND = "test_coverage"
19
20
  STATIC_CHECK_KIND = "static_check"
20
21
  TYPE_CHECK_KIND = "type_check"
@@ -802,12 +803,18 @@ def validate_deferred_acceptance_criteria(
802
803
  or not isinstance(policy, dict)
803
804
  or policy.get("enforcement") != "enforce"
804
805
  or not isinstance(e2e_policy, dict)
805
- or contract.get("version") != 1
806
+ or type(contract.get("version")) is not int
807
+ or contract["version"] not in (1, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
806
808
  or contract.get("execution_phase") != "verification_final"
807
809
  or not isinstance(snapshot.get("fingerprint"), str)
808
810
  or contract.get("qa_test_level_policy_fingerprint") != snapshot["fingerprint"]
809
- or e2e_policy.get("enabled") is not True
810
- or e2e_policy.get("workflow_auto_run") is not True
811
+ or (
812
+ contract["version"] == 1
813
+ and (
814
+ e2e_policy.get("enabled") is not True
815
+ or e2e_policy.get("workflow_auto_run") is not True
816
+ )
817
+ )
811
818
  ):
812
819
  return set(), ["Final QA acceptance contract does not match the frozen testing policy"]
813
820
 
@@ -829,6 +836,8 @@ def validate_deferred_acceptance_criteria(
829
836
  return set(), ["coverage-matrix.json does not match the frozen acceptance criteria"]
830
837
 
831
838
  cases = (test_cases or {}).get("test_cases") or []
839
+ if not isinstance(cases, list):
840
+ return set(), ["Final QA acceptance requires a test_cases array"]
832
841
  prepared_intents = {
833
842
  case.get("intent_id") or case.get("test_intent_id")
834
843
  for case in cases
@@ -852,8 +861,74 @@ def validate_deferred_acceptance_criteria(
852
861
  or any(not isinstance(value, str) or not value for value in intent_ids)
853
862
  ):
854
863
  return set(), ["Final QA acceptance contract has an invalid test intent mapping"]
855
- if not set(intent_ids).issubset(prepared_intents):
856
- return set(), [f"Final QA acceptance criterion {identifier} has no prepared E2E test case"]
864
+ if contract["version"] == 1:
865
+ if not set(intent_ids).issubset(prepared_intents):
866
+ return set(), [f"Final QA acceptance criterion {identifier} has no prepared E2E test case"]
867
+ else:
868
+ requirements = item.get("test_intents")
869
+ if (
870
+ not isinstance(requirements, list)
871
+ or len(requirements) != len(intent_ids)
872
+ or any(not isinstance(requirement, dict) for requirement in requirements)
873
+ or any(
874
+ not isinstance(requirement.get("id"), str)
875
+ for requirement in requirements
876
+ )
877
+ or len(set(intent_ids)) != len(intent_ids)
878
+ or {requirement.get("id") for requirement in requirements} != set(intent_ids)
879
+ ):
880
+ return set(), ["Final QA acceptance contract has invalid verification obligations"]
881
+ for requirement in requirements:
882
+ target = requirement.get("automation_target")
883
+ expected_refs = requirement.get("acceptance_criteria_refs")
884
+ if (
885
+ not isinstance(expected_refs, list)
886
+ or any(not isinstance(reference, str) for reference in expected_refs)
887
+ or identifier not in expected_refs
888
+ or not set(expected_refs).issubset(expected)
889
+ ):
890
+ return set(), ["Final QA acceptance contract has invalid verification references"]
891
+ if target == "e2e":
892
+ prepared = (
893
+ e2e_policy.get("enabled") is True
894
+ and e2e_policy.get("workflow_auto_run") is True
895
+ and requirement["id"] in prepared_intents
896
+ )
897
+ elif target == "manual":
898
+ reason = requirement.get("manual_reason_code")
899
+ if not isinstance(reason, str) or not reason.strip():
900
+ return set(), ["Final QA acceptance contract has invalid manual obligations"]
901
+ prepared = any(
902
+ isinstance(case, dict)
903
+ and isinstance(case.get("id"), str)
904
+ and bool(case["id"].strip())
905
+ and (case.get("intent_id") or case.get("test_intent_id")) == requirement["id"]
906
+ and case.get("test_type") == "manual"
907
+ and case.get("manual_reason_code") == reason
908
+ and isinstance(case.get("acceptance_criteria_refs"), list)
909
+ and {
910
+ _canonical_acceptance_criteria_reference(reference)
911
+ for reference in case["acceptance_criteria_refs"]
912
+ } == set(expected_refs)
913
+ and isinstance(case.get("steps"), list)
914
+ and bool(case["steps"])
915
+ and all(
916
+ isinstance(step, dict)
917
+ and isinstance(step.get("action"), str)
918
+ and bool(step["action"].strip())
919
+ for step in case["steps"]
920
+ )
921
+ and not case.get("script_path")
922
+ and not case.get("script_content")
923
+ for case in cases
924
+ )
925
+ else:
926
+ return set(), ["Final QA acceptance contract has an unsupported verification target"]
927
+ if not prepared:
928
+ return set(), [
929
+ f"Final QA acceptance criterion {identifier} has no prepared "
930
+ f"{target} test case for intent {requirement['id']}"
931
+ ]
857
932
  eligible.add(identifier)
858
933
  return {
859
934
  item["id"] for item, identifier in zip(criteria, identifiers)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.76.3
3
+ Version: 1.77.1
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.76.3"
3
+ version = "1.77.1"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes