jettypod 4.4.116 → 4.4.120

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 (162) hide show
  1. package/.env +7 -0
  2. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
  3. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
  4. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
  5. package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
  6. package/apps/dashboard/app/api/usage/route.ts +17 -0
  7. package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
  8. package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
  9. package/apps/dashboard/app/connect-claude/page.tsx +24 -0
  10. package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
  11. package/apps/dashboard/app/demo/gates/page.tsx +42 -42
  12. package/apps/dashboard/app/design-system/page.tsx +868 -0
  13. package/apps/dashboard/app/globals.css +6 -2
  14. package/apps/dashboard/app/install-claude/page.tsx +9 -7
  15. package/apps/dashboard/app/layout.tsx +17 -5
  16. package/apps/dashboard/app/login/page.tsx +250 -0
  17. package/apps/dashboard/app/page.tsx +11 -9
  18. package/apps/dashboard/app/settings/page.tsx +4 -2
  19. package/apps/dashboard/app/signup/page.tsx +245 -0
  20. package/apps/dashboard/app/subscribe/page.tsx +11 -0
  21. package/apps/dashboard/app/welcome/page.tsx +24 -1
  22. package/apps/dashboard/app/work/[id]/page.tsx +34 -50
  23. package/apps/dashboard/components/AppShell.tsx +95 -55
  24. package/apps/dashboard/components/CardMenu.tsx +56 -13
  25. package/apps/dashboard/components/ClaudePanel.tsx +301 -582
  26. package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
  27. package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
  28. package/apps/dashboard/components/CopyableId.tsx +3 -3
  29. package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
  30. package/apps/dashboard/components/DragContext.tsx +75 -65
  31. package/apps/dashboard/components/DraggableCard.tsx +6 -46
  32. package/apps/dashboard/components/DropZone.tsx +2 -2
  33. package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
  34. package/apps/dashboard/components/EditableTitle.tsx +26 -6
  35. package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
  36. package/apps/dashboard/components/EpicGroup.tsx +329 -0
  37. package/apps/dashboard/components/GateCard.tsx +100 -16
  38. package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
  39. package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
  40. package/apps/dashboard/components/JettyLoader.tsx +38 -0
  41. package/apps/dashboard/components/KanbanBoard.tsx +147 -766
  42. package/apps/dashboard/components/KanbanCard.tsx +506 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
  44. package/apps/dashboard/components/MainNav.tsx +20 -54
  45. package/apps/dashboard/components/MessageBlock.tsx +391 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -15
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
  53. package/apps/dashboard/components/ReviewFooter.tsx +141 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -18
  55. package/apps/dashboard/components/SubscribeContent.tsx +206 -0
  56. package/apps/dashboard/components/TestTree.tsx +15 -14
  57. package/apps/dashboard/components/TipCard.tsx +177 -0
  58. package/apps/dashboard/components/Toast.tsx +5 -5
  59. package/apps/dashboard/components/TypeIcon.tsx +56 -0
  60. package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
  62. package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
  64. package/apps/dashboard/components/WorkItemTree.tsx +9 -28
  65. package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
  66. package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
  67. package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
  68. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
  69. package/apps/dashboard/components/ui/Button.tsx +104 -0
  70. package/apps/dashboard/components/ui/Input.tsx +78 -0
  71. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
  72. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
  73. package/apps/dashboard/contexts/UsageContext.tsx +155 -0
  74. package/apps/dashboard/contexts/usageHelpers.js +9 -0
  75. package/apps/dashboard/electron/ipc-handlers.js +281 -88
  76. package/apps/dashboard/electron/main.js +691 -131
  77. package/apps/dashboard/electron/preload.js +25 -4
  78. package/apps/dashboard/electron/session-manager.js +163 -0
  79. package/apps/dashboard/electron-builder.config.js +3 -5
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/lib/backlog-parser.ts +50 -0
  83. package/apps/dashboard/lib/claude-process-manager.ts +50 -11
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/db-bridge.ts +33 -0
  86. package/apps/dashboard/lib/db.ts +136 -20
  87. package/apps/dashboard/lib/kanban-utils.ts +70 -0
  88. package/apps/dashboard/lib/run-migrations.js +27 -2
  89. package/apps/dashboard/lib/session-state-machine.ts +3 -0
  90. package/apps/dashboard/lib/session-stream-manager.ts +144 -38
  91. package/apps/dashboard/lib/shadows.ts +7 -0
  92. package/apps/dashboard/lib/tests.ts +3 -1
  93. package/apps/dashboard/lib/utils.ts +6 -0
  94. package/apps/dashboard/next.config.js +35 -14
  95. package/apps/dashboard/package.json +6 -3
  96. package/apps/dashboard/public/bug-icon.svg +9 -0
  97. package/apps/dashboard/public/buoy-icon.svg +9 -0
  98. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  99. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  100. package/apps/dashboard/public/in-flight-seagull.svg +9 -0
  101. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  102. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  103. package/apps/dashboard/public/jettypod_logo.png +0 -0
  104. package/apps/dashboard/public/pier-icon.svg +14 -0
  105. package/apps/dashboard/public/star-icon.svg +9 -0
  106. package/apps/dashboard/public/wrench-icon.svg +9 -0
  107. package/apps/dashboard/scripts/upload-to-r2.js +89 -0
  108. package/apps/dashboard/scripts/ws-server.js +191 -0
  109. package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
  110. package/apps/update-server/package.json +16 -0
  111. package/apps/update-server/schema.sql +31 -0
  112. package/apps/update-server/src/index.ts +1085 -0
  113. package/apps/update-server/tsconfig.json +16 -0
  114. package/apps/update-server/wrangler.toml +35 -0
  115. package/cucumber.js +9 -3
  116. package/docs/COMMAND_REFERENCE.md +34 -0
  117. package/hooks/post-checkout +32 -75
  118. package/hooks/post-merge +111 -10
  119. package/jest.setup.js +1 -0
  120. package/jettypod.js +54 -116
  121. package/lib/chore-taxonomy.js +33 -10
  122. package/lib/database.js +36 -16
  123. package/lib/db-watcher.js +1 -1
  124. package/lib/git-hooks/pre-commit +1 -1
  125. package/lib/jettypod-backup.js +27 -4
  126. package/lib/migrations/027-plan-at-creation-column.js +33 -0
  127. package/lib/migrations/028-ready-for-review-column.js +27 -0
  128. package/lib/migrations/029-remove-autoincrement.js +307 -0
  129. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  130. package/lib/migrations/index.js +47 -4
  131. package/lib/schema.js +13 -6
  132. package/lib/seed-onboarding.js +101 -69
  133. package/lib/update-command/index.js +9 -175
  134. package/lib/work-commands/index.js +129 -16
  135. package/lib/work-tracking/index.js +86 -46
  136. package/lib/worktree-diagnostics.js +16 -16
  137. package/lib/worktree-facade.js +1 -1
  138. package/lib/worktree-manager.js +8 -8
  139. package/lib/worktree-reconciler.js +5 -5
  140. package/package.json +9 -2
  141. package/scripts/ndjson-to-cucumber-json.js +152 -0
  142. package/scripts/postinstall.js +25 -0
  143. package/skills-templates/bug-mode/SKILL.md +39 -28
  144. package/skills-templates/bug-planning/SKILL.md +25 -29
  145. package/skills-templates/chore-mode/SKILL.md +131 -68
  146. package/skills-templates/chore-mode/verification.js +51 -10
  147. package/skills-templates/chore-planning/SKILL.md +47 -18
  148. package/skills-templates/epic-planning/SKILL.md +68 -48
  149. package/skills-templates/external-transition/SKILL.md +47 -47
  150. package/skills-templates/feature-planning/SKILL.md +83 -73
  151. package/skills-templates/production-mode/SKILL.md +49 -49
  152. package/skills-templates/request-routing/SKILL.md +27 -14
  153. package/skills-templates/simple-improvement/SKILL.md +68 -44
  154. package/skills-templates/speed-mode/SKILL.md +209 -128
  155. package/skills-templates/stable-mode/SKILL.md +105 -94
  156. package/templates/bdd-guidance.md +139 -0
  157. package/templates/bdd-scaffolding/wait.js +18 -0
  158. package/templates/bdd-scaffolding/world.js +19 -0
  159. package/.jettypod-backup/work.db +0 -0
  160. package/apps/dashboard/app/access-code/page.tsx +0 -110
  161. package/lib/discovery-checkpoint.js +0 -123
  162. package/skills-templates/project-discovery/SKILL.md +0 -372
