forgexa-cli 1.10.1__tar.gz → 1.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.10.1
3
+ Version: 1.10.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: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.10.1"
2
+ __version__ = "1.10.3"
@@ -396,7 +396,7 @@ except (ImportError, ModuleNotFoundError):
396
396
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
397
397
  # Kept in sync with pyproject.toml version via bump-version.sh.
398
398
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
399
- DAEMON_VERSION = "1.10.1"
399
+ DAEMON_VERSION = "1.10.3"
400
400
 
401
401
 
402
402
  def _detect_client_type() -> str:
@@ -648,6 +648,31 @@ class TaskResult:
648
648
  git: dict = field(default_factory=dict)
649
649
 
650
650
 
651
+ def _resolve_git_author(project: dict) -> tuple[str, str]:
652
+ """Resolve git commit author (name, email) using a fallback chain.
653
+
654
+ Priority:
655
+ 1. Task triggering user — populated by the API server from ``graph.triggered_by``
656
+ (the human who submitted/re-triggered the workflow).
657
+ 2. Runtime owner user — populated by the API server from ``runtime.owner``
658
+ (the user who registered this daemon).
659
+ 3. Hardcoded fallback — ``"Forgexa Agent" / "agent@forgexa.net"``
660
+ (backwards-compatible default when neither is available).
661
+
662
+ Using real user identities makes commits auditable: customers can trace
663
+ each automated commit back to the person who initiated the workflow.
664
+ """
665
+ name = (project.get("triggered_by_git_name") or "").strip()
666
+ email = (project.get("triggered_by_git_email") or "").strip()
667
+ if name and email:
668
+ return name, email
669
+ name = (project.get("runtime_owner_git_name") or "").strip()
670
+ email = (project.get("runtime_owner_git_email") or "").strip()
671
+ if name and email:
672
+ return name, email
673
+ return "Forgexa Agent", "agent@forgexa.net"
674
+
675
+
651
676
  # ── Type-aware analysis outputs (inline fallback for standalone daemons) ──
652
677
  # Mirrors type_workflow_profiles.py — used when import is unavailable (CLI/Desktop).
