forgexa-cli 1.76.3__tar.gz → 1.77.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 (31) hide show
  1. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/agent_core.py +77 -0
  4. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/daemon.py +34 -7
  5. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/runtime_evidence.py +80 -5
  6. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/pyproject.toml +1 -1
  8. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/README.md +0 -0
  9. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/aws_logs_evidence.py +0 -0
  14. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/main.py +0 -0
  15. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/py.typed +0 -0
  16. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli/zcode_protocol.py +0 -0
  17. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  18. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  19. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  20. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/requires.txt +0 -0
  21. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  22. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/setup.cfg +0 -0
  23. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_auth_and_runtime_commands.py +0 -0
  24. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_autoupgrade.py +0 -0
  25. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_check_command.py +0 -0
  26. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  27. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_local_bind_commands.py +0 -0
  28. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_runtime_credentials.py +0 -0
  29. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_session_credentials.py +0 -0
  30. {forgexa_cli-1.76.3 → forgexa_cli-1.77.0}/tests/test_silent_install.py +0 -0
  31. {forgexa_cli-1.76.3 → forgexa_cli-1.77.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.76.3
3
+ Version: 1.77.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.76.3"
2
+ __version__ = "1.77.0"
@@ -103,6 +103,61 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
103
103
  )
104
104
 
105
105
 
106
+ def extract_implementation_audit(text: str) -> dict | None:
107
+ """Read the last complete audit envelope from mixed assistant output."""
108
+ decoder = json.JSONDecoder()
109
+ offset = 0
110
+ report = None
111
+ report_end = 0
112
+ while offset < len(text):
113
+ start = text.find("{", offset)
114
+ if start < 0:
115
+ break
116
+ try:
117
+ value, end = decoder.raw_decode(text, start)
118
+ except json.JSONDecodeError:
119
+ offset = start + 1
120
+ continue
121
+ if isinstance(value, dict) and "implementation_audit" in value:
122
+ report = value["implementation_audit"] if isinstance(value["implementation_audit"], dict) else None
123
+ report_end = end
124
+ offset = end
125
+ if '"implementation_audit"' in text[report_end:]:
126
+ return None
127
+ return report
128
+
129
+
130
+ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
131
+ """Keep contract-owned status and hashed evidence without local free text."""
132
+ from hashlib import sha256
133
+
134
+ if not assess_implementation_audit(contract, report)["report_valid"]:
135
+ return None
136
+ summary = {"audit_id": contract["audit_id"], "task_checks": [], "acceptance_checks": []}
137
+ for key in ("task_checks", "acceptance_checks"):
138
+ for row in report[key]:
139
+ evidence = []
140
+ for reference in row["evidence"][:20]:
141
+ digest = reference.removeprefix("local-evidence-sha256:")
142
+ evidence.append(reference if digest != reference and len(digest) == 64
143
+ and all(character in "0123456789abcdef" for character in digest)
144
+ else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
145
+ check = {"status": row["status"], "evidence": evidence}
146
+ if key == "task_checks":
147
+ check["task_id"] = row["task_id"]
148
+ else:
149
+ check.update(index=row["index"], task_ids=row["task_ids"])
150
+ if row["status"] != "covered":
151
+ check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
152
+ if row.get("plan_revision"):
153
+ check["plan_revision"] = {
154
+ "reason": "Local evidence conflicts with the approved plan",
155
+ "proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
156
+ }
157
+ summary[key].append(check)
158
+ return summary
159
+
160
+
106
161
  def assess_implementation_audit(contract: dict, report: object) -> dict:
107
162
  """Validate implementation coverage independently of later runtime acceptance."""
108
163
  issues: list[str] = []
@@ -111,6 +166,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
111
166
  "audit_id": contract.get("audit_id"), "report_valid": False,
112
167
  "node_complete": False, "status": "incomplete", "issues": issues,
113
168
  "findings": findings, "followup_task_ids": [], "report": report,
169
+ "revision_requests": [],
114
170
  }
