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 +83 -0
- package/core/agentic/memory-system.ts +216 -0
- package/core/agentic/prompt-builder.ts +87 -13
- package/core/commands/analysis.ts +167 -10
- package/core/infrastructure/config-manager.ts +24 -0
- package/core/services/memory-service.ts +21 -0
- package/core/types/config.ts +6 -0
- package/core/types/memory.ts +70 -0
- package/core/utils/output.ts +2 -2
- package/dist/bin/prjct.mjs +378 -26
- package/package.json +1 -1
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
|
-
|
|
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
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|