jettypod 4.4.106 → 4.4.108

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.
Binary file
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { listSessions, createSession, linkSession, getSessionByWorkItem } from '@/lib/db';
2
+ import { listSessions, createSession, linkSession, getSessionByWorkItem, closeSession } from '@/lib/db';
3
3
 
4
4
  export const dynamic = 'force-dynamic';
5
5
 
@@ -59,3 +59,25 @@ export async function PATCH(request: Request) {
59
59
  return NextResponse.json({ error: 'Failed to link session' }, { status: 500 });
60
60
  }
61
61
  }
62
+
63
+ // DELETE /api/claude/sessions - Close a session
64
+ export async function DELETE(request: Request) {
65
+ try {
66
+ const { searchParams } = new URL(request.url);
67
+ const sessionId = searchParams.get('sessionId');
68
+
69
+ if (!sessionId) {
70
+ return NextResponse.json({ error: 'sessionId required' }, { status: 400 });
71
+ }
72
+
73
+ const success = closeSession(parseInt(sessionId, 10));
74
+ if (!success) {
75
+ return NextResponse.json({ error: 'Session not found' }, { status: 404 });
76
+ }
77
+
78
+ return NextResponse.json({ success: true });
79
+ } catch (error) {
80
+ console.error('Failed to close session:', error);
81
+ return NextResponse.json({ error: 'Failed to close session' }, { status: 500 });
82
+ }
83
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Metadata } from "next";
2
2
  import { Geist, Geist_Mono } from "next/font/google";
3
3
  import "./globals.css";
4
+ import { getProjectName } from "@/lib/db";
4
5
 
5
6
  const geistSans = Geist({
6
7
  variable: "--font-geist-sans",
@@ -12,10 +13,13 @@ const geistMono = Geist_Mono({
12
13
  subsets: ["latin"],
13
14
  });
14
15
 
15
- export const metadata: Metadata = {
16
- title: "Create Next App",
17
- description: "Generated by create next app",
18
- };
16
+ export function generateMetadata(): Metadata {
17
+ const projectName = getProjectName();
18
+ return {
19
+ title: `JettyPod—${projectName}`,
20
+ description: "JettyPod dashboard",
21
+ };
22
+ }
19
23
 
20
24
  export default function RootLayout({
21
25
  children,
@@ -19,6 +19,25 @@ function unescapeContent(content: string | undefined): string {
19
19
  .replace(/\\"/g, '"');
20
20
  }
21
21
 
22
+ // Detect skill file content that shouldn't be displayed to users
23
+ // Skills are internal context for Claude, not user-facing output
24
+ function isSkillContent(content: string | undefined): boolean {
25
+ if (!content) return false;
26
+ const skillPatterns = [
27
+ 'FORBIDDEN during this skill',
28
+ 'ALLOWED during this skill',
29
+ 'READ-ONLY PHASE',
30
+ 'Base directory for this skill:',
31
+ '# Request Routing Skill',
32
+ '# Simple Improvement Skill',
33
+ '# Bug Planning Skill',
34
+ '# Chore Planning Skill',
35
+ '# Feature Planning Skill',
36
+ '# Epic Planning Skill',
37
+ ];
38
+ return skillPatterns.some(pattern => content.includes(pattern));
39
+ }
40
+
22
41
  interface ClaudePanelProps {
23
42
  isOpen: boolean;
24
43
  workItemId: string;
@@ -40,6 +59,7 @@ interface ClaudePanelProps {
40
59
  standaloneSessions?: SessionItem[];
41
60
  onSelectSession?: (sessionId: string) => void;
42
61
  onNewSession?: () => void;
62
+ onCloseSession?: (sessionId: string) => void;
43
63
  showSessionList?: boolean;
44
64
  }
45
65
 
@@ -62,6 +82,7 @@ export function ClaudePanel({
62
82
  standaloneSessions = [],
63
83
  onSelectSession,
64
84
  onNewSession,
85
+ onCloseSession,
65
86
  showSessionList: showSessionListProp,
66
87
  }: ClaudePanelProps) {
67
88
  // Show session list by default when no active session, or when explicitly requested
@@ -218,6 +239,7 @@ export function ClaudePanel({
218
239
  sessions={standaloneSessions}
219
240
  onSelectSession={onSelectSession || (() => {})}
220
241
  onNewSession={onNewSession || (() => {})}
242
+ onCloseSession={onCloseSession}
221
243
  />
222
244
  ) : (
223
245
  <>
@@ -280,6 +302,10 @@ function MessageBlock({ message }: { message: ClaudeMessage }) {
280
302
  }
281
303
 
282
304
  if (message.type === 'assistant' || message.type === 'text') {
305
+ // Hide skill content - it's internal context, not user-facing output
306
+ if (isSkillContent(message.content)) {
307
+ return null;
308
+ }
283
309
  return (
284
310
  <div className="bg-zinc-800/50 rounded-lg p-3" data-testid="output-block">
285
311
  <div className="text-zinc-200 text-sm [&_p]:my-1 [&_h1]:text-lg [&_h1]:font-bold [&_h1]:my-2 [&_h2]:text-base [&_h2]:font-semibold [&_h2]:my-2 [&_h3]:font-semibold [&_h3]:my-1 [&_pre]:bg-zinc-900 [&_pre]:p-2 [&_pre]:rounded [&_pre]:overflow-x-auto [&_pre]:my-2 [&_pre]:text-xs [&_code]:text-zinc-300 [&_code]:bg-zinc-900/50 [&_code]:px-1 [&_code]:rounded [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_ul]:list-disc [&_ul]:ml-4 [&_ol]:list-decimal [&_ol]:ml-4 [&_li]:my-0.5 [&_a]:text-blue-400 [&_a]:underline [&_blockquote]:border-l-2 [&_blockquote]:border-zinc-500 [&_blockquote]:pl-3 [&_blockquote]:italic [&_table]:text-xs [&_table]:w-full [&_th]:bg-zinc-800 [&_th]:px-2 [&_th]:py-1 [&_th]:text-left [&_td]:px-2 [&_td]:py-1 [&_td]:border-t [&_td]:border-zinc-700">
@@ -303,14 +329,9 @@ function MessageBlock({ message }: { message: ClaudeMessage }) {
303
329
  );
304
330
  }
305
331
 
332
+ // Hide tool_result messages - they're noise (Claude Code terminal doesn't show them either)
306
333
  if (message.type === 'tool_result') {
307
- return (
308
- <div className="bg-zinc-800/30 border border-zinc-700/50 rounded-lg p-3">
309
- <pre className="text-xs text-zinc-400 overflow-x-auto whitespace-pre-wrap">
310
- {unescapeContent(message.result || message.content)}
311
- </pre>
312
- </div>
313
- );
334
+ return null;
314
335
  }
315
336
 
316
337
  if (message.type === 'error') {
@@ -533,6 +533,32 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
533
533
  setShowSessionList(false);
534
534
  }, [sessions, standaloneSessions]);
535
535
 
536
+ const handleCloseSession = useCallback(async (sessionId: string) => {
537
+ // Call API to mark as completed in DB
538
+ try {
539
+ await fetch(`/api/claude/sessions?sessionId=${sessionId}`, {
540
+ method: 'DELETE',
541
+ });
542
+ } catch (error) {
543
+ console.error('Failed to close session:', error);
544
+ }
545
+
546
+ // Remove from standalone sessions list
547
+ setStandaloneSessions(prev => prev.filter(s => s.id !== sessionId));
548
+
549
+ // Remove from in-memory sessions
550
+ setSessions(prev => {
551
+ const updated = new Map(prev);
552
+ updated.delete(sessionId);
553
+ return updated;
554
+ });
555
+
556
+ // If this was the active session, clear it
557
+ if (activeSessionId === sessionId) {
558
+ setActiveSessionId(null);
559
+ }
560
+ }, [activeSessionId]);
561
+
536
562
  const handleOpenSessionList = useCallback(() => {
537
563
  setShowSessionList(true);
538
564
  setClaudePanelOpen(true);
@@ -639,6 +665,7 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
639
665
  standaloneSessions={standaloneSessions}
640
666
  onSelectSession={handleSelectStandaloneSession}
641
667
  onNewSession={handleNewSession}
668
+ onCloseSession={handleCloseSession}
642
669
  showSessionList={showSessionList}
643
670
  />
644
671
  </div>
@@ -14,12 +14,14 @@ interface SessionListProps {
14
14
  sessions: SessionItem[];
15
15
  onSelectSession: (sessionId: string) => void;
16
16
  onNewSession: () => void;
17
+ onCloseSession?: (sessionId: string) => void;
17
18
  }
18
19
 
19
20
  export function SessionList({
20
21
  sessions,
21
22
  onSelectSession,
22
23
  onNewSession,
24
+ onCloseSession,
23
25
  }: SessionListProps) {
24
26
  return (
25
27
  <div className="flex-1 flex flex-col" data-testid="session-list">
@@ -46,31 +48,48 @@ export function SessionList({
46
48
  ) : (
47
49
  <div className="divide-y divide-zinc-800">
48
50
  {sessions.map((session) => (
49
- <motion.button
51
+ <div
50
52
  key={session.id}
51
- onClick={() => onSelectSession(session.id)}
52
- className="w-full px-4 py-3 text-left hover:bg-zinc-800/50 transition-colors"
53
- whileHover={{ x: 4 }}
53
+ className="flex items-center hover:bg-zinc-800/50 transition-colors"
54
54
  data-testid={`session-item-${session.id}`}
55
55
  >
56
- <div className="flex items-center gap-2">
57
- <SessionIcon hasFeature={!!session.featureId} />
58
- <div className="flex-1 min-w-0">
59
- <p className="text-sm font-medium text-white truncate">
60
- {session.featureId ? session.featureTitle : session.title}
61
- </p>
62
- {session.featureId && (
63
- <span className="inline-flex items-center px-1.5 py-0.5 mt-1 text-xs font-medium bg-blue-900/50 text-blue-300 rounded">
64
- #{session.featureId}
65
- </span>
66
- )}
67
- {!session.featureId && (
68
- <p className="text-xs text-zinc-500 mt-0.5">Unlinked session</p>
69
- )}
56
+ <motion.button
57
+ onClick={() => onSelectSession(session.id)}
58
+ className="flex-1 px-4 py-3 text-left"
59
+ whileHover={{ x: 4 }}
60
+ >
61
+ <div className="flex items-center gap-2">
62
+ <SessionIcon hasFeature={!!session.featureId} />
63
+ <div className="flex-1 min-w-0">
64
+ <p className="text-sm font-medium text-white truncate">
65
+ {session.featureId ? session.featureTitle : session.title}
66
+ </p>
67
+ {session.featureId && (
68
+ <span className="inline-flex items-center px-1.5 py-0.5 mt-1 text-xs font-medium bg-blue-900/50 text-blue-300 rounded">
69
+ #{session.featureId}
70
+ </span>
71
+ )}
72
+ {!session.featureId && (
73
+ <p className="text-xs text-zinc-500 mt-0.5">Unlinked session</p>
74
+ )}
75
+ </div>
76
+ <ChevronIcon />
70
77
  </div>
71
- <ChevronIcon />
72
- </div>
73
- </motion.button>
78
+ </motion.button>
79
+ {onCloseSession && (
80
+ <button
81
+ onClick={(e) => {
82
+ e.stopPropagation();
83
+ onCloseSession(session.id);
84
+ }}
85
+ className="p-2 mr-2 rounded hover:bg-zinc-700 text-zinc-500 hover:text-zinc-300 transition-colors"
86
+ aria-label="Close session"
87
+ data-testid={`close-session-${session.id}`}
88
+ >
89
+ <CloseIcon />
90
+ </button>
91
+ )}
92
+ </div>
74
93
  ))}
75
94
  </div>
76
95
  )}
@@ -118,3 +137,21 @@ function ChevronIcon() {
118
137
  </svg>
119
138
  );
120
139
  }
140
+
141
+ function CloseIcon() {
142
+ return (
143
+ <svg
144
+ className="w-4 h-4"
145
+ fill="none"
146
+ stroke="currentColor"
147
+ viewBox="0 0 24 24"
148
+ >
149
+ <path
150
+ strokeLinecap="round"
151
+ strokeLinejoin="round"
152
+ strokeWidth={2}
153
+ d="M6 18L18 6M6 6l12 12"
154
+ />
155
+ </svg>
156
+ );
157
+ }
@@ -23,6 +23,7 @@ interface UseClaudeSessionsReturn {
23
23
  switchSession: (workItemId: string) => void;
24
24
  stopSession: (workItemId: string) => void;
25
25
  retrySession: (workItemId: string) => void;
26
+ closeSession: (workItemId: string, dbSessionId?: number) => Promise<void>;
26
27
  closeAllSessions: () => void;
27
28
  isSessionRunning: (workItemId: string) => boolean;
28
29
  }
@@ -234,6 +235,38 @@ export function useClaudeSessions(): UseClaudeSessionsReturn {
234
235
  }
235
236
  }, [sessions, startSession]);
236
237
 
238
+ const closeSession = useCallback(async (workItemId: string, dbSessionId?: number) => {
239
+ const session = sessions.get(workItemId);
240
+
241
+ // Abort if running
242
+ if (session?.abortController) {
243
+ session.abortController.abort();
244
+ }
245
+
246
+ // Call API to mark as completed in DB (if we have a DB session ID)
247
+ if (dbSessionId) {
248
+ try {
249
+ await fetch(`/api/claude/sessions?sessionId=${dbSessionId}`, {
250
+ method: 'DELETE',
251
+ });
252
+ } catch (error) {
253
+ console.error('Failed to close session in DB:', error);
254
+ }
255
+ }
256
+
257
+ // Remove from local state
258
+ setSessions((prev) => {
259
+ const newMap = new Map(prev);
260
+ newMap.delete(workItemId);
261
+ return newMap;
262
+ });
263
+
264
+ // If this was the active session, clear it
265
+ if (activeSessionId === workItemId) {
266
+ setActiveSessionId(null);
267
+ }
268
+ }, [sessions, activeSessionId]);
269
+
237
270
  const closeAllSessions = useCallback(() => {
238
271
  sessions.forEach((session) => {
239
272
  if (session.abortController) {
@@ -259,6 +292,7 @@ export function useClaudeSessions(): UseClaudeSessionsReturn {
259
292
  switchSession,
260
293
  stopSession,
261
294
  retrySession,
295
+ closeSession,
262
296
  closeAllSessions,
263
297
  isSessionRunning,
264
298
  };
@@ -649,3 +649,18 @@ export function getSession(sessionId: number): ClaudeSession | null {
649
649
  db.close();
650
650
  }
651
651
  }
652
+
653
+ // Close a session by ID (mark as completed)
654
+ export function closeSession(sessionId: number): boolean {
655
+ const db = getWriteDb();
656
+ try {
657
+ const result = db.prepare(`
658
+ UPDATE claude_sessions
659
+ SET status = 'completed', completed_at = datetime('now')
660
+ WHERE id = ?
661
+ `).run(sessionId);
662
+ return result.changes > 0;
663
+ } finally {
664
+ db.close();
665
+ }
666
+ }
package/jettypod.js CHANGED
@@ -302,7 +302,9 @@ JettyPod: Structured workflow system with skills that guide complex workflows.
302
302
 
303
303
  ## ⚠️ CRITICAL: All Work Starts with request-routing
304
304
 
305
- **WHY:** request-routing creates a safe workspace (worktree) where your changes can actually be committed. Without it, you'll edit files on main, then discover the pre-commit hook blocks you—leaving uncommitted changes that can't be saved.
305
+ **WHY:** The workflow initiated by request-routing eventually creates a safe workspace (worktree) where your changes can actually be committed. Without starting this workflow, you'll edit files on main, then discover the pre-commit hook blocks you—leaving uncommitted changes that can't be saved.
306
+
307
+ **WHEN WORKTREE IS CREATED:** The worktree is created when \`jettypod work start\` runs—this happens in mode skills (speed-mode, stable-mode, chore-mode, bug-mode) or simple-improvement, NOT immediately in request-routing. Planning skills (epic-planning, feature-planning, chore-planning, bug-planning) are READ-ONLY investigation phases—do not edit files during planning.
306
308
 
307
309
  **FIRST RESPONSE RULE:** If user describes ANY code change, your FIRST action must be invoking request-routing. Not after reading files. Not after understanding the problem. FIRST.
308
310
 
@@ -313,10 +315,10 @@ JettyPod: Structured workflow system with skills that guide complex workflows.
313
315
  - "refactor", "migrate", "upgrade" (technical work)
314
316
  - "I noticed...", "I'm thinking..." (when followed by desired change)
315
317
 
316
- **⚠️ ANTI-PATTERN: Do NOT edit files before invoking request-routing.**
318
+ **⚠️ ANTI-PATTERN: Do NOT edit files before the worktree exists.**
317
319
  Wrong: See problem → edit files → realize you're on main → ask about workflow
318
320
  Wrong: Read files → understand problem → try to fix → get blocked → create work item
319
- Right: User describes work → invoke request-routing → skill creates worktree → then edit
321
+ Right: User describes work → invoke request-routing → complete workflow until \`work start\` creates worktree → then edit
320
322
 
321
323
  ## ⚠️ CRITICAL: Skills are MANDATORY for workflows
322
324
  Skills auto-activate and MUST complete their full workflow:
@@ -48,8 +48,9 @@ function backupDatabase() {
48
48
 
49
49
  fs.copyFileSync(sourcePath, backupPath);
50
50
 
51
- // Stage the backup file
52
- execSync(`git add "${backupPath}"`, {
51
+ // Stage the backup file (force-add because .jettypod-backup is gitignored
52
+ // to prevent worktree symlink corruption, but backups must be committed)
53
+ execSync(`git add -f "${backupPath}"`, {
53
54
  stdio: ['pipe', 'pipe', 'pipe']
54
55
  });
55
56
 
@@ -43,7 +43,12 @@ function checkBranchRestriction() {
43
43
  console.error(' 1. Undo your edits: git checkout .');
44
44
  console.error(' 2. Start the workflow: Invoke request-routing skill');
45
45
  console.error('');
46
- console.error('The skill creates a worktree where commits are allowed.');
46
+ console.error('HOW IT WORKS:');
47
+ console.error(' request-routing → planning skill → mode skill → work start');
48
+ console.error('');
49
+ console.error(' The worktree is created when `jettypod work start` runs.');
50
+ console.error(' Planning skills are READ-ONLY investigation phases.');
51
+ console.error(' Only edit files AFTER the worktree exists.');
47
52
  console.error('');
48
53
  return false;
49
54
  } catch (err) {
@@ -2006,15 +2006,32 @@ async function testsMerge(featureId) {
2006
2006
  return Promise.reject(new Error(`Failed to check worktree status: ${err.message}`));
2007
2007
  }
2008
2008
 
2009
- // Check if there are commits to merge
2009
+ // Detect default branch (main/master)
2010
+ let defaultBranch;
2010
2011
  try {
2011
- const mainBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
2012
+ defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
2012
2013
  cwd: gitRoot,
2013
2014
  encoding: 'utf8',
2014
2015
  stdio: 'pipe'
2015
2016
  }).trim().replace('refs/remotes/origin/', '');
2017
+ } catch {
2018
+ // Fallback: check which common branch names exist
2019
+ try {
2020
+ execSync('git rev-parse --verify main', { cwd: gitRoot, stdio: 'pipe' });
2021
+ defaultBranch = 'main';
2022
+ } catch {
2023
+ try {
2024
+ execSync('git rev-parse --verify master', { cwd: gitRoot, stdio: 'pipe' });
2025
+ defaultBranch = 'master';
2026
+ } catch {
2027
+ return Promise.reject(new Error('Could not detect default branch (tried main, master)'));
2028
+ }
2029
+ }
2030
+ }
2016
2031
 
2017
- const commitCount = execSync(`git rev-list --count ${mainBranch}..${branchName}`, {
2032
+ // Check if there are commits to merge
2033
+ try {
2034
+ const commitCount = execSync(`git rev-list --count ${defaultBranch}..${branchName}`, {
2018
2035
  cwd: gitRoot,
2019
2036
  encoding: 'utf8',
2020
2037
  stdio: 'pipe'
@@ -2028,10 +2045,10 @@ async function testsMerge(featureId) {
2028
2045
  console.log('⚠️ Could not check commit count, proceeding with merge');
2029
2046
  }
2030
2047
 
2031
- // Merge the test branch to main
2048
+ // Merge the test branch to default branch
2032
2049
  try {
2033
- // First, ensure we're on main
2034
- execSync('git checkout main', {
2050
+ // First, ensure we're on the default branch
2051
+ execSync(`git checkout ${defaultBranch}`, {
2035
2052
  cwd: gitRoot,
2036
2053
  encoding: 'utf8',
2037
2054
  stdio: 'pipe'
@@ -2044,7 +2061,7 @@ async function testsMerge(featureId) {
2044
2061
  stdio: 'pipe'
2045
2062
  });
2046
2063
 
2047
- console.log('✅ Merged test branch to main');
2064
+ console.log(`✅ Merged test branch to ${defaultBranch}`);
2048
2065
  } catch (err) {
2049
2066
  return Promise.reject(new Error(
2050
2067
  `Merge failed: ${err.message}\n\n` +
@@ -2288,15 +2305,32 @@ async function prototypeMerge(workItemId) {
2288
2305
  return Promise.reject(new Error(`Failed to check worktree status: ${err.message}`));
2289
2306
  }
2290
2307
 
2291
- // Check if there are commits to merge
2308
+ // Detect default branch (main/master)
2309
+ let defaultBranch;
2292
2310
  try {
2293
- const mainBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
2311
+ defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
2294
2312
  cwd: gitRoot,
2295
2313
  encoding: 'utf8',
2296
2314
  stdio: 'pipe'
2297
2315
  }).trim().replace('refs/remotes/origin/', '');
2316
+ } catch {
2317
+ // Fallback: check which common branch names exist
2318
+ try {
2319
+ execSync('git rev-parse --verify main', { cwd: gitRoot, stdio: 'pipe' });
2320
+ defaultBranch = 'main';
2321
+ } catch {
2322
+ try {
2323
+ execSync('git rev-parse --verify master', { cwd: gitRoot, stdio: 'pipe' });
2324
+ defaultBranch = 'master';
2325
+ } catch {
2326
+ return Promise.reject(new Error('Could not detect default branch (tried main, master)'));
2327
+ }
2328
+ }
2329
+ }
2298
2330
 
2299
- const commitCount = execSync(`git rev-list --count ${mainBranch}..${branchName}`, {
2331
+ // Check if there are commits to merge
2332
+ try {
2333
+ const commitCount = execSync(`git rev-list --count ${defaultBranch}..${branchName}`, {
2300
2334
  cwd: gitRoot,
2301
2335
  encoding: 'utf8',
2302
2336
  stdio: 'pipe'
@@ -2310,10 +2344,10 @@ async function prototypeMerge(workItemId) {
2310
2344
  console.log('⚠️ Could not check commit count, proceeding with merge');
2311
2345
  }
2312
2346
 
2313
- // Merge the prototype branch to main
2347
+ // Merge the prototype branch to default branch
2314
2348
  try {
2315
- // First, ensure we're on main
2316
- execSync('git checkout main', {
2349
+ // First, ensure we're on the default branch
2350
+ execSync(`git checkout ${defaultBranch}`, {
2317
2351
  cwd: gitRoot,
2318
2352
  encoding: 'utf8',
2319
2353
  stdio: 'pipe'
@@ -2326,7 +2360,7 @@ async function prototypeMerge(workItemId) {
2326
2360
  stdio: 'pipe'
2327
2361
  });
2328
2362
 
2329
- console.log('✅ Merged prototype branch to main');
2363
+ console.log(`✅ Merged prototype branch to ${defaultBranch}`);
2330
2364
  } catch (err) {
2331
2365
  return Promise.reject(new Error(
2332
2366
  `Merge failed: ${err.message}\n\n` +
@@ -558,6 +558,17 @@ function updateStatus(id, status) {
558
558
 
559
559
  // CRITICAL: Merge to main BEFORE cleanup
560
560
  if (isGitRepo()) {
561
+ // Check if branch exists before attempting merge
562
+ const branchExists = execSync(`git branch --list ${worktree.branch_name}`, {
563
+ cwd: gitRoot,
564
+ encoding: 'utf8',
565
+ stdio: 'pipe'
566
+ }).trim();
567
+
568
+ if (!branchExists) {
569
+ // Branch is gone (likely already merged/deleted) - skip merge, just cleanup stale record
570
+ console.warn(`⚠️ Branch "${worktree.branch_name}" no longer exists - cleaning up stale record`);
571
+ } else {
561
572
  try {
562
573
  // Test merge to detect conflicts (without actually merging)
563
574
  const mergeResult = execSync(`git merge --no-commit --no-ff ${worktree.branch_name}`, {
@@ -624,6 +635,7 @@ function updateStatus(id, status) {
624
635
  console.warn(` Worktree preserved for manual resolution`);
625
636
  throw new Error('Merge failed - worktree preserved');
626
637
  }
638
+ } // end else (branch exists)
627
639
  }
628
640
 
629
641
  // Only cleanup after successful merge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.106",
3
+ "version": "4.4.108",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {