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.
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/PKG-INFO +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/agent_core.py +12 -4
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/daemon.py +142 -3
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/pyproject.toml +1 -1
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/README.md +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/setup.cfg +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.84.1 → forgexa_cli-1.84.3}/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.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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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.
|
|
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
|
-
|
|
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"
|
|
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
|