jettypod 4.4.107 → 4.4.109
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/layout.tsx +8 -4
- package/apps/dashboard/components/ClaudePanel.tsx +81 -98
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +35 -11
- package/claude-hooks/global-guardrails.js +27 -0
- package/lib/work-commands/index.js +48 -14
- package/package.json +1 -1
- package/skills-templates/bug-planning/SKILL.md +0 -19
- package/skills-templates/chore-planning/SKILL.md +0 -17
- package/skills-templates/epic-planning/SKILL.md +0 -17
- package/skills-templates/feature-planning/SKILL.md +0 -21
package/.jettypod-backup/work.db
CHANGED
|
Binary file
|
|
@@ -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,
|
|
@@ -6,7 +6,7 @@ import ReactMarkdown from 'react-markdown';
|
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import type { ClaudeMessage, ClaudeStreamStatus } from '../hooks/useClaudeStream';
|
|
8
8
|
import { ClaudePanelInput } from './ClaudePanelInput';
|
|
9
|
-
import {
|
|
9
|
+
import type { SessionItem } from './SessionList';
|
|
10
10
|
import type { Session } from './RealTimeKanbanWrapper';
|
|
11
11
|
|
|
12
12
|
// Unescape content that may have literal \n, \t, \r from JSON stringification
|
|
@@ -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;
|
|
@@ -38,10 +57,8 @@ interface ClaudePanelProps {
|
|
|
38
57
|
onSwitchSession?: (id: string) => void;
|
|
39
58
|
// Standalone session support
|
|
40
59
|
standaloneSessions?: SessionItem[];
|
|
41
|
-
onSelectSession?: (sessionId: string) => void;
|
|
42
60
|
onNewSession?: () => void;
|
|
43
61
|
onCloseSession?: (sessionId: string) => void;
|
|
44
|
-
showSessionList?: boolean;
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
export function ClaudePanel({
|
|
@@ -61,13 +78,7 @@ export function ClaudePanel({
|
|
|
61
78
|
activeSessionId,
|
|
62
79
|
onSwitchSession,
|
|
63
80
|
standaloneSessions = [],
|
|
64
|
-
onSelectSession,
|
|
65
|
-
onNewSession,
|
|
66
|
-
onCloseSession,
|
|
67
|
-
showSessionList: showSessionListProp,
|
|
68
81
|
}: ClaudePanelProps) {
|
|
69
|
-
// Show session list by default when no active session, or when explicitly requested
|
|
70
|
-
const showSessionList = showSessionListProp ?? (!activeSessionId && !workItemId);
|
|
71
82
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
72
83
|
|
|
73
84
|
// Detect if current session is standalone (not tied to a work item)
|
|
@@ -91,61 +102,45 @@ export function ClaudePanel({
|
|
|
91
102
|
className="fixed right-0 top-0 h-full w-[480px] bg-zinc-900 border-l border-zinc-800 flex flex-col z-50"
|
|
92
103
|
data-testid="claude-panel"
|
|
93
104
|
>
|
|
94
|
-
{/* Header
|
|
95
|
-
|
|
96
|
-
<div className="flex items-center
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
{/* Header */}
|
|
106
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-800">
|
|
107
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
108
|
+
<StatusIndicator status={status} />
|
|
109
|
+
<div className="min-w-0">
|
|
110
|
+
<h2 className="text-sm font-semibold text-white truncate" data-testid="panel-title">
|
|
111
|
+
#{workItemId} {workItemTitle}
|
|
112
|
+
</h2>
|
|
113
|
+
<p className="text-xs text-zinc-500">
|
|
114
|
+
{status === 'connecting' && 'Connecting...'}
|
|
115
|
+
{status === 'streaming' && 'Claude is working...'}
|
|
116
|
+
{status === 'done' && 'Complete'}
|
|
117
|
+
{status === 'error' && 'Error occurred'}
|
|
118
|
+
{status === 'idle' && 'Ready'}
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex items-center gap-2">
|
|
123
|
+
<button
|
|
124
|
+
onClick={onMinimize}
|
|
125
|
+
className="p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white transition-colors"
|
|
126
|
+
aria-label="Minimize panel"
|
|
127
|
+
data-testid="minimize-button"
|
|
128
|
+
>
|
|
129
|
+
<MinimizeIcon />
|
|
130
|
+
</button>
|
|
100
131
|
<button
|
|
101
132
|
onClick={onClose}
|
|
102
133
|
className="p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white transition-colors"
|
|
103
|
-
aria-label="
|
|
134
|
+
aria-label="Slide away panel"
|
|
104
135
|
data-testid="close-button"
|
|
105
136
|
>
|
|
106
137
|
<SlideAwayIcon />
|
|
107
138
|
</button>
|
|
108
139
|
</div>
|
|
109
|
-
|
|
110
|
-
<div className="flex items-center justify-between px-4 py-3 border-b border-zinc-800">
|
|
111
|
-
<div className="flex items-center gap-3 min-w-0">
|
|
112
|
-
<StatusIndicator status={status} />
|
|
113
|
-
<div className="min-w-0">
|
|
114
|
-
<h2 className="text-sm font-semibold text-white truncate" data-testid="panel-title">
|
|
115
|
-
#{workItemId} {workItemTitle}
|
|
116
|
-
</h2>
|
|
117
|
-
<p className="text-xs text-zinc-500">
|
|
118
|
-
{status === 'connecting' && 'Connecting...'}
|
|
119
|
-
{status === 'streaming' && 'Claude is working...'}
|
|
120
|
-
{status === 'done' && 'Complete'}
|
|
121
|
-
{status === 'error' && 'Error occurred'}
|
|
122
|
-
{status === 'idle' && 'Ready'}
|
|
123
|
-
</p>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
<div className="flex items-center gap-2">
|
|
127
|
-
<button
|
|
128
|
-
onClick={onMinimize}
|
|
129
|
-
className="p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white transition-colors"
|
|
130
|
-
aria-label="Minimize panel"
|
|
131
|
-
data-testid="minimize-button"
|
|
132
|
-
>
|
|
133
|
-
<MinimizeIcon />
|
|
134
|
-
</button>
|
|
135
|
-
<button
|
|
136
|
-
onClick={onClose}
|
|
137
|
-
className="p-1.5 rounded hover:bg-zinc-800 text-zinc-400 hover:text-white transition-colors"
|
|
138
|
-
aria-label="Slide away panel"
|
|
139
|
-
data-testid="close-button"
|
|
140
|
-
>
|
|
141
|
-
<SlideAwayIcon />
|
|
142
|
-
</button>
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
140
|
+
</div>
|
|
146
141
|
|
|
147
|
-
{/* Session Tabs - shown when multiple sessions exist
|
|
148
|
-
{
|
|
142
|
+
{/* Session Tabs - shown when multiple sessions exist */}
|
|
143
|
+
{sessions && sessions.size > 1 && (
|
|
149
144
|
<div className="flex border-b border-zinc-800 bg-zinc-900/50" data-testid="session-tabs">
|
|
150
145
|
{Array.from(sessions.entries()).map(([id, session]) => (
|
|
151
146
|
<button
|
|
@@ -169,8 +164,8 @@ export function ClaudePanel({
|
|
|
169
164
|
</div>
|
|
170
165
|
)}
|
|
171
166
|
|
|
172
|
-
{/* Progress bar
|
|
173
|
-
{
|
|
167
|
+
{/* Progress bar */}
|
|
168
|
+
{status === 'streaming' && (
|
|
174
169
|
<div className="h-0.5 bg-zinc-800 overflow-hidden">
|
|
175
170
|
<motion.div
|
|
176
171
|
className="h-full bg-blue-500"
|
|
@@ -182,8 +177,8 @@ export function ClaudePanel({
|
|
|
182
177
|
</div>
|
|
183
178
|
)}
|
|
184
179
|
|
|
185
|
-
{/* Error banner
|
|
186
|
-
{
|
|
180
|
+
{/* Error banner */}
|
|
181
|
+
{status === 'error' && error && (
|
|
187
182
|
<div className="bg-red-900/50 border-b border-red-800/50 px-4 py-3" data-testid="error-banner">
|
|
188
183
|
<div className="flex items-start gap-3">
|
|
189
184
|
<ErrorIcon />
|
|
@@ -214,40 +209,29 @@ export function ClaudePanel({
|
|
|
214
209
|
</div>
|
|
215
210
|
)}
|
|
216
211
|
|
|
217
|
-
{/* Content
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
<div
|
|
228
|
-
|
|
229
|
-
className="flex-1 overflow-y-auto p-4 space-y-3"
|
|
230
|
-
data-testid="panel-content"
|
|
231
|
-
>
|
|
232
|
-
{messages.map((message, index) => (
|
|
233
|
-
<MessageBlock key={index} message={message} />
|
|
234
|
-
))}
|
|
235
|
-
{messages.length === 0 && status === 'idle' && (
|
|
236
|
-
<div className="text-zinc-500 text-sm text-center py-8">
|
|
237
|
-
{isStandalone ? "What's next?" : 'Click Start to begin working on this item'}
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
212
|
+
{/* Content */}
|
|
213
|
+
<div
|
|
214
|
+
ref={contentRef}
|
|
215
|
+
className="flex-1 overflow-y-auto p-4 space-y-3"
|
|
216
|
+
data-testid="panel-content"
|
|
217
|
+
>
|
|
218
|
+
{messages.map((message, index) => (
|
|
219
|
+
<MessageBlock key={index} message={message} />
|
|
220
|
+
))}
|
|
221
|
+
{messages.length === 0 && status === 'idle' && (
|
|
222
|
+
<div className="text-zinc-500 text-sm text-center py-8">
|
|
223
|
+
{isStandalone ? "What's next?" : 'Click Start to begin working on this item'}
|
|
240
224
|
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
241
227
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)}
|
|
250
|
-
</>
|
|
228
|
+
{/* Input field - visible when session is active or for idle standalone sessions */}
|
|
229
|
+
{(status === 'streaming' || status === 'done' || (status === 'idle' && isStandalone)) && (
|
|
230
|
+
<ClaudePanelInput
|
|
231
|
+
onSendMessage={onSendMessage}
|
|
232
|
+
disabled={status === 'streaming'}
|
|
233
|
+
placeholder="Type a message..."
|
|
234
|
+
/>
|
|
251
235
|
)}
|
|
252
236
|
</motion.div>
|
|
253
237
|
)}
|
|
@@ -283,6 +267,10 @@ function MessageBlock({ message }: { message: ClaudeMessage }) {
|
|
|
283
267
|
}
|
|
284
268
|
|
|
285
269
|
if (message.type === 'assistant' || message.type === 'text') {
|
|
270
|
+
// Hide skill content - it's internal context, not user-facing output
|
|
271
|
+
if (isSkillContent(message.content)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
286
274
|
return (
|
|
287
275
|
<div className="bg-zinc-800/50 rounded-lg p-3" data-testid="output-block">
|
|
288
276
|
<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">
|
|
@@ -306,14 +294,9 @@ function MessageBlock({ message }: { message: ClaudeMessage }) {
|
|
|
306
294
|
);
|
|
307
295
|
}
|
|
308
296
|
|
|
297
|
+
// Hide tool_result messages - they're noise (Claude Code terminal doesn't show them either)
|
|
309
298
|
if (message.type === 'tool_result') {
|
|
310
|
-
return
|
|
311
|
-
<div className="bg-zinc-800/30 border border-zinc-700/50 rounded-lg p-3">
|
|
312
|
-
<pre className="text-xs text-zinc-400 overflow-x-auto whitespace-pre-wrap">
|
|
313
|
-
{unescapeContent(message.result || message.content)}
|
|
314
|
-
</pre>
|
|
315
|
-
</div>
|
|
316
|
-
);
|
|
299
|
+
return null;
|
|
317
300
|
}
|
|
318
301
|
|
|
319
302
|
if (message.type === 'error') {
|
|
@@ -85,7 +85,16 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
85
85
|
|
|
86
86
|
// Standalone sessions state
|
|
87
87
|
const [standaloneSessions, setStandaloneSessions] = useState<SessionItem[]>([]);
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
// Persist active session ID to sessionStorage
|
|
90
|
+
const ACTIVE_SESSION_KEY = 'jettypod-active-session-id';
|
|
91
|
+
|
|
92
|
+
// Sync activeSessionId to sessionStorage when it changes
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (activeSessionId) {
|
|
95
|
+
sessionStorage.setItem(ACTIVE_SESSION_KEY, activeSessionId);
|
|
96
|
+
}
|
|
97
|
+
}, [activeSessionId]);
|
|
89
98
|
|
|
90
99
|
// Get active session for the stream hook
|
|
91
100
|
const activeSession = activeSessionId ? sessions.get(activeSessionId) : null;
|
|
@@ -495,7 +504,7 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
495
504
|
|
|
496
505
|
// Don't auto-start for standalone sessions - wait for user input
|
|
497
506
|
setActiveSessionId(sessionId);
|
|
498
|
-
|
|
507
|
+
setClaudePanelOpen(true);
|
|
499
508
|
} catch {
|
|
500
509
|
// Silently fail
|
|
501
510
|
}
|
|
@@ -505,7 +514,6 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
505
514
|
// Check if session is already loaded
|
|
506
515
|
if (sessions.has(sessionId)) {
|
|
507
516
|
setActiveSessionId(sessionId);
|
|
508
|
-
setShowSessionList(false);
|
|
509
517
|
return;
|
|
510
518
|
}
|
|
511
519
|
|
|
@@ -530,7 +538,6 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
530
538
|
});
|
|
531
539
|
|
|
532
540
|
setActiveSessionId(sessionId);
|
|
533
|
-
setShowSessionList(false);
|
|
534
541
|
}, [sessions, standaloneSessions]);
|
|
535
542
|
|
|
536
543
|
const handleCloseSession = useCallback(async (sessionId: string) => {
|
|
@@ -559,10 +566,29 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
559
566
|
}
|
|
560
567
|
}, [activeSessionId]);
|
|
561
568
|
|
|
562
|
-
const
|
|
563
|
-
|
|
569
|
+
const handleOpenSessionPanel = useCallback(() => {
|
|
570
|
+
// Try to restore last active session from sessionStorage
|
|
571
|
+
const savedSessionId = sessionStorage.getItem(ACTIVE_SESSION_KEY);
|
|
572
|
+
|
|
573
|
+
// Check if saved session still exists
|
|
574
|
+
if (savedSessionId && sessions.has(savedSessionId)) {
|
|
575
|
+
setActiveSessionId(savedSessionId);
|
|
576
|
+
setClaudePanelOpen(true);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Fall back to first available session
|
|
581
|
+
const firstSessionId = sessions.keys().next().value;
|
|
582
|
+
if (firstSessionId) {
|
|
583
|
+
setActiveSessionId(firstSessionId);
|
|
584
|
+
setClaudePanelOpen(true);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// No sessions exist - create a new standalone session
|
|
589
|
+
handleNewSession();
|
|
564
590
|
setClaudePanelOpen(true);
|
|
565
|
-
}, []);
|
|
591
|
+
}, [sessions, handleNewSession]);
|
|
566
592
|
|
|
567
593
|
const wsUrl = typeof window !== 'undefined'
|
|
568
594
|
? `ws://${window.location.hostname}:8080`
|
|
@@ -615,9 +641,9 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
615
641
|
</span>
|
|
616
642
|
</div>
|
|
617
643
|
<button
|
|
618
|
-
onClick={
|
|
644
|
+
onClick={handleOpenSessionPanel}
|
|
619
645
|
className="px-3 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white rounded transition-colors"
|
|
620
|
-
data-testid="open-session-
|
|
646
|
+
data-testid="open-session-panel-button"
|
|
621
647
|
>
|
|
622
648
|
Claude Sessions
|
|
623
649
|
</button>
|
|
@@ -663,10 +689,8 @@ function RealTimeKanbanContent({ initialData, initialDecisions }: RealTimeKanban
|
|
|
663
689
|
activeSessionId={activeSessionId}
|
|
664
690
|
onSwitchSession={handleSwitchSession}
|
|
665
691
|
standaloneSessions={standaloneSessions}
|
|
666
|
-
onSelectSession={handleSelectStandaloneSession}
|
|
667
692
|
onNewSession={handleNewSession}
|
|
668
693
|
onCloseSession={handleCloseSession}
|
|
669
|
-
showSessionList={showSessionList}
|
|
670
694
|
/>
|
|
671
695
|
</div>
|
|
672
696
|
);
|
|
@@ -205,6 +205,33 @@ function evaluateBashCommand(command, inputRef, cwd) {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// BLOCKED: git worktree prune/remove (use jettypod work cleanup)
|
|
209
|
+
if (/git\s+worktree\s+(prune|remove)\b/.test(strippedCommand)) {
|
|
210
|
+
return {
|
|
211
|
+
allowed: false,
|
|
212
|
+
message: 'Manual worktree cleanup is blocked',
|
|
213
|
+
hint: 'Use jettypod work cleanup <id> to remove worktrees.'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// BLOCKED: Direct deletion of worktree directories
|
|
218
|
+
if (/rm\s+.*\.jettypod-work\//.test(strippedCommand)) {
|
|
219
|
+
return {
|
|
220
|
+
allowed: false,
|
|
221
|
+
message: 'Direct worktree directory deletion is blocked',
|
|
222
|
+
hint: 'Use jettypod work cleanup <id> to remove worktrees.'
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// BLOCKED: Branch deletion (use jettypod work cleanup)
|
|
227
|
+
if (/git\s+branch\s+-[dD]\b/.test(strippedCommand)) {
|
|
228
|
+
return {
|
|
229
|
+
allowed: false,
|
|
230
|
+
message: 'Manual branch deletion is blocked',
|
|
231
|
+
hint: 'Use jettypod work cleanup <id> to remove branches.'
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
// BLOCKED: Direct SQL mutation to work.db
|
|
209
236
|
if (/sqlite3\s+.*work\.db/.test(strippedCommand)) {
|
|
210
237
|
const sqlCommand = command.toLowerCase();
|
|
@@ -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` +
|
package/package.json
CHANGED
|
@@ -17,25 +17,6 @@ description: Guide structured bug investigation with symptom capture, hypothesis
|
|
|
17
17
|
|
|
18
18
|
Guides Claude through systematic bug investigation. Produces a bug work item with clear breadcrumbs for implementation.
|
|
19
19
|
|
|
20
|
-
## ⚠️ READ-ONLY PHASE
|
|
21
|
-
|
|
22
|
-
**This skill is an investigation phase. No worktree exists yet.**
|
|
23
|
-
|
|
24
|
-
🚫 **FORBIDDEN during this skill:**
|
|
25
|
-
- Writing or editing any code files
|
|
26
|
-
- Creating new files
|
|
27
|
-
- Making implementation changes
|
|
28
|
-
- Adding temporary debugging code
|
|
29
|
-
|
|
30
|
-
✅ **ALLOWED during this skill:**
|
|
31
|
-
- Reading files to understand the codebase
|
|
32
|
-
- Running `jettypod` commands to create work items
|
|
33
|
-
- Running diagnostic commands (git log, grep, etc.)
|
|
34
|
-
- Asking the user questions
|
|
35
|
-
- Analyzing symptoms and forming hypotheses
|
|
36
|
-
|
|
37
|
-
**The worktree is created in Phase 6** when `jettypod work start` runs before invoking bug-mode.
|
|
38
|
-
|
|
39
20
|
## Instructions
|
|
40
21
|
|
|
41
22
|
When this skill is activated, you are investigating a bug to identify root cause and plan the fix.
|
|
@@ -7,23 +7,6 @@ description: Guide standalone chore planning with automatic type classification
|
|
|
7
7
|
|
|
8
8
|
Guides Claude through standalone chore planning including automatic type classification, loading type-specific guidance from the taxonomy, building enriched context, and routing to chore-mode for execution. For chores under **technical epics**, detects this ancestry and passes context to skip mode progression.
|
|
9
9
|
|
|
10
|
-
## ⚠️ READ-ONLY PHASE
|
|
11
|
-
|
|
12
|
-
**This skill is a planning/investigation phase. No worktree exists yet.**
|
|
13
|
-
|
|
14
|
-
🚫 **FORBIDDEN during this skill:**
|
|
15
|
-
- Writing or editing any code files
|
|
16
|
-
- Creating new files
|
|
17
|
-
- Making implementation changes
|
|
18
|
-
|
|
19
|
-
✅ **ALLOWED during this skill:**
|
|
20
|
-
- Reading files to understand the codebase
|
|
21
|
-
- Running `jettypod` commands to create work items
|
|
22
|
-
- Asking the user questions
|
|
23
|
-
- Analyzing and planning
|
|
24
|
-
|
|
25
|
-
**The worktree is created later** when chore-mode runs `jettypod work start`.
|
|
26
|
-
|
|
27
10
|
## Instructions
|
|
28
11
|
|
|
29
12
|
When this skill is activated, you are helping plan a standalone chore (one without a parent feature). Follow this structured approach:
|
|
@@ -7,23 +7,6 @@ description: Guide epic planning with feature brainstorming and optional archite
|
|
|
7
7
|
|
|
8
8
|
Guides Claude through comprehensive epic planning including feature identification and architectural decisions. For **technical epics**, skips feature brainstorming and creates chores directly.
|
|
9
9
|
|
|
10
|
-
## ⚠️ READ-ONLY PHASE
|
|
11
|
-
|
|
12
|
-
**This skill is a planning/investigation phase. No worktree exists yet.**
|
|
13
|
-
|
|
14
|
-
🚫 **FORBIDDEN during this skill:**
|
|
15
|
-
- Writing or editing any code files
|
|
16
|
-
- Creating new files
|
|
17
|
-
- Making implementation changes
|
|
18
|
-
|
|
19
|
-
✅ **ALLOWED during this skill:**
|
|
20
|
-
- Reading files to understand the codebase
|
|
21
|
-
- Running `jettypod` commands to create work items
|
|
22
|
-
- Asking the user questions
|
|
23
|
-
- Analyzing and planning
|
|
24
|
-
|
|
25
|
-
**The worktree is created later** when a mode skill (speed-mode, chore-mode, etc.) runs `jettypod work start`.
|
|
26
|
-
|
|
27
10
|
## Instructions
|
|
28
11
|
|
|
29
12
|
When this skill is activated, you are helping plan an epic. Follow this structured approach:
|
|
@@ -7,27 +7,6 @@ description: Guide feature planning with UX approach exploration and BDD scenari
|
|
|
7
7
|
|
|
8
8
|
Guides Claude through feature planning including UX approach exploration, optional prototyping, and BDD scenario generation.
|
|
9
9
|
|
|
10
|
-
## ⚠️ READ-ONLY UNTIL WORKTREE EXISTS
|
|
11
|
-
|
|
12
|
-
**This skill starts as a planning/investigation phase. No worktree exists initially.**
|
|
13
|
-
|
|
14
|
-
🚫 **FORBIDDEN until a worktree is created:**
|
|
15
|
-
- Writing or editing any code files
|
|
16
|
-
- Creating new files in the main repository
|
|
17
|
-
|
|
18
|
-
✅ **ALLOWED during planning phases:**
|
|
19
|
-
- Reading files to understand the codebase
|
|
20
|
-
- Running `jettypod` commands
|
|
21
|
-
- Asking the user questions
|
|
22
|
-
- Analyzing and planning
|
|
23
|
-
|
|
24
|
-
**Worktrees are created at specific steps:**
|
|
25
|
-
- `work prototype start` (Step 4) - for UX prototyping
|
|
26
|
-
- `work tests start` (Step 7) - for BDD test authoring
|
|
27
|
-
- `work start` (Step 13) - for chore implementation (invokes speed-mode)
|
|
28
|
-
|
|
29
|
-
**Only write files AFTER the relevant worktree command succeeds.**
|
|
30
|
-
|
|
31
10
|
## Instructions
|
|
32
11
|
|
|
33
12
|
When this skill is activated, you are helping discover the best approach for a feature. Follow this structured approach:
|