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,136 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from ..issues import IssueManager
|
|
6
|
+
from ..config_manager import config_manager
|
|
7
|
+
from ai_sprint.telemetry import FailureModeCapture
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def issue():
|
|
11
|
+
"""Manage platform governance issues."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@issue.command(name="create")
|
|
15
|
+
@click.option("--from-telemetry", is_flag=True, help="Create issue from latest telemetry failure")
|
|
16
|
+
@click.option("--title", help="Manual title for the issue")
|
|
17
|
+
def issue_create(from_telemetry, title):
|
|
18
|
+
"""Create a new governance issue."""
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
manager = IssueManager()
|
|
23
|
+
|
|
24
|
+
if from_telemetry:
|
|
25
|
+
capture = FailureModeCapture()
|
|
26
|
+
failures = capture.get_failures(limit=1)
|
|
27
|
+
if not failures:
|
|
28
|
+
console.print("[yellow]No recent failures found in telemetry.[/yellow]")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
failure = failures[0]
|
|
32
|
+
issue_path = manager.create_from_telemetry(failure, title=title)
|
|
33
|
+
console.print(f"[bold green]✓ Issue created from telemetry:[/bold green] {issue_path.name}")
|
|
34
|
+
else:
|
|
35
|
+
# Future: manual issue creation wizard
|
|
36
|
+
console.print("[yellow]Manual issue creation not yet implemented. Use --from-telemetry.[/yellow]")
|
|
37
|
+
|
|
38
|
+
@issue.command(name="resolve")
|
|
39
|
+
@click.argument("issue_id")
|
|
40
|
+
@click.option("--message", "-m", help="Resolution message")
|
|
41
|
+
@click.option("--pr", help="Pull Request URL")
|
|
42
|
+
def issue_resolve(issue_id, message, pr):
|
|
43
|
+
"""Mark an issue as resolved."""
|
|
44
|
+
from rich.console import Console
|
|
45
|
+
import subprocess
|
|
46
|
+
|
|
47
|
+
console = Console()
|
|
48
|
+
manager = IssueManager()
|
|
49
|
+
|
|
50
|
+
# 1. Get Resolution Metadata
|
|
51
|
+
resolution_meta = {
|
|
52
|
+
"user": config_manager.get_user(),
|
|
53
|
+
"resolved_at": datetime.now().isoformat(),
|
|
54
|
+
"pr_url": pr
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Get Current Commit
|
|
58
|
+
try:
|
|
59
|
+
commit = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
|
|
60
|
+
resolution_meta["commit_sha"] = commit
|
|
61
|
+
except Exception:
|
|
62
|
+
resolution_meta["commit_sha"] = "unknown"
|
|
63
|
+
|
|
64
|
+
# 2. Update Status
|
|
65
|
+
success = manager.update_status(issue_id, "resolved", resolution_meta)
|
|
66
|
+
|
|
67
|
+
if success:
|
|
68
|
+
console.print(f"[bold green]✓ Issue {issue_id} marked as resolved.[/bold green]")
|
|
69
|
+
console.print(msg="Resolution metadata captured.", style="dim")
|
|
70
|
+
else:
|
|
71
|
+
console.print(f"[bold red]Error:[/bold red] Issue {issue_id} not found.")
|
|
72
|
+
|
|
73
|
+
@issue.command(name="sync")
|
|
74
|
+
@click.option("--input", "-i", type=click.Path(exists=True), help="Input issue file/directory (optional)")
|
|
75
|
+
def issue_sync(input):
|
|
76
|
+
"""Sync local governance issues to the OneCoder API."""
|
|
77
|
+
import asyncio
|
|
78
|
+
from ..sync import sync_issues, ProjectConfig
|
|
79
|
+
from ..api_client import get_api_client
|
|
80
|
+
|
|
81
|
+
# Setup
|
|
82
|
+
token = config_manager.get_token()
|
|
83
|
+
if not token:
|
|
84
|
+
click.secho("Error: Not logged in.", fg="red")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
repo_root = Path.cwd() # Should use find_repo_root from sync.py really, importing locally
|
|
88
|
+
proj_config = ProjectConfig(repo_root)
|
|
89
|
+
project_id = proj_config.get_project_id()
|
|
90
|
+
|
|
91
|
+
if not project_id:
|
|
92
|
+
click.secho("Error: Project ID not found.", fg="red")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
client = get_api_client(token)
|
|
96
|
+
|
|
97
|
+
# Run Sync
|
|
98
|
+
asyncio.run(sync_issues(client, project_id))
|
|
99
|
+
|
|
100
|
+
@issue.command(name="list")
|
|
101
|
+
def issue_list():
|
|
102
|
+
"""List all local governance issues."""
|
|
103
|
+
from rich.console import Console
|
|
104
|
+
from rich.table import Table
|
|
105
|
+
console = Console()
|
|
106
|
+
|
|
107
|
+
manager = IssueManager()
|
|
108
|
+
issues_dir = manager.issues_dir
|
|
109
|
+
if not issues_dir.exists():
|
|
110
|
+
console.print("[yellow]No .issues directory found.[/yellow]")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
table = Table(title="Local Governance Issues")
|
|
114
|
+
table.add_column("ID", style="cyan")
|
|
115
|
+
table.add_column("Title", style="white")
|
|
116
|
+
table.add_column("Status", style="magenta")
|
|
117
|
+
|
|
118
|
+
for item in sorted(issues_dir.iterdir()):
|
|
119
|
+
if item.is_file() and item.suffix == ".md" and item.name != "README.md":
|
|
120
|
+
match = re.match(r"^(\d{3})-(.+)\.md$", item.name)
|
|
121
|
+
if match:
|
|
122
|
+
issue_id = match.group(1)
|
|
123
|
+
issue_title = match.group(2).replace("-", " ").capitalize()
|
|
124
|
+
|
|
125
|
+
# Try to parse status from file
|
|
126
|
+
content = item.read_text()
|
|
127
|
+
if any(x in content for x in ["Resolved", "🟢 Resolved", "🟢 **Resolved**"]):
|
|
128
|
+
status = "[green]Resolved[/green]"
|
|
129
|
+
elif any(x in content for x in ["Open", "🔴 Open", "🔴 **Open**"]):
|
|
130
|
+
status = "[red]Open[/red]"
|
|
131
|
+
else:
|
|
132
|
+
status = "[yellow]Unknown[/yellow]"
|
|
133
|
+
|
|
134
|
+
table.add_row(issue_id, issue_title, status)
|
|
135
|
+
|
|
136
|
+
console.print(table)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
@click.command()
|
|
6
|
+
@click.option("--n", default=50, help="Number of lines to show (default 50).")
|
|
7
|
+
@click.option("--follow", "-f", is_flag=True, help="Follow log output that updates.")
|
|
8
|
+
def logs(n, follow):
|
|
9
|
+
"""View recent OneCoder logs."""
|
|
10
|
+
log_file = Path.home() / ".onecoder" / "logs" / "onecoder.log"
|
|
11
|
+
|
|
12
|
+
if not log_file.exists():
|
|
13
|
+
click.echo(f"No log file found at {log_file}")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
click.secho(f"Reading logs from {log_file}...", dim=True)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from collections import deque
|
|
20
|
+
with open(log_file, 'r') as f:
|
|
21
|
+
if follow:
|
|
22
|
+
# Print last n lines first
|
|
23
|
+
lines = deque(f, maxlen=n)
|
|
24
|
+
for line in lines:
|
|
25
|
+
sys.stdout.write(line)
|
|
26
|
+
|
|
27
|
+
# Follow loop
|
|
28
|
+
import time
|
|
29
|
+
while True:
|
|
30
|
+
line = f.readline()
|
|
31
|
+
if line:
|
|
32
|
+
sys.stdout.write(line)
|
|
33
|
+
else:
|
|
34
|
+
time.sleep(0.1)
|
|
35
|
+
else:
|
|
36
|
+
# Just print last n lines
|
|
37
|
+
lines = deque(f, maxlen=n)
|
|
38
|
+
for line in lines:
|
|
39
|
+
sys.stdout.write(line)
|
|
40
|
+
|
|
41
|
+
except KeyboardInterrupt:
|
|
42
|
+
pass
|
|
43
|
+
except Exception as e:
|
|
44
|
+
click.report_exception(e) # Keep traceback if needed
|
|
45
|
+
click.secho(f"Error reading logs: {e}", fg="red")
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import json
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from .auth import require_login
|
|
10
|
+
from ..onboarding import onboard_project
|
|
11
|
+
from ..knowledge import ProjectKnowledge
|
|
12
|
+
from ..distillation import SprintDistiller
|
|
13
|
+
from ..sync import sync_project_context
|
|
14
|
+
from ..alignment import AlignmentTracker
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.option("--directory", default=".", help="Project directory")
|
|
18
|
+
@require_login
|
|
19
|
+
def init(directory):
|
|
20
|
+
"""Onboards the current project into OneCoder."""
|
|
21
|
+
onboard_project(directory)
|
|
22
|
+
|
|
23
|
+
@click.command()
|
|
24
|
+
@click.option("--directory", default=".", help="Root directory to scan")
|
|
25
|
+
def status(directory):
|
|
26
|
+
"""Shows the platform-wide status by aggregating all sub-project sprints."""
|
|
27
|
+
console = Console()
|
|
28
|
+
root_path = Path(directory).resolve()
|
|
29
|
+
|
|
30
|
+
console.print(f"[bold cyan]Scanning for OneCoder sub-projects in:[/bold cyan] {root_path}")
|
|
31
|
+
|
|
32
|
+
# Discovery logic
|
|
33
|
+
sprint_dirs = []
|
|
34
|
+
for p in root_path.rglob(".sprint"):
|
|
35
|
+
if "node_modules" in p.parts or ".git" in p.parts:
|
|
36
|
+
continue
|
|
37
|
+
sprint_dirs.append(p)
|
|
38
|
+
|
|
39
|
+
if not sprint_dirs:
|
|
40
|
+
console.print("[yellow]No .sprint directories found.[/yellow]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
table = Table(title="Platform-Wide Sprint Status")
|
|
44
|
+
table.add_column("Component", style="cyan")
|
|
45
|
+
table.add_column("Current Sprint", style="magenta")
|
|
46
|
+
table.add_column("Status", style="green")
|
|
47
|
+
table.add_column("Primary Goal", style="white")
|
|
48
|
+
table.add_column("Tasks (D/T)", justify="right")
|
|
49
|
+
|
|
50
|
+
for sd in sprint_dirs:
|
|
51
|
+
component_name = sd.parent.name
|
|
52
|
+
# Look for active sprint (usually the most recent subdirectory that isn't empty)
|
|
53
|
+
sprint_shards = [d for d in sd.iterdir() if d.is_dir()]
|
|
54
|
+
if not sprint_shards:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
# For now, let's take the latest/active one.
|
|
58
|
+
# In a real scenario, we might want to check sprint.json for status.
|
|
59
|
+
active_sprint = sorted(sprint_shards)[-1]
|
|
60
|
+
state_file = active_sprint / "sprint.json"
|
|
61
|
+
|
|
62
|
+
if state_file.exists():
|
|
63
|
+
try:
|
|
64
|
+
with open(state_file, "r") as f:
|
|
65
|
+
state = json.load(f)
|
|
66
|
+
|
|
67
|
+
sprint_id = state.get("sprintId", active_sprint.name)
|
|
68
|
+
status_obj = state.get("status", {})
|
|
69
|
+
status_str = f"{status_obj.get('state', 'unknown')} ({status_obj.get('phase', 'init')})"
|
|
70
|
+
goal = state.get("goals", {}).get("primary", "N/A")
|
|
71
|
+
|
|
72
|
+
tasks = state.get("tasks", [])
|
|
73
|
+
done_tasks = len([t for t in tasks if t.get("status") == "done"])
|
|
74
|
+
total_tasks = len(tasks)
|
|
75
|
+
|
|
76
|
+
table.add_row(
|
|
77
|
+
component_name,
|
|
78
|
+
sprint_id,
|
|
79
|
+
status_str,
|
|
80
|
+
goal or "N/A",
|
|
81
|
+
f"{done_tasks}/{total_tasks}"
|
|
82
|
+
)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
table.add_row(component_name, active_sprint.name, "Error", str(e), "-")
|
|
85
|
+
else:
|
|
86
|
+
table.add_row(component_name, active_sprint.name, "No sprint.json", "-", "-")
|
|
87
|
+
|
|
88
|
+
console.print(table)
|
|
89
|
+
|
|
90
|
+
@click.command(name="suggest")
|
|
91
|
+
@click.option("--limit", default=3, help="Number of past sprints to analyze")
|
|
92
|
+
@click.option("--dry-run", is_flag=True, help="Don't call LLM, just show context")
|
|
93
|
+
def sprint_suggest(limit, dry_run):
|
|
94
|
+
"""
|
|
95
|
+
Suggests next tasks based on sprint history and learnings.
|
|
96
|
+
"""
|
|
97
|
+
from ..sprint_collector import SprintCollector
|
|
98
|
+
from ..agents import TaskSuggester
|
|
99
|
+
|
|
100
|
+
console = Console()
|
|
101
|
+
repo_root = Path.cwd()
|
|
102
|
+
|
|
103
|
+
# 1. Collect Context
|
|
104
|
+
with console.status("[bold green]Harvesting sprint context..."):
|
|
105
|
+
collector = SprintCollector(repo_root)
|
|
106
|
+
context = collector.get_recent_context(limit=limit)
|
|
107
|
+
|
|
108
|
+
if not context:
|
|
109
|
+
console.print("[yellow]No sprint context found. Run this from a repository with .sprint history.[/yellow]")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
if dry_run:
|
|
113
|
+
console.print(Panel(json.dumps(context, indent=2), title="Captured Context (Dry Run)"))
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# 2. Get Suggestions from LLM
|
|
117
|
+
with console.status("[bold purple]Analyzing patterns and generating suggestions..."):
|
|
118
|
+
suggester = TaskSuggester()
|
|
119
|
+
suggestions = suggester.suggest_next_tasks(context)
|
|
120
|
+
|
|
121
|
+
if not suggestions:
|
|
122
|
+
console.print("[yellow]No suggestions generated (or LLM unavailable).[/yellow]")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# 3. Present Suggestions
|
|
126
|
+
console.print("\n[bold]Suggested Next Tasks:[/bold]")
|
|
127
|
+
for i, task in enumerate(suggestions, 1):
|
|
128
|
+
type_style = {
|
|
129
|
+
"feature": "blue",
|
|
130
|
+
"fix": "red",
|
|
131
|
+
"chore": "dim",
|
|
132
|
+
"governance": "magenta"
|
|
133
|
+
}.get(task.get("type", "task"), "white")
|
|
134
|
+
|
|
135
|
+
console.print(f"\n{i}. [{type_style}]{task['title']}[/{type_style}]")
|
|
136
|
+
console.print(f" [dim]{task['rationale']}[/dim]")
|
|
137
|
+
|
|
138
|
+
# 4. Interactive Delegation (Optional)
|
|
139
|
+
if click.confirm("\nDelegate one of these tasks now?", default=False):
|
|
140
|
+
choice = click.prompt("Enter task number", type=int, default=1)
|
|
141
|
+
if 1 <= choice <= len(suggestions):
|
|
142
|
+
selected = suggestions[choice-1]
|
|
143
|
+
prompt = selected['title']
|
|
144
|
+
click.echo(f"\nrun: onecoder delegate \"{prompt}\"")
|
|
145
|
+
|
|
146
|
+
@click.command()
|
|
147
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON for machine consumption")
|
|
148
|
+
def knowledge(json_output):
|
|
149
|
+
"""
|
|
150
|
+
Shows the platform-wide knowledge awareness.
|
|
151
|
+
Aggregates L2 (Durable Project Guidelines) and L1 (Active Sprint Context).
|
|
152
|
+
"""
|
|
153
|
+
pk = ProjectKnowledge()
|
|
154
|
+
|
|
155
|
+
if json_output:
|
|
156
|
+
click.echo(json.dumps(pk.aggregate_knowledge(), indent=2))
|
|
157
|
+
else:
|
|
158
|
+
click.echo(pk.get_rag_ready_output())
|
|
159
|
+
|
|
160
|
+
@click.command()
|
|
161
|
+
@click.option("--sprint", "sprint_id", required=True, help="Sprint ID to distill learnings from")
|
|
162
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON for machine consumption")
|
|
163
|
+
def distill(sprint_id, json_output):
|
|
164
|
+
"""
|
|
165
|
+
Distills learnings from a completed sprint and updates ANTIGRAVITY.md.
|
|
166
|
+
"""
|
|
167
|
+
console = Console()
|
|
168
|
+
distiller = SprintDistiller()
|
|
169
|
+
|
|
170
|
+
if not json_output:
|
|
171
|
+
console.print(f"[bold green]Distilling learnings from {sprint_id}...[/bold green]")
|
|
172
|
+
|
|
173
|
+
result = distiller.distill_sprint(sprint_id)
|
|
174
|
+
|
|
175
|
+
if "error" in result:
|
|
176
|
+
if json_output:
|
|
177
|
+
click.echo(json.dumps(result))
|
|
178
|
+
else:
|
|
179
|
+
console.print(f"[bold red]Error:[/bold red] {result['error']}")
|
|
180
|
+
else:
|
|
181
|
+
if json_output:
|
|
182
|
+
click.echo(json.dumps(result))
|
|
183
|
+
else:
|
|
184
|
+
console.print(f"[bold green]Success![/bold green] Extracted {result['learnings_extracted']} learnings from {sprint_id}.")
|
|
185
|
+
if result["updated"]:
|
|
186
|
+
console.print(f"Updated awareness file: [cyan]{result['updated']}[/cyan]")
|
|
187
|
+
|
|
188
|
+
@click.command()
|
|
189
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be synced without actually syncing")
|
|
190
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON for machine consumption")
|
|
191
|
+
def sync(dry_run, json_output):
|
|
192
|
+
"""
|
|
193
|
+
Syncs the local project context (specifications, governance, learnings, sprints) to the API.
|
|
194
|
+
"""
|
|
195
|
+
if json_output:
|
|
196
|
+
# We might want to pass this down to sync_project_context
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
asyncio.run(sync_project_context())
|
|
200
|
+
|
|
201
|
+
@click.command()
|
|
202
|
+
@click.option("--sync", is_flag=True, help="Sync alignment data to API")
|
|
203
|
+
def alignment(sync):
|
|
204
|
+
"""
|
|
205
|
+
Tracks project progress, aligns with roadmap, and captures PR/commit aspects.
|
|
206
|
+
"""
|
|
207
|
+
console = Console()
|
|
208
|
+
tracker = AlignmentTracker()
|
|
209
|
+
|
|
210
|
+
with console.status("[bold green]Analyzing alignment..."):
|
|
211
|
+
# Log current state
|
|
212
|
+
log_msg = tracker.log_current_state()
|
|
213
|
+
|
|
214
|
+
# Analyze roadmap (Agentic Semantic Check)
|
|
215
|
+
alignment_data = tracker.check_roadmap_alignment_agentic()
|
|
216
|
+
|
|
217
|
+
# Fetch PRs
|
|
218
|
+
prs = tracker.fetch_recent_prs()
|
|
219
|
+
|
|
220
|
+
# Capture suggestions
|
|
221
|
+
suggestions = tracker.capture_suggestions()
|
|
222
|
+
|
|
223
|
+
# Agentic Summary (Pass the detailed alignment data)
|
|
224
|
+
summary = tracker.summarize_alignment_agentic(alignment_data, prs, suggestions)
|
|
225
|
+
|
|
226
|
+
console.print(Panel(f"[bold]{log_msg}[/bold]", title="Alignment Log", expand=False))
|
|
227
|
+
|
|
228
|
+
# Agentic Summary Panel
|
|
229
|
+
console.print(Panel(summary, title="🤖 Agentic Assessment", border_style="cyan"))
|
|
230
|
+
|
|
231
|
+
# Roadmap Table
|
|
232
|
+
roadmap_table = Table(title="Semantic Roadmap Alignment")
|
|
233
|
+
roadmap_table.add_column("Status", style="bold")
|
|
234
|
+
roadmap_table.add_column("Aligned Items", style="cyan")
|
|
235
|
+
|
|
236
|
+
status_style = "green" if alignment_data["status"] == "ALIGNED" else ("yellow" if alignment_data["status"] == "DRIFTING" else "red")
|
|
237
|
+
|
|
238
|
+
roadmap_table.add_row(
|
|
239
|
+
f"[{status_style}]{alignment_data['status']}[/]",
|
|
240
|
+
", ".join(alignment_data["aligned_items"]) if alignment_data["aligned_items"] else "None"
|
|
241
|
+
)
|
|
242
|
+
console.print(roadmap_table)
|
|
243
|
+
|
|
244
|
+
# Drift Items
|
|
245
|
+
if alignment_data.get("drift_items"):
|
|
246
|
+
console.print("\n[bold yellow]Detected Drift (Work not in Roadmap):[/bold yellow]")
|
|
247
|
+
for item in alignment_data["drift_items"]:
|
|
248
|
+
console.print(f" • {item}")
|
|
249
|
+
|
|
250
|
+
# PRs Table
|
|
251
|
+
if prs:
|
|
252
|
+
pr_table = Table(title="Recent Merged PRs")
|
|
253
|
+
pr_table.add_column("PR #", style="dim")
|
|
254
|
+
pr_table.add_column("Title", style="white")
|
|
255
|
+
pr_table.add_column("Author", style="cyan")
|
|
256
|
+
|
|
257
|
+
for pr in prs:
|
|
258
|
+
pr_table.add_row(str(pr["number"]), pr["title"], pr["author"]["login"])
|
|
259
|
+
console.print(pr_table)
|
|
260
|
+
|
|
261
|
+
# Suggestions
|
|
262
|
+
if suggestions:
|
|
263
|
+
console.print("\n[bold]Suggestions for Next Sprint:[/bold]")
|
|
264
|
+
for s in suggestions[:5]:
|
|
265
|
+
console.print(f" • {s}")
|
|
266
|
+
|
|
267
|
+
if sync:
|
|
268
|
+
click.echo("\nSyncing alignment data to API...")
|
|
269
|
+
# TODO: Implement API sync once endpoints are ready
|
|
270
|
+
click.echo("Sync scheduled for next platform update.")
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import asyncio
|
|
3
|
+
import threading
|
|
4
|
+
import uvicorn
|
|
5
|
+
import webbrowser
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
import socket
|
|
9
|
+
from ..ipc_auth import IPCAuthServer, get_token_from_ipc
|
|
10
|
+
|
|
11
|
+
# Global flag for graceful shutdown
|
|
12
|
+
shutdown_event = asyncio.Event()
|
|
13
|
+
|
|
14
|
+
def check_port_available(port: int) -> bool:
|
|
15
|
+
"""Check if a port is available."""
|
|
16
|
+
try:
|
|
17
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
18
|
+
s.bind(("127.0.0.1", port))
|
|
19
|
+
return True
|
|
20
|
+
except OSError:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
async def check_servers_running() -> bool:
|
|
24
|
+
"""Check if OneCoder servers are running."""
|
|
25
|
+
try:
|
|
26
|
+
token = await get_token_from_ipc()
|
|
27
|
+
return token is not None
|
|
28
|
+
except:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def run_api_server(port: int = 8000):
|
|
32
|
+
"""Runs the FastAPI server."""
|
|
33
|
+
from ..api import app
|
|
34
|
+
uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
|
|
35
|
+
|
|
36
|
+
async def run_servers_async(port: int = 8000):
|
|
37
|
+
"""Run both API and IPC servers concurrently."""
|
|
38
|
+
# Start API server in a thread
|
|
39
|
+
api_thread = threading.Thread(target=run_api_server, args=(port,), daemon=True)
|
|
40
|
+
api_thread.start()
|
|
41
|
+
|
|
42
|
+
# Give API server time to start
|
|
43
|
+
await asyncio.sleep(1)
|
|
44
|
+
|
|
45
|
+
# Start IPC server in main async loop
|
|
46
|
+
ipc_server = IPCAuthServer()
|
|
47
|
+
try:
|
|
48
|
+
await ipc_server.start()
|
|
49
|
+
except asyncio.CancelledError:
|
|
50
|
+
click.echo("Shutting down gracefully...")
|
|
51
|
+
|
|
52
|
+
def signal_handler(signum, frame):
|
|
53
|
+
"""Handle shutdown signals."""
|
|
54
|
+
click.echo("\nReceived shutdown signal. Cleaning up...")
|
|
55
|
+
sys.exit(0)
|
|
56
|
+
|
|
57
|
+
@click.command()
|
|
58
|
+
@click.option("--port", default=8000, help="API server port")
|
|
59
|
+
def serve(port):
|
|
60
|
+
"""Starts the Agent API and IPC Auth servers."""
|
|
61
|
+
# Check if port is available
|
|
62
|
+
if not check_port_available(port):
|
|
63
|
+
click.echo(f"Error: Port {port} is already in use.")
|
|
64
|
+
click.echo(f"Check for running processes: lsof -i :{port}")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
click.echo(f"Starting OneCoder servers on port {port}...")
|
|
68
|
+
click.echo("Press Ctrl+C to stop.")
|
|
69
|
+
|
|
70
|
+
# Set up signal handlers
|
|
71
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
72
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
73
|
+
|
|
74
|
+
# Run servers
|
|
75
|
+
try:
|
|
76
|
+
asyncio.run(run_servers_async(port))
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
click.echo("\nShutting down...")
|
|
79
|
+
|
|
80
|
+
@click.command()
|
|
81
|
+
@click.option("--auto-start", is_flag=True, help="Auto-start servers if not running")
|
|
82
|
+
def web(auto_start):
|
|
83
|
+
"""Launches the secure Web UI."""
|
|
84
|
+
|
|
85
|
+
async def launch():
|
|
86
|
+
# Check if servers are running
|
|
87
|
+
servers_running = await check_servers_running()
|
|
88
|
+
|
|
89
|
+
if not servers_running:
|
|
90
|
+
if auto_start:
|
|
91
|
+
click.echo("Servers not running. Starting in background...")
|
|
92
|
+
# Start servers in background thread
|
|
93
|
+
server_thread = threading.Thread(
|
|
94
|
+
target=lambda: asyncio.run(run_servers_async()), daemon=True
|
|
95
|
+
)
|
|
96
|
+
server_thread.start()
|
|
97
|
+
# Wait for servers to be ready
|
|
98
|
+
await asyncio.sleep(2)
|
|
99
|
+
|
|
100
|
+
# Verify they started
|
|
101
|
+
if not await check_servers_running():
|
|
102
|
+
click.echo("Error: Failed to start servers automatically.")
|
|
103
|
+
return
|
|
104
|
+
else:
|
|
105
|
+
click.echo("Error: Servers not running.")
|
|
106
|
+
click.echo(
|
|
107
|
+
"Run 'onecoder serve' in another terminal or use --auto-start"
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Fetch token
|
|
112
|
+
token = await get_token_from_ipc()
|
|
113
|
+
if not token:
|
|
114
|
+
click.echo("Error: Could not fetch authentication token.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Launch browser
|
|
118
|
+
url = f"http://127.0.0.1:8000/?token={token}"
|
|
119
|
+
click.echo(f"Launching Web UI: {url}")
|
|
120
|
+
webbrowser.open(url)
|
|
121
|
+
|
|
122
|
+
if auto_start:
|
|
123
|
+
click.echo("\nServers running in background. Press Ctrl+C to stop.")
|
|
124
|
+
try:
|
|
125
|
+
# Keep running if we auto-started
|
|
126
|
+
await asyncio.Event().wait()
|
|
127
|
+
except KeyboardInterrupt:
|
|
128
|
+
click.echo("\nShutting down...")
|
|
129
|
+
|
|
130
|
+
asyncio.run(launch())
|
|
131
|
+
|
|
132
|
+
@click.command()
|
|
133
|
+
@click.option("--auto-start", is_flag=True, help="Auto-start servers if not running")
|
|
134
|
+
@click.option("--api-url", help="Override the API URL")
|
|
135
|
+
def tui(auto_start, api_url):
|
|
136
|
+
"""Launches the modern Textual TUI."""
|
|
137
|
+
|
|
138
|
+
async def launch():
|
|
139
|
+
# Check if servers are running
|
|
140
|
+
servers_running = await check_servers_running()
|
|
141
|
+
|
|
142
|
+
if not servers_running:
|
|
143
|
+
if auto_start:
|
|
144
|
+
click.echo("Servers not running. Starting in background...")
|
|
145
|
+
# Start servers in background thread
|
|
146
|
+
server_thread = threading.Thread(
|
|
147
|
+
target=lambda: asyncio.run(run_servers_async()), daemon=True
|
|
148
|
+
)
|
|
149
|
+
server_thread.start()
|
|
150
|
+
# Wait for servers to be ready
|
|
151
|
+
await asyncio.sleep(2)
|
|
152
|
+
|
|
153
|
+
# Verify they started
|
|
154
|
+
if not await check_servers_running():
|
|
155
|
+
click.echo("Error: Failed to start servers automatically.")
|
|
156
|
+
return
|
|
157
|
+
else:
|
|
158
|
+
click.echo("Error: Servers not running.")
|
|
159
|
+
click.echo(
|
|
160
|
+
"Run 'onecoder serve' in another terminal or use --auto-start"
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Launch Textual TUI
|
|
165
|
+
from ..tui.app import OneCoderApp
|
|
166
|
+
|
|
167
|
+
app = OneCoderApp(api_url=api_url)
|
|
168
|
+
await app.run_async()
|
|
169
|
+
|
|
170
|
+
asyncio.run(launch())
|