forgexa-cli 1.22.2__tar.gz → 1.22.3__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.22.2 → forgexa_cli-1.22.3}/PKG-INFO +1 -1
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/daemon.py +177 -17
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/pyproject.toml +1 -1
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/README.md +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/setup.cfg +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.22.
|
|
2
|
+
__version__ = "1.22.3"
|
|
@@ -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.22.
|
|
558
|
+
DAEMON_VERSION = "1.22.3"
|
|
559
559
|
|
|
560
560
|
|
|
561
561
|
def _detect_client_type() -> str:
|
|
@@ -1012,6 +1012,8 @@ class TaskResult:
|
|
|
1012
1012
|
# Key values:
|
|
1013
1013
|
# "all_agents_rate_limited" — daemon tried every installed agent, all
|
|
1014
1014
|
# hit rate/quota limits. Server must NOT retry on the same runtime.
|
|
1015
|
+
# "validation_retry_exhausted" — required-output validation still failed
|
|
1016
|
+
# after the allowed repair agents. Server must not auto-retry it.
|
|
1015
1017
|
failure_code: str = ""
|
|
1016
1018
|
# Optional structured preflight result for project/runtime preflight failures.
|
|
1017
1019
|
preflight: dict | None = None
|
|
@@ -7692,6 +7694,22 @@ def _extract_testing_artifacts(workspace_path: Path, output_dir: str) -> dict |
|
|
|
7692
7694
|
return None
|
|
7693
7695
|
|
|
7694
7696
|
|
|
7697
|
+
def _case_evidence_entries(value: Any) -> list[dict[str, Any]]:
|
|
7698
|
+
"""Normalize list and keyed-object case_evidence forms into entries."""
|
|
7699
|
+
if isinstance(value, list):
|
|
7700
|
+
return [entry for entry in value if isinstance(entry, dict)]
|
|
7701
|
+
if isinstance(value, dict):
|
|
7702
|
+
entries: list[dict[str, Any]] = []
|
|
7703
|
+
for case_id, entry in value.items():
|
|
7704
|
+
if not isinstance(entry, dict):
|
|
7705
|
+
continue
|
|
7706
|
+
normalized = dict(entry)
|
|
7707
|
+
normalized.setdefault("test_case_id", str(case_id))
|
|
7708
|
+
entries.append(normalized)
|
|
7709
|
+
return entries
|
|
7710
|
+
return []
|
|
7711
|
+
|
|
7712
|
+
|
|
7695
7713
|
def _validate_test_evidence(
|
|
7696
7714
|
base: Path, workspace_path: Path, e2e_case_ids: list[str], max_anchor_checks: int = 50
|
|
7697
7715
|
) -> list[str]:
|
|
@@ -7725,12 +7743,13 @@ def _validate_test_evidence(
|
|
|
7725
7743
|
"must ground their selectors/routes in source-file evidence."
|
|
7726
7744
|
)
|
|
7727
7745
|
|
|
7728
|
-
case_evidence = ev.get("case_evidence")
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
if
|
|
7733
|
-
|
|
7746
|
+
case_evidence = _case_evidence_entries(ev.get("case_evidence"))
|
|
7747
|
+
entries_by_case_id: dict[str, list[dict[str, Any]]] = {}
|
|
7748
|
+
for entry in case_evidence:
|
|
7749
|
+
case_id = str(entry.get("test_case_id") or "").strip()
|
|
7750
|
+
if case_id:
|
|
7751
|
+
entries_by_case_id.setdefault(case_id, []).append(entry)
|
|
7752
|
+
covered_ids = set(entries_by_case_id)
|
|
7734
7753
|
missing_cases = [cid for cid in e2e_case_ids if cid not in covered_ids]
|
|
7735
7754
|
if missing_cases:
|
|
7736
7755
|
issues.append(
|
|
@@ -7739,6 +7758,54 @@ def _validate_test_evidence(
|
|
|
7739
7758
|
f"with route_refs/locator_refs."
|
|
7740
7759
|
)
|
|
7741
7760
|
|
|
7761
|
+
route_ids = {
|
|
7762
|
+
str(route.get("id"))
|
|
7763
|
+
for route in routes
|
|
7764
|
+
if isinstance(route, dict) and route.get("id")
|
|
7765
|
+
}
|
|
7766
|
+
locator_ids = {
|
|
7767
|
+
str(locator.get("id"))
|
|
7768
|
+
for locator in locators
|
|
7769
|
+
if isinstance(locator, dict) and locator.get("id")
|
|
7770
|
+
}
|
|
7771
|
+
for case_id in e2e_case_ids:
|
|
7772
|
+
entries = entries_by_case_id.get(case_id, [])
|
|
7773
|
+
if not entries:
|
|
7774
|
+
continue
|
|
7775
|
+
if len(entries) > 1:
|
|
7776
|
+
issues.append(
|
|
7777
|
+
f"test-evidence.json has multiple case_evidence entries for e2e test case "
|
|
7778
|
+
f"{case_id}. Keep exactly one entry per test case."
|
|
7779
|
+
)
|
|
7780
|
+
continue
|
|
7781
|
+
entry = entries[0]
|
|
7782
|
+
route_refs = entry.get("route_refs")
|
|
7783
|
+
locator_refs = entry.get("locator_refs")
|
|
7784
|
+
if not isinstance(route_refs, list) or not isinstance(locator_refs, list):
|
|
7785
|
+
issues.append(
|
|
7786
|
+
f"test-evidence.json case_evidence for {case_id} must define "
|
|
7787
|
+
"route_refs and locator_refs as arrays."
|
|
7788
|
+
)
|
|
7789
|
+
continue
|
|
7790
|
+
if not route_refs and not locator_refs:
|
|
7791
|
+
issues.append(
|
|
7792
|
+
f"test-evidence.json case_evidence for {case_id} must reference at least "
|
|
7793
|
+
"one route or locator."
|
|
7794
|
+
)
|
|
7795
|
+
continue
|
|
7796
|
+
unknown_routes = [str(ref) for ref in route_refs if str(ref) not in route_ids]
|
|
7797
|
+
unknown_locators = [str(ref) for ref in locator_refs if str(ref) not in locator_ids]
|
|
7798
|
+
if unknown_routes:
|
|
7799
|
+
issues.append(
|
|
7800
|
+
f"test-evidence.json case_evidence for {case_id} references unknown "
|
|
7801
|
+
f"route_refs: {unknown_routes[:10]}."
|
|
7802
|
+
)
|
|
7803
|
+
if unknown_locators:
|
|
7804
|
+
issues.append(
|
|
7805
|
+
f"test-evidence.json case_evidence for {case_id} references unknown "
|
|
7806
|
+
f"locator_refs: {unknown_locators[:10]}."
|
|
7807
|
+
)
|
|
7808
|
+
|
|
7742
7809
|
checked = 0
|
|
7743
7810
|
for item in list(locators) + list(routes):
|
|
7744
7811
|
if checked >= max_anchor_checks:
|
|
@@ -8969,9 +9036,9 @@ class RuntimeDaemon:
|
|
|
8969
9036
|
# 4.5 Layer 2: Validation gate — check outputs before committing
|
|
8970
9037
|
if result.status == "success":
|
|
8971
9038
|
try:
|
|
8972
|
-
result = await self.
|
|
9039
|
+
result, agent = await self._validate_with_agent_fallback(
|
|
8973
9040
|
agent, task, workspace_path, result,
|
|
8974
|
-
reporter, on_output_chunk,
|
|
9041
|
+
reporter, on_output_chunk, tried_agents=tried_agents,
|
|
8975
9042
|
default_branch=default_branch, before_sha=node_before_sha,
|
|
8976
9043
|
)
|
|
8977
9044
|
# Re-collect git info if validation triggered retries
|
|
@@ -9023,9 +9090,9 @@ class RuntimeDaemon:
|
|
|
9023
9090
|
# failure) — re-run the validation gate and update git state.
|
|
9024
9091
|
if result.status == "success":
|
|
9025
9092
|
try:
|
|
9026
|
-
result = await self.
|
|
9093
|
+
result, agent = await self._validate_with_agent_fallback(
|
|
9027
9094
|
agent, task, workspace_path, result,
|
|
9028
|
-
reporter, on_output_chunk,
|
|
9095
|
+
reporter, on_output_chunk, tried_agents=tried_agents,
|
|
9029
9096
|
default_branch=default_branch, before_sha=node_before_sha,
|
|
9030
9097
|
)
|
|
9031
9098
|
pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
|
|
@@ -9230,7 +9297,11 @@ class RuntimeDaemon:
|
|
|
9230
9297
|
result.metrics["actual_agent"] = agent.agent_id
|
|
9231
9298
|
if agent.agent_id != task.agent_type:
|
|
9232
9299
|
result.metrics["original_agent"] = task.agent_type
|
|
9233
|
-
result.metrics["fallback_reason"] =
|
|
9300
|
+
result.metrics["fallback_reason"] = (
|
|
9301
|
+
result.metrics.get("fallback_reason")
|
|
9302
|
+
or fallback_reason
|
|
9303
|
+
or "validation_retry_exhausted"
|
|
9304
|
+
)
|
|
9234
9305
|
await reporter.report_progress(task.task_id, 100, "completed" if result.status == "success" else "failed")
|
|
9235
9306
|
await reporter.report_complete(task.task_id, result)
|
|
9236
9307
|
|
|
@@ -10069,6 +10140,78 @@ class RuntimeDaemon:
|
|
|
10069
10140
|
|
|
10070
10141
|
return info
|
|
10071
10142
|
|
|
10143
|
+
async def _validate_with_agent_fallback(
|
|
10144
|
+
self,
|
|
10145
|
+
agent: "DiscoveredAgent",
|
|
10146
|
+
task: TaskInfo,
|
|
10147
|
+
workspace_path: Path,
|
|
10148
|
+
result: TaskResult,
|
|
10149
|
+
reporter: "ProgressReporter",
|
|
10150
|
+
on_chunk: Any,
|
|
10151
|
+
tried_agents: set[str],
|
|
10152
|
+
default_branch: str = "",
|
|
10153
|
+
before_sha: str = "",
|
|
10154
|
+
) -> tuple[TaskResult, "DiscoveredAgent"]:
|
|
10155
|
+
"""Repair blocking validation failures without repeating an agent blindly."""
|
|
10156
|
+
tried_agents.add(agent.agent_id)
|
|
10157
|
+
result = await self._validate_and_retry(
|
|
10158
|
+
agent,
|
|
10159
|
+
task,
|
|
10160
|
+
workspace_path,
|
|
10161
|
+
result,
|
|
10162
|
+
reporter,
|
|
10163
|
+
on_chunk,
|
|
10164
|
+
max_retries=1,
|
|
10165
|
+
default_branch=default_branch,
|
|
10166
|
+
before_sha=before_sha,
|
|
10167
|
+
)
|
|
10168
|
+
if result.status != "failed" or result.failure_code != "validation_retry_exhausted":
|
|
10169
|
+
return result, agent
|
|
10170
|
+
|
|
10171
|
+
fallback_agent = self._select_fallback_agent(
|
|
10172
|
+
agent.agent_id,
|
|
10173
|
+
task.fallback_chain,
|
|
10174
|
+
tried_agents,
|
|
10175
|
+
)
|
|
10176
|
+
if not fallback_agent:
|
|
10177
|
+
result.metrics["validation_agents_tried"] = sorted(tried_agents)
|
|
10178
|
+
result.metrics["validation_agent_fallback_available"] = False
|
|
10179
|
+
result.error = (
|
|
10180
|
+
f"{result.error} No alternate agent is available on this runtime; "
|
|
10181
|
+
"retry after selecting a different agent or runtime."
|
|
10182
|
+
)
|
|
10183
|
+
return result, agent
|
|
10184
|
+
|
|
10185
|
+
tried_agents.add(fallback_agent.agent_id)
|
|
10186
|
+
await reporter.report_progress(
|
|
10187
|
+
task.task_id,
|
|
10188
|
+
92,
|
|
10189
|
+
f"validation_agent_fallback:{fallback_agent.agent_id}",
|
|
10190
|
+
output_lines=[
|
|
10191
|
+
"[daemon] Required-output validation still failed after the first repair, "
|
|
10192
|
+
f"switching from {agent.agent_id} to {fallback_agent.agent_id}.",
|
|
10193
|
+
],
|
|
10194
|
+
)
|
|
10195
|
+
previous_retries = int(result.metrics.get("validation_retries_used", 0) or 0)
|
|
10196
|
+
result = await self._validate_and_retry(
|
|
10197
|
+
fallback_agent,
|
|
10198
|
+
task,
|
|
10199
|
+
workspace_path,
|
|
10200
|
+
result,
|
|
10201
|
+
reporter,
|
|
10202
|
+
on_chunk,
|
|
10203
|
+
max_retries=1,
|
|
10204
|
+
default_branch=default_branch,
|
|
10205
|
+
before_sha=before_sha,
|
|
10206
|
+
)
|
|
10207
|
+
result.metrics["validation_agents_tried"] = sorted(tried_agents)
|
|
10208
|
+
result.metrics["validation_agent_fallback_available"] = True
|
|
10209
|
+
result.metrics["validation_retries_used"] = previous_retries + int(
|
|
10210
|
+
result.metrics.get("validation_retries_used", 0) or 0
|
|
10211
|
+
)
|
|
10212
|
+
result.metrics["fallback_reason"] = "validation_retry_exhausted"
|
|
10213
|
+
return result, fallback_agent
|
|
10214
|
+
|
|
10072
10215
|
async def _validate_and_retry(
|
|
10073
10216
|
self,
|
|
10074
10217
|
agent: "DiscoveredAgent",
|
|
@@ -10085,8 +10228,10 @@ class RuntimeDaemon:
|
|
|
10085
10228
|
|
|
10086
10229
|
Layer 2 of the reflection mechanism. Runs deterministic checks
|
|
10087
10230
|
(file existence, syntax, JSON validity) after agent completion but
|
|
10088
|
-
before git commit.
|
|
10089
|
-
|
|
10231
|
+
before git commit. If issues are found, builds a fix prompt listing all
|
|
10232
|
+
problems and gives the current agent one focused repair attempt. The
|
|
10233
|
+
caller routes a still-blocking failure to a different agent when one is
|
|
10234
|
+
available.
|
|
10090
10235
|
|
|
10091
10236
|
Returns the (possibly updated) TaskResult.
|
|
10092
10237
|
"""
|
|
@@ -10204,13 +10349,27 @@ class RuntimeDaemon:
|
|
|
10204
10349
|
)
|
|
10205
10350
|
remaining = self._validate_outputs(workspace_path, task, result)
|
|
10206
10351
|
if remaining:
|
|
10207
|
-
# Distinguish
|
|
10352
|
+
# Distinguish blocking artifact-contract failures from advisory syntax
|
|
10353
|
+
# warnings. Evidence coverage and references are release-blocking even
|
|
10354
|
+
# when the file itself exists.
|
|
10208
10355
|
scope_violations = [
|
|
10209
10356
|
issue
|
|
10210
10357
|
for issue in remaining
|
|
10211
10358
|
if issue.startswith("Verification work item modified requirement analysis assets:")
|
|
10212
10359
|
]
|
|
10213
|
-
critical_patterns = (
|
|
10360
|
+
critical_patterns = (
|
|
10361
|
+
"missing",
|
|
10362
|
+
"not found",
|
|
10363
|
+
"is empty",
|
|
10364
|
+
"not valid json",
|
|
10365
|
+
"case_evidence",
|
|
10366
|
+
"test-evidence.json has no",
|
|
10367
|
+
"evidence item",
|
|
10368
|
+
"anchor not found",
|
|
10369
|
+
"uncovered acceptance criteria",
|
|
10370
|
+
"no p0 priority test cases",
|
|
10371
|
+
"contains no test cases",
|
|
10372
|
+
)
|
|
10214
10373
|
critical_issues = [
|
|
10215
10374
|
iss for iss in remaining
|
|
10216
10375
|
if any(p in iss.lower() for p in critical_patterns)
|
|
@@ -10228,8 +10387,9 @@ class RuntimeDaemon:
|
|
|
10228
10387
|
result.failure_code = "verification_work_item_scope_violation"
|
|
10229
10388
|
result.error = "; ".join(scope_violations[:3])
|
|
10230
10389
|
else:
|
|
10390
|
+
result.failure_code = "validation_retry_exhausted"
|
|
10231
10391
|
result.error = (
|
|
10232
|
-
|
|
10392
|
+
"Agent failed to satisfy required output validation: "
|
|
10233
10393
|
+ "; ".join(critical_issues[:3])
|
|
10234
10394
|
)
|
|
10235
10395
|
else:
|
|
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
|