forgexa-cli 1.83.0__tar.gz → 1.84.0__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 (34) hide show
  1. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/PKG-INFO +3 -3
  2. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/README.md +2 -2
  3. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/__init__.py +1 -1
  4. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/agent_core.py +56 -5
  5. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/daemon.py +145 -70
  6. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/PKG-INFO +3 -3
  7. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/pyproject.toml +1 -1
  8. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_check_command.py +3 -3
  9. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/aws_logs_evidence.py +0 -0
  14. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/main.py +0 -0
  15. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/observability_contract.py +0 -0
  16. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/observability_evidence.py +0 -0
  17. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/py.typed +0 -0
  18. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/runtime_evidence.py +0 -0
  19. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli/zcode_protocol.py +0 -0
  20. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  21. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  22. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  23. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/requires.txt +0 -0
  24. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  25. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/setup.cfg +0 -0
  26. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_auth_and_runtime_commands.py +0 -0
  27. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_autoupgrade.py +0 -0
  28. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.83.0 → forgexa_cli-1.84.0}/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.83.0
3
+ Version: 1.84.0
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
@@ -37,9 +37,9 @@ Requires-Dist: socksio>=1.0.0; extra == "daemon"
37
37
  Command-line client and agent runtime for the [Forgexa](https://forgexa.net) platform.
38
38
 
39
39
  Communicates with the Forgexa server via REST API.
40
- Includes a built-in **daemon** with thirteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, and DeepSeek Harness. Six standard candidates are automatically routed; seven require explicit selection. Harness (`deepseek`) is a developer preview for personal Runtimes/Desktop, pinned to 0.2.0-rc.2 and Node 22.19+ within 22.x or 24+. It owns a managed runtime profile with read-only external credentials, without copying personal settings. Shared pools/native Windows are blocked. After exact-build live acceptance, `forgexa agent approve-live deepseek` records an attestation; `forgexa check --agent deepseek --quick` avoids model calls, while normal `check` runs a potentially billable minimal task. See [global/local installation and limits](../documentation/guide/agent-installation.md#deepseek-harness-developer-preview).
40
+ Includes a built-in **daemon** with fourteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, DeepSeek Harness, and MiniMax Code. Six standard candidates are automatically routed; eight require explicit selection. MiniMax Code and Harness require owned personal Runtimes/Desktop and local build/profile acceptance. Harness retains its exact 0.2.0-rc.2 and read-only external credential requirements. See [installation and limits](../documentation/guide/agent-installation.md).
41
41
 
42
- MiniMax Code is an additional acceptance-pending personal preview, not part of the supported count above. Run `forgexa agent minimax-init` to create an empty private profile, authenticate in that profile, then use `forgexa agent minimax-status` and `forgexa agent approve-live minimax --evidence PATH` after real safety acceptance. Personal credentials are never copied; Agent-only approval is refused. See [MiniMax setup, evidence and recovery](../documentation/guide/agent-installation.md#minimax-code).
42
+ MiniMax Code is included in that count, supporting reviewed 0.7.0 with 0.6.13 compatibility. Run `forgexa agent minimax-init` to create an empty private profile, authenticate there, then use `forgexa agent minimax-status` and `forgexa agent approve-live minimax --evidence PATH` after real local acceptance. Personal credentials are never copied; Agent-only approval is refused. `forgexa check --agent minimax --quick` is non-billable; normal `check` runs a potentially billable task. See [MiniMax setup, evidence and recovery](../documentation/guide/agent-installation.md#minimax-code).
43
43
 
44
44
  ## Installation
45
45
 
@@ -3,9 +3,9 @@
3
3
  Command-line client and agent runtime for the [Forgexa](https://forgexa.net) platform.
4
4
 
5
5
  Communicates with the Forgexa server via REST API.
6
- Includes a built-in **daemon** with thirteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, and DeepSeek Harness. Six standard candidates are automatically routed; seven require explicit selection. Harness (`deepseek`) is a developer preview for personal Runtimes/Desktop, pinned to 0.2.0-rc.2 and Node 22.19+ within 22.x or 24+. It owns a managed runtime profile with read-only external credentials, without copying personal settings. Shared pools/native Windows are blocked. After exact-build live acceptance, `forgexa agent approve-live deepseek` records an attestation; `forgexa check --agent deepseek --quick` avoids model calls, while normal `check` runs a potentially billable minimal task. See [global/local installation and limits](../documentation/guide/agent-installation.md#deepseek-harness-developer-preview).
6
+ Includes a built-in **daemon** with fourteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, DeepSeek Harness, and MiniMax Code. Six standard candidates are automatically routed; eight require explicit selection. MiniMax Code and Harness require owned personal Runtimes/Desktop and local build/profile acceptance. Harness retains its exact 0.2.0-rc.2 and read-only external credential requirements. See [installation and limits](../documentation/guide/agent-installation.md).
7
7
 
8
- MiniMax Code is an additional acceptance-pending personal preview, not part of the supported count above. Run `forgexa agent minimax-init` to create an empty private profile, authenticate in that profile, then use `forgexa agent minimax-status` and `forgexa agent approve-live minimax --evidence PATH` after real safety acceptance. Personal credentials are never copied; Agent-only approval is refused. See [MiniMax setup, evidence and recovery](../documentation/guide/agent-installation.md#minimax-code).
8
+ MiniMax Code is included in that count, supporting reviewed 0.7.0 with 0.6.13 compatibility. Run `forgexa agent minimax-init` to create an empty private profile, authenticate there, then use `forgexa agent minimax-status` and `forgexa agent approve-live minimax --evidence PATH` after real local acceptance. Personal credentials are never copied; Agent-only approval is refused. `forgexa check --agent minimax --quick` is non-billable; normal `check` runs a potentially billable task. See [MiniMax setup, evidence and recovery](../documentation/guide/agent-installation.md#minimax-code).
9
9
 
10
10
  ## Installation
11
11
 
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.83.0"
2
+ __version__ = "1.84.0"
@@ -67,6 +67,12 @@ except ImportError:
67
67
 
68
68
  logger = logging.getLogger("agent_core")
69
69
 
70
+ IMPLEMENTATION_AUDIT_PROTOCOL_VERSION = 3
71
+ LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS = frozenset({
72
+ "validation_issues", "validation_issue_details", "timeout_recovery_validation_issues",
73
+ "timeout_recovery_original_failure", "task_delivery_issue",
74
+ })
75
+
70
76
 
71
77
  def assess_approved_plan_conformance(baseline: dict, report: object) -> dict:
72
78
  result = {
@@ -271,8 +277,41 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
271
277
  "files": [], "evidence": row.get("evidence") or [],
272
278
  } for task_id in pending_verification_ids)
273
279
  implementation_statuses = {row["task_id"]: row["status"] for row in checks}
280
+ deferred_contract = contract.get("deferred_acceptance_verification")
281
+ reserved_indexes = set()
282
+ if (
283
+ isinstance(deferred_contract, dict) and type(deferred_contract.get("version")) is int
284
+ and deferred_contract["version"] in (1, 2, 3)
285
+ and deferred_contract.get("execution_phase") == "verification_final"
286
+ and isinstance(deferred_contract.get("workflow_id"), str) and deferred_contract["workflow_id"]
287
+ and isinstance(deferred_contract.get("analysis_revision_id"), str) and deferred_contract["analysis_revision_id"]
288
+ and re.fullmatch(r"[0-9a-f]{64}", str(deferred_contract.get("qa_test_level_policy_fingerprint", "")))
289
+ ):
290
+ reserved_ids = deferred_contract.get("acceptance_criteria_ids")
291
+ criteria = deferred_contract.get("criteria")
292
+ if (
293
+ isinstance(reserved_ids, list) and reserved_ids and all(isinstance(value, str) for value in reserved_ids)
294
+ and len(set(reserved_ids)) == len(reserved_ids) and isinstance(criteria, list)
295
+ and len(criteria) == len(reserved_ids)
296
+ and reserved_ids == [criterion.get("id") for criterion in criteria if isinstance(criterion, dict)]
297
+ ):
298
+ for criterion_id in reserved_ids:
299
+ match = re.fullmatch(r"AC-([0-9]+)", criterion_id)
300
+ if match is None or not 1 <= int(match.group(1)) <= len(contract.get("acceptance_criteria") or []):
301
+ reserved_indexes.clear()
302
+ break
303
+ reserved_indexes.add(int(match.group(1)))
304
+ if len(reserved_indexes) != len(reserved_ids):
305
+ reserved_indexes.clear()
274
306
  acceptance_checks = []
307
+ deferred_acceptance = []
275
308
  for row in report["acceptance_checks"]:
309
+ if (
310
+ row["index"] in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
311
+ and all(implementation_statuses.get(task_id) == "covered" for task_id in row["task_ids"])
312
+ ):
313
+ deferred_acceptance.append(row["index"])
314
+ continue
276
315
  if (
277
316
  pending_verification_ids and row["status"] in ("blocked", "unverified")
278
317
  and row.get("plan_revision") is None
@@ -285,7 +324,9 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
285
324
  "files": row.get("files") or [], "evidence": row["evidence"],
286
325
  } for task_id in pending_verification_ids)
287
326
  acceptance_checks.append(row)
288
- result = assess_implementation_audit({**contract, "version": 1}, {
327
+ result = assess_implementation_audit({
328
+ **contract, "version": 1, "final_verification": contract.get("final_verification") and not deferred_acceptance,
329
+ }, {
289
330
  **report, "task_checks": checks, "acceptance_checks": acceptance_checks,
290
331
  })
291
332
  result["report"] = report
@@ -293,6 +334,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
293
334
  result["report_valid"] = not result["issues"]
294
335
  result["node_complete"] = result["node_complete"] and not issues
295
336
  result["verification_pending"] = sorted({item["task_id"] for item in pending})
337
+ result["deferred_acceptance"] = sorted(deferred_acceptance)
296
338
  result["findings"].extend(pending)
297
339
  for request in original["revision_requests"]:
298
340
  if request not in result["revision_requests"]:
@@ -302,7 +344,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
302
344
  if contract.get("final_verification") or request["task_id"] == contract.get("subject_task_id")
303
345
  ]
304
346
  result["followup_task_ids"] = sorted({finding["task_id"] for finding in result["findings"]})
305
- if issues or pending or result["revision_requests"]:
347
+ if issues or pending or result["revision_requests"] or deferred_acceptance:
306
348
  result["status"] = "incomplete"
307
349
  if result["report_valid"] and result["blocking_revision_requests"]:
308
350
  result["status"] = "plan_revision_required"
@@ -312,7 +354,8 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
312
354
 
313
355
  def assess_implementation_audit(contract: dict, report: object) -> dict:
314
356
  """Validate implementation coverage independently of later runtime acceptance."""
315
- if contract.get("version") == 2:
357
+ version = contract.get("version", 1)
358
+ if type(version) is int and version == 2:
316
359
  return _assess_implementation_contributions(contract, report)
317
360
  issues: list[str] = []
318
361
  findings: list[dict] = []
@@ -322,6 +365,9 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
322
365
  "findings": findings, "followup_task_ids": [], "report": report,
323
366
  "revision_requests": [], "blocking_reasons": [],
324
367
  }
368
+ if type(version) is not int or version not in (1, 2):
369
+ issues.append("Implementation audit contract version is unsupported")
370
+ return result
325
371
  if (
326
372
  not isinstance(contract.get("audit_id"), str) or not contract["audit_id"]
327
373
  or not isinstance(report, dict) or report.get("audit_id") != contract["audit_id"]
@@ -2681,6 +2727,7 @@ def _reject_nonfinite_json(value: str) -> None:
2681
2727
 
2682
2728
 
2683
2729
  MINIMAX_MAX_OUTPUT_BYTES = 4 * 1024 * 1024
2730
+ MINIMAX_SUPPORTED_VERSIONS = ("0.6.13", "0.7.0")
2684
2731
  MINIMAX_POLICY_VERSION = 1
2685
2732
  MINIMAX_ACCEPTANCE_CHECKS = (
2686
2733
  "start", "resume", "cancel", "timeout", "permissions", "filesystem",
@@ -2790,8 +2837,12 @@ def minimax_build_info(binary: str) -> dict:
2790
2837
  if root is None or node is None or not Path(node).is_file():
2791
2838
  raise RuntimeError("MiniMax requires a verifiable official package and Node installation")
2792
2839
  metadata = json.loads((root / "package.json").read_text(encoding="utf-8"))
2793
- if metadata.get("version") != "0.6.13":
2794
- raise RuntimeError("MiniMax requires the fixed 0.6.13 candidate; other builds need protocol review")
2840
+ if metadata.get("version") not in MINIMAX_SUPPORTED_VERSIONS:
2841
+ raise RuntimeError(
2842
+ "MiniMax Code requires a protocol-reviewed version ("
2843
+ + ", ".join(MINIMAX_SUPPORTED_VERSIONS)
2844
+ + "); review other builds before use"
2845
+ )
2795
2846
  env = deepseek_harness_probe_env()
2796
2847
  version = subprocess.run([node, "-p", "process.versions.node"], capture_output=True,
2797
2848
  text=True, timeout=10, env=env)
@@ -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.83.0"
1237
+ DAEMON_VERSION = "1.84.0"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -1772,11 +1772,17 @@ def _build_runtime_critic_prompt(
1772
1772
  ]
1773
1773
  parts.extend((
1774
1774
  "## Approved Responsibility Boundaries", json.dumps(responsibilities, ensure_ascii=True, sort_keys=True),
1775
+ "This is the designated final completeness review: recheck every approved task and criterion "
1776
+ "assigned to this phase, including other contributors' unresolved source obligations. Only an "
1777
+ "explicit server-owned deferred verification contract reserves later execution; required "
1778
+ "prepared assets and in-scope checks remain mandatory."
1779
+ if audit_contract.get("final_verification") else
1775
1780
  "The feature's acceptance criteria remain mandatory, but they are not all obligations of this node. "
1776
1781
  "Assess this task's approved responsibility and required in-scope checks. Execution and sign-off "
1777
1782
  "assigned to other approved tasks stay pending under those owners; their absence alone does not "
1778
1783
  "invalidate a complete source contribution. A task that owns runtime verification still requires "
1779
- "that evidence. Independently inspect committed source and checks; implementer claims are not "
1784
+ "that evidence.",
1785
+ "Independently inspect committed source and checks; implementer claims are not "
1780
1786
  "independent proof. Missing assigned source, actual failed checks and genuine deviations remain blocking. "
1781
1787
  "For unverified, name the exact assigned obligation and the unavailable source or check evidence, "
1782
1788
  "rather than substituting the entire feature's unexecuted acceptance for this task's review.",
@@ -1879,6 +1885,56 @@ def _format_reflection_findings(findings: object) -> str:
1879
1885
  return "\n".join(lines)
1880
1886
 
1881
1887
 
1888
+ class _OutputValidationIssue(str):
1889
+ code: str
1890
+ severity: str
1891
+
1892
+ def __new__(cls, code: str, message: str, severity: str = "error") -> "_OutputValidationIssue":
1893
+ issue = super().__new__(cls, message)
1894
+ issue.code = code
1895
+ issue.severity = severity
1896
+ return issue
1897
+
1898
+
1899
+ def _output_validation_issue(issue: str) -> _OutputValidationIssue:
1900
+ if isinstance(issue, _OutputValidationIssue):
1901
+ return issue
1902
+ for prefix, code in (
1903
+ ("Planned task file has no implementation evidence:", "planned_task_incomplete"),
1904
+ ("Verification work item modified requirement analysis assets:", "verification_work_item_scope_violation"),
1905
+ ("Uncovered acceptance criteria in coverage-matrix.json:", "acceptance_criteria_uncovered"),
1906
+ ):
1907
+ if issue.startswith(prefix):
1908
+ return _OutputValidationIssue(code, issue)
1909
+ if issue.startswith("qa_policy."):
1910
+ return _OutputValidationIssue(issue.partition(":")[0], issue)
1911
+ if issue.startswith("Possible syntax issue in "):
1912
+ return _OutputValidationIssue("syntax_advisory", issue, "warning")
1913
+ return _OutputValidationIssue("output_validation_failed", issue)
1914
+
1915
+
1916
+ def _record_output_validation_issues(result: "TaskResult", issues: list[str]) -> None:
1917
+ result.metrics["validation_issues"] = [str(issue) for issue in issues]
1918
+ result.metrics["validation_issue_details"] = [
1919
+ {"code": normalized.code, "severity": normalized.severity, "message": str(normalized)}
1920
+ for issue in issues for normalized in [_output_validation_issue(issue)]
1921
+ ]
1922
+
1923
+
1924
+ def _mark_output_validation_unavailable(result: "TaskResult", snapshot: object = None) -> None:
1925
+ if result.status == "failed" and result.failure_code:
1926
+ return
1927
+ if _qa_source_test_validation_required(snapshot):
1928
+ _mark_qa_source_validation_unavailable(result)
1929
+ return
1930
+ result.status = "failed"
1931
+ result.failure_code = "validation_unavailable"
1932
+ result.error = "Output validation could not complete; inspect the runtime validation log before retrying."
1933
+ _record_output_validation_issues(result, [
1934
+ _OutputValidationIssue("validation_unavailable", result.error),
1935
+ ])
1936
+
1937
+
1882
1938
  def _filter_local_bind_task_result(result: "TaskResult") -> None:
1883
1939
  """Strip raw local-environment output from ordinary completion fields.
1884
1940
 
@@ -1892,7 +1948,8 @@ def _filter_local_bind_task_result(result: "TaskResult") -> None:
1892
1948
  result.stdout = ""
1893
1949
  result.stderr = ""
1894
1950
  result.observations = []
1895
- result.metrics.pop("validation_issues", None)
1951
+ for key in agent_core.LOCAL_BIND_PRIVATE_VALIDATION_METRIC_KEYS:
1952
+ result.metrics.pop(key, None)
1896
1953
  critic_review = result.critic_review
1897
1954
  if isinstance(critic_review, dict) and isinstance(critic_review.get("result"), dict):
1898
1955
  result.critic_review = {
@@ -6692,6 +6749,7 @@ class ProcessManager:
6692
6749
 
6693
6750
  elapsed = time.monotonic() - start_time
6694
6751
  result.metrics["duration_seconds"] = round(elapsed, 2)
6752
+ result.metrics["actual_agent"] = agent.agent_id
6695
6753
 
6696
6754
  # Detect agent-level failures despite exit code 0
6697
6755
  output_failure = None if agent_core.canonical_agent_id(agent.agent_id) in {"deepseek", "minimax"} else self._detect_agent_output_failure(result, agent.agent_id)
@@ -10022,7 +10080,7 @@ class ServerConnection:
10022
10080
  "acceptance_verification": {
10023
10081
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
10024
10082
  },
10025
- "implementation_audit": {"protocol_version": 3},
10083
+ "implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
10026
10084
  "task_delivery": {"protocol_version": 1},
10027
10085
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10028
10086
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
@@ -10448,7 +10506,7 @@ class ServerConnection:
10448
10506
  "acceptance_verification": {
10449
10507
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
10450
10508
  },
10451
- "implementation_audit": {"protocol_version": 3},
10509
+ "implementation_audit": {"protocol_version": agent_core.IMPLEMENTATION_AUDIT_PROTOCOL_VERSION},
10452
10510
  "task_delivery": {"protocol_version": 1},
10453
10511
  "recording_suggestions": {"protocol_version": 1, "isolated_workspace": True},
10454
10512
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
@@ -12480,6 +12538,9 @@ class RuntimeDaemon:
12480
12538
  ):
12481
12539
  return False
12482
12540
 
12541
+ result.metrics["timeout_recovery_original_failure"] = {
12542
+ "status": result.status, "failure_code": result.failure_code, "error": result.error,
12543
+ }
12483
12544
  validation_issues = self._validate_outputs(workspace_path, task, result)
12484
12545
  if validation_issues:
12485
12546
  result.metrics["timeout_recovery_validation_issues"] = validation_issues
@@ -13365,17 +13426,14 @@ class RuntimeDaemon:
13365
13426
  reporter, on_output_chunk, tried_agents=tried_agents,
13366
13427
  default_branch=default_branch, before_sha=delivery_before_sha,
13367
13428
  )
13368
- # Re-collect git info if validation triggered retries
13369
- if result.metrics.get("validation_issues") is not None or True:
13370
- pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
13429
+ # Re-collect after validation, including a successful repair.
13430
+ pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
13371
13431
  except Exception:
13372
- if _qa_source_test_validation_required(
13432
+ _mark_output_validation_unavailable(
13433
+ result,
13373
13434
  (task.input_data or {}).get("qa_test_level_policy_snapshot")
13374
- ):
13375
- _mark_qa_source_validation_unavailable(result)
13376
- logger.exception("Validation gate error for task %s (refusing to proceed)", task.task_id)
13377
- else:
13378
- logger.exception("Validation gate error for task %s (proceeding anyway)", task.task_id)
13435
+ )
13436
+ logger.exception("Validation gate error for task %s (refusing to proceed)", task.task_id)
13379
13437
 
13380
13438
  if result.failure_code == "verification_work_item_scope_violation" and not is_local_bind:
13381
13439
  await self._discard_verification_scope_changes(
@@ -13449,8 +13507,11 @@ class RuntimeDaemon:
13449
13507
  )
13450
13508
  pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
13451
13509
  except Exception:
13510
+ _mark_output_validation_unavailable(
13511
+ result, (task.input_data or {}).get("qa_test_level_policy_snapshot"),
13512
+ )
13452
13513
  logger.exception(
13453
- "Post-validation gate error for task %s (proceeding anyway)",
13514
+ "Post-validation gate error for task %s (refusing to proceed)",
13454
13515
  task.task_id,
13455
13516
  )
13456
13517
  break
@@ -14210,9 +14271,10 @@ class RuntimeDaemon:
14210
14271
  result.status = "failed"
14211
14272
  result.failure_code = "approved_plan_baseline_modified"
14212
14273
  result.error = "Implementation modified frozen analysis assets: " + ", ".join(forbidden_paths[:5])
14213
- issues.append(result.error)
14274
+ issues.append(_OutputValidationIssue(result.failure_code, result.error))
14214
14275
 
14215
14276
  audit_contract = input_data.get("implementation_audit_contract")
14277
+ result.metrics.pop("implementation_audit_node_complete", None)
14216
14278
  if isinstance(audit_contract, dict):
14217
14279
  agent_id = str(result.metrics.get("actual_agent") or getattr(task, "agent_type", "") or "")
14218
14280
  audit_report = agent_core.extract_implementation_audit(_daemon_extract_agent_output(agent_id, result.stdout))
@@ -14223,6 +14285,9 @@ class RuntimeDaemon:
14223
14285
  if isinstance(audit_report, dict):
14224
14286
  result.observations.append({"type": "implementation_audit", "data": audit_report})
14225
14287
  audit_result = agent_core.assess_implementation_audit(audit_contract, audit_report)
14288
+ result.metrics["implementation_audit_node_complete"] = bool(
14289
+ audit_result["report_valid"] and audit_result["node_complete"]
14290
+ )
14226
14291
  if audit_result["report_valid"] and audit_result["status"] == "plan_revision_required":
14227
14292
  result.status = "failed"
14228
14293
  result.failure_code = "implementation_plan_revision_required"
@@ -14230,11 +14295,12 @@ class RuntimeDaemon:
14230
14295
  result.error = "Approved analysis and task plan require revision before implementation can continue: " + "; ".join(
14231
14296
  f"{request['task_id']}: {request['reason']}" for request in requests[:3]
14232
14297
  )
14233
- issues.append(result.error)
14298
+ issues.append(_OutputValidationIssue(result.failure_code, result.error))
14234
14299
  elif not audit_result["report_valid"] or not audit_result["node_complete"]:
14235
- issues.extend(audit_result["issues"] or audit_result.get("blocking_reasons") or [
14300
+ issues.extend(_OutputValidationIssue("implementation_audit_incomplete", issue) for issue in (
14301
+ audit_result["issues"] or audit_result.get("blocking_reasons") or [
14236
14302
  "The selected approved task still has implementation gaps",
14237
- ])
14303
+ ]))
14238
14304
 
14239
14305
  if node_type == "coding" or (
14240
14306
  node_type == "fix" and input_data.get("task") == "phase_remediation_fix"
@@ -14242,11 +14308,12 @@ class RuntimeDaemon:
14242
14308
  delivered_files = list(result.files_changed)
14243
14309
  delivered_files.extend(input_data.get("planned_task_predecessor_files") or [])
14244
14310
  for missing_path in agent_core.missing_planned_task_files(input_data, delivered_files):
14245
- issues.append(
14311
+ issues.append(_OutputValidationIssue(
14312
+ "planned_task_incomplete",
14246
14313
  f"Planned task file has no implementation evidence: {missing_path}. "
14247
14314
  "Complete the planned change or report the task as blocked; "
14248
14315
  "documentation-only changes do not complete a source-code task."
14249
- )
14316
+ ))
14250
14317
 
14251
14318
  qa_policy, qa_policy_issues = _parse_frozen_qa_test_level_policy(
14252
14319
  input_data.get("qa_test_level_policy_snapshot")
@@ -14266,10 +14333,11 @@ class RuntimeDaemon:
14266
14333
  if _is_requirement_analysis_path(path)
14267
14334
  )
14268
14335
  if forbidden_paths:
14269
- issues.append(
14336
+ issues.append(_OutputValidationIssue(
14337
+ "verification_work_item_scope_violation",
14270
14338
  "Verification work item modified requirement analysis assets: "
14271
14339
  + ", ".join(forbidden_paths[:5])
14272
- )
14340
+ ))
14273
14341
 
14274
14342
  if verification_work_item and node_type == "analysis":
14275
14343
  analysis_doc_path = str(input_data.get("verification_analysis_doc_path", "") or "")
@@ -14425,24 +14493,29 @@ class RuntimeDaemon:
14425
14493
  # Syntax-check modified Python and JS/TS files
14426
14494
  for f in result.files_changed:
14427
14495
  fpath = workspace_path / f
14428
- if not fpath.exists():
14429
- continue
14430
- if f.endswith(".py"):
14431
- try:
14496
+ try:
14497
+ if not fpath.exists():
14498
+ continue
14499
+ if f.endswith(".py"):
14432
14500
  import ast
14433
- ast.parse(fpath.read_text(encoding="utf-8"), filename=f)
14434
- except SyntaxError as e:
14435
- issues.append(f"Python syntax error in {f}: {e.msg} (line {e.lineno})")
14436
- elif f.endswith((".js", ".ts", ".jsx", ".tsx")):
14437
- # Basic bracket/brace balance check
14438
- content = fpath.read_text(encoding="utf-8")
14439
- opens = content.count("{") + content.count("(") + content.count("[")
14440
- closes = content.count("}") + content.count(")") + content.count("]")
14441
- if abs(opens - closes) > 2:
14442
- issues.append(
14443
- f"Possible syntax issue in {f}: "
14444
- f"bracket imbalance (open={opens}, close={closes})"
14445
- )
14501
+ ast.parse(fpath.read_bytes(), filename=f)
14502
+ elif f.endswith((".js", ".ts", ".jsx", ".tsx")):
14503
+ content = fpath.read_text(encoding="utf-8")
14504
+ opens = content.count("{") + content.count("(") + content.count("[")
14505
+ closes = content.count("}") + content.count(")") + content.count("]")
14506
+ if abs(opens - closes) > 2:
14507
+ issues.append(_OutputValidationIssue(
14508
+ "syntax_advisory", f"Possible syntax issue in {f}: "
14509
+ f"bracket imbalance (open={opens}, close={closes})", "warning",
14510
+ ))
14511
+ except SyntaxError as error:
14512
+ issues.append(_OutputValidationIssue(
14513
+ "source_syntax_invalid", f"Python syntax error in {f}: {error.msg} (line {error.lineno})",
14514
+ ))
14515
+ except (OSError, UnicodeError):
14516
+ issues.append(_OutputValidationIssue(
14517
+ "validation_unavailable", f"Output validation could not read source file: {f}",
14518
+ ))
14446
14519
 
14447
14520
  # Task 6 (P6 fix): warning-level phase_handoffs check for non-analysis
14448
14521
  # nodes. These nodes SHOULD append a phase_handoffs entry to analysis.json
@@ -14767,10 +14840,11 @@ class RuntimeDaemon:
14767
14840
  ids = ", ".join(ac["id"] for ac in uncovered[:20])
14768
14841
  if len(uncovered) > 20:
14769
14842
  ids += f" (+{len(uncovered) - 20} more)"
14770
- issues.append(
14843
+ issues.append(_OutputValidationIssue(
14844
+ "acceptance_criteria_uncovered",
14771
14845
  "Uncovered acceptance criteria in "
14772
14846
  f"coverage-matrix.json: {ids}"
14773
- )
14847
+ ))
14774
14848
  except (_json.JSONDecodeError, UnicodeDecodeError) as e:
14775
14849
  issues.append(f"coverage-matrix.json is not valid JSON: {e}")
14776
14850
  elif (
@@ -16214,12 +16288,18 @@ class RuntimeDaemon:
16214
16288
  before_sha=before_sha,
16215
16289
  preserved_paths=preserved_paths,
16216
16290
  )
16217
- issues = self._validate_outputs(workspace_path, task, result)
16291
+ issues = [_output_validation_issue(issue) for issue in self._validate_outputs(workspace_path, task, result)]
16218
16292
  if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16293
+ _record_output_validation_issues(result, issues)
16294
+ logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
16219
16295
  return result
16220
16296
  if require_source_inventory:
16221
- issues.extend(inventory_issues)
16297
+ issues.extend(_output_validation_issue(issue) for issue in inventory_issues)
16298
+ advisory_issues = [issue for issue in issues if issue.severity == "warning"]
16299
+ issues = [issue for issue in issues if issue.severity != "warning"]
16222
16300
  if not issues:
16301
+ if advisory_issues:
16302
+ _record_output_validation_issues(result, advisory_issues)
16223
16303
  self._record_testing_telemetry(
16224
16304
  workspace_path, task, result, retries_used=attempt
16225
16305
  )
@@ -16234,7 +16314,7 @@ class RuntimeDaemon:
16234
16314
  if (
16235
16315
  agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
16236
16316
  and all(
16237
- issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
16317
+ issue.code == "acceptance_criteria_uncovered"
16238
16318
  for issue in issues
16239
16319
  )
16240
16320
  ):
@@ -16245,7 +16325,7 @@ class RuntimeDaemon:
16245
16325
  "Resolve the unmet criteria or missing test prerequisites, "
16246
16326
  "then rerun verification."
16247
16327
  )
16248
- result.metrics["validation_issues"] = issues
16328
+ _record_output_validation_issues(result, issues)
16249
16329
  self._record_testing_telemetry(
16250
16330
  workspace_path, task, result, retries_used=attempt
16251
16331
  )
@@ -16343,11 +16423,13 @@ class RuntimeDaemon:
16343
16423
  before_sha=before_sha,
16344
16424
  preserved_paths=preserved_paths,
16345
16425
  )
16346
- remaining = self._validate_outputs(workspace_path, task, result)
16426
+ remaining = [_output_validation_issue(issue) for issue in self._validate_outputs(workspace_path, task, result)]
16347
16427
  if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16428
+ _record_output_validation_issues(result, remaining)
16429
+ logger.warning("Output validation stopped task %s (%s)", task.task_id, result.failure_code)
16348
16430
  return result
16349
16431
  if require_source_inventory:
16350
- remaining.extend(inventory_issues)
16432
+ remaining.extend(_output_validation_issue(issue) for issue in inventory_issues)
16351
16433
  if remaining:
16352
16434
  if getattr(task, "workspace_resolution", None) == "local_bind":
16353
16435
  _append_local_bind_output_log(task.task_id, [
@@ -16357,7 +16439,7 @@ class RuntimeDaemon:
16357
16439
  if (
16358
16440
  agent_core.is_implementation_testing_task(task.node_type, task.input_data or {})
16359
16441
  and all(
16360
- issue.startswith("Uncovered acceptance criteria in coverage-matrix.json:")
16442
+ issue.code == "acceptance_criteria_uncovered"
16361
16443
  for issue in remaining
16362
16444
  )
16363
16445
  ):
@@ -16368,7 +16450,7 @@ class RuntimeDaemon:
16368
16450
  "Resolve the unmet criteria or missing test prerequisites, "
16369
16451
  "then rerun verification."
16370
16452
  )
16371
- result.metrics["validation_issues"] = remaining
16453
+ _record_output_validation_issues(result, remaining)
16372
16454
  self._record_testing_telemetry(
16373
16455
  workspace_path, task, result, retries_used=max_retries
16374
16456
  )
@@ -16379,35 +16461,22 @@ class RuntimeDaemon:
16379
16461
  scope_violations = [
16380
16462
  issue
16381
16463
  for issue in remaining
16382
- if issue.startswith("Verification work item modified requirement analysis assets:")
16464
+ if issue.code == "verification_work_item_scope_violation"
16383
16465
  ]
16384
- critical_patterns = (
16385
- "missing",
16386
- "not found",
16387
- "is empty",
16388
- "not valid json",
16389
- "invalid acceptance criteria",
16390
- "case_evidence",
16391
- "test-evidence.json has no",
16392
- "evidence item",
16393
- "anchor not found",
16394
- "uncovered acceptance criteria",
16395
- "no p0 priority test cases",
16396
- "contains no test cases",
16397
- )
16398
16466
  critical_issues = [
16399
- iss for iss in remaining
16400
- if any(p in iss.lower() for p in critical_patterns)
16467
+ issue for issue in remaining if issue.severity != "warning"
16401
16468
  ]
16402
16469
  qa_policy_violations = [
16403
- issue for issue in remaining if issue.startswith("qa_policy.")
16470
+ issue for issue in remaining if issue.code.startswith("qa_policy.")
16404
16471
  ]
16405
16472
  planned_delivery_issues = [
16406
16473
  issue for issue in remaining
16407
- if issue.startswith("Planned task file has no implementation evidence:")
16474
+ if issue.code == "planned_task_incomplete"
16408
16475
  ]
16476
+ audit_incomplete = result.metrics.get("implementation_audit_node_complete") is False
16409
16477
  blocking_issues = list(dict.fromkeys(
16410
16478
  critical_issues + scope_violations + qa_policy_violations + planned_delivery_issues
16479
+ + (remaining if audit_incomplete else [])
16411
16480
  ))
16412
16481
  if blocking_issues:
16413
16482
  # Agent didn't produce required output — mark as failed
@@ -16424,6 +16493,12 @@ class RuntimeDaemon:
16424
16493
  elif planned_delivery_issues:
16425
16494
  result.failure_code = "planned_task_incomplete"
16426
16495
  result.error = "; ".join(planned_delivery_issues[:3])
16496
+ elif any(issue.code == "validation_unavailable" for issue in remaining):
16497
+ result.failure_code = "validation_unavailable"
16498
+ result.error = "; ".join(critical_issues[:3])
16499
+ elif audit_incomplete:
16500
+ result.failure_code = "implementation_audit_incomplete"
16501
+ result.error = "; ".join(remaining[:3])
16427
16502
  else:
16428
16503
  result.failure_code = "validation_retry_exhausted"
16429
16504
  result.error = (
@@ -16437,7 +16512,7 @@ class RuntimeDaemon:
16437
16512
  "(proceeding anyway)",
16438
16513
  len(remaining), max_retries, task.task_id,
16439
16514
  )
16440
- result.metrics["validation_issues"] = remaining
16515
+ _record_output_validation_issues(result, remaining)
16441
16516
  self._record_testing_telemetry(workspace_path, task, result, retries_used=max_retries)
16442
16517
  return result
16443
16518
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.83.0
3
+ Version: 1.84.0
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
@@ -37,9 +37,9 @@ Requires-Dist: socksio>=1.0.0; extra == "daemon"
37
37
  Command-line client and agent runtime for the [Forgexa](https://forgexa.net) platform.
38
38
 
39
39
  Communicates with the Forgexa server via REST API.
40
- Includes a built-in **daemon** with thirteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, and DeepSeek Harness. Six standard candidates are automatically routed; seven require explicit selection. Harness (`deepseek`) is a developer preview for personal Runtimes/Desktop, pinned to 0.2.0-rc.2 and Node 22.19+ within 22.x or 24+. It owns a managed runtime profile with read-only external credentials, without copying personal settings. Shared pools/native Windows are blocked. After exact-build live acceptance, `forgexa agent approve-live deepseek` records an attestation; `forgexa check --agent deepseek --quick` avoids model calls, while normal `check` runs a potentially billable minimal task. See [global/local installation and limits](../documentation/guide/agent-installation.md#deepseek-harness-developer-preview).
40
+ Includes a built-in **daemon** with fourteen integrations: Claude Code, Codex, GitHub Copilot, Gemini CLI, OpenCode, Kimi Code, MiMo Code, Qwen Code, CodeBuddy, Qoder CLI, TraeCode CLI, ZCode, DeepSeek Harness, and MiniMax Code. Six standard candidates are automatically routed; eight require explicit selection. MiniMax Code and Harness require owned personal Runtimes/Desktop and local build/profile acceptance. Harness retains its exact 0.2.0-rc.2 and read-only external credential requirements. See [installation and limits](../documentation/guide/agent-installation.md).
41
41
 
42
- MiniMax Code is an additional acceptance-pending personal preview, not part of the supported count above. Run `forgexa agent minimax-init` to create an empty private profile, authenticate in that profile, then use `forgexa agent minimax-status` and `forgexa agent approve-live minimax --evidence PATH` after real safety acceptance. Personal credentials are never copied; Agent-only approval is refused. See [MiniMax setup, evidence and recovery](../documentation/guide/agent-installation.md#minimax-code).
42
+ MiniMax Code is included in that count, supporting reviewed 0.7.0 with 0.6.13 compatibility. Run `forgexa agent minimax-init` to create an empty private profile, authenticate there, then use `forgexa agent minimax-status` and `forgexa agent approve-live minimax --evidence PATH` after real local acceptance. Personal credentials are never copied; Agent-only approval is refused. `forgexa check --agent minimax --quick` is non-billable; normal `check` runs a potentially billable task. See [MiniMax setup, evidence and recovery](../documentation/guide/agent-installation.md#minimax-code).
43
43
 
44
44
  ## Installation
45
45
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.83.0"
3
+ version = "1.84.0"
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"
@@ -67,7 +67,7 @@ def test_minimax_live_approval_forwards_bound_evidence(tmp_path, monkeypatch, ca
67
67
 
68
68
  def approve(binary, document):
69
69
  captured.append((binary, document))
70
- return {"fingerprint": "bound-build", "version": "0.6.13"}
70
+ return {"fingerprint": "bound-build", "version": "0.7.0"}
71
71
 
72
72
  monkeypatch.setattr(agent_core, "approve_minimax_build", approve)
73
73
  main.cmd_agent_approve_live(SimpleNamespace(agent_id="minimax", binary="/fake/mcode", evidence=str(path)))
@@ -77,7 +77,7 @@ def test_minimax_live_approval_forwards_bound_evidence(tmp_path, monkeypatch, ca
77
77
 
78
78
  def test_minimax_quick_check_reports_pending_without_live_calls(monkeypatch, capsys):
79
79
  monkeypatch.setattr(daemon.AgentDiscovery, "AGENT_REGISTRY", {"minimax": {"commands": ["mcode"], "detect": "mcode --version"}})
80
- agent = _agent("minimax", "0.6.13")
80
+ agent = _agent("minimax", "0.7.0")
81
81
  agent.min_version_ok = False
82
82
  agent.version_warning = "MiniMax build/profile acceptance missing"
83
83
  monkeypatch.setattr(daemon.AgentDiscovery, "discover", AsyncMock(return_value=[agent]))
@@ -97,7 +97,7 @@ async def test_minimax_live_check_discloses_profile_provider_and_budget(tmp_path
97
97
  "root": tmp_path / "profile", "policy": {"provider": "dedicated", "region": "cn"},
98
98
  })
99
99
  manager = SimpleNamespace(run_agent=AsyncMock(return_value=SimpleNamespace(status="success")))
100
- assert await main._live_check_agent(manager, _agent("minimax", "0.6.13"), 15) == (True, "")
100
+ assert await main._live_check_agent(manager, _agent("minimax", "0.7.0"), 15) == (True, "")
101
101
  output = capsys.readouterr().err
102
102
  assert "profile=profile" in output and "provider=dedicated" in output
103
103
  assert "timeout=15s" in output and "max_steps=60" in output and "charges" in output
File without changes