forgexa-cli 1.77.0__tar.gz → 1.77.2__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.77.0 → forgexa_cli-1.77.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/agent_core.py +96 -15
  4. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/daemon.py +18 -6
  5. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/runtime_evidence.py +2 -2
  6. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/pyproject.toml +1 -1
  8. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/README.md +0 -0
  9. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/aws_logs_evidence.py +0 -0
  14. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/main.py +0 -0
  15. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/py.typed +0 -0
  16. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli/zcode_protocol.py +0 -0
  17. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  18. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  19. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  20. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/requires.txt +0 -0
  21. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  22. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/setup.cfg +0 -0
  23. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_auth_and_runtime_commands.py +0 -0
  24. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_autoupgrade.py +0 -0
  25. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_check_command.py +0 -0
  26. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  27. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_local_bind_commands.py +0 -0
  28. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_runtime_credentials.py +0 -0
  29. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_session_credentials.py +0 -0
  30. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/tests/test_silent_install.py +0 -0
  31. {forgexa_cli-1.77.0 → forgexa_cli-1.77.2}/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.77.0
3
+ Version: 1.77.2
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.77.0"
2
+ __version__ = "1.77.2"
@@ -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:
@@ -133,40 +142,104 @@ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
133
142
 
134
143
  if not assess_implementation_audit(contract, report)["report_valid"]:
135
144
  return None
145
+ def redact_check(row: dict) -> dict:
146
+ evidence = []
147
+ for reference in row["evidence"][:20]:
148
+ digest = reference.removeprefix("local-evidence-sha256:")
149
+ evidence.append(reference if digest != reference and len(digest) == 64
150
+ and all(character in "0123456789abcdef" for character in digest)
151
+ else "local-evidence-sha256:" + sha256(reference.encode()).hexdigest())
152
+ check = {"status": row["status"], "evidence": evidence}
153
+ if row["status"] != "covered":
154
+ check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
155
+ if row.get("plan_revision"):
156
+ check["plan_revision"] = {
157
+ "reason": "Local evidence conflicts with the approved plan",
158
+ "proposal": "Inspect the source in the requirement workspace and revise analysis and tasks together",
159
+ }
160
+ return check
161
+
136
162
  summary = {"audit_id": contract["audit_id"], "task_checks": [], "acceptance_checks": []}
163
+ coding_ids = {task["id"] for task in contract.get("tasks") or [] if task.get("type") in ("coding", "fix")}
137
164
  for key in ("task_checks", "acceptance_checks"):
138
165
  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}
166
+ check = redact_check(row)
146
167
  if key == "task_checks":
147
168
  check["task_id"] = row["task_id"]
169
+ if contract.get("version") == 2 and row["task_id"] in coding_ids and isinstance(row.get("implementation_check"), dict):
170
+ check["implementation_check"] = redact_check(row["implementation_check"])
148
171
  else:
149
172
  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
173
  summary[key].append(check)
158
174
  return summary
159
175
 
160
176
 
