forgexa-cli 1.85.3__tar.gz → 1.85.4__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.3 → forgexa_cli-1.85.4}/PKG-INFO +1 -1
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/daemon.py +122 -16
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/pyproject.toml +1 -1
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/README.md +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/setup.cfg +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.85.3 → forgexa_cli-1.85.4}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.85.
|
|
2
|
+
__version__ = "1.85.4"
|
|
@@ -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.85.
|
|
1237
|
+
DAEMON_VERSION = "1.85.4"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -1663,6 +1663,8 @@ _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
|
+
_RUNTIME_CRITIC_DESIGN_MAX_FILES = 128
|
|
1667
|
+
_RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS = 200
|
|
1666
1668
|
_RUNTIME_CRITIC_SYSTEM_PROMPT = """You are the Critic Agent for a software delivery gate.
|
|
1667
1669
|
Review only the supplied task context, code diff and committed source snapshots. You are running in an
|
|
1668
1670
|
isolated review capsule, not the project workspace. Do not write or modify files,
|
|
@@ -3075,9 +3077,25 @@ def _runtime_critic_reference_candidates(
|
|
|
3075
3077
|
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)
|
|
3076
3078
|
):
|
|
3077
3079
|
directories.add(candidate)
|
|
3080
|
+
alias_roots = {}
|
|
3081
|
+
for path in [*source_paths, *explicit]:
|
|
3082
|
+
parts = Path(path).parts
|
|
3083
|
+
if safe_path(path) and len(parts) > 4 and parts[:2] == ("app", "code"):
|
|
3084
|
+
alias_roots.setdefault(parts[2], set()).add("app/code")
|
|
3085
|
+
elif safe_path(path) and len(parts) > 2 and parts[0] == "vendor":
|
|
3086
|
+
alias_roots.setdefault(parts[1], set()).add("vendor")
|
|
3087
|
+
for reference in tuple(explicit):
|
|
3088
|
+
roots = alias_roots.get(Path(reference).parts[0], set())
|
|
3089
|
+
if len(roots) == 1:
|
|
3090
|
+
rooted = safe_path(next(iter(roots)) + "/" + reference)
|
|
3091
|
+
if rooted is not None and (inventory is None or rooted in inventory):
|
|
3092
|
+
explicit.add(rooted)
|
|
3093
|
+
directories.add(Path(rooted).parent.as_posix())
|
|
3094
|
+
php_context = php_context or any(Path(path).suffix.lower() == ".php" for path in explicit)
|
|
3078
3095
|
names = set(re.findall(
|
|
3079
3096
|
r"(?<![\w./\\:-])([A-Za-z0-9_][A-Za-z0-9_.-]*\.(?:php|py|tsx?|jsx?|xml|json|md|ya?ml|toml|html|phtml))\b", text,
|
|
3080
3097
|
))
|
|
3098
|
+
names.update(re.findall(r"(?<![\w./\\:-])(composer\.lock)\b", text))
|
|
3081
3099
|
if php_context:
|
|
3082
3100
|
names.update(name + ".php" for name in re.findall(
|
|
3083
3101
|
r"(?<![\w./\\:-])([A-Z][A-Za-z0-9_]*)::[A-Za-z_][A-Za-z0-9_]*\b", text,
|
|
@@ -3087,14 +3105,30 @@ def _runtime_critic_reference_candidates(
|
|
|
3087
3105
|
r"(?<![\w.\\:-])(?=[A-Za-z0-9]*[a-z])([A-Z][A-Za-z0-9]*[A-Z][A-Za-z0-9]*)(?![\w.\\:-])", bare_text,
|
|
3088
3106
|
))
|
|
3089
3107
|
module_roots = sorted({
|
|
3090
|
-
"/".join(Path(path).parts[:4]) for path in source_paths
|
|
3108
|
+
"/".join(Path(path).parts[:4]) for path in [*source_paths, *explicit]
|
|
3091
3109
|
if safe_path(path) and len(Path(path).parts) > 4 and Path(path).parts[:2] == ("app", "code")
|
|
3092
3110
|
and Path(path).suffix.lower() == ".php"
|
|
3093
3111
|
})[:5]
|
|
3094
3112
|
known_names = {Path(path).name for path in [*source_paths, *explicit]}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3113
|
+
root_files = names & {"composer.lock", "AGENTS.md"}
|
|
3114
|
+
candidates = set(explicit) | root_files
|
|
3115
|
+
if any(name.endswith("Factory.php") for name in names | known_names):
|
|
3116
|
+
configurations = {f"{root}/etc/di.xml" for root in module_roots}
|
|
3117
|
+
if inventory is not None:
|
|
3118
|
+
configurations.intersection_update(inventory)
|
|
3119
|
+
if inventory is None or len(configurations) == 1:
|
|
3120
|
+
candidates.update(configurations)
|
|
3121
|
+
for reference in explicit:
|
|
3122
|
+
if Path(reference).suffix.lower() != ".xml" or reference.startswith(("app/code/", "vendor/")):
|
|
3123
|
+
continue
|
|
3124
|
+
module_references = {f"{root}/{reference}" for root in module_roots}
|
|
3125
|
+
if inventory is not None:
|
|
3126
|
+
module_references.intersection_update(inventory)
|
|
3127
|
+
if len(module_references) != 1:
|
|
3128
|
+
continue
|
|
3129
|
+
candidates.update(module_references)
|
|
3130
|
+
for name in sorted(names - known_names - root_files)[:20]:
|
|
3131
|
+
if inventory is not None and module_roots and Path(name).suffix.lower() in {".php", ".xml"}:
|
|
3098
3132
|
matches = [path for path in inventory if Path(path).name == name
|
|
3099
3133
|
and any(path.startswith(root + "/") for root in module_roots)]
|
|
3100
3134
|
if len(matches) == 1 and (path := safe_path(matches[0])) is not None:
|
|
@@ -3102,9 +3136,14 @@ def _runtime_critic_reference_candidates(
|
|
|
3102
3136
|
continue
|
|
3103
3137
|
candidates.update(path for directory in sorted(directories)[:5]
|
|
3104
3138
|
if (path := safe_path(f"{directory}/{name}")) is not None)
|
|
3105
|
-
if name.
|
|
3139
|
+
if Path(name).suffix.lower() in {".php", ".xml"}:
|
|
3106
3140
|
candidates.update(f"{root}/{name}" for root in module_roots)
|
|
3107
|
-
|
|
3141
|
+
primary = (explicit | root_files) - set(source_paths)
|
|
3142
|
+
ordered = sorted(primary, key=lambda path: (
|
|
3143
|
+
path not in root_files, inventory is None or path not in inventory,
|
|
3144
|
+
not path.startswith(("app/code/", "vendor/", "setup/", "build/", "config/")), path,
|
|
3145
|
+
))
|
|
3146
|
+
return (ordered + sorted(candidates - primary - set(source_paths)))[:128]
|
|
3108
3147
|
|
|
3109
3148
|
|
|
3110
3149
|
def _try_acquire_local_workspace_file_lock(binding_id: str, workspace_path: Path | None = None):
|
|
@@ -4008,8 +4047,15 @@ class WorkspaceManager:
|
|
|
4008
4047
|
|
|
4009
4048
|
if remote_branch.strip():
|
|
4010
4049
|
try:
|
|
4050
|
+
# The tracking ref is a cache of remote state and the remote is
|
|
4051
|
+
# authoritative (its existence and tip were just confirmed via
|
|
4052
|
+
# ls-remote). Update it even when the remote was rewritten: a
|
|
4053
|
+
# fresh-analysis reset legitimately force-pushes this branch,
|
|
4054
|
+
# and a non-forced refspec is rejected non-fast-forward against
|
|
4055
|
+
# the stale tracking ref forever — the same cache-alignment
|
|
4056
|
+
# rule every other fetch in this file already follows.
|
|
4011
4057
|
await self._git(
|
|
4012
|
-
"fetch", "origin", f"{target_branch}:refs/remotes/origin/{target_branch}",
|
|
4058
|
+
"fetch", "origin", f"+{target_branch}:refs/remotes/origin/{target_branch}",
|
|
4013
4059
|
cwd=local_path,
|
|
4014
4060
|
)
|
|
4015
4061
|
except RuntimeError as exc:
|
|
@@ -4021,6 +4067,19 @@ class WorkspaceManager:
|
|
|
4021
4067
|
"rev-list", f"origin/{target_branch}..HEAD", "--count", cwd=local_path,
|
|
4022
4068
|
)
|
|
4023
4069
|
if ahead_output.strip() not in ("", "0"):
|
|
4070
|
+
behind_output = await self._git(
|
|
4071
|
+
"rev-list", f"HEAD..origin/{target_branch}", "--count", cwd=local_path,
|
|
4072
|
+
)
|
|
4073
|
+
if behind_output.strip() not in ("", "0"):
|
|
4074
|
+
raise RuntimeError(
|
|
4075
|
+
f"local_binding_unavailable: local branch '{target_branch}' diverged "
|
|
4076
|
+
f"from origin/{target_branch} (ahead {ahead_output.strip()}, behind "
|
|
4077
|
+
f"{behind_output.strip()}); the remote branch was rewritten — for "
|
|
4078
|
+
"example by a fresh-analysis reset, whose retired tips are preserved "
|
|
4079
|
+
"under 'forgexa/fresh-analysis-backup/*' tags. Align your local branch "
|
|
4080
|
+
f"(git reset --hard origin/{target_branch}) after keeping anything you "
|
|
4081
|
+
"need, then retry."
|
|
4082
|
+
)
|
|
4024
4083
|
raise RuntimeError(
|
|
4025
4084
|
f"local_binding_unavailable: local branch '{target_branch}' has "
|
|
4026
4085
|
f"{ahead_output.strip()} unpushed commit(s) ahead of origin — push them "
|
|
@@ -4058,7 +4117,7 @@ class WorkspaceManager:
|
|
|
4058
4117
|
|
|
4059
4118
|
try:
|
|
4060
4119
|
await self._git(
|
|
4061
|
-
"fetch", "origin", f"{default_branch}:refs/remotes/origin/{default_branch}",
|
|
4120
|
+
"fetch", "origin", f"+{default_branch}:refs/remotes/origin/{default_branch}",
|
|
4062
4121
|
cwd=local_path,
|
|
4063
4122
|
)
|
|
4064
4123
|
local_head = (await self._git("rev-parse", "HEAD", cwd=local_path)).strip()
|
|
@@ -5677,7 +5736,7 @@ class WorkspaceManager:
|
|
|
5677
5736
|
try:
|
|
5678
5737
|
await self._git(
|
|
5679
5738
|
"fetch", "origin",
|
|
5680
|
-
f"{default_branch}:refs/remotes/origin/{default_branch}",
|
|
5739
|
+
f"+{default_branch}:refs/remotes/origin/{default_branch}",
|
|
5681
5740
|
cwd=main_repo,
|
|
5682
5741
|
timeout=settings.GIT_FETCH_TIMEOUT,
|
|
5683
5742
|
project_key=project_key,
|
|
@@ -14018,6 +14077,7 @@ class RuntimeDaemon:
|
|
|
14018
14077
|
)
|
|
14019
14078
|
if isinstance(baseline, dict) and re.fullmatch(r"[0-9a-fA-F]{40}", after_sha):
|
|
14020
14079
|
planned = baseline.get("task") or {}
|
|
14080
|
+
source_deadline = time.monotonic() + _RUNTIME_CRITIC_SOURCE_TIMEOUT_SECONDS if planned.get("type") == "design" else None
|
|
14021
14081
|
paths = agent_core.missing_planned_task_files({
|
|
14022
14082
|
"files": planned.get("files") or [], "items": planned.get("items") or [],
|
|
14023
14083
|
}, [])
|
|
@@ -14109,17 +14169,52 @@ class RuntimeDaemon:
|
|
|
14109
14169
|
"items": related.get("items") or [],
|
|
14110
14170
|
}, []))
|
|
14111
14171
|
reference_baseline = {**baseline, "related_tasks": reference_tasks}
|
|
14172
|
+
document_sources = {}
|
|
14173
|
+
requirement_key = str(getattr(task, "requirement_key", "") or "")
|
|
14174
|
+
if source_deadline is not None and requirement_key:
|
|
14175
|
+
document_paths = [path for path in dict.fromkeys([*assigned_paths, *changed_files])
|
|
14176
|
+
if path.startswith(f"docs/requirements/{requirement_key}/implement/")
|
|
14177
|
+
and path.endswith(".md") and ".." not in Path(path).parts
|
|
14178
|
+
and re.fullmatch(r"[A-Za-z0-9_./-]+", path) and not _is_sensitive_local_path(path)][:8]
|
|
14179
|
+
document_bytes = 0
|
|
14180
|
+
for path in document_paths:
|
|
14181
|
+
seconds_left = source_deadline - time.monotonic()
|
|
14182
|
+
if seconds_left <= 0:
|
|
14183
|
+
coverage["source_collection_timed_out"] = True
|
|
14184
|
+
coverage["design_reference_documents_unavailable"] = True
|
|
14185
|
+
break
|
|
14186
|
+
try:
|
|
14187
|
+
source = await self.workspace_manager._git(
|
|
14188
|
+
"show", f"{after_sha}:{path}", cwd=workspace_path, timeout=min(10, max(1, int(seconds_left))),
|
|
14189
|
+
)
|
|
14190
|
+
encoded_size = len(source.encode("utf-8"))
|
|
14191
|
+
if document_bytes + encoded_size > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
14192
|
+
coverage["design_reference_documents_unavailable"] = True
|
|
14193
|
+
continue
|
|
14194
|
+
document_sources[path] = source
|
|
14195
|
+
document_bytes += encoded_size
|
|
14196
|
+
except Exception:
|
|
14197
|
+
coverage["design_reference_documents_unavailable"] = True
|
|
14198
|
+
reference_baseline = {**reference_baseline, "task": {
|
|
14199
|
+
**planned, "description": str(planned.get("description") or "") + "\n" + "\n".join(document_sources.values()),
|
|
14200
|
+
}}
|
|
14112
14201
|
reference_candidates = _runtime_critic_reference_candidates(reference_baseline, primary_paths)
|
|
14113
14202
|
if reference_candidates:
|
|
14114
14203
|
try:
|
|
14115
14204
|
reference_modules = sorted({
|
|
14116
14205
|
Path(path).parent.as_posix() for path in reference_candidates
|
|
14117
14206
|
if len(Path(path).parts) == 5 and Path(path).parts[:2] == ("app", "code")
|
|
14118
|
-
and Path(path).suffix.lower()
|
|
14207
|
+
and Path(path).suffix.lower() in {".php", ".xml"}
|
|
14119
14208
|
})[:5]
|
|
14209
|
+
inventory_timeout = 10
|
|
14210
|
+
if source_deadline is not None:
|
|
14211
|
+
seconds_left = source_deadline - time.monotonic()
|
|
14212
|
+
if seconds_left <= 0:
|
|
14213
|
+
raise TimeoutError("Source collection deadline reached")
|
|
14214
|
+
inventory_timeout = min(10, max(1, int(seconds_left)))
|
|
14120
14215
|
inventory = await self.workspace_manager._git(
|
|
14121
14216
|
"ls-tree", "-r", "--name-only", after_sha, "--", *reference_candidates, *reference_modules,
|
|
14122
|
-
cwd=workspace_path, timeout=
|
|
14217
|
+
cwd=workspace_path, timeout=inventory_timeout,
|
|
14123
14218
|
)
|
|
14124
14219
|
if len(inventory.encode("utf-8")) > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
14125
14220
|
raise ValueError("Committed reference inventory exceeds its bounded evidence budget")
|
|
@@ -14134,15 +14229,25 @@ class RuntimeDaemon:
|
|
|
14134
14229
|
coverage["candidate_files"] = len(paths)
|
|
14135
14230
|
remaining = _RUNTIME_CRITIC_DIFF_MAX_CHARS - len(diff or "")
|
|
14136
14231
|
source_context = []
|
|
14137
|
-
|
|
14232
|
+
source_file_limit = _RUNTIME_CRITIC_DESIGN_MAX_FILES if planned.get("type") == "design" else 20
|
|
14233
|
+
for path in paths[:source_file_limit]:
|
|
14234
|
+
source_timeout = 10
|
|
14235
|
+
if source_deadline is not None:
|
|
14236
|
+
seconds_left = source_deadline - time.monotonic()
|
|
14237
|
+
if seconds_left <= 0:
|
|
14238
|
+
coverage["source_collection_timed_out"] = True
|
|
14239
|
+
break
|
|
14240
|
+
source_timeout = min(10, max(1, int(seconds_left)))
|
|
14138
14241
|
normalized = Path(path)
|
|
14139
14242
|
if normalized.is_absolute() or ".." in normalized.parts or path.endswith("/"):
|
|
14140
14243
|
coverage["skipped_paths"] += 1
|
|
14141
14244
|
continue
|
|
14142
14245
|
try:
|
|
14143
|
-
source =
|
|
14144
|
-
|
|
14145
|
-
|
|
14246
|
+
source = document_sources.get(path)
|
|
14247
|
+
if source is None:
|
|
14248
|
+
source = await self.workspace_manager._git(
|
|
14249
|
+
"show", f"{after_sha}:{path}", cwd=workspace_path, timeout=source_timeout,
|
|
14250
|
+
)
|
|
14146
14251
|
except Exception as error:
|
|
14147
14252
|
coverage["unreadable_files"] += 1
|
|
14148
14253
|
detail = str(error).lower()
|
|
@@ -14176,6 +14281,7 @@ class RuntimeDaemon:
|
|
|
14176
14281
|
coverage["capsule_incomplete"] = (
|
|
14177
14282
|
coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
|
|
14178
14283
|
or coverage.get("verification_inventory_unavailable", False)
|
|
14284
|
+
or coverage.get("design_reference_documents_unavailable", False)
|
|
14179
14285
|
)
|
|
14180
14286
|
result.metrics["runtime_critic_coverage"] = coverage
|
|
14181
14287
|
if not diff:
|
|
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
|