653
678
  _ANALYSIS_OUTPUTS_BY_TYPE: dict[str, list[str]] = {
@@ -1206,9 +1231,10 @@ class WorkspaceManager:
1206
1231
  readme = ws_path / "README.md"
1207
1232
  readme.write_text(f"# {project_key}\n\nInitialized by Forgexa.\n")
1208
1233
  await self._git("add", ".", cwd=ws_path)
1234
+ _git_name, _git_email = _resolve_git_author(project)
1209
1235
  await self._git(
1210
- "-c", "user.name=Forgexa Agent",
1211
- "-c", "user.email=agent@forgexa.net",
1236
+ "-c", f"user.name={_git_name}",
1237
+ "-c", f"user.email={_git_email}",
1212
1238
  "commit", "-m", "Initial commit",
1213
1239
  cwd=ws_path,
1214
1240
  )
@@ -4569,9 +4595,10 @@ class RuntimeDaemon:
4569
4595
  # Remove physical files
4570
4596
  shutil.rmtree(str(dir_to_wipe), ignore_errors=True)
4571
4597
  # Commit the wipe so the branch diff is clean
4598
+ _git_name, _git_email = _resolve_git_author(task.project)
4572
4599
  await self._git(
4573
- "-c", "user.name=Forgexa Agent",
4574
- "-c", "user.email=agent@forgexa.net",
4600
+ "-c", f"user.name={_git_name}",
4601
+ "-c", f"user.email={_git_email}",
4575
4602
  "commit", "-m",
4576
4603
  f"cleanup: wipe analysis docs in {output_dir_norm} before fresh re-analysis",
4577
4604
  cwd=workspace_path,
@@ -5106,13 +5133,23 @@ class RuntimeDaemon:
5106
5133
 
5107
5134
  # Analysis deliverables live in analysis_output_dir (docs/requirements/...)
5108
5135
  _input = task.input_data or {}
5109
- doc_dir = (
5110
- _input.get("analysis_output_dir")
5111
- or _input.get("context", {}).get("analysis_output_dir")
5112
- or _input.get("output_dir")
5113
- or _input.get("context", {}).get("output_dir")
5114
- or ""
5115
- )
5136
+ _top_dir = (_input.get("analysis_output_dir") or "").replace("\\", "/").rstrip("/")
5137
+ _ctx_dir = ((_input.get("context") or {}).get("analysis_output_dir") or "").replace("\\", "/").rstrip("/")
5138
+ # Prefer the deeper (v2) path — context always has the authoritative
5139
+ # req_analysis_dir() value while the top-level field may contain a
5140
+ # stale v1 path (e.g. "docs/requirements/KEY") from nodes created
5141
+ # before the Phase 2 doc-paths migration (2026-06-10).
5142
+ if _ctx_dir and len(_ctx_dir.split("/")) > len(_top_dir.split("/")):
5143
+ doc_dir = _ctx_dir
5144
+ elif _top_dir:
5145
+ doc_dir = _top_dir
5146
+ else:
5147
+ doc_dir = (
5148
+ _ctx_dir
5149
+ or _input.get("output_dir")
5150
+ or (_input.get("context") or {}).get("output_dir")
5151
+ or ""
5152
+ )
5116
5153
  if doc_dir:
5117
5154
  base = workspace_path / doc_dir
5118
5155
  else:
@@ -5561,11 +5598,22 @@ class RuntimeDaemon:
5561
5598
 
5562
5599
  # Build a targeted fix prompt: original task + validation issues.
5563
5600
  _input = task.input_data or {}
5564
- _fix_doc_dir = (
5565
- _input.get("output_dir")
5566
- or _input.get("context", {}).get("output_dir")
5567
- or ""
5568
- )
5601
+ # For analysis nodes prefer analysis_output_dir (may be v2 while output_dir
5602
+ # is still a stale v1 path in pre-migration nodes).
5603
+ if task.node_type == "analysis":
5604
+ _fix_doc_dir = (
5605
+ _input.get("analysis_output_dir")
5606
+ or (_input.get("context") or {}).get("analysis_output_dir")
5607
+ or _input.get("output_dir")
5608
+ or (_input.get("context") or {}).get("output_dir")
5609
+ or ""
5610
+ )
5611
+ else:
5612
+ _fix_doc_dir = (
5613
+ _input.get("output_dir")
5614
+ or (_input.get("context") or {}).get("output_dir")
5615
+ or ""
5616
+ )
5569
5617
  fix_prompt = (
5570
5618
  f"{original_prompt}\n\n"
5571
5619
  "---\n\n"
@@ -5787,13 +5835,15 @@ class RuntimeDaemon:
5787
5835
  except Exception as msg_err:
5788
5836
  logger.warning("Failed to build rich commit message: %s — using fallback", msg_err)
5789
5837
  commit_msg = f"{task.node_type}({task.requirement_key or task.task_id}): {display_title}"
5838
+ _git_name, _git_email = _resolve_git_author(task.project)
5790
5839
  proc = await asyncio.create_subprocess_exec(
5791
5840
  "git", "commit", "-m", commit_msg,
5792
5841
  cwd=str(workspace_path),
5793
5842
  stdout=asyncio.subprocess.PIPE,
5794
5843
  stderr=asyncio.subprocess.PIPE,
5795
- env={**os.environ, "GIT_AUTHOR_NAME": "Forgexa Agent", "GIT_AUTHOR_EMAIL": "agent@forgexa.net",
5796
- "GIT_COMMITTER_NAME": "Forgexa Agent", "GIT_COMMITTER_EMAIL": "agent@forgexa.net"},
5844
+ env={**os.environ,
5845
+ "GIT_AUTHOR_NAME": _git_name, "GIT_AUTHOR_EMAIL": _git_email,
5846
+ "GIT_COMMITTER_NAME": _git_name, "GIT_COMMITTER_EMAIL": _git_email},
5797
5847
  )
5798
5848
  await proc.communicate()
5799
5849
  has_new_commit = True
@@ -5833,7 +5883,7 @@ class RuntimeDaemon:
5833
5883
  return {"push_error": f"Would push to default branch {current_branch}"}
5834
5884
 
5835
5885
  # ── Push ──
5836
- push_error = await self._push_branch(workspace_path, project_key)
5886
+ push_error = await self._push_branch(workspace_path, project_key, task=task)
5837
5887
  if push_error:
5838
5888
  return {"push_error": push_error}
5839
5889
  return {}
@@ -6127,6 +6177,7 @@ class RuntimeDaemon:
6127
6177
  """
6128
6178
  git = self.workspace_manager._git
6129
6179
  target = f"origin/{default_branch}"
6180
+ _git_name, _git_email = _resolve_git_author(task.project)
6130
6181
 
6131
6182
  # Fetch latest remote state
6132
6183
  try:
@@ -6174,8 +6225,8 @@ class RuntimeDaemon:
6174
6225
  # ── Tier 1: rebase (safe — branch not yet on remote) ──
6175
6226
  try:
6176
6227
  await git(
6177
- "-c", "user.name=Forgexa Agent",
6178
- "-c", "user.email=agent@forgexa.net",
6228
+ "-c", f"user.name={_git_name}",
6229
+ "-c", f"user.email={_git_email}",
6179
6230
  "rebase", target,
6180
6231
  cwd=workspace_path, timeout=120,
6181
6232
  )
@@ -6197,8 +6248,8 @@ class RuntimeDaemon:
6197
6248
  # ── Tier 2: merge ──
6198
6249
  try:
6199
6250
  await git(
6200
- "-c", "user.name=Forgexa Agent",
6201
- "-c", "user.email=agent@forgexa.net",
6251
+ "-c", f"user.name={_git_name}",
6252
+ "-c", f"user.email={_git_email}",
6202
6253
  "merge", target, "--no-edit",
6203
6254
  cwd=workspace_path, timeout=120,
6204
6255
  )
@@ -6221,8 +6272,7 @@ class RuntimeDaemon:
6221
6272
  in a clean state and push whatever we had before.
6222
6273
  """
6223
6274
  git = self.workspace_manager._git
6224
-
6225
- # 1. List conflicted files
6275
+ _git_name, _git_email = _resolve_git_author(task.project)
6226
6276
  try:
6227
6277
  diff_out = await git(
6228
6278
  "diff", "--name-only", "--diff-filter=U", cwd=workspace_path,
@@ -6235,8 +6285,8 @@ class RuntimeDaemon:
6235
6285
  # No actual conflicts (merge completed or something unusual)
6236
6286
  try:
6237
6287
  await git(
6238
- "-c", "user.name=Forgexa Agent",
6239
- "-c", "user.email=agent@forgexa.net",
6288
+ "-c", f"user.name={_git_name}",
6289
+ "-c", f"user.email={_git_email}",
6240
6290
  "merge", "--continue", cwd=workspace_path,
6241
6291
  )
6242
6292
  except RuntimeError:
@@ -6361,8 +6411,8 @@ class RuntimeDaemon:
6361
6411
  await git("add", "-A", cwd=workspace_path)
6362
6412
  try:
6363
6413
  await git(
6364
- "-c", "user.name=Forgexa Agent",
6365
- "-c", "user.email=agent@forgexa.net",
6414
+ "-c", f"user.name={_git_name}",
6415
+ "-c", f"user.email={_git_email}",
6366
6416
  "commit", "--no-edit",
6367
6417
  cwd=workspace_path,
6368
6418
  )
@@ -6378,9 +6428,13 @@ class RuntimeDaemon:
6378
6428
  except RuntimeError:
6379
6429
  await git("reset", "--hard", "HEAD", cwd=workspace_path)
6380
6430
 
6381
- async def _push_branch(self, workspace_path: Path, project_key: str = "default") -> str | None:
6431
+ async def _push_branch(
6432
+ self, workspace_path: Path, project_key: str = "default",
6433
+ task: "TaskInfo | None" = None,
6434
+ ) -> str | None:
6382
6435
  """Push the current branch to origin. Returns error message on failure, None on success."""
6383
6436
  git = self.workspace_manager._git
6437
+ _git_name, _git_email = _resolve_git_author(task.project if task else {})
6384
6438
  try:
6385
6439
  # Get current branch name
6386
6440
  branch = (await git("rev-parse", "--abbrev-ref", "HEAD", cwd=workspace_path)).strip()
@@ -6517,8 +6571,8 @@ class RuntimeDaemon:
6517
6571
  cwd=workspace_path,
6518
6572
  )
6519
6573
  await git(
6520
- "-c", "user.name=Forgexa Agent",
6521
- "-c", "user.email=agent@forgexa.net",
6574
+ "-c", f"user.name={_git_name}",
6575
+ "-c", f"user.email={_git_email}",
6522
6576
  "cherry-pick", *new_shas,
6523
6577
  cwd=workspace_path,
6524
6578
  )
@@ -6566,8 +6620,8 @@ class RuntimeDaemon:
6566
6620
  # version. This is correct: on an agent-managed feature
6567
6621
  # branch the latest agent output is always authoritative.
6568
6622
  await git(
6569
- "-c", "user.name=Forgexa Agent",
6570
- "-c", "user.email=agent@forgexa.net",
6623
+ "-c", f"user.name={_git_name}",
6624
+ "-c", f"user.email={_git_email}",
6571
6625
  "rebase", "-X", "theirs", f"origin/{branch}",
6572
6626
  cwd=workspace_path,
6573
6627
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.10.1
3
+ Version: 1.10.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: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.10.1"
3
+ version = "1.10.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 = { text = "MIT" }
File without changes
File without changes