forgexa-cli 1.85.4__tar.gz → 1.86.1__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.85.4 → forgexa_cli-1.86.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/agent_core.py +75 -0
  4. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/daemon.py +56 -7
  5. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/README.md +0 -0
  8. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/setup.cfg +0 -0
  25. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/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.85.4
3
+ Version: 1.86.1
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.85.4"
2
+ __version__ = "1.86.1"
@@ -747,6 +747,81 @@ def task_failure_domain(
747
747
  return "execution"
748
748
 
749
749
 
750
+ def lock_provenance_projection(path: str, source: str) -> str | None:
751
+ if path != "composer.lock" or len(source.encode()) > 10_000_000:
752
+ return None
753
+ try:
754
+ lock = json.loads(source)
755
+ except (ValueError, TypeError):
756
+ return None
757
+ if not isinstance(lock, dict) or not isinstance(lock.get("packages"), list):
758
+ return None
759
+ if not isinstance(lock.get("packages-dev", []), list):
760
+ return None
761
+ packages = []
762
+ for package in [*lock["packages"], *(lock.get("packages-dev") or [])]:
763
+ if not isinstance(package, dict) or not isinstance(package.get("name"), str) or not isinstance(package.get("version"), str):
764
+ return None
765
+ item = {"name": package["name"][:200], "version": package["version"][:100]}
766
+ for field in ("source", "dist"):
767
+ reference = (package.get(field) or {}).get("reference") if isinstance(package.get(field), dict) else None
768
+ if isinstance(reference, str) and re.fullmatch(r"[0-9a-fA-F]{7,64}", reference):
769
+ item[field + "_reference"] = reference
770
+ packages.append(item)
771
+ return json.dumps({"representation": "dependency_provenance_projection", "source_path": path,
772
+ "source_sha256": hashlib.sha256(source.encode()).hexdigest(), "source_bytes": len(source.encode()),
773
+ "limitation": "Parsed dependency identities only; not full lock source or implementation semantics", "packages": packages,
774
+ }, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
775
+
776
+
777
+ def task_review_policy(node_type: str, task_input: dict, *, changed_files: list[str] | None = None) -> dict:
778
+ baseline = task_input.get("approved_plan_baseline")
779
+ reasons = []
780
+ fingerprint = None
781
+ if isinstance(baseline, dict):
782
+ expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
783
+ fingerprint = hashlib.sha256(json.dumps(expected, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
784
+ if type(baseline.get("version")) is not int or baseline["version"] != 1 or baseline.get("fingerprint") != fingerprint:
785
+ reasons.append("baseline_invalid")
786
+ else:
787
+ reasons.append("baseline_unavailable")
788
+ planned = (baseline or {}).get("task") if isinstance(baseline, dict) else None
789
+ planned = planned if isinstance(planned, dict) else {}
790
+ files = missing_planned_task_files({"files": planned.get("files") or [], "items": planned.get("items") or []}, [])
791
+ files = sorted(set(files) | {path for path in changed_files or [] if isinstance(path, str)})
792
+ audit = task_input.get("implementation_audit_contract") or {}
793
+ if node_type not in {"coding", "fix", "testing"} or planned.get("type") not in {"coding", "fix", "testing"}:
794
+ reasons.append("review_stage")
795
+ if isinstance(audit, dict) and audit.get("final_verification") is True or task_input.get("verification_work_item"):
796
+ reasons.append("final_verification")
797
+ if not files:
798
+ reasons.append("scope_unknown")
799
+ patterns = {
800
+ "sensitive_configuration": r"(?:^|/)(?:config|system|di|acl|webapi|db_schema)\.xml$|(?:^|/)(?:\.github|deploy|infrastructure)/|(?:^|/)(?:dockerfile|compose[^/]*)$",
801
+ "payment_or_crypto": r"payment|checkout|encrypt|decrypt|crypto|secret|credential|webhook|idempot|refund|compensat",
802
+ "data_migration": r"migrat|alembic|(?:^|/)setup/|db_schema|(?:^|/)(?:composer\.lock|package-lock\.json|yarn\.lock|cargo\.lock)$",
803
+ "authorization": r"(?:^|/)(?:auth[^/]*|permission[^/]*|acl|security|tenant[^/]*)[/.]",
804
+ }
805
+ for reason, pattern in patterns.items():
806
+ if any(re.search(pattern, path, re.IGNORECASE) for path in files):
807
+ reasons.append(reason)
808
+ if node_type in {"coding", "fix"} and files and all(path.startswith("docs/") for path in files):
809
+ reasons.append("scope_unknown")
810
+ decision = {"version": 1, "mode": "required" if reasons else "advisory", "reasons": sorted(set(reasons))}
811
+ decision["fingerprint"] = hashlib.sha256(json.dumps({
812
+ **decision, "baseline_fingerprint": fingerprint,
813
+ "qa_policy": str(task_input.get("qa_test_level_policy_fingerprint") or ""),
814
+ "change_scope": str(task_input.get("change_scope") or ""),
815
+ }, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
816
+ return decision
817
+
818
+
819
+ def review_blocks_completion(policy: dict, assessment: dict) -> bool:
820
+ return not assessment.get("node_complete", assessment.get("status") == "conformant") and (
821
+ policy.get("mode") != "advisory" or assessment.get("status") == "deviation"
822
+ )
823
+
824
+
750
825
  _WORKSPACE_INSTRUCTION_PATHS = (
751
826
  "AGENTS.md",
752
827
  "CLAUDE.md",
@@ -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.85.4"
1237
+ DAEMON_VERSION = "1.86.1"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -1663,7 +1663,7 @@ _RUNTIME_CRITIC_TIMEOUT_SECONDS = 300
1663
1663
  _RUNTIME_CRITIC_DIFF_MAX_CHARS = 30_000
1664
1664
  _RUNTIME_CRITIC_OUTPUT_MAX_CHARS = 20_000
1665
1665
  _RUNTIME_CRITIC_SOURCE_MAX_BYTES = 1_000_000
1666
- _RUNTIME_CRITIC_DESIGN_MAX_FILES = 128
1666
+ _RUNTIME_CRITIC_SOURCE_MAX_FILES = 128
1667
1667
  _RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS = 200
1668
1668
  _RUNTIME_CRITIC_SYSTEM_PROMPT = """You are the Critic Agent for a software delivery gate.
1669
1669
  Review only the supplied task context, code diff and committed source snapshots. You are running in an
@@ -13120,6 +13120,11 @@ class RuntimeDaemon:
13120
13120
  "This indicates a git clone or worktree setup failure (check SSH/network)."
13121
13121
  )
13122
13122
  logger.info("Workspace ready: %s", workspace_path)
13123
+ recovery = (task.input_data or {}).get("verification_recovery")
13124
+ if isinstance(recovery, dict) and recovery.get("version") == 1 and recovery.get("mode") == "review_only":
13125
+ result = await self._execute_review_only(task, agent, workspace_path)
13126
+ await report_task_complete(result)
13127
+ return
13123
13128
  self._ensure_task_output_dir(workspace_path, task)
13124
13129
 
13125
13130
  try:
@@ -13937,7 +13942,9 @@ class RuntimeDaemon:
13937
13942
  before_sha=node_before_sha,
13938
13943
  )
13939
13944
 
13940
- if result.status == "success" and task.critic_review_required:
13945
+ review_policy = agent_core.task_review_policy(task.node_type, task.input_data or {}, changed_files=result.files_changed)
13946
+ result.metrics["task_review_policy"] = review_policy
13947
+ if result.status == "success" and review_policy["mode"] == "required" and (task.critic_review_required or isinstance((task.input_data or {}).get("approved_plan_baseline"), dict)):
13941
13948
  await self._collect_task_delivery_git(
13942
13949
  task, workspace_path, result, attempt_before_sha=node_before_sha,
13943
13950
  )
@@ -13990,6 +13997,25 @@ class RuntimeDaemon:
13990
13997
  project_lock.release()
13991
13998
  _release_local_workspace_file_lock(local_bind_file_lock)
13992
13999
 
14000
+ async def _execute_review_only(self, task, agent, workspace_path) -> TaskResult:
14001
+ recovery = (task.input_data or {})["verification_recovery"]
14002
+ snapshot = recovery.get("snapshot") or {}
14003
+ expected = recovery.get("commit_sha")
14004
+ head = (await self.workspace_manager._git("rev-parse", "HEAD", cwd=workspace_path, timeout=10)).strip()
14005
+ if not isinstance(expected, str) or not re.fullmatch(r"[0-9a-f]{40}", expected) or head != expected:
14006
+ return TaskResult(status="failed", exit_code=-1, stdout="", stderr="", failure_code="approved_plan_baseline_modified",
14007
+ error="Read-only review source changed; preserve the checkout and refresh recovery evidence")
14008
+ result = TaskResult(status="success", exit_code=0, stdout="", stderr="",
14009
+ metrics=dict(snapshot.get("metrics") or {}), git=dict(snapshot.get("git") or {}),
14010
+ artifacts=list(snapshot.get("artifacts") or []), observations=list(snapshot.get("observations") or []),
14011
+ evidence_summary=snapshot.get("evidence_summary"), files_changed=list((snapshot.get("git") or {}).get("files_changed") or []))
14012
+ for key in ("token_input", "token_output", "token_total", "input_tokens", "output_tokens", "cost_usd", "num_turns", "duration_ms"):
14013
+ result.metrics.pop(key, None)
14014
+ result.critic_review = await self._run_runtime_critic_review(task, agent, workspace_path, result,
14015
+ before_sha=str(result.git.get("before_sha") or expected))
14016
+ result.metrics["verification_recovery_mode"] = "review_only"
14017
+ return result
14018
+
13993
14019
  async def _collect_runtime_critic_diff(
13994
14020
  self,
13995
14021
  workspace_path: Path,
@@ -14061,8 +14087,10 @@ class RuntimeDaemon:
14061
14087
 
14062
14088
  after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
14063
14089
  committed_sources: dict[str, str] = {}
14090
+ projected_provenance = set()
14064
14091
  unavailable_sources: list[dict[str, str]] = []
14065
14092
  source_bytes = 0
14093
+ assigned_paths = []
14066
14094
  coverage = {
14067
14095
  "candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
14068
14096
  "skipped_paths": 0, "omitted_files": 0, "prompt_excerpt_truncated": False,
@@ -14077,10 +14105,14 @@ class RuntimeDaemon:
14077
14105
  )
14078
14106
  if isinstance(baseline, dict) and re.fullmatch(r"[0-9a-fA-F]{40}", after_sha):
14079
14107
  planned = baseline.get("task") or {}
14080
- source_deadline = time.monotonic() + _RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS if planned.get("type") == "design" else None
14081
14108
  paths = agent_core.missing_planned_task_files({
14082
14109
  "files": planned.get("files") or [], "items": planned.get("items") or [],
14083
14110
  }, [])
14111
+ source_file_limit = (
14112
+ _RUNTIME_CRITIC_SOURCE_MAX_FILES if planned.get("type") == "design"
14113
+ else min(_RUNTIME_CRITIC_SOURCE_MAX_FILES, max(20, len(set(paths + changed_files))))
14114
+ )
14115
+ source_deadline = time.monotonic() + _RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS if source_file_limit > 20 else None
14084
14116
  assigned_paths = list(paths)
14085
14117
  primary_paths = list(paths)
14086
14118
  if planned.get("type") in ("testing", "review") and getattr(task, "requirement_key", ""):
@@ -14229,7 +14261,6 @@ class RuntimeDaemon:
14229
14261
  coverage["candidate_files"] = len(paths)
14230
14262
  remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
14231
14263
  source_context = []
14232
- source_file_limit = _RUNTIME_CRITIC_DESIGN_MAX_FILES if planned.get("type") == "design" else 20
14233
14264
  for path in paths[:source_file_limit]:
14234
14265
  source_timeout = 10
14235
14266
  if source_deadline is not None:
@@ -14263,6 +14294,15 @@ class RuntimeDaemon:
14263
14294
  unavailable_sources.append({"path": path, "reason": reason})
14264
14295
  continue
14265
14296
  encoded_size = len(source.encode("utf-8"))
14297
+ if source_bytes + encoded_size > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14298
+ projection = agent_core.lock_provenance_projection(path, source)
14299
+ if projection is not None and source_bytes + len(projection.encode()) <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14300
+ committed_sources[path + ".provenance.json"] = projection
14301
+ projected_provenance.add(path)
14302
+ source_bytes += len(projection.encode())
14303
+ source_context.append("\n## Parsed dependency provenance (not complete source or semantic proof)\n" + projection[:max(remaining, 0)])
14304
+ remaining -= len(projection)
14305
+ continue
14266
14306
  if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14267
14307
  committed_sources[path] = source
14268
14308
  source_bytes += encoded_size
@@ -14276,14 +14316,23 @@ class RuntimeDaemon:
14276
14316
  changed_files.append(path)
14277
14317
  if source_context:
14278
14318
  diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
14279
- coverage["full_source_files"] = len(committed_sources)
14280
- coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
14319
+ coverage["full_source_files"] = len(committed_sources) - len(projected_provenance)
14320
+ coverage["projected_provenance_files"] = len(projected_provenance)
14321
+ coverage["omitted_files"] = coverage["candidate_files"] - len(committed_sources)
14281
14322
  coverage["capsule_incomplete"] = (
14282
14323
  coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
14283
14324
  or coverage.get("verification_inventory_unavailable", False)
14284
14325
  or coverage.get("design_reference_documents_unavailable", False)
14285
14326
  )
14286
14327
  result.metrics["runtime_critic_coverage"] = coverage
14328
+ required_sources = set(assigned_paths) | set((result.git or {}).get("files_changed") or result.files_changed)
14329
+ required_sources = {path for path in required_sources if isinstance(path, str)}
14330
+ coverage["required_source_files"] = len(required_sources)
14331
+ coverage["missing_required_source_files"] = len(required_sources - set(committed_sources) - projected_provenance)
14332
+ if required_sources and coverage["missing_required_source_files"]:
14333
+ if not is_local_bind:
14334
+ result.metrics["runtime_critic_evidence"] = {"reviewed_commit_sha": after_sha, "unavailable_sources": unavailable_sources}
14335
+ return {"collection_status": "unavailable", "failure_code": "runtime_critic_evidence_incomplete", **metadata}
14287
14336
  if not diff:
14288
14337
  return {
14289
14338
  "collection_status": "unavailable",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.85.4
3
+ Version: 1.86.1
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.85.4"
3
+ version = "1.86.1"
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