jettypod 4.4.10 → 4.4.11

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.
Files changed (32) hide show
  1. package/apps/dashboard/README.md +36 -0
  2. package/apps/dashboard/app/favicon.ico +0 -0
  3. package/apps/dashboard/app/globals.css +122 -0
  4. package/apps/dashboard/app/layout.tsx +34 -0
  5. package/apps/dashboard/app/page.tsx +25 -0
  6. package/apps/dashboard/app/work/[id]/page.tsx +193 -0
  7. package/apps/dashboard/components/KanbanBoard.tsx +201 -0
  8. package/apps/dashboard/components/WorkItemTree.tsx +116 -0
  9. package/apps/dashboard/components.json +22 -0
  10. package/apps/dashboard/eslint.config.mjs +18 -0
  11. package/apps/dashboard/lib/db.ts +270 -0
  12. package/apps/dashboard/lib/utils.ts +6 -0
  13. package/apps/dashboard/next.config.ts +7 -0
  14. package/apps/dashboard/package.json +33 -0
  15. package/apps/dashboard/postcss.config.mjs +7 -0
  16. package/apps/dashboard/public/file.svg +1 -0
  17. package/apps/dashboard/public/globe.svg +1 -0
  18. package/apps/dashboard/public/next.svg +1 -0
  19. package/apps/dashboard/public/vercel.svg +1 -0
  20. package/apps/dashboard/public/window.svg +1 -0
  21. package/apps/dashboard/tsconfig.json +34 -0
  22. package/jettypod.js +41 -0
  23. package/lib/current-work.js +10 -18
  24. package/lib/migrations/016-workflow-checkpoints-table.js +70 -0
  25. package/lib/migrations/017-backfill-epic-id.js +54 -0
  26. package/lib/workflow-checkpoint.js +204 -0
  27. package/package.json +7 -2
  28. package/skills-templates/chore-mode/SKILL.md +3 -0
  29. package/skills-templates/epic-planning/SKILL.md +225 -154
  30. package/skills-templates/feature-planning/SKILL.md +172 -87
  31. package/skills-templates/speed-mode/SKILL.md +161 -338
  32. package/skills-templates/stable-mode/SKILL.md +8 -2
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import type { WorkItem } from '@/lib/db';
6
+
7
+ const typeIcons: Record<string, string> = {
8
+ epic: '🎯',
9
+ feature: '✨',
10
+ chore: '🔧',
11
+ bug: '🐛',
12
+ };
13
+
14
+ const statusColors: Record<string, string> = {
15
+ backlog: 'text-zinc-500',
16
+ todo: 'text-zinc-500',
17
+ in_progress: 'text-blue-600 dark:text-blue-400',
18
+ done: 'text-green-600 dark:text-green-400',
19
+ cancelled: 'text-red-500',
20
+ };
21
+
22
+ const modeLabels: Record<string, { label: string; color: string }> = {
23
+ speed: { label: 'speed', color: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200' },
24
+ stable: { label: 'stable', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' },
25
+ production: { label: 'prod', color: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' },
26
+ };
27
+
28
+ interface WorkItemNodeProps {
29
+ item: WorkItem;
30
+ depth?: number;
31
+ }
32
+
33
+ function WorkItemNode({ item, depth = 0 }: WorkItemNodeProps) {
34
+ const [expanded, setExpanded] = useState(true);
35
+ const hasChildren = item.children && item.children.length > 0;
36
+
37
+ return (
38
+ <div className="select-none">
39
+ <div
40
+ className={`flex items-center gap-2 py-1.5 px-2 rounded hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors group`}
41
+ style={{ paddingLeft: `${depth * 20 + 8}px` }}
42
+ >
43
+ {/* Expand/collapse toggle */}
44
+ {hasChildren ? (
45
+ <button
46
+ onClick={() => setExpanded(!expanded)}
47
+ className="w-4 h-4 flex items-center justify-center text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300"
48
+ >
49
+ {expanded ? '▼' : '▶'}
50
+ </button>
51
+ ) : (
52
+ <span className="w-4" />
53
+ )}
54
+
55
+ {/* Type icon */}
56
+ <span className="text-sm">{typeIcons[item.type] || '📄'}</span>
57
+
58
+ {/* ID */}
59
+ <span className="text-zinc-400 text-sm font-mono">#{item.id}</span>
60
+
61
+ {/* Title */}
62
+ <Link
63
+ href={`/work/${item.id}`}
64
+ className={`flex-1 truncate hover:underline ${statusColors[item.status]}`}
65
+ >
66
+ {item.title}
67
+ </Link>
68
+
69
+ {/* Mode badge */}
70
+ {item.mode && modeLabels[item.mode] && (
71
+ <span className={`text-xs px-1.5 py-0.5 rounded ${modeLabels[item.mode].color}`}>
72
+ {modeLabels[item.mode].label}
73
+ </span>
74
+ )}
75
+
76
+ {/* Children count */}
77
+ {hasChildren && (
78
+ <span className="text-xs text-zinc-400">
79
+ ({item.children!.length})
80
+ </span>
81
+ )}
82
+ </div>
83
+
84
+ {/* Children */}
85
+ {expanded && hasChildren && (
86
+ <div>
87
+ {item.children!.map((child) => (
88
+ <WorkItemNode key={child.id} item={child} depth={depth + 1} />
89
+ ))}
90
+ </div>
91
+ )}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ interface WorkItemTreeProps {
97
+ items: WorkItem[];
98
+ }
99
+
100
+ export function WorkItemTree({ items }: WorkItemTreeProps) {
101
+ if (items.length === 0) {
102
+ return (
103
+ <div className="text-zinc-500 text-center py-8">
104
+ No work items found
105
+ </div>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <div className="font-mono text-sm">
111
+ {items.map((item) => (
112
+ <WorkItemNode key={item.id} item={item} />
113
+ ))}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "registries": {}
22
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,270 @@
1
+ // Dashboard database access layer
2
+ // Reads from JettyPod SQLite database
3
+
4
+ import Database from 'better-sqlite3';
5
+ import path from 'path';
6
+ import { execSync } from 'child_process';
7
+
8
+ // Types matching JettyPod schema
9
+ export interface WorkItem {
10
+ id: number;
11
+ type: 'epic' | 'feature' | 'chore' | 'bug';
12
+ title: string;
13
+ description: string | null;
14
+ status: 'backlog' | 'todo' | 'in_progress' | 'done' | 'cancelled';
15
+ parent_id: number | null;
16
+ epic_id: number | null;
17
+ branch_name: string | null;
18
+ mode: 'speed' | 'stable' | 'production' | null;
19
+ phase: string | null;
20
+ completed_at: string | null;
21
+ created_at: string;
22
+ children?: WorkItem[];
23
+ }
24
+
25
+ export interface Decision {
26
+ id: number;
27
+ work_item_id: number;
28
+ aspect: string;
29
+ decision: string;
30
+ rationale: string;
31
+ created_at: string;
32
+ }
33
+
34
+ function getGitRoot(): string {
35
+ try {
36
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
37
+ } catch {
38
+ throw new Error('Not in a git repository');
39
+ }
40
+ }
41
+
42
+ function getDbPath(): string {
43
+ const gitRoot = getGitRoot();
44
+ return path.join(gitRoot, '.jettypod', 'work.db');
45
+ }
46
+
47
+ function getDb(): Database.Database {
48
+ const dbPath = getDbPath();
49
+ return new Database(dbPath, { readonly: true });
50
+ }
51
+
52
+ export function getAllWorkItems(): WorkItem[] {
53
+ const db = getDb();
54
+ try {
55
+ const items = db.prepare(`
56
+ SELECT id, type, title, description, status, parent_id, epic_id,
57
+ branch_name, mode, phase, completed_at, created_at
58
+ FROM work_items
59
+ ORDER BY id
60
+ `).all() as WorkItem[];
61
+ return items;
62
+ } finally {
63
+ db.close();
64
+ }
65
+ }
66
+
67
+ export function getWorkItem(id: number): WorkItem | null {
68
+ const db = getDb();
69
+ try {
70
+ const item = db.prepare(`
71
+ SELECT id, type, title, description, status, parent_id, epic_id,
72
+ branch_name, mode, phase, completed_at, created_at
73
+ FROM work_items
74
+ WHERE id = ?
75
+ `).get(id) as WorkItem | undefined;
76
+ return item || null;
77
+ } finally {
78
+ db.close();
79
+ }
80
+ }
81
+
82
+ export function getChildWorkItems(parentId: number): WorkItem[] {
83
+ const db = getDb();
84
+ try {
85
+ const items = db.prepare(`
86
+ SELECT id, type, title, description, status, parent_id, epic_id,
87
+ branch_name, mode, phase, completed_at, created_at
88
+ FROM work_items
89
+ WHERE parent_id = ?
90
+ ORDER BY id
91
+ `).all(parentId) as WorkItem[];
92
+ return items;
93
+ } finally {
94
+ db.close();
95
+ }
96
+ }
97
+
98
+ export function getDecisionsForWorkItem(workItemId: number): Decision[] {
99
+ const db = getDb();
100
+ try {
101
+ const decisions = db.prepare(`
102
+ SELECT id, work_item_id, aspect, decision, rationale, created_at
103
+ FROM discovery_decisions
104
+ WHERE work_item_id = ?
105
+ ORDER BY created_at
106
+ `).all(workItemId) as Decision[];
107
+ return decisions;
108
+ } finally {
109
+ db.close();
110
+ }
111
+ }
112
+
113
+ // Build a tree structure from flat work items
114
+ export function buildWorkItemTree(items: WorkItem[]): WorkItem[] {
115
+ const itemMap = new Map<number, WorkItem>();
116
+ const roots: WorkItem[] = [];
117
+
118
+ // First pass: create map and initialize children arrays
119
+ for (const item of items) {
120
+ itemMap.set(item.id, { ...item, children: [] });
121
+ }
122
+
123
+ // Second pass: build tree structure
124
+ for (const item of items) {
125
+ const node = itemMap.get(item.id)!;
126
+ if (item.parent_id && itemMap.has(item.parent_id)) {
127
+ const parent = itemMap.get(item.parent_id)!;
128
+ parent.children!.push(node);
129
+ } else {
130
+ roots.push(node);
131
+ }
132
+ }
133
+
134
+ return roots;
135
+ }
136
+
137
+ export function getBacklogTree(): WorkItem[] {
138
+ const items = getAllWorkItems();
139
+ return buildWorkItemTree(items);
140
+ }
141
+
142
+ export function getActiveWork(): WorkItem[] {
143
+ const db = getDb();
144
+ try {
145
+ const items = db.prepare(`
146
+ SELECT id, type, title, description, status, parent_id, epic_id,
147
+ branch_name, mode, phase, completed_at, created_at
148
+ FROM work_items
149
+ WHERE status = 'in_progress'
150
+ ORDER BY id
151
+ `).all() as WorkItem[];
152
+ return items;
153
+ } finally {
154
+ db.close();
155
+ }
156
+ }
157
+
158
+ export function getRecentlyCompleted(limit: number = 10): WorkItem[] {
159
+ const db = getDb();
160
+ try {
161
+ const items = db.prepare(`
162
+ SELECT id, type, title, description, status, parent_id, epic_id,
163
+ branch_name, mode, phase, completed_at, created_at
164
+ FROM work_items
165
+ WHERE status = 'done'
166
+ ORDER BY completed_at DESC
167
+ LIMIT ?
168
+ `).all(limit) as WorkItem[];
169
+ return items;
170
+ } finally {
171
+ db.close();
172
+ }
173
+ }
174
+
175
+ // Get items for Kanban view - features, standalone chores, bugs (not feature children)
176
+ export interface KanbanGroup {
177
+ epicId: number | null;
178
+ epicTitle: string | null;
179
+ items: WorkItem[];
180
+ }
181
+
182
+ export interface InFlightItem extends WorkItem {
183
+ epicTitle: string | null;
184
+ }
185
+
186
+ export interface KanbanData {
187
+ inFlight: InFlightItem[];
188
+ backlog: Map<string, KanbanGroup>;
189
+ done: Map<string, KanbanGroup>;
190
+ }
191
+
192
+ export function getKanbanData(doneLimit: number = 50): KanbanData {
193
+ const db = getDb();
194
+ try {
195
+ // Get all epics for lookup
196
+ const epics = db.prepare(`
197
+ SELECT id, title FROM work_items WHERE type = 'epic'
198
+ `).all() as { id: number; title: string }[];
199
+ const epicMap = new Map(epics.map(e => [e.id, e.title]));
200
+
201
+ // Get kanban-eligible items:
202
+ // - Features (type = 'feature')
203
+ // - Chores that are NOT children of features (parent is null, or parent is an epic)
204
+ // - Bugs if they exist
205
+ const allItems = db.prepare(`
206
+ SELECT w.id, w.type, w.title, w.description, w.status, w.parent_id, w.epic_id,
207
+ w.branch_name, w.mode, w.phase, w.completed_at, w.created_at,
208
+ p.type as parent_type
209
+ FROM work_items w
210
+ LEFT JOIN work_items p ON w.parent_id = p.id
211
+ WHERE w.type IN ('feature', 'chore', 'bug')
212
+ AND (w.parent_id IS NULL OR p.type = 'epic')
213
+ ORDER BY w.id
214
+ `).all() as (WorkItem & { parent_type: string | null })[];
215
+
216
+ const inFlight: InFlightItem[] = [];
217
+ const backlogGroups = new Map<string, KanbanGroup>();
218
+ const doneGroups = new Map<string, KanbanGroup>();
219
+
220
+ // Helper to get or create group
221
+ function getGroup(groups: Map<string, KanbanGroup>, item: WorkItem): KanbanGroup {
222
+ const epicId = item.parent_id || item.epic_id;
223
+ const key = epicId ? `epic-${epicId}` : 'ungrouped';
224
+
225
+ if (!groups.has(key)) {
226
+ groups.set(key, {
227
+ epicId: epicId,
228
+ epicTitle: epicId ? epicMap.get(epicId) || null : null,
229
+ items: []
230
+ });
231
+ }
232
+ return groups.get(key)!;
233
+ }
234
+
235
+ for (const item of allItems) {
236
+ // Strip parent_type from the item
237
+ const { parent_type, ...cleanItem } = item;
238
+
239
+ if (cleanItem.status === 'in_progress') {
240
+ const epicId = cleanItem.parent_id || cleanItem.epic_id;
241
+ const epicTitle = epicId ? epicMap.get(epicId) || null : null;
242
+ inFlight.push({ ...cleanItem, epicTitle });
243
+ } else if (cleanItem.status === 'backlog' || cleanItem.status === 'todo') {
244
+ const group = getGroup(backlogGroups, cleanItem);
245
+ group.items.push(cleanItem);
246
+ } else if (cleanItem.status === 'done') {
247
+ const group = getGroup(doneGroups, cleanItem);
248
+ group.items.push(cleanItem);
249
+ }
250
+ }
251
+
252
+ // Limit done items
253
+ let doneCount = 0;
254
+ for (const [key, group] of doneGroups) {
255
+ if (doneCount >= doneLimit) {
256
+ doneGroups.delete(key);
257
+ continue;
258
+ }
259
+ const remaining = doneLimit - doneCount;
260
+ if (group.items.length > remaining) {
261
+ group.items = group.items.slice(0, remaining);
262
+ }
263
+ doneCount += group.items.length;
264
+ }
265
+
266
+ return { inFlight, backlog: backlogGroups, done: doneGroups };
267
+ } finally {
268
+ db.close();
269
+ }
270
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "dashboard",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "better-sqlite3": "^12.5.0",
13
+ "class-variance-authority": "^0.7.1",
14
+ "clsx": "^2.1.1",
15
+ "lucide-react": "^0.555.0",
16
+ "next": "16.0.6",
17
+ "react": "19.2.0",
18
+ "react-dom": "19.2.0",
19
+ "tailwind-merge": "^3.4.0"
20
+ },
21
+ "devDependencies": {
22
+ "@tailwindcss/postcss": "^4",
23
+ "@types/better-sqlite3": "^7.6.13",
24
+ "@types/node": "^20",
25
+ "@types/react": "^19",
26
+ "@types/react-dom": "^19",
27
+ "eslint": "^9",
28
+ "eslint-config-next": "16.0.6",
29
+ "tailwindcss": "^4",
30
+ "tw-animate-css": "^1.4.0",
31
+ "typescript": "^5"
32
+ }
33
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
package/jettypod.js CHANGED
@@ -2183,6 +2183,47 @@ Quick commands:
2183
2183
  break;
2184
2184
  }
2185
2185
 
2186
+ case 'workflow': {
2187
+ const workflowSubcommand = args[0];
2188
+
2189
+ if (workflowSubcommand === 'resume') {
2190
+ const { getDb } = require('./lib/database');
2191
+ const { getCheckpoint, getCurrentBranch } = require('./lib/workflow-checkpoint');
2192
+
2193
+ try {
2194
+ const db = getDb();
2195
+ const branchName = getCurrentBranch();
2196
+ const checkpoint = await getCheckpoint(db, branchName);
2197
+
2198
+ if (!checkpoint) {
2199
+ console.log('No interrupted workflow found');
2200
+ } else {
2201
+ const stepInfo = checkpoint.total_steps
2202
+ ? `Step ${checkpoint.current_step} of ${checkpoint.total_steps}`
2203
+ : `Step ${checkpoint.current_step}`;
2204
+
2205
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2206
+ console.log('Found interrupted workflow');
2207
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2208
+ console.log(`Skill: ${checkpoint.skill_name}`);
2209
+ console.log(stepInfo);
2210
+ if (checkpoint.work_item_id) {
2211
+ console.log(`Work Item: #${checkpoint.work_item_id}`);
2212
+ }
2213
+ }
2214
+ } catch (err) {
2215
+ console.error(`Error: ${err.message}`);
2216
+ process.exit(1);
2217
+ }
2218
+ } else {
2219
+ console.log('Usage: jettypod workflow resume');
2220
+ console.log('');
2221
+ console.log('Commands:');
2222
+ console.log(' resume Check for and resume interrupted workflows');
2223
+ }
2224
+ break;
2225
+ }
2226
+
2186
2227
  default:
2187
2228
  // Smart mode: auto-initialize if needed, otherwise show guidance
2188
2229
  if (!fs.existsSync('.jettypod')) {
@@ -34,34 +34,26 @@ async function getCurrentWork() {
34
34
  // Not in a git repo or other git error - that's ok
35
35
  }
36
36
 
37
- // Query for in_progress work item
37
+ // If no work item ID from branch, we're on main - return null
38
+ // Current work only applies within worktrees, not root/main branch
39
+ if (!workItemId) {
40
+ return null;
41
+ }
42
+
43
+ // Query for the specific in_progress work item matching our branch
38
44
  let row;
39
45
  try {
40
46
  row = await new Promise((resolve, reject) => {
41
- const query = workItemId
42
- ? // If we have an ID from branch name, get that specific item
43
- `SELECT
47
+ const query = `SELECT
44
48
  wi.id, wi.title, wi.type, wi.status, wi.parent_id, wi.epic_id,
45
49
  parent.title as parent_title,
46
50
  epic.title as epic_title
47
51
  FROM work_items wi
48
52
  LEFT JOIN work_items parent ON wi.parent_id = parent.id
49
53
  LEFT JOIN work_items epic ON wi.epic_id = epic.id
50
- WHERE wi.id = ? AND wi.status = 'in_progress'`
51
- : // Otherwise get any in_progress item (for main branch)
52
- `SELECT
53
- wi.id, wi.title, wi.type, wi.status, wi.parent_id, wi.epic_id,
54
- parent.title as parent_title,
55
- epic.title as epic_title
56
- FROM work_items wi
57
- LEFT JOIN work_items parent ON wi.parent_id = parent.id
58
- LEFT JOIN work_items epic ON wi.epic_id = epic.id
59
- WHERE wi.status = 'in_progress'
60
- LIMIT 1`;
61
-
62
- const params = workItemId ? [workItemId] : [];
54
+ WHERE wi.id = ? AND wi.status = 'in_progress'`;
63
55
 
64
- db.get(query, params, (err, row) => {
56
+ db.get(query, [workItemId], (err, row) => {
65
57
  if (err) {
66
58
  return reject(err);
67
59
  }