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.
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/api/claude/sessions/route.ts +23 -1
- package/apps/dashboard/app/layout.tsx +8 -4
- package/apps/dashboard/components/ClaudePanel.tsx +28 -7
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +27 -0
- package/apps/dashboard/components/SessionList.tsx +58 -21
- package/apps/dashboard/hooks/useClaudeSessions.ts +34 -0
- package/apps/dashboard/lib/db.ts +15 -0
- package/jettypod.js +5 -3
- package/lib/git-hooks/post-merge +3 -2
- package/lib/git-hooks/pre-commit +6 -1
- package/lib/work-commands/index.js +48 -14
- package/lib/work-tracking/index.js +12 -0
- package/package.json +1 -1
package/.jettypod-backup/work.db
CHANGED
|
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
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
<
|
|
51
|
+
<div
|
|
50
52
|
key={session.id}
|
|
51
|
-
|
|
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
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{session.featureId
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
};
|
package/apps/dashboard/lib/db.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 →
|
|
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:
|
package/lib/git-hooks/post-merge
CHANGED
|
@@ -48,8 +48,9 @@ function backupDatabase() {
|
|
|
48
48
|
|
|
49
49
|
fs.copyFileSync(sourcePath, backupPath);
|
|
50
50
|
|
|
51
|
-
// Stage the backup file
|
|
52
|
-
|
|
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
|
|
package/lib/git-hooks/pre-commit
CHANGED
|
@@ -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('
|
|
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
|
-
//
|
|
2009
|
+
// Detect default branch (main/master)
|
|
2010
|
+
let defaultBranch;
|
|
2010
2011
|
try {
|
|
2011
|
-
|
|
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
|
-
|
|
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
|
|
2048
|
+
// Merge the test branch to default branch
|
|
2032
2049
|
try {
|
|
2033
|
-
// First, ensure we're on
|
|
2034
|
-
execSync(
|
|
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(
|
|
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
|
-
//
|
|
2308
|
+
// Detect default branch (main/master)
|
|
2309
|
+
let defaultBranch;
|
|
2292
2310
|
try {
|
|
2293
|
-
|
|
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
|
-
|
|
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
|
|
2347
|
+
// Merge the prototype branch to default branch
|
|
2314
2348
|
try {
|
|
2315
|
-
// First, ensure we're on
|
|
2316
|
-
execSync(
|
|
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(
|
|
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
|