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.
Files changed (34) hide show
  1. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/agent_core.py +38 -8
  4. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/daemon.py +44 -5
  5. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/README.md +0 -0
  8. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/setup.cfg +0 -0
  25. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.85.0 → forgexa_cli-1.85.1}/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.85.0
3
+ Version: 1.85.1
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.85.0"
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["index"], task_ids=row["task_ids"])
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
- row["index"] in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
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(row["index"])
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.get("index")
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
- type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
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
- issues.append("Implementation audit has invalid acceptance criterion or task references")
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 {row['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.0"
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") == "design":
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"(?<![\w./\\:-])[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+", text):
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"] = coverage["omitted_files"] > 0 or coverage.get("reference_inventory_unavailable", False)
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"))}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.85.0
3
+ Version: 1.85.1
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.85.0"
3
+ version = "1.85.1"
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