forgexa-cli 1.10.6__tar.gz → 1.10.8__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.6
3
+ Version: 1.10.8
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.6"
2
+ __version__ = "1.10.8"
@@ -404,7 +404,7 @@ except (ImportError, ModuleNotFoundError):
404
404
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
405
405
  # Kept in sync with pyproject.toml version via bump-version.sh.
406
406
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
407
- DAEMON_VERSION = "1.10.6"
407
+ DAEMON_VERSION = "1.10.8"
408
408
 
409
409
 
410
410
  def _detect_client_type() -> str:
@@ -2248,6 +2248,17 @@ class ProcessManager:
2248
2248
  )
2249
2249
  if not has_token_usage and ProcessManager._should_scan_short_success_stdout(stdout, signals):
2250
2250
  error_channels = "\n".join(filter(None, [error_channels, stdout.strip()]))
2251
+ # Always include structured error messages parsed from JSONL stream events
2252
+ # (result events with is_error=True, error events, turn.failed payloads).
2253
+ # These are safe to scan unconditionally because they are structured — NOT
2254
+ # free-form agent work text. This catches mid-session model overload errors
2255
+ # (e.g. 529 overloaded_error) that appear as structured events AFTER some
2256
+ # assistant turns have already been emitted. Without this, such errors are
2257
+ # invisible to the stdout-scan guard (has_assistant_events=True → no scan)
2258
+ # yet still signal that the underlying model call failed.
2259
+ structured_errors = signals.get("error_messages", [])
2260
+ if structured_errors:
2261
+ error_channels = "\n".join(filter(None, [error_channels, "\n".join(structured_errors)]))
2251
2262
  return error_channels
2252
2263
 
2253
2264
  @staticmethod
@@ -5793,6 +5804,106 @@ class RuntimeDaemon:
5793
5804
  except Exception as e:
5794
5805
  logger.warning("Failed to read design artifact: %s", e)
5795
5806
 
5807
+ async def _remove_root_scratch_files(
5808
+ self, workspace_path: Path, task: TaskInfo, git_status_output: str
5809
+ ) -> None:
5810
+ """Remove unauthorized scratch/intermediate files created in the repo root.
5811
+
5812
+ Agents sometimes create working notes (findings.md, progress.md,
5813
+ task_plan.md, notes.md, etc.) directly in the repository root instead of
5814
+ inside the designated docs/requirements/<key>/ output directory. These
5815
+ files:
5816
+ 1. Pollute the root directory with unstructured content.
5817
+ 2. May survive into subsequent agent runs and cause cross-requirement
5818
+ context contamination.
5819
+ 3. Can cause confusion when multiple requirements share a worktree.
5820
+
5821
+ This method removes any NEW (untracked or staged-as-add) markdown file
5822
+ found at the repository root that is not a recognised project-level doc,
5823
+ and logs a warning so the prompt can be improved.
5824
+
5825
+ NOTE: This only catches files that the agent has NOT yet committed.
5826
+ Files the agent self-committed (e.g. via `git add -A && git commit`)
5827
+ are prevented by the prompt-level prohibitions in CRITICAL REQUIREMENT
5828
+ and the type-specific IMPORTANT constraints.
5829
+ """
5830
+ # Well-known root-level markdown files that must never be removed
5831
+ _PROTECTED = frozenset({
5832
+ "README.md", "readme.md",
5833
+ "CONTRIBUTING.md", "contributing.md",
5834
+ "CHANGELOG.md", "changelog.md",
5835
+ "LICENSE.md", "license.md",
5836
+ "SECURITY.md", "security.md",
5837
+ "CODE_OF_CONDUCT.md",
5838
+ "CONVENTIONS.md",
5839
+ "WORKFLOW.md",
5840
+ })
5841
+
5842
+ removed: list[str] = []
5843
+ for raw_line in git_status_output.splitlines():
5844
+ if len(raw_line) < 4:
5845
+ continue
5846
+ index_status = raw_line[0]
5847
+ worktree_status = raw_line[1]
5848
+ file_path = raw_line[3:].strip().strip('"')
5849
+
5850
+ # Only new files: untracked (??) or staged as addition (A)
5851
+ is_new_untracked = (index_status == "?" and worktree_status == "?")
5852
+ is_staged_new = (index_status == "A")
5853
+ if not (is_new_untracked or is_staged_new):
5854
+ continue
5855
+
5856
+ # Only root-level files (no path separator)
5857
+ if "/" in file_path or "\\" in file_path:
5858
+ continue
5859
+
5860
+ # Only markdown files
5861
+ if not file_path.lower().endswith(".md"):
5862
+ continue
5863
+
5864
+ # Skip protected project-level docs
5865
+ if file_path in _PROTECTED:
5866
+ continue
5867
+
5868
+ # Unstage if it was already staged
5869
+ if is_staged_new:
5870
+ try:
5871
+ proc = await asyncio.create_subprocess_exec(
5872
+ "git", "restore", "--staged", file_path,
5873
+ cwd=str(workspace_path),
5874
+ stdout=asyncio.subprocess.PIPE,
5875
+ stderr=asyncio.subprocess.PIPE,
5876
+ )
5877
+ await proc.communicate()
5878
+ except Exception as exc:
5879
+ logger.warning(
5880
+ "Failed to unstage root scratch file '%s': %s", file_path, exc
5881
+ )
5882
+
5883
+ # Delete the file
5884
+ target = workspace_path / file_path
5885
+ if target.exists():
5886
+ try:
5887
+ target.unlink()
5888
+ removed.append(file_path)
5889
+ except Exception as exc:
5890
+ logger.warning(
5891
+ "Failed to remove root scratch file '%s': %s", file_path, exc
5892
+ )
5893
+
5894
+ if removed:
5895
+ logger.warning(
5896
+ "Task %s (req: %s, node_type: %s): removed %d unauthorized root-level "
5897
+ "markdown file(s) before commit: [%s]. "
5898
+ "All documentation must be placed inside docs/requirements/<key>/. "
5899
+ "Check agent prompt for STRICT FILE PLACEMENT RULES compliance.",
5900
+ task.task_id,
5901
+ task.requirement_key or "?",
5902
+ task.node_type,
5903
+ len(removed),
5904
+ ", ".join(removed),
5905
+ )
5906
+
5796
5907
  async def _auto_commit(self, workspace_path: Path, task: TaskInfo) -> dict:
5797
5908
  """Auto-commit and push agent changes.
5798
5909
 
@@ -5824,6 +5935,14 @@ class RuntimeDaemon:
5824
5935
  stdout, _ = await proc.communicate()
5825
5936
 
5826
5937
  if stdout.strip():
5938
+ # Safety net: remove any scratch/intermediate .md files the agent
5939
+ # created directly in the repository root before staging everything.
5940
+ # Agents sometimes write working notes (findings.md, progress.md,
5941
+ # task_plan.md, etc.) to cwd instead of the designated output dir.
5942
+ # These files pollute the root and cause cross-requirement context
5943
+ # contamination on subsequent agent runs.
5944
+ await self._remove_root_scratch_files(workspace_path, task, stdout.decode())
5945
+
5827
5946
  # Stage all changes
5828
5947
  proc = await asyncio.create_subprocess_exec(
5829
5948
  "git", "add", "-A",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.10.6
3
+ Version: 1.10.8
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.6"
3
+ version = "1.10.8"
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