forgexa-cli 1.85.0__tar.gz → 1.85.1__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.0 → forgexa_cli-1.85.1}/PKG-INFO +1 -1
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/agent_core.py +38 -8
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/daemon.py +44 -5
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/pyproject.toml +1 -1
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/README.md +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/aws_logs_evidence.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/observability_contract.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/observability_evidence.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/setup.cfg +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_publish_script.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/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.1"
|
|
@@ -211,6 +211,17 @@ def extract_implementation_audit(text: str) -> dict | None:
|
|
|
211
211
|
return report
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
def _implementation_acceptance_index(row: dict) -> int | None:
|
|
215
|
+
index = row.get("index", row.get("acceptance_index"))
|
|
216
|
+
if type(index) is not int:
|
|
217
|
+
return None
|
|
218
|
+
if "index" in row and "acceptance_index" in row and (
|
|
219
|
+
type(row["acceptance_index"]) is not int or row["acceptance_index"] != index
|
|
220
|
+
):
|
|
221
|
+
return None
|
|
222
|
+
return index
|
|
223
|
+
|
|
224
|
+
|
|
214
225
|
def redact_implementation_audit(contract: dict, report: object) -> dict | None:
|
|
215
226
|
"""Keep contract-owned status and hashed evidence without local free text."""
|
|
216
227
|
from hashlib import sha256
|
|
@@ -244,7 +255,7 @@ def redact_implementation_audit(contract: dict, report: object) -> dict | None:
|
|
|
244
255
|
if contract.get("version") == 2 and row["task_id"] in coding_ids and isinstance(row.get("implementation_check"), dict):
|
|
245
256
|
check["implementation_check"] = redact_check(row["implementation_check"])
|
|
246
257
|
else:
|
|
247
|
-
check.update(index=row
|
|
258
|
+
check.update(index=_implementation_acceptance_index(row), task_ids=row["task_ids"])
|
|
248
259
|
summary[key].append(check)
|
|
249
260
|
return summary
|
|
250
261
|
|
|
@@ -328,8 +339,9 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
328
339
|
and implementation_statuses.get(subject) == "covered"
|
|
329
340
|
)
|
|
330
341
|
for row in report["acceptance_checks"]:
|
|
342
|
+
index = _implementation_acceptance_index(row)
|
|
331
343
|
if (
|
|
332
|
-
|
|
344
|
+
index in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
|
|
333
345
|
and (
|
|
334
346
|
all(implementation_statuses.get(task_id) == "covered" for task_id in row["task_ids"])
|
|
335
347
|
or prepared_verifier and subject in row["task_ids"] and all(
|
|
@@ -339,7 +351,7 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
|
|
|
339
351
|
)
|
|
340
352
|
)
|
|
341
353
|
):
|
|
342
|
-
deferred_acceptance.append(
|
|
354
|
+
deferred_acceptance.append(index)
|
|
343
355
|
continue
|
|
344
356
|
if (
|
|
345
357
|
pending_verification_ids and row["status"] in ("blocked", "unverified")
|
|
@@ -418,7 +430,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
418
430
|
checked_criteria: set[int] = set()
|
|
419
431
|
criterion_count = len(contract.get("acceptance_criteria") or [])
|
|
420
432
|
for kind, rows in (("task", task_checks), ("acceptance", acceptance_checks)):
|
|
421
|
-
for row in rows:
|
|
433
|
+
for position, row in enumerate(rows, 1):
|
|
422
434
|
if not isinstance(row, dict):
|
|
423
435
|
issues.append(f"Invalid implementation {kind} check")
|
|
424
436
|
continue
|
|
@@ -445,8 +457,19 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
445
457
|
checked[task_id] = status
|
|
446
458
|
related = [task_id]
|
|
447
459
|
else:
|
|
448
|
-
index = row
|
|
460
|
+
index = _implementation_acceptance_index(row)
|
|
449
461
|
related = row.get("task_ids")
|
|
462
|
+
if index is None:
|
|
463
|
+
detail = (
|
|
464
|
+
"index and acceptance_index must be identical integers"
|
|
465
|
+
if "index" in row and "acceptance_index" in row else
|
|
466
|
+
"index must be a one-based integer; acceptance_index is an exact alias, but id is not an index"
|
|
467
|
+
)
|
|
468
|
+
issues.append(
|
|
469
|
+
f"Implementation audit has invalid acceptance criterion or task references; "
|
|
470
|
+
f"acceptance row {position}: {detail}"
|
|
471
|
+
)
|
|
472
|
+
continue
|
|
450
473
|
if type(index) is int and not 1 <= index <= criterion_count:
|
|
451
474
|
if criterion_count == 0:
|
|
452
475
|
issues.append(
|
|
@@ -462,11 +485,18 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
462
485
|
)
|
|
463
486
|
continue
|
|
464
487
|
if (
|
|
465
|
-
|
|
488
|
+
index in checked_criteria
|
|
466
489
|
or not isinstance(related, list) or not related
|
|
467
490
|
or any(not isinstance(task_id, str) or task_id not in expected for task_id in related)
|
|
468
491
|
):
|
|
469
|
-
|
|
492
|
+
detail = (
|
|
493
|
+
"duplicate index" if index in checked_criteria else
|
|
494
|
+
"task_ids must be a non-empty list of approved top-level task IDs"
|
|
495
|
+
)
|
|
496
|
+
issues.append(
|
|
497
|
+
f"Implementation audit has invalid acceptance criterion or task references; "
|
|
498
|
+
f"acceptance index {index}: {detail}"
|
|
499
|
+
)
|
|
470
500
|
continue
|
|
471
501
|
checked_criteria.add(index)
|
|
472
502
|
if len(related) > 1:
|
|
@@ -477,7 +507,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
|
|
|
477
507
|
description = row.get("description")
|
|
478
508
|
paths = row.get("files") or []
|
|
479
509
|
if not isinstance(description, str) or not description.strip() or not isinstance(paths, list):
|
|
480
|
-
reference = f"task '{row['task_id']}'" if kind == "task" else f"acceptance index {
|
|
510
|
+
reference = f"task '{row['task_id']}'" if kind == "task" else f"acceptance index {index}"
|
|
481
511
|
invalid_fields = []
|
|
482
512
|
if not isinstance(description, str) or not description.strip():
|
|
483
513
|
invalid_fields.append("description must be non-empty text")
|
|
@@ -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.1"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -3040,7 +3040,7 @@ def _runtime_critic_reference_candidates(
|
|
|
3040
3040
|
if isinstance(related, dict):
|
|
3041
3041
|
texts.append(str(related.get("description") or ""))
|
|
3042
3042
|
texts.extend(str(item.get("description") or "") for item in related.get("items") or [] if isinstance(item, dict))
|
|
3043
|
-
if planned.get("type")
|
|
3043
|
+
if planned.get("type") in ("design", "testing", "review"):
|
|
3044
3044
|
texts.append(json.dumps({"analysis": baseline.get("approved_analysis"),
|
|
3045
3045
|
"documents": baseline.get("analysis_documents")}, ensure_ascii=True)[:200_000])
|
|
3046
3046
|
text = re.sub(r"https?://\S+", " ", "\n".join(texts))
|
|
@@ -3063,7 +3063,7 @@ def _runtime_critic_reference_candidates(
|
|
|
3063
3063
|
php_context = any(Path(path).suffix.lower() == ".php" for path in source_paths)
|
|
3064
3064
|
directories = {Path(path).parent.as_posix() for path in source_paths if safe_path(path)} - {"."}
|
|
3065
3065
|
explicit = set()
|
|
3066
|
-
for token in re.findall(r"(?<![
|
|
3066
|
+
for token in re.findall(r"(?<![A-Za-z0-9_./\\:-])[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+", text):
|
|
3067
3067
|
candidate = safe_path(token)
|
|
3068
3068
|
if candidate is None:
|
|
3069
3069
|
continue
|
|
@@ -13998,6 +13998,7 @@ class RuntimeDaemon:
|
|
|
13998
13998
|
|
|
13999
13999
|
after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
|
|
14000
14000
|
committed_sources: dict[str, str] = {}
|
|
14001
|
+
unavailable_sources: list[dict[str, str]] = []
|
|
14001
14002
|
source_bytes = 0
|
|
14002
14003
|
coverage = {
|
|
14003
14004
|
"candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
|
|
@@ -14026,6 +14027,27 @@ class RuntimeDaemon:
|
|
|
14026
14027
|
"test-cases.json", "coverage-matrix.json", "test-evidence.json", "test-report.md",
|
|
14027
14028
|
}
|
|
14028
14029
|
))
|
|
14030
|
+
if output_dir.startswith(f"docs/requirements/{task.requirement_key}/") and ".." not in Path(output_dir).parts:
|
|
14031
|
+
verification_dir = output_dir + "/verification"
|
|
14032
|
+
try:
|
|
14033
|
+
verification_paths = await self.workspace_manager._git(
|
|
14034
|
+
"ls-tree", "-r", "--name-only", "-z", after_sha, "--", *asset_paths, verification_dir,
|
|
14035
|
+
cwd=workspace_path, timeout=10,
|
|
14036
|
+
)
|
|
14037
|
+
if len(verification_paths.encode("utf-8")) > _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
14038
|
+
raise ValueError("Committed verification inventory exceeds its bounded evidence budget")
|
|
14039
|
+
existing_artifacts = set(verification_paths.split("\0"))
|
|
14040
|
+
asset_paths = [path for path in asset_paths if path in existing_artifacts]
|
|
14041
|
+
paths.extend(path for path in verification_paths.split("\0") if (
|
|
14042
|
+
path.startswith(verification_dir + "/") and ".." not in Path(path).parts
|
|
14043
|
+
and re.fullmatch(r"[A-Za-z0-9_./-]+", path)
|
|
14044
|
+
and Path(path).suffix.lower() in {".php", ".py", ".ts", ".js", ".json", ".md", ".txt"}
|
|
14045
|
+
and not _is_sensitive_local_path(path)
|
|
14046
|
+
and Path(path).name.lower() not in {"env.php", "auth.json", "credentials.json", "secrets.json", "service-account.json"}
|
|
14047
|
+
and not any(part in {".git", ".forgexa", ".aws", ".ssh", ".env"} or part.startswith(".env.") for part in Path(path).parts)
|
|
14048
|
+
))
|
|
14049
|
+
except Exception:
|
|
14050
|
+
coverage["verification_inventory_unavailable"] = True
|
|
14029
14051
|
paths.extend(asset_paths)
|
|
14030
14052
|
support_dirs = sorted({(Path(path).parent / "support").as_posix() for path in asset_paths if path.startswith("tests/")})
|
|
14031
14053
|
for directory in support_dirs[:5]:
|
|
@@ -14117,8 +14139,19 @@ class RuntimeDaemon:
|
|
|
14117
14139
|
source = await self.workspace_manager._git(
|
|
14118
14140
|
"show", f"{after_sha}:{path}", cwd=workspace_path, timeout=10,
|
|
14119
14141
|
)
|
|
14120
|
-
except Exception:
|
|
14142
|
+
except Exception as error:
|
|
14121
14143
|
coverage["unreadable_files"] += 1
|
|
14144
|
+
detail = str(error).lower()
|
|
14145
|
+
reason = "source_git_read_failed"
|
|
14146
|
+
if isinstance(error, UnicodeDecodeError):
|
|
14147
|
+
reason = "source_encoding_unavailable"
|
|
14148
|
+
elif isinstance(error, TimeoutError) or "timed out after" in detail:
|
|
14149
|
+
reason = "source_read_timeout"
|
|
14150
|
+
elif "cannot be mapped" in detail or "cannot allocate memory" in detail:
|
|
14151
|
+
reason = "source_memory_unavailable"
|
|
14152
|
+
elif "does not exist in" in detail or "exists on disk, but not in" in detail:
|
|
14153
|
+
reason = "source_path_unavailable"
|
|
14154
|
+
unavailable_sources.append({"path": path, "reason": reason})
|
|
14122
14155
|
continue
|
|
14123
14156
|
encoded_size = len(source.encode("utf-8"))
|
|
14124
14157
|
if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
|
|
@@ -14136,7 +14169,10 @@ class RuntimeDaemon:
|
|
|
14136
14169
|
diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
|
|
14137
14170
|
coverage["full_source_files"] = len(committed_sources)
|
|
14138
14171
|
coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
|
|
14139
|
-
coverage["capsule_incomplete"] =
|
|
14172
|
+
coverage["capsule_incomplete"] = (
|
|
14173
|
+
coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
|
|
14174
|
+
or coverage.get("verification_inventory_unavailable", False)
|
|
14175
|
+
)
|
|
14140
14176
|
result.metrics["runtime_critic_coverage"] = coverage
|
|
14141
14177
|
if not diff:
|
|
14142
14178
|
return {
|
|
@@ -14164,6 +14200,8 @@ class RuntimeDaemon:
|
|
|
14164
14200
|
work_item={},
|
|
14165
14201
|
)
|
|
14166
14202
|
review_task.input_prompt += "\n## Review Evidence Coverage\n" + json.dumps(coverage, sort_keys=True)
|
|
14203
|
+
if unavailable_sources:
|
|
14204
|
+
review_task.input_prompt += "\n## Unavailable Committed Sources\n" + json.dumps(unavailable_sources, sort_keys=True)
|
|
14167
14205
|
if coverage["capsule_incomplete"]:
|
|
14168
14206
|
review_task.input_prompt += (
|
|
14169
14207
|
"\nSome candidate files were not available as complete source. Assess the evidence you can inspect; "
|
|
@@ -14183,6 +14221,7 @@ class RuntimeDaemon:
|
|
|
14183
14221
|
"reviewed_commit_sha": after_sha,
|
|
14184
14222
|
"baseline_fingerprint": baseline.get("fingerprint") if isinstance(baseline, dict) else None,
|
|
14185
14223
|
"input_sha256": hashlib.sha256(review_task.input_prompt.encode("utf-8")).hexdigest(),
|
|
14224
|
+
"unavailable_sources": unavailable_sources,
|
|
14186
14225
|
"snapshots": [
|
|
14187
14226
|
{"path": path, "sha256": hashlib.sha256(source.encode("utf-8")).hexdigest(),
|
|
14188
14227
|
"bytes": len(source.encode("utf-8"))}
|
|
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
|