forgexa-cli 1.84.1__tar.gz → 1.84.3__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.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/agent_core.py +12 -4
  4. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/daemon.py +142 -3
  5. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/pyproject.toml +1 -1
  7. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/README.md +0 -0
  8. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/setup.cfg +0 -0
  25. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/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.3
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.3"
@@ -419,10 +419,18 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
419
419
  index = row.get("index")
420
420
  related = row.get("task_ids")
421
421
  if type(index) is int and not 1 <= index <= criterion_count:
422
- issues.append(
423
- f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
424
- "report the baseline conflict instead of dropping or renumbering requirements"
425
- )
422
+ if criterion_count == 0:
423
+ issues.append(
424
+ f"Acceptance index {index} is invalid because the frozen baseline has no acceptance criteria; "
425
+ "return acceptance_checks=[] and retain all approved task obligations. If approved documents "
426
+ "prove criteria are missing, request an evidenced plan_revision instead of inventing or "
427
+ "renumbering requirements"
428
+ )
429
+ else:
430
+ issues.append(
431
+ f"Acceptance index {index} is outside the frozen baseline 1-{criterion_count}; "
432
+ "report the baseline conflict instead of dropping or renumbering requirements"
433
+ )
426
434
  continue
427
435
  if (
428
436
  type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
@@ -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.3"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -3018,6 +3018,83 @@ 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(
3022
+ baseline: dict, source_paths: list[str], *, inventory: list[str] | None = None,
3023
+ ) -> list[str]:
3024
+ planned = baseline.get("task") or {}
3025
+ texts = [str(planned.get("description") or "")]
3026
+ texts.extend(str(item.get("description") or "") for item in planned.get("items") or [] if isinstance(item, dict))
3027
+ if planned.get("type") in ("testing", "review"):
3028
+ for related in baseline.get("related_tasks") or []:
3029
+ if isinstance(related, dict):
3030
+ texts.append(str(related.get("description") or ""))
3031
+ texts.extend(str(item.get("description") or "") for item in related.get("items") or [] if isinstance(item, dict))
3032
+ if planned.get("type") == "design":
3033
+ texts.append(json.dumps({"analysis": baseline.get("approved_analysis"),
3034
+ "documents": baseline.get("analysis_documents")}, ensure_ascii=True)[:200_000])
3035
+ text = re.sub(r"https?://\S+", " ", "\n".join(texts))
3036
+ suffixes = {".php", ".py", ".ts", ".tsx", ".js", ".jsx", ".xml", ".json", ".md", ".yml", ".yaml", ".toml", ".html", ".phtml"}
3037
+
3038
+ def safe_path(value: str) -> str | None:
3039
+ path = Path(value)
3040
+ if path.is_absolute() or ".." in path.parts or not path.parts:
3041
+ return None
3042
+ normalized = path.as_posix().rstrip(".")
3043
+ path = Path(normalized)
3044
+ if normalized.startswith("docs/requirements/") or _is_sensitive_local_path(normalized):
3045
+ return None
3046
+ if path.name.lower() in ("env.php", "auth.json", "credentials.json", "secrets.json", "service-account.json"):
3047
+ return None
3048
+ if any(part in (".git", ".forgexa", ".aws", ".ssh", ".env") or part.startswith(".env.") for part in path.parts):
3049
+ return None
3050
+ return normalized
3051
+
3052
+ php_context = any(Path(path).suffix.lower() == ".php" for path in source_paths)
3053
+ directories = {Path(path).parent.as_posix() for path in source_paths if safe_path(path)} - {"."}
3054
+ explicit = set()
3055
+ for token in re.findall(r"(?<![\w./\\:-])[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+", text):
3056
+ candidate = safe_path(token)
3057
+ if candidate is None:
3058
+ continue
3059
+ if Path(candidate).suffix.lower() in suffixes:
3060
+ explicit.add(candidate)
3061
+ directories.add(Path(candidate).parent.as_posix())
3062
+ elif not Path(candidate).suffix and not (
3063
+ php_context and all(re.fullmatch(r"[A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*", part) for part in Path(candidate).parts)
3064
+ ):
3065
+ directories.add(candidate)
3066
+ names = set(re.findall(
3067
+ r"(?<![\w./\\:-])([A-Za-z0-9_][A-Za-z0-9_.-]*\.(?:php|py|tsx?|jsx?|xml|json|md|ya?ml|toml|html|phtml))\b", text,
3068
+ ))
3069
+ if php_context:
3070
+ names.update(name + ".php" for name in re.findall(
3071
+ r"(?<![\w./\\:-])([A-Z][A-Za-z0-9_]*)::[A-Za-z_][A-Za-z0-9_]*\b", text,
3072
+ ))
3073
+ bare_text = re.sub(r"(?<!\w)(?:[A-Za-z]:[\\/]|/|\.\.?/)[^\s,;()]+", " ", text)
3074
+ names.update(name + ".php" for name in re.findall(
3075
+ r"(?<![\w.\\:-])(?=[A-Za-z0-9]*[a-z])([A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*)(?![\w.\\:-])", bare_text,
3076
+ ))
3077
+ module_roots = sorted({
3078
+ "/".join(Path(path).parts[:4]) for path in source_paths
3079
+ if safe_path(path) and len(Path(path).parts) > 4 and Path(path).parts[:2] == ("app", "code")
3080
+ and Path(path).suffix.lower() == ".php"
3081
+ })[:5]
3082
+ known_names = {Path(path).name for path in [*source_paths, *explicit]}
3083
+ candidates = set(explicit)
3084
+ for name in sorted(names - known_names)[:20]:
3085
+ if inventory is not None and module_roots and name.endswith(".php"):
3086
+ matches = [path for path in inventory if Path(path).name == name
3087
+ and any(path.startswith(root + "/") for root in module_roots)]
3088
+ if len(matches) == 1 and (path := safe_path(matches[0])) is not None:
3089
+ candidates.add(path)
3090
+ continue
3091
+ candidates.update(path for directory in sorted(directories)[:5]
3092
+ if (path := safe_path(f"{directory}/{name}")) is not None)
3093
+ if name.endswith(".php"):
3094
+ candidates.update(f"{root}/{name}" for root in module_roots)
3095
+ return sorted(candidates - set(source_paths))[:128]
3096
+
3097
+
3021
3098
  def _try_acquire_local_workspace_file_lock(binding_id: str, workspace_path: Path | None = None):
