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.
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/PKG-INFO +1 -1
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/agent_core.py +30 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/daemon.py +95 -9
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/observability_evidence.py +6 -1
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/pyproject.toml +1 -1
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/README.md +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/setup.cfg +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.78.2 → forgexa_cli-1.79.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
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.
|
|
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":
|
|
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":
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|