prjct-cli 0.12.2 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CLAUDE.md +18 -6
  3. package/core/data/index.ts +19 -5
  4. package/core/data/md-base-manager.ts +203 -0
  5. package/core/data/md-queue-manager.ts +179 -0
  6. package/core/data/md-state-manager.ts +133 -0
  7. package/core/serializers/index.ts +20 -0
  8. package/core/serializers/queue-serializer.ts +210 -0
  9. package/core/serializers/state-serializer.ts +136 -0
  10. package/core/utils/file-helper.ts +12 -0
  11. package/package.json +1 -1
  12. package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
  13. package/packages/web/app/page.tsx +1 -6
  14. package/packages/web/app/project/[id]/page.tsx +34 -1
  15. package/packages/web/app/project/[id]/stats/page.tsx +11 -5
  16. package/packages/web/app/settings/page.tsx +2 -221
  17. package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
  18. package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
  19. package/packages/web/components/BlockersCard/index.ts +2 -0
  20. package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
  21. package/packages/web/lib/projects.ts +28 -27
  22. package/packages/web/lib/services/projects.server.ts +25 -21
  23. package/packages/web/lib/services/stats.server.ts +355 -57
  24. package/packages/web/package.json +0 -2
  25. package/templates/commands/decision.md +226 -0
  26. package/templates/commands/done.md +100 -68
  27. package/templates/commands/feature.md +102 -103
  28. package/templates/commands/idea.md +41 -38
  29. package/templates/commands/now.md +94 -33
  30. package/templates/commands/pause.md +90 -30
  31. package/templates/commands/ship.md +179 -74
  32. package/templates/commands/sync.md +324 -200
  33. package/packages/web/app/api/migrate/route.ts +0 -46
  34. package/packages/web/app/api/settings/route.ts +0 -97
  35. package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
  36. package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
  37. package/packages/web/components/MigrationGate/index.ts +0 -1
  38. package/packages/web/lib/json-loader.ts +0 -630
  39. package/packages/web/lib/services/migration.server.ts +0 -580
@@ -1,27 +1,110 @@
1
1
  /**
2
2
  * Stats Service (Server-only)
3
3
  *
4
- * Direct data access for Server Components.
5
- * No API calls needed - reads directly from filesystem.
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
- export type { UnifiedJsonData, StateJson, QueueJson, MetricsJson, ProjectInsights }
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
- topBlockers: [],
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
- * Get project stats - cached per request
191
+ * Calculate estimate accuracy from sessions
192
+ * Returns percentage of tasks completed within ±20% of estimate
110
193
  */
111
- export const getStats = cache(async (projectId: string): Promise<StatsResult> => {
112
- const hasJson = await hasJsonState(projectId)
113
-
114
- if (hasJson) {
115
- const jsonData = await loadUnifiedJsonData(projectId)
116
- return {
117
- state: jsonData.state,
118
- queue: jsonData.queue,
119
- metrics: jsonData.metrics,
120
- insights: jsonData.insights,
121
- agents: jsonData.agents,
122
- ideas: jsonData.ideas,
123
- roadmap: jsonData.roadmap,
124
- shipped: jsonData.shipped,
125
- outcomes: jsonData.outcomes,
126
- hasData: jsonData.hasJsonData,
127
- isLegacy: false
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
- // Fallback to legacy markdown parsing
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 legacyStats = await getLegacyStats(projectId)
134
- return {
135
- ...EMPTY_STATS_RESULT,
136
- insights: {
137
- ...DEFAULT_INSIGHTS,
138
- healthScore: 50,
139
- recommendations: ['Run /p:sync to enable JSON format']
140
- },
141
- hasData: true,
142
- isLegacy: true,
143
- legacyStats
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 EMPTY_STATS_RESULT
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