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,7 +1,3 @@
1
- const { execSync } = require('child_process');
2
- const https = require('https');
3
- const fs = require('fs');
4
- const path = require('path');
5
1
  const packageJson = require('../../package.json');
6
2
 
7
3
  /**
@@ -12,184 +8,22 @@ function getCurrentVersion() {
12
8
  return packageJson.version;
13
9
  }
14
10
 
15
- /**
16
- * Check npm registry for latest version
17
- * @returns {Promise<string>} Latest version from npm
18
- */
19
- function getLatestVersion() {
20
- return new Promise((resolve, reject) => {
21
- const packageName = packageJson.name;
22
- const url = `https://registry.npmjs.org/${packageName}/latest`;
23
-
24
- const request = https.get(url, (res) => {
25
- let data = '';
26
-
27
- if (res.statusCode !== 200) {
28
- reject(new Error(`HTTP ${res.statusCode}`));
29
- return;
30
- }
31
-
32
- res.on('data', (chunk) => {
33
- data += chunk;
34
- });
35
-
36
- res.on('end', () => {
37
- try {
38
- const json = JSON.parse(data);
39
- if (!json.version) {
40
- reject(new Error('No version found in npm response'));
41
- return;
42
- }
43
- resolve(json.version);
44
- } catch (err) {
45
- reject(new Error(`Invalid JSON response: ${err.message}`));
46
- }
47
- });
48
- });
49
-
50
- // Set timeout for network request (30 seconds)
51
- request.setTimeout(30000, () => {
52
- request.destroy();
53
- reject(new Error('Request timeout - network too slow'));
54
- });
55
-
56
- request.on('error', (err) => {
57
- // Provide specific error messages for common network errors
58
- if (err.code === 'ENOTFOUND') {
59
- reject(new Error('network error - DNS lookup failed (check internet connection)'));
60
- } else if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
61
- reject(new Error('network error - connection timeout'));
62
- } else if (err.code === 'ECONNREFUSED') {
63
- reject(new Error('network error - connection refused'));
64
- } else if (err.code === 'ECONNRESET') {
65
- reject(new Error('network error - connection reset'));
66
- } else {
67
- reject(err);
68
- }
69
- });
70
- });
71
- }
72
-
73
- /**
74
- * Update jettypod to latest version using npm
75
- * @param {string} version - Version to update to (default: latest)
76
- * @returns {boolean} True if update succeeded
77
- */
78
- function updateJettyPod(version = 'latest') {
79
- const packageName = packageJson.name;
80
- try {
81
- console.log(`📦 Installing jettypod@${version}...`);
82
-
83
- // Use npm to update globally
84
- execSync(`npm install -g ${packageName}@${version}`, {
85
- stdio: 'inherit'
86
- });
87
-
88
- // Clear dashboard .next folder to force rebuild with new assets
89
- try {
90
- const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
91
- const dashboardNextPath = path.join(globalRoot, 'jettypod', 'apps', 'dashboard', '.next');
92
- if (fs.existsSync(dashboardNextPath)) {
93
- fs.rmSync(dashboardNextPath, { recursive: true, force: true });
94
- console.log('🧹 Cleared dashboard cache');
95
- }
96
- } catch {
97
- // Non-fatal - dashboard will still work, just might use old cache
98
- }
99
-
100
- return true;
101
- } catch (err) {
102
- console.log('');
103
- console.error(`❌ Update failed`);
104
- console.log('');
105
-
106
- // Provide specific error messages for common failures
107
- const errorOutput = err.stderr ? err.stderr.toString() : '';
108
-
109
- if (err.message.includes('EACCES') || err.message.includes('EPERM') ||
110
- errorOutput.includes('EACCES') || errorOutput.includes('EPERM')) {
111
- console.error('Permission denied - try running with sudo:');
112
- console.log(` sudo npm install -g ${packageName}@${version}`);
113
- console.log('');
114
- console.error('Or configure npm to use a different directory:');
115
- console.log(' mkdir ~/.npm-global');
116
- console.log(' npm config set prefix ~/.npm-global');
117
- console.log(' export PATH=~/.npm-global/bin:$PATH');
118
- } else if (errorOutput.includes('ENOSPC')) {
119
- console.error('Not enough disk space to install update');
120
- console.log('Free up disk space and try again');
121
- } else if (errorOutput.includes('404') || errorOutput.includes('E404')) {
122
- console.error(`Version ${version} not found in npm registry`);
123
- } else if (errorOutput.includes('network') || errorOutput.includes('ETIMEDOUT') ||
124
- errorOutput.includes('ENOTFOUND')) {
125
- console.error('Network error during npm install');
126
- console.log('Check your internet connection and try again');
127
- } else {
128
- console.error(`Error details: ${err.message}`);
129
- }
130
-
131
- console.log('');
132
- console.error('Manual update:');
133
- console.log(` npm install -g ${packageName}@${version}`);
134
-
135
- return false;
136
- }
137
- }
138
-
139
11
  /**
140
12
  * Run the update command
141
- * @param {Object} options - Optional dependencies for testing
142
- * @param {Function} options.getCurrentVersion - Function to get current version
143
- * @param {Function} options.getLatestVersion - Function to get latest version
144
- * @param {Function} options.updateJettyPod - Function to update jettypod
13
+ * The JettyPod app is the single distribution point — CLI updates ship with app updates.
145
14
  */
146
- async function runUpdate(options = {}) {
147
- const _getCurrentVersion = options.getCurrentVersion || getCurrentVersion;
148
- const _getLatestVersion = options.getLatestVersion || getLatestVersion;
149
- const _updateJettyPod = options.updateJettyPod || updateJettyPod;
150
-
151
- console.log('🔍 Checking for updates...');
152
-
153
- const currentVersion = _getCurrentVersion();
154
- console.log(`Current version: ${currentVersion}`);
155
-
156
- let latestVersion;
157
- try {
158
- latestVersion = await _getLatestVersion();
159
- } catch (err) {
160
- console.log(`Cannot check for updates: ${err.message}`);
161
- console.log('');
162
- console.log('You can still manually update with:');
163
- console.log(` npm install -g ${packageJson.name}@latest`);
164
- return false;
165
- }
166
-
167
- console.log(`Latest version: ${latestVersion}`);
15
+ async function runUpdate() {
16
+ const currentVersion = getCurrentVersion();
17
+ console.log(`JettyPod v${currentVersion}`);
168
18
  console.log('');
169
-
170
- if (currentVersion === latestVersion) {
171
- console.log(`Already on latest version: ${latestVersion}`);
172
- return true;
173
- }
174
-
175
- console.log(`New version available: ${latestVersion} (current: ${currentVersion})`);
19
+ console.log('The CLI is bundled with the JettyPod app.');
20
+ console.log('Updates are delivered automatically through the app.');
176
21
  console.log('');
177
-
178
- const success = _updateJettyPod(latestVersion);
179
-
180
- if (success) {
181
- console.log('');
182
- console.log(`✅ JettyPod updated to ${latestVersion}`);
183
- console.log('');
184
- return true;
185
- }
186
-
187
- return false;
22
+ console.log('To check for app updates: open JettyPod and it will auto-update.');
23
+ return true;
188
24
  }
189
25
 
190
26
  module.exports = {
191
27
  runUpdate,
192
- getCurrentVersion,
193
- getLatestVersion,
194
- updateJettyPod
28
+ getCurrentVersion
195
29
  };
@@ -1148,9 +1148,12 @@ async function cleanupWorktrees(options = {}) {
1148
1148
  * Clean up a specific worktree after merge
1149
1149
  * Should be run from main repo after cd'ing out of the worktree
1150
1150
  * @param {number} workItemId - The work item ID to clean up
1151
+ * @param {Object} [options] - Cleanup options
1152
+ * @param {boolean} [options.force] - Force cleanup even if worktree is still active (skips merge requirement)
1151
1153
  * @returns {Promise<Object>} Result with success status
1152
1154
  */
1153
- async function cleanupWorkItem(workItemId) {
1155
+ async function cleanupWorkItem(workItemId, options = {}) {
1156
+ const { force = false } = options;
1154
1157
  const db = getDb();
1155
1158
  const gitRoot = getGitRoot();
1156
1159
 
@@ -1183,13 +1186,89 @@ async function cleanupWorkItem(workItemId) {
1183
1186
  return { success: true, message: 'No worktree to clean up' };
1184
1187
  }
1185
1188
 
1186
- if (worktree.status === 'active') {
1189
+ if (worktree.status === 'active' && !force) {
1187
1190
  return Promise.reject(new Error(
1188
1191
  `Worktree for #${workItemId} is still active.\n` +
1189
- `Run 'jettypod work merge ${workItemId}' first.`
1192
+ `Run 'jettypod work merge ${workItemId}' first, or use --force to skip merge.`
1190
1193
  ));
1191
1194
  }
1192
1195
 
1196
+ // Force cleanup of active worktree — warn about unmerged work
1197
+ if (worktree.status === 'active' && force) {
1198
+ const warnings = [];
1199
+
1200
+ // Check for uncommitted changes in the worktree
1201
+ if (worktree.worktree_path && fs.existsSync(worktree.worktree_path)) {
1202
+ try {
1203
+ const dirtyFiles = execSync('git status --porcelain', {
1204
+ cwd: worktree.worktree_path,
1205
+ stdio: 'pipe'
1206
+ }).toString().trim();
1207
+ if (dirtyFiles) {
1208
+ const fileCount = dirtyFiles.split('\n').length;
1209
+ warnings.push(`${fileCount} uncommitted change${fileCount !== 1 ? 's' : ''}`);
1210
+ }
1211
+ } catch {
1212
+ // Worktree may be in a bad state — not a blocker for force cleanup
1213
+ }
1214
+ }
1215
+
1216
+ // Check for unmerged commits on the branch
1217
+ if (worktree.branch_name) {
1218
+ try {
1219
+ const unmergedLog = execSync(`git log main..${worktree.branch_name} --oneline`, {
1220
+ cwd: gitRoot,
1221
+ stdio: 'pipe'
1222
+ }).toString().trim();
1223
+ if (unmergedLog) {
1224
+ const commitCount = unmergedLog.split('\n').length;
1225
+ warnings.push(`${commitCount} unmerged commit${commitCount !== 1 ? 's' : ''}`);
1226
+ }
1227
+ } catch {
1228
+ // Branch may not exist or may not have diverged — not a blocker
1229
+ }
1230
+ }
1231
+
1232
+ // Display warnings and prompt for confirmation
1233
+ console.log(`\n⚠️ Force cleanup of active worktree for #${workItemId}: ${worktree.title}\n`);
1234
+ if (warnings.length > 0) {
1235
+ console.log(` This worktree has ${warnings.join(' and ')}.\n This data will be lost and cannot be recovered easily.\n`);
1236
+ } else {
1237
+ console.log(` No unmerged commits or uncommitted changes detected.\n`);
1238
+ }
1239
+
1240
+ // Interactive confirmation
1241
+ const confirmed = await new Promise((resolve) => {
1242
+ const readline = require('readline');
1243
+ const rl = readline.createInterface({
1244
+ input: process.stdin,
1245
+ output: process.stdout
1246
+ });
1247
+ rl.question('Proceed with force cleanup? (y/N): ', (answer) => {
1248
+ rl.close();
1249
+ resolve(answer.trim().toLowerCase() === 'y');
1250
+ });
1251
+ });
1252
+
1253
+ if (!confirmed) {
1254
+ console.log('Cancelled.');
1255
+ return { success: false, message: 'Force cleanup cancelled by user' };
1256
+ }
1257
+
1258
+ // Update work item status to cancelled
1259
+ await new Promise((resolve, reject) => {
1260
+ db.run(
1261
+ `UPDATE work_items SET status = 'cancelled' WHERE id = ?`,
1262
+ [workItemId],
1263
+ (err) => {
1264
+ if (err) return reject(err);
1265
+ resolve();
1266
+ }
1267
+ );
1268
+ });
1269
+ console.log(`📋 Work item #${workItemId} marked as cancelled`);
1270
+ }
1271
+
1193
1272
  console.log(`Cleaning up worktree for #${workItemId}: ${worktree.title}`);
1194
1273
 
1195
1274
  // Remove git worktree if it exists
@@ -1561,6 +1640,8 @@ async function mergeWork(options = {}) {
1561
1640
  // and should never block branch switching
1562
1641
  const generatedFilesForReset = [
1563
1642
  'cucumber-results.json',
1643
+ 'cucumber-results.ndjson',
1644
+ '@rerun.txt',
1564
1645
  'package-lock.json',
1565
1646
  'yarn.lock',
1566
1647
  'pnpm-lock.yaml'
@@ -1700,6 +1781,8 @@ async function mergeWork(options = {}) {
1700
1781
  // Check if all conflicts are in generated files that can be auto-resolved
1701
1782
  const generatedFiles = [
1702
1783
  'cucumber-results.json',
1784
+ 'cucumber-results.ndjson',
1785
+ '@rerun.txt',
1703
1786
  'package-lock.json',
1704
1787
  'yarn.lock',
1705
1788
  'pnpm-lock.yaml'
@@ -1902,20 +1985,50 @@ async function mergeWork(options = {}) {
1902
1985
  `Test worktrees (tests/* branches) merge BDD scenarios without completing the feature.`
1903
1986
  ));
1904
1987
  } else {
1905
- // Chore or bug worktree: mark as done
1906
- console.log(`Marking ${currentWork.type} as done...`);
1907
- const completedAt = new Date().toISOString();
1908
- await new Promise((resolve, reject) => {
1909
- db.run(
1910
- `UPDATE work_items SET status = 'done', completed_at = ? WHERE id = ?`,
1911
- [completedAt, currentWork.id],
1912
- (err) => {
1988
+ // Chore or bug worktree
1989
+ // Check if this is a standalone kanban-visible item (no parent or parent is epic)
1990
+ let isStandalone = !currentWork.parent_id;
1991
+ if (!isStandalone && currentWork.parent_id) {
1992
+ const parentItem = await new Promise((resolve, reject) => {
1993
+ db.get('SELECT type FROM work_items WHERE id = ?', [currentWork.parent_id], (err, row) => {
1913
1994
  if (err) return reject(err);
1914
- resolve();
1915
- }
1916
- );
1917
- });
1918
- console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} marked as done`);
1995
+ resolve(row);
1996
+ });
1997
+ });
1998
+ isStandalone = parentItem && parentItem.type === 'epic';
1999
+ }
2000
+
2001
+ if (isStandalone) {
2002
+ // Standalone item: set ready_for_review instead of marking done
2003
+ // Keep status as in_progress so it stays in the In Flight column
2004
+ // User accepts/rejects via the kanban board
2005
+ await new Promise((resolve, reject) => {
2006
+ db.run(
2007
+ `UPDATE work_items SET ready_for_review = 1 WHERE id = ?`,
2008
+ [currentWork.id],
2009
+ (err) => {
2010
+ if (err) return reject(err);
2011
+ resolve();
2012
+ }
2013
+ );
2014
+ });
2015
+ console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} ready for review`);
2016
+ } else {
2017
+ // Chore/bug under a feature: mark as done (feature handles the accept flow)
2018
+ console.log(`Marking ${currentWork.type} as done...`);
2019
+ const completedAt = new Date().toISOString();
2020
+ await new Promise((resolve, reject) => {
2021
+ db.run(
2022
+ `UPDATE work_items SET status = 'done', completed_at = ? WHERE id = ?`,
2023
+ [completedAt, currentWork.id],
2024
+ (err) => {
2025
+ if (err) return reject(err);
2026
+ resolve();
2027
+ }
2028
+ );
2029
+ });
2030
+ console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} marked as done`);
2031
+ }
1919
2032
  }
1920
2033
 
1921
2034
  // Mark worktree as merged but DON'T delete it yet
@@ -256,6 +256,22 @@ function create(type, title, description = '', parentId = null, mode = null, nee
256
256
  }
257
257
 
258
258
  function continueWithValidatedParent(epicId) {
259
+ // Read current plan from auth.json for usage tracking
260
+ let planAtCreation = null;
261
+ try {
262
+ const os = require('os');
263
+ const path = require('path');
264
+ const fs = require('fs');
265
+ const authPath = path.join(os.homedir(), 'Library', 'Application Support', 'jettypod', 'auth.json');
266
+ if (fs.existsSync(authPath)) {
267
+ const authData = JSON.parse(fs.readFileSync(authPath, 'utf8'));
268
+ planAtCreation = authData?.user?.plan || 'free';
269
+ } else {
270
+ planAtCreation = 'free';
271
+ }
272
+ } catch {
273
+ planAtCreation = 'free';
274
+ }
259
275
 
260
276
  // Only features have modes - they start with mode=NULL (no mode until implementation starts)
261
277
  // Epics, chores, and bugs don't have modes (always NULL)
@@ -282,8 +298,8 @@ function create(type, title, description = '', parentId = null, mode = null, nee
282
298
  // Set phase for features (discovery when mode=NULL, implementation when mode is set, NULL for everything else)
283
299
  const phase = type === 'feature' ? (mode ? 'implementation' : 'discovery') : null;
284
300
 
285
- const sql = `INSERT INTO work_items (type, title, description, parent_id, epic_id, mode, needs_discovery, phase, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
286
- db.run(sql, [type, title, description, parentId, epicId, mode, needsDiscovery ? 1 : 0, phase, 'backlog'], function(err) {
301
+ const sql = `INSERT INTO work_items (type, title, description, parent_id, epic_id, mode, needs_discovery, phase, status, plan_at_creation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
302
+ db.run(sql, [type, title, description, parentId, epicId, mode, needsDiscovery ? 1 : 0, phase, 'backlog', planAtCreation], function(err) {
287
303
  if (err) {
288
304
  return reject(err);
289
305
  }
@@ -776,31 +792,19 @@ function updateStatus(id, status) {
776
792
  return resolve();
777
793
  }
778
794
 
779
- // Auto-close epics when all children are done
795
+ // Items under epics: set ready_for_review and keep in_progress for accept/reject
780
796
  if (parent.type === 'epic') {
781
- // Check if all children of this epic are done
782
- db.all(
783
- 'SELECT id, status FROM work_items WHERE parent_id = ?',
784
- [parent.id],
785
- (err, children) => {
797
+ // Set ready_for_review on the item and revert to in_progress
798
+ db.run(
799
+ 'UPDATE work_items SET ready_for_review = 1, status = ?, completed_at = NULL WHERE id = ?',
800
+ ['in_progress', item.id],
801
+ (err) => {
786
802
  if (err) {
787
- return resolve();
788
- }
789
-
790
- const allDone = children.every(child => child.status === 'done');
791
- if (allDone) {
792
- const epicCompletedAt = new Date().toISOString();
793
- db.run('UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?', ['done', epicCompletedAt, parent.id], (err) => {
794
- if (err) {
795
- console.error(`Failed to auto-close epic: ${err.message}`);
796
- } else {
797
- console.log(`✓ Epic #${parent.id} also completed (all children done)`);
798
- }
799
- resolve();
800
- });
803
+ console.error(`Failed to set ready_for_review: ${err.message}`);
801
804
  } else {
802
- resolve();
805
+ console.log(`✓ Work item #${item.id} ready for review`);
803
806
  }
807
+ resolve();
804
808
  }
805
809
  );
806
810
  }
@@ -838,12 +842,12 @@ function updateStatus(id, status) {
838
842
  }
839
843
 
840
844
  if (featureComplete) {
841
- const featureCompletedAt = new Date().toISOString();
842
- db.run('UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?', ['done', featureCompletedAt, parent.id], (err) => {
845
+ // Set ready_for_review instead of auto-closing — let user accept/reject
846
+ db.run('UPDATE work_items SET ready_for_review = 1 WHERE id = ?', [parent.id], (err) => {
843
847
  if (err) {
844
- console.error(`Failed to auto-close feature: ${err.message}`);
848
+ console.error(`Failed to set ready_for_review: ${err.message}`);
845
849
  } else {
846
- console.log(`✓ Feature #${parent.id} completed (all ${featureMode} mode chores done)`);
850
+ console.log(`✓ Feature #${parent.id} ready for review (all ${featureMode} mode chores done)`);
847
851
  }
848
852
  resolve();
849
853
  });
@@ -860,6 +864,21 @@ function updateStatus(id, status) {
860
864
  resolve();
861
865
  }
862
866
  });
867
+ } else if (status === 'done' && !item.parent_id) {
868
+ // Standalone item (no parent) marked done via CLI
869
+ // Set ready_for_review and revert to in_progress so user can accept/reject via kanban
870
+ db.run(
871
+ 'UPDATE work_items SET ready_for_review = 1, status = ?, completed_at = NULL WHERE id = ?',
872
+ ['in_progress', item.id],
873
+ (err) => {
874
+ if (err) {
875
+ console.error(`Failed to set ready_for_review: ${err.message}`);
876
+ } else {
877
+ console.log(`✓ Work item #${item.id} ready for review`);
878
+ }
879
+ resolve();
880
+ }
881
+ );
863
882
  } else {
864
883
  resolve();
865
884
  }
@@ -1165,25 +1184,46 @@ async function main() {
1165
1184
 
1166
1185
  switch(command) {
1167
1186
  case 'create': {
1168
- const type = args[0];
1169
- const title = args[1];
1170
- const desc = args[2] || '';
1187
+ // Support --from=<file> for truncation-safe creation via JSON file
1188
+ const fromArg = args.find(a => a.startsWith('--from='));
1189
+ let type, title, desc, parentId, mode, needsDiscovery;
1171
1190
 
1172
- let parentId = null;
1173
- let mode = null;
1174
- let needsDiscovery = false;
1175
-
1176
- args.forEach(arg => {
1177
- if (arg.startsWith('--parent=')) {
1178
- parentId = parseInt(arg.split('=')[1]);
1179
- }
1180
- if (arg.startsWith('--mode=')) {
1181
- mode = arg.split('=')[1];
1182
- }
1183
- if (arg === '--needs-discovery') {
1184
- needsDiscovery = true;
1191
+ if (fromArg) {
1192
+ const fs = require('fs');
1193
+ const filePath = fromArg.split('=').slice(1).join('=');
1194
+ try {
1195
+ const raw = fs.readFileSync(filePath, 'utf8');
1196
+ const data = JSON.parse(raw);
1197
+ type = data.type;
1198
+ title = data.title;
1199
+ desc = data.description || '';
1200
+ parentId = data.parent ? parseInt(data.parent) : null;
1201
+ mode = data.mode || null;
1202
+ needsDiscovery = !!data.needsDiscovery;
1203
+ } catch (e) {
1204
+ console.error(`Error reading --from file: ${e.message}`);
1205
+ process.exit(1);
1185
1206
  }
1186
- });
1207
+ } else {
1208
+ type = args[0];
1209
+ title = args[1];
1210
+ desc = args[2] || '';
1211
+ parentId = null;
1212
+ mode = null;
1213
+ needsDiscovery = false;
1214
+
1215
+ args.forEach(arg => {
1216
+ if (arg.startsWith('--parent=')) {
1217
+ parentId = parseInt(arg.split('=')[1]);
1218
+ }
1219
+ if (arg.startsWith('--mode=')) {
1220
+ mode = arg.split('=')[1];
1221
+ }
1222
+ if (arg === '--needs-discovery') {
1223
+ needsDiscovery = true;
1224
+ }
1225
+ });
1226
+ }
1187
1227
 
1188
1228
  try {
1189
1229
  const newId = await create(type, title, desc, parentId, mode, needsDiscovery);
@@ -1258,7 +1298,7 @@ async function main() {
1258
1298
  LEFT JOIN work_items p ON w.parent_id = p.id
1259
1299
  LEFT JOIN work_items e ON w.epic_id = e.id
1260
1300
  WHERE w.status = 'in_progress'
1261
- ORDER BY w.id ASC
1301
+ ORDER BY w.ready_for_review DESC, w.id ASC
1262
1302
  `, [], (err, rows) => {
1263
1303
  if (err) return reject(err);
1264
1304
  resolve(rows || []);
@@ -1502,7 +1542,7 @@ async function main() {
1502
1542
  LEFT JOIN work_items p ON w.parent_id = p.id
1503
1543
  LEFT JOIN work_items e ON w.epic_id = e.id
1504
1544
  WHERE w.status = 'in_progress'
1505
- ORDER BY w.id ASC
1545
+ ORDER BY w.ready_for_review DESC, w.id ASC
1506
1546
  `, [], (err, rows) => {
1507
1547
  if (err) return reject(err);
1508
1548
  resolve(rows || []);
@@ -99,7 +99,7 @@ async function runDiagnostics(db, repoPath, options = {}) {
99
99
  count: reconcileResults.staleDbEntries.length,
100
100
  message: `Found ${reconcileResults.staleDbEntries.length} database entries with missing filesystem directories`,
101
101
  details: reconcileResults.staleDbEntries,
102
- recommendation: 'Run reconciliation with cleanup=true to mark as corrupted'
102
+ recommendation: 'Run reconciliation with cleanup=true to mark as cleaned'
103
103
  });
104
104
  }
105
105
 
@@ -192,23 +192,23 @@ async function runDiagnostics(db, repoPath, options = {}) {
192
192
  async function checkDatabaseIntegrity(db) {
193
193
  const issues = [];
194
194
 
195
- // Check for corrupted worktrees
196
- const corruptedCount = await new Promise((resolve, reject) => {
197
- db.get('SELECT COUNT(*) as count FROM worktrees WHERE status = ?', ['corrupted'], (err, row) => {
195
+ // Check for cleaned worktrees (terminal state — informational only)
196
+ const cleanedCount = await new Promise((resolve, reject) => {
197
+ db.get('SELECT COUNT(*) as count FROM worktrees WHERE status = ?', ['cleaned'], (err, row) => {
198
198
  if (err) reject(err);
199
199
  else resolve(row.count);
200
200
  });
201
201
  });
202
202
 
203
- if (corruptedCount > 0) {
203
+ if (cleanedCount > 0) {
204
204
  issues.push({
205
205
  severity: SEVERITY.INFO,
206
206
  category: 'database',
207
- type: 'corrupted_worktrees',
208
- count: corruptedCount,
209
- message: `Found ${corruptedCount} corrupted worktree(s) in database`,
210
- details: { count: corruptedCount },
211
- recommendation: 'These worktrees can be safely cleaned up or ignored'
207
+ type: 'cleaned_worktrees',
208
+ count: cleanedCount,
209
+ message: `Found ${cleanedCount} cleaned worktree(s) in database`,
210
+ details: { count: cleanedCount },
211
+ recommendation: 'These are historical records no action needed'
212
212
  });
213
213
  }
214
214
 
@@ -232,7 +232,7 @@ async function checkDatabaseIntegrity(db) {
232
232
  count: orphanedWorktrees.length,
233
233
  message: `Found ${orphanedWorktrees.length} worktree(s) with missing work items`,
234
234
  details: orphanedWorktrees,
235
- recommendation: 'Mark these worktrees as corrupted'
235
+ recommendation: 'Mark these worktrees as cleaned'
236
236
  });
237
237
  }
238
238
 
@@ -265,7 +265,7 @@ async function checkFilesystemHealth(db, repoPath) {
265
265
  type: 'missing_worktree_directory',
266
266
  message: `Active worktree directory missing: ${worktreePath}`,
267
267
  details: { worktree },
268
- recommendation: 'Mark worktree as corrupted and recreate if needed'
268
+ recommendation: 'Mark worktree as cleaned and recreate if needed'
269
269
  });
270
270
  continue;
271
271
  }
@@ -293,7 +293,7 @@ async function checkFilesystemHealth(db, repoPath) {
293
293
  type: 'permission_issue',
294
294
  message: `Cannot read/write worktree directory: ${worktreePath}`,
295
295
  details: { worktree, error: err.message },
296
- recommendation: 'Fix directory permissions or mark worktree as corrupted'
296
+ recommendation: 'Fix directory permissions or mark worktree as cleaned'
297
297
  });
298
298
  }
299
299
  }
@@ -344,11 +344,11 @@ function generateRecommendations(report) {
344
344
  });
345
345
  }
346
346
 
347
- if (report.issues.some(i => i.type === 'corrupted_worktrees')) {
347
+ if (report.issues.some(i => i.type === 'cleaned_worktrees')) {
348
348
  recommendations.push({
349
349
  priority: 'low',
350
- action: 'Clean up corrupted worktree entries from database',
351
- reason: 'Keeping database clean improves query performance'
350
+ action: 'Cleaned worktree entries are historical records',
351
+ reason: 'No action needed these are part of the audit trail'
352
352
  });
353
353
  }
354
354
 
@@ -117,7 +117,7 @@ async function startWork(workItem, options = {}) {
117
117
  /**
118
118
  * Stop work on a work item with graceful degradation
119
119
  *
120
- * Attempts to cleanup worktree. If that fails, marks as corrupted and continues.
120
+ * Attempts to cleanup worktree. If that fails, marks as cleaned and continues.
121
121
  *
122
122
  * @param {number} worktreeId - Worktree ID (can be null if working in main)
123
123
  * @param {Object} options - Optional configuration