prjct-cli 0.20.0 → 0.21.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 (236) hide show
  1. package/CHANGELOG.md +24 -6
  2. package/CLAUDE.md +56 -15
  3. package/README.md +5 -6
  4. package/bin/prjct +59 -42
  5. package/bin/prjct.ts +60 -0
  6. package/core/__tests__/agentic/memory-system.test.ts +18 -3
  7. package/core/__tests__/agentic/plan-mode.test.ts +55 -26
  8. package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
  9. package/core/__tests__/utils/project-commands.test.ts +72 -0
  10. package/core/agentic/agent-router.ts +3 -12
  11. package/core/agentic/command-executor.ts +372 -3
  12. package/core/agentic/context-builder.ts +7 -27
  13. package/core/agentic/ground-truth.ts +604 -5
  14. package/core/agentic/index.ts +180 -0
  15. package/core/agentic/loop-detector.ts +418 -4
  16. package/core/agentic/memory-system.ts +857 -3
  17. package/core/agentic/plan-mode.ts +491 -4
  18. package/core/agentic/prompt-builder.ts +44 -65
  19. package/core/agentic/services.ts +13 -5
  20. package/core/agentic/skill-loader.ts +112 -0
  21. package/core/agentic/smart-context.ts +37 -122
  22. package/core/agentic/template-loader.ts +79 -122
  23. package/core/agentic/tool-registry.ts +5 -11
  24. package/core/agents/index.ts +1 -1
  25. package/core/agents/performance.ts +4 -2
  26. package/core/bus/bus.ts +262 -0
  27. package/core/bus/index.ts +3 -313
  28. package/core/commands/analysis.ts +5 -5
  29. package/core/commands/analytics.ts +11 -11
  30. package/core/commands/base.ts +33 -209
  31. package/core/commands/cleanup.ts +148 -0
  32. package/core/commands/command-data.ts +346 -0
  33. package/core/commands/commands.ts +216 -0
  34. package/core/commands/design.ts +83 -0
  35. package/core/commands/index.ts +13 -207
  36. package/core/commands/maintenance.ts +52 -473
  37. package/core/commands/planning.ts +3 -3
  38. package/core/commands/register.ts +104 -0
  39. package/core/commands/registry.ts +441 -0
  40. package/core/commands/setup.ts +25 -9
  41. package/core/commands/shipping.ts +48 -11
  42. package/core/commands/snapshots.ts +299 -0
  43. package/core/commands/workflow.ts +2 -2
  44. package/core/constants/index.ts +254 -4
  45. package/core/domain/agent-loader.ts +5 -6
  46. package/core/domain/task-stack.ts +555 -4
  47. package/core/errors.ts +127 -1
  48. package/core/events/events.ts +87 -0
  49. package/core/events/index.ts +4 -138
  50. package/core/index.ts +15 -23
  51. package/core/infrastructure/agent-detector.ts +126 -201
  52. package/core/infrastructure/author-detector.ts +99 -171
  53. package/core/infrastructure/command-installer.ts +476 -4
  54. package/core/infrastructure/config-manager.ts +41 -37
  55. package/core/infrastructure/path-manager.ts +59 -9
  56. package/core/infrastructure/permission-manager.ts +286 -0
  57. package/core/integrations/notion/client.ts +323 -0
  58. package/core/integrations/notion/index.ts +43 -0
  59. package/core/integrations/notion/setup.ts +230 -0
  60. package/core/integrations/notion/sync.ts +311 -0
  61. package/core/integrations/notion/templates.ts +234 -0
  62. package/core/outcomes/analyzer.ts +7 -41
  63. package/core/outcomes/index.ts +1 -1
  64. package/core/outcomes/recorder.ts +1 -1
  65. package/core/plugin/builtin/notion.ts +178 -0
  66. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  67. package/core/plugin/loader.ts +5 -5
  68. package/core/plugin/registry.ts +2 -2
  69. package/core/schemas/ideas.ts +85 -54
  70. package/core/schemas/index.ts +14 -33
  71. package/core/schemas/permissions.ts +177 -0
  72. package/core/schemas/project.ts +39 -12
  73. package/core/schemas/roadmap.ts +94 -59
  74. package/core/schemas/schemas.ts +39 -0
  75. package/core/schemas/shipped.ts +87 -60
  76. package/core/schemas/state.ts +110 -70
  77. package/core/server/index.ts +21 -0
  78. package/core/server/routes.ts +165 -0
  79. package/core/server/server.ts +136 -0
  80. package/core/server/sse.ts +135 -0
  81. package/core/services/agent-service.ts +170 -0
  82. package/core/services/breakdown-service.ts +126 -0
  83. package/core/services/index.ts +21 -0
  84. package/core/services/memory-service.ts +108 -0
  85. package/core/services/project-service.ts +146 -0
  86. package/core/services/skill-service.ts +253 -0
  87. package/core/session/compaction.ts +257 -0
  88. package/core/session/index.ts +20 -8
  89. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  90. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  91. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  92. package/core/session/utils.ts +1 -1
  93. package/core/storage/ideas-storage.ts +10 -26
  94. package/core/storage/index.ts +14 -162
  95. package/core/storage/queue-storage.ts +13 -11
  96. package/core/storage/shipped-storage.ts +4 -17
  97. package/core/storage/state-storage.ts +35 -43
  98. package/core/storage/storage-manager.ts +42 -52
  99. package/core/storage/storage.ts +160 -0
  100. package/core/sync/auth-config.ts +1 -8
  101. package/core/sync/index.ts +17 -10
  102. package/core/sync/oauth-handler.ts +1 -6
  103. package/core/sync/sync-client.ts +6 -34
  104. package/core/sync/sync-manager.ts +11 -40
  105. package/core/types/agentic.ts +577 -0
  106. package/core/types/agents.ts +145 -0
  107. package/core/types/bus.ts +82 -0
  108. package/core/types/commands.ts +366 -0
  109. package/core/types/config.ts +70 -0
  110. package/core/types/core.ts +96 -0
  111. package/core/types/domain.ts +71 -0
  112. package/core/types/events.ts +42 -0
  113. package/core/types/fs.ts +56 -0
  114. package/core/types/index.ts +396 -500
  115. package/core/types/infrastructure.ts +196 -0
  116. package/core/types/integrations.ts +57 -0
  117. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  118. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  119. package/core/types/plugin.ts +25 -0
  120. package/core/types/server.ts +54 -0
  121. package/core/types/services.ts +65 -0
  122. package/core/types/session.ts +135 -0
  123. package/core/types/storage.ts +148 -0
  124. package/core/types/sync.ts +121 -0
  125. package/core/types/task.ts +72 -0
  126. package/core/types/template.ts +24 -0
  127. package/core/types/utils.ts +90 -0
  128. package/core/utils/cache.ts +195 -0
  129. package/core/utils/collection-filters.ts +245 -0
  130. package/core/utils/date-helper.ts +1 -5
  131. package/core/utils/file-helper.ts +20 -10
  132. package/core/utils/jsonl-helper.ts +5 -8
  133. package/core/utils/markdown-builder.ts +277 -0
  134. package/core/utils/project-commands.ts +132 -0
  135. package/core/utils/runtime.ts +119 -0
  136. package/dist/bin/prjct.mjs +12568 -0
  137. package/package.json +13 -8
  138. package/scripts/build.js +106 -0
  139. package/scripts/postinstall.js +50 -8
  140. package/templates/agentic/subagent-generation.md +1 -1
  141. package/templates/commands/init.md +43 -0
  142. package/templates/commands/notion-setup.md +191 -0
  143. package/templates/commands/serve.md +118 -0
  144. package/templates/commands/ship.md +13 -2
  145. package/templates/commands/skill.md +110 -0
  146. package/templates/commands/sync.md +1 -1
  147. package/templates/commands/test.md +23 -4
  148. package/templates/mcp-config.json +28 -0
  149. package/templates/permissions/default.jsonc +60 -0
  150. package/templates/permissions/permissive.jsonc +49 -0
  151. package/templates/permissions/strict.jsonc +62 -0
  152. package/templates/skills/code-review.md +47 -0
  153. package/templates/skills/debug.md +61 -0
  154. package/templates/skills/refactor.md +47 -0
  155. package/templates/subagents/domain/devops.md +1 -1
  156. package/templates/subagents/domain/testing.md +6 -10
  157. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  158. package/templates/tools/bash.txt +22 -0
  159. package/templates/tools/edit.txt +18 -0
  160. package/templates/tools/glob.txt +19 -0
  161. package/templates/tools/grep.txt +21 -0
  162. package/templates/tools/read.txt +14 -0
  163. package/templates/tools/task.txt +20 -0
  164. package/templates/tools/webfetch.txt +16 -0
  165. package/templates/tools/websearch.txt +18 -0
  166. package/templates/tools/write.txt +17 -0
  167. package/core/agentic/command-executor/command-executor.ts +0 -312
  168. package/core/agentic/command-executor/index.ts +0 -16
  169. package/core/agentic/command-executor/status-signal.ts +0 -38
  170. package/core/agentic/command-executor/types.ts +0 -79
  171. package/core/agentic/ground-truth/index.ts +0 -76
  172. package/core/agentic/ground-truth/types.ts +0 -33
  173. package/core/agentic/ground-truth/utils.ts +0 -48
  174. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  175. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  176. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  177. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  178. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  179. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  180. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  181. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  182. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  183. package/core/agentic/ground-truth/verifiers.ts +0 -6
  184. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  185. package/core/agentic/loop-detector/hallucination.ts +0 -71
  186. package/core/agentic/loop-detector/index.ts +0 -41
  187. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  188. package/core/agentic/loop-detector/types.ts +0 -66
  189. package/core/agentic/memory-system/history.ts +0 -53
  190. package/core/agentic/memory-system/index.ts +0 -192
  191. package/core/agentic/memory-system/patterns.ts +0 -156
  192. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  193. package/core/agentic/memory-system/session.ts +0 -21
  194. package/core/agentic/plan-mode/approval.ts +0 -57
  195. package/core/agentic/plan-mode/constants.ts +0 -44
  196. package/core/agentic/plan-mode/index.ts +0 -28
  197. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  198. package/core/agentic/plan-mode/types.ts +0 -193
  199. package/core/agents/types.ts +0 -126
  200. package/core/command-registry/categories.ts +0 -23
  201. package/core/command-registry/commands.ts +0 -15
  202. package/core/command-registry/core-commands.ts +0 -344
  203. package/core/command-registry/index.ts +0 -158
  204. package/core/command-registry/optional-commands.ts +0 -163
  205. package/core/command-registry/setup-commands.ts +0 -83
  206. package/core/command-registry/types.ts +0 -59
  207. package/core/command-registry.ts +0 -9
  208. package/core/commands/types.ts +0 -185
  209. package/core/commands.ts +0 -11
  210. package/core/constants/formats.ts +0 -187
  211. package/core/context-sync.ts +0 -18
  212. package/core/data/index.ts +0 -27
  213. package/core/data/md-base-manager.ts +0 -203
  214. package/core/data/md-ideas-manager.ts +0 -155
  215. package/core/data/md-queue-manager.ts +0 -180
  216. package/core/data/md-shipped-manager.ts +0 -90
  217. package/core/data/md-state-manager.ts +0 -137
  218. package/core/domain/task-stack/index.ts +0 -19
  219. package/core/domain/task-stack/parser.ts +0 -86
  220. package/core/domain/task-stack/storage.ts +0 -123
  221. package/core/domain/task-stack/task-stack.ts +0 -340
  222. package/core/domain/task-stack/types.ts +0 -51
  223. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  224. package/core/infrastructure/command-installer/global-config.ts +0 -136
  225. package/core/infrastructure/command-installer/index.ts +0 -25
  226. package/core/infrastructure/command-installer/types.ts +0 -41
  227. package/core/infrastructure/session-manager/index.ts +0 -23
  228. package/core/infrastructure/session-manager/types.ts +0 -45
  229. package/core/infrastructure/session-manager.ts +0 -8
  230. package/core/serializers/ideas-serializer.ts +0 -187
  231. package/core/serializers/index.ts +0 -36
  232. package/core/serializers/queue-serializer.ts +0 -210
  233. package/core/serializers/shipped-serializer.ts +0 -108
  234. package/core/serializers/state-serializer.ts +0 -136
  235. package/core/session/types.ts +0 -29
  236. /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
@@ -1,8 +1,862 @@
1
1
  /**
2
2
  * Memory System
3
- * Re-exports from memory-system/index.ts for backwards compatibility.
3
+ * Tracks user preferences, decisions, and learned patterns.
4
+ *
5
+ * Three-tier memory system:
6
+ * - Tier 1: Session (ephemeral) - single command context
7
+ * - Tier 2: Patterns (persistent) - learned preferences and decisions
8
+ * - Tier 3: History (JSONL) - append-only audit log
9
+ *
10
+ * @module agentic/memory-system
11
+ * @version 3.3
4
12
  */
5
13
 
6
- import memorySystem from './memory-system/index'
14
+ import fs from 'fs/promises'
15
+ import path from 'path'
16
+ import pathManager from '../infrastructure/path-manager'
17
+ import { isNotFoundError } from '../types/fs'
18
+ import { generateUUID } from '../schemas'
19
+ import { getTimestamp, getTodayKey } from '../utils/date-helper'
20
+ import { appendJsonLine, getLastJsonLines } from '../utils/jsonl-helper'
21
+ import { ensureDir } from '../utils/file-helper'
22
+
23
+ // Re-export types from canonical location
24
+ export type {
25
+ Memory,
26
+ MemoryTag,
27
+ MemoryDatabase,
28
+ HistoryEntry,
29
+ HistoryEventType,
30
+ Decision,
31
+ Workflow,
32
+ Preference,
33
+ Patterns,
34
+ MemoryContext,
35
+ MemoryContextParams,
36
+ } from '../types/memory'
37
+
38
+ export { MEMORY_TAGS } from '../types/memory'
39
+
40
+ import type {
41
+ Memory,
42
+ MemoryTag,
43
+ MemoryDatabase,
44
+ HistoryEntry,
45
+ HistoryEventType,
46
+ Decision,
47
+ Workflow,
48
+ Preference,
49
+ Patterns,
50
+ MemoryContext,
51
+ } from '../types/memory'
52
+
53
+ import { MEMORY_TAGS } from '../types/memory'
54
+
55
+ // =============================================================================
56
+ // Base Store
57
+ // =============================================================================
58
+
59
+ /**
60
+ * CachedStore - Abstract base class for memory system stores
61
+ *
62
+ * Eliminates duplicated cache/load/save patterns across:
63
+ * - PatternStore (~40 lines of boilerplate)
64
+ * - SemanticMemories (~40 lines of boilerplate)
65
+ *
66
+ * Provides:
67
+ * - Lazy loading with project-scoped cache
68
+ * - Automatic directory creation on save
69
+ * - Reset functionality
70
+ * - Path management via pathManager
71
+ */
72
+ export abstract class CachedStore<T> {
73
+ private _data: T | null = null
74
+ private _loaded: boolean = false
75
+ private _projectId: string | null = null
76
+
77
+ /**
78
+ * Get the filename for this store (e.g., 'patterns.json', 'memories.json')
79
+ */
80
+ protected abstract getFilename(): string
81
+
82
+ /**
83
+ * Get default data structure when file doesn't exist
84
+ */
85
+ protected abstract getDefault(): T
86
+
87
+ /**
88
+ * Optional: subdirectory within memory folder
89
+ */
90
+ protected getSubdirectory(): string | null {
91
+ return null
92
+ }
93
+
94
+ /**
95
+ * Get full path for the store file
96
+ */
97
+ protected getPath(projectId: string): string {
98
+ const basePath = path.join(pathManager.getGlobalProjectPath(projectId), 'memory')
99
+
100
+ const subdir = this.getSubdirectory()
101
+ if (subdir) {
102
+ return path.join(basePath, subdir, this.getFilename())
103
+ }
104
+
105
+ return path.join(basePath, this.getFilename())
106
+ }
107
+
108
+ /**
109
+ * Load data from disk (with caching)
110
+ * Returns cached data if same project and already loaded
111
+ */
112
+ async load(projectId: string): Promise<T> {
113
+ // Return cached if same project and loaded
114
+ if (this._loaded && this._data && this._projectId === projectId) {
115
+ return this._data
116
+ }
117
+
118
+ // Load from disk
119
+ const filePath = this.getPath(projectId)
120
+
121
+ try {
122
+ const content = await fs.readFile(filePath, 'utf-8')
123
+ this._data = JSON.parse(content) as T
124
+ // Allow subclasses to normalize data after load
125
+ this.afterLoad(this._data)
126
+ } catch (error) {
127
+ if (isNotFoundError(error)) {
128
+ this._data = this.getDefault()
129
+ } else {
130
+ throw error
131
+ }
132
+ }
133
+
134
+ this._loaded = true
135
+ this._projectId = projectId
136
+
137
+ return this._data
138
+ }
139
+
140
+ /**
141
+ * Hook for subclasses to normalize data after loading
142
+ * E.g., ensuring all index keys exist
143
+ */
144
+ protected afterLoad(_data: T): void {
145
+ // Override in subclass if needed
146
+ }
147
+
148
+ /**
149
+ * Save data to disk
150
+ */
151
+ async save(projectId: string): Promise<void> {
152
+ if (!this._data) return
153
+
154
+ const filePath = this.getPath(projectId)
155
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
156
+ await fs.writeFile(filePath, JSON.stringify(this._data, null, 2), 'utf-8')
157
+ }
158
+
159
+ /**
160
+ * Get cached data without loading (may be null)
161
+ */
162
+ protected getData(): T | null {
163
+ return this._data
164
+ }
165
+
166
+ /**
167
+ * Set data directly (for subclass modifications)
168
+ */
169
+ protected setData(data: T): void {
170
+ this._data = data
171
+ }
172
+
173
+ /**
174
+ * Update data with a transform function, then save
175
+ */
176
+ async update(projectId: string, updater: (data: T) => T): Promise<T> {
177
+ const data = await this.load(projectId)
178
+ const updated = updater(data)
179
+ this._data = updated
180
+ await this.save(projectId)
181
+ return updated
182
+ }
183
+
184
+ /**
185
+ * Check if data has been loaded for a project
186
+ */
187
+ isLoaded(projectId?: string): boolean {
188
+ if (projectId) {
189
+ return this._loaded && this._projectId === projectId
190
+ }
191
+ return this._loaded
192
+ }
193
+
194
+ /**
195
+ * Reset cache (forces reload on next access)
196
+ */
197
+ reset(): void {
198
+ this._data = null
199
+ this._loaded = false
200
+ this._projectId = null
201
+ }
202
+ }
203
+
204
+ // =============================================================================
205
+ // Session Store (Tier 1)
206
+ // =============================================================================
207
+
208
+ /**
209
+ * Session Memory - Tier 1
210
+ * Ephemeral, single command context.
211
+ */
212
+ export class SessionStore {
213
+ private _sessionMemory: Map<string, { value: unknown; timestamp: number }> = new Map()
214
+
215
+ setSession(key: string, value: unknown): void {
216
+ this._sessionMemory.set(key, { value, timestamp: Date.now() })
217
+ }
218
+
219
+ getSession(key: string): unknown {
220
+ const entry = this._sessionMemory.get(key)
221
+ return entry?.value
222
+ }
223
+
224
+ clearSession(): void {
225
+ this._sessionMemory.clear()
226
+ }
227
+ }
228
+
229
+ // =============================================================================
230
+ // History Store (Tier 3)
231
+ // =============================================================================
232
+
233
+ /**
234
+ * History - Tier 3
235
+ * Append-only JSONL audit log with temporal fragmentation.
236
+ */
237
+ export class HistoryStore {
238
+ private _getSessionPath(projectId: string): string {
239
+ const now = new Date()
240
+ const yearMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
241
+ const day = getTodayKey()
242
+
243
+ return path.join(pathManager.getGlobalProjectPath(projectId), 'memory', 'sessions', yearMonth, `${day}.jsonl`)
244
+ }
245
+
246
+ async appendHistory(projectId: string, entry: Record<string, unknown> & { type: HistoryEventType }): Promise<void> {
247
+ const sessionPath = this._getSessionPath(projectId)
248
+ await ensureDir(path.dirname(sessionPath))
249
+
250
+ const logEntry: HistoryEntry = {
251
+ ts: getTimestamp(),
252
+ ...entry,
253
+ type: entry.type,
254
+ }
255
+
256
+ await appendJsonLine(sessionPath, logEntry)
257
+ }
258
+
259
+ async getRecentHistory(projectId: string, limit: number = 20): Promise<HistoryEntry[]> {
260
+ const sessionPath = this._getSessionPath(projectId)
261
+ return getLastJsonLines<HistoryEntry>(sessionPath, limit)
262
+ }
263
+ }
264
+
265
+ // =============================================================================
266
+ // Pattern Store (Tier 2)
267
+ // =============================================================================
268
+
269
+ /**
270
+ * Patterns - Tier 2
271
+ * Persistent learned preferences and decisions.
272
+ */
273
+ export class PatternStore extends CachedStore<Patterns> {
274
+ protected getFilename(): string {
275
+ return 'patterns.json'
276
+ }
277
+
278
+ protected getDefault(): Patterns {
279
+ return {
280
+ version: 1,
281
+ decisions: {},
282
+ preferences: {},
283
+ workflows: {},
284
+ counters: {},
285
+ }
286
+ }
287
+
288
+ // Convenience alias for backward compatibility
289
+ async loadPatterns(projectId: string): Promise<Patterns> {
290
+ return this.load(projectId)
291
+ }
292
+
293
+ async savePatterns(projectId: string): Promise<void> {
294
+ return this.save(projectId)
295
+ }
296
+
297
+ async recordDecision(projectId: string, key: string, value: string, context: string = ''): Promise<void> {
298
+ const patterns = await this.load(projectId)
299
+ const now = getTimestamp()
300
+
301
+ if (!patterns.decisions[key]) {
302
+ patterns.decisions[key] = {
303
+ value,
304
+ count: 1,
305
+ firstSeen: now,
306
+ lastSeen: now,
307
+ confidence: 'low',
308
+ contexts: [context].filter(Boolean),
309
+ }
310
+ } else {
311
+ const decision = patterns.decisions[key]
312
+
313
+ if (decision.value === value) {
314
+ decision.count++
315
+ decision.lastSeen = now
316
+ if (context && !decision.contexts.includes(context)) {
317
+ decision.contexts.push(context)
318
+ }
319
+
320
+ if (decision.count >= 5) {
321
+ decision.confidence = 'high'
322
+ } else if (decision.count >= 3) {
323
+ decision.confidence = 'medium'
324
+ }
325
+ } else {
326
+ decision.value = value
327
+ decision.count = 1
328
+ decision.lastSeen = now
329
+ decision.confidence = 'low'
330
+ }
331
+ }
332
+
333
+ await this.save(projectId)
334
+ }
335
+
336
+ async getDecision(projectId: string, key: string): Promise<{ value: string; confidence: string } | null> {
337
+ const patterns = await this.load(projectId)
338
+ const decision = patterns.decisions[key]
339
+
340
+ if (!decision) return null
341
+ if (decision.confidence === 'low') return null
342
+
343
+ return { value: decision.value, confidence: decision.confidence }
344
+ }
345
+
346
+ async hasPattern(projectId: string, key: string): Promise<boolean> {
347
+ const decision = await this.getDecision(projectId, key)
348
+ return decision !== null
349
+ }
350
+
351
+ async recordWorkflow(projectId: string, workflowName: string, pattern: Record<string, unknown>): Promise<void> {
352
+ const patterns = await this.load(projectId)
353
+ const now = getTimestamp()
354
+
355
+ if (!patterns.workflows[workflowName]) {
356
+ patterns.workflows[workflowName] = {
357
+ ...pattern,
358
+ count: 1,
359
+ firstSeen: now,
360
+ lastSeen: now,
361
+ }
362
+ } else {
363
+ patterns.workflows[workflowName].count++
364
+ patterns.workflows[workflowName].lastSeen = now
365
+ }
366
+
367
+ await this.save(projectId)
368
+ }
369
+
370
+ async getWorkflow(projectId: string, workflowName: string): Promise<Workflow | null> {
371
+ const patterns = await this.load(projectId)
372
+ const workflow = patterns.workflows[workflowName]
373
+
374
+ if (!workflow || workflow.count < 3) return null
375
+ return workflow
376
+ }
377
+
378
+ async setPreference(projectId: string, key: string, value: Preference['value']): Promise<void> {
379
+ const patterns = await this.load(projectId)
380
+ patterns.preferences[key] = { value, updatedAt: getTimestamp() }
381
+ await this.save(projectId)
382
+ }
383
+
384
+ async getPreference(projectId: string, key: string, defaultValue: unknown = null): Promise<unknown> {
385
+ const patterns = await this.load(projectId)
386
+ return patterns.preferences[key]?.value ?? defaultValue
387
+ }
388
+
389
+ async getPatternsSummary(projectId: string) {
390
+ const patterns = await this.load(projectId)
391
+
392
+ return {
393
+ decisions: Object.keys(patterns.decisions).length,
394
+ learnedDecisions: Object.values(patterns.decisions).filter((d) => d.confidence !== 'low').length,
395
+ workflows: Object.keys(patterns.workflows).length,
396
+ preferences: Object.keys(patterns.preferences).length,
397
+ }
398
+ }
399
+ }
400
+
401
+ // =============================================================================
402
+ // Semantic Memories
403
+ // =============================================================================
404
+
405
+ /**
406
+ * Semantic Memories
407
+ * P3.3: Tagged, searchable, CRUD memory operations.
408
+ */
409
+ export class SemanticMemories extends CachedStore<MemoryDatabase> {
410
+ protected getFilename(): string {
411
+ return 'memories.json'
412
+ }
413
+
414
+ protected getDefault(): MemoryDatabase {
415
+ return {
416
+ version: 1,
417
+ memories: [],
418
+ index: this._createEmptyIndex(),
419
+ }
420
+ }
421
+
422
+ protected afterLoad(db: MemoryDatabase): void {
423
+ this._normalizeIndex(db)
424
+ }
425
+
426
+ private _createEmptyIndex(): Record<string, string[]> {
427
+ const tags = Object.values(MEMORY_TAGS)
428
+ const index: Record<string, string[]> = {}
429
+ for (const tag of tags) index[tag] = []
430
+ return index
431
+ }
432
+
433
+ private _normalizeIndex(db: MemoryDatabase): void {
434
+ // Reason: older persisted files may not include newer tags; ensure all tags are present.
435
+ const tags = Object.values(MEMORY_TAGS)
436
+ for (const tag of tags) {
437
+ if (!db.index[tag]) db.index[tag] = []
438
+ }
439
+ }
440
+
441
+ private _coerceTags(tags: string[]): MemoryTag[] {
442
+ const allowed = new Set<MemoryTag>(Object.values(MEMORY_TAGS) as MemoryTag[])
443
+ return tags.filter((t): t is MemoryTag => allowed.has(t as MemoryTag))
444
+ }
445
+
446
+ // Convenience alias for backward compatibility
447
+ async loadMemories(projectId: string): Promise<MemoryDatabase> {
448
+ return this.load(projectId)
449
+ }
450
+
451
+ async saveMemories(projectId: string): Promise<void> {
452
+ return this.save(projectId)
453
+ }
454
+
455
+ async createMemory(
456
+ projectId: string,
457
+ {
458
+ title,
459
+ content,
460
+ tags = [],
461
+ userTriggered = false,
462
+ }: { title: string; content: string; tags?: string[]; userTriggered?: boolean }
463
+ ): Promise<string> {
464
+ const db = await this.load(projectId)
465
+ const parsedTags = this._coerceTags(tags)
466
+ const now = getTimestamp()
467
+
468
+ const memory: Memory = {
469
+ id: generateUUID(),
470
+ title,
471
+ content,
472
+ tags: parsedTags,
473
+ userTriggered,
474
+ createdAt: now,
475
+ updatedAt: now,
476
+ }
477
+
478
+ db.memories.push(memory)
479
+
480
+ for (const tag of parsedTags) {
481
+ db.index[tag].push(memory.id)
482
+ }
483
+
484
+ await this.save(projectId)
485
+ return memory.id
486
+ }
487
+
488
+ async updateMemory(
489
+ projectId: string,
490
+ memoryId: string,
491
+ updates: { title?: string; content?: string; tags?: string[] }
492
+ ): Promise<boolean> {
493
+ const db = await this.load(projectId)
494
+
495
+ const index = db.memories.findIndex((m) => m.id === memoryId)
496
+ if (index === -1) return false
497
+
498
+ const memory = db.memories[index]
499
+ const oldTags = memory.tags || []
500
+
501
+ if (updates.title) memory.title = updates.title
502
+ if (updates.content) memory.content = updates.content
503
+ if (updates.tags) {
504
+ const newTags = this._coerceTags(updates.tags)
505
+ for (const tag of oldTags) {
506
+ db.index[tag] = db.index[tag].filter((id: string) => id !== memoryId)
507
+ }
508
+ for (const tag of newTags) {
509
+ db.index[tag].push(memoryId)
510
+ }
511
+ memory.tags = newTags
512
+ }
513
+
514
+ memory.updatedAt = getTimestamp()
515
+ await this.save(projectId)
516
+ return true
517
+ }
518
+
519
+ async deleteMemory(projectId: string, memoryId: string): Promise<boolean> {
520
+ const db = await this.load(projectId)
521
+
522
+ const index = db.memories.findIndex((m) => m.id === memoryId)
523
+ if (index === -1) return false
524
+
525
+ const memory = db.memories[index]
526
+
527
+ for (const tag of memory.tags || []) {
528
+ if (db.index[tag]) {
529
+ db.index[tag] = db.index[tag].filter((id) => id !== memoryId)
530
+ }
531
+ }
532
+
533
+ db.memories.splice(index, 1)
534
+ await this.save(projectId)
535
+ return true
536
+ }
537
+
538
+ async findByTags(projectId: string, tags: string[], matchAll: boolean = false): Promise<Memory[]> {
539
+ const db = await this.load(projectId)
540
+ const parsedTags = this._coerceTags(tags)
541
+
542
+ if (matchAll) {
543
+ return db.memories.filter((m) => parsedTags.every((tag) => (m.tags || []).includes(tag)))
544
+ } else {
545
+ const matchingIds = new Set<string>()
546
+ for (const tag of parsedTags) {
547
+ const ids = db.index[tag]
548
+ ids.forEach((id: string) => matchingIds.add(id))
549
+ }
550
+ return db.memories.filter((m) => matchingIds.has(m.id))
551
+ }
552
+ }
553
+
554
+ async searchMemories(projectId: string, query: string): Promise<Memory[]> {
555
+ const db = await this.load(projectId)
556
+ const queryLower = query.toLowerCase()
557
+
558
+ return db.memories.filter(
559
+ (m) => m.title.toLowerCase().includes(queryLower) || m.content.toLowerCase().includes(queryLower)
560
+ )
561
+ }
562
+
563
+ async getRelevantMemories(projectId: string, context: MemoryContext, limit: number = 5): Promise<Memory[]> {
564
+ const db = await this.load(projectId)
565
+
566
+ const scored = db.memories.map((memory) => {
567
+ let score = 0
568
+
569
+ const contextTags = this._extractContextTags(context)
570
+ for (const tag of memory.tags || []) {
571
+ if (contextTags.includes(tag)) score += 10
572
+ }
573
+
574
+ const age = Date.now() - new Date(memory.updatedAt).getTime()
575
+ const daysSinceUpdate = age / (1000 * 60 * 60 * 24)
576
+ score += Math.max(0, 5 - daysSinceUpdate)
577
+
578
+ if (memory.userTriggered) score += 5
579
+
580
+ const keywords = this._extractKeywords(context)
581
+ for (const keyword of keywords) {
582
+ if (memory.content.toLowerCase().includes(keyword)) score += 2
583
+ if (memory.title.toLowerCase().includes(keyword)) score += 3
584
+ }
585
+
586
+ return { ...memory, _score: score }
587
+ })
588
+
589
+ return scored
590
+ .filter((m) => m._score > 0)
591
+ .sort((a, b) => b._score - a._score)
592
+ .slice(0, limit)
593
+ .map(({ _score, ...memory }) => memory as Memory)
594
+ }
595
+
596
+ private _extractContextTags(context: MemoryContext): string[] {
597
+ const tags: string[] = []
598
+
599
+ const commandTags: Record<string, string[]> = {
600
+ ship: [MEMORY_TAGS.COMMIT_STYLE, MEMORY_TAGS.SHIP_WORKFLOW, MEMORY_TAGS.TEST_BEHAVIOR],
601
+ feature: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.CODE_STYLE],
602
+ done: [MEMORY_TAGS.SHIP_WORKFLOW],
603
+ analyze: [MEMORY_TAGS.TECH_STACK, MEMORY_TAGS.ARCHITECTURE],
604
+ spec: [MEMORY_TAGS.ARCHITECTURE, MEMORY_TAGS.CODE_STYLE],
605
+ }
606
+
607
+ if (context.commandName && commandTags[context.commandName]) {
608
+ tags.push(...commandTags[context.commandName])
609
+ }
610
+
611
+ return tags
612
+ }
613
+
614
+ private _extractKeywords(context: MemoryContext): string[] {
615
+ const keywords: string[] = []
616
+
617
+ if (context.params?.description) {
618
+ keywords.push(...(context.params.description as string).toLowerCase().split(/\s+/))
619
+ }
620
+ if (context.params?.feature) {
621
+ keywords.push(...(context.params.feature as string).toLowerCase().split(/\s+/))
622
+ }
623
+
624
+ const stopWords = ['the', 'a', 'an', 'is', 'are', 'to', 'for', 'and', 'or', 'in']
625
+ return keywords.filter((k) => k.length > 2 && !stopWords.includes(k))
626
+ }
627
+
628
+ async autoRemember(projectId: string, decisionType: string, value: string, context: string = ''): Promise<void> {
629
+ const tagMap: Record<string, string[]> = {
630
+ commit_footer: [MEMORY_TAGS.COMMIT_STYLE],
631
+ branch_naming: [MEMORY_TAGS.BRANCH_NAMING],
632
+ test_before_ship: [MEMORY_TAGS.TEST_BEHAVIOR, MEMORY_TAGS.SHIP_WORKFLOW],
633
+ preferred_agent: [MEMORY_TAGS.AGENT_PREFERENCE],
634
+ code_style: [MEMORY_TAGS.CODE_STYLE],
635
+ verbosity: [MEMORY_TAGS.OUTPUT_VERBOSITY],
636
+ }
637
+
638
+ const tags = tagMap[decisionType] || []
639
+
640
+ const existing = await this.searchMemories(projectId, decisionType)
641
+ if (existing.length > 0) {
642
+ await this.updateMemory(projectId, existing[0].id, {
643
+ content: `${decisionType}: ${value}`,
644
+ tags,
645
+ })
646
+ } else {
647
+ await this.createMemory(projectId, {
648
+ title: `Preference: ${decisionType}`,
649
+ content: `${decisionType}: ${value}${context ? `\nContext: ${context}` : ''}`,
650
+ tags,
651
+ userTriggered: true,
652
+ })
653
+ }
654
+ }
655
+
656
+ async getAllMemories(projectId: string): Promise<Memory[]> {
657
+ const db = await this.load(projectId)
658
+ return db.memories
659
+ }
660
+
661
+ async getMemoryStats(projectId: string) {
662
+ const db = await this.load(projectId)
663
+
664
+ const tagCounts: Record<string, number> = {}
665
+ for (const [tag, ids] of Object.entries(db.index)) {
666
+ tagCounts[tag] = ids.length
667
+ }
668
+
669
+ return {
670
+ totalMemories: db.memories.length,
671
+ userTriggered: db.memories.filter((m) => m.userTriggered).length,
672
+ tagCounts,
673
+ oldestMemory: db.memories[0]?.createdAt,
674
+ newestMemory: db.memories[db.memories.length - 1]?.createdAt,
675
+ }
676
+ }
677
+ }
678
+
679
+ // =============================================================================
680
+ // Memory System (Main Class)
681
+ // =============================================================================
682
+
683
+ /**
684
+ * Three-tier memory system for learning user patterns.
685
+ * Tier 1: Session (ephemeral), Tier 2: Patterns (persistent), Tier 3: History (JSONL)
686
+ */
687
+ export class MemorySystem {
688
+ private _semanticMemories: SemanticMemories
689
+ private _patternStore: PatternStore
690
+ private _historyStore: HistoryStore
691
+ private _sessionStore: SessionStore
692
+
693
+ constructor() {
694
+ this._semanticMemories = new SemanticMemories()
695
+ this._patternStore = new PatternStore()
696
+ this._historyStore = new HistoryStore()
697
+ this._sessionStore = new SessionStore()
698
+ }
699
+
700
+ // ===========================================================================
701
+ // P3.3: SEMANTIC MEMORIES
702
+ // ===========================================================================
703
+
704
+ loadMemories(projectId: string) {
705
+ return this._semanticMemories.loadMemories(projectId)
706
+ }
707
+
708
+ saveMemories(projectId: string) {
709
+ return this._semanticMemories.saveMemories(projectId)
710
+ }
711
+
712
+ createMemory(
713
+ projectId: string,
714
+ options: { title: string; content: string; tags?: string[]; userTriggered?: boolean }
715
+ ): Promise<string> {
716
+ return this._semanticMemories.createMemory(projectId, options)
717
+ }
718
+
719
+ updateMemory(
720
+ projectId: string,
721
+ memoryId: string,
722
+ updates: { title?: string; content?: string; tags?: string[] }
723
+ ): Promise<boolean> {
724
+ return this._semanticMemories.updateMemory(projectId, memoryId, updates)
725
+ }
726
+
727
+ deleteMemory(projectId: string, memoryId: string): Promise<boolean> {
728
+ return this._semanticMemories.deleteMemory(projectId, memoryId)
729
+ }
730
+
731
+ findByTags(projectId: string, tags: string[], matchAll?: boolean): Promise<Memory[]> {
732
+ return this._semanticMemories.findByTags(projectId, tags, matchAll)
733
+ }
734
+
735
+ searchMemories(projectId: string, query: string): Promise<Memory[]> {
736
+ return this._semanticMemories.searchMemories(projectId, query)
737
+ }
738
+
739
+ getRelevantMemories(projectId: string, context: MemoryContext, limit?: number): Promise<Memory[]> {
740
+ return this._semanticMemories.getRelevantMemories(projectId, context, limit)
741
+ }
742
+
743
+ autoRemember(projectId: string, decisionType: string, value: string, context?: string): Promise<void> {
744
+ return this._semanticMemories.autoRemember(projectId, decisionType, value, context)
745
+ }
746
+
747
+ getAllMemories(projectId: string): Promise<Memory[]> {
748
+ return this._semanticMemories.getAllMemories(projectId)
749
+ }
750
+
751
+ getMemoryStats(projectId: string) {
752
+ return this._semanticMemories.getMemoryStats(projectId)
753
+ }
754
+
755
+ // ===========================================================================
756
+ // TIER 1: Session Memory
757
+ // ===========================================================================
758
+
759
+ setSession(key: string, value: unknown): void {
760
+ this._sessionStore.setSession(key, value)
761
+ }
762
+
763
+ getSession(key: string): unknown {
764
+ return this._sessionStore.getSession(key)
765
+ }
766
+
767
+ clearSession(): void {
768
+ this._sessionStore.clearSession()
769
+ }
770
+
771
+ // ===========================================================================
772
+ // TIER 2: Patterns
773
+ // ===========================================================================
774
+
775
+ loadPatterns(projectId: string) {
776
+ return this._patternStore.loadPatterns(projectId)
777
+ }
778
+
779
+ savePatterns(projectId: string) {
780
+ return this._patternStore.savePatterns(projectId)
781
+ }
782
+
783
+ recordDecision(projectId: string, key: string, value: string, context?: string): Promise<void> {
784
+ return this._patternStore.recordDecision(projectId, key, value, context)
785
+ }
786
+
787
+ getDecision(projectId: string, key: string): Promise<{ value: string; confidence: string } | null> {
788
+ return this._patternStore.getDecision(projectId, key)
789
+ }
790
+
791
+ hasPattern(projectId: string, key: string): Promise<boolean> {
792
+ return this._patternStore.hasPattern(projectId, key)
793
+ }
794
+
795
+ recordWorkflow(projectId: string, workflowName: string, pattern: Record<string, unknown>): Promise<void> {
796
+ return this._patternStore.recordWorkflow(projectId, workflowName, pattern)
797
+ }
798
+
799
+ getWorkflow(projectId: string, workflowName: string): Promise<Workflow | null> {
800
+ return this._patternStore.getWorkflow(projectId, workflowName)
801
+ }
802
+
803
+ setPreference(projectId: string, key: string, value: Preference['value']): Promise<void> {
804
+ return this._patternStore.setPreference(projectId, key, value)
805
+ }
806
+
807
+ getPreference(projectId: string, key: string, defaultValue?: unknown): Promise<unknown> {
808
+ return this._patternStore.getPreference(projectId, key, defaultValue)
809
+ }
810
+
811
+ getPatternsSummary(projectId: string) {
812
+ return this._patternStore.getPatternsSummary(projectId)
813
+ }
814
+
815
+ // ===========================================================================
816
+ // TIER 3: History
817
+ // ===========================================================================
818
+
819
+ appendHistory(projectId: string, entry: Record<string, unknown> & { type: HistoryEventType }): Promise<void> {
820
+ return this._historyStore.appendHistory(projectId, entry)
821
+ }
822
+
823
+ getRecentHistory(projectId: string, limit?: number) {
824
+ return this._historyStore.getRecentHistory(projectId, limit)
825
+ }
826
+
827
+ // ===========================================================================
828
+ // CONVENIENCE: Combined operations
829
+ // ===========================================================================
830
+
831
+ async getSmartDecision(projectId: string, key: string): Promise<string | null> {
832
+ const sessionValue = this.getSession(`decision:${key}`)
833
+ if (sessionValue !== undefined) return sessionValue as string
834
+
835
+ const pattern = await this.getDecision(projectId, key)
836
+ if (pattern) return pattern.value
837
+
838
+ return null
839
+ }
840
+
841
+ async learnDecision(projectId: string, key: string, value: string, context: string = ''): Promise<void> {
842
+ this.setSession(`decision:${key}`, value)
843
+ await this.recordDecision(projectId, key, value, context)
844
+ await this.appendHistory(projectId, { type: 'decision', key, value, context })
845
+ }
846
+
847
+ /**
848
+ * Reset internal state (for testing)
849
+ */
850
+ resetState(): void {
851
+ this._sessionStore.clearSession()
852
+ this._semanticMemories.reset()
853
+ this._patternStore.reset()
854
+ }
855
+ }
856
+
857
+ // =============================================================================
858
+ // Default Export
859
+ // =============================================================================
860
+
861
+ const memorySystem = new MemorySystem()
7
862
  export default memorySystem
8
- export { MemorySystem, MEMORY_TAGS } from './memory-system/index'