forgexa-cli 1.84.4__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.84.4 → forgexa_cli-1.85.1}/PKG-INFO +1 -1
  2. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/agent_core.py +69 -11
  4. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/daemon.py +74 -7
  5. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/pyproject.toml +1 -1
  7. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/README.md +0 -0
  8. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/setup.cfg +0 -0
  25. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_publish_script.py +0 -0
  31. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_runtime_credentials.py +0 -0
  32. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_session_credentials.py +0 -0
  33. {forgexa_cli-1.84.4 → forgexa_cli-1.85.1}/tests/test_silent_install.py +0 -0
  34. {forgexa_cli-1.84.4 → 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.84.4
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.84.4"
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
 
@@ -305,22 +316,42 @@ def _assess_implementation_contributions(contract: dict, report: object) -> dict
305
316
  and len(criteria) == len(reserved_ids)
306
317
  and reserved_ids == [criterion.get("id") for criterion in criteria if isinstance(criterion, dict)]
307
318
  ):
319
+ index_map = deferred_contract.get("acceptance_criteria_indices")
320
+ expected_indices = {identifier: index for index, identifier in enumerate(reserved_ids, 1)}
321
+ valid_index_map = (
322
+ isinstance(index_map, dict) and set(index_map) == set(expected_indices)
323
+ and all(type(value) is int and value == expected_indices[identifier] for identifier, value in index_map.items())
324
+ )
308
325
  for criterion_id in reserved_ids:
309
326
  match = re.fullmatch(r"AC-([0-9]+)", criterion_id)
310
- if match is None or not 1 <= int(match.group(1)) <= len(contract.get("acceptance_criteria") or []):
327
+ index = index_map[criterion_id] if valid_index_map else int(match.group(1)) if index_map is None and match else 0
328
+ if not 1 <= index <= len(contract.get("acceptance_criteria") or []):
311
329
  reserved_indexes.clear()
312
330
  break
313
- reserved_indexes.add(int(match.group(1)))
331
+ reserved_indexes.add(index)
314
332
  if len(reserved_indexes) != len(reserved_ids):
315
333
  reserved_indexes.clear()
316
334
  acceptance_checks = []
317
335
  deferred_acceptance = []
336
+ subject = contract.get("subject_task_id")
337
+ prepared_verifier = (
338
+ not contract.get("final_verification") and task_types.get(subject) in ("testing", "verify")
339
+ and implementation_statuses.get(subject) == "covered"
340
+ )
318
341
  for row in report["acceptance_checks"]:
342
+ index = _implementation_acceptance_index(row)
319
343
  if (
320
- row["index"] in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
321
- and all(implementation_statuses.get(task_id) == "covered" for task_id in row["task_ids"])
344
+ index in reserved_indexes and row["status"] == "unverified" and row.get("plan_revision") is None
345
+ and (
346
+ all(implementation_statuses.get(task_id) == "covered" for task_id in row["task_ids"])
347
+ or prepared_verifier and subject in row["task_ids"] and all(
348
+ implementation_statuses.get(task_id) == "covered"
349
+ or task_id not in implementation_statuses and task_types.get(task_id) in ("coding", "fix")
350
+ for task_id in row["task_ids"]
351
+ )
352
+ )
322
353
  ):
323
- deferred_acceptance.append(row["index"])
354
+ deferred_acceptance.append(index)
324
355
  continue
