prjct-cli 0.52.0 → 0.54.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 CHANGED
@@ -1,5 +1,88 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.54.0] - 2026-01-30
4
+
5
+ ### Features
6
+
7
+ - Add showMetrics config option - PRJ-70 (#82)
8
+ - Selective memory retrieval based on task relevance - PRJ-107 (#81)
9
+ - Add session stats to p. stats command - PRJ-89 (#80)
10
+
11
+
12
+ ## [0.55.1] - 2026-01-30
13
+
14
+ ### Added
15
+
16
+ - **showMetrics config option** (PRJ-70)
17
+ - Add `showMetrics` boolean to `LocalConfig` (prjct.config.json)
18
+ - Defaults to `true` for new projects and existing projects without setting
19
+ - Added `getShowMetrics()` and `setShowMetrics()` to ConfigManager
20
+
21
+
22
+ ## [0.55.0] - 2026-01-30
23
+
24
+ ### Features
25
+
26
+ - Selective memory retrieval based on task relevance - PRJ-107
27
+
28
+
29
+ ## [0.55.0] - 2026-01-30
30
+
31
+ ### Added
32
+
33
+ - **Selective memory retrieval based on task relevance** (PRJ-107)
34
+ - Added `getRelevantMemoriesWithMetrics()` for domain-based filtering
35
+ - Relevance scoring considers: domain match (25pts), tag match (20pts), recency (15pts), confidence (20pts), keywords (15pts), user triggered (5pts)
36
+ - Returns retrieval metrics: total, considered, returned, filtering ratio, avg score
37
+ - New types: `RelevantMemoryQuery`, `ScoredMemory`, `MemoryRetrievalResult`, `TaskDomain`
38
+ - Integrates with PRJ-104 confidence scoring
39
+
40
+
41
+ ## [0.54.0] - 2026-01-30
42
+
43
+ ### Features
44
+
45
+ - Session stats in p. stats command - PRJ-89
46
+
47
+
48
+ ## [0.54.0] - 2026-01-30
49
+
50
+ ### Added
51
+
52
+ - **Session stats in `p. stats` command** (PRJ-89)
53
+ - Shows today's activity: duration, tasks completed, features shipped
54
+ - Displays agents used during the session with frequency
55
+ - Shows learned patterns: decisions, preferences, workflows
56
+ - Enhanced JSON and export modes include session data
57
+ - Added `getRecentEvents()` to memoryService
58
+
59
+
60
+ ## [0.53.0] - 2026-01-30
61
+
62
+ ### Features
63
+
64
+ - Lazy template loading with TTL cache - PRJ-76 (#79)
65
+
66
+
67
+ ## [0.53.0] - 2026-01-30
68
+
69
+ ### Features
70
+
71
+ - Lazy template loading with TTL cache - PRJ-76
72
+
73
+
74
+ ## [0.53.0] - 2026-01-30
75
+
76
+ ### Added
77
+
78
+ - **Lazy template loading with TTL cache** (PRJ-76)
79
+ - Templates now loaded on-demand with 60-second TTL cache
80
+ - Added `getTemplate()` method with per-file caching
81
+ - `loadChecklists()` and `loadChecklistRouting()` now use TTL cache
82
+ - Added `clearTemplateCache()` method for testing/forced refresh
83
+ - Reduces disk I/O for frequently accessed templates
84
+
85
+
3
86
  ## [0.52.0] - 2026-01-30
4
87
 
5
88
  ### Features
@@ -30,9 +30,13 @@ export type {
30
30
  MemoryContext,
31
31
  MemoryContextParams,
32
32
  MemoryDatabase,
33
+ MemoryRetrievalResult,
33
34
  MemoryTag,
34
35
  Patterns,
35
36
  Preference,
37
+ RelevantMemoryQuery,
38
+ ScoredMemory,
39
+ TaskDomain,
36
40
  Workflow,
37
41
  } from '../types/memory'
38
42
 
@@ -44,9 +48,13 @@ import type {
44
48
  Memory,
45
49
  MemoryContext,
46
50
  MemoryDatabase,
51
+ MemoryRetrievalResult,
47
52
  MemoryTag,
48
53
  Patterns,
49
54
  Preference,
55
+ RelevantMemoryQuery,
56
+ ScoredMemory,
57
+ TaskDomain,
50
58
  Workflow,
51
59
  } from '../types/memory'
52
60
 
@@ -690,6 +698,202 @@ export class SemanticMemories extends CachedStore<MemoryDatabase> {
690
698
  .map(({ _score, ...memory }) => memory as Memory)
691
699
  }
692
700
 
701
+ /**
702
+ * Enhanced memory retrieval with domain-based filtering and metrics.
703
+ * Implements selective memory retrieval based on task relevance.
704
+ * @see PRJ-107
705
+ */
706
+ async getRelevantMemoriesWithMetrics(
707
+ projectId: string,
708
+ query: RelevantMemoryQuery
709
+ ): Promise<MemoryRetrievalResult> {
710
+ const db = await this.load(projectId)
711
+ const totalMemories = db.memories.length
712
+
713
+ if (totalMemories === 0) {
714
+ return {
715
+ memories: [],
716
+ metrics: {
717
+ totalMemories: 0,
718
+ memoriesConsidered: 0,
719
+ memoriesReturned: 0,
720
+ filteringRatio: 0,
721
+ avgRelevanceScore: 0,
722
+ },
723
+ }
724
+ }
725
+
726
+ const maxResults = query.maxResults ?? 10
727
+ const minRelevance = query.minRelevance ?? 10
728
+
729
+ // Score all memories
730
+ const scored: ScoredMemory[] = db.memories.map((memory) => {
731
+ const breakdown = {
732
+ domainMatch: 0,
733
+ tagMatch: 0,
734
+ recency: 0,
735
+ confidence: 0,
736
+ keywords: 0,
737
+ userTriggered: 0,
738
+ }
739
+
740
+ // Domain match scoring (0-25 points)
741
+ if (query.taskDomain) {
742
+ const domainTags = this._getDomainTags(query.taskDomain)
743
+ const matchingTags = (memory.tags || []).filter((tag) => domainTags.includes(tag))
744
+ breakdown.domainMatch = Math.min(25, matchingTags.length * 10)
745
+ }
746
+
747
+ // Tag match from command context (0-20 points)
748
+ if (query.commandName) {
749
+ const commandTags = this._getCommandTags(query.commandName)
750
+ const matchingTags = (memory.tags || []).filter((tag) => commandTags.includes(tag))
751
+ breakdown.tagMatch = Math.min(20, matchingTags.length * 8)
752
+ }
753
+
754
+ // Recency scoring (0-15 points)
755
+ const age = Date.now() - new Date(memory.updatedAt).getTime()
756
+ const daysSinceUpdate = age / (1000 * 60 * 60 * 24)
757
+ breakdown.recency = Math.max(0, Math.round(15 - daysSinceUpdate * 0.5))
758
+
759
+ // Confidence scoring (0-20 points) - PRJ-104 integration
760
+ if (memory.confidence) {
761
+ breakdown.confidence =
762
+ memory.confidence === 'high' ? 20 : memory.confidence === 'medium' ? 12 : 5
763
+ } else if (memory.observationCount) {
764
+ // Fallback to observation count
765
+ breakdown.confidence = Math.min(20, memory.observationCount * 3)
766
+ }
767
+
768
+ // Keyword matching (0-15 points)
769
+ if (query.taskDescription) {
770
+ const keywords = this._extractKeywordsFromText(query.taskDescription)
771
+ let keywordScore = 0
772
+ for (const keyword of keywords) {
773
+ if (memory.content.toLowerCase().includes(keyword)) keywordScore += 2
774
+ if (memory.title.toLowerCase().includes(keyword)) keywordScore += 3
775
+ }
776
+ breakdown.keywords = Math.min(15, keywordScore)
777
+ }
778
+
779
+ // User triggered bonus (0-5 points)
780
+ if (memory.userTriggered) {
781
+ breakdown.userTriggered = 5
782
+ }
783
+
784
+ const relevanceScore =
785
+ breakdown.domainMatch +
786
+ breakdown.tagMatch +
787
+ breakdown.recency +
788
+ breakdown.confidence +
789
+ breakdown.keywords +
790
+ breakdown.userTriggered
791
+
792
+ return {
793
+ ...memory,
794
+ relevanceScore,
795
+ scoreBreakdown: breakdown,
796
+ }
797
+ })
798
+
799
+ // Filter by minimum relevance
800
+ const considered = scored.filter((m) => m.relevanceScore >= minRelevance)
801
+
802
+ // Sort by relevance and take top N
803
+ const sorted = considered.sort((a, b) => b.relevanceScore - a.relevanceScore)
804
+ const returned = sorted.slice(0, maxResults)
805
+
806
+ // Calculate average relevance
807
+ const avgRelevanceScore =
808
+ returned.length > 0
809
+ ? Math.round(returned.reduce((sum, m) => sum + m.relevanceScore, 0) / returned.length)
810
+ : 0
811
+
812
+ return {
813
+ memories: returned,
814
+ metrics: {
815
+ totalMemories,
816
+ memoriesConsidered: considered.length,
817
+ memoriesReturned: returned.length,
818
+ filteringRatio: totalMemories > 0 ? returned.length / totalMemories : 0,
819
+ avgRelevanceScore,
820
+ },
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Map task domain to relevant memory tags.
826
+ * @see PRJ-107
827
+ */
828
+ private _getDomainTags(domain: TaskDomain): MemoryTag[] {
829
+ const domainTagMap: Record<TaskDomain, MemoryTag[]> = {
830
+ frontend: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.FILE_STRUCTURE, MEMORY_TAGS.ARCHITECTURE],
831
+ backend: [
832
+ MEMORY_TAGS.CODE_STYLE,
833
+ MEMORY_TAGS.ARCHITECTURE,
834
+ MEMORY_TAGS.DEPENDENCIES,
835
+ MEMORY_TAGS.TECH_STACK,
836
+ ],
837
+ devops: [MEMORY_TAGS.SHIP_WORKFLOW, MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.DEPENDENCIES],
838
+ docs: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.NAMING_CONVENTION],
839
+ testing: [MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.CODE_STYLE],
840
+ database: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.NAMING_CONVENTION],
841
+ general: Object.values(MEMORY_TAGS) as MemoryTag[],
842
+ }
843
+ return domainTagMap[domain] || []
844
+ }
845
+
846
+ /**
847
+ * Map command to relevant memory tags.
848
+ * @see PRJ-107
849
+ */
850
+ private _getCommandTags(commandName: string): MemoryTag[] {
851
+ const commandTags: Record<string, MemoryTag[]> = {
852
+ ship: [MEMORY_TAGS.COMMIT_STYLE, MEMORY_TAGS.SHIP_WORKFLOW, MEMORY_TAGS.TEST_BEHAVIOR],
853
+ feature: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.CODE_STYLE],
854
+ done: [MEMORY_TAGS.SHIP_WORKFLOW],
855
+ analyze: [MEMORY_TAGS.TECH_STACK, MEMORY_TAGS.ARCHITECTURE],
856
+ spec: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.CODE_STYLE],
857
+ task: [MEMORY_TAGS.BRANCH_NAMING, MEMORY_TAGS.CODE_STYLE],
858
+ sync: [MEMORY_TAGS.TECH_STACK, MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.DEPENDENCIES],
859
+ test: [MEMORY_TAGS.TEST_BEHAVIOR],
860
+ bug: [MEMORY_TAGS.CODE_STYLE, MEMORY_TAGS.TEST_BEHAVIOR],
861
+ }
862
+ return commandTags[commandName] || []
863
+ }
864
+
865
+ /**
866
+ * Extract keywords from text for matching.
867
+ */
868
+ private _extractKeywordsFromText(text: string): string[] {
869
+ const words = text.toLowerCase().split(/\s+/)
870
+ const stopWords = new Set([
871
+ 'the',
872
+ 'a',
873
+ 'an',
874
+ 'is',
875
+ 'are',
876
+ 'to',
877
+ 'for',
878
+ 'and',
879
+ 'or',
880
+ 'in',
881
+ 'on',
882
+ 'at',
883
+ 'by',
884
+ 'with',
885
+ 'from',
886
+ 'as',
887
+ 'it',
888
+ 'this',
889
+ 'that',
890
+ 'be',
891
+ 'have',
892
+ 'has',
893
+ ])
894
+ return words.filter((w) => w.length > 2 && !stopWords.has(w))
895
+ }
896
+
693
897
  private _extractContextTags(context: MemoryContext): string[] {
694
898
  const tags: string[] = []
695
899
 
@@ -863,6 +1067,18 @@ export class MemorySystem {
863
1067
  return this._semanticMemories.getMemoryStats(projectId)
864
1068
  }
865
1069
 
1070
+ /**
1071
+ * Get relevant memories with domain-based filtering and metrics.
1072
+ * Implements selective memory retrieval based on task relevance.
1073
+ * @see PRJ-107
1074
+ */
1075
+ getRelevantMemoriesWithMetrics(
1076
+ projectId: string,
1077
+ query: RelevantMemoryQuery
1078
+ ): Promise<MemoryRetrievalResult> {
1079
+ return this._semanticMemories.getRelevantMemoriesWithMetrics(projectId, query)
1080
+ }
1081
+
866
1082
  // ===========================================================================
867
1083
  // TIER 1: Session Memory
868
1084
  // ===========================================================================
@@ -43,17 +43,73 @@ type Agent = PromptAgent
43
43
  type Context = PromptContext
44
44
  type State = PromptState
45
45
 
46
+ /**
47
+ * Cached template entry with TTL support
48
+ * @see PRJ-76
49
+ */
50
+ interface CachedTemplate {
51
+ content: string
52
+ loadedAt: number
53
+ }
54
+
46
55
  /**
47
56
  * Builds prompts for Claude using templates, context, and learned patterns.
48
57
  * Supports plan mode, think blocks, and quality checklists.
49
58
  * Auto-injects unified state and performance insights.
59
+ *
60
+ * Uses lazy loading for templates with 60s TTL cache.
61
+ * @see PRJ-76
50
62
  */
51
63
  class PromptBuilder {
52
64
  private _checklistsCache: Record<string, string> | null = null
65
+ private _checklistsCacheTime: number = 0
53
66
  private _checklistRoutingCache: string | null = null
67
+ private _checklistRoutingCacheTime: number = 0
54
68
  private _currentContext: Context | null = null
55
69
  private _stateCache: Map<string, { state: ProjectState; timestamp: number }> = new Map()
56
70
  private _stateCacheTTL = 5000 // 5 seconds
71
+ private _templateCache: Map<string, CachedTemplate> = new Map()
72
+ private readonly TEMPLATE_CACHE_TTL_MS = 60_000 // 60 seconds
73
+
74
+ /**
75
+ * Get a template with TTL caching.
76
+ * Returns cached content if within TTL, otherwise loads from disk.
77
+ * @see PRJ-76
78
+ */
79
+ getTemplate(templatePath: string): string | null {
80
+ const cached = this._templateCache.get(templatePath)
81
+ const now = Date.now()
82
+
83
+ if (cached && now - cached.loadedAt < this.TEMPLATE_CACHE_TTL_MS) {
84
+ return cached.content
85
+ }
86
+
87
+ try {
88
+ if (fs.existsSync(templatePath)) {
89
+ const content = fs.readFileSync(templatePath, 'utf-8')
90
+ this._templateCache.set(templatePath, { content, loadedAt: now })
91
+ return content
92
+ }
93
+ } catch (error) {
94
+ if (!isNotFoundError(error)) {
95
+ console.error(`Template loading warning: ${(error as Error).message}`)
96
+ }
97
+ }
98
+
99
+ return null
100
+ }
101
+
102
+ /**
103
+ * Clear the template cache (for testing or forced refresh)
104
+ * @see PRJ-76
105
+ */
106
+ clearTemplateCache(): void {
107
+ this._templateCache.clear()
108
+ this._checklistsCache = null
109
+ this._checklistsCacheTime = 0
110
+ this._checklistRoutingCache = null
111
+ this._checklistRoutingCacheTime = 0
112
+ }
57
113
 
58
114
  /**
59
115
  * Reset context (for testing)
@@ -71,9 +127,16 @@ class PromptBuilder {
71
127
 
72
128
  /**
73
129
  * Load quality checklists from templates/checklists/
130
+ * Uses lazy loading with TTL cache.
131
+ * @see PRJ-76
74
132
  */
75
133
  loadChecklists(): Record<string, string> {
76
- if (this._checklistsCache) return this._checklistsCache
134
+ const now = Date.now()
135
+
136
+ // Check if cache is still valid
137
+ if (this._checklistsCache && now - this._checklistsCacheTime < this.TEMPLATE_CACHE_TTL_MS) {
138
+ return this._checklistsCache
139
+ }
77
140
 
78
141
  const checklistsDir = path.join(__dirname, '..', '..', 'templates', 'checklists')
79
142
  const checklists: Record<string, string> = {}
@@ -83,8 +146,12 @@ class PromptBuilder {
83
146
  const files = fs.readdirSync(checklistsDir).filter((f) => f.endsWith('.md'))
84
147
  for (const file of files) {
85
148
  const name = file.replace('.md', '')
86
- const content = fs.readFileSync(path.join(checklistsDir, file), 'utf-8')
87
- checklists[name] = content
149
+ const templatePath = path.join(checklistsDir, file)
150
+ // Use getTemplate for individual files to leverage per-file caching
151
+ const content = this.getTemplate(templatePath)
152
+ if (content) {
153
+ checklists[name] = content
154
+ }
88
155
  }
89
156
  }
90
157
  } catch (error) {
@@ -95,6 +162,7 @@ class PromptBuilder {
95
162
  }
96
163
 
97
164
  this._checklistsCache = checklists
165
+ this._checklistsCacheTime = now
98
166
  return checklists
99
167
  }
100
168
 
@@ -215,9 +283,19 @@ class PromptBuilder {
215
283
 
216
284
  /**
217
285
  * Load checklist routing template for Claude to decide which checklists apply
286
+ * Uses lazy loading with TTL cache.
287
+ * @see PRJ-76
218
288
  */
219
289
  loadChecklistRouting(): string | null {
220
- if (this._checklistRoutingCache) return this._checklistRoutingCache
290
+ const now = Date.now()
291
+
292
+ // Check if cache is still valid
293
+ if (
294
+ this._checklistRoutingCache &&
295
+ now - this._checklistRoutingCacheTime < this.TEMPLATE_CACHE_TTL_MS
296
+ ) {
297
+ return this._checklistRoutingCache
298
+ }
221
299
 
222
300
  const routingPath = path.join(
223
301
  __dirname,
@@ -228,15 +306,11 @@ class PromptBuilder {
228
306
  'checklist-routing.md'
229
307
  )
230
308
 
231
- try {
232
- if (fs.existsSync(routingPath)) {
233
- this._checklistRoutingCache = fs.readFileSync(routingPath, 'utf-8')
234
- }
235
- } catch (error) {
236
- // Silent fail - checklist routing is optional
237
- if (!isNotFoundError(error)) {
238
- console.error(`Checklist routing warning: ${(error as Error).message}`)
239
- }
309
+ // Use getTemplate for consistent caching behavior
310
+ const content = this.getTemplate(routingPath)
311
+ if (content) {
312
+ this._checklistRoutingCache = content
313
+ this._checklistRoutingCacheTime = now
240
314
  }
241
315
 
242
316
  return this._checklistRoutingCache || null