forgexa-cli 1.47.4__tar.gz → 1.48.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 (28) hide show
  1. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/agent_core.py +62 -2
  4. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/daemon.py +329 -2
  5. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/pyproject.toml +1 -1
  7. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/README.md +0 -0
  8. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  15. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  16. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  17. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli.egg-info/requires.txt +0 -0
  18. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  19. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/setup.cfg +0 -0
  20. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_auth_and_runtime_commands.py +0 -0
  21. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_autoupgrade.py +0 -0
  22. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_check_command.py +0 -0
  23. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  24. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_local_bind_commands.py +0 -0
  25. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_runtime_credentials.py +0 -0
  26. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_session_credentials.py +0 -0
  27. {forgexa_cli-1.47.4 → forgexa_cli-1.48.0}/tests/test_silent_install.py +0 -0
  28. {forgexa_cli-1.47.4 → forgexa_cli-1.48.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.47.4
3
+ Version: 1.48.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.47.4"
2
+ __version__ = "1.48.0"
@@ -1616,14 +1616,74 @@ def build_opencode_command(
1616
1616
  return cmd
1617
1617
 
1618
1618
 
1619
- def opencode_base_env(env: dict[str, str], *, data_home: str | Path) -> dict[str, str]:
1620
- """Isolate each opencode run in its own XDG data directory.
1619
+ def opencode_base_env(
1620
+ env: dict[str, str], *, data_home: str | Path, require_windows_shell: bool = False,
1621
+ ) -> dict[str, str]:
1622
+ """Isolate each OpenCode run in its own XDG data directory.
1621
1623
 
1622
1624
  Prevents concurrent opencode processes from racing on the shared SQLite
1623
1625
  WAL file ('Failed to run the query PRAGMA wal_checkpoint(PASSIVE)').
1624
1626
  """
1625
1627
  env = dict(env)
1626
1628
  env["XDG_DATA_HOME"] = str(data_home)
1629
+ return configure_opencode_windows_shell(env, required=require_windows_shell)
1630
+
1631
+
1632
+ def _find_opencode_windows_shell(env: dict[str, str]) -> str | None:
1633
+ configured = env.get("OPENCODE_GIT_BASH_PATH", "").strip()
1634
+ if configured and Path(configured).is_file():
1635
+ return configured
1636
+
1637
+ git = shutil.which("git", path=env.get("PATH"))
1638
+ if git:
1639
+ git_path = Path(git)
1640
+ for git_root in (git_path.parent.parent, git_path.parent.parent.parent):
1641
+ git_bash = git_root / "bin" / "bash.exe"
1642
+ if git_bash.is_file():
1643
+ return str(git_bash)
1644
+
1645
+ return shutil.which("pwsh", path=env.get("PATH"))
1646
+
1647
+
1648
+ def configure_opencode_windows_shell(
1649
+ env: dict[str, str], *, required: bool = False,
1650
+ ) -> dict[str, str]:
1651
+ """Force OpenCode off classic Windows PowerShell for agent tool calls."""
1652
+ env = dict(env)
1653
+ if sys.platform != "win32":
1654
+ return env
1655
+
1656
+ shell = _find_opencode_windows_shell(env)
1657
+ if not shell:
1658
+ message = (
1659
+ "OpenCode on Windows requires Git Bash or PowerShell 7 (pwsh) so "
1660
+ "agent tool output is UTF-8. Install Git for Windows or PowerShell 7, "
1661
+ "then retry or select another agent."
1662
+ )
1663
+ if required:
1664
+ raise RuntimeError(message)
1665
+ logger.warning(message)
1666
+ return env
1667
+
1668
+ raw_config = env.get("OPENCODE_CONFIG_CONTENT", "").strip()
1669
+ config: dict[str, Any] = {}
1670
+ if raw_config:
1671
+ try:
1672
+ parsed = json.loads(raw_config)
1673
+ except json.JSONDecodeError as exc:
1674
+ raise RuntimeError(
1675
+ "OPENCODE_CONFIG_CONTENT must be a JSON object to configure "
1676
+ "OpenCode's Windows tool shell."
1677
+ ) from exc
1678
+ if not isinstance(parsed, dict):
1679
+ raise RuntimeError(
1680
+ "OPENCODE_CONFIG_CONTENT must be a JSON object to configure "
1681
+ "OpenCode's Windows tool shell."
1682
+ )
1683
+ config = parsed
1684
+
1685
+ config["shell"] = shell
1686
+ env["OPENCODE_CONFIG_CONTENT"] = json.dumps(config, ensure_ascii=False)
1627
1687
  return env
1628
1688
 
1629
1689
 
@@ -1120,7 +1120,7 @@ except (ImportError, ModuleNotFoundError):
1120
1120
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1121
1121
  # Kept in sync with pyproject.toml version via bump-version.sh.
1122
1122
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1123
- DAEMON_VERSION = "1.47.4"
1123
+ DAEMON_VERSION = "1.48.0"
1124
1124
 
1125
1125
 
1126
1126
  def _detect_client_type() -> str:
@@ -1460,6 +1460,7 @@ class TaskInfo:
1460
1460
  execution_attempt_id: str | None = None
1461
1461
  runtime_evidence_plan: dict | None = None
1462
1462
  local_evidence_upload: bool = False
1463
+ critic_review_required: bool = False
1463
1464
 
1464
1465
 
1465
1466
  @dataclass
@@ -1490,6 +1491,105 @@ class TaskResult:
1490
1491
  has_verified_completion: bool = False
1491
1492
  execution_attempt_id: str | None = None
1492
1493
  evidence_summary: dict | None = None
1494
+ critic_review: dict | None = None
1495
+
1496
+
1497
+ _RUNTIME_CRITIC_TIMEOUT_SECONDS = 120
1498
+ _RUNTIME_CRITIC_DIFF_MAX_CHARS = 30_000
1499
+ _RUNTIME_CRITIC_OUTPUT_MAX_CHARS = 20_000
1500
+ _RUNTIME_CRITIC_SYSTEM_PROMPT = """You are the Critic Agent for a software delivery gate.
1501
+ Review only the supplied task context and code diff. You are running in an
1502
+ isolated review capsule, not the project workspace. Do not write or modify files,
1503
+ run mutating commands, create commits, or make network changes.
1504
+
1505
+ Return exactly one JSON object with this schema:
1506
+ {
1507
+ "verdict": "approve" | "request_changes" | "reject",
1508
+ "score": 0.0 to 1.0,
1509
+ "summary": "brief assessment",
1510
+ "dimension_scores": {
1511
+ "correctness": 0.0 to 1.0,
1512
+ "security": 0.0 to 1.0,
1513
+ "performance": 0.0 to 1.0,
1514
+ "style": 0.0 to 1.0,
1515
+ "testing": 0.0 to 1.0,
1516
+ "error_handling": 0.0 to 1.0
1517
+ },
1518
+ "issues": [{
1519
+ "severity": "critical" | "major" | "minor" | "info",
1520
+ "category": "security" | "performance" | "correctness" | "style" | "testing" | "error_handling",
1521
+ "title": "short title",
1522
+ "file": "repository-relative changed file path",
1523
+ "line": 1,
1524
+ "end_line": 1,
1525
+ "message": "actionable defect description",
1526
+ "evidence": "concrete basis from the supplied diff",
1527
+ "impact": "consequence if unresolved",
1528
+ "suggestion": "minimal fix direction",
1529
+ "verification": "focused verification step"
1530
+ }]
1531
+ }
1532
+
1533
+ Only report findings grounded in the supplied diff. Return an empty issues array
1534
+ when no actionable defects are found."""
1535
+
1536
+
1537
+ def _runtime_critic_test_summary(observations: list[dict]) -> str:
1538
+ for observation in reversed(observations):
1539
+ if not isinstance(observation, dict):
1540
+ continue
1541
+ payload = observation.get("content")
1542
+ if not isinstance(payload, dict):
1543
+ payload = observation
1544
+ if payload.get("type") != "test_results":
1545
+ continue
1546
+ summary = payload.get("summary")
1547
+ if isinstance(summary, str) and summary.strip():
1548
+ return summary.strip()[:5000]
1549
+ return ""
1550
+
1551
+
1552
+ def _build_runtime_critic_prompt(
1553
+ task: TaskInfo,
1554
+ *,
1555
+ changed_files: list[str],
1556
+ diff: str,
1557
+ test_summary: str,
1558
+ ) -> str:
1559
+ work_item = task.work_item or {}
1560
+ title = work_item.get("title") if isinstance(work_item, dict) else ""
1561
+ title = title if isinstance(title, str) and title.strip() else "Unnamed task"
1562
+ parts = [
1563
+ _RUNTIME_CRITIC_SYSTEM_PROMPT,
1564
+ "",
1565
+ f"Task: {title[:500]}",
1566
+ f"Task type: {task.node_type or 'coding'}",
1567
+ "",
1568
+ f"Changed files ({len(changed_files)}):",
1569
+ ]
1570
+ parts.extend(f"- {path}" for path in changed_files[:50])
1571
+ parts.extend(("", "## Code Diff", "```diff", diff, "```"))
1572
+ if test_summary:
1573
+ parts.extend(("", "## Test Results", test_summary))
1574
+ parts.append("\nReturn only the JSON review object.")
1575
+ return "\n".join(parts)
1576
+
1577
+
1578
+ def _runtime_critic_attempt(agent_id: str, result: TaskResult) -> dict:
1579
+ if result.status == "success":
1580
+ outcome = "success"
1581
+ elif result.failure_code == "agent_idle_timeout":
1582
+ outcome = "timeout"
1583
+ elif result.exit_code not in (0, None):
1584
+ outcome = "exit_code"
1585
+ else:
1586
+ outcome = "agent_error"
1587
+ attempt = {"agent": agent_id, "outcome": outcome}
1588
+ duration_seconds = (result.metrics or {}).get("duration_seconds")
1589
+ if isinstance(duration_seconds, (int, float)) and not isinstance(duration_seconds, bool):
1590
+ if duration_seconds >= 0:
1591
+ attempt["duration_ms"] = int(duration_seconds * 1000)
1592
+ return attempt
1493
1593
 
1494
1594
 
1495
1595
  _ACTIONABLE_REFLECTION_KINDS = frozenset({
@@ -1561,6 +1661,23 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
1561
1661
  result.stdout = ""
1562
1662
  result.stderr = ""
1563
1663
  result.observations = []
1664
+ critic_review = result.critic_review
1665
+ if isinstance(critic_review, dict) and isinstance(critic_review.get("result"), dict):
1666
+ result.critic_review = {
1667
+ "collection_status": "completed",
1668
+ "result": critic_review["result"],
1669
+ }
1670
+ if isinstance(critic_review.get("agent_used"), str):
1671
+ result.critic_review["agent_used"] = critic_review["agent_used"]
1672
+ if isinstance(critic_review.get("agent_attempts"), list):
1673
+ result.critic_review["agent_attempts"] = critic_review["agent_attempts"]
1674
+ elif isinstance(critic_review, dict):
1675
+ result.critic_review = {
1676
+ "collection_status": "unavailable",
1677
+ "failure_code": str(
1678
+ critic_review.get("failure_code") or "runtime_critic_local_bind_private"
1679
+ )[:64],
1680
+ }
1564
1681
  if result.error:
1565
1682
  result.error = (
1566
1683
  "Local task failed. Review this task in Forgexa Desktop on the "
@@ -5291,6 +5408,8 @@ class ProcessManager:
5291
5408
  """Classify a fallback-eligible failure without conflating its cause."""
5292
5409
  if result.failure_code in {"all_agents_rate_limited", "all_agents_unavailable"}:
5293
5410
  return None
5411
+ if result.failure_code == "agent_platform_unsupported":
5412
+ return "agent_unavailable"
5294
5413
  error_text = ProcessManager._failure_pattern_channels(result).lower()
5295
5414
  if any(p in error_text for p in ProcessManager.RATE_LIMIT_PATTERNS):
5296
5415
  return "rate_limit"
@@ -5938,6 +6057,7 @@ class ProcessManager:
5938
6057
  "design": 50,
5939
6058
  "testing": 60,
5940
6059
  "review": 40,
6060
+ "critic_review": 1,
5941
6061
  "fix": 60,
5942
6062
  }
5943
6063
  max_turns = int(os.environ.get(
@@ -6188,7 +6308,21 @@ class ProcessManager:
6188
6308
  auth_src = Path.home() / ".local" / "share" / "opencode" / "auth.json"
6189
6309
  if auth_src.exists():
6190
6310
  shutil.copy2(auth_src, isolated_data_dir / "auth.json")
6191
- env = agent_core.opencode_base_env(os.environ.copy(), data_home=tmp_data_root)
6311
+ try:
6312
+ env = agent_core.opencode_base_env(
6313
+ os.environ.copy(),
6314
+ data_home=tmp_data_root,
6315
+ require_windows_shell=True,
6316
+ )
6317
+ except RuntimeError as exc:
6318
+ return TaskResult(
6319
+ status="failed",
6320
+ exit_code=-1,
6321
+ stdout="",
6322
+ stderr="",
6323
+ error=str(exc),
6324
+ failure_code="agent_platform_unsupported",
6325
+ )
6192
6326
  result = await self._run_cli(cmd, cwd, timeout, task_id, on_chunk=on_chunk, env=env)
6193
6327
  output = "\n".join(
6194
6328
  part for part in (result.stdout, result.stderr, result.error) if part
@@ -7314,6 +7448,7 @@ class ProgressReporter:
7314
7448
  "artifacts": result.artifacts,
7315
7449
  "observations": result.observations,
7316
7450
  "evidence_summary": result.evidence_summary,
7451
+ "critic_review": result.critic_review,
7317
7452
  "metrics": result.metrics,
7318
7453
  "git": result.git,
7319
7454
  }
@@ -7924,6 +8059,7 @@ class TaskPoller:
7924
8059
  if isinstance(t.get("runtime_evidence_plan"), dict) else None
7925
8060
  ),
7926
8061
  local_evidence_upload=t.get("local_evidence_upload") is True,
8062
+ critic_review_required=t.get("critic_review_required") is True,
7927
8063
  ))
7928
8064
  return tasks
7929
8065
  except Exception as e:
@@ -10703,6 +10839,16 @@ class RuntimeDaemon:
10703
10839
  before_sha=node_before_sha,
10704
10840
  )
10705
10841
 
10842
+ if result.status == "success" and task.critic_review_required:
10843
+ await reporter.report_progress(task.task_id, 95, "running_critic_review")
10844
+ result.critic_review = await self._run_runtime_critic_review(
10845
+ task,
10846
+ agent,
10847
+ workspace_path,
10848
+ result,
10849
+ before_sha=node_before_sha,
10850
+ )
10851
+
10706
10852
  # 6. Report completion (include actual agent used if different from requested)
10707
10853
  result.metrics["actual_agent"] = agent.agent_id
10708
10854
  if agent.agent_id != task.agent_type:
@@ -10731,6 +10877,187 @@ class RuntimeDaemon:
10731
10877
  project_lock.release()
10732
10878
  _release_local_workspace_file_lock(local_bind_file_lock)
10733
10879
 
10880
+ async def _collect_runtime_critic_diff(
10881
+ self,
10882
+ workspace_path: Path,
10883
+ *,
10884
+ before_sha: str,
10885
+ after_sha: str,
10886
+ changed_files: list[str],
10887
+ ) -> str | None:
10888
+ if not before_sha or not after_sha or not changed_files:
10889
+ return None
10890
+ try:
10891
+ diff = await self.workspace_manager._git(
10892
+ "diff",
10893
+ "--no-ext-diff",
10894
+ "--unified=3",
10895
+ before_sha,
10896
+ after_sha,
10897
+ "--",
10898
+ *changed_files,
10899
+ cwd=workspace_path,
10900
+ timeout=60,
10901
+ )
10902
+ except Exception:
10903
+ logger.warning(
10904
+ "Could not collect frozen diff for Runtime Critic review of commit %s",
10905
+ after_sha[:12],
10906
+ exc_info=True,
10907
+ )
10908
+ return None
10909
+ return diff[:_RUNTIME_CRITIC_DIFF_MAX_CHARS]
10910
+
10911
+ async def _run_runtime_critic_review(
10912
+ self,
10913
+ task: TaskInfo,
10914
+ agent: DiscoveredAgent,
10915
+ workspace_path: Path,
10916
+ result: TaskResult,
10917
+ *,
10918
+ before_sha: str,
10919
+ ) -> dict:
10920
+ """Run a one-turn Critic on the selected Runtime Agent in a temp capsule.
10921
+
10922
+ The agent receives only the frozen diff and metadata in its prompt. Its
10923
+ cwd is an empty temporary directory, never the task workspace.
10924
+ """
10925
+ agent_id = str(getattr(agent, "agent_id", "")).strip().lower()
10926
+ if not agent_id:
10927
+ return {
10928
+ "collection_status": "unavailable",
10929
+ "failure_code": "runtime_critic_agent_unknown",
10930
+ }
10931
+ metadata = {"agent_used": agent_id}
10932
+ is_local_bind = task.workspace_resolution == "local_bind"
10933
+ if is_local_bind and not task.local_evidence_upload:
10934
+ return {
10935
+ "collection_status": "unavailable",
10936
+ "failure_code": "runtime_critic_local_evidence_upload_disabled",
10937
+ **metadata,
10938
+ }
10939
+
10940
+ changed_files = [
10941
+ path.replace("\\", "/").lstrip("./")
10942
+ for path in result.files_changed
10943
+ if isinstance(path, str) and path.strip()
10944
+ ]
10945
+ if not changed_files:
10946
+ return {"collection_status": "not_applicable"}
10947
+
10948
+ after_sha = str((result.git or {}).get("commit_sha") or "").strip()
10949
+ diff = await self._collect_runtime_critic_diff(
10950
+ workspace_path,
10951
+ before_sha=before_sha,
10952
+ after_sha=after_sha,
10953
+ changed_files=changed_files,
10954
+ )
10955
+ if not diff:
10956
+ return {
10957
+ "collection_status": "unavailable",
10958
+ "failure_code": "runtime_critic_diff_unavailable",
10959
+ **metadata,
10960
+ }
10961
+
10962
+ review_task = TaskInfo(
10963
+ task_id=f"{task.task_id}-critic",
10964
+ graph_id=task.graph_id,
10965
+ node_type="critic_review",
10966
+ agent_type=agent_id,
10967
+ input_prompt=_build_runtime_critic_prompt(
10968
+ task,
10969
+ changed_files=changed_files,
10970
+ diff=diff,
10971
+ test_summary=_runtime_critic_test_summary(result.observations),
10972
+ ),
10973
+ input_data={"critic_review": True},
10974
+ timeout_seconds=_RUNTIME_CRITIC_TIMEOUT_SECONDS,
10975
+ max_retries=0,
10976
+ retry_count=0,
10977
+ project={},
10978
+ work_item={},
10979
+ )
10980
+ try:
10981
+ with tempfile.TemporaryDirectory(
10982
+ prefix=f"forgexa-critic-{task.task_id[:8]}-"
10983
+ ) as review_dir:
10984
+ review_result = await self.process_manager.run_agent(
10985
+ agent,
10986
+ review_task,
10987
+ Path(review_dir),
10988
+ )
10989
+ except asyncio.CancelledError:
10990
+ raise
10991
+ except Exception:
10992
+ logger.warning(
10993
+ "Runtime Critic execution failed for task %s",
10994
+ task.task_id,
10995
+ exc_info=True,
10996
+ )
10997
+ return {
10998
+ "collection_status": "unavailable",
10999
+ "failure_code": "runtime_critic_exception",
11000
+ "agent_attempts": [{"agent": agent_id, "outcome": "agent_error"}],
11001
+ **metadata,
11002
+ }
11003
+
11004
+ attempt = _runtime_critic_attempt(agent_id, review_result)
11005
+ if review_result.status != "success":
11006
+ return {
11007
+ "collection_status": "unavailable",
11008
+ "failure_code": review_result.failure_code or "runtime_critic_agent_error",
11009
+ "agent_attempts": [attempt],
11010
+ **metadata,
11011
+ }
11012
+
11013
+ review_output = _daemon_extract_agent_output(
11014
+ agent_id,
11015
+ review_result.stdout or "",
11016
+ ).strip()
11017
+ if not review_output:
11018
+ attempt["outcome"] = "empty_output"
11019
+ return {
11020
+ "collection_status": "unavailable",
11021
+ "failure_code": "runtime_critic_empty_output",
11022
+ "agent_attempts": [attempt],
11023
+ **metadata,
11024
+ }
11025
+ if is_local_bind:
11026
+ structured_result = _daemon_extract_json(review_output)
11027
+ if not isinstance(structured_result, dict):
11028
+ attempt["outcome"] = "invalid_output"
11029
+ return {
11030
+ "collection_status": "unavailable",
11031
+ "failure_code": "runtime_critic_invalid_output",
11032
+ "agent_attempts": [attempt],
11033
+ **metadata,
11034
+ }
11035
+ encoded_result = json.dumps(
11036
+ structured_result,
11037
+ ensure_ascii=True,
11038
+ separators=(",", ":"),
11039
+ )
11040
+ if len(encoded_result) > _RUNTIME_CRITIC_OUTPUT_MAX_CHARS:
11041
+ attempt["outcome"] = "output_too_large"
11042
+ return {
11043
+ "collection_status": "unavailable",
11044
+ "failure_code": "runtime_critic_output_too_large",
11045
+ "agent_attempts": [attempt],
11046
+ **metadata,
11047
+ }
11048
+ return {
11049
+ "collection_status": "completed",
11050
+ "result": structured_result,
11051
+ "agent_attempts": [attempt],
11052
+ **metadata,
11053
+ }
11054
+ return {
11055
+ "collection_status": "completed",
11056
+ "output": review_output[:_RUNTIME_CRITIC_OUTPUT_MAX_CHARS],
11057
+ "agent_attempts": [attempt],
11058
+ **metadata,
11059
+ }
11060
+
10734
11061
  def _select_fallback_agent(
10735
11062
  self, current_agent_id: str, fallback_chain: list[str] | None, tried: set[str]
10736
11063
  ) -> DiscoveredAgent | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.47.4
3
+ Version: 1.48.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.47.4"
3
+ version = "1.48.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