325
356
  if (
326
357
  pending_verification_ids and row["status"] in ("blocked", "unverified")
@@ -399,7 +430,7 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
399
430
  checked_criteria: set[int] = set()
400
431
  criterion_count = len(contract.get("acceptance_criteria") or [])
401
432
  for kind, rows in (("task", task_checks), ("acceptance", acceptance_checks)):
402
- for row in rows:
433
+ for position, row in enumerate(rows, 1):
403
434
  if not isinstance(row, dict):
404
435
  issues.append(f"Invalid implementation {kind} check")
405
436
  continue
@@ -426,8 +457,19 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
426
457
  checked[task_id] = status
427
458
  related = [task_id]
428
459
  else:
429
- index = row.get("index")
460
+ index = _implementation_acceptance_index(row)
430
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
431
473
  if type(index) is int and not 1 <= index <= criterion_count:
432
474
  if criterion_count == 0:
433
475
  issues.append(
@@ -443,11 +485,18 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
443
485
  )
444
486
  continue
445
487
  if (
446
- type(index) is not int or not 1 <= index <= criterion_count or index in checked_criteria
488
+ index in checked_criteria
447
489
  or not isinstance(related, list) or not related
448
490
  or any(not isinstance(task_id, str) or task_id not in expected for task_id in related)
449
491
  ):
450
- 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
+ )
451
500
  continue
452
501
  checked_criteria.add(index)
453
502
  if len(related) > 1:
@@ -458,7 +507,16 @@ def assess_implementation_audit(contract: dict, report: object) -> dict:
458
507
  description = row.get("description")
459
508
  paths = row.get("files") or []
460
509
  if not isinstance(description, str) or not description.strip() or not isinstance(paths, list):
461
- issues.append("Implementation gaps require an actionable description and file list")
510
+ reference = f"task '{row['task_id']}'" if kind == "task" else f"acceptance index {index}"
511
+ invalid_fields = []
512
+ if not isinstance(description, str) or not description.strip():
513
+ invalid_fields.append("description must be non-empty text")
514
+ if not isinstance(paths, list):
515
+ invalid_fields.append("files must be a list; use files=[] when no source file is implicated")
516
+ issues.append(
517
+ "Implementation gaps require an actionable description and file list; "
518
+ + reference + ": " + "; ".join(invalid_fields)
519
+ )
462
520
  continue
