forgexa-cli 1.21.2__tar.gz → 1.21.4__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 (22) hide show
  1. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/PKG-INFO +1 -1
  2. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/autoupgrade.py +1 -1
  4. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/daemon.py +210 -98
  5. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/main.py +117 -29
  6. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/pyproject.toml +1 -1
  8. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_auth_and_runtime_commands.py +1 -1
  9. forgexa_cli-1.21.4/tests/test_check_command.py +264 -0
  10. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_silent_install.py +170 -2
  11. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_upgrade_observability.py +2 -0
  12. forgexa_cli-1.21.2/tests/test_check_command.py +0 -142
  13. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/README.md +0 -0
  14. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/_build_config.py +0 -0
  15. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/py.typed +0 -0
  16. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  17. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  18. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
  19. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/requires.txt +0 -0
  20. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/top_level.txt +0 -0
  21. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/setup.cfg +0 -0
  22. {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_autoupgrade.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.21.2
3
+ Version: 1.21.4
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.21.2"
2
+ __version__ = "1.21.4"
@@ -78,7 +78,7 @@ def _state_path() -> Path:
78
78
 
79
79
 
80
80
  def empty_state() -> dict:
81
- return {"active": None, "last_failure": None, "last_success": None}
81
+ return {"active": None, "last_failure": None, "last_success": None, "last_skip": None}
82
82
 
83
83
 
84
84
  def read_state() -> dict:
@@ -555,7 +555,7 @@ except (ImportError, ModuleNotFoundError):
555
555
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
556
556
  # Kept in sync with pyproject.toml version via bump-version.sh.
557
557
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
558
- DAEMON_VERSION = "1.21.2"
558
+ DAEMON_VERSION = "1.21.4"
559
559
 
560
560
 
561
561
  def _detect_client_type() -> str:
@@ -771,6 +771,27 @@ class DiscoveredAgent:
771
771
  version: str
772
772
  invoke_modes: list[str]
773
773
  compatibility_level: str
774
+ min_version_ok: bool = True
775
+ version_warning: str = ""
776
+
777
+
778
+ def _agent_dicts_for_wire(agents: "list[DiscoveredAgent]") -> list[dict]:
779
+ """Serialize discovered agents for the register/heartbeat wire payload.
780
+
781
+ Single source of truth for this shape so register(), the initial
782
+ heartbeat payload, and periodic heartbeat updates never drift apart.
783
+ """
784
+ return [
785
+ {
786
+ "agent_id": a.agent_id,
787
+ "version": a.version,
788
+ "invoke_modes": a.invoke_modes,
789
+ "compatibility_level": a.compatibility_level,
790
+ "min_version_ok": a.min_version_ok,
791
+ "version_warning": a.version_warning,
792
+ }
793
+ for a in agents
794
+ ]
774
795
 
775
796
 
776
797
  def _prepare_agent_command(command: list[str]) -> list[str]:
@@ -811,6 +832,71 @@ def _prepare_agent_command(command: list[str]) -> list[str]:
811
832
  return command
812
833
 
813
834
 
835
+ def _windows_agent_needs_task_file_redirect(agent_command: str) -> bool:
836
+ """Return True when ``agent_command``'s prompt must be redirected through
837
+ a task file instead of being passed as a literal CLI argument.
838
+
839
+ Root cause: on Windows, ``.cmd``/``.bat`` launchers (the standard shim
840
+ format for globally-installed npm packages, e.g. Copilot CLI, Kimi Code)
841
+ are invoked through ``cmd.exe /c`` (see ``_prepare_agent_command``). The
842
+ command line handed to ``cmd.exe`` is built by ``subprocess.list2cmdline``,
843
+ which quotes arguments using the C-runtime/``CommandLineToArgvW``
844
+ convention -- but ``cmd.exe`` then RE-TOKENIZES that same string using its
845
+ own, different shell grammar before forwarding the remaining text to the
846
+ batch script (typically via ``%*``). Any cmd.exe metacharacter in the
847
+ prompt (``"``, ``&``, ``|``, ``^``, ``<``, ``>``, ``%``) survives the
848
+ first quoting pass but breaks the second, either splitting the prompt
849
+ into multiple arguments (observed as Copilot's "your prompt was not
850
+ quoted, so the extra words were treated as separate arguments" error) or,
851
+ in the worst case, allowing prompt content to inject additional shell
852
+ commands. Native ``.exe`` targets are unaffected: a real EXE's C-runtime
853
+ argv parsing is exactly what ``list2cmdline`` guarantees. ``.ps1``
854
+ targets are also unaffected because ``_prepare_agent_command`` forwards
855
+ their arguments as a real argv list to ``powershell.exe -File``, not
856
+ through a second string-based shell.
857
+ """
858
+ if sys.platform != "win32":
859
+ return False
860
+ return Path(agent_command).suffix.lower() in {".cmd", ".bat"}
861
+
862
+
863
+ def _redirect_prompt_through_task_file(
864
+ cwd: Path, prompt: str, task_id: str, agent_label: str,
865
+ ) -> tuple[str, Path | None]:
866
+ """Write ``prompt`` to a workspace-local file and return a short, fixed,
867
+ shell-metacharacter-free instruction telling the agent to read it instead.
868
+
869
+ Returns ``(effective_prompt, task_file)``. ``task_file`` is the path the
870
+ caller must delete after the run, or ``None`` if writing failed, in which
871
+ case ``effective_prompt`` falls back to the original ``prompt`` unchanged.
872
+ """
873
+ task_file = Path(cwd) / ".forgexa-task.md"
874
+ try:
875
+ task_file.write_text(prompt, encoding="utf-8")
876
+ # Prevent accidental git commit of this ephemeral file.
877
+ git_exclude = Path(cwd) / ".git" / "info" / "exclude"
878
+ if git_exclude.exists():
879
+ excl = git_exclude.read_text(encoding="utf-8", errors="replace")
880
+ if ".forgexa-task.md" not in excl:
881
+ git_exclude.write_text(
882
+ excl.rstrip("\n") + "\n.forgexa-task.md\n", encoding="utf-8",
883
+ )
884
+ logger.debug(
885
+ "%s task %s: wrote %d-char prompt to .forgexa-task.md",
886
+ agent_label, task_id, len(prompt),
887
+ )
888
+ return (
889
+ "Read the file .forgexa-task.md for your complete task "
890
+ "specification. Follow all instructions in it exactly."
891
+ ), task_file
892
+ except Exception as exc:
893
+ logger.warning(
894
+ "%s task %s: could not write prompt file (%s); using prompt directly",
895
+ agent_label, task_id, exc,
896
+ )
897
+ return prompt, None
898
+
899
+
814
900
  def _agent_process_group_kwargs() -> dict[str, bool]:
815
901
  """Isolate agent process groups where the platform supports POSIX sessions."""
816
902
  # Windows cleanup uses taskkill /T, and starting cmd.exe with a new session
@@ -1112,11 +1198,6 @@ def _daemon_extract_claude_output(raw: str) -> str:
1112
1198
  return raw
1113
1199
 
1114
1200
 
1115
- # OpenCode requires v1.4.0+ for --dangerously-skip-permissions (added 2026-04-08).
1116
- # Without it, headless opencode run blocks on permission prompts and exits with code 1.
1117
- _OPENCODE_MIN_VERSION = (1, 4, 0)
1118
-
1119
-
1120
1201
  def _parse_semver(v: str) -> "tuple[int, int, int] | None":
1121
1202
  """Parse a semver string into a comparable (major, minor, patch) tuple.
1122
1203
 
@@ -1133,6 +1214,40 @@ def _parse_semver(v: str) -> "tuple[int, int, int] | None":
1133
1214
  return None
1134
1215
 
1135
1216
 
1217
+ def _check_agent_min_version(agent_id: str, version: str, spec: dict) -> "tuple[bool, str]":
1218
+ """Check a discovered agent's version against its registry minimum, if any.
1219
+
1220
+ Returns (min_version_ok, version_warning). version_warning is empty
1221
+ whenever there's nothing to warn about: no minimum is defined for this
1222
+ agent (``spec`` has no ``min_version`` key), the version string couldn't
1223
+ be parsed (never treated as a hard failure -- unparseable versions are
1224
+ skipped, not rejected, matching the pre-existing opencode behavior), or
1225
+ the installed version already meets the minimum.
1226
+
1227
+ This is the single source of truth for per-agent minimum-version gating,
1228
+ consumed by both AgentDiscovery.discover() (so `forgexa check` and the
1229
+ daemon's own heartbeat can report incompatibility instantly, without
1230
+ running a real task) and ProcessManager.run_agent() (a defense-in-depth
1231
+ dispatch-time gate for any caller that bypasses discovery-time filtering).
1232
+ """
1233
+ min_version = spec.get("min_version")
1234
+ if not min_version:
1235
+ return True, ""
1236
+ parsed = _parse_semver(version)
1237
+ if parsed is None:
1238
+ return True, ""
1239
+ if parsed >= tuple(min_version):
1240
+ return True, ""
1241
+ min_str = ".".join(str(x) for x in min_version)
1242
+ reason = spec.get("min_version_reason", "")
1243
+ upgrade_hint = spec.get("upgrade_hint", f"upgrade the {agent_id} CLI")
1244
+ warning = f"{agent_id} v{version} is older than the minimum supported v{min_str}+."
1245
+ if reason:
1246
+ warning += f" {reason}"
1247
+ warning += f" Fix: {upgrade_hint}."
1248
+ return False, warning
1249
+
1250
+
1136
1251
  def _daemon_extract_opencode_output(raw: str) -> str:
1137
1252
  """Extract text content from OpenCode JSONL output."""
1138
1253
  parts: list[str] = []
@@ -1494,6 +1609,13 @@ class AgentDiscovery:
1494
1609
  "env_path_override": "FACTORY_OPENCODE_PATH",
1495
1610
  "env_model_override": "FACTORY_OPENCODE_MODEL",
1496
1611
  "compatibility_level": "L2",
1612
+ "min_version": (1, 4, 0),
1613
+ "min_version_reason": (
1614
+ "--dangerously-skip-permissions (required so headless runs "
1615
+ "don't block on interactive permission prompts) was added "
1616
+ "in v1.4.0 (2026-04-08)."
1617
+ ),
1618
+ "upgrade_hint": "opencode upgrade",
1497
1619
  },
1498
1620
  "gemini": {
1499
1621
  "commands": ["gemini"],
@@ -1508,6 +1630,14 @@ class AgentDiscovery:
1508
1630
  "invoke_modes": ["cli"],
1509
1631
  "env_path_override": "FACTORY_KIMI_PATH",
1510
1632
  "compatibility_level": "L2",
1633
+ "min_version": (0, 18, 0),
1634
+ "min_version_reason": (
1635
+ "Non-interactive auto-approval via a bare -p <prompt> (no "
1636
+ "--yolo/--auto) and the current on-disk auth/session layout "
1637
+ "both assume Kimi Code v0.18.0+; older versions require "
1638
+ "--yolo/--auto for auto-approval and use a different auth "
1639
+ "storage layout."
1640
+ ),
1511
1641
  },
1512
1642
  "copilot": {
1513
1643
  "commands": ["copilot"],
@@ -1652,12 +1782,20 @@ class AgentDiscovery:
1652
1782
  agent_id, resolved,
1653
1783
  )
1654
1784
  continue
1785
+ min_version_ok, version_warning = _check_agent_min_version(agent_id, version, spec)
1786
+ if not min_version_ok:
1787
+ logger.warning(
1788
+ "Agent %s found at %s (v%s) but below minimum version: %s",
1789
+ agent_id, resolved, version, version_warning,
1790
+ )
1655
1791
  available.append(DiscoveredAgent(
1656
1792
  agent_id=agent_id,
1657
1793
  command=resolved,
1658
1794
  version=version,
1659
1795
  invoke_modes=spec["invoke_modes"],
1660
1796
  compatibility_level=spec["compatibility_level"],
1797
+ min_version_ok=min_version_ok,
1798
+ version_warning=version_warning,
1661
1799
  ))
1662
1800
  logger.info("Discovered agent: %s v%s (%s)", agent_id, version, resolved)
1663
1801
  return available
@@ -4240,6 +4378,22 @@ class ProcessManager:
4240
4378
  on_chunk: Any = None,
4241
4379
  ) -> TaskResult:
4242
4380
  """Execute an agent CLI and collect results."""
4381
+ # Centralized, agent-agnostic version gate: AgentDiscovery.discover()
4382
+ # already computed min_version_ok/version_warning against
4383
+ # AGENT_REGISTRY[agent_id]["min_version"] (see _check_agent_min_version).
4384
+ # Refuse to dispatch rather than letting a known-incompatible CLI
4385
+ # invocation fail later with a confusing/unrelated error.
4386
+ if not agent.min_version_ok:
4387
+ logger.warning(
4388
+ "Refusing to run task %s on agent '%s': %s",
4389
+ task.task_id, agent.agent_id, agent.version_warning,
4390
+ )
4391
+ return TaskResult(
4392
+ status="failed", exit_code=-1, stdout="", stderr="",
4393
+ error=agent.version_warning,
4394
+ failure_code="agent_version_incompatible",
4395
+ )
4396
+
4243
4397
  prompt = self._build_prompt(task)
4244
4398
  timeout = task.timeout_seconds or settings.AGENT_TIMEOUT
4245
4399
 
@@ -4854,36 +5008,14 @@ class ProcessManager:
4854
5008
  opencode runs do not share the same SQLite database, which causes
4855
5009
  'Failed to run the query PRAGMA wal_checkpoint(PASSIVE)' errors and
4856
5010
  exit code 1 when multiple tasks run simultaneously.
4857
- """
4858
- # --dangerously-skip-permissions was introduced in opencode v1.4.0 (2026-04-08).
4859
- # Older versions silently ignore the flag but then block on permission prompts
4860
- # in headless mode (no TTY), eventually exiting with code 1 after a long wait.
4861
- # Fail fast here so the error is immediately actionable.
4862
- _oc_ver = _parse_semver(agent.version)
4863
- if _oc_ver is None:
4864
- # Non-standard version string (e.g. 'unknown', 'latest') — skip version
4865
- # check rather than incorrectly rejecting a potentially valid agent.
4866
- logger.warning(
4867
- "Could not parse opencode version %r; skipping version check",
4868
- agent.version,
4869
- )
4870
- elif _oc_ver < _OPENCODE_MIN_VERSION:
4871
- _min_str = ".".join(str(x) for x in _OPENCODE_MIN_VERSION)
4872
- return TaskResult(
4873
- status="failed",
4874
- exit_code=-1,
4875
- stdout="",
4876
- stderr="",
4877
- error=(
4878
- f"OpenCode {agent.version} is too old for headless execution. "
4879
- f"Version {_min_str}+ is required: --dangerously-skip-permissions "
4880
- f"(which auto-approves permission prompts in non-interactive mode) "
4881
- f"was added in v{_min_str} on 2026-04-08. "
4882
- f"Upgrade with: opencode upgrade"
4883
- ),
4884
- failure_code="agent_version_incompatible",
4885
- )
4886
5011
 
5012
+ Version compatibility (opencode requires v1.4.0+ for
5013
+ --dangerously-skip-permissions -- older versions silently ignore the
5014
+ flag but then block on permission prompts in headless mode) is
5015
+ checked centrally in ProcessManager.run_agent() before dispatch, via
5016
+ AGENT_REGISTRY["opencode"]["min_version"] / _check_agent_min_version().
5017
+ No need to re-check it here.
5018
+ """
4887
5019
  cmd = [
4888
5020
  agent.command, "run",
4889
5021
  "--format", "json",
@@ -4975,11 +5107,30 @@ class ProcessManager:
4975
5107
  ),
4976
5108
  )
4977
5109
 
4978
- cmd = [agent.command, "-p", prompt]
4979
- result = await self._run_cli(
4980
- cmd, cwd, timeout, task_id, on_chunk=on_chunk,
4981
- stream_stderr=True,
4982
- )
5110
+ # Same Windows cmd.exe quoting/injection hazard as Copilot CLI (see
5111
+ # _windows_agent_needs_task_file_redirect): Kimi's npm-installed
5112
+ # kimi.cmd/.bat shim is invoked through cmd.exe, which can corrupt or
5113
+ # split a prompt passed directly as a -p argument. Redirect through a
5114
+ # task file whenever that launcher type is in play.
5115
+ _effective_prompt = prompt
5116
+ _task_file: Path | None = None
5117
+ if _windows_agent_needs_task_file_redirect(agent.command):
5118
+ _effective_prompt, _task_file = _redirect_prompt_through_task_file(
5119
+ cwd, prompt, task_id, "Kimi Code",
5120
+ )
5121
+
5122
+ cmd = [agent.command, "-p", _effective_prompt]
5123
+ try:
5124
+ result = await self._run_cli(
5125
+ cmd, cwd, timeout, task_id, on_chunk=on_chunk,
5126
+ stream_stderr=True,
5127
+ )
5128
+ finally:
5129
+ if _task_file is not None:
5130
+ try:
5131
+ _task_file.unlink(missing_ok=True)
5132
+ except Exception:
5133
+ pass
4983
5134
 
4984
5135
  # ── Post-run: degarble Kimi Code 0.18.0 error messages ───────────────
4985
5136
  # Kimi Code 0.18.0 has a bug where it streams 404/auth error text
@@ -5349,39 +5500,24 @@ class ProcessManager:
5349
5500
 
5350
5501
  # On Windows, batch/cmd wrappers (e.g. copilot.BAT from the VS Code
5351
5502
  # Copilot Chat extension) invoke cmd.exe when run via CreateProcess.
5352
- # cmd.exe has an ~8191-char command-line limit; long multi-line prompts
5353
- # are silently truncated so the agent runs but never sees the output
5354
- # file instructions — producing no required deliverables.
5355
- # Fix: write the full prompt to a workspace-local file and pass a short
5356
- # single-line read-instruction via -p to stay under the limit.
5503
+ # This causes two independent problems for a prompt passed as a
5504
+ # literal -p argument: (1) cmd.exe's ~8191-char command-line limit
5505
+ # silently truncates long multi-line prompts, and (2) cmd.exe
5506
+ # re-tokenizes the already-quoted command line using its own shell
5507
+ # grammar, so any cmd.exe metacharacter in the prompt (", &, |, ^, <,
5508
+ # >, %) corrupts the quoting boundary and splits the prompt into
5509
+ # multiple arguments -- surfaced by Copilot as "your prompt was not
5510
+ # quoted, so the extra words were treated as separate arguments".
5511
+ # See _windows_agent_needs_task_file_redirect for the full mechanism.
5512
+ # Fix: write the full prompt to a workspace-local file and pass a
5513
+ # short, fixed, metacharacter-free read-instruction via -p instead --
5514
+ # for every prompt on this launcher type, not just multi-line ones.
5357
5515
  _task_file: Path | None = None
5358
5516
  _effective_prompt = prompt
5359
- if sys.platform == "win32" and "\n" in prompt:
5360
- _task_file = Path(cwd) / ".forgexa-task.md"
5361
- try:
5362
- _task_file.write_text(prompt, encoding="utf-8")
5363
- # Prevent accidental git commit of this ephemeral file
5364
- _git_exclude = Path(cwd) / ".git" / "info" / "exclude"
5365
- if _git_exclude.exists():
5366
- _excl = _git_exclude.read_text(encoding="utf-8", errors="replace")
5367
- if ".forgexa-task.md" not in _excl:
5368
- _git_exclude.write_text(
5369
- _excl.rstrip("\n") + "\n.forgexa-task.md\n", encoding="utf-8",
5370
- )
5371
- _effective_prompt = (
5372
- "Read the file .forgexa-task.md for your complete task "
5373
- "specification. Follow all instructions in it exactly."
5374
- )
5375
- logger.debug(
5376
- "Copilot task %s: wrote %d-char prompt to .forgexa-task.md",
5377
- task_id, len(prompt),
5378
- )
5379
- except Exception as _tf_err:
5380
- logger.warning(
5381
- "Copilot task %s: could not write prompt file (%s); using -p directly",
5382
- task_id, _tf_err,
5383
- )
5384
- _task_file = None
5517
+ if _windows_agent_needs_task_file_redirect(agent.command):
5518
+ _effective_prompt, _task_file = _redirect_prompt_through_task_file(
5519
+ cwd, prompt, task_id, "Copilot",
5520
+ )
5385
5521
 
5386
5522
  cmd += ["-C", str(cwd), "-p", _effective_prompt]
5387
5523
  cmd = _prepare_agent_command(cmd)
@@ -7110,15 +7246,7 @@ class ServerConnection:
7110
7246
  """Register this daemon with the server."""
7111
7247
  self._agents = agents
7112
7248
  self._max_concurrent_cache = max_concurrent
7113
- agent_dicts = [
7114
- {
7115
- "agent_id": a.agent_id,
7116
- "version": a.version,
7117
- "invoke_modes": a.invoke_modes,
7118
- "compatibility_level": a.compatibility_level,
7119
- }
7120
- for a in agents
7121
- ]
7249
+ agent_dicts = _agent_dicts_for_wire(agents)
7122
7250
  async def _register_once():
7123
7251
  return await self.client.post(
7124
7252
  f"{self.server_url}/api/v1/runtimes/register",
@@ -7192,15 +7320,7 @@ class ServerConnection:
7192
7320
  )
7193
7321
  self.heartbeat._on_success = _on_auth_success
7194
7322
  self.heartbeat._on_auth_failure = _on_auth_failure
7195
- self.heartbeat.update(0, [
7196
- {
7197
- "agent_id": a.agent_id,
7198
- "version": a.version,
7199
- "invoke_modes": a.invoke_modes,
7200
- "compatibility_level": a.compatibility_level,
7201
- }
7202
- for a in agents
7203
- ])
7323
+ self.heartbeat.update(0, _agent_dicts_for_wire(agents))
7204
7324
  self.poller = TaskPoller(
7205
7325
  self.client, self.server_url, self.runtime_id, poll_interval,
7206
7326
  )
@@ -11778,15 +11898,7 @@ class RuntimeDaemon:
11778
11898
  return f"Push failed: {e}"
11779
11899
 
11780
11900
  def _agents_as_dicts(self) -> list[dict]:
11781
- return [
11782
- {
11783
- "agent_id": a.agent_id,
11784
- "version": a.version,
11785
- "invoke_modes": a.invoke_modes,
11786
- "compatibility_level": a.compatibility_level,
11787
- }
11788
- for a in self.agents
11789
- ]
11901
+ return _agent_dicts_for_wire(self.agents)
11790
11902
 
11791
11903
  async def _cleanup_stale_tmp_dirs(self) -> None:
11792
11904
  """Remove stale Copilot/Claude temp dirs from prior OOM-killed daemon runs.