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.
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 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,
@@ -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 { SessionList, type SessionItem } from './SessionList';
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 - different for session list vs chat */}
95
- {showSessionList ? (
96
- <div className="flex items-center justify-between px-4 py-3 border-b border-zinc-800">
97
- <h2 className="text-sm font-semibold text-white" data-testid="panel-title">
98
- Claude Sessions
99
- </h2>
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="Close panel"
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 and NOT in session list view */}
148
- {!showSessionList && sessions && sessions.size > 1 && (
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 - only show when in chat view */}
173
- {!showSessionList && status === 'streaming' && (
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 - only show when in chat view */}
186
- {!showSessionList && status === 'error' && error && (
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 - Session List or Chat */}
218
- {showSessionList ? (
219
- <SessionList
220
- sessions={standaloneSessions}
221
- onSelectSession={onSelectSession || (() => {})}
222
- onNewSession={onNewSession || (() => {})}
223
- onCloseSession={onCloseSession}
224
- />
225
- ) : (
226
- <>
227
- <div
228
- ref={contentRef}
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
- {/* Input field - visible when session is active or for idle standalone sessions */}
243
- {(status === 'streaming' || status === 'done' || (status === 'idle' && isStandalone)) && (
244
- <ClaudePanelInput
245
- onSendMessage={onSendMessage}
246
- disabled={status === 'streaming'}
247
- placeholder="Type a message..."
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
- const [showSessionList, setShowSessionList] = useState(false);
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
- setShowSessionList(false);
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 handleOpenSessionList = useCallback(() => {
563
- setShowSessionList(true);
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={handleOpenSessionList}
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-list-button"
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
- // 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` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.107",
3
+ "version": "4.4.109",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {
@@ -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: