forgexa-cli 1.85.1__tar.gz → 1.85.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 (34) hide show
  1. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/agent_core.py +140 -4
  4. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/daemon.py +16 -8
  5. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/pyproject.toml +1 -1
  7. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/README.md +0 -0
  8. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/setup.cfg +0 -0
  25. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.85.1 → forgexa_cli-1.85.2}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.85.1 → forgexa_cli-1.85.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.85.1
3
+ Version: 1.85.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.85.1"
2
+ __version__ = "1.85.2"
@@ -68,6 +68,11 @@ except ImportError:
68
68
  logger = logging.getLogger("agent_core")
69
69
 
70
70
  IMPLEMENTATION_AUDIT_PROTOCOL_VERSION = 3
71
+ IMPLEMENTATION_ENVIRONMENT_BLOCKERS = frozenset({
72
+ "environment_unavailable", "service_not_running", "dependency_unavailable",
73
+ "permission_required", "test_data_unavailable", "target_mismatch",
74
+ })
75
+ TASK_FAILURE_DOMAINS = frozenset({"code_quality", "execution", "environment", "evidence", "delivery", "plan", "verification"})
71
76
  LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS = frozenset({
72
77
  "validation_issues", "validation_issue_details", "timeout_recovery_validation_issues",
73
78
  "timeout_recovery_original_failure", "task_delivery_issue",
@@ -238,6 +243,8 @@ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
238
243
  check = {"status": row["status"], "evidence": evidence}
239
244
  if row["status"] != "covered":
240
245
  check.update(description="Local implementation issue remains unresolved; inspect local task evidence.", files=[])
246
+ if row.get("blocking_reason") in IMPLEMENTATION_ENVIRONMENT_BLOCKERS:
247
+ check["blocking_reason"] = row["blocking_reason"]
241
248
  if row.get("plan_revision"):
242
249
  check["plan_revision"] = {
243
250
  "reason": "Local evidence conflicts with the approved plan",
@@ -281,7 +288,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
281
288
  checks.append(row)
282
289
  continue
283
290
  check = {"task_id": row.get("task_id"), **{
284
- key: implementation[key] for key in ("status", "evidence", "description", "files", "plan_revision")
291
+ key: implementation[key] for key in ("status", "evidence", "description", "files", "plan_revision", "blocking_reason")
285
292
  if key in implementation
286
293
  }}
287
294
  if row.get("plan_revision") is not None and implementation.get("status") != "covered":
@@ -296,6 +303,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
296
303
  "task_id": task_id, "status": row.get("status"),
297
304
  "description": row.get("description") or "Runtime acceptance remains pending",
298
305
  "files": [], "evidence": row.get("evidence") or [],
306
+ **({"blocking_reason": row["blocking_reason"]} if row.get("blocking_reason") else {}),
299
307
  } for task_id in pending_verification_ids)
300
308
  implementation_statuses = {row["task_id"]: row["status"] for row in checks}
301
309
  deferred_contract = contract.get("deferred_acceptance_verification")
@@ -363,6 +371,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
363
371
  pending.extend({
364
372
  "task_id": task_id, "status": row["status"], "description": row["description"],
365
373
  "files": row.get("files") or [], "evidence": row["evidence"],
374
+ **({"blocking_reason": row["blocking_reason"]} if row.get("blocking_reason") else {}),
366
375
  } for task_id in pending_verification_ids)
367
376
  acceptance_checks.append(row)
368
377
  result = assess_implementation_audit({
@@ -443,6 +452,13 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
443
452
  ):
444
453
  issues.append(f"Implementation {kind} check requires an explicit status and concrete evidence")
445
454
  continue
455
+ blocking_reason = row.get("blocking_reason")
456
+ if blocking_reason is not None and (
457
+ not isinstance(blocking_reason, str) or blocking_reason not in IMPLEMENTATION_ENVIRONMENT_BLOCKERS
458
+ or status not in ("blocked", "unverified") or row.get("plan_revision") is not None
459
+ ):
460
+ issues.append("Implementation blocking_reason must identify an allowed unmet environment prerequisite on a blocked or unverified check")
461
+ continue
446
462
  if status == "covered" and row.get("plan_revision") is not None:
447
463
  issues.append(
448
464
  "A covered implementation check cannot request a plan revision; "
@@ -541,6 +557,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
541
557
  "task_id": task_id, "status": status, "description": description,
542
558
  "files": paths, "evidence": evidence,
543
559
  **({"plan_revision": revision} if revision is not None else {}),
560
+ **({"blocking_reason": blocking_reason} if blocking_reason is not None else {}),
544
561
  } for task_id in related)
545
562
  required = expected if contract.get("final_verification") else {subject}
546
563
  if required - checked.keys():
@@ -563,6 +580,57 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
563
580
  return result
564
581
 
565
582
 
583
+ def implementation_audit_failure_domain(contract: dict, assessment: dict) -> str | None:
584
+ if not assessment.get("report_valid"):
585
+ return "evidence"
586
+ if assessment.get("node_complete"):
587
+ return None
588
+ if assessment.get("status") == "plan_revision_required":
589
+ return "plan"
590
+ subject = contract.get("subject_task_id")
591
+ findings = [finding for finding in assessment.get("findings") or [] if finding.get("task_id") == subject]
592
+ if findings and all(
593
+ finding.get("status") in ("blocked", "unverified")
594
+ and finding.get("blocking_reason") in IMPLEMENTATION_ENVIRONMENT_BLOCKERS for finding in findings
595
+ ):
596
+ return "environment"
597
+ role = next((task.get("type") for task in contract.get("tasks") or [] if task.get("id") == subject), None)
598
+ if role in ("coding", "fix") and any(finding.get("status") == "gap" for finding in findings):
599
+ return "code_quality"
600
+ return "verification"
601
+
602
+
603
+ def task_failure_domain(
604
+ failure_code: str | None, *, audit_contract: dict | None = None,
605
+ audit_assessment: dict | None = None, conformance: dict | None = None,
606
+ payload_failed: bool = False,
607
+ ) -> str:
608
+ if failure_code in {
609
+ "planned_task_incomplete", "planned_task_baseline_invalid", "approved_plan_baseline_modified",
610
+ "remediation_scope_violation", "verification_work_item_scope_violation", "uncommitted_workspace_changes",
611
+ "forbidden_branch_merge",
612
+ }:
613
+ return "delivery"
614
+ if failure_code == "implementation_plan_revision_required":
615
+ return "plan"
616
+ if payload_failed:
617
+ return "execution"
618
+ if failure_code == "implementation_audit_incomplete":
619
+ return implementation_audit_failure_domain(audit_contract or {}, audit_assessment or {}) or "verification"
620
+ if failure_code in {"approved_plan_deviation", "approved_plan_conformance_unverified"}:
621
+ assessment = conformance or {}
622
+ if assessment.get("reason") == "approved_plan_review_unavailable":
623
+ return "execution"
624
+ if assessment.get("reason") or assessment.get("diagnostic_code"):
625
+ return "evidence"
626
+ return "code_quality" if assessment.get("status") == "deviation" else "verification"
627
+ if failure_code == "acceptance_criteria_uncovered":
628
+ return "verification"
629
+ if failure_code in {"validation_unavailable", "validation_retry_exhausted"}:
630
+ return "evidence"
631
+ return "execution"
632
+
633
+
566
634
  _WORKSPACE_INSTRUCTION_PATHS = (
567
635
  "AGENTS.md",
568
636
  "CLAUDE.md",
@@ -1889,7 +1957,7 @@ def has_agent_token_usage(*sources: object) -> bool:
1889
1957
  return False
1890
1958
 
1891
1959
 
1892
- def _extract_output_signals(text: str) -> dict[str, Any]:
1960
+ def _extract_output_signals(text: str, usage_source: object = None) -> dict[str, Any]:
1893
1961
  """Parse stdout/stderr-like streams for success and failure signals.
1894
1962
 
1895
1963
  Uses structural signal detection — NOT keyword matching on result text.
@@ -1933,6 +2001,8 @@ def _extract_output_signals(text: str) -> dict[str, Any]:
1933
2001
 
1934
2002
  if ev_type == "turn.completed":
1935
2003
  has_turn_completed = True
2004
+ has_turn_failed = False
2005
+ error_messages.clear()
1936
2006
  elif ev_type == "turn.failed":
1937
2007
  has_turn_failed = True
1938
2008
  err = data.get("error") or {}
@@ -1962,10 +2032,18 @@ def _extract_output_signals(text: str) -> dict[str, Any]:
1962
2032
  # Claude result format: {"type":"result","result":"...","is_error":false,...}
1963
2033
  if "exitCode" in data:
1964
2034
  # Copilot JSONL result
1965
- exit_code = int(data.get("exitCode") or 0)
2035
+ raw_exit_code = data["exitCode"]
2036
+ if type(raw_exit_code) is not int and not (
2037
+ isinstance(raw_exit_code, str) and re.fullmatch(r"[+-]?[0-9]+", raw_exit_code)
2038
+ ):
2039
+ error_messages.append("Agent result has an invalid exitCode")
2040
+ continue
2041
+ exit_code = int(raw_exit_code)
1966
2042
  if exit_code == 0:
1967
2043
  has_result = True
1968
2044
  has_meaningful_content = True
2045
+ has_turn_failed = False
2046
+ error_messages.clear()
1969
2047
  else:
1970
2048
  error_messages.append(f"Copilot exited with code {exit_code}")
1971
2049
  else:
@@ -1996,12 +2074,14 @@ def _extract_output_signals(text: str) -> dict[str, Any]:
1996
2074
  # events appeared, the CLI never made an API call. The result
1997
2075
  # text is a CLI-level error (e.g. "API Error: Connection error.")
1998
2076
  # rather than the agent's actual work output.
1999
- no_api_call = not has_agent_token_usage(data) and not has_assistant_events
2077
+ no_api_call = not has_agent_token_usage(data, usage_source) and not has_assistant_events
2000
2078
  if no_api_call and result_text:
2001
2079
  error_messages.append(result_text)
2002
2080
  else:
2003
2081
  has_result = True
2004
2082
  has_meaningful_content = True
2083
+ has_turn_failed = False
2084
+ error_messages.clear()
2005
2085
  elif ev_type == "session.error":
2006
2086
  err = data.get("data") or {}
2007
2087
  if isinstance(err, dict):
@@ -4988,6 +5068,62 @@ def is_copilot_model_discovery_auth_error(*messages: str | None) -> bool:
4988
5068
  )
4989
5069
 
4990
5070
 
5071
+ # Transport failures a plain re-run can recover from. 421 Misdirected Request
5072
+ # is deliberately absent: it keeps its own recovery (retry without token env
5073
+ # vars) because it has historically indicated a credential-routing mismatch,
5074
+ # not a network blip.
5075
+ COPILOT_TRANSIENT_TRANSPORT_ERROR_PATTERNS = (
5076
+ "timed out",
5077
+ "timeout",
5078
+ "unexpected eof",
5079
+ "error sending request",
5080
+ "connection reset",
5081
+ "connection refused",
5082
+ "connection timed out",
5083
+ "connection error",
5084
+ "connection closed",
5085
+ "connection terminated",
5086
+ "network is unreachable",
5087
+ "no such host",
5088
+ "temporary failure in name resolution",
5089
+ )
5090
+
5091
+ COPILOT_STABLE_AUTH_ERROR_PATTERNS = (
5092
+ "unauthorized",
5093
+ "forbidden",
5094
+ "http 401",
5095
+ "http 403",
5096
+ "not logged in",
5097
+ "authentication required",
5098
+ "invalid api key",
5099
+ "your github token may be invalid",
5100
+ "rejected the cli github credential",
5101
+ )
5102
+
5103
+
5104
+ def is_copilot_model_discovery_transient_transport_error(*messages: str | None) -> bool:
5105
+ """Return whether Copilot failed model discovery on a transient transport error.
5106
+
5107
+ Matches connect timeouts, unexpected EOF, resets and DNS hiccups during
5108
+ model discovery — the class of failures observed as intermittent on
5109
+ unstable routes (a daemon-side rerun typically succeeds, and the same
5110
+ login works interactively), as opposed to stable auth/config breakage.
5111
+ Copilot appends its remediation boilerplate ("To resolve this, ... run
5112
+ '/login' ...") to every discovery failure, so auth patterns are only
5113
+ trusted when they appear before that boilerplate.
5114
+ """
5115
+ detail = " ".join(str(message or "").lower() for message in messages)
5116
+ detail = detail.partition("to resolve this")[0]
5117
+ if not (
5118
+ "retrieve the list of available models" in detail
5119
+ or "failed to load models" in detail
5120
+ ):
5121
+ return False
5122
+ if any(pattern in detail for pattern in COPILOT_STABLE_AUTH_ERROR_PATTERNS):
5123
+ return False
5124
+ return any(pattern in detail for pattern in COPILOT_TRANSIENT_TRANSPORT_ERROR_PATTERNS)
5125
+
5126
+
4991
5127
  def is_runtime_resource_exhaustion_error(*messages: str | None) -> bool:
4992
5128
  """Return whether the OS refused an agent process, thread, or file watch."""
4993
5129
  detail = " ".join(str(message or "").lower() for message in messages)
@@ -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.85.1"
1237
+ DAEMON_VERSION = "1.85.2"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -6439,8 +6439,8 @@ class ProcessManager:
6439
6439
  int(result.metrics.get("token_input", 0) or 0)
6440
6440
  + int(result.metrics.get("token_output", 0) or 0)
6441
6441
  ) > 0
6442
- signals = ProcessManager._extract_output_signals(
6443
- "\n".join(part for part in (stdout, stderr) if part)
6442
+ signals = agent_core._extract_output_signals(
6443
+ "\n".join(part for part in (stdout, stderr) if part), result.metrics,
6444
6444
  )
6445
6445
  if not has_token_usage and ProcessManager._should_scan_short_success_stdout(stdout, signals):
6446
6446
  error_channels = "\n".join(filter(None, [error_channels, stdout.strip()]))
@@ -8484,15 +8484,18 @@ class ProcessManager:
8484
8484
 
8485
8485
  model_discovery_detail = "\n".join([*structured_errors, stderr]).lower()
8486
8486
  if (
8487
- "could not retrieve the list of available models" in model_discovery_detail
8488
- and "timed out" in model_discovery_detail
8487
+ agent_core.is_copilot_model_discovery_transient_transport_error(
8488
+ *structured_errors, stderr,
8489
+ )
8490
+ and "misdirected request" not in model_discovery_detail
8489
8491
  and not _retry_model_discovery_timeout
8490
8492
  and not (copilot_exit == 0 and returncode == 0)
8491
8493
  and not signals["has_meaningful_content"]
8492
8494
  and not self._copilot_called_any_tools(stdout)
8493
8495
  ):
8494
8496
  logger.warning(
8495
- "Copilot model discovery timed out before work for task %s; "
8497
+ "Copilot model discovery hit a transient transport error "
8498
+ "(timeout / connect reset) before work for task %s; "
8496
8499
  "retrying once with the same credentials and configuration",
8497
8500
  task_id,
8498
8501
  )
@@ -14466,6 +14469,7 @@ class RuntimeDaemon:
14466
14469
  audit_contract = input_data.get("implementation_audit_contract")
14467
14470
  result.metrics.pop("implementation_audit_node_complete", None)
14468
14471
  result.metrics.pop("implementation_audit_subject_blocked", None)
14472
+ result.metrics.pop("failure_domain", None)
14469
14473
  if isinstance(audit_contract, dict):
14470
14474
  agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
14471
14475
  audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
@@ -14476,13 +14480,17 @@ class RuntimeDaemon:
14476
14480
  if isinstance(audit_report, dict):
14477
14481
  result.observations.append({"type": "implementation_audit", "data": audit_report})
14478
14482
  audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
14483
+ failure_domain = agent_core.implementation_audit_failure_domain(audit_contract, audit_result)
14484
+ if failure_domain is not None:
14485
+ result.metrics["failure_domain"] = failure_domain
14479
14486
  result.metrics["implementation_audit_node_complete"] = bool(
14480
14487
  audit_result["report_valid"] and audit_result["node_complete"]
14481
14488
  )
14482
14489
  result.metrics["implementation_audit_subject_blocked"] = bool(
14483
14490
  audit_result["report_valid"] and not audit_result["node_complete"]
14484
- and any(finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
14485
- for finding in audit_result.get("findings") or [])
14491
+ and (failure_domain == "environment" or any(
14492
+ finding.get("task_id") == audit_contract.get("subject_task_id") and finding.get("status") == "blocked"
14493
+ for finding in audit_result.get("findings") or []))
14486
14494
  )
14487
14495
  if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
14488
14496
  result.status = "failed"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.85.1
3
+ Version: 1.85.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.85.1"
3
+ version = "1.85.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