forgexa-cli 1.79.2__tar.gz → 1.79.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.
Files changed (33) hide show
  1. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/agent_core.py +28 -0
  4. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/daemon.py +109 -9
  5. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/pyproject.toml +1 -1
  7. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/README.md +0 -0
  8. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/aws_evidence.py +0 -0
  12. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/aws_logs_evidence.py +0 -0
  13. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/observability_contract.py +0 -0
  15. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/observability_evidence.py +0 -0
  16. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/py.typed +0 -0
  17. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/runtime_evidence.py +0 -0
  18. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli/zcode_protocol.py +0 -0
  19. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  20. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  21. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  22. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli.egg-info/requires.txt +0 -0
  23. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  24. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/setup.cfg +0 -0
  25. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_auth_and_runtime_commands.py +0 -0
  26. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_autoupgrade.py +0 -0
  27. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_check_command.py +0 -0
  28. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
  29. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_local_bind_commands.py +0 -0
  30. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_runtime_credentials.py +0 -0
  31. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_session_credentials.py +0 -0
  32. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/tests/test_silent_install.py +0 -0
  33. {forgexa_cli-1.79.2 → forgexa_cli-1.79.3}/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.79.2
3
+ Version: 1.79.3
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.79.2"
2
+ __version__ = "1.79.3"
@@ -106,6 +106,25 @@ def is_implementation_testing_task(node_type: str, input_data: dict) -> bool:
106
106
  )
107
107
 
108
108
 
109
+ def validate_task_delivery_baseline(
110
+ baseline: object, node_id: str, graph_id: str, branch: str | None = None,
111
+ ) -> str:
112
+ if not isinstance(baseline, dict) or (
113
+ type(baseline.get("version")) is not int or baseline["version"] != 1
114
+ or set(baseline) - {"version", "node_id", "graph_id", "branch", "before_sha", "anchor_node_id"}
115
+ or not baseline.get("node_id") or not baseline.get("graph_id")
116
+ or baseline.get("node_id") != str(node_id)
117
+ or baseline.get("graph_id") != str(graph_id)
118
+ or not isinstance(baseline.get("branch"), str)
119
+ or not baseline["branch"].strip()
120
+ or len(baseline["branch"]) > 255
121
+ or branch is not None and baseline["branch"] != branch
122
+ or not re.fullmatch(r"[0-9a-fA-F]{40}", str(baseline.get("before_sha") or ""))
123
+ ):
124
+ raise ValueError("Planned task delivery baseline does not match its task, graph, or branch")
125
+ return baseline["before_sha"].lower()
126
+
127
+
109
128
  def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> list[str]:
110
129
  """Find planned files without delivery evidence, including grouped sub-tasks."""
111
130
  def normalize_path(value: object) -> str | None:
@@ -142,6 +161,15 @@ def missing_planned_task_files(task_input: dict, changed_files: list[str]) -> li
142
161
  )
143
162
 
144
163
 
164
+ def requires_task_delivery_baseline(node_type: str, task_input: dict) -> bool:
165
+ return (
166
+ node_type == "coding" or node_type == "fix" and task_input.get("task") == "phase_remediation_fix"
167
+ ) and (
168
+ task_input.get("planned_task_git_baseline") is not None
169
+ or bool(missing_planned_task_files(task_input, []))
170
+ )
171
+
172
+
145
173
  def extract_implementation_audit(text: str) -> dict | None:
146
174
  """Read the last complete audit envelope from mixed assistant output."""
147
175
  decoder = json.JSONDecoder()
@@ -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.79.2"
1237
+ DAEMON_VERSION = "1.79.3"
1238
1238
 
1239
1239
 
1240
1240
  def _detect_client_type() -> str:
@@ -8950,6 +8950,7 @@ class ProgressReporter:
8950
8950
  lines_added: int = 0,
8951
8951
  lines_removed: int = 0,
8952
8952
  execution_attempt_id: str | None = None,
8953
+ task_delivery_baseline: dict | None = None,
8953
8954
  ) -> bool:
8954
8955
  output_lines = [
8955
8956
  *self._pending_output_lines.get(task_id, []),
@@ -8957,7 +8958,7 @@ class ProgressReporter:
8957
8958
  ]
8958
8959
  activity_lines = activity_lines or []
8959
8960
  files_changed = files_changed or []
8960
- if self._should_skip_progress(
8961
+ if task_delivery_baseline is None and self._should_skip_progress(
8961
8962
  task_id,
8962
8963
  progress_percent,
8963
8964
  current_step,
@@ -8981,12 +8982,15 @@ class ProgressReporter:
8981
8982
  "lines_added": lines_added,
8982
8983
  "lines_removed": lines_removed,
8983
8984
  "execution_attempt_id": execution_attempt_id,
8985
+ **({"task_delivery_baseline": task_delivery_baseline} if task_delivery_baseline is not None else {}),
8984
8986
  },
8985
8987
  timeout=10,
8986
8988
  )
8987
8989
  if 200 <= response.status_code < 300:
8988
8990
  if response.json().get("status") == "ignored":
8989
8991
  return False
8992
+ if task_delivery_baseline is not None and response.json().get("task_delivery_baseline") != task_delivery_baseline:
8993
+ return False
8990
8994
  self._pending_output_lines.pop(task_id, None)
8991
8995
  self._last_progress_sent[task_id] = {
8992
8996
  "progress_percent": progress_percent,
@@ -9946,6 +9950,7 @@ class ServerConnection:
9946
9950
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
9947
9951
  },
9948
9952
  "implementation_audit": {"protocol_version": 3},
9953
+ "task_delivery": {"protocol_version": 1},
9949
9954
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
9950
9955
  },
9951
9956
  "server_url": self.server_url,
@@ -10370,6 +10375,7 @@ class ServerConnection:
10370
10375
  "protocol_version": runtime_evidence.DEFERRED_ACCEPTANCE_VERIFICATION_PROTOCOL_VERSION,
10371
10376
  },
10372
10377
  "implementation_audit": {"protocol_version": 3},
10378
+ "task_delivery": {"protocol_version": 1},
10373
10379
  "local_evidence": {"protocol_version": 1, "structured_summary": True},
10374
10380
  "task_attachment_staging": {"protocol_version": 1, "runtime_proxy": True, "image_agents": ["claude"]},
10375
10381
  },
@@ -12608,6 +12614,53 @@ class RuntimeDaemon:
12608
12614
  f"wipe_analysis_dir failed for {output_dir_norm}: {_wipe_err}"
12609
12615
  ) from _wipe_err
12610
12616
 
12617
+ async def _resolve_task_delivery_before_sha(
12618
+ self, task: TaskInfo, workspace_path: Path, attempt_before_sha: str,
12619
+ ) -> str:
12620
+ baseline = (task.input_data or {}).get("planned_task_git_baseline")
12621
+ if baseline is None and not agent_core.requires_task_delivery_baseline(task.node_type, task.input_data or {}):
12622
+ return attempt_before_sha
12623
+ if not re.fullmatch(r"[0-9a-fA-F]{40}", attempt_before_sha):
12624
+ raise ValueError("Planned task attempt has no verified starting commit")
12625
+ try:
12626
+ branch = (await self.workspace_manager._git(
12627
+ "rev-parse", "--abbrev-ref", "HEAD", cwd=workspace_path, timeout=10,
12628
+ )).strip()
12629
+ except Exception as error:
12630
+ raise ValueError("Planned task delivery branch could not be verified") from error
12631
+ if baseline is None:
12632
+ baseline = {
12633
+ "version": 1, "node_id": task.task_id, "graph_id": task.graph_id,
12634
+ "branch": branch, "before_sha": attempt_before_sha,
12635
+ }
12636
+ task.input_data = {**(task.input_data or {}), "planned_task_git_baseline": baseline}
12637
+ before_sha = agent_core.validate_task_delivery_baseline(
12638
+ baseline, task.task_id, task.graph_id, branch,
12639
+ )
12640
+ try:
12641
+ await self.workspace_manager._git(
12642
+ "merge-base", "--is-ancestor", before_sha, "HEAD", cwd=workspace_path, timeout=10,
12643
+ )
12644
+ except Exception as error:
12645
+ raise ValueError("Planned task delivery baseline is not a verified ancestor of HEAD") from error
12646
+ return before_sha
12647
+
12648
+ async def _collect_task_delivery_git(
12649
+ self, task: TaskInfo, workspace_path: Path, result: TaskResult, *, attempt_before_sha: str,
12650
+ ) -> None:
12651
+ if task.node_type != "coding" and not (
12652
+ task.node_type == "fix" and (task.input_data or {}).get("task") == "phase_remediation_fix"
12653
+ ):
12654
+ return
12655
+ before_sha = await self._resolve_task_delivery_before_sha(task, workspace_path, attempt_before_sha)
12656
+ git_info = await self.process_manager._collect_git_info_vs_parent(workspace_path, before_sha=before_sha)
12657
+ if git_info.get("files_changed_verified") is not True:
12658
+ raise ValueError("Planned task cumulative Git delivery could not be verified")
12659
+ result.git = {**(result.git or {}), **git_info, "attempt_before_sha": attempt_before_sha}
12660
+ result.files_changed = list(git_info.get("files_changed") or [])
12661
+ result.lines_added = git_info.get("lines_added", 0)
12662
+ result.lines_removed = git_info.get("lines_removed", 0)
12663
+
12611
12664
  async def _execute_task(self, task: TaskInfo, conn: ServerConnection):