3022
3099
  """Non-blocking OS-level file lock keyed by binding_id.
3023
3100
 
@@ -13925,6 +14002,11 @@ class RuntimeDaemon:
13925
14002
  after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
13926
14003
  committed_sources: dict[str, str] = {}
13927
14004
  source_bytes = 0
14005
+ coverage = {
14006
+ "candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
14007
+ "skipped_paths": 0, "omitted_files": 0, "prompt_excerpt_truncated": False,
14008
+ "capsule_incomplete": False,
14009
+ }
13928
14010
  diff = await self._collect_runtime_critic_diff(
13929
14011
  workspace_path,
13930
14012
  before_sha=(baseline.get("source_base_sha") if isinstance(baseline, dict)
@@ -13937,6 +14019,7 @@ class RuntimeDaemon:
13937
14019
  paths = agent_core.missing_planned_task_files({
13938
14020
  "files": planned.get("files") or [], "items": planned.get("items") or [],
13939
14021
  }, [])
14022
+ primary_paths = list(paths)
13940
14023
  if planned.get("type") in ("testing", "review") and getattr(task, "requirement_key", ""):
13941
14024
  output_dir = str((task.input_data or {}).get("output_dir") or "").replace("\\", "/").rstrip("/")
13942
14025
  artifact_paths = WorkspaceManager._local_requirement_artifact_paths(task, workspace_path)
@@ -13959,10 +14042,26 @@ class RuntimeDaemon:
13959
14042
  path for path in support_paths.splitlines()[:20]
13960
14043
  if path.startswith(directory + "/") and path.endswith((".ts", ".js", ".py"))
13961
14044
  )
14045
+ reference_tasks = []
13962
14046
  audit_contract = (task.input_data or {}).get("implementation_audit_contract")
13963
14047
  if isinstance(audit_contract, dict) and baseline.get("plan_id") is not None and audit_contract.get("plan_id") == baseline["plan_id"]:
13964
14048
  approved_tasks = {entry["id"]: entry for entry in audit_contract.get("tasks") or [] if isinstance(entry, dict) and isinstance(entry.get("id"), str)}
13965
14049
  pending = list(planned.get("depends_on") or [])
14050
+ if (
14051
+ planned.get("type") == "design" and (task.input_data or {}).get("requirement_type") == "bugfix"
14052
+ and audit_contract.get("subject_task_id") == planned.get("id")
14053
+ ):
14054
+ diagnosis_dependents = {planned["id"]}
14055
+ while True:
14056
+ expanded = diagnosis_dependents | {
14057
+ task_id for task_id, entry in approved_tasks.items()
14058
+ if any(dependency in diagnosis_dependents for dependency in entry.get("depends_on") or [])
14059
+ }
14060
+ if expanded == diagnosis_dependents:
14061
+ break
14062
+ diagnosis_dependents = expanded
14063
+ pending.extend(task_id for task_id in diagnosis_dependents
14064
+ if task_id in approved_tasks and approved_tasks[task_id].get("type") == "coding")
13966
14065
  visited = set()
13967
14066
  while pending:
13968
14067
  dependency_id = pending.pop()
@@ -13971,29 +14070,58 @@ class RuntimeDaemon:
13971
14070
  visited.add(dependency_id)
13972
14071
  dependency = approved_tasks.get(dependency_id)
13973
14072
  if dependency is not None:
13974
- paths.extend(agent_core.missing_planned_task_files({
14073
+ if audit_contract.get("subject_task_id") == planned.get("id"):
14074
+ reference_tasks.append(dependency)
14075
+ dependency_paths = agent_core.missing_planned_task_files({
13975
14076
  "files": (dependency.get("input") or {}).get("files") or [],
13976
14077
  "items": dependency.get("items") or [],
13977
- }, []))
14078
+ }, [])
14079
+ paths.extend(dependency_paths)
14080
+ primary_paths.extend(dependency_paths)
13978
14081
  pending.extend(dependency.get("depends_on") or [])
13979
14082
  for related in baseline.get("related_tasks") or []:
13980
14083
  paths.extend(agent_core.missing_planned_task_files({
13981
14084
  "files": (related.get("input") or {}).get("files") or [],
13982
14085
  "items": related.get("items") or [],
13983
14086
  }, []))
14087
+ reference_baseline = {**baseline, "related_tasks": reference_tasks}
14088
+ reference_candidates = _runtime_critic_reference_candidates(reference_baseline, primary_paths)
14089
+ if reference_candidates:
14090
+ try:
14091
+ reference_modules = sorted({
14092
+ Path(path).parent.as_posix() for path in reference_candidates
14093
+ if len(Path(path).parts) == 5 and Path(path).parts[:2] == ("app", "code")
14094
+ and Path(path).suffix.lower() == ".php"
14095
+ })[:5]
14096
+ inventory = await self.workspace_manager._git(
14097
+ "ls-tree", "-r", "--name-only", after_sha, "--", *reference_candidates, *reference_modules,
14098
+ cwd=workspace_path, timeout=10,
14099
+ )
14100
+ if len(inventory.encode("utf-8")) > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
14101
+ raise ValueError("Committed reference inventory exceeds its bounded evidence budget")
14102
+ inventory_paths = inventory.splitlines()
14103
+ inventory_set = set(inventory_paths)
14104
+ reference_candidates = _runtime_critic_reference_candidates(reference_baseline, primary_paths, inventory=inventory_paths)
14105
+ confirmed = [path for path in reference_candidates if path in inventory_set]
14106
+ paths = [*primary_paths, *confirmed, *paths]
14107
+ except Exception:
14108
+ coverage["reference_inventory_unavailable"] = True
13984
14109
  paths.extend(changed_files)
13985
14110
  paths = list(dict.fromkeys(paths))
14111
+ coverage["candidate_files"] = len(paths)
13986
14112
  remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
13987
14113
  source_context = []
13988
14114
  for path in paths[:20]:
13989
14115
  normalized = Path(path)
13990
14116
  if normalized.is_absolute() or ".." in normalized.parts or path.endswith("/"):
14117
+ coverage["skipped_paths"] += 1
13991
14118
  continue
13992
14119
  try:
13993
14120
  source = await self.workspace_manager._git(
13994
14121
  "show", f"{after_sha}:{path}", cwd=workspace_path, timeout=10,
13995
14122
  )
13996
14123
  except Exception:
14124
+ coverage["unreadable_files"] += 1
13997
14125
  continue
13998
14126
  encoded_size = len(source.encode("utf-8"))
13999
14127
  if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
@@ -14002,12 +14130,17 @@ class RuntimeDaemon:
14002
14130
  excerpt = source[:min(max(remaining, 0), 16000)]
14003
14131
  source_context.append(f"\n## Committed source {after_sha}:{path}\n{excerpt}")
14004
14132
  if len(excerpt) < len(source):
14133
+ coverage["prompt_excerpt_truncated"] = True
14005
14134
  source_context.append("\nSource truncated; do not infer omitted obligations as conformant.")
14006
14135
  remaining -= len(excerpt)
14007
14136
  if path not in changed_files:
14008
14137
  changed_files.append(path)
14009
14138
  if source_context:
14010
14139
  diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
14140
+ coverage["full_source_files"] = len(committed_sources)
14141
+ coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
14142
+ coverage["capsule_incomplete"] = coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
14143
+ result.metrics["runtime_critic_coverage"] = coverage
14011
14144
  if not diff:
14012
14145
  return {
14013
14146
  "collection_status": "unavailable",
@@ -14033,6 +14166,12 @@ class RuntimeDaemon:
14033
14166
  project={},
14034
14167
  work_item={},
14035
14168
  )
14169
+ review_task.input_prompt += "\n## Review Evidence Coverage\n" + json.dumps(coverage, sort_keys=True)
14170
+ if coverage["capsule_incomplete"]:
14171
+ review_task.input_prompt += (
14172
+ "\nSome candidate files were not available as complete source. Assess the evidence you can inspect; "
14173
+ "keep unresolved responsibilities unverified rather than inferring omitted source is conformant."
14174
+ )
14036
14175
  if committed_sources:
14037
14176
  review_task.input_prompt += (
14038
14177
  "\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.3
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.3"
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