prjct-cli 1.12.0 → 1.14.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.
@@ -21,15 +21,19 @@ import { promisify } from 'node:util'
21
21
  import { findRelevantFiles } from '../context-tools/files-tool'
22
22
  import { getRecentFiles } from '../context-tools/recent-tool'
23
23
  import { extractSignatures } from '../context-tools/signatures-tool'
24
+ import { calculateVelocity, formatVelocityContext } from '../domain/velocity'
24
25
  import configManager from '../infrastructure/config-manager'
25
26
  import pathManager from '../infrastructure/path-manager'
26
- import { stateStorage } from '../storage'
27
+ import outcomeRecorder from '../outcomes/recorder'
28
+ import { DEFAULT_VELOCITY_CONFIG } from '../schemas/velocity'
29
+ import { analysisStorage, stateStorage } from '../storage'
27
30
  import type {
28
31
  LoadedAgent,
29
32
  LoadedSkill,
30
33
  OrchestratorContext,
31
34
  OrchestratorSubtask,
32
35
  RealCodebaseContext,
36
+ SealedAnalysisContext,
33
37
  } from '../types'
34
38
  import { getErrorMessage, isNotFoundError } from '../types/fs'
35
39
  import domainClassifier, { type ProjectContext } from './domain-classifier'
@@ -76,8 +80,12 @@ export class OrchestratorExecutor {
76
80
  // Step 5: Load skills from agent frontmatter
77
81
  const skills = await this.loadSkills(agents)
78
82
 
79
- // Step 6: Gather real codebase context proactively
80
- const realContext = await this.gatherRealContext(taskDescription, projectPath)
83
+ // Step 6: Gather real codebase context, sealed analysis, and velocity in parallel
84
+ const [realContext, sealedAnalysis, velocityContext] = await Promise.all([
85
+ this.gatherRealContext(taskDescription, projectPath),
86
+ this.loadSealedAnalysis(projectId),
87
+ this.loadVelocityContext(projectId),
88
+ ])
81
89
 
82
90
  // Step 7: Determine if fragmentation is needed
83
91
  const requiresFragmentation = this.shouldFragment(domains, taskDescription)
@@ -101,6 +109,8 @@ export class OrchestratorExecutor {
101
109
  conventions: repoAnalysis?.conventions || [],
102
110
  },
103
111
  realContext,
112
+ sealedAnalysis,
113
+ velocityContext,
104
114
  }
105
115
  }
106
116
 
@@ -197,6 +207,52 @@ export class OrchestratorExecutor {
197
207
  }
198
208
  }
199
209
 
