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/logger.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from logging.handlers import RotatingFileHandler
|
|
5
|
+
|
|
6
|
+
def configure_logging(verbose: bool = False):
|
|
7
|
+
"""Configures logging for the OneCoder CLI."""
|
|
8
|
+
|
|
9
|
+
# define logs directory
|
|
10
|
+
log_dir = Path.home() / ".onecoder" / "logs"
|
|
11
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
log_file = log_dir / "onecoder.log"
|
|
13
|
+
|
|
14
|
+
# Set base level
|
|
15
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
16
|
+
|
|
17
|
+
# Create root logger
|
|
18
|
+
root_logger = logging.getLogger()
|
|
19
|
+
root_logger.setLevel(level)
|
|
20
|
+
|
|
21
|
+
# Clear existing handlers
|
|
22
|
+
if root_logger.handlers:
|
|
23
|
+
root_logger.handlers.clear()
|
|
24
|
+
|
|
25
|
+
# File Handler (Always logs DEBUG)
|
|
26
|
+
file_handler = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5)
|
|
27
|
+
file_handler.setLevel(logging.DEBUG)
|
|
28
|
+
file_formatter = logging.Formatter(
|
|
29
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
30
|
+
)
|
|
31
|
+
file_handler.setFormatter(file_formatter)
|
|
32
|
+
root_logger.addHandler(file_handler)
|
|
33
|
+
|
|
34
|
+
# Console Handler (Logs INFO by default, DEBUG if verbose)
|
|
35
|
+
console_handler = logging.StreamHandler()
|
|
36
|
+
console_handler.setLevel(level)
|
|
37
|
+
console_formatter = logging.Formatter('%(message)s') # Simple format for console
|
|
38
|
+
console_handler.setFormatter(console_formatter)
|
|
39
|
+
root_logger.addHandler(console_handler)
|
|
40
|
+
|
|
41
|
+
logging.debug(f"Logging initialized. Verbose: {verbose}")
|
|
42
|
+
return log_file
|
onecoder/metrics.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
|
|
7
|
+
def find_repo_root() -> Path:
|
|
8
|
+
current = Path.cwd()
|
|
9
|
+
while current != current.parent:
|
|
10
|
+
if (current / ".git").exists():
|
|
11
|
+
return current
|
|
12
|
+
current = current.parent
|
|
13
|
+
return Path.cwd()
|
|
14
|
+
|
|
15
|
+
class TTUMetrics:
|
|
16
|
+
_instance = None
|
|
17
|
+
_first_tool_called = False
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.repo_root = find_repo_root()
|
|
21
|
+
self.sprint_dir = self.repo_root / ".sprint"
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def get_instance(cls):
|
|
25
|
+
if cls._instance is None:
|
|
26
|
+
cls._instance = cls()
|
|
27
|
+
return cls._instance
|
|
28
|
+
|
|
29
|
+
def track_first_tool_call(self):
|
|
30
|
+
"""Called by tools or agent to mark the first actual action."""
|
|
31
|
+
if self._first_tool_called:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
self._first_tool_called = True
|
|
35
|
+
self._record_ttu()
|
|
36
|
+
|
|
37
|
+
def _record_ttu(self):
|
|
38
|
+
"""Finds the active task and records TTU if not already set."""
|
|
39
|
+
if not self.sprint_dir.exists():
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
# 1. Find the active sprint
|
|
43
|
+
active_sprint_id = os.environ.get("ACTIVE_SPRINT_ID")
|
|
44
|
+
if not active_sprint_id:
|
|
45
|
+
# Try to find an active sprint directory
|
|
46
|
+
sprint_dirs = [d for d in self.sprint_dir.iterdir() if d.is_dir() and d.name[0].isdigit()]
|
|
47
|
+
if not sprint_dirs:
|
|
48
|
+
return
|
|
49
|
+
# Heuristic: the one with the highest number is likely active
|
|
50
|
+
active_sprint_dir = sorted(sprint_dirs)[-1]
|
|
51
|
+
else:
|
|
52
|
+
active_sprint_dir = self.sprint_dir / active_sprint_id
|
|
53
|
+
|
|
54
|
+
if not active_sprint_dir.exists():
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
sprint_json_path = active_sprint_dir / "sprint.json"
|
|
58
|
+
if not sprint_json_path.exists():
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with open(sprint_json_path, 'r') as f:
|
|
63
|
+
state = json.load(f)
|
|
64
|
+
|
|
65
|
+
# 2. Find in-progress task
|
|
66
|
+
tasks = state.get("tasks", [])
|
|
67
|
+
active_task = None
|
|
68
|
+
for task in tasks:
|
|
69
|
+
if task.get("status") == "in-progress":
|
|
70
|
+
active_task = task
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if not active_task:
|
|
74
|
+
# Fallback: check if any tasks were started but not completed
|
|
75
|
+
for task in tasks:
|
|
76
|
+
if task.get("startedAt") and not task.get("completedAt"):
|
|
77
|
+
active_task = task
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
if not active_task:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# 3. Check if TTU already recorded
|
|
84
|
+
if active_task.get("ttuSeconds") is not None:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# 4. Calculate TTU
|
|
88
|
+
started_at_str = active_task.get("startedAt")
|
|
89
|
+
if not started_at_str:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Handle ISO format (might have 'Z' or offset)
|
|
93
|
+
try:
|
|
94
|
+
started_at = datetime.fromisoformat(started_at_str.replace('Z', '+00:00'))
|
|
95
|
+
except ValueError:
|
|
96
|
+
# Fallback for older formats or varying ISO implementations
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
now = datetime.now().astimezone() # Ensure timezone awareness if started_at has it
|
|
100
|
+
|
|
101
|
+
# If started_at is naive, make now naive too
|
|
102
|
+
if started_at.tzinfo is None:
|
|
103
|
+
now = datetime.now()
|
|
104
|
+
else:
|
|
105
|
+
# started_at has tz, ensuring now has the same or comparable tz
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
diff = now - started_at
|
|
109
|
+
ttu_seconds = int(diff.total_seconds())
|
|
110
|
+
|
|
111
|
+
# Don't record negative TTU (clock skew or start after first call?)
|
|
112
|
+
if ttu_seconds < 0:
|
|
113
|
+
ttu_seconds = 0
|
|
114
|
+
|
|
115
|
+
# 5. Update state and save
|
|
116
|
+
active_task["ttuSeconds"] = ttu_seconds
|
|
117
|
+
|
|
118
|
+
with open(sprint_json_path, 'w') as f:
|
|
119
|
+
json.dump(state, f, indent=2)
|
|
120
|
+
|
|
121
|
+
# Passive logging
|
|
122
|
+
log_dir = active_sprint_dir / "logs"
|
|
123
|
+
log_dir.mkdir(exist_ok=True)
|
|
124
|
+
with open(log_dir / "metrics.log", "a") as log:
|
|
125
|
+
log.write(f"[{datetime.now().isoformat()}] TTU: {ttu_seconds}s for task {active_task.get('id')}\n")
|
|
126
|
+
|
|
127
|
+
except Exception:
|
|
128
|
+
# Passive tracking should never crash the main flow
|
|
129
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
class DelegationSession(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Represents a delegated task session.
|
|
9
|
+
"""
|
|
10
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
11
|
+
task_id: str
|
|
12
|
+
backend: str # local-tui, jules, browser, etc.
|
|
13
|
+
status: str = "pending" # pending, running, completed, failed
|
|
14
|
+
|
|
15
|
+
# Context
|
|
16
|
+
worktree_path: Optional[str] = None
|
|
17
|
+
tmux_session: Optional[str] = None
|
|
18
|
+
command: Optional[str] = None
|
|
19
|
+
parent_branch: Optional[str] = None
|
|
20
|
+
sprint_id: Optional[str] = None
|
|
21
|
+
spec_id: Optional[str] = None
|
|
22
|
+
external_id: Optional[str] = None # For remote sessions (e.g. Jules ID)
|
|
23
|
+
|
|
24
|
+
# Results
|
|
25
|
+
result: Optional[str] = None
|
|
26
|
+
error: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
# Metadata
|
|
29
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
30
|
+
updated_at: datetime = Field(default_factory=datetime.now)
|
|
31
|
+
|
|
32
|
+
def mark_running(self, tmux_session: Optional[str] = None, worktree_path: Optional[str] = None):
|
|
33
|
+
self.status = "running"
|
|
34
|
+
self.tmux_session = tmux_session
|
|
35
|
+
self.worktree_path = worktree_path
|
|
36
|
+
self.updated_at = datetime.now()
|
|
37
|
+
|
|
38
|
+
def mark_completed(self, result: str):
|
|
39
|
+
self.status = "completed"
|
|
40
|
+
self.result = result
|
|
41
|
+
self.updated_at = datetime.now()
|
|
42
|
+
|
|
43
|
+
def mark_failed(self, error: str):
|
|
44
|
+
self.status = "failed"
|
|
45
|
+
self.error = error
|
|
46
|
+
self.updated_at = datetime.now()
|
onecoder/onboarding.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import click
|
|
3
|
+
import json
|
|
4
|
+
import asyncio
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, Optional
|
|
7
|
+
from .tools.kit_tools import kit_index_tool, kit_file_tree_tool, kit_symbols_tool
|
|
8
|
+
from .api_client import get_api_client
|
|
9
|
+
from .config_manager import config_manager
|
|
10
|
+
from .sync import sync_project_context, ProjectConfig
|
|
11
|
+
|
|
12
|
+
async def _ensure_project_setup(client, directory: str) -> Optional[str]:
|
|
13
|
+
"""
|
|
14
|
+
Ensures a workspace is associated and a project is created on the server.
|
|
15
|
+
Returns the project_id.
|
|
16
|
+
"""
|
|
17
|
+
click.secho("\n[Project Setup] Verifying workspace and project association...", fg="cyan", bold=True)
|
|
18
|
+
|
|
19
|
+
# Load local config
|
|
20
|
+
p_config = ProjectConfig(Path(directory))
|
|
21
|
+
config_data = p_config.load()
|
|
22
|
+
workspace_id = config_data.get("workspace_id")
|
|
23
|
+
project_id = config_data.get("project_id")
|
|
24
|
+
|
|
25
|
+
# 1. Ensure Workspace
|
|
26
|
+
if not workspace_id:
|
|
27
|
+
workspaces = await client.get_workspaces()
|
|
28
|
+
if workspaces:
|
|
29
|
+
click.echo("Found available workspaces:")
|
|
30
|
+
for idx, ws in enumerate(workspaces, 1):
|
|
31
|
+
click.echo(f" {idx}. {ws['name']} ({ws['id']})")
|
|
32
|
+
|
|
33
|
+
choice = click.prompt("Select a workspace number or type 'new' to create one", default="1")
|
|
34
|
+
if choice.lower() == "new":
|
|
35
|
+
ws_name = click.prompt("Enter new workspace name")
|
|
36
|
+
ws_result = await client.create_workspace(ws_name)
|
|
37
|
+
workspace_id = ws_result["id"]
|
|
38
|
+
else:
|
|
39
|
+
workspace_id = workspaces[int(choice)-1]["id"]
|
|
40
|
+
else:
|
|
41
|
+
click.echo("No workspaces found.")
|
|
42
|
+
ws_name = click.prompt("Create default workspace name", default="Personal")
|
|
43
|
+
ws_result = await client.create_workspace(ws_name)
|
|
44
|
+
workspace_id = ws_result["id"]
|
|
45
|
+
|
|
46
|
+
# Save workspace ID
|
|
47
|
+
config_data["workspace_id"] = workspace_id
|
|
48
|
+
p_config.save(config_data)
|
|
49
|
+
click.secho(f"ā Associated with workspace ID: {workspace_id}", fg="green")
|
|
50
|
+
else:
|
|
51
|
+
click.echo(f"ā Using existing workspace ID: {workspace_id}")
|
|
52
|
+
|
|
53
|
+
# 2. Ensure Project
|
|
54
|
+
if not project_id:
|
|
55
|
+
# Check if project exists on server (by name or some other linking? For now just create)
|
|
56
|
+
# We assume if we don't have an ID, we haven't linked it.
|
|
57
|
+
# Use directory name as default project name
|
|
58
|
+
default_name = os.path.basename(os.path.abspath(directory))
|
|
59
|
+
proj_name = click.prompt("Enter project name on server", default=default_name)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# We try to create. If name collision logic isn't robust in API, this might error.
|
|
63
|
+
# Assuming API handles duplicates or we just handle error.
|
|
64
|
+
# Ideally we check list first, but create is fine.
|
|
65
|
+
new_project = await client.create_project(proj_name, workspace_id)
|
|
66
|
+
project_id = new_project["id"]
|
|
67
|
+
|
|
68
|
+
config_data["project_id"] = project_id
|
|
69
|
+
p_config.save(config_data)
|
|
70
|
+
click.secho(f"ā Created/Linked project ID: {project_id}", fg="green")
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
click.secho(f"Warning: Failed to create project: {e}", fg="yellow")
|
|
74
|
+
# Fallback: maybe list projects and let user select?
|
|
75
|
+
# For now, proceed without ID (some features might be limited)
|
|
76
|
+
return None
|
|
77
|
+
else:
|
|
78
|
+
click.echo(f"ā Using existing project ID: {project_id}")
|
|
79
|
+
|
|
80
|
+
return project_id
|
|
81
|
+
|
|
82
|
+
def onboard_project(directory: str = "."):
|
|
83
|
+
"""
|
|
84
|
+
Onboards a project into the OneCoder platform.
|
|
85
|
+
"""
|
|
86
|
+
directory = os.path.abspath(directory)
|
|
87
|
+
click.secho(f"š Kickstarting OneCoder Onboarding for: {directory}", fg="cyan", bold=True)
|
|
88
|
+
|
|
89
|
+
# Check for existing .sprint directory
|
|
90
|
+
sprint_dir = os.path.join(directory, ".sprint")
|
|
91
|
+
if not os.path.exists(sprint_dir):
|
|
92
|
+
click.echo("No .sprint directory found. Setting up 'Sprint 000'...")
|
|
93
|
+
os.makedirs(sprint_dir, exist_ok=True)
|
|
94
|
+
with open(os.path.join(sprint_dir, "README.md"), "w") as f:
|
|
95
|
+
f.write("# Sprint 000: Initialization\n\nInitial onboarding and project setup.")
|
|
96
|
+
|
|
97
|
+
# --- Sync Preferences ---
|
|
98
|
+
token = config_manager.get_token()
|
|
99
|
+
preferences = {
|
|
100
|
+
"artifacts": {
|
|
101
|
+
"auto_capture": True,
|
|
102
|
+
"destination": "sprint_context",
|
|
103
|
+
"types": ["assessment", "implementation_plan", "task", "walkthrough"]
|
|
104
|
+
},
|
|
105
|
+
"sprint": {
|
|
106
|
+
"auto_close_validation": True,
|
|
107
|
+
"require_walkthrough": True
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if token:
|
|
112
|
+
click.echo("Syncing preferences from API...")
|
|
113
|
+
try:
|
|
114
|
+
client = get_api_client(token)
|
|
115
|
+
api_prefs = asyncio.run(client.get_preferences())
|
|
116
|
+
preferences.update(api_prefs)
|
|
117
|
+
click.echo("ā Preferences synced successfully.")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
click.secho(f"! Failed to sync preferences: {e}. Aborting.", fg="red")
|
|
120
|
+
return
|
|
121
|
+
else:
|
|
122
|
+
click.secho("Error: No authentication token found. Please run 'onecoder login'.", fg="red")
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# --- Check for Existing Agent/Config Docs (Refinement) ---
|
|
126
|
+
agent_docs = {
|
|
127
|
+
"AGENTS.md": Path(directory) / "AGENTS.md",
|
|
128
|
+
"CLAUDE.md": Path(directory) / "CLAUDE.md",
|
|
129
|
+
"GEMINI.md": Path(directory) / "GEMINI.md",
|
|
130
|
+
".cursorrules": Path(directory) / ".cursorrules"
|
|
131
|
+
}
|
|
132
|
+
readme_path = Path(directory) / "README.md"
|
|
133
|
+
|
|
134
|
+
detected_agent_docs = {name: p for name, p in agent_docs.items() if p.exists()}
|
|
135
|
+
|
|
136
|
+
scan_data = {}
|
|
137
|
+
skip_deep_scan = False
|
|
138
|
+
|
|
139
|
+
if detected_agent_docs:
|
|
140
|
+
click.secho("š¤ Agent configuration detected. Optimizing onboarding...", fg="green")
|
|
141
|
+
combined_context = ""
|
|
142
|
+
if readme_path.exists():
|
|
143
|
+
combined_context += f"--- README.md ---\n{readme_path.read_text()}\n\n"
|
|
144
|
+
|
|
145
|
+
for name, p in detected_agent_docs.items():
|
|
146
|
+
combined_context += f"--- {name} ---\n{p.read_text()}\n\n"
|
|
147
|
+
|
|
148
|
+
scan_data["focused_context"] = combined_context
|
|
149
|
+
skip_deep_scan = True
|
|
150
|
+
|
|
151
|
+
# --- Check for Existing Artifacts (Optimization - Legacy Path) ---
|
|
152
|
+
spec_path = Path(directory) / "SPECIFICATION.md"
|
|
153
|
+
gov_path = Path(directory) / "governance.yaml"
|
|
154
|
+
|
|
155
|
+
if spec_path.exists() and gov_path.exists():
|
|
156
|
+
if click.confirm("\nš Existing project artifacts/configuration detected. Skip analysis and sync to remote?", default=True):
|
|
157
|
+
click.echo("Skipping analysis...")
|
|
158
|
+
try:
|
|
159
|
+
# Ensure project setup
|
|
160
|
+
asyncio.run(_ensure_project_setup(client, directory))
|
|
161
|
+
# Invoke Sync
|
|
162
|
+
click.echo("Syncing project context...")
|
|
163
|
+
asyncio.run(sync_project_context())
|
|
164
|
+
return
|
|
165
|
+
except Exception as e:
|
|
166
|
+
click.secho(f"Error during optimized sync: {e}", fg="red")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# --- Phase 1: Deep Codebase Scan (Skipped if focused context available) ---
|
|
170
|
+
if not skip_deep_scan:
|
|
171
|
+
click.secho("\n[Phase 1] Running deep codebase scan...", fg="yellow")
|
|
172
|
+
|
|
173
|
+
index_json = kit_index_tool(directory)
|
|
174
|
+
file_tree = kit_file_tree_tool(directory)
|
|
175
|
+
symbols = kit_symbols_tool(directory)
|
|
176
|
+
|
|
177
|
+
scan_data.update({
|
|
178
|
+
"index_summary": index_json[:2000] + "..." if len(index_json) > 2000 else index_json,
|
|
179
|
+
"file_tree": file_tree,
|
|
180
|
+
"symbols_summary": symbols[:2000] + "..." if len(symbols) > 2000 else symbols
|
|
181
|
+
})
|
|
182
|
+
else:
|
|
183
|
+
click.secho("\n[Phase 1] Skipping deep codebase scan (Agent Docs found).", fg="yellow")
|
|
184
|
+
|
|
185
|
+
# --- Phase 2: Agent Insight Generation (Server-Side) ---
|
|
186
|
+
click.secho("[Phase 2] Analyzing codebase insights (Server-Side)...", fg="yellow")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
results = asyncio.run(client.analyze_project(scan_data))
|
|
190
|
+
insights = results.get("insights", {})
|
|
191
|
+
click.echo(f"Insights generated. Architecture detected: {insights.get('architecture', 'Unknown')}")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
click.secho(f"ā Analysis failed: {e}", fg="red")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# --- Phase 2.5: Workspace & Project Setup ---
|
|
197
|
+
# Used same helper as optimization path
|
|
198
|
+
try:
|
|
199
|
+
asyncio.run(_ensure_project_setup(client, directory))
|
|
200
|
+
except Exception as e:
|
|
201
|
+
click.secho(f"! Project setup failed: {e}", fg="yellow")
|
|
202
|
+
# Continue locally? Or abort?
|
|
203
|
+
# We continue to allow local artifact generation even if sync setup invalid.
|
|
204
|
+
|
|
205
|
+
# --- Phase 3: Clarification Interview ---
|
|
206
|
+
click.secho("\n[Phase 3] Clarification Interview", fg="magenta", bold=True)
|
|
207
|
+
click.echo("The agent has some clarifying questions based on the scan:")
|
|
208
|
+
|
|
209
|
+
user_feedback = _conduct_interview(insights.get("clarifications", []))
|
|
210
|
+
|
|
211
|
+
# --- Phase 4: Artifact Finalization (Server-Side) ---
|
|
212
|
+
click.secho("\n[Phase 4] Finalizing Artifacts...", fg="yellow")
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
feedback_str = json.dumps(user_feedback)
|
|
216
|
+
final_results = asyncio.run(client.analyze_project(scan_data, user_feedback=feedback_str))
|
|
217
|
+
final_specs = final_results.get("artifacts", {})
|
|
218
|
+
|
|
219
|
+
if not final_specs:
|
|
220
|
+
click.secho("ā ļø Server returned no artifacts. Using fallbacks.", fg="yellow")
|
|
221
|
+
final_specs = {}
|
|
222
|
+
|
|
223
|
+
_write_project_artifacts(directory, final_specs)
|
|
224
|
+
update_agents_md(directory, insights.get("summary", "Project initialized."))
|
|
225
|
+
|
|
226
|
+
click.secho("\nā
Onboarding complete!", fg="green", bold=True)
|
|
227
|
+
click.echo("Review SPECIFICATION.md and governance.yaml.")
|
|
228
|
+
|
|
229
|
+
# Auto-Sync after successful onboarding
|
|
230
|
+
if click.confirm("Sync generated artifacts to remote?", default=True):
|
|
231
|
+
asyncio.run(sync_project_context())
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
click.secho(f"ā Artifact generation failed: {e}", fg="red")
|
|
235
|
+
|
|
236
|
+
def _conduct_interview(questions: list) -> Dict[str, str]:
|
|
237
|
+
"""Interactive CLI interview."""
|
|
238
|
+
responses = {}
|
|
239
|
+
for i, q in enumerate(questions, 1):
|
|
240
|
+
click.echo(f"\nQ{i}: {q}")
|
|
241
|
+
response = click.prompt("Answer", type=str)
|
|
242
|
+
responses[q] = response
|
|
243
|
+
return responses
|
|
244
|
+
|
|
245
|
+
def _write_project_artifacts(directory: str, artifacts: Dict):
|
|
246
|
+
"""Writes the final artifacts to disk."""
|
|
247
|
+
spec_path = os.path.join(directory, "SPECIFICATION.md")
|
|
248
|
+
gov_path = os.path.join(directory, "governance.yaml")
|
|
249
|
+
|
|
250
|
+
with open(spec_path, "w") as f:
|
|
251
|
+
f.write(artifacts.get("specification_md", ""))
|
|
252
|
+
click.echo(f"Created {spec_path}")
|
|
253
|
+
|
|
254
|
+
with open(gov_path, "w") as f:
|
|
255
|
+
f.write(artifacts.get("governance_yaml", ""))
|
|
256
|
+
click.echo(f"Created {gov_path}")
|
|
257
|
+
|
|
258
|
+
def update_agents_md(directory: str, summary: str):
|
|
259
|
+
"""Updates AGENTS.md."""
|
|
260
|
+
agents_path = os.path.join(directory, "AGENTS.md")
|
|
261
|
+
content = f"# Agent Guidelines\n\n## Summary\n{summary}\n\n## Rules\n1. Follow specs.\n2. Adhere to governance.yaml."
|
|
262
|
+
with open(agents_path, "w") as f:
|
|
263
|
+
f.write(content)
|
|
264
|
+
click.echo(f"Updated {agents_path}")
|