prjct-cli 0.18.2 → 0.20.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 (246) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CLAUDE.md +74 -211
  3. package/core/agentic/prompt-builder.ts +3 -7
  4. package/core/command-registry/optional-commands.ts +0 -20
  5. package/core/infrastructure/command-installer/command-installer.ts +8 -1
  6. package/core/infrastructure/command-installer/global-config.ts +31 -1
  7. package/core/infrastructure/command-installer/index.ts +1 -1
  8. package/core/infrastructure/setup.ts +3 -0
  9. package/package.json +3 -17
  10. package/templates/agentic/agents/uxui.md +210 -0
  11. package/templates/commands/bug.md +219 -41
  12. package/templates/commands/done.md +57 -258
  13. package/templates/commands/feature.md +368 -80
  14. package/templates/commands/now.md +72 -277
  15. package/templates/commands/ship.md +167 -246
  16. package/templates/commands/sync.md +62 -3
  17. package/templates/commands/test.md +160 -20
  18. package/templates/global/CLAUDE.md +40 -205
  19. package/templates/global/docs/agents.md +88 -0
  20. package/templates/global/docs/architecture.md +103 -0
  21. package/templates/global/docs/commands.md +98 -0
  22. package/templates/global/docs/validation.md +95 -0
  23. package/bin/dev.js +0 -216
  24. package/bin/serve.js +0 -361
  25. package/packages/web/README.md +0 -36
  26. package/packages/web/app/api/claude/sessions/route.ts +0 -44
  27. package/packages/web/app/api/claude/status/route.ts +0 -34
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
  29. package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
  30. package/packages/web/app/api/projects/[id]/route.ts +0 -29
  31. package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
  32. package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
  33. package/packages/web/app/api/projects/route.ts +0 -16
  34. package/packages/web/app/api/sessions/current/route.ts +0 -132
  35. package/packages/web/app/api/sessions/history/route.ts +0 -204
  36. package/packages/web/app/error.tsx +0 -34
  37. package/packages/web/app/favicon.ico +0 -0
  38. package/packages/web/app/globals.css +0 -198
  39. package/packages/web/app/layout.tsx +0 -53
  40. package/packages/web/app/loading.tsx +0 -7
  41. package/packages/web/app/not-found.tsx +0 -25
  42. package/packages/web/app/page.tsx +0 -12
  43. package/packages/web/app/project/[id]/code/layout.tsx +0 -18
  44. package/packages/web/app/project/[id]/code/page.tsx +0 -408
  45. package/packages/web/app/project/[id]/error.tsx +0 -41
  46. package/packages/web/app/project/[id]/loading.tsx +0 -9
  47. package/packages/web/app/project/[id]/not-found.tsx +0 -27
  48. package/packages/web/app/project/[id]/page.tsx +0 -384
  49. package/packages/web/app/project/[id]/reports/page.tsx +0 -59
  50. package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
  51. package/packages/web/app/sessions/page.tsx +0 -165
  52. package/packages/web/app/settings/page.tsx +0 -151
  53. package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
  54. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
  55. package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
  56. package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
  57. package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
  58. package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
  59. package/packages/web/components/ActivityTimeline/index.ts +0 -2
  60. package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
  61. package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
  62. package/packages/web/components/AgentsCard/index.ts +0 -2
  63. package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
  64. package/packages/web/components/AppSidebar/index.ts +0 -1
  65. package/packages/web/components/BackLink/BackLink.tsx +0 -18
  66. package/packages/web/components/BackLink/BackLink.types.ts +0 -5
  67. package/packages/web/components/BackLink/index.ts +0 -2
  68. package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
  69. package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
  70. package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
  71. package/packages/web/components/BentoCard/index.ts +0 -2
  72. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
  73. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
  74. package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
  75. package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
  76. package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
  77. package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
  78. package/packages/web/components/BentoGrid/index.ts +0 -2
  79. package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
  80. package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
  81. package/packages/web/components/BlockersCard/index.ts +0 -2
  82. package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
  83. package/packages/web/components/CommandBar/index.ts +0 -1
  84. package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
  85. package/packages/web/components/CommandButton/index.ts +0 -1
  86. package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
  87. package/packages/web/components/ConnectionStatus/index.ts +0 -1
  88. package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
  89. package/packages/web/components/DashboardContent/index.ts +0 -1
  90. package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
  91. package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
  92. package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
  93. package/packages/web/components/DateGroup/index.ts +0 -2
  94. package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
  95. package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
  96. package/packages/web/components/EmptyState/index.ts +0 -2
  97. package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
  98. package/packages/web/components/EventRow/EventRow.tsx +0 -49
  99. package/packages/web/components/EventRow/EventRow.types.ts +0 -7
  100. package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
  101. package/packages/web/components/EventRow/index.ts +0 -2
  102. package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
  103. package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
  104. package/packages/web/components/ExpandButton/index.ts +0 -2
  105. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
  106. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
  107. package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
  108. package/packages/web/components/HealthGradientBackground/index.ts +0 -2
  109. package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
  110. package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
  111. package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
  112. package/packages/web/components/HeroSection/hooks/index.ts +0 -2
  113. package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
  114. package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
  115. package/packages/web/components/HeroSection/index.ts +0 -2
  116. package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
  117. package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
  118. package/packages/web/components/IdeasCard/index.ts +0 -2
  119. package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
  120. package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
  121. package/packages/web/components/InsightMessage/index.ts +0 -2
  122. package/packages/web/components/Logo/Logo.tsx +0 -65
  123. package/packages/web/components/Logo/index.ts +0 -1
  124. package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
  125. package/packages/web/components/MarkdownContent/index.ts +0 -1
  126. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
  127. package/packages/web/components/MasonryGrid/index.ts +0 -1
  128. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
  129. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
  130. package/packages/web/components/MomentumWidget/index.ts +0 -2
  131. package/packages/web/components/NowCard/NowCard.tsx +0 -118
  132. package/packages/web/components/NowCard/NowCard.types.ts +0 -16
  133. package/packages/web/components/NowCard/index.ts +0 -2
  134. package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
  135. package/packages/web/components/PageHeader/index.ts +0 -1
  136. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
  137. package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
  138. package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
  139. package/packages/web/components/ProgressRing/index.ts +0 -2
  140. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
  141. package/packages/web/components/ProjectAvatar/index.ts +0 -1
  142. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
  143. package/packages/web/components/ProjectColorDot/index.ts +0 -1
  144. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
  145. package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
  146. package/packages/web/components/Providers/Providers.tsx +0 -48
  147. package/packages/web/components/Providers/index.ts +0 -1
  148. package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
  149. package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
  150. package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
  151. package/packages/web/components/QueueCard/index.ts +0 -2
  152. package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
  153. package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
  154. package/packages/web/components/RecoverCard/index.ts +0 -2
  155. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
  156. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
  157. package/packages/web/components/RoadmapCard/index.ts +0 -2
  158. package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
  159. package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
  160. package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
  161. package/packages/web/components/ShipsCard/index.ts +0 -2
  162. package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
  163. package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
  164. package/packages/web/components/SparklineChart/index.ts +0 -2
  165. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
  166. package/packages/web/components/StatsMasonry/index.ts +0 -1
  167. package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
  168. package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
  169. package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
  170. package/packages/web/components/StreakCard/index.ts +0 -2
  171. package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
  172. package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
  173. package/packages/web/components/TasksCounter/index.ts +0 -2
  174. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
  175. package/packages/web/components/TechStackBadges/index.ts +0 -1
  176. package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
  177. package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
  178. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
  179. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
  180. package/packages/web/components/TerminalDock/index.ts +0 -2
  181. package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
  182. package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
  183. package/packages/web/components/TerminalTabs/index.ts +0 -1
  184. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
  185. package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
  186. package/packages/web/components/VelocityBadge/index.ts +0 -2
  187. package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
  188. package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
  189. package/packages/web/components/VelocityCard/index.ts +0 -2
  190. package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
  191. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
  192. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
  193. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
  194. package/packages/web/components/WeeklyReports/index.ts +0 -4
  195. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
  196. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
  197. package/packages/web/components/WeeklySparkline/index.ts +0 -2
  198. package/packages/web/components/charts/SessionsChart.tsx +0 -175
  199. package/packages/web/components/ui/alert-dialog.tsx +0 -157
  200. package/packages/web/components/ui/badge.tsx +0 -46
  201. package/packages/web/components/ui/button.tsx +0 -60
  202. package/packages/web/components/ui/card.tsx +0 -92
  203. package/packages/web/components/ui/chart.tsx +0 -385
  204. package/packages/web/components/ui/dialog.tsx +0 -143
  205. package/packages/web/components/ui/drawer.tsx +0 -135
  206. package/packages/web/components/ui/dropdown-menu.tsx +0 -257
  207. package/packages/web/components/ui/input.tsx +0 -21
  208. package/packages/web/components/ui/scroll-area.tsx +0 -58
  209. package/packages/web/components/ui/select.tsx +0 -187
  210. package/packages/web/components/ui/sheet.tsx +0 -139
  211. package/packages/web/components/ui/tabs.tsx +0 -66
  212. package/packages/web/components/ui/tooltip.tsx +0 -61
  213. package/packages/web/components.json +0 -22
  214. package/packages/web/context/GlobalTerminalContext.tsx +0 -538
  215. package/packages/web/context/TerminalContext.tsx +0 -45
  216. package/packages/web/context/TerminalTabsContext.tsx +0 -181
  217. package/packages/web/eslint.config.mjs +0 -18
  218. package/packages/web/hooks/useClaudeTerminal.ts +0 -425
  219. package/packages/web/hooks/useProjectStats.ts +0 -93
  220. package/packages/web/hooks/useProjects.ts +0 -73
  221. package/packages/web/lib/actions/projects.ts +0 -15
  222. package/packages/web/lib/commands.ts +0 -81
  223. package/packages/web/lib/format.ts +0 -23
  224. package/packages/web/lib/generate-week-report.ts +0 -285
  225. package/packages/web/lib/parse-prjct-files.ts +0 -1123
  226. package/packages/web/lib/project-colors.ts +0 -58
  227. package/packages/web/lib/projects.ts +0 -506
  228. package/packages/web/lib/pty.ts +0 -101
  229. package/packages/web/lib/query-config.ts +0 -44
  230. package/packages/web/lib/services/index.ts +0 -9
  231. package/packages/web/lib/services/projects.server.ts +0 -66
  232. package/packages/web/lib/services/stats.server.ts +0 -562
  233. package/packages/web/lib/unified-loader.ts +0 -396
  234. package/packages/web/lib/utils.ts +0 -6
  235. package/packages/web/next-env.d.ts +0 -6
  236. package/packages/web/next.config.ts +0 -7
  237. package/packages/web/package.json +0 -57
  238. package/packages/web/postcss.config.mjs +0 -7
  239. package/packages/web/public/file.svg +0 -1
  240. package/packages/web/public/globe.svg +0 -1
  241. package/packages/web/public/next.svg +0 -1
  242. package/packages/web/public/vercel.svg +0 -1
  243. package/packages/web/public/window.svg +0 -1
  244. package/packages/web/server.ts +0 -312
  245. package/packages/web/tsconfig.json +0 -34
  246. package/templates/commands/serve.md +0 -121
