jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Migration: Remove AUTOINCREMENT from all tables
3
+ *
4
+ * AUTOINCREMENT maintains a persistent counter in sqlite_sequence that never
5
+ * decreases. If any row is ever inserted with a high explicit ID (test data,
6
+ * corrupted import, etc.), all future IDs inflate permanently.
7
+ *
8
+ * Plain INTEGER PRIMARY KEY still auto-assigns IDs (max(id)+1) but without
9
+ * the ratcheting counter. This is the SQLite-recommended approach.
10
+ */
11
+
12
+ module.exports = {
13
+ id: '029-remove-autoincrement',
14
+ description: 'Remove AUTOINCREMENT from all tables to prevent ID counter inflation',
15
+
16
+ async up(db) {
17
+ // Each table that uses AUTOINCREMENT needs to be recreated.
18
+ // SQLite doesn't support ALTER TABLE to change column definitions.
19
+ //
20
+ // Strategy: create _new table without AUTOINCREMENT, copy data, drop old, rename.
21
+ // All done inside serialize() for safety.
22
+
23
+ const tables = [
24
+ {
25
+ name: 'work_items',
26
+ create: `CREATE TABLE work_items_new (
27
+ id INTEGER PRIMARY KEY,
28
+ type TEXT NOT NULL,
29
+ title TEXT NOT NULL,
30
+ description TEXT,
31
+ status TEXT DEFAULT 'backlog',
32
+ parent_id INTEGER,
33
+ epic_id INTEGER,
34
+ branch_name TEXT,
35
+ file_paths TEXT,
36
+ commit_sha TEXT,
37
+ mode TEXT,
38
+ current INTEGER DEFAULT 0,
39
+ phase TEXT,
40
+ prototype_files TEXT,
41
+ discovery_winner TEXT,
42
+ discovery_rationale TEXT,
43
+ scenario_file TEXT,
44
+ completed_at TEXT,
45
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
46
+ needs_discovery INTEGER DEFAULT 0,
47
+ worktree_path TEXT,
48
+ display_order INTEGER,
49
+ architectural_decision TEXT,
50
+ discovery_completed_at TEXT,
51
+ rejection_reason TEXT,
52
+ rejected_at TEXT,
53
+ plan_at_creation TEXT DEFAULT NULL,
54
+ ready_for_review INTEGER DEFAULT 0,
55
+ conversational INTEGER DEFAULT 0
56
+ )`,
57
+ indexes: []
58
+ },
59
+ {
60
+ name: 'claude_sessions',
61
+ create: `CREATE TABLE claude_sessions_new (
62
+ id INTEGER PRIMARY KEY,
63
+ work_item_id INTEGER UNIQUE,
64
+ title TEXT NOT NULL,
65
+ session_title TEXT,
66
+ status TEXT NOT NULL CHECK(status IN ('active', 'completed', 'error', 'orphaned')),
67
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
68
+ completed_at TEXT,
69
+ content TEXT,
70
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id) ON DELETE SET NULL
71
+ )`,
72
+ indexes: [
73
+ 'CREATE INDEX idx_claude_sessions_status ON claude_sessions(status)',
74
+ 'CREATE INDEX idx_claude_sessions_work_item ON claude_sessions(work_item_id)'
75
+ ]
76
+ },
77
+ {
78
+ name: 'discovery_decisions',
79
+ create: `CREATE TABLE discovery_decisions_new (
80
+ id INTEGER PRIMARY KEY,
81
+ work_item_id INTEGER NOT NULL,
82
+ aspect TEXT NOT NULL,
83
+ decision TEXT NOT NULL,
84
+ rationale TEXT NOT NULL,
85
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
86
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
87
+ )`,
88
+ indexes: []
89
+ },
90
+ {
91
+ name: 'env_vars',
92
+ create: `CREATE TABLE env_vars_new (
93
+ id INTEGER PRIMARY KEY,
94
+ name TEXT NOT NULL UNIQUE,
95
+ value TEXT NOT NULL,
96
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
97
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
98
+ )`,
99
+ indexes: []
100
+ },
101
+ {
102
+ name: 'external_readiness_checklist',
103
+ create: `CREATE TABLE external_readiness_checklist_new (
104
+ id INTEGER PRIMARY KEY,
105
+ category TEXT NOT NULL,
106
+ item_key TEXT NOT NULL,
107
+ title TEXT NOT NULL,
108
+ description TEXT,
109
+ completed INTEGER DEFAULT 0,
110
+ completed_at DATETIME,
111
+ UNIQUE(category, item_key)
112
+ )`,
113
+ indexes: []
114
+ },
115
+ {
116
+ name: 'merge_locks',
117
+ create: `CREATE TABLE merge_locks_new (
118
+ id INTEGER PRIMARY KEY,
119
+ locked_by TEXT NOT NULL,
120
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
121
+ operation TEXT NOT NULL DEFAULT 'merging',
122
+ work_item_id INTEGER NOT NULL,
123
+ heartbeat_at TEXT NOT NULL DEFAULT (datetime('now'))
124
+ )`,
125
+ indexes: [
126
+ 'CREATE INDEX idx_merge_locks_locked_at ON merge_locks(locked_at)'
127
+ ]
128
+ },
129
+ {
130
+ name: 'skill_executions',
131
+ create: `CREATE TABLE skill_executions_new (
132
+ id INTEGER PRIMARY KEY,
133
+ work_item_id INTEGER NOT NULL,
134
+ skill_name TEXT NOT NULL,
135
+ status TEXT DEFAULT 'in_progress',
136
+ step_reached INTEGER DEFAULT 1,
137
+ context_json TEXT,
138
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
139
+ completed_at DATETIME,
140
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
141
+ )`,
142
+ indexes: []
143
+ },
144
+ {
145
+ name: 'test_runs',
146
+ create: `CREATE TABLE test_runs_new (
147
+ id INTEGER PRIMARY KEY,
148
+ run_at TEXT NOT NULL DEFAULT (datetime('now')),
149
+ total_scenarios INTEGER NOT NULL,
150
+ passed INTEGER NOT NULL,
151
+ failed INTEGER NOT NULL,
152
+ pending INTEGER NOT NULL,
153
+ duration_ms INTEGER NOT NULL
154
+ )`,
155
+ indexes: []
156
+ },
157
+ {
158
+ name: 'test_scenarios',
159
+ create: `CREATE TABLE test_scenarios_new (
160
+ id INTEGER PRIMARY KEY,
161
+ feature_file TEXT NOT NULL,
162
+ scenario_name TEXT NOT NULL,
163
+ UNIQUE(feature_file, scenario_name)
164
+ )`,
165
+ indexes: []
166
+ },
167
+ {
168
+ name: 'test_results',
169
+ create: `CREATE TABLE test_results_new (
170
+ id INTEGER PRIMARY KEY,
171
+ test_run_id INTEGER NOT NULL,
172
+ scenario_id INTEGER NOT NULL,
173
+ status TEXT NOT NULL CHECK(status IN ('passed', 'failed', 'pending')),
174
+ duration_ms INTEGER NOT NULL DEFAULT 0,
175
+ error_message TEXT,
176
+ failed_step TEXT,
177
+ run_at TEXT NOT NULL DEFAULT (datetime('now')),
178
+ FOREIGN KEY (test_run_id) REFERENCES test_runs(id),
179
+ FOREIGN KEY (scenario_id) REFERENCES test_scenarios(id)
180
+ )`,
181
+ indexes: [
182
+ 'CREATE INDEX idx_test_results_run ON test_results(test_run_id)',
183
+ 'CREATE INDEX idx_test_results_scenario ON test_results(scenario_id)'
184
+ ]
185
+ },
186
+ {
187
+ name: 'workflow_checkpoints',
188
+ create: `CREATE TABLE workflow_checkpoints_new (
189
+ id INTEGER PRIMARY KEY,
190
+ skill_name TEXT NOT NULL,
191
+ current_step INTEGER NOT NULL,
192
+ total_steps INTEGER,
193
+ context_json TEXT,
194
+ branch_name TEXT NOT NULL,
195
+ work_item_id INTEGER,
196
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
197
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
198
+ )`,
199
+ indexes: [
200
+ 'CREATE INDEX idx_workflow_checkpoints_branch ON workflow_checkpoints(branch_name)'
201
+ ]
202
+ },
203
+ {
204
+ name: 'workflow_gates',
205
+ create: `CREATE TABLE workflow_gates_new (
206
+ id INTEGER PRIMARY KEY,
207
+ work_item_id INTEGER NOT NULL,
208
+ gate_name TEXT NOT NULL,
209
+ passed_at DATETIME,
210
+ UNIQUE(work_item_id, gate_name),
211
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
212
+ )`,
213
+ indexes: []
214
+ },
215
+ {
216
+ name: 'worktree_sessions',
217
+ create: `CREATE TABLE worktree_sessions_new (
218
+ id INTEGER PRIMARY KEY,
219
+ worktree_path TEXT UNIQUE NOT NULL,
220
+ work_item_id INTEGER NOT NULL,
221
+ branch_name TEXT NOT NULL,
222
+ last_activity DATETIME DEFAULT CURRENT_TIMESTAMP,
223
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
224
+ )`,
225
+ indexes: [
226
+ 'CREATE INDEX idx_worktree_sessions_path ON worktree_sessions(worktree_path)',
227
+ 'CREATE INDEX idx_worktree_sessions_work_item ON worktree_sessions(work_item_id)'
228
+ ]
229
+ },
230
+ {
231
+ name: 'worktrees',
232
+ create: `CREATE TABLE worktrees_new (
233
+ id INTEGER PRIMARY KEY,
234
+ work_item_id INTEGER NOT NULL,
235
+ branch_name TEXT NOT NULL,
236
+ worktree_path TEXT NOT NULL,
237
+ status TEXT NOT NULL CHECK(status IN ('active', 'merging', 'merged', 'cleanup_pending', 'corrupted')),
238
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
239
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
240
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
241
+ )`,
242
+ indexes: [
243
+ 'CREATE INDEX idx_worktrees_work_item_id ON worktrees(work_item_id)',
244
+ 'CREATE INDEX idx_worktrees_status ON worktrees(status)'
245
+ ]
246
+ }
247
+ ];
248
+
249
+ return new Promise((resolve, reject) => {
250
+ db.run('PRAGMA foreign_keys = OFF', (err) => {
251
+ if (err) return reject(err);
252
+
253
+ db.serialize(() => {
254
+ for (const table of tables) {
255
+ // 1. Create new table without AUTOINCREMENT
256
+ db.run(table.create, (err) => {
257
+ if (err) return reject(new Error(`Failed to create ${table.name}_new: ${err.message}`));
258
+ });
259
+
260
+ // 2. Copy all data
261
+ db.run(`INSERT INTO ${table.name}_new SELECT * FROM ${table.name}`, (err) => {
262
+ if (err) return reject(new Error(`Failed to copy data for ${table.name}: ${err.message}`));
263
+ });
264
+
265
+ // 3. Drop old indexes
266
+ for (const idx of table.indexes) {
267
+ const idxName = idx.match(/CREATE INDEX (\S+)/)?.[1];
268
+ if (idxName) {
269
+ db.run(`DROP INDEX IF EXISTS ${idxName}`, (err) => {
270
+ if (err) return reject(new Error(`Failed to drop index ${idxName}: ${err.message}`));
271
+ });
272
+ }
273
+ }
274
+
275
+ // 4. Drop old table
276
+ db.run(`DROP TABLE "${table.name}"`, (err) => {
277
+ if (err) return reject(new Error(`Failed to drop ${table.name}: ${err.message}`));
278
+ });
279
+
280
+ // 5. Rename new table
281
+ db.run(`ALTER TABLE ${table.name}_new RENAME TO ${table.name}`, (err) => {
282
+ if (err) return reject(new Error(`Failed to rename ${table.name}_new: ${err.message}`));
283
+ });
284
+
285
+ // 6. Recreate indexes
286
+ for (const idx of table.indexes) {
287
+ db.run(idx, (err) => {
288
+ if (err) return reject(new Error(`Failed to create index: ${err.message}`));
289
+ });
290
+ }
291
+ }
292
+
293
+ // 7. Drop sqlite_sequence (no longer needed without AUTOINCREMENT)
294
+ db.run('DROP TABLE IF EXISTS sqlite_sequence', (err) => {
295
+ if (err) return reject(new Error(`Failed to drop sqlite_sequence: ${err.message}`));
296
+ });
297
+
298
+ // 8. Re-enable foreign keys
299
+ db.run('PRAGMA foreign_keys = ON', (err) => {
300
+ if (err) return reject(err);
301
+ resolve();
302
+ });
303
+ });
304
+ });
305
+ });
306
+ }
307
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Migration: Rename worktree status 'corrupted' to 'cleaned'
3
+ *
4
+ * The 'corrupted' status was misleading — it's the terminal state for worktrees
5
+ * that have been successfully cleaned up, not actually corrupted data.
6
+ * Renaming to 'cleaned' for clarity.
7
+ *
8
+ * SQLite doesn't support ALTER CONSTRAINT, so we recreate the table.
9
+ */
10
+
11
+ module.exports = {
12
+ id: '029-rename-corrupted-to-cleaned',
13
+ description: 'Rename worktree status corrupted to cleaned for clarity',
14
+
15
+ async up(db) {
16
+ return new Promise((resolve, reject) => {
17
+ db.serialize(() => {
18
+ // 1. Update existing records
19
+ db.run(`UPDATE worktrees SET status = 'cleaned' WHERE status = 'corrupted'`, (err) => {
20
+ if (err) return reject(err);
21
+ });
22
+
23
+ // 2. Create new table with updated constraint
24
+ db.run(`
25
+ CREATE TABLE IF NOT EXISTS worktrees_new (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ work_item_id INTEGER NOT NULL,
28
+ branch_name TEXT NOT NULL,
29
+ worktree_path TEXT NOT NULL,
30
+ status TEXT NOT NULL CHECK(status IN ('active', 'merging', 'merged', 'cleanup_pending', 'cleaned')),
31
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
32
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
33
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
34
+ )
35
+ `, (err) => {
36
+ if (err) return reject(err);
37
+ });
38
+
39
+ // 3. Copy data
40
+ db.run(`
41
+ INSERT INTO worktrees_new (id, work_item_id, branch_name, worktree_path, status, created_at, updated_at)
42
+ SELECT id, work_item_id, branch_name, worktree_path, status, created_at, updated_at
43
+ FROM worktrees
44
+ `, (err) => {
45
+ if (err) return reject(err);
46
+ });
47
+
48
+ // 4. Drop old indexes
49
+ db.run('DROP INDEX IF EXISTS idx_worktrees_work_item_id', (err) => {
50
+ if (err) return reject(err);
51
+ });
52
+
53
+ db.run('DROP INDEX IF EXISTS idx_worktrees_status', (err) => {
54
+ if (err) return reject(err);
55
+ });
56
+
57
+ // 5. Drop old table
58
+ db.run('DROP TABLE worktrees', (err) => {
59
+ if (err) return reject(err);
60
+ });
61
+
62
+ // 6. Rename new table
63
+ db.run('ALTER TABLE worktrees_new RENAME TO worktrees', (err) => {
64
+ if (err) return reject(err);
65
+ });
66
+
67
+ // 7. Recreate indexes
68
+ db.run(`
69
+ CREATE INDEX idx_worktrees_work_item_id
70
+ ON worktrees(work_item_id)
71
+ `, (err) => {
72
+ if (err) return reject(err);
73
+ });
74
+
75
+ db.run(`
76
+ CREATE INDEX idx_worktrees_status
77
+ ON worktrees(status)
78
+ `, (err) => {
79
+ if (err) return reject(err);
80
+ resolve();
81
+ });
82
+ });
83
+ });
84
+ },
85
+
86
+ async down(db) {
87
+ return new Promise((resolve, reject) => {
88
+ db.serialize(() => {
89
+ db.run(`UPDATE worktrees SET status = 'corrupted' WHERE status = 'cleaned'`, (err) => {
90
+ if (err) return reject(err);
91
+ });
92
+
93
+ db.run(`
94
+ CREATE TABLE IF NOT EXISTS worktrees_new (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ work_item_id INTEGER NOT NULL,
97
+ branch_name TEXT NOT NULL,
98
+ worktree_path TEXT NOT NULL,
99
+ status TEXT NOT NULL CHECK(status IN ('active', 'merging', 'merged', 'cleanup_pending', 'corrupted')),
100
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
101
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
102
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
103
+ )
104
+ `, (err) => {
105
+ if (err) return reject(err);
106
+ });
107
+
108
+ db.run(`
109
+ INSERT INTO worktrees_new (id, work_item_id, branch_name, worktree_path, status, created_at, updated_at)
110
+ SELECT id, work_item_id, branch_name, worktree_path, status, created_at, updated_at
111
+ FROM worktrees
112
+ `, (err) => {
113
+ if (err) return reject(err);
114
+ });
115
+
116
+ db.run('DROP INDEX IF EXISTS idx_worktrees_work_item_id', (err) => {
117
+ if (err) return reject(err);
118
+ });
119
+
120
+ db.run('DROP INDEX IF EXISTS idx_worktrees_status', (err) => {
121
+ if (err) return reject(err);
122
+ });
123
+
124
+ db.run('DROP TABLE worktrees', (err) => {
125
+ if (err) return reject(err);
126
+ });
127
+
128
+ db.run('ALTER TABLE worktrees_new RENAME TO worktrees', (err) => {
129
+ if (err) return reject(err);
130
+ });
131
+
132
+ db.run(`
133
+ CREATE INDEX idx_worktrees_work_item_id
134
+ ON worktrees(work_item_id)
135
+ `, (err) => {
136
+ if (err) return reject(err);
137
+ });
138
+
139
+ db.run(`
140
+ CREATE INDEX idx_worktrees_status
141
+ ON worktrees(status)
142
+ `, (err) => {
143
+ if (err) return reject(err);
144
+ resolve();
145
+ });
146
+ });
147
+ });
148
+ }
149
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Migration: Add rejection round tracking columns to work_items
3
+ *
4
+ * Purpose: Support rejection round nesting by tracking how many times
5
+ * a parent item has been rejected and which round spawned each child.
6
+ *
7
+ * Columns:
8
+ * - rejection_count INTEGER DEFAULT 0: Number of times this item has been rejected
9
+ * - rejection_round INTEGER: Which rejection round spawned this child item (null = original)
10
+ * - rejection_history TEXT: JSON array of [{round, reason, at}] for all rejections
11
+ */
12
+
13
+ module.exports = {
14
+ id: '030-rejection-round-columns',
15
+ description: 'Add rejection_count, rejection_round, and rejection_history columns to work_items',
16
+
17
+ async up(db) {
18
+ return new Promise((resolve, reject) => {
19
+ db.run(`ALTER TABLE work_items ADD COLUMN rejection_count INTEGER DEFAULT 0`, (err) => {
20
+ if (err) return reject(err);
21
+ db.run(`ALTER TABLE work_items ADD COLUMN rejection_round INTEGER`, (err) => {
22
+ if (err) return reject(err);
23
+ db.run(`ALTER TABLE work_items ADD COLUMN rejection_history TEXT`, (err) => {
24
+ if (err) return reject(err);
25
+ resolve();
26
+ });
27
+ });
28
+ });
29
+ });
30
+ },
31
+
32
+ async down(db) {
33
+ // SQLite 3.35+ supports DROP COLUMN, but for safety we use the backup approach
34
+ return new Promise((resolve, reject) => {
35
+ db.all(`PRAGMA table_info(work_items)`, (err, columns) => {
36
+ if (err) return reject(err);
37
+ const keepColumns = columns
38
+ .map(c => c.name)
39
+ .filter(n => n !== 'rejection_count' && n !== 'rejection_round' && n !== 'rejection_history');
40
+ const columnList = keepColumns.join(', ');
41
+ db.run(`CREATE TABLE work_items_backup AS SELECT ${columnList} FROM work_items`, (err) => {
42
+ if (err) return reject(err);
43
+ db.run(`DROP TABLE work_items`, (err) => {
44
+ if (err) return reject(err);
45
+ db.run(`ALTER TABLE work_items_backup RENAME TO work_items`, (err) => {
46
+ if (err) return reject(err);
47
+ resolve();
48
+ });
49
+ });
50
+ });
51
+ });
52
+ });
53
+ }
54
+ };
@@ -0,0 +1,17 @@
1
+ module.exports = {
2
+ id: '031-session-isolation-index',
3
+ description: 'Add index on claude_sessions for efficient session isolation filtering',
4
+
5
+ async up(db) {
6
+ await new Promise((resolve, reject) => {
7
+ db.run(`
8
+ CREATE INDEX IF NOT EXISTS idx_claude_sessions_active_work
9
+ ON claude_sessions(status, work_item_id)
10
+ WHERE work_item_id IS NOT NULL AND status = 'active'
11
+ `, (err) => {
12
+ if (err) reject(err);
13
+ else resolve();
14
+ });
15
+ });
16
+ }
17
+ };
@@ -20,11 +20,16 @@ async function runMigrations(db) {
20
20
  });
21
21
  });
