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.
Files changed (73) hide show
  1. onecoder/agent.py +95 -0
  2. onecoder/agentic_tool_search/__init__.py +0 -0
  3. onecoder/agentic_tool_search/dynamic_tool_search.py +64 -0
  4. onecoder/agentic_tool_search/registry.py +33 -0
  5. onecoder/agents/__init__.py +7 -0
  6. onecoder/agents/documentation_agent.py +12 -0
  7. onecoder/agents/file_reader_agent.py +19 -0
  8. onecoder/agents/file_writer_agent.py +19 -0
  9. onecoder/agents/orchestrator_agent.py +51 -0
  10. onecoder/agents/refactoring_agent.py +12 -0
  11. onecoder/agents/research_agent.py +31 -0
  12. onecoder/agents/task_suggestion_agent.py +88 -0
  13. onecoder/alignment.py +236 -0
  14. onecoder/api.py +162 -0
  15. onecoder/api_client.py +112 -0
  16. onecoder/backends/base.py +22 -0
  17. onecoder/backends/local_tui.py +65 -0
  18. onecoder/blackboard.py +102 -0
  19. onecoder/cli.py +108 -0
  20. onecoder/commands/__init__.py +1 -0
  21. onecoder/commands/auth.py +78 -0
  22. onecoder/commands/ci.py +29 -0
  23. onecoder/commands/delegate.py +557 -0
  24. onecoder/commands/doctor.py +40 -0
  25. onecoder/commands/issue.py +136 -0
  26. onecoder/commands/logs.py +45 -0
  27. onecoder/commands/project.py +270 -0
  28. onecoder/commands/server.py +170 -0
  29. onecoder/config_manager.py +87 -0
  30. onecoder/constants.py +9 -0
  31. onecoder/diagnostics/__init__.py +2 -0
  32. onecoder/diagnostics/env_scan.py +207 -0
  33. onecoder/discovery.py +101 -0
  34. onecoder/distillation.py +236 -0
  35. onecoder/evaluation/__init__.py +1 -0
  36. onecoder/evaluation/ttu.py +176 -0
  37. onecoder/governance/__init__.py +0 -0
  38. onecoder/governance/probllm.py +91 -0
  39. onecoder/hooks.py +74 -0
  40. onecoder/ipc_auth.py +200 -0
  41. onecoder/issues.py +188 -0
  42. onecoder/jules_client.py +343 -0
  43. onecoder/knowledge.py +106 -0
  44. onecoder/llm.py +61 -0
  45. onecoder/logger.py +42 -0
  46. onecoder/metrics.py +129 -0
  47. onecoder/models/delegation.py +46 -0
  48. onecoder/onboarding.py +264 -0
  49. onecoder/review.py +233 -0
  50. onecoder/services/delegation_service.py +209 -0
  51. onecoder/services/validation_service.py +104 -0
  52. onecoder/sessions.py +186 -0
  53. onecoder/sprint_collector.py +165 -0
  54. onecoder/sync.py +167 -0
  55. onecoder/tmux.py +86 -0
  56. onecoder/tools/__init__.py +10 -0
  57. onecoder/tools/executor.py +53 -0
  58. onecoder/tools/external_tools.py +106 -0
  59. onecoder/tools/file_tools.py +77 -0
  60. onecoder/tools/interface.py +25 -0
  61. onecoder/tools/jules_tools.py +122 -0
  62. onecoder/tools/kit_tools.py +122 -0
  63. onecoder/tools/registry.py +32 -0
  64. onecoder/tui/__init__.py +5 -0
  65. onecoder/tui/app.py +263 -0
  66. onecoder/tui/commands.py +150 -0
  67. onecoder/tui/widgets.py +92 -0
  68. onecoder/worktree.py +186 -0
  69. onecoder-0.0.2.dist-info/METADATA +17 -0
  70. onecoder-0.0.2.dist-info/RECORD +73 -0
  71. onecoder-0.0.2.dist-info/WHEEL +5 -0
  72. onecoder-0.0.2.dist-info/entry_points.txt +2 -0
  73. 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}")