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
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ESNext"],
7
+ "types": ["@cloudflare/workers-types"],
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }
@@ -0,0 +1,35 @@
1
+ name = "jettypod-update-server"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2025-02-01"
4
+
5
+ # R2 bucket for release artifacts (DMG, ZIP, latest-mac.yml)
6
+ [[r2_buckets]]
7
+ binding = "RELEASE_ARTIFACTS"
8
+ bucket_name = "jettypod-releases"
9
+
10
+ # D1 database for user accounts + usage tracking
11
+ [[d1_databases]]
12
+ binding = "AUTH_DB"
13
+ database_name = "jettypod-auth"
14
+ database_id = "80205398-8f56-4e04-a612-4a348458098f"
15
+
16
+ # KV namespace for OTP codes (with TTL)
17
+ [[kv_namespaces]]
18
+ binding = "AUTH_KV"
19
+ id = "91b43f72715e4c26a15b1dc0371af225"
20
+
21
+ # Stripe secrets - set via wrangler:
22
+ # wrangler secret put STRIPE_SECRET_KEY
23
+ # wrangler secret put STRIPE_WEBHOOK_SECRET
24
+ # wrangler secret put STRIPE_MONTHLY_PRICE_ID
25
+ # wrangler secret put STRIPE_LIFETIME_PRICE_ID
26
+ # Auth secrets - set via wrangler:
27
+ # wrangler secret put GOOGLE_CLIENT_ID
28
+ # wrangler secret put GOOGLE_CLIENT_SECRET
29
+ # wrangler secret put JWT_SECRET
30
+ # wrangler secret put RESEND_API_KEY
31
+ # Do NOT put actual keys here. Use wrangler secrets for sensitive values.
32
+
33
+ # Environment variables (non-secret)
34
+ [vars]
35
+ ENVIRONMENT = "production"
package/cucumber.js CHANGED
@@ -1,7 +1,13 @@
1
+ const path = require('path');
2
+
1
3
  module.exports = {
2
4
  default: {
3
- require: ['features/**/steps.js', 'features/**/simple-steps.js', 'features/**/*.steps.js'],
4
- format: ['progress', 'json:cucumber-results.json'],
5
- publishQuiet: true
5
+ require: ['features/support/*.js', 'features/**/steps.js', 'features/**/simple-steps.js', 'features/**/*.steps.js'],
6
+ format: [
7
+ 'progress',
8
+ `json:${path.resolve(__dirname, 'cucumber-results.json')}`,
9
+ `message:${path.resolve(__dirname, 'cucumber-results.ndjson')}`,
10
+ `rerun:${path.resolve(__dirname, '@rerun.txt')}`,
11
+ ]
6
12
  }
7
13
  };
@@ -98,8 +98,36 @@ jettypod project discover complete \
98
98
 
99
99
  ## Work Item Management
100
100
 
101
+ ### `jettypod work create --from=<filepath>`
102
+
103
+ Create any work item type from a JSON file. **This is the preferred method** — it avoids shell quoting issues and is truncation-safe (a truncated file path errors immediately instead of hanging).
104
+
105
+ ```bash
106
+ # Write JSON file first, then create
107
+ jettypod work create --from=/tmp/jettypod-create.json
108
+ ```
109
+
110
+ **JSON file format:**
111
+ ```json
112
+ {
113
+ "type": "feature",
114
+ "title": "Login Form",
115
+ "description": "User login UI component",
116
+ "parent": 5,
117
+ "mode": "speed",
118
+ "needsDiscovery": false
119
+ }
120
+ ```
121
+
122
+ **Required fields:** `type` (epic/feature/chore/bug), `title`
123
+ **Optional fields:** `description`, `parent` (ID), `mode` (speed/stable/production), `needsDiscovery` (boolean)
124
+
125
+ ---
126
+
101
127
  ### `jettypod work create epic <title> [description] [flags]`
102
128
 
129
+ > **Prefer `--from` (above)** for Claude Code usage — positional args risk shell hangs from truncated quotes.
130
+
103
131
  Create a new epic.
104
132
 
105
133
  ```bash
@@ -126,6 +154,8 @@ jettypod work create epic "Real-time Updates" "Live data sync" --needs-discovery
126
154
 
127
155
  ### `jettypod work create feature <title> [description] [flags]`
128
156
 
157
+ > **Prefer `--from` (above)** for Claude Code usage — positional args risk shell hangs from truncated quotes.
158
+
129
159
  Create a new feature.
130
160
 
131
161
  ```bash
