forgexa-cli 1.86.1__tar.gz → 1.86.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 (34) hide show
  1. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/agent_core.py +73 -4
  4. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/daemon.py +54 -8
  5. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/pyproject.toml +1 -1
  7. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/README.md +0 -0
  8. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/setup.cfg +0 -0
  25. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.86.1 → forgexa_cli-1.86.3}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.86.1 → forgexa_cli-1.86.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.86.1
3
+ Version: 1.86.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.86.1"
2
+ __version__ = "1.86.3"
@@ -309,6 +309,14 @@ def requires_task_delivery_baseline(node_type: str, task_input: dict) -> bool:
309
309
 
310
310
 
311
311
  def extract_implementation_audit(text: str) -> dict | None:
312
+ return extract_agent_report(text, "implementation_audit")
313
+
314
+
315
+ def extract_execution_delivery(text: str) -> dict | None:
316
+ return extract_agent_report(text, "execution_delivery")
317
+
318
+
319
+ def extract_agent_report(text: str, field: str) -> dict | None:
312
320
  """Read the last complete audit envelope from mixed assistant output."""
313
321
  decoder = json.JSONDecoder()
314
322
  offset = 0
@@ -323,11 +331,11 @@ def extract_implementation_audit(text: str) -> dict | None:
323
331
  except json.JSONDecodeError:
324
332
  offset = start + 1
325
333
  continue
326
- if isinstance(value, dict) and "implementation_audit" in value:
327
- report = value["implementation_audit"] if isinstance(value["implementation_audit"], dict) else None
334
+ if isinstance(value, dict) and field in value:
335
+ report = value[field] if isinstance(value[field], dict) else None
328
336
  report_end = end
329
337
  offset = end
330
- if '"implementation_audit"' in text[report_end:]:
338
+ if json.dumps(field) in text[report_end:]:
331
339
  return None
332
340
  return report
333
341
 
@@ -774,9 +782,56 @@ def lock_provenance_projection(path: str, source: str) -> str | None:
774
782
  }, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
775
783
 
776
784
 