210
+ /**
211
+ * Load sealed/active analysis from analysis storage (PRJ-260).
212
+ * Returns sealed if available, otherwise draft as fallback.
213
+ * Returns null if no analysis exists (graceful degradation).
214
+ */
215
+ private async loadSealedAnalysis(projectId: string): Promise<SealedAnalysisContext | null> {
216
+ try {
217
+ const analysis = await analysisStorage.getActive(projectId)
218
+ if (!analysis) return null
219
+
220
+ return {
221
+ languages: analysis.languages,
222
+ frameworks: analysis.frameworks,
223
+ packageManager: analysis.packageManager,
224
+ sourceDir: analysis.sourceDir,
225
+ testDir: analysis.testDir,
226
+ fileCount: analysis.fileCount,
227
+ patterns: analysis.patterns,
228
+ antiPatterns: analysis.antiPatterns,
229
+ status: analysis.status ?? 'draft',
230
+ commitHash: analysis.commitHash,
231
+ }
232
+ } catch {
233
+ // Graceful degradation — analysis is optional enhancement
234
+ return null
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Load velocity context for estimation guidance (PRJ-296).
240
+ * Returns formatted string for prompt injection, or null if no data.
241
+ */
242
+ private async loadVelocityContext(projectId: string): Promise<string | null> {
243
+ try {
244
+ const outcomes = await outcomeRecorder.getAll(projectId)
245
+ if (outcomes.length === 0) return null
246
+
247
+ const metrics = calculateVelocity(outcomes, DEFAULT_VELOCITY_CONFIG)
248
+ if (metrics.sprints.length === 0) return null
249
+
250
+ return formatVelocityContext(metrics)
251
+ } catch {
252
+ return null
253
+ }
254
+ }
255
+
200
256
  /**
201
257
  * Load repo-analysis.json for project context
202
258
  */
@@ -542,10 +542,50 @@ class PromptBuilder {
542
542
  // =========================================================================
543
543
 
544
544
  if (orchestratorContext) {
545
+ const sa = orchestratorContext.sealedAnalysis
545
546
  parts.push('\n## PROJECT ANALYSIS (Sealed)\n')
546
547
  parts.push(`**Ecosystem**: ${orchestratorContext.project.ecosystem}\n`)
547
548
  parts.push(`**Primary Domain**: ${orchestratorContext.primaryDomain}\n`)
548
- parts.push(`**Domains**: ${orchestratorContext.detectedDomains.join(', ')}\n\n`)
549
+ parts.push(`**Domains**: ${orchestratorContext.detectedDomains.join(', ')}\n`)
550
+
551
+ // Inject sealed analysis data (PRJ-260)
552
+ if (sa) {
553
+ if (sa.languages.length > 0) {
554
+ parts.push(`**Languages**: ${sa.languages.join(', ')}\n`)
555
+ }
556
+ if (sa.frameworks.length > 0) {
557
+ parts.push(`**Frameworks**: ${sa.frameworks.join(', ')}\n`)
558
+ }
559
+ if (sa.packageManager) {
560
+ parts.push(`**Package Manager**: ${sa.packageManager}\n`)
561
+ }
562
+ if (sa.sourceDir) {
563
+ parts.push(`**Source Dir**: ${sa.sourceDir}\n`)
564
+ }
565
+ if (sa.testDir) {
566
+ parts.push(`**Test Dir**: ${sa.testDir}\n`)
567
+ }
568
+ parts.push(`**Files Analyzed**: ${sa.fileCount}\n`)
569
+ parts.push(
570
+ `**Analysis Status**: ${sa.status}${sa.commitHash ? ` (commit: ${sa.commitHash.slice(0, 8)})` : ''}\n`
571
+ )
572
+
573
+ if (sa.patterns.length > 0) {
574
+ parts.push('\n### Code Patterns (Follow These)\n')
575
+ for (const p of sa.patterns) {
576
+ parts.push(`- **${p.name}**: ${p.description}${p.location ? ` (${p.location})` : ''}\n`)
577
+ }
578
+ }
579
+
580
+ if (sa.antiPatterns.length > 0) {
581
+ parts.push('\n### Anti-Patterns (Avoid These)\n')
582
+ for (const ap of sa.antiPatterns) {
583
+ parts.push(`- **${ap.issue}** in \`${ap.file}\` — ${ap.suggestion}\n`)
584
+ }
585
+ }
586
+ }
587
+
588
+ parts.push('\n')
549
589
  }
550
590
 
551
591
  const needsPatterns = commandContext.patterns
@@ -648,6 +688,7 @@ class PromptBuilder {
648
688
  // =========================================================================
649
689
 
650
690
  if (projectPath) {
691
+ const sa = orchestratorContext?.sealedAnalysis
651
692
  const groundTruth: ProjectGroundTruth = {
652
693
  projectPath,
653
694
  language: orchestratorContext?.project?.ecosystem,
@@ -655,6 +696,10 @@ class PromptBuilder {
655
696
  domains: this.extractDomains(state),
656
697
  fileCount: context.files?.length || context.filteredSize || 0,
657
698
  availableAgents: orchestratorContext?.agents?.map((a) => a.name) || [],
699
+ // Inject sealed analysis data for enriched grounding (PRJ-260)
700
+ analysisLanguages: sa?.languages || [],
701
+ analysisFrameworks: sa?.frameworks || [],
702
+ analysisPackageManager: sa?.packageManager,
658
703
  }
659
704
  parts.push(`\n${buildAntiHallucinationBlock(groundTruth)}\n`)
660
705
  } else {
@@ -721,6 +766,13 @@ class PromptBuilder {
721
766
  parts.push('\n')
722
767
  }
723
768
 
769
+ // Velocity context (PRJ-296) — estimation guidance from historical data
770
+ if (orchestratorContext?.velocityContext) {
771
+ parts.push('\n### VELOCITY (Historical Estimation Data)\n\n')
772
+ parts.push(orchestratorContext.velocityContext)
773
+ parts.push('\n\n')
774
+ }
775
+
724
776
  // Learned patterns
725
777
  if (learnedPatterns && Object.keys(learnedPatterns).some((k) => learnedPatterns[k])) {
726
778
  parts.push('\n## PROJECT DEFAULTS (apply automatically)\n')
@@ -193,6 +193,23 @@ export const COMMANDS: CommandMeta[] = [
193
193
  'Command duration breakdown',
194
194
  ],
195
195
  },
196
+ {
197
+ name: 'velocity',
198
+ group: 'core',
199
+ description: 'Sprint-based velocity dashboard with trend detection and projections',
200
+ usage: { claude: '/p:velocity', terminal: 'prjct velocity [backlogPoints]' },
201
+ params: '[backlogPoints]',
202
+ implemented: true,
203
+ hasTemplate: false,
204
+ requiresProject: true,
205
+ features: [
206
+ 'Sprint-by-sprint velocity breakdown',
207
+ 'Trend detection (improving/stable/declining)',
208
+ 'Estimation accuracy tracking',
209
+ 'Over/under estimation pattern detection',
210
+ 'Completion projections for backlog',
211
+ ],
212
+ },
196
213
  {
197
214
  name: 'suggest',
198
215
  group: 'core',
@@ -31,6 +31,7 @@ import { PerformanceCommands } from './performance'
31
31
  import { PlanningCommands } from './planning'
32
32
  import { SetupCommands } from './setup'
33
33
  import { ShippingCommands } from './shipping'
34
+ import { VelocityCommands } from './velocity'
34
35
  import { WorkflowCommands } from './workflow'
35
36
 
36
37
  /**
@@ -47,6 +48,7 @@ class PrjctCommands {
47
48
  private maintenance: MaintenanceCommands
48
49
  private analysis: AnalysisCommands
49
50
  private setupCmds: SetupCommands
51
+ private velocityCmds: VelocityCommands
50
52
  private contextCmds: ContextCommands
51
53
 
52
54
  // Shared state
@@ -64,6 +66,7 @@ class PrjctCommands {
64
66
  this.maintenance = new MaintenanceCommands()
65
67
  this.analysis = new AnalysisCommands()
66
68
  this.setupCmds = new SetupCommands()
69
+ this.velocityCmds = new VelocityCommands()
67
70
  this.contextCmds = new ContextCommands()
68
71
 
69
72
  this.agent = null
@@ -149,6 +152,15 @@ class PrjctCommands {
149
152
  return this.performanceCmds.perf(period, projectPath)
150
153
  }
151
154
 
155
+ // ========== Velocity Commands ==========
156
+
157
+ async velocity(
158
+ backlogPoints: string = '0',
159
+ projectPath: string = process.cwd()
160
+ ): Promise<CommandResult> {
161
+ return this.velocityCmds.velocity(backlogPoints, projectPath)
162
+ }
163
+
152
164
  // ========== Maintenance Commands ==========
153
165
 
154
166
  async cleanup(
@@ -19,6 +19,7 @@ import { commandRegistry } from './registry'
19
19
  import { SetupCommands } from './setup'
20
20
  import { ShippingCommands } from './shipping'
21
21
  import { UninstallCommands } from './uninstall'
22
+ import { VelocityCommands } from './velocity'
22
23
  import { WorkflowCommands } from './workflow'
23
24
 
24
25
  // Singleton instances of command groups
@@ -31,6 +32,7 @@ const maintenance = new MaintenanceCommands()
31
32
  const analysis = new AnalysisCommands()
32
33
  const setup = new SetupCommands()
33
34
  const context = new ContextCommands()
35
+ const velocityCmd = new VelocityCommands()
34
36
  const uninstallCmd = new UninstallCommands()
35
37
 
36
38
  /**
@@ -78,6 +80,9 @@ export function registerAllCommands(): void {
78
80
  // Performance commands
79
81
  commandRegistry.registerMethod('perf', performance, 'perf', getMeta('perf'))
80
82
 
83
+ // Velocity commands
84
+ commandRegistry.registerMethod('velocity', velocityCmd, 'velocity', getMeta('velocity'))
85
+
81
86
  // Maintenance commands
82
87
  commandRegistry.registerMethod('cleanup', maintenance, 'cleanup', getMeta('cleanup'))
83
88
  commandRegistry.registerMethod('design', maintenance, 'design', getMeta('design'))
@@ -117,5 +122,6 @@ export {
117
122
  analysis,
118
123
  setup,
119
124
  context,
125
+ velocityCmd,
120
126
  uninstallCmd,
121
127
  }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Velocity Commands: velocity
3
+ * Sprint-based velocity dashboard
4
+ *
5
+ * @see PRJ-296
6
+ */
7
+
8
+ import chalk from 'chalk'
9
+ import { calculateVelocity, projectCompletion } from '../domain/velocity'
10
+ import outcomeRecorder from '../outcomes/recorder'
11
+ import type { VelocityConfig } from '../schemas/velocity'
12
+ import { DEFAULT_VELOCITY_CONFIG } from '../schemas/velocity'
13
+ import { velocityStorage } from '../storage/velocity-storage'
14
+ import type { CommandResult } from '../types'
15
+ import { getErrorMessage } from '../types/fs'
16
+ import { configManager, out, PrjctCommandsBase } from './base'
17
+
18
+ export class VelocityCommands extends PrjctCommandsBase {
19
+ /**
20
+ * prjct velocity - Velocity dashboard
21
+ */
22
+ async velocity(
23
+ backlogPoints: string = '0',
24
+ projectPath: string = process.cwd()
25
+ ): Promise<CommandResult> {
26
+ try {
27
+ const initResult = await this.ensureProjectInit(projectPath)
28
+ if (!initResult.success) return initResult
29
+
30
+ const projectId = await configManager.getProjectId(projectPath)
31
+ if (!projectId) {
32
+ out.failWithHint('NO_PROJECT_ID')
33
+ return { success: false, error: 'No project ID found' }
34
+ }
35
+
36
+ // Load velocity config from project config (or defaults)
37
+ const config = await this.loadVelocityConfig(projectPath)
38
+
39
+ // Load all outcomes
40
+ const outcomes = await outcomeRecorder.getAll(projectId)
41
+
42
+ if (outcomes.length === 0) {
43
+ console.log(`\n${chalk.dim('No velocity data yet.')}`)
44
+ console.log(`${chalk.dim('Complete tasks with estimates to build velocity history.')}\n`)
45
+ return { success: true, message: 'No data' }
46
+ }
47
+
48
+ // Calculate velocity metrics
49
+ const metrics = calculateVelocity(outcomes, config)
50
+
51
+ // Save for context injection
52
+ await velocityStorage.saveMetrics(projectId, metrics)
53
+
54
+ // Render dashboard
55
+ console.log(
56
+ `\n${chalk.cyan('Sprint Velocity')} ${chalk.dim(`(last ${config.windowSize ?? 6} sprints)`)}`
57
+ )
58
+ console.log('═'.repeat(60))
59
+
60
+ // Sprint table
61
+ const recentSprints = metrics.sprints.slice(-(config.windowSize ?? 6))
62
+ for (const sprint of recentSprints) {
63
+ const accuracyColor =
64
+ sprint.estimationAccuracy >= 80
65
+ ? chalk.green
66
+ : sprint.estimationAccuracy >= 60
67
+ ? chalk.yellow
68
+ : chalk.red
69
+ console.log(
70
+ ` Sprint ${String(sprint.sprintNumber).padStart(2)}: ${chalk.bold(`${sprint.pointsCompleted} pts`)} | ${sprint.tasksCompleted} tasks | accuracy: ${accuracyColor(`${sprint.estimationAccuracy}%`)}`
71
+ )
72
+ }
73
+
74
+ console.log('')
75
+ const trendIcon =
76
+ metrics.velocityTrend === 'improving'
77
+ ? chalk.green('↑')
78
+ : metrics.velocityTrend === 'declining'
79
+ ? chalk.red('↓')
80
+ : chalk.dim('→')
81
+ console.log(
82
+ ` Average: ${chalk.bold(`${metrics.averageVelocity} pts/sprint`)} | Trend: ${trendIcon} ${metrics.velocityTrend}`
83
+ )
84
+ console.log(
85
+ ` Estimation accuracy: ${chalk.bold(`${metrics.estimationAccuracy}%`)} ${chalk.dim(`(±${config.accuracyTolerance ?? 20}% tolerance)`)}`
86
+ )
87
+
88
+ // Patterns
89
+ if (metrics.underEstimated.length > 0 || metrics.overEstimated.length > 0) {
90
+ console.log(`\n ${chalk.dim('Patterns:')}`)
91
+ for (const p of metrics.underEstimated) {
92
+ console.log(
93
+ ` ${chalk.yellow('⚠')} ${p.category} tasks underestimated by avg ${chalk.bold(`${p.avgVariance}%`)}`
94
+ )
95
+ }
96
+ for (const p of metrics.overEstimated) {
97
+ console.log(
98
+ ` ${chalk.green('✓')} ${p.category} tasks estimated within ${chalk.bold(`${p.avgVariance}%`)}`
99
+ )
100
+ }
101
+ }
102
+
103
+ // Projection (if backlog points provided)
104
+ const points = parseInt(backlogPoints, 10)
105
+ if (points > 0 && metrics.averageVelocity > 0) {
106
+ const projection = projectCompletion(points, metrics.averageVelocity, config)
107
+ const dateStr = projection.estimatedDate
108
+ ? new Date(projection.estimatedDate).toLocaleDateString('en-US', {
109
+ month: 'short',
110
+ day: 'numeric',
111
+ year: 'numeric',
112
+ })
113
+ : 'unknown'
114
+ console.log(`\n ${chalk.dim('Projection:')}`)
115
+ console.log(` Backlog: ${chalk.bold(`${points} pts`)} remaining`)
116
+ console.log(
117
+ ` At current velocity: ~${projection.sprints} sprints (${projection.sprints * (config.sprintLengthDays ?? 7)} days)`
118
+ )
119
+ console.log(` Estimated completion: ${chalk.bold(dateStr)}`)
120
+ }
121
+
122
+ console.log('═'.repeat(60))
123
+ console.log('')
124
+
125
+ return { success: true }
126
+ } catch (error) {
127
+ out.fail(getErrorMessage(error))
128
+ return { success: false, error: getErrorMessage(error) }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Load velocity config from project or use defaults.
134
+ * Velocity config can be added to prjct.config.json as { velocity: { sprintLengthDays, ... } }
135
+ */
136
+ private async loadVelocityConfig(projectPath: string): Promise<VelocityConfig> {
137
+ try {
138
+ const config = await configManager.readConfig(projectPath)
139
+ // Read velocity config from extended config (not typed in LocalConfig yet)
140
+ const raw = config as Record<string, unknown> | null
141
+ if (raw?.velocity && typeof raw.velocity === 'object') {
142
+ return { ...DEFAULT_VELOCITY_CONFIG, ...(raw.velocity as Partial<VelocityConfig>) }
143
+ }
144
+ } catch {
145
+ // Use defaults
146
+ }
147
+ return DEFAULT_VELOCITY_CONFIG
148
+ }
149
+ }