onecoder 0.0.2__py3-none-any.whl
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.
- onecoder/agent.py +95 -0
- onecoder/agentic_tool_search/__init__.py +0 -0
- onecoder/agentic_tool_search/dynamic_tool_search.py +64 -0
- onecoder/agentic_tool_search/registry.py +33 -0
- onecoder/agents/__init__.py +7 -0
- onecoder/agents/documentation_agent.py +12 -0
- onecoder/agents/file_reader_agent.py +19 -0
- onecoder/agents/file_writer_agent.py +19 -0
- onecoder/agents/orchestrator_agent.py +51 -0
- onecoder/agents/refactoring_agent.py +12 -0
- onecoder/agents/research_agent.py +31 -0
- onecoder/agents/task_suggestion_agent.py +88 -0
- onecoder/alignment.py +236 -0
- onecoder/api.py +162 -0
- onecoder/api_client.py +112 -0
- onecoder/backends/base.py +22 -0
- onecoder/backends/local_tui.py +65 -0
- onecoder/blackboard.py +102 -0
- onecoder/cli.py +108 -0
- onecoder/commands/__init__.py +1 -0
- onecoder/commands/auth.py +78 -0
- onecoder/commands/ci.py +29 -0
- onecoder/commands/delegate.py +557 -0
- onecoder/commands/doctor.py +40 -0
- onecoder/commands/issue.py +136 -0
- onecoder/commands/logs.py +45 -0
- onecoder/commands/project.py +270 -0
- onecoder/commands/server.py +170 -0
- onecoder/config_manager.py +87 -0
- onecoder/constants.py +9 -0
- onecoder/diagnostics/__init__.py +2 -0
- onecoder/diagnostics/env_scan.py +207 -0
- onecoder/discovery.py +101 -0
- onecoder/distillation.py +236 -0
- onecoder/evaluation/__init__.py +1 -0
- onecoder/evaluation/ttu.py +176 -0
- onecoder/governance/__init__.py +0 -0
- onecoder/governance/probllm.py +91 -0
- onecoder/hooks.py +74 -0
- onecoder/ipc_auth.py +200 -0
- onecoder/issues.py +188 -0
- onecoder/jules_client.py +343 -0
- onecoder/knowledge.py +106 -0
- onecoder/llm.py +61 -0
- onecoder/logger.py +42 -0
- onecoder/metrics.py +129 -0
- onecoder/models/delegation.py +46 -0
- onecoder/onboarding.py +264 -0
- onecoder/review.py +233 -0
- onecoder/services/delegation_service.py +209 -0
- onecoder/services/validation_service.py +104 -0
- onecoder/sessions.py +186 -0
- onecoder/sprint_collector.py +165 -0
- onecoder/sync.py +167 -0
- onecoder/tmux.py +86 -0
- onecoder/tools/__init__.py +10 -0
- onecoder/tools/executor.py +53 -0
- onecoder/tools/external_tools.py +106 -0
- onecoder/tools/file_tools.py +77 -0
- onecoder/tools/interface.py +25 -0
- onecoder/tools/jules_tools.py +122 -0
- onecoder/tools/kit_tools.py +122 -0
- onecoder/tools/registry.py +32 -0
- onecoder/tui/__init__.py +5 -0
- onecoder/tui/app.py +263 -0
- onecoder/tui/commands.py +150 -0
- onecoder/tui/widgets.py +92 -0
- onecoder/worktree.py +186 -0
- onecoder-0.0.2.dist-info/METADATA +17 -0
- onecoder-0.0.2.dist-info/RECORD +73 -0
- onecoder-0.0.2.dist-info/WHEEL +5 -0
- onecoder-0.0.2.dist-info/entry_points.txt +2 -0
- onecoder-0.0.2.dist-info/top_level.txt +1 -0
onecoder/review.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, List, Optional
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.markdown import Markdown
|
|
11
|
+
|
|
12
|
+
# ADK Imports
|
|
13
|
+
try:
|
|
14
|
+
from google.adk.agents import LlmAgent
|
|
15
|
+
from google.adk.models.lite_llm import LiteLlm
|
|
16
|
+
except ImportError:
|
|
17
|
+
# Fallback/Mock if ADK not present
|
|
18
|
+
LlmAgent = None
|
|
19
|
+
LiteLlm = None
|
|
20
|
+
|
|
21
|
+
from .tools.kit_tools import kit_index_tool, kit_symbols_tool, kit_file_tree_tool
|
|
22
|
+
from .llm import LLMClient # keep for fallback
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from ai_sprint.state import SprintStateManager
|
|
26
|
+
from ai_sprint.policy import PolicyEngine
|
|
27
|
+
from ai_sprint.trace import trace_specifications
|
|
28
|
+
except ImportError as e:
|
|
29
|
+
SprintStateManager = None
|
|
30
|
+
PolicyEngine = None
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
class CodeReviewer:
|
|
35
|
+
def __init__(self, project_root: Optional[str] = None):
|
|
36
|
+
if project_root:
|
|
37
|
+
self.project_root = Path(project_root).absolute()
|
|
38
|
+
else:
|
|
39
|
+
# Try to find project root by looking for .git
|
|
40
|
+
current = Path.cwd().absolute()
|
|
41
|
+
while current != current.parent:
|
|
42
|
+
if (current / ".git").exists():
|
|
43
|
+
self.project_root = current
|
|
44
|
+
break
|
|
45
|
+
current = current.parent
|
|
46
|
+
else:
|
|
47
|
+
self.project_root = Path.cwd().absolute()
|
|
48
|
+
|
|
49
|
+
self.console = Console()
|
|
50
|
+
self.use_agent = (LlmAgent is not None)
|
|
51
|
+
|
|
52
|
+
def _get_git_diff(self, target: str = "main") -> str:
|
|
53
|
+
"""Get the git diff for the review context."""
|
|
54
|
+
try:
|
|
55
|
+
# If local changes (staged or unstaged)
|
|
56
|
+
res = subprocess.run(
|
|
57
|
+
["git", "diff", target], capture_output=True, text=True
|
|
58
|
+
)
|
|
59
|
+
return res.stdout
|
|
60
|
+
except Exception:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
def _get_sprint_context(self) -> Dict[str, Any]:
|
|
64
|
+
"""Fetch active sprint context."""
|
|
65
|
+
if not SprintStateManager:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
# Try to find active sprint
|
|
69
|
+
sprint_dir = self.project_root / ".sprint"
|
|
70
|
+
if not sprint_dir.exists():
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
# Simplified: find first active sprint
|
|
74
|
+
for item in sprint_dir.iterdir():
|
|
75
|
+
if item.is_dir() and item.name.startswith("0"):
|
|
76
|
+
# Check status
|
|
77
|
+
status_file = item / ".status"
|
|
78
|
+
if not status_file.exists() or status_file.read_text().strip() == "Active":
|
|
79
|
+
manager = SprintStateManager(item)
|
|
80
|
+
return manager.get_context_summary(None) # Get full summary
|
|
81
|
+
return {}
|
|
82
|
+
|
|
83
|
+
def _get_governance_rules(self) -> str:
|
|
84
|
+
"""Read governance.yaml."""
|
|
85
|
+
gov_file = self.project_root / "governance.yaml"
|
|
86
|
+
if gov_file.exists():
|
|
87
|
+
return gov_file.read_text()
|
|
88
|
+
return "No governance.yaml found."
|
|
89
|
+
|
|
90
|
+
def review(self, pr_id: Optional[str] = None, local: bool = False):
|
|
91
|
+
"""Perform a policy-grounded review."""
|
|
92
|
+
self.console.print("[cyan]Gathering Context Triad (Code, Intent, Law)...[/cyan]")
|
|
93
|
+
|
|
94
|
+
# 1. Code (The Change)
|
|
95
|
+
diff = self._get_git_diff("HEAD" if local else "main")
|
|
96
|
+
if not diff:
|
|
97
|
+
self.console.print("[yellow]No code changes detected.[/yellow]")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# 2. Intent (Sprint Context)
|
|
101
|
+
sprint_ctx = self._get_sprint_context() or {}
|
|
102
|
+
goal = sprint_ctx.get("goal", "Unknown Goal")
|
|
103
|
+
active_task_obj = sprint_ctx.get("active_task") or {}
|
|
104
|
+
active_task = active_task_obj.get("title", "Unknown Task")
|
|
105
|
+
|
|
106
|
+
# 3. Law (Governance)
|
|
107
|
+
rules = self._get_governance_rules()
|
|
108
|
+
|
|
109
|
+
# 4. The Verdict (Deterministic L1 Status)
|
|
110
|
+
verification_file = self.project_root / ".verification_results.json"
|
|
111
|
+
l1_verdict = "No deterministic L1 verification results found."
|
|
112
|
+
if verification_file.exists():
|
|
113
|
+
try:
|
|
114
|
+
l1_verdict = verification_file.read_text()
|
|
115
|
+
except Exception as e:
|
|
116
|
+
l1_verdict = f"Error reading L1 verification: {e}"
|
|
117
|
+
|
|
118
|
+
# Construct System Prompt (Intelligence)
|
|
119
|
+
system_prompt = f"""
|
|
120
|
+
You are the Governance Enforcer for OneCoder.
|
|
121
|
+
|
|
122
|
+
**THE RULES (governance.yaml)**:
|
|
123
|
+
{rules}
|
|
124
|
+
|
|
125
|
+
**THE INTENT (Sprint Context)**:
|
|
126
|
+
Goal: {goal}
|
|
127
|
+
Active Task: {active_task}
|
|
128
|
+
|
|
129
|
+
**THE VERDICT (Deterministic L1 Status)**:
|
|
130
|
+
{l1_verdict}
|
|
131
|
+
|
|
132
|
+
**YOUR MISSION**:
|
|
133
|
+
1. Verify if the Code fulfills the Intent.
|
|
134
|
+
2. PRIORITY: If there are L1 failures in THE VERDICT, analyze the specific error logs and provide a mitigation plan to fix them.
|
|
135
|
+
3. Check for any other violations of The Rules (especially Banned Files, Architecture constraints).
|
|
136
|
+
4. Assess code quality (Security, Performance).
|
|
137
|
+
5. If the diff is ambiguous, USE YOUR TOOLS (kit_index, kit_symbols, etc.) to investigate the repository context.
|
|
138
|
+
|
|
139
|
+
**OUTPUT FORMAT**:
|
|
140
|
+
You must ALWAYS respond with a Strict JSON object:
|
|
141
|
+
{{
|
|
142
|
+
"pass": boolean,
|
|
143
|
+
"violations": ["list", "of", "strings"],
|
|
144
|
+
"feedback": "markdown string summarizing the review",
|
|
145
|
+
"mitigation_notes": "Technical notes for a coding agent to fix the violations. Be specific about files and changes needed."
|
|
146
|
+
}}
|
|
147
|
+
"""
|
|
148
|
+
user_message = f"**THE CODE (Git Diff)**:\n{diff[:10000]} # Truncated"
|
|
149
|
+
|
|
150
|
+
self.console.print("[cyan]Consulting Intelligence (Agentic Reviewer)...[/cyan]")
|
|
151
|
+
|
|
152
|
+
if self.use_agent:
|
|
153
|
+
try:
|
|
154
|
+
# Use Agentic flow
|
|
155
|
+
asyncio.run(self._run_agent_review(system_prompt, user_message))
|
|
156
|
+
return
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self.console.print(f"[yellow]Agentic review failed ({e}). Falling back to simple LLM.[/yellow]")
|
|
159
|
+
|
|
160
|
+
# Fallback to simple LLM
|
|
161
|
+
self._run_simple_review(system_prompt, user_message)
|
|
162
|
+
|
|
163
|
+
async def _run_agent_review(self, system_prompt: str, user_message: str):
|
|
164
|
+
"""Run the review using LlmAgent with tools."""
|
|
165
|
+
model_name = os.getenv("ONECODER_MODEL", "openrouter/xiaomi/mimo-v2-flash:free")
|
|
166
|
+
model = LiteLlm(model_name)
|
|
167
|
+
|
|
168
|
+
agent = LlmAgent(
|
|
169
|
+
name="reviewer_agent",
|
|
170
|
+
model=model,
|
|
171
|
+
instruction=system_prompt,
|
|
172
|
+
tools=[kit_index_tool, kit_symbols_tool, kit_file_tree_tool],
|
|
173
|
+
output_key="final_response"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Agent run
|
|
177
|
+
# We append user message to history or as prompt
|
|
178
|
+
# LlmAgent usually takes a user prompt
|
|
179
|
+
response = await agent.run(user_message)
|
|
180
|
+
|
|
181
|
+
# Parse output - Agent returns a string usually, hopefully JSON if instructed
|
|
182
|
+
try:
|
|
183
|
+
# Clean up markdown code blocks if present
|
|
184
|
+
clean_response = response.strip()
|
|
185
|
+
if clean_response.startswith("```json"):
|
|
186
|
+
clean_response = clean_response[7:]
|
|
187
|
+
if clean_response.endswith("```"):
|
|
188
|
+
clean_response = clean_response[:-3]
|
|
189
|
+
|
|
190
|
+
result = json.loads(clean_response)
|
|
191
|
+
self._render_result(result)
|
|
192
|
+
except json.JSONDecodeError:
|
|
193
|
+
self.console.print(f"[red]Agent output is not valid JSON:[/red] {response}")
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
def _run_simple_review(self, system_prompt: str, user_message: str):
|
|
197
|
+
"""Run legacy simple review."""
|
|
198
|
+
llm = LLMClient()
|
|
199
|
+
prompt = system_prompt + "\n\n" + user_message
|
|
200
|
+
result = llm.generate_json(prompt)
|
|
201
|
+
self._render_result(result)
|
|
202
|
+
|
|
203
|
+
def _render_result(self, result: Dict[str, Any]):
|
|
204
|
+
passed = result.get("pass", False)
|
|
205
|
+
color = "green" if passed else "red"
|
|
206
|
+
|
|
207
|
+
self.console.print(Panel(
|
|
208
|
+
Markdown(result.get("feedback", "")),
|
|
209
|
+
title=f"[{color}]Review Result: {'PASSED' if passed else 'FAILED'}[/{color}]",
|
|
210
|
+
border_style=color
|
|
211
|
+
))
|
|
212
|
+
|
|
213
|
+
mitigation_notes = result.get("mitigation_notes")
|
|
214
|
+
if mitigation_notes:
|
|
215
|
+
self.console.print("\n[bold yellow]Mitigation Notes for Agent:[/bold yellow]")
|
|
216
|
+
self.console.print(mitigation_notes)
|
|
217
|
+
|
|
218
|
+
# Persist state for delegation
|
|
219
|
+
try:
|
|
220
|
+
state_dir = self.project_root / ".onecoder"
|
|
221
|
+
state_dir.mkdir(exist_ok=True)
|
|
222
|
+
state_file = state_dir / "review_state.json"
|
|
223
|
+
state_file.write_text(json.dumps(result, indent=2))
|
|
224
|
+
except Exception as e:
|
|
225
|
+
self.console.print(f"[dim]Failed to save review state: {e}[/dim]")
|
|
226
|
+
|
|
227
|
+
if not passed:
|
|
228
|
+
self.console.print("\n[bold red]Violations:[/bold red]")
|
|
229
|
+
for v in result.get("violations", []):
|
|
230
|
+
self.console.print(f" ❌ {v}")
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
else:
|
|
233
|
+
self.console.print("\n[bold green]Policy Checks Passed.[/bold green]")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..models.delegation import DelegationSession
|
|
7
|
+
from ..worktree import WorktreeManager
|
|
8
|
+
from ..tmux import TmuxManager
|
|
9
|
+
from ..blackboard import BlackboardMemory
|
|
10
|
+
from ..backends.base import BaseBackend
|
|
11
|
+
from ..backends.local_tui import LocalTUIBackend
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class DelegationService:
|
|
16
|
+
"""
|
|
17
|
+
Orchestrates delegated tasks using WorktreeManager and TmuxManager.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, blackboard: Optional[BlackboardMemory] = None):
|
|
21
|
+
self.worktree_mgr = WorktreeManager()
|
|
22
|
+
self.tmux_mgr = TmuxManager()
|
|
23
|
+
self.blackboard = blackboard or BlackboardMemory()
|
|
24
|
+
self.sessions_namespace = "delegation_sessions"
|
|
25
|
+
self.backends: Dict[str, BaseBackend] = {
|
|
26
|
+
"local-tui": LocalTUIBackend(self)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def create_session(self, task_id: str, backend: str = "local-tui", command: Optional[str] = None, external_id: Optional[str] = None) -> DelegationSession:
|
|
30
|
+
"""Creates and persists a new delegation session."""
|
|
31
|
+
parent_branch = self.worktree_mgr.get_current_branch()
|
|
32
|
+
active_sprint_id = os.environ.get("ACTIVE_SPRINT_ID")
|
|
33
|
+
|
|
34
|
+
# Try to find spec_id from knowledge
|
|
35
|
+
spec_id = None
|
|
36
|
+
try:
|
|
37
|
+
from ..knowledge import ProjectKnowledge
|
|
38
|
+
pk = ProjectKnowledge()
|
|
39
|
+
knowledge = pk.get_l1_context()
|
|
40
|
+
if knowledge:
|
|
41
|
+
# Usually spec_id is in the goal or active task metadata in some implementations
|
|
42
|
+
# but for now we look for SPEC- prefix in active task title or goal
|
|
43
|
+
goal = knowledge.get("goal", "")
|
|
44
|
+
active_task = knowledge.get("active_task", {})
|
|
45
|
+
|
|
46
|
+
import re
|
|
47
|
+
spec_match = re.search(r"(SPEC-[A-Z0-9.-]+)", goal + " " + active_task.get("title", ""))
|
|
48
|
+
if spec_match:
|
|
49
|
+
spec_id = spec_match.group(1)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
session = DelegationSession(
|
|
54
|
+
task_id=task_id,
|
|
55
|
+
backend=backend,
|
|
56
|
+
command=command,
|
|
57
|
+
parent_branch=parent_branch,
|
|
58
|
+
sprint_id=active_sprint_id,
|
|
59
|
+
spec_id=spec_id,
|
|
60
|
+
external_id=external_id
|
|
61
|
+
)
|
|
62
|
+
self._save_session(session)
|
|
63
|
+
return session
|
|
64
|
+
|
|
65
|
+
def _save_session(self, session: DelegationSession):
|
|
66
|
+
"""Persists session to blackboard memory."""
|
|
67
|
+
self.blackboard.set(
|
|
68
|
+
key=session.id,
|
|
69
|
+
value=session.model_dump(mode='json'),
|
|
70
|
+
scope="global",
|
|
71
|
+
namespace=self.sessions_namespace
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def get_session(self, session_id: str) -> Optional[DelegationSession]:
|
|
75
|
+
"""Retrieves a session from blackboard."""
|
|
76
|
+
data = self.blackboard.get(
|
|
77
|
+
key=session_id,
|
|
78
|
+
scope="global",
|
|
79
|
+
namespace=self.sessions_namespace
|
|
80
|
+
)
|
|
81
|
+
if data:
|
|
82
|
+
return DelegationSession(**data)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def list_sessions(self) -> List[DelegationSession]:
|
|
86
|
+
"""Lists all active delegation sessions."""
|
|
87
|
+
all_data = self.blackboard.get_all(scope="global", namespace=self.sessions_namespace)
|
|
88
|
+
return [DelegationSession(**data) for data in all_data.values()]
|
|
89
|
+
|
|
90
|
+
async def start_session(self, session: DelegationSession, context_metadata: Optional[str] = None) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Starts a delegation session using the appropriate backend.
|
|
93
|
+
"""
|
|
94
|
+
backend = self.backends.get(session.backend)
|
|
95
|
+
if not backend:
|
|
96
|
+
raise ValueError(f"Unknown backend: {session.backend}")
|
|
97
|
+
|
|
98
|
+
return await backend.spawn(session)
|
|
99
|
+
|
|
100
|
+
def stop_session(self, session_id: str, cleanup: bool = True):
|
|
101
|
+
"""Stops a session and optionally cleans up resources."""
|
|
102
|
+
session = self.get_session(session_id)
|
|
103
|
+
if not session:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if session.tmux_session:
|
|
107
|
+
self.tmux_mgr.kill_session(session.tmux_session)
|
|
108
|
+
|
|
109
|
+
if cleanup and session.worktree_path:
|
|
110
|
+
self.worktree_mgr.remove_worktree(session.id, delete_branch=True)
|
|
111
|
+
|
|
112
|
+
session.status = "stopped"
|
|
113
|
+
self._save_session(session)
|
|
114
|
+
|
|
115
|
+
def register_task_in_sprint(self, title: str, repo_root: Path) -> tuple[Optional[str], Optional[str]]:
|
|
116
|
+
"""
|
|
117
|
+
Registers a new task in the active sprint's TODO.md and returns the generated ID.
|
|
118
|
+
Returns: (sprint_id, task_id)
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
from ai_sprint.state import SprintStateManager
|
|
122
|
+
except ImportError:
|
|
123
|
+
logger.warning("ai_sprint SDK not available. Cannot register task.")
|
|
124
|
+
return None, None
|
|
125
|
+
|
|
126
|
+
sprint_dir = repo_root / ".sprint"
|
|
127
|
+
if not sprint_dir.exists():
|
|
128
|
+
return None, None
|
|
129
|
+
|
|
130
|
+
# 1. Detect Active Sprint
|
|
131
|
+
# Priority: ACTIVE_SPRINT_ID env > First Active Sprint found
|
|
132
|
+
active_sprint_id = os.environ.get("ACTIVE_SPRINT_ID")
|
|
133
|
+
|
|
134
|
+
if not active_sprint_id:
|
|
135
|
+
# Simple discovery: Find first dir with .status containing "Active" or missing .status
|
|
136
|
+
for item in sorted(sprint_dir.iterdir()):
|
|
137
|
+
if item.is_dir() and item.name[0].isdigit(): # basic check for sprint dir
|
|
138
|
+
status_file = item / ".status"
|
|
139
|
+
if not status_file.exists() or status_file.read_text().strip() == "Active":
|
|
140
|
+
active_sprint_id = item.name
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
if not active_sprint_id:
|
|
144
|
+
return None, None
|
|
145
|
+
|
|
146
|
+
target_sprint_dir = sprint_dir / active_sprint_id
|
|
147
|
+
todo_file = target_sprint_dir / "TODO.md"
|
|
148
|
+
|
|
149
|
+
if not todo_file.exists():
|
|
150
|
+
return None, None
|
|
151
|
+
|
|
152
|
+
# 2. Append to TODO.md
|
|
153
|
+
# specific format: "- [ ] {title}"
|
|
154
|
+
try:
|
|
155
|
+
with open(todo_file, "a") as f:
|
|
156
|
+
f.write(f"\n- [ ] {title}\n")
|
|
157
|
+
|
|
158
|
+
# 3. Sync and Retrieve ID
|
|
159
|
+
state_mgr = SprintStateManager(target_sprint_dir)
|
|
160
|
+
state_mgr.sync_from_files()
|
|
161
|
+
task_id = state_mgr.get_task_id_by_title(title)
|
|
162
|
+
|
|
163
|
+
return active_sprint_id, task_id
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error(f"Failed to register task in sprint: {e}")
|
|
167
|
+
return None, None
|
|
168
|
+
|
|
169
|
+
def _inject_context(self, wt_path: Path, session: DelegationSession, metadata: Optional[str] = None):
|
|
170
|
+
"""Writes a BOOTSTRAP.md to the worktree to reduce agent TTU."""
|
|
171
|
+
bootstrap_path = wt_path / "BOOTSTRAP.md"
|
|
172
|
+
content = f"# Task: {session.task_id}\n\n"
|
|
173
|
+
content += f"## Branch Context\nParent Branch: `{session.parent_branch}`\n"
|
|
174
|
+
if session.sprint_id:
|
|
175
|
+
content += f"Active Sprint: `{session.sprint_id}`\n"
|
|
176
|
+
if session.spec_id:
|
|
177
|
+
content += f"Spec ID: `{session.spec_id}`\n"
|
|
178
|
+
|
|
179
|
+
content += f"\n## Instructions\n{session.command or 'No command provided.'}\n\n"
|
|
180
|
+
content += "## Repository Context\n"
|
|
181
|
+
|
|
182
|
+
# Try to gather automated knowledge
|
|
183
|
+
try:
|
|
184
|
+
from ..knowledge import ProjectKnowledge
|
|
185
|
+
pk = ProjectKnowledge()
|
|
186
|
+
knowledge = pk.get_rag_ready_output()
|
|
187
|
+
content += knowledge
|
|
188
|
+
except Exception:
|
|
189
|
+
content += "Automated context gathering failed.\n"
|
|
190
|
+
|
|
191
|
+
# Inject Mitigation Notes from recent review
|
|
192
|
+
try:
|
|
193
|
+
# Assume we are running from project root or can find it
|
|
194
|
+
review_state_path = Path(".onecoder/review_state.json")
|
|
195
|
+
if review_state_path.exists():
|
|
196
|
+
import json
|
|
197
|
+
state = json.loads(review_state_path.read_text())
|
|
198
|
+
mitigation_notes = state.get("mitigation_notes")
|
|
199
|
+
if mitigation_notes:
|
|
200
|
+
content += f"\n## Mitigation Notes (Recent Review)\n{mitigation_notes}\n"
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.warning(f"Failed to inject review notes: {e}")
|
|
203
|
+
|
|
204
|
+
if metadata:
|
|
205
|
+
content += f"\n## Additional Metadata\n{metadata}\n"
|
|
206
|
+
|
|
207
|
+
with open(bootstrap_path, "w") as f:
|
|
208
|
+
f.write(content)
|
|
209
|
+
logger.info(f"Injected context at {bootstrap_path}")
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List, Any, Dict, Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class BaseValidationRule(ABC):
|
|
10
|
+
"""Base class for all validation rules."""
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def validate(self, context: Dict[str, Any]) -> bool:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def name(self) -> str:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class FileExistsRule(BaseValidationRule):
|
|
21
|
+
"""Checks if a file exists relative to a base path."""
|
|
22
|
+
def __init__(self, filepath: str):
|
|
23
|
+
self.filepath = filepath
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
return f"FileExists({self.filepath})"
|
|
28
|
+
|
|
29
|
+
def validate(self, context: Dict[str, Any]) -> bool:
|
|
30
|
+
base_path = context.get("worktree_path")
|
|
31
|
+
if not base_path:
|
|
32
|
+
return False
|
|
33
|
+
return (Path(base_path) / self.filepath).exists()
|
|
34
|
+
|
|
35
|
+
class CommandSuccessRule(BaseValidationRule):
|
|
36
|
+
"""Checks if a command executes successfully (exit code 0)."""
|
|
37
|
+
def __init__(self, command: str):
|
|
38
|
+
self.command = command
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def name(self) -> str:
|
|
42
|
+
return f"CommandSuccess({self.command})"
|
|
43
|
+
|
|
44
|
+
def validate(self, context: Dict[str, Any]) -> bool:
|
|
45
|
+
base_path = context.get("worktree_path")
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
self.command,
|
|
49
|
+
shell=True,
|
|
50
|
+
cwd=base_path,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
check=False
|
|
53
|
+
)
|
|
54
|
+
return result.returncode == 0
|
|
55
|
+
except Exception:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
class ValidationReport:
|
|
59
|
+
"""Structured report for validation results."""
|
|
60
|
+
def __init__(self, session_id: str, all_passed: bool, results: List[Dict[str, Any]]):
|
|
61
|
+
self.session_id = session_id
|
|
62
|
+
self.all_passed = all_passed
|
|
63
|
+
self.results = results
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"session_id": self.session_id,
|
|
68
|
+
"all_passed": self.all_passed,
|
|
69
|
+
"results": self.results
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class ValidationService:
|
|
73
|
+
"""
|
|
74
|
+
ValidationService runs a set of rules against a delegation session.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def validate_session(self, session_context: Dict[str, Any], rules: List[BaseValidationRule]) -> ValidationReport:
|
|
78
|
+
"""
|
|
79
|
+
Runs all rules and returns a structured ValidationReport.
|
|
80
|
+
"""
|
|
81
|
+
session_id = session_context.get("session_id", "unknown")
|
|
82
|
+
results = []
|
|
83
|
+
all_passed = True
|
|
84
|
+
|
|
85
|
+
for rule in rules:
|
|
86
|
+
try:
|
|
87
|
+
passed = rule.validate(session_context)
|
|
88
|
+
results.append({
|
|
89
|
+
"rule": rule.name,
|
|
90
|
+
"passed": passed,
|
|
91
|
+
"error": None
|
|
92
|
+
})
|
|
93
|
+
if not passed:
|
|
94
|
+
all_passed = False
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Rule {rule.name} failed with error: {e}")
|
|
97
|
+
results.append({
|
|
98
|
+
"rule": rule.name,
|
|
99
|
+
"passed": False,
|
|
100
|
+
"error": str(e)
|
|
101
|
+
})
|
|
102
|
+
all_passed = False
|
|
103
|
+
|
|
104
|
+
return ValidationReport(session_id, all_passed, results)
|