12612
12665
  """Execute a single task, reporting to the originating server connection."""
12613
12666
  reporter = conn.reporter
@@ -12617,7 +12670,22 @@ class RuntimeDaemon:
12617
12670
  # skipped for a node bound to the user's own real directory. See
12618
12671
  # docs/designs/forgexa-ide-design.md §6.2.
12619
12672
  is_local_bind = task.workspace_resolution == "local_bind"
12673
+ workspace_path: Path | None = None
12674
+ node_before_sha = ""
12675
+ delivery_before_sha = ""
12620
12676
  async def report_task_complete(result: TaskResult) -> None:
12677
+ if workspace_path is not None and node_before_sha:
12678
+ try:
12679
+ await self._collect_task_delivery_git(
12680
+ task, workspace_path, result, attempt_before_sha=node_before_sha,
12681
+ )
12682
+ except ValueError as error:
12683
+ if result.status == "success":
12684
+ result.status = "failed"
12685
+ result.failure_code = "planned_task_baseline_invalid"
12686
+ result.error = str(error)
12687
+ else:
12688
+ result.metrics["task_delivery_issue"] = str(error)
12621
12689
  if is_local_bind:
12622
12690
  if result.error:
12623
12691
  _append_local_bind_output_log(task.task_id, [f"[daemon] {result.error}"])
@@ -12640,7 +12708,6 @@ class RuntimeDaemon:
12640
12708
  return
12641
12709
 
12642
12710
  default_branch = str((task.project or {}).get("default_branch") or "").strip()
12643
- node_before_sha = ""
12644
12711
  # Use workspace-level granularity (requirement_workflow_id or graph_id) so that
12645
12712
  # tasks for DIFFERENT requirements can run in parallel on the same daemon, while
12646
12713
  # tasks for the SAME requirement (e.g. analysis + retry) are still serialized to
@@ -12839,6 +12906,29 @@ class RuntimeDaemon:
12839
12906
  _before_sha_err,
12840
12907
  )
12841
12908
 
12909
+ try:
12910
+ delivery_before_sha = await self._resolve_task_delivery_before_sha(task, workspace_path, node_before_sha)
12911
+ except ValueError as error:
12912
+ await report_task_complete(TaskResult(
12913
+ status="failed", exit_code=-1, stdout="", stderr="", error=str(error),
12914
+ failure_code="planned_task_baseline_invalid", execution_attempt_id=task.execution_attempt_id,
12915
+ ))
12916
+ return
12917
+
12918
+ if agent_core.requires_task_delivery_baseline(task.node_type, task.input_data or {}):
12919
+ confirmed = await reporter.report_progress(
12920
+ task.task_id, 9, "freezing_task_delivery_baseline",
12921
+ execution_attempt_id=task.execution_attempt_id,
12922
+ task_delivery_baseline=task.input_data["planned_task_git_baseline"],
12923
+ )
12924
+ if not confirmed:
12925
+ await report_task_complete(TaskResult(
12926
+ status="failed", exit_code=-1, stdout="", stderr="",
12927
+ error="Server did not confirm the durable task delivery baseline; update the backend and verify connectivity before retrying",
12928
+ failure_code="planned_task_baseline_invalid", execution_attempt_id=task.execution_attempt_id,
12929
+ ))
12930
+ return
12931
+
12842
12932
  # 3. Run agent with real-time output streaming + periodic progress heartbeat
12843
12933
  await reporter.report_progress(task.task_id, 10, "running_agent", execution_attempt_id=task.execution_attempt_id)
12844
12934
 