22
22
 
23
- // Get list of applied migrations
24
- const applied = await new Promise((resolve, reject) => {
25
- db.all('SELECT id FROM migrations', [], (err, rows) => {
23
+ // Create _meta table for schema versioning
24
+ await new Promise((resolve, reject) => {
25
+ db.run(`
26
+ CREATE TABLE IF NOT EXISTS _meta (
27
+ key TEXT PRIMARY KEY,
28
+ value TEXT
29
+ )
30
+ `, (err) => {
26
31
  if (err) reject(err);
27
- else resolve(rows.map(r => r.id));
32
+ else resolve();
28
33
  });
29
34
  });
30
35
 
@@ -34,6 +39,32 @@ async function runMigrations(db) {
34
39
  .filter(f => f.endsWith('.js') && f !== 'index.js' && !f.endsWith('.test.js'))
35
40
  .sort();
36
41
 
42
+ const knownMigrationCount = files.length;
43
+
44
+ // Check schema version for forward-compatibility
45
+ const currentVersion = await new Promise((resolve, reject) => {
46
+ db.get('SELECT value FROM _meta WHERE key = ?', ['schema_version'], (err, row) => {
47
+ if (err) reject(err);
48
+ else resolve(row ? parseInt(row.value, 10) : 0);
49
+ });
50
+ });
51
+
52
+ if (currentVersion > knownMigrationCount) {
53
+ console.warn(
54
+ `⚠️ Database schema version (${currentVersion}) is newer than this version of JettyPod supports (${knownMigrationCount}).` +
55
+ '\n Please update JettyPod to avoid compatibility issues.'
56
+ );
57
+ return; // Don't run migrations — the DB is from a newer version
58
+ }
59
+
60
+ // Get list of applied migrations
61
+ const applied = await new Promise((resolve, reject) => {
62
+ db.all('SELECT id FROM migrations', [], (err, rows) => {
63
+ if (err) reject(err);
64
+ else resolve(rows.map(r => r.id));
65
+ });
66
+ });
67
+
37
68
  // Run pending migrations
38
69
  for (const file of files) {
39
70
  const migration = require(path.join(migrationsDir, file));
@@ -69,6 +100,18 @@ async function runMigrations(db) {
69
100
  throw err;
70
101
  }
71
102
  }
103
+
104
+ // Update schema version to reflect current state
105
+ await new Promise((resolve, reject) => {
106
+ db.run(
107
+ 'INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)',
108
+ ['schema_version', String(knownMigrationCount)],
109
+ (err) => {
110
+ if (err) reject(err);
111
+ else resolve();
112
+ }
113
+ );
114
+ });
72
115
  }
73
116
 
74
117
  module.exports = { runMigrations };
package/lib/schema.js CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  const SCHEMA_SQL = `
13
13
  CREATE TABLE IF NOT EXISTS work_items (
14
- id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ id INTEGER PRIMARY KEY,
15
15
  type TEXT NOT NULL,
16
16
  title TEXT NOT NULL,
17
17
  description TEXT,
@@ -42,7 +42,7 @@ const SCHEMA_SQL = `
42
42
  );
43
43
 
44
44
  CREATE TABLE IF NOT EXISTS external_readiness_checklist (
45
- id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ id INTEGER PRIMARY KEY,
46
46
  category TEXT NOT NULL,
47
47
  item_key TEXT NOT NULL,
48
48
  title TEXT NOT NULL,
@@ -53,7 +53,7 @@ const SCHEMA_SQL = `
53
53
  );
54
54
 
55
55
  CREATE TABLE IF NOT EXISTS discovery_decisions (
56
- id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ id INTEGER PRIMARY KEY,
57
57
  work_item_id INTEGER NOT NULL,
58
58
  aspect TEXT NOT NULL,
59
59
  decision TEXT NOT NULL,
@@ -63,7 +63,7 @@ const SCHEMA_SQL = `
63
63
  );
64
64
 
65
65
  CREATE TABLE IF NOT EXISTS skill_executions (
66
- id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ id INTEGER PRIMARY KEY,
67
67
  work_item_id INTEGER NOT NULL,
68
68
  skill_name TEXT NOT NULL,
69
69
  status TEXT DEFAULT 'in_progress',
@@ -75,13 +75,18 @@ const SCHEMA_SQL = `
75
75
  );
76
76
 
77
77
  CREATE TABLE IF NOT EXISTS workflow_gates (
78
- id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ id INTEGER PRIMARY KEY,
79
79
  work_item_id INTEGER NOT NULL,
80
80
  gate_name TEXT NOT NULL,
81
81
  passed_at DATETIME,
82
82
  UNIQUE(work_item_id, gate_name),
83
83
  FOREIGN KEY (work_item_id) REFERENCES work_items(id)
84
84
  );
85
+
86
+ CREATE TABLE IF NOT EXISTS _meta (
87
+ key TEXT PRIMARY KEY,
88
+ value TEXT
89
+ );
85
90
  `;
86
91
 
87
92
  /**
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  const ONBOARDING_EPIC = {
10
- title: 'Project Onboarding',
10
+ title: 'Project Planning',
11
11
  description: 'Get your project set up and planned. Work through these chores one at a time — each one is a short conversation.'
12
12
  };
13
13