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.
Files changed (21) hide show
  1. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/daemon.py +177 -17
  4. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/pyproject.toml +1 -1
  6. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/README.md +0 -0
  7. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/autoupgrade.py +0 -0
  9. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/main.py +0 -0
  10. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli/py.typed +0 -0
  11. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  12. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  13. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  14. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/requires.txt +0 -0
  15. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  16. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/setup.cfg +0 -0
  17. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_auth_and_runtime_commands.py +0 -0
  18. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_autoupgrade.py +0 -0
  19. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_check_command.py +0 -0
  20. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_silent_install.py +0 -0
  21. {forgexa_cli-1.22.2 → forgexa_cli-1.22.3}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.22.2
3
+ Version: 1.22.3
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.22.2"
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.2"
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") or []
7729
- covered_ids = {
7730
- str(entry.get("test_case_id"))
7731
- for entry in case_evidence
7732
- if isinstance(entry, dict) and entry.get("test_case_id")
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._validate_and_retry(
9039
+ result, agent = await self._validate_with_agent_fallback(
8973
9040
  agent, task, workspace_path, result,
8974
- reporter, on_output_chunk, max_retries=2,
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._validate_and_retry(
9093
+ result, agent = await self._validate_with_agent_fallback(
9027
9094
  agent, task, workspace_path, result,
9028
- reporter, on_output_chunk, max_retries=2,
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"] = 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. If issues are found, builds a fix prompt listing
10089
- all problems and re-invokes the same agent.
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 critical issues (no output produced) from minor ones (syntax)
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 = ("missing", "not found", "is empty")
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
- f"Agent failed to produce required output after {max_retries} retries: "
10392
+ "Agent failed to satisfy required output validation: "
10233
10393
  + "; ".join(critical_issues[:3])
10234
10394
  )
10235
10395
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.22.2
3
+ Version: 1.22.3
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.22.2"
3
+ version = "1.22.3"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes