OK.
This commit is contained in:
@@ -15,10 +15,7 @@ Fixed 329/330 tests passing for asyncio_decoupling_refactor_20260306 track.
|
|||||||
- Issue in the _pending_gui_tasks queue - tracks aren't being processed
|
- Issue in the _pending_gui_tasks queue - tracks aren't being processed
|
||||||
|
|
||||||
## CRITICAL MCP TOOL LESSON
|
## CRITICAL MCP TOOL LESSON
|
||||||
When using manual-slop_edit_file, parameters are CAMEL CASE:
|
When using manual-slop_edit_file, parameters are lower_snake_case:
|
||||||
- oldString (NOT old_string)
|
|
||||||
- newString (NOT new_string)
|
|
||||||
- replaceAll (NOT replace_all)
|
|
||||||
|
|
||||||
The tool schema shows camelCase. Never assume snake_case. Always verify params from schema.
|
The tool schema shows camelCase. Never assume snake_case. Always verify params from schema.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# mcp_client.py
|
# mcp_client.py
|
||||||
"""
|
"""
|
||||||
Note(Gemini):
|
Note(Gemini):
|
||||||
MCP-style file context tools for manual_slop.
|
MCP-style file context tools for manual_slop.
|
||||||
@@ -322,7 +322,7 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
|||||||
new_content += "\n"
|
new_content += "\n"
|
||||||
new_lines = new_content.splitlines(keepends=True) if new_content else []
|
new_lines = new_content.splitlines(keepends=True) if new_content else []
|
||||||
lines[start_idx:end_idx] = new_lines
|
lines[start_idx:end_idx] = new_lines
|
||||||
p.write_text("".join(lines), encoding="utf-8", newline="")
|
p.write_text("".join(lines), encoding="utf-8")
|
||||||
return f"Successfully updated lines {start_line}-{end_line} in {path}"
|
return f"Successfully updated lines {start_line}-{end_line} in {path}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"ERROR updating slice in '{path}': {e}"
|
return f"ERROR updating slice in '{path}': {e}"
|
||||||
@@ -341,7 +341,7 @@ def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = F
|
|||||||
if not old_string:
|
if not old_string:
|
||||||
return "ERROR: old_string cannot be empty"
|
return "ERROR: old_string cannot be empty"
|
||||||
try:
|
try:
|
||||||
content = p.read_text(encoding="utf-8", newline="")
|
content = p.read_text(encoding="utf-8")
|
||||||
if old_string not in content:
|
if old_string not in content:
|
||||||
return f"ERROR: old_string not found in '{path}'"
|
return f"ERROR: old_string not found in '{path}'"
|
||||||
count = content.count(old_string)
|
count = content.count(old_string)
|
||||||
@@ -349,11 +349,11 @@ def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = F
|
|||||||
return f"ERROR: Found {count} matches for old_string in '{path}'. Use replace_all=true or provide more context to make it unique."
|
return f"ERROR: Found {count} matches for old_string in '{path}'. Use replace_all=true or provide more context to make it unique."
|
||||||
if replace_all:
|
if replace_all:
|
||||||
new_content = content.replace(old_string, new_string)
|
new_content = content.replace(old_string, new_string)
|
||||||
p.write_text(new_content, encoding="utf-8", newline="")
|
p.write_text(new_content, encoding="utf-8")
|
||||||
return f"Successfully replaced {count} occurrences in '{path}'"
|
return f"Successfully replaced {count} occurrences in '{path}'"
|
||||||
else:
|
else:
|
||||||
new_content = content.replace(old_string, new_string, 1)
|
new_content = content.replace(old_string, new_string, 1)
|
||||||
p.write_text(new_content, encoding="utf-8", newline="")
|
p.write_text(new_content, encoding="utf-8")
|
||||||
return f"Successfully replaced 1 occurrence in '{path}'"
|
return f"Successfully replaced 1 occurrence in '{path}'"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"ERROR editing '{path}': {e}"
|
return f"ERROR editing '{path}': {e}"
|
||||||
@@ -734,10 +734,10 @@ def get_tree(path: str, max_depth: int = 2) -> str:
|
|||||||
entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")]
|
entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")]
|
||||||
for i, entry in enumerate(entries):
|
for i, entry in enumerate(entries):
|
||||||
is_last = (i == len(entries) - 1)
|
is_last = (i == len(entries) - 1)
|
||||||
connector = "└── " if is_last else "├── "
|
connector = "└── " if is_last else "├── "
|
||||||
lines.append(f"{prefix}{connector}{entry.name}")
|
lines.append(f"{prefix}{connector}{entry.name}")
|
||||||
if entry.is_dir():
|
if entry.is_dir():
|
||||||
extension = " " if is_last else "│ "
|
extension = " " if is_last else "│ "
|
||||||
lines.extend(_build_tree(entry, current_depth + 1, prefix + extension))
|
lines.extend(_build_tree(entry, current_depth + 1, prefix + extension))
|
||||||
return lines
|
return lines
|
||||||
tree_lines = [f"{p.name}/"] + _build_tree(p, 1)
|
tree_lines = [f"{p.name}/"] + _build_tree(p, 1)
|
||||||
@@ -1416,3 +1416,5 @@ MCP_TOOL_SPECS: list[dict[str, Any]] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ def save_config(config: dict[str, Any]) -> None:
|
|||||||
with open(CONFIG_PATH, "wb") as f:
|
with open(CONFIG_PATH, "wb") as f:
|
||||||
tomli_w.dump(config, f)
|
tomli_w.dump(config, f)
|
||||||
|
|
||||||
# Global constants for agent tools
|
|
||||||
AGENT_TOOL_NAMES = [
|
AGENT_TOOL_NAMES = [
|
||||||
"run_powershell",
|
"run_powershell",
|
||||||
"read_file",
|
"read_file",
|
||||||
@@ -40,7 +39,6 @@ AGENT_TOOL_NAMES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
|
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
|
||||||
"""Parse stored history strings back to disc entry dicts."""
|
|
||||||
import re
|
import re
|
||||||
entries = []
|
entries = []
|
||||||
for raw in history_strings:
|
for raw in history_strings:
|
||||||
@@ -64,10 +62,6 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Ticket:
|
class Ticket:
|
||||||
"""
|
|
||||||
Represents a discrete unit of work within a track.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
description: str
|
description: str
|
||||||
status: str = "todo"
|
status: str = "todo"
|
||||||
@@ -81,16 +75,13 @@ class Ticket:
|
|||||||
retry_count: int = 0
|
retry_count: int = 0
|
||||||
|
|
||||||
def mark_blocked(self, reason: str) -> None:
|
def mark_blocked(self, reason: str) -> None:
|
||||||
"""Sets the ticket status to 'blocked' and records the reason."""
|
|
||||||
self.status = "blocked"
|
self.status = "blocked"
|
||||||
self.blocked_reason = reason
|
self.blocked_reason = reason
|
||||||
|
|
||||||
def mark_complete(self) -> None:
|
def mark_complete(self) -> None:
|
||||||
"""Sets the ticket status to 'completed'."""
|
|
||||||
self.status = "completed"
|
self.status = "completed"
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None) -> Any:
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
"""Helper to provide dictionary-like access to dataclass fields."""
|
|
||||||
return getattr(self, key, default)
|
return getattr(self, key, default)
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
@@ -127,16 +118,11 @@ class Ticket:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Track:
|
class Track:
|
||||||
"""
|
|
||||||
Represents a collection of tickets that together form an architectural track or epic.
|
|
||||||
"""
|
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
description: str
|
description: str
|
||||||
tickets: List[Ticket] = field(default_factory=list)
|
tickets: List[Ticket] = field(default_factory=list)
|
||||||
|
|
||||||
def get_executable_tickets(self) -> List[Ticket]:
|
def get_executable_tickets(self) -> List[Ticket]:
|
||||||
"""Returns tickets that are ready to be executed (dependencies met)."""
|
|
||||||
from src.dag_engine import TrackDAG
|
from src.dag_engine import TrackDAG
|
||||||
dag = TrackDAG(self.tickets)
|
dag = TrackDAG(self.tickets)
|
||||||
return dag.get_ready_tasks()
|
return dag.get_ready_tasks()
|
||||||
@@ -159,10 +145,6 @@ class Track:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WorkerContext:
|
class WorkerContext:
|
||||||
"""
|
|
||||||
State preserved for a specific worker throughout its ticket lifecycle.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ticket_id: str
|
ticket_id: str
|
||||||
model_name: str
|
model_name: str
|
||||||
messages: List[Dict[str, Any]] = field(default_factory=list)
|
messages: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
@@ -172,17 +154,17 @@ class WorkerContext:
|
|||||||
class Metadata:
|
class Metadata:
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
status: str
|
status: Optional[str] = None
|
||||||
created_at: Union[str, Any]
|
created_at: Optional[datetime.datetime] = None
|
||||||
updated_at: Union[str, Any]
|
updated_at: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"status": self.status,
|
"status": self.status,
|
||||||
"created_at": str(self.created_at),
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||||
"updated_at": str(self.updated_at),
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -193,16 +175,16 @@ class Metadata:
|
|||||||
try:
|
try:
|
||||||
created = datetime.datetime.fromisoformat(created)
|
created = datetime.datetime.fromisoformat(created)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
created = None
|
||||||
if isinstance(updated, str):
|
if isinstance(updated, str):
|
||||||
try:
|
try:
|
||||||
updated = datetime.datetime.fromisoformat(updated)
|
updated = datetime.datetime.fromisoformat(updated)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
updated = None
|
||||||
return cls(
|
return cls(
|
||||||
id=data["id"],
|
id=data["id"],
|
||||||
name=data.get("name", ""),
|
name=data.get("name", ""),
|
||||||
status=data.get("status", "todo"),
|
status=data.get("status"),
|
||||||
created_at=created,
|
created_at=created,
|
||||||
updated_at=updated,
|
updated_at=updated,
|
||||||
)
|
)
|
||||||
@@ -215,16 +197,39 @@ class TrackState:
|
|||||||
tasks: List[Ticket] = field(default_factory=list)
|
tasks: List[Ticket] = field(default_factory=list)
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
serialized_discussion = []
|
||||||
|
for item in self.discussion:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
new_item = dict(item)
|
||||||
|
if "ts" in new_item and isinstance(new_item["ts"], datetime.datetime):
|
||||||
|
new_item["ts"] = new_item["ts"].isoformat()
|
||||||
|
serialized_discussion.append(new_item)
|
||||||
|
else:
|
||||||
|
serialized_discussion.append(item)
|
||||||
return {
|
return {
|
||||||
"metadata": self.metadata.to_dict(),
|
"metadata": self.metadata.to_dict(),
|
||||||
"discussion": self.discussion,
|
"discussion": serialized_discussion,
|
||||||
"tasks": [t.to_dict() for t in self.tasks],
|
"tasks": [t.to_dict() for t in self.tasks],
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> "TrackState":
|
def from_dict(cls, data: Dict[str, Any]) -> "TrackState":
|
||||||
|
discussion = data.get("discussion", [])
|
||||||
|
parsed_discussion = []
|
||||||
|
for item in discussion:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
new_item = dict(item)
|
||||||
|
ts = new_item.get("ts")
|
||||||
|
if isinstance(ts, str):
|
||||||
|
try:
|
||||||
|
new_item["ts"] = datetime.datetime.fromisoformat(ts)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
parsed_discussion.append(new_item)
|
||||||
|
else:
|
||||||
|
parsed_discussion.append(item)
|
||||||
return cls(
|
return cls(
|
||||||
metadata=Metadata.from_dict(data["metadata"]),
|
metadata=Metadata.from_dict(data["metadata"]),
|
||||||
discussion=data.get("discussion", []),
|
discussion=parsed_discussion,
|
||||||
tasks=[Ticket.from_dict(t) for t in data.get("tasks", [])],
|
tasks=[Ticket.from_dict(t) for t in data.get("tasks", [])],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ def _make_app(**kwargs):
|
|||||||
app.ui_new_ticket_desc = ""
|
app.ui_new_ticket_desc = ""
|
||||||
app.ui_new_ticket_target = ""
|
app.ui_new_ticket_target = ""
|
||||||
app.ui_new_ticket_deps = ""
|
app.ui_new_ticket_deps = ""
|
||||||
|
app.ui_new_ticket_deps = ""
|
||||||
|
app.ui_selected_ticket_id = ""
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def _make_imgui_mock():
|
def _make_imgui_mock():
|
||||||
m = MagicMock()
|
m = MagicMock()
|
||||||
m.begin_table.return_value = False
|
m.begin_table.return_value = False
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from src.gui_2 import App
|
|||||||
def app_instance() -> Any:
|
def app_instance() -> Any:
|
||||||
with (
|
with (
|
||||||
patch("src.models.load_config", return_value={"ai": {}, "projects": {}}),
|
patch("src.models.load_config", return_value={"ai": {}, "projects": {}}),
|
||||||
patch("src.gui_2.save_config"),
|
patch("src.models.save_config"),
|
||||||
patch("src.gui_2.project_manager"),
|
patch("src.gui_2.project_manager"),
|
||||||
patch("src.app_controller.project_manager") as mock_pm,
|
patch("src.app_controller.project_manager") as mock_pm,
|
||||||
patch("src.gui_2.session_logger"),
|
patch("src.gui_2.session_logger"),
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ def test_per_tier_model_persistence():
|
|||||||
patch("src.gui_2.project_manager.load_project", return_value={}),
|
patch("src.gui_2.project_manager.load_project", return_value={}),
|
||||||
patch("src.gui_2.project_manager.migrate_from_legacy_config", return_value={}),
|
patch("src.gui_2.project_manager.migrate_from_legacy_config", return_value={}),
|
||||||
patch("src.gui_2.project_manager.save_project"),
|
patch("src.gui_2.project_manager.save_project"),
|
||||||
patch("src.gui_2.save_config"),
|
patch("src.models.save_config"),
|
||||||
patch("src.gui_2.theme.load_from_config"),
|
patch("src.gui_2.theme.load_from_config"),
|
||||||
patch("src.gui_2.ai_client.set_provider"),
|
patch("src.gui_2.ai_client.set_provider"),
|
||||||
patch("src.gui_2.ai_client.list_models", return_value=["gpt-4", "claude-3"]),
|
patch("src.gui_2.ai_client.list_models", return_value=["gpt-4", "claude-3"]),
|
||||||
@@ -83,8 +83,10 @@ def test_retry_escalation():
|
|||||||
|
|
||||||
with patch.object(engine.engine, "tick") as mock_tick:
|
with patch.object(engine.engine, "tick") as mock_tick:
|
||||||
# First tick returns ticket, second tick returns empty list to stop loop
|
# First tick returns ticket, second tick returns empty list to stop loop
|
||||||
mock_tick.side_effect = [[ticket], []]
|
mock_tick.side_effect = iter([[ticket], []])
|
||||||
|
|
||||||
|
engine.run()
|
||||||
|
engine.run()
|
||||||
engine.run()
|
engine.run()
|
||||||
|
|
||||||
assert ticket.retry_count == 1
|
assert ticket.retry_count == 1
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from src.gui_2 import App
|
|||||||
def app_instance() -> Generator[App, None, None]:
|
def app_instance() -> Generator[App, None, None]:
|
||||||
with (
|
with (
|
||||||
patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}),
|
patch('src.models.load_config', return_value={'ai': {'provider': 'gemini', 'model': 'gemini-2.5-flash-lite'}, 'projects': {}}),
|
||||||
patch('src.gui_2.save_config'),
|
patch('src.models.save_config'),
|
||||||
patch('src.gui_2.project_manager'),
|
patch('src.gui_2.project_manager'),
|
||||||
patch('src.gui_2.session_logger'),
|
patch('src.gui_2.session_logger'),
|
||||||
patch('src.gui_2.immapp.run'),
|
patch('src.gui_2.immapp.run'),
|
||||||
|
|||||||
Reference in New Issue
Block a user