463
521
  if any(
464
522
  not isinstance(path, str) or not path.strip()
@@ -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.4"
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
@@ -4843,15 +4843,43 @@ class WorkspaceManager:
4843
4843
  if branch_name == default_branch:
4844
4844
  raise RuntimeError("Fresh analysis cannot reset the project default branch")
4845
4845
  await self._git("check-ref-format", branch_ref, cwd=repository)
4846
- for name in (default_branch, branch_name):
4846
+ await self._git(
4847
+ "fetch", "origin", f"+{default_branch}:refs/remotes/origin/{default_branch}", cwd=repository,
4848
+ timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
4849
+ )
4850
+ tracking_ref = f"refs/remotes/origin/{branch_name}"
4851
+ advertised = (await self._git(
4852
+ "ls-remote", "--heads", "origin", branch_ref, cwd=repository,
4853
+ timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
4854
+ )).strip()
4855
+ if not advertised:
4856
+ await self._git("update-ref", "-d", tracking_ref, cwd=repository)
4857
+ logger.info("Fresh analysis branch %s is absent on origin; removed its stale tracking reference", branch_name)
4858
+ return
4859
+ remote_fields = advertised.split()
4860
+ if len(remote_fields) != 2 or remote_fields[1] != branch_ref or not re.fullmatch(r"[0-9a-fA-F]{40}", remote_fields[0]):
4861
+ raise RuntimeError("Fresh analysis requires an exact remote branch advertisement")
4862
+ try:
4847
4863
  await self._git(
4848
- "fetch", "origin", f"+{name}:refs/remotes/origin/{name}", cwd=repository,
4864
+ "fetch", "origin", f"+{branch_name}:{tracking_ref}", cwd=repository,
4849
4865
  timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
4850
4866
  )
4867
+ except RuntimeError:
4868
+ remaining = (await self._git(
4869
+ "ls-remote", "--heads", "origin", branch_ref, cwd=repository,
4870
+ timeout=settings.GIT_FETCH_TIMEOUT, project_key=project_key,
4871
+ )).strip()
4872
+ if remaining:
4873
+ raise
4874
+ await self._git("update-ref", "-d", tracking_ref, cwd=repository)
4875
+ logger.info("Fresh analysis branch %s disappeared during fetch; removed its stale tracking reference", branch_name)
4876
+ return
4851
4877
  old_sha = (await self._git("rev-parse", "--verify", f"refs/remotes/origin/{branch_name}", cwd=repository)).strip()
4852
4878
  default_sha = (await self._git("rev-parse", "--verify", f"refs/remotes/origin/{default_branch}", cwd=repository)).strip()
4853
4879
  if not all(re.fullmatch(r"[0-9a-fA-F]{40}", value) for value in (old_sha, default_sha)):
4854
4880
  raise RuntimeError("Fresh analysis requires verified remote branch commits")
4881
+ if old_sha != remote_fields[0]:
4882
+ raise RuntimeError("Remote requirement branch changed during fresh analysis preparation")
4855
4883
  if old_sha == default_sha:
4856
4884
  return
4857
4885
  backup_ref = f"refs/tags/forgexa/fresh-analysis-backup/{branch_name}/{old_sha}"
@@ -13970,6 +13998,7 @@ class RuntimeDaemon:
13970
13998
 
13971
13999
  after_sha = str((result.git or {}).get("commit_sha") or before_sha or "").strip()
13972
14000
  committed_sources: dict[str, str] = {}
14001
+ unavailable_sources: list[dict[str, str]] = []
13973
14002
  source_bytes = 0
13974
14003
  coverage = {
13975
14004
  "candidate_files": 0, "full_source_files": 0, "unreadable_files": 0,
@@ -13998,6 +14027,27 @@ class RuntimeDaemon:
13998
14027
  "test-cases.json", "coverage-matrix.json", "test-evidence.json", "test-report.md",
13999
14028
  }
14000
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
14001
14051
  paths.extend(asset_paths)
14002
14052
  support_dirs = sorted({(Path(path).parent / "support").as_posix() for path in asset_paths if path.startswith("tests/")})
14003
14053
  for directory in support_dirs[:5]:
@@ -14089,8 +14139,19 @@ class RuntimeDaemon:
14089
14139
  source = await self.workspace_manager._git(
14090
14140
  "show", f"{after_sha}:{path}", cwd=workspace_path, timeout=10,
14091
14141
  )
14092
- except Exception:
14142
+ except Exception as error:
14093
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})
14094
14155
  continue
14095
14156
  encoded_size = len(source.encode("utf-8"))
14096
14157
  if source_bytes + encoded_size <= _RUNTIME_CRITIC_SOURCE_MAX_BYTES:
@@ -14108,7 +14169,10 @@ class RuntimeDaemon:
14108
14169
  diff = (diff or "No new changes; review committed delivery instead.") + "".join(source_context)
14109
14170
  coverage["full_source_files"] = len(committed_sources)
14110
14171
  coverage["omitted_files"] = coverage["candidate_files"] - coverage["full_source_files"]
14111
- 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
+ )
14112
14176
  result.metrics["runtime_critic_coverage"] = coverage
14113
14177
  if not diff:
14114
14178
  return {
@@ -14136,6 +14200,8 @@ class RuntimeDaemon:
14136
14200
  work_item={},
14137
14201
  )
14138
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)
14139
14205
  if coverage["capsule_incomplete"]:
14140
14206
  review_task.input_prompt += (
14141
14207
  "\nSome candidate files were not available as complete source. Assess the evidence you can inspect; "
@@ -14155,6 +14221,7 @@ class RuntimeDaemon:
14155
14221
  "reviewed_commit_sha": after_sha,
14156
14222
  "baseline_fingerprint": baseline.get("fingerprint") if isinstance(baseline, dict) else None,
14157
14223
  "input_sha256": hashlib.sha256(review_task.input_prompt.encode("utf-8")).hexdigest(),
14224
+ "unavailable_sources": unavailable_sources,
14158
14225
  "snapshots": [
14159
14226
  {"path": path, "sha256": hashlib.sha256(source.encode("utf-8")).hexdigest(),
14160
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.84.4
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.84.4"
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