jettypod 4.4.62 → 4.4.63

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.
@@ -277,7 +277,7 @@ export function getKanbanData(doneLimit: number = 50): KanbanData {
277
277
  const epicId = cleanItem.parent_id || cleanItem.epic_id;
278
278
  const epicTitle = epicId ? epicMap.get(epicId) || null : null;
279
279
  inFlight.push({ ...cleanItem, epicTitle });
280
- } else if (cleanItem.status === 'backlog' || cleanItem.status === 'todo') {
280
+ } else if (cleanItem.status === 'backlog' || cleanItem.status === 'todo' || cleanItem.status === null) {
281
281
  const group = getGroup(backlogGroups, cleanItem);
282
282
  group.items.push(cleanItem);
283
283
  } else if (cleanItem.status === 'done') {
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "ws-server",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "node server.js"
9
+ },
10
+ "dependencies": {
11
+ "ws": "^8.18.0"
12
+ }
13
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WebSocket Server for Real-time Dashboard Updates
4
+ *
5
+ * Watches the JettyPod work.db file for changes and broadcasts
6
+ * update messages to all connected dashboard clients.
7
+ */
8
+
9
+ const { WebSocketServer } = require('ws');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // Configuration
14
+ const PORT = process.env.WS_PORT || 8080;
15
+ const DB_PATH = process.env.JETTYPOD_DB_PATH || path.join(process.cwd(), '.jettypod', 'work.db');
16
+
17
+ // Track connected clients
18
+ const clients = new Set();
19
+
20
+ // Create WebSocket server
21
+ const wss = new WebSocketServer({ port: PORT });
22
+
23
+ console.log(`WebSocket server starting on port ${PORT}`);
24
+ console.log(`Watching database: ${DB_PATH}`);
25
+
26
+ // Handle new connections
27
+ wss.on('connection', (ws) => {
28
+ clients.add(ws);
29
+ console.log(`Client connected. Total clients: ${clients.size}`);
30
+
31
+ // Send initial connection confirmation
32
+ ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
33
+
34
+ // Handle client disconnect
35
+ ws.on('close', () => {
36
+ clients.delete(ws);
37
+ console.log(`Client disconnected. Total clients: ${clients.size}`);
38
+ });
39
+
40
+ // Handle errors
41
+ ws.on('error', (error) => {
42
+ console.error('WebSocket error:', error.message);
43
+ clients.delete(ws);
44
+ });
45
+ });
46
+
47
+ // Broadcast message to all connected clients
48
+ function broadcast(message) {
49
+ const payload = JSON.stringify(message);
50
+ for (const client of clients) {
51
+ if (client.readyState === 1) { // WebSocket.OPEN
52
+ client.send(payload);
53
+ }
54
+ }
55
+ }
56
+
57
+ // Watch database file for changes
58
+ let debounceTimer = null;
59
+ const DEBOUNCE_MS = 100; // Debounce rapid changes
60
+
61
+ function watchDatabase() {
62
+ // Check if database exists
63
+ if (!fs.existsSync(DB_PATH)) {
64
+ console.log(`Database not found at ${DB_PATH}, waiting...`);
65
+ // Retry in 5 seconds
66
+ setTimeout(watchDatabase, 5000);
67
+ return;
68
+ }
69
+
70
+ console.log(`Watching database for changes...`);
71
+
72
+ fs.watch(DB_PATH, (eventType, filename) => {
73
+ // Debounce to handle rapid SQLite writes
74
+ if (debounceTimer) {
75
+ clearTimeout(debounceTimer);
76
+ }
77
+
78
+ debounceTimer = setTimeout(() => {
79
+ console.log(`Database changed: ${eventType}`);
80
+ broadcast({
81
+ type: 'db_change',
82
+ event: eventType,
83
+ timestamp: Date.now()
84
+ });
85
+ }, DEBOUNCE_MS);
86
+ });
87
+ }
88
+
89
+ // Start watching
90
+ watchDatabase();
91
+
92
+ // Handle server shutdown gracefully
93
+ process.on('SIGINT', () => {
94
+ console.log('\nShutting down WebSocket server...');
95
+ wss.close(() => {
96
+ console.log('Server closed');
97
+ process.exit(0);
98
+ });
99
+ });
100
+
101
+ console.log(`WebSocket server running on ws://localhost:${PORT}`);
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Chore Planning Guardrails Hook
4
+ *
5
+ * Blocks work start for standalone chores until:
6
+ * 1. A chore-planning workflow exists for the chore
7
+ * 2. The workflow has completed (type classified, guidance loaded)
8
+ *
9
+ * This ensures type classification and guidance loading is done before starting work.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Read hook input from stdin
16
+ let input = '';
17
+ process.stdin.on('data', chunk => input += chunk);
18
+ process.stdin.on('end', async () => {
19
+ try {
20
+ const hookInput = JSON.parse(input);
21
+ const { tool_name, tool_input, cwd } = hookInput;
22
+
23
+ // Only check Bash commands
24
+ if (tool_name !== 'Bash') {
25
+ allow();
26
+ return;
27
+ }
28
+
29
+ const command = tool_input.command || '';
30
+
31
+ // Only check jettypod work start commands
32
+ if (!/jettypod\s+work\s+start/.test(command)) {
33
+ allow();
34
+ return;
35
+ }
36
+
37
+ // Extract work item ID from command
38
+ const idMatch = command.match(/jettypod\s+work\s+start\s+(\d+)/);
39
+ if (!idMatch) {
40
+ // No ID specified, allow
41
+ allow();
42
+ return;
43
+ }
44
+
45
+ const workItemId = parseInt(idMatch[1]);
46
+ const result = await checkChorePlanningGuardrail(workItemId, cwd);
47
+
48
+ if (result.allowed) {
49
+ allow();
50
+ } else {
51
+ deny(result.message, result.hint);
52
+ }
53
+ } catch (err) {
54
+ // Fail open on errors
55
+ allow();
56
+ }
57
+ });
58
+
59
+ /**
60
+ * Check if this work start should be blocked
61
+ */
62
+ async function checkChorePlanningGuardrail(workItemId, cwd) {
63
+ const dbPath = findDatabasePath(cwd);
64
+ if (!dbPath) {
65
+ return { allowed: true };
66
+ }
67
+
68
+ try {
69
+ const sqlite3 = require('better-sqlite3');
70
+ const db = sqlite3(dbPath, { readonly: true });
71
+
72
+ // Get the work item
73
+ const workItem = db.prepare(`
74
+ SELECT id, type, parent_id, title FROM work_items WHERE id = ?
75
+ `).get(workItemId);
76
+
77
+ if (!workItem) {
78
+ db.close();
79
+ return { allowed: true };
80
+ }
81
+
82
+ // Only check standalone chores (chores with no parent)
83
+ if (workItem.type !== 'chore') {
84
+ db.close();
85
+ return { allowed: true };
86
+ }
87
+
88
+ // If chore has a parent, it's not standalone - allow
89
+ if (workItem.parent_id) {
90
+ db.close();
91
+ return { allowed: true };
92
+ }
93
+
94
+ // This is a standalone chore - check for chore-planning workflow
95
+ const workflow = db.prepare(`
96
+ SELECT id, status, step_reached
97
+ FROM skill_executions
98
+ WHERE work_item_id = ? AND skill_name = 'chore-planning'
99
+ ORDER BY id DESC
100
+ LIMIT 1
101
+ `).get(workItemId);
102
+
103
+ db.close();
104
+
105
+ // If no workflow exists, block
106
+ if (!workflow) {
107
+ return {
108
+ allowed: false,
109
+ message: 'Cannot start standalone chore: type must be classified first',
110
+ hint: 'Use the chore-planning skill to classify the chore type and load guidance.'
111
+ };
112
+ }
113
+
114
+ // If workflow is not completed, block
115
+ if (workflow.status !== 'completed') {
116
+ return {
117
+ allowed: false,
118
+ message: 'Cannot start standalone chore: type must be classified first',
119
+ hint: 'Complete the chore-planning skill to classify the chore type.'
120
+ };
121
+ }
122
+
123
+ // Workflow completed, allow
124
+ return { allowed: true };
125
+
126
+ } catch (err) {
127
+ // Fail open
128
+ return { allowed: true };
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Find the jettypod database path
134
+ */
135
+ function findDatabasePath(cwd) {
136
+ let dir = cwd || process.cwd();
137
+ while (dir !== path.dirname(dir)) {
138
+ const dbPath = path.join(dir, '.jettypod', 'work.db');
139
+ if (fs.existsSync(dbPath)) {
140
+ return dbPath;
141
+ }
142
+ dir = path.dirname(dir);
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Allow the action
149
+ */
150
+ function allow() {
151
+ console.log(JSON.stringify({
152
+ hookSpecificOutput: {
153
+ hookEventName: "PreToolUse",
154
+ permissionDecision: "allow"
155
+ }
156
+ }));
157
+ process.exit(0);
158
+ }
159
+
160
+ /**
161
+ * Deny the action with explanation
162
+ */
163
+ function deny(message, hint) {
164
+ const reason = `āŒ ${message}\n\nšŸ’” Hint: ${hint}`;
165
+
166
+ console.log(JSON.stringify({
167
+ hookSpecificOutput: {
168
+ hookEventName: "PreToolUse",
169
+ permissionDecision: "deny",
170
+ permissionDecisionReason: reason
171
+ }
172
+ }));
173
+ process.exit(0);
174
+ }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Epic Planning Guardrails Hook
4
+ *
5
+ * Blocks feature creation under an epic until:
6
+ * 1. An epic-planning workflow exists for the epic
7
+ * 2. The workflow has reached step 6 (feature creation step)
8
+ *
9
+ * This ensures feature brainstorming is completed before creating features.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Read hook input from stdin
16
+ let input = '';
17
+ process.stdin.on('data', chunk => input += chunk);
18
+ process.stdin.on('end', async () => {
19
+ try {
20
+ const hookInput = JSON.parse(input);
21
+ const { tool_name, tool_input, cwd } = hookInput;
22
+
23
+ // Only check Bash commands
24
+ if (tool_name !== 'Bash') {
25
+ allow();
26
+ return;
27
+ }
28
+
29
+ const command = tool_input.command || '';
30
+
31
+ // Only check jettypod work create feature commands with --parent
32
+ if (!/jettypod\s+work\s+create\s+feature/.test(command)) {
33
+ allow();
34
+ return;
35
+ }
36
+
37
+ // Extract parent ID from command
38
+ const parentMatch = command.match(/--parent[=\s]+(\d+)/);
39
+ if (!parentMatch) {
40
+ // No parent specified - standalone feature, allow
41
+ allow();
42
+ return;
43
+ }
44
+
45
+ const parentId = parseInt(parentMatch[1]);
46
+ const result = await checkEpicPlanningGuardrail(parentId, cwd);
47
+
48
+ if (result.allowed) {
49
+ allow();
50
+ } else {
51
+ deny(result.message, result.hint);
52
+ }
53
+ } catch (err) {
54
+ // Fail open on errors
55
+ allow();
56
+ }
57
+ });
58
+
59
+ /**
60
+ * Check if this feature creation should be blocked
61
+ */
62
+ async function checkEpicPlanningGuardrail(parentId, cwd) {
63
+ const dbPath = findDatabasePath(cwd);
64
+ if (!dbPath) {
65
+ return { allowed: true };
66
+ }
67
+
68
+ try {
69
+ const sqlite3 = require('better-sqlite3');
70
+ const db = sqlite3(dbPath, { readonly: true });
71
+
72
+ // Check if parent is an epic
73
+ const parent = db.prepare(`
74
+ SELECT id, type FROM work_items WHERE id = ?
75
+ `).get(parentId);
76
+
77
+ if (!parent) {
78
+ db.close();
79
+ return { allowed: true };
80
+ }
81
+
82
+ // If parent isn't an epic, allow (chores can be created under features without epic-planning)
83
+ if (parent.type !== 'epic') {
84
+ db.close();
85
+ return { allowed: true };
86
+ }
87
+
88
+ // Parent is an epic - check for epic-planning workflow
89
+ const workflow = db.prepare(`
90
+ SELECT id, status, step_reached
91
+ FROM skill_executions
92
+ WHERE work_item_id = ? AND skill_name = 'epic-planning'
93
+ ORDER BY id DESC
94
+ LIMIT 1
95
+ `).get(parentId);
96
+
97
+ db.close();
98
+
99
+ // If no workflow exists, block
100
+ if (!workflow) {
101
+ return {
102
+ allowed: false,
103
+ message: 'Cannot create feature: Epic brainstorm must be confirmed first',
104
+ hint: 'Use the epic-planning skill to brainstorm and confirm features before creating them.'
105
+ };
106
+ }
107
+
108
+ // If workflow is completed, allow
109
+ if (workflow.status === 'completed') {
110
+ return { allowed: true };
111
+ }
112
+
113
+ // If workflow hasn't reached step 6 (Create Features), block
114
+ // Step 3: Brainstorm Features
115
+ // Step 4: Architectural Decision
116
+ // Step 5: Build Prototypes
117
+ // Step 6: Create Features (user confirmed)
118
+ if (workflow.step_reached < 6) {
119
+ return {
120
+ allowed: false,
121
+ message: 'Cannot create feature: Epic brainstorm must be confirmed first',
122
+ hint: 'Continue epic-planning to confirm the brainstormed features before creating them.'
123
+ };
124
+ }
125
+
126
+ // Workflow is at step 6 or beyond, allow
127
+ return { allowed: true };
128
+
129
+ } catch (err) {
130
+ // Fail open
131
+ return { allowed: true };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Find the jettypod database path
137
+ */
138
+ function findDatabasePath(cwd) {
139
+ let dir = cwd || process.cwd();
140
+ while (dir !== path.dirname(dir)) {
141
+ const dbPath = path.join(dir, '.jettypod', 'work.db');
142
+ if (fs.existsSync(dbPath)) {
143
+ return dbPath;
144
+ }
145
+ dir = path.dirname(dir);
146
+ }
147
+ return null;
148
+ }
149
+
150
+ /**
151
+ * Allow the action
152
+ */
153
+ function allow() {
154
+ console.log(JSON.stringify({
155
+ hookSpecificOutput: {
156
+ hookEventName: "PreToolUse",
157
+ permissionDecision: "allow"
158
+ }
159
+ }));
160
+ process.exit(0);
161
+ }
162
+
163
+ /**
164
+ * Deny the action with explanation
165
+ */
166
+ function deny(message, hint) {
167
+ const reason = `āŒ ${message}\n\nšŸ’” Hint: ${hint}`;
168
+
169
+ console.log(JSON.stringify({
170
+ hookSpecificOutput: {
171
+ hookEventName: "PreToolUse",
172
+ permissionDecision: "deny",
173
+ permissionDecisionReason: reason
174
+ }
175
+ }));
176
+ process.exit(0);
177
+ }
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * External Transition Guardrails Hook
4
+ *
5
+ * Blocks production deploy commands until:
6
+ * 1. Project is in external state (or allow if internal)
7
+ * 2. All infrastructure chores from the Infrastructure Readiness epic are done
8
+ *
9
+ * This ensures infrastructure work is completed before production deployment.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Production deploy patterns - commands that deploy to production
16
+ const PROD_DEPLOY_PATTERNS = [
17
+ /vercel\s+deploy\s+--prod/,
18
+ /vercel\s+--prod/,
19
+ /railway\s+up\s+.*production/,
20
+ /railway\s+up\s+.*--environment\s+production/,
21
+ /fly\s+deploy\s+--app\s+.*-prod/,
22
+ /gcloud\s+app\s+deploy\s+.*--promote/,
23
+ /aws\s+deploy/,
24
+ /kubectl\s+apply.*production/,
25
+ /helm\s+upgrade.*production/,
26
+ ];
27
+
28
+ // Read hook input from stdin
29
+ let input = '';
30
+ process.stdin.on('data', chunk => input += chunk);
31
+ process.stdin.on('end', async () => {
32
+ try {
33
+ const hookInput = JSON.parse(input);
34
+ const { tool_name, tool_input, cwd } = hookInput;
35
+
36
+ // Only check Bash commands
37
+ if (tool_name !== 'Bash') {
38
+ allow();
39
+ return;
40
+ }
41
+
42
+ const command = tool_input.command || '';
43
+
44
+ // Check if this is a production deploy command
45
+ const isProdDeploy = PROD_DEPLOY_PATTERNS.some(pattern => pattern.test(command));
46
+ if (!isProdDeploy) {
47
+ allow();
48
+ return;
49
+ }
50
+
51
+ const result = await checkExternalTransitionGuardrail(cwd);
52
+
53
+ if (result.allowed) {
54
+ allow();
55
+ } else {
56
+ deny(result.message, result.hint);
57
+ }
58
+ } catch (err) {
59
+ // Fail open on errors
60
+ allow();
61
+ }
62
+ });
63
+
64
+ /**
65
+ * Check if this production deploy should be blocked
66
+ */
67
+ async function checkExternalTransitionGuardrail(cwd) {
68
+ const dbPath = findDatabasePath(cwd);
69
+ if (!dbPath) {
70
+ return { allowed: true };
71
+ }
72
+
73
+ try {
74
+ const sqlite3 = require('better-sqlite3');
75
+ const db = sqlite3(dbPath, { readonly: true });
76
+
77
+ // Check project state
78
+ const stateRow = db.prepare(`
79
+ SELECT value FROM project_config WHERE key = 'state'
80
+ `).get();
81
+
82
+ // If project is internal, allow (no infrastructure requirements yet)
83
+ if (!stateRow || stateRow.value !== 'external') {
84
+ db.close();
85
+ return { allowed: true };
86
+ }
87
+
88
+ // Project is external - check for Infrastructure Readiness epic
89
+ const infraEpic = db.prepare(`
90
+ SELECT id FROM work_items
91
+ WHERE type = 'epic' AND title = 'Infrastructure Readiness'
92
+ LIMIT 1
93
+ `).get();
94
+
95
+ if (!infraEpic) {
96
+ // No infrastructure epic - allow (might have been created differently)
97
+ db.close();
98
+ return { allowed: true };
99
+ }
100
+
101
+ // Check for incomplete chores under the infrastructure epic
102
+ const incompleteChores = db.prepare(`
103
+ SELECT COUNT(*) as count
104
+ FROM work_items
105
+ WHERE parent_id = ? AND type = 'chore' AND status NOT IN ('done', 'cancelled')
106
+ `).get(infraEpic.id);
107
+
108
+ db.close();
109
+
110
+ if (incompleteChores.count > 0) {
111
+ return {
112
+ allowed: false,
113
+ message: 'Cannot deploy to production: infrastructure work must be completed first',
114
+ hint: `Complete all infrastructure chores (${incompleteChores.count} remaining) before deploying to production.`
115
+ };
116
+ }
117
+
118
+ // All infrastructure chores complete
119
+ return { allowed: true };
120
+
121
+ } catch (err) {
122
+ // Fail open
123
+ return { allowed: true };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Find the jettypod database path
129
+ */
130
+ function findDatabasePath(cwd) {
131
+ let dir = cwd || process.cwd();
132
+ while (dir !== path.dirname(dir)) {
133
+ const dbPath = path.join(dir, '.jettypod', 'work.db');
134
+ if (fs.existsSync(dbPath)) {
135
+ return dbPath;
136
+ }
137
+ dir = path.dirname(dir);
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Allow the action
144
+ */
145
+ function allow() {
146
+ console.log(JSON.stringify({
147
+ hookSpecificOutput: {
148
+ hookEventName: "PreToolUse",
149
+ permissionDecision: "allow"
150
+ }
151
+ }));
152
+ process.exit(0);
153
+ }
154
+
155
+ /**
156
+ * Deny the action with explanation
157
+ */
158
+ function deny(message, hint) {
159
+ const reason = `āŒ ${message}\n\nšŸ’” Hint: ${hint}`;
160
+
161
+ console.log(JSON.stringify({
162
+ hookSpecificOutput: {
163
+ hookEventName: "PreToolUse",
164
+ permissionDecision: "deny",
165
+ permissionDecisionReason: reason
166
+ }
167
+ }));
168
+ process.exit(0);
169
+ }
@@ -156,8 +156,15 @@ function evaluateBashCommand(command, inputRef, cwd) {
156
156
  };
157
157
  }
158
158
 
159
- // BLOCKED: Manual branch creation
160
- if (/git\s+checkout\s+-b\b/.test(strippedCommand) || /git\s+branch\s+(?!-d|-D)/.test(strippedCommand)) {
159
+ // BLOCKED: Manual branch creation (but allow read-only git branch commands)
160
+ // git checkout -b <branch> - creates branch
161
+ // git branch <name> - creates branch (no flags)
162
+ // git branch -d/-D - deletes branch (allowed)
163
+ // git branch --show-current, -l, -a, -r, -v, --list, --merged, etc. - read-only (allowed)
164
+ const isBranchCreation =
165
+ /git\s+checkout\s+-b\b/.test(strippedCommand) ||
166
+ /git\s+branch\s+[^-\s][^\s]*\s*$/.test(strippedCommand); // git branch <name> with no flags
167
+ if (isBranchCreation) {
161
168
  return {
162
169
  allowed: false,
163
170
  message: 'Manual branch creation is blocked',
@@ -226,12 +233,16 @@ function getActiveWorktreePathFromDB(cwd) {
226
233
  // Try better-sqlite3 first
227
234
  const sqlite3 = require('better-sqlite3');
228
235
  const db = sqlite3(dbPath, { readonly: true });
229
- const row = db.prepare(
230
- `SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
231
- ).get();
236
+ // Get ALL active worktrees, then find the one that contains our cwd
237
+ const rows = db.prepare(
238
+ `SELECT worktree_path FROM worktrees WHERE status = 'active'`
239
+ ).all();
232
240
  db.close();
233
- const result = row ? row.worktree_path : null;
234
- debug('db_lookup', { status: 'success', method: 'better-sqlite3', worktree: result });
241
+
242
+ // Find the worktree whose path is a prefix of cwd (we're inside it)
243
+ const matchingWorktree = rows.find(row => cwd.startsWith(row.worktree_path));
244
+ const result = matchingWorktree ? matchingWorktree.worktree_path : null;
245
+ debug('db_lookup', { status: 'success', method: 'better-sqlite3', worktree: result, candidates: rows.length });
235
246
  return result;
236
247
  } catch (err) {
237
248
  debug('db_lookup', { status: 'fallback', reason: err.message });
@@ -239,15 +250,19 @@ function getActiveWorktreePathFromDB(cwd) {
239
250
  const { spawnSync } = require('child_process');
240
251
  const result = spawnSync('sqlite3', [
241
252
  dbPath,
242
- `SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
253
+ `-list`,
254
+ `SELECT worktree_path FROM worktrees WHERE status = 'active'`
243
255
  ], { encoding: 'utf-8' });
244
256
 
245
257
  if (result.error || result.status !== 0) {
246
258
  debug('db_lookup', { status: 'cli_failed', error: result.error?.message || 'non-zero exit' });
247
259
  return null;
248
260
  }
249
- const worktree = result.stdout.trim() || null;
250
- debug('db_lookup', { status: 'success', method: 'sqlite3-cli', worktree });
261
+ // Parse all worktree paths and find the one containing cwd
262
+ const paths = result.stdout.trim().split('\n').filter(Boolean);
263
+ const matchingPath = paths.find(p => cwd.startsWith(p));
264
+ const worktree = matchingPath || null;
265
+ debug('db_lookup', { status: 'success', method: 'sqlite3-cli', worktree, candidates: paths.length });
251
266
  return worktree;
252
267
  }
253
268
  }