prjct-cli 0.44.1 → 0.45.3

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 (207) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/bin/prjct.ts +131 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +287 -29
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +49 -8
  43. package/core/commands/commands.ts +60 -23
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +14 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +583 -0
  59. package/core/context-tools/imports-tool.ts +403 -0
  60. package/core/context-tools/index.ts +433 -0
  61. package/core/context-tools/recent-tool.ts +307 -0
  62. package/core/context-tools/signatures-tool.ts +501 -0
  63. package/core/context-tools/summary-tool.ts +307 -0
  64. package/core/context-tools/token-counter.ts +284 -0
  65. package/core/context-tools/types.ts +253 -0
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -12
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +143 -0
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +170 -329
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -13
  157. package/core/storage/metrics-storage.ts +320 -0
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -302
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +49 -0
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18907 -13189
  194. package/dist/core/infrastructure/command-installer.js +96 -111
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +256 -257
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -15,21 +15,23 @@
15
15
  * @version 1.0.0
16
16
  */
17
17
 
18
- import fs from 'fs/promises'
19
- import path from 'path'
20
- import { exec } from 'child_process'
21
- import { promisify } from 'util'
22
- import pathManager from '../infrastructure/path-manager'
23
- import configManager from '../infrastructure/config-manager'
24
- import dateHelper from '../utils/date-helper'
18
+ import { exec } from 'node:child_process'
19
+ import fs from 'node:fs/promises'
20
+ import path from 'node:path'
21
+ import { promisify } from 'node:util'
25
22
  import {
26
- generateAIToolContexts,
27
23
  DEFAULT_AI_TOOLS,
28
- resolveToolIds,
29
24
  detectInstalledTools,
25
+ generateAIToolContexts,
30
26
  type ProjectContext,
31
- type GenerateResult,
27
+ resolveToolIds,
32
28
  } from '../ai-tools'
29
+ import configManager from '../infrastructure/config-manager'
30
+ import pathManager from '../infrastructure/path-manager'
31
+ import { metricsStorage } from '../storage/metrics-storage'
32
+ import dateHelper from '../utils/date-helper'
33
+ import { ContextFileGenerator } from './context-generator'
34
+ import { type StackDetection, StackDetector } from './stack-detector'
33
35
 
34
36
  const execAsync = promisify(exec)
35
37
 
@@ -69,16 +71,6 @@ interface Commands {
69
71
  format: string
70
72
  }
71
73
 
72
- interface StackDetection {
73
- hasFrontend: boolean
74
- hasBackend: boolean
75
- hasDatabase: boolean
76
- hasDocker: boolean
77
- hasTesting: boolean
78
- frontendType: 'web' | 'mobile' | 'both' | null
79
- frameworks: string[]
80
- }
81
-
82
74
  interface AgentInfo {
83
75
  name: string
84
76
  type: 'workflow' | 'domain'
@@ -91,6 +83,13 @@ interface AIToolResult {
91
83
  success: boolean
92
84
  }
93
85
 
86
+ interface SyncMetrics {
87
+ duration: number // Sync duration in ms
88
+ originalSize: number // Estimated tokens before compression
89
+ filteredSize: number // Actual tokens in context files
90
+ compressionRate: number // Percentage saved
91
+ }
92
+
94
93
  interface SyncResult {
95
94
  success: boolean
96
95
  projectId: string
@@ -103,11 +102,12 @@ interface SyncResult {
103
102
  skills: { agent: string; skill: string }[]
104
103
  contextFiles: string[]
105
104
  aiTools: AIToolResult[]
105
+ syncMetrics?: SyncMetrics
106
106
  error?: string
107
107
  }
108
108
 
109
109
  interface SyncOptions {
110
- aiTools?: string[] // AI tools to generate context for (default: claude, cursor)
110
+ aiTools?: string[] // AI tools to generate context for (default: claude, cursor)
111
111
  }
112
112
 
113
113
  // ============================================================================
@@ -129,6 +129,7 @@ class SyncService {
129
129
  */
130
130
  async sync(projectPath: string = process.cwd(), options: SyncOptions = {}): Promise<SyncResult> {
131
131
  this.projectPath = projectPath
132
+ const startTime = Date.now()
132
133
 
133
134
  // Resolve AI tools: supports 'auto', 'all', or specific list
134
135
  let aiToolIds: string[]
@@ -166,16 +167,22 @@ class SyncService {
166
167
  this.globalPath = pathManager.getGlobalProjectPath(this.projectId)
167
168
  this.cliVersion = await this.getCliVersion()
168
169
 
169
- // 2. Ensure directories exist
170
- await this.ensureDirectories()
170
+ // 2. Ensure directories exist (non-blocking)
171
+ const ensureDirsPromise = this.ensureDirectories()
172
+
173
+ // 3. Gather all data IN PARALLEL (30-50% speedup)
174
+ // These operations are independent and can run concurrently
175
+ const [git, stats, commands, stack] = await Promise.all([
176
+ this.analyzeGit(),
177
+ this.gatherStats(),
178
+ this.detectCommands(),
179
+ this.detectStack(),
180
+ ])
171
181
 
172
- // 3. Gather all data
173
- const git = await this.analyzeGit()
174
- const stats = await this.gatherStats()
175
- const commands = await this.detectCommands()
176
- const stack = await this.detectStack()
182
+ // Wait for directories before writing files
183
+ await ensureDirsPromise
177
184
 
178
- // 4. Generate all files
185
+ // 4. Generate all files (depends on gathered data)
179
186
  const agents = await this.generateAgents(stack, stats)
180
187
  const skills = this.configureSkills(agents)
181
188
  const contextFiles = await this.generateContextFiles(git, stats, commands, agents)
@@ -196,8 +203,8 @@ class SyncService {
196
203
  hasChanges: git.hasChanges,
197
204
  commands,
198
205
  agents: {
199
- workflow: agents.filter(a => a.type === 'workflow').map(a => a.name),
200
- domain: agents.filter(a => a.type === 'domain').map(a => a.name),
206
+ workflow: agents.filter((a) => a.type === 'workflow').map((a) => a.name),
207
+ domain: agents.filter((a) => a.type === 'domain').map((a) => a.name),
201
208
  },
202
209
  }
203
210
 
@@ -208,14 +215,16 @@ class SyncService {
208
215
  aiToolIds
209
216
  )
210
217
 
211
- // 6. Update project.json
212
- await this.updateProjectJson(git, stats)
218
+ // 6-8. Update files IN PARALLEL (write to different files)
219
+ await Promise.all([
220
+ this.updateProjectJson(git, stats),
221
+ this.updateStateJson(stats, stack),
222
+ this.logToMemory(git, stats),
223
+ ])
213
224
 
214
- // 7. Update state.json with enterprise fields
215
- await this.updateStateJson(stats, stack)
216
-
217
- // 8. Log to memory
218
- await this.logToMemory(git, stats)
225
+ // 9. Record metrics for value dashboard
226
+ const duration = Date.now() - startTime
227
+ const syncMetrics = await this.recordSyncMetrics(stats, contextFiles, agents, duration)
219
228
 
220
229
  return {
221
230
  success: true,
@@ -228,11 +237,12 @@ class SyncService {
228
237
  agents,
229
238
  skills,
230
239
  contextFiles,
231
- aiTools: aiToolResults.map(r => ({
240
+ aiTools: aiToolResults.map((r) => ({
232
241
  toolId: r.toolId,
233
242
  outputFile: r.outputFile,
234
243
  success: r.success,
235
244
  })),
245
+ syncMetrics,
236
246
  }
237
247
  } catch (error) {
238
248
  return {
@@ -258,9 +268,10 @@ class SyncService {
258
268
 
259
269
  private async ensureDirectories(): Promise<void> {
260
270
  const dirs = ['storage', 'context', 'agents', 'memory', 'analysis', 'config', 'sync']
261
- for (const dir of dirs) {
262
- await fs.mkdir(path.join(this.globalPath, dir), { recursive: true })
263
- }
271
+ // Create all directories IN PARALLEL
272
+ await Promise.all(
273
+ dirs.map((dir) => fs.mkdir(path.join(this.globalPath, dir), { recursive: true }))
274
+ )
264
275
  }
265
276
 
266
277
  // ==========================================================================
@@ -291,13 +302,13 @@ class SyncService {
291
302
  const { stdout: commits } = await execAsync('git rev-list --count HEAD', {
292
303
  cwd: this.projectPath,
293
304
  })
294
- data.commits = parseInt(commits.trim()) || 0
305
+ data.commits = parseInt(commits.trim(), 10) || 0
295
306
 
296
307
  // Contributors
297
308
  const { stdout: contributors } = await execAsync('git shortlog -sn --all | wc -l', {
298
309
  cwd: this.projectPath,
299
310
  })
300
- data.contributors = parseInt(contributors.trim()) || 0
311
+ data.contributors = parseInt(contributors.trim(), 10) || 0
301
312
 
302
313
  // Status
303
314
  const { stdout: status } = await execAsync('git status --porcelain', {
@@ -335,7 +346,7 @@ class SyncService {
335
346
  const { stdout: weekly } = await execAsync('git log --oneline --since="1 week ago" | wc -l', {
336
347
  cwd: this.projectPath,
337
348
  })
338
- data.weeklyCommits = parseInt(weekly.trim()) || 0
349
+ data.weeklyCommits = parseInt(weekly.trim(), 10) || 0
339
350
  } catch {
340
351
  // Not a git repo - use defaults
341
352
  }
@@ -364,7 +375,7 @@ class SyncService {
364
375
  'find . -type f \\( -name "*.js" -o -name "*.ts" -o -name "*.tsx" -o -name "*.py" -o -name "*.go" -o -name "*.rs" \\) -not -path "./node_modules/*" -not -path "./.git/*" | wc -l',
365
376
  { cwd: this.projectPath }
366
377
  )
367
- stats.fileCount = parseInt(stdout.trim()) || 0
378
+ stats.fileCount = parseInt(stdout.trim(), 10) || 0
368
379
  } catch {
369
380
  stats.fileCount = 0
370
381
  }
@@ -407,7 +418,7 @@ class SyncService {
407
418
  stats.ecosystem = 'Go'
408
419
  stats.languages.push('Go')
409
420
  }
410
- if (await this.fileExists('requirements.txt') || await this.fileExists('pyproject.toml')) {
421
+ if ((await this.fileExists('requirements.txt')) || (await this.fileExists('pyproject.toml'))) {
411
422
  stats.ecosystem = 'Python'
412
423
  stats.languages.push('Python')
413
424
  }
@@ -493,60 +504,8 @@ class SyncService {
493
504
  // ==========================================================================
494
505
 
495
506
  private async detectStack(): Promise<StackDetection> {
496
- const stack: StackDetection = {
497
- hasFrontend: false,
498
- hasBackend: false,
499
- hasDatabase: false,
500
- hasDocker: false,
501
- hasTesting: false,
502
- frontendType: null,
503
- frameworks: [],
504
- }
505
-
506
- try {
507
- const pkgPath = path.join(this.projectPath, 'package.json')
508
- const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'))
509
- const deps = { ...pkg.dependencies, ...pkg.devDependencies }
510
-
511
- // Frontend detection
512
- if (deps.react || deps.vue || deps.svelte || deps['@angular/core']) {
513
- stack.hasFrontend = true
514
- stack.frontendType = 'web'
515
- }
516
- if (deps['react-native'] || deps.expo) {
517
- stack.hasFrontend = true
518
- stack.frontendType = stack.frontendType === 'web' ? 'both' : 'mobile'
519
- }
520
-
521
- // Backend detection
522
- if (deps.express || deps.fastify || deps.hono || deps.koa || deps.nest) {
523
- stack.hasBackend = true
524
- }
525
-
526
- // Database detection
527
- if (deps.prisma || deps.mongoose || deps.pg || deps.mysql2 || deps.sequelize) {
528
- stack.hasDatabase = true
529
- }
530
-
531
- // Testing detection
532
- if (deps.jest || deps.vitest || deps.mocha || pkg.devDependencies?.['bun-types']) {
533
- stack.hasTesting = true
534
- }
535
-
536
- // Collect frameworks
537
- if (deps.react) stack.frameworks.push('React')
538
- if (deps.next) stack.frameworks.push('Next.js')
539
- if (deps.express) stack.frameworks.push('Express')
540
- if (deps.hono) stack.frameworks.push('Hono')
541
- } catch {
542
- // No package.json
543
- }
544
-
545
- // Docker detection
546
- stack.hasDocker =
547
- (await this.fileExists('Dockerfile')) || (await this.fileExists('docker-compose.yml'))
548
-
549
- return stack
507
+ const detector = new StackDetector(this.projectPath)
508
+ return detector.detect()
550
509
  }
551
510
 
552
511
  // ==========================================================================
@@ -569,40 +528,43 @@ class SyncService {
569
528
  // Directory might not exist yet
570
529
  }
571
530
 
572
- // Workflow agents (always generated)
531
+ // Workflow agents (always generated) - IN PARALLEL
573
532
  const workflowAgents = ['prjct-workflow', 'prjct-planner', 'prjct-shipper']
533
+ await Promise.all(workflowAgents.map((name) => this.generateWorkflowAgent(name, agentsPath)))
574
534
  for (const name of workflowAgents) {
575
- await this.generateWorkflowAgent(name, agentsPath)
576
535
  agents.push({ name, type: 'workflow' })
577
536
  }
578
537
 
579
- // Domain agents (based on stack)
580
- if (stack.hasFrontend) {
581
- await this.generateDomainAgent('frontend', agentsPath, stats, stack)
582
- agents.push({ name: 'frontend', type: 'domain', skill: 'javascript-typescript' })
538
+ // Domain agents (based on stack) - COLLECT AND GENERATE IN PARALLEL
539
+ const domainAgentsToGenerate: { name: string; skill?: string }[] = []
583
540
 
584
- await this.generateDomainAgent('uxui', agentsPath, stats, stack)
585
- agents.push({ name: 'uxui', type: 'domain', skill: 'frontend-design' })
541
+ if (stack.hasFrontend) {
542
+ domainAgentsToGenerate.push({ name: 'frontend', skill: 'javascript-typescript' })
543
+ domainAgentsToGenerate.push({ name: 'uxui', skill: 'frontend-design' })
586
544
  }
587
-
588
545
  if (stack.hasBackend) {
589
- await this.generateDomainAgent('backend', agentsPath, stats, stack)
590
- agents.push({ name: 'backend', type: 'domain', skill: 'javascript-typescript' })
546
+ domainAgentsToGenerate.push({ name: 'backend', skill: 'javascript-typescript' })
591
547
  }
592
-
593
548
  if (stack.hasDatabase) {
594
- await this.generateDomainAgent('database', agentsPath, stats, stack)
595
- agents.push({ name: 'database', type: 'domain' })
549
+ domainAgentsToGenerate.push({ name: 'database' })
596
550
  }
597
-
598
551
  if (stack.hasTesting) {
599
- await this.generateDomainAgent('testing', agentsPath, stats, stack)
600
- agents.push({ name: 'testing', type: 'domain', skill: 'developer-kit' })
552
+ domainAgentsToGenerate.push({ name: 'testing', skill: 'developer-kit' })
601
553
  }
602
-
603
554
  if (stack.hasDocker) {
604
- await this.generateDomainAgent('devops', agentsPath, stats, stack)
605
- agents.push({ name: 'devops', type: 'domain', skill: 'developer-kit' })
555
+ domainAgentsToGenerate.push({ name: 'devops', skill: 'developer-kit' })
556
+ }
557
+
558
+ // Generate all domain agents IN PARALLEL
559
+ await Promise.all(
560
+ domainAgentsToGenerate.map((agent) =>
561
+ this.generateDomainAgent(agent.name, agentsPath, stats, stack)
562
+ )
563
+ )
564
+
565
+ // Add to agents list
566
+ for (const agent of domainAgentsToGenerate) {
567
+ agents.push({ name: agent.name, type: 'domain', skill: agent.skill })
606
568
  }
607
569
 
608
570
  return agents
@@ -761,212 +723,13 @@ You are the ${name} expert for this project. Apply best practices for the detect
761
723
  commands: Commands,
762
724
  agents: AgentInfo[]
763
725
  ): Promise<string[]> {
764
- const contextPath = path.join(this.globalPath, 'context')
765
- const files: string[] = []
766
-
767
- // Generate CLAUDE.md
768
- await this.generateClaudeMd(contextPath, git, stats, commands, agents)
769
- files.push('context/CLAUDE.md')
770
-
771
- // Generate now.md
772
- await this.generateNowMd(contextPath)
773
- files.push('context/now.md')
774
-
775
- // Generate next.md
776
- await this.generateNextMd(contextPath)
777
- files.push('context/next.md')
778
-
779
- // Generate ideas.md
780
- await this.generateIdeasMd(contextPath)
781
- files.push('context/ideas.md')
782
-
783
- // Generate shipped.md
784
- await this.generateShippedMd(contextPath)
785
- files.push('context/shipped.md')
786
-
787
- return files
788
- }
789
-
790
- private async generateClaudeMd(
791
- contextPath: string,
792
- git: GitData,
793
- stats: ProjectStats,
794
- commands: Commands,
795
- agents: AgentInfo[]
796
- ): Promise<void> {
797
- const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
798
- const domainAgents = agents.filter((a) => a.type === 'domain').map((a) => a.name)
799
-
800
- const content = `# ${stats.name} - Project Rules
801
- <!-- projectId: ${this.projectId} -->
802
- <!-- Generated: ${dateHelper.getTimestamp()} -->
803
- <!-- Ecosystem: ${stats.ecosystem} | Type: ${stats.projectType} -->
804
-
805
- ## THIS PROJECT (${stats.ecosystem})
806
-
807
- **Type:** ${stats.projectType}
808
- **Path:** ${this.projectPath}
809
-
810
- ### Commands (USE THESE, NOT OTHERS)
811
-
812
- | Action | Command |
813
- |--------|---------|
814
- | Install dependencies | \`${commands.install}\` |
815
- | Run dev server | \`${commands.dev}\` |
816
- | Run tests | \`${commands.test}\` |
817
- | Build | \`${commands.build}\` |
818
- | Lint | \`${commands.lint}\` |
819
- | Format | \`${commands.format}\` |
820
-
821
- ### Code Conventions
822
-
823
- - **Languages**: ${stats.languages.join(', ') || 'Not detected'}
824
- - **Frameworks**: ${stats.frameworks.join(', ') || 'Not detected'}
825
-
826
- ---
827
-
828
- ## PRJCT RULES
829
-
830
- ### Path Resolution
831
- **ALL prjct writes go to**: \`~/.prjct-cli/projects/${this.projectId}/\`
832
- - NEVER write to \`.prjct/\`
833
- - NEVER write to \`./\` for prjct data
834
-
835
- ### Workflow
836
- \`\`\`
837
- p. sync → p. task "desc" → [work] → p. done → p. ship
838
- \`\`\`
839
-
840
- | Command | Action |
841
- |---------|--------|
842
- | \`p. sync\` | Re-analyze project |
843
- | \`p. task X\` | Start task |
844
- | \`p. done\` | Complete subtask |
845
- | \`p. ship X\` | Ship feature |
846
-
847
- ---
848
-
849
- ## PROJECT STATE
850
-
851
- | Field | Value |
852
- |-------|-------|
853
- | Name | ${stats.name} |
854
- | Version | ${stats.version} |
855
- | Ecosystem | ${stats.ecosystem} |
856
- | Branch | ${git.branch} |
857
- | Files | ~${stats.fileCount} |
858
- | Commits | ${git.commits} |
859
-
860
- ---
861
-
862
- ## AGENTS
863
-
864
- Load from \`~/.prjct-cli/projects/${this.projectId}/agents/\`:
865
-
866
- **Workflow**: ${workflowAgents.join(', ')}
867
- **Domain**: ${domainAgents.join(', ') || 'none'}
868
- `
869
-
870
- await fs.writeFile(path.join(contextPath, 'CLAUDE.md'), content, 'utf-8')
871
- }
872
-
873
- private async generateNowMd(contextPath: string): Promise<void> {
874
- // Read current task from state
875
- let currentTask = null
876
- try {
877
- const statePath = path.join(this.globalPath, 'storage', 'state.json')
878
- const state = JSON.parse(await fs.readFile(statePath, 'utf-8'))
879
- currentTask = state.currentTask
880
- } catch {
881
- // No state file
882
- }
883
-
884
- const content = currentTask
885
- ? `# NOW
886
-
887
- **${currentTask.description}**
888
-
889
- Started: ${currentTask.startedAt}
890
- ${currentTask.branch ? `Branch: ${currentTask.branch.name}` : ''}
891
- `
892
- : `# NOW
893
-
894
- _No active task_
895
-
896
- Use \`p. task "description"\` to start working.
897
- `
898
-
899
- await fs.writeFile(path.join(contextPath, 'now.md'), content, 'utf-8')
900
- }
901
-
902
- private async generateNextMd(contextPath: string): Promise<void> {
903
- let queue: { tasks: { description: string; priority?: string }[] } = { tasks: [] }
904
- try {
905
- const queuePath = path.join(this.globalPath, 'storage', 'queue.json')
906
- queue = JSON.parse(await fs.readFile(queuePath, 'utf-8'))
907
- } catch {
908
- // No queue file
909
- }
910
-
911
- const content = `# NEXT
912
-
913
- ${
914
- queue.tasks.length > 0
915
- ? queue.tasks.map((t, i) => `${i + 1}. ${t.description}${t.priority ? ` [${t.priority}]` : ''}`).join('\n')
916
- : '_Empty queue_'
917
- }
918
- `
919
-
920
- await fs.writeFile(path.join(contextPath, 'next.md'), content, 'utf-8')
921
- }
922
-
923
- private async generateIdeasMd(contextPath: string): Promise<void> {
924
- let ideas: { ideas: { text: string; priority?: string }[] } = { ideas: [] }
925
- try {
926
- const ideasPath = path.join(this.globalPath, 'storage', 'ideas.json')
927
- ideas = JSON.parse(await fs.readFile(ideasPath, 'utf-8'))
928
- } catch {
929
- // No ideas file
930
- }
931
-
932
- const content = `# IDEAS
933
-
934
- ${
935
- ideas.ideas.length > 0
936
- ? ideas.ideas.map((i) => `- ${i.text}${i.priority ? ` [${i.priority}]` : ''}`).join('\n')
937
- : '_No ideas captured yet_'
938
- }
939
- `
940
-
941
- await fs.writeFile(path.join(contextPath, 'ideas.md'), content, 'utf-8')
942
- }
943
-
944
- private async generateShippedMd(contextPath: string): Promise<void> {
945
- let shipped: { shipped: { name: string; version?: string; shippedAt: string }[] } = {
946
- shipped: [],
947
- }
948
- try {
949
- const shippedPath = path.join(this.globalPath, 'storage', 'shipped.json')
950
- shipped = JSON.parse(await fs.readFile(shippedPath, 'utf-8'))
951
- } catch {
952
- // No shipped file
953
- }
954
-
955
- const content = `# SHIPPED 🚀
956
-
957
- ${
958
- shipped.shipped.length > 0
959
- ? shipped.shipped
960
- .slice(-10)
961
- .map((s) => `- **${s.name}**${s.version ? ` v${s.version}` : ''} - ${s.shippedAt}`)
962
- .join('\n')
963
- : '_Nothing shipped yet_'
964
- }
965
-
966
- **Total shipped:** ${shipped.shipped.length}
967
- `
726
+ const generator = new ContextFileGenerator({
727
+ projectId: this.projectId!,
728
+ projectPath: this.projectPath,
729
+ globalPath: this.globalPath,
730
+ })
968
731
 
969
- await fs.writeFile(path.join(contextPath, 'shipped.md'), content, 'utf-8')
732
+ return generator.generate({ branch: git.branch, commits: git.commits }, stats, commands, agents)
970
733
  }
971
734
 
972
735
  // ==========================================================================
@@ -1039,7 +802,7 @@ ${
1039
802
  state.lastSync = dateHelper.getTimestamp()
1040
803
  state.lastUpdated = dateHelper.getTimestamp()
1041
804
  state.context = {
1042
- ...(state.context as Record<string, unknown> || {}),
805
+ ...((state.context as Record<string, unknown>) || {}),
1043
806
  lastSession: dateHelper.getTimestamp(),
1044
807
  lastAction: 'Synced project',
1045
808
  nextAction: 'Run `p. task "description"` to start working',
@@ -1064,7 +827,85 @@ ${
1064
827
  commitCount: git.commits,
1065
828
  }
1066
829
 
1067
- await fs.appendFile(memoryPath, JSON.stringify(event) + '\n', 'utf-8')
830
+ await fs.appendFile(memoryPath, `${JSON.stringify(event)}\n`, 'utf-8')
831
+ }
832
+
833
+ // ==========================================================================
834
+ // METRICS RECORDING
835
+ // ==========================================================================
836
+
837
+ /**
838
+ * Record sync metrics for the value dashboard
839
+ *
840
+ * Calculates token savings by comparing:
841
+ * - Original: Estimated tokens if we sent all source files
842
+ * - Filtered: Actual tokens in generated context files
843
+ *
844
+ * Token estimation: ~4 chars per token (industry standard)
845
+ */
846
+ private async recordSyncMetrics(
847
+ stats: ProjectStats,
848
+ contextFiles: string[],
849
+ agents: AgentInfo[],
850
+ duration: number
851
+ ): Promise<SyncMetrics> {
852
+ const CHARS_PER_TOKEN = 4
853
+
854
+ // Calculate filtered size (actual context files generated)
855
+ let filteredChars = 0
856
+ for (const file of contextFiles) {
857
+ try {
858
+ const filePath = path.join(this.globalPath, file)
859
+ const content = await fs.readFile(filePath, 'utf-8')
860
+ filteredChars += content.length
861
+ } catch {
862
+ // File might not exist, skip
863
+ }
864
+ }
865
+
866
+ // Also count agent files
867
+ for (const agent of agents) {
868
+ try {
869
+ const agentPath = path.join(this.globalPath, 'agents', `${agent.name}.md`)
870
+ const content = await fs.readFile(agentPath, 'utf-8')
871
+ filteredChars += content.length
872
+ } catch {
873
+ // Skip if not found
874
+ }
875
+ }
876
+
877
+ const filteredSize = Math.floor(filteredChars / CHARS_PER_TOKEN)
878
+
879
+ // Estimate original size (what it would take without prjct)
880
+ // Conservative estimate: avg 500 tokens per source file
881
+ // Plus overhead for manually creating context
882
+ const avgTokensPerFile = 500
883
+ const originalSize = stats.fileCount * avgTokensPerFile
884
+
885
+ // Calculate compression rate
886
+ const compressionRate =
887
+ originalSize > 0 ? Math.max(0, (originalSize - filteredSize) / originalSize) : 0
888
+
889
+ // Record to storage
890
+ try {
891
+ await metricsStorage.recordSync(this.projectId!, {
892
+ originalSize,
893
+ filteredSize,
894
+ duration,
895
+ isWatch: false,
896
+ agents: agents.filter((a) => a.type === 'domain').map((a) => a.name),
897
+ })
898
+ } catch (error) {
899
+ // Non-blocking - metrics are nice to have
900
+ console.error('Warning: Failed to record metrics:', (error as Error).message)
901
+ }
902
+
903
+ return {
904
+ duration,
905
+ originalSize,
906
+ filteredSize,
907
+ compressionRate,
908
+ }
1068
909
  }
1069
910
 
1070
911
  // ==========================================================================
@@ -1144,4 +985,4 @@ ${
1144
985
  }
1145
986
 
1146
987
  export const syncService = new SyncService()
1147
- export { SyncService, SyncResult }
988
+ export { SyncService, type SyncResult }