forgexa-cli 1.78.2__tar.gz → 1.79.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 (33) hide show
  1. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/agent_core.py +30 -0
  4. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/daemon.py +95 -9
  5. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/observability_evidence.py +6 -1
  6. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/pyproject.toml +1 -1
  8. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/README.md +0 -0
  9. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/aws_logs_evidence.py +0 -0
  14. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/main.py +0 -0
  15. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/observability_contract.py +0 -0
  16. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/setup.cfg +0 -0
  25. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_runtime_credentials.py +0 -0
  31. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_session_credentials.py +0 -0
  32. {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_silent_install.py +0 -0
  33. {forgexa_cli-1.78.2 → forgexa_cli-1.79.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.78.2
3
+ Version: 1.79.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
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.78.2"
2
+ __version__ = "1.79.0"
@@ -67,6 +67,36 @@ except ImportError:
67
67
  logger = logging.getLogger("agent_core")
68
68
 
69
69
 
70
+ def assess_approved_plan_conformance(baseline: dict, report: object) -> dict:
71
+ result = {
72
+ "status": "unverified", "baseline_fingerprint": baseline.get("fingerprint"),
73
+ "task_id": (baseline.get("task") or {}).get("id"), "evidence_refs": [],
74
+ }
75
+ expected = {key: value for key, value in baseline.items() if key != "fingerprint"}
76
+ fingerprint = hashlib.sha256(json.dumps(
77
+ expected, sort_keys=True, ensure_ascii=True, separators=(",", ":"),
78
+ ).encode()).hexdigest()
79
+ if baseline.get("version") != 1 or result["baseline_fingerprint"] != fingerprint:
80
+ result["reason"] = "approved_plan_baseline_invalid"
81
+ return result
82
+ if not isinstance(report, dict) or (
83
+ report.get("baseline_fingerprint") != fingerprint
84
+ or report.get("task_id") != result["task_id"]
85
+ or report.get("status") not in ("conformant", "deviation", "unverified")
86
+ ):
87
+ result["reason"] = "approved_plan_review_missing_or_mismatched"
88
+ return result
89
+ evidence = report.get("evidence")
90
+ if not isinstance(evidence, list) or not evidence or any(
91
+ not isinstance(item, str) or not item.strip() for item in evidence
92
+ ):
93
+ result["reason"] = "approved_plan_review_evidence_missing"
94
+ return result
95
+ result["status"] = report["status"]
96
+ result["evidence_refs"] = [hashlib.sha256(item.encode()).hexdigest() for item in evidence]
97
+ return result
98
+
99
+
70
100
  def is_implementation_testing_task(node_type: str, input_data: dict) -> bool:
71
101
  """Retain the testing responsibility of a phase-remediation wrapper."""
72
102
  fix_context = input_data.get("fix_context")
@@ -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.78.2"
1237
+ DAEMON_VERSION = "1.79.0"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -1732,6 +1732,36 @@ def _build_runtime_critic_prompt(
1732
1732
  "",
1733
1733
  f"Changed files ({len(changed_files)}):",
1734
1734
  ]
1735
+ baseline = (task.input_data or {}).get("approved_plan_baseline")
1736
+ if isinstance(baseline, dict):
1737
+ parts.extend((
1738
+ "", "## Frozen Approved Plan Baseline",
1739
+ "Judge implementation against this approved baseline, not execution speculation. "
1740
+ "Implementation details may refine it but cannot silently replace its responsibilities, "
1741
+ "interfaces, required files or acceptance criteria. Report insufficient evidence honestly.",
1742
+ json.dumps(baseline, ensure_ascii=True, sort_keys=True),
1743
+ 'Also return "plan_conformance":{"baseline_fingerprint":"<frozen fingerprint>",'
1744
+ '"task_id":"<frozen task id>","status":"conformant|deviation|unverified",'
1745
+ '"evidence":["concrete supplied source/test reference"]}. '
1746
+ "Assess every assigned obligation, including behavior omitted from the diff. "
1747
+ "Use deviation for an actual conflict or omission; use unverified when supplied evidence "
1748
+ "cannot establish conformance. A high quality score never replaces this conclusion. "
1749
+ "Do not infer missing changes or execution as passing, and do not approve a different plan.",
1750
+ "Judge only this task's assigned contribution and required in-scope checks. Other tasks' "
1751
+ "unfinished work and server-deferred live acceptance do not invalidate a complete implementation. "
1752
+ "Missing assigned source or required test assets and actual failed checks remain blocking.",
1753
+ ))
1754
+ for key in ("deferred_acceptance_verification", "qa_test_level_policy_snapshot"):
1755
+ if isinstance((task.input_data or {}).get(key), dict):
1756
+ parts.extend((f"## Frozen {key}", json.dumps(task.input_data[key], ensure_ascii=True, sort_keys=True)))
1757
+ else:
1758
+ planned_context = {
1759
+ key: (task.input_data or {}).get(key)
1760
+ for key in ("task_instructions", "description", "items", "files", "acceptance_criteria")
1761
+ if (task.input_data or {}).get(key)
1762
+ }
1763
+ if planned_context:
1764
+ parts.extend(("", "## Planned Task Context", json.dumps(planned_context, ensure_ascii=True)))
1735
1765
  parts.extend(f"- {path}" for path in changed_files[:50])
1736
1766
  parts.extend(("", "## Code Diff", "```diff", diff, "```"))
1737
1767
  if test_summary:
@@ -9915,7 +9945,7 @@ class ServerConnection:
9915
9945
  "acceptance_verification": {
9916
9946
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
9917
9947
  },
9918
- "implementation_audit": {"protocol_version": 2},
9948
+ "implementation_audit": {"protocol_version": 3},
9919
9949
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
9920
9950
  },
9921
9951
  "server_url": self.server_url,
@@ -10339,7 +10369,7 @@ class ServerConnection:
10339
10369
  "acceptance_verification": {
10340
10370
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
10341
10371
  },
10342
- "implementation_audit": {"protocol_version": 2},
10372
+ "implementation_audit": {"protocol_version": 3},
10343
10373
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10344
10374
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10345
10375
  },