@@ -1,9 +0,0 @@
1
- /**
2
- * Services Index
3
- *
4
- * Server-only services for direct data access.
5
- * Import these in Server Components instead of using API routes.
6
- */
7
-
8
- export * from './stats.server'
9
- export * from './projects.server'
@@ -1,66 +0,0 @@
1
- /**
2
- * Projects Service (Server-only)
3
- *
4
- * MD-First Architecture: Reads directly from MD files.
5
- * No JSON fallback - MD is the source of truth.
6
- */
7
-
8
- import 'server-only'
9
- import { cache } from 'react'
10
- import { getProjects as getProjectsList, getProject as getMdProject } from '@/lib/projects'
11
-
12
- // Types for project data
13
- export interface ProjectJson {
14
- projectId: string
15
- name: string
16
- repoPath?: string | null
17
- techStack: string[]
18
- fileCount: number
19
- commitCount: number
20
- createdAt: string
21
- lastSync: string
22
- version?: string | null
23
- // Counts - DRY: single source of truth for dashboard/detail
24
- shippedCount: number
25
- nextTasksCount: number
26
- completionRate: number
27
- }
28
-
29
- /**
30
- * Get single project by ID - cached per request
31
- *
32
- * MD-First: Uses MD files as source of truth
33
- */
34
- export const getProject = cache(async (projectId: string): Promise<ProjectJson | null> => {
35
- try {
36
- const project = await getMdProject(projectId)
37
- if (project) {
38
- return {
39
- projectId: project.id,
40
- name: project.name,
41
- repoPath: project.repoPath,
42
- techStack: project.techStack || [],
43
- fileCount: project.filesCount ? parseInt(project.filesCount) : 0,
44
- commitCount: project.commitsCount ? parseInt(project.commitsCount) : 0,
45
- createdAt: new Date().toISOString(),
46
- lastSync: new Date().toISOString(),
47
- version: project.version || null,
48
- // DRY: Pass through counts from lib/projects.ts (single source of truth)
49
- shippedCount: project.shippedCount ?? 0,
50
- nextTasksCount: project.nextTasksCount ?? 0,
51
- completionRate: project.completionRate ?? 0
52
- }
53
- }
54
- } catch {
55
- // Project not found
56
- }
57
-
58
- return null
59
- })
60
-
61
- /**
62
- * Get all projects - cached per request
63
- */
64
- export const getProjects = cache(async () => {
65
- return getProjectsList()
66
- })
@@ -1,562 +0,0 @@
1
- /**
2
- * Stats Service (Server-only)
3
- *
4
- * MD-First Architecture: Reads directly from MD files.
5
- * No JSON fallback - MD is the source of truth.
6
- */
7
-
8
- import 'server-only'
9
- import { cache } from 'react'
10
- import { exec } from 'child_process'
11
- import { promisify } from 'util'
12
- import { getProjectStats as getMdStats, type ProjectStats, type SessionDay } from '@/lib/parse-prjct-files'
13
- import { getProjects } from './projects.server'
14
-
15
- // Types for MD-based stats
16
- export interface StateJson {
17
- currentTask: {
18
- id?: string
19
- description: string
20
- startedAt?: string
21
- sessionId?: string
22
- feature?: string
23
- agent?: string
24
- } | null
25
- previousTask?: {
26
- id?: string
27
- description: string
28
- status: string
29
- startedAt?: string
30
- pausedAt?: string
31
- } | null
32
- lastUpdated?: string
33
- }
34
-
35
- export interface QueueTask {
36
- id: string
37
- description: string
38
- priority: 'low' | 'medium' | 'high' | 'critical'
39
- type: 'feature' | 'bug' | 'improvement' | 'chore'
40
- completed: boolean
41
- createdAt: string
42
- completedAt?: string
43
- section: 'active' | 'backlog' | 'previously_active'
44
- agent?: string
45
- originFeature?: string
46
- }
47
-
48
- export interface QueueJson {
49
- tasks: QueueTask[]
50
- lastUpdated: string
51
- }
52
-
53
- export interface MetricsJson {
54
- recentActivity?: Array<{ timestamp: string; type?: string; description?: string; action?: string }>
55
- velocity?: { tasksPerDay?: number }
56
- currentSprint?: { tasksCompleted?: number }
57
- }
58
-
59
- export interface Blocker {
60
- task: string
61
- reason: string
62
- since: string
63
- daysBlocked: number
64
- }
65
-
66
- export interface ProjectInsights {
67
- healthScore: number
68
- estimateAccuracy: number
69
- blockers: Blocker[]
70
- recommendations: string[]
71
- }
72
-
73
- export interface RoadmapFeature {
74
- name: string
75
- status?: 'pending' | 'active' | 'shipped' | 'completed'
76
- tasks: Array<{
77
- description: string
78
- completed: boolean
79
- }>
80
- }
81
-
82
- export interface ShippedItem {
83
- name: string
84
- date?: string
85
- shippedAt?: string
86
- duration?: string
87
- }
88
-
89
- export interface UnifiedJsonData {
90
- state: StateJson | null
91
- queue: QueueJson | null
92
- metrics: MetricsJson | null
93
- insights: ProjectInsights
94
- agents: Array<{
95
- name: string
96
- role?: string
97
- description?: string
98
- successRate?: number
99
- tasksCompleted?: number
100
- bestFor?: string[]
101
- }>
102
- ideas: { ideas: Array<{ text: string; status?: string; priority?: string }> } | null
103
- roadmap: { features: RoadmapFeature[] } | null
104
- shipped: { items: ShippedItem[] } | null
105
- outcomes: Array<{ type: string }>
106
- hasJsonData: boolean
107
- }
108
-
109
- // Activity type for recent activity tracking
110
- export interface RecentActivity {
111
- timestamp: string
112
- type: string
113
- description?: string
114
- duration?: string
115
- }
116
-
117
- const execAsync = promisify(exec)
118
-
119
- /**
120
- * Global stats for dashboard (userName, totalProjects)
121
- */
122
- export interface GlobalStats {
123
- userName: string
124
- totalProjects: number
125
- }
126
-
127
- async function getGitUserName(): Promise<string> {
128
- try {
129
- const { stdout } = await execAsync('git config user.name')
130
- return stdout.trim() || 'Developer'
131
- } catch {
132
- return 'Developer'
133
- }
134
- }
135
-
136
- /**
137
- * Get global stats for dashboard - cached per request
138
- */
139
- export const getGlobalStats = cache(async (): Promise<GlobalStats> => {
140
- const [projects, userName] = await Promise.all([
141
- getProjects(),
142
- getGitUserName()
143
- ])
144
-
145
- return {
146
- userName,
147
- totalProjects: projects.length
148
- }
149
- })
150
-
151
- /**
152
- * Unified stats result that works with both JSON and legacy formats
153
- */
154
- export interface StatsResult {
155
- state: StateJson | null
156
- queue: QueueJson | null
157
- metrics: MetricsJson | null
158
- insights: ProjectInsights
159
- agents: UnifiedJsonData['agents']
160
- ideas: UnifiedJsonData['ideas']
161
- roadmap: UnifiedJsonData['roadmap']
162
- shipped: UnifiedJsonData['shipped']
163
- outcomes: UnifiedJsonData['outcomes']
164
- hasData: boolean
165
- isLegacy: boolean
166
- legacyStats?: ProjectStats
167
- }
168
-
169
- const DEFAULT_INSIGHTS: ProjectInsights = {
170
- healthScore: 0,
171
- estimateAccuracy: 0,
172
- blockers: [],
173
- recommendations: ['Run /p:sync to initialize project']
174
- }
175
-
176
- const EMPTY_STATS_RESULT: StatsResult = {
177
- state: null,
178
- queue: null,
179
- metrics: null,
180
- insights: DEFAULT_INSIGHTS,
181
- agents: [],
182
- ideas: null,
183
- roadmap: null,
184
- shipped: null,
185
- outcomes: [],
186
- hasData: false,
187
- isLegacy: false
188
- }
189
-
190
- /**
191
- * Calculate estimate accuracy from sessions
192
- * Returns percentage of tasks completed within ±20% of estimate
193
- */
194
- function calculateEstimateAccuracy(sessions: SessionDay[]): number {
195
- let tasksWithEstimate = 0
196
- let accurateTasks = 0
197
-
198
- for (const session of sessions) {
199
- for (const event of session.events) {
200
- // Look for task_complete events with estimate and actual duration
201
- if (event.type === 'task_complete' || event.type === 'session_completed') {
202
- const e = event as { estimate?: string | number; duration?: string | number; actual?: number }
203
- if (e.estimate && (e.duration || e.actual)) {
204
- tasksWithEstimate++
205
-
206
- // Parse estimate (e.g., "2h" -> 7200 seconds, or raw number)
207
- const estimateSec = typeof e.estimate === 'number'
208
- ? e.estimate
209
- : parseTimeToSeconds(String(e.estimate))
210
-
211
- // Parse actual
212
- const actualSec = typeof e.actual === 'number'
213
- ? e.actual
214
- : typeof e.duration === 'number'
215
- ? e.duration
216
- : parseTimeToSeconds(String(e.duration || '0'))
217
-
218
- if (estimateSec > 0 && actualSec > 0) {
219
- const ratio = actualSec / estimateSec
220
- // Within ±20% is "accurate"
221
- if (ratio >= 0.8 && ratio <= 1.2) {
222
- accurateTasks++
223
- }
224
- }
225
- }
226
- }
227
- }
228
- }
229
-
230
- return tasksWithEstimate > 0
231
- ? Math.round((accurateTasks / tasksWithEstimate) * 100)
232
- : 0
233
- }
234
-
235
- /**
236
- * Parse time string to seconds (e.g., "2h" -> 7200, "30m" -> 1800)
237
- */
238
- function parseTimeToSeconds(time: string): number {
239
- const hours = time.match(/(\d+(?:\.\d+)?)\s*h/i)
240
- const minutes = time.match(/(\d+(?:\.\d+)?)\s*m/i)
241
- const seconds = time.match(/(\d+(?:\.\d+)?)\s*s/i)
242
-
243
- let total = 0
244
- if (hours) total += parseFloat(hours[1]) * 3600
245
- if (minutes) total += parseFloat(minutes[1]) * 60
246
- if (seconds) total += parseFloat(seconds[1])
247
-
248
- // If just a number, assume seconds
249
- if (total === 0 && /^\d+$/.test(time.trim())) {
250
- total = parseInt(time.trim())
251
- }
252
-
253
- return total
254
- }
255
-
256
- /**
257
- * Extract blockers from timeline events
258
- */
259
- function extractBlockers(timeline: Array<{ ts: string; type: string }>): Blocker[] {
260
- const blockers: Blocker[] = []
261
- const now = new Date()
262
-
263
- for (const event of timeline) {
264
- // Look for pause events with reason "blocked"
265
- if (event.type === 'pause' || event.type === 'session_paused') {
266
- const e = event as { reason?: string; note?: string; task?: string; ts: string }
267
- if (e.reason === 'blocked') {
268
- const pauseDate = new Date(e.ts)
269
- const daysBlocked = Math.floor((now.getTime() - pauseDate.getTime()) / (1000 * 60 * 60 * 24))
270
-
271
- blockers.push({
272
- task: e.task || 'Unknown task',
273
- reason: e.note || 'Blocked',
274
- since: e.ts,
275
- daysBlocked
276
- })
277
- }
278
- }
279
- }
280
-
281
- // Only return unresolved blockers (check if there's a resume after the pause)
282
- // For now, return all - we'll refine this when we have resume tracking
283
- return blockers.slice(0, 5) // Top 5 blockers
284
- }
285
-
286
- /**
287
- * Get project stats - cached per request
288
- *
289
- * MD-First Architecture: MD files are the source of truth.
290
- * No JSON fallback - all data comes from MD.
291
- */
292
- export const getStats = cache(async (projectId: string): Promise<StatsResult> => {
293
- try {
294
- const mdStats = await getMdStats(projectId)
295
-
296
- // Check if we have meaningful data
297
- const hasData = Boolean(
298
- mdStats.currentTask ||
299
- mdStats.queue.length > 0 ||
300
- mdStats.shipped.length > 0 ||
301
- mdStats.timeline.length > 0 ||
302
- mdStats.summary.totalEvents > 0
303
- )
304
-
305
- if (hasData) {
306
- // Calculate real metrics
307
- const estimateAccuracy = calculateEstimateAccuracy(mdStats.sessions)
308
- const blockers = extractBlockers(mdStats.timeline)
309
-
310
- return {
311
- ...EMPTY_STATS_RESULT,
312
- insights: {
313
- healthScore: calculateHealthScoreV2(mdStats, estimateAccuracy, blockers),
314
- estimateAccuracy,
315
- blockers,
316
- recommendations: generateRecommendations(mdStats, estimateAccuracy, blockers)
317
- },
318
- hasData: true,
319
- isLegacy: true,
320
- legacyStats: mdStats
321
- }
322
- }
323
- } catch {
324
- // MD parsing failed - return empty stats
325
- }
326
-
327
- return EMPTY_STATS_RESULT
328
- })
329
-
330
- /**
331
- * Calculate health score V2 - based on real data
332
- *
333
- * Formula:
334
- * - estimateAccuracy (25): % of tasks within ±20% of estimate
335
- * - completionRate (25): tasks completed / tasks started (last 7 days)
336
- * - noBlockers (25): penalize if blocked tasks exist
337
- * - recentActivity (25): days active in last week
338
- */
339
- function calculateHealthScoreV2(
340
- stats: ProjectStats,
341
- estimateAccuracy: number,
342
- blockers: Blocker[]
343
- ): number {
344
- const { timeline, sessions, currentTask } = stats
345
-
346
- // Estimate accuracy score (0-25)
347
- // If no estimates yet, give benefit of doubt (15/25)
348
- const accuracyScore = estimateAccuracy > 0
349
- ? Math.round((estimateAccuracy / 100) * 25)
350
- : 15
351
-
352
- // Completion rate (0-25)
353
- const recentSessions = sessions.slice(0, 7)
354
- let tasksStarted = 0
355
- let tasksCompleted = 0
356
- for (const session of recentSessions) {
357
- tasksStarted += session.tasksStarted
358
- tasksCompleted += session.tasksCompleted
359
- }
360
- const completionRate = tasksStarted > 0 ? tasksCompleted / tasksStarted : 0
361
- const completionScore = Math.round(Math.min(1, completionRate) * 25)
362
-
363
- // No blockers score (0-25)
364
- // Full points if no blockers, -5 per blocker, -10 per blocker > 3 days
365
- let blockerPenalty = 0
366
- for (const blocker of blockers) {
367
- blockerPenalty += blocker.daysBlocked > 3 ? 10 : 5
368
- }
369
- const blockerScore = Math.max(0, 25 - blockerPenalty)
370
-
371
- // Activity score (0-25)
372
- // Count unique active days in last 7 days
373
- const lastWeek = new Date()
374
- lastWeek.setDate(lastWeek.getDate() - 7)
375
- const recentDays = new Set(
376
- timeline
377
- .filter(e => new Date(e.ts) > lastWeek)
378
- .map(e => e.ts.split('T')[0])
379
- )
380
- // Bonus for having current task
381
- const activityBase = Math.min(7, recentDays.size) * 3 // up to 21
382
- const currentTaskBonus = currentTask ? 4 : 0
383
- const activityScore = Math.min(25, activityBase + currentTaskBonus)
384
-
385
- return Math.min(100, accuracyScore + completionScore + blockerScore + activityScore)
386
- }
387
-
388
- /**
389
- * Legacy health score calculation (kept for fallback)
390
- */
391
- function calculateHealthFromMd(stats: ProjectStats): number {
392
- const { metrics, currentTask, queue, timeline } = stats
393
- const velocity = metrics?.velocity?.tasksPerDay ?? 0
394
- const hasCurrentTask = Boolean(currentTask)
395
- const queueSize = queue?.length ?? 0
396
- const recentActivity = timeline?.slice(0, 7).length ?? 0
397
-
398
- const velocityScore = Math.min(30, velocity * 15)
399
- const taskScore = hasCurrentTask ? 20 : 0
400
- const queueScore = queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
401
- const activityScore = Math.min(30, recentActivity * 5)
402
-
403
- return Math.min(100, Math.round(velocityScore + taskScore + queueScore + activityScore))
404
- }
405
-
406
- /**
407
- * Generate recommendations based on MD stats and insights
408
- */
409
- function generateRecommendations(
410
- stats: ProjectStats,
411
- estimateAccuracy: number,
412
- blockers: Blocker[]
413
- ): string[] {
414
- const recommendations: string[] = []
415
-
416
- // Blocker-based recommendations (highest priority)
417
- if (blockers.length > 0) {
418
- const oldestBlocker = blockers.reduce((a, b) => a.daysBlocked > b.daysBlocked ? a : b)
419
- if (oldestBlocker.daysBlocked > 3) {
420
- recommendations.push(`Blocker "${oldestBlocker.reason}" is ${oldestBlocker.daysBlocked} days old - needs attention`)
421
- } else {
422
- recommendations.push(`${blockers.length} blocked task(s) - review blockers`)
423
- }
424
- }
425
-
426
- // Estimate accuracy recommendations
427
- if (estimateAccuracy > 0 && estimateAccuracy < 50) {
428
- recommendations.push('Estimates often off - consider adding 30% buffer')
429
- } else if (estimateAccuracy >= 80) {
430
- recommendations.push('Great estimation accuracy - keep it up!')
431
- }
432
-
433
- // Task state recommendations
434
- if (!stats.currentTask) {
435
- recommendations.push('Start a task with /p:now')
436
- }
437
-
438
- if (stats.queue.length === 0) {
439
- recommendations.push('Add tasks to queue with /p:next')
440
- }
441
-
442
- if (stats.ideas.pending.length > 10) {
443
- recommendations.push('Review and prioritize pending ideas')
444
- }
445
-
446
- if (stats.agents.length === 0) {
447
- recommendations.push('Run /p:sync to generate agents')
448
- }
449
-
450
- // Default positive message
451
- if (recommendations.length === 0) {
452
- recommendations.push('Keep shipping!')
453
- }
454
-
455
- return recommendations.slice(0, 4) // Max 4 recommendations
456
- }
457
-
458
- /**
459
- * Calculate streak from metrics (pure function, no mutation)
460
- */
461
- export function calculateStreak(metrics: MetricsJson | null): number {
462
- if (!metrics?.recentActivity?.length) return 0
463
-
464
- const activityDates = new Set(
465
- metrics.recentActivity.map((a: { timestamp: string }) => new Date(a.timestamp).toISOString().split('T')[0])
466
- )
467
-
468
- const today = new Date()
469
- today.setHours(0, 0, 0, 0)
470
-
471
- // Generate last 30 days as array
472
- const days = Array.from({ length: 30 }, (_, i) => {
473
- const date = new Date(today)
474
- date.setDate(date.getDate() - i)
475
- return date.toISOString().split('T')[0]
476
- })
477
-
478
- // Find first gap (day without activity)
479
- const firstGapIndex = days.findIndex(date => !activityDates.has(date))
480
-
481
- // If today has no activity, check if yesterday does
482
- if (firstGapIndex === 0) {
483
- const yesterdayHasActivity = activityDates.has(days[1])
484
- if (!yesterdayHasActivity) return 0
485
- // Start counting from yesterday
486
- const remainingDays = days.slice(1)
487
- const gapFromYesterday = remainingDays.findIndex(date => !activityDates.has(date))
488
- return gapFromYesterday === -1 ? remainingDays.length : gapFromYesterday
489
- }
490
-
491
- return firstGapIndex === -1 ? days.length : firstGapIndex
492
- }
493
-
494
-
495
- /**
496
- * Get insight message based on stats
497
- */
498
- export function getInsightMessage(stats: StatsResult, streak: number): string {
499
- if (!stats.hasData) return 'Run /p:sync to get started'
500
- if (stats.state?.currentTask) return `Working on: ${stats.state.currentTask.description}`
501
- if (streak >= 7) return `${streak} day streak! You're on fire! 🔥`
502
- if (streak >= 3) return `${streak} day streak - keep it going!`
503
-
504
- const queueLength = stats.queue?.tasks?.filter(t => !t.completed).length ?? 0
505
- if (queueLength > 0) return `${queueLength} tasks in queue`
506
- return 'Ready to start working'
507
- }
508
-
509
- /**
510
- * Calculate velocity change percentage
511
- */
512
- export function getVelocityChange(velocity: number): number {
513
- if (velocity > 2) return 15
514
- if (velocity > 1) return 5
515
- if (velocity > 0) return 0
516
- return -10
517
- }
518
-
519
- /**
520
- * Get weekly velocity data from metrics (last 7 days)
521
- */
522
- export function getWeeklyVelocityData(metrics: MetricsJson | null): number[] {
523
- if (!metrics?.recentActivity?.length) return []
524
-
525
- const today = new Date()
526
-
527
- return Array.from({ length: 7 }, (_, i) => {
528
- const date = new Date(today)
529
- date.setDate(date.getDate() - (6 - i))
530
- const dateStr = date.toISOString().split('T')[0]
531
-
532
- return (metrics.recentActivity || []).filter((e: { timestamp: string }) =>
533
- e.timestamp?.startsWith(dateStr)
534
- ).length
535
- })
536
- }
537
-
538
- /**
539
- * Calculate health score from stats
540
- */
541
- export function calculateHealthScore(stats: StatsResult): number {
542
- if (!stats.hasData) return 0
543
- if (stats.insights.healthScore > 0) return stats.insights.healthScore
544
-
545
- // Fallback for legacy
546
- if (stats.isLegacy && stats.legacyStats) {
547
- const { metrics, currentTask, queue, timeline } = stats.legacyStats
548
- const velocity = metrics?.velocity?.tasksPerDay ?? 0
549
- const hasCurrentTask = Boolean(currentTask)
550
- const queueSize = queue?.length ?? 0
551
- const recentActivity = timeline?.slice(0, 7).length ?? 0
552
-
553
- const velocityScore = Math.min(30, velocity * 15)
554
- const taskScore = hasCurrentTask ? 20 : 0
555
- const queueScore = queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
556
- const activityScore = Math.min(30, recentActivity * 5)
557
-
558
- return Math.min(100, Math.round(velocityScore + taskScore + queueScore + activityScore))
559
- }
560
-
561
- return 50
562
- }