115
171
  if (
116
172
  not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
@@ -156,6 +212,12 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
156
212
  else:
157
213
  index = row.get("index")
158
214
  related = row.get("task_ids")
215
+ if type(index) is int and not 1 <= index <= criterion_count:
216
+ issues.append(
217
+ f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
218
+ "report the baseline conflict instead of dropping or renumbering requirements"
219
+ )
220
+ continue
159
221
  if (
160
222
  type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
161
223
  or not isinstance(related, list) or not related
@@ -177,9 +239,22 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
177
239
  ):
178
240
  issues.append("Implementation gap paths must be repository-relative")
179
241
  continue
242
+ revision = row.get("plan_revision")
243
+ if revision is not None:
244
+ if not isinstance(revision, dict) or any(
245
+ not isinstance(revision.get(key), str) or not revision[key].strip()
246
+ for key in ("reason", "proposal")
247
+ ):
248
+ issues.append("Plan revision requests require evidence, reason and a concrete proposal")
249
+ continue
250
+ result["revision_requests"].extend({
251
+ "task_id": task_id, "reason": revision["reason"], "proposal": revision["proposal"],
252
+ "evidence": evidence, "files": paths,
253
+ } for task_id in related)
180
254
  findings.extend({
181
255
  "task_id": task_id, "status": status, "description": description,
182
256
  "files": paths, "evidence": evidence,
257
+ **({"plan_revision": revision} if revision is not None else {}),
183
258
  } for task_id in related)
184
259
  required = expected if contract.get("final_verification") else {subject}
185
260
  if required - checked.keys():
@@ -193,6 +268,8 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
193
268
  )
194
269
  result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
195
270
  result["status"] = "complete" if not issues and not findings else "incomplete"
271
+ if result["report_valid"] and result["revision_requests"]:
272
+ result["status"] = "plan_revision_required"
196
273
  return result
197
274
 
198
275
 
@@ -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.0"
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
  )
@@ -13866,8 +13881,7 @@ class RuntimeDaemon:
13866
13881
  audit_contract = input_data.get("implementation_audit_contract")
13867
13882
  if isinstance(audit_contract, dict):
13868
13883
  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
13884
+ audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
13871
13885
  result.observations = [
13872
13886
  observation for observation in result.observations
13873
13887
  if observation.get("type") != "implementation_audit"
@@ -13875,7 +13889,12 @@ class RuntimeDaemon:
13875
13889
  if isinstance(audit_report, dict):
13876
13890
  result.observations.append({"type": "implementation_audit", "data": audit_report})
13877
13891
  audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
13878
- if not audit_result["report_valid"] or not audit_result["node_complete"]:
13892
+ if audit_result["report_valid"] and audit_result["revision_requests"]:
13893
+ result.status = "failed"
13894
+ result.failure_code = "implementation_plan_revision_required"
13895
+ result.error = "Approved analysis and task plan require revision before implementation can continue"
13896
+ issues.append(result.error)
13897
+ elif not audit_result["report_valid"] or not audit_result["node_complete"]:
13879
13898
  issues.extend(audit_result["issues"] or ["The selected approved task still has implementation gaps"])
13880
13899
 
13881
13900
  if node_type == "coding" or (
@@ -14415,7 +14434,11 @@ class RuntimeDaemon:
14415
14434
  )
14416
14435
  except (_json.JSONDecodeError, UnicodeDecodeError) as e:
14417
14436
  issues.append(f"coverage-matrix.json is not valid JSON: {e}")
14418
- elif _requires_structured_artifacts or _requires_e2e_artifacts:
14437
+ elif (
14438
+ _requires_structured_artifacts
14439
+ or _requires_e2e_artifacts
14440
+ or _input.get("deferred_acceptance_verification") is not None
14441
+ ):
14419
14442
  issues.append(f"coverage-matrix.json not found in {doc_dir or 'workspace root'}")
14420
14443
 
14421
14444
  # --- test-evidence.json validation (M1 evidence bundle) ---
@@ -15840,6 +15863,8 @@ class RuntimeDaemon:
15840
15863
  preserved_paths=preserved_paths,
15841
15864
  )
15842
15865
  issues = self._validate_outputs(workspace_path, task, result)
15866
+ if result.failure_code == "implementation_plan_revision_required":
15867
+ return result
15843
15868
  if require_source_inventory:
15844
15869
  issues.extend(inventory_issues)
15845
15870
  if not issues:
@@ -15967,6 +15992,8 @@ class RuntimeDaemon:
15967
15992
  preserved_paths=preserved_paths,
15968
15993
  )
15969
15994
  remaining = self._validate_outputs(workspace_path, task, result)
15995
+ if result.failure_code == "implementation_plan_revision_required":
15996
+ return result
15970
15997
  if require_source_inventory:
15971
15998
  remaining.extend(inventory_issues)
15972
15999
  if remaining:
@@ -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.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.76.3"
3
+ version = "1.77.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