177
+ def _assess_implementation_contributions(contract: dict, report: object) -> dict:
178
+ original = assess_implementation_audit({**contract, "version": 1}, report)
179
+ if not original["report_valid"]:
180
+ return original
181
+ task_types = {task["id"]: task.get("type") for task in contract.get("tasks") or []}
182
+ verification_ids = [task_id for task_id, task_type in task_types.items() if task_type in ("testing", "verify")]
183
+ verification_statuses = {row["task_id"]: row["status"] for row in report["task_checks"]}
184
+ checks = []
185
+ issues = []
186
+ pending = []
187
+ for row in report["task_checks"]:
188
+ if not isinstance(row, dict) or task_types.get(row.get("task_id")) not in ("coding", "fix"):
189
+ checks.append(row)
190
+ continue
191
+ implementation = row.get("implementation_check")
192
+ if not isinstance(implementation, dict):
193
+ issues.append(f"Approved coding task '{row.get('task_id')}' requires an evidenced implementation_check")
194
+ checks.append(row)
195
+ continue
196
+ check = {"task_id": row.get("task_id"), **{
197
+ key: implementation[key] for key in ("status", "evidence", "description", "files", "plan_revision")
198
+ if key in implementation
199
+ }}
200
+ if row.get("plan_revision") is not None:
201
+ check["plan_revision"] = row["plan_revision"]
202
+ checks.append(check)
203
+ if implementation.get("status") == "covered" and row.get("status") == "gap":
204
+ issues.append(f"Approved task '{row.get('task_id')}' reports a gap despite claiming implementation coverage")
205
+ if implementation.get("status") == "covered" and row.get("status") in ("blocked", "unverified"):
206
+ if not verification_ids:
207
+ issues.append("Runtime acceptance is pending but the approved plan has no verification task")
208
+ pending.extend({
209
+ "task_id": task_id, "status": row.get("status"),
210
+ "description": row.get("description") or "Runtime acceptance remains pending",
211
+ "files": [], "evidence": row.get("evidence") or [],
212
+ } for task_id in verification_ids if verification_statuses.get(task_id) != "covered")
213
+ result = assess_implementation_audit({**contract, "version": 1}, {**report, "task_checks": checks})
214
+ result["report"] = report
215
+ result["issues"].extend(issues)
216
+ result["report_valid"] = not result["issues"]
217
+ result["node_complete"] = result["node_complete"] and not issues
218
+ result["verification_pending"] = sorted({item["task_id"] for item in pending})
219
+ result["findings"].extend(pending)
220
+ for request in original["revision_requests"]:
221
+ if request not in result["revision_requests"]:
222
+ result["revision_requests"].append(request)
223
+ result["followup_task_ids"] = sorted({finding["task_id"] for finding in result["findings"]})
224
+ if issues or pending:
225
+ result["status"] = "incomplete"
226
+ if result["report_valid"] and result["revision_requests"]:
227
+ result["status"] = "plan_revision_required"
228
+ result["node_complete"] = False
229
+ return result
230
+
231
+
161
232
  def assess_implementation_audit(contract: dict, report: object) -> dict:
162
233
  """Validate implementation coverage independently of later runtime acceptance."""
234
+ if contract.get("version") == 2:
235
+ return _assess_implementation_contributions(contract, report)
163
236
  issues: list[str] = []
164
237
  findings: list[dict] = []
165
238
  result = {
166
239
  "audit_id": contract.get("audit_id"), "report_valid": False,
167
240
  "node_complete": False, "status": "incomplete", "issues": issues,
168
241
  "findings": findings, "followup_task_ids": [], "report": report,
169
- "revision_requests": [],
242
+ "revision_requests": [], "blocking_reasons": [],
170
243
  }
171
244
  if (
172
245
  not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
@@ -226,6 +299,10 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
226
299
  issues.append("Implementation audit has invalid acceptance criterion or task references")
227
300
  continue
228
301
  checked_criteria.add(index)
302
+ if len(related) > 1:
303
+ unresolved_contributors = [task_id for task_id in related if checked.get(task_id) != "covered"]
304
+ if unresolved_contributors:
305
+ related = unresolved_contributors
229
306
  if status != "covered":
230
307
  description = row.get("description")
231
308
  paths = row.get("files") or []
@@ -266,6 +343,10 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
266
343
  not issues and checked.get(subject) == "covered"
267
344
  and not any(finding["task_id"] == subject for finding in findings)
268
345
  )
346
+ result["blocking_reasons"] = list(dict.fromkeys(
347
+ f"Approved task '{subject}' is {finding['status']}: {finding['description']}"
348
+ for finding in findings if finding["task_id"] == subject
349
+ ))
269
350
  result["followup_task_ids"] = sorted({finding["task_id"] for finding in findings})
270
351
  result["status"] = "complete" if not issues and not findings else "incomplete"
271
352
  if result["report_valid"] and result["revision_requests"]:
@@ -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.77.0"
1237
+ DAEMON_VERSION = "1.77.2"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -9915,6 +9915,7 @@ class ServerConnection:
9915
9915
  "acceptance_verification": {
9916
9916
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
9917
9917
  },
9918
+ "implementation_audit": {"protocol_version": 2},
9918
9919
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
9919
9920
  },
9920
9921
  "server_url": self.server_url,
@@ -10338,6 +10339,7 @@ class ServerConnection:
10338
10339
  "acceptance_verification": {
10339
10340
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
10340
10341
  },
