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
package/jettypod.js CHANGED
@@ -496,9 +496,9 @@ Skills auto-activate and MUST complete their full workflow:
496
496
  ✅ ALWAYS let skills complete autonomously before taking manual actions
497
497
 
498
498
  ## Basic Commands (for non-workflow operations)
499
- jettypod work create epic "<title>"
500
- jettypod work create feature "<title>" --parent=<id>
501
- jettypod work create chore "<title>" --parent=<id>
499
+ # To create work items: Write JSON to /tmp/jettypod-create.json, then:
500
+ jettypod work create --from=/tmp/jettypod-create.json
501
+ # JSON format: {"type":"epic|feature|chore|bug","title":"...","description":"...","parent":<id>}
502
502
  jettypod work start <id> # Creates worktree branch
503
503
  jettypod work merge # Merges worktree back to main
504
504
  jettypod work cleanup <id> # Cleanup worktree after merge
@@ -565,6 +565,39 @@ Be direct and opinionated. Skip hedging, apologies, and excessive politeness. Le
565
565
  // Default for missing project name
566
566
  const projectName = config.name || 'unknown-project';
567
567
 
568
+ // Design system context injection
569
+ let designSystemSection = '';
570
+ const designSystemDir = config.designSystemDir;
571
+ if (designSystemDir && typeof designSystemDir === 'string' && fs.existsSync(designSystemDir)) {
572
+ const allowedExts = ['.css', '.scss', '.less', '.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt'];
573
+ const files = fs.readdirSync(designSystemDir).filter(f => {
574
+ const ext = path.extname(f).toLowerCase();
575
+ return allowedExts.includes(ext);
576
+ }).sort();
577
+
578
+ if (files.length > 0) {
579
+ let fileContents = '';
580
+ for (const file of files) {
581
+ const filePath = path.join(designSystemDir, file);
582
+ try {
583
+ const stat = fs.statSync(filePath);
584
+ if (stat.isFile()) {
585
+ const content = fs.readFileSync(filePath, 'utf-8');
586
+ fileContents += `\n### ${file}\n\`\`\`\n${content}\n\`\`\`\n`;
587
+ }
588
+ } catch (e) {
589
+ // Skip unreadable files
590
+ }
591
+ }
592
+ if (fileContents) {
593
+ designSystemSection = `
594
+ <design_system>
595
+ ## Design System Reference
596
+ ${fileContents}</design_system>`;
597
+ }
598
+ }
599
+ }
600
+
568
601
  return `<claude_context project="${projectName}">
569
602
  ${currentWorkSection}
570
603
  <project_state>
@@ -573,7 +606,7 @@ ${projectState} - ${projectStateDescription}
573
606
  <tech_stack>
574
607
  ${techStack}
575
608
  </tech_stack>${modeTag}
576
- </claude_context>${jettypodEssentials}
609
+ </claude_context>${designSystemSection}${jettypodEssentials}
577
610
  ${communicationStyle}
578
611
  ${outputRules}`;
579
612
  },
@@ -671,8 +704,58 @@ async function generateClaude(options = {}) {
671
704
  }
672
705
  }
673
706
 
707
+ /**
708
+ * Sync Claude hook files from claude-hooks/ source templates to .jettypod/hooks/
709
+ * Uses content hashing — only copies when source differs from installed.
710
+ * Called on every CLI invocation via ensureClaudeHooks().
711
+ */
712
+ function syncClaudeHookFiles() {
713
+ const sourceDir = path.join(__dirname, 'claude-hooks');
714
+ const destDir = path.join('.jettypod', 'hooks');
715
+
716
+ if (!fs.existsSync(sourceDir) || !fs.existsSync('.jettypod')) {
717
+ return; // No source hooks or no jettypod project
718
+ }
719
+
720
+ if (!fs.existsSync(destDir)) {
721
+ fs.mkdirSync(destDir, { recursive: true });
722
+ }
723
+
724
+ // Only sync the 3 hooks that are installed by init
725
+ const hooksToSync = [
726
+ 'global-guardrails.js',
727
+ 'protect-claude-md.js',
728
+ 'enforce-skill-activation.js'
729
+ ];
730
+
731
+ const crypto = require('crypto');
732
+ const hash = (content) => crypto.createHash('md5').update(content).digest('hex');
733
+
734
+ for (const hookFile of hooksToSync) {
735
+ const sourcePath = path.join(sourceDir, hookFile);
736
+ const destPath = path.join(destDir, hookFile);
737
+
738
+ if (!fs.existsSync(sourcePath)) continue;
739
+
740
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
741
+
742
+ // Skip if installed hook is already up to date
743
+ if (fs.existsSync(destPath)) {
744
+ const destContent = fs.readFileSync(destPath, 'utf-8');
745
+ if (hash(sourceContent) === hash(destContent)) continue;
746
+ }
747
+
748
+ fs.copyFileSync(sourcePath, destPath);
749
+ fs.chmodSync(destPath, 0o755);
750
+ }
751
+ }
752
+
674
753
  // Ensure Claude Code hooks are up to date (called on every jettypod launch)
