prjct-cli 1.7.5 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -1
- package/bin/prjct.ts +14 -0
- package/core/__tests__/agentic/command-context.test.ts +281 -0
- package/core/__tests__/domain/fibonacci.test.ts +113 -0
- package/core/__tests__/infrastructure/performance-tracker.test.ts +328 -0
- package/core/agentic/command-classifier.ts +141 -0
- package/core/agentic/command-context.ts +168 -0
- package/core/agentic/prompt-builder.ts +28 -55
- package/core/commands/command-data.ts +17 -0
- package/core/commands/commands.ts +9 -0
- package/core/commands/performance.ts +114 -0
- package/core/commands/register.ts +6 -0
- package/core/commands/workflow.ts +87 -4
- package/core/config/command-context.config.json +66 -0
- package/core/domain/fibonacci.ts +128 -0
- package/core/index.ts +25 -1
- package/core/infrastructure/performance-tracker.ts +326 -0
- package/core/schemas/command-context.ts +29 -0
- package/core/schemas/performance.ts +128 -0
- package/core/schemas/state.ts +6 -0
- package/core/storage/state-storage.ts +21 -0
- package/dist/bin/prjct.mjs +1926 -1239
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs/promises'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { outcomeAnalyzer } from '../outcomes'
|
|
15
|
+
import type { CommandContextEntry } from '../schemas/command-context'
|
|
15
16
|
import { queueStorage, stateStorage } from '../storage'
|
|
16
17
|
import type {
|
|
17
18
|
LearnedPatterns,
|
|
@@ -28,6 +29,7 @@ import type {
|
|
|
28
29
|
import { getErrorMessage, isNotFoundError } from '../types/fs'
|
|
29
30
|
import { fileExists } from '../utils/fs-helpers'
|
|
30
31
|
import { PACKAGE_ROOT } from '../utils/version'
|
|
32
|
+
import { loadCommandContextConfig, resolveCommandContextFull } from './command-context'
|
|
31
33
|
import {
|
|
32
34
|
DEFAULT_BUDGETS,
|
|
33
35
|
filterSkillsByDomains,
|
|
@@ -144,18 +146,14 @@ class PromptBuilder {
|
|
|
144
146
|
|
|
145
147
|
/**
|
|
146
148
|
* Get additional modules needed for SMART commands (PRJ-94)
|
|
147
|
-
*
|
|
149
|
+
* Now config-driven via command-context.config.json (PRJ-298)
|
|
148
150
|
*/
|
|
149
|
-
getModulesForCommand(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
work: ['CLAUDE-intelligence.md', 'CLAUDE-storage.md'],
|
|
156
|
-
spec: ['CLAUDE-intelligence.md'],
|
|
157
|
-
}
|
|
158
|
-
return smartCommands[commandName] || []
|
|
151
|
+
getModulesForCommand(_commandName: string, commandContext?: CommandContextEntry): string[] {
|
|
152
|
+
if (commandContext) {
|
|
153
|
+
return commandContext.modules
|
|
154
|
+
}
|
|
155
|
+
// Fallback if called without config (shouldn't happen after PRJ-298)
|
|
156
|
+
return []
|
|
159
157
|
}
|
|
160
158
|
|
|
161
159
|
/**
|
|
@@ -411,21 +409,20 @@ class PromptBuilder {
|
|
|
411
409
|
// Store context for use in helper methods
|
|
412
410
|
this._currentContext = context
|
|
413
411
|
|
|
414
|
-
//
|
|
412
|
+
// PRJ-298: Config-driven command context (replaces 4 hardcoded lists)
|
|
415
413
|
const commandName = template.frontmatter?.name?.replace('p:', '') || ''
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const needsAgent = agentCommands.includes(commandName)
|
|
414
|
+
let commandContext: CommandContextEntry
|
|
415
|
+
try {
|
|
416
|
+
const config = await loadCommandContextConfig()
|
|
417
|
+
const resolved = resolveCommandContextFull(config, commandName, template)
|
|
418
|
+
commandContext = resolved.entry
|
|
419
|
+
} catch {
|
|
420
|
+
// Fallback: sensible defaults if config fails to load
|
|
421
|
+
commandContext = { agents: true, patterns: true, checklist: false, modules: [] }
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Agent assignment (config-driven)
|
|
425
|
+
const needsAgent = commandContext.agents
|
|
429
426
|
|
|
430
427
|
if (agent && needsAgent) {
|
|
431
428
|
parts.push(`# AGENT: ${agent.name}\n`)
|
|
@@ -591,21 +588,8 @@ class PromptBuilder {
|
|
|
591
588
|
)
|
|
592
589
|
}
|
|
593
590
|
|
|
594
|
-
// OPTIMIZED: Only include patterns for code-modifying commands
|
|
595
|
-
const
|
|
596
|
-
'now',
|
|
597
|
-
'build',
|
|
598
|
-
'feature',
|
|
599
|
-
'design',
|
|
600
|
-
'cleanup',
|
|
601
|
-
'fix',
|
|
602
|
-
'bug',
|
|
603
|
-
'test',
|
|
604
|
-
'init',
|
|
605
|
-
'spec',
|
|
606
|
-
'work',
|
|
607
|
-
]
|
|
608
|
-
const needsPatterns = codeCommands.includes(commandName)
|
|
591
|
+
// OPTIMIZED: Only include patterns for code-modifying commands (config-driven, PRJ-298)
|
|
592
|
+
const needsPatterns = commandContext.patterns
|
|
609
593
|
|
|
610
594
|
// Include code patterns analysis for code-modifying commands
|
|
611
595
|
const codePatternsContent = state?.codePatterns || ''
|
|
@@ -636,8 +620,8 @@ class PromptBuilder {
|
|
|
636
620
|
// CRITICAL: Compressed rules
|
|
637
621
|
parts.push(this.buildCriticalRules())
|
|
638
622
|
|
|
639
|
-
// PRJ-94: Inject additional modules for SMART commands
|
|
640
|
-
const additionalModules = this.getModulesForCommand(commandName)
|
|
623
|
+
// PRJ-94/PRJ-298: Inject additional modules for SMART commands (config-driven)
|
|
624
|
+
const additionalModules = this.getModulesForCommand(commandName, commandContext)
|
|
641
625
|
if (additionalModules.length > 0) {
|
|
642
626
|
for (const moduleName of additionalModules) {
|
|
643
627
|
const moduleContent = await this.loadModule(moduleName)
|
|
@@ -698,19 +682,8 @@ class PromptBuilder {
|
|
|
698
682
|
)
|
|
699
683
|
}
|
|
700
684
|
|
|
701
|
-
// P4.1: Quality Checklists
|
|
702
|
-
|
|
703
|
-
'now',
|
|
704
|
-
'build',
|
|
705
|
-
'feature',
|
|
706
|
-
'design',
|
|
707
|
-
'fix',
|
|
708
|
-
'bug',
|
|
709
|
-
'cleanup',
|
|
710
|
-
'spec',
|
|
711
|
-
'work',
|
|
712
|
-
]
|
|
713
|
-
if (checklistCommands.includes(commandName)) {
|
|
685
|
+
// P4.1: Quality Checklists (config-driven, PRJ-298)
|
|
686
|
+
if (commandContext.checklist) {
|
|
714
687
|
const routing = await this.loadChecklistRouting()
|
|
715
688
|
const checklists = await this.loadChecklists()
|
|
716
689
|
|
|
@@ -176,6 +176,23 @@ export const COMMANDS: CommandMeta[] = [
|
|
|
176
176
|
'Per-package CLAUDE.md generation',
|
|
177
177
|
],
|
|
178
178
|
},
|
|
179
|
+
{
|
|
180
|
+
name: 'perf',
|
|
181
|
+
group: 'core',
|
|
182
|
+
description: 'Performance dashboard - startup, memory, context, and handoff metrics',
|
|
183
|
+
usage: { claude: '/p:perf', terminal: 'prjct perf [days]' },
|
|
184
|
+
params: '[days]',
|
|
185
|
+
implemented: true,
|
|
186
|
+
hasTemplate: false,
|
|
187
|
+
requiresProject: true,
|
|
188
|
+
features: [
|
|
189
|
+
'Startup time tracking',
|
|
190
|
+
'Memory usage snapshots',
|
|
191
|
+
'Context correctness rate',
|
|
192
|
+
'Subtask handoff rate',
|
|
193
|
+
'Command duration breakdown',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
179
196
|
{
|
|
180
197
|
name: 'suggest',
|
|
181
198
|
group: 'core',
|
|
@@ -27,6 +27,7 @@ import { AnalysisCommands } from './analysis'
|
|
|
27
27
|
import { AnalyticsCommands } from './analytics'
|
|
28
28
|
import { ContextCommands } from './context'
|
|
29
29
|
import { MaintenanceCommands } from './maintenance'
|
|
30
|
+
import { PerformanceCommands } from './performance'
|
|
30
31
|
import { PlanningCommands } from './planning'
|
|
31
32
|
import { SetupCommands } from './setup'
|
|
32
33
|
import { ShippingCommands } from './shipping'
|
|
@@ -42,6 +43,7 @@ class PrjctCommands {
|
|
|
42
43
|
private planning: PlanningCommands
|
|
43
44
|
private shipping: ShippingCommands
|
|
44
45
|
private analytics: AnalyticsCommands
|
|
46
|
+
private performanceCmds: PerformanceCommands
|
|
45
47
|
private maintenance: MaintenanceCommands
|
|
46
48
|
private analysis: AnalysisCommands
|
|
47
49
|
private setupCmds: SetupCommands
|
|
@@ -58,6 +60,7 @@ class PrjctCommands {
|
|
|
58
60
|
this.planning = new PlanningCommands()
|
|
59
61
|
this.shipping = new ShippingCommands()
|
|
60
62
|
this.analytics = new AnalyticsCommands()
|
|
63
|
+
this.performanceCmds = new PerformanceCommands()
|
|
61
64
|
this.maintenance = new MaintenanceCommands()
|
|
62
65
|
this.analysis = new AnalysisCommands()
|
|
63
66
|
this.setupCmds = new SetupCommands()
|
|
@@ -140,6 +143,12 @@ class PrjctCommands {
|
|
|
140
143
|
return this.analytics.help(topic, projectPath)
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
// ========== Performance Commands ==========
|
|
147
|
+
|
|
148
|
+
async perf(period: string = '7', projectPath: string = process.cwd()): Promise<CommandResult> {
|
|
149
|
+
return this.performanceCmds.perf(period, projectPath)
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
// ========== Maintenance Commands ==========
|
|
144
153
|
|
|
145
154
|
async cleanup(
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Commands: perf
|
|
3
|
+
* Dashboard for CLI performance metrics
|
|
4
|
+
*
|
|
5
|
+
* @see PRJ-297
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk'
|
|
9
|
+
import performanceTracker from '../infrastructure/performance-tracker'
|
|
10
|
+
import type { CommandResult } from '../types'
|
|
11
|
+
import { getErrorMessage } from '../types/fs'
|
|
12
|
+
import { configManager, out, PrjctCommandsBase } from './base'
|
|
13
|
+
|
|
14
|
+
// Target thresholds for display
|
|
15
|
+
const TARGETS = {
|
|
16
|
+
startup: { max: 500, unit: 'ms' },
|
|
17
|
+
heapMB: { max: 80, unit: 'MB' },
|
|
18
|
+
contextRate: { min: 100, unit: '%' },
|
|
19
|
+
handoffRate: { min: 100, unit: '%' },
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function statusIcon(value: number, target: number, mode: 'below' | 'above'): string {
|
|
23
|
+
if (mode === 'below') {
|
|
24
|
+
return value <= target ? chalk.green('✓') : chalk.yellow('⚠')
|
|
25
|
+
}
|
|
26
|
+
return value >= target ? chalk.green('✓') : chalk.yellow('⚠')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class PerformanceCommands extends PrjctCommandsBase {
|
|
30
|
+
/**
|
|
31
|
+
* prjct perf - Performance dashboard
|
|
32
|
+
*/
|
|
33
|
+
async perf(period: string = '7', projectPath: string = process.cwd()): Promise<CommandResult> {
|
|
34
|
+
try {
|
|
35
|
+
const initResult = await this.ensureProjectInit(projectPath)
|
|
36
|
+
if (!initResult.success) return initResult
|
|
37
|
+
|
|
38
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
39
|
+
if (!projectId) {
|
|
40
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
41
|
+
return { success: false, error: 'No project ID found' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const days = parseInt(period, 10) || 7
|
|
45
|
+
const report = await performanceTracker.getReport(projectId, days)
|
|
46
|
+
|
|
47
|
+
const hasData =
|
|
48
|
+
report.startup ||
|
|
49
|
+
report.memory ||
|
|
50
|
+
report.contextCorrectness ||
|
|
51
|
+
report.subtaskHandoff ||
|
|
52
|
+
report.commandDurations
|
|
53
|
+
|
|
54
|
+
if (!hasData) {
|
|
55
|
+
console.log(`\n${chalk.dim('No performance data yet.')}`)
|
|
56
|
+
console.log(`${chalk.dim('Metrics are collected automatically as you use the CLI.')}\n`)
|
|
57
|
+
return { success: true, message: 'No data' }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log(`\n${chalk.cyan('Performance Report')} ${chalk.dim(`(last ${days} days)`)}`)
|
|
61
|
+
console.log('═'.repeat(55))
|
|
62
|
+
|
|
63
|
+
// Startup time
|
|
64
|
+
if (report.startup) {
|
|
65
|
+
const icon = statusIcon(report.startup.avg, TARGETS.startup.max, 'below')
|
|
66
|
+
console.log(
|
|
67
|
+
` Startup: avg ${chalk.bold(`${report.startup.avg}ms`)} ${chalk.dim(`(min ${report.startup.min}, max ${report.startup.max}, n=${report.startup.count})`)} ${icon} ${chalk.dim(`target: <${TARGETS.startup.max}ms`)}`
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Memory
|
|
72
|
+
if (report.memory) {
|
|
73
|
+
const icon = statusIcon(report.memory.peakHeapMB, TARGETS.heapMB.max, 'below')
|
|
74
|
+
console.log(
|
|
75
|
+
` Memory: avg ${chalk.bold(`${report.memory.avgHeapMB}MB`)} heap, peak ${report.memory.peakHeapMB}MB, rss ${report.memory.avgRssMB}MB ${icon} ${chalk.dim(`target: <${TARGETS.heapMB.max}MB`)}`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Context correctness
|
|
80
|
+
if (report.contextCorrectness) {
|
|
81
|
+
const icon = statusIcon(report.contextCorrectness.rate, TARGETS.contextRate.min, 'above')
|
|
82
|
+
console.log(
|
|
83
|
+
` Context: ${chalk.bold(`${report.contextCorrectness.rate}%`)} tasks received sync ${chalk.dim(`(${report.contextCorrectness.receivedSync}/${report.contextCorrectness.total})`)} ${icon} ${chalk.dim(`target: ${TARGETS.contextRate.min}%`)}`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Subtask handoff
|
|
88
|
+
if (report.subtaskHandoff) {
|
|
89
|
+
const icon = statusIcon(report.subtaskHandoff.rate, TARGETS.handoffRate.min, 'above')
|
|
90
|
+
console.log(
|
|
91
|
+
` Handoff: ${chalk.bold(`${report.subtaskHandoff.rate}%`)} subtasks with output ${chalk.dim(`(${report.subtaskHandoff.outputPopulated}/${report.subtaskHandoff.total})`)} ${icon} ${chalk.dim(`target: ${TARGETS.handoffRate.min}%`)}`
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Command durations
|
|
96
|
+
if (report.commandDurations && Object.keys(report.commandDurations).length > 0) {
|
|
97
|
+
console.log(`\n ${chalk.dim('Command Durations:')}`)
|
|
98
|
+
for (const [cmd, summary] of Object.entries(report.commandDurations)) {
|
|
99
|
+
console.log(
|
|
100
|
+
` ${cmd.padEnd(12)} avg ${chalk.bold(`${summary.avg}ms`)} ${chalk.dim(`(min ${summary.min}, max ${summary.max}, n=${summary.count})`)}`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('═'.repeat(55))
|
|
106
|
+
console.log('')
|
|
107
|
+
|
|
108
|
+
return { success: true }
|
|
109
|
+
} catch (error) {
|
|
110
|
+
out.fail(getErrorMessage(error))
|
|
111
|
+
return { success: false, error: getErrorMessage(error) }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -13,6 +13,7 @@ import { AnalyticsCommands } from './analytics'
|
|
|
13
13
|
import { CATEGORIES, COMMANDS } from './command-data'
|
|
14
14
|
import { ContextCommands } from './context'
|
|
15
15
|
import { MaintenanceCommands } from './maintenance'
|
|
16
|
+
import { PerformanceCommands } from './performance'
|
|
16
17
|
import { PlanningCommands } from './planning'
|
|
17
18
|
import { commandRegistry } from './registry'
|
|
18
19
|
import { SetupCommands } from './setup'
|
|
@@ -25,6 +26,7 @@ const workflow = new WorkflowCommands()
|
|
|
25
26
|
const planning = new PlanningCommands()
|
|
26
27
|
const shipping = new ShippingCommands()
|
|
27
28
|
const analytics = new AnalyticsCommands()
|
|
29
|
+
const performance = new PerformanceCommands()
|
|
28
30
|
const maintenance = new MaintenanceCommands()
|
|
29
31
|
const analysis = new AnalysisCommands()
|
|
30
32
|
const setup = new SetupCommands()
|
|
@@ -73,6 +75,9 @@ export function registerAllCommands(): void {
|
|
|
73
75
|
commandRegistry.registerMethod('dash', analytics, 'dash', getMeta('dash'))
|
|
74
76
|
commandRegistry.registerMethod('help', analytics, 'help', getMeta('help'))
|
|
75
77
|
|
|
78
|
+
// Performance commands
|
|
79
|
+
commandRegistry.registerMethod('perf', performance, 'perf', getMeta('perf'))
|
|
80
|
+
|
|
76
81
|
// Maintenance commands
|
|
77
82
|
commandRegistry.registerMethod('cleanup', maintenance, 'cleanup', getMeta('cleanup'))
|
|
78
83
|
commandRegistry.registerMethod('design', maintenance, 'design', getMeta('design'))
|
|
@@ -105,6 +110,7 @@ export {
|
|
|
105
110
|
planning,
|
|
106
111
|
shipping,
|
|
107
112
|
analytics,
|
|
113
|
+
performance,
|
|
108
114
|
maintenance,
|
|
109
115
|
analysis,
|
|
110
116
|
setup,
|
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import commandExecutor from '../agentic/command-executor'
|
|
12
12
|
import { templateExecutor } from '../agentic/template-executor'
|
|
13
|
+
import {
|
|
14
|
+
type FibonacciPoint,
|
|
15
|
+
isValidPoint,
|
|
16
|
+
pointsToMinutes,
|
|
17
|
+
pointsToTimeRange,
|
|
18
|
+
} from '../domain/fibonacci'
|
|
13
19
|
import { linearService } from '../integrations/linear'
|
|
20
|
+
import outcomeRecorder from '../outcomes/recorder'
|
|
14
21
|
import { generateUUID } from '../schemas'
|
|
15
22
|
import { queueStorage, stateStorage } from '../storage'
|
|
16
23
|
import type { CommandResult } from '../types'
|
|
@@ -132,6 +139,20 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
132
139
|
task,
|
|
133
140
|
agenticMode: true,
|
|
134
141
|
availableAgents,
|
|
142
|
+
// Fibonacci estimation helpers for templates
|
|
143
|
+
fibonacci: {
|
|
144
|
+
isValidPoint,
|
|
145
|
+
pointsToMinutes,
|
|
146
|
+
pointsToTimeRange,
|
|
147
|
+
storeEstimate: async (points: FibonacciPoint) => {
|
|
148
|
+
const minutes = pointsToMinutes(points)
|
|
149
|
+
await stateStorage.updateCurrentTask(projectId, {
|
|
150
|
+
estimatedPoints: points,
|
|
151
|
+
estimatedMinutes: minutes.typical,
|
|
152
|
+
})
|
|
153
|
+
return minutes
|
|
154
|
+
},
|
|
155
|
+
},
|
|
135
156
|
}
|
|
136
157
|
} else {
|
|
137
158
|
// Read from storage (JSON is source of truth)
|
|
@@ -187,9 +208,44 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
187
208
|
|
|
188
209
|
const task = currentTask.description
|
|
189
210
|
let duration = ''
|
|
211
|
+
let actualMinutes = 0
|
|
190
212
|
if (currentTask.startedAt) {
|
|
191
213
|
const started = new Date(currentTask.startedAt)
|
|
192
214
|
duration = dateHelper.calculateDuration(started)
|
|
215
|
+
actualMinutes = Math.round((Date.now() - started.getTime()) / 60_000)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Record outcome with estimation data if available
|
|
219
|
+
const estimatedMinutes = (currentTask as { estimatedMinutes?: number }).estimatedMinutes
|
|
220
|
+
const estimatedPoints = (currentTask as { estimatedPoints?: number }).estimatedPoints
|
|
221
|
+
try {
|
|
222
|
+
await outcomeRecorder.record(projectId, {
|
|
223
|
+
sessionId: currentTask.sessionId,
|
|
224
|
+
command: 'done',
|
|
225
|
+
task,
|
|
226
|
+
startedAt: currentTask.startedAt,
|
|
227
|
+
completedAt: dateHelper.getTimestamp(),
|
|
228
|
+
estimatedDuration: estimatedMinutes ? formatMinutesToDuration(estimatedMinutes) : '0m',
|
|
229
|
+
actualDuration: duration || '0m',
|
|
230
|
+
variance: estimatedMinutes ? formatVariance(actualMinutes - estimatedMinutes) : '+0m',
|
|
231
|
+
completedAsPlanned: true,
|
|
232
|
+
qualityScore: 3,
|
|
233
|
+
tags: [(currentTask as { linearId?: string }).linearId].filter(Boolean) as string[],
|
|
234
|
+
})
|
|
235
|
+
} catch {
|
|
236
|
+
// Outcome recording failure should not block workflow
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Build variance display
|
|
240
|
+
let varianceDisplay = ''
|
|
241
|
+
if (estimatedPoints && estimatedMinutes) {
|
|
242
|
+
const diff = actualMinutes - estimatedMinutes
|
|
243
|
+
const pct =
|
|
244
|
+
estimatedMinutes > 0
|
|
245
|
+
? Math.round(((actualMinutes - estimatedMinutes) / estimatedMinutes) * 100)
|
|
246
|
+
: 0
|
|
247
|
+
const sign = diff >= 0 ? '+' : ''
|
|
248
|
+
varianceDisplay = ` | est: ${estimatedPoints}pt (${formatMinutesToDuration(estimatedMinutes)}) → ${sign}${pct}%`
|
|
193
249
|
}
|
|
194
250
|
|
|
195
251
|
// Write-through: Complete task (JSON → MD → Event)
|
|
@@ -204,16 +260,16 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
204
260
|
if (apiKey && creds.linear?.teamId) {
|
|
205
261
|
await linearService.initializeFromApiKey(apiKey, creds.linear.teamId)
|
|
206
262
|
await linearService.markDone(linearId)
|
|
207
|
-
out.done(`${task}${duration ? ` (${duration})` : ''} → Linear ✓`)
|
|
263
|
+
out.done(`${task}${duration ? ` (${duration}${varianceDisplay})` : ''} → Linear ✓`)
|
|
208
264
|
} else {
|
|
209
|
-
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
265
|
+
out.done(`${task}${duration ? ` (${duration}${varianceDisplay})` : ''}`)
|
|
210
266
|
}
|
|
211
267
|
} catch {
|
|
212
268
|
// Linear sync failed silently - don't block the workflow
|
|
213
|
-
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
269
|
+
out.done(`${task}${duration ? ` (${duration}${varianceDisplay})` : ''}`)
|
|
214
270
|
}
|
|
215
271
|
} else {
|
|
216
|
-
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
272
|
+
out.done(`${task}${duration ? ` (${duration}${varianceDisplay})` : ''}`)
|
|
217
273
|
}
|
|
218
274
|
showStateInfo('completed')
|
|
219
275
|
showNextSteps('done')
|
|
@@ -221,6 +277,9 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
221
277
|
await this.logToMemory(projectPath, 'task_completed', {
|
|
222
278
|
task,
|
|
223
279
|
duration,
|
|
280
|
+
estimatedPoints,
|
|
281
|
+
estimatedMinutes,
|
|
282
|
+
actualMinutes,
|
|
224
283
|
timestamp: dateHelper.getTimestamp(),
|
|
225
284
|
})
|
|
226
285
|
|
|
@@ -417,3 +476,27 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
417
476
|
}
|
|
418
477
|
}
|
|
419
478
|
}
|
|
479
|
+
|
|
480
|
+
// =============================================================================
|
|
481
|
+
// Helpers
|
|
482
|
+
// =============================================================================
|
|
483
|
+
|
|
484
|
+
/** Format minutes to a human-readable duration string */
|
|
485
|
+
function formatMinutesToDuration(minutes: number): string {
|
|
486
|
+
if (minutes < 60) return `${minutes}m`
|
|
487
|
+
const hours = Math.floor(minutes / 60)
|
|
488
|
+
const mins = minutes % 60
|
|
489
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Format a variance in minutes to a string like "+30m" or "-15m" */
|
|
493
|
+
function formatVariance(diffMinutes: number): string {
|
|
494
|
+
const sign = diffMinutes >= 0 ? '+' : '-'
|
|
495
|
+
const abs = Math.abs(diffMinutes)
|
|
496
|
+
if (abs >= 60) {
|
|
497
|
+
const hours = Math.floor(abs / 60)
|
|
498
|
+
const mins = abs % 60
|
|
499
|
+
return mins > 0 ? `${sign}${hours}h ${mins}m` : `${sign}${hours}h`
|
|
500
|
+
}
|
|
501
|
+
return `${sign}${abs}m`
|
|
502
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"description": "Maps commands to context sections for prompt injection. Wildcard * ensures new commands get sensible defaults instead of zero context.",
|
|
4
|
+
"commands": {
|
|
5
|
+
"now": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
6
|
+
"build": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
7
|
+
"feature": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
8
|
+
"design": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
9
|
+
"fix": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
10
|
+
"bug": {
|
|
11
|
+
"agents": true,
|
|
12
|
+
"patterns": true,
|
|
13
|
+
"checklist": true,
|
|
14
|
+
"modules": ["CLAUDE-intelligence.md"]
|
|
15
|
+
},
|
|
16
|
+
"test": { "agents": true, "patterns": true, "checklist": false, "modules": [] },
|
|
17
|
+
"work": {
|
|
18
|
+
"agents": true,
|
|
19
|
+
"patterns": true,
|
|
20
|
+
"checklist": true,
|
|
21
|
+
"modules": ["CLAUDE-intelligence.md", "CLAUDE-storage.md"]
|
|
22
|
+
},
|
|
23
|
+
"cleanup": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
24
|
+
"spec": {
|
|
25
|
+
"agents": true,
|
|
26
|
+
"patterns": true,
|
|
27
|
+
"checklist": true,
|
|
28
|
+
"modules": ["CLAUDE-intelligence.md"]
|
|
29
|
+
},
|
|
30
|
+
"init": { "agents": false, "patterns": true, "checklist": false, "modules": [] },
|
|
31
|
+
"task": {
|
|
32
|
+
"agents": false,
|
|
33
|
+
"patterns": false,
|
|
34
|
+
"checklist": false,
|
|
35
|
+
"modules": ["CLAUDE-intelligence.md", "CLAUDE-storage.md"]
|
|
36
|
+
},
|
|
37
|
+
"ship": {
|
|
38
|
+
"agents": false,
|
|
39
|
+
"patterns": true,
|
|
40
|
+
"checklist": true,
|
|
41
|
+
"modules": ["CLAUDE-intelligence.md", "CLAUDE-storage.md"]
|
|
42
|
+
},
|
|
43
|
+
"done": {
|
|
44
|
+
"agents": false,
|
|
45
|
+
"patterns": false,
|
|
46
|
+
"checklist": true,
|
|
47
|
+
"modules": ["CLAUDE-storage.md"]
|
|
48
|
+
},
|
|
49
|
+
"review": { "agents": true, "patterns": true, "checklist": true, "modules": [] },
|
|
50
|
+
"refactor": {
|
|
51
|
+
"agents": true,
|
|
52
|
+
"patterns": true,
|
|
53
|
+
"checklist": true,
|
|
54
|
+
"modules": ["CLAUDE-intelligence.md"]
|
|
55
|
+
},
|
|
56
|
+
"sync": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
57
|
+
"dash": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
58
|
+
"next": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
59
|
+
"pause": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
60
|
+
"resume": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
61
|
+
"idea": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
62
|
+
"history": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
63
|
+
"status": { "agents": false, "patterns": false, "checklist": false, "modules": [] },
|
|
64
|
+
"*": { "agents": true, "patterns": true, "checklist": false, "modules": [] }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fibonacci Estimation Module
|
|
3
|
+
*
|
|
4
|
+
* Provides Fibonacci-based story point estimation with
|
|
5
|
+
* points-to-time conversion and historical suggestion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import outcomeRecorder from '../outcomes/recorder'
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Constants
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/** Valid Fibonacci story points */
|
|
15
|
+
export const FIBONACCI_POINTS = [1, 2, 3, 5, 8, 13, 21] as const
|
|
16
|
+
export type FibonacciPoint = (typeof FIBONACCI_POINTS)[number]
|
|
17
|
+
|
|
18
|
+
/** Default points-to-minutes mapping */
|
|
19
|
+
const DEFAULT_MINUTES_MAP: Record<FibonacciPoint, { min: number; max: number; typical: number }> = {
|
|
20
|
+
1: { min: 5, max: 15, typical: 10 },
|
|
21
|
+
2: { min: 15, max: 30, typical: 20 },
|
|
22
|
+
3: { min: 30, max: 60, typical: 45 },
|
|
23
|
+
5: { min: 60, max: 120, typical: 90 },
|
|
24
|
+
8: { min: 120, max: 240, typical: 180 },
|
|
25
|
+
13: { min: 240, max: 480, typical: 360 },
|
|
26
|
+
21: { min: 480, max: 960, typical: 720 },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Validation
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
/** Check if a number is a valid Fibonacci point */
|
|
34
|
+
export const isValidPoint = (n: number): n is FibonacciPoint =>
|
|
35
|
+
FIBONACCI_POINTS.includes(n as FibonacciPoint)
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Points-to-Time Conversion
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/** Get the time range for a given point value */
|
|
42
|
+
export const pointsToMinutes = (
|
|
43
|
+
points: FibonacciPoint
|
|
44
|
+
): { min: number; max: number; typical: number } => {
|
|
45
|
+
return DEFAULT_MINUTES_MAP[points]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Format a minute count as a human-readable duration */
|
|
49
|
+
export const formatMinutes = (minutes: number): string => {
|
|
50
|
+
if (minutes < 60) return `${minutes}m`
|
|
51
|
+
const hours = Math.floor(minutes / 60)
|
|
52
|
+
const mins = minutes % 60
|
|
53
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Get a human-readable time range for a point value */
|
|
57
|
+
export const pointsToTimeRange = (points: FibonacciPoint): string => {
|
|
58
|
+
const range = pointsToMinutes(points)
|
|
59
|
+
return `${formatMinutes(range.min)}–${formatMinutes(range.max)}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Historical Suggestion
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Suggest a point estimate based on historical outcomes for similar task types.
|
|
68
|
+
* Returns null if not enough data (< 3 outcomes).
|
|
69
|
+
*/
|
|
70
|
+
export const suggestFromHistory = async (
|
|
71
|
+
projectId: string,
|
|
72
|
+
taskType: string
|
|
73
|
+
): Promise<{ points: FibonacciPoint; basedOn: number } | null> => {
|
|
74
|
+
const outcomes = await outcomeRecorder.getAll(projectId)
|
|
75
|
+
|
|
76
|
+
// Filter by task type tag
|
|
77
|
+
const relevant = outcomes.filter((o) => o.tags?.includes(taskType))
|
|
78
|
+
|
|
79
|
+
if (relevant.length < 3) return null
|
|
80
|
+
|
|
81
|
+
// Calculate average actual duration in minutes
|
|
82
|
+
const totalMinutes = relevant.reduce((sum, o) => {
|
|
83
|
+
return sum + parseDuration(o.actualDuration)
|
|
84
|
+
}, 0)
|
|
85
|
+
const avgMinutes = totalMinutes / relevant.length
|
|
86
|
+
|
|
87
|
+
// Find closest Fibonacci point by typical time
|
|
88
|
+
const closest = findClosestPoint(avgMinutes)
|
|
89
|
+
|
|
90
|
+
return { points: closest, basedOn: relevant.length }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Helpers
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/** Find the Fibonacci point whose typical time is closest to the given minutes */
|
|
98
|
+
export const findClosestPoint = (minutes: number): FibonacciPoint => {
|
|
99
|
+
let closest: FibonacciPoint = 1
|
|
100
|
+
let smallestDiff = Number.POSITIVE_INFINITY
|
|
101
|
+
|
|
102
|
+
for (const point of FIBONACCI_POINTS) {
|
|
103
|
+
const diff = Math.abs(DEFAULT_MINUTES_MAP[point].typical - minutes)
|
|
104
|
+
if (diff < smallestDiff) {
|
|
105
|
+
smallestDiff = diff
|
|
106
|
+
closest = point
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return closest
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Parse a duration string like "2h 30m" to minutes */
|
|
114
|
+
const parseDuration = (duration: string): number => {
|
|
115
|
+
let minutes = 0
|
|
116
|
+
|
|
117
|
+
const hourMatch = duration.match(/(\d+)h/)
|
|
118
|
+
if (hourMatch) {
|
|
119
|
+
minutes += Number.parseInt(hourMatch[1], 10) * 60
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const minMatch = duration.match(/(\d+)m/)
|
|
123
|
+
if (minMatch) {
|
|
124
|
+
minutes += Number.parseInt(minMatch[1], 10)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return minutes
|
|
128
|
+
}
|