prjct-cli 0.12.1 → 0.13.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 +43 -0
- package/CLAUDE.md +18 -6
- package/bin/serve.js +12 -30
- package/core/data/index.ts +19 -5
- package/core/data/md-base-manager.ts +203 -0
- package/core/data/md-queue-manager.ts +179 -0
- package/core/data/md-state-manager.ts +133 -0
- package/core/serializers/index.ts +20 -0
- package/core/serializers/queue-serializer.ts +210 -0
- package/core/serializers/state-serializer.ts +136 -0
- package/core/utils/file-helper.ts +12 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
- package/packages/web/app/project/[id]/page.tsx +34 -1
- package/packages/web/app/project/[id]/stats/page.tsx +11 -5
- package/packages/web/app/settings/page.tsx +2 -221
- package/packages/web/components/AppSidebar/AppSidebar.tsx +5 -3
- package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
- package/packages/web/components/BlockersCard/index.ts +2 -0
- package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
- package/packages/web/lib/projects.ts +28 -27
- package/packages/web/lib/services/projects.server.ts +25 -21
- package/packages/web/lib/services/stats.server.ts +355 -57
- package/packages/web/package.json +0 -2
- package/templates/commands/decision.md +226 -0
- package/templates/commands/done.md +100 -68
- package/templates/commands/feature.md +102 -103
- package/templates/commands/idea.md +41 -38
- package/templates/commands/now.md +94 -33
- package/templates/commands/pause.md +90 -30
- package/templates/commands/ship.md +179 -74
- package/templates/commands/sync.md +324 -200
- package/packages/web/app/api/migrate/route.ts +0 -46
- package/packages/web/app/api/settings/route.ts +0 -97
- package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
- package/packages/web/lib/json-loader.ts +0 -630
- package/packages/web/lib/services/migration.server.ts +0 -600
|
@@ -1,27 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stats Service (Server-only)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* No
|
|
4
|
+
* MD-First Architecture: Reads directly from MD files.
|
|
5
|
+
* No JSON fallback - MD is the source of truth.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import 'server-only'
|
|
9
9
|
import { cache } from 'react'
|
|
10
10
|
import { exec } from 'child_process'
|
|
11
11
|
import { promisify } from 'util'
|
|
12
|
-
import {
|
|
13
|
-
loadUnifiedJsonData,
|
|
14
|
-
hasJsonState,
|
|
15
|
-
type UnifiedJsonData,
|
|
16
|
-
type StateJson,
|
|
17
|
-
type QueueJson,
|
|
18
|
-
type MetricsJson,
|
|
19
|
-
type ProjectInsights,
|
|
20
|
-
} from '@/lib/json-loader'
|
|
21
|
-
import { getProjectStats as getLegacyStats, type ProjectStats } from '@/lib/parse-prjct-files'
|
|
12
|
+
import { getProjectStats as getMdStats, type ProjectStats, type SessionDay } from '@/lib/parse-prjct-files'
|
|
22
13
|
import { getProjects } from './projects.server'
|
|
23
14
|
|
|
24
|
-
|
|
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
|
+
}
|
|
25
108
|
|
|
26
109
|
// Activity type for recent activity tracking
|
|
27
110
|
export interface RecentActivity {
|
|
@@ -86,8 +169,7 @@ export interface StatsResult {
|
|
|
86
169
|
const DEFAULT_INSIGHTS: ProjectInsights = {
|
|
87
170
|
healthScore: 0,
|
|
88
171
|
estimateAccuracy: 0,
|
|
89
|
-
|
|
90
|
-
patternsDetected: [],
|
|
172
|
+
blockers: [],
|
|
91
173
|
recommendations: ['Run /p:sync to initialize project']
|
|
92
174
|
}
|
|
93
175
|
|
|
@@ -106,47 +188,273 @@ const EMPTY_STATS_RESULT: StatsResult = {
|
|
|
106
188
|
}
|
|
107
189
|
|
|
108
190
|
/**
|
|
109
|
-
*
|
|
191
|
+
* Calculate estimate accuracy from sessions
|
|
192
|
+
* Returns percentage of tasks completed within ±20% of estimate
|
|
110
193
|
*/
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
128
278
|
}
|
|
129
279
|
}
|
|
130
280
|
|
|
131
|
-
//
|
|
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> => {
|
|
132
293
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|
|
144
322
|
}
|
|
145
323
|
} catch {
|
|
146
|
-
return
|
|
324
|
+
// MD parsing failed - return empty stats
|
|
147
325
|
}
|
|
326
|
+
|
|
327
|
+
return EMPTY_STATS_RESULT
|
|
148
328
|
})
|
|
149
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
|
+
|
|
150
458
|
/**
|
|
151
459
|
* Calculate streak from metrics (pure function, no mutation)
|
|
152
460
|
*/
|
|
@@ -183,16 +491,6 @@ export function calculateStreak(metrics: MetricsJson | null): number {
|
|
|
183
491
|
return firstGapIndex === -1 ? days.length : firstGapIndex
|
|
184
492
|
}
|
|
185
493
|
|
|
186
|
-
/**
|
|
187
|
-
* Get health emoji based on score
|
|
188
|
-
*/
|
|
189
|
-
export function getHealthEmoji(score: number): string {
|
|
190
|
-
if (score >= 80) return '🔥'
|
|
191
|
-
if (score >= 60) return '💪'
|
|
192
|
-
if (score >= 40) return '👍'
|
|
193
|
-
if (score >= 20) return '🌱'
|
|
194
|
-
return '💤'
|
|
195
|
-
}
|
|
196
494
|
|
|
197
495
|
/**
|
|
198
496
|
* Get insight message based on stats
|
|
@@ -231,7 +529,7 @@ export function getWeeklyVelocityData(metrics: MetricsJson | null): number[] {
|
|
|
231
529
|
date.setDate(date.getDate() - (6 - i))
|
|
232
530
|
const dateStr = date.toISOString().split('T')[0]
|
|
233
531
|
|
|
234
|
-
return metrics.recentActivity.filter((e: { timestamp: string }) =>
|
|
532
|
+
return (metrics.recentActivity || []).filter((e: { timestamp: string }) =>
|
|
235
533
|
e.timestamp?.startsWith(dateStr)
|
|
236
534
|
).length
|
|
237
535
|
})
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"lint": "eslint"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@ai-sdk/openai": "^2.0.80",
|
|
15
14
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
16
15
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
17
16
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
@@ -24,7 +23,6 @@
|
|
|
24
23
|
"@xterm/addon-fit": "^0.10.0",
|
|
25
24
|
"@xterm/addon-web-links": "^0.11.0",
|
|
26
25
|
"@xterm/xterm": "^5.5.0",
|
|
27
|
-
"ai": "^5.0.108",
|
|
28
26
|
"class-variance-authority": "^0.7.1",
|
|
29
27
|
"clsx": "^2.1.1",
|
|
30
28
|
"lucide-react": "^0.556.0",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
allowed-tools: [Read, Write, Bash]
|
|
3
|
+
description: 'Log architectural or important decisions'
|
|
4
|
+
timestamp-rule: 'GetTimestamp() for ALL timestamps'
|
|
5
|
+
architecture: 'MD-first - MD files are source of truth'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /p:decision - Log Important Decisions
|
|
9
|
+
|
|
10
|
+
## Architecture: MD-First
|
|
11
|
+
|
|
12
|
+
**Source of Truth**: `planning/decisions.md`
|
|
13
|
+
|
|
14
|
+
MD files are the source of truth. Write directly to MD files.
|
|
15
|
+
|
|
16
|
+
## Context Variables
|
|
17
|
+
- `{projectId}`: From `.prjct/prjct.config.json`
|
|
18
|
+
- `{globalPath}`: `~/.prjct-cli/projects/{projectId}`
|
|
19
|
+
- `{decisionsPath}`: `{globalPath}/planning/decisions.md`
|
|
20
|
+
- `{memoryPath}`: `{globalPath}/memory/context.jsonl`
|
|
21
|
+
- `{decision}`: User-provided decision (required)
|
|
22
|
+
- `{reasoning}`: User-provided reasoning (optional)
|
|
23
|
+
- `{alternatives}`: User-provided alternatives considered (optional)
|
|
24
|
+
|
|
25
|
+
## Decision Format
|
|
26
|
+
|
|
27
|
+
Capture WHY decisions were made to avoid repeating mistakes:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
/p:decision "Use REST instead of GraphQL"
|
|
31
|
+
--reason "Simpler for this use case, team familiarity"
|
|
32
|
+
--alternatives "GraphQL, gRPC"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or interactive prompt:
|
|
36
|
+
```
|
|
37
|
+
Decision: Use REST instead of GraphQL
|
|
38
|
+
|
|
39
|
+
Why did you make this decision?
|
|
40
|
+
> Simpler for this use case, team familiarity
|
|
41
|
+
|
|
42
|
+
What alternatives did you consider?
|
|
43
|
+
> GraphQL, gRPC
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Step 1: Read Config
|
|
47
|
+
|
|
48
|
+
READ: `.prjct/prjct.config.json`
|
|
49
|
+
EXTRACT: `projectId`
|
|
50
|
+
|
|
51
|
+
IF file not found:
|
|
52
|
+
OUTPUT: "No prjct project. Run /p:init first."
|
|
53
|
+
STOP
|
|
54
|
+
|
|
55
|
+
## Step 2: Validate Input
|
|
56
|
+
|
|
57
|
+
IF {decision} is empty:
|
|
58
|
+
ASK: "What decision did you make?"
|
|
59
|
+
SET: {decision} = user response
|
|
60
|
+
|
|
61
|
+
IF {reasoning} is empty:
|
|
62
|
+
ASK: "Why did you make this decision?"
|
|
63
|
+
SET: {reasoning} = user response
|
|
64
|
+
|
|
65
|
+
IF {alternatives} is empty:
|
|
66
|
+
ASK: "What alternatives did you consider? (optional, press Enter to skip)"
|
|
67
|
+
SET: {alternatives} = user response OR "none documented"
|
|
68
|
+
|
|
69
|
+
## Step 3: Generate Decision Entry
|
|
70
|
+
|
|
71
|
+
GENERATE: {decisionId} = "dec_" + 8 random alphanumeric chars
|
|
72
|
+
SET: {now} = GetTimestamp()
|
|
73
|
+
|
|
74
|
+
## Step 4: Read/Create Decisions File
|
|
75
|
+
|
|
76
|
+
READ: `{decisionsPath}` (or create default if not exists)
|
|
77
|
+
|
|
78
|
+
Default structure:
|
|
79
|
+
```markdown
|
|
80
|
+
# Decisions
|
|
81
|
+
|
|
82
|
+
Architecture decisions and their reasoning.
|
|
83
|
+
|
|
84
|
+
## Recent
|
|
85
|
+
|
|
86
|
+
_No decisions logged yet_
|
|
87
|
+
|
|
88
|
+
## Archive
|
|
89
|
+
|
|
90
|
+
_No archived decisions_
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Step 5: Update Decisions File (MD)
|
|
94
|
+
|
|
95
|
+
Parse existing content and add new decision under "## Recent" section:
|
|
96
|
+
|
|
97
|
+
```markdown
|
|
98
|
+
# Decisions
|
|
99
|
+
|
|
100
|
+
Architecture decisions and their reasoning.
|
|
101
|
+
|
|
102
|
+
## Recent
|
|
103
|
+
|
|
104
|
+
### {decision}
|
|
105
|
+
- **ID**: {decisionId}
|
|
106
|
+
- **Date**: {now}
|
|
107
|
+
- **Reasoning**: {reasoning}
|
|
108
|
+
- **Alternatives**: {alternatives}
|
|
109
|
+
- **Context**: {current task from now.md if active}
|
|
110
|
+
|
|
111
|
+
{...existing recent decisions}
|
|
112
|
+
|
|
113
|
+
## Archive
|
|
114
|
+
|
|
115
|
+
{...existing archive}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
WRITE: `{decisionsPath}`
|
|
119
|
+
|
|
120
|
+
## Step 6: Log to Memory
|
|
121
|
+
|
|
122
|
+
APPEND to: `{memoryPath}`
|
|
123
|
+
|
|
124
|
+
Single line (JSONL):
|
|
125
|
+
```json
|
|
126
|
+
{"timestamp":"{now}","action":"decision_logged","decisionId":"{decisionId}","decision":"{decision}","reasoning":"{reasoning}","alternatives":"{alternatives}"}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Output
|
|
130
|
+
|
|
131
|
+
SUCCESS:
|
|
132
|
+
```
|
|
133
|
+
📝 Decision logged: {decision}
|
|
134
|
+
|
|
135
|
+
ID: {decisionId}
|
|
136
|
+
Reasoning: {reasoning}
|
|
137
|
+
Alternatives: {alternatives}
|
|
138
|
+
|
|
139
|
+
Next:
|
|
140
|
+
• Continue working on your task
|
|
141
|
+
• /p:recap - Review decisions made
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Error Handling
|
|
145
|
+
|
|
146
|
+
| Error | Response | Action |
|
|
147
|
+
|-------|----------|--------|
|
|
148
|
+
| No project | "No prjct project" | STOP |
|
|
149
|
+
| No decision provided | Ask for decision | WAIT |
|
|
150
|
+
| Write fails | Log warning | CONTINUE |
|
|
151
|
+
|
|
152
|
+
## Examples
|
|
153
|
+
|
|
154
|
+
### Example 1: Full Decision with All Fields
|
|
155
|
+
**Input:** `/p:decision "Use Zustand for state management" --reason "Lighter than Redux, better DX" --alternatives "Redux, MobX, Jotai"`
|
|
156
|
+
|
|
157
|
+
**Output:**
|
|
158
|
+
```
|
|
159
|
+
📝 Decision logged: Use Zustand for state management
|
|
160
|
+
|
|
161
|
+
ID: dec_abc12345
|
|
162
|
+
Reasoning: Lighter than Redux, better DX
|
|
163
|
+
Alternatives: Redux, MobX, Jotai
|
|
164
|
+
|
|
165
|
+
Next: Continue working | /p:recap
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Example 2: Interactive Mode
|
|
169
|
+
**Input:** `/p:decision`
|
|
170
|
+
|
|
171
|
+
**Prompt flow:**
|
|
172
|
+
```
|
|
173
|
+
What decision did you make?
|
|
174
|
+
> Use PostgreSQL instead of MongoDB
|
|
175
|
+
|
|
176
|
+
Why did you make this decision?
|
|
177
|
+
> Need relational data with joins, better ACID compliance
|
|
178
|
+
|
|
179
|
+
What alternatives did you consider? (Enter to skip)
|
|
180
|
+
> MongoDB, SQLite
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Output:**
|
|
184
|
+
```
|
|
185
|
+
📝 Decision logged: Use PostgreSQL instead of MongoDB
|
|
186
|
+
|
|
187
|
+
ID: dec_xyz98765
|
|
188
|
+
Reasoning: Need relational data with joins, better ACID compliance
|
|
189
|
+
Alternatives: MongoDB, SQLite
|
|
190
|
+
|
|
191
|
+
Next: Continue working | /p:recap
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Example 3: Quick Decision (minimal)
|
|
195
|
+
**Input:** `/p:decision "Skip tests for MVP"`
|
|
196
|
+
|
|
197
|
+
**Prompt:**
|
|
198
|
+
```
|
|
199
|
+
Why did you make this decision?
|
|
200
|
+
> Time constraint, will add before launch
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Output:**
|
|
204
|
+
```
|
|
205
|
+
📝 Decision logged: Skip tests for MVP
|
|
206
|
+
|
|
207
|
+
ID: dec_qrs45678
|
|
208
|
+
Reasoning: Time constraint, will add before launch
|
|
209
|
+
Alternatives: none documented
|
|
210
|
+
|
|
211
|
+
Next: Continue working | /p:recap
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Use Cases
|
|
215
|
+
|
|
216
|
+
When to log a decision:
|
|
217
|
+
- Choosing between technologies/libraries
|
|
218
|
+
- Architectural pattern selection
|
|
219
|
+
- Trade-off decisions (speed vs quality)
|
|
220
|
+
- Deviation from best practices (and why)
|
|
221
|
+
- Postponing technical debt intentionally
|
|
222
|
+
|
|
223
|
+
This creates institutional memory to avoid:
|
|
224
|
+
- Repeating the same debates
|
|
225
|
+
- Forgetting why something was done a certain way
|
|
226
|
+
- Making inconsistent choices across the codebase
|