forgexa-cli 1.13.4__tar.gz → 1.13.5__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.4
3
+ Version: 1.13.5
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.4"
2
+ __version__ = "1.13.5"
@@ -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.4"
526
+ DAEMON_VERSION = "1.13.5"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -3137,6 +3137,31 @@ class ProcessManager:
3137
3137
  and not signals["error_messages"]
3138
3138
  )
3139
3139
 
3140
+ @staticmethod
3141
+ def _copilot_called_any_tools(stdout: str) -> bool:
3142
+ """Return True if the Copilot output contains at least one tool call.
3143
+
3144
+ Used to distinguish an early API-rejection failure (exitCode=1 with
3145
+ no work done) from a mid-run failure where the CLI did attempt tool
3146
+ calls before encountering an error.
3147
+ """
3148
+ for raw in (stdout or "").split("\n"):
3149
+ raw = raw.strip()
3150
+ if not raw:
3151
+ continue
3152
+ try:
3153
+ data = json.loads(raw)
3154
+ except json.JSONDecodeError:
3155
+ continue
3156
+ if isinstance(data, dict):
3157
+ ev = data.get("type", "")
3158
+ # tool.execution_start / tool_use / assistant.tool_calls are
3159
+ # all signals that at least one tool was invoked.
3160
+ if ev in ("tool.execution_start", "tool_use", "tool_result",
3161
+ "tool.execution_complete", "assistant.tool_calls"):
3162
+ return True
3163
+ return False
3164
+
3140
3165
  @staticmethod
3141
3166
  def is_rate_limited(result: "TaskResult") -> bool:
3142
3167
  """Check if an agent failure warrants trying a different agent.
@@ -4097,6 +4122,8 @@ class ProcessManager:
4097
4122
  async def _run_copilot(
4098
4123
  self, agent: DiscoveredAgent, prompt: str, cwd: Path, timeout: int, task_id: str,
4099
4124
  on_chunk: Any = None,
4125
+ *,
4126
+ _retry_without_effort: bool = False,
4100
4127
  ) -> TaskResult:
4101
4128
  """Run GitHub Copilot CLI in non-interactive JSON-streaming mode.
4102
4129
 
@@ -4105,30 +4132,50 @@ class ProcessManager:
4105
4132
  ``gh auth login`` or GITHUB_TOKEN env var must be set.
4106
4133
 
4107
4134
  Flags:
4108
- -p / --prompt Non-interactive prompt (exits after completion).
4135
+ -p / --prompt Non-interactive prompt (exits after completion).
4109
4136
  --output-format json JSONL stream of session events.
4110
- --allow-all Grant all tool + path + URL permissions required
4111
- for autonomous file-editing tasks.
4112
- -C <dir> Change working directory before execution.
4137
+ --allow-all Grant all tool + path + URL permissions required
4138
+ for autonomous file-editing tasks.
4139
+ -C <dir> Change working directory before execution.
4140
+
4141
+ Compatibility note:
4142
+ ``--effort`` is only included when ``FACTORY_COPILOT_REASONING`` is
4143
+ explicitly set to a non-empty value. Older Copilot CLI releases (and
4144
+ some VS Code–bundled variants) don't accept ``--effort``; passing it
4145
+ makes the CLI return exitCode=1 in the JSONL result event immediately
4146
+ without calling any tools. Making it opt-in avoids this incompatibility
4147
+ while still allowing operators to tune effort on supported versions.
4113
4148
  """
4114
4149
  env = os.environ.copy()
4115
4150
  env["TERM"] = "dumb" # suppress TTY-detection that suspends the process
4116
4151
  env["COPILOT_HOME"] = self._prepare_copilot_home()
4117
4152
 
4118
4153
  model_override = os.environ.get("FACTORY_COPILOT_MODEL")
4119
- reasoning = os.environ.get("FACTORY_COPILOT_REASONING", "medium")
4154
+ # FACTORY_COPILOT_REASONING is opt-in: only pass --effort when the env
4155
+ # var is explicitly set to a non-empty string. The default (unset / "")
4156
+ # intentionally omits --effort so that Copilot CLI versions that don't
4157
+ # support the flag don't immediately fail with exitCode=1.
4158
+ reasoning = os.environ.get("FACTORY_COPILOT_REASONING", "").strip()
4120
4159
 
4121
4160
  cmd = [
4122
4161
  agent.command,
4123
4162
  "--output-format", "json",
4124
4163
  "--allow-all",
4125
- "--effort", reasoning,
4126
- "-C", str(cwd),
4127
- "-p", prompt,
4128
4164
  ]
4165
+ if reasoning and not _retry_without_effort:
4166
+ cmd += ["--effort", reasoning]
4167
+ cmd += ["-C", str(cwd), "-p", prompt]
4129
4168
  if model_override:
4130
4169
  cmd = [agent.command, "--model", model_override] + cmd[1:]
4131
4170
 
4171
+ # Log the exact invocation (redact prompt body) so operators can
4172
+ # reproduce failures manually and diagnose CLI version incompatibilities.
4173
+ _cmd_display = " ".join(
4174
+ f'"{a}"' if " " in a else a
4175
+ for a in cmd[:-1] # omit the -p value (potentially huge/sensitive)
4176
+ ) + ' -p "<prompt>"'
4177
+ logger.debug("Copilot invocation for task %s: %s", task_id, _cmd_display)
4178
+
4132
4179
  try:
4133
4180
  proc = await asyncio.create_subprocess_exec(
4134
4181
  *cmd,
@@ -4178,6 +4225,38 @@ class ProcessManager:
4178
4225
  metrics=metrics,
4179
4226
  )
4180
4227
  else:
4228
+ # --- Immediate-failure retry ladder ---
4229
+ # If Copilot exited with code 1 and no tools were called, the CLI
4230
+ # rejected the initial API request before doing any work. This is a
4231
+ # flag-incompatibility or auth/quota symptom; retry with progressively
4232
+ # stripped flags to auto-heal across CLI versions.
4233
+ #
4234
+ # Retry 1: drop --effort (added in newer ghcs; unsupported in older
4235
+ # VS Code-bundled versions).
4236
+ # Retry 2: if still failing AND we haven't already tried both drops,
4237
+ # the error is likely auth/quota — log actionable hint.
4238
+ _no_tools = not self._copilot_called_any_tools(stdout)
4239
+ if effective_rc == 1 and _no_tools and reasoning and not _retry_without_effort:
4240
+ logger.warning(
4241
+ "Copilot exitCode=1 with no tool calls for task %s — "
4242
+ "retrying without --effort (flag may be unsupported by this CLI version). "
4243
+ "Invocation was: %s",
4244
+ task_id, _cmd_display,
4245
+ )
4246
+ return await self._run_copilot(
4247
+ agent, prompt, cwd, timeout, task_id, on_chunk,
4248
+ _retry_without_effort=True,
4249
+ )
4250
+ # Exhausted retries — emit an actionable diagnostic hint
4251
+ if effective_rc == 1 and _no_tools:
4252
+ logger.error(
4253
+ "Copilot exitCode=1 with no tool calls for task %s after retries. "
4254
+ "Likely causes: (1) GitHub auth expired — run `gh auth status` and "
4255
+ "`gh auth login --scopes copilot`; (2) Copilot subscription inactive; "
4256
+ "(3) --allow-all flag not supported by this CLI version (%s). "
4257
+ "To reproduce manually: %s",
4258
+ task_id, agent.version, _cmd_display,
4259
+ )
4181
4260
  return TaskResult(
4182
4261
  status="failed",
4183
4262
  exit_code=effective_rc,
@@ -6668,8 +6747,36 @@ class RuntimeDaemon:
6668
6747
  if not intents:
6669
6748
  issues.append("test-intent.json contains no test intents")
6670
6749
  for ti in intents[:20]:
6671
- if not ti.get("id") or not (ti.get("title") or ti.get("description")):
6672
- issues.append(f"Test intent missing 'id' or 'title'/'description': {ti.get('id', '?')}")
6750
+ # Accept common alternative field names that agents generate:
6751
+ # id also accept "test_id", "intent_id"
6752
+ # title → also accept "description", "summary", "name"
6753
+ _ti_id = (
6754
+ ti.get("id")
6755
+ or ti.get("test_id")
6756
+ or ti.get("intent_id")
6757
+ )
6758
+ _ti_label = (
6759
+ ti.get("title")
6760
+ or ti.get("description")
6761
+ or ti.get("summary")
6762
+ or ti.get("name")
6763
+ )
6764
+ if not _ti_id or not _ti_label:
6765
+ # Include actual keys present so the retry prompt gives
6766
+ # the agent precise feedback on what to fix.
6767
+ _present_keys = list(ti.keys())[:8]
6768
+ if not _ti_id:
6769
+ issues.append(
6770
+ f"Test intent missing required 'id' field "
6771
+ f"(found keys: {_present_keys}). "
6772
+ f"Each intent MUST have an 'id' field (e.g. \"id\": \"TI-001\")."
6773
+ )
6774
+ else:
6775
+ issues.append(
6776
+ f"Test intent '{_ti_id}' missing required 'title' or 'description' field "
6777
+ f"(found keys: {_present_keys}). "
6778
+ f"Each intent MUST have a 'title' field (e.g. \"title\": \"...\")."
6779
+ )
6673
6780
  break
6674
6781
  except _json.JSONDecodeError as e:
6675
6782
  issues.append(f"test-intent.json is not valid JSON: {e}")
@@ -27,6 +27,16 @@ Usage:
27
27
  forgexa gates pending
28
28
  forgexa config show
29
29
  forgexa --help
30
+
31
+ Daemon shortcut commands (aliases — no need to type `daemon`):
32
+ forgexa start -d
33
+ forgexa stop
34
+ forgexa restart
35
+ forgexa status
36
+ forgexa status --verbose
37
+ forgexa logs -f
38
+ forgexa logs -n 100 -f
39
+ forgexa agents
30
40
  """
31
41
  from __future__ import annotations
32
42
 
@@ -1387,6 +1397,59 @@ def main() -> None:
1387
1397
  )
1388
1398
  daemon_agents_p.add_argument("--all", action="store_true", help="Include all users' runtimes (admin only)")
1389
1399
 
1400
+ # ── Daemon shortcut commands ───────────────────────────────────────────────
1401
+ # Top-level aliases so users can type `forgexa start` instead of
1402
+ # `forgexa daemon start`. Both forms are fully equivalent.
1403
+ _start_sc = sub.add_parser(
1404
+ "start",
1405
+ help="Start local daemon (alias: forgexa daemon start)",
1406
+ )
1407
+ _start_sc.add_argument(
1408
+ "-d", "--detach",
1409
+ action="store_true",
1410
+ help="Run in background (recommended for production use)",
1411
+ )
1412
+ _start_sc.add_argument("--server-url", default=None, help="Server URL to connect to")
1413
+
1414
+ sub.add_parser(
1415
+ "stop",
1416
+ help="Stop local daemon (alias: forgexa daemon stop)",
1417
+ )
1418
+
1419
+ _restart_sc = sub.add_parser(
1420
+ "restart",
1421
+ help="Restart local daemon (alias: forgexa daemon restart)",
1422
+ )
1423
+ _restart_sc.add_argument("--server-url", default=None, help="Server URL to connect to")
1424
+
1425
+ _status_sc = sub.add_parser(
1426
+ "status",
1427
+ help="Daemon status and agents (alias: forgexa daemon status)",
1428
+ )
1429
+ _status_sc.add_argument("--all", action="store_true", help="List all runtimes (platform admin only)")
1430
+ _status_sc.add_argument("-v", "--verbose", action="store_true", help="Expanded per-agent detail")
1431
+
1432
+ _logs_sc = sub.add_parser(
1433
+ "logs",
1434
+ help="Stream daemon logs (alias: forgexa daemon logs)",
1435
+ )
1436
+ _logs_sc.add_argument(
1437
+ "-n", "--lines",
1438
+ type=int, default=50, metavar="N",
1439
+ help="Show last N lines before following (default: 50)",
1440
+ )
1441
+ _logs_sc.add_argument(
1442
+ "-f", "--follow",
1443
+ action="store_true",
1444
+ help="Stream new log lines as they arrive (like tail -f)",
1445
+ )
1446
+
1447
+ _agents_sc = sub.add_parser(
1448
+ "agents",
1449
+ help="List AI agents (alias: forgexa daemon agents)",
1450
+ )
1451
+ _agents_sc.add_argument("--all", action="store_true", help="Include all users' runtimes (admin only)")
1452
+
1390
1453
  # runtimes
1391
1454
  rt_p = sub.add_parser("runtimes", help="Runtime management")
1392
1455
  rt_sub = rt_p.add_subparsers(dest="rt_cmd")
@@ -1489,6 +1552,13 @@ def main() -> None:
1489
1552
  "logs": cmd_daemon_logs,
1490
1553
  "agents": cmd_daemon_agents,
1491
1554
  }.get(a.daemon_cmd, lambda _: daemon_p.print_help())(a),
1555
+ # Daemon shortcut aliases (identical handlers, no `daemon` prefix needed)
1556
+ "start": cmd_daemon_start,
1557
+ "stop": cmd_daemon_stop,
1558
+ "restart": cmd_daemon_restart,
1559
+ "status": cmd_daemon_status,
1560
+ "logs": cmd_daemon_logs,
1561
+ "agents": cmd_daemon_agents,
1492
1562
  "runtimes": lambda a: {
1493
1563
  "list": cmd_runtimes_list,
1494
1564
  }.get(a.rt_cmd, lambda _: rt_p.print_help())(a),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.13.4
3
+ Version: 1.13.5
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.4"
3
+ version = "1.13.5"
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