@@ -155,6 +185,8 @@ jettypod work create feature "Dashboard" "User dashboard" --mode=stable
155
185
 
156
186
  ### `jettypod work create chore <title> [description] [flags]`
157
187
 
188
+ > **Prefer `--from` (above)** for Claude Code usage — positional args risk shell hangs from truncated quotes.
189
+
158
190
  Create a chore (non-feature work).
159
191
 
160
192
  ```bash
@@ -170,6 +202,8 @@ jettypod work create chore "Refactor auth module" --parent=5
170
202
 
171
203
  ### `jettypod work create bug <title> [description] [flags]`
172
204
 
205
+ > **Prefer `--from` (above)** for Claude Code usage — positional args risk shell hangs from truncated quotes.
206
+
173
207
  Create a bug.
174
208
 
175
209
  ```bash
@@ -7,96 +7,53 @@
7
7
  * 2. Imports database snapshots after checkout
8
8
  */
9
9
 
10
- const { importAll } = require('jettypod/lib/db-import');
11
- const { walCheckpoint } = require('jettypod/lib/database');
12
10
  const { execSync } = require('child_process');
13
11
 
14
- (async () => {
15
- const cwd = process.cwd();
12
+ const cwd = process.cwd();
16
13
 
17
- // FIRST: Check if we're in a JettyPod worktree and prevent default branch checkout
18
- if (cwd.includes('.jettypod-work')) {
14
+ // Prevent checking out default branch in JettyPod worktrees
15
+ if (cwd.includes('.jettypod-work')) {
16
+ try {
17
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
18
+ encoding: 'utf8',
19
+ stdio: ['pipe', 'pipe', 'pipe']
20
+ }).trim();
21
+
22
+ // Detect default branch
23
+ let defaultBranch;
19
24
  try {
20
- // Get the branch we just checked out
21
- const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
25
+ defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
22
26
  encoding: 'utf8',
23
27
  stdio: ['pipe', 'pipe', 'pipe']
24
- }).trim();
25
-
26
- // Detect default branch
27
- let defaultBranch;
28
+ }).trim().replace('refs/remotes/origin/', '');
29
+ } catch {
28
30
  try {
29
- // Try to get the default branch from git config
30
- defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
31
- encoding: 'utf8',
32
- stdio: ['pipe', 'pipe', 'pipe']
33
- }).trim().replace('refs/remotes/origin/', '');
31
+ execSync('git rev-parse --verify main', { stdio: ['pipe', 'pipe', 'pipe'] });
32
+ defaultBranch = 'main';
34
33
  } catch {
35
- // Fallback: check which common branch names exist
36
34
  try {
37
- execSync('git rev-parse --verify main', { stdio: ['pipe', 'pipe', 'pipe'] });
38
- defaultBranch = 'main';
35
+ execSync('git rev-parse --verify master', { stdio: ['pipe', 'pipe', 'pipe'] });
36
+ defaultBranch = 'master';
39
37
  } catch {
40
- try {
41
- execSync('git rev-parse --verify master', { stdio: ['pipe', 'pipe', 'pipe'] });
42
- defaultBranch = 'master';
43
- } catch {
44
- // Can't determine default branch - skip check
45
- defaultBranch = null;
46
- }
38
+ defaultBranch = null;
47
39
  }
48
40
  }
41
+ }
49
42
 
