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.
- 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 +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,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 @@
|
|
|
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')) {
|
package/lib/current-work.js
CHANGED
|
@@ -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
|
-
//
|
|
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 =
|
|
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,
|
|
56
|
+
db.get(query, [workItemId], (err, row) => {
|
|
65
57
|
if (err) {
|
|
66
58
|
return reject(err);
|
|
67
59
|
}
|