@@ -26,8 +26,10 @@ export interface WorkItem {
26
26
  completed_at: string | null;
27
27
  rejection_reason: string | null;
28
28
  rejected_at: string | null;
29
+ ready_for_review: number;
29
30
  created_at: string;
30
31
  display_order: number | null;
32
+ conversational: number;
31
33
  children?: WorkItem[];
32
34
  chores?: WorkItem[];
33
35
  bugs?: WorkItem[];
@@ -44,7 +46,7 @@ export interface Decision {
44
46
  created_at: string;
45
47
  }
46
48
 
47
- function getProjectRoot(): string | null {
49
+ export function getProjectRoot(): string | null {
48
50
  // Use JETTYPOD_PROJECT_PATH if set (passed by Electron main process)
49
51
  if (process.env.JETTYPOD_PROJECT_PATH) {
50
52
  return process.env.JETTYPOD_PROJECT_PATH;
@@ -108,6 +110,9 @@ function ensureColumns(db: Database.Database): void {
108
110
  tryAddTo('work_items', 'display_order', 'INTEGER DEFAULT NULL');
109
111
  tryAddTo('work_items', 'rejection_reason', 'TEXT');
110
112
  tryAddTo('work_items', 'rejected_at', 'TEXT');
113
+ tryAddTo('work_items', 'conversational', 'INTEGER DEFAULT 0');
114
+ tryAddTo('work_items', 'plan_at_creation', 'TEXT DEFAULT NULL');
115
+ tryAddTo('work_items', 'ready_for_review', 'INTEGER DEFAULT 0');
111
116
  } catch {
112
117
  // Table might not exist yet - ensureSchema will handle it
113
118
  }
@@ -140,7 +145,7 @@ function ensureColumns(db: Database.Database): void {
140
145
  }
141
146
 
142
147
  // Check if a project folder has no implementation code
143
- function isBlankProject(projectPath: string): boolean {
148
+ export function isBlankProject(projectPath: string): boolean {
144
149
  const IMPL_PATTERNS = [
145
150
  /\.js$/, /\.ts$/, /\.jsx$/, /\.tsx$/, /\.mjs$/, /\.cjs$/,
146
151
  /\.py$/, /\.rb$/, /\.go$/, /\.rs$/, /\.java$/, /\.kt$/,
@@ -171,33 +176,48 @@ function isBlankProject(projectPath: string): boolean {
171
176
  return !scan(projectPath, projectPath);
172
177
  }
173
178
 
179
+ // Check if any onboarding chore has been started (moved out of backlog)
180
+ export function hasOnboardingStarted(): boolean {
181
+ try {
182
+ const db = getDb();
183
+ const result = db.prepare(`
184
+ SELECT COUNT(*) as count FROM work_items
185
+ WHERE parent_id = (SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project Planning')
186
+ AND status != 'backlog'
187
+ `).get() as { count: number } | undefined;
188
+ return (result?.count ?? 0) > 0;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+
174
194
  // Seed onboarding epic into a blank project's database if not already present
175
195
  function seedOnboardingIfBlank(db: Database.Database, dbPath: string): void {
176
196
  try {
177
197
  const projectPath = path.dirname(path.dirname(dbPath)); // .jettypod/work.db -> project root
178
198
  if (!isBlankProject(projectPath)) return;
179
199
 
180
- const existing = db.prepare("SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project Onboarding'").get();
200
+ const existing = db.prepare("SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project Planning'").get();
181
201
  if (existing) return;
182
202
 
183
203
  const insert = db.prepare(
184
- 'INSERT INTO work_items (type, title, description, parent_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)'
204
+ 'INSERT INTO work_items (type, title, description, parent_id, status, created_at, conversational) VALUES (?, ?, ?, ?, ?, ?, ?)'
185
205
  );
186
206
  const now = new Date().toISOString();
187
207
 
188
- const epicResult = insert.run('epic', 'Project Onboarding',
189
- 'Get your project set up and planned. Work through these chores one at a time — each one is a short conversation.', null, 'backlog', now);
208
+ const epicResult = insert.run('epic', 'Project Planning',
209
+ 'Get your project set up and planned. Work through these chores one at a time — each one is a short conversation.', null, 'backlog', now, 0);
190
210
  const epicId = epicResult.lastInsertRowid;
191
211
 
192
212
  const chores = [
193
- { title: 'Align on the user journey', desc: 'Help the user define what their product does.\n\nCLAUDE SESSION GUIDANCE:\nOpen with: "What do users DO in this product?"\n\nOUTCOME:\n- A clear description of the core user journey' },
213
+ { title: 'Align on the user journey', desc: 'Help the user define what their product does.\n\nCLAUDE SESSION GUIDANCE:\nOpen with: "What do users do in this product?"\n\nOUTCOME:\n- A clear description of the core user journey' },
194
214
  { title: 'Explore UX approaches', desc: 'Help the user decide how the product should feel.\n\nCONTEXT FROM PREVIOUS CHORE:\nRead previous decisions first.\n\nCLAUDE SESSION GUIDANCE:\nPresent 3 UX approaches.\n\nOUTCOME:\n- 3 UX options compared\n- A winner chosen' },
195
215
  { title: 'Choose a tech stack', desc: 'Help the user pick the right tech stack.\n\nCONTEXT FROM PREVIOUS CHORES:\nRead previous decisions first.\n\nCLAUDE SESSION GUIDANCE:\nPresent 3 tech stack options.\n\nOUTCOME:\n- A tech stack chosen with rationale' },
196
216
  { title: 'Break the project into epics', desc: 'Break the project into buildable phases.\n\nCONTEXT FROM PREVIOUS CHORES:\nRead all previous decisions.\n\nCLAUDE SESSION GUIDANCE:\nPropose 3-5 epics.\n\nOUTCOME:\n- 3-5 epics created in the backlog' },
197
217
  ];
198
218
 
199
219
  for (const chore of chores) {
200
- insert.run('chore', chore.title, chore.desc, epicId, 'backlog', now);
220
+ insert.run('chore', chore.title, chore.desc, epicId, 'backlog', now, 1);
201
221
  }
202
222
  } catch (err) {
203
223
  console.error('Could not seed onboarding:', err instanceof Error ? err.message : err);
@@ -283,7 +303,7 @@ export function getAllWorkItems(): WorkItem[] {
283
303
  const db = getDb();
284
304
  const items = db.prepare(`
285
305
  SELECT id, type, title, description, status, parent_id, epic_id,
286
- branch_name, mode, phase, completed_at, created_at
306
+ branch_name, mode, phase, completed_at, created_at, conversational
287
307
  FROM work_items
288
308
  ORDER BY id
289
309
  `).all() as WorkItem[];
@@ -294,18 +314,33 @@ export function getWorkItem(id: number): WorkItem | null {
294
314
  const db = getDb();
295
315
  const item = db.prepare(`
296
316
  SELECT id, type, title, description, status, parent_id, epic_id,
297
- branch_name, mode, phase, completed_at, created_at
317
+ branch_name, mode, phase, completed_at, created_at, conversational,
318
+ ready_for_review, rejection_reason
298
319
  FROM work_items
299
320
  WHERE id = ?
300
321
  `).get(id) as WorkItem | undefined;
301
322
  return item || null;
302
323
  }
303
324
 
325
+ export function createWorkItem(
326
+ type: 'epic' | 'feature' | 'chore' | 'bug',
327
+ title: string,
328
+ description?: string,
329
+ parentId?: number
330
+ ): WorkItem {
331
+ const db = getWriteDb();
332
+ const now = new Date().toISOString();
333
+ const result = db.prepare(
334
+ 'INSERT INTO work_items (type, title, description, parent_id, status, created_at, conversational) VALUES (?, ?, ?, ?, ?, ?, ?)'
335
+ ).run(type, title, description || null, parentId || null, 'backlog', now, 0);
336
+ return getWorkItem(result.lastInsertRowid as number)!;
337
+ }
338
+
304
339
  export function getChildWorkItems(parentId: number): WorkItem[] {
305
340
  const db = getDb();
306
341
  const items = db.prepare(`
307
342
  SELECT id, type, title, description, status, parent_id, epic_id,
308
- branch_name, mode, phase, completed_at, created_at
343
+ branch_name, mode, phase, completed_at, created_at, conversational
309
344
  FROM work_items
310
345
  WHERE parent_id = ?
311
346
  ORDER BY id
@@ -388,7 +423,7 @@ export function getActiveWork(): WorkItem[] {
388
423
  const db = getDb();
389
424
  const items = db.prepare(`
390
425
  SELECT id, type, title, description, status, parent_id, epic_id,
391
- branch_name, mode, phase, completed_at, created_at
426
+ branch_name, mode, phase, completed_at, created_at, conversational
392
427
  FROM work_items
393
428
  WHERE status = 'in_progress'
394
429
  ORDER BY id
@@ -400,7 +435,7 @@ export function getRecentlyCompleted(limit: number = 10): WorkItem[] {
400
435
  const db = getDb();
401
436
  const items = db.prepare(`
402
437
  SELECT id, type, title, description, status, parent_id, epic_id,
403
- branch_name, mode, phase, completed_at, created_at
438
+ branch_name, mode, phase, completed_at, created_at, conversational
404
439
  FROM work_items
405
440
  WHERE status = 'done'
406
441
  ORDER BY completed_at DESC
@@ -437,7 +472,7 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
437
472
  // Get all chores that belong to features (for chore expansion)
438
473
  const featureChores = db.prepare(`
439
474
  SELECT c.id, c.type, c.title, c.description, c.status, c.parent_id, c.epic_id,
440
- c.branch_name, c.mode, c.phase, c.completed_at, c.created_at,
475
+ c.branch_name, c.mode, c.phase, c.completed_at, c.created_at, c.conversational,
441
476
  wc.current_step, wc.total_steps
442
477
  FROM work_items c
443
478
  INNER JOIN work_items f ON c.parent_id = f.id
@@ -459,7 +494,7 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
459
494
  // Get all bugs that belong to features (for bug expansion)
460
495
  const featureBugs = db.prepare(`
461
496
  SELECT b.id, b.type, b.title, b.description, b.status, b.parent_id, b.epic_id,
462
- b.branch_name, b.mode, b.phase, b.completed_at, b.created_at
497
+ b.branch_name, b.mode, b.phase, b.completed_at, b.created_at, b.conversational
463
498
  FROM work_items b
464
499
  INNER JOIN work_items f ON b.parent_id = f.id
465
500
  WHERE b.type = 'bug' AND f.type = 'feature'
@@ -482,7 +517,8 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
482
517
  // - Bugs if they exist
483
518
  const allItems = db.prepare(`
484
519
  SELECT w.id, w.type, w.title, w.description, w.status, w.parent_id, w.epic_id,
485
- w.branch_name, w.mode, w.phase, w.completed_at, w.created_at, w.display_order,
520
+ w.branch_name, w.mode, w.phase, w.completed_at, w.created_at, w.conversational, w.display_order,
521
+ w.ready_for_review,
486
522
  p.type as parent_type,
487
523
  wc.current_step, wc.total_steps
488
524
  FROM work_items w
@@ -547,6 +583,13 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
547
583
  }
548
584
  }
549
585
 
586
+ // Sort in-flight: review items first (ascending by id), then non-review (ascending by id)
587
+ inFlight.sort((a, b) => {
588
+ if (a.ready_for_review && !b.ready_for_review) return -1;
589
+ if (!a.ready_for_review && b.ready_for_review) return 1;
590
+ return a.id - b.id;
591
+ });
592
+
550
593
  // Always ensure an ungrouped entry exists in backlog for drag-drop target
551
594
  if (!backlogGroups.has('ungrouped')) {
552
595
  backlogGroups.set('ungrouped', {
@@ -610,17 +653,42 @@ export function updateWorkItemDescription(id: number, description: string): bool
610
653
 
611
654
  export function updateWorkItemStatus(id: number, status: string, rejectionReason?: string): boolean {
612
655
  const db = getWriteDb();
656
+ // Normalize rejection reason: treat empty/whitespace-only as no rejection
657
+ const normalizedRejection = rejectionReason?.trim() || undefined;
613
658
  const completedAt = status === 'done' ? new Date().toISOString() : null;
614
- const rejectedAt = rejectionReason ? new Date().toISOString() : null;
615
- const result = db.prepare(`
616
- UPDATE work_items SET status = ?, completed_at = ?, rejection_reason = ?, rejected_at = ? WHERE id = ?
617
- `).run(status, completedAt, rejectionReason ?? null, rejectedAt, id);
659
+ const rejectedAt = normalizedRejection ? new Date().toISOString() : null;
660
+ // Clear ready_for_review on accept or reject
661
+ const readyForReview = (status === 'done' || normalizedRejection) ? 0 : undefined;
662
+ const result = readyForReview !== undefined
663
+ ? db.prepare(`
664
+ UPDATE work_items SET status = ?, completed_at = ?, rejection_reason = ?, rejected_at = ?, ready_for_review = ? WHERE id = ?
665
+ `).run(status, completedAt, normalizedRejection ?? null, rejectedAt, readyForReview, id)
666
+ : db.prepare(`
667
+ UPDATE work_items SET status = ?, completed_at = ?, rejection_reason = ?, rejected_at = ? WHERE id = ?
668
+ `).run(status, completedAt, normalizedRejection ?? null, rejectedAt, id);
618
669
 
619
670
  // When work item is cancelled, mark any linked sessions as orphaned
620
671
  if (status === 'cancelled' && result.changes > 0) {
621
672
  orphanSessionsByWorkItem(id);
622
673
  }
623
674
 
675
+ // When a chore is marked done, check if all sibling chores are done
676
+ // and set ready_for_review on the parent feature.
677
+ // Edge cases: no chores (length 0) → no auto-set; cancelled chores ≠ done → no auto-set
678
+ if (status === 'done' && result.changes > 0) {
679
+ const item = db.prepare('SELECT parent_id, type FROM work_items WHERE id = ?').get(id) as { parent_id: number | null; type: string } | undefined;
680
+ if (item?.parent_id && item.type === 'chore') {
681
+ const parent = db.prepare('SELECT id, type FROM work_items WHERE id = ?').get(item.parent_id) as { id: number; type: string } | undefined;
682
+ if (parent?.type === 'feature') {
683
+ const siblings = db.prepare('SELECT status FROM work_items WHERE parent_id = ? AND type = ?').all(parent.id, 'chore') as { status: string }[];
684
+ const allDone = siblings.length > 0 && siblings.every(s => s.status === 'done');
685
+ if (allDone) {
686
+ db.prepare('UPDATE work_items SET ready_for_review = 1 WHERE id = ?').run(parent.id);
687
+ }
688
+ }
689
+ }
690
+ }
691
+
624
692
  return result.changes > 0;
625
693
  }
626
694
 
@@ -1224,3 +1292,51 @@ export function setMainBranch(branch: string | null): void {
1224
1292
  }
1225
1293
  writeConfig(config);
1226
1294
  }
1295
+
1296
+ // ==================== Usage Tracking ====================
1297
+
1298
+ const FREE_WEEKLY_LIMIT = 20;
1299
+
1300
+ /**
1301
+ * Get the Monday 00:00:00 UTC of the current week.
1302
+ * Returns format matching SQLite's CURRENT_TIMESTAMP ("YYYY-MM-DD HH:MM:SS")
1303
+ * so lexicographic comparison in WHERE clauses works correctly.
1304
+ */
1305
+ function getCurrentWeekStart(): string {
1306
+ const now = new Date();
1307
+ const day = now.getUTCDay(); // 0=Sun, 1=Mon, ...
1308
+ const diff = day === 0 ? 6 : day - 1; // days since Monday
1309
+ const monday = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - diff));
1310
+ return monday.toISOString().replace('T', ' ').replace('.000Z', '');
1311
+ }
1312
+
1313
+ export interface WeeklyUsage {
1314
+ used: number;
1315
+ limit: number;
1316
+ remaining: number;
1317
+ allowed: boolean;
1318
+ }
1319
+
1320
+ /**
1321
+ * Count work items created on the free plan during the current week.
1322
+ */
1323
+ export function getWeeklyUsage(): WeeklyUsage {
1324
+ const db = getDb();
1325
+ const weekStart = getCurrentWeekStart();
1326
+ const projectRoot = getProjectRoot();
1327
+
1328
+ console.log('[usage] getWeeklyUsage called', { projectRoot, weekStart });
1329
+
1330
+ const row = db.prepare(
1331
+ `SELECT COUNT(*) as count FROM work_items
1332
+ WHERE plan_at_creation = 'free' AND created_at >= ?`
1333
+ ).get(weekStart) as { count: number } | undefined;
1334
+
1335
+ const used = row?.count ?? 0;
1336
+ const remaining = Math.max(0, FREE_WEEKLY_LIMIT - used);
1337
+ const result = { used, limit: FREE_WEEKLY_LIMIT, remaining, allowed: remaining > 0 };
1338
+
1339
+ console.log('[usage] getWeeklyUsage result', { row, result });
1340
+
1341
+ return result;
1342
+ }
@@ -0,0 +1,70 @@
1
+ import type { InFlightItem, KanbanGroup, WorkItem } from '@/lib/db';
2
+
3
+ export interface KanbanData {
4
+ inFlight: InFlightItem[];
5
+ backlog: Map<string, KanbanGroup>;
6
+ done: Map<string, KanbanGroup>;
7
+ }
8
+
9
+ // Helper to find item by ID in the kanban data
10
+ export function findItemById(data: KanbanData, id: number): { item: InFlightItem; status: string } | null {
11
+ const inFlightItem = data.inFlight.find(item => item.id === id);
12
+ if (inFlightItem) return { item: inFlightItem, status: 'in_progress' };
13
+
14
+ for (const group of data.backlog.values()) {
15
+ const backlogItem = group.items.find(item => item.id === id);
16
+ if (backlogItem) return { item: backlogItem as InFlightItem, status: 'backlog' };
17
+ }
18
+
19
+ for (const group of data.done.values()) {
20
+ const doneItem = group.items.find(item => item.id === id);
21
+ if (doneItem) return { item: doneItem as InFlightItem, status: 'done' };
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ // Extract onboarding chore items from kanban data for the OnboardingWelcome component
28
+ export function getOnboardingItems(data: KanbanData): WorkItem[] {
29
+ for (const group of data.backlog.values()) {
30
+ if (group.epicTitle === 'Project Planning') {
31
+ return group.items;
32
+ }
33
+ }
34
+ return [];
35
+ }
36
+
37
+ // Build a status map from kanban data (item id -> status string)
38
+ export function buildStatusMap(kanbanData: KanbanData): Map<number, string> {
39
+ const map = new Map<number, string>();
40
+ for (const item of kanbanData.inFlight) {
41
+ map.set(item.id, item.status);
42
+ }
43
+ for (const group of kanbanData.backlog.values()) {
44
+ for (const item of group.items) {
45
+ map.set(item.id, item.status);
46
+ }
47
+ }
48
+ for (const group of kanbanData.done.values()) {
49
+ for (const item of group.items) {
50
+ map.set(item.id, item.status);
51
+ }
52
+ }
53
+ return map;
54
+ }
55
+
56
+ // Build a mode map from kanban data (feature id -> mode) for detecting mode transitions
57
+ export function buildModeMap(kanbanData: KanbanData): Map<number, string | null> {
58
+ const map = new Map<number, string | null>();
59
+ const collectFeatures = (items: WorkItem[]) => {
60
+ for (const item of items) {
61
+ if (item.type === 'feature') {
62
+ map.set(item.id, item.mode);
63
+ }
64
+ }
65
+ };
66
+ collectFeatures(kanbanData.inFlight);
67
+ for (const group of kanbanData.backlog.values()) collectFeatures(group.items);
68
+ for (const group of kanbanData.done.values()) collectFeatures(group.items);
69
+ return map;
70
+ }
@@ -99,14 +99,36 @@ function runMigrations(betterDb) {
99
99
  )
100
100
  `);
101
101
 
102
- // Get already-applied migrations
103
- const applied = betterDb.prepare('SELECT id FROM migrations').all().map(r => r.id);
102
+ // Create _meta table for schema versioning
103
+ betterDb.exec(`
104
+ CREATE TABLE IF NOT EXISTS _meta (
105
+ key TEXT PRIMARY KEY,
106
+ value TEXT
107
+ )
108
+ `);
104
109
 
105
110
  // Load migration files
106
111
  const files = fs.readdirSync(migrationsDir)
107
112
  .filter(f => f.endsWith('.js') && f !== 'index.js' && !f.endsWith('.test.js'))
108
113
  .sort();
109
114
 
115
+ const knownMigrationCount = files.length;
116
+
117
+ // Check schema version for forward-compatibility
118
+ const versionRow = betterDb.prepare('SELECT value FROM _meta WHERE key = ?').get('schema_version');
119
+ const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
120
+
121
+ if (currentVersion > knownMigrationCount) {
122
+ console.warn(
123
+ `⚠️ Database schema version (${currentVersion}) is newer than this version of JettyPod supports (${knownMigrationCount}).` +
124
+ '\n Please update JettyPod to avoid compatibility issues.'
125
+ );
126
+ return; // Don't run migrations — the DB is from a newer version
127
+ }
128
+
129
+ // Get already-applied migrations
130
+ const applied = betterDb.prepare('SELECT id FROM migrations').all().map(r => r.id);
131
+
110
132
  // Create shim for callback-based migration API
111
133
  const shim = createDbShim(betterDb);
112
134
 
@@ -140,6 +162,9 @@ function runMigrations(betterDb) {
140
162
  throw err;
141
163
  }
142
164
  }
165
+
166
+ // Update schema version to reflect current state
167
+ betterDb.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run('schema_version', String(knownMigrationCount));
143
168
  }
144
169
 
145
170
  module.exports = { runMigrations };
@@ -43,6 +43,7 @@ export type SessionState = 'idle' | 'connecting' | 'streaming' | 'done' | 'error
43
43
  /** All possible state transition events */
44
44
  export type SessionEvent =
45
45
  | 'SEND' // User sends a message
46
+ | 'QUEUE' // User sends a message while streaming — queued for later
46
47
  | 'CONNECTED' // Connection established, streaming begins
47
48
  | 'COMPLETE' // Stream completed successfully
48
49
  | 'ERROR' // An error occurred
@@ -82,11 +83,13 @@ const TRANSITIONS: Record<SessionState, Partial<Record<SessionEvent, SessionStat
82
83
  CLEAR: 'idle',
83
84
  },
84
85
  connecting: {
86
+ QUEUE: 'connecting', // Queue message while connecting (stays in same state)
85
87
  CONNECTED: 'streaming',
86
88
  ERROR: 'error',
87
89
  STOP: 'idle',
88
90
  },
89
91
  streaming: {
92
+ QUEUE: 'streaming', // Queue message while streaming (stays in same state)
90
93
  COMPLETE: 'done',
91
94
  ERROR: 'error',
92
95
  STOP: 'idle',