50
- // Check if we checked out the default branch
51
- if (defaultBranch && currentBranch === defaultBranch) {
52
- console.error('');
53
- console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
54
- console.error(' ERROR: Cannot checkout default branch in worktree');
55
- console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
56
- console.error('');
57
- console.error(`You are in a JettyPod worktree for isolated work.`);
58
- console.error(`Checking out ${defaultBranch} in a worktree bypasses the merge workflow.`);
59
- console.error('');
60
- console.error('To merge your changes:');
61
- console.error(' 1. Commit your changes: git add . && git commit -m "..."');
62
- console.error(' 2. Use JettyPod merge: jettypod work merge');
63
- console.error('');
64
- console.error('This ensures proper worktree cleanup and database updates.');
65
- console.error('');
66
- console.error('Reverting checkout...');
67
- console.error('');
68
-
69
- // Revert to previous branch
70
- try {
71
- execSync('git checkout -', { stdio: 'inherit' });
72
- } catch (revertErr) {
73
- console.error('Failed to revert checkout. You may need to manually switch branches.');
74
- }
43
+ if (defaultBranch && currentBranch === defaultBranch) {
44
+ console.error('\n❌ Cannot checkout default branch in worktree');
45
+ console.error(`Checking out ${defaultBranch} bypasses the merge workflow.\n`);
46
+ console.error('To merge: jettypod work merge\n');
47
+ console.error('Reverting checkout...\n');
75
48
 
76
- process.exit(1);
49
+ try {
50
+ execSync('git checkout -', { stdio: 'inherit' });
51
+ } catch (revertErr) {
52
+ console.error('Failed to revert. Manually switch branches.');
77
53
  }
78
- } catch (err) {
79
- // If we can't determine the branch, allow the checkout
80
- // (better to be permissive than block legitimate operations)
54
+ process.exit(1);
81
55
  }
82
- }
83
-
84
- // SECOND: Checkpoint WAL before importing to prevent corruption
85
- // This flushes any pending writes to the main database file
86
- try {
87
- await walCheckpoint();
88
- } catch (err) {
89
- // Checkpoint failure shouldn't block - just log and continue
90
- console.error('Post-checkout hook warning: WAL checkpoint failed:', err.message);
91
- }
92
-
93
- // THIRD: Import database snapshots
94
- try {
95
- await importAll();
96
- process.exit(0);
97
56
  } catch (err) {
98
- // Log error but don't block checkout
99
- console.error('Post-checkout hook warning:', err.message);
100
- process.exit(0);
57
+ // Can't determine branch - allow checkout
101
58
  }
102
- })();
59
+ }
package/hooks/post-merge CHANGED
@@ -1,17 +1,118 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { importAll } = require('jettypod/lib/db-import');
3
+ // JettyPod Git Hook: post-merge
4
+ // Backs up database to .jettypod-backup/ when merging to main
5
+ // Uses --no-verify to bypass pre-commit tests for data-only commits
4
6
 
5
- (async () => {
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+
11
+ const jettypodDir = path.join(process.cwd(), '.jettypod');
12
+ const backupDir = path.join(process.cwd(), '.jettypod-backup');
13
+
14
+ // Backup database and commit
15
+ function backupDatabase() {
16
+ if (!fs.existsSync(jettypodDir)) {
17
+ return; // No JettyPod directory, skip backup
18
+ }
19
+
20
+ // Only backup on main/master branch
21
+ let currentBranch;
6
22
  try {
7
- // Import JSON snapshots into databases after merge
8
- await importAll();
23
+ currentBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
24
+ } catch {
25
+ return;
26
+ }
27
+
28
+ if (currentBranch !== 'main' && currentBranch !== 'master') {
29
+ return;
30
+ }
31
+
32
+ console.log('\n💾 Backing up database...\n');
33
+
34
+ try {
35
+ // Ensure backup directory exists
36
+ if (!fs.existsSync(backupDir)) {
37
+ fs.mkdirSync(backupDir, { recursive: true });
38
+ }
39
+
40
+ // Backup work.db using sqlite3 .backup command
41
+ // CRITICAL: fs.copyFileSync does NOT work with WAL mode - it only copies
42
+ // the base .db file, missing all data in the -wal file. sqlite3 .backup
43
+ // reads the logical database (including WAL) and writes a clean standalone file.
44
+ const sourcePath = path.join(jettypodDir, 'work.db');
45
+ const backupPath = path.join(backupDir, 'work.db');
9
46
 
10
- // Exit successfully - merge should not be blocked
11
- process.exit(0);
47
+ if (!fs.existsSync(sourcePath)) {
48
+ console.log('⚠️ No work.db found, skipping backup\n');
49
+ return;
50
+ }
51
+
52
+ execSync(`sqlite3 "${sourcePath}" ".backup '${backupPath}'"`, {
53
+ stdio: ['pipe', 'pipe', 'pipe']
54
+ });
55
+
56
+ // SAFETY: Validate backup didn't lose data
57
+ // If an existing backup has significantly more items, refuse to overwrite
58
+ const newCount = parseInt(execSync(
59
+ `sqlite3 "${backupPath}" "SELECT count(*) FROM work_items"`,
60
+ { encoding: 'utf-8', stdio: 'pipe' }
61
+ ).trim()) || 0;
62
+
63
+ if (newCount < 2) {
64
+ console.log('⚠️ Backup has fewer than 2 items - likely corrupted, skipping commit\n');
65
+ return;
66
+ }
67
+
68
+ // Check against git's previous version of the backup
69
+ try {
70
+ const oldCount = parseInt(execSync(
71
+ `git show HEAD:.jettypod-backup/work.db 2>/dev/null | sqlite3 "" "SELECT count(*) FROM work_items"`,
72
+ { encoding: 'utf-8', stdio: 'pipe' }
73
+ ).trim()) || 0;
74
+
75
+ if (oldCount > 0 && newCount < oldCount * 0.5) {
76
+ console.log(`⚠️ Backup shrank from ${oldCount} to ${newCount} items (>50% loss) - refusing to commit\n`);
77
+ return;
78
+ }
79
+ } catch {
80
+ // No previous backup in git or can't read it - proceed
81
+ }
82
+
83
+ // Stage the backup file (force-add because .jettypod-backup is gitignored
84
+ // to prevent worktree symlink corruption, but backups must be committed)
85
+ execSync(`git add -f "${backupPath}"`, {
86
+ stdio: ['pipe', 'pipe', 'pipe']
87
+ });
88
+
89
+ // Check if there are any staged changes
90
+ try {
91
+ execSync('git diff --cached --quiet', {
92
+ stdio: ['pipe', 'pipe', 'pipe']
93
+ });
94
+ // No staged changes, nothing to commit
95
+ console.log('✅ Database backup unchanged\n');
96
+ return;
97
+ } catch {
98
+ // Has staged changes, continue to commit
99
+ }
100
+
101
+ execSync('git commit --no-verify -m "chore: Update database backup"', {
102
+ stdio: ['pipe', 'pipe', 'pipe']
103
+ });
104
+
105
+ // Push the backup commit
106
+ execSync('git push', {
107
+ stdio: ['pipe', 'pipe', 'pipe']
108
+ });
109
+
110
+ console.log('✅ Database backed up and pushed\n');
12
111
  } catch (err) {
13
- // Log error but don't block merge
14
- console.error('Post-merge hook warning:', err.message);
15
- process.exit(0);
112
+ console.error('⚠️ Database backup failed:', err.message);
113
+ // Don't block - backup is important but not critical
16
114
  }
17
- })();
115
+ }
116
+
117
+ // Main
118
+ backupDatabase();
package/jest.setup.js ADDED
@@ -0,0 +1 @@
1
+ process.env.NODE_ENV = 'test';
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
@@ -877,6 +877,11 @@ async function initializeProject() {
877
877
  }
