codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
"""Workspace management for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
A workspace represents a CodeFRAME-managed repository. Each workspace has:
|
|
4
|
+
- A .codeframe/ directory for state storage
|
|
5
|
+
- A SQLite database for persistent state
|
|
6
|
+
- Configuration and event logs
|
|
7
|
+
|
|
8
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sqlite3
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _utc_now() -> datetime:
|
|
20
|
+
"""Get current UTC time as timezone-aware datetime."""
|
|
21
|
+
return datetime.now(timezone.utc)
|
|
22
|
+
|
|
23
|
+
# State directory name
|
|
24
|
+
CODEFRAME_DIR = ".codeframe"
|
|
25
|
+
STATE_DB_NAME = "state.db"
|
|
26
|
+
|
|
27
|
+
# Per-workspace config file written by the Settings page (issue #556).
|
|
28
|
+
# Owned by the UI layer today; kept here so a future core consumer can
|
|
29
|
+
# read it without importing from codeframe/ui/.
|
|
30
|
+
WORKSPACE_CONFIG_FILENAME = "workspace_config.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Workspace:
|
|
35
|
+
"""Represents a CodeFRAME workspace.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
id: Unique workspace identifier (UUID)
|
|
39
|
+
repo_path: Absolute path to the repository
|
|
40
|
+
state_dir: Path to .codeframe/ directory
|
|
41
|
+
created_at: When the workspace was initialized
|
|
42
|
+
tech_stack: Natural language description of the project's technology stack
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
id: str
|
|
46
|
+
repo_path: Path
|
|
47
|
+
state_dir: Path
|
|
48
|
+
created_at: datetime
|
|
49
|
+
tech_stack: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def db_path(self) -> Path:
|
|
53
|
+
"""Path to the SQLite state database."""
|
|
54
|
+
return self.state_dir / STATE_DB_NAME
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_state_dir(repo_path: Path) -> Path:
|
|
58
|
+
"""Get the .codeframe/ directory path for a repository."""
|
|
59
|
+
return repo_path / CODEFRAME_DIR
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _init_database(db_path: Path) -> None:
|
|
63
|
+
"""Initialize the workspace SQLite database with v2 schema.
|
|
64
|
+
|
|
65
|
+
Creates tables for:
|
|
66
|
+
- workspaces: Workspace metadata
|
|
67
|
+
- prds: Product requirements documents
|
|
68
|
+
- tasks: Task state machine
|
|
69
|
+
- events: Append-only event log
|
|
70
|
+
- blockers: Human-in-the-loop blockers
|
|
71
|
+
- checkpoints: State snapshots
|
|
72
|
+
"""
|
|
73
|
+
conn = sqlite3.connect(db_path)
|
|
74
|
+
cursor = conn.cursor()
|
|
75
|
+
|
|
76
|
+
# Workspace metadata
|
|
77
|
+
cursor.execute("""
|
|
78
|
+
CREATE TABLE IF NOT EXISTS workspace (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
repo_path TEXT NOT NULL,
|
|
81
|
+
tech_stack TEXT,
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
updated_at TEXT NOT NULL
|
|
84
|
+
)
|
|
85
|
+
""")
|
|
86
|
+
|
|
87
|
+
# PRD storage with versioning support
|
|
88
|
+
cursor.execute("""
|
|
89
|
+
CREATE TABLE IF NOT EXISTS prds (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
workspace_id TEXT NOT NULL,
|
|
92
|
+
title TEXT,
|
|
93
|
+
content TEXT NOT NULL,
|
|
94
|
+
metadata TEXT,
|
|
95
|
+
created_at TEXT NOT NULL,
|
|
96
|
+
version INTEGER DEFAULT 1,
|
|
97
|
+
parent_id TEXT,
|
|
98
|
+
change_summary TEXT,
|
|
99
|
+
chain_id TEXT,
|
|
100
|
+
depends_on TEXT,
|
|
101
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
102
|
+
FOREIGN KEY (parent_id) REFERENCES prds(id),
|
|
103
|
+
FOREIGN KEY (chain_id) REFERENCES prds(id)
|
|
104
|
+
)
|
|
105
|
+
""")
|
|
106
|
+
|
|
107
|
+
# Task state machine (Golden Path statuses)
|
|
108
|
+
cursor.execute("""
|
|
109
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
workspace_id TEXT NOT NULL,
|
|
112
|
+
prd_id TEXT,
|
|
113
|
+
title TEXT NOT NULL,
|
|
114
|
+
description TEXT,
|
|
115
|
+
status TEXT NOT NULL DEFAULT 'BACKLOG',
|
|
116
|
+
priority INTEGER DEFAULT 0,
|
|
117
|
+
depends_on TEXT DEFAULT '[]',
|
|
118
|
+
estimated_hours REAL,
|
|
119
|
+
complexity_score INTEGER CHECK(complexity_score IS NULL OR (complexity_score BETWEEN 1 AND 5)),
|
|
120
|
+
uncertainty_level TEXT CHECK(uncertainty_level IS NULL OR uncertainty_level IN ('low', 'medium', 'high')),
|
|
121
|
+
parent_id TEXT,
|
|
122
|
+
lineage TEXT DEFAULT '[]',
|
|
123
|
+
is_leaf INTEGER DEFAULT 1,
|
|
124
|
+
hierarchical_id TEXT,
|
|
125
|
+
created_at TEXT NOT NULL,
|
|
126
|
+
updated_at TEXT NOT NULL,
|
|
127
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
128
|
+
FOREIGN KEY (prd_id) REFERENCES prds(id),
|
|
129
|
+
CHECK (status IN ('BACKLOG', 'READY', 'IN_PROGRESS', 'BLOCKED', 'FAILED', 'DONE', 'MERGED'))
|
|
130
|
+
)
|
|
131
|
+
""")
|
|
132
|
+
|
|
133
|
+
# Migration: Add columns to existing tasks table
|
|
134
|
+
# SQLite doesn't support IF NOT EXISTS for ALTER TABLE, so we check first
|
|
135
|
+
cursor.execute("PRAGMA table_info(tasks)")
|
|
136
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
137
|
+
if "depends_on" not in columns:
|
|
138
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN depends_on TEXT DEFAULT '[]'")
|
|
139
|
+
if "estimated_hours" not in columns:
|
|
140
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN estimated_hours REAL")
|
|
141
|
+
if "complexity_score" not in columns:
|
|
142
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN complexity_score INTEGER")
|
|
143
|
+
if "uncertainty_level" not in columns:
|
|
144
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN uncertainty_level TEXT")
|
|
145
|
+
if "github_issue_number" not in columns:
|
|
146
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN github_issue_number INTEGER")
|
|
147
|
+
if "parent_id" not in columns:
|
|
148
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN parent_id TEXT")
|
|
149
|
+
if "lineage" not in columns:
|
|
150
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN lineage TEXT DEFAULT '[]'")
|
|
151
|
+
if "is_leaf" not in columns:
|
|
152
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN is_leaf INTEGER DEFAULT 1")
|
|
153
|
+
if "hierarchical_id" not in columns:
|
|
154
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN hierarchical_id TEXT")
|
|
155
|
+
if "requirement_ids" not in columns:
|
|
156
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN requirement_ids TEXT DEFAULT '[]'")
|
|
157
|
+
if "external_url" not in columns:
|
|
158
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN external_url TEXT")
|
|
159
|
+
if "auto_close_github_issue" not in columns:
|
|
160
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN auto_close_github_issue INTEGER DEFAULT 0")
|
|
161
|
+
|
|
162
|
+
# Append-only event log
|
|
163
|
+
cursor.execute("""
|
|
164
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
165
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
166
|
+
workspace_id TEXT NOT NULL,
|
|
167
|
+
event_type TEXT NOT NULL,
|
|
168
|
+
payload TEXT,
|
|
169
|
+
created_at TEXT NOT NULL,
|
|
170
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id)
|
|
171
|
+
)
|
|
172
|
+
""")
|
|
173
|
+
|
|
174
|
+
# Blockers (human-in-the-loop)
|
|
175
|
+
cursor.execute("""
|
|
176
|
+
CREATE TABLE IF NOT EXISTS blockers (
|
|
177
|
+
id TEXT PRIMARY KEY,
|
|
178
|
+
workspace_id TEXT NOT NULL,
|
|
179
|
+
task_id TEXT,
|
|
180
|
+
question TEXT NOT NULL,
|
|
181
|
+
answer TEXT,
|
|
182
|
+
status TEXT NOT NULL DEFAULT 'OPEN',
|
|
183
|
+
created_at TEXT NOT NULL,
|
|
184
|
+
answered_at TEXT,
|
|
185
|
+
created_by TEXT NOT NULL DEFAULT 'human',
|
|
186
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
187
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
188
|
+
CHECK (status IN ('OPEN', 'ANSWERED', 'RESOLVED')),
|
|
189
|
+
CHECK (created_by IN ('system', 'agent', 'human'))
|
|
190
|
+
)
|
|
191
|
+
""")
|
|
192
|
+
|
|
193
|
+
# Migration: Add created_by column to existing blockers table
|
|
194
|
+
cursor.execute("PRAGMA table_info(blockers)")
|
|
195
|
+
blocker_columns = {row[1] for row in cursor.fetchall()}
|
|
196
|
+
if "created_by" not in blocker_columns:
|
|
197
|
+
cursor.execute("ALTER TABLE blockers ADD COLUMN created_by TEXT NOT NULL DEFAULT 'human'")
|
|
198
|
+
|
|
199
|
+
# Checkpoints (state snapshots)
|
|
200
|
+
cursor.execute("""
|
|
201
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
202
|
+
id TEXT PRIMARY KEY,
|
|
203
|
+
workspace_id TEXT NOT NULL,
|
|
204
|
+
name TEXT NOT NULL,
|
|
205
|
+
snapshot TEXT NOT NULL,
|
|
206
|
+
created_at TEXT NOT NULL,
|
|
207
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id)
|
|
208
|
+
)
|
|
209
|
+
""")
|
|
210
|
+
|
|
211
|
+
# Runs (agent execution records)
|
|
212
|
+
cursor.execute("""
|
|
213
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
214
|
+
id TEXT PRIMARY KEY,
|
|
215
|
+
workspace_id TEXT NOT NULL,
|
|
216
|
+
task_id TEXT NOT NULL,
|
|
217
|
+
status TEXT NOT NULL DEFAULT 'RUNNING',
|
|
218
|
+
started_at TEXT NOT NULL,
|
|
219
|
+
completed_at TEXT,
|
|
220
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
221
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
222
|
+
CHECK (status IN ('RUNNING', 'COMPLETED', 'FAILED', 'BLOCKED'))
|
|
223
|
+
)
|
|
224
|
+
""")
|
|
225
|
+
|
|
226
|
+
# Batch runs (multi-task orchestration)
|
|
227
|
+
cursor.execute("""
|
|
228
|
+
CREATE TABLE IF NOT EXISTS batch_runs (
|
|
229
|
+
id TEXT PRIMARY KEY,
|
|
230
|
+
workspace_id TEXT NOT NULL,
|
|
231
|
+
task_ids TEXT NOT NULL,
|
|
232
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
233
|
+
strategy TEXT NOT NULL DEFAULT 'serial',
|
|
234
|
+
max_parallel INTEGER NOT NULL DEFAULT 4,
|
|
235
|
+
on_failure TEXT NOT NULL DEFAULT 'continue',
|
|
236
|
+
started_at TEXT NOT NULL,
|
|
237
|
+
completed_at TEXT,
|
|
238
|
+
results TEXT,
|
|
239
|
+
engine TEXT NOT NULL DEFAULT 'plan',
|
|
240
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
241
|
+
CHECK (status IN ('PENDING', 'RUNNING', 'COMPLETED', 'PARTIAL', 'FAILED', 'CANCELLED'))
|
|
242
|
+
)
|
|
243
|
+
""")
|
|
244
|
+
|
|
245
|
+
# Run logs (structured logging per run)
|
|
246
|
+
cursor.execute("""
|
|
247
|
+
CREATE TABLE IF NOT EXISTS run_logs (
|
|
248
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
249
|
+
run_id TEXT NOT NULL,
|
|
250
|
+
task_id TEXT NOT NULL,
|
|
251
|
+
timestamp TEXT NOT NULL,
|
|
252
|
+
log_level TEXT NOT NULL DEFAULT 'INFO',
|
|
253
|
+
category TEXT NOT NULL,
|
|
254
|
+
message TEXT NOT NULL,
|
|
255
|
+
metadata TEXT,
|
|
256
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
257
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
258
|
+
CHECK (log_level IN ('DEBUG', 'INFO', 'WARNING', 'ERROR'))
|
|
259
|
+
)
|
|
260
|
+
""")
|
|
261
|
+
|
|
262
|
+
# Diagnostic reports (LLM analysis of run failures)
|
|
263
|
+
cursor.execute("""
|
|
264
|
+
CREATE TABLE IF NOT EXISTS diagnostic_reports (
|
|
265
|
+
id TEXT PRIMARY KEY,
|
|
266
|
+
task_id TEXT NOT NULL,
|
|
267
|
+
run_id TEXT NOT NULL,
|
|
268
|
+
root_cause TEXT NOT NULL,
|
|
269
|
+
failure_category TEXT NOT NULL,
|
|
270
|
+
severity TEXT NOT NULL,
|
|
271
|
+
recommendations TEXT NOT NULL,
|
|
272
|
+
log_summary TEXT,
|
|
273
|
+
created_at TEXT NOT NULL,
|
|
274
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
275
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
276
|
+
CHECK (severity IN ('critical', 'high', 'medium', 'low'))
|
|
277
|
+
)
|
|
278
|
+
""")
|
|
279
|
+
|
|
280
|
+
# Engine performance tracking: per-run log
|
|
281
|
+
cursor.execute("""
|
|
282
|
+
CREATE TABLE IF NOT EXISTS run_engine_log (
|
|
283
|
+
run_id TEXT PRIMARY KEY,
|
|
284
|
+
engine TEXT NOT NULL,
|
|
285
|
+
task_id TEXT NOT NULL,
|
|
286
|
+
workspace_id TEXT NOT NULL,
|
|
287
|
+
status TEXT NOT NULL,
|
|
288
|
+
duration_ms INTEGER,
|
|
289
|
+
tokens_used INTEGER DEFAULT 0,
|
|
290
|
+
gates_passed INTEGER,
|
|
291
|
+
self_corrections INTEGER DEFAULT 0,
|
|
292
|
+
created_at TEXT NOT NULL
|
|
293
|
+
)
|
|
294
|
+
""")
|
|
295
|
+
|
|
296
|
+
# Engine performance tracking: aggregate stats
|
|
297
|
+
cursor.execute("""
|
|
298
|
+
CREATE TABLE IF NOT EXISTS engine_stats (
|
|
299
|
+
workspace_id TEXT NOT NULL,
|
|
300
|
+
engine TEXT NOT NULL,
|
|
301
|
+
metric TEXT NOT NULL,
|
|
302
|
+
value REAL NOT NULL DEFAULT 0.0,
|
|
303
|
+
updated_at TEXT NOT NULL,
|
|
304
|
+
PRIMARY KEY (workspace_id, engine, metric)
|
|
305
|
+
)
|
|
306
|
+
""")
|
|
307
|
+
|
|
308
|
+
# Execution trace tables (for debug/replay mode)
|
|
309
|
+
cursor.execute("""
|
|
310
|
+
CREATE TABLE IF NOT EXISTS execution_steps (
|
|
311
|
+
id TEXT PRIMARY KEY,
|
|
312
|
+
run_id TEXT NOT NULL,
|
|
313
|
+
step_number INTEGER NOT NULL,
|
|
314
|
+
step_type TEXT NOT NULL,
|
|
315
|
+
description TEXT NOT NULL,
|
|
316
|
+
started_at TEXT NOT NULL,
|
|
317
|
+
completed_at TEXT,
|
|
318
|
+
status TEXT NOT NULL DEFAULT 'started',
|
|
319
|
+
input_context TEXT,
|
|
320
|
+
output_result TEXT,
|
|
321
|
+
metadata TEXT,
|
|
322
|
+
FOREIGN KEY (run_id) REFERENCES runs(id)
|
|
323
|
+
)
|
|
324
|
+
""")
|
|
325
|
+
|
|
326
|
+
cursor.execute("""
|
|
327
|
+
CREATE TABLE IF NOT EXISTS llm_interactions (
|
|
328
|
+
id TEXT PRIMARY KEY,
|
|
329
|
+
run_id TEXT NOT NULL,
|
|
330
|
+
step_id TEXT NOT NULL,
|
|
331
|
+
prompt TEXT NOT NULL,
|
|
332
|
+
response TEXT NOT NULL,
|
|
333
|
+
model TEXT NOT NULL,
|
|
334
|
+
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
335
|
+
timestamp TEXT NOT NULL,
|
|
336
|
+
purpose TEXT NOT NULL DEFAULT 'execution',
|
|
337
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
338
|
+
FOREIGN KEY (step_id) REFERENCES execution_steps(id)
|
|
339
|
+
)
|
|
340
|
+
""")
|
|
341
|
+
|
|
342
|
+
cursor.execute("""
|
|
343
|
+
CREATE TABLE IF NOT EXISTS file_operations (
|
|
344
|
+
id TEXT PRIMARY KEY,
|
|
345
|
+
run_id TEXT NOT NULL,
|
|
346
|
+
step_id TEXT NOT NULL,
|
|
347
|
+
operation_type TEXT NOT NULL,
|
|
348
|
+
file_path TEXT NOT NULL,
|
|
349
|
+
content_before TEXT,
|
|
350
|
+
content_after TEXT,
|
|
351
|
+
timestamp TEXT NOT NULL,
|
|
352
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
353
|
+
FOREIGN KEY (step_id) REFERENCES execution_steps(id),
|
|
354
|
+
CHECK (operation_type IN ('create', 'edit', 'delete'))
|
|
355
|
+
)
|
|
356
|
+
""")
|
|
357
|
+
|
|
358
|
+
cursor.execute("""
|
|
359
|
+
CREATE TABLE IF NOT EXISTS cloud_run_metadata (
|
|
360
|
+
run_id TEXT PRIMARY KEY,
|
|
361
|
+
sandbox_minutes REAL NOT NULL,
|
|
362
|
+
cost_usd_estimate REAL NOT NULL,
|
|
363
|
+
files_uploaded INTEGER NOT NULL,
|
|
364
|
+
files_downloaded INTEGER NOT NULL,
|
|
365
|
+
credential_scan_blocked INTEGER NOT NULL,
|
|
366
|
+
created_at TEXT NOT NULL
|
|
367
|
+
)
|
|
368
|
+
""")
|
|
369
|
+
|
|
370
|
+
# Create indexes for common queries
|
|
371
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_workspace ON tasks(workspace_id)")
|
|
372
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)")
|
|
373
|
+
# Atomic duplicate-import protection (#565): one task per (workspace, issue
|
|
374
|
+
# URL). SQLite treats NULLs as distinct, so non-imported tasks (NULL
|
|
375
|
+
# external_url) are unaffected.
|
|
376
|
+
cursor.execute(
|
|
377
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_external_url "
|
|
378
|
+
"ON tasks(workspace_id, external_url)"
|
|
379
|
+
)
|
|
380
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_id)")
|
|
381
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_blockers_workspace ON blockers(workspace_id)")
|
|
382
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_blockers_status ON blockers(status)")
|
|
383
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_batch_runs_workspace ON batch_runs(workspace_id)")
|
|
384
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_batch_runs_status ON batch_runs(status)")
|
|
385
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prds_parent ON prds(parent_id)")
|
|
386
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prds_chain ON prds(chain_id)")
|
|
387
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prds_depends_on ON prds(depends_on)")
|
|
388
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_run_logs_run ON run_logs(run_id)")
|
|
389
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_run_logs_task ON run_logs(task_id)")
|
|
390
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_diagnostic_reports_task ON diagnostic_reports(task_id)")
|
|
391
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_diagnostic_reports_run ON diagnostic_reports(run_id)")
|
|
392
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_run_engine_log_ws_engine ON run_engine_log(workspace_id, engine)")
|
|
393
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_engine_stats_ws ON engine_stats(workspace_id, engine)")
|
|
394
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_execution_steps_run ON execution_steps(run_id)")
|
|
395
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_execution_steps_run_step ON execution_steps(run_id, step_number)")
|
|
396
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_llm_interactions_run ON llm_interactions(run_id)")
|
|
397
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_llm_interactions_step ON llm_interactions(step_id)")
|
|
398
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_file_operations_run ON file_operations(run_id)")
|
|
399
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_file_operations_step ON file_operations(step_id)")
|
|
400
|
+
|
|
401
|
+
conn.commit()
|
|
402
|
+
conn.close()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _ensure_schema_upgrades(db_path: Path) -> None:
|
|
406
|
+
"""Ensure schema upgrades for existing databases.
|
|
407
|
+
|
|
408
|
+
This function is idempotent and adds any new tables/columns
|
|
409
|
+
that were added after the initial schema creation.
|
|
410
|
+
"""
|
|
411
|
+
conn = sqlite3.connect(db_path)
|
|
412
|
+
cursor = conn.cursor()
|
|
413
|
+
|
|
414
|
+
# Check if batch_runs table exists, if not create it
|
|
415
|
+
cursor.execute(
|
|
416
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='batch_runs'"
|
|
417
|
+
)
|
|
418
|
+
if not cursor.fetchone():
|
|
419
|
+
cursor.execute("""
|
|
420
|
+
CREATE TABLE IF NOT EXISTS batch_runs (
|
|
421
|
+
id TEXT PRIMARY KEY,
|
|
422
|
+
workspace_id TEXT NOT NULL,
|
|
423
|
+
task_ids TEXT NOT NULL,
|
|
424
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
425
|
+
strategy TEXT NOT NULL DEFAULT 'serial',
|
|
426
|
+
max_parallel INTEGER NOT NULL DEFAULT 4,
|
|
427
|
+
on_failure TEXT NOT NULL DEFAULT 'continue',
|
|
428
|
+
started_at TEXT NOT NULL,
|
|
429
|
+
completed_at TEXT,
|
|
430
|
+
results TEXT,
|
|
431
|
+
engine TEXT NOT NULL DEFAULT 'plan',
|
|
432
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
433
|
+
CHECK (status IN ('PENDING', 'RUNNING', 'COMPLETED', 'PARTIAL', 'FAILED', 'CANCELLED'))
|
|
434
|
+
)
|
|
435
|
+
""")
|
|
436
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_batch_runs_workspace ON batch_runs(workspace_id)")
|
|
437
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_batch_runs_status ON batch_runs(status)")
|
|
438
|
+
conn.commit()
|
|
439
|
+
else:
|
|
440
|
+
# Add engine column if it doesn't exist (migration for existing databases)
|
|
441
|
+
cursor.execute("PRAGMA table_info(batch_runs)")
|
|
442
|
+
batch_columns = {row[1] for row in cursor.fetchall()}
|
|
443
|
+
if "engine" not in batch_columns:
|
|
444
|
+
cursor.execute("ALTER TABLE batch_runs ADD COLUMN engine TEXT NOT NULL DEFAULT 'plan'")
|
|
445
|
+
conn.commit()
|
|
446
|
+
|
|
447
|
+
# Add tech_stack column to workspace table if it doesn't exist
|
|
448
|
+
# First check if workspace table exists
|
|
449
|
+
cursor.execute(
|
|
450
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='workspace'"
|
|
451
|
+
)
|
|
452
|
+
if cursor.fetchone():
|
|
453
|
+
cursor.execute("PRAGMA table_info(workspace)")
|
|
454
|
+
columns = {row[1] for row in cursor.fetchall()}
|
|
455
|
+
if "tech_stack" not in columns:
|
|
456
|
+
cursor.execute("ALTER TABLE workspace ADD COLUMN tech_stack TEXT")
|
|
457
|
+
conn.commit()
|
|
458
|
+
|
|
459
|
+
# Add versioning columns to prds table if they don't exist
|
|
460
|
+
# First check if prds table exists
|
|
461
|
+
cursor.execute(
|
|
462
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='prds'"
|
|
463
|
+
)
|
|
464
|
+
if cursor.fetchone():
|
|
465
|
+
cursor.execute("PRAGMA table_info(prds)")
|
|
466
|
+
prd_columns = {row[1] for row in cursor.fetchall()}
|
|
467
|
+
if "version" not in prd_columns:
|
|
468
|
+
cursor.execute("ALTER TABLE prds ADD COLUMN version INTEGER DEFAULT 1")
|
|
469
|
+
conn.commit()
|
|
470
|
+
if "parent_id" not in prd_columns:
|
|
471
|
+
cursor.execute("ALTER TABLE prds ADD COLUMN parent_id TEXT")
|
|
472
|
+
conn.commit()
|
|
473
|
+
if "change_summary" not in prd_columns:
|
|
474
|
+
cursor.execute("ALTER TABLE prds ADD COLUMN change_summary TEXT")
|
|
475
|
+
conn.commit()
|
|
476
|
+
if "chain_id" not in prd_columns:
|
|
477
|
+
cursor.execute("ALTER TABLE prds ADD COLUMN chain_id TEXT")
|
|
478
|
+
# Backfill chain_id for existing PRDs (set to their own id if no parent)
|
|
479
|
+
cursor.execute("""
|
|
480
|
+
UPDATE prds SET chain_id = id
|
|
481
|
+
WHERE chain_id IS NULL AND parent_id IS NULL
|
|
482
|
+
""")
|
|
483
|
+
conn.commit()
|
|
484
|
+
|
|
485
|
+
# Add depends_on column to prds table if it doesn't exist
|
|
486
|
+
# Re-check prd_columns as it may have changed
|
|
487
|
+
cursor.execute("PRAGMA table_info(prds)")
|
|
488
|
+
prd_columns = {row[1] for row in cursor.fetchall()}
|
|
489
|
+
if "depends_on" not in prd_columns:
|
|
490
|
+
cursor.execute("ALTER TABLE prds ADD COLUMN depends_on TEXT")
|
|
491
|
+
conn.commit()
|
|
492
|
+
|
|
493
|
+
# Add indexes for PRD version chain queries
|
|
494
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prds_parent ON prds(parent_id)")
|
|
495
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prds_chain ON prds(chain_id)")
|
|
496
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_prds_depends_on ON prds(depends_on)")
|
|
497
|
+
conn.commit()
|
|
498
|
+
|
|
499
|
+
# Add new columns to tasks table if they don't exist
|
|
500
|
+
cursor.execute(
|
|
501
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'"
|
|
502
|
+
)
|
|
503
|
+
if cursor.fetchone():
|
|
504
|
+
cursor.execute("PRAGMA table_info(tasks)")
|
|
505
|
+
task_columns = {row[1] for row in cursor.fetchall()}
|
|
506
|
+
if "depends_on" not in task_columns:
|
|
507
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN depends_on TEXT DEFAULT '[]'")
|
|
508
|
+
conn.commit()
|
|
509
|
+
if "estimated_hours" not in task_columns:
|
|
510
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN estimated_hours REAL")
|
|
511
|
+
conn.commit()
|
|
512
|
+
if "complexity_score" not in task_columns:
|
|
513
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN complexity_score INTEGER")
|
|
514
|
+
conn.commit()
|
|
515
|
+
if "uncertainty_level" not in task_columns:
|
|
516
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN uncertainty_level TEXT")
|
|
517
|
+
conn.commit()
|
|
518
|
+
if "parent_id" not in task_columns:
|
|
519
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN parent_id TEXT")
|
|
520
|
+
conn.commit()
|
|
521
|
+
if "lineage" not in task_columns:
|
|
522
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN lineage TEXT DEFAULT '[]'")
|
|
523
|
+
conn.commit()
|
|
524
|
+
if "is_leaf" not in task_columns:
|
|
525
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN is_leaf INTEGER DEFAULT 1")
|
|
526
|
+
conn.commit()
|
|
527
|
+
if "hierarchical_id" not in task_columns:
|
|
528
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN hierarchical_id TEXT")
|
|
529
|
+
conn.commit()
|
|
530
|
+
if "requirement_ids" not in task_columns:
|
|
531
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN requirement_ids TEXT DEFAULT '[]'")
|
|
532
|
+
conn.commit()
|
|
533
|
+
if "github_issue_number" not in task_columns:
|
|
534
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN github_issue_number INTEGER")
|
|
535
|
+
conn.commit()
|
|
536
|
+
if "external_url" not in task_columns:
|
|
537
|
+
cursor.execute("ALTER TABLE tasks ADD COLUMN external_url TEXT")
|
|
538
|
+
conn.commit()
|
|
539
|
+
if "auto_close_github_issue" not in task_columns:
|
|
540
|
+
cursor.execute(
|
|
541
|
+
"ALTER TABLE tasks ADD COLUMN auto_close_github_issue INTEGER DEFAULT 0"
|
|
542
|
+
)
|
|
543
|
+
conn.commit()
|
|
544
|
+
# Atomic duplicate-import protection (#565) for existing workspaces.
|
|
545
|
+
cursor.execute(
|
|
546
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_external_url "
|
|
547
|
+
"ON tasks(workspace_id, external_url)"
|
|
548
|
+
)
|
|
549
|
+
conn.commit()
|
|
550
|
+
|
|
551
|
+
# Ensure runs table exists before creating dependent tables (run_logs, diagnostic_reports)
|
|
552
|
+
cursor.execute(
|
|
553
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='runs'"
|
|
554
|
+
)
|
|
555
|
+
if not cursor.fetchone():
|
|
556
|
+
cursor.execute("""
|
|
557
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
558
|
+
id TEXT PRIMARY KEY,
|
|
559
|
+
workspace_id TEXT NOT NULL,
|
|
560
|
+
task_id TEXT NOT NULL,
|
|
561
|
+
status TEXT NOT NULL DEFAULT 'RUNNING',
|
|
562
|
+
started_at TEXT NOT NULL,
|
|
563
|
+
completed_at TEXT,
|
|
564
|
+
FOREIGN KEY (workspace_id) REFERENCES workspace(id),
|
|
565
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
566
|
+
CHECK (status IN ('RUNNING', 'COMPLETED', 'FAILED', 'BLOCKED'))
|
|
567
|
+
)
|
|
568
|
+
""")
|
|
569
|
+
conn.commit()
|
|
570
|
+
|
|
571
|
+
# Add run_logs table if it doesn't exist
|
|
572
|
+
cursor.execute(
|
|
573
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='run_logs'"
|
|
574
|
+
)
|
|
575
|
+
if not cursor.fetchone():
|
|
576
|
+
cursor.execute("""
|
|
577
|
+
CREATE TABLE IF NOT EXISTS run_logs (
|
|
578
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
579
|
+
run_id TEXT NOT NULL,
|
|
580
|
+
task_id TEXT NOT NULL,
|
|
581
|
+
timestamp TEXT NOT NULL,
|
|
582
|
+
log_level TEXT NOT NULL DEFAULT 'INFO',
|
|
583
|
+
category TEXT NOT NULL,
|
|
584
|
+
message TEXT NOT NULL,
|
|
585
|
+
metadata TEXT,
|
|
586
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
587
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
588
|
+
CHECK (log_level IN ('DEBUG', 'INFO', 'WARNING', 'ERROR'))
|
|
589
|
+
)
|
|
590
|
+
""")
|
|
591
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_run_logs_run ON run_logs(run_id)")
|
|
592
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_run_logs_task ON run_logs(task_id)")
|
|
593
|
+
conn.commit()
|
|
594
|
+
|
|
595
|
+
# Add diagnostic_reports table if it doesn't exist
|
|
596
|
+
cursor.execute(
|
|
597
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='diagnostic_reports'"
|
|
598
|
+
)
|
|
599
|
+
if not cursor.fetchone():
|
|
600
|
+
cursor.execute("""
|
|
601
|
+
CREATE TABLE IF NOT EXISTS diagnostic_reports (
|
|
602
|
+
id TEXT PRIMARY KEY,
|
|
603
|
+
task_id TEXT NOT NULL,
|
|
604
|
+
run_id TEXT NOT NULL,
|
|
605
|
+
root_cause TEXT NOT NULL,
|
|
606
|
+
failure_category TEXT NOT NULL,
|
|
607
|
+
severity TEXT NOT NULL,
|
|
608
|
+
recommendations TEXT NOT NULL,
|
|
609
|
+
log_summary TEXT,
|
|
610
|
+
created_at TEXT NOT NULL,
|
|
611
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
612
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
613
|
+
CHECK (severity IN ('critical', 'high', 'medium', 'low'))
|
|
614
|
+
)
|
|
615
|
+
""")
|
|
616
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_diagnostic_reports_task ON diagnostic_reports(task_id)")
|
|
617
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_diagnostic_reports_run ON diagnostic_reports(run_id)")
|
|
618
|
+
conn.commit()
|
|
619
|
+
|
|
620
|
+
# Add run_engine_log table for engine performance tracking
|
|
621
|
+
cursor.execute("""
|
|
622
|
+
CREATE TABLE IF NOT EXISTS run_engine_log (
|
|
623
|
+
run_id TEXT PRIMARY KEY,
|
|
624
|
+
engine TEXT NOT NULL,
|
|
625
|
+
task_id TEXT NOT NULL,
|
|
626
|
+
workspace_id TEXT NOT NULL,
|
|
627
|
+
status TEXT NOT NULL,
|
|
628
|
+
duration_ms INTEGER,
|
|
629
|
+
tokens_used INTEGER DEFAULT 0,
|
|
630
|
+
gates_passed INTEGER,
|
|
631
|
+
self_corrections INTEGER DEFAULT 0,
|
|
632
|
+
created_at TEXT NOT NULL
|
|
633
|
+
)
|
|
634
|
+
""")
|
|
635
|
+
cursor.execute(
|
|
636
|
+
"CREATE INDEX IF NOT EXISTS idx_run_engine_log_ws_engine "
|
|
637
|
+
"ON run_engine_log(workspace_id, engine)"
|
|
638
|
+
)
|
|
639
|
+
conn.commit()
|
|
640
|
+
|
|
641
|
+
# Add engine_stats table for aggregate engine metrics
|
|
642
|
+
cursor.execute("""
|
|
643
|
+
CREATE TABLE IF NOT EXISTS engine_stats (
|
|
644
|
+
workspace_id TEXT NOT NULL,
|
|
645
|
+
engine TEXT NOT NULL,
|
|
646
|
+
metric TEXT NOT NULL,
|
|
647
|
+
value REAL NOT NULL DEFAULT 0.0,
|
|
648
|
+
updated_at TEXT NOT NULL,
|
|
649
|
+
PRIMARY KEY (workspace_id, engine, metric)
|
|
650
|
+
)
|
|
651
|
+
""")
|
|
652
|
+
cursor.execute(
|
|
653
|
+
"CREATE INDEX IF NOT EXISTS idx_engine_stats_ws "
|
|
654
|
+
"ON engine_stats(workspace_id, engine)"
|
|
655
|
+
)
|
|
656
|
+
conn.commit()
|
|
657
|
+
|
|
658
|
+
# Add execution trace tables for debug/replay mode
|
|
659
|
+
cursor.execute("""
|
|
660
|
+
CREATE TABLE IF NOT EXISTS execution_steps (
|
|
661
|
+
id TEXT PRIMARY KEY,
|
|
662
|
+
run_id TEXT NOT NULL,
|
|
663
|
+
step_number INTEGER NOT NULL,
|
|
664
|
+
step_type TEXT NOT NULL,
|
|
665
|
+
description TEXT NOT NULL,
|
|
666
|
+
started_at TEXT NOT NULL,
|
|
667
|
+
completed_at TEXT,
|
|
668
|
+
status TEXT NOT NULL DEFAULT 'started',
|
|
669
|
+
input_context TEXT,
|
|
670
|
+
output_result TEXT,
|
|
671
|
+
metadata TEXT,
|
|
672
|
+
FOREIGN KEY (run_id) REFERENCES runs(id)
|
|
673
|
+
)
|
|
674
|
+
""")
|
|
675
|
+
cursor.execute("""
|
|
676
|
+
CREATE TABLE IF NOT EXISTS llm_interactions (
|
|
677
|
+
id TEXT PRIMARY KEY,
|
|
678
|
+
run_id TEXT NOT NULL,
|
|
679
|
+
step_id TEXT NOT NULL,
|
|
680
|
+
prompt TEXT NOT NULL,
|
|
681
|
+
response TEXT NOT NULL,
|
|
682
|
+
model TEXT NOT NULL,
|
|
683
|
+
tokens_used INTEGER NOT NULL DEFAULT 0,
|
|
684
|
+
timestamp TEXT NOT NULL,
|
|
685
|
+
purpose TEXT NOT NULL DEFAULT 'execution',
|
|
686
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
687
|
+
FOREIGN KEY (step_id) REFERENCES execution_steps(id)
|
|
688
|
+
)
|
|
689
|
+
""")
|
|
690
|
+
cursor.execute("""
|
|
691
|
+
CREATE TABLE IF NOT EXISTS file_operations (
|
|
692
|
+
id TEXT PRIMARY KEY,
|
|
693
|
+
run_id TEXT NOT NULL,
|
|
694
|
+
step_id TEXT NOT NULL,
|
|
695
|
+
operation_type TEXT NOT NULL,
|
|
696
|
+
file_path TEXT NOT NULL,
|
|
697
|
+
content_before TEXT,
|
|
698
|
+
content_after TEXT,
|
|
699
|
+
timestamp TEXT NOT NULL,
|
|
700
|
+
FOREIGN KEY (run_id) REFERENCES runs(id),
|
|
701
|
+
FOREIGN KEY (step_id) REFERENCES execution_steps(id),
|
|
702
|
+
CHECK (operation_type IN ('create', 'edit', 'delete'))
|
|
703
|
+
)
|
|
704
|
+
""")
|
|
705
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_execution_steps_run ON execution_steps(run_id)")
|
|
706
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_execution_steps_run_step ON execution_steps(run_id, step_number)")
|
|
707
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_llm_interactions_run ON llm_interactions(run_id)")
|
|
708
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_llm_interactions_step ON llm_interactions(step_id)")
|
|
709
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_file_operations_run ON file_operations(run_id)")
|
|
710
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_file_operations_step ON file_operations(step_id)")
|
|
711
|
+
# Add cloud_run_metadata table for E2B cloud execution tracking
|
|
712
|
+
cursor.execute("""
|
|
713
|
+
CREATE TABLE IF NOT EXISTS cloud_run_metadata (
|
|
714
|
+
run_id TEXT PRIMARY KEY,
|
|
715
|
+
sandbox_minutes REAL NOT NULL,
|
|
716
|
+
cost_usd_estimate REAL NOT NULL,
|
|
717
|
+
files_uploaded INTEGER NOT NULL,
|
|
718
|
+
files_downloaded INTEGER NOT NULL,
|
|
719
|
+
credential_scan_blocked INTEGER NOT NULL,
|
|
720
|
+
created_at TEXT NOT NULL
|
|
721
|
+
)
|
|
722
|
+
""")
|
|
723
|
+
conn.commit()
|
|
724
|
+
|
|
725
|
+
conn.close()
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def create_or_load_workspace(repo_path: Path, tech_stack: Optional[str] = None) -> Workspace:
|
|
729
|
+
"""Create a new workspace or load an existing one.
|
|
730
|
+
|
|
731
|
+
This is idempotent - calling it on an already-initialized repo
|
|
732
|
+
will return the existing workspace (tech_stack is ignored if workspace exists).
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
repo_path: Path to the repository (must exist)
|
|
736
|
+
tech_stack: Optional natural language description of the project's tech stack
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Workspace object with metadata
|
|
740
|
+
|
|
741
|
+
Raises:
|
|
742
|
+
FileNotFoundError: If repo_path doesn't exist
|
|
743
|
+
NotADirectoryError: If repo_path is not a directory
|
|
744
|
+
"""
|
|
745
|
+
repo_path = repo_path.resolve()
|
|
746
|
+
|
|
747
|
+
if not repo_path.exists():
|
|
748
|
+
raise FileNotFoundError(f"Repository path does not exist: {repo_path}")
|
|
749
|
+
|
|
750
|
+
if not repo_path.is_dir():
|
|
751
|
+
raise NotADirectoryError(f"Repository path is not a directory: {repo_path}")
|
|
752
|
+
|
|
753
|
+
state_dir = _get_state_dir(repo_path)
|
|
754
|
+
db_path = state_dir / STATE_DB_NAME
|
|
755
|
+
|
|
756
|
+
# Check if workspace already exists
|
|
757
|
+
if state_dir.exists() and db_path.exists():
|
|
758
|
+
return get_workspace(repo_path)
|
|
759
|
+
|
|
760
|
+
# Create .codeframe/ directory
|
|
761
|
+
state_dir.mkdir(exist_ok=True)
|
|
762
|
+
|
|
763
|
+
# Initialize database
|
|
764
|
+
_init_database(db_path)
|
|
765
|
+
|
|
766
|
+
# Create workspace record
|
|
767
|
+
workspace_id = str(uuid.uuid4())
|
|
768
|
+
now = _utc_now().isoformat()
|
|
769
|
+
|
|
770
|
+
conn = sqlite3.connect(db_path)
|
|
771
|
+
cursor = conn.cursor()
|
|
772
|
+
cursor.execute(
|
|
773
|
+
"INSERT INTO workspace (id, repo_path, tech_stack, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
774
|
+
(workspace_id, str(repo_path), tech_stack, now, now),
|
|
775
|
+
)
|
|
776
|
+
conn.commit()
|
|
777
|
+
conn.close()
|
|
778
|
+
|
|
779
|
+
return Workspace(
|
|
780
|
+
id=workspace_id,
|
|
781
|
+
repo_path=repo_path,
|
|
782
|
+
state_dir=state_dir,
|
|
783
|
+
created_at=datetime.fromisoformat(now),
|
|
784
|
+
tech_stack=tech_stack,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def get_workspace(repo_path: Path) -> Workspace:
|
|
789
|
+
"""Load an existing workspace.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
repo_path: Path to the repository
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Workspace object
|
|
796
|
+
|
|
797
|
+
Raises:
|
|
798
|
+
FileNotFoundError: If no workspace exists at this path
|
|
799
|
+
"""
|
|
800
|
+
repo_path = repo_path.resolve()
|
|
801
|
+
state_dir = _get_state_dir(repo_path)
|
|
802
|
+
db_path = state_dir / STATE_DB_NAME
|
|
803
|
+
|
|
804
|
+
if not state_dir.exists() or not db_path.exists():
|
|
805
|
+
raise FileNotFoundError(f"No workspace found at {repo_path}")
|
|
806
|
+
|
|
807
|
+
# Ensure schema is up to date for existing workspaces
|
|
808
|
+
_ensure_schema_upgrades(db_path)
|
|
809
|
+
|
|
810
|
+
conn = sqlite3.connect(db_path)
|
|
811
|
+
cursor = conn.cursor()
|
|
812
|
+
cursor.execute("SELECT id, repo_path, tech_stack, created_at FROM workspace LIMIT 1")
|
|
813
|
+
row = cursor.fetchone()
|
|
814
|
+
conn.close()
|
|
815
|
+
|
|
816
|
+
if not row:
|
|
817
|
+
raise FileNotFoundError("Workspace database exists but contains no workspace record")
|
|
818
|
+
|
|
819
|
+
return Workspace(
|
|
820
|
+
id=row[0],
|
|
821
|
+
repo_path=Path(row[1]),
|
|
822
|
+
state_dir=state_dir,
|
|
823
|
+
created_at=datetime.fromisoformat(row[3]),
|
|
824
|
+
tech_stack=row[2],
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def get_db_connection(workspace: Workspace) -> sqlite3.Connection:
|
|
829
|
+
"""Get a database connection for a workspace.
|
|
830
|
+
|
|
831
|
+
The caller is responsible for closing the connection.
|
|
832
|
+
|
|
833
|
+
Args:
|
|
834
|
+
workspace: Workspace object
|
|
835
|
+
|
|
836
|
+
Returns:
|
|
837
|
+
SQLite connection
|
|
838
|
+
"""
|
|
839
|
+
return sqlite3.connect(workspace.db_path)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def workspace_exists(repo_path: Path) -> bool:
|
|
843
|
+
"""Check if a workspace exists at the given path.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
repo_path: Path to check
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
True if workspace exists, False otherwise
|
|
850
|
+
"""
|
|
851
|
+
state_dir = _get_state_dir(repo_path.resolve())
|
|
852
|
+
db_path = state_dir / STATE_DB_NAME
|
|
853
|
+
return state_dir.exists() and db_path.exists()
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def update_workspace_tech_stack(repo_path: Path, tech_stack: Optional[str]) -> Workspace:
|
|
857
|
+
"""Update the tech_stack for an existing workspace.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
repo_path: Path to the repository
|
|
861
|
+
tech_stack: New tech stack description (or None to clear)
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
Updated Workspace object
|
|
865
|
+
|
|
866
|
+
Raises:
|
|
867
|
+
FileNotFoundError: If no workspace exists at this path
|
|
868
|
+
"""
|
|
869
|
+
repo_path = repo_path.resolve()
|
|
870
|
+
state_dir = _get_state_dir(repo_path)
|
|
871
|
+
db_path = state_dir / STATE_DB_NAME
|
|
872
|
+
|
|
873
|
+
if not state_dir.exists() or not db_path.exists():
|
|
874
|
+
raise FileNotFoundError(f"No workspace found at {repo_path}")
|
|
875
|
+
|
|
876
|
+
now = _utc_now().isoformat()
|
|
877
|
+
|
|
878
|
+
conn = sqlite3.connect(db_path)
|
|
879
|
+
cursor = conn.cursor()
|
|
880
|
+
cursor.execute(
|
|
881
|
+
"UPDATE workspace SET tech_stack = ?, updated_at = ?",
|
|
882
|
+
(tech_stack, now),
|
|
883
|
+
)
|
|
884
|
+
conn.commit()
|
|
885
|
+
conn.close()
|
|
886
|
+
|
|
887
|
+
return get_workspace(repo_path)
|