@@ -13198,7 +13288,7 @@ class RuntimeDaemon:
13198
13288
  result, agent = await self._validate_with_agent_fallback(
13199
13289
  agent, task, workspace_path, result,
13200
13290
  reporter, on_output_chunk, tried_agents=tried_agents,
13201
- default_branch=default_branch, before_sha=node_before_sha,
13291
+ default_branch=default_branch, before_sha=delivery_before_sha,
13202
13292
  )
13203
13293
  # Re-collect git info if validation triggered retries
13204
13294
  if result.metrics.get("validation_issues") is not None or True:
@@ -13280,7 +13370,7 @@ class RuntimeDaemon:
13280
13370
  result, agent = await self._validate_with_agent_fallback(
13281
13371
  agent, task, workspace_path, result,
13282
13372
  reporter, on_output_chunk, tried_agents=tried_agents,
13283
- default_branch=default_branch, before_sha=node_before_sha,
13373
+ default_branch=default_branch, before_sha=delivery_before_sha,
13284
13374
  )
13285
13375
  pre_commit_git = await self.process_manager._collect_git_info(workspace_path)
13286
13376
  except Exception:
@@ -13416,7 +13506,7 @@ class RuntimeDaemon:
13416
13506
  try:
13417
13507
  result = await self._validate_and_retry(
13418
13508
  agent, task, workspace_path, result, reporter, on_output_chunk,
13419
- max_retries=0, default_branch=default_branch, before_sha=node_before_sha,
13509
+ max_retries=0, default_branch=default_branch, before_sha=delivery_before_sha,
13420
13510
  )
13421
13511
  except Exception:
13422
13512
  _mark_qa_source_validation_unavailable(result)
@@ -13526,7 +13616,7 @@ class RuntimeDaemon:
13526
13616
  post_commit_git = await self.process_manager._collect_git_info_vs_parent(
13527
13617
  workspace_path,
13528
13618
  default_branch=default_branch,
13529
- before_sha=node_before_sha,
13619
+ before_sha=delivery_before_sha,
13530
13620
  )
13531
13621
  result.git = post_commit_git
13532
13622
  result.files_changed = result.git.get("files_changed", [])
@@ -13574,13 +13664,16 @@ class RuntimeDaemon:
13574
13664
  )
13575
13665
 
13576
13666
  if result.status == "success" and task.critic_review_required:
13667
+ await self._collect_task_delivery_git(
13668
+ task, workspace_path, result, attempt_before_sha=node_before_sha,
13669
+ )
13577
13670
  await reporter.report_progress(task.task_id, 95, "running_critic_review", execution_attempt_id=task.execution_attempt_id)
13578
13671
  result.critic_review = await self._run_runtime_critic_review(
13579
13672
  task,
13580
13673
  agent,
13581
13674
  workspace_path,
13582
13675
  result,
13583
- before_sha=node_before_sha,
13676
+ before_sha=delivery_before_sha,
13584
13677
  )
13585
13678
 
13586
13679
  # 6. Report completion (include actual agent used if different from requested)
@@ -16146,8 +16239,12 @@ class RuntimeDaemon:
16146
16239
  qa_policy_violations = [
16147
16240
  issue for issue in remaining if issue.startswith("qa_policy.")
16148
16241
  ]
16242
+ planned_delivery_issues = [
16243
+ issue for issue in remaining
16244
+ if issue.startswith("Planned task file has no implementation evidence:")
16245
+ ]
16149
16246
  blocking_issues = list(dict.fromkeys(
16150
- critical_issues + scope_violations + qa_policy_violations
16247
+ critical_issues + scope_violations + qa_policy_violations + planned_delivery_issues
16151
16248
  ))
16152
16249
  if blocking_issues:
16153
16250
  # Agent didn't produce required output — mark as failed
@@ -16161,6 +16258,9 @@ class RuntimeDaemon:
16161
16258
  if scope_violations:
16162
16259
  result.failure_code = "verification_work_item_scope_violation"
16163
16260
  result.error = "; ".join(scope_violations[:3])
16261
+ elif planned_delivery_issues:
16262
+ result.failure_code = "planned_task_incomplete"
16263
+ result.error = "; ".join(planned_delivery_issues[:3])
16164
16264
  else:
16165
16265
  result.failure_code = "validation_retry_exhausted"
16166
16266
  result.error = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.79.2
3
+ Version: 1.79.3
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.79.2"
3
+ version = "1.79.3"
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