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.
Files changed (34) hide show
  1. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/daemon.py +97 -3
  4. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/pyproject.toml +1 -1
  6. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/README.md +0 -0
  7. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/setup.cfg +0 -0
  25. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.84.1 → forgexa_cli-1.84.2}/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.84.1
3
+ Version: 1.84.2
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.84.1"
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.1"
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
- paths.extend(agent_core.missing_planned_task_files({
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.84.1
3
+ Version: 1.84.2
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.84.1"
3
+ version = "1.84.2"
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