forgexa-cli 1.76.0__tar.gz → 1.76.1__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.76.0 → forgexa_cli-1.76.1}/PKG-INFO +1 -1
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/agent_core.py +37 -1
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/daemon.py +65 -13
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/pyproject.toml +1 -1
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/README.md +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/setup.cfg +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.76.0 → forgexa_cli-1.76.1}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.76.
|
|
2
|
+
__version__ = "1.76.1"
|
|
@@ -55,7 +55,7 @@ import tempfile
|
|
|
55
55
|
import time
|
|
56
56
|
from contextlib import contextmanager
|
|
57
57
|
from dataclasses import dataclass, field
|
|
58
|
-
from pathlib import Path
|
|
58
|
+
from pathlib import Path, PurePosixPath
|
|
59
59
|
from typing import Any
|
|
60
60
|
|
|
61
61
|
# fcntl is Unix-only; on Windows we use msvcrt instead (see acquire_file_lock)
|
|
@@ -67,6 +67,42 @@ except ImportError:
|
|
|
67
67
|
logger = logging.getLogger("agent_core")
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> list[str]:
|
|
71
|
+
"""Find planned files without delivery evidence, including grouped sub-tasks."""
|
|
72
|
+
def normalize_path(value: object) -> str | None:
|
|
73
|
+
if not isinstance(value, str) or not value.strip():
|
|
74
|
+
return None
|
|
75
|
+
path = PurePosixPath(value.strip().replace("\\", "/"))
|
|
76
|
+
if path.is_absolute() or ".." in path.parts or not path.parts:
|
|
77
|
+
return None
|
|
78
|
+
return path.as_posix()
|
|
79
|
+
|
|
80
|
+
file_lists = [task_input.get("files")]
|
|
81
|
+
for item in task_input.get("items") or []:
|
|
82
|
+
if isinstance(item, dict):
|
|
83
|
+
file_lists.append(item.get("files"))
|
|
84
|
+
item_input = item.get("input")
|
|
85
|
+
if isinstance(item_input, dict):
|
|
86
|
+
file_lists.append(item_input.get("files"))
|
|
87
|
+
planned_paths = {
|
|
88
|
+
((normalize_path(path) or path).rstrip("/") + "/")
|
|
89
|
+
if path.endswith("/") else (normalize_path(path) or path)
|
|
90
|
+
for paths in file_lists if isinstance(paths, list)
|
|
91
|
+
for path in paths if isinstance(path, str) and path.strip()
|
|
92
|
+
}
|
|
93
|
+
changed_paths = {
|
|
94
|
+
normalized for path in changed_files
|
|
95
|
+
if (normalized := normalize_path(path)) is not None
|
|
96
|
+
}
|
|
97
|
+
return sorted(
|
|
98
|
+
path for path in planned_paths
|
|
99
|
+
if not any(
|
|
100
|
+
changed == path or (path.endswith("/") and changed.startswith(path))
|
|
101
|
+
for changed in changed_paths
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
70
106
|
_WORKSPACE_INSTRUCTION_PATHS = (
|
|
71
107
|
"AGENTS.md",
|
|
72
108
|
"CLAUDE.md",
|
|
@@ -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.76.
|
|
1237
|
+
DAEMON_VERSION = "1.76.1"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -6329,6 +6329,18 @@ class ProcessManager:
|
|
|
6329
6329
|
return "unavailable"
|
|
6330
6330
|
return "unable to run"
|
|
6331
6331
|
|
|
6332
|
+
@staticmethod
|
|
6333
|
+
def format_all_agents_failure(
|
|
6334
|
+
failure_kinds: list[str], attempts: list[tuple[str, str]],
|
|
6335
|
+
) -> str:
|
|
6336
|
+
label = ProcessManager.all_agents_failure_label(failure_kinds)
|
|
6337
|
+
agent_order = ", ".join(agent_id for agent_id, _error in attempts)
|
|
6338
|
+
details = "\n".join(
|
|
6339
|
+
f"[{agent_id}] {error or 'Agent failed without an error message'}"
|
|
6340
|
+
for agent_id, error in attempts
|
|
6341
|
+
)
|
|
6342
|
+
return f"All agents {label} (tried: {agent_order}). Agent failures:\n{details}"
|
|
6343
|
+
|
|
6332
6344
|
@staticmethod
|
|
6333
6345
|
def is_idle_timeout_without_work(result: "TaskResult") -> bool:
|
|
6334
6346
|
"""Return whether an idle-killed agent left no work worth preserving.
|
|
@@ -7953,6 +7965,7 @@ class ProcessManager:
|
|
|
7953
7965
|
disable_builtin_mcps: bool = False,
|
|
7954
7966
|
_retry_without_effort: bool = False,
|
|
7955
7967
|
_retry_without_token_env: bool = False,
|
|
7968
|
+
_retry_model_discovery_timeout: bool = False,
|
|
7956
7969
|
_isolated_copilot_home: str | None = None,
|
|
7957
7970
|
_auth_state: dict[str, bool] | None = None,
|
|
7958
7971
|
) -> TaskResult:
|
|
@@ -8219,12 +8232,40 @@ class ProcessManager:
|
|
|
8219
8232
|
disable_builtin_mcps=True,
|
|
8220
8233
|
_retry_without_effort=_retry_without_effort,
|
|
8221
8234
|
_retry_without_token_env=_retry_without_token_env,
|
|
8235
|
+
_retry_model_discovery_timeout=_retry_model_discovery_timeout,
|
|
8222
8236
|
_isolated_copilot_home=_isolated_copilot_home,
|
|
8223
8237
|
_auth_state=_auth_state,
|
|
8224
8238
|
)
|
|
8225
8239
|
retry_result.metrics["copilot_builtin_mcp_fallback"] = True
|
|
8226
8240
|
return retry_result
|
|
8227
8241
|
|
|
8242
|
+
model_discovery_detail = "\n".join([*structured_errors, stderr]).lower()
|
|
8243
|
+
if (
|
|
8244
|
+
"could not retrieve the list of available models" in model_discovery_detail
|
|
8245
|
+
and "timed out" in model_discovery_detail
|
|
8246
|
+
and not _retry_model_discovery_timeout
|
|
8247
|
+
and not (copilot_exit == 0 and returncode == 0)
|
|
8248
|
+
and not signals["has_meaningful_content"]
|
|
8249
|
+
and not self._copilot_called_any_tools(stdout)
|
|
8250
|
+
):
|
|
8251
|
+
logger.warning(
|
|
8252
|
+
"Copilot model discovery timed out before work for task %s; "
|
|
8253
|
+
"retrying once with the same credentials and configuration",
|
|
8254
|
+
task_id,
|
|
8255
|
+
)
|
|
8256
|
+
retry_result = await self._run_copilot(
|
|
8257
|
+
agent, prompt, cwd, timeout, task_id, on_chunk,
|
|
8258
|
+
stream_stderr=stream_stderr,
|
|
8259
|
+
disable_builtin_mcps=disable_builtin_mcps,
|
|
8260
|
+
_retry_without_effort=_retry_without_effort,
|
|
8261
|
+
_retry_without_token_env=_retry_without_token_env,
|
|
8262
|
+
_retry_model_discovery_timeout=True,
|
|
8263
|
+
_isolated_copilot_home=_isolated_copilot_home,
|
|
8264
|
+
_auth_state=_auth_state,
|
|
8265
|
+
)
|
|
8266
|
+
retry_result.metrics["copilot_model_discovery_timeout_recovery"] = True
|
|
8267
|
+
return retry_result
|
|
8268
|
+
|
|
8228
8269
|
model_discovery_error = agent_core.is_copilot_model_discovery_auth_error(
|
|
8229
8270
|
*structured_errors,
|
|
8230
8271
|
stderr,
|
|
@@ -8264,6 +8305,7 @@ class ProcessManager:
|
|
|
8264
8305
|
disable_builtin_mcps=disable_builtin_mcps,
|
|
8265
8306
|
_retry_without_effort=_retry_without_effort,
|
|
8266
8307
|
_retry_without_token_env=True,
|
|
8308
|
+
_retry_model_discovery_timeout=_retry_model_discovery_timeout,
|
|
8267
8309
|
_isolated_copilot_home=_isolated_copilot_home,
|
|
8268
8310
|
_auth_state=_auth_state,
|
|
8269
8311
|
)
|
|
@@ -8316,6 +8358,7 @@ class ProcessManager:
|
|
|
8316
8358
|
disable_builtin_mcps=disable_builtin_mcps,
|
|
8317
8359
|
_retry_without_effort=_retry_without_effort,
|
|
8318
8360
|
_retry_without_token_env=True,
|
|
8361
|
+
_retry_model_discovery_timeout=_retry_model_discovery_timeout,
|
|
8319
8362
|
_isolated_copilot_home=_isolated_copilot_home,
|
|
8320
8363
|
_auth_state=_auth_state,
|
|
8321
8364
|
)
|
|
@@ -8381,6 +8424,7 @@ class ProcessManager:
|
|
|
8381
8424
|
disable_builtin_mcps=disable_builtin_mcps,
|
|
8382
8425
|
_retry_without_effort=True,
|
|
8383
8426
|
_retry_without_token_env=_retry_without_token_env,
|
|
8427
|
+
_retry_model_discovery_timeout=_retry_model_discovery_timeout,
|
|
8384
8428
|
_isolated_copilot_home=_isolated_copilot_home,
|
|
8385
8429
|
_auth_state=_auth_state,
|
|
8386
8430
|
)
|
|
@@ -12877,6 +12921,7 @@ class RuntimeDaemon:
|
|
|
12877
12921
|
else "timed out"
|
|
12878
12922
|
)
|
|
12879
12923
|
fallback_failure_kinds = [agent_failure_kind or fallback_reason] if fallback_reason else []
|
|
12924
|
+
fallback_attempt_errors = [(agent.agent_id, result.error)] if fallback_reason else []
|
|
12880
12925
|
_skip_fallback = False
|
|
12881
12926
|
if fallback_reason:
|
|
12882
12927
|
_pre_fallback_git = await self.process_manager._collect_git_info(workspace_path)
|
|
@@ -12949,6 +12994,7 @@ class RuntimeDaemon:
|
|
|
12949
12994
|
result = await self.process_manager.run_agent(
|
|
12950
12995
|
agent, task, workspace_path, on_chunk=on_output_chunk,
|
|
12951
12996
|
)
|
|
12997
|
+
fallback_attempt_errors.append((agent.agent_id, result.error))
|
|
12952
12998
|
finally:
|
|
12953
12999
|
progress_stop2.set()
|
|
12954
13000
|
ticker2.cancel()
|
|
@@ -12989,12 +13035,8 @@ class RuntimeDaemon:
|
|
|
12989
13035
|
)
|
|
12990
13036
|
|
|
12991
13037
|
if ProcessManager.is_agent_fallback_eligible(result):
|
|
12992
|
-
|
|
12993
|
-
fallback_failure_kinds,
|
|
12994
|
-
)
|
|
12995
|
-
result.error = (
|
|
12996
|
-
f"All agents {failure_label} (tried: {', '.join(tried_agents)}). "
|
|
12997
|
-
f"Original error: {result.error}"
|
|
13038
|
+
result.error = ProcessManager.format_all_agents_failure(
|
|
13039
|
+
fallback_failure_kinds, fallback_attempt_errors,
|
|
12998
13040
|
)
|
|
12999
13041
|
result.status = "failed"
|
|
13000
13042
|
result.failure_code = ProcessManager.all_agents_failure_code(
|
|
@@ -13139,6 +13181,9 @@ class RuntimeDaemon:
|
|
|
13139
13181
|
post_validation_failure_kinds = (
|
|
13140
13182
|
[post_validation_failure_kind] if post_validation_failure_kind else []
|
|
13141
13183
|
)
|
|
13184
|
+
post_validation_attempt_errors = (
|
|
13185
|
+
[(agent.agent_id, result.error)] if post_validation_failure_kind else []
|
|
13186
|
+
)
|
|
13142
13187
|
post_validation_failure_label = (
|
|
13143
13188
|
"rate-limited"
|
|
13144
13189
|
if post_validation_failure_kind == "rate_limit"
|
|
@@ -13172,6 +13217,7 @@ class RuntimeDaemon:
|
|
|
13172
13217
|
result = await self.process_manager.run_agent(
|
|
13173
13218
|
agent, task, workspace_path, on_chunk=on_output_chunk,
|
|
13174
13219
|
)
|
|
13220
|
+
post_validation_attempt_errors.append((agent.agent_id, result.error))
|
|
13175
13221
|
fallback_failure_kind = ProcessManager.agent_failure_kind(result)
|
|
13176
13222
|
if fallback_failure_kind:
|
|
13177
13223
|
post_validation_failure_kinds.append(fallback_failure_kind)
|
|
@@ -13207,12 +13253,8 @@ class RuntimeDaemon:
|
|
|
13207
13253
|
# If every agent we tried is still rate-limited, signal the server
|
|
13208
13254
|
# NOT to re-enqueue — it would hit the same quota wall immediately.
|
|
13209
13255
|
if ProcessManager.is_agent_fallback_eligible(result):
|
|
13210
|
-
|
|
13211
|
-
post_validation_failure_kinds,
|
|
13212
|
-
)
|
|
13213
|
-
result.error = (
|
|
13214
|
-
f"All agents {failure_label} (tried: {', '.join(tried_agents)}). "
|
|
13215
|
-
f"Original error: {result.error}"
|
|
13256
|
+
result.error = ProcessManager.format_all_agents_failure(
|
|
13257
|
+
post_validation_failure_kinds, post_validation_attempt_errors,
|
|
13216
13258
|
)
|
|
13217
13259
|
result.status = "failed"
|
|
13218
13260
|
result.failure_code = ProcessManager.all_agents_failure_code(
|
|
@@ -13810,6 +13852,16 @@ class RuntimeDaemon:
|
|
|
13810
13852
|
req_type = input_data.get("requirement_type", "feature")
|
|
13811
13853
|
verification_work_item = bool(input_data.get("verification_work_item"))
|
|
13812
13854
|
|
|
13855
|
+
if node_type == "coding":
|
|
13856
|
+
delivered_files = list(result.files_changed)
|
|
13857
|
+
delivered_files.extend(input_data.get("planned_task_predecessor_files") or [])
|
|
13858
|
+
for missing_path in agent_core.missing_planned_task_files(input_data, delivered_files):
|
|
13859
|
+
issues.append(
|
|
13860
|
+
f"Planned task file has no implementation evidence: {missing_path}. "
|
|
13861
|
+
"Complete the planned change or report the task as blocked; "
|
|
13862
|
+
"documentation-only changes do not complete a source-code task."
|
|
13863
|
+
)
|
|
13864
|
+
|
|
13813
13865
|
qa_policy, qa_policy_issues = _parse_frozen_qa_test_level_policy(
|
|
13814
13866
|
input_data.get("qa_test_level_policy_snapshot")
|
|
13815
13867
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|