Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bbf24a9
rpg_edit: Add a shared CoderMind Explain View renderer with `write_com
QingtaoLi1 Jun 30, 2026
416764d
fix(cmind): handle RPG updates from git subdirectory workspaces
QingtaoLi1 Jun 30, 2026
2b443e9
fix(cmind): correct update_rpg report delta reporting
QingtaoLi1 Jul 1, 2026
f6b79a6
Update .gitignore.
QingtaoLi1 Jul 1, 2026
9c2db7d
rpg_edit: At _normalize_artifacts() lines 224-240 and _render_artifact
QingtaoLi1 Jul 1, 2026
fd5a29c
fix(cmind): clear smoke test baseline failures
QingtaoLi1 Jul 1, 2026
da7bc2b
rpg_edit: merge to base branch instead of main branch.
QingtaoLi1 Jul 1, 2026
2acefc3
fix: GitRunner init parameter main_branch.
QingtaoLi1 Jul 1, 2026
294727f
fix: handle list-valued RPG meta paths in graph search
QingtaoLi1 Jul 1, 2026
5e6930b
rpg_edit: Add the structured event contract currently missing from the
QingtaoLi1 Jul 1, 2026
f916fe3
rpg_edit: merge 674a41c3
QingtaoLi1 Jul 1, 2026
46423f9
rpg_edit: Extend _publish_review_report() at scripts/rpg_edit/review.p
QingtaoLi1 Jul 1, 2026
fe67ac1
rpg_edit: merge a8151bb7
QingtaoLi1 Jul 1, 2026
40a51ba
fix(dep_graph): missing property.
QingtaoLi1 Jul 1, 2026
ccf9e62
fix(rpg_edit): clear recovered code errors
QingtaoLi1 Jul 1, 2026
915a04b
rpg_edit: Add RPG_EDIT_APPLY_RESULT_FILE beside RPG_EDIT_PLAN_FILE/RPG
QingtaoLi1 Jul 1, 2026
fe2bd7b
rpg_edit: merge a8151bb7
QingtaoLi1 Jul 1, 2026
f54b1bf
version: update to 0.1.10
QingtaoLi1 Jul 2, 2026
88186db
fix(update_graphs): use actual result status for update_rpg report
HuYaSen Jul 6, 2026
fe30e63
decoder: fix abstractmethod usage — make RepoNode ABC and remove abst…
HuYaSen Jul 6, 2026
4e0c966
rpg_edit: guard rollback-branch to rpg-edit/* and add regression test
HuYaSen Jul 6, 2026
8d43e18
update_graphs: prefer explicit status or infer 'error' when result co…
HuYaSen Jul 6, 2026
bce3c6a
plan: preserve state.message for warnings instead of overwriting reason
HuYaSen Jul 6, 2026
9579945
update_graphs: make _git_delta_files use --relative for subdirectory …
HuYaSen Jul 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CoderMind/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,5 @@ plans/
.mcp.json
.github/agents/
.github/prompts/

.claude/commands/
2 changes: 1 addition & 1 deletion CoderMind/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cmind-cli"
version = "0.1.9"
version = "0.1.10"
description = "CoderMind CLI - A tool to generate feature trees for repository planning and code generation."
requires-python = ">=3.12"
dependencies = [
Expand Down
54 changes: 53 additions & 1 deletion CoderMind/scripts/check_code_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@

import json
import argparse
import sys
from pathlib import Path
from typing import Dict, Any, List, Tuple

SCRIPTS_DIR = Path(__file__).resolve().parent
if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR))

# Import centralized paths and state loader
from common.paths import (
TASKS_FILE,
Expand All @@ -24,6 +29,8 @@
cmd_for,
REPO_DIR,
)
from common.run_events import ArtifactEvent, CommandRun, StepEvent, VerificationEvent
from common.run_report import write_command_report
from common.execution_state import load_code_gen_state
from common.execution_state import load_code_gen_state as _load_state, save_code_gen_state as _save_state
from common.execution_state import complete_batch as _complete_batch
Expand Down Expand Up @@ -427,6 +434,46 @@ def determine_state(
return result


def _write_code_gen_report(result: Dict[str, Any]) -> str | None:
try:
stats = result.get("stats") or {}
report_path = write_command_report(CommandRun(
command="code_gen",
title="CoderMind code_gen Progress View",
status=result.get("type"),
summary=[
{"label": "state", "value": result.get("type", "")},
{"label": "total", "value": stats.get("total_tasks", 0)},
{"label": "completed", "value": stats.get("completed", 0)},
{"label": "failed", "value": stats.get("failed", 0)},
{"label": "remaining", "value": stats.get("remaining", 0)},
{"label": "current batch", "value": (result.get("current_batch") or {}).get("batch_id", "")},
{"label": "next batch", "value": result.get("next_batch", "")},
],
steps=[
StepEvent(name="determine_state", status=result.get("type"), reason=result.get("message", "")),
StepEvent(name="current_batch", status=(result.get("current_batch") or {}).get("phase", "none"), reason=(result.get("current_batch") or {}).get("file_path", "")),
StepEvent(name="next_action", status="available" if result.get("next_action") else "missing", reason=result.get("next_action", "")),
],
artifacts=[
ArtifactEvent(label="tasks", path=TASKS_FILE),
ArtifactEvent(label="code_gen_state", path=STATE_FILE),
],
verification=[VerificationEvent(name="state", status=result.get("type"), detail=result.get("message", ""))],
evidence={
"type": result.get("type"),
"stats": stats,
"current_batch": result.get("current_batch"),
"next_batch": result.get("next_batch"),
"next_action": result.get("next_action"),
},
))
return str(report_path)
except Exception as exc:
result["report_error"] = str(exc)
return None


def print_status(result: Dict[str, Any], json_output: bool = False) -> None:
"""Print the status in human-readable or JSON format."""
if json_output:
Expand Down Expand Up @@ -474,7 +521,9 @@ def print_status(result: Dict[str, Any], json_output: bool = False) -> None:
# Next batch info
if result.get("next_batch"):
print(f"\n Next Batch: {result['next_batch']}")

if result.get("report_path"):
print(f"\n Report: {result['report_path']}")

# Guidance
print("\n " + "─" * 60)

Expand Down Expand Up @@ -516,6 +565,9 @@ def main():
args = parser.parse_args()

result = determine_state(args.tasks, args.state)
report_path = _write_code_gen_report(result)
if report_path:
result["report_path"] = report_path
print_status(result, json_output=args.json)

# Return exit code based on state
Expand Down
205 changes: 165 additions & 40 deletions CoderMind/scripts/common/git_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,12 @@ class GitRunner:
def __init__(
self,
repo_path: str,
main_branch: str = "main",
main_branch: str = MAIN_BRANCH,
logger: Optional[logging.Logger] = None
):
self.repo_path = Path(repo_path)
self.logger = logger or logging.getLogger(__name__)
self.main_branch = self.MAIN_BRANCH
self.main_branch = main_branch

# Ensure repo exists and is a git repo
self._ensure_git_repository()
Expand Down Expand Up @@ -149,7 +149,7 @@ def _ensure_git_repository(self) -> None:
if not git_dir.exists():
self.logger.info("Initializing git repository...")
self.repo_path.mkdir(parents=True, exist_ok=True)
self.run_git(["init", "-b", self.MAIN_BRANCH])
self.run_git(["init", "-b", self.main_branch])

# Configure safe directory
self.run_git([
Expand Down Expand Up @@ -391,21 +391,36 @@ def get_diff(
to_commit: str = "HEAD"
) -> str:
"""Get diff between commits.

Args:
from_commit: Start commit (default: parent of to_commit)
to_commit: End commit (default: HEAD)

Returns:
Diff content as string
"""
if from_commit:
result = self.run_git(["diff", from_commit, to_commit])
else:
result = self.run_git(["diff", f"{to_commit}^", to_commit])

return result.stdout if result.success else ""


def get_file_diffs(
self,
from_commit: Optional[str] = None,
to_commit: str = "HEAD",
files: Optional[List[str]] = None,
) -> List[Dict[str, str]]:
"""Get per-file diff rows between commits."""
return file_diffs_between(
self.repo_path,
from_commit,
to_commit,
files=files,
py_only=False,
)

def get_changed_files(
self,
from_commit: Optional[str] = None,
Expand Down Expand Up @@ -565,6 +580,34 @@ def read_head(repo_dir: str | Path) -> Optional[dict]:
}


def git_workspace_prefix(workspace_dir: str | Path) -> str:
"""Return the path from the git root to ``workspace_dir``.

Returns ``""`` when ``workspace_dir`` is the git root or when git metadata
cannot be read. This keeps callers safe outside git repositories.
"""
if not workspace_dir:
return ""
workspace_path = Path(workspace_dir)
if not workspace_path.is_dir():
return ""

git_root = _run_git_readonly(
["rev-parse", "--show-toplevel"],
workspace_path,
)
if not git_root:
return ""

try:
rel = workspace_path.resolve().relative_to(Path(git_root).resolve())
except ValueError:
return ""

rel_str = rel.as_posix()
return "" if rel_str == "." else rel_str


# ---------------------------------------------------------------------------
# Diff helpers — produce ``(modified, renames)`` from various git scopes.
# ---------------------------------------------------------------------------
Expand All @@ -589,6 +632,52 @@ def read_head(repo_dir: str | Path) -> Optional[dict]:
_GIT_STATUS_COPY_PREFIX = "C"


def _parse_name_status_rows(
raw: Optional[str],
*,
py_only: bool = True,
) -> List[Dict[str, str]]:
rows: List[Dict[str, str]] = []
if not raw:
return rows

def _keep(p: str) -> bool:
return (not py_only) or p.endswith(".py")

for line in raw.splitlines():
parts = line.split("\t")
if len(parts) < 2:
continue
status = parts[0]
if status.startswith(_GIT_STATUS_RENAME_PREFIX) or status.startswith(
_GIT_STATUS_COPY_PREFIX
):
if len(parts) < 3:
continue
old_path, new_path = parts[1], parts[2]
if _keep(new_path) or _keep(old_path):
rows.append({
"file": new_path,
"change_type": "rename" if status.startswith(_GIT_STATUS_RENAME_PREFIX) else "copy",
"old_file": old_path,
"status": status,
})
continue
path = parts[1]
if not _keep(path):
continue
if status == _GIT_STATUS_ADDED:
change_type = "add"
elif status == _GIT_STATUS_DELETED:
change_type = "delete"
elif status == _GIT_STATUS_MODIFIED:
change_type = "modify"
else:
continue
rows.append({"file": path, "change_type": change_type, "status": status})
return rows


def _parse_name_status(
raw: Optional[str],
*,
Expand Down Expand Up @@ -616,40 +705,76 @@ def _parse_name_status(
if not raw:
return modified, renames

def _keep(p: str) -> bool:
return (not py_only) or p.endswith(".py")

for line in raw.splitlines():
parts = line.split("\t")
if len(parts) < 2:
continue
status = parts[0]
if status.startswith(_GIT_STATUS_RENAME_PREFIX) or status.startswith(
_GIT_STATUS_COPY_PREFIX
):
if len(parts) < 3:
for row in _parse_name_status_rows(raw, py_only=py_only):
path = row.get("file", "")
if row.get("change_type") in ("rename", "copy"):
old_path = row.get("old_file", "")
if old_path and path:
renames[old_path] = path
if py_only and not path.endswith(".py"):
continue
old_path, new_path = parts[1], parts[2]
if _keep(new_path) or _keep(old_path):
renames[old_path] = new_path
# update_files() treats the OLD path as a deletion (via
# ``renames``) and the NEW path as something it must
# reparse — so we surface the new path through the
# modified list as well.
if _keep(new_path):
modified.append(new_path)
continue
path = parts[1]
if not _keep(path):
continue
if status in (_GIT_STATUS_ADDED, _GIT_STATUS_DELETED, _GIT_STATUS_MODIFIED):
if path:
modified.append(path)
# Type / unmerged / other status letters → ignore (caller will
# fall back to full sync via the safety threshold if there are
# many of them).
return modified, renames


def _diff_range_args(
from_commit: Optional[str] = None,
to_commit: str = "HEAD",
) -> List[str]:
if from_commit:
return [from_commit, to_commit]
return [f"{to_commit}^", to_commit]


def file_diffs_between(
repo_dir: str | Path,
from_commit: Optional[str] = None,
to_commit: str = "HEAD",
*,
files: Optional[List[str]] = None,
py_only: bool = False,
) -> List[Dict[str, str]]:
"""Return per-file diff rows without mutating git state."""
if not repo_dir:
return []
repo_path = Path(repo_dir)
if not repo_path.is_dir():
return []

range_args = _diff_range_args(from_commit, to_commit)
raw_status = _run_git_readonly(
["diff", "--relative", "--name-status", "-M", *range_args],
repo_path,
)
rows = _parse_name_status_rows(raw_status, py_only=py_only)
if not rows:
return []

selected = {str(path) for path in files or [] if path}
diff_rows: List[Dict[str, str]] = []
for row in rows:
file_path = row.get("file", "")
old_file = row.get("old_file", "")
if selected and file_path not in selected and old_file not in selected:
continue
pathspecs = [path for path in [old_file, file_path] if path]
raw_diff = _run_git_readonly(
["diff", *range_args, "--", *pathspecs],
repo_path,
timeout=10.0,
)
diff_row = {
"file": file_path,
"change_type": row.get("change_type", "modify"),
"diff": raw_diff or "",
}
if old_file:
diff_row["old_file"] = old_file
diff_rows.append(diff_row)
return diff_rows


def staged_changes(
repo_dir: str | Path,
) -> Tuple[List[str], Dict[str, str]]:
Expand All @@ -669,14 +794,14 @@ def staged_changes(
if not repo_path.is_dir():
return [], {}
raw = _run_git_readonly(
["diff", "--cached", "--name-status", "-M", "HEAD"],
["diff", "--cached", "--relative", "--name-status", "-M", "HEAD"],
repo_path,
)
if raw is None:
# ``HEAD`` may not exist yet (unborn branch); try without it so
# the very first commit's staged files still get picked up.
raw = _run_git_readonly(
["diff", "--cached", "--name-status", "-M"],
["diff", "--cached", "--relative", "--name-status", "-M"],
repo_path,
)
return _parse_name_status(raw)
Expand Down Expand Up @@ -706,7 +831,7 @@ def working_tree_changes(
return [], {}

raw = _run_git_readonly(
["diff", "--name-status", "-M", "HEAD"],
["diff", "--relative", "--name-status", "-M", "HEAD"],
repo_path,
)
modified, renames = _parse_name_status(raw)
Expand Down Expand Up @@ -749,7 +874,7 @@ def changed_files_between(
if not repo_path.is_dir():
return [], {}
raw = _run_git_readonly(
["diff", "--name-status", "-M", f"{old_ref}..{new_ref}"],
["diff", "--relative", "--name-status", "-M", f"{old_ref}..{new_ref}"],
repo_path,
)
return _parse_name_status(raw)
Expand Down
Loading
Loading