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
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}")