878
878
  });
879
879
  if (result) {
880
+ // Mark onboarding chores as conversational (no worktree, no 5s delay)
881
+ const db = getDb();
882
+ for (const choreId of result.choreIds) {
883
+ db.run('UPDATE work_items SET conversational = 1 WHERE id = ?', [choreId]);
884
+ }
880
885
  console.log(`📋 Created onboarding epic with ${result.choreIds.length} chores`);
881
886
  console.log(` Start with: jettypod work start ${result.choreIds[0]}`);
882
887
  }
@@ -1259,110 +1264,10 @@ if (!commandsWithoutDb.includes(command) && fs.existsSync('.jettypod')) {
1259
1264
 
1260
1265
  switch (command) {
1261
1266
  case 'update': {
1262
- // Update jettypod to latest version
1267
+ // CLI is bundled with the app — no independent update mechanism
1263
1268
  const updateCommand = require('./lib/update-command');
1264
- const success = await updateCommand.runUpdate();
1265
-
1266
- // Always refresh skills and hooks in current project after update attempt
1267
- if (fs.existsSync('.jettypod')) {
1268
- // Update Claude Code hooks first
1269
- ensureClaudeHooks();
1270
-
1271
- console.log('');
1272
- console.log('🔄 Refreshing skills in current project...');
1273
-
1274
- // Use the existing skills update logic from initializeProject
1275
- // Allow tests to override skills source directory
1276
- const skillsSourceDir = process.env.JETTYPOD_SKILLS_SOURCE_DIR ||
1277
- path.join(__dirname, 'skills-templates');
1278
- const skillsDestDir = path.join('.claude', 'skills');
1279
- let backupDir = null;
1280
-
1281
- // Backup existing skills before updating
1282
- if (fs.existsSync(skillsDestDir)) {
1283
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1284
- backupDir = path.join('.claude', `skills.backup-${timestamp}`);
1285
-
1286
- let counter = 1;
1287
- while (fs.existsSync(backupDir)) {
1288
- backupDir = path.join('.claude', `skills.backup-${timestamp}-${counter}`);
1289
- counter++;
1290
- }
1291
-
1292
- try {
1293
- fs.renameSync(skillsDestDir, backupDir);
1294
-
1295
- if (!fs.existsSync(backupDir)) {
1296
- throw new Error('Backup directory was not created');
1297
- }
1298
-
1299
- const backupContents = fs.readdirSync(backupDir);
1300
- if (backupContents.length === 0) {
1301
- throw new Error('Backup directory is empty');
1302
- }
1303
-
1304
- console.log(`💾 Backed up existing skills to ${backupDir}`);
1305
- } catch (err) {
1306
- console.error(`❌ Could not backup skills: ${err.message}`);
1307
- throw new Error(`Could not backup existing skills: ${err.message}`);
1308
- }
1309
- }
1310
-
1311
- // Copy skills from jettypod to project
1312
- if (!fs.existsSync(skillsSourceDir)) {
1313
- console.warn('⚠️ Skills source directory not found');
1314
- } else {
1315
- try {
1316
- const copyRecursive = (src, dest) => {
1317
- if (!fs.existsSync(dest)) {
1318
- fs.mkdirSync(dest, { recursive: true });
1319
- }
1320
- const entries = fs.readdirSync(src, { withFileTypes: true });
1321
- for (const entry of entries) {
1322
- const srcPath = path.join(src, entry.name);
1323
- const destPath = path.join(dest, entry.name);
1324
- if (entry.isDirectory()) {
1325
- copyRecursive(srcPath, destPath);
1326
- } else {
1327
- fs.copyFileSync(srcPath, destPath);
1328
- }
1329
- }
1330
- };
1331
-
1332
- copyRecursive(skillsSourceDir, skillsDestDir);
1333
- console.log('✅ Skills refreshed');
1334
- } catch (err) {
1335
- console.error(`❌ Could not refresh skills: ${err.message}`);
1336
- }
1337
- }
1338
-
1339
- // Ensure session.md is gitignored (fixes existing projects)
1340
- ensureJettypodGitignores();
1341
-
1342
- // Auto-commit any changes made by update (skills, hooks, gitignore, etc.)
1343
- // This prevents untracked changes from breaking worktree merges
1344
- try {
1345
- const { execSync } = require('child_process');
1346
- // Check if there are any changes to commit
1347
- const status = execSync('git status --porcelain', { encoding: 'utf-8' });
1348
- if (status.trim()) {
1349
- execSync('git add -A');
1350
- // Use --no-verify to bypass pre-commit hooks - this is an automated system update
1351
- execSync('git commit --no-verify -m "chore: jettypod update - refresh skills and hooks"');
1352
- console.log('✅ Update changes committed');
1353
- }
1354
- } catch (err) {
1355
- // Log the error so users know what happened
1356
- console.log('');
1357
- console.log('⚠️ Could not auto-commit skill updates');
1358
- console.log(' Reason:', err.message.split('\n')[0]);
1359
- console.log('');
1360
- console.log(' You may have untracked changes in .claude/skills/');
1361
- console.log(' Run: git add .claude && git commit -m "chore: update skills"');
1362
- }
1363
- }
1364
-
1365
- process.exit(success ? 0 : 1);
1269
+ await updateCommand.runUpdate();
1270
+ process.exit(0);
1366
1271
  break;
1367
1272
  }
1368
1273
 
@@ -1449,13 +1354,14 @@ switch (command) {
1449
1354
  } else if (subcommand === 'cleanup') {
1450
1355
  const workCommands = require('./lib/work-commands/index.js');
1451
1356
  const dryRun = args.includes('--dry-run');
1357
+ const force = args.includes('--force');
1452
1358
  // Find numeric arg for specific work item cleanup
1453
1359
  const workItemId = args.find(a => /^\d+$/.test(a)) ? parseInt(args.find(a => /^\d+$/.test(a))) : null;
1454
1360
 
1455
1361
  try {
1456
1362
  if (workItemId) {
1457
1363
  // Clean up specific work item's worktree
1458
- await workCommands.cleanupWorkItem(workItemId);
1364
+ await workCommands.cleanupWorkItem(workItemId, { force });
1459
1365
  } else {
1460
1366
  // Batch cleanup of all orphaned worktrees
1461
1367
  const results = await workCommands.cleanupWorktrees({ dryRun });
@@ -2200,10 +2106,6 @@ switch (command) {
2200
2106
  }
2201
2107
  });
2202
2108
 
2203
- // Clear checkpoint
2204
- const checkpoint = require('./lib/discovery-checkpoint');
2205
- checkpoint.clearCheckpoint();
2206
-
2207
2109
  await generateClaude({ autoCommit: true });
2208
2110
 
2209
2111
  console.log('✅ Project discovery complete!');
@@ -2352,7 +2254,33 @@ switch (command) {
2352
2254
  break;
2353
2255
  }
2354
2256
 
2355
- case undefined:
2257
+ case undefined: {
2258
+ // Auto-update: if running from a stale global install, reinstall from source
2259
+ // Skip in worktrees - worktree copies always differ from source and updating
2260
+ // the global install won't change the worktree file, causing infinite recursion
2261
+ const devLinkPath = path.join(require('os').homedir(), '.jettypod-dev.json');
2262
+ if (fs.existsSync(devLinkPath) && !__dirname.includes('.jettypod-work')) {
2263
+ try {
2264
+ const devInfo = JSON.parse(fs.readFileSync(devLinkPath, 'utf-8'));
2265
+ const sourceDir = devInfo.sourceDir;
2266
+ if (sourceDir && fs.existsSync(path.join(sourceDir, 'jettypod.js'))) {
2267
+ const sourceContent = fs.readFileSync(path.join(sourceDir, 'jettypod.js'));
2268
+ const installedContent = fs.readFileSync(path.join(__dirname, 'jettypod.js'));
2269
+ if (!sourceContent.equals(installedContent)) {
2270
+ console.log('🔄 Source has changed, updating jettypod...');
2271
+ const { execSync: execSyncUpdate } = require('child_process');
2272
+ execSyncUpdate('npm install -g .', { cwd: sourceDir, stdio: 'inherit' });
2273
+ console.log('✅ Updated. Relaunching...');
2274
+ const { execFileSync } = require('child_process');
2275
+ execFileSync(process.argv[0], process.argv.slice(1), { stdio: 'inherit', cwd: process.cwd() });
2276
+ process.exit(0);
2277
+ }
2278
+ }
2279
+ } catch (e) {
2280
+ // Auto-update check failed — continue with current version
2281
+ }
2282
+ }
2283
+
2356
2284
  // Smart detection when no command provided
2357
2285
  if (!fs.existsSync('.jettypod/config.json')) {
2358
2286
  // New project - auto-initialize
@@ -2456,12 +2384,13 @@ switch (command) {
2456
2384
 
2457
2385
  // Start dashboard in dev mode for live reload
2458
2386
  console.log('🚀 Starting dashboard (dev mode)...');
2459
- const dashboardProcess = spawn('npm', ['run', 'dev', '--', '-p', String(availablePort)], {
2387
+ const dashboardProcess = spawn('npm', ['run', 'dev'], {
2460
2388
  cwd: dashboardPath,
2461
2389
  detached: true,
2462
2390
  stdio: 'ignore',
2463
2391
  env: {
2464
2392
  ...process.env,
2393
+ PORT: String(availablePort),
2465
2394
  JETTYPOD_PROJECT_PATH: process.cwd(),
2466
2395
  JETTYPOD_WS_PORT: String(WS_PORT)
2467
2396
  }
@@ -2519,6 +2448,7 @@ Quick commands:
2519
2448
  }
2520
2449
  }
2521
2450
  break;
2451
+ }
2522
2452
 
2523
2453
  case 'trash': {
2524
2454
  // Trash management commands
@@ -2651,9 +2581,17 @@ Quick commands:
2651
2581
  const impact = graph.getImpact(targetFile);
2652
2582
 
2653
2583
  if (impact.error) {
2654
- console.error(`\n❌ ${impact.error}`);
2655
- console.log('');
2656
- console.log('Make sure the file path is relative to the project root.');
2584
+ console.log(`\n❌ ${impact.error}`);
2585
+ if (impact.suggestions && impact.suggestions.length > 0) {
2586
+ console.log('');
2587
+ console.log('Did you mean one of these?');
2588
+ for (const s of impact.suggestions) {
2589
+ console.log(` ${s}`);
2590
+ }
2591
+ } else {
2592
+ console.log('');
2593
+ console.log('Make sure the file path is relative to the project root.');
2594
+ }
2657
2595
  process.exit(1);
2658
2596
  }
2659
2597