jettypod 4.4.116 → 4.4.120
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/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
2
|
-
const https = require('https');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
1
|
const packageJson = require('../../package.json');
|
|
6
2
|
|
|
7
3
|
/**
|
|
@@ -12,184 +8,22 @@ function getCurrentVersion() {
|
|
|
12
8
|
return packageJson.version;
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
/**
|
|
16
|
-
* Check npm registry for latest version
|
|
17
|
-
* @returns {Promise<string>} Latest version from npm
|
|
18
|
-
*/
|
|
19
|
-
function getLatestVersion() {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const packageName = packageJson.name;
|
|
22
|
-
const url = `https://registry.npmjs.org/${packageName}/latest`;
|
|
23
|
-
|
|
24
|
-
const request = https.get(url, (res) => {
|
|
25
|
-
let data = '';
|
|
26
|
-
|
|
27
|
-
if (res.statusCode !== 200) {
|
|
28
|
-
reject(new Error(`HTTP ${res.statusCode}`));
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
res.on('data', (chunk) => {
|
|
33
|
-
data += chunk;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
res.on('end', () => {
|
|
37
|
-
try {
|
|
38
|
-
const json = JSON.parse(data);
|
|
39
|
-
if (!json.version) {
|
|
40
|
-
reject(new Error('No version found in npm response'));
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
resolve(json.version);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
reject(new Error(`Invalid JSON response: ${err.message}`));
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Set timeout for network request (30 seconds)
|
|
51
|
-
request.setTimeout(30000, () => {
|
|
52
|
-
request.destroy();
|
|
53
|
-
reject(new Error('Request timeout - network too slow'));
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
request.on('error', (err) => {
|
|
57
|
-
// Provide specific error messages for common network errors
|
|
58
|
-
if (err.code === 'ENOTFOUND') {
|
|
59
|
-
reject(new Error('network error - DNS lookup failed (check internet connection)'));
|
|
60
|
-
} else if (err.code === 'ETIMEDOUT' || err.code === 'ESOCKETTIMEDOUT') {
|
|
61
|
-
reject(new Error('network error - connection timeout'));
|
|
62
|
-
} else if (err.code === 'ECONNREFUSED') {
|
|
63
|
-
reject(new Error('network error - connection refused'));
|
|
64
|
-
} else if (err.code === 'ECONNRESET') {
|
|
65
|
-
reject(new Error('network error - connection reset'));
|
|
66
|
-
} else {
|
|
67
|
-
reject(err);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Update jettypod to latest version using npm
|
|
75
|
-
* @param {string} version - Version to update to (default: latest)
|
|
76
|
-
* @returns {boolean} True if update succeeded
|
|
77
|
-
*/
|
|
78
|
-
function updateJettyPod(version = 'latest') {
|
|
79
|
-
const packageName = packageJson.name;
|
|
80
|
-
try {
|
|
81
|
-
console.log(`📦 Installing jettypod@${version}...`);
|
|
82
|
-
|
|
83
|
-
// Use npm to update globally
|
|
84
|
-
execSync(`npm install -g ${packageName}@${version}`, {
|
|
85
|
-
stdio: 'inherit'
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Clear dashboard .next folder to force rebuild with new assets
|
|
89
|
-
try {
|
|
90
|
-
const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
|
|
91
|
-
const dashboardNextPath = path.join(globalRoot, 'jettypod', 'apps', 'dashboard', '.next');
|
|
92
|
-
if (fs.existsSync(dashboardNextPath)) {
|
|
93
|
-
fs.rmSync(dashboardNextPath, { recursive: true, force: true });
|
|
94
|
-
console.log('🧹 Cleared dashboard cache');
|
|
95
|
-
}
|
|
96
|
-
} catch {
|
|
97
|
-
// Non-fatal - dashboard will still work, just might use old cache
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return true;
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.log('');
|
|
103
|
-
console.error(`❌ Update failed`);
|
|
104
|
-
console.log('');
|
|
105
|
-
|
|
106
|
-
// Provide specific error messages for common failures
|
|
107
|
-
const errorOutput = err.stderr ? err.stderr.toString() : '';
|
|
108
|
-
|
|
109
|
-
if (err.message.includes('EACCES') || err.message.includes('EPERM') ||
|
|
110
|
-
errorOutput.includes('EACCES') || errorOutput.includes('EPERM')) {
|
|
111
|
-
console.error('Permission denied - try running with sudo:');
|
|
112
|
-
console.log(` sudo npm install -g ${packageName}@${version}`);
|
|
113
|
-
console.log('');
|
|
114
|
-
console.error('Or configure npm to use a different directory:');
|
|
115
|
-
console.log(' mkdir ~/.npm-global');
|
|
116
|
-
console.log(' npm config set prefix ~/.npm-global');
|
|
117
|
-
console.log(' export PATH=~/.npm-global/bin:$PATH');
|
|
118
|
-
} else if (errorOutput.includes('ENOSPC')) {
|
|
119
|
-
console.error('Not enough disk space to install update');
|
|
120
|
-
console.log('Free up disk space and try again');
|
|
121
|
-
} else if (errorOutput.includes('404') || errorOutput.includes('E404')) {
|
|
122
|
-
console.error(`Version ${version} not found in npm registry`);
|
|
123
|
-
} else if (errorOutput.includes('network') || errorOutput.includes('ETIMEDOUT') ||
|
|
124
|
-
errorOutput.includes('ENOTFOUND')) {
|
|
125
|
-
console.error('Network error during npm install');
|
|
126
|
-
console.log('Check your internet connection and try again');
|
|
127
|
-
} else {
|
|
128
|
-
console.error(`Error details: ${err.message}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
console.log('');
|
|
132
|
-
console.error('Manual update:');
|
|
133
|
-
console.log(` npm install -g ${packageName}@${version}`);
|
|
134
|
-
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
11
|
/**
|
|
140
12
|
* Run the update command
|
|
141
|
-
*
|
|
142
|
-
* @param {Function} options.getCurrentVersion - Function to get current version
|
|
143
|
-
* @param {Function} options.getLatestVersion - Function to get latest version
|
|
144
|
-
* @param {Function} options.updateJettyPod - Function to update jettypod
|
|
13
|
+
* The JettyPod app is the single distribution point — CLI updates ship with app updates.
|
|
145
14
|
*/
|
|
146
|
-
async function runUpdate(
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
const _updateJettyPod = options.updateJettyPod || updateJettyPod;
|
|
150
|
-
|
|
151
|
-
console.log('🔍 Checking for updates...');
|
|
152
|
-
|
|
153
|
-
const currentVersion = _getCurrentVersion();
|
|
154
|
-
console.log(`Current version: ${currentVersion}`);
|
|
155
|
-
|
|
156
|
-
let latestVersion;
|
|
157
|
-
try {
|
|
158
|
-
latestVersion = await _getLatestVersion();
|
|
159
|
-
} catch (err) {
|
|
160
|
-
console.log(`Cannot check for updates: ${err.message}`);
|
|
161
|
-
console.log('');
|
|
162
|
-
console.log('You can still manually update with:');
|
|
163
|
-
console.log(` npm install -g ${packageJson.name}@latest`);
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
console.log(`Latest version: ${latestVersion}`);
|
|
15
|
+
async function runUpdate() {
|
|
16
|
+
const currentVersion = getCurrentVersion();
|
|
17
|
+
console.log(`JettyPod v${currentVersion}`);
|
|
168
18
|
console.log('');
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
console.log(`Already on latest version: ${latestVersion}`);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
console.log(`New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
19
|
+
console.log('The CLI is bundled with the JettyPod app.');
|
|
20
|
+
console.log('Updates are delivered automatically through the app.');
|
|
176
21
|
console.log('');
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (success) {
|
|
181
|
-
console.log('');
|
|
182
|
-
console.log(`✅ JettyPod updated to ${latestVersion}`);
|
|
183
|
-
console.log('');
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return false;
|
|
22
|
+
console.log('To check for app updates: open JettyPod and it will auto-update.');
|
|
23
|
+
return true;
|
|
188
24
|
}
|
|
189
25
|
|
|
190
26
|
module.exports = {
|
|
191
27
|
runUpdate,
|
|
192
|
-
getCurrentVersion
|
|
193
|
-
getLatestVersion,
|
|
194
|
-
updateJettyPod
|
|
28
|
+
getCurrentVersion
|
|
195
29
|
};
|
|
@@ -1148,9 +1148,12 @@ async function cleanupWorktrees(options = {}) {
|
|
|
1148
1148
|
* Clean up a specific worktree after merge
|
|
1149
1149
|
* Should be run from main repo after cd'ing out of the worktree
|
|
1150
1150
|
* @param {number} workItemId - The work item ID to clean up
|
|
1151
|
+
* @param {Object} [options] - Cleanup options
|
|
1152
|
+
* @param {boolean} [options.force] - Force cleanup even if worktree is still active (skips merge requirement)
|
|
1151
1153
|
* @returns {Promise<Object>} Result with success status
|
|
1152
1154
|
*/
|
|
1153
|
-
async function cleanupWorkItem(workItemId) {
|
|
1155
|
+
async function cleanupWorkItem(workItemId, options = {}) {
|
|
1156
|
+
const { force = false } = options;
|
|
1154
1157
|
const db = getDb();
|
|
1155
1158
|
const gitRoot = getGitRoot();
|
|
1156
1159
|
|
|
@@ -1183,13 +1186,89 @@ async function cleanupWorkItem(workItemId) {
|
|
|
1183
1186
|
return { success: true, message: 'No worktree to clean up' };
|
|
1184
1187
|
}
|
|
1185
1188
|
|
|
1186
|
-
if (worktree.status === 'active') {
|
|
1189
|
+
if (worktree.status === 'active' && !force) {
|
|
1187
1190
|
return Promise.reject(new Error(
|
|
1188
1191
|
`Worktree for #${workItemId} is still active.\n` +
|
|
1189
|
-
`Run 'jettypod work merge ${workItemId}' first.`
|
|
1192
|
+
`Run 'jettypod work merge ${workItemId}' first, or use --force to skip merge.`
|
|
1190
1193
|
));
|
|
1191
1194
|
}
|
|
1192
1195
|
|
|
1196
|
+
// Force cleanup of active worktree — warn about unmerged work
|
|
1197
|
+
if (worktree.status === 'active' && force) {
|
|
1198
|
+
const warnings = [];
|
|
1199
|
+
|
|
1200
|
+
// Check for uncommitted changes in the worktree
|
|
1201
|
+
if (worktree.worktree_path && fs.existsSync(worktree.worktree_path)) {
|
|
1202
|
+
try {
|
|
1203
|
+
const dirtyFiles = execSync('git status --porcelain', {
|
|
1204
|
+
cwd: worktree.worktree_path,
|
|
1205
|
+
stdio: 'pipe'
|
|
1206
|
+
}).toString().trim();
|
|
1207
|
+
if (dirtyFiles) {
|
|
1208
|
+
const fileCount = dirtyFiles.split('\n').length;
|
|
1209
|
+
warnings.push(`${fileCount} uncommitted change${fileCount !== 1 ? 's' : ''}`);
|
|
1210
|
+
}
|
|
1211
|
+
} catch {
|
|
1212
|
+
// Worktree may be in a bad state — not a blocker for force cleanup
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Check for unmerged commits on the branch
|
|
1217
|
+
if (worktree.branch_name) {
|
|
1218
|
+
try {
|
|
1219
|
+
const unmergedLog = execSync(`git log main..${worktree.branch_name} --oneline`, {
|
|
1220
|
+
cwd: gitRoot,
|
|
1221
|
+
stdio: 'pipe'
|
|
1222
|
+
}).toString().trim();
|
|
1223
|
+
if (unmergedLog) {
|
|
1224
|
+
const commitCount = unmergedLog.split('\n').length;
|
|
1225
|
+
warnings.push(`${commitCount} unmerged commit${commitCount !== 1 ? 's' : ''}`);
|
|
1226
|
+
}
|
|
1227
|
+
} catch {
|
|
1228
|
+
// Branch may not exist or may not have diverged — not a blocker
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Display warnings and prompt for confirmation
|
|
1233
|
+
console.log(`\n⚠️ Force cleanup of active worktree for #${workItemId}: ${worktree.title}\n`);
|
|
1234
|
+
if (warnings.length > 0) {
|
|
1235
|
+
console.log(` This worktree has ${warnings.join(' and ')}.\n This data will be lost and cannot be recovered easily.\n`);
|
|
1236
|
+
} else {
|
|
1237
|
+
console.log(` No unmerged commits or uncommitted changes detected.\n`);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Interactive confirmation
|
|
1241
|
+
const confirmed = await new Promise((resolve) => {
|
|
1242
|
+
const readline = require('readline');
|
|
1243
|
+
const rl = readline.createInterface({
|
|
1244
|
+
input: process.stdin,
|
|
1245
|
+
output: process.stdout
|
|
1246
|
+
});
|
|
1247
|
+
rl.question('Proceed with force cleanup? (y/N): ', (answer) => {
|
|
1248
|
+
rl.close();
|
|
1249
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
if (!confirmed) {
|
|
1254
|
+
console.log('Cancelled.');
|
|
1255
|
+
return { success: false, message: 'Force cleanup cancelled by user' };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Update work item status to cancelled
|
|
1259
|
+
await new Promise((resolve, reject) => {
|
|
1260
|
+
db.run(
|
|
1261
|
+
`UPDATE work_items SET status = 'cancelled' WHERE id = ?`,
|
|
1262
|
+
[workItemId],
|
|
1263
|
+
(err) => {
|
|
1264
|
+
if (err) return reject(err);
|
|
1265
|
+
resolve();
|
|
1266
|
+
}
|
|
1267
|
+
);
|
|
1268
|
+
});
|
|
1269
|
+
console.log(`📋 Work item #${workItemId} marked as cancelled`);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1193
1272
|
console.log(`Cleaning up worktree for #${workItemId}: ${worktree.title}`);
|
|
1194
1273
|
|
|
1195
1274
|
// Remove git worktree if it exists
|
|
@@ -1561,6 +1640,8 @@ async function mergeWork(options = {}) {
|
|
|
1561
1640
|
// and should never block branch switching
|
|
1562
1641
|
const generatedFilesForReset = [
|
|
1563
1642
|
'cucumber-results.json',
|
|
1643
|
+
'cucumber-results.ndjson',
|
|
1644
|
+
'@rerun.txt',
|
|
1564
1645
|
'package-lock.json',
|
|
1565
1646
|
'yarn.lock',
|
|
1566
1647
|
'pnpm-lock.yaml'
|
|
@@ -1700,6 +1781,8 @@ async function mergeWork(options = {}) {
|
|
|
1700
1781
|
// Check if all conflicts are in generated files that can be auto-resolved
|
|
1701
1782
|
const generatedFiles = [
|
|
1702
1783
|
'cucumber-results.json',
|
|
1784
|
+
'cucumber-results.ndjson',
|
|
1785
|
+
'@rerun.txt',
|
|
1703
1786
|
'package-lock.json',
|
|
1704
1787
|
'yarn.lock',
|
|
1705
1788
|
'pnpm-lock.yaml'
|
|
@@ -1902,20 +1985,50 @@ async function mergeWork(options = {}) {
|
|
|
1902
1985
|
`Test worktrees (tests/* branches) merge BDD scenarios without completing the feature.`
|
|
1903
1986
|
));
|
|
1904
1987
|
} else {
|
|
1905
|
-
// Chore or bug worktree
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
[completedAt, currentWork.id],
|
|
1912
|
-
(err) => {
|
|
1988
|
+
// Chore or bug worktree
|
|
1989
|
+
// Check if this is a standalone kanban-visible item (no parent or parent is epic)
|
|
1990
|
+
let isStandalone = !currentWork.parent_id;
|
|
1991
|
+
if (!isStandalone && currentWork.parent_id) {
|
|
1992
|
+
const parentItem = await new Promise((resolve, reject) => {
|
|
1993
|
+
db.get('SELECT type FROM work_items WHERE id = ?', [currentWork.parent_id], (err, row) => {
|
|
1913
1994
|
if (err) return reject(err);
|
|
1914
|
-
resolve();
|
|
1915
|
-
}
|
|
1916
|
-
);
|
|
1917
|
-
|
|
1918
|
-
|
|
1995
|
+
resolve(row);
|
|
1996
|
+
});
|
|
1997
|
+
});
|
|
1998
|
+
isStandalone = parentItem && parentItem.type === 'epic';
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (isStandalone) {
|
|
2002
|
+
// Standalone item: set ready_for_review instead of marking done
|
|
2003
|
+
// Keep status as in_progress so it stays in the In Flight column
|
|
2004
|
+
// User accepts/rejects via the kanban board
|
|
2005
|
+
await new Promise((resolve, reject) => {
|
|
2006
|
+
db.run(
|
|
2007
|
+
`UPDATE work_items SET ready_for_review = 1 WHERE id = ?`,
|
|
2008
|
+
[currentWork.id],
|
|
2009
|
+
(err) => {
|
|
2010
|
+
if (err) return reject(err);
|
|
2011
|
+
resolve();
|
|
2012
|
+
}
|
|
2013
|
+
);
|
|
2014
|
+
});
|
|
2015
|
+
console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} ready for review`);
|
|
2016
|
+
} else {
|
|
2017
|
+
// Chore/bug under a feature: mark as done (feature handles the accept flow)
|
|
2018
|
+
console.log(`Marking ${currentWork.type} as done...`);
|
|
2019
|
+
const completedAt = new Date().toISOString();
|
|
2020
|
+
await new Promise((resolve, reject) => {
|
|
2021
|
+
db.run(
|
|
2022
|
+
`UPDATE work_items SET status = 'done', completed_at = ? WHERE id = ?`,
|
|
2023
|
+
[completedAt, currentWork.id],
|
|
2024
|
+
(err) => {
|
|
2025
|
+
if (err) return reject(err);
|
|
2026
|
+
resolve();
|
|
2027
|
+
}
|
|
2028
|
+
);
|
|
2029
|
+
});
|
|
2030
|
+
console.log(`✅ ${currentWork.type.charAt(0).toUpperCase() + currentWork.type.slice(1)} #${currentWork.id} marked as done`);
|
|
2031
|
+
}
|
|
1919
2032
|
}
|
|
1920
2033
|
|
|
1921
2034
|
// Mark worktree as merged but DON'T delete it yet
|
|
@@ -256,6 +256,22 @@ function create(type, title, description = '', parentId = null, mode = null, nee
|
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
function continueWithValidatedParent(epicId) {
|
|
259
|
+
// Read current plan from auth.json for usage tracking
|
|
260
|
+
let planAtCreation = null;
|
|
261
|
+
try {
|
|
262
|
+
const os = require('os');
|
|
263
|
+
const path = require('path');
|
|
264
|
+
const fs = require('fs');
|
|
265
|
+
const authPath = path.join(os.homedir(), 'Library', 'Application Support', 'jettypod', 'auth.json');
|
|
266
|
+
if (fs.existsSync(authPath)) {
|
|
267
|
+
const authData = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
268
|
+
planAtCreation = authData?.user?.plan || 'free';
|
|
269
|
+
} else {
|
|
270
|
+
planAtCreation = 'free';
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
planAtCreation = 'free';
|
|
274
|
+
}
|
|
259
275
|
|
|
260
276
|
// Only features have modes - they start with mode=NULL (no mode until implementation starts)
|
|
261
277
|
// Epics, chores, and bugs don't have modes (always NULL)
|
|
@@ -282,8 +298,8 @@ function create(type, title, description = '', parentId = null, mode = null, nee
|
|
|
282
298
|
// Set phase for features (discovery when mode=NULL, implementation when mode is set, NULL for everything else)
|
|
283
299
|
const phase = type === 'feature' ? (mode ? 'implementation' : 'discovery') : null;
|
|
284
300
|
|
|
285
|
-
const sql = `INSERT INTO work_items (type, title, description, parent_id, epic_id, mode, needs_discovery, phase, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
286
|
-
db.run(sql, [type, title, description, parentId, epicId, mode, needsDiscovery ? 1 : 0, phase, 'backlog'], function(err) {
|
|
301
|
+
const sql = `INSERT INTO work_items (type, title, description, parent_id, epic_id, mode, needs_discovery, phase, status, plan_at_creation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
302
|
+
db.run(sql, [type, title, description, parentId, epicId, mode, needsDiscovery ? 1 : 0, phase, 'backlog', planAtCreation], function(err) {
|
|
287
303
|
if (err) {
|
|
288
304
|
return reject(err);
|
|
289
305
|
}
|
|
@@ -776,31 +792,19 @@ function updateStatus(id, status) {
|
|
|
776
792
|
return resolve();
|
|
777
793
|
}
|
|
778
794
|
|
|
779
|
-
//
|
|
795
|
+
// Items under epics: set ready_for_review and keep in_progress for accept/reject
|
|
780
796
|
if (parent.type === 'epic') {
|
|
781
|
-
//
|
|
782
|
-
db.
|
|
783
|
-
'
|
|
784
|
-
[
|
|
785
|
-
(err
|
|
797
|
+
// Set ready_for_review on the item and revert to in_progress
|
|
798
|
+
db.run(
|
|
799
|
+
'UPDATE work_items SET ready_for_review = 1, status = ?, completed_at = NULL WHERE id = ?',
|
|
800
|
+
['in_progress', item.id],
|
|
801
|
+
(err) => {
|
|
786
802
|
if (err) {
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const allDone = children.every(child => child.status === 'done');
|
|
791
|
-
if (allDone) {
|
|
792
|
-
const epicCompletedAt = new Date().toISOString();
|
|
793
|
-
db.run('UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?', ['done', epicCompletedAt, parent.id], (err) => {
|
|
794
|
-
if (err) {
|
|
795
|
-
console.error(`Failed to auto-close epic: ${err.message}`);
|
|
796
|
-
} else {
|
|
797
|
-
console.log(`✓ Epic #${parent.id} also completed (all children done)`);
|
|
798
|
-
}
|
|
799
|
-
resolve();
|
|
800
|
-
});
|
|
803
|
+
console.error(`Failed to set ready_for_review: ${err.message}`);
|
|
801
804
|
} else {
|
|
802
|
-
|
|
805
|
+
console.log(`✓ Work item #${item.id} ready for review`);
|
|
803
806
|
}
|
|
807
|
+
resolve();
|
|
804
808
|
}
|
|
805
809
|
);
|
|
806
810
|
}
|
|
@@ -838,12 +842,12 @@ function updateStatus(id, status) {
|
|
|
838
842
|
}
|
|
839
843
|
|
|
840
844
|
if (featureComplete) {
|
|
841
|
-
|
|
842
|
-
db.run('UPDATE work_items SET
|
|
845
|
+
// Set ready_for_review instead of auto-closing — let user accept/reject
|
|
846
|
+
db.run('UPDATE work_items SET ready_for_review = 1 WHERE id = ?', [parent.id], (err) => {
|
|
843
847
|
if (err) {
|
|
844
|
-
console.error(`Failed to
|
|
848
|
+
console.error(`Failed to set ready_for_review: ${err.message}`);
|
|
845
849
|
} else {
|
|
846
|
-
console.log(`✓ Feature #${parent.id}
|
|
850
|
+
console.log(`✓ Feature #${parent.id} ready for review (all ${featureMode} mode chores done)`);
|
|
847
851
|
}
|
|
848
852
|
resolve();
|
|
849
853
|
});
|
|
@@ -860,6 +864,21 @@ function updateStatus(id, status) {
|
|
|
860
864
|
resolve();
|
|
861
865
|
}
|
|
862
866
|
});
|
|
867
|
+
} else if (status === 'done' && !item.parent_id) {
|
|
868
|
+
// Standalone item (no parent) marked done via CLI
|
|
869
|
+
// Set ready_for_review and revert to in_progress so user can accept/reject via kanban
|
|
870
|
+
db.run(
|
|
871
|
+
'UPDATE work_items SET ready_for_review = 1, status = ?, completed_at = NULL WHERE id = ?',
|
|
872
|
+
['in_progress', item.id],
|
|
873
|
+
(err) => {
|
|
874
|
+
if (err) {
|
|
875
|
+
console.error(`Failed to set ready_for_review: ${err.message}`);
|
|
876
|
+
} else {
|
|
877
|
+
console.log(`✓ Work item #${item.id} ready for review`);
|
|
878
|
+
}
|
|
879
|
+
resolve();
|
|
880
|
+
}
|
|
881
|
+
);
|
|
863
882
|
} else {
|
|
864
883
|
resolve();
|
|
865
884
|
}
|
|
@@ -1165,25 +1184,46 @@ async function main() {
|
|
|
1165
1184
|
|
|
1166
1185
|
switch(command) {
|
|
1167
1186
|
case 'create': {
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1187
|
+
// Support --from=<file> for truncation-safe creation via JSON file
|
|
1188
|
+
const fromArg = args.find(a => a.startsWith('--from='));
|
|
1189
|
+
let type, title, desc, parentId, mode, needsDiscovery;
|
|
1171
1190
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1191
|
+
if (fromArg) {
|
|
1192
|
+
const fs = require('fs');
|
|
1193
|
+
const filePath = fromArg.split('=').slice(1).join('=');
|
|
1194
|
+
try {
|
|
1195
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
1196
|
+
const data = JSON.parse(raw);
|
|
1197
|
+
type = data.type;
|
|
1198
|
+
title = data.title;
|
|
1199
|
+
desc = data.description || '';
|
|
1200
|
+
parentId = data.parent ? parseInt(data.parent) : null;
|
|
1201
|
+
mode = data.mode || null;
|
|
1202
|
+
needsDiscovery = !!data.needsDiscovery;
|
|
1203
|
+
} catch (e) {
|
|
1204
|
+
console.error(`Error reading --from file: ${e.message}`);
|
|
1205
|
+
process.exit(1);
|
|
1185
1206
|
}
|
|
1186
|
-
}
|
|
1207
|
+
} else {
|
|
1208
|
+
type = args[0];
|
|
1209
|
+
title = args[1];
|
|
1210
|
+
desc = args[2] || '';
|
|
1211
|
+
parentId = null;
|
|
1212
|
+
mode = null;
|
|
1213
|
+
needsDiscovery = false;
|
|
1214
|
+
|
|
1215
|
+
args.forEach(arg => {
|
|
1216
|
+
if (arg.startsWith('--parent=')) {
|
|
1217
|
+
parentId = parseInt(arg.split('=')[1]);
|
|
1218
|
+
}
|
|
1219
|
+
if (arg.startsWith('--mode=')) {
|
|
1220
|
+
mode = arg.split('=')[1];
|
|
1221
|
+
}
|
|
1222
|
+
if (arg === '--needs-discovery') {
|
|
1223
|
+
needsDiscovery = true;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1187
1227
|
|
|
1188
1228
|
try {
|
|
1189
1229
|
const newId = await create(type, title, desc, parentId, mode, needsDiscovery);
|
|
@@ -1258,7 +1298,7 @@ async function main() {
|
|
|
1258
1298
|
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
1259
1299
|
LEFT JOIN work_items e ON w.epic_id = e.id
|
|
1260
1300
|
WHERE w.status = 'in_progress'
|
|
1261
|
-
ORDER BY w.id ASC
|
|
1301
|
+
ORDER BY w.ready_for_review DESC, w.id ASC
|
|
1262
1302
|
`, [], (err, rows) => {
|
|
1263
1303
|
if (err) return reject(err);
|
|
1264
1304
|
resolve(rows || []);
|
|
@@ -1502,7 +1542,7 @@ async function main() {
|
|
|
1502
1542
|
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
1503
1543
|
LEFT JOIN work_items e ON w.epic_id = e.id
|
|
1504
1544
|
WHERE w.status = 'in_progress'
|
|
1505
|
-
ORDER BY w.id ASC
|
|
1545
|
+
ORDER BY w.ready_for_review DESC, w.id ASC
|
|
1506
1546
|
`, [], (err, rows) => {
|
|
1507
1547
|
if (err) return reject(err);
|
|
1508
1548
|
resolve(rows || []);
|
|
@@ -99,7 +99,7 @@ async function runDiagnostics(db, repoPath, options = {}) {
|
|
|
99
99
|
count: reconcileResults.staleDbEntries.length,
|
|
100
100
|
message: `Found ${reconcileResults.staleDbEntries.length} database entries with missing filesystem directories`,
|
|
101
101
|
details: reconcileResults.staleDbEntries,
|
|
102
|
-
recommendation: 'Run reconciliation with cleanup=true to mark as
|
|
102
|
+
recommendation: 'Run reconciliation with cleanup=true to mark as cleaned'
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -192,23 +192,23 @@ async function runDiagnostics(db, repoPath, options = {}) {
|
|
|
192
192
|
async function checkDatabaseIntegrity(db) {
|
|
193
193
|
const issues = [];
|
|
194
194
|
|
|
195
|
-
// Check for
|
|
196
|
-
const
|
|
197
|
-
db.get('SELECT COUNT(*) as count FROM worktrees WHERE status = ?', ['
|
|
195
|
+
// Check for cleaned worktrees (terminal state — informational only)
|
|
196
|
+
const cleanedCount = await new Promise((resolve, reject) => {
|
|
197
|
+
db.get('SELECT COUNT(*) as count FROM worktrees WHERE status = ?', ['cleaned'], (err, row) => {
|
|
198
198
|
if (err) reject(err);
|
|
199
199
|
else resolve(row.count);
|
|
200
200
|
});
|
|
201
201
|
});
|
|
202
202
|
|
|
203
|
-
if (
|
|
203
|
+
if (cleanedCount > 0) {
|
|
204
204
|
issues.push({
|
|
205
205
|
severity: SEVERITY.INFO,
|
|
206
206
|
category: 'database',
|
|
207
|
-
type: '
|
|
208
|
-
count:
|
|
209
|
-
message: `Found ${
|
|
210
|
-
details: { count:
|
|
211
|
-
recommendation: 'These
|
|
207
|
+
type: 'cleaned_worktrees',
|
|
208
|
+
count: cleanedCount,
|
|
209
|
+
message: `Found ${cleanedCount} cleaned worktree(s) in database`,
|
|
210
|
+
details: { count: cleanedCount },
|
|
211
|
+
recommendation: 'These are historical records — no action needed'
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -232,7 +232,7 @@ async function checkDatabaseIntegrity(db) {
|
|
|
232
232
|
count: orphanedWorktrees.length,
|
|
233
233
|
message: `Found ${orphanedWorktrees.length} worktree(s) with missing work items`,
|
|
234
234
|
details: orphanedWorktrees,
|
|
235
|
-
recommendation: 'Mark these worktrees as
|
|
235
|
+
recommendation: 'Mark these worktrees as cleaned'
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -265,7 +265,7 @@ async function checkFilesystemHealth(db, repoPath) {
|
|
|
265
265
|
type: 'missing_worktree_directory',
|
|
266
266
|
message: `Active worktree directory missing: ${worktreePath}`,
|
|
267
267
|
details: { worktree },
|
|
268
|
-
recommendation: 'Mark worktree as
|
|
268
|
+
recommendation: 'Mark worktree as cleaned and recreate if needed'
|
|
269
269
|
});
|
|
270
270
|
continue;
|
|
271
271
|
}
|
|
@@ -293,7 +293,7 @@ async function checkFilesystemHealth(db, repoPath) {
|
|
|
293
293
|
type: 'permission_issue',
|
|
294
294
|
message: `Cannot read/write worktree directory: ${worktreePath}`,
|
|
295
295
|
details: { worktree, error: err.message },
|
|
296
|
-
recommendation: 'Fix directory permissions or mark worktree as
|
|
296
|
+
recommendation: 'Fix directory permissions or mark worktree as cleaned'
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
}
|
|
@@ -344,11 +344,11 @@ function generateRecommendations(report) {
|
|
|
344
344
|
});
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
if (report.issues.some(i => i.type === '
|
|
347
|
+
if (report.issues.some(i => i.type === 'cleaned_worktrees')) {
|
|
348
348
|
recommendations.push({
|
|
349
349
|
priority: 'low',
|
|
350
|
-
action: '
|
|
351
|
-
reason: '
|
|
350
|
+
action: 'Cleaned worktree entries are historical records',
|
|
351
|
+
reason: 'No action needed — these are part of the audit trail'
|
|
352
352
|
});
|
|
353
353
|
}
|
|
354
354
|
|
package/lib/worktree-facade.js
CHANGED
|
@@ -117,7 +117,7 @@ async function startWork(workItem, options = {}) {
|
|
|
117
117
|
/**
|
|
118
118
|
* Stop work on a work item with graceful degradation
|
|
119
119
|
*
|
|
120
|
-
* Attempts to cleanup worktree. If that fails, marks as
|
|
120
|
+
* Attempts to cleanup worktree. If that fails, marks as cleaned and continues.
|
|
121
121
|
*
|
|
122
122
|
* @param {number} worktreeId - Worktree ID (can be null if working in main)
|
|
123
123
|
* @param {Object} options - Optional configuration
|