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,40 @@
1
+ {
2
+ "tickets": [
3
+ {
4
+ "title": "Implement calculator module with scientific operations",
5
+ "description": "Create app/utils/calculator.py with six scientific calculator functions:\n\n1. multiply(a, b) - multiply two numbers\n2. divide(a, b) - divide with ZeroDivisionError handling when b is 0\n3. power(base, exp) - raise base to exponent power\n4. sqrt(n) - square root with ValueError for negative numbers\n5. modulo(a, b) - remainder operation\n6. factorial(n) - factorial for non-negative integers (raise ValueError for negative)\n\nAll functions must:\n- Include proper type hints (use float | int for numeric types)\n- Have comprehensive docstrings following Google/NumPy style\n- Handle edge cases appropriately (division by zero, negative sqrt, negative factorial)\n- Export functions in app/utils/__init__.py\n\nAcceptance criteria:\n- File app/utils/calculator.py exists with all 6 functions\n- All functions have type hints and docstrings\n- Functions raise appropriate exceptions for invalid inputs\n- Functions are exported in app/utils/__init__.py",
6
+ "priority_bucket": "P1",
7
+ "priority_rationale": "Core feature implementation - required foundation for calculator functionality",
8
+ "verification": [
9
+ "test -f app/utils/calculator.py",
10
+ "python -c 'from app.utils import multiply, divide, power, sqrt, modulo, factorial; assert multiply(2, 3) == 6'",
11
+ "python -c 'from app.utils.calculator import divide; import pytest; pytest.raises(ZeroDivisionError, divide, 1, 0)'"
12
+ ],
13
+ "notes": "Use math.sqrt and math.factorial from standard library. For modulo, use % operator. Ensure type hints support both int and float inputs."
14
+ },
15
+ {
16
+ "title": "Add comprehensive test coverage for calculator module",
17
+ "description": "Create tests/test_calculator.py with comprehensive test coverage for all calculator functions.\n\nTest requirements:\n- Test normal operation for each function with various inputs\n- Test error cases: ZeroDivisionError for divide(1, 0), ValueError for sqrt(-1) and factorial(-1)\n- Test edge cases: factorial(0) = 1, power(2, 0) = 1, modulo with negative numbers\n- Test type handling: ensure functions work with both int and float inputs\n- Use pytest fixtures where appropriate\n- Follow existing test patterns from tests/test_middleware.py\n\nAcceptance criteria:\n- File tests/test_calculator.py exists\n- All 6 functions have test coverage\n- Error cases are tested and raise correct exceptions\n- Edge cases are covered\n- Tests pass with pytest",
18
+ "priority_bucket": "P1",
19
+ "priority_rationale": "Critical for ensuring code quality and preventing regressions - tests validate correctness of core functionality",
20
+ "verification": [
21
+ "test -f tests/test_calculator.py",
22
+ "pytest tests/test_calculator.py -v",
23
+ "pytest tests/test_calculator.py --cov=app.utils.calculator --cov-report=term-missing | grep -q '100%' || pytest tests/test_calculator.py --cov=app.utils.calculator"
24
+ ],
25
+ "notes": "Use pytest.raises context manager for exception testing. Consider parametrize for testing multiple input combinations."
26
+ },
27
+ {
28
+ "title": "Update README.md to document calculator module",
29
+ "description": "Create or update README.md in project root to document the calculator module and its scientific operations.\n\nDocumentation should include:\n- Overview of the calculator module\n- List of available functions with brief descriptions\n- Usage examples for each function\n- Error handling information (what exceptions are raised and when)\n- Installation/setup instructions if needed\n- Link to test file for examples\n\nAcceptance criteria:\n- README.md exists in project root\n- Calculator module is documented with all 6 functions\n- Usage examples are provided\n- Error handling is documented\n- Documentation is clear and follows project conventions",
30
+ "priority_bucket": "P2",
31
+ "priority_rationale": "Important for usability and maintainability, but not blocking for functionality - documentation helps users understand the module",
32
+ "verification": [
33
+ "test -f README.md",
34
+ "grep -q 'calculator' README.md",
35
+ "grep -q 'multiply\\|divide\\|power\\|sqrt\\|modulo\\|factorial' README.md"
36
+ ],
37
+ "notes": "If README.md doesn't exist, create a new one. If it exists, add a new section for the calculator module. Include code examples using triple backticks."
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1,591 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # CANARY TESTS FOR ALMA KANBAN EXECUTOR INTEGRATION
4
+ # =============================================================================
5
+ #
6
+ # Prerequisites:
7
+ # 1. Redis running: redis-server
8
+ # 2. Backend running: cd backend && source venv/bin/activate && uvicorn app.main:app --reload
9
+ # 3. Worker running: cd backend && source venv/bin/activate && celery -A app.worker worker --loglevel=info
10
+ #
11
+ # Usage:
12
+ # ./canary_tests.sh [test_number]
13
+ # ./canary_tests.sh # Run all tests
14
+ # ./canary_tests.sh 1 # Run only Canary 1
15
+ # ./canary_tests.sh 4 # Run only Canary 4
16
+
17
+ set -e
18
+
19
+ API_BASE="${API_BASE:-http://localhost:8000}"
20
+ POLL_INTERVAL=2
21
+ MAX_POLLS=30
22
+
23
+ # Colors for output
24
+ RED='\033[0;31m'
25
+ GREEN='\033[0;32m'
26
+ YELLOW='\033[1;33m'
27
+ NC='\033[0m' # No Color
28
+
29
+ log_info() {
30
+ echo -e "${GREEN}[INFO]${NC} $1"
31
+ }
32
+
33
+ log_warn() {
34
+ echo -e "${YELLOW}[WARN]${NC} $1"
35
+ }
36
+
37
+ log_error() {
38
+ echo -e "${RED}[ERROR]${NC} $1"
39
+ }
40
+
41
+ log_step() {
42
+ echo -e "\n${GREEN}>>> $1${NC}"
43
+ }
44
+
45
+ # Create a goal and return its ID
46
+ create_goal() {
47
+ local title="$1"
48
+ local response=$(curl -s -X POST "$API_BASE/goals" \
49
+ -H "Content-Type: application/json" \
50
+ -d "{\"title\": \"$title\"}")
51
+ echo "$response" | jq -r '.id'
52
+ }
53
+
54
+ # Create a ticket and return its ID
55
+ create_ticket() {
56
+ local goal_id="$1"
57
+ local title="$2"
58
+ local description="${3:-}"
59
+ local response=$(curl -s -X POST "$API_BASE/tickets" \
60
+ -H "Content-Type: application/json" \
61
+ -d "{\"goal_id\": \"$goal_id\", \"title\": \"$title\", \"description\": \"$description\"}")
62
+ echo "$response" | jq -r '.id'
63
+ }
64
+
65
+ # Get ticket state
66
+ get_ticket_state() {
67
+ local ticket_id="$1"
68
+ curl -s "$API_BASE/tickets/$ticket_id" | jq -r '.state'
69
+ }
70
+
71
+ # Get ticket details
72
+ get_ticket() {
73
+ local ticket_id="$1"
74
+ curl -s "$API_BASE/tickets/$ticket_id"
75
+ }
76
+
77
+ # Run execute job
78
+ run_execute() {
79
+ local ticket_id="$1"
80
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/run"
81
+ }
82
+
83
+ # Run verify job
84
+ run_verify() {
85
+ local ticket_id="$1"
86
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/verify"
87
+ }
88
+
89
+ # Run resume (for interactive flow)
90
+ run_resume() {
91
+ local ticket_id="$1"
92
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/resume"
93
+ }
94
+
95
+ # Get jobs for ticket
96
+ get_jobs() {
97
+ local ticket_id="$1"
98
+ curl -s "$API_BASE/tickets/$ticket_id/jobs"
99
+ }
100
+
101
+ # Get evidence for ticket
102
+ get_evidence() {
103
+ local ticket_id="$1"
104
+ curl -s "$API_BASE/tickets/$ticket_id/evidence"
105
+ }
106
+
107
+ # Wait for ticket to reach a state (or one of several states)
108
+ wait_for_state() {
109
+ local ticket_id="$1"
110
+ shift
111
+ local expected_states=("$@")
112
+ local polls=0
113
+
114
+ log_info "Waiting for ticket to reach state: ${expected_states[*]}"
115
+
116
+ while [ $polls -lt $MAX_POLLS ]; do
117
+ local current_state=$(get_ticket_state "$ticket_id")
118
+
119
+ for expected in "${expected_states[@]}"; do
120
+ if [ "$current_state" = "$expected" ]; then
121
+ log_info "Ticket reached state: $current_state"
122
+ return 0
123
+ fi
124
+ done
125
+
126
+ log_info "Current state: $current_state (poll $((polls + 1))/$MAX_POLLS)"
127
+ sleep $POLL_INTERVAL
128
+ ((polls++))
129
+ done
130
+
131
+ log_error "Timeout waiting for state. Current: $current_state, Expected: ${expected_states[*]}"
132
+ return 1
133
+ }
134
+
135
+ # Verify evidence exists
136
+ verify_evidence() {
137
+ local ticket_id="$1"
138
+ local expected_kinds=("$@")
139
+
140
+ local evidence=$(get_evidence "$ticket_id")
141
+ local found_kinds=$(echo "$evidence" | jq -r '.evidence[].kind' | sort -u)
142
+
143
+ log_info "Found evidence kinds: $found_kinds"
144
+
145
+ for kind in "${expected_kinds[@]:1}"; do
146
+ if ! echo "$found_kinds" | grep -q "$kind"; then
147
+ log_warn "Missing evidence kind: $kind"
148
+ fi
149
+ done
150
+ }
151
+
152
+ # =============================================================================
153
+ # CANARY 1: Trivial Change (Claude Headless)
154
+ # =============================================================================
155
+ # Expected flow:
156
+ # proposed → executing → verifying → needs_human (default on_success)
157
+ # Evidence:
158
+ # executor_stdout, executor_meta, git_diff_stat, git_diff_patch,
159
+ # verify_stdout, verify_meta
160
+ # =============================================================================
161
+ canary_1() {
162
+ log_step "CANARY 1: Trivial Change (Claude Headless)"
163
+
164
+ log_info "Creating goal..."
165
+ local goal_id=$(create_goal "Canary Test Goal 1")
166
+ log_info "Goal ID: $goal_id"
167
+
168
+ log_info "Creating ticket..."
169
+ local ticket_id=$(create_ticket "$goal_id" "Add a comment to README" "Add a comment at the top of README.md")
170
+ log_info "Ticket ID: $ticket_id"
171
+
172
+ log_info "Transitioning to planned then executing..."
173
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
174
+ -H "Content-Type: application/json" \
175
+ -d '{"to_state": "planned", "actor_type": "human", "reason": "Planning canary test"}'
176
+
177
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
178
+ -H "Content-Type: application/json" \
179
+ -d '{"to_state": "executing", "actor_type": "human", "reason": "Executing canary test"}'
180
+
181
+ log_info "Running execute job..."
182
+ local execute_response=$(run_execute "$ticket_id")
183
+ log_info "Execute job: $(echo "$execute_response" | jq -c '{id, status}')"
184
+
185
+ # Wait for verification to complete (auto-triggered)
186
+ # First wait for verifying, then wait for needs_human (or blocked if failure)
187
+ wait_for_state "$ticket_id" "verifying" "blocked" "needs_human"
188
+
189
+ local state=$(get_ticket_state "$ticket_id")
190
+ if [ "$state" = "verifying" ]; then
191
+ log_info "In verifying state, waiting for verification to complete..."
192
+ wait_for_state "$ticket_id" "needs_human" "blocked" "done"
193
+ fi
194
+
195
+ # Check final state
196
+ local final_state=$(get_ticket_state "$ticket_id")
197
+ log_info "Final state: $final_state"
198
+
199
+ # Verify evidence
200
+ log_info "Checking evidence..."
201
+ verify_evidence "$ticket_id" executor_meta executor_stdout git_diff_stat git_diff_patch verify_meta verify_stdout
202
+
203
+ # Show evidence summary
204
+ local evidence=$(get_evidence "$ticket_id")
205
+ log_info "Evidence count: $(echo "$evidence" | jq '.total')"
206
+
207
+ if [ "$final_state" = "needs_human" ] || [ "$final_state" = "done" ]; then
208
+ log_info "✅ CANARY 1 PASSED: Ticket reached expected state ($final_state)"
209
+ elif [ "$final_state" = "blocked" ]; then
210
+ log_warn "⚠️ CANARY 1 PARTIAL: Ticket blocked (possibly no changes or executor failed)"
211
+ log_info "Check job logs for details"
212
+ else
213
+ log_error "❌ CANARY 1 FAILED: Unexpected state ($final_state)"
214
+ return 1
215
+ fi
216
+ }
217
+
218
+ # =============================================================================
219
+ # CANARY 2: No-op (No Changes)
220
+ # =============================================================================
221
+ # Expected flow:
222
+ # proposed → executing → blocked (reason: no changes)
223
+ # =============================================================================
224
+ canary_2() {
225
+ log_step "CANARY 2: No-op (No Changes)"
226
+
227
+ log_info "Creating goal..."
228
+ local goal_id=$(create_goal "Canary Test Goal 2")
229
+ log_info "Goal ID: $goal_id"
230
+
231
+ log_info "Creating ticket..."
232
+ local ticket_id=$(create_ticket "$goal_id" "Review code, no changes needed" "Just review the code structure, don't make any changes")
233
+ log_info "Ticket ID: $ticket_id"
234
+
235
+ log_info "Transitioning to planned then executing..."
236
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
237
+ -H "Content-Type: application/json" \
238
+ -d '{"to_state": "planned", "actor_type": "human", "reason": "Planning canary test"}'
239
+
240
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
241
+ -H "Content-Type: application/json" \
242
+ -d '{"to_state": "executing", "actor_type": "human", "reason": "Executing canary test"}'
243
+
244
+ log_info "Running execute job..."
245
+ local execute_response=$(run_execute "$ticket_id")
246
+ log_info "Execute job: $(echo "$execute_response" | jq -c '{id, status}')"
247
+
248
+ # Wait for blocked state (no changes should result in blocked)
249
+ wait_for_state "$ticket_id" "blocked" "verifying" "needs_human"
250
+
251
+ # Check final state
252
+ local final_state=$(get_ticket_state "$ticket_id")
253
+ log_info "Final state: $final_state"
254
+
255
+ # Get events to check reason
256
+ local events=$(curl -s "$API_BASE/tickets/$ticket_id/events")
257
+ local last_reason=$(echo "$events" | jq -r '.events[-1].reason')
258
+ log_info "Last transition reason: $last_reason"
259
+
260
+ if [ "$final_state" = "blocked" ]; then
261
+ if echo "$last_reason" | grep -qi "no.*change"; then
262
+ log_info "✅ CANARY 2 PASSED: Ticket blocked with 'no changes' reason"
263
+ else
264
+ log_warn "⚠️ CANARY 2 PARTIAL: Ticket blocked but reason unclear"
265
+ fi
266
+ else
267
+ log_warn "⚠️ CANARY 2 UNEXPECTED: State is $final_state (expected blocked)"
268
+ fi
269
+ }
270
+
271
+ # =============================================================================
272
+ # CANARY 3: YOLO Refusal
273
+ # =============================================================================
274
+ # Expected flow:
275
+ # executing → needs_human (YOLO refused due to empty allowlist)
276
+ #
277
+ # NOTE: This test requires yolo_mode: true in draft.yaml
278
+ # with an empty yolo_allowlist
279
+ # =============================================================================
280
+ canary_3() {
281
+ log_step "CANARY 3: YOLO Refusal (requires yolo_mode: true, empty allowlist)"
282
+
283
+ log_warn "This test requires modifying draft.yaml:"
284
+ log_warn " yolo_mode: true"
285
+ log_warn " yolo_allowlist: []"
286
+ log_warn ""
287
+ log_warn "If not configured, this test will behave like Canary 1."
288
+
289
+ log_info "Creating goal..."
290
+ local goal_id=$(create_goal "Canary Test Goal 3")
291
+ log_info "Goal ID: $goal_id"
292
+
293
+ log_info "Creating ticket..."
294
+ local ticket_id=$(create_ticket "$goal_id" "Test YOLO refusal" "This should be refused if YOLO is enabled with empty allowlist")
295
+ log_info "Ticket ID: $ticket_id"
296
+
297
+ log_info "Transitioning to planned then executing..."
298
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
299
+ -H "Content-Type: application/json" \
300
+ -d '{"to_state": "planned", "actor_type": "human", "reason": "Planning canary test"}'
301
+
302
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
303
+ -H "Content-Type: application/json" \
304
+ -d '{"to_state": "executing", "actor_type": "human", "reason": "Executing canary test"}'
305
+
306
+ log_info "Running execute job..."
307
+ local execute_response=$(run_execute "$ticket_id")
308
+ log_info "Execute job: $(echo "$execute_response" | jq -c '{id, status}')"
309
+
310
+ # Wait for needs_human (YOLO refused) or other states
311
+ wait_for_state "$ticket_id" "needs_human" "verifying" "blocked"
312
+
313
+ # Check final state
314
+ local final_state=$(get_ticket_state "$ticket_id")
315
+ log_info "Final state: $final_state"
316
+
317
+ # Get events to check reason
318
+ local events=$(curl -s "$API_BASE/tickets/$ticket_id/events")
319
+ local last_reason=$(echo "$events" | jq -r '.events[-1].reason')
320
+ log_info "Last transition reason: $last_reason"
321
+
322
+ if [ "$final_state" = "needs_human" ]; then
323
+ if echo "$last_reason" | grep -qi "yolo.*refused\|allowlist"; then
324
+ log_info "✅ CANARY 3 PASSED: YOLO refused with proper reason"
325
+ else
326
+ log_warn "⚠️ CANARY 3 UNCERTAIN: needs_human but reason doesn't mention YOLO"
327
+ log_info "Reason: $last_reason"
328
+ fi
329
+ else
330
+ log_warn "⚠️ CANARY 3 NOTE: YOLO mode may not be enabled ($final_state)"
331
+ fi
332
+ }
333
+
334
+ # =============================================================================
335
+ # CANARY 4: Cursor Interactive + Resume
336
+ # =============================================================================
337
+ # Expected flow:
338
+ # executing → needs_human (interactive executor)
339
+ # [human makes changes]
340
+ # resume → verifying → needs_human (or blocked if no changes)
341
+ #
342
+ # NOTE: This test requires preferred_executor: "cursor" in draft.yaml
343
+ # OR cursor CLI must be the only available executor
344
+ # =============================================================================
345
+ canary_4() {
346
+ log_step "CANARY 4: Cursor Interactive + Resume"
347
+
348
+ log_warn "This test requires either:"
349
+ log_warn " 1. preferred_executor: 'cursor' in draft.yaml"
350
+ log_warn " 2. OR: Only Cursor CLI available (no Claude CLI)"
351
+ log_warn ""
352
+ log_warn "If Claude is available and preferred, this test will behave like Canary 1."
353
+
354
+ log_info "Creating goal..."
355
+ local goal_id=$(create_goal "Canary Test Goal 4")
356
+ log_info "Goal ID: $goal_id"
357
+
358
+ log_info "Creating ticket..."
359
+ local ticket_id=$(create_ticket "$goal_id" "Interactive change test" "This requires human interaction if using Cursor")
360
+ log_info "Ticket ID: $ticket_id"
361
+
362
+ log_info "Transitioning to planned then executing..."
363
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
364
+ -H "Content-Type: application/json" \
365
+ -d '{"to_state": "planned", "actor_type": "human", "reason": "Planning canary test"}'
366
+
367
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
368
+ -H "Content-Type: application/json" \
369
+ -d '{"to_state": "executing", "actor_type": "human", "reason": "Executing canary test"}'
370
+
371
+ log_info "Running execute job..."
372
+ local execute_response=$(run_execute "$ticket_id")
373
+ log_info "Execute job: $(echo "$execute_response" | jq -c '{id, status}')"
374
+
375
+ # Wait for initial state
376
+ wait_for_state "$ticket_id" "needs_human" "verifying" "blocked"
377
+
378
+ local state_after_execute=$(get_ticket_state "$ticket_id")
379
+ log_info "State after execute: $state_after_execute"
380
+
381
+ if [ "$state_after_execute" = "needs_human" ]; then
382
+ log_info "Ticket is in needs_human state (interactive mode)"
383
+
384
+ # Get events to see why
385
+ local events=$(curl -s "$API_BASE/tickets/$ticket_id/events")
386
+ local last_reason=$(echo "$events" | jq -r '.events[-1].reason')
387
+
388
+ if echo "$last_reason" | grep -qi "interactive\|cursor"; then
389
+ log_info "Confirmed: Interactive executor (Cursor)"
390
+ log_info ""
391
+ log_info "=== MANUAL STEP REQUIRED ==="
392
+ log_info "1. Make some changes in the worktree"
393
+ log_info "2. Then call: curl -X POST $API_BASE/tickets/$ticket_id/resume"
394
+ log_info "==========================="
395
+ log_info ""
396
+
397
+ # Ask if user wants to test resume
398
+ read -p "Did you make changes? Run resume? (y/N): " do_resume
399
+
400
+ if [ "$do_resume" = "y" ] || [ "$do_resume" = "Y" ]; then
401
+ log_info "Running resume..."
402
+ local resume_response=$(run_resume "$ticket_id")
403
+ log_info "Resume job: $(echo "$resume_response" | jq -c '{id, status}')"
404
+
405
+ # Wait for final state
406
+ wait_for_state "$ticket_id" "verifying" "blocked" "needs_human"
407
+
408
+ local state_after_resume=$(get_ticket_state "$ticket_id")
409
+ if [ "$state_after_resume" = "verifying" ]; then
410
+ log_info "Resumed successfully, now in verifying"
411
+ wait_for_state "$ticket_id" "needs_human" "done" "blocked"
412
+ fi
413
+
414
+ local final_state=$(get_ticket_state "$ticket_id")
415
+ log_info "Final state: $final_state"
416
+
417
+ if [ "$final_state" = "needs_human" ] || [ "$final_state" = "done" ]; then
418
+ log_info "✅ CANARY 4 PASSED: Full interactive flow completed"
419
+ elif [ "$final_state" = "blocked" ]; then
420
+ log_warn "⚠️ CANARY 4 PARTIAL: Blocked (possibly no changes in worktree)"
421
+ fi
422
+ else
423
+ log_info "Skipping resume step"
424
+ log_info "✅ CANARY 4 PARTIAL: Interactive detection worked"
425
+ fi
426
+ else
427
+ log_warn "needs_human but not due to interactive executor"
428
+ log_info "Reason: $last_reason"
429
+ fi
430
+ else
431
+ log_warn "⚠️ CANARY 4 NOTE: Did not enter interactive mode"
432
+ log_info "This means Claude (headless) executor was used"
433
+ fi
434
+ }
435
+
436
+ # =============================================================================
437
+ # CANARY 5: Verification Failure (Syntax Error)
438
+ # =============================================================================
439
+ # Expected flow:
440
+ # executing → verifying → blocked (verification failed)
441
+ #
442
+ # This test proves the verification pipeline can actually fail.
443
+ # Creates a ticket that requests introducing a syntax error.
444
+ # =============================================================================
445
+ canary_5() {
446
+ log_step "CANARY 5: Verification Failure (Syntax Error)"
447
+
448
+ log_info "This test verifies that broken code is correctly blocked."
449
+ log_info "It requests a change that would break Python syntax."
450
+ log_info ""
451
+
452
+ log_info "Creating goal..."
453
+ local goal_id=$(create_goal "Canary Test Goal 5 - Break Things")
454
+ log_info "Goal ID: $goal_id"
455
+
456
+ log_info "Creating ticket that requests syntax error..."
457
+ local ticket_id=$(create_ticket "$goal_id" \
458
+ "Introduce syntax error in worker.py" \
459
+ "Add 'def broken_function(' (missing closing paren and body) to backend/app/worker.py. This should fail verification.")
460
+ log_info "Ticket ID: $ticket_id"
461
+
462
+ log_info "Transitioning to planned then executing..."
463
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
464
+ -H "Content-Type: application/json" \
465
+ -d '{"to_state": "planned", "actor_type": "human", "reason": "Planning canary test"}'
466
+
467
+ curl -s -X POST "$API_BASE/tickets/$ticket_id/transition" \
468
+ -H "Content-Type: application/json" \
469
+ -d '{"to_state": "executing", "actor_type": "human", "reason": "Executing canary test"}'
470
+
471
+ log_info "Running execute job..."
472
+ local execute_response=$(run_execute "$ticket_id")
473
+ log_info "Execute job: $(echo "$execute_response" | jq -c '{id, status}')"
474
+
475
+ # Wait for verifying (if executor makes changes) or blocked (if no changes)
476
+ wait_for_state "$ticket_id" "verifying" "blocked" "needs_human"
477
+
478
+ local state_after_execute=$(get_ticket_state "$ticket_id")
479
+ log_info "State after execute: $state_after_execute"
480
+
481
+ if [ "$state_after_execute" = "verifying" ]; then
482
+ log_info "Executor made changes, now in verifying..."
483
+ log_info "Waiting for verification to complete (should fail)..."
484
+
485
+ # Wait for blocked (verification should fail)
486
+ wait_for_state "$ticket_id" "blocked" "needs_human" "done"
487
+
488
+ local final_state=$(get_ticket_state "$ticket_id")
489
+ log_info "Final state: $final_state"
490
+
491
+ if [ "$final_state" = "blocked" ]; then
492
+ # Get events to check reason
493
+ local events=$(curl -s "$API_BASE/tickets/$ticket_id/events")
494
+ local last_reason=$(echo "$events" | jq -r '.events[-1].reason')
495
+ log_info "Block reason: $last_reason"
496
+
497
+ # Check evidence for verify_meta
498
+ local evidence=$(get_evidence "$ticket_id")
499
+ local verify_meta=$(echo "$evidence" | jq -r '.evidence[] | select(.kind == "verify_meta")')
500
+
501
+ if [ -n "$verify_meta" ]; then
502
+ log_info "Found verify_meta evidence"
503
+ local verify_exit=$(echo "$verify_meta" | jq -r '.exit_code')
504
+ log_info "Verify exit code: $verify_exit"
505
+
506
+ if [ "$verify_exit" != "0" ]; then
507
+ log_info "✅ CANARY 5 PASSED: Verification failed and blocked ticket"
508
+ log_info "Evidence shows failing verify command with exit code $verify_exit"
509
+ else
510
+ log_warn "⚠️ CANARY 5 UNEXPECTED: Blocked but verify_meta shows exit code 0"
511
+ fi
512
+ else
513
+ log_info "✅ CANARY 5 PASSED: Ticket blocked (verify_meta may not be captured yet)"
514
+ fi
515
+ elif [ "$final_state" = "needs_human" ] || [ "$final_state" = "done" ]; then
516
+ log_error "❌ CANARY 5 FAILED: Verification passed when it should have failed"
517
+ log_error "This means either:"
518
+ log_error " 1. Executor didn't actually introduce a syntax error"
519
+ log_error " 2. Verification commands aren't checking syntax (update draft.yaml)"
520
+ return 1
521
+ fi
522
+ elif [ "$state_after_execute" = "blocked" ]; then
523
+ # Could be blocked due to no changes
524
+ local events=$(curl -s "$API_BASE/tickets/$ticket_id/events")
525
+ local last_reason=$(echo "$events" | jq -r '.events[-1].reason')
526
+
527
+ if echo "$last_reason" | grep -qi "no.*change"; then
528
+ log_warn "⚠️ CANARY 5 SKIPPED: Executor produced no changes (can't test verification failure)"
529
+ log_info "This is expected if the executor refused to break things"
530
+ else
531
+ log_info "✅ CANARY 5 PASSED: Blocked during execution"
532
+ fi
533
+ else
534
+ log_warn "⚠️ CANARY 5 UNEXPECTED: State is $state_after_execute"
535
+ fi
536
+ }
537
+
538
+ # =============================================================================
539
+ # MAIN
540
+ # =============================================================================
541
+
542
+ main() {
543
+ echo "=============================================="
544
+ echo " ALMA KANBAN CANARY TESTS"
545
+ echo "=============================================="
546
+ echo ""
547
+ echo "API Base: $API_BASE"
548
+ echo ""
549
+
550
+ # Check if API is reachable
551
+ if ! curl -s "$API_BASE/board" > /dev/null 2>&1; then
552
+ log_error "Cannot reach API at $API_BASE"
553
+ log_error "Make sure the backend is running"
554
+ exit 1
555
+ fi
556
+ log_info "API is reachable"
557
+
558
+ local test_num="${1:-all}"
559
+
560
+ case "$test_num" in
561
+ 1) canary_1 ;;
562
+ 2) canary_2 ;;
563
+ 3) canary_3 ;;
564
+ 4) canary_4 ;;
565
+ 5) canary_5 ;;
566
+ all)
567
+ canary_1
568
+ echo ""
569
+ canary_2
570
+ echo ""
571
+ canary_3
572
+ echo ""
573
+ canary_4
574
+ echo ""
575
+ canary_5
576
+ ;;
577
+ *)
578
+ log_error "Unknown test: $test_num"
579
+ echo "Usage: $0 [1|2|3|4|5|all]"
580
+ exit 1
581
+ ;;
582
+ esac
583
+
584
+ echo ""
585
+ echo "=============================================="
586
+ echo " CANARY TESTS COMPLETE"
587
+ echo "=============================================="
588
+ }
589
+
590
+ main "$@"
591
+
Binary file
@@ -0,0 +1,3 @@
1
+ # Keep this directory for job logs
2
+
3
+