jettypod 4.4.118 → 4.4.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -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
@@ -1252,13 +1331,12 @@ async function cleanupWorkItem(workItemId) {
1252
1331
  * Pushes feature branch, checks out main, merges, and pushes main
1253
1332
  * Post-merge hook will mark work item as done and cleanup worktree
1254
1333
  * @param {Object} options - Merge options
1255
- * @param {boolean} options.withTransition - Hold lock for transition phase (BDD generation)
1256
1334
  * @param {boolean} options.releaseLock - Release held lock only (no merge)
1257
- * @returns {Promise<void|Object>} Returns {lockHeld: true} if withTransition, void otherwise
1335
+ * @returns {Promise<void>}
1258
1336
  * @throws {Error} If no current work, git operations fail, or not in git repo
1259
1337
  */
1260
1338
  async function mergeWork(options = {}) {
1261
- const { withTransition = false, releaseLock = false, featureBranch = null, workItemId = null } = options;
1339
+ const { releaseLock = false, featureBranch = null, workItemId = null } = options;
1262
1340
 
1263
1341
  // Handle lock release-only mode first (can run from anywhere)
1264
1342
  if (releaseLock) {
@@ -1561,6 +1639,8 @@ async function mergeWork(options = {}) {
1561
1639
  // and should never block branch switching
1562
1640
  const generatedFilesForReset = [
1563
1641
  'cucumber-results.json',
1642
+ 'cucumber-results.ndjson',
1643
+ '@rerun.txt',
1564
1644
  'package-lock.json',
1565
1645
  'yarn.lock',
1566
1646
  'pnpm-lock.yaml'
@@ -1700,6 +1780,8 @@ async function mergeWork(options = {}) {
1700
1780
  // Check if all conflicts are in generated files that can be auto-resolved
1701
1781
  const generatedFiles = [
1702
1782
  'cucumber-results.json',
1783
+ 'cucumber-results.ndjson',
1784
+ '@rerun.txt',
1703
1785
  'package-lock.json',
1704
1786
  'yarn.lock',
1705
1787
  'pnpm-lock.yaml'
@@ -1931,7 +2013,7 @@ async function mergeWork(options = {}) {
1931
2013
  });
1932
2014
  console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} ready for review`);
1933
2015
  } else {
1934
- // Chore/bug under a feature: mark as done (feature handles the accept flow)
2016
+ // Chore/bug under a feature: mark as done, then check if feature is complete
1935
2017
  console.log(`Marking ${currentWork.type} as done...`);
1936
2018
  const completedAt = new Date().toISOString();
1937
2019
  await new Promise((resolve, reject) => {
@@ -1945,6 +2027,60 @@ async function mergeWork(options = {}) {
1945
2027
  );
1946
2028
  });
1947
2029
  console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} marked as done`);
2030
+
2031
+ // Check if all sibling chores under the parent feature are now done
2032
+ // If so, and mode progression is complete, set ready_for_review on the parent
2033
+ const parent = await new Promise((resolve, reject) => {
2034
+ db.get('SELECT id, type, mode FROM work_items WHERE id = ?', [currentWork.parent_id], (err, row) => {
2035
+ if (err) return reject(err);
2036
+ resolve(row);
2037
+ });
2038
+ });
2039
+
2040
+ if (parent && parent.type === 'feature') {
2041
+ const siblingChores = await new Promise((resolve, reject) => {
2042
+ db.all(
2043
+ 'SELECT id, status FROM work_items WHERE parent_id = ? AND type = ?',
2044
+ [parent.id, 'chore'],
2045
+ (err, rows) => {
2046
+ if (err) return reject(err);
2047
+ resolve(rows || []);
2048
+ }
2049
+ );
2050
+ });
2051
+
2052
+ const allChoresDone = siblingChores.length > 0 && siblingChores.every(c => c.status === 'done');
2053
+ if (allChoresDone) {
2054
+ const config = await new Promise((resolve, reject) => {
2055
+ db.get('SELECT project_state FROM project_config WHERE id = 1', [], (err, row) => {
2056
+ if (err) return reject(err);
2057
+ resolve(row);
2058
+ });
2059
+ });
2060
+ const projectState = (config && config.project_state) || 'internal';
2061
+ const featureMode = parent.mode || 'speed';
2062
+
2063
+ let featureComplete = false;
2064
+ if (projectState === 'internal') {
2065
+ featureComplete = (featureMode === 'stable');
2066
+ } else if (projectState === 'external') {
2067
+ featureComplete = (featureMode === 'production');
2068
+ }
2069
+
2070
+ if (featureComplete) {
2071
+ await new Promise((resolve, reject) => {
2072
+ db.run('UPDATE work_items SET ready_for_review = 1 WHERE id = ?', [parent.id], (err) => {
2073
+ if (err) return reject(err);
2074
+ resolve();
2075
+ });
2076
+ });
2077
+ console.log(`✅ Feature #${parent.id} ready for review (all ${featureMode} mode chores done)`);
2078
+ } else {
2079
+ const nextMode = featureMode === 'speed' ? 'stable' : 'production';
2080
+ console.log(`✓ All ${featureMode} mode chores done. Feature #${parent.id} ready for ${nextMode} mode.`);
2081
+ }
2082
+ }
2083
+ }
1948
2084
  }
1949
2085
  }
1950
2086
 
@@ -1975,20 +2111,9 @@ async function mergeWork(options = {}) {
1975
2111
  console.log(' Worktrees accumulate until cleaned up.');
1976
2112
  }
1977
2113
 
1978
- if (withTransition) {
1979
- // Hold lock for transition phase (BDD generation)
1980
- // Work is done and worktree is cleaned up, but lock is held so no other merges
1981
- // happen while Claude generates stable scenarios on main
1982
- console.log('⚠️ Merge lock held for transition phase');
1983
- console.log(' Skills will release lock after generating stable scenarios');
1984
- console.log(' Release lock with: jettypod work merge --release-lock');
1985
- return Promise.resolve({ lockHeld: true });
1986
- }
1987
-
1988
2114
  return Promise.resolve();
1989
2115
  } finally {
1990
- // Release lock unless holding for transition
1991
- if (lock && !withTransition) {
2116
+ if (lock) {
1992
2117
  try {
1993
2118
  await lock.release();
1994
2119
  console.log('✅ Merge lock released');
@@ -298,8 +298,12 @@ function create(type, title, description = '', parentId = null, mode = null, nee
298
298
  // Set phase for features (discovery when mode=NULL, implementation when mode is set, NULL for everything else)
299
299
  const phase = type === 'feature' ? (mode ? 'implementation' : 'discovery') : null;
300
300
 
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) {
301
+ // If parent has been rejected, tag this child with the current rejection round
302
+ let rejectionRound = null;
303
+
304
+ function doInsert() {
305
+ const sql = `INSERT INTO work_items (type, title, description, parent_id, epic_id, mode, needs_discovery, phase, status, plan_at_creation, rejection_round) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
306
+ db.run(sql, [type, title, description, parentId, epicId, mode, needsDiscovery ? 1 : 0, phase, 'backlog', planAtCreation, rejectionRound], function(err) {
303
307
  if (err) {
304
308
  return reject(err);
305
309
  }
@@ -327,6 +331,21 @@ function create(type, title, description = '', parentId = null, mode = null, nee
327
331
  resolve(newId);
328
332
  }
329
333
  });
334
+ }
335
+
336
+ if (parentId) {
337
+ db.get('SELECT rejection_count FROM work_items WHERE id = ?', [parentId], (err, parentRow) => {
338
+ if (err) {
339
+ return reject(err);
340
+ }
341
+ if (parentRow && parentRow.rejection_count > 0) {
342
+ rejectionRound = parentRow.rejection_count;
343
+ }
344
+ doInsert();
345
+ });
346
+ } else {
347
+ doInsert();
348
+ }
330
349
  }
331
350
  });
332
351
  }
@@ -398,13 +417,32 @@ function filterProductionItems(items, itemsById) {
398
417
  * Get all work items as hierarchical tree structure
399
418
  * @param {boolean} includeCompleted - Include done/cancelled items (default: false)
400
419
  * @param {boolean} showAll - Show all items including production (bypasses internal/external filtering)
420
+ * @param {number|null} sessionId - If provided, hide items owned by OTHER active sessions
401
421
  * @returns {Promise<Array>} Root work items with nested children
402
422
  * @throws {Error} If database query fails
403
423
  */
404
- function getTree(includeCompleted = false, showAll = false) {
424
+ function getTree(includeCompleted = false, showAll = false, sessionId = null) {
405
425
  return new Promise((resolve, reject) => {
406
- const whereClause = includeCompleted ? '' : "WHERE (status NOT IN ('done', 'cancelled') OR status IS NULL)";
407
- db.all(`SELECT * FROM work_items ${whereClause} ORDER BY parent_id, id`, [], (err, rows) => {
426
+ const conditions = [];
427
+ const params = [];
428
+
429
+ if (!includeCompleted) {
430
+ conditions.push("(status NOT IN ('done', 'cancelled') OR status IS NULL)");
431
+ }
432
+
433
+ // Session isolation: hide items actively owned by OTHER sessions
434
+ if (sessionId) {
435
+ conditions.push(`work_items.id NOT IN (
436
+ SELECT cs.work_item_id FROM claude_sessions cs
437
+ WHERE cs.work_item_id IS NOT NULL
438
+ AND cs.status = 'active'
439
+ AND cs.id != ?
440
+ )`);
441
+ params.push(sessionId);
442
+ }
443
+
444
+ const whereClause = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
445
+ db.all(`SELECT * FROM work_items ${whereClause} ORDER BY parent_id, id`, params, (err, rows) => {
408
446
  if (err) {
409
447
  return reject(new Error(`Failed to fetch work items: ${err.message}`));
410
448
  }
@@ -901,6 +939,14 @@ function setScenario(id, scenarioFile) {
901
939
  });
902
940
  }
903
941
 
942
+ // Set QA steps
943
+ function setQaSteps(id, qaStepsJson) {
944
+ db.run(`UPDATE work_items SET qa_steps = ? WHERE id = ?`, [qaStepsJson, id], () => {
945
+ const steps = JSON.parse(qaStepsJson);
946
+ console.log(`Set #${id} qa_steps (${steps.length} steps)`);
947
+ });
948
+ }
949
+
904
950
  // Set mode
905
951
  function setMode(id, mode) {
906
952
  return new Promise((resolve, reject) => {
@@ -1184,25 +1230,46 @@ async function main() {
1184
1230
 
1185
1231
  switch(command) {
1186
1232
  case 'create': {
1187
- const type = args[0];
1188
- const title = args[1];
1189
- const desc = args[2] || '';
1233
+ // Support --from=<file> for truncation-safe creation via JSON file
1234
+ const fromArg = args.find(a => a.startsWith('--from='));
1235
+ let type, title, desc, parentId, mode, needsDiscovery;
1190
1236
 
1191
- let parentId = null;
1192
- let mode = null;
1193
- let needsDiscovery = false;
1194
-
1195
- args.forEach(arg => {
1196
- if (arg.startsWith('--parent=')) {
1197
- parentId = parseInt(arg.split('=')[1]);
1198
- }
1199
- if (arg.startsWith('--mode=')) {
1200
- mode = arg.split('=')[1];
1201
- }
1202
- if (arg === '--needs-discovery') {
1203
- needsDiscovery = true;
1237
+ if (fromArg) {
1238
+ const fs = require('fs');
1239
+ const filePath = fromArg.split('=').slice(1).join('=');
1240
+ try {
1241
+ const raw = fs.readFileSync(filePath, 'utf8');
1242
+ const data = JSON.parse(raw);
1243
+ type = data.type;
1244
+ title = data.title;
1245
+ desc = data.description || '';
1246
+ parentId = data.parent ? parseInt(data.parent) : null;
1247
+ mode = data.mode || null;
1248
+ needsDiscovery = !!data.needsDiscovery;
1249
+ } catch (e) {
1250
+ console.error(`Error reading --from file: ${e.message}`);
1251
+ process.exit(1);
1204
1252
  }
1205
- });
1253
+ } else {
1254
+ type = args[0];
1255
+ title = args[1];
1256
+ desc = args[2] || '';
1257
+ parentId = null;
1258
+ mode = null;
1259
+ needsDiscovery = false;
1260
+
1261
+ args.forEach(arg => {
1262
+ if (arg.startsWith('--parent=')) {
1263
+ parentId = parseInt(arg.split('=')[1]);
1264
+ }
1265
+ if (arg.startsWith('--mode=')) {
1266
+ mode = arg.split('=')[1];
1267
+ }
1268
+ if (arg === '--needs-discovery') {
1269
+ needsDiscovery = true;
1270
+ }
1271
+ });
1272
+ }
1206
1273
 
1207
1274
  try {
1208
1275
  const newId = await create(type, title, desc, parentId, mode, needsDiscovery);
@@ -1277,7 +1344,7 @@ async function main() {
1277
1344
  LEFT JOIN work_items p ON w.parent_id = p.id
1278
1345
  LEFT JOIN work_items e ON w.epic_id = e.id
1279
1346
  WHERE w.status = 'in_progress'
1280
- ORDER BY w.id ASC
1347
+ ORDER BY w.ready_for_review DESC, w.id ASC
1281
1348
  `, [], (err, rows) => {
1282
1349
  if (err) return reject(err);
1283
1350
  resolve(rows || []);
@@ -1511,7 +1578,23 @@ async function main() {
1511
1578
  expandedIds = new Set(ids);
1512
1579
  }
1513
1580
 
1514
- // Query for ALL in_progress items
1581
+ // Session isolation for backlog display
1582
+ const sessionId = process.env.JETTYPOD_SESSION_ID ? parseInt(process.env.JETTYPOD_SESSION_ID, 10) : null;
1583
+
1584
+ // Query for ALL in_progress items (with session filtering)
1585
+ const activeParams = [];
1586
+ let activeSessionFilter = '';
1587
+ if (sessionId) {
1588
+ activeSessionFilter = `
1589
+ AND w.id NOT IN (
1590
+ SELECT cs.work_item_id FROM claude_sessions cs
1591
+ WHERE cs.work_item_id IS NOT NULL
1592
+ AND cs.status = 'active'
1593
+ AND cs.id != ?
1594
+ )`;
1595
+ activeParams.push(sessionId);
1596
+ }
1597
+
1515
1598
  const activeItems = await new Promise((resolve, reject) => {
1516
1599
  db.all(`
1517
1600
  SELECT w.id, w.title, w.type, w.status,
@@ -1521,8 +1604,9 @@ async function main() {
1521
1604
  LEFT JOIN work_items p ON w.parent_id = p.id
1522
1605
  LEFT JOIN work_items e ON w.epic_id = e.id
1523
1606
  WHERE w.status = 'in_progress'
1524
- ORDER BY w.id ASC
1525
- `, [], (err, rows) => {
1607
+ ${activeSessionFilter}
1608
+ ORDER BY w.ready_for_review DESC, w.id ASC
1609
+ `, activeParams, (err, rows) => {
1526
1610
  if (err) return reject(err);
1527
1611
  resolve(rows || []);
1528
1612
  });
@@ -1652,7 +1736,7 @@ async function main() {
1652
1736
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1653
1737
  console.log('📋 BACKLOG');
1654
1738
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1655
- const items = await getTree(false, showAll);
1739
+ const items = await getTree(false, showAll, sessionId);
1656
1740
 
1657
1741
  printTree(items, '', true, expandedIds);
1658
1742
 
@@ -1728,6 +1812,40 @@ async function main() {
1728
1812
  break;
1729
1813
  }
1730
1814
 
1815
+ case 'set-qa-steps': {
1816
+ const id = parseInt(args[0]);
1817
+ if (isNaN(id)) {
1818
+ console.error('Error: Work item ID is required');
1819
+ console.log('Usage: jettypod work set-qa-steps <id> --from=<path>');
1820
+ process.exit(1);
1821
+ }
1822
+ const fromArg = args.find(a => a.startsWith('--from='));
1823
+ if (!fromArg) {
1824
+ console.error('Error: --from=<path> is required');
1825
+ console.log('Usage: jettypod work set-qa-steps <id> --from=<path>');
1826
+ process.exit(1);
1827
+ }
1828
+ const filePath = fromArg.replace('--from=', '');
1829
+ const fs = require('fs');
1830
+ if (!fs.existsSync(filePath)) {
1831
+ console.error(`Error: File not found: ${filePath}`);
1832
+ process.exit(1);
1833
+ }
1834
+ const qaJson = fs.readFileSync(filePath, 'utf8');
1835
+ try {
1836
+ const parsed = JSON.parse(qaJson);
1837
+ if (!Array.isArray(parsed)) {
1838
+ console.error('Error: QA steps must be a JSON array');
1839
+ process.exit(1);
1840
+ }
1841
+ } catch (e) {
1842
+ console.error(`Error: Invalid JSON in ${filePath}: ${e.message}`);
1843
+ process.exit(1);
1844
+ }
1845
+ setQaSteps(id, qaJson);
1846
+ break;
1847
+ }
1848
+
1731
1849
  case 'current': {
1732
1850
  if (!args[0]) {
1733
1851
  // No ID provided - show current work
@@ -2573,6 +2691,9 @@ Commands:
2573
2691
  jettypod work set-scenario <id> <file>
2574
2692
  Set scenario_file for a feature (e.g., features/my-feature.feature)
2575
2693
 
2694
+ jettypod work set-qa-steps <id> --from=<path>
2695
+ Set QA steps from a JSON file (array of {section, text, detail})
2696
+
2576
2697
  jettypod work current <id>
2577
2698
  Set as current work item
2578
2699