prjct-cli 0.13.3 → 0.15.0
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/CHANGELOG.md +106 -0
- package/bin/prjct +10 -13
- package/core/agentic/memory-system/semantic-memories.ts +2 -1
- package/core/agentic/plan-mode/plan-mode.ts +2 -1
- package/core/agentic/prompt-builder.ts +22 -43
- package/core/agentic/services.ts +5 -5
- package/core/agentic/smart-context.ts +7 -2
- package/core/command-registry/core-commands.ts +54 -29
- package/core/command-registry/optional-commands.ts +64 -0
- package/core/command-registry/setup-commands.ts +18 -3
- package/core/commands/analysis.ts +21 -68
- package/core/commands/analytics.ts +247 -213
- package/core/commands/base.ts +1 -1
- package/core/commands/index.ts +41 -36
- package/core/commands/maintenance.ts +300 -31
- package/core/commands/planning.ts +233 -22
- package/core/commands/setup.ts +3 -8
- package/core/commands/shipping.ts +14 -18
- package/core/commands/types.ts +8 -6
- package/core/commands/workflow.ts +105 -100
- package/core/context/generator.ts +317 -0
- package/core/context-sync.ts +7 -350
- package/core/data/index.ts +13 -32
- package/core/data/md-ideas-manager.ts +155 -0
- package/core/data/md-queue-manager.ts +4 -3
- package/core/data/md-shipped-manager.ts +90 -0
- package/core/data/md-state-manager.ts +11 -7
- package/core/domain/agent-generator.ts +23 -63
- package/core/events/index.ts +143 -0
- package/core/index.ts +17 -14
- package/core/infrastructure/capability-installer.ts +13 -149
- package/core/infrastructure/migrator/project-scanner.ts +2 -1
- package/core/infrastructure/path-manager.ts +4 -6
- package/core/infrastructure/setup.ts +3 -0
- package/core/infrastructure/uuid-migration.ts +750 -0
- package/core/outcomes/recorder.ts +2 -1
- package/core/plugin/loader.ts +4 -7
- package/core/plugin/registry.ts +3 -3
- package/core/schemas/index.ts +23 -25
- package/core/schemas/state.ts +1 -0
- package/core/serializers/ideas-serializer.ts +187 -0
- package/core/serializers/index.ts +16 -0
- package/core/serializers/shipped-serializer.ts +108 -0
- package/core/session/utils.ts +3 -9
- package/core/storage/ideas-storage.ts +273 -0
- package/core/storage/index.ts +204 -0
- package/core/storage/queue-storage.ts +297 -0
- package/core/storage/shipped-storage.ts +223 -0
- package/core/storage/state-storage.ts +235 -0
- package/core/storage/storage-manager.ts +175 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
- package/packages/web/app/api/sessions/current/route.ts +132 -0
- package/packages/web/app/api/sessions/history/route.ts +96 -14
- package/packages/web/app/globals.css +5 -0
- package/packages/web/app/layout.tsx +2 -0
- package/packages/web/app/project/[id]/code/layout.tsx +18 -0
- package/packages/web/app/project/[id]/code/page.tsx +408 -0
- package/packages/web/app/project/[id]/page.tsx +359 -389
- package/packages/web/app/project/[id]/reports/page.tsx +59 -0
- package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
- package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
- package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
- package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
- package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
- package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
- package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
- package/packages/web/components/CommandBar/index.ts +1 -0
- package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
- package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
- package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
- package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
- package/packages/web/components/EventRow/EventRow.tsx +4 -4
- package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
- package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
- package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
- package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
- package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
- package/packages/web/components/MasonryGrid/index.ts +1 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
- package/packages/web/components/MomentumWidget/index.ts +2 -0
- package/packages/web/components/NowCard/NowCard.tsx +81 -56
- package/packages/web/components/NowCard/NowCard.types.ts +1 -0
- package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
- package/packages/web/components/PageHeader/index.ts +1 -0
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
- package/packages/web/components/ProjectColorDot/index.ts +1 -0
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
- package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
- package/packages/web/components/Providers/Providers.tsx +4 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
- package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
- package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
- package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
- package/packages/web/components/RecoverCard/index.ts +2 -0
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
- package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
- package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
- package/packages/web/components/StatsMasonry/index.ts +1 -0
- package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
- package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
- package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
- package/packages/web/components/TerminalDock/index.ts +2 -0
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
- package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
- package/packages/web/components/WeeklyReports/index.ts +4 -0
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
- package/packages/web/components/charts/SessionsChart.tsx +6 -3
- package/packages/web/components/ui/dialog.tsx +143 -0
- package/packages/web/components/ui/drawer.tsx +135 -0
- package/packages/web/components/ui/select.tsx +187 -0
- package/packages/web/context/GlobalTerminalContext.tsx +538 -0
- package/packages/web/lib/commands.ts +81 -0
- package/packages/web/lib/generate-week-report.ts +285 -0
- package/packages/web/lib/parse-prjct-files.ts +56 -55
- package/packages/web/lib/project-colors.ts +58 -0
- package/packages/web/lib/projects.ts +58 -5
- package/packages/web/lib/services/projects.server.ts +11 -1
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +5 -1
- package/templates/commands/analyze.md +39 -3
- package/templates/commands/ask.md +58 -3
- package/templates/commands/bug.md +117 -26
- package/templates/commands/dash.md +95 -158
- package/templates/commands/done.md +130 -148
- package/templates/commands/feature.md +125 -103
- package/templates/commands/git.md +18 -3
- package/templates/commands/idea.md +121 -38
- package/templates/commands/init.md +124 -20
- package/templates/commands/migrate-all.md +63 -28
- package/templates/commands/migrate.md +140 -0
- package/templates/commands/next.md +115 -5
- package/templates/commands/now.md +146 -82
- package/templates/commands/pause.md +89 -74
- package/templates/commands/redo.md +6 -4
- package/templates/commands/resume.md +141 -59
- package/templates/commands/ship.md +103 -231
- package/templates/commands/spec.md +98 -8
- package/templates/commands/suggest.md +22 -2
- package/templates/commands/sync.md +192 -203
- package/templates/commands/undo.md +6 -4
- package/core/data/agents-manager.ts +0 -76
- package/core/data/analysis-manager.ts +0 -83
- package/core/data/base-manager.ts +0 -156
- package/core/data/ideas-manager.ts +0 -81
- package/core/data/outcomes-manager.ts +0 -96
- package/core/data/project-manager.ts +0 -75
- package/core/data/roadmap-manager.ts +0 -118
- package/core/data/shipped-manager.ts +0 -65
- package/core/data/state-manager.ts +0 -214
- package/core/state/index.ts +0 -25
- package/core/state/manager.ts +0 -376
- package/core/state/types.ts +0 -185
- package/core/utils/project-capabilities.ts +0 -156
- package/core/view-generator.ts +0 -536
- package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
- package/packages/web/app/project/[id]/stats/page.tsx +0 -253
- package/templates/agent-assignment.md +0 -72
- package/templates/analysis/project-analysis.md +0 -78
- package/templates/checklists/accessibility.md +0 -33
- package/templates/commands/build.md +0 -17
- package/templates/commands/decision.md +0 -226
- package/templates/commands/fix.md +0 -79
- package/templates/commands/help.md +0 -61
- package/templates/commands/progress.md +0 -14
- package/templates/commands/recap.md +0 -14
- package/templates/commands/roadmap.md +0 -52
- package/templates/commands/status.md +0 -17
- package/templates/commands/task.md +0 -63
- package/templates/commands/work.md +0 -44
- package/templates/commands/workflow.md +0 -12
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Storage
|
|
3
|
+
*
|
|
4
|
+
* Manages task queue via storage/queue.json
|
|
5
|
+
* Generates context/next.md for Claude
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StorageManager } from './storage-manager'
|
|
9
|
+
import { generateUUID } from '../schemas'
|
|
10
|
+
import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
|
|
11
|
+
|
|
12
|
+
class QueueStorage extends StorageManager<QueueJson> {
|
|
13
|
+
constructor() {
|
|
14
|
+
super('queue.json')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected getDefault(): QueueJson {
|
|
18
|
+
return {
|
|
19
|
+
tasks: [],
|
|
20
|
+
lastUpdated: ''
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected getMdFilename(): string {
|
|
25
|
+
return 'next.md'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
29
|
+
return `queue.${action}d`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected toMarkdown(data: QueueJson): string {
|
|
33
|
+
const lines = ['# Priority Queue', '']
|
|
34
|
+
|
|
35
|
+
const activeTasks = data.tasks.filter(t => t.section === 'active' && !t.completed)
|
|
36
|
+
const backlogTasks = data.tasks.filter(t => t.section === 'backlog' && !t.completed)
|
|
37
|
+
const previouslyActive = data.tasks.filter(t => t.section === 'previously_active' && !t.completed)
|
|
38
|
+
|
|
39
|
+
// Active section
|
|
40
|
+
lines.push('## Active Tasks')
|
|
41
|
+
if (activeTasks.length > 0) {
|
|
42
|
+
activeTasks.forEach((task, i) => {
|
|
43
|
+
const checkbox = task.completed ? '[x]' : '[ ]'
|
|
44
|
+
const priority = task.priority !== 'medium' ? ` [${task.priority.toUpperCase()}]` : ''
|
|
45
|
+
const agent = task.agent ? ` @${task.agent}` : ''
|
|
46
|
+
const origin = task.originFeature ? ` (from: ${task.originFeature})` : ''
|
|
47
|
+
const bug = task.type === 'bug' ? ' \u{1F41B}' : ''
|
|
48
|
+
lines.push(`${i + 1}. ${checkbox}${bug}${priority} ${task.description}${agent}${origin}`)
|
|
49
|
+
})
|
|
50
|
+
} else {
|
|
51
|
+
lines.push('_No active tasks_')
|
|
52
|
+
}
|
|
53
|
+
lines.push('')
|
|
54
|
+
|
|
55
|
+
// Previously active section (if any)
|
|
56
|
+
if (previouslyActive.length > 0) {
|
|
57
|
+
lines.push('## Previously Active')
|
|
58
|
+
previouslyActive.forEach(task => {
|
|
59
|
+
lines.push(`- [ ] ${task.description}`)
|
|
60
|
+
})
|
|
61
|
+
lines.push('')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Backlog section
|
|
65
|
+
lines.push('## Backlog')
|
|
66
|
+
if (backlogTasks.length > 0) {
|
|
67
|
+
backlogTasks.forEach(task => {
|
|
68
|
+
const priority = task.priority !== 'medium' ? ` [${task.priority.toUpperCase()}]` : ''
|
|
69
|
+
const bug = task.type === 'bug' ? ' \u{1F41B}' : ''
|
|
70
|
+
lines.push(`- [ ]${bug}${priority} ${task.description}`)
|
|
71
|
+
})
|
|
72
|
+
} else {
|
|
73
|
+
lines.push('_No backlog items_')
|
|
74
|
+
}
|
|
75
|
+
lines.push('')
|
|
76
|
+
|
|
77
|
+
return lines.join('\n')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// =========== Domain Methods ===========
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get all tasks
|
|
84
|
+
*/
|
|
85
|
+
async getTasks(projectId: string): Promise<QueueTask[]> {
|
|
86
|
+
const queue = await this.read(projectId)
|
|
87
|
+
return queue.tasks
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get active (non-backlog) tasks
|
|
92
|
+
*/
|
|
93
|
+
async getActiveTasks(projectId: string): Promise<QueueTask[]> {
|
|
94
|
+
const queue = await this.read(projectId)
|
|
95
|
+
return queue.tasks.filter(t => t.section === 'active' && !t.completed)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get backlog tasks
|
|
100
|
+
*/
|
|
101
|
+
async getBacklog(projectId: string): Promise<QueueTask[]> {
|
|
102
|
+
const queue = await this.read(projectId)
|
|
103
|
+
return queue.tasks.filter(t => t.section === 'backlog' && !t.completed)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get next task (highest priority incomplete)
|
|
108
|
+
*/
|
|
109
|
+
async getNextTask(projectId: string): Promise<QueueTask | null> {
|
|
110
|
+
const tasks = await this.getActiveTasks(projectId)
|
|
111
|
+
return this.sortTasks(tasks)[0] || null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add a task to the queue
|
|
116
|
+
*/
|
|
117
|
+
async addTask(
|
|
118
|
+
projectId: string,
|
|
119
|
+
task: Omit<QueueTask, 'id' | 'createdAt' | 'completed' | 'completedAt'>
|
|
120
|
+
): Promise<QueueTask> {
|
|
121
|
+
const newTask: QueueTask = {
|
|
122
|
+
...task,
|
|
123
|
+
id: generateUUID(),
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
completed: false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await this.update(projectId, (queue) => ({
|
|
129
|
+
tasks: [...queue.tasks, newTask],
|
|
130
|
+
lastUpdated: new Date().toISOString()
|
|
131
|
+
}))
|
|
132
|
+
|
|
133
|
+
// Publish incremental event
|
|
134
|
+
await this.publishEvent(projectId, 'queue.task_added', {
|
|
135
|
+
taskId: newTask.id,
|
|
136
|
+
description: newTask.description,
|
|
137
|
+
priority: newTask.priority,
|
|
138
|
+
section: newTask.section
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return newTask
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Add multiple tasks
|
|
146
|
+
*/
|
|
147
|
+
async addTasks(
|
|
148
|
+
projectId: string,
|
|
149
|
+
tasks: Omit<QueueTask, 'id' | 'createdAt' | 'completed' | 'completedAt'>[]
|
|
150
|
+
): Promise<QueueTask[]> {
|
|
151
|
+
const now = new Date().toISOString()
|
|
152
|
+
const newTasks: QueueTask[] = tasks.map(task => ({
|
|
153
|
+
...task,
|
|
154
|
+
id: generateUUID(),
|
|
155
|
+
createdAt: now,
|
|
156
|
+
completed: false
|
|
157
|
+
}))
|
|
158
|
+
|
|
159
|
+
await this.update(projectId, (queue) => ({
|
|
160
|
+
tasks: [...queue.tasks, ...newTasks],
|
|
161
|
+
lastUpdated: now
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
// Publish event for batch add
|
|
165
|
+
await this.publishEvent(projectId, 'queue.tasks_added', {
|
|
166
|
+
count: newTasks.length,
|
|
167
|
+
tasks: newTasks.map(t => ({ id: t.id, description: t.description }))
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
return newTasks
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove a task
|
|
175
|
+
*/
|
|
176
|
+
async removeTask(projectId: string, taskId: string): Promise<void> {
|
|
177
|
+
await this.update(projectId, (queue) => ({
|
|
178
|
+
tasks: queue.tasks.filter(t => t.id !== taskId),
|
|
179
|
+
lastUpdated: new Date().toISOString()
|
|
180
|
+
}))
|
|
181
|
+
|
|
182
|
+
await this.publishEvent(projectId, 'queue.task_removed', { taskId })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Mark a task as completed
|
|
187
|
+
*/
|
|
188
|
+
async completeTask(projectId: string, taskId: string): Promise<QueueTask | null> {
|
|
189
|
+
let completedTask: QueueTask | null = null
|
|
190
|
+
|
|
191
|
+
await this.update(projectId, (queue) => {
|
|
192
|
+
const tasks = queue.tasks.map(t => {
|
|
193
|
+
if (t.id === taskId) {
|
|
194
|
+
completedTask = {
|
|
195
|
+
...t,
|
|
196
|
+
completed: true,
|
|
197
|
+
completedAt: new Date().toISOString()
|
|
198
|
+
}
|
|
199
|
+
return completedTask
|
|
200
|
+
}
|
|
201
|
+
return t
|
|
202
|
+
})
|
|
203
|
+
return { tasks, lastUpdated: new Date().toISOString() }
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
if (completedTask) {
|
|
207
|
+
await this.publishEvent(projectId, 'queue.task_completed', {
|
|
208
|
+
taskId,
|
|
209
|
+
description: completedTask.description,
|
|
210
|
+
completedAt: completedTask.completedAt
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return completedTask
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Move task to different section
|
|
219
|
+
*/
|
|
220
|
+
async moveToSection(
|
|
221
|
+
projectId: string,
|
|
222
|
+
taskId: string,
|
|
223
|
+
section: TaskSection
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
await this.update(projectId, (queue) => ({
|
|
226
|
+
tasks: queue.tasks.map(t =>
|
|
227
|
+
t.id === taskId ? { ...t, section } : t
|
|
228
|
+
),
|
|
229
|
+
lastUpdated: new Date().toISOString()
|
|
230
|
+
}))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set task priority
|
|
235
|
+
*/
|
|
236
|
+
async setPriority(
|
|
237
|
+
projectId: string,
|
|
238
|
+
taskId: string,
|
|
239
|
+
priority: Priority
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
await this.update(projectId, (queue) => ({
|
|
242
|
+
tasks: queue.tasks.map(t =>
|
|
243
|
+
t.id === taskId ? { ...t, priority } : t
|
|
244
|
+
),
|
|
245
|
+
lastUpdated: new Date().toISOString()
|
|
246
|
+
}))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clear completed tasks
|
|
251
|
+
*/
|
|
252
|
+
async clearCompleted(projectId: string): Promise<number> {
|
|
253
|
+
const queue = await this.read(projectId)
|
|
254
|
+
const completedCount = queue.tasks.filter(t => t.completed).length
|
|
255
|
+
|
|
256
|
+
await this.update(projectId, (q) => ({
|
|
257
|
+
tasks: q.tasks.filter(t => !t.completed),
|
|
258
|
+
lastUpdated: new Date().toISOString()
|
|
259
|
+
}))
|
|
260
|
+
|
|
261
|
+
return completedCount
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Sort tasks by priority and section
|
|
266
|
+
*/
|
|
267
|
+
private sortTasks(tasks: QueueTask[]): QueueTask[] {
|
|
268
|
+
const priorityOrder: Record<Priority, number> = {
|
|
269
|
+
critical: 0,
|
|
270
|
+
high: 1,
|
|
271
|
+
medium: 2,
|
|
272
|
+
low: 3
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const sectionOrder: Record<TaskSection, number> = {
|
|
276
|
+
active: 0,
|
|
277
|
+
previously_active: 1,
|
|
278
|
+
backlog: 2
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return [...tasks].sort((a, b) => {
|
|
282
|
+
// Section first
|
|
283
|
+
const sectionDiff = sectionOrder[a.section] - sectionOrder[b.section]
|
|
284
|
+
if (sectionDiff !== 0) return sectionDiff
|
|
285
|
+
|
|
286
|
+
// Then priority
|
|
287
|
+
const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority]
|
|
288
|
+
if (priorityDiff !== 0) return priorityDiff
|
|
289
|
+
|
|
290
|
+
// Then creation date
|
|
291
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const queueStorage = new QueueStorage()
|
|
297
|
+
export default queueStorage
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped Storage
|
|
3
|
+
*
|
|
4
|
+
* Manages shipped features via storage/shipped.json
|
|
5
|
+
* Generates context/shipped.md for Claude
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StorageManager } from './storage-manager'
|
|
9
|
+
import { generateUUID } from '../schemas'
|
|
10
|
+
|
|
11
|
+
export interface ShippedFeature {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
shippedAt: string
|
|
15
|
+
version: string
|
|
16
|
+
description?: string
|
|
17
|
+
tasks?: string[] // Task IDs that were part of this ship
|
|
18
|
+
duration?: string // How long it took
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ShippedJson {
|
|
22
|
+
shipped: ShippedFeature[]
|
|
23
|
+
lastUpdated: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ShippedStorage extends StorageManager<ShippedJson> {
|
|
27
|
+
constructor() {
|
|
28
|
+
super('shipped.json')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected getDefault(): ShippedJson {
|
|
32
|
+
return {
|
|
33
|
+
shipped: [],
|
|
34
|
+
lastUpdated: ''
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected getMdFilename(): string {
|
|
39
|
+
return 'shipped.md'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected getEventType(action: 'update' | 'create' | 'delete'): string {
|
|
43
|
+
return `shipped.${action}d`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected toMarkdown(data: ShippedJson): string {
|
|
47
|
+
const lines = ['# SHIPPED \u{1F680}', '']
|
|
48
|
+
|
|
49
|
+
if (data.shipped.length === 0) {
|
|
50
|
+
lines.push('_No features shipped yet. Use /p:ship to celebrate!_')
|
|
51
|
+
lines.push('')
|
|
52
|
+
return lines.join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Group by month
|
|
56
|
+
const byMonth = new Map<string, ShippedFeature[]>()
|
|
57
|
+
|
|
58
|
+
data.shipped.forEach(ship => {
|
|
59
|
+
const date = new Date(ship.shippedAt)
|
|
60
|
+
const month = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' })
|
|
61
|
+
|
|
62
|
+
if (!byMonth.has(month)) {
|
|
63
|
+
byMonth.set(month, [])
|
|
64
|
+
}
|
|
65
|
+
byMonth.get(month)!.push(ship)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Render by month (most recent first)
|
|
69
|
+
const sortedMonths = Array.from(byMonth.keys()).sort((a, b) => {
|
|
70
|
+
const dateA = new Date(byMonth.get(a)![0].shippedAt)
|
|
71
|
+
const dateB = new Date(byMonth.get(b)![0].shippedAt)
|
|
72
|
+
return dateB.getTime() - dateA.getTime()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
sortedMonths.forEach(month => {
|
|
76
|
+
lines.push(`## ${month}`)
|
|
77
|
+
lines.push('')
|
|
78
|
+
|
|
79
|
+
const ships = byMonth.get(month)!.sort(
|
|
80
|
+
(a, b) => new Date(b.shippedAt).getTime() - new Date(a.shippedAt).getTime()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
ships.forEach(ship => {
|
|
84
|
+
const date = new Date(ship.shippedAt).toLocaleDateString('en-US', {
|
|
85
|
+
month: 'short',
|
|
86
|
+
day: 'numeric'
|
|
87
|
+
})
|
|
88
|
+
const version = ship.version ? ` v${ship.version}` : ''
|
|
89
|
+
const duration = ship.duration ? ` (${ship.duration})` : ''
|
|
90
|
+
lines.push(`- **${ship.name}**${version}${duration} - ${date}`)
|
|
91
|
+
if (ship.description) {
|
|
92
|
+
lines.push(` _${ship.description}_`)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
lines.push('')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Stats
|
|
100
|
+
lines.push('---')
|
|
101
|
+
lines.push('')
|
|
102
|
+
lines.push(`**Total shipped:** ${data.shipped.length}`)
|
|
103
|
+
lines.push('')
|
|
104
|
+
|
|
105
|
+
return lines.join('\n')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =========== Domain Methods ===========
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get all shipped features
|
|
112
|
+
*/
|
|
113
|
+
async getAll(projectId: string): Promise<ShippedFeature[]> {
|
|
114
|
+
const data = await this.read(projectId)
|
|
115
|
+
return data.shipped
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get recent shipped features
|
|
120
|
+
*/
|
|
121
|
+
async getRecent(projectId: string, limit: number = 5): Promise<ShippedFeature[]> {
|
|
122
|
+
const data = await this.read(projectId)
|
|
123
|
+
return data.shipped
|
|
124
|
+
.sort((a, b) => new Date(b.shippedAt).getTime() - new Date(a.shippedAt).getTime())
|
|
125
|
+
.slice(0, limit)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Add a shipped feature
|
|
130
|
+
*/
|
|
131
|
+
async addShipped(
|
|
132
|
+
projectId: string,
|
|
133
|
+
feature: Omit<ShippedFeature, 'id' | 'shippedAt'>
|
|
134
|
+
): Promise<ShippedFeature> {
|
|
135
|
+
const shipped: ShippedFeature = {
|
|
136
|
+
...feature,
|
|
137
|
+
id: generateUUID(),
|
|
138
|
+
shippedAt: new Date().toISOString()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await this.update(projectId, (data) => ({
|
|
142
|
+
shipped: [shipped, ...data.shipped], // Prepend
|
|
143
|
+
lastUpdated: new Date().toISOString()
|
|
144
|
+
}))
|
|
145
|
+
|
|
146
|
+
// Publish event
|
|
147
|
+
await this.publishEvent(projectId, 'feature.shipped', {
|
|
148
|
+
shipId: shipped.id,
|
|
149
|
+
name: shipped.name,
|
|
150
|
+
version: shipped.version,
|
|
151
|
+
shippedAt: shipped.shippedAt
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return shipped
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get shipped by version
|
|
159
|
+
*/
|
|
160
|
+
async getByVersion(
|
|
161
|
+
projectId: string,
|
|
162
|
+
version: string
|
|
163
|
+
): Promise<ShippedFeature | undefined> {
|
|
164
|
+
const data = await this.read(projectId)
|
|
165
|
+
return data.shipped.find(s => s.version === version)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get count
|
|
170
|
+
*/
|
|
171
|
+
async getCount(projectId: string): Promise<number> {
|
|
172
|
+
const data = await this.read(projectId)
|
|
173
|
+
return data.shipped.length
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get shipped in date range
|
|
178
|
+
*/
|
|
179
|
+
async getByDateRange(
|
|
180
|
+
projectId: string,
|
|
181
|
+
startDate: Date,
|
|
182
|
+
endDate: Date
|
|
183
|
+
): Promise<ShippedFeature[]> {
|
|
184
|
+
const data = await this.read(projectId)
|
|
185
|
+
return data.shipped.filter(s => {
|
|
186
|
+
const date = new Date(s.shippedAt)
|
|
187
|
+
return date >= startDate && date <= endDate
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get stats for a period
|
|
193
|
+
*/
|
|
194
|
+
async getStats(
|
|
195
|
+
projectId: string,
|
|
196
|
+
period: 'week' | 'month' | 'year' = 'month'
|
|
197
|
+
): Promise<{ count: number; period: string }> {
|
|
198
|
+
const now = new Date()
|
|
199
|
+
let startDate: Date
|
|
200
|
+
|
|
201
|
+
switch (period) {
|
|
202
|
+
case 'week':
|
|
203
|
+
startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
204
|
+
break
|
|
205
|
+
case 'month':
|
|
206
|
+
startDate = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
207
|
+
break
|
|
208
|
+
case 'year':
|
|
209
|
+
startDate = new Date(now.getFullYear(), 0, 1)
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const shipped = await this.getByDateRange(projectId, startDate, now)
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
count: shipped.length,
|
|
217
|
+
period
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const shippedStorage = new ShippedStorage()
|
|
223
|
+
export default shippedStorage
|