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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import asyncio
|
|
4
|
+
import subprocess
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
|
|
11
|
+
from ..services.delegation_service import DelegationService
|
|
12
|
+
from ..services.validation_service import ValidationService, FileExistsRule, CommandSuccessRule
|
|
13
|
+
from ..jules_client import JulesAPIClient, JulesAPIError, JulesAuthError
|
|
14
|
+
|
|
15
|
+
@click.command()
|
|
16
|
+
@click.option("--limit", default=10, help="Number of recent sessions to show")
|
|
17
|
+
def delegate_list(limit):
|
|
18
|
+
"""List active delegation sessions and their status."""
|
|
19
|
+
console = Console()
|
|
20
|
+
service = DelegationService()
|
|
21
|
+
sessions = service.list_sessions()
|
|
22
|
+
|
|
23
|
+
# Sort by created_at desc
|
|
24
|
+
sessions.sort(key=lambda s: s.created_at, reverse=True)
|
|
25
|
+
|
|
26
|
+
table = Table(title="Delegation Sessions")
|
|
27
|
+
table.add_column("Session ID", style="cyan", no_wrap=True)
|
|
28
|
+
table.add_column("Task ID (Sprint)", style="magenta")
|
|
29
|
+
table.add_column("Status", style="bold")
|
|
30
|
+
table.add_column("Age", style="white")
|
|
31
|
+
table.add_column("Backend", style="blue")
|
|
32
|
+
|
|
33
|
+
now = datetime.now()
|
|
34
|
+
|
|
35
|
+
for s in sessions[:limit]:
|
|
36
|
+
# Calculate age
|
|
37
|
+
age = now - s.created_at
|
|
38
|
+
age_str = str(age).split('.')[0] # Remove microseconds
|
|
39
|
+
|
|
40
|
+
status_style = "green" if s.status == "running" else "red" if s.status == "failed" else "white"
|
|
41
|
+
|
|
42
|
+
table.add_row(
|
|
43
|
+
s.id[:8],
|
|
44
|
+
s.task_id,
|
|
45
|
+
f"[{status_style}]{s.status}[/]",
|
|
46
|
+
age_str,
|
|
47
|
+
s.backend
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
console.print(table)
|
|
51
|
+
|
|
52
|
+
@click.command()
|
|
53
|
+
@click.argument("session_id")
|
|
54
|
+
def delegate_status(session_id):
|
|
55
|
+
"""Check the status of a local delegation session."""
|
|
56
|
+
console = Console()
|
|
57
|
+
service = DelegationService()
|
|
58
|
+
session = service.get_session(session_id)
|
|
59
|
+
|
|
60
|
+
if not session:
|
|
61
|
+
console.print(f"[bold red]Error:[/bold red] Session {session_id} not found.")
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
status_color = "green" if session.status == "running" else "yellow"
|
|
65
|
+
panel = Panel(
|
|
66
|
+
f"[bold]Status:[/bold] [{status_color}]{session.status}[/]\n"
|
|
67
|
+
f"[bold]Backend:[/bold] {session.backend}\n"
|
|
68
|
+
f"[bold]Worktree:[/bold] {session.worktree_path}\n"
|
|
69
|
+
f"[bold]Tmux Session:[/bold] {session.tmux_session or 'N/A'}\n"
|
|
70
|
+
f"[bold]Task ID:[/bold] {session.task_id}",
|
|
71
|
+
title=f"Session: {session_id}"
|
|
72
|
+
)
|
|
73
|
+
console.print(panel)
|
|
74
|
+
|
|
75
|
+
@click.command()
|
|
76
|
+
@click.argument("session_id")
|
|
77
|
+
def delegate_validate(session_id):
|
|
78
|
+
"""Manually trigger validation for a local delegation session."""
|
|
79
|
+
console = Console()
|
|
80
|
+
dg_service = DelegationService()
|
|
81
|
+
session = dg_service.get_session(session_id)
|
|
82
|
+
|
|
83
|
+
if not session:
|
|
84
|
+
console.print(f"[bold red]Error:[/bold red] Session {session_id} not found.")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if not session.worktree_path:
|
|
88
|
+
console.print("[bold red]Error:[/bold red] Session has no associated worktree path.")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
val_service = ValidationService()
|
|
92
|
+
# For now, we'll use some default rules or search for a validation spec in the worktree
|
|
93
|
+
rules = [
|
|
94
|
+
FileExistsRule("README.md"), # Basic sanity check
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Check for a specific validation script if it exists
|
|
98
|
+
if (Path(session.worktree_path) / "validate.sh").exists():
|
|
99
|
+
rules.append(CommandSuccessRule("bash validate.sh"))
|
|
100
|
+
|
|
101
|
+
context = {
|
|
102
|
+
"session_id": session_id,
|
|
103
|
+
"worktree_path": session.worktree_path,
|
|
104
|
+
"task_id": session.task_id
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
with console.status("[bold green]Running validation rules..."):
|
|
108
|
+
report = val_service.validate_session(context, rules)
|
|
109
|
+
|
|
110
|
+
table = Table(title=f"Validation Report: {session_id}")
|
|
111
|
+
table.add_column("Rule", style="cyan")
|
|
112
|
+
table.add_column("Passed", style="bold")
|
|
113
|
+
table.add_column("Error", style="red")
|
|
114
|
+
|
|
115
|
+
for res in report.results:
|
|
116
|
+
passed_str = "[green]YES[/green]" if res["passed"] else "[red]NO[/red]"
|
|
117
|
+
table.add_row(res["rule"], passed_str, res["error"] or "-")
|
|
118
|
+
|
|
119
|
+
console.print(table)
|
|
120
|
+
|
|
121
|
+
if report.all_passed:
|
|
122
|
+
console.print("\n[bold green]✓ All validation rules passed![/bold green]")
|
|
123
|
+
else:
|
|
124
|
+
console.print("\n[bold red]✗ Some validation rules failed.[/bold red]")
|
|
125
|
+
|
|
126
|
+
@click.command()
|
|
127
|
+
@click.argument("session_id")
|
|
128
|
+
@click.option("--message", "-m", help="Commit message", default="feat: complete delegated task")
|
|
129
|
+
@click.option("--force", is_flag=True, help="Force finish despite validation failures")
|
|
130
|
+
@click.option("--cleanup/--no-cleanup", default=None, help="Cleanup worktree after finish")
|
|
131
|
+
def delegate_finish(session_id, message, force, cleanup):
|
|
132
|
+
"""
|
|
133
|
+
Finishes a delegation session: validates, commits tracking metadata, and cleans up.
|
|
134
|
+
"""
|
|
135
|
+
console = Console()
|
|
136
|
+
dg_service = DelegationService()
|
|
137
|
+
session = dg_service.get_session(session_id)
|
|
138
|
+
|
|
139
|
+
if not session:
|
|
140
|
+
console.print(f"[bold red]Error:[/bold red] Session {session_id} not found.")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
# --- Remote Jules Governance ---
|
|
144
|
+
if session.backend == "jules":
|
|
145
|
+
if not session.external_id:
|
|
146
|
+
console.print("[red]Error: Remote session missing external Jules ID.[/red]")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
console.print(f"[cyan]Governing remote Jules session {session.external_id}...[/cyan]")
|
|
150
|
+
client = JulesAPIClient()
|
|
151
|
+
|
|
152
|
+
# 1. Detect PR
|
|
153
|
+
pr_info = client.detect_pr_output(session.external_id)
|
|
154
|
+
if not pr_info:
|
|
155
|
+
console.print("[yellow]No PR detected yet for this session. Is it complete?[/yellow]")
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
pr_url = pr_info["url"]
|
|
159
|
+
console.print(f"Found PR: {pr_url}")
|
|
160
|
+
|
|
161
|
+
# 2. Fetch PR Locally for Validation
|
|
162
|
+
# We need to checkout the PR branch to validate and merge it.
|
|
163
|
+
# Assuming we are in the repo root.
|
|
164
|
+
try:
|
|
165
|
+
console.print("[cyan]Fetching PR branch...[/cyan]")
|
|
166
|
+
subprocess.run(["gh", "pr", "checkout", pr_url], check=True)
|
|
167
|
+
|
|
168
|
+
# 3. Validate
|
|
169
|
+
val_service = ValidationService()
|
|
170
|
+
rules = [FileExistsRule("README.md")] # Default rules
|
|
171
|
+
|
|
172
|
+
context = {"session_id": session_id, "worktree_path": os.getcwd()} # We are in main repo now
|
|
173
|
+
|
|
174
|
+
with console.status("[bold green]Validating remote work..."):
|
|
175
|
+
report = val_service.validate_session(context, rules)
|
|
176
|
+
|
|
177
|
+
if not report.all_passed:
|
|
178
|
+
console.print("[bold red]Validation Failed.[/bold red]")
|
|
179
|
+
if not force and not click.confirm("Force proceed?"):
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# 4. Governed Merge
|
|
183
|
+
parent_branch = session.parent_branch or "main" # Fallback
|
|
184
|
+
current_pr_branch = subprocess.check_output(["git", "branch", "--show-current"], text=True).strip()
|
|
185
|
+
|
|
186
|
+
console.print(f"[cyan]Switching to parent branch {parent_branch}...[/cyan]")
|
|
187
|
+
subprocess.run(["git", "checkout", parent_branch], check=True)
|
|
188
|
+
|
|
189
|
+
console.print(f"[cyan]Merging PR branch {current_pr_branch}...[/cyan]")
|
|
190
|
+
subprocess.run(["git", "merge", "--no-ff", "-m", f"chore: merge remote task {session.task_id}", current_pr_branch], check=True)
|
|
191
|
+
|
|
192
|
+
# 5. Track in Sprint
|
|
193
|
+
# Let's explicitly append trailers to HEAD (the merge commit)
|
|
194
|
+
# trailers = [
|
|
195
|
+
# f"Sprint-Id: {session.sprint_id}",
|
|
196
|
+
# f"Task-Id: {session.task_id}",
|
|
197
|
+
# "Status: done"
|
|
198
|
+
# ]
|
|
199
|
+
# subprocess.run(...)
|
|
200
|
+
console.print("[green]✓ Remote task merged and governed.[/green]")
|
|
201
|
+
|
|
202
|
+
if cleanup:
|
|
203
|
+
subprocess.run(["git", "branch", "-D", current_pr_branch], check=True)
|
|
204
|
+
|
|
205
|
+
except subprocess.CalledProcessError as e:
|
|
206
|
+
console.print(f"[red]Error during remote finish: {e}[/red]")
|
|
207
|
+
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# --- Local Worktree Logic (Existing) ---
|
|
211
|
+
val_service = ValidationService()
|
|
212
|
+
rules = [FileExistsRule("README.md")]
|
|
213
|
+
if session.worktree_path and (Path(session.worktree_path) / "validate.sh").exists():
|
|
214
|
+
rules.append(CommandSuccessRule("bash validate.sh"))
|
|
215
|
+
|
|
216
|
+
context = {
|
|
217
|
+
"session_id": session_id,
|
|
218
|
+
"worktree_path": session.worktree_path
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
with console.status("[bold green]Validating session..."):
|
|
222
|
+
report = val_service.validate_session(context, rules)
|
|
223
|
+
|
|
224
|
+
if not report.all_passed:
|
|
225
|
+
console.print("[bold red]Validation Failed per rules.[/bold red]")
|
|
226
|
+
if not force:
|
|
227
|
+
console.print("Use --force to override.")
|
|
228
|
+
if not click.confirm("Do you want to force finish despite validation failures?"):
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# 2. Commit with Tracking Metadata
|
|
232
|
+
console.print("[cyan]Committing with tracking metadata...[/cyan]")
|
|
233
|
+
|
|
234
|
+
# 2. Atomic Commit in worktree
|
|
235
|
+
if session.worktree_path and os.path.exists(session.worktree_path):
|
|
236
|
+
console.print("[cyan]Finalizing work with atomic commit...[/cyan]")
|
|
237
|
+
try:
|
|
238
|
+
# Using sprint commit with metadata
|
|
239
|
+
cmd = ["sprint", "commit", "-m", message, "--task-id", session.task_id, "--status", "done", "--validation", "Passed"]
|
|
240
|
+
|
|
241
|
+
# Pass python path if needed (heuristic)
|
|
242
|
+
env = os.environ.copy()
|
|
243
|
+
if "PYTHONPATH" not in env:
|
|
244
|
+
env["PYTHONPATH"] = str(dg_service.worktree_mgr.project_root / "sprint-cli" / "src")
|
|
245
|
+
|
|
246
|
+
subprocess.run(cmd, cwd=session.worktree_path, check=True, capture_output=True, env=env)
|
|
247
|
+
console.print("[bold green]✓ Work committed to task branch.[/bold green]")
|
|
248
|
+
except subprocess.CalledProcessError as e:
|
|
249
|
+
# Check if it failed because there's nothing to commit
|
|
250
|
+
if "No staged changes to audit" in e.stdout.decode() or "nothing to commit" in e.stdout.decode():
|
|
251
|
+
console.print("[bold green]✓ Work already committed to task branch (clean).[/bold green]")
|
|
252
|
+
else:
|
|
253
|
+
console.print(f"[yellow]Sprint tracking failed: {e.stderr.decode()}[/yellow]")
|
|
254
|
+
console.print("Falling back to standard git commit to save work...")
|
|
255
|
+
try:
|
|
256
|
+
subprocess.run(["git", "add", "."], cwd=session.worktree_path, check=True)
|
|
257
|
+
subprocess.run(["git", "commit", "-m", message], cwd=session.worktree_path, check=True)
|
|
258
|
+
console.print("[bold green]✓ Work saved via git commit (untracked in sprint).[/bold green]")
|
|
259
|
+
except subprocess.CalledProcessError as git_e:
|
|
260
|
+
if "nothing to commit" in git_e.stdout.decode() or "nothing to commit" in git_e.stderr.decode():
|
|
261
|
+
console.print("[bold green]✓ Work already saved (clean).[/bold green]")
|
|
262
|
+
else:
|
|
263
|
+
console.print(f"[bold red]Critical Error:[/bold red] Failed to commit work: {git_e}")
|
|
264
|
+
return # Do not cleanup if commit failed
|
|
265
|
+
else:
|
|
266
|
+
console.print("[yellow]Worktree not found or already removed. Skipping commit step.[/yellow]")
|
|
267
|
+
|
|
268
|
+
# 3. Merge Back to Parent Branch
|
|
269
|
+
parent_branch = session.parent_branch
|
|
270
|
+
if not parent_branch:
|
|
271
|
+
console.print("[yellow]No parent branch recorded for this session. Skipping auto-merge.[/yellow]")
|
|
272
|
+
else:
|
|
273
|
+
console.print(f"[cyan]Removing worktree {session.worktree_path} to allow merge...[/cyan]")
|
|
274
|
+
dg_service.worktree_mgr.remove_worktree(session_id, delete_branch=False)
|
|
275
|
+
|
|
276
|
+
console.print(f"[cyan]Merging task branch into {parent_branch}...[/cyan]")
|
|
277
|
+
|
|
278
|
+
# 3.2. Ensure compatibility first: Rebase task branch onto parent
|
|
279
|
+
if not dg_service.worktree_mgr.rebase_onto(session_id, parent_branch):
|
|
280
|
+
console.print("[bold red]Rebase failed![/bold red] Manual intervention required.")
|
|
281
|
+
console.print(f"[yellow]Restoring worktree for conflict resolution...[/yellow]")
|
|
282
|
+
dg_service.worktree_mgr.create_worktree(session_id, base_ref=f"task/{session_id}")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# 3.3. Perform the merge
|
|
286
|
+
if dg_service.worktree_mgr.merge_task_branch(session_id, parent_branch):
|
|
287
|
+
console.print(f"[bold green]✓ Successfully merged into {parent_branch}[/bold green]")
|
|
288
|
+
else:
|
|
289
|
+
console.print("[bold red]Merge failed![/bold red] Manual intervention required.")
|
|
290
|
+
console.print(f"[yellow]Restoring worktree for conflict resolution...[/yellow]")
|
|
291
|
+
dg_service.worktree_mgr.create_worktree(session_id, base_ref=f"task/{session_id}")
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
# 4. Final Cleanup (Branch deletion)
|
|
295
|
+
should_cleanup = cleanup
|
|
296
|
+
if should_cleanup is None:
|
|
297
|
+
should_cleanup = click.confirm(f"Delete task branch 'task/{session_id}'?")
|
|
298
|
+
|
|
299
|
+
if should_cleanup:
|
|
300
|
+
# We already removed the worktree, just delete the branch now.
|
|
301
|
+
dg_service.worktree_mgr.remove_worktree(session_id, delete_branch=True)
|
|
302
|
+
console.print("[bold green]✓ Task branch deleted.[/bold green]")
|
|
303
|
+
|
|
304
|
+
@click.command()
|
|
305
|
+
@click.option("--limit", default=5, help="Number of recent sessions to show")
|
|
306
|
+
def jules_sessions(limit):
|
|
307
|
+
"""List recent Jules sessions and their status."""
|
|
308
|
+
console = Console()
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
client = JulesAPIClient()
|
|
312
|
+
|
|
313
|
+
# For now, we'll need to track sessions locally or via API
|
|
314
|
+
# This is a simplified version that shows cached sessions
|
|
315
|
+
if not client._session_cache:
|
|
316
|
+
console.print("[yellow]No cached sessions found.[/yellow]")
|
|
317
|
+
console.print("Create a session with: onecoder delegate \"your task\"")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
table = Table(title="Recent Jules Sessions")
|
|
321
|
+
table.add_column("Session ID", style="cyan")
|
|
322
|
+
table.add_column("Title", style="white")
|
|
323
|
+
table.add_column("State", style="green")
|
|
324
|
+
table.add_column("PR URL", style="blue")
|
|
325
|
+
|
|
326
|
+
for session_id, session in list(client._session_cache.items())[:limit]:
|
|
327
|
+
pr_output = client.detect_pr_output(session_id)
|
|
328
|
+
pr_url = pr_output["url"] if pr_output else "-"
|
|
329
|
+
|
|
330
|
+
table.add_row(
|
|
331
|
+
session_id,
|
|
332
|
+
session.title[:50] if session.title else "N/A",
|
|
333
|
+
session.state,
|
|
334
|
+
pr_url
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
console.print(table)
|
|
338
|
+
|
|
339
|
+
except JulesAuthError:
|
|
340
|
+
console.print("[bold red]Error:[/bold red] JULES_API_KEY not set", style="red")
|
|
341
|
+
except JulesAPIError as e:
|
|
342
|
+
console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red")
|
|
343
|
+
except Exception as e:
|
|
344
|
+
console.print(f"[bold red]Unexpected error:[/bold red] {str(e)}", style="red")
|
|
345
|
+
|
|
346
|
+
@click.command()
|
|
347
|
+
@click.argument("prompt")
|
|
348
|
+
@click.option("--local", is_flag=True, help="Run as a local isolated delegation session (using Gemini CLI in a worktree/tmux)")
|
|
349
|
+
@click.option("--source", help="GitHub source (e.g., sources/github/owner/repo)")
|
|
350
|
+
@click.option("--branch", default="main", help="Starting branch")
|
|
351
|
+
@click.option("--watch", is_flag=True, help="Monitor progress in real-time (enforces timeout)")
|
|
352
|
+
@click.option("--timeout", default=120, help="Timeout in seconds (default: 120s). Only enforced with --watch.")
|
|
353
|
+
@click.option("--poll-interval", default=10, help="Polling interval in seconds (default: 10s)")
|
|
354
|
+
def delegate(prompt, local, source, branch, watch, timeout, poll_interval):
|
|
355
|
+
"""Delegate a coding task to Google Jules or a local agent."""
|
|
356
|
+
console = Console()
|
|
357
|
+
|
|
358
|
+
if local:
|
|
359
|
+
async def run_local():
|
|
360
|
+
service = DelegationService()
|
|
361
|
+
|
|
362
|
+
# --- Traceable Task Registration ---
|
|
363
|
+
repo_root = Path.cwd()
|
|
364
|
+
task_id = None
|
|
365
|
+
|
|
366
|
+
with console.status("[bold green]Registering task in sprint..."):
|
|
367
|
+
sprint_id, registered_task_id = service.register_task_in_sprint(prompt, repo_root)
|
|
368
|
+
|
|
369
|
+
if registered_task_id:
|
|
370
|
+
task_id = registered_task_id
|
|
371
|
+
console.print(f"[green]✓ Registered in Sprint {sprint_id}:[/green] [bold]{task_id}[/bold]")
|
|
372
|
+
else:
|
|
373
|
+
# Fallback to ad-hoc ID
|
|
374
|
+
safe_prompt = "".join(c if c.isalnum() else "_" for c in prompt)[:30]
|
|
375
|
+
task_id = f"local-{safe_prompt}"
|
|
376
|
+
console.print(f"[yellow]Warning: Could not register in sprint. Using ad-hoc ID:[/yellow] {task_id}")
|
|
377
|
+
|
|
378
|
+
session = service.create_session(task_id=task_id, command=prompt)
|
|
379
|
+
|
|
380
|
+
with console.status("[bold green]Spawning local isolated session..."):
|
|
381
|
+
connection_info = await service.start_session(session)
|
|
382
|
+
|
|
383
|
+
console.print(f"[bold green]✓[/bold green] Task delegated to local worktree: [cyan]{session.worktree_path}[/cyan]")
|
|
384
|
+
console.print(f"To attach, run: [bold]{connection_info}[/bold]")
|
|
385
|
+
|
|
386
|
+
if not watch:
|
|
387
|
+
console.print(f"\n[dim]To monitor this session later, use: onecoder delegate-list[/dim]")
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
# --- Watch Logic with Timeout ---
|
|
391
|
+
import time
|
|
392
|
+
from datetime import datetime, timedelta
|
|
393
|
+
|
|
394
|
+
start_time = datetime.now()
|
|
395
|
+
max_duration = timedelta(seconds=timeout)
|
|
396
|
+
|
|
397
|
+
console.print(f"\n[bold yellow]Watching session (Timeout: {timeout}s)...[/bold yellow]")
|
|
398
|
+
console.print("Press Ctrl+C to stop watching (session will continue unless timeout reached)\n")
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
with console.status("Monitoring session...") as status:
|
|
402
|
+
while True:
|
|
403
|
+
# 1. Check Timeout
|
|
404
|
+
elapsed = datetime.now() - start_time
|
|
405
|
+
if elapsed > max_duration:
|
|
406
|
+
console.print(f"\n[bold yellow]Timeout reached ({timeout}s). Stopping watch loop...[/bold yellow]")
|
|
407
|
+
console.print("[yellow]Session will continue in background. Use 'delegate-finish' to merge work.[/yellow]")
|
|
408
|
+
service.stop_session(session.id, cleanup=False)
|
|
409
|
+
break
|
|
410
|
+
|
|
411
|
+
# 2. Check Status (Refresh from blackboard)
|
|
412
|
+
current_session = service.get_session(session.id)
|
|
413
|
+
if not current_session:
|
|
414
|
+
console.print("[red]Session lost.[/red]")
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
# Update UX
|
|
418
|
+
status.update(f"Monitoring... Age: {str(elapsed).split('.')[0]} Status: {current_session.status}")
|
|
419
|
+
|
|
420
|
+
if current_session.status in ["completed", "failed", "stopped"]:
|
|
421
|
+
console.print(f"\nSession finished with status: [bold]{current_session.status}[/bold]")
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# 3. Wait
|
|
425
|
+
await asyncio.sleep(poll_interval)
|
|
426
|
+
|
|
427
|
+
except KeyboardInterrupt:
|
|
428
|
+
console.print("\n[yellow]Stopped watching. Session continues in background.[/yellow]")
|
|
429
|
+
console.print(f"To list sessions: onecoder delegate-list")
|
|
430
|
+
|
|
431
|
+
asyncio.run(run_local())
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
# --- Remote Jules Logic ---
|
|
435
|
+
service = DelegationService()
|
|
436
|
+
repo_root = Path.cwd()
|
|
437
|
+
task_id = "jules-task"
|
|
438
|
+
sprint_id = None
|
|
439
|
+
|
|
440
|
+
with console.status("[bold green]Registering task in sprint (Governance)..."):
|
|
441
|
+
sprint_id, registered_task_id = service.register_task_in_sprint(prompt, repo_root)
|
|
442
|
+
|
|
443
|
+
if registered_task_id:
|
|
444
|
+
task_id = registered_task_id
|
|
445
|
+
console.print(f"[green]✓ Registered in Sprint {sprint_id}:[/green] [bold]{task_id}[/bold]")
|
|
446
|
+
else:
|
|
447
|
+
# Fallback
|
|
448
|
+
console.print(f"[yellow]Warning: Could not register in sprint.[/yellow]")
|
|
449
|
+
|
|
450
|
+
# 2. Governance: Enforce Push
|
|
451
|
+
try:
|
|
452
|
+
current_branch = subprocess.check_output(["git", "branch", "--show-current"], text=True).strip()
|
|
453
|
+
if branch == "main" or branch == current_branch:
|
|
454
|
+
subprocess.check_call(["git", "remote", "update"], stderr=subprocess.DEVNULL)
|
|
455
|
+
status = subprocess.check_output(["git", "status", "-uno"], text=True)
|
|
456
|
+
if "Your branch is ahead of" in status:
|
|
457
|
+
console.print("[bold red]Governance Block:[/bold red] You have unpushed commits.")
|
|
458
|
+
console.print("Jules cannot see your local changes. Please push before delegating.")
|
|
459
|
+
if not click.confirm("Ignore and proceed (Risk of regression)?"):
|
|
460
|
+
return
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
# 3. Governance: Context Injection
|
|
465
|
+
governance_context = f"\n\n[GOVERNANCE META]\nTask-Id: {task_id}\n"
|
|
466
|
+
if sprint_id:
|
|
467
|
+
governance_context += f"Sprint-Id: {sprint_id}\n"
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
from ..knowledge import ProjectKnowledge
|
|
471
|
+
import re
|
|
472
|
+
pk = ProjectKnowledge()
|
|
473
|
+
knowledge = pk.get_l1_context()
|
|
474
|
+
if knowledge:
|
|
475
|
+
goal = knowledge.get("goal", "")
|
|
476
|
+
active_task = knowledge.get("active_task", {})
|
|
477
|
+
spec_match = re.search(r"(SPEC-[A-Z0-9.-]+)", goal + " " + active_task.get("title", ""))
|
|
478
|
+
if spec_match:
|
|
479
|
+
spec_id = spec_match.group(1)
|
|
480
|
+
governance_context += f"Spec-Id: {spec_id}\n"
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
enhanced_prompt = prompt + governance_context
|
|
485
|
+
console.print(f"[dim]Injected governance context into prompt for {task_id}[/dim]")
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
client = JulesAPIClient()
|
|
489
|
+
|
|
490
|
+
with console.status("[bold green]Creating Jules session..."):
|
|
491
|
+
jules_session = client.create_session(enhanced_prompt, source=source, branch=branch)
|
|
492
|
+
|
|
493
|
+
console.print(f"[bold green]✓[/bold green] Session created: [cyan]{jules_session.id}[/cyan]")
|
|
494
|
+
console.print(f"Task: {prompt}")
|
|
495
|
+
console.print(f"Source: {source or os.environ.get('JULES_SOURCE', 'N/A')}")
|
|
496
|
+
console.print(f"Monitor: https://jules.google.com/sessions/{jules_session.id}\n")
|
|
497
|
+
|
|
498
|
+
# 4. Governance: Persist Local Session Record
|
|
499
|
+
local_session = service.create_session(
|
|
500
|
+
task_id=task_id,
|
|
501
|
+
backend="jules",
|
|
502
|
+
command=prompt
|
|
503
|
+
)
|
|
504
|
+
local_session.external_id = jules_session.id
|
|
505
|
+
local_session.sprint_id = sprint_id
|
|
506
|
+
service._save_session(local_session)
|
|
507
|
+
console.print(f"[dim]Persisted governance record for session {local_session.id}[/dim]")
|
|
508
|
+
|
|
509
|
+
# Watch mode
|
|
510
|
+
if watch:
|
|
511
|
+
console.print("\n[bold yellow]Watching session progress...[/bold yellow]")
|
|
512
|
+
console.print("Press Ctrl+C to stop watching\n")
|
|
513
|
+
|
|
514
|
+
def progress_callback(session, activities):
|
|
515
|
+
"""Callback for progress updates."""
|
|
516
|
+
table = Table(title=f"Session {session.id}")
|
|
517
|
+
table.add_column("Time", style="cyan")
|
|
518
|
+
table.add_column("Activity", style="white")
|
|
519
|
+
|
|
520
|
+
for activity in activities[:5]:
|
|
521
|
+
if activity.activity_type == "plan_generated":
|
|
522
|
+
table.add_row("Plan", "Plan generated")
|
|
523
|
+
elif activity.activity_type == "progress_updated":
|
|
524
|
+
progress = activity.data.get("progressUpdated", {})
|
|
525
|
+
table.add_row("Progress", progress.get("title", "Update"))
|
|
526
|
+
elif activity.activity_type == "session_completed":
|
|
527
|
+
table.add_row("Complete", "✓ Session completed")
|
|
528
|
+
|
|
529
|
+
console.print(table)
|
|
530
|
+
|
|
531
|
+
# Check for PR
|
|
532
|
+
pr_output = client.detect_pr_output(session.id)
|
|
533
|
+
if pr_output:
|
|
534
|
+
console.print(Panel(
|
|
535
|
+
f"[bold green]PR Created![/bold green]\n"
|
|
536
|
+
f"URL: {pr_output['url']}\n"
|
|
537
|
+
f"Title: {pr_output['title']}",
|
|
538
|
+
title="Pull Request"
|
|
539
|
+
))
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
final_session = client.poll_until_complete(
|
|
543
|
+
session.id,
|
|
544
|
+
callback=progress_callback,
|
|
545
|
+
max_iterations=60
|
|
546
|
+
)
|
|
547
|
+
console.print(f"\n[bold]Final state:[/bold] {final_session.state}")
|
|
548
|
+
except KeyboardInterrupt:
|
|
549
|
+
console.print("\n[yellow]Stopped watching. Session continues in background.[/yellow]")
|
|
550
|
+
|
|
551
|
+
except JulesAuthError:
|
|
552
|
+
console.print("[bold red]Error:[/bold red] JULES_API_KEY not set", style="red")
|
|
553
|
+
console.print("Set your API key: export JULES_API_KEY=your_key")
|
|
554
|
+
except JulesAPIError as e:
|
|
555
|
+
console.print(f"[bold red]Error:[/bold red] {str(e)}", style="red")
|
|
556
|
+
except Exception as e:
|
|
557
|
+
console.print(f"[bold red]Unexpected error:[/bold red] {str(e)}", style="red")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from onecoder.diagnostics.env_scan import EnvDoctor, EnvFinding
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def doctor():
|
|
12
|
+
"""Diagnostic tools for environment, ports, and gateways."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@doctor.command(name="env")
|
|
17
|
+
@click.option("--json", "json_output", is_flag=True, help="Print JSON output.")
|
|
18
|
+
def doctor_env(json_output: bool):
|
|
19
|
+
"""Scan env files across components and flag mismatches."""
|
|
20
|
+
runner = EnvDoctor()
|
|
21
|
+
findings = runner.run()
|
|
22
|
+
artifact_path = runner.write_artifact(findings)
|
|
23
|
+
|
|
24
|
+
if json_output:
|
|
25
|
+
click.echo(EnvDoctor.to_json(findings))
|
|
26
|
+
else:
|
|
27
|
+
_print_human(findings, artifact_path)
|
|
28
|
+
|
|
29
|
+
if EnvDoctor.has_failures(findings):
|
|
30
|
+
raise click.exceptions.Exit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _print_human(findings: list[EnvFinding], artifact_path: Path):
|
|
34
|
+
click.echo("Environment diagnostics")
|
|
35
|
+
for finding in findings:
|
|
36
|
+
prefix = finding.status.upper()
|
|
37
|
+
location = f" ({finding.file})" if finding.file else ""
|
|
38
|
+
tt_hint = f" [see {finding.tt_id}]" if finding.tt_id else ""
|
|
39
|
+
click.echo(f"- [{prefix}] {finding.component}::{finding.check}{location} - {finding.message}{tt_hint}")
|
|
40
|
+
click.echo(f"\nSaved report to {artifact_path}")
|