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.
Files changed (250) hide show
  1. package/app/backend/.env.example +9 -0
  2. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
  3. package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
  4. package/app/backend/CURL_EXAMPLES.md +335 -0
  5. package/app/backend/ENV_SETUP.md +65 -0
  6. package/app/backend/alembic/env.py +71 -0
  7. package/app/backend/alembic/script.py.mako +28 -0
  8. package/app/backend/alembic/versions/001_initial_schema.py +104 -0
  9. package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
  10. package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
  11. package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
  12. package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
  13. package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
  14. package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
  15. package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
  16. package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
  17. package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
  18. package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
  19. package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
  20. package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
  21. package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
  22. package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
  23. package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
  24. package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
  25. package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
  26. package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
  27. package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
  28. package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
  29. package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
  30. package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
  31. package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
  32. package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
  33. package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
  34. package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
  35. package/app/backend/alembic/versions/add_job_variant.py +34 -0
  36. package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
  37. package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
  38. package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
  39. package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
  40. package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
  41. package/app/backend/alembic.ini +89 -0
  42. package/app/backend/app/__init__.py +3 -0
  43. package/app/backend/app/data_dir.py +85 -0
  44. package/app/backend/app/database.py +70 -0
  45. package/app/backend/app/database_sync.py +64 -0
  46. package/app/backend/app/dependencies/__init__.py +5 -0
  47. package/app/backend/app/dependencies/auth.py +80 -0
  48. package/app/backend/app/dependencies.py +43 -0
  49. package/app/backend/app/exceptions.py +178 -0
  50. package/app/backend/app/executors/__init__.py +1 -0
  51. package/app/backend/app/executors/adapters/__init__.py +1 -0
  52. package/app/backend/app/executors/adapters/aider.py +152 -0
  53. package/app/backend/app/executors/adapters/amazon_q.py +103 -0
  54. package/app/backend/app/executors/adapters/amp.py +123 -0
  55. package/app/backend/app/executors/adapters/claude.py +177 -0
  56. package/app/backend/app/executors/adapters/cline.py +127 -0
  57. package/app/backend/app/executors/adapters/codex.py +167 -0
  58. package/app/backend/app/executors/adapters/copilot.py +202 -0
  59. package/app/backend/app/executors/adapters/cursor.py +87 -0
  60. package/app/backend/app/executors/adapters/droid.py +123 -0
  61. package/app/backend/app/executors/adapters/gemini.py +132 -0
  62. package/app/backend/app/executors/adapters/goose.py +131 -0
  63. package/app/backend/app/executors/adapters/opencode.py +123 -0
  64. package/app/backend/app/executors/adapters/qwen.py +123 -0
  65. package/app/backend/app/executors/plugins/__init__.py +1 -0
  66. package/app/backend/app/executors/registry.py +202 -0
  67. package/app/backend/app/executors/spec.py +226 -0
  68. package/app/backend/app/main.py +486 -0
  69. package/app/backend/app/middleware/__init__.py +13 -0
  70. package/app/backend/app/middleware/idempotency.py +426 -0
  71. package/app/backend/app/middleware/rate_limit.py +312 -0
  72. package/app/backend/app/middleware/security_headers.py +43 -0
  73. package/app/backend/app/middleware/timeout.py +37 -0
  74. package/app/backend/app/models/__init__.py +56 -0
  75. package/app/backend/app/models/agent_conversation_history.py +56 -0
  76. package/app/backend/app/models/agent_session.py +127 -0
  77. package/app/backend/app/models/analysis_cache.py +49 -0
  78. package/app/backend/app/models/base.py +9 -0
  79. package/app/backend/app/models/board.py +79 -0
  80. package/app/backend/app/models/board_repo.py +68 -0
  81. package/app/backend/app/models/cost_budget.py +42 -0
  82. package/app/backend/app/models/enums.py +40 -0
  83. package/app/backend/app/models/evidence.py +132 -0
  84. package/app/backend/app/models/goal.py +102 -0
  85. package/app/backend/app/models/idempotency_entry.py +30 -0
  86. package/app/backend/app/models/job.py +163 -0
  87. package/app/backend/app/models/job_queue.py +39 -0
  88. package/app/backend/app/models/kv_store.py +28 -0
  89. package/app/backend/app/models/merge_checklist.py +87 -0
  90. package/app/backend/app/models/normalized_log.py +100 -0
  91. package/app/backend/app/models/planner_lock.py +43 -0
  92. package/app/backend/app/models/rate_limit_entry.py +25 -0
  93. package/app/backend/app/models/repo.py +66 -0
  94. package/app/backend/app/models/review_comment.py +91 -0
  95. package/app/backend/app/models/review_summary.py +69 -0
  96. package/app/backend/app/models/revision.py +130 -0
  97. package/app/backend/app/models/ticket.py +223 -0
  98. package/app/backend/app/models/ticket_event.py +83 -0
  99. package/app/backend/app/models/user.py +47 -0
  100. package/app/backend/app/models/workspace.py +71 -0
  101. package/app/backend/app/redis_client.py +119 -0
  102. package/app/backend/app/routers/__init__.py +29 -0
  103. package/app/backend/app/routers/agents.py +296 -0
  104. package/app/backend/app/routers/auth.py +94 -0
  105. package/app/backend/app/routers/board.py +885 -0
  106. package/app/backend/app/routers/dashboard.py +351 -0
  107. package/app/backend/app/routers/debug.py +528 -0
  108. package/app/backend/app/routers/evidence.py +96 -0
  109. package/app/backend/app/routers/executors.py +324 -0
  110. package/app/backend/app/routers/goals.py +574 -0
  111. package/app/backend/app/routers/jobs.py +448 -0
  112. package/app/backend/app/routers/maintenance.py +172 -0
  113. package/app/backend/app/routers/merge.py +360 -0
  114. package/app/backend/app/routers/planner.py +537 -0
  115. package/app/backend/app/routers/pull_requests.py +382 -0
  116. package/app/backend/app/routers/repos.py +263 -0
  117. package/app/backend/app/routers/revisions.py +939 -0
  118. package/app/backend/app/routers/settings.py +267 -0
  119. package/app/backend/app/routers/tickets.py +2003 -0
  120. package/app/backend/app/routers/webhooks.py +143 -0
  121. package/app/backend/app/routers/websocket.py +249 -0
  122. package/app/backend/app/schemas/__init__.py +109 -0
  123. package/app/backend/app/schemas/board.py +87 -0
  124. package/app/backend/app/schemas/common.py +33 -0
  125. package/app/backend/app/schemas/evidence.py +87 -0
  126. package/app/backend/app/schemas/goal.py +90 -0
  127. package/app/backend/app/schemas/job.py +97 -0
  128. package/app/backend/app/schemas/merge.py +139 -0
  129. package/app/backend/app/schemas/planner.py +500 -0
  130. package/app/backend/app/schemas/repo.py +187 -0
  131. package/app/backend/app/schemas/review.py +137 -0
  132. package/app/backend/app/schemas/revision.py +114 -0
  133. package/app/backend/app/schemas/ticket.py +238 -0
  134. package/app/backend/app/schemas/ticket_event.py +72 -0
  135. package/app/backend/app/schemas/workspace.py +19 -0
  136. package/app/backend/app/services/__init__.py +31 -0
  137. package/app/backend/app/services/agent_memory_service.py +223 -0
  138. package/app/backend/app/services/agent_registry.py +346 -0
  139. package/app/backend/app/services/agent_session_manager.py +318 -0
  140. package/app/backend/app/services/agent_session_service.py +219 -0
  141. package/app/backend/app/services/agent_tools.py +379 -0
  142. package/app/backend/app/services/auth_service.py +98 -0
  143. package/app/backend/app/services/autonomy_service.py +380 -0
  144. package/app/backend/app/services/board_repo_service.py +201 -0
  145. package/app/backend/app/services/board_service.py +326 -0
  146. package/app/backend/app/services/cleanup_service.py +1085 -0
  147. package/app/backend/app/services/config_service.py +908 -0
  148. package/app/backend/app/services/context_gatherer.py +557 -0
  149. package/app/backend/app/services/cost_tracking_service.py +293 -0
  150. package/app/backend/app/services/cursor_log_normalizer.py +536 -0
  151. package/app/backend/app/services/delivery_pipeline.py +440 -0
  152. package/app/backend/app/services/executor_service.py +634 -0
  153. package/app/backend/app/services/git_host/__init__.py +11 -0
  154. package/app/backend/app/services/git_host/factory.py +87 -0
  155. package/app/backend/app/services/git_host/github.py +270 -0
  156. package/app/backend/app/services/git_host/gitlab.py +194 -0
  157. package/app/backend/app/services/git_host/protocol.py +75 -0
  158. package/app/backend/app/services/git_merge_simple.py +346 -0
  159. package/app/backend/app/services/git_ops.py +384 -0
  160. package/app/backend/app/services/github_service.py +233 -0
  161. package/app/backend/app/services/goal_service.py +113 -0
  162. package/app/backend/app/services/job_service.py +423 -0
  163. package/app/backend/app/services/job_watchdog_service.py +424 -0
  164. package/app/backend/app/services/langchain_adapter.py +122 -0
  165. package/app/backend/app/services/llm_provider_clients.py +351 -0
  166. package/app/backend/app/services/llm_service.py +285 -0
  167. package/app/backend/app/services/log_normalizer.py +342 -0
  168. package/app/backend/app/services/log_stream_service.py +276 -0
  169. package/app/backend/app/services/merge_checklist_service.py +264 -0
  170. package/app/backend/app/services/merge_service.py +784 -0
  171. package/app/backend/app/services/orchestrator_log.py +84 -0
  172. package/app/backend/app/services/planner_service.py +1662 -0
  173. package/app/backend/app/services/planner_tick_sync.py +1040 -0
  174. package/app/backend/app/services/queued_message_service.py +156 -0
  175. package/app/backend/app/services/reliability_wrapper.py +389 -0
  176. package/app/backend/app/services/repo_discovery_service.py +318 -0
  177. package/app/backend/app/services/review_service.py +334 -0
  178. package/app/backend/app/services/revision_service.py +389 -0
  179. package/app/backend/app/services/safe_autopilot.py +510 -0
  180. package/app/backend/app/services/sqlite_worker.py +372 -0
  181. package/app/backend/app/services/task_dispatch.py +135 -0
  182. package/app/backend/app/services/ticket_generation_service.py +1781 -0
  183. package/app/backend/app/services/ticket_service.py +486 -0
  184. package/app/backend/app/services/udar_planner_service.py +1007 -0
  185. package/app/backend/app/services/webhook_service.py +126 -0
  186. package/app/backend/app/services/workspace_service.py +465 -0
  187. package/app/backend/app/services/worktree_file_service.py +92 -0
  188. package/app/backend/app/services/worktree_validator.py +213 -0
  189. package/app/backend/app/sqlite_kv.py +278 -0
  190. package/app/backend/app/state_machine.py +128 -0
  191. package/app/backend/app/templates/__init__.py +5 -0
  192. package/app/backend/app/templates/registry.py +243 -0
  193. package/app/backend/app/utils/__init__.py +5 -0
  194. package/app/backend/app/utils/artifact_reader.py +87 -0
  195. package/app/backend/app/utils/circuit_breaker.py +229 -0
  196. package/app/backend/app/utils/db_retry.py +136 -0
  197. package/app/backend/app/utils/ignored_fields.py +123 -0
  198. package/app/backend/app/utils/validators.py +54 -0
  199. package/app/backend/app/websocket/__init__.py +5 -0
  200. package/app/backend/app/websocket/manager.py +179 -0
  201. package/app/backend/app/websocket/state_tracker.py +113 -0
  202. package/app/backend/app/worker.py +3190 -0
  203. package/app/backend/calculator_tickets.json +40 -0
  204. package/app/backend/canary_tests.sh +591 -0
  205. package/app/backend/celerybeat-schedule +0 -0
  206. package/app/backend/celerybeat-schedule-shm +0 -0
  207. package/app/backend/celerybeat-schedule-wal +0 -0
  208. package/app/backend/logs/.gitkeep +3 -0
  209. package/app/backend/multiplication_division_implementation_tickets.json +55 -0
  210. package/app/backend/multiplication_division_tickets.json +42 -0
  211. package/app/backend/pyproject.toml +45 -0
  212. package/app/backend/requirements-dev.txt +8 -0
  213. package/app/backend/requirements.txt +20 -0
  214. package/app/backend/run.sh +30 -0
  215. package/app/backend/run_with_logs.sh +10 -0
  216. package/app/backend/scientific_calculator_tickets.json +40 -0
  217. package/app/backend/scripts/extract_openapi.py +21 -0
  218. package/app/backend/scripts/seed_demo.py +187 -0
  219. package/app/backend/setup_demo_review.py +302 -0
  220. package/app/backend/test_actual_parse.py +41 -0
  221. package/app/backend/test_agent_streaming.py +61 -0
  222. package/app/backend/test_parse.py +51 -0
  223. package/app/backend/test_streaming.py +51 -0
  224. package/app/backend/test_subprocess_streaming.py +50 -0
  225. package/app/backend/tests/__init__.py +1 -0
  226. package/app/backend/tests/conftest.py +46 -0
  227. package/app/backend/tests/test_auth.py +341 -0
  228. package/app/backend/tests/test_autonomy_service.py +391 -0
  229. package/app/backend/tests/test_cleanup_service_safety.py +417 -0
  230. package/app/backend/tests/test_middleware.py +279 -0
  231. package/app/backend/tests/test_planner_providers.py +290 -0
  232. package/app/backend/tests/test_planner_unblock.py +183 -0
  233. package/app/backend/tests/test_revision_invariants.py +618 -0
  234. package/app/backend/tests/test_sqlite_kv.py +290 -0
  235. package/app/backend/tests/test_sqlite_worker.py +353 -0
  236. package/app/backend/tests/test_task_dispatch.py +100 -0
  237. package/app/backend/tests/test_ticket_validation.py +304 -0
  238. package/app/backend/tests/test_udar_agent.py +693 -0
  239. package/app/backend/tests/test_webhook_service.py +184 -0
  240. package/app/backend/tickets_output.json +59 -0
  241. package/app/backend/user_management_tickets.json +50 -0
  242. package/app/backend/uvicorn.log +0 -0
  243. package/app/draft.yaml +313 -0
  244. package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
  245. package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
  246. package/app/frontend/dist/index.html +14 -0
  247. package/app/frontend/dist/vite.svg +1 -0
  248. package/app/frontend/package.json +101 -0
  249. package/bin/cli.js +527 -0
  250. 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()