forgexa-cli 1.13.5__tar.gz → 1.14.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.13.5
3
+ Version: 1.14.0
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: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.13.5"
2
+ __version__ = "1.14.0"
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
523
523
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
524
524
  # Kept in sync with pyproject.toml version via bump-version.sh.
525
525
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
526
- DAEMON_VERSION = "1.13.5"
526
+ DAEMON_VERSION = "1.14.0"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -1585,6 +1585,14 @@ class WorkspaceManager:
1585
1585
  "connection closed",
1586
1586
  "connection refused",
1587
1587
  "network is unreachable",
1588
+ # DNS resolution failures — transient (network reconnect, DHCP renewal,
1589
+ # macOS resolver flap). macOS/BSD SSH prints "nodename nor servname
1590
+ # provided"; Linux SSH/git prints "name or service not known" or
1591
+ # "temporary failure in name resolution".
1592
+ "nodename nor servname",
1593
+ "name or service not known",
1594
+ "could not resolve host",
1595
+ "temporary failure in name resolution",
1588
1596
  )
1589
1597
  return any(marker in err for marker in retry_markers)
1590
1598
 
@@ -2899,9 +2907,12 @@ class ProcessManager:
2899
2907
  "connection reset",
2900
2908
  "connection timed out",
2901
2909
  "connection error",
2910
+ "failed to get response from the ai model",
2902
2911
  "name or service not known",
2903
2912
  "no such host",
2904
2913
  "network is unreachable",
2914
+ "websockettransporterror",
2915
+ "websocket receive failed",
2905
2916
  # "api error" removed: too broad — matches agent-generated code/output
2906
2917
  # discussing API errors. Real API transport errors are covered by the
2907
2918
  # connection patterns above (refused, reset, timed out, etc.).
@@ -3032,6 +3043,21 @@ class ProcessManager:
3032
3043
  else:
3033
3044
  has_result = True
3034
3045
  has_meaningful_content = True
3046
+ elif ev_type == "session.error":
3047
+ err = data.get("data") or {}
3048
+ if isinstance(err, dict):
3049
+ msg = (
3050
+ err.get("message")
3051
+ or err.get("error")
3052
+ or err.get("errorType")
3053
+ or ""
3054
+ )
3055
+ if msg:
3056
+ error_messages.append(str(msg))
3057
+ elif isinstance(err, str) and err.strip():
3058
+ error_messages.append(err)
3059
+ elif isinstance(data.get("message"), str) and data.get("message", "").strip():
3060
+ error_messages.append(data["message"])
3035
3061
  elif ev_type == "error":
3036
3062
  msg = data.get("message", "")
3037
3063
  if msg:
@@ -4198,7 +4224,27 @@ class ProcessManager:
4198
4224
  # Copilot always exits 0 on normal completion; check result.exitCode
4199
4225
  # from the JSONL "result" event for a true success signal.
4200
4226
  copilot_exit = self._extract_copilot_exit_code(stdout)
4201
- completed_without_result = self._copilot_completed_without_result(stdout)
4227
+ signals = self._extract_output_signals(stdout)
4228
+ completed_without_result = bool(
4229
+ signals["has_turn_completed"]
4230
+ and signals["has_assistant_events"]
4231
+ and signals["has_meaningful_content"]
4232
+ and not signals["has_result"]
4233
+ and not signals["has_turn_failed"]
4234
+ and not signals["error_messages"]
4235
+ )
4236
+ structured_errors = [
4237
+ str(msg).strip()
4238
+ for msg in (signals.get("error_messages") or [])
4239
+ if str(msg).strip()
4240
+ ]
4241
+ copilot_specific_error = ""
4242
+ for msg in reversed(structured_errors):
4243
+ prefix = "Copilot exited with code "
4244
+ suffix = msg[len(prefix):].strip() if msg.startswith(prefix) else ""
4245
+ if not (msg.startswith(prefix) and suffix.isdigit()):
4246
+ copilot_specific_error = msg
4247
+ break
4202
4248
  effective_rc = copilot_exit if copilot_exit is not None else returncode
4203
4249
 
4204
4250
  if effective_rc == 0 and returncode == 0:
@@ -4236,7 +4282,13 @@ class ProcessManager:
4236
4282
  # Retry 2: if still failing AND we haven't already tried both drops,
4237
4283
  # the error is likely auth/quota — log actionable hint.
4238
4284
  _no_tools = not self._copilot_called_any_tools(stdout)
4239
- if effective_rc == 1 and _no_tools and reasoning and not _retry_without_effort:
4285
+ if (
4286
+ effective_rc == 1
4287
+ and _no_tools
4288
+ and reasoning
4289
+ and not _retry_without_effort
4290
+ and not copilot_specific_error
4291
+ ):
4240
4292
  logger.warning(
4241
4293
  "Copilot exitCode=1 with no tool calls for task %s — "
4242
4294
  "retrying without --effort (flag may be unsupported by this CLI version). "
@@ -4248,7 +4300,7 @@ class ProcessManager:
4248
4300
  _retry_without_effort=True,
4249
4301
  )
4250
4302
  # Exhausted retries — emit an actionable diagnostic hint
4251
- if effective_rc == 1 and _no_tools:
4303
+ if effective_rc == 1 and _no_tools and not copilot_specific_error:
4252
4304
  logger.error(
4253
4305
  "Copilot exitCode=1 with no tool calls for task %s after retries. "
4254
4306
  "Likely causes: (1) GitHub auth expired — run `gh auth status` and "
@@ -4257,12 +4309,18 @@ class ProcessManager:
4257
4309
  "To reproduce manually: %s",
4258
4310
  task_id, agent.version, _cmd_display,
4259
4311
  )
4312
+ if copilot_specific_error:
4313
+ failure_error = copilot_specific_error[:1500]
4314
+ elif stderr.strip():
4315
+ failure_error = f"Copilot exited with code {effective_rc}: {stderr[-500:].strip()}"
4316
+ else:
4317
+ failure_error = f"Copilot exited with code {effective_rc}"
4260
4318
  return TaskResult(
4261
4319
  status="failed",
4262
4320
  exit_code=effective_rc,
4263
4321
  stdout=stdout[-settings.AGENT_MAX_OUTPUT_SIZE:],
4264
4322
  stderr=stderr[-10000:],
4265
- error=f"Copilot exited with code {effective_rc}: {stderr[-500:]}",
4323
+ error=failure_error,
4266
4324
  metrics=metrics,
4267
4325
  )
4268
4326
  except asyncio.TimeoutError as exc:
@@ -6553,6 +6611,39 @@ class RuntimeDaemon:
6553
6611
  if result.status == "success" and task.node_type == "fix":
6554
6612
  await self._collect_bugfix_artifacts(workspace_path, task, result)
6555
6613
 
6614
+ # 4.9 For review/coding/testing nodes: attach updated analysis.json inline
6615
+ # so the backend always receives the latest snapshot even if push fails.
6616
+ # This closes the mirror-sync gap: review agents update analysis.json
6617
+ # (open_risks, phase_handoffs) and this step ensures the content reaches
6618
+ # runtimes.py as an inline artifact regardless of push success.
6619
+ if result.status == "success" and task.node_type in ("review", "coding", "testing"):
6620
+ analysis_dir = (
6621
+ (task.input_data or {}).get("analysis_output_dir", "")
6622
+ or ""
6623
+ ).replace("\\", "/").lstrip("./").rstrip("/")
6624
+ if analysis_dir:
6625
+ _aj_path = workspace_path / analysis_dir / "analysis.json"
6626
+ if _aj_path.exists() and _aj_path.stat().st_size > 0:
6627
+ try:
6628
+ _aj_rel = str(_aj_path.relative_to(workspace_path)).replace("\\", "/")
6629
+ _existing_paths = {a.get("path", "").replace("\\", "/") for a in result.artifacts}
6630
+ if _aj_rel not in _existing_paths:
6631
+ _aj_content = _aj_path.read_text(encoding="utf-8", errors="replace")
6632
+ result.artifacts.append({
6633
+ "path": _aj_rel,
6634
+ "content": _aj_content,
6635
+ "type": "application/json",
6636
+ })
6637
+ logger.debug(
6638
+ "Task %s (%s): attached updated analysis.json inline (%d bytes)",
6639
+ task.task_id, task.node_type, len(_aj_content),
6640
+ )
6641
+ except Exception as _aj_err:
6642
+ logger.debug(
6643
+ "Task %s: could not attach analysis.json artifact: %s",
6644
+ task.task_id, _aj_err,
6645
+ )
6646
+
6556
6647
  # 5. Auto-commit and push if changes exist
6557
6648
  if result.status == "success":
6558
6649
  commit_result = await self._auto_commit(workspace_path, task)
@@ -6733,9 +6824,31 @@ class RuntimeDaemon:
6733
6824
  json_path = base / "analysis.json"
6734
6825
  if json_path.exists() and json_path.stat().st_size > 0:
6735
6826
  try:
6736
- _json.loads(json_path.read_text(encoding="utf-8"))
6827
+ _analysis_data = _json.loads(json_path.read_text(encoding="utf-8"))
6737
6828
  except _json.JSONDecodeError as e:
6738
6829
  issues.append(f"analysis.json is not valid JSON: {e}")
6830
+ _analysis_data = None
6831
+ # Task 6: validate phase_handoffs and open_risks fields
6832
+ if _analysis_data is not None:
6833
+ if "open_risks" not in _analysis_data:
6834
+ issues.append(
6835
+ "analysis.json missing 'open_risks' field "
6836
+ "(expected empty array [] at minimum)"
6837
+ )
6838
+ handoffs = _analysis_data.get("phase_handoffs")
6839
+ if not handoffs or not isinstance(handoffs, list):
6840
+ issues.append(
6841
+ "analysis.json missing 'phase_handoffs' array "
6842
+ "(must contain at least one entry with phase='analysis')"
6843
+ )
6844
+ elif not any(
6845
+ isinstance(h, dict) and h.get("phase") == "analysis"
6846
+ for h in handoffs
6847
+ ):
6848
+ issues.append(
6849
+ "analysis.json phase_handoffs has no entry "
6850
+ "with phase='analysis'"
6851
+ )
6739
6852
 
6740
6853
  # Validate test-intent.json if required by this type
6741
6854
  if "test-intent.json" in required_files:
@@ -6790,7 +6903,7 @@ class RuntimeDaemon:
6790
6903
  elif design_path.stat().st_size == 0:
6791
6904
  issues.append(f"Design document is empty: {doc_dir}/design.md")
6792
6905
 
6793
- elif node_type in ("coding", "testing", "fix"):
6906
+ elif node_type in ("coding", "testing", "fix", "review"):
6794
6907
  # Syntax-check modified Python and JS/TS files
6795
6908
  for f in result.files_changed:
6796
6909
  fpath = workspace_path / f
@@ -6813,6 +6926,29 @@ class RuntimeDaemon:
6813
6926
  f"bracket imbalance (open={opens}, close={closes})"
6814
6927
  )
6815
6928
 
6929
+ # Task 6 (P6 fix): warning-level phase_handoffs check for non-analysis
6930
+ # nodes. These nodes SHOULD append a phase_handoffs entry to analysis.json
6931
+ # after completion. Missing entry is NOT blocking (agent may have skipped
6932
+ # WRITEBACK) but is logged for observability and instructions iteration.
6933
+ _ao_dir = (task.input_data or {}).get("analysis_output_dir", "")
6934
+ if _ao_dir:
6935
+ _aj_p = workspace_path / _ao_dir.lstrip("./").rstrip("/").replace("\\", "/") / "analysis.json"
6936
+ if _aj_p.exists() and _aj_p.stat().st_size > 0:
6937
+ try:
6938
+ _aj_d = _json.loads(_aj_p.read_text(encoding="utf-8"))
6939
+ _handoffs = _aj_d.get("phase_handoffs") or []
6940
+ if not any(
6941
+ isinstance(h, dict) and h.get("phase") == node_type
6942
+ for h in _handoffs
6943
+ ):
6944
+ logger.info(
6945
+ "Node %s (type=%s) did not append phase_handoffs "
6946
+ "entry to analysis.json — agent may have skipped WRITEBACK",
6947
+ task.task_id, node_type,
6948
+ )
6949
+ except (_json.JSONDecodeError, UnicodeDecodeError):
6950
+ pass # analysis.json corrupt — already flagged elsewhere
6951
+
6816
6952
  # Testing-specific: validate structured test assets
6817
6953
  if node_type == "testing":
6818
6954
  # Determine which checks to run for this requirement type.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.13.5
3
+ Version: 1.14.0
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: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.13.5"
3
+ version = "1.14.0"
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 = { text = "MIT" }
File without changes
File without changes