jettypod 4.4.10 → 4.4.12
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/apps/dashboard/README.md +36 -0
- package/apps/dashboard/app/favicon.ico +0 -0
- package/apps/dashboard/app/globals.css +122 -0
- package/apps/dashboard/app/layout.tsx +34 -0
- package/apps/dashboard/app/page.tsx +25 -0
- package/apps/dashboard/app/work/[id]/page.tsx +193 -0
- package/apps/dashboard/components/KanbanBoard.tsx +201 -0
- package/apps/dashboard/components/WorkItemTree.tsx +116 -0
- package/apps/dashboard/components.json +22 -0
- package/apps/dashboard/eslint.config.mjs +18 -0
- package/apps/dashboard/lib/db.ts +270 -0
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.ts +7 -0
- package/apps/dashboard/package.json +33 -0
- package/apps/dashboard/postcss.config.mjs +7 -0
- package/apps/dashboard/public/file.svg +1 -0
- package/apps/dashboard/public/globe.svg +1 -0
- package/apps/dashboard/public/next.svg +1 -0
- package/apps/dashboard/public/vercel.svg +1 -0
- package/apps/dashboard/public/window.svg +1 -0
- package/apps/dashboard/tsconfig.json +34 -0
- package/jettypod.js +41 -0
- package/lib/current-work.js +10 -18
- package/lib/migrations/016-workflow-checkpoints-table.js +70 -0
- package/lib/migrations/017-backfill-epic-id.js +54 -0
- package/lib/workflow-checkpoint.js +204 -0
- package/package.json +7 -2
- package/skills-templates/chore-mode/SKILL.md +3 -0
- package/skills-templates/epic-planning/SKILL.md +225 -154
- package/skills-templates/feature-planning/SKILL.md +172 -87
- package/skills-templates/speed-mode/SKILL.md +161 -338
- package/skills-templates/stable-mode/SKILL.md +150 -176
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: Create workflow_checkpoints table
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Enable persistence of workflow state across sessions so interrupted
|
|
5
|
+
* workflows can be resumed. Stores skill name, current step, and context JSON.
|
|
6
|
+
*
|
|
7
|
+
* Why this is critical:
|
|
8
|
+
* - Crash recovery - resume workflows after unexpected session termination
|
|
9
|
+
* - Branch-aware - checkpoints are tied to specific worktree/branch context
|
|
10
|
+
* - Context preservation - stores full workflow state as JSON for resume
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
id: '016-workflow-checkpoints-table',
|
|
15
|
+
description: 'Create workflow_checkpoints table for session resume capability',
|
|
16
|
+
|
|
17
|
+
async up(db) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
db.run(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS workflow_checkpoints (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
skill_name TEXT NOT NULL,
|
|
23
|
+
current_step INTEGER NOT NULL,
|
|
24
|
+
total_steps INTEGER,
|
|
25
|
+
context_json TEXT,
|
|
26
|
+
branch_name TEXT NOT NULL,
|
|
27
|
+
work_item_id INTEGER,
|
|
28
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
30
|
+
)
|
|
31
|
+
`, (err) => {
|
|
32
|
+
if (err) {
|
|
33
|
+
return reject(err);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create index on branch_name for quick lookup by current branch
|
|
37
|
+
db.run(`
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_checkpoints_branch
|
|
39
|
+
ON workflow_checkpoints(branch_name)
|
|
40
|
+
`, (err) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
return reject(err);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resolve();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async down(db) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
// Drop index first
|
|
54
|
+
db.run('DROP INDEX IF EXISTS idx_workflow_checkpoints_branch', (err) => {
|
|
55
|
+
if (err) {
|
|
56
|
+
return reject(err);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Drop table
|
|
60
|
+
db.run('DROP TABLE IF EXISTS workflow_checkpoints', (err) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
return reject(err);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration 017: Backfill epic_id field
|
|
3
|
+
*
|
|
4
|
+
* The epic_id field provides a direct reference to the top-level epic,
|
|
5
|
+
* avoiding tree traversal when querying items by epic.
|
|
6
|
+
*
|
|
7
|
+
* This migration backfills epic_id for existing items:
|
|
8
|
+
* - Features: epic_id = parent_id (where parent is an epic)
|
|
9
|
+
* - Chores: epic_id = parent's epic_id (inherit from parent)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
id: '017-backfill-epic-id',
|
|
14
|
+
|
|
15
|
+
up: (db) => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
db.serialize(() => {
|
|
18
|
+
// Backfill epic_id for features (parent is epic)
|
|
19
|
+
db.run(`
|
|
20
|
+
UPDATE work_items
|
|
21
|
+
SET epic_id = parent_id
|
|
22
|
+
WHERE type = 'feature'
|
|
23
|
+
AND parent_id IS NOT NULL
|
|
24
|
+
AND epic_id IS NULL
|
|
25
|
+
AND (SELECT type FROM work_items p WHERE p.id = work_items.parent_id) = 'epic'
|
|
26
|
+
`, (err) => {
|
|
27
|
+
if (err) return reject(err);
|
|
28
|
+
|
|
29
|
+
// Backfill epic_id for chores (inherit from parent)
|
|
30
|
+
db.run(`
|
|
31
|
+
UPDATE work_items
|
|
32
|
+
SET epic_id = (SELECT epic_id FROM work_items p WHERE p.id = work_items.parent_id)
|
|
33
|
+
WHERE type = 'chore'
|
|
34
|
+
AND parent_id IS NOT NULL
|
|
35
|
+
AND epic_id IS NULL
|
|
36
|
+
`, (err) => {
|
|
37
|
+
if (err) return reject(err);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
down: (db) => {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
// Clear all epic_id values
|
|
48
|
+
db.run(`UPDATE work_items SET epic_id = NULL`, (err) => {
|
|
49
|
+
if (err) return reject(err);
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Checkpoint - Persistence for Session Resume
|
|
3
|
+
*
|
|
4
|
+
* This module provides checkpoint creation, update, and retrieval for workflow
|
|
5
|
+
* state persistence. Enables resuming interrupted workflows across sessions.
|
|
6
|
+
*
|
|
7
|
+
* Core principles:
|
|
8
|
+
* - Branch-aware: checkpoints are tied to specific git branch context
|
|
9
|
+
* - Atomic updates: each step transition updates the checkpoint
|
|
10
|
+
* - Context preservation: stores full workflow state as JSON
|
|
11
|
+
* - Auto-cleanup: checkpoints are deleted on successful completion
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current git branch name
|
|
18
|
+
*
|
|
19
|
+
* @returns {string} Current branch name
|
|
20
|
+
*/
|
|
21
|
+
function getCurrentBranch() {
|
|
22
|
+
try {
|
|
23
|
+
return execSync('git branch --show-current', { encoding: 'utf8' }).trim();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new workflow checkpoint
|
|
31
|
+
*
|
|
32
|
+
* @param {Object} db - SQLite database connection
|
|
33
|
+
* @param {Object} options - Checkpoint options
|
|
34
|
+
* @param {string} options.skillName - Name of the skill (e.g., 'feature-planning')
|
|
35
|
+
* @param {number} options.currentStep - Current step number (1-indexed)
|
|
36
|
+
* @param {number} [options.totalSteps] - Total number of steps (optional)
|
|
37
|
+
* @param {Object} [options.context] - Workflow context to preserve (optional)
|
|
38
|
+
* @param {string} [options.branchName] - Git branch name (defaults to current branch)
|
|
39
|
+
* @param {number} [options.workItemId] - Associated work item ID (optional)
|
|
40
|
+
* @returns {Promise<Object>} Created checkpoint with id
|
|
41
|
+
*/
|
|
42
|
+
async function createCheckpoint(db, options) {
|
|
43
|
+
if (!db) {
|
|
44
|
+
throw new Error('Database connection required');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
skillName,
|
|
49
|
+
currentStep,
|
|
50
|
+
totalSteps = null,
|
|
51
|
+
context = {},
|
|
52
|
+
branchName = getCurrentBranch(),
|
|
53
|
+
workItemId = null
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
const contextJson = JSON.stringify(context);
|
|
57
|
+
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
db.run(
|
|
60
|
+
`INSERT INTO workflow_checkpoints
|
|
61
|
+
(skill_name, current_step, total_steps, context_json, branch_name, work_item_id)
|
|
62
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
63
|
+
[skillName, currentStep, totalSteps, contextJson, branchName, workItemId],
|
|
64
|
+
function(err) {
|
|
65
|
+
if (err) {
|
|
66
|
+
reject(err);
|
|
67
|
+
} else {
|
|
68
|
+
resolve({
|
|
69
|
+
id: this.lastID,
|
|
70
|
+
skill_name: skillName,
|
|
71
|
+
current_step: currentStep,
|
|
72
|
+
total_steps: totalSteps,
|
|
73
|
+
context_json: contextJson,
|
|
74
|
+
branch_name: branchName,
|
|
75
|
+
work_item_id: workItemId
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Update an existing workflow checkpoint
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} db - SQLite database connection
|
|
87
|
+
* @param {string} branchName - Git branch name to identify checkpoint
|
|
88
|
+
* @param {Object} updates - Fields to update
|
|
89
|
+
* @param {number} [updates.currentStep] - New step number
|
|
90
|
+
* @param {number} [updates.totalSteps] - New total steps
|
|
91
|
+
* @param {Object} [updates.context] - New context (merged with existing)
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
async function updateCheckpoint(db, branchName, updates) {
|
|
95
|
+
const fields = [];
|
|
96
|
+
const values = [];
|
|
97
|
+
|
|
98
|
+
if (updates.currentStep !== undefined) {
|
|
99
|
+
fields.push('current_step = ?');
|
|
100
|
+
values.push(updates.currentStep);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (updates.totalSteps !== undefined) {
|
|
104
|
+
fields.push('total_steps = ?');
|
|
105
|
+
values.push(updates.totalSteps);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (updates.context !== undefined) {
|
|
109
|
+
fields.push('context_json = ?');
|
|
110
|
+
values.push(JSON.stringify(updates.context));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Always update the updated_at timestamp
|
|
114
|
+
fields.push('updated_at = CURRENT_TIMESTAMP');
|
|
115
|
+
|
|
116
|
+
values.push(branchName);
|
|
117
|
+
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
db.run(
|
|
120
|
+
`UPDATE workflow_checkpoints SET ${fields.join(', ')} WHERE branch_name = ?`,
|
|
121
|
+
values,
|
|
122
|
+
(err) => {
|
|
123
|
+
if (err) {
|
|
124
|
+
reject(err);
|
|
125
|
+
} else {
|
|
126
|
+
resolve();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get checkpoint for a specific branch
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} db - SQLite database connection
|
|
137
|
+
* @param {string} [branchName] - Git branch name (defaults to current branch)
|
|
138
|
+
* @returns {Promise<Object|null>} Checkpoint or null if not found
|
|
139
|
+
*/
|
|
140
|
+
async function getCheckpoint(db, branchName = getCurrentBranch()) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
db.get(
|
|
143
|
+
`SELECT * FROM workflow_checkpoints WHERE branch_name = ?`,
|
|
144
|
+
[branchName],
|
|
145
|
+
(err, row) => {
|
|
146
|
+
if (err) {
|
|
147
|
+
reject(err);
|
|
148
|
+
} else if (!row) {
|
|
149
|
+
resolve(null);
|
|
150
|
+
} else {
|
|
151
|
+
// Parse context_json back to object
|
|
152
|
+
resolve({
|
|
153
|
+
...row,
|
|
154
|
+
context: row.context_json ? JSON.parse(row.context_json) : {}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete checkpoint for a specific branch (on successful completion)
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} db - SQLite database connection
|
|
166
|
+
* @param {string} [branchName] - Git branch name (defaults to current branch)
|
|
167
|
+
* @returns {Promise<void>}
|
|
168
|
+
*/
|
|
169
|
+
async function deleteCheckpoint(db, branchName = getCurrentBranch()) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
db.run(
|
|
172
|
+
`DELETE FROM workflow_checkpoints WHERE branch_name = ?`,
|
|
173
|
+
[branchName],
|
|
174
|
+
(err) => {
|
|
175
|
+
if (err) {
|
|
176
|
+
reject(err);
|
|
177
|
+
} else {
|
|
178
|
+
resolve();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if a checkpoint exists for the current branch
|
|
187
|
+
*
|
|
188
|
+
* @param {Object} db - SQLite database connection
|
|
189
|
+
* @param {string} [branchName] - Git branch name (defaults to current branch)
|
|
190
|
+
* @returns {Promise<boolean>} True if checkpoint exists
|
|
191
|
+
*/
|
|
192
|
+
async function hasCheckpoint(db, branchName = getCurrentBranch()) {
|
|
193
|
+
const checkpoint = await getCheckpoint(db, branchName);
|
|
194
|
+
return checkpoint !== null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
createCheckpoint,
|
|
199
|
+
updateCheckpoint,
|
|
200
|
+
getCheckpoint,
|
|
201
|
+
deleteCheckpoint,
|
|
202
|
+
hasCheckpoint,
|
|
203
|
+
getCurrentBranch
|
|
204
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jettypod",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.12",
|
|
4
4
|
"description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
|
|
5
5
|
"main": "jettypod.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,10 +34,14 @@
|
|
|
34
34
|
"test:unit": "NODE_ENV=test jest",
|
|
35
35
|
"test:unit:watch": "NODE_ENV=test jest --watch",
|
|
36
36
|
"test:cleanup": "node scripts/test-cleanup.js",
|
|
37
|
-
"prepublishOnly": "echo '🔄 Syncing skills to templates...' && cp -r .claude/skills/* skills-templates/ && echo '✅ Skills synced'"
|
|
37
|
+
"prepublishOnly": "echo '🔄 Syncing skills to templates...' && cp -r .claude/skills/* skills-templates/ && echo '✅ Skills synced'",
|
|
38
|
+
"dashboard": "npm run dev --prefix apps/dashboard",
|
|
39
|
+
"dashboard:build": "npm run build --prefix apps/dashboard",
|
|
40
|
+
"dashboard:start": "npm run start --prefix apps/dashboard"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"@cucumber/cucumber": "^10.0.1",
|
|
44
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
41
45
|
"chai": "^6.0.1",
|
|
42
46
|
"jest": "^30.1.3"
|
|
43
47
|
},
|
|
@@ -54,6 +58,7 @@
|
|
|
54
58
|
"coverageDirectory": "coverage"
|
|
55
59
|
},
|
|
56
60
|
"dependencies": {
|
|
61
|
+
"better-sqlite3": "^12.5.0",
|
|
57
62
|
"chalk": "^4.1.2",
|
|
58
63
|
"sqlite3": "^5.1.7"
|
|
59
64
|
},
|
|
@@ -304,6 +304,9 @@ git add .
|
|
|
304
304
|
git commit -m "chore: [brief description]"
|
|
305
305
|
git push
|
|
306
306
|
|
|
307
|
+
# Switch to main repo before merging (merge cannot run from inside worktree)
|
|
308
|
+
cd $(git rev-parse --git-common-dir)/..
|
|
309
|
+
|
|
307
310
|
# Merge to main (auto-marks chore done)
|
|
308
311
|
jettypod work merge
|
|
309
312
|
```
|