forgexa-cli 1.76.1__tar.gz → 1.76.3__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.1 → forgexa_cli-1.76.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/agent_core.py +93 -0
  4. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/daemon.py +51 -4
  5. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/runtime_evidence.py +80 -0
  6. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/pyproject.toml +1 -1
  8. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/README.md +0 -0
  9. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/aws_logs_evidence.py +0 -0
  14. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/main.py +0 -0
  15. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/py.typed +0 -0
  16. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli/zcode_protocol.py +0 -0
  17. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  18. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  19. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  20. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/requires.txt +0 -0
  21. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  22. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/setup.cfg +0 -0
  23. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_auth_and_runtime_commands.py +0 -0
  24. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_autoupgrade.py +0 -0
  25. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_check_command.py +0 -0
  26. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
  27. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_local_bind_commands.py +0 -0
  28. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_runtime_credentials.py +0 -0
  29. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_session_credentials.py +0 -0
  30. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/tests/test_silent_install.py +0 -0
  31. {forgexa_cli-1.76.1 → forgexa_cli-1.76.3}/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.1
3
+ Version: 1.76.3
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.1"
2
+ __version__ = "1.76.3"
@@ -103,6 +103,99 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
103
103
  )
104
104
 
105
105
 
106
+ def assess_implementation_audit(contract: dict, report: object) -> dict:
107
+ """Validate implementation coverage independently of later runtime acceptance."""
108
+ issues: list[str] = []
109
+ findings: list[dict] = []
110
+ result = {
111
+ "audit_id": contract.get("audit_id"), "report_valid": False,
112
+ "node_complete": False, "status": "incomplete", "issues": issues,
113
+ "findings": findings, "followup_task_ids": [], "report": report,
114
+ }
115
+ if (
116
+ not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
117
+ or not isinstance(report, dict) or report.get("audit_id") != contract["audit_id"]
118
+ ):
119
+ issues.append("Implementation audit is missing or belongs to another remediation task")
120
+ return result
121
+ if set(report) - {"audit_id", "task_checks", "acceptance_checks"}:
122
+ issues.append("Implementation audit contains unsupported fields")
123
+ expected = {task["id"] for task in contract.get("tasks") or []}
124
+ subject = contract.get("subject_task_id")
125
+ if not expected or subject not in expected:
126
+ issues.append("Implementation audit has no approved task baseline")
127
+ task_checks = report.get("task_checks")
128
+ acceptance_checks = report.get("acceptance_checks")
129
+ if not isinstance(task_checks, list) or not isinstance(acceptance_checks, list):
130
+ issues.append("Implementation audit requires task_checks and acceptance_checks lists")
131
+ return result
132
+ checked: dict[str, str] = {}
133
+ checked_criteria: set[int] = set()
134
+ criterion_count = len(contract.get("acceptance_criteria") or [])
135
+ for kind, rows in (("task", task_checks), ("acceptance", acceptance_checks)):
136
+ for row in rows:
137
+ if not isinstance(row, dict):
138
+ issues.append(f"Invalid implementation {kind} check")
139
+ continue
140
+ evidence = row.get("evidence")
141
+ status = row.get("status")
142
+ if (
143
+ not isinstance(status, str) or status not in {"covered", "gap", "blocked", "unverified"}
144
+ or not isinstance(evidence, list) or not evidence
145
+ or any(not isinstance(item, str) or not item.strip() for item in evidence)
146
+ ):
147
+ issues.append(f"Implementation {kind} check requires an explicit status and concrete evidence")
148
+ continue
149
+ if kind == "task":
150
+ task_id = row.get("task_id")
151
+ if not isinstance(task_id, str) or task_id not in expected or task_id in checked:
152
+ issues.append("Implementation audit has unknown or duplicate task IDs")
153
+ continue
154
+ checked[task_id] = status
155
+ related = [task_id]
156
+ else:
157
+ index = row.get("index")
158
+ related = row.get("task_ids")
159
+ if (
160
+ type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
161
+ or not isinstance(related, list) or not related
162
+ or any(not isinstance(task_id, str) or task_id not in expected for task_id in related)
163
+ ):
164
+ issues.append("Implementation audit has invalid acceptance criterion or task references")
165
+ continue
166
+ checked_criteria.add(index)
167
+ if status != "covered":
168
+ description = row.get("description")
169
+ paths = row.get("files") or []
170
+ if not isinstance(description, str) or not description.strip() or not isinstance(paths, list):
171
+ issues.append("Implementation gaps require an actionable description and file list")
172
+ continue
173
+ if any(
174
+ not isinstance(path, str) or not path.strip()
175
+ or PurePosixPath(path).is_absolute() or ".." in PurePosixPath(path).parts
176
+ for path in paths
177
+ ):
178
+ issues.append("Implementation gap paths must be repository-relative")
179
+ continue
180
+ findings.extend({
181
+ "task_id": task_id, "status": status, "description": description,
182
+ "files": paths, "evidence": evidence,
183
+ } for task_id in related)
184
+ required = expected if contract.get("final_verification") else {subject}
185
+ if required - checked.keys():
186
+ issues.append("Implementation audit omitted required approved tasks")
187
+ if contract.get("final_verification") and len(checked_criteria) != criterion_count:
188
+ issues.append("Implementation audit omitted acceptance criteria")
189
+ result["report_valid"] = not issues
190
+ result["node_complete"] = (
191
+ not issues and checked.get(subject) == "covered"
192
+ and not any(finding["task_id"] == subject for finding in findings)
193
+ )
194
+ result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
195
+ result["status"] = "complete" if not issues and not findings else "incomplete"
196
+ return result
197
+
198
+
106
199
  _WORKSPACE_INSTRUCTION_PATHS = (
107
200
  "AGENTS.md",
108
201
  "CLAUDE.md",
@@ -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.1"
1237
+ DAEMON_VERSION = "1.76.3"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -13404,6 +13404,17 @@ class RuntimeDaemon:
13404
13404
  result.git = {}
13405
13405
 
13406
13406
  if result.status == "success":
13407
+ acceptance = result.metrics.get("acceptance_verification") or {}
13408
+ if acceptance.get("status") == "pending_final_qa":
13409
+ await reporter.report_progress(
13410
+ task.task_id, 94, "acceptance_pending_final_qa",
13411
+ execution_attempt_id=task.execution_attempt_id,
13412
+ output_lines=[
13413
+ "[daemon] E2E test assets are prepared; acceptance remains pending "
13414
+ "post-deployment final QA: " + ", ".join(acceptance["criteria"]),
13415
+ "[daemon] No browser acceptance or release approval is claimed.",
13416
+ ],
13417
+ )
13407
13418
  if is_local_bind:
13408
13419
  # Restricted commit/push adapter for the user's real
13409
13420
  # directory: never `git add -A` sensitive paths, never
@@ -13852,7 +13863,24 @@ class RuntimeDaemon:
13852
13863
  req_type = input_data.get("requirement_type", "feature")
13853
13864
  verification_work_item = bool(input_data.get("verification_work_item"))
13854
13865
 
13855
- if node_type == "coding":
13866
+ audit_contract = input_data.get("implementation_audit_contract")
13867
+ if isinstance(audit_contract, dict):
13868
+ 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
13871
+ result.observations = [
13872
+ observation for observation in result.observations
13873
+ if observation.get("type") != "implementation_audit"
13874
+ ]
13875
+ if isinstance(audit_report, dict):
13876
+ result.observations.append({"type": "implementation_audit", "data": audit_report})
13877
+ 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"])
13880
+
13881
+ if node_type == "coding" or (
13882
+ node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
13883
+ ):
13856
13884
  delivered_files = list(result.files_changed)
13857
13885
  delivered_files.extend(input_data.get("planned_task_predecessor_files") or [])
13858
13886
  for missing_path in agent_core.missing_planned_task_files(input_data, delivered_files):
@@ -14317,6 +14345,7 @@ class RuntimeDaemon:
14317
14345
  ))
14318
14346
 
14319
14347
  # --- coverage-matrix.json validation ---
14348
+ result.metrics.pop("acceptance_verification", None)
14320
14349
  if req_type == "task":
14321
14350
  result.metrics.pop("acceptance_criteria_manual_review_required", None)
14322
14351
  cm_path = base / "coverage-matrix.json"
@@ -14359,9 +14388,27 @@ class RuntimeDaemon:
14359
14388
  "coverage_matrix_invalid"
14360
14389
  )
14361
14390
  else:
14362
- uncovered = [ac for ac in ac_list if ac["status"] != "covered"]
14391
+ pending, contract_issues = (
14392
+ runtime_evidence.validate_deferred_acceptance_criteria(
14393
+ _input, cm_data, _tc_data,
14394
+ )
14395
+ )
14396
+ issues.extend(contract_issues)
14397
+ if pending:
14398
+ contract = _input["deferred_acceptance_verification"]
14399
+ result.metrics["acceptance_verification"] = {
14400
+ "status": "pending_final_qa",
14401
+ "analysis_revision_id": contract["analysis_revision_id"],
14402
+ "criteria": [ac["id"] for ac in ac_list if ac["id"] in pending],
14403
+ }
14404
+ uncovered = [
14405
+ ac for ac in ac_list
14406
+ if ac["status"] != "covered" and ac["id"] not in pending
14407
+ ]
14363
14408
  if uncovered:
14364
- ids = ", ".join(ac["id"] for ac in uncovered[:5])
14409
+ ids = ", ".join(ac["id"] for ac in uncovered[:20])
14410
+ if len(uncovered) > 20:
14411
+ ids += f" (+{len(uncovered) - 20} more)"
14365
14412
  issues.append(
14366
14413
  "Uncovered acceptance criteria in "
14367
14414
  f"coverage-matrix.json: {ids}"
@@ -783,6 +783,86 @@ def parse_acceptance_criteria_coverage_artifacts(
783
783
  }
784
784
 
785
785
 
786
+ def validate_deferred_acceptance_criteria(
787
+ input_data: dict[str, Any],
788
+ coverage_matrix: dict[str, Any],
789
+ test_cases: dict[str, Any] | None,
790
+ ) -> tuple[set[str], list[str]]:
791
+ """Validate a server-owned final-QA handoff without claiming test success."""
792
+ contract = input_data.get("deferred_acceptance_verification")
793
+ if contract is None:
794
+ return set(), []
795
+ snapshot = input_data.get("qa_test_level_policy_snapshot") or {}
796
+ policy = snapshot.get("policy") if isinstance(snapshot, dict) else None
797
+ levels = policy.get("levels") if isinstance(policy, dict) else None
798
+ e2e_policy = levels.get("e2e") if isinstance(levels, dict) else None
799
+ if (
800
+ not isinstance(contract, dict)
801
+ or not isinstance(snapshot, dict)
802
+ or not isinstance(policy, dict)
803
+ or policy.get("enforcement") != "enforce"
804
+ or not isinstance(e2e_policy, dict)
805
+ or contract.get("version") != 1
806
+ or contract.get("execution_phase") != "verification_final"
807
+ or not isinstance(snapshot.get("fingerprint"), str)
808
+ 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
+ ):
812
+ return set(), ["Final QA acceptance contract does not match the frozen testing policy"]
813
+
814
+ raw_expected = contract.get("acceptance_criteria_ids")
815
+ raw_deferred = contract.get("criteria")
816
+ if (
817
+ not isinstance(raw_expected, list)
818
+ or not raw_expected
819
+ or not isinstance(raw_deferred, list)
820
+ or not raw_deferred
821
+ ):
822
+ return set(), ["Final QA acceptance contract has no frozen acceptance criteria"]
823
+ expected = {_canonical_acceptance_criteria_reference(value) for value in raw_expected}
824
+ criteria = coverage_matrix["acceptance_criteria"]
825
+ identifiers = [_canonical_acceptance_criteria_reference(item["id"]) for item in criteria]
826
+ if None in expected or None in identifiers or len(set(identifiers)) != len(identifiers):
827
+ return set(), ["coverage-matrix.json has invalid or duplicate acceptance criterion IDs"]
828
+ if set(identifiers) != expected:
829
+ return set(), ["coverage-matrix.json does not match the frozen acceptance criteria"]
830
+
831
+ cases = (test_cases or {}).get("test_cases") or []
832
+ prepared_intents = {
833
+ case.get("intent_id") or case.get("test_intent_id")
834
+ for case in cases
835
+ if isinstance(case, dict)
836
+ and case.get("test_type") == "e2e"
837
+ and isinstance(case.get("intent_id") or case.get("test_intent_id"), str)
838
+ and case.get("id")
839
+ and case.get("script_path")
840
+ and case.get("script_content")
841
+ }
842
+ eligible = set()
843
+ for item in raw_deferred:
844
+ if not isinstance(item, dict):
845
+ return set(), ["Final QA acceptance contract has an invalid criterion mapping"]
846
+ identifier = _canonical_acceptance_criteria_reference(item.get("id"))
847
+ intent_ids = item.get("test_intent_ids")
848
+ if (
849
+ identifier not in expected
850
+ or not isinstance(intent_ids, list)
851
+ or not intent_ids
852
+ or any(not isinstance(value, str) or not value for value in intent_ids)
853
+ ):
854
+ 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"]
857
+ eligible.add(identifier)
858
+ return {
859
+ item["id"] for item, identifier in zip(criteria, identifiers)
860
+ if identifier in eligible
861
+ and item["status"] == "unverifiable"
862
+ and item.get("covered_by", []) == []
863
+ }, []
864
+
865
+
786
866
  async def run_acceptance_criteria_coverage_evidence(
787
867
  workspace_path: str | Path,
788
868
  check: object,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.76.1
3
+ Version: 1.76.3
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.1"
3
+ version = "1.76.3"
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