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
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Chore Type Taxonomy
3
- * Defines the 4 chore types and their workflow guidance for the chore-planning and chore-mode skills.
3
+ * Defines the 5 chore types and their workflow guidance for the chore-planning and chore-mode skills.
4
+ *
5
+ * Types fall into two categories:
6
+ * - Behavior-preserving (refactor, dependency, cleanup, tooling): Run existing affected tests, never write new tests
7
+ * - Behavior-adding (enhancement): TDD with red-green-refactor cycle, writes new tests first
4
8
  */
5
9
 
6
10
  /**
@@ -11,7 +15,8 @@ const CHORE_TYPES = Object.freeze({
11
15
  REFACTOR: 'refactor',
12
16
  DEPENDENCY: 'dependency',
13
17
  CLEANUP: 'cleanup',
14
- TOOLING: 'tooling'
18
+ TOOLING: 'tooling',
19
+ ENHANCEMENT: 'enhancement'
15
20
  });
16
21
 
17
22
  /**
@@ -33,14 +38,14 @@ const CHORE_TYPE_GUIDANCE = Object.freeze({
33
38
  'Consider breaking into smaller refactors if scope is large'
34
39
  ],
35
40
  verification: [
36
- 'All existing tests pass without modification',
41
+ 'Affected tests pass without modification',
37
42
  'No new functionality added (that requires new tests)',
38
43
  'Code review confirms behavior preservation',
39
44
  'Performance is not degraded'
40
45
  ],
41
46
  testHandling: {
42
47
  required: true,
43
- approach: 'Run all tests for affected modules before and after. Update test file paths/imports if moved. Do NOT change test assertions - if tests fail, the refactor broke behavior.'
48
+ approach: 'Run tests for affected and potentially impacted modules. Update test file paths/imports if moved. Do NOT change test assertions - if tests fail, the refactor broke behavior.'
44
49
  }
45
50
  },
46
51
  [CHORE_TYPES.DEPENDENCY]: {
@@ -51,14 +56,14 @@ const CHORE_TYPE_GUIDANCE = Object.freeze({
51
56
  'Consider update strategy: one at a time vs batch'
52
57
  ],
53
58
  verification: [
54
- 'All tests pass after update',
59
+ 'Affected and potentially impacted tests pass after update',
55
60
  'Application builds successfully',
56
61
  'No new deprecation warnings (or documented)',
57
62
  'Security vulnerabilities addressed (if security update)'
58
63
  ],
59
64
  testHandling: {
60
65
  required: false,
61
- approach: 'Run full test suite to catch regressions. No new tests needed unless migrating to new API patterns. Document any test changes needed due to library API changes.'
66
+ approach: 'Run tests for affected and potentially impacted modules to catch regressions. No new tests needed unless migrating to new API patterns. Document any test changes needed due to library API changes.'
62
67
  }
63
68
  },
64
69
  [CHORE_TYPES.CLEANUP]: {
@@ -69,14 +74,14 @@ const CHORE_TYPE_GUIDANCE = Object.freeze({
69
74
  'Consider impact on git history/blame'
70
75
  ],
71
76
  verification: [
72
- 'All tests still pass',
77
+ 'Affected tests still pass',
73
78
  'No broken imports or references',
74
79
  'Application runs correctly',
75
80
  'Removed code was actually unused'
76
81
  ],
77
82
  testHandling: {
78
83
  required: false,
79
- approach: 'Run existing tests to ensure nothing breaks. Remove tests only if they test deleted code. No new tests needed for cleanup work.'
84
+ approach: 'Run tests for affected modules to ensure nothing breaks. Remove tests only if they test deleted code. No new tests needed for cleanup work.'
80
85
  }
81
86
  },
82
87
  [CHORE_TYPES.TOOLING]: {
@@ -96,6 +101,24 @@ const CHORE_TYPE_GUIDANCE = Object.freeze({
96
101
  required: false,
97
102
  approach: 'Verify tooling changes work via manual testing or CI runs. Add integration tests only if tooling is complex. Focus on verification over unit testing for infrastructure.'
98
103
  }
104
+ },
105
+ [CHORE_TYPES.ENHANCEMENT]: {
106
+ scope: [
107
+ 'Define what new behavior is being added',
108
+ 'Identify where the behavior integrates with existing code',
109
+ 'Plan test cases BEFORE writing implementation (TDD)',
110
+ 'Keep scope focused - one behavior per chore'
111
+ ],
112
+ verification: [
113
+ 'New tests written BEFORE implementation (TDD red phase)',
114
+ 'All new tests pass (TDD green phase)',
115
+ 'Code is clean and well-structured (TDD refactor phase)',
116
+ 'Existing affected tests still pass'
117
+ ],
118
+ testHandling: {
119
+ required: true,
120
+ approach: 'TDD red-green-refactor: Write failing tests first that define the new behavior, then implement minimum code to pass, then refactor. Run affected tests to ensure no regressions.'
121
+ }
99
122
  }
100
123
  });
101
124
 
@@ -136,7 +159,7 @@ function isValidChoreType(type) {
136
159
  function getGuidance(type) {
137
160
  if (type === null || type === undefined) {
138
161
  throw new Error(
139
- 'Chore type is required. Valid types: refactor, dependency, cleanup, tooling'
162
+ 'Chore type is required. Valid types: refactor, dependency, cleanup, tooling, enhancement'
140
163
  );
141
164
  }
142
165
 
@@ -144,7 +167,7 @@ function getGuidance(type) {
144
167
 
145
168
  if (!normalized || !VALID_CHORE_TYPES.includes(normalized)) {
146
169
  throw new Error(
147
- `Invalid chore type: "${type}". Valid types: refactor, dependency, cleanup, tooling`
170
+ `Invalid chore type: "${type}". Valid types: refactor, dependency, cleanup, tooling, enhancement`
148
171
  );
149
172
  }
150
173
 
package/lib/database.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const sqlite3 = require('sqlite3').verbose();
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
- const { getGitRoot } = require('./git-root');
4
+ const { getGitRoot, resetCache: resetGitRootCache } = require('./git-root');
5
5
  const { runMigrations } = require('./migrations');
6
6
  const { getSchemaSQL } = require('./schema');
7
7
 
@@ -12,6 +12,7 @@ let cachedDbPath = null;
12
12
  let cachedGitRoot = null;
13
13
  let isClosing = false;
14
14
  let migrationPromise = null;
15
+ let schemaGeneration = 0;
15
16
 
16
17
  /**
17
18
  * Get the root directory for jettypod operations
@@ -29,14 +30,19 @@ function getRepoRoot() {
29
30
  return cachedGitRoot;
30
31
  }
31
32
 
33
+ // Track which NODE_ENV was used to compute the cached path
34
+ let cachedNodeEnv = undefined;
35
+
32
36
  // Dynamic getters for paths (always use main repo, not worktree)
33
37
  function getJettypodDir() {
34
38
  const root = getRepoRoot();
35
- if (!cachedJettypodDir || cachedJettypodDir !== path.join(root, '.jettypod')) {
39
+ const currentNodeEnv = process.env.NODE_ENV;
40
+ if (!cachedJettypodDir || cachedJettypodDir !== path.join(root, '.jettypod') || cachedNodeEnv !== currentNodeEnv) {
36
41
  cachedJettypodDir = path.join(root, '.jettypod');
37
42
  // Use separate database for tests
38
- const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
43
+ const dbFileName = currentNodeEnv === 'test' ? 'test-work.db' : 'work.db';
39
44
  cachedDbPath = path.join(cachedJettypodDir, dbFileName);
45
+ cachedNodeEnv = currentNodeEnv;
40
46
  }
41
47
  return cachedJettypodDir;
42
48
  }
@@ -131,11 +137,18 @@ function getDb() {
131
137
  * @throws {Error} If schema creation fails
132
138
  */
133
139
  function initSchema() {
140
+ // Capture db reference and generation to prevent race conditions.
141
+ // Module-level getDb() calls (e.g. in work-tracking/index.js) start initSchema() async.
142
+ // If tests call resetDb() + getDb() before the old callback fires, the old callback
143
+ // would read the reassigned module-level `db` and run migrations on the wrong connection.
144
+ const localDb = db;
145
+ const myGeneration = ++schemaGeneration;
146
+
134
147
  // Create a promise that resolves when migrations complete
135
148
  migrationPromise = new Promise((resolve, reject) => {
136
- db.serialize(() => {
149
+ localDb.serialize(() => {
137
150
  // Create core tables using the shared schema module (single source of truth)
138
- db.exec(getSchemaSQL(), (err) => {
151
+ localDb.exec(getSchemaSQL(), (err) => {
139
152
  if (err) {
140
153
  throw new Error(`Failed to create core tables: ${err.message}`);
141
154
  }
@@ -147,20 +160,25 @@ function initSchema() {
147
160
  console.warn(`ALTER TABLE ADD COLUMN ${columnName} failed:`, err.message);
148
161
  }
149
162
  };
150
- db.run(`ALTER TABLE work_items ADD COLUMN branch_name TEXT`, handleAlterError('branch_name'));
151
- db.run(`ALTER TABLE work_items ADD COLUMN file_paths TEXT`, handleAlterError('file_paths'));
152
- db.run(`ALTER TABLE work_items ADD COLUMN commit_sha TEXT`, handleAlterError('commit_sha'));
153
- db.run(`ALTER TABLE work_items ADD COLUMN mode TEXT`, handleAlterError('mode'));
154
- db.run(`ALTER TABLE work_items ADD COLUMN current INTEGER DEFAULT 0`, handleAlterError('current'));
155
- db.run(`ALTER TABLE work_items ADD COLUMN needs_discovery INTEGER DEFAULT 0`, handleAlterError('needs_discovery'));
156
- db.run(`ALTER TABLE work_items ADD COLUMN worktree_path TEXT`, handleAlterError('worktree_path'));
157
- db.run(`ALTER TABLE work_items ADD COLUMN discovery_rationale TEXT`, handleAlterError('discovery_rationale'));
158
- db.run(`ALTER TABLE work_items ADD COLUMN display_order INTEGER`, handleAlterError('display_order'));
159
- db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async (err) => {
163
+ localDb.run(`ALTER TABLE work_items ADD COLUMN branch_name TEXT`, handleAlterError('branch_name'));
164
+ localDb.run(`ALTER TABLE work_items ADD COLUMN file_paths TEXT`, handleAlterError('file_paths'));
165
+ localDb.run(`ALTER TABLE work_items ADD COLUMN commit_sha TEXT`, handleAlterError('commit_sha'));
166
+ localDb.run(`ALTER TABLE work_items ADD COLUMN mode TEXT`, handleAlterError('mode'));
167
+ localDb.run(`ALTER TABLE work_items ADD COLUMN current INTEGER DEFAULT 0`, handleAlterError('current'));
168
+ localDb.run(`ALTER TABLE work_items ADD COLUMN needs_discovery INTEGER DEFAULT 0`, handleAlterError('needs_discovery'));
169
+ localDb.run(`ALTER TABLE work_items ADD COLUMN worktree_path TEXT`, handleAlterError('worktree_path'));
170
+ localDb.run(`ALTER TABLE work_items ADD COLUMN discovery_rationale TEXT`, handleAlterError('discovery_rationale'));
171
+ localDb.run(`ALTER TABLE work_items ADD COLUMN display_order INTEGER`, handleAlterError('display_order'));
172
+ localDb.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async (err) => {
160
173
  handleAlterError('architectural_decision')(err);
174
+ // If the singleton was reset since we started, abort — a new initSchema() owns migrations now
175
+ if (myGeneration !== schemaGeneration) {
176
+ resolve();
177
+ return;
178
+ }
161
179
  // Run data migrations after all schema operations complete
162
180
  try {
163
- await runMigrations(db);
181
+ await runMigrations(localDb);
164
182
  resolve();
165
183
  } catch (err) {
166
184
  console.error('Migration failed:', err.message);
@@ -303,12 +321,14 @@ async function closeDb() {
303
321
  * Forces the singleton to be recreated on next getDb() call
304
322
  */
305
323
  function resetDb() {
324
+ schemaGeneration++; // Invalidate any in-flight initSchema callbacks
306
325
  db = null;
307
326
  cachedJettypodDir = null;
308
327
  cachedDbPath = null;
309
328
  cachedGitRoot = null;
310
329
  isClosing = false;
311
330
  migrationPromise = null;
331
+ resetGitRootCache();
312
332
  }
313
333
 
314
334
  /**
package/lib/db-watcher.js CHANGED
@@ -21,7 +21,7 @@ let onChange = null;
21
21
  let watchedPath = null;
22
22
 
23
23
  // Polling interval in milliseconds
24
- const POLL_MS = 50;
24
+ const POLL_MS = 500;
25
25
 
26
26
  /**
27
27
  * Start watching the database file for changes
@@ -88,7 +88,7 @@ async function exportSnapshots() {
88
88
  const paths = await exportAll();
89
89
 
90
90
  // Stage the snapshot files
91
- execSync(`git add "${paths.work}" "${paths.database}"`, {
91
+ execSync(`git add -f "${paths.work}" "${paths.database}"`, {
92
92
  stdio: ['pipe', 'pipe', 'pipe']
93
93
  });
94
94
 
@@ -75,10 +75,33 @@ async function createBackup(gitRoot, reason = 'unknown', options = {}) {
75
75
  const backupPath = path.join(backupBaseDir, backupName);
76
76
 
77
77
  try {
78
- // Copy entire .jettypod directory
79
- execSync(`cp -R "${jettypodPath}" "${backupPath}"`, {
80
- stdio: 'pipe'
81
- });
78
+ // Copy .jettypod directory structure
79
+ fs.mkdirSync(backupPath, { recursive: true });
80
+
81
+ // Copy all files, using sqlite3 .backup for .db files (WAL-safe)
82
+ // CRITICAL: fs.copyFileSync/cp does NOT work with SQLite WAL mode -
83
+ // it only copies the base .db file, missing data in the -wal file.
84
+ const entries = fs.readdirSync(jettypodPath);
85
+ for (const entry of entries) {
86
+ const srcFile = path.join(jettypodPath, entry);
87
+ const destFile = path.join(backupPath, entry);
88
+ const stat = fs.statSync(srcFile);
89
+
90
+ if (stat.isDirectory()) {
91
+ execSync(`cp -R "${srcFile}" "${destFile}"`, { stdio: 'pipe' });
92
+ } else if (entry.endsWith('.db') && !entry.endsWith('-shm') && !entry.endsWith('-wal')) {
93
+ // Use sqlite3 .backup for database files (handles WAL mode correctly)
94
+ try {
95
+ execSync(`sqlite3 "${srcFile}" ".backup '${destFile}'"`, { stdio: 'pipe' });
96
+ } catch (dbErr) {
97
+ // Fall back to file copy if sqlite3 fails (e.g., empty db)
98
+ fs.copyFileSync(srcFile, destFile);
99
+ }
100
+ } else if (!entry.endsWith('-shm') && !entry.endsWith('-wal')) {
101
+ // Copy non-WAL files normally (skip -shm/-wal as .backup handles them)
102
+ fs.copyFileSync(srcFile, destFile);
103
+ }
104
+ }
82
105
 
83
106
  // Verify backup was created
84
107
  if (!fs.existsSync(backupPath)) {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Migration: Add plan_at_creation column to work_items
3
+ *
4
+ * Purpose: Track which plan the user was on when creating each work item.
5
+ * Used for local usage tracking — only work items created on the 'free'
6
+ * plan count toward the weekly limit.
7
+ */
8
+
9
+ module.exports = {
10
+ id: '027-plan-at-creation-column',
11
+ description: 'Add plan_at_creation column to work_items',
12
+
13
+ async up(db) {
14
+ return new Promise((resolve, reject) => {
15
+ db.run(`ALTER TABLE work_items ADD COLUMN plan_at_creation TEXT DEFAULT NULL`, (err) => {
16
+ // Ignore "duplicate column" — column may already exist if a previous
17
+ // run succeeded at ALTER TABLE but crashed before recording the migration.
18
+ if (err && !err.message.includes('duplicate column')) return reject(err);
19
+ // Backfill existing work items — assume free plan for all existing items
20
+ db.run(`UPDATE work_items SET plan_at_creation = 'free' WHERE plan_at_creation IS NULL`, (err2) => {
21
+ if (err2) return reject(err2);
22
+ resolve();
23
+ });
24
+ });
25
+ });
26
+ },
27
+
28
+ async down(db) {
29
+ // SQLite doesn't support DROP COLUMN before 3.35.0
30
+ // Column will just be ignored if not used
31
+ return Promise.resolve();
32
+ }
33
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Migration: Add ready_for_review column to work_items
3
+ *
4
+ * Purpose: Gate accept/reject button visibility on kanban cards.
5
+ * When ready_for_review = 1, the card shows accept/reject buttons.
6
+ * Auto-set when all child chores complete; cleared on rejection.
7
+ */
8
+
9
+ module.exports = {
10
+ id: '028-ready-for-review-column',
11
+ description: 'Add ready_for_review column to work_items',
12
+
13
+ async up(db) {
14
+ return new Promise((resolve, reject) => {
15
+ db.run(`ALTER TABLE work_items ADD COLUMN ready_for_review INTEGER DEFAULT 0`, (err) => {
16
+ if (err) return reject(err);
17
+ resolve();
18
+ });
19
+ });
20
+ },
21
+
22
+ async down(db) {
23
+ // SQLite doesn't support DROP COLUMN before 3.35.0
24
+ // Column will just be ignored if not used
25
+ return Promise.resolve();
26
+ }
27
+ };
@@ -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
+ };