@@ -13646,7 +13676,8 @@ class RuntimeDaemon:
13646
13676
  }
13647
13677
  metadata = {"agent_used": agent_id}
13648
13678
  is_local_bind = task.workspace_resolution == "local_bind"
13649
- if is_local_bind and not task.local_evidence_upload:
13679
+ baseline = (task.input_data or {}).get("approved_plan_baseline")
13680
+ if is_local_bind and not task.local_evidence_upload and not isinstance(baseline, dict):
13650
13681
  return {
13651
13682
  "collection_status": "unavailable",
13652
13683
  "failure_code": "runtime_critic_local_evidence_upload_disabled",
@@ -13658,16 +13689,49 @@ class RuntimeDaemon:
13658
13689
  for path in result.files_changed
13659
13690
  if isinstance(path, str) and path.strip()
13660
13691
  ]
13661
- if not changed_files:
13692
+ if not changed_files and not isinstance(baseline, dict):
13662
13693
  return {"collection_status": "not_applicable"}
13663
13694
 
13664
- after_sha = str((result.git or {}).get("commit_sha") or "").strip()
13695
+ after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
13665
13696
  diff = await self._collect_runtime_critic_diff(
13666
13697
  workspace_path,
13667
- before_sha=before_sha,
13698
+ before_sha=(baseline.get("source_base_sha") if isinstance(baseline, dict)
13699
+ and (baseline.get("task") or {}).get("type") == "review" else None) or before_sha,
13668
13700
  after_sha=after_sha,
13669
13701
  changed_files=changed_files,
13670
13702
  )
13703
+ if isinstance(baseline, dict) and re.fullmatch(r"[0-9a-fA-F]{40}", after_sha):
13704
+ planned = baseline.get("task") or {}
13705
+ paths = agent_core.missing_planned_task_files({
13706
+ "files": planned.get("files") or [], "items": planned.get("items") or [],
13707
+ }, [])
13708
+ for related in baseline.get("related_tasks") or []:
13709
+ paths.extend(agent_core.missing_planned_task_files({
13710
+ "files": (related.get("input") or {}).get("files") or [],
13711
+ "items": related.get("items") or [],
13712
+ }, []))
13713
+ paths = list(dict.fromkeys(paths))
13714
+ remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
13715
+ source_context = []
13716
+ for path in paths[:20]:
13717
+ normalized = Path(path)
13718
+ if normalized.is_absolute() or ".." in normalized.parts or path.endswith("/") or remaining <= 0:
13719
+ continue
13720
+ try:
13721
+ source = await self.workspace_manager._git(
13722
+ "show", f"{after_sha}:{path}", cwd=workspace_path, timeout=10,
13723
+ )
13724
+ except Exception:
13725
+ continue
13726
+ excerpt = source[:min(remaining, 16000)]
13727
+ source_context.append(f"\n## Committed source {after_sha}:{path}\n{excerpt}")
13728
+ if len(excerpt) < len(source):
13729
+ source_context.append("\nSource truncated; do not infer omitted obligations as conformant.")
13730
+ remaining -= len(excerpt)
13731
+ if path not in changed_files:
13732
+ changed_files.append(path)
13733
+ if source_context:
13734
+ diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
13671
13735
  if not diff:
13672
13736
  return {
13673
13737
  "collection_status": "unavailable",
@@ -13748,6 +13812,20 @@ class RuntimeDaemon:
13748
13812
  "agent_attempts": [attempt],
13749
13813
  **metadata,
13750
13814
  }
13815
+ if not task.local_evidence_upload and isinstance(baseline, dict):
13816
+ conformance = agent_core.assess_approved_plan_conformance(baseline, structured_result.get("plan_conformance"))
13817
+ score = structured_result.get("score")
13818
+ verdict = structured_result.get("verdict")
13819
+ structured_result = {
13820
+ "score": score if type(score) in (int, float) and 0 <= score <= 1 else 0.5,
13821
+ "verdict": verdict if verdict in ("approve", "request_changes", "reject") else "request_changes",
13822
+ "summary": "Private local review; source and issue details remain local.", "issues": [],
13823
+ "plan_conformance": {
13824
+ "status": conformance["status"], "task_id": conformance["task_id"],
13825
+ "baseline_fingerprint": conformance["baseline_fingerprint"],
13826
+ "evidence": conformance["evidence_refs"],
13827
+ },
13828
+ }
13751
13829
  encoded_result = json.dumps(
13752
13830
  structured_result,
13753
13831
  ensure_ascii=True,
@@ -13885,6 +13963,14 @@ class RuntimeDaemon:
13885
13963
  node_type = "testing"
13886
13964
  req_type = input_data.get("requirement_type", "feature")
13887
13965
  verification_work_item = bool(input_data.get("verification_work_item"))
13966
+ if isinstance(input_data.get("approved_plan_baseline"), dict):
13967
+ forbidden_paths = sorted(path for path in ProcessManager._normalize_repo_paths(result.files_changed)
13968
+ if _is_requirement_analysis_path(path))
13969
+ if forbidden_paths:
13970
+ result.status = "failed"
13971
+ result.failure_code = "approved_plan_baseline_modified"
13972
+ result.error = "Implementation modified frozen analysis assets: " + ", ".join(forbidden_paths[:5])
13973
+ issues.append(result.error)
13888
13974
 
13889
13975
  audit_contract = input_data.get("implementation_audit_contract")
13890
13976
  if isinstance(audit_contract, dict):
@@ -15873,7 +15959,7 @@ class RuntimeDaemon:
15873
15959
  preserved_paths=preserved_paths,
15874
15960
  )
15875
15961
  issues = self._validate_outputs(workspace_path, task, result)
15876
- if result.failure_code == "implementation_plan_revision_required":
15962
+ if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
15877
15963
  return result
15878
15964
  if require_source_inventory:
15879
15965
  issues.extend(inventory_issues)
@@ -16002,7 +16088,7 @@ class RuntimeDaemon:
16002
16088
  preserved_paths=preserved_paths,
16003
16089
  )
16004
16090
  remaining = self._validate_outputs(workspace_path, task, result)
16005
- if result.failure_code == "implementation_plan_revision_required":
16091
+ if result.failure_code in ("implementation_plan_revision_required", "approved_plan_baseline_modified"):
16006
16092
  return result
16007
16093
  if require_source_inventory:
16008
16094
  remaining.extend(inventory_issues)
@@ -6,6 +6,7 @@ from importlib import import_module
6
6
 
7
7
  EVIDENCE_PROTOCOL_VERSION = 1
8
8
  PAYLOAD_MAX_BYTES = 32 * 1024
9
+ METADATA_MAX_BYTES = 128 * 1024
9
10
  TURN_MAX_BYTES = 64 * 1024
10
11
  ORIGIN_FIELDS = frozenset({"issuer", "workspace_id", "project_id", "user_id"})
11
12
 
@@ -72,7 +73,11 @@ def validate_payload(
72
73
  raise EvidenceError("observability_evidence_integrity_failed")
73
74
  _reference, metadata_model, payload_model, canonical, loads = _contract()
74
75
  try:
75
- current = metadata_model.model_validate_json(canonical(metadata), strict=True)
76
+ metadata_bytes = (
77
+ metadata.encode("utf-8") if isinstance(metadata, str) else canonical(metadata)
78
+ )
79
+ metadata_value = loads(metadata_bytes, max_bytes=METADATA_MAX_BYTES)
80
+ current = metadata_model.model_validate_json(canonical(metadata_value), strict=True)
76
81
  parsed = loads(encoded)
77
82
  body = payload_model.model_validate_json(canonical(parsed), strict=True)
78
83
  if canonical(body.model_dump(mode="json")) != encoded:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.78.2
3
+ Version: 1.79.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.78.2"
3
+ version = "1.79.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"
File without changes
File without changes