draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Create demo data for PR review flow."""
|
|
2
|
+
import sqlite3
|
|
3
|
+
import uuid
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
DB = "kanban.db"
|
|
8
|
+
BOARD_ID = "40c6e796-b75d-47d0-9e9d-ea525b70e271"
|
|
9
|
+
TICKET_ID = "8b383839-cbec-45af-86ee-c7708d075cbe" # JWT auth ticket
|
|
10
|
+
GOAL_ID = "d1213e05-a49c-4b30-8033-64de449e587f"
|
|
11
|
+
|
|
12
|
+
conn = sqlite3.connect(DB)
|
|
13
|
+
c = conn.cursor()
|
|
14
|
+
|
|
15
|
+
# 1. Transition ticket to needs_human
|
|
16
|
+
c.execute("UPDATE tickets SET state = ? WHERE id = ?", ("needs_human", TICKET_ID))
|
|
17
|
+
|
|
18
|
+
# 2. Create a Job record
|
|
19
|
+
job_id = str(uuid.uuid4())
|
|
20
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
21
|
+
c.execute(
|
|
22
|
+
"""INSERT INTO jobs (id, ticket_id, board_id, kind, status, created_at, started_at, finished_at)
|
|
23
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
24
|
+
(job_id, TICKET_ID, BOARD_ID, "execute", "succeeded", now, now, now),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# 3. Create evidence files on disk
|
|
28
|
+
evidence_dir = os.path.join(".smartkanban", "evidence", TICKET_ID)
|
|
29
|
+
os.makedirs(evidence_dir, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
diff_stat = """ src/middleware/auth.js | 45 +++++++++++++++++++++++++++++++++++++++++++++
|
|
32
|
+
src/routes/auth.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
33
|
+
src/models/db.js | 12 ++++++++++--
|
|
34
|
+
src/index.js | 8 ++++++--
|
|
35
|
+
package.json | 4 +++-
|
|
36
|
+
5 files changed, 142 insertions(+), 5 deletions(-)"""
|
|
37
|
+
|
|
38
|
+
diff_patch = '''diff --git a/package.json b/package.json
|
|
39
|
+
index 1a2b3c4..5d6e7f8 100644
|
|
40
|
+
--- a/package.json
|
|
41
|
+
+++ b/package.json
|
|
42
|
+
@@ -10,7 +10,9 @@
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"express": "^4.18.2",
|
|
45
|
+
"better-sqlite3": "^9.4.3",
|
|
46
|
+
- "cors": "^2.8.5"
|
|
47
|
+
+ "cors": "^2.8.5",
|
|
48
|
+
+ "jsonwebtoken": "^9.0.2",
|
|
49
|
+
+ "bcryptjs": "^2.4.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"jest": "^29.7.0",
|
|
53
|
+
diff --git a/src/models/db.js b/src/models/db.js
|
|
54
|
+
index 2b3c4d5..8e9f0a1 100644
|
|
55
|
+
--- a/src/models/db.js
|
|
56
|
+
+++ b/src/models/db.js
|
|
57
|
+
@@ -8,6 +8,16 @@ db.exec(`
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
+db.exec(`
|
|
62
|
+
+ CREATE TABLE IF NOT EXISTS users (
|
|
63
|
+
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
+ username TEXT UNIQUE NOT NULL,
|
|
65
|
+
+ password TEXT NOT NULL,
|
|
66
|
+
+ email TEXT,
|
|
67
|
+
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
68
|
+
+ )
|
|
69
|
+
+`);
|
|
70
|
+
+
|
|
71
|
+
module.exports = db;
|
|
72
|
+
diff --git a/src/middleware/auth.js b/src/middleware/auth.js
|
|
73
|
+
new file mode 100644
|
|
74
|
+
index 0000000..3f4a5b6
|
|
75
|
+
--- /dev/null
|
|
76
|
+
+++ b/src/middleware/auth.js
|
|
77
|
+
@@ -0,0 +1,45 @@
|
|
78
|
+
+const jwt = require("jsonwebtoken");
|
|
79
|
+
+
|
|
80
|
+
+const JWT_SECRET = process.env.JWT_SECRET || "dev-secret-key";
|
|
81
|
+
+
|
|
82
|
+
+function authenticateToken(req, res, next) {
|
|
83
|
+
+ const authHeader = req.headers["authorization"];
|
|
84
|
+
+ const token = authHeader && authHeader.split(" ")[1];
|
|
85
|
+
+
|
|
86
|
+
+ if (!token) {
|
|
87
|
+
+ return res.status(401).json({ error: "Access token required" });
|
|
88
|
+
+ }
|
|
89
|
+
+
|
|
90
|
+
+ try {
|
|
91
|
+
+ const decoded = jwt.verify(token, JWT_SECRET);
|
|
92
|
+
+ req.user = decoded;
|
|
93
|
+
+ next();
|
|
94
|
+
+ } catch (err) {
|
|
95
|
+
+ return res.status(401).json({ error: "Invalid or expired token" });
|
|
96
|
+
+ }
|
|
97
|
+
+}
|
|
98
|
+
+
|
|
99
|
+
+function generateToken(user) {
|
|
100
|
+
+ return jwt.sign(
|
|
101
|
+
+ { id: user.id, username: user.username },
|
|
102
|
+
+ JWT_SECRET,
|
|
103
|
+
+ { expiresIn: "24h" }
|
|
104
|
+
+ );
|
|
105
|
+
+}
|
|
106
|
+
+
|
|
107
|
+
+function hashPassword(password) {
|
|
108
|
+
+ const bcrypt = require("bcryptjs");
|
|
109
|
+
+ return bcrypt.hashSync(password, 10);
|
|
110
|
+
+}
|
|
111
|
+
+
|
|
112
|
+
+function comparePassword(password, hash) {
|
|
113
|
+
+ const bcrypt = require("bcryptjs");
|
|
114
|
+
+ return bcrypt.compareSync(password, hash);
|
|
115
|
+
+}
|
|
116
|
+
+
|
|
117
|
+
+module.exports = {
|
|
118
|
+
+ authenticateToken,
|
|
119
|
+
+ generateToken,
|
|
120
|
+
+ hashPassword,
|
|
121
|
+
+ comparePassword,
|
|
122
|
+
+ JWT_SECRET
|
|
123
|
+
+};
|
|
124
|
+
diff --git a/src/routes/auth.js b/src/routes/auth.js
|
|
125
|
+
new file mode 100644
|
|
126
|
+
index 0000000..7c8d9e0
|
|
127
|
+
--- /dev/null
|
|
128
|
+
+++ b/src/routes/auth.js
|
|
129
|
+
@@ -0,0 +1,78 @@
|
|
130
|
+
+const express = require("express");
|
|
131
|
+
+const router = express.Router();
|
|
132
|
+
+const db = require("../models/db");
|
|
133
|
+
+const { generateToken, hashPassword, comparePassword } = require("../middleware/auth");
|
|
134
|
+
+
|
|
135
|
+
+// POST /api/auth/register
|
|
136
|
+
+router.post("/register", (req, res) => {
|
|
137
|
+
+ const { username, password, email } = req.body;
|
|
138
|
+
+
|
|
139
|
+
+ if (!username || !password) {
|
|
140
|
+
+ return res.status(400).json({ error: "Username and password are required" });
|
|
141
|
+
+ }
|
|
142
|
+
+
|
|
143
|
+
+ if (password.length < 6) {
|
|
144
|
+
+ return res.status(400).json({ error: "Password must be at least 6 characters" });
|
|
145
|
+
+ }
|
|
146
|
+
+
|
|
147
|
+
+ try {
|
|
148
|
+
+ const existing = db.prepare("SELECT id FROM users WHERE username = ?").get(username);
|
|
149
|
+
+ if (existing) {
|
|
150
|
+
+ return res.status(409).json({ error: "Username already exists" });
|
|
151
|
+
+ }
|
|
152
|
+
+
|
|
153
|
+
+ const hashedPassword = hashPassword(password);
|
|
154
|
+
+ const result = db.prepare(
|
|
155
|
+
+ "INSERT INTO users (username, password, email) VALUES (?, ?, ?)"
|
|
156
|
+
+ ).run(username, hashedPassword, email || null);
|
|
157
|
+
+
|
|
158
|
+
+ const user = { id: result.lastInsertRowid, username };
|
|
159
|
+
+ const token = generateToken(user);
|
|
160
|
+
+
|
|
161
|
+
+ res.status(201).json({ user: { id: user.id, username }, token });
|
|
162
|
+
+ } catch (err) {
|
|
163
|
+
+ res.status(500).json({ error: "Registration failed" });
|
|
164
|
+
+ }
|
|
165
|
+
+});
|
|
166
|
+
+
|
|
167
|
+
+// POST /api/auth/login
|
|
168
|
+
+router.post("/login", (req, res) => {
|
|
169
|
+
+ const { username, password } = req.body;
|
|
170
|
+
+
|
|
171
|
+
+ if (!username || !password) {
|
|
172
|
+
+ return res.status(400).json({ error: "Username and password are required" });
|
|
173
|
+
+ }
|
|
174
|
+
+
|
|
175
|
+
+ try {
|
|
176
|
+
+ const user = db.prepare("SELECT * FROM users WHERE username = ?").get(username);
|
|
177
|
+
+
|
|
178
|
+
+ if (!user) {
|
|
179
|
+
+ return res.status(401).json({ error: "Invalid credentials" });
|
|
180
|
+
+ }
|
|
181
|
+
+
|
|
182
|
+
+ if (!comparePassword(password, user.password)) {
|
|
183
|
+
+ return res.status(401).json({ error: "Invalid credentials" });
|
|
184
|
+
+ }
|
|
185
|
+
+
|
|
186
|
+
+ const token = generateToken({ id: user.id, username: user.username });
|
|
187
|
+
+ res.json({ user: { id: user.id, username: user.username }, token });
|
|
188
|
+
+ } catch (err) {
|
|
189
|
+
+ res.status(500).json({ error: "Login failed" });
|
|
190
|
+
+ }
|
|
191
|
+
+});
|
|
192
|
+
+
|
|
193
|
+
+// GET /api/auth/me - Get current user
|
|
194
|
+
+router.get("/me", (req, res) => {
|
|
195
|
+
+ if (!req.user) {
|
|
196
|
+
+ return res.status(401).json({ error: "Not authenticated" });
|
|
197
|
+
+ }
|
|
198
|
+
+
|
|
199
|
+
+ const user = db.prepare("SELECT id, username, email, created_at FROM users WHERE id = ?").get(req.user.id);
|
|
200
|
+
+ if (!user) {
|
|
201
|
+
+ return res.status(404).json({ error: "User not found" });
|
|
202
|
+
+ }
|
|
203
|
+
+
|
|
204
|
+
+ res.json({ user });
|
|
205
|
+
+});
|
|
206
|
+
+
|
|
207
|
+
+module.exports = router;
|
|
208
|
+
diff --git a/src/index.js b/src/index.js
|
|
209
|
+
index 4d5e6f7..9a0b1c2 100644
|
|
210
|
+
--- a/src/index.js
|
|
211
|
+
+++ b/src/index.js
|
|
212
|
+
@@ -2,6 +2,8 @@ const express = require("express");
|
|
213
|
+
const cors = require("cors");
|
|
214
|
+
const taskRoutes = require("./routes/tasks");
|
|
215
|
+
const projectRoutes = require("./routes/projects");
|
|
216
|
+
+const authRoutes = require("./routes/auth");
|
|
217
|
+
+const { authenticateToken } = require("./middleware/auth");
|
|
218
|
+
const { errorHandler } = require("./middleware/errorHandler");
|
|
219
|
+
|
|
220
|
+
const app = express();
|
|
221
|
+
@@ -12,8 +14,10 @@ app.use(express.json());
|
|
222
|
+
|
|
223
|
+
app.get("/health", (req, res) => res.json({ status: "ok" }));
|
|
224
|
+
|
|
225
|
+
-app.use("/api/tasks", taskRoutes);
|
|
226
|
+
-app.use("/api/projects", projectRoutes);
|
|
227
|
+
+app.use("/api/auth", authRoutes);
|
|
228
|
+
+
|
|
229
|
+
+app.use("/api/tasks", authenticateToken, taskRoutes);
|
|
230
|
+
+app.use("/api/projects", authenticateToken, projectRoutes);
|
|
231
|
+
|
|
232
|
+
app.use(errorHandler);
|
|
233
|
+
'''
|
|
234
|
+
|
|
235
|
+
# Write evidence files
|
|
236
|
+
stat_path = os.path.join(evidence_dir, f"{job_id}_stat.txt")
|
|
237
|
+
patch_path = os.path.join(evidence_dir, f"{job_id}_patch.txt")
|
|
238
|
+
with open(stat_path, "w") as f:
|
|
239
|
+
f.write(diff_stat)
|
|
240
|
+
with open(patch_path, "w") as f:
|
|
241
|
+
f.write(diff_patch)
|
|
242
|
+
|
|
243
|
+
# 4. Create Evidence records
|
|
244
|
+
stat_evidence_id = str(uuid.uuid4())
|
|
245
|
+
patch_evidence_id = str(uuid.uuid4())
|
|
246
|
+
|
|
247
|
+
c.execute(
|
|
248
|
+
"""INSERT INTO evidence (id, ticket_id, job_id, kind, command, exit_code, stdout_path, created_at)
|
|
249
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
250
|
+
(
|
|
251
|
+
stat_evidence_id,
|
|
252
|
+
TICKET_ID,
|
|
253
|
+
job_id,
|
|
254
|
+
"git_diff_stat",
|
|
255
|
+
"git diff --stat",
|
|
256
|
+
0,
|
|
257
|
+
os.path.abspath(stat_path),
|
|
258
|
+
now,
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
c.execute(
|
|
263
|
+
"""INSERT INTO evidence (id, ticket_id, job_id, kind, command, exit_code, stdout_path, created_at)
|
|
264
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
265
|
+
(
|
|
266
|
+
patch_evidence_id,
|
|
267
|
+
TICKET_ID,
|
|
268
|
+
job_id,
|
|
269
|
+
"git_diff_patch",
|
|
270
|
+
"git diff",
|
|
271
|
+
0,
|
|
272
|
+
os.path.abspath(patch_path),
|
|
273
|
+
now,
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# 5. Create Revision record
|
|
278
|
+
revision_id = str(uuid.uuid4())
|
|
279
|
+
c.execute(
|
|
280
|
+
"""INSERT INTO revisions (id, ticket_id, job_id, number, status, diff_stat_evidence_id, diff_patch_evidence_id, created_at)
|
|
281
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
282
|
+
(
|
|
283
|
+
revision_id,
|
|
284
|
+
TICKET_ID,
|
|
285
|
+
job_id,
|
|
286
|
+
1,
|
|
287
|
+
"open",
|
|
288
|
+
stat_evidence_id,
|
|
289
|
+
patch_evidence_id,
|
|
290
|
+
now,
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
conn.commit()
|
|
295
|
+
conn.close()
|
|
296
|
+
|
|
297
|
+
print(f"Ticket: {TICKET_ID}")
|
|
298
|
+
print(f"Job: {job_id}")
|
|
299
|
+
print(f"Revision: {revision_id}")
|
|
300
|
+
print(f"Evidence stat: {stat_evidence_id}")
|
|
301
|
+
print(f"Evidence patch: {patch_evidence_id}")
|
|
302
|
+
print("Done - ticket is now in needs_human with a revision")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test parsing the actual agent response."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, ".")
|
|
9
|
+
|
|
10
|
+
# Read the actual agent response
|
|
11
|
+
with open("/tmp/tmpi59wry37_agent_response.txt") as f:
|
|
12
|
+
response = f.read()
|
|
13
|
+
|
|
14
|
+
print(f"Response length: {len(response)} chars")
|
|
15
|
+
print(f"First 200 chars: {response[:200]}")
|
|
16
|
+
print()
|
|
17
|
+
|
|
18
|
+
# Try to find JSON in code blocks first (same logic as the service)
|
|
19
|
+
json_block_pattern = r"```(?:json)?\s*(\{[\s\S]*?\})\s*```"
|
|
20
|
+
matches = re.findall(json_block_pattern, response)
|
|
21
|
+
|
|
22
|
+
print(f"Found {len(matches)} JSON blocks")
|
|
23
|
+
|
|
24
|
+
for i, match in enumerate(matches, 1):
|
|
25
|
+
print(f"\nBlock {i} length: {len(match)} chars")
|
|
26
|
+
print(f"First 100 chars: {match[:100]}")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
data = json.loads(match)
|
|
30
|
+
if "tickets" in data:
|
|
31
|
+
print("✅ Valid JSON with tickets!")
|
|
32
|
+
print(f"Tickets: {len(data['tickets'])}")
|
|
33
|
+
for ticket in data["tickets"]:
|
|
34
|
+
print(f" - {ticket.get('title', 'NO TITLE')[:80]}")
|
|
35
|
+
else:
|
|
36
|
+
print("❌ JSON valid but no 'tickets' key")
|
|
37
|
+
except json.JSONDecodeError as e:
|
|
38
|
+
print(f"❌ JSON parse error: {e}")
|
|
39
|
+
print(f"Error at position {e.pos}")
|
|
40
|
+
if e.pos < len(match):
|
|
41
|
+
print(f"Context: ...{match[max(0, e.pos - 50) : e.pos + 50]}...")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test agent CLI streaming."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_agent_streaming():
|
|
9
|
+
"""Test cursor-agent streaming with a real prompt."""
|
|
10
|
+
print("Testing cursor-agent streaming...")
|
|
11
|
+
|
|
12
|
+
# Get repo root
|
|
13
|
+
repo_root = Path(__file__).parent.parent
|
|
14
|
+
agent_path = Path.home() / ".local/bin/cursor-agent"
|
|
15
|
+
|
|
16
|
+
# Simple prompt
|
|
17
|
+
prompt = """Analyze this repository and list 3 files you see. Output in JSON:
|
|
18
|
+
{"files": ["file1", "file2", "file3"]}"""
|
|
19
|
+
|
|
20
|
+
cmd = [str(agent_path), "--print", "--workspace", str(repo_root), prompt]
|
|
21
|
+
|
|
22
|
+
print(f"Running: {' '.join(cmd[:3])} <prompt>")
|
|
23
|
+
print("Waiting for output...\n")
|
|
24
|
+
|
|
25
|
+
received_lines = []
|
|
26
|
+
|
|
27
|
+
def stream_callback(line: str):
|
|
28
|
+
print(f"CALLBACK: {line}")
|
|
29
|
+
received_lines.append(line)
|
|
30
|
+
|
|
31
|
+
# Test with Popen and line-by-line reading
|
|
32
|
+
process = subprocess.Popen(
|
|
33
|
+
cmd,
|
|
34
|
+
cwd=repo_root,
|
|
35
|
+
stdout=subprocess.PIPE,
|
|
36
|
+
stderr=subprocess.PIPE,
|
|
37
|
+
text=True,
|
|
38
|
+
bufsize=1, # Line buffered
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
output_lines = []
|
|
42
|
+
while True:
|
|
43
|
+
line = process.stdout.readline()
|
|
44
|
+
if not line and process.poll() is not None:
|
|
45
|
+
break
|
|
46
|
+
if line:
|
|
47
|
+
output_lines.append(line)
|
|
48
|
+
stream_callback(line.rstrip())
|
|
49
|
+
|
|
50
|
+
process.wait(timeout=60)
|
|
51
|
+
|
|
52
|
+
print(f"\nReceived {len(received_lines)} lines")
|
|
53
|
+
print(f"Return code: {process.returncode}")
|
|
54
|
+
|
|
55
|
+
if len(output_lines) > 0:
|
|
56
|
+
print("\nFull output:")
|
|
57
|
+
print("".join(output_lines[:50])) # First 50 lines
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
test_agent_streaming()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test parsing the agent response."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
# Sample agent output (based on what we saw in the stream)
|
|
8
|
+
agent_output = """Now I understand the context. This is the Draft backend project, and the goal is to add calculator functionality (multiplication and division) to the `app/utils` module. Based on the existing code structure and the previous ticket examples I found, I'll generate appropriate tickets.
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"tickets": [
|
|
13
|
+
{
|
|
14
|
+
"title": "Test ticket",
|
|
15
|
+
"description": "Test description",
|
|
16
|
+
"priority_bucket": "P1",
|
|
17
|
+
"priority_rationale": "Test rationale",
|
|
18
|
+
"verification": ["test command"],
|
|
19
|
+
"notes": "Test notes",
|
|
20
|
+
"blocked_by": null
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
```"""
|
|
25
|
+
|
|
26
|
+
print("Testing JSON extraction...")
|
|
27
|
+
|
|
28
|
+
# Try to extract JSON from markdown code block
|
|
29
|
+
json_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", agent_output, re.DOTALL)
|
|
30
|
+
if json_match:
|
|
31
|
+
json_str = json_match.group(1)
|
|
32
|
+
print(f"Found JSON in markdown: {len(json_str)} chars")
|
|
33
|
+
print(f"JSON preview: {json_str[:200]}...")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(json_str)
|
|
37
|
+
print("✅ Parsed successfully!")
|
|
38
|
+
print(f"Tickets: {len(data.get('tickets', []))}")
|
|
39
|
+
for ticket in data.get("tickets", []):
|
|
40
|
+
print(f" - {ticket.get('title')}")
|
|
41
|
+
except json.JSONDecodeError as e:
|
|
42
|
+
print(f"❌ JSON parse error: {e}")
|
|
43
|
+
else:
|
|
44
|
+
print("❌ No JSON found in markdown block")
|
|
45
|
+
|
|
46
|
+
# Also try parsing the full output
|
|
47
|
+
try:
|
|
48
|
+
data = json.loads(agent_output)
|
|
49
|
+
print(f"✅ Direct parse successful: {len(data.get('tickets', []))} tickets")
|
|
50
|
+
except:
|
|
51
|
+
print("❌ Direct parse failed (expected)")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test script to verify streaming callback works."""
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Add backend to path
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
10
|
+
|
|
11
|
+
from app.database import get_db
|
|
12
|
+
from app.services.ticket_generation_service import TicketGenerationService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def test_streaming():
|
|
16
|
+
"""Test streaming callback."""
|
|
17
|
+
print("Testing streaming...")
|
|
18
|
+
|
|
19
|
+
# Get DB session
|
|
20
|
+
async for db in get_db():
|
|
21
|
+
service = TicketGenerationService(db)
|
|
22
|
+
|
|
23
|
+
# Goal ID from earlier
|
|
24
|
+
goal_id = "b7e723f4-030f-45d9-8c6b-eae97fd5d72f"
|
|
25
|
+
|
|
26
|
+
received_lines = []
|
|
27
|
+
|
|
28
|
+
def stream_callback(line: str):
|
|
29
|
+
print(f"CALLBACK: {line}")
|
|
30
|
+
received_lines.append(line)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
result = await service.generate_from_goal(
|
|
34
|
+
goal_id=goal_id,
|
|
35
|
+
include_readme=False,
|
|
36
|
+
validate_tickets=False, # Skip validation for test
|
|
37
|
+
stream_callback=stream_callback,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
print(f"\nReceived {len(received_lines)} lines from agent")
|
|
41
|
+
print(f"Generated {len(result.tickets)} tickets")
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Error: {e}")
|
|
45
|
+
import traceback
|
|
46
|
+
|
|
47
|
+
traceback.print_exc()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
asyncio.run(test_streaming())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test subprocess streaming directly."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_subprocess_streaming():
|
|
9
|
+
"""Test that subprocess.Popen can stream output."""
|
|
10
|
+
print("Testing subprocess streaming...")
|
|
11
|
+
|
|
12
|
+
# Get repo root
|
|
13
|
+
repo_root = Path(__file__).parent.parent
|
|
14
|
+
|
|
15
|
+
# Simple test command that produces output
|
|
16
|
+
cmd = ["echo", "Line 1\nLine 2\nLine 3"]
|
|
17
|
+
|
|
18
|
+
received_lines = []
|
|
19
|
+
|
|
20
|
+
def stream_callback(line: str):
|
|
21
|
+
print(f"CALLBACK: {line}")
|
|
22
|
+
received_lines.append(line)
|
|
23
|
+
|
|
24
|
+
# Test with Popen and line-by-line reading
|
|
25
|
+
process = subprocess.Popen(
|
|
26
|
+
cmd,
|
|
27
|
+
cwd=repo_root,
|
|
28
|
+
stdout=subprocess.PIPE,
|
|
29
|
+
stderr=subprocess.PIPE,
|
|
30
|
+
text=True,
|
|
31
|
+
bufsize=1, # Line buffered
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
output_lines = []
|
|
35
|
+
while True:
|
|
36
|
+
line = process.stdout.readline()
|
|
37
|
+
if not line and process.poll() is not None:
|
|
38
|
+
break
|
|
39
|
+
if line:
|
|
40
|
+
output_lines.append(line)
|
|
41
|
+
stream_callback(line.rstrip())
|
|
42
|
+
|
|
43
|
+
process.wait()
|
|
44
|
+
|
|
45
|
+
print(f"\nReceived {len(received_lines)} lines")
|
|
46
|
+
print(f"Return code: {process.returncode}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
test_subprocess_streaming()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for Draft backend."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Pytest configuration and fixtures for tests."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
7
|
+
from sqlalchemy.pool import StaticPool
|
|
8
|
+
|
|
9
|
+
from app.models.base import Base
|
|
10
|
+
|
|
11
|
+
# Use in-memory SQLite for tests
|
|
12
|
+
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope="function")
|
|
16
|
+
async def db() -> AsyncGenerator[AsyncSession, None]:
|
|
17
|
+
"""Create a fresh database session for each test.
|
|
18
|
+
|
|
19
|
+
Uses in-memory SQLite with a single connection to ensure
|
|
20
|
+
all operations see the same data.
|
|
21
|
+
"""
|
|
22
|
+
engine = create_async_engine(
|
|
23
|
+
TEST_DATABASE_URL,
|
|
24
|
+
echo=False,
|
|
25
|
+
connect_args={"check_same_thread": False},
|
|
26
|
+
poolclass=StaticPool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Create all tables
|
|
30
|
+
async with engine.begin() as conn:
|
|
31
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
32
|
+
|
|
33
|
+
# Create session
|
|
34
|
+
async_session = async_sessionmaker(
|
|
35
|
+
engine,
|
|
36
|
+
class_=AsyncSession,
|
|
37
|
+
expire_on_commit=False,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async with async_session() as session:
|
|
41
|
+
yield session
|
|
42
|
+
|
|
43
|
+
# Clean up
|
|
44
|
+
async with engine.begin() as conn:
|
|
45
|
+
await conn.run_sync(Base.metadata.drop_all)
|
|
46
|
+
await engine.dispose()
|