10342
+ "implementation_audit": {"protocol_version": 2},
10341
10343
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10342
10344
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10343
10345
  },
@@ -13875,6 +13877,12 @@ class RuntimeDaemon:
13875
13877
  issues: list[str] = []
13876
13878
  node_type = task.node_type
13877
13879
  input_data = task.input_data or {}
13880
+ fix_context = input_data.get("fix_context")
13881
+ if node_type == "testing" or (
13882
+ node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
13883
+ and isinstance(fix_context, dict) and fix_context.get("failed_node_type") == "testing"
13884
+ ):
13885
+ node_type = "testing"
13878
13886
  req_type = input_data.get("requirement_type", "feature")
13879
13887
  verification_work_item = bool(input_data.get("verification_work_item"))
13880
13888
 
@@ -13895,7 +13903,9 @@ class RuntimeDaemon:
13895
13903
  result.error = "Approved analysis and task plan require revision before implementation can continue"
13896
13904
  issues.append(result.error)
13897
13905
  elif not audit_result["report_valid"] or not audit_result["node_complete"]:
13898
- issues.extend(audit_result["issues"] or ["The selected approved task still has implementation gaps"])
13906
+ issues.extend(audit_result["issues"] or audit_result.get("blocking_reasons") or [
13907
+ "The selected approved task still has implementation gaps",
13908
+ ])
13899
13909
 
13900
13910
  if node_type == "coding" or (
13901
13911
  node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
@@ -15880,7 +15890,7 @@ class RuntimeDaemon:
15880
15890
  ])
15881
15891
 
15882
15892
  if (
15883
- task.node_type == "testing"
15893
+ agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
15884
15894
  and all(
15885
15895
  issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
15886
15896
  for issue in issues
@@ -16003,7 +16013,7 @@ class RuntimeDaemon:
16003
16013
  *[f"[daemon] {issue}" for issue in remaining],
16004
16014
  ])
16005
16015
  if (
16006
- task.node_type == "testing"
16016
+ agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
16007
16017
  and all(
16008
16018
  issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
16009
16019
  for issue in remaining
@@ -16162,7 +16172,7 @@ class RuntimeDaemon:
16162
16172
  result.metrics["validation_issue_categories"] = sorted(
16163
16173
  {_categorize_validation_issue(str(issue)) for issue in issues}
16164
16174
  )
16165
- if task.node_type != "testing":
16175
+ if not agent_core.is_implementation_testing_task(task.node_type, task.input_data or {}):
16166
16176
  return
16167
16177
  input_data = task.input_data or {}
16168
16178
  context = input_data.get("context") or {}
@@ -17616,7 +17626,9 @@ class RuntimeDaemon:
17616
17626
  before_sha: str,
17617
17627
  ) -> None:
17618
17628
  """Bind generated testing artifacts to the committed testing attempt."""
17619
- if task.workspace_resolution != "cloud_worktree" or task.node_type != "testing":
17629
+ if task.workspace_resolution != "cloud_worktree" or not agent_core.is_implementation_testing_task(
17630
+ task.node_type, task.input_data or {},
17631
+ ):
17620
17632
  return
17621
17633
 
17622
17634
  after_sha = str((result.git or {}).get("commit_sha") or "").strip()
@@ -15,7 +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
+ DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION = 3
19
19
  TEST_COVERAGE_CHECK_KIND = "test_coverage"
20
20
  STATIC_CHECK_KIND = "static_check"
21
21
  TYPE_CHECK_KIND = "type_check"
@@ -804,7 +804,7 @@ def validate_deferred_acceptance_criteria(
804
804
  or policy.get("enforcement") != "enforce"
805
805
  or not isinstance(e2e_policy, dict)
806
806
  or type(contract.get("version")) is not int
807
- or contract["version"] not in (1, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
807
+ or contract["version"] not in (1, 2, DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION)
808
808
  or contract.get("execution_phase") != "verification_final"
809
809
  or not isinstance(snapshot.get("fingerprint"), str)
810
810
  or contract.get("qa_test_level_policy_fingerprint") != snapshot["fingerprint"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.77.0
3
+ Version: 1.77.2
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.77.0"
3
+ version = "1.77.2"
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