jettypod 4.4.94 → 4.4.96

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.js CHANGED
@@ -289,6 +289,8 @@ jettypod work merge # Merges worktree back to main
289
289
  jettypod work cleanup <id> # Cleanup worktree after merge
290
290
  jettypod work tests <feature-id> # Create test worktree for BDD
291
291
  jettypod work tests merge <id> # Merge tests to main
292
+ jettypod work prototype start <id> <approach> # Create prototype worktree
293
+ jettypod work prototype merge <id> # Merge prototype to main
292
294
  jettypod work status <id> cancelled
293
295
  jettypod backlog
294
296
  jettypod impact <file> # Show tests/features affected
@@ -1506,6 +1508,69 @@ switch (command) {
1506
1508
  process.exit(1);
1507
1509
  }
1508
1510
  }
1511
+ } else if (subcommand === 'prototype') {
1512
+ // Handle: work prototype start <feature-id> <approach-name> OR work prototype merge <feature-id>
1513
+ const workCommands = require('./lib/work-commands/index.js');
1514
+
1515
+ if (args[1] === 'merge') {
1516
+ // work prototype merge <feature-id>
1517
+ const featureId = parseInt(args[2]);
1518
+ if (!featureId) {
1519
+ console.log('Usage: jettypod work prototype merge <feature-id>');
1520
+ console.log('');
1521
+ console.log('Merges a prototype worktree back to main and cleans up.');
1522
+ console.log('');
1523
+ console.log('This command:');
1524
+ console.log(' 1. Verifies all changes are committed');
1525
+ console.log(' 2. Merges the prototype branch to main');
1526
+ console.log(' 3. Pushes to remote');
1527
+ console.log(' 4. Removes the worktree');
1528
+ console.log(' 5. Deletes the prototype branch');
1529
+ process.exit(1);
1530
+ }
1531
+ try {
1532
+ await workCommands.prototypeMerge(featureId);
1533
+ } catch (err) {
1534
+ console.error(`Error: ${err.message}`);
1535
+ process.exit(1);
1536
+ }
1537
+ } else if (args[1] === 'start') {
1538
+ // work prototype start <feature-id> <approach-name>
1539
+ const featureId = parseInt(args[2]);
1540
+ const approachName = args[3];
1541
+ if (!featureId || !approachName) {
1542
+ console.log('Usage: jettypod work prototype start <feature-id> <approach-name>');
1543
+ console.log('');
1544
+ console.log('Creates a worktree for building prototypes for a feature.');
1545
+ console.log('');
1546
+ console.log('Examples:');
1547
+ console.log(' jettypod work prototype start 42 simple-form');
1548
+ console.log(' jettypod work prototype start 42 wizard-flow');
1549
+ console.log('');
1550
+ console.log('Prototypes are written in /prototypes/feature-<id>-<approach>/ directory.');
1551
+ console.log('When done, merge with: jettypod work prototype merge <feature-id>');
1552
+ process.exit(1);
1553
+ }
1554
+ try {
1555
+ await workCommands.prototypeWork(featureId, approachName);
1556
+ } catch (err) {
1557
+ console.error(`Error: ${err.message}`);
1558
+ process.exit(1);
1559
+ }
1560
+ } else {
1561
+ // work prototype (show usage)
1562
+ console.log('Usage: jettypod work prototype start <feature-id> <approach-name>');
1563
+ console.log(' jettypod work prototype merge <feature-id>');
1564
+ console.log('');
1565
+ console.log('Commands:');
1566
+ console.log(' work prototype start <id> <approach> Create a worktree for prototyping');
1567
+ console.log(' work prototype merge <id> Merge prototype worktree to main');
1568
+ console.log('');
1569
+ console.log('Examples:');
1570
+ console.log(' jettypod work prototype start 42 simple-form');
1571
+ console.log(' jettypod work prototype merge 42');
1572
+ process.exit(1);
1573
+ }
1509
1574
  } else {
1510
1575
  // CRITICAL SAFETY CHECK: Prevent work status changes from within a worktree
1511
1576
  // This prevents cleanup failures and orphaned worktrees
@@ -2126,6 +2126,253 @@ async function testsMerge(featureId) {
2126
2126
  return Promise.resolve();
2127
2127
  }
2128
2128
 
2129
+ /**
2130
+ * Create a worktree for building prototypes for a feature or epic
2131
+ * @param {number} workItemId - Feature or Epic ID to create prototype worktree for
2132
+ * @param {string} approachName - Name of the approach being prototyped (e.g., "simple-form", "wizard")
2133
+ * @returns {Promise<Object>} Worktree info with path and branch
2134
+ * @throws {Error} If work item not found or not a feature/epic
2135
+ */
2136
+ async function prototypeWork(workItemId, approachName) {
2137
+ if (!workItemId || isNaN(workItemId) || workItemId < 1) {
2138
+ return Promise.reject(new Error('Invalid work item ID'));
2139
+ }
2140
+
2141
+ if (!approachName || typeof approachName !== 'string' || !approachName.trim()) {
2142
+ return Promise.reject(new Error('Approach name is required (e.g., "simple-form", "wizard")'));
2143
+ }
2144
+
2145
+ // Sanitize approach name for use in branch/path
2146
+ const sanitizedApproach = approachName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
2147
+
2148
+ const paths = getPaths();
2149
+
2150
+ if (!fs.existsSync(paths.jettypodDir)) {
2151
+ return Promise.reject(new Error('JettyPod not initialized. Run: jettypod init'));
2152
+ }
2153
+
2154
+ if (!fs.existsSync(paths.dbPath)) {
2155
+ return Promise.reject(new Error('Work database not found. Run: jettypod init'));
2156
+ }
2157
+
2158
+ const db = getDb();
2159
+
2160
+ return new Promise((resolve, reject) => {
2161
+ // Get feature or epic work item
2162
+ db.get(
2163
+ `SELECT * FROM work_items WHERE id = ? AND type IN ('feature', 'epic')`,
2164
+ [workItemId],
2165
+ async (err, workItem) => {
2166
+ if (err) {
2167
+ return reject(new Error(`Database error: ${err.message}`));
2168
+ }
2169
+
2170
+ if (!workItem) {
2171
+ return reject(new Error(`Feature or epic #${workItemId} not found`));
2172
+ }
2173
+
2174
+ // Create worktree with prototype/ prefix
2175
+ try {
2176
+ const gitRoot = getGitRoot();
2177
+ const branchPrefix = workItem.type === 'epic' ? 'prototype/epic' : 'prototype/feature';
2178
+ const worktreeResult = await worktreeManager.createWorktree(workItem, {
2179
+ repoPath: gitRoot,
2180
+ branchPrefix: branchPrefix,
2181
+ pathPrefix: `prototype-`,
2182
+ branchSuffix: `-${sanitizedApproach}`
2183
+ });
2184
+
2185
+ // Set as current work
2186
+ setCurrentWork({
2187
+ id: workItem.id,
2188
+ type: workItem.type,
2189
+ title: workItem.title,
2190
+ status: workItem.status || 'in_progress',
2191
+ worktreePath: worktreeResult.worktree_path,
2192
+ branchName: worktreeResult.branch_name
2193
+ });
2194
+
2195
+ console.log(`✅ Created prototype worktree: ${worktreeResult.worktree_path}`);
2196
+ console.log(`📁 IMPORTANT: Use absolute paths for file operations:`);
2197
+ console.log(` ${worktreeResult.worktree_path}/prototypes/${workItem.type}-${workItemId}-${sanitizedApproach}/ (correct)`);
2198
+ console.log(` prototypes/... (wrong - creates in main repo)`);
2199
+ console.log(`\nPrototyping "${approachName}" for: [#${workItem.id}] ${workItem.title}`);
2200
+
2201
+ resolve(worktreeResult);
2202
+ } catch (worktreeErr) {
2203
+ reject(new Error(`Failed to create prototype worktree: ${worktreeErr.message}`));
2204
+ }
2205
+ }
2206
+ );
2207
+ });
2208
+ }
2209
+
2210
+ /**
2211
+ * Merge a prototype worktree back to main and clean up
2212
+ * @param {number} workItemId - Feature or Epic ID whose prototype worktree to merge
2213
+ * @returns {Promise<void>}
2214
+ */
2215
+ async function prototypeMerge(workItemId) {
2216
+ if (!workItemId || isNaN(workItemId) || workItemId < 1) {
2217
+ return Promise.reject(new Error('Invalid work item ID'));
2218
+ }
2219
+
2220
+ // Auto-chdir to main repo if running from inside a worktree
2221
+ const cwd = process.cwd();
2222
+ if (cwd.includes('.jettypod-work')) {
2223
+ const mainRepo = getGitRoot();
2224
+ console.log('📂 Changing to main repository for merge...');
2225
+ process.chdir(mainRepo);
2226
+ }
2227
+
2228
+ const db = getDb();
2229
+ const gitRoot = getGitRoot();
2230
+
2231
+ // Find the prototype worktree for this feature or epic (branch starts with prototype/feature- or prototype/epic-)
2232
+ const worktree = await new Promise((resolve, reject) => {
2233
+ db.get(
2234
+ `SELECT * FROM worktrees
2235
+ WHERE work_item_id = ?
2236
+ AND (branch_name LIKE 'prototype/feature-%' OR branch_name LIKE 'prototype/epic-%')
2237
+ AND status = 'active'`,
2238
+ [workItemId],
2239
+ (err, row) => {
2240
+ if (err) return reject(err);
2241
+ resolve(row);
2242
+ }
2243
+ );
2244
+ });
2245
+
2246
+ if (!worktree) {
2247
+ return Promise.reject(new Error(
2248
+ `No active prototype worktree found for #${workItemId}.\n\n` +
2249
+ `Create one with: jettypod work prototype start ${workItemId} <approach-name>`
2250
+ ));
2251
+ }
2252
+
2253
+ const worktreePath = worktree.worktree_path;
2254
+ const branchName = worktree.branch_name;
2255
+
2256
+ console.log(`⏳ Merging prototype worktree for #${workItemId}...`);
2257
+ console.log(` Branch: ${branchName}`);
2258
+ console.log(` Path: ${worktreePath}`);
2259
+
2260
+ // Check for uncommitted changes in the worktree
2261
+ try {
2262
+ const status = execSync('git status --porcelain', {
2263
+ cwd: worktreePath,
2264
+ encoding: 'utf8',
2265
+ stdio: 'pipe'
2266
+ }).trim();
2267
+
2268
+ if (status) {
2269
+ return Promise.reject(new Error(
2270
+ `Uncommitted changes in prototype worktree:\n${status}\n\n` +
2271
+ `Commit your changes first:\n` +
2272
+ ` cd ${worktreePath}\n` +
2273
+ ` git add .\n` +
2274
+ ` git commit -m "Add prototype for #${workItemId}"`
2275
+ ));
2276
+ }
2277
+ } catch (err) {
2278
+ return Promise.reject(new Error(`Failed to check worktree status: ${err.message}`));
2279
+ }
2280
+
2281
+ // Check if there are commits to merge
2282
+ try {
2283
+ const mainBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
2284
+ cwd: gitRoot,
2285
+ encoding: 'utf8',
2286
+ stdio: 'pipe'
2287
+ }).trim().replace('refs/remotes/origin/', '');
2288
+
2289
+ const commitCount = execSync(`git rev-list --count ${mainBranch}..${branchName}`, {
2290
+ cwd: gitRoot,
2291
+ encoding: 'utf8',
2292
+ stdio: 'pipe'
2293
+ }).trim();
2294
+
2295
+ if (commitCount === '0') {
2296
+ console.log('⚠️ No commits to merge - worktree has no new changes');
2297
+ }
2298
+ } catch (err) {
2299
+ // Non-fatal - proceed anyway
2300
+ console.log('⚠️ Could not check commit count, proceeding with merge');
2301
+ }
2302
+
2303
+ // Merge the prototype branch to main
2304
+ try {
2305
+ // First, ensure we're on main
2306
+ execSync('git checkout main', {
2307
+ cwd: gitRoot,
2308
+ encoding: 'utf8',
2309
+ stdio: 'pipe'
2310
+ });
2311
+
2312
+ // Merge the prototype branch
2313
+ execSync(`git merge ${branchName} --no-ff -m "Merge prototype for #${workItemId}"`, {
2314
+ cwd: gitRoot,
2315
+ encoding: 'utf8',
2316
+ stdio: 'pipe'
2317
+ });
2318
+
2319
+ console.log('✅ Merged prototype branch to main');
2320
+ } catch (err) {
2321
+ return Promise.reject(new Error(
2322
+ `Merge failed: ${err.message}\n\n` +
2323
+ `Resolve conflicts manually, then run:\n` +
2324
+ ` jettypod work prototype merge ${workItemId}`
2325
+ ));
2326
+ }
2327
+
2328
+ // Push to remote
2329
+ try {
2330
+ execSync('git push', {
2331
+ cwd: gitRoot,
2332
+ encoding: 'utf8',
2333
+ stdio: 'pipe'
2334
+ });
2335
+ console.log('✅ Pushed to remote');
2336
+ } catch (err) {
2337
+ console.log('⚠️ Failed to push (non-fatal):', err.message);
2338
+ }
2339
+
2340
+ // Mark worktree as merged but DON'T delete it yet
2341
+ // This prevents shell CWD corruption when merge is run from inside the worktree
2342
+ await new Promise((resolve, reject) => {
2343
+ db.run(
2344
+ `UPDATE worktrees SET status = 'merged' WHERE id = ?`,
2345
+ [worktree.id],
2346
+ (err) => {
2347
+ if (err) return reject(err);
2348
+ resolve();
2349
+ }
2350
+ );
2351
+ });
2352
+
2353
+ // Clear current work if it was pointing to this worktree
2354
+ const currentWork = await getCurrentWork();
2355
+ if (currentWork && currentWork.worktreePath === worktreePath) {
2356
+ clearCurrentWork();
2357
+ }
2358
+
2359
+ console.log('');
2360
+ console.log(`✅ Prototype worktree for #${workItemId} merged successfully`);
2361
+
2362
+ // Instruct user to cleanup worktree separately - be aggressive so AI doesn't forget
2363
+ if (fs.existsSync(worktreePath)) {
2364
+ console.log('');
2365
+ console.log('⚠️ CLEANUP REQUIRED - Run these commands NOW:');
2366
+ console.log('');
2367
+ console.log(` cd ${gitRoot}`);
2368
+ console.log(` jettypod work cleanup ${workItemId}`);
2369
+ console.log('');
2370
+ console.log(' Worktrees accumulate until cleaned up.');
2371
+ }
2372
+
2373
+ return Promise.resolve();
2374
+ }
2375
+
2129
2376
  module.exports = {
2130
2377
  startWork,
2131
2378
  stopWork,
@@ -2134,5 +2381,7 @@ module.exports = {
2134
2381
  cleanupWorkItem,
2135
2382
  mergeWork,
2136
2383
  testsWork,
2137
- testsMerge
2384
+ testsMerge,
2385
+ prototypeWork,
2386
+ prototypeMerge
2138
2387
  };
@@ -88,10 +88,11 @@ async function createWorktree(workItem, options = {}) {
88
88
  // Generate branch name and worktree path
89
89
  const titleSlug = slugify(workItem.title || 'item');
90
90
  const branchPrefix = options.branchPrefix || 'feature/work';
91
+ const branchSuffix = options.branchSuffix || '';
91
92
  const pathPrefix = options.pathPrefix || '';
92
- const branchName = `${branchPrefix}-${workItem.id}-${titleSlug}`;
93
+ const branchName = `${branchPrefix}-${workItem.id}-${titleSlug}${branchSuffix}`;
93
94
  const worktreeBasePath = path.join(gitRoot, '.jettypod-work');
94
- const worktreePath = path.join(worktreeBasePath, `${pathPrefix}${workItem.id}-${titleSlug}`);
95
+ const worktreePath = path.join(worktreeBasePath, `${pathPrefix}${workItem.id}-${titleSlug}${branchSuffix}`);
95
96
 
96
97
  let worktreeId = null;
97
98
  let branchCreated = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.94",
3
+ "version": "4.4.96",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {
@@ -301,7 +301,7 @@ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
301
301
 
302
302
  ```bash
303
303
  # Step 1: Merge (can run from worktree - it won't delete it)
304
- jettypod work merge
304
+ jettypod work merge <bug-id>
305
305
  ```
306
306
 
307
307
  ```bash
@@ -366,7 +366,7 @@ Before ending bug-mode skill, ensure:
366
366
 
367
367
  **Merge fix (CRITICAL: cd to main repo separately):**
368
368
  ```bash
369
- jettypod work merge # Merge current bug
369
+ jettypod work merge <bug-id> # ALWAYS include bug ID
370
370
  ```
371
371
 
372
372
  ```bash
@@ -67,6 +67,14 @@ Your shell's working directory was likely inside a worktree that was deleted. Th
67
67
 
68
68
  **Your task:** Acknowledge the context passed from chore-planning.
69
69
 
70
+ **🚫 FORBIDDEN: Writing Files at This Step**
71
+ ```
72
+ ❌ Write tool to any path
73
+ ❌ Edit tool to any path
74
+ ❌ Any file creation or modification
75
+ ```
76
+ **This is a CONTEXT step.** File writes happen in Step 5.
77
+
70
78
  You will receive:
71
79
  - `choreContext.chore` - Chore ID, title, description
72
80
  - `choreContext.classification` - Type and confidence
@@ -149,10 +157,34 @@ Resolve the issue before continuing.
149
157
 
150
158
  **Move to Step 3 only if worktree was created successfully.**
151
159
 
160
+ **🔒 WORKTREE PATH LOCK**
161
+
162
+ After worktree validation passes, capture and lock the path:
163
+ - `WORKTREE_PATH` - the absolute path from the query result
164
+
165
+ **Display:**
166
+
167
+ ```
168
+ 🔒 WORKTREE LOCK ACTIVE
169
+ Path: ${WORKTREE_PATH}
170
+
171
+ All file writes will use this path.
172
+ ```
173
+
174
+ **From this point forward, ALL file operations MUST use paths starting with `${WORKTREE_PATH}/`**
175
+
152
176
  ### Step 3: Establish Test Baseline
153
177
 
154
178
  **Your task:** Run tests to establish baseline before making changes.
155
179
 
180
+ **🚫 FORBIDDEN: Writing Files at This Step**
181
+ ```
182
+ ❌ Write tool to any path
183
+ ❌ Edit tool to any path
184
+ ❌ Any file creation or modification
185
+ ```
186
+ **This is a TESTING step.** File writes happen in Step 5.
187
+
156
188
  **Test handling varies by type:**
157
189
 
158
190
  **For REFACTOR:**
@@ -205,6 +237,14 @@ jettypod workflow checkpoint <chore-id> --step=3
205
237
 
206
238
  **Your task:** Display warnings and constraints based on chore type.
207
239
 
240
+ **🚫 FORBIDDEN: Writing Files at This Step**
241
+ ```
242
+ ❌ Write tool to any path
243
+ ❌ Edit tool to any path
244
+ ❌ Any file creation or modification
245
+ ```
246
+ **This is a GUIDANCE step.** File writes happen in Step 5.
247
+
208
248
  **Load guidance from taxonomy:**
209
249
 
210
250
  ```javascript
@@ -258,6 +298,10 @@ const guidance = getGuidance('[chore-type]');
258
298
 
259
299
  ### Step 5: Execute with Iteration
260
300
 
301
+ **🔒 WORKTREE PATH REQUIRED:** All file writes MUST use the `WORKTREE_PATH` captured in Step 2.
302
+
303
+ **✅ NOW you may write files** - worktree is locked, guidance is displayed.
304
+
261
305
  **Your task:** Make changes and iterate until tests pass. Max 5 iterations.
262
306
 
263
307
  ```
@@ -231,21 +231,50 @@ Present exactly 3 approaches. Fill in the bracketed parts with actual content ba
231
231
 
232
232
  If user wants to prototype approaches:
233
233
 
234
- 1. Create prototype directory using the epic ID from Step 1:
235
- ```bash
236
- mkdir -p prototypes/epic-${EPIC_ID}-${APPROACH_NAME}
237
- ```
234
+ **Sub-step 1: Create prototype worktree**
235
+
236
+ ```bash
237
+ jettypod work prototype start ${EPIC_ID} [approach-name]
238
+ ```
239
+
240
+ Example (if epic ID is 5, approach is "websockets"):
241
+ ```bash
242
+ jettypod work prototype start 5 websockets
243
+ ```
244
+
245
+ This creates a worktree at `.jettypod-work/prototype-<id>-<slug>-<approach>/` where prototypes can be safely built and committed.
246
+
247
+ **🛑 STOP AND CHECK:** Verify worktree was created successfully before proceeding.
238
248
 
239
- **Example** (if epic ID is 5, approach is "websockets"):
249
+ **Sub-step 2: Build prototypes in the worktree**
250
+
251
+ 1. Build prototypes using **absolute paths** to the worktree:
252
+ - `<worktree_path>/prototypes/epic-${EPIC_ID}-${APPROACH_NAME}/`
253
+ 2. Build 2-3 working prototypes demonstrating the architectural difference
254
+ 3. **Commit the prototype** in the worktree:
240
255
  ```bash
241
- mkdir -p prototypes/epic-5-websockets
256
+ cd <worktree_path>
257
+ git add .
258
+ git commit -m "Add prototype: [approach-name] for epic #${EPIC_ID}"
242
259
  ```
243
260
 
244
- 2. Build 2-3 working prototypes demonstrating the architectural difference
261
+ **Sub-step 3: Test and decide**
262
+
263
+ After user tests, ask which approach they prefer.
264
+
265
+ **Sub-step 4: Merge prototype**
266
+
267
+ After user has tested (regardless of whether they pick this approach):
268
+
269
+ ```bash
270
+ jettypod work prototype merge ${EPIC_ID}
271
+ cd <main-repo-path>
272
+ jettypod work cleanup ${EPIC_ID}
273
+ ```
245
274
 
246
- 3. After user tests, ask which approach they prefer
275
+ This merges prototype files to main (in `/prototypes/` directory) and cleans up the worktree.
247
276
 
248
- 4. When user picks a winner → Go to Step 6 (set `ARCH_DECISION_MADE = true`)
277
+ When user picks a winner → Go to Step 6 (set `ARCH_DECISION_MADE = true`)
249
278
 
250
279
  ### Step 5C: Skip Architecture (User Already Knows)
251
280