675
754
  function ensureClaudeHooks() {
755
+ // Sync hook FILES from claude-hooks/ source templates to .jettypod/hooks/
756
+ // Uses content hashing — only copies when source differs from installed
757
+ syncClaudeHookFiles();
758
+
676
759
  // Create .claude directory if needed
677
760
  if (!fs.existsSync('.claude')) {
678
761
  fs.mkdirSync('.claude', { recursive: true });
@@ -1233,6 +1316,15 @@ if (fs.existsSync('.git')) {
1233
1316
  // Runs independently of .git check since subdirectories don't have .git files
1234
1317
  ensureJettypodSymlink();
1235
1318
 
1319
+ // Sync Claude Code hook files and settings on every CLI invocation
1320
+ if (fs.existsSync('.jettypod')) {
1321
+ try {
1322
+ ensureClaudeHooks();
1323
+ } catch (err) {
1324
+ // Don't fail CLI if hook sync fails
1325
+ }
1326
+ }
1327
+
1236
1328
  if (!commandsWithoutDb.includes(command) && fs.existsSync('.jettypod')) {
1237
1329
  try {
1238
1330
  const { validateOnStartup } = require('./lib/database');
@@ -1250,7 +1342,8 @@ if (!commandsWithoutDb.includes(command) && fs.existsSync('.jettypod')) {
1250
1342
  console.log(`🧹 Cleaned ${cleaned} stale merge lock(s) from previous session`);
1251
1343
  }
1252
1344
  } catch (lockCleanupErr) {
1253
- // Non-fatal: don't block CLI startup if cleanup fails
1345
+ // Non-fatal: don't block CLI startup, but log so failures are visible
1346
+ console.warn(`Warning: Merge lock cleanup failed: ${lockCleanupErr.message}`);
1254
1347
  }
1255
1348
  } catch (err) {
1256
1349
  console.error('');
@@ -1264,110 +1357,10 @@ if (!commandsWithoutDb.includes(command) && fs.existsSync('.jettypod')) {
1264
1357
 
1265
1358
  switch (command) {
1266
1359
  case 'update': {
1267
- // Update jettypod to latest version
1360
+ // CLI is bundled with the app — no independent update mechanism
1268
1361
  const updateCommand = require('./lib/update-command');
1269
- const success = await updateCommand.runUpdate();
1270
-
1271
- // Always refresh skills and hooks in current project after update attempt
1272
- if (fs.existsSync('.jettypod')) {
1273
- // Update Claude Code hooks first
1274
- ensureClaudeHooks();
1275
-
1276
- console.log('');
1277
- console.log('🔄 Refreshing skills in current project...');
1278
-
1279
- // Use the existing skills update logic from initializeProject
1280
- // Allow tests to override skills source directory
1281
- const skillsSourceDir = process.env.JETTYPOD_SKILLS_SOURCE_DIR ||
1282
- path.join(__dirname, 'skills-templates');
1283
- const skillsDestDir = path.join('.claude', 'skills');
1284
- let backupDir = null;
1285
-
1286
- // Backup existing skills before updating
1287
- if (fs.existsSync(skillsDestDir)) {
1288
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1289
- backupDir = path.join('.claude', `skills.backup-${timestamp}`);
1290
-
1291
- let counter = 1;
1292
- while (fs.existsSync(backupDir)) {
1293
- backupDir = path.join('.claude', `skills.backup-${timestamp}-${counter}`);
1294
- counter++;
1295
- }
1296
-
1297
- try {
1298
- fs.renameSync(skillsDestDir, backupDir);
1299
-
1300
- if (!fs.existsSync(backupDir)) {
1301
- throw new Error('Backup directory was not created');
1302
- }
1303
-
1304
- const backupContents = fs.readdirSync(backupDir);
1305
- if (backupContents.length === 0) {
1306
- throw new Error('Backup directory is empty');
1307
- }
1308
-
1309
- console.log(`💾 Backed up existing skills to ${backupDir}`);
1310
- } catch (err) {
1311
- console.error(`❌ Could not backup skills: ${err.message}`);
1312
- throw new Error(`Could not backup existing skills: ${err.message}`);
1313
- }
1314
- }
1315
-
1316
- // Copy skills from jettypod to project
1317
- if (!fs.existsSync(skillsSourceDir)) {
1318
- console.warn('⚠️ Skills source directory not found');
1319
- } else {
1320
- try {
1321
- const copyRecursive = (src, dest) => {
1322
- if (!fs.existsSync(dest)) {
1323
- fs.mkdirSync(dest, { recursive: true });
1324
- }
1325
- const entries = fs.readdirSync(src, { withFileTypes: true });
1326
- for (const entry of entries) {
1327
- const srcPath = path.join(src, entry.name);
1328
- const destPath = path.join(dest, entry.name);
1329
- if (entry.isDirectory()) {
1330
- copyRecursive(srcPath, destPath);
1331
- } else {
1332
- fs.copyFileSync(srcPath, destPath);
1333
- }
1334
- }
1335
- };
1336
-
1337
- copyRecursive(skillsSourceDir, skillsDestDir);
1338
- console.log('✅ Skills refreshed');
1339
- } catch (err) {
1340
- console.error(`❌ Could not refresh skills: ${err.message}`);
1341
- }
1342
- }
1343
-
1344
- // Ensure session.md is gitignored (fixes existing projects)
1345
- ensureJettypodGitignores();
1346
-
1347
- // Auto-commit any changes made by update (skills, hooks, gitignore, etc.)
1348
- // This prevents untracked changes from breaking worktree merges
1349
- try {
1350
- const { execSync } = require('child_process');
1351
- // Check if there are any changes to commit
1352
- const status = execSync('git status --porcelain', { encoding: 'utf-8' });
1353
- if (status.trim()) {
1354
- execSync('git add -A');
1355
- // Use --no-verify to bypass pre-commit hooks - this is an automated system update
1356
- execSync('git commit --no-verify -m "chore: jettypod update - refresh skills and hooks"');
1357
- console.log('✅ Update changes committed');
1358
- }
1359
- } catch (err) {
1360
- // Log the error so users know what happened
1361
- console.log('');
1362
- console.log('⚠️ Could not auto-commit skill updates');
1363
- console.log(' Reason:', err.message.split('\n')[0]);
1364
- console.log('');
1365
- console.log(' You may have untracked changes in .claude/skills/');
1366
- console.log(' Run: git add .claude && git commit -m "chore: update skills"');
1367
- }
1368
- }
1369
-
1370
- process.exit(success ? 0 : 1);
1362
+ await updateCommand.runUpdate();
1363
+ process.exit(0);
1371
1364
  break;
1372
1365
  }
1373
1366
 
@@ -1454,13 +1447,14 @@ switch (command) {
1454
1447
  } else if (subcommand === 'cleanup') {
1455
1448
  const workCommands = require('./lib/work-commands/index.js');
1456
1449
  const dryRun = args.includes('--dry-run');
1450
+ const force = args.includes('--force');
1457
1451
  // Find numeric arg for specific work item cleanup
1458
1452
  const workItemId = args.find(a => /^\d+$/.test(a)) ? parseInt(args.find(a => /^\d+$/.test(a))) : null;
1459
1453
 
1460
1454
  try {
1461
1455
  if (workItemId) {
1462
1456
  // Clean up specific work item's worktree
1463
- await workCommands.cleanupWorkItem(workItemId);
1457
+ await workCommands.cleanupWorkItem(workItemId, { force });
1464
1458
  } else {
1465
1459
  // Batch cleanup of all orphaned worktrees
1466
1460
  const results = await workCommands.cleanupWorktrees({ dryRun });
@@ -1483,12 +1477,11 @@ switch (command) {
1483
1477
  const workCommands = require('./lib/work-commands/index.js');
1484
1478
  try {
1485
1479
  // Parse merge flags and optional work item ID from args
1486
- const withTransition = args.includes('--with-transition');
1487
1480
  const releaseLock = args.includes('--release-lock');
1488
1481
  // Find numeric arg that isn't a flag
1489
1482
  const workItemId = args.find(a => /^\d+$/.test(a)) ? parseInt(args.find(a => /^\d+$/.test(a))) : null;
1490
1483
 
1491
- await workCommands.mergeWork({ withTransition, releaseLock, workItemId });
1484
+ await workCommands.mergeWork({ releaseLock, workItemId });
1492
1485
  } catch (err) {
1493
1486
  console.error(`Error: ${err.message}`);
1494
1487
  process.exit(1);
@@ -2353,7 +2346,33 @@ switch (command) {
2353
2346
  break;
2354
2347
  }
2355
2348
 
2356
- case undefined:
2349
+ case undefined: {
2350
+ // Auto-update: if running from a stale global install, reinstall from source
2351
+ // Skip in worktrees - worktree copies always differ from source and updating
2352
+ // the global install won't change the worktree file, causing infinite recursion
2353
+ const devLinkPath = path.join(require('os').homedir(), '.jettypod-dev.json');
2354
+ if (fs.existsSync(devLinkPath) && !__dirname.includes('.jettypod-work')) {
2355
+ try {
2356
+ const devInfo = JSON.parse(fs.readFileSync(devLinkPath, 'utf-8'));
2357
+ const sourceDir = devInfo.sourceDir;
2358
+ if (sourceDir && fs.existsSync(path.join(sourceDir, 'jettypod.js'))) {
2359
+ const sourceContent = fs.readFileSync(path.join(sourceDir, 'jettypod.js'));
2360
+ const installedContent = fs.readFileSync(path.join(__dirname, 'jettypod.js'));
2361
+ if (!sourceContent.equals(installedContent)) {
2362
+ console.log('🔄 Source has changed, updating jettypod...');
2363
+ const { execSync: execSyncUpdate } = require('child_process');
2364
+ execSyncUpdate('npm install -g .', { cwd: sourceDir, stdio: 'inherit' });
2365
+ console.log('✅ Updated. Relaunching...');
2366
+ const { execFileSync } = require('child_process');
2367
+ execFileSync(process.argv[0], process.argv.slice(1), { stdio: 'inherit', cwd: process.cwd() });
2368
+ process.exit(0);
2369
+ }
2370
+ }
2371
+ } catch (e) {
2372
+ // Auto-update check failed — continue with current version
2373
+ }
2374
+ }
2375
+
2357
2376
  // Smart detection when no command provided
2358
2377
  if (!fs.existsSync('.jettypod/config.json')) {
2359
2378
  // New project - auto-initialize
@@ -2457,12 +2476,13 @@ switch (command) {
2457
2476
 
2458
2477
  // Start dashboard in dev mode for live reload
2459
2478
  console.log('🚀 Starting dashboard (dev mode)...');
2460
- const dashboardProcess = spawn('npm', ['run', 'dev', '--', '-p', String(availablePort)], {
2479
+ const dashboardProcess = spawn('npm', ['run', 'dev'], {
2461
2480
  cwd: dashboardPath,
2462
2481
  detached: true,
2463
2482
  stdio: 'ignore',
2464
2483
  env: {
2465
2484
  ...process.env,
2485
+ PORT: String(availablePort),
2466
2486
  JETTYPOD_PROJECT_PATH: process.cwd(),
2467
2487
  JETTYPOD_WS_PORT: String(WS_PORT)
2468
2488
  }
@@ -2520,6 +2540,7 @@ Quick commands:
2520
2540
  }
2521
2541
  }
2522
2542
  break;
2543
+ }
2523
2544
 
2524
2545
  case 'trash': {
2525
2546
  // Trash management commands
@@ -2652,9 +2673,17 @@ Quick commands:
2652
2673
  const impact = graph.getImpact(targetFile);
2653
2674
 
2654
2675
  if (impact.error) {
2655
- console.error(`\n❌ ${impact.error}`);
2656
- console.log('');
2657
- console.log('Make sure the file path is relative to the project root.');
2676
+ console.log(`\n❌ ${impact.error}`);
2677
+ if (impact.suggestions && impact.suggestions.length > 0) {
2678
+ console.log('');
2679
+ console.log('Did you mean one of these?');
2680
+ for (const s of impact.suggestions) {
2681
+ console.log(` ${s}`);
2682
+ }
2683
+ } else {
2684
+ console.log('');
2685
+ console.log('Make sure the file path is relative to the project root.');
2686
+ }
2658
2687
  process.exit(1);
2659
2688
  }
2660
2689
 
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function checkCucumberInstalled(projectRoot) {
7
+ const cucumberPath = path.join(projectRoot, 'node_modules', '@cucumber', 'cucumber');
8
+ const exists = fs.existsSync(cucumberPath);
9
+ return {
10
+ id: 'cucumber-installed',
11
+ label: '@cucumber/cucumber installed',
12
+ passed: exists,
13
+ error: exists ? null : '@cucumber/cucumber is not installed. Run: npm install --save-dev @cucumber/cucumber',
14
+ };
15
+ }
16
+
17
+ function checkTestBddScript(projectRoot) {
18
+ const pkgPath = path.join(projectRoot, 'package.json');
19
+ let hasScript = false;
20
+ if (fs.existsSync(pkgPath)) {
21
+ try {
22
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
23
+ hasScript = !!(pkg.scripts && pkg.scripts['test:bdd']);
24
+ } catch (_) {
25
+ // Corrupt or unreadable package.json — treat as missing script
26
+ }
27
+ }
28
+ return {
29
+ id: 'test-bdd-script',
30
+ label: 'test:bdd script in package.json',
31
+ passed: hasScript,
32
+ error: hasScript ? null : 'Missing "test:bdd" script in package.json. Add: "test:bdd": "NODE_ENV=test cucumber-js"',
33
+ };
34
+ }
35
+
36
+ function checkCucumberConfig(projectRoot) {
37
+ const configPath = path.join(projectRoot, 'cucumber.js');
38
+ const exists = fs.existsSync(configPath);
39
+ return {
40
+ id: 'cucumber-config',
41
+ label: 'cucumber.js config file',
42
+ passed: exists,
43
+ error: exists ? null : 'Missing cucumber.js configuration file in project root',
44
+ };
45
+ }
46
+
47
+ function checkFeaturesDirectory(projectRoot) {
48
+ const featuresDir = path.join(projectRoot, 'features');
49
+ const stepDefsDir = path.join(projectRoot, 'features', 'step_definitions');
50
+ const exists = fs.existsSync(featuresDir) && fs.existsSync(stepDefsDir);
51
+ return {
52
+ id: 'features-directory',
53
+ label: 'features/ directory with step_definitions/',
54
+ passed: exists,
55
+ error: exists ? null : 'Missing features/ directory or features/step_definitions/ subdirectory',
56
+ };
57
+ }
58
+
59
+ function checkSupportDirectory(projectRoot) {
60
+ const supportDir = path.join(projectRoot, 'features', 'support');
61
+ const hooksFile = path.join(supportDir, 'hooks.js');
62
+ const exists = fs.existsSync(supportDir) && fs.existsSync(hooksFile);
63
+ return {
64
+ id: 'support-hooks',
65
+ label: 'features/support/hooks.js',
66
+ passed: exists,
67
+ error: exists ? null : 'Missing features/support/hooks.js file',
68
+ };
69
+ }
70
+
71
+ function buildSetupPrompt(projectRoot, failedChecks) {
72
+ const missing = failedChecks.map(c => `- ${c.label}: ${c.error}`).join('\n');
73
+ return `The following BDD test infrastructure is missing in this project:\n\n${missing}\n\nPlease set up the missing BDD test infrastructure so tests can run.`;
74
+ }
75
+
76
+ function checkBddPrerequisites(projectRoot) {
77
+ const checks = [
78
+ checkCucumberInstalled(projectRoot),
79
+ checkTestBddScript(projectRoot),
80
+ checkCucumberConfig(projectRoot),
81
+ checkFeaturesDirectory(projectRoot),
82
+ checkSupportDirectory(projectRoot),
83
+ ];
84
+
85
+ const ready = checks.every(c => c.passed);
86
+ const failed = checks.filter(c => !c.passed);
87
+
88
+ const result = { ready, checks };
89
+ if (!ready) {
90
+ result.setupPrompt = buildSetupPrompt(projectRoot, failed);
91
+ }
92
+
93
+ return result;
94
+ }
95
+
96
+ module.exports = { checkBddPrerequisites };
@@ -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