777
- def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None) -> dict:
785
+ def implementation_delivery_contract(node_type: str, task_input: dict, *, supported: bool, graph_type: str = "execution") -> dict | None:
786
+ if supported is not True or not isinstance(node_type, str) or not isinstance(graph_type, str) or node_type not in {"design", "coding", "testing", "review", "fix"} or graph_type not in {"execution", "implementation", "fix"}:
787
+ return None
788
+ if task_input.get("verification_work_item") or task_input.get("execution_actor") == "human":
789
+ return None
790
+ recovery = task_input.get("verification_recovery")
791
+ if isinstance(recovery, dict) and recovery.get("mode") == "review_only" and not task_input.get("implementation_delivery"):
792
+ return None
793
+ baseline = task_input.get("approved_plan_baseline")
794
+ if not isinstance(baseline, dict) or assess_approved_plan_conformance(baseline, None).get("reason") == "approved_plan_baseline_invalid":
795
+ return None
796
+ task_id = (baseline.get("task") or {}).get("id")
797
+ if not isinstance(task_id, str) or not task_id:
798
+ return None
799
+ contract = {"version": 1, "mode": "execution_and_artifacts", "node_type": node_type, "graph_type": graph_type,
800
+ "task_id": task_id, "baseline_fingerprint": baseline.get("fingerprint"),
801
+ "qa_policy": task_input.get("qa_test_level_policy_snapshot"), "change_scope": task_input.get("change_scope")}
802
+ contract["fingerprint"] = hashlib.sha256(json.dumps(contract, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
803
+ return contract
804
+
805
+
806
+ def uses_controlled_delivery(task_input: dict) -> bool:
807
+ contract = task_input.get("implementation_delivery")
808
+ if not isinstance(contract, dict):
809
+ return False
810
+ expected = implementation_delivery_contract(contract.get("node_type"), task_input, supported=True, graph_type=contract.get("graph_type"))
811
+ return expected is not None and contract == expected
812
+
813
+
814
+ def assess_execution_delivery(contract: dict, report: object) -> dict:
815
+ if not isinstance(report, dict) or type(report.get("version")) is not int or report["version"] != 1:
816
+ return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration is missing or invalid"]}
817
+ if set(report) != {"version", "contract_fingerprint", "status"} or report.get("contract_fingerprint") != contract.get("fingerprint") or not isinstance(report.get("status"), str) or report["status"] not in {"complete", "partial", "blocked"}:
818
+ return {"report_valid": False, "status": "unknown", "issues": ["Execution delivery declaration does not match the current contract"]}
819
+ return {"report_valid": True, "status": report["status"], "issues": []}
820
+
821
+
822
+ def redact_execution_delivery(report: object) -> dict | None:
823
+ if not isinstance(report, dict):
824
+ return None
825
+ return {"version": report.get("version") if type(report.get("version")) is int else 0,
826
+ "contract_fingerprint": report.get("contract_fingerprint") if isinstance(report.get("contract_fingerprint"), str) and re.fullmatch(r"[0-9a-f]{64}", report["contract_fingerprint"]) else "",
827
+ "status": report.get("status") if report.get("status") in ("complete", "partial", "blocked") else "unknown"}
828
+
829
+
830
+ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None, policy_supported: bool = True) -> dict:
778
831
  baseline = task_input.get("approved_plan_baseline")
779
832
  reasons = []
833
+ if policy_supported is not True:
834
+ reasons.append("runtime_policy_upgrade_required")
780
835
  fingerprint = None
781
836
  if isinstance(baseline, dict):
782
837
  expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
@@ -808,6 +863,8 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
808
863
  if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
809
864
  reasons.append("scope_unknown")
810
865
  decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
866
+ if uses_controlled_delivery(task_input):
867
+ decision.update(mode="advisory", decision_scope="controlled_delivery", delivery_contract=task_input["implementation_delivery"]["fingerprint"])
811
868
  decision["fingerprint"] = hashlib.sha256(json.dumps({
812
869
  **decision, "baseline_fingerprint": fingerprint,
813
870
  "qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
@@ -817,6 +874,8 @@ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[
817
874
 
818
875
 
819
876
  def review_blocks_completion(policy: dict, assessment: dict) -> bool:
877
+ if policy.get("decision_scope") == "controlled_delivery":
878
+ return False
820
879
  return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
821
880
  policy.get("mode") != "advisory" or assessment.get("status") == "deviation"
822
881
  )
@@ -2166,6 +2225,7 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
2166
2225
  contains words like 'rate limit', 'connection error', 'API error', etc.
2167
2226
  (e.g. implementing a rate-limiter feature, or documenting error codes).
2168
2227
  """
2228
+ terminal_status = None
2169
2229
  has_turn_completed = False
2170
2230
  has_turn_failed = False
2171
2231
  has_result = False
@@ -2191,10 +2251,12 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
2191
2251
  ev_type = str(data.get("type", ""))
2192
2252
 
2193
2253
  if ev_type == "turn.completed":
2254
+ terminal_status = "success"
2194
2255
  has_turn_completed = True
2195
2256
  has_turn_failed = False
2196
2257
  error_messages.clear()
2197
2258
  elif ev_type == "turn.failed":
2259
+ terminal_status = "failed"
2198
2260
  has_turn_failed = True
2199
2261
  err = data.get("error") or {}
2200
2262
  if isinstance(err, dict):
@@ -2227,19 +2289,23 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
2227
2289
  if type(raw_exit_code) is not int and not (
2228
2290
  isinstance(raw_exit_code, str) and re.fullmatch(r"[+-]?[0-9]+", raw_exit_code)
2229
2291
  ):
2292
+ terminal_status = "failed"
2230
2293
  error_messages.append("Agent result has an invalid exitCode")
2231
2294
  continue
2232
2295
  exit_code = int(raw_exit_code)
2233
2296
  if exit_code == 0:
2297
+ terminal_status = "success"
2234
2298
  has_result = True
2235
2299
  has_meaningful_content = True
2236
2300
  has_turn_failed = False
2237
2301
  error_messages.clear()
2238
2302
  else:
2303
+ terminal_status = "failed"
2239
2304
  error_messages.append(f"Copilot exited with code {exit_code}")
2240
2305
  else:
2241
2306
  result_text = str(data.get("result", "") or "")
2242
2307
  if data.get("is_error"):
2308
+ terminal_status = "failed"
2243
2309
  raw_errors = data.get("errors")
2244
2310
  if isinstance(raw_errors, list):
2245
2311
  err_text = "; ".join(
@@ -2267,8 +2333,10 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
2267
2333
  # rather than the agent's actual work output.
2268
2334
  no_api_call = not has_agent_token_usage(data, usage_source) and not has_assistant_events
2269
2335
  if no_api_call and result_text:
2336
+ terminal_status = "failed"
2270
2337
  error_messages.append(result_text)
2271
2338
  else:
2339
+ terminal_status = "success"
2272
2340
  has_result = True
2273
2341
  has_meaningful_content = True
2274
2342
  has_turn_failed = False
@@ -2312,6 +2380,7 @@ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str,
2312
2380
  has_meaningful_content = True
2313
2381
 
2314
2382
  return {
2383
+ "terminal_status": "failed" if error_messages or has_turn_failed else terminal_status,
2315
2384
  "has_turn_completed": has_turn_completed,
2316
2385
  "has_turn_failed": has_turn_failed,
2317
2386
  "has_result": has_result,
@@ -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.86.1"
1237
+ DAEMON_VERSION = "1.86.3"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -6502,6 +6502,8 @@ class ProcessManager:
6502
6502
  signals = agent_core._extract_output_signals(
6503
6503
  "\n".join(part for part in (stdout, stderr) if part), result.metrics,
6504
6504
  )
6505
+ if result.exit_code == 0 and not error and (signals.get("terminal_status") == "success" or result.has_verified_completion is True and signals.get("terminal_status") != "failed"):
6506
+ error_channels = ""
6505
6507
  if not has_token_usage and ProcessManager._should_scan_short_success_stdout(stdout, signals):
6506
6508
  error_channels = "\n".join(filter(None, [error_channels, stdout.strip()]))
6507
6509
  # Always include structured error messages parsed from JSONL stream events
@@ -10219,6 +10221,9 @@ class ServerConnection:
10219
10221
  },
10220
10222
  "implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
10221
10223
  "task_delivery": {"protocol_version": 1},
10224
+ "verification_recovery": {"protocol_version": 1},
10225
+ "task_review_policy": {"protocol_version": 1},
10226
+ "implementation_delivery": {"protocol_version": 1},
10222
10227
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10223
10228
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10224
10229
  },
@@ -10645,6 +10650,9 @@ class ServerConnection:
10645
10650
  },
10646
10651
  "implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
10647
10652
  "task_delivery": {"protocol_version": 1},
10653
+ "verification_recovery": {"protocol_version": 1},
10654
+ "task_review_policy": {"protocol_version": 1},
10655
+ "implementation_delivery": {"protocol_version": 1},
10648
10656
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10649
10657
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10650
10658
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
@@ -11434,6 +11442,20 @@ class RuntimeDaemon:
11434
11442
  if managed_policy is not _MANAGED_WSL_POLICY:
11435
11443
  raise ValueError("wsl_runtime_entry_not_admitted")
11436
11444
  self.managed_policy = managed_policy
11445
+ # Ops-tunable staging budget. Keep aligned with the server's
11446
+ # MAX_ANALYSIS_ATTACHMENT_COUNT: the server only admits analysis
11447
+ # selections within its own budget, so a lower value here would
11448
+ # reject tasks the server considered valid.
11449
+ try:
11450
+ self._TASK_ATTACHMENT_MAX_COUNT = max(
11451
+ 1, int(os.environ.get("DAEMON_TASK_ATTACHMENT_MAX_COUNT", self._TASK_ATTACHMENT_MAX_COUNT))
11452
+ )
11453
+ except ValueError:
11454
+ logger.warning(
11455
+ "Ignoring invalid DAEMON_TASK_ATTACHMENT_MAX_COUNT=%r; using %d",
11456
+ os.environ.get("DAEMON_TASK_ATTACHMENT_MAX_COUNT"),
11457
+ self._TASK_ATTACHMENT_MAX_COUNT,
11458
+ )
11437
11459
  # Compute stable hardware fingerprint from machine-id / MAC address.
11438
11460
  # This allows the server to identify the same physical machine even when
11439
11461
  # its IP address changes (e.g., DHCP reassignment, home vs. office).
@@ -13944,7 +13966,7 @@ class RuntimeDaemon:
13944
13966
 
13945
13967
  review_policy = agent_core.task_review_policy(task.node_type, task.input_data or {}, changed_files=result.files_changed)
13946
13968
  result.metrics["task_review_policy"] = review_policy
13947
- if result.status == "success" and review_policy["mode"] == "required" and (task.critic_review_required or isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13969
+ if result.status == "success" and (task.critic_review_required or review_policy["mode"] == "required" and isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13948
13970
  await self._collect_task_delivery_git(
13949
13971
  task, workspace_path, result, attempt_before_sha=node_before_sha,
13950
13972
  )
@@ -14615,6 +14637,16 @@ class RuntimeDaemon:
14615
14637
  issues: list[str] = []
14616
14638
  node_type = task.node_type
14617
14639
  input_data = task.input_data or {}
14640
+ controlled_delivery = agent_core.uses_controlled_delivery(input_data)
14641
+ if controlled_delivery:
14642
+ report = agent_core.extract_execution_delivery(_daemon_extract_agent_output(str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or ""), result.stdout))
14643
+ delivery = agent_core.assess_execution_delivery(input_data["implementation_delivery"], report)
14644
+ result.metrics["execution_delivery"] = agent_core.redact_execution_delivery(report)
14645
+ result.metrics["execution_delivery_valid"] = delivery["report_valid"]
14646
+ if not delivery["report_valid"]:
14647
+ issues.extend(_OutputValidationIssue("validation_unavailable", message) for message in delivery["issues"])
14648
+ elif delivery["status"] != "complete":
14649
+ issues.append(_OutputValidationIssue("planned_task_incomplete", "Agent explicitly reported incomplete execution delivery"))
14618
14650
  fix_context = input_data.get("fix_context")
14619
14651
  if node_type == "testing" or (
14620
14652
  node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
@@ -14647,18 +14679,18 @@ class RuntimeDaemon:
14647
14679
  result.observations.append({"type": "implementation_audit", "data": audit_report})
14648
14680
  audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
14649
14681
  failure_domain = agent_core.implementation_audit_failure_domain(audit_contract, audit_result)
14650
- if failure_domain is not None:
14682
+ if failure_domain is not None and not controlled_delivery:
14651
14683
  result.metrics["failure_domain"] = failure_domain
14652
14684
  result.metrics["implementation_audit_node_complete"] = bool(
14653
- audit_result["report_valid"] and audit_result["node_complete"]
14685
+ controlled_delivery or audit_result["report_valid"] and audit_result["node_complete"]
14654
14686
  )
14655
14687
  result.metrics["implementation_audit_subject_blocked"] = bool(
14656
- audit_result["report_valid"] and not audit_result["node_complete"]
14688
+ not controlled_delivery and audit_result["report_valid"] and not audit_result["node_complete"]
14657
14689
  and (failure_domain == "environment" or any(
14658
14690
  finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
14659
14691
  for finding in audit_result.get("findings") or []))
14660
14692
  )
14661
- if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
14693
+ if not controlled_delivery and audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
14662
14694
  result.status = "failed"
14663
14695
  result.failure_code = "implementation_plan_revision_required"
14664
14696
  requests = audit_result.get("blocking_revision_requests", audit_result["revision_requests"])
@@ -14666,7 +14698,7 @@ class RuntimeDaemon:
14666
14698
  f"{request['task_id']}: {request['reason']}" for request in requests[:3]
14667
14699
  )
14668
14700
  issues.append(_OutputValidationIssue(result.failure_code, result.error))
14669
- elif not audit_result["report_valid"] or not audit_result["node_complete"]:
14701
+ elif not controlled_delivery and (not audit_result["report_valid"] or not audit_result["node_complete"]):
14670
14702
  issues.extend(_OutputValidationIssue("implementation_audit_incomplete", issue) for issue in (
14671
14703
  audit_result["issues"] or audit_result.get("blocking_reasons") or [
14672
14704
  "The selected approved task still has implementation gaps",
@@ -15207,7 +15239,9 @@ class RuntimeDaemon:
15207
15239
  ac for ac in ac_list
15208
15240
  if ac["status"] != "covered" and ac["id"] not in pending
15209
15241
  ]
15210
- if uncovered:
15242
+ if uncovered and controlled_delivery:
15243
+ result.metrics["business_acceptance"] = {"status": "pending_human_qa", "criteria": [ac["id"] for ac in uncovered]}
15244
+ elif uncovered:
15211
15245
  ids = ", ".join(ac["id"] for ac in uncovered[:20])
15212
15246
  if len(uncovered) > 20:
15213
15247
  ids += f" (+{len(uncovered) - 20} more)"
@@ -16682,6 +16716,18 @@ class RuntimeDaemon:
16682
16716
  *[f"[daemon] {issue}" for issue in issues],
16683
16717
  ])
16684
16718
 
16719
+ if (
16720
+ agent_core.uses_controlled_delivery(task.input_data or {})
16721
+ and result.metrics.get("execution_delivery_valid") is True
16722
+ and (result.metrics.get("execution_delivery") or {}).get("status") in {"partial", "blocked"}
16723
+ ):
16724
+ result.status = "failed"
16725
+ result.failure_code = "planned_task_incomplete"
16726
+ result.error = "Agent explicitly reported incomplete execution delivery; preserve work and resolve prerequisites before an authorized retry"
16727
+ _record_output_validation_issues(result, issues)
16728
+ self._record_testing_telemetry(workspace_path, task, result, retries_used=attempt)
16729
+ return result
16730
+
16685
16731
  if (
16686
16732
  result.metrics.get("failure_domain") == "environment" and all(
16687
16733
  issue.code in {"implementation_audit_incomplete", "acceptance_criteria_uncovered"} for issue in issues
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.86.1
3
+ Version: 1.86.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.86.1"
3
+ version = "1.86.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