forgexa-cli 1.84.1__tar.gz → 1.84.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.
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/PKG-INFO +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/daemon.py +97 -3
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/pyproject.toml +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/README.md +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/setup.cfg +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.84.
|
|
2
|
+
__version__ = "1.84.2"
|
|
@@ -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.84.
|
|
1237
|
+
DAEMON_VERSION = "1.84.2"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -3018,6 +3018,52 @@ def _is_sensitive_local_path(path: str) -> bool:
|
|
|
3018
3018
|
return any(pattern.search(normalized) for pattern in _LOCAL_BIND_SENSITIVE_PATH_PATTERNS)
|
|
3019
3019
|
|
|
3020
3020
|
|
|
3021
|
+
def _runtime_critic_reference_candidates(baseline: dict, source_paths: list[str]) -> list[str]:
|
|
3022
|
+
planned = baseline.get("task") or {}
|
|
3023
|
+
texts = [str(planned.get("description") or "")]
|
|
3024
|
+
texts.extend(str(item.get("description") or "") for item in planned.get("items") or [] if isinstance(item, dict))
|
|
3025
|
+
if planned.get("type") == "design":
|
|
3026
|
+
texts.append(json.dumps({"analysis": baseline.get("approved_analysis"),
|
|
3027
|
+
"documents": baseline.get("analysis_documents")}, ensure_ascii=True)[:200_000])
|
|
3028
|
+
text = re.sub(r"https?://\S+", " ", "\n".join(texts))
|
|
3029
|
+
suffixes = {".php", ".py", ".ts", ".tsx", ".js", ".jsx", ".xml", ".json", ".md", ".yml", ".yaml", ".toml", ".html", ".phtml"}
|
|
3030
|
+
|
|
3031
|
+
def safe_path(value: str) -> str | None:
|
|
3032
|
+
path = Path(value)
|
|
3033
|
+
if path.is_absolute() or ".." in path.parts or not path.parts:
|
|
3034
|
+
return None
|
|
3035
|
+
normalized = path.as_posix().rstrip(".")
|
|
3036
|
+
path = Path(normalized)
|
|
3037
|
+
if normalized.startswith("docs/requirements/") or _is_sensitive_local_path(normalized):
|
|
3038
|
+
return None
|
|
3039
|
+
if path.name.lower() in ("env.php", "auth.json", "credentials.json", "secrets.json", "service-account.json"):
|
|
3040
|
+
return None
|
|
3041
|
+
if any(part in (".git", ".forgexa", ".aws", ".ssh", ".env") or part.startswith(".env.") for part in path.parts):
|
|
3042
|
+
return None
|
|
3043
|
+
return normalized
|
|
3044
|
+
|
|
3045
|
+
directories = {Path(path).parent.as_posix() for path in source_paths if safe_path(path)} - {"."}
|
|
3046
|
+
explicit = set()
|
|
3047
|
+
for token in re.findall(r"(?<![\w./\\:-])[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+", text):
|
|
3048
|
+
candidate = safe_path(token)
|
|
3049
|
+
if candidate is None:
|
|
3050
|
+
continue
|
|
3051
|
+
if Path(candidate).suffix.lower() in suffixes:
|
|
3052
|
+
explicit.add(candidate)
|
|
3053
|
+
directories.add(Path(candidate).parent.as_posix())
|
|
3054
|
+
elif not Path(candidate).suffix:
|
|
3055
|
+
directories.add(candidate)
|
|
3056
|
+
names = set(re.findall(
|
|
3057
|
+
r"(?<![\w./\\:-])([A-Za-z0-9_][A-Za-z0-9_.-]*\.(?:php|py|tsx?|jsx?|xml|json|md|ya?ml|toml|html|phtml))\b", text,
|
|
3058
|
+
))
|
|
3059
|
+
known_names = {Path(path).name for path in [*source_paths, *explicit]}
|
|
3060
|
+
candidates = set(explicit)
|
|
3061
|
+
for name in sorted(names - known_names)[:20]:
|
|
3062
|
+
candidates.update(path for directory in sorted(directories)[:5]
|
|
3063
|
+
if (path := safe_path(f"{directory}/{name}")) is not None)
|
|
3064
|
+
return sorted(candidates - set(source_paths))[:128]
|
|
3065
|
+
|
|
3066
|
+
|
|
3021
3067
|
def _try_acquire_local_workspace_file_lock(binding_id: str, workspace_path: Path | None = None):
|
|
3022
3068
|
"""Non-blocking OS-level file lock keyed by binding_id.
|
|
3023
3069
|
|
|
@@ -13925,6 +13971,11 @@ class RuntimeDaemon:
|
|
|
13925
13971
|
after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
|
|
13926
13972
|
committed_sources: dict[str, str] = {}
|
|
13927
13973
|
source_bytes = 0
|
|
13974
|
+
coverage = {
|
|
13975
|
+
"candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
|
|
13976
|
+
"skipped_paths": 0, "omitted_files": 0, "prompt_excerpt_truncated": False,
|
|
13977
|
+
"capsule_incomplete": False,
|
|
13978
|
+
}
|
|
13928
13979
|
diff = await self._collect_runtime_critic_diff(
|
|
13929
13980
|
workspace_path,
|
|
13930
13981
|
before_sha=(baseline.get("source_base_sha") if isinstance(baseline, dict)
|
|
@@ -13937,6 +13988,7 @@ class RuntimeDaemon:
|
|
|
13937
13988
|
paths = agent_core.missing_planned_task_files({
|
|
13938
13989
|
"files": planned.get("files") or [], "items": planned.get("items") or [],
|
|
13939
13990
|
}, [])
|
|
13991
|
+
primary_paths = list(paths)
|
|
13940
13992
|
if planned.get("type") in ("testing", "review") and getattr(task, "requirement_key", ""):
|
|
13941
13993
|
output_dir = str((task.input_data or {}).get("output_dir") or "").replace("\\", "/").rstrip("/")
|
|
13942
13994
|
artifact_paths = WorkspaceManager._local_requirement_artifact_paths(task, workspace_path)
|
|
@@ -13963,6 +14015,21 @@ class RuntimeDaemon:
|
|
|
13963
14015
|
if isinstance(audit_contract, dict) and baseline.get("plan_id") is not None and audit_contract.get("plan_id") == baseline["plan_id"]:
|
|
13964
14016
|
approved_tasks = {entry["id"]: entry for entry in audit_contract.get("tasks") or [] if isinstance(entry, dict) and isinstance(entry.get("id"), str)}
|
|
13965
14017
|
pending = list(planned.get("depends_on") or [])
|
|
14018
|
+
if (
|
|
14019
|
+
planned.get("type") == "design" and (task.input_data or {}).get("requirement_type") == "bugfix"
|
|
14020
|
+
and audit_contract.get("subject_task_id") == planned.get("id")
|
|
14021
|
+
):
|
|
14022
|
+
diagnosis_dependents = {planned["id"]}
|
|
14023
|
+
while True:
|
|
14024
|
+
expanded = diagnosis_dependents | {
|
|
14025
|
+
task_id for task_id, entry in approved_tasks.items()
|
|
14026
|
+
if any(dependency in diagnosis_dependents for dependency in entry.get("depends_on") or [])
|
|
14027
|
+
}
|
|
14028
|
+
if expanded == diagnosis_dependents:
|
|
14029
|
+
break
|
|
14030
|
+
diagnosis_dependents = expanded
|
|
14031
|
+
pending.extend(task_id for task_id in diagnosis_dependents
|
|
14032
|
+
if task_id in approved_tasks and approved_tasks[task_id].get("type") == "coding")
|
|
13966
14033
|
visited = set()
|
|
13967
14034
|
while pending:
|
|
13968
14035
|
dependency_id = pending.pop()
|
|
@@ -13971,29 +14038,45 @@ class RuntimeDaemon:
|
|
|
13971
14038
|
visited.add(dependency_id)
|
|
13972
14039
|
dependency = approved_tasks.get(dependency_id)
|
|
13973
14040
|
if dependency is not None:
|
|
13974
|
-
|
|
14041
|
+
dependency_paths = agent_core.missing_planned_task_files({
|
|
13975
14042
|
"files": (dependency.get("input") or {}).get("files") or [],
|
|
13976
14043
|
"items": dependency.get("items") or [],
|
|
13977
|
-
}, [])
|
|
14044
|
+
}, [])
|
|
14045
|
+
paths.extend(dependency_paths)
|
|
14046
|
+
primary_paths.extend(dependency_paths)
|
|
13978
14047
|
pending.extend(dependency.get("depends_on") or [])
|
|
13979
14048
|
for related in baseline.get("related_tasks") or []:
|
|
13980
14049
|
paths.extend(agent_core.missing_planned_task_files({
|
|
13981
14050
|
"files": (related.get("input") or {}).get("files") or [],
|
|
13982
14051
|
"items": related.get("items") or [],
|
|
13983
14052
|
}, []))
|
|
14053
|
+
reference_candidates = _runtime_critic_reference_candidates(baseline, primary_paths)
|
|
14054
|
+
if reference_candidates:
|
|
14055
|
+
try:
|
|
14056
|
+
inventory = await self.workspace_manager._git(
|
|
14057
|
+
"ls-tree", "-r", "--name-only", after_sha, "--", *reference_candidates,
|
|
14058
|
+
cwd=workspace_path, timeout=10,
|
|
14059
|
+
)
|
|
14060
|
+
confirmed = [path for path in inventory.splitlines() if path in reference_candidates]
|
|
14061
|
+
paths = [*primary_paths, *confirmed, *paths]
|
|
14062
|
+
except Exception:
|
|
14063
|
+
coverage["reference_inventory_unavailable"] = True
|
|
13984
14064
|
paths.extend(changed_files)
|
|
13985
14065
|
paths = list(dict.fromkeys(paths))
|
|
14066
|
+
coverage["candidate_files"] = len(paths)
|
|
13986
14067
|
remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
|
|
13987
14068
|
source_context = []
|
|
13988
14069
|
for path in paths[:20]:
|
|
13989
14070
|
normalized = Path(path)
|
|
13990
14071
|
if normalized.is_absolute() or ".." in normalized.parts or path.endswith("/"):
|
|
14072
|
+
coverage["skipped_paths"] += 1
|
|
13991
14073
|
continue
|
|
13992
14074
|
try:
|
|
13993
14075
|
source = await self.workspace_manager._git(
|
|
13994
14076
|
"show", f"{after_sha}:{path}", cwd=workspace_path, timeout=10,
|
|
13995
14077
|
)
|
|
13996
14078
|
except Exception:
|
|
14079
|
+
coverage["unreadable_files"] += 1
|
|
13997
14080
|
continue
|
|
13998
14081
|
encoded_size = len(source.encode("utf-8"))
|
|
13999
14082
|
if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
@@ -14002,12 +14085,17 @@ class RuntimeDaemon:
|
|
|
14002
14085
|
excerpt = source[:min(max(remaining, 0), 16000)]
|
|
14003
14086
|
source_context.append(f"\n## Committed source {after_sha}:{path}\n{excerpt}")
|
|
14004
14087
|
if len(excerpt) < len(source):
|
|
14088
|
+
coverage["prompt_excerpt_truncated"] = True
|
|
14005
14089
|
source_context.append("\nSource truncated; do not infer omitted obligations as conformant.")
|
|
14006
14090
|
remaining -= len(excerpt)
|
|
14007
14091
|
if path not in changed_files:
|
|
14008
14092
|
changed_files.append(path)
|
|
14009
14093
|
if source_context:
|
|
14010
14094
|
diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
|
|
14095
|
+
coverage["full_source_files"] = len(committed_sources)
|
|
14096
|
+
coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
|
|
14097
|
+
coverage["capsule_incomplete"] = coverage["omitted_files"] > 0
|
|
14098
|
+
result.metrics["runtime_critic_coverage"] = coverage
|
|
14011
14099
|
if not diff:
|
|
14012
14100
|
return {
|
|
14013
14101
|
"collection_status": "unavailable",
|
|
@@ -14033,6 +14121,12 @@ class RuntimeDaemon:
|
|
|
14033
14121
|
project={},
|
|
14034
14122
|
work_item={},
|
|
14035
14123
|
)
|
|
14124
|
+
review_task.input_prompt += "\n## Review Evidence Coverage\n" + json.dumps(coverage, sort_keys=True)
|
|
14125
|
+
if coverage["capsule_incomplete"]:
|
|
14126
|
+
review_task.input_prompt += (
|
|
14127
|
+
"\nSome candidate files were not available as complete source. Assess the evidence you can inspect; "
|
|
14128
|
+
"keep unresolved responsibilities unverified rather than inferring omitted source is conformant."
|
|
14129
|
+
)
|
|
14036
14130
|
if committed_sources:
|
|
14037
14131
|
review_task.input_prompt += (
|
|
14038
14132
|
"\n## Read-Only Committed Source Capsule\n"
|
|
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
|
|
File without changes
|