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.
Files changed (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. 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