forgexa-cli 1.57.5__tar.gz → 1.57.7__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.57.5 → forgexa_cli-1.57.7}/PKG-INFO +1 -1
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/daemon.py +222 -40
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/pyproject.toml +1 -1
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/README.md +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/setup.cfg +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.57.5 → forgexa_cli-1.57.7}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.57.
|
|
2
|
+
__version__ = "1.57.7"
|
|
@@ -1150,7 +1150,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1150
1150
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1151
1151
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1152
1152
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1153
|
-
DAEMON_VERSION = "1.57.
|
|
1153
|
+
DAEMON_VERSION = "1.57.7"
|
|
1154
1154
|
|
|
1155
1155
|
|
|
1156
1156
|
def _detect_client_type() -> str:
|
|
@@ -1736,6 +1736,7 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
|
1736
1736
|
result.stdout = ""
|
|
1737
1737
|
result.stderr = ""
|
|
1738
1738
|
result.observations = []
|
|
1739
|
+
result.metrics.pop("validation_issues", None)
|
|
1739
1740
|
critic_review = result.critic_review
|
|
1740
1741
|
if isinstance(critic_review, dict) and isinstance(critic_review.get("result"), dict):
|
|
1741
1742
|
result.critic_review = {
|
|
@@ -1755,8 +1756,11 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
|
|
|
1755
1756
|
}
|
|
1756
1757
|
if result.error:
|
|
1757
1758
|
result.error = (
|
|
1758
|
-
"
|
|
1759
|
-
"
|
|
1759
|
+
"Task verification is incomplete: acceptance criteria remain uncovered. "
|
|
1760
|
+
"Review the validation log in Forgexa Desktop; resolve the criteria and "
|
|
1761
|
+
"commit or stash local changes before retrying."
|
|
1762
|
+
if result.failure_code == "acceptance_criteria_uncovered"
|
|
1763
|
+
else "Local task failed. Review this task in Forgexa Desktop on the running device."
|
|
1760
1764
|
)
|
|
1761
1765
|
|
|
1762
1766
|
def _prepare_ai_job_output(
|
|
@@ -5503,6 +5507,10 @@ class _TaskAttachmentDownloadError(RuntimeError):
|
|
|
5503
5507
|
"""Raised when a required task attachment cannot be staged safely."""
|
|
5504
5508
|
|
|
5505
5509
|
|
|
5510
|
+
class _TaskAttachmentUnavailableError(_TaskAttachmentDownloadError):
|
|
5511
|
+
"""Raised when the attachment source is unavailable but the task can proceed."""
|
|
5512
|
+
|
|
5513
|
+
|
|
5506
5514
|
def _workspace_has_recent_activity(
|
|
5507
5515
|
workspace_path: "Path", since_monotonic: float, max_depth: int = 4
|
|
5508
5516
|
) -> bool:
|
|
@@ -5623,6 +5631,8 @@ class ProcessManager:
|
|
|
5623
5631
|
"authentication required",
|
|
5624
5632
|
"authenticate with the github",
|
|
5625
5633
|
"gh auth login",
|
|
5634
|
+
"your github token may be invalid",
|
|
5635
|
+
"rejected the cli github credential",
|
|
5626
5636
|
]
|
|
5627
5637
|
|
|
5628
5638
|
def __init__(self):
|
|
@@ -5726,7 +5736,9 @@ class ProcessManager:
|
|
|
5726
5736
|
@staticmethod
|
|
5727
5737
|
def agent_failure_kind(result: "TaskResult") -> str | None:
|
|
5728
5738
|
"""Classify a fallback-eligible failure without conflating its cause."""
|
|
5729
|
-
if result.failure_code in {
|
|
5739
|
+
if result.failure_code in {
|
|
5740
|
+
"all_agents_rate_limited", "all_agents_unavailable", "acceptance_criteria_uncovered",
|
|
5741
|
+
}:
|
|
5730
5742
|
return None
|
|
5731
5743
|
if result.failure_code == "agent_platform_unsupported":
|
|
5732
5744
|
return "agent_unavailable"
|
|
@@ -7634,6 +7646,59 @@ class ProcessManager:
|
|
|
7634
7646
|
return retry_result
|
|
7635
7647
|
_auth_state["persist"] = False
|
|
7636
7648
|
|
|
7649
|
+
auth_detail = " ".join(
|
|
7650
|
+
[*structured_errors, stderr]
|
|
7651
|
+
).lower()
|
|
7652
|
+
token_auth_error = (
|
|
7653
|
+
"your github token may be invalid" in auth_detail
|
|
7654
|
+
or "rejected the cli github credential" in auth_detail
|
|
7655
|
+
)
|
|
7656
|
+
if token_auth_error and not (copilot_exit == 0 and returncode == 0):
|
|
7657
|
+
_auth_state["persist"] = False
|
|
7658
|
+
if (
|
|
7659
|
+
token_auth_error
|
|
7660
|
+
and not (copilot_exit == 0 and returncode == 0)
|
|
7661
|
+
and not signals["has_meaningful_content"]
|
|
7662
|
+
and not self._copilot_called_any_tools(stdout)
|
|
7663
|
+
):
|
|
7664
|
+
recovery_home = (
|
|
7665
|
+
Path(_master_copilot_home)
|
|
7666
|
+
if auth_env_var and _master_copilot_home
|
|
7667
|
+
else agent_core.copilot_recovery_home(_isolated_copilot_home)
|
|
7668
|
+
)
|
|
7669
|
+
if (
|
|
7670
|
+
not _retry_without_token_env
|
|
7671
|
+
and recovery_home
|
|
7672
|
+
and (recovery_home / "config.json").is_file()
|
|
7673
|
+
):
|
|
7674
|
+
restored = await asyncio.to_thread(
|
|
7675
|
+
agent_core.replace_copilot_auth_state,
|
|
7676
|
+
recovery_home,
|
|
7677
|
+
_isolated_copilot_home,
|
|
7678
|
+
)
|
|
7679
|
+
can_retry_with_login = bool(auth_env_var or restored)
|
|
7680
|
+
else:
|
|
7681
|
+
can_retry_with_login = False
|
|
7682
|
+
if can_retry_with_login:
|
|
7683
|
+
logger.warning(
|
|
7684
|
+
"Copilot token authentication failed before work for task %s; "
|
|
7685
|
+
"retrying once with stored login and token environment variables removed",
|
|
7686
|
+
task_id,
|
|
7687
|
+
)
|
|
7688
|
+
retry_result = await self._run_copilot(
|
|
7689
|
+
agent, prompt, cwd, timeout, task_id, on_chunk,
|
|
7690
|
+
stream_stderr=stream_stderr,
|
|
7691
|
+
disable_builtin_mcps=disable_builtin_mcps,
|
|
7692
|
+
_retry_without_effort=_retry_without_effort,
|
|
7693
|
+
_retry_without_token_env=True,
|
|
7694
|
+
_isolated_copilot_home=_isolated_copilot_home,
|
|
7695
|
+
_auth_state=_auth_state,
|
|
7696
|
+
)
|
|
7697
|
+
_auth_state["persist"] = retry_result.status == "success"
|
|
7698
|
+
retry_result.metrics["copilot_auth_recovery"] = True
|
|
7699
|
+
return retry_result
|
|
7700
|
+
_auth_state["persist"] = False
|
|
7701
|
+
|
|
7637
7702
|
if copilot_exit == 0 and returncode == 0:
|
|
7638
7703
|
return TaskResult(
|
|
7639
7704
|
status="success",
|
|
@@ -7731,6 +7796,12 @@ class ProcessManager:
|
|
|
7731
7796
|
failure_error = f"Copilot exited with code {effective_rc}: {stderr[-500:].strip()}"
|
|
7732
7797
|
else:
|
|
7733
7798
|
failure_error = f"Copilot exited with code {effective_rc}"
|
|
7799
|
+
if token_auth_error:
|
|
7800
|
+
auth_source = auth_env_var or "stored Copilot login"
|
|
7801
|
+
failure_error = (
|
|
7802
|
+
f"Copilot authentication failed using {auth_source}. "
|
|
7803
|
+
f"{failure_error}"
|
|
7804
|
+
)
|
|
7734
7805
|
logger.error(
|
|
7735
7806
|
"Copilot task %s failed (process_rc=%s, result_exit=%s): %s",
|
|
7736
7807
|
task_id,
|
|
@@ -10568,9 +10639,22 @@ class RuntimeDaemon:
|
|
|
10568
10639
|
timeout=httpx.Timeout(connect=10.0, read=30.0, write=10.0, pool=10.0),
|
|
10569
10640
|
) as response:
|
|
10570
10641
|
if not 200 <= response.status_code < 300:
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10642
|
+
error = f"Attachment '{attachment_name}' download failed with HTTP {response.status_code}"
|
|
10643
|
+
if response.status_code in {409, 502}:
|
|
10644
|
+
await response.aread()
|
|
10645
|
+
try:
|
|
10646
|
+
detail = response.json()
|
|
10647
|
+
except ValueError:
|
|
10648
|
+
detail = None
|
|
10649
|
+
if isinstance(detail, dict) and detail.get("detail") == "Task attachment integrity check failed":
|
|
10650
|
+
raise _TaskAttachmentDownloadError(error)
|
|
10651
|
+
if response.status_code == 409 and isinstance(detail, dict) and detail.get("detail") == "Task attachment source no longer matches stored checksum":
|
|
10652
|
+
raise _TaskAttachmentUnavailableError(
|
|
10653
|
+
f"Attachment '{attachment_name}' is unavailable: {detail['detail']}"
|
|
10654
|
+
)
|
|
10655
|
+
if response.status_code in {404, 502, 503, 504}:
|
|
10656
|
+
raise _TaskAttachmentUnavailableError(error)
|
|
10657
|
+
raise _TaskAttachmentDownloadError(error)
|
|
10574
10658
|
content_length = response.headers.get("content-length")
|
|
10575
10659
|
if content_length:
|
|
10576
10660
|
try:
|
|
@@ -10604,6 +10688,10 @@ class RuntimeDaemon:
|
|
|
10604
10688
|
temporary_path.replace(destination)
|
|
10605
10689
|
except _TaskAttachmentDownloadError:
|
|
10606
10690
|
raise
|
|
10691
|
+
except httpx.RequestError as exc:
|
|
10692
|
+
raise _TaskAttachmentUnavailableError(
|
|
10693
|
+
f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
|
|
10694
|
+
) from exc
|
|
10607
10695
|
except httpx.HTTPError as exc:
|
|
10608
10696
|
raise _TaskAttachmentDownloadError(
|
|
10609
10697
|
f"Attachment '{attachment_name}' download failed ({type(exc).__name__})"
|
|
@@ -10619,7 +10707,7 @@ class RuntimeDaemon:
|
|
|
10619
10707
|
timeout=timeout_seconds,
|
|
10620
10708
|
)
|
|
10621
10709
|
except asyncio.TimeoutError as exc:
|
|
10622
|
-
raise
|
|
10710
|
+
raise _TaskAttachmentUnavailableError(
|
|
10623
10711
|
f"Attachment '{attachment_name}' download timed out after "
|
|
10624
10712
|
f"{timeout_seconds:.0f}s"
|
|
10625
10713
|
) from exc
|
|
@@ -10642,6 +10730,7 @@ class RuntimeDaemon:
|
|
|
10642
10730
|
|
|
10643
10731
|
staging_dir = Path(tempfile.mkdtemp(prefix=f"forgexa-attachments-{task.task_id[:8]}-"))
|
|
10644
10732
|
staged: list[tuple[str, list[str], str]] = []
|
|
10733
|
+
unavailable: list[tuple[str, list[str], str]] = []
|
|
10645
10734
|
deadline = time.monotonic() + self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS
|
|
10646
10735
|
remaining_bytes = self._TASK_ATTACHMENT_MAX_TOTAL_BYTES
|
|
10647
10736
|
try:
|
|
@@ -10670,12 +10759,6 @@ class RuntimeDaemon:
|
|
|
10670
10759
|
raise _TaskAttachmentDownloadError(
|
|
10671
10760
|
f"Attachment '{file_name}' cannot be staged securely because it has no artifact ID"
|
|
10672
10761
|
)
|
|
10673
|
-
remaining_seconds = deadline - time.monotonic()
|
|
10674
|
-
if remaining_seconds <= 0:
|
|
10675
|
-
raise _TaskAttachmentDownloadError(
|
|
10676
|
-
f"Task attachment staging timed out after "
|
|
10677
|
-
f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
|
|
10678
|
-
)
|
|
10679
10762
|
if not conn.runtime_id:
|
|
10680
10763
|
raise _TaskAttachmentDownloadError(
|
|
10681
10764
|
f"Attachment '{file_name}' cannot be staged because this runtime is not registered"
|
|
@@ -10684,16 +10767,27 @@ class RuntimeDaemon:
|
|
|
10684
10767
|
f"{conn.server_url.rstrip('/')}/api/v1/runtimes/{conn.runtime_id}/"
|
|
10685
10768
|
f"tasks/{task.task_id}/attachments/{artifact_id}"
|
|
10686
10769
|
)
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
10694
|
-
|
|
10695
|
-
|
|
10696
|
-
|
|
10770
|
+
try:
|
|
10771
|
+
remaining_seconds = deadline - time.monotonic()
|
|
10772
|
+
if remaining_seconds <= 0:
|
|
10773
|
+
raise _TaskAttachmentUnavailableError(
|
|
10774
|
+
f"Task attachment staging timed out after "
|
|
10775
|
+
f"{self._TASK_ATTACHMENT_DOWNLOAD_TIMEOUT_SECONDS}s"
|
|
10776
|
+
)
|
|
10777
|
+
await self._download_task_attachment(
|
|
10778
|
+
conn.client,
|
|
10779
|
+
source_url,
|
|
10780
|
+
destination,
|
|
10781
|
+
attachment_name=file_name,
|
|
10782
|
+
expected_size=expected_size or None,
|
|
10783
|
+
expected_checksum=str(attachment.get("checksum") or ""),
|
|
10784
|
+
max_bytes=min(self._TASK_ATTACHMENT_MAX_BYTES, remaining_bytes),
|
|
10785
|
+
timeout_seconds=remaining_seconds,
|
|
10786
|
+
)
|
|
10787
|
+
except _TaskAttachmentUnavailableError as exc:
|
|
10788
|
+
logger.warning("Task %s attachment unavailable: %s", task.task_id, exc)
|
|
10789
|
+
unavailable.append((file_name, download_url_aliases, str(exc)))
|
|
10790
|
+
continue
|
|
10697
10791
|
remaining_bytes -= destination.stat().st_size
|
|
10698
10792
|
staged.append((file_name, download_url_aliases, str(destination)))
|
|
10699
10793
|
except Exception:
|
|
@@ -10701,10 +10795,29 @@ class RuntimeDaemon:
|
|
|
10701
10795
|
raise
|
|
10702
10796
|
|
|
10703
10797
|
task.input_prompt = self._rewrite_staged_attachment_references(task.input_prompt, staged)
|
|
10798
|
+
if unavailable:
|
|
10799
|
+
notices = []
|
|
10800
|
+
for file_name, download_urls, error in unavailable:
|
|
10801
|
+
notices.append(f"- `{file_name}`: {error}")
|
|
10802
|
+
for download_url in download_urls:
|
|
10803
|
+
command_pattern = r"`curl -L '" + re.escape(download_url) + r"' -o /tmp/[^`]+`"
|
|
10804
|
+
task.input_prompt = re.sub(command_pattern, "[attachment unavailable]", task.input_prompt)
|
|
10805
|
+
task.input_prompt = task.input_prompt.replace(f"Download: {download_url}", "Attachment unavailable")
|
|
10806
|
+
task.input_prompt = task.input_prompt.replace(download_url, "[attachment unavailable]")
|
|
10807
|
+
task.input_prompt = "\n".join([
|
|
10808
|
+
"## Unavailable Attachments",
|
|
10809
|
+
"These files could not be retrieved. Continue with available inputs and report any missing evidence; do not retry their download URLs.",
|
|
10810
|
+
*notices,
|
|
10811
|
+
"",
|
|
10812
|
+
task.input_prompt,
|
|
10813
|
+
])
|
|
10704
10814
|
input_data = dict(task.input_data or {})
|
|
10705
10815
|
input_data["prompt"] = task.input_prompt
|
|
10706
10816
|
task.input_data = input_data
|
|
10707
10817
|
logger.info("Prepared %d attachment(s) for task %s", len(staged), task.task_id)
|
|
10818
|
+
if not staged:
|
|
10819
|
+
shutil.rmtree(staging_dir, ignore_errors=True)
|
|
10820
|
+
return None
|
|
10708
10821
|
return staging_dir
|
|
10709
10822
|
|
|
10710
10823
|
@staticmethod
|
|
@@ -13052,7 +13165,7 @@ class RuntimeDaemon:
|
|
|
13052
13165
|
# are *optional*: if they exist they are validated, but their
|
|
13053
13166
|
# absence is not an error (the agent only writes regression
|
|
13054
13167
|
# tests + test-report.md).
|
|
13055
|
-
_skip_test_artifacts =
|
|
13168
|
+
_skip_test_artifacts = req_type == "task"
|
|
13056
13169
|
_requires_structured_artifacts = False
|
|
13057
13170
|
# Scope-adaptive pipeline (Phase 1): a content_only change
|
|
13058
13171
|
# scope (analysis-predicted, machine-verified server-side)
|
|
@@ -13074,19 +13187,28 @@ class RuntimeDaemon:
|
|
|
13074
13187
|
# importable (standalone vendored daemon execution).
|
|
13075
13188
|
_skip_test_artifacts = True
|
|
13076
13189
|
|
|
13077
|
-
#
|
|
13078
|
-
#
|
|
13079
|
-
#
|
|
13080
|
-
|
|
13081
|
-
|
|
13082
|
-
|
|
13083
|
-
|
|
13084
|
-
|
|
13190
|
+
# Keep e2e evidence validation active for any requirement type.
|
|
13191
|
+
# An enforced policy requires artifacts only when the analysis
|
|
13192
|
+
# contract expects test intent or the analysis supplied one.
|
|
13193
|
+
_analysis_dir = (
|
|
13194
|
+
input_data.get("analysis_output_dir")
|
|
13195
|
+
or (input_data.get("context") or {}).get("analysis_output_dir")
|
|
13196
|
+
or ""
|
|
13197
|
+
)
|
|
13198
|
+
_has_test_intent = bool(_analysis_dir) and (
|
|
13199
|
+
workspace_path / str(_analysis_dir) / "test-intent.json"
|
|
13200
|
+
).is_file()
|
|
13085
13201
|
if (
|
|
13086
13202
|
_skip_test_artifacts
|
|
13087
13203
|
and _change_scope != "content_only"
|
|
13088
13204
|
and (
|
|
13089
|
-
(
|
|
13205
|
+
(
|
|
13206
|
+
(qa_policy or {}).get("enforcement") == "enforce"
|
|
13207
|
+
and (
|
|
13208
|
+
"test-intent.json" in _get_analysis_outputs_for_type(req_type, _change_scope)
|
|
13209
|
+
or _has_test_intent
|
|
13210
|
+
)
|
|
13211
|
+
)
|
|
13090
13212
|
or _testing_intents_have_e2e(workspace_path, input_data)
|
|
13091
13213
|
)
|
|
13092
13214
|
):
|
|
@@ -13261,15 +13383,30 @@ class RuntimeDaemon:
|
|
|
13261
13383
|
))
|
|
13262
13384
|
|
|
13263
13385
|
# --- coverage-matrix.json validation ---
|
|
13386
|
+
if req_type == "task":
|
|
13387
|
+
result.metrics.pop("acceptance_criteria_manual_review_required", None)
|
|
13264
13388
|
cm_path = base / "coverage-matrix.json"
|
|
13265
13389
|
if cm_path.exists():
|
|
13266
13390
|
try:
|
|
13267
|
-
|
|
13268
|
-
|
|
13269
|
-
|
|
13270
|
-
|
|
13271
|
-
|
|
13272
|
-
|
|
13391
|
+
cm_content = cm_path.read_text(encoding="utf-8")
|
|
13392
|
+
cm_data = _json.loads(cm_content)
|
|
13393
|
+
if req_type == "task":
|
|
13394
|
+
coverage = runtime_evidence.parse_acceptance_criteria_coverage_artifacts(
|
|
13395
|
+
_json.dumps(_tc_data) if _tc_data is not None else None,
|
|
13396
|
+
cm_content,
|
|
13397
|
+
)
|
|
13398
|
+
links = coverage.get("coverage_links") or []
|
|
13399
|
+
if coverage.get("collection_status") != "completed" or not links:
|
|
13400
|
+
reason = coverage.get("failure_reason") or "coverage_matrix_empty"
|
|
13401
|
+
issues.append(f"coverage-matrix.json has invalid acceptance criteria: {reason}")
|
|
13402
|
+
else:
|
|
13403
|
+
result.metrics["acceptance_criteria_manual_review_required"] = True
|
|
13404
|
+
else:
|
|
13405
|
+
ac_list = cm_data.get("acceptance_criteria", [])
|
|
13406
|
+
uncovered = [ac for ac in ac_list if ac.get("status") != "covered"]
|
|
13407
|
+
if uncovered:
|
|
13408
|
+
ids = ", ".join(ac.get("id", "?") for ac in uncovered[:5])
|
|
13409
|
+
issues.append(f"Uncovered acceptance criteria in coverage-matrix.json: {ids}")
|
|
13273
13410
|
except (_json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
13274
13411
|
issues.append(f"coverage-matrix.json is not valid JSON: {e}")
|
|
13275
13412
|
elif _requires_structured_artifacts or _requires_e2e_artifacts:
|
|
@@ -14693,6 +14830,29 @@ class RuntimeDaemon:
|
|
|
14693
14830
|
)
|
|
14694
14831
|
return result # All validations passed
|
|
14695
14832
|
|
|
14833
|
+
if getattr(task, "workspace_resolution", None) == "local_bind":
|
|
14834
|
+
_append_local_bind_output_log(task.task_id, [
|
|
14835
|
+
"[daemon] Output validation found these issues:",
|
|
14836
|
+
*[f"[daemon] {issue}" for issue in issues],
|
|
14837
|
+
])
|
|
14838
|
+
|
|
14839
|
+
if (
|
|
14840
|
+
task.node_type == "testing"
|
|
14841
|
+
and (task.input_data or {}).get("requirement_type") == "task"
|
|
14842
|
+
and all(
|
|
14843
|
+
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
14844
|
+
for issue in issues
|
|
14845
|
+
)
|
|
14846
|
+
):
|
|
14847
|
+
result.status = "failed"
|
|
14848
|
+
result.failure_code = "acceptance_criteria_uncovered"
|
|
14849
|
+
result.error = f"Task verification is incomplete: {issues[0]}"
|
|
14850
|
+
result.metrics["validation_issues"] = issues
|
|
14851
|
+
self._record_testing_telemetry(
|
|
14852
|
+
workspace_path, task, result, retries_used=attempt
|
|
14853
|
+
)
|
|
14854
|
+
return result
|
|
14855
|
+
|
|
14696
14856
|
issues_text = "\n".join(f"- {iss}" for iss in issues)
|
|
14697
14857
|
logger.warning(
|
|
14698
14858
|
"Validation gate: %d issues found for task %s (attempt %d/%d):\n%s",
|
|
@@ -14786,6 +14946,27 @@ class RuntimeDaemon:
|
|
|
14786
14946
|
)
|
|
14787
14947
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
14788
14948
|
if remaining:
|
|
14949
|
+
if getattr(task, "workspace_resolution", None) == "local_bind":
|
|
14950
|
+
_append_local_bind_output_log(task.task_id, [
|
|
14951
|
+
"[daemon] Output validation remains incomplete:",
|
|
14952
|
+
*[f"[daemon] {issue}" for issue in remaining],
|
|
14953
|
+
])
|
|
14954
|
+
if (
|
|
14955
|
+
task.node_type == "testing"
|
|
14956
|
+
and (task.input_data or {}).get("requirement_type") == "task"
|
|
14957
|
+
and all(
|
|
14958
|
+
issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
|
|
14959
|
+
for issue in remaining
|
|
14960
|
+
)
|
|
14961
|
+
):
|
|
14962
|
+
result.status = "failed"
|
|
14963
|
+
result.failure_code = "acceptance_criteria_uncovered"
|
|
14964
|
+
result.error = f"Task verification is incomplete: {remaining[0]}"
|
|
14965
|
+
result.metrics["validation_issues"] = remaining
|
|
14966
|
+
self._record_testing_telemetry(
|
|
14967
|
+
workspace_path, task, result, retries_used=max_retries
|
|
14968
|
+
)
|
|
14969
|
+
return result
|
|
14789
14970
|
# Distinguish blocking artifact-contract failures from advisory syntax
|
|
14790
14971
|
# warnings. Evidence coverage and references are release-blocking even
|
|
14791
14972
|
# when the file itself exists.
|
|
@@ -14799,6 +14980,7 @@ class RuntimeDaemon:
|
|
|
14799
14980
|
"not found",
|
|
14800
14981
|
"is empty",
|
|
14801
14982
|
"not valid json",
|
|
14983
|
+
"invalid acceptance criteria",
|
|
14802
14984
|
"case_evidence",
|
|
14803
14985
|
"test-evidence.json has no",
|
|
14804
14986
|
"evidence item",
|
|
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
|