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.
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/PKG-INFO +1 -1
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/agent_core.py +75 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/daemon.py +56 -7
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/pyproject.toml +1 -1
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/README.md +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/setup.cfg +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.85.4 → forgexa_cli-1.86.1}/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.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.
|
|
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
|
-
|
|
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
|
-
|
|
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["
|
|
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",
|
|
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
|
|
File without changes
|
|
File without changes
|