forgexa-cli 1.47.0__tar.gz → 1.47.2__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 (29) hide show
  1. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/daemon.py +326 -4
  4. forgexa_cli-1.47.2/forgexa_cli/runtime_evidence.py +1452 -0
  5. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/pyproject.toml +1 -1
  7. forgexa_cli-1.47.0/forgexa_cli/runtime_evidence.py +0 -583
  8. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/README.md +0 -0
  9. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/agent_core.py +0 -0
  12. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/autoupgrade.py +0 -0
  13. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli/py.typed +0 -0
  15. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/setup.cfg +0 -0
  21. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.47.0 → forgexa_cli-1.47.2}/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.47.0
3
+ Version: 1.47.2
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.47.0"
2
+ __version__ = "1.47.2"
@@ -1075,7 +1075,7 @@ except (ImportError, ModuleNotFoundError):
1075
1075
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1076
1076
  # Kept in sync with pyproject.toml version via bump-version.sh.
1077
1077
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1078
- DAEMON_VERSION = "1.47.0"
1078
+ DAEMON_VERSION = "1.47.2"
1079
1079
 
1080
1080
 
1081
1081
  def _detect_client_type() -> str:
@@ -5992,8 +5992,13 @@ class ProcessManager:
5992
5992
  if not match:
5993
5993
  return None
5994
5994
  missing_model, suggested_model = match.groups()
5995
- if missing_model.strip().lower() == suggested_model.lower():
5995
+ missing_model = missing_model.strip()
5996
+ if missing_model.lower() == suggested_model.lower():
5996
5997
  return None
5998
+ if "/" not in suggested_model and "/" in missing_model:
5999
+ provider = missing_model.split("/", 1)[0]
6000
+ if provider:
6001
+ suggested_model = f"{provider}/{suggested_model}"
5997
6002
  return suggested_model
5998
6003
 
5999
6004
  async def _run_opencode(
@@ -10446,6 +10451,24 @@ class RuntimeDaemon:
10446
10451
  result,
10447
10452
  before_sha=node_before_sha,
10448
10453
  )
10454
+ await self._collect_runtime_type_evidence(
10455
+ workspace_path,
10456
+ task,
10457
+ result,
10458
+ before_sha=node_before_sha,
10459
+ )
10460
+ await self._collect_runtime_security_evidence(
10461
+ workspace_path,
10462
+ task,
10463
+ result,
10464
+ before_sha=node_before_sha,
10465
+ )
10466
+ await self._collect_runtime_acceptance_criteria_coverage_evidence(
10467
+ workspace_path,
10468
+ task,
10469
+ result,
10470
+ before_sha=node_before_sha,
10471
+ )
10449
10472
 
10450
10473
  # 6. Report completion (include actual agent used if different from requested)
10451
10474
  result.metrics["actual_agent"] = agent.agent_id
@@ -13275,11 +13298,15 @@ class RuntimeDaemon:
13275
13298
  "summary": "The committed source revision is unavailable for evidence collection.",
13276
13299
  }
13277
13300
  else:
13301
+ evidence_check = {
13302
+ **check,
13303
+ "output_dir": self._resolve_task_output_dir(task),
13304
+ }
13278
13305
  run = await self._run_runtime_evidence_in_worktree(
13279
13306
  workspace_path,
13280
13307
  task,
13281
13308
  after_sha,
13282
- check,
13309
+ evidence_check,
13283
13310
  runtime_evidence.run_test_coverage_evidence,
13284
13311
  )
13285
13312
 
@@ -13327,7 +13354,10 @@ class RuntimeDaemon:
13327
13354
  "artifact_refs": [artifact_ref],
13328
13355
  "runtime_evidence": evidence_identity,
13329
13356
  }
13330
- for key in ("test_pass_rate", "passed", "failed", "skipped", "errors", "total", "failed_tests"):
13357
+ for key in (
13358
+ "test_pass_rate", "passed", "failed", "skipped", "errors", "total",
13359
+ "failed_tests", "test_case_results", "exit_code",
13360
+ ):
13331
13361
  if key in run:
13332
13362
  test_observation[key] = run[key]
13333
13363
  if run.get("failure_reason"):
@@ -13491,6 +13521,298 @@ class RuntimeDaemon:
13491
13521
  observation["collection_failure_reason"] = run["failure_reason"]
13492
13522
  result.observations.append(observation)
13493
13523
 
13524
+ async def _collect_runtime_type_evidence(
13525
+ self,
13526
+ workspace_path: Path,
13527
+ task: TaskInfo,
13528
+ result: TaskResult,
13529
+ *,
13530
+ before_sha: str,
13531
+ ) -> None:
13532
+ """Run a frozen mypy check in a disposable post-commit worktree."""
13533
+ if (
13534
+ task.workspace_resolution != "cloud_worktree"
13535
+ or task.node_type not in ("testing", "fix")
13536
+ or not isinstance(task.runtime_evidence_plan, dict)
13537
+ ):
13538
+ return
13539
+
13540
+ plan = task.runtime_evidence_plan
13541
+ plan_hash = plan.get("plan_hash")
13542
+ checks = plan.get("checks")
13543
+ check = next(
13544
+ (
13545
+ item for item in checks
13546
+ if isinstance(item, dict) and item.get("kind") == runtime_evidence.TYPE_CHECK_KIND
13547
+ ),
13548
+ None,
13549
+ ) if isinstance(checks, list) else None
13550
+ if not isinstance(check, dict):
13551
+ return
13552
+ after_sha = str((result.git or {}).get("commit_sha") or "").strip()
13553
+ if (
13554
+ plan.get("version") != runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION
13555
+ or not isinstance(plan_hash, str)
13556
+ or re.fullmatch(r"[0-9a-f]{64}", plan_hash) is None
13557
+ ):
13558
+ run = {
13559
+ "status": "invalid_plan",
13560
+ "collection_status": "unavailable",
13561
+ "failure_reason": "invalid_frozen_plan",
13562
+ "summary": "The frozen type-check evidence plan is invalid.",
13563
+ }
13564
+ elif re.fullmatch(r"[0-9a-fA-F]{40}", after_sha) is None:
13565
+ run = {
13566
+ "status": "source_commit_unavailable",
13567
+ "collection_status": "unavailable",
13568
+ "failure_reason": "source_commit_unavailable",
13569
+ "summary": "The committed source revision is unavailable for type checking.",
13570
+ }
13571
+ else:
13572
+ run = await self._run_runtime_evidence_in_worktree(
13573
+ workspace_path,
13574
+ task,
13575
+ after_sha,
13576
+ check,
13577
+ runtime_evidence.run_type_check_evidence,
13578
+ )
13579
+
13580
+ source_commit_range = {"after_sha": after_sha} if after_sha else {}
13581
+ if re.fullmatch(r"[0-9a-fA-F]{40}", before_sha or ""):
13582
+ source_commit_range["before_sha"] = before_sha
13583
+ report = {
13584
+ "version": runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION,
13585
+ "plan_hash": plan_hash if isinstance(plan_hash, str) else "",
13586
+ "execution_attempt_id": task.execution_attempt_id,
13587
+ "check_kind": runtime_evidence.TYPE_CHECK_KIND,
13588
+ "command": check.get("command"),
13589
+ "source_commit_range": source_commit_range,
13590
+ "result": run,
13591
+ }
13592
+ report_content = json.dumps(report, sort_keys=True, separators=(",", ":"))
13593
+ report_checksum = hashlib.sha256(report_content.encode("utf-8")).hexdigest()
13594
+ artifact_path = (
13595
+ f".forgexa/evidence/{task.task_id}/{after_sha[:12] or 'unavailable'}/"
13596
+ "type-check.json"
13597
+ )
13598
+ artifact_ref = {"path": artifact_path, "checksum": report_checksum}
13599
+ result.artifacts.append({
13600
+ "path": artifact_path,
13601
+ "content": report_content,
13602
+ "content_hash": report_checksum,
13603
+ "type": "application/vnd.forgexa.gate-evidence+json",
13604
+ "system_generated": True,
13605
+ "artifact_role": "gate_evidence_report",
13606
+ })
13607
+
13608
+ evidence_identity = {
13609
+ "version": runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION,
13610
+ "plan_hash": report["plan_hash"],
13611
+ "check_kind": runtime_evidence.TYPE_CHECK_KIND,
13612
+ "execution_attempt_id": task.execution_attempt_id,
13613
+ "source_commit_range": source_commit_range,
13614
+ }
13615
+ observation = {
13616
+ "type": "lint_results",
13617
+ "collection_status": run.get("collection_status"),
13618
+ "summary": run.get("summary"),
13619
+ "lint_tool": "mypy",
13620
+ "lint_command": check.get("command"),
13621
+ "command": check.get("command"),
13622
+ "artifact_refs": [artifact_ref],
13623
+ "runtime_evidence": evidence_identity,
13624
+ }
13625
+ for key in ("lint_score", "issues", "type_errors", "truncated_count"):
13626
+ if key in run:
13627
+ observation[key] = run[key]
13628
+ if run.get("failure_reason"):
13629
+ observation["collection_failure_reason"] = run["failure_reason"]
13630
+ result.observations.append(observation)
13631
+
13632
+ async def _collect_runtime_security_evidence(
13633
+ self,
13634
+ workspace_path: Path,
13635
+ task: TaskInfo,
13636
+ result: TaskResult,
13637
+ *,
13638
+ before_sha: str,
13639
+ ) -> None:
13640
+ """Run a frozen Bandit scan in a disposable post-commit worktree."""
13641
+ if (
13642
+ task.workspace_resolution != "cloud_worktree"
13643
+ or task.node_type not in ("testing", "fix")
13644
+ or not isinstance(task.runtime_evidence_plan, dict)
13645
+ ):
13646
+ return
13647
+
13648
+ plan = task.runtime_evidence_plan
13649
+ plan_hash = plan.get("plan_hash")
13650
+ checks = plan.get("checks")
13651
+ check = next(
13652
+ (
13653
+ item for item in checks
13654
+ if isinstance(item, dict) and item.get("kind") == runtime_evidence.SECURITY_CHECK_KIND
13655
+ ),
13656
+ None,
13657
+ ) if isinstance(checks, list) else None
13658
+ if not isinstance(check, dict):
13659
+ return
13660
+ after_sha = str((result.git or {}).get("commit_sha") or "").strip()
13661
+ if (
13662
+ plan.get("version") != runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION
13663
+ or not isinstance(plan_hash, str)
13664
+ or re.fullmatch(r"[0-9a-f]{64}", plan_hash) is None
13665
+ ):
13666
+ run = {
13667
+ "status": "invalid_plan",
13668
+ "collection_status": "unavailable",
13669
+ "failure_reason": "invalid_frozen_plan",
13670
+ "summary": "The frozen security-check evidence plan is invalid.",
13671
+ }
13672
+ elif re.fullmatch(r"[0-9a-fA-F]{40}", after_sha) is None:
13673
+ run = {
13674
+ "status": "source_commit_unavailable",
13675
+ "collection_status": "unavailable",
13676
+ "failure_reason": "source_commit_unavailable",
13677
+ "summary": "The committed source revision is unavailable for security scanning.",
13678
+ }
13679
+ else:
13680
+ run = await self._run_runtime_evidence_in_worktree(
13681
+ workspace_path,
13682
+ task,
13683
+ after_sha,
13684
+ check,
13685
+ runtime_evidence.run_security_check_evidence,
13686
+ )
13687
+
13688
+ source_commit_range = {"after_sha": after_sha} if after_sha else {}
13689
+ if re.fullmatch(r"[0-9a-fA-F]{40}", before_sha or ""):
13690
+ source_commit_range["before_sha"] = before_sha
13691
+ report = {
13692
+ "version": runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION,
13693
+ "plan_hash": plan_hash if isinstance(plan_hash, str) else "",
13694
+ "execution_attempt_id": task.execution_attempt_id,
13695
+ "check_kind": runtime_evidence.SECURITY_CHECK_KIND,
13696
+ "command": check.get("command"),
13697
+ "source_commit_range": source_commit_range,
13698
+ "result": run,
13699
+ }
13700
+ report_content = json.dumps(report, sort_keys=True, separators=(",", ":"))
13701
+ report_checksum = hashlib.sha256(report_content.encode("utf-8")).hexdigest()
13702
+ artifact_path = (
13703
+ f".forgexa/evidence/{task.task_id}/{after_sha[:12] or 'unavailable'}/"
13704
+ "security-check.json"
13705
+ )
13706
+ artifact_ref = {"path": artifact_path, "checksum": report_checksum}
13707
+ result.artifacts.append({
13708
+ "path": artifact_path,
13709
+ "content": report_content,
13710
+ "content_hash": report_checksum,
13711
+ "type": "application/vnd.forgexa.gate-evidence+json",
13712
+ "system_generated": True,
13713
+ "artifact_role": "gate_evidence_report",
13714
+ })
13715
+
13716
+ evidence_identity = {
13717
+ "version": runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION,
13718
+ "plan_hash": report["plan_hash"],
13719
+ "check_kind": runtime_evidence.SECURITY_CHECK_KIND,
13720
+ "execution_attempt_id": task.execution_attempt_id,
13721
+ "source_commit_range": source_commit_range,
13722
+ }
13723
+ observation = {
13724
+ "type": "security_scan",
13725
+ "collection_status": run.get("collection_status"),
13726
+ "summary": run.get("summary"),
13727
+ "scanner": "bandit",
13728
+ "command": check.get("command"),
13729
+ "artifact_refs": [artifact_ref],
13730
+ "runtime_evidence": evidence_identity,
13731
+ }
13732
+ for key in ("security_scan_score", "vulnerabilities", "truncated_count"):
13733
+ if key in run:
13734
+ observation[key] = run[key]
13735
+ if run.get("failure_reason"):
13736
+ observation["collection_failure_reason"] = run["failure_reason"]
13737
+ result.observations.append(observation)
13738
+
13739
+ async def _collect_runtime_acceptance_criteria_coverage_evidence(
13740
+ self,
13741
+ workspace_path: Path,
13742
+ task: TaskInfo,
13743
+ result: TaskResult,
13744
+ *,
13745
+ before_sha: str,
13746
+ ) -> None:
13747
+ """Bind generated testing artifacts to the committed testing attempt."""
13748
+ if task.workspace_resolution != "cloud_worktree" or task.node_type != "testing":
13749
+ return
13750
+
13751
+ after_sha = str((result.git or {}).get("commit_sha") or "").strip()
13752
+ check = {"output_dir": self._resolve_task_output_dir(task)}
13753
+ if re.fullmatch(r"[0-9a-fA-F]{40}", after_sha) is None:
13754
+ run = {
13755
+ "status": "source_commit_unavailable",
13756
+ "collection_status": "unavailable",
13757
+ "failure_reason": "source_commit_unavailable",
13758
+ "coverage_links": [],
13759
+ "source_artifacts": [],
13760
+ "summary": "The committed source revision is unavailable for acceptance-criteria evidence.",
13761
+ }
13762
+ else:
13763
+ run = await self._run_runtime_evidence_in_worktree(
13764
+ workspace_path,
13765
+ task,
13766
+ after_sha,
13767
+ check,
13768
+ runtime_evidence.run_acceptance_criteria_coverage_evidence,
13769
+ )
13770
+ run.setdefault("coverage_links", [])
13771
+ run.setdefault("source_artifacts", [])
13772
+
13773
+ source_commit_range = {"after_sha": after_sha} if after_sha else {}
13774
+ if re.fullmatch(r"[0-9a-fA-F]{40}", before_sha or ""):
13775
+ source_commit_range["before_sha"] = before_sha
13776
+ report = {
13777
+ "version": runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION,
13778
+ "execution_attempt_id": task.execution_attempt_id,
13779
+ "check_kind": runtime_evidence.ACCEPTANCE_CRITERIA_COVERAGE_CHECK_KIND,
13780
+ "source_commit_range": source_commit_range,
13781
+ "result": run,
13782
+ }
13783
+ report_content = json.dumps(report, sort_keys=True, separators=(",", ":"))
13784
+ report_checksum = hashlib.sha256(report_content.encode("utf-8")).hexdigest()
13785
+ artifact_path = (
13786
+ f".forgexa/evidence/{task.task_id}/{after_sha[:12] or 'unavailable'}/"
13787
+ "acceptance-criteria.json"
13788
+ )
13789
+ artifact_ref = {"path": artifact_path, "checksum": report_checksum}
13790
+ result.artifacts.append({
13791
+ "path": artifact_path,
13792
+ "content": report_content,
13793
+ "content_hash": report_checksum,
13794
+ "type": "application/vnd.forgexa.gate-evidence+json",
13795
+ "system_generated": True,
13796
+ "artifact_role": "gate_evidence_report",
13797
+ })
13798
+ observation = {
13799
+ "type": runtime_evidence.ACCEPTANCE_CRITERIA_COVERAGE_CHECK_KIND,
13800
+ "collection_status": run.get("collection_status"),
13801
+ "summary": run.get("summary"),
13802
+ "artifact_refs": [artifact_ref],
13803
+ "runtime_evidence": {
13804
+ "version": runtime_evidence.RUNTIME_EVIDENCE_PLAN_VERSION,
13805
+ "check_kind": runtime_evidence.ACCEPTANCE_CRITERIA_COVERAGE_CHECK_KIND,
13806
+ "execution_attempt_id": task.execution_attempt_id,
13807
+ "source_commit_range": source_commit_range,
13808
+ },
13809
+ "coverage_links": run.get("coverage_links", []),
13810
+ "source_artifacts": run.get("source_artifacts", []),
13811
+ }
13812
+ if run.get("failure_reason"):
13813
+ observation["collection_failure_reason"] = run["failure_reason"]
13814
+ result.observations.append(observation)
13815
+
13494
13816
  async def _collect_staged_diff_stats(self, cwd: Path) -> dict:
13495
13817
  """Collect staged diff stats for building a rich commit message."""
13496
13818
  status_map: dict[str, str] = {}