jettypod 4.4.37 → 4.4.39
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/lib/database.js +67 -21
- package/lib/merge-lock.js +17 -32
- package/lib/preflight-validator.js +66 -0
- package/lib/production-standards-reader.js +3 -3
- package/lib/work-commands/index.js +13 -1
- package/lib/work-tracking/index.js +131 -80
- package/package.json +1 -1
package/lib/database.js
CHANGED
|
@@ -100,10 +100,16 @@ function getDb() {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
// Enable WAL mode for better concurrency and avoid locking issues
|
|
103
|
-
db.run('PRAGMA journal_mode = WAL')
|
|
104
|
-
|
|
103
|
+
db.run('PRAGMA journal_mode = WAL', (err) => {
|
|
104
|
+
if (err) console.warn('PRAGMA journal_mode failed:', err.message);
|
|
105
|
+
});
|
|
106
|
+
db.run('PRAGMA synchronous = NORMAL', (err) => {
|
|
107
|
+
if (err) console.warn('PRAGMA synchronous failed:', err.message);
|
|
108
|
+
});
|
|
105
109
|
// Reduce busy timeout to fail faster in tests
|
|
106
|
-
db.run('PRAGMA busy_timeout = 5000')
|
|
110
|
+
db.run('PRAGMA busy_timeout = 5000', (err) => {
|
|
111
|
+
if (err) console.warn('PRAGMA busy_timeout failed:', err.message);
|
|
112
|
+
});
|
|
107
113
|
|
|
108
114
|
initSchema();
|
|
109
115
|
} catch (err) {
|
|
@@ -141,6 +147,7 @@ function initSchema() {
|
|
|
141
147
|
phase TEXT,
|
|
142
148
|
prototype_files TEXT,
|
|
143
149
|
discovery_winner TEXT,
|
|
150
|
+
discovery_rationale TEXT,
|
|
144
151
|
scenario_file TEXT,
|
|
145
152
|
completed_at TEXT,
|
|
146
153
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
@@ -196,29 +203,30 @@ function initSchema() {
|
|
|
196
203
|
}
|
|
197
204
|
});
|
|
198
205
|
|
|
199
|
-
// Migrations: Add columns if they don't exist
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
db.run(`ALTER TABLE work_items ADD COLUMN
|
|
206
|
-
db.run(`ALTER TABLE work_items ADD COLUMN
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
db.run(`ALTER TABLE work_items ADD COLUMN
|
|
213
|
-
db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async () => {
|
|
206
|
+
// Migrations: Add columns if they don't exist (for databases created before these columns were in base schema)
|
|
207
|
+
const handleAlterError = (columnName) => (err) => {
|
|
208
|
+
if (err && !err.message.includes('duplicate column')) {
|
|
209
|
+
console.warn(`ALTER TABLE ADD COLUMN ${columnName} failed:`, err.message);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
db.run(`ALTER TABLE work_items ADD COLUMN branch_name TEXT`, handleAlterError('branch_name'));
|
|
213
|
+
db.run(`ALTER TABLE work_items ADD COLUMN file_paths TEXT`, handleAlterError('file_paths'));
|
|
214
|
+
db.run(`ALTER TABLE work_items ADD COLUMN commit_sha TEXT`, handleAlterError('commit_sha'));
|
|
215
|
+
db.run(`ALTER TABLE work_items ADD COLUMN mode TEXT`, handleAlterError('mode'));
|
|
216
|
+
db.run(`ALTER TABLE work_items ADD COLUMN current INTEGER DEFAULT 0`, handleAlterError('current'));
|
|
217
|
+
db.run(`ALTER TABLE work_items ADD COLUMN needs_discovery INTEGER DEFAULT 0`, handleAlterError('needs_discovery'));
|
|
218
|
+
db.run(`ALTER TABLE work_items ADD COLUMN worktree_path TEXT`, handleAlterError('worktree_path'));
|
|
219
|
+
db.run(`ALTER TABLE work_items ADD COLUMN discovery_rationale TEXT`, handleAlterError('discovery_rationale'));
|
|
220
|
+
db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async (err) => {
|
|
221
|
+
handleAlterError('architectural_decision')(err);
|
|
214
222
|
// Run data migrations after all schema operations complete (skip in test environments)
|
|
215
223
|
if (process.env.NODE_ENV !== 'test') {
|
|
216
224
|
try {
|
|
217
225
|
await runMigrations(db);
|
|
218
226
|
resolve();
|
|
219
227
|
} catch (err) {
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
console.error('Migration failed:', err.message);
|
|
229
|
+
reject(err);
|
|
222
230
|
}
|
|
223
231
|
} else {
|
|
224
232
|
resolve();
|
|
@@ -228,11 +236,48 @@ function initSchema() {
|
|
|
228
236
|
});
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Validate that all expected columns exist in work_items table
|
|
241
|
+
* @param {sqlite3.Database} database - Database connection to validate
|
|
242
|
+
* @returns {Promise<void>} Resolves if valid, rejects with missing columns
|
|
243
|
+
*/
|
|
244
|
+
function validateSchema(database) {
|
|
245
|
+
const expectedColumns = [
|
|
246
|
+
'id', 'type', 'title', 'description', 'status', 'parent_id', 'epic_id',
|
|
247
|
+
'branch_name', 'file_paths', 'commit_sha', 'mode', 'current', 'phase',
|
|
248
|
+
'prototype_files', 'discovery_winner', 'discovery_rationale', 'scenario_file',
|
|
249
|
+
'completed_at', 'created_at', 'needs_discovery', 'worktree_path', 'architectural_decision'
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
database.all(`PRAGMA table_info(work_items)`, [], (err, rows) => {
|
|
254
|
+
if (err) {
|
|
255
|
+
return reject(new Error(`Failed to get schema info: ${err.message}`));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const existingColumns = rows.map(r => r.name);
|
|
259
|
+
const missingColumns = expectedColumns.filter(col => !existingColumns.includes(col));
|
|
260
|
+
|
|
261
|
+
if (missingColumns.length > 0) {
|
|
262
|
+
console.error('Missing columns in work_items table:', missingColumns.join(', '));
|
|
263
|
+
return reject(new Error(`Schema validation failed: missing columns: ${missingColumns.join(', ')}`));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
resolve();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
231
271
|
/**
|
|
232
272
|
* Close database connection
|
|
233
273
|
* @returns {Promise<void>} Resolves when database is closed
|
|
234
274
|
*/
|
|
235
|
-
function closeDb() {
|
|
275
|
+
async function closeDb() {
|
|
276
|
+
// Wait for any pending migrations before closing
|
|
277
|
+
if (migrationPromise) {
|
|
278
|
+
try { await migrationPromise; } catch { /* ignore migration errors on close */ }
|
|
279
|
+
}
|
|
280
|
+
|
|
236
281
|
return new Promise((resolve) => {
|
|
237
282
|
// Guard against concurrent close attempts
|
|
238
283
|
if (isClosing || !db) {
|
|
@@ -292,6 +337,7 @@ module.exports = {
|
|
|
292
337
|
getDbPath,
|
|
293
338
|
getJettypodDir,
|
|
294
339
|
waitForMigrations,
|
|
340
|
+
validateSchema,
|
|
295
341
|
dbPath, // Deprecated: use getDbPath() for dynamic path
|
|
296
342
|
jettypodDir // Deprecated: use getJettypodDir() for dynamic path
|
|
297
343
|
};
|
package/lib/merge-lock.js
CHANGED
|
@@ -115,18 +115,30 @@ function insertLock(db, workItemId, lockedBy) {
|
|
|
115
115
|
* Clean up stale locks that are older than the threshold
|
|
116
116
|
*
|
|
117
117
|
* @param {Object} db - SQLite database connection
|
|
118
|
-
* @param {number}
|
|
119
|
-
* @returns {Promise<
|
|
118
|
+
* @param {number|Object} staleThresholdOrOptions - Age threshold in milliseconds (default: 120000), or options object with staleThreshold
|
|
119
|
+
* @returns {Promise<number>} Count of locks removed
|
|
120
120
|
*/
|
|
121
|
-
function cleanupStaleLocks(db,
|
|
121
|
+
function cleanupStaleLocks(db, staleThresholdOrOptions = 120000) {
|
|
122
|
+
if (!db) {
|
|
123
|
+
return Promise.reject(new Error('Database connection required'));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Support both number (old API) and options object (new API)
|
|
127
|
+
let staleThresholdMs;
|
|
128
|
+
if (typeof staleThresholdOrOptions === 'object') {
|
|
129
|
+
staleThresholdMs = staleThresholdOrOptions.staleThreshold || 120000;
|
|
130
|
+
} else {
|
|
131
|
+
staleThresholdMs = staleThresholdOrOptions;
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
return new Promise((resolve, reject) => {
|
|
123
135
|
db.run(
|
|
124
136
|
`DELETE FROM merge_locks
|
|
125
137
|
WHERE (julianday('now') - julianday(locked_at)) * 86400000 > ?`,
|
|
126
138
|
[staleThresholdMs],
|
|
127
|
-
(err)
|
|
139
|
+
function(err) {
|
|
128
140
|
if (err) return reject(err);
|
|
129
|
-
resolve();
|
|
141
|
+
resolve(this.changes); // Return count of deleted locks
|
|
130
142
|
}
|
|
131
143
|
);
|
|
132
144
|
});
|
|
@@ -179,33 +191,6 @@ function createLockHandle(db, lockId, lockedBy, workItemId) {
|
|
|
179
191
|
};
|
|
180
192
|
}
|
|
181
193
|
|
|
182
|
-
/**
|
|
183
|
-
* Clean up stale locks older than threshold
|
|
184
|
-
*
|
|
185
|
-
* @param {Object} db - SQLite database connection
|
|
186
|
-
* @param {Object} options - Optional configuration
|
|
187
|
-
* @param {number} options.staleThreshold - Age threshold in ms (default: 300000 = 5 minutes)
|
|
188
|
-
* @returns {Promise<number>} Count of locks removed
|
|
189
|
-
*/
|
|
190
|
-
async function cleanupStaleLocks(db, options = {}) {
|
|
191
|
-
if (!db) {
|
|
192
|
-
throw new Error('Database connection required');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const staleThreshold = options.staleThreshold || 300000; // 5 minutes default
|
|
196
|
-
const staleTime = new Date(Date.now() - staleThreshold).toISOString();
|
|
197
|
-
|
|
198
|
-
return new Promise((resolve, reject) => {
|
|
199
|
-
db.run(
|
|
200
|
-
'DELETE FROM merge_locks WHERE locked_at < ?',
|
|
201
|
-
[staleTime],
|
|
202
|
-
function(err) {
|
|
203
|
-
if (err) reject(err);
|
|
204
|
-
else resolve(this.changes); // Return count of deleted locks
|
|
205
|
-
}
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
194
|
|
|
210
195
|
/**
|
|
211
196
|
* Sleep for specified milliseconds
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Preflight Validator - validates prerequisites before workflow transitions
|
|
2
|
+
// Speed mode implementation - checks scenario_file and chores exist
|
|
3
|
+
|
|
4
|
+
const { getDb } = require('./database');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate feature prerequisites before transitioning to implementation
|
|
8
|
+
* @param {number} featureId - Feature ID to validate
|
|
9
|
+
* @returns {Promise<{success: boolean, failures: Array<{check: string, remediation: string}>}>}
|
|
10
|
+
*/
|
|
11
|
+
function validateFeatureToImplement(featureId) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const db = getDb();
|
|
14
|
+
const failures = [];
|
|
15
|
+
|
|
16
|
+
// Get feature details
|
|
17
|
+
db.get(
|
|
18
|
+
'SELECT id, title, type, phase, scenario_file FROM work_items WHERE id = ?',
|
|
19
|
+
[featureId],
|
|
20
|
+
(err, feature) => {
|
|
21
|
+
if (err) {
|
|
22
|
+
return reject(new Error(`Failed to get feature: ${err.message}`));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!feature) {
|
|
26
|
+
return reject(new Error(`Feature #${featureId} not found`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check 1: scenario_file must be set
|
|
30
|
+
if (!feature.scenario_file) {
|
|
31
|
+
failures.push({
|
|
32
|
+
check: 'scenario_file',
|
|
33
|
+
remediation: 'Use the feature-planning skill to generate BDD scenarios first.\n Invoke skill: feature-planning'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check 2: chores must exist
|
|
38
|
+
db.all(
|
|
39
|
+
'SELECT id, title FROM work_items WHERE parent_id = ? AND type = ?',
|
|
40
|
+
[featureId, 'chore'],
|
|
41
|
+
(err, chores) => {
|
|
42
|
+
if (err) {
|
|
43
|
+
return reject(new Error(`Failed to get chores: ${err.message}`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!chores || chores.length === 0) {
|
|
47
|
+
failures.push({
|
|
48
|
+
check: 'chores_exist',
|
|
49
|
+
remediation: 'Complete feature planning to generate speed mode chores.\n Invoke skill: feature-planning'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resolve({
|
|
54
|
+
success: failures.length === 0,
|
|
55
|
+
failures: failures
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
validateFeatureToImplement
|
|
66
|
+
};
|
|
@@ -11,7 +11,7 @@ async function readStandards() {
|
|
|
11
11
|
|
|
12
12
|
// Check file exists
|
|
13
13
|
if (!fs.existsSync(standardsPath)) {
|
|
14
|
-
throw new Error('Production standards not found');
|
|
14
|
+
throw new Error('Production standards file not found');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Read file with specific error handling
|
|
@@ -59,7 +59,7 @@ async function readStandards() {
|
|
|
59
59
|
function validateStandardsFormat(standards) {
|
|
60
60
|
// Validate standards object exists
|
|
61
61
|
if (!standards) {
|
|
62
|
-
throw new Error('
|
|
62
|
+
throw new Error('Standards object is null or undefined');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Validate required top-level fields
|
|
@@ -98,7 +98,7 @@ function validateStandardsFormat(standards) {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
if (!standard.acceptance) {
|
|
101
|
-
throw new Error(`
|
|
101
|
+
throw new Error(`Standard ${standard.id} missing required field: acceptance`);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Validate infrastructure-scoped standards have all required fields
|
|
@@ -55,7 +55,7 @@ async function startWork(id) {
|
|
|
55
55
|
// Get work item
|
|
56
56
|
db.get(`
|
|
57
57
|
SELECT w.*,
|
|
58
|
-
p.title as parent_title, p.id as parent_id, p.scenario_file as parent_scenario_file, p.mode as parent_mode,
|
|
58
|
+
p.title as parent_title, p.id as parent_id, p.scenario_file as parent_scenario_file, p.mode as parent_mode, p.phase as parent_phase, p.type as parent_type,
|
|
59
59
|
e.title as epic_title, e.id as epic_id
|
|
60
60
|
FROM work_items w
|
|
61
61
|
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
@@ -139,6 +139,18 @@ async function startWork(id) {
|
|
|
139
139
|
return reject(new Error(`Cannot start epic #${id} directly.\n\nTo plan this epic's features, use the epic-planning skill:\n Invoke skill: epic-planning\n\nOr start an existing feature/chore:\n jettypod backlog # to see available work items`));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Validate chores have their parent feature in implementation phase
|
|
143
|
+
if (workItem.type === 'chore' && workItem.parent_type === 'feature' && workItem.parent_phase !== 'implementation') {
|
|
144
|
+
return reject(new Error(
|
|
145
|
+
`Cannot start chore - feature not in implementation phase\n\n` +
|
|
146
|
+
`Chore #${id}: ${workItem.title}\n` +
|
|
147
|
+
`Parent Feature #${workItem.parent_id}: ${workItem.parent_title}\n` +
|
|
148
|
+
` Phase: ${workItem.parent_phase || 'discovery'}\n\n` +
|
|
149
|
+
`To transition the feature to implementation phase:\n` +
|
|
150
|
+
` jettypod work implement ${workItem.parent_id} --winner="..." --rationale="..."`
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
// Update status to in_progress if currently todo
|
|
143
155
|
const finalStatus = workItem.status === 'todo' ? 'in_progress' : workItem.status;
|
|
144
156
|
|
|
@@ -76,8 +76,8 @@ function create(type, title, description = '', parentId = null, mode = null, nee
|
|
|
76
76
|
return continueWithValidatedParent(null);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// Check parent's mode and
|
|
80
|
-
db.get('SELECT id, title, type, mode, phase, epic_id FROM work_items WHERE id = ?', [parentId], (err, parent) => {
|
|
79
|
+
// Check parent's mode, phase, and scenario_file
|
|
80
|
+
db.get('SELECT id, title, type, mode, phase, scenario_file, epic_id FROM work_items WHERE id = ?', [parentId], (err, parent) => {
|
|
81
81
|
if (err) {
|
|
82
82
|
return reject(new Error(`Failed to validate parent: ${err.message}`));
|
|
83
83
|
}
|
|
@@ -90,19 +90,19 @@ function create(type, title, description = '', parentId = null, mode = null, nee
|
|
|
90
90
|
return reject(new Error(`Chores can only be created under features or epics. Parent #${parentId} is a ${parent.type}`));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// For chores under features, validate
|
|
94
|
-
//
|
|
95
|
-
|
|
93
|
+
// For chores under features, validate prerequisites exist
|
|
94
|
+
// Allow chores in discovery phase if scenario_file is set (feature-planning creates chores before work implement)
|
|
95
|
+
// Otherwise require mode to be set
|
|
96
|
+
if (parent.type === 'feature' && !parent.mode && !parent.scenario_file) {
|
|
96
97
|
return reject(new Error(
|
|
97
|
-
`Cannot create chore for feature without
|
|
98
|
+
`Cannot create chore for feature without BDD scenarios.\n\n` +
|
|
98
99
|
`Feature #${parent.id} "${parent.title}"\n` +
|
|
99
100
|
` Phase: ${parent.phase || 'not set'}\n` +
|
|
100
101
|
` Mode: ${parent.mode || 'not set'}\n` +
|
|
101
102
|
` Scenario file: ${parent.scenario_file || 'not set'}\n\n` +
|
|
102
|
-
`Chores can only be created after
|
|
103
|
-
`
|
|
104
|
-
`
|
|
105
|
-
` jettypod work implement ${parent.id} --scenario-file="features/[feature].feature" --winner="..." --rationale="..."`
|
|
103
|
+
`Chores can only be created after BDD scenarios are generated.\n\n` +
|
|
104
|
+
`Use the feature-planning skill to generate scenarios and chores:\n` +
|
|
105
|
+
` Invoke skill: feature-planning`
|
|
106
106
|
));
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -2006,6 +2006,9 @@ async function main() {
|
|
|
2006
2006
|
}
|
|
2007
2007
|
|
|
2008
2008
|
case 'implement': {
|
|
2009
|
+
// Ensure migrations are complete before accessing database
|
|
2010
|
+
await waitForMigrations();
|
|
2011
|
+
|
|
2009
2012
|
const featureId = parseInt(args[0]);
|
|
2010
2013
|
|
|
2011
2014
|
if (!featureId || isNaN(featureId)) {
|
|
@@ -2057,14 +2060,15 @@ async function main() {
|
|
|
2057
2060
|
|
|
2058
2061
|
// Validate that BDD scenarios exist (either from flag or database)
|
|
2059
2062
|
if (!scenarioFile) {
|
|
2060
|
-
console.error(`Error:
|
|
2063
|
+
console.error(`Error: Cannot implement - no BDD scenarios`);
|
|
2061
2064
|
console.log('');
|
|
2062
2065
|
console.log('Discovery is not complete without BDD scenarios.');
|
|
2063
2066
|
console.log('');
|
|
2064
|
-
console.log('
|
|
2065
|
-
console.log(
|
|
2067
|
+
console.log('Use the feature-planning skill to generate BDD scenarios:');
|
|
2068
|
+
console.log(' Invoke skill: feature-planning');
|
|
2066
2069
|
console.log('');
|
|
2067
|
-
console.log('Or
|
|
2070
|
+
console.log('Or provide the scenario file directly:');
|
|
2071
|
+
console.log(` jettypod work implement ${featureId} --scenario-file="features/[feature-name].feature" --winner="..." --rationale="..."`);
|
|
2068
2072
|
process.exit(1);
|
|
2069
2073
|
}
|
|
2070
2074
|
|
|
@@ -2084,79 +2088,126 @@ async function main() {
|
|
|
2084
2088
|
process.exit(1);
|
|
2085
2089
|
}
|
|
2086
2090
|
|
|
2087
|
-
//
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
// Prepare values
|
|
2093
|
-
const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
|
|
2094
|
-
const winnerValue = winner || null;
|
|
2095
|
-
const rationaleValue = rationale || null;
|
|
2096
|
-
const scenarioFileValue = scenarioFile; // Already validated above
|
|
2097
|
-
|
|
2098
|
-
// Update query: if transitioning, set phase, mode, and scenario_file; if updating, just update decision fields
|
|
2099
|
-
let updateSql;
|
|
2100
|
-
let updateParams;
|
|
2101
|
-
|
|
2102
|
-
if (isTransition) {
|
|
2103
|
-
// When transitioning to implementation, ensure status is 'todo' (ready for work)
|
|
2104
|
-
// This prevents status from being left as NULL or in a discovery-related state
|
|
2105
|
-
updateSql = `UPDATE work_items SET phase = 'implementation', mode = 'speed', status = 'todo', scenario_file = ?, prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
|
|
2106
|
-
updateParams = [scenarioFileValue, prototypeFilesValue, winnerValue, rationaleValue, featureId];
|
|
2107
|
-
} else {
|
|
2108
|
-
updateSql = `UPDATE work_items SET prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
|
|
2109
|
-
updateParams = [prototypeFilesValue, winnerValue, rationaleValue, featureId];
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
db.run(
|
|
2113
|
-
updateSql,
|
|
2114
|
-
updateParams,
|
|
2115
|
-
(err) => {
|
|
2091
|
+
// Check if chores exist - required before transitioning to implementation
|
|
2092
|
+
db.all(
|
|
2093
|
+
'SELECT id, title, status FROM work_items WHERE parent_id = ? AND type = ? ORDER BY created_at ASC',
|
|
2094
|
+
[featureId, 'chore'],
|
|
2095
|
+
(err, chores) => {
|
|
2116
2096
|
if (err) {
|
|
2117
|
-
console.error(`Error: ${err.message}`);
|
|
2097
|
+
console.error(`Error checking chores: ${err.message}`);
|
|
2118
2098
|
process.exit(1);
|
|
2119
2099
|
}
|
|
2120
2100
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2101
|
+
if (!chores || chores.length === 0) {
|
|
2102
|
+
console.error(`Error: Cannot implement - no chores exist`);
|
|
2103
|
+
console.log('');
|
|
2104
|
+
console.log('Feature planning must generate speed mode chores before transitioning to implementation.');
|
|
2105
|
+
console.log('');
|
|
2106
|
+
console.log('Complete feature planning first:');
|
|
2107
|
+
console.log(' Invoke skill: feature-planning');
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Get first chore for auto-start
|
|
2112
|
+
const firstChore = chores[0];
|
|
2113
|
+
|
|
2114
|
+
// Determine if this is a transition or an update
|
|
2115
|
+
// Also handle the case where phase is implementation but mode is not set (partial state)
|
|
2116
|
+
const isTransition = feature.phase === 'discovery' || !feature.mode;
|
|
2117
|
+
const isUpdate = feature.phase === 'implementation' && feature.mode;
|
|
2118
|
+
|
|
2119
|
+
// Prepare values
|
|
2120
|
+
const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
|
|
2121
|
+
const winnerValue = winner || null;
|
|
2122
|
+
const rationaleValue = rationale || null;
|
|
2123
|
+
const scenarioFileValue = scenarioFile; // Already validated above
|
|
2124
|
+
|
|
2125
|
+
// Update query: if transitioning, set phase, mode, and scenario_file; if updating, just update decision fields
|
|
2126
|
+
let updateSql;
|
|
2127
|
+
let updateParams;
|
|
2128
|
+
|
|
2123
2129
|
if (isTransition) {
|
|
2124
|
-
|
|
2130
|
+
// When transitioning to implementation, ensure status is 'todo' (ready for work)
|
|
2131
|
+
// This prevents status from being left as NULL or in a discovery-related state
|
|
2132
|
+
updateSql = `UPDATE work_items SET phase = 'implementation', mode = 'speed', status = 'todo', scenario_file = ?, prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
|
|
2133
|
+
updateParams = [scenarioFileValue, prototypeFilesValue, winnerValue, rationaleValue, featureId];
|
|
2125
2134
|
} else {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2129
|
-
console.log('');
|
|
2130
|
-
console.log(`Title: ${feature.title}`);
|
|
2131
|
-
console.log('Phase: Implementation');
|
|
2132
|
-
console.log('Mode: Speed');
|
|
2133
|
-
console.log(`Scenarios: ${scenarioFileValue}`);
|
|
2134
|
-
if (prototypes.length > 0) {
|
|
2135
|
-
console.log(`Prototypes: ${prototypes.join(', ')}`);
|
|
2135
|
+
updateSql = `UPDATE work_items SET prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
|
|
2136
|
+
updateParams = [prototypeFilesValue, winnerValue, rationaleValue, featureId];
|
|
2136
2137
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2138
|
+
|
|
2139
|
+
db.run(
|
|
2140
|
+
updateSql,
|
|
2141
|
+
updateParams,
|
|
2142
|
+
(err) => {
|
|
2143
|
+
if (err) {
|
|
2144
|
+
console.error(`Error: ${err.message}`);
|
|
2145
|
+
process.exit(1);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
console.log('');
|
|
2149
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2150
|
+
if (isTransition) {
|
|
2151
|
+
console.log(`✅ Feature #${featureId} transitioned to Implementation Phase`);
|
|
2152
|
+
} else {
|
|
2153
|
+
console.log(`✅ Feature #${featureId} discovery decision updated`);
|
|
2154
|
+
}
|
|
2155
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2156
|
+
console.log('');
|
|
2157
|
+
console.log(`Title: ${feature.title}`);
|
|
2158
|
+
console.log('Phase: Implementation');
|
|
2159
|
+
console.log('Mode: Speed');
|
|
2160
|
+
console.log(`Scenarios: ${scenarioFileValue}`);
|
|
2161
|
+
if (prototypes.length > 0) {
|
|
2162
|
+
console.log(`Prototypes: ${prototypes.join(', ')}`);
|
|
2163
|
+
}
|
|
2164
|
+
if (winner) {
|
|
2165
|
+
console.log(`Winner: ${winner}`);
|
|
2166
|
+
}
|
|
2167
|
+
if (rationale) {
|
|
2168
|
+
console.log(`Rationale: ${rationale}`);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// Auto-start first chore if transitioning
|
|
2172
|
+
if (isTransition && firstChore) {
|
|
2173
|
+
console.log('');
|
|
2174
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2175
|
+
console.log('🚀 Auto-starting first chore...');
|
|
2176
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2177
|
+
|
|
2178
|
+
// Import and call startWork
|
|
2179
|
+
const { startWork } = require('../work-commands');
|
|
2180
|
+
startWork(firstChore.id).then(() => {
|
|
2181
|
+
console.log('');
|
|
2182
|
+
console.log('✅ First chore started! Invoke skill: speed-mode');
|
|
2183
|
+
console.log('');
|
|
2184
|
+
}).catch((startErr) => {
|
|
2185
|
+
console.log('');
|
|
2186
|
+
console.log(`⚠️ Feature transitioned but failed to auto-start chore: ${startErr.message}`);
|
|
2187
|
+
console.log(` Manually start with: jettypod work start ${firstChore.id}`);
|
|
2188
|
+
console.log('');
|
|
2189
|
+
});
|
|
2190
|
+
} else {
|
|
2191
|
+
console.log('');
|
|
2192
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2193
|
+
console.log('🚀 Speed Mode: Prove It Works');
|
|
2194
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2195
|
+
console.log('');
|
|
2196
|
+
console.log('Build code that passes happy path scenarios.');
|
|
2197
|
+
console.log('Focus:');
|
|
2198
|
+
console.log(' • Happy path only');
|
|
2199
|
+
console.log(' • Single file when possible');
|
|
2200
|
+
console.log(' • localStorage for data');
|
|
2201
|
+
console.log(' • Basic try/catch');
|
|
2202
|
+
console.log('');
|
|
2203
|
+
console.log('⚠️ Speed Mode is a checkpoint - pass through quickly!');
|
|
2204
|
+
console.log('');
|
|
2205
|
+
console.log('Next: Elevate to Stable Mode');
|
|
2206
|
+
console.log(` jettypod work elevate ${featureId} stable`);
|
|
2207
|
+
console.log('');
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
);
|
|
2160
2211
|
}
|
|
2161
2212
|
);
|
|
2162
2213
|
});
|