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.
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/PKG-INFO +1 -1
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/autoupgrade.py +1 -1
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/daemon.py +210 -98
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/main.py +117 -29
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/pyproject.toml +1 -1
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_auth_and_runtime_commands.py +1 -1
- forgexa_cli-1.21.4/tests/test_check_command.py +264 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_silent_install.py +170 -2
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_upgrade_observability.py +2 -0
- forgexa_cli-1.21.2/tests/test_check_command.py +0 -142
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/README.md +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/setup.cfg +0 -0
- {forgexa_cli-1.21.2 → forgexa_cli-1.21.4}/tests/test_autoupgrade.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.21.
|
|
2
|
+
__version__ = "1.21.4"
|
|
@@ -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.
|
|
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
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
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
|
-
#
|
|
5353
|
-
#
|
|
5354
|
-
#
|
|
5355
|
-
#
|
|
5356
|
-
#
|
|
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
|
|
5360
|
-
_task_file =
|
|
5361
|
-
|
|
5362
|
-
|
|
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.
|