prjct-cli 0.42.0 → 0.44.1

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 (44) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/core/agentic/command-executor.ts +15 -5
  3. package/core/ai-tools/formatters.ts +302 -0
  4. package/core/ai-tools/generator.ts +124 -0
  5. package/core/ai-tools/index.ts +15 -0
  6. package/core/ai-tools/registry.ts +195 -0
  7. package/core/cli/linear.ts +61 -2
  8. package/core/commands/analysis.ts +36 -2
  9. package/core/commands/commands.ts +2 -2
  10. package/core/commands/planning.ts +8 -4
  11. package/core/commands/shipping.ts +9 -7
  12. package/core/commands/workflow.ts +67 -17
  13. package/core/index.ts +3 -1
  14. package/core/infrastructure/ai-provider.ts +11 -36
  15. package/core/integrations/issue-tracker/types.ts +7 -1
  16. package/core/integrations/linear/client.ts +56 -24
  17. package/core/integrations/linear/index.ts +3 -0
  18. package/core/integrations/linear/sync.ts +313 -0
  19. package/core/schemas/index.ts +3 -0
  20. package/core/schemas/issues.ts +144 -0
  21. package/core/schemas/state.ts +3 -0
  22. package/core/services/sync-service.ts +71 -4
  23. package/core/utils/agent-stream.ts +138 -0
  24. package/core/utils/branding.ts +2 -3
  25. package/core/utils/next-steps.ts +95 -0
  26. package/core/utils/output.ts +26 -0
  27. package/core/workflow/index.ts +6 -0
  28. package/core/workflow/state-machine.ts +185 -0
  29. package/dist/bin/prjct.mjs +2382 -541
  30. package/package.json +1 -1
  31. package/templates/_bases/tracker-base.md +11 -0
  32. package/templates/commands/done.md +18 -13
  33. package/templates/commands/git.md +143 -54
  34. package/templates/commands/merge.md +121 -13
  35. package/templates/commands/review.md +1 -1
  36. package/templates/commands/ship.md +165 -20
  37. package/templates/commands/sync.md +17 -0
  38. package/templates/commands/task.md +123 -17
  39. package/templates/global/ANTIGRAVITY.md +2 -4
  40. package/templates/global/CLAUDE.md +115 -28
  41. package/templates/global/CURSOR.mdc +1 -3
  42. package/templates/global/GEMINI.md +2 -4
  43. package/templates/global/WINDSURF.md +1 -3
  44. package/templates/subagents/workflow/prjct-shipper.md +1 -2
@@ -22,6 +22,14 @@ import { promisify } from 'util'
22
22
  import pathManager from '../infrastructure/path-manager'
23
23
  import configManager from '../infrastructure/config-manager'
24
24
  import dateHelper from '../utils/date-helper'
25
+ import {
26
+ generateAIToolContexts,
27
+ DEFAULT_AI_TOOLS,
28
+ resolveToolIds,
29
+ detectInstalledTools,
30
+ type ProjectContext,
31
+ type GenerateResult,
32
+ } from '../ai-tools'
25
33
 
26
34
  const execAsync = promisify(exec)
27
35
 
@@ -77,6 +85,12 @@ interface AgentInfo {
77
85
  skill?: string
78
86
  }
79
87
 
88
+ interface AIToolResult {
89
+ toolId: string
90
+ outputFile: string
91
+ success: boolean
92
+ }
93
+
80
94
  interface SyncResult {
81
95
  success: boolean
82
96
  projectId: string
@@ -88,9 +102,14 @@ interface SyncResult {
88
102
  agents: AgentInfo[]
89
103
  skills: { agent: string; skill: string }[]
90
104
  contextFiles: string[]
105
+ aiTools: AIToolResult[]
91
106
  error?: string
92
107
  }
93
108
 
109
+ interface SyncOptions {
110
+ aiTools?: string[] // AI tools to generate context for (default: claude, cursor)
111
+ }
112
+
94
113
  // ============================================================================
95
114
  // SYNC SERVICE
96
115
  // ============================================================================
@@ -108,9 +127,22 @@ class SyncService {
108
127
  /**
109
128
  * Main sync method - does everything in one call
110
129
  */
111
- async sync(projectPath: string = process.cwd()): Promise<SyncResult> {
130
+ async sync(projectPath: string = process.cwd(), options: SyncOptions = {}): Promise<SyncResult> {
112
131
  this.projectPath = projectPath
113
132
 
133
+ // Resolve AI tools: supports 'auto', 'all', or specific list
134
+ let aiToolIds: string[]
135
+ if (!options.aiTools || options.aiTools.length === 0) {
136
+ aiToolIds = DEFAULT_AI_TOOLS
137
+ } else if (options.aiTools[0] === 'auto') {
138
+ aiToolIds = detectInstalledTools(projectPath)
139
+ if (aiToolIds.length === 0) aiToolIds = ['claude'] // fallback
140
+ } else if (options.aiTools[0] === 'all') {
141
+ aiToolIds = resolveToolIds('all', projectPath)
142
+ } else {
143
+ aiToolIds = options.aiTools
144
+ }
145
+
114
146
  try {
115
147
  // 1. Get project config
116
148
  this.projectId = await configManager.getProjectId(projectPath)
@@ -126,6 +158,7 @@ class SyncService {
126
158
  agents: [],
127
159
  skills: [],
128
160
  contextFiles: [],
161
+ aiTools: [],
129
162
  error: 'No prjct project. Run p. init first.',
130
163
  }
131
164
  }
@@ -147,13 +180,41 @@ class SyncService {
147
180
  const skills = this.configureSkills(agents)
148
181
  const contextFiles = await this.generateContextFiles(git, stats, commands, agents)
149
182
 
150
- // 5. Update project.json
183
+ // 5. Generate AI tool context files (multi-agent output)
184
+ const projectContext: ProjectContext = {
185
+ projectId: this.projectId,
186
+ name: stats.name,
187
+ version: stats.version,
188
+ ecosystem: stats.ecosystem,
189
+ projectType: stats.projectType,
190
+ languages: stats.languages,
191
+ frameworks: stats.frameworks,
192
+ repoPath: this.projectPath,
193
+ branch: git.branch,
194
+ fileCount: stats.fileCount,
195
+ commits: git.commits,
196
+ hasChanges: git.hasChanges,
197
+ commands,
198
+ 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),
201
+ },
202
+ }
203
+
204
+ const aiToolResults = await generateAIToolContexts(
205
+ projectContext,
206
+ this.globalPath,
207
+ this.projectPath,
208
+ aiToolIds
209
+ )
210
+
211
+ // 6. Update project.json
151
212
  await this.updateProjectJson(git, stats)
152
213
 
153
- // 6. Update state.json with enterprise fields
214
+ // 7. Update state.json with enterprise fields
154
215
  await this.updateStateJson(stats, stack)
155
216
 
156
- // 7. Log to memory
217
+ // 8. Log to memory
157
218
  await this.logToMemory(git, stats)
158
219
 
159
220
  return {
@@ -167,6 +228,11 @@ class SyncService {
167
228
  agents,
168
229
  skills,
169
230
  contextFiles,
231
+ aiTools: aiToolResults.map(r => ({
232
+ toolId: r.toolId,
233
+ outputFile: r.outputFile,
234
+ success: r.success,
235
+ })),
170
236
  }
171
237
  } catch (error) {
172
238
  return {
@@ -180,6 +246,7 @@ class SyncService {
180
246
  agents: [],
181
247
  skills: [],
182
248
  contextFiles: [],
249
+ aiTools: [],
183
250
  error: (error as Error).message,
184
251
  }
185
252
  }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Agent Activity Stream
3
+ *
4
+ * Shows real-time agent activity during task execution.
5
+ * Provides visibility into which agents are working and what they're doing.
6
+ */
7
+
8
+ import chalk from 'chalk'
9
+
10
+ /**
11
+ * Domain icons for visual identification
12
+ */
13
+ const DOMAIN_ICONS: Record<string, string> = {
14
+ database: '💾',
15
+ backend: '🔧',
16
+ frontend: '📦',
17
+ testing: '🧪',
18
+ devops: '🚀',
19
+ uxui: '🎨',
20
+ security: '🔒',
21
+ docs: '📝',
22
+ api: '🌐',
23
+ default: '⚡',
24
+ }
25
+
26
+ /**
27
+ * Get icon for a domain
28
+ */
29
+ function getIcon(domain: string): string {
30
+ return DOMAIN_ICONS[domain.toLowerCase()] || DOMAIN_ICONS.default
31
+ }
32
+
33
+ /**
34
+ * Agent Stream - Visual activity tracker
35
+ */
36
+ class AgentStream {
37
+ private currentAgent: string | null = null
38
+ private startTime: number = 0
39
+ private quiet: boolean = false
40
+
41
+ /**
42
+ * Set quiet mode (no output)
43
+ */
44
+ setQuiet(quiet: boolean): void {
45
+ this.quiet = quiet
46
+ }
47
+
48
+ /**
49
+ * Show orchestration start
50
+ */
51
+ orchestrate(domains: string[]): void {
52
+ if (this.quiet) return
53
+ console.log(chalk.cyan(`\n🎯 Orchestrating: ${domains.join(', ')} domains detected\n`))
54
+ }
55
+
56
+ /**
57
+ * Start an agent activity block
58
+ */
59
+ startAgent(name: string, domain: string, description?: string): void {
60
+ if (this.quiet) return
61
+
62
+ this.currentAgent = name
63
+ this.startTime = Date.now()
64
+
65
+ const icon = getIcon(domain)
66
+ console.log(chalk.cyan(`┌─ ${icon} ${name} (${domain})`))
67
+
68
+ if (description) {
69
+ console.log(chalk.dim(`│ ${description}`))
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Show progress within current agent
75
+ */
76
+ progress(message: string): void {
77
+ if (this.quiet || !this.currentAgent) return
78
+ console.log(chalk.dim(`│ └── ${message}`))
79
+ }
80
+
81
+ /**
82
+ * Show multiple progress items
83
+ */
84
+ progressList(items: string[]): void {
85
+ if (this.quiet || !this.currentAgent) return
86
+ for (const item of items) {
87
+ console.log(chalk.dim(`│ └── ${item}`))
88
+ }
89
+ }
90
+
91
+ /**
92
+ * End current agent block
93
+ */
94
+ endAgent(success: boolean = true): void {
95
+ if (this.quiet || !this.currentAgent) return
96
+
97
+ const duration = Date.now() - this.startTime
98
+ const durationStr = this.formatDuration(duration)
99
+
100
+ const icon = success ? chalk.green('✓') : chalk.red('✗')
101
+ const status = success ? 'Complete' : 'Failed'
102
+
103
+ console.log(`└─ ${icon} ${status} ${chalk.dim(`(${durationStr})`)}\n`)
104
+ this.currentAgent = null
105
+ }
106
+
107
+ /**
108
+ * Show a simple status line (no block)
109
+ */
110
+ status(icon: string, message: string): void {
111
+ if (this.quiet) return
112
+ console.log(`${icon} ${message}`)
113
+ }
114
+
115
+ /**
116
+ * Show task completion summary
117
+ */
118
+ complete(taskName: string, totalDuration?: number): void {
119
+ if (this.quiet) return
120
+
121
+ const durationStr = totalDuration ? ` ${chalk.dim(`[${this.formatDuration(totalDuration)}]`)}` : ''
122
+ console.log(chalk.green(`✅ ${taskName}${durationStr}`))
123
+ }
124
+
125
+ /**
126
+ * Format duration in human-readable form
127
+ */
128
+ private formatDuration(ms: number): string {
129
+ if (ms < 1000) return `${ms}ms`
130
+ const seconds = (ms / 1000).toFixed(1)
131
+ return `${seconds}s`
132
+ }
133
+ }
134
+
135
+ // Singleton instance
136
+ export const agentStream = new AgentStream()
137
+
138
+ export default agentStream
@@ -65,9 +65,8 @@ const branding: Branding = {
65
65
  footer: '⚡ prjct'
66
66
  },
67
67
 
68
- // Default Git commit footer (Claude - for backward compatibility)
69
- commitFooter: `🤖 Generated with [p/](https://www.prjct.app/)
70
- Designed for [Claude](https://www.anthropic.com/claude)`,
68
+ // Default Git commit footer (generic)
69
+ commitFooter: `Generated with [p/](https://www.prjct.app/)`,
71
70
 
72
71
  // URLs
73
72
  urls: {
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Next Steps - Show explicit guidance after each command
3
+ *
4
+ * Uses the workflow state machine to show valid commands
5
+ * for the current state.
6
+ */
7
+
8
+ import chalk from 'chalk'
9
+ import { workflowStateMachine, type WorkflowState } from '../workflow/state-machine'
10
+
11
+ interface NextStep {
12
+ cmd: string
13
+ desc: string
14
+ }
15
+
16
+ /**
17
+ * Command descriptions for display
18
+ */
19
+ const CMD_DESCRIPTIONS: Record<string, string> = {
20
+ task: 'Start new task',
21
+ done: 'Complete current task',
22
+ pause: 'Pause and switch context',
23
+ resume: 'Continue paused task',
24
+ ship: 'Ship the feature',
25
+ next: 'View task queue',
26
+ sync: 'Analyze project',
27
+ bug: 'Report a bug',
28
+ idea: 'Capture an idea',
29
+ }
30
+
31
+ /**
32
+ * Map command to resulting workflow state
33
+ */
34
+ const COMMAND_TO_STATE: Record<string, WorkflowState> = {
35
+ task: 'working',
36
+ done: 'completed',
37
+ 'done-subtask': 'working', // Still working on subtasks
38
+ pause: 'paused',
39
+ resume: 'working',
40
+ ship: 'shipped',
41
+ next: 'idle',
42
+ sync: 'idle',
43
+ init: 'idle',
44
+ bug: 'working',
45
+ idea: 'idle',
46
+ }
47
+
48
+ /**
49
+ * Show next steps after a command
50
+ */
51
+ export function showNextSteps(command: string, options: { quiet?: boolean } = {}): void {
52
+ if (options.quiet) return
53
+
54
+ // Get the state after this command
55
+ const resultingState = COMMAND_TO_STATE[command] || 'idle'
56
+
57
+ // Get valid commands for that state
58
+ const validCommands = workflowStateMachine.getValidCommands(resultingState)
59
+
60
+ if (validCommands.length === 0) return
61
+
62
+ const steps: NextStep[] = validCommands.map(cmd => ({
63
+ cmd: `p. ${cmd}`,
64
+ desc: CMD_DESCRIPTIONS[cmd] || cmd,
65
+ }))
66
+
67
+ console.log(chalk.dim('\nNext:'))
68
+ for (const step of steps) {
69
+ const cmd = chalk.cyan(step.cmd.padEnd(12))
70
+ console.log(chalk.dim(` ${cmd} → ${step.desc}`))
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Get next steps for a command (for programmatic use)
76
+ */
77
+ export function getNextSteps(command: string): NextStep[] {
78
+ const resultingState = COMMAND_TO_STATE[command] || 'idle'
79
+ const validCommands = workflowStateMachine.getValidCommands(resultingState)
80
+
81
+ return validCommands.map(cmd => ({
82
+ cmd: `p. ${cmd}`,
83
+ desc: CMD_DESCRIPTIONS[cmd] || cmd,
84
+ }))
85
+ }
86
+
87
+ /**
88
+ * Show current state info
89
+ */
90
+ export function showStateInfo(state: WorkflowState): void {
91
+ const info = workflowStateMachine.getStateInfo(state)
92
+ console.log(chalk.dim(`📍 State: ${chalk.white(state.toUpperCase())} - ${info.description}`))
93
+ }
94
+
95
+ export default { showNextSteps, getNextSteps, showStateInfo }
@@ -26,6 +26,8 @@ interface Output {
26
26
  fail(msg: string): Output
27
27
  warn(msg: string): Output
28
28
  stop(): Output
29
+ step(current: number, total: number, msg: string): Output
30
+ progress(current: number, total: number, msg?: string): Output
29
31
  }
30
32
 
31
33
  const out: Output = {
@@ -75,6 +77,30 @@ const out: Output = {
75
77
  clear()
76
78
  }
77
79
  return this
80
+ },
81
+
82
+ // Step counter: [3/7] Running tests...
83
+ step(current: number, total: number, msg: string) {
84
+ this.stop()
85
+ const counter = chalk.dim(`[${current}/${total}]`)
86
+ interval = setInterval(() => {
87
+ process.stdout.write(`\r${branding.cli.spin(frame++, `${counter} ${truncate(msg, 35)}`)}`)
88
+ }, SPEED)
89
+ return this
90
+ },
91
+
92
+ // Progress bar: [████░░░░] 50% Analyzing...
93
+ progress(current: number, total: number, msg?: string) {
94
+ this.stop()
95
+ const percent = Math.round((current / total) * 100)
96
+ const filled = Math.round(percent / 10)
97
+ const empty = 10 - filled
98
+ const bar = chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty))
99
+ const text = msg ? ` ${truncate(msg, 25)}` : ''
100
+ interval = setInterval(() => {
101
+ process.stdout.write(`\r${branding.cli.spin(frame++, `[${bar}] ${percent}%${text}`)}`)
102
+ }, SPEED)
103
+ return this
78
104
  }
79
105
  }
80
106
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Workflow Module
3
+ * State machine and transition management for prjct workflow.
4
+ */
5
+
6
+ export * from './state-machine'
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Workflow State Machine
3
+ * Explicit states with valid transitions for prjct workflow.
4
+ *
5
+ * States: idle → working → completed → shipped
6
+ * ↓↑
7
+ * paused
8
+ */
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export type WorkflowState = 'idle' | 'working' | 'paused' | 'completed' | 'shipped'
15
+
16
+ export type WorkflowCommand = 'task' | 'done' | 'pause' | 'resume' | 'ship' | 'next'
17
+
18
+ interface StateDefinition {
19
+ transitions: WorkflowCommand[]
20
+ prompt: string
21
+ description: string
22
+ }
23
+
24
+ interface TransitionResult {
25
+ valid: boolean
26
+ error?: string
27
+ suggestion?: string
28
+ }
29
+
30
+ // =============================================================================
31
+ // State Definitions
32
+ // =============================================================================
33
+
34
+ const WORKFLOW_STATES: Record<WorkflowState, StateDefinition> = {
35
+ idle: {
36
+ transitions: ['task', 'next'],
37
+ prompt: "p. task <description> Start working",
38
+ description: 'No active task',
39
+ },
40
+ working: {
41
+ transitions: ['done', 'pause'],
42
+ prompt: "p. done Complete task | p. pause Switch context",
43
+ description: 'Task in progress',
44
+ },
45
+ paused: {
46
+ transitions: ['resume', 'task'],
47
+ prompt: "p. resume Continue | p. task <new> Start different",
48
+ description: 'Task paused',
49
+ },
50
+ completed: {
51
+ transitions: ['ship', 'task', 'next'],
52
+ prompt: "p. ship Ship it | p. task <next> Start next",
53
+ description: 'Task completed',
54
+ },
55
+ shipped: {
56
+ transitions: ['task', 'next'],
57
+ prompt: "p. task <description> Start new task",
58
+ description: 'Feature shipped',
59
+ },
60
+ }
61
+
62
+ // =============================================================================
63
+ // State Machine
64
+ // =============================================================================
65
+
66
+ export class WorkflowStateMachine {
67
+ /**
68
+ * Get current state from storage state
69
+ */
70
+ getCurrentState(storageState: { currentTask?: { status?: string } | null }): WorkflowState {
71
+ const task = storageState?.currentTask
72
+
73
+ if (!task) {
74
+ return 'idle'
75
+ }
76
+
77
+ const status = task.status?.toLowerCase()
78
+
79
+ switch (status) {
80
+ case 'in_progress':
81
+ case 'working':
82
+ return 'working'
83
+ case 'paused':
84
+ return 'paused'
85
+ case 'completed':
86
+ case 'done':
87
+ return 'completed'
88
+ case 'shipped':
89
+ return 'shipped'
90
+ default:
91
+ return task ? 'working' : 'idle'
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Check if a command is valid for the current state
97
+ */
98
+ canTransition(currentState: WorkflowState, command: WorkflowCommand): TransitionResult {
99
+ const stateConfig = WORKFLOW_STATES[currentState]
100
+
101
+ if (stateConfig.transitions.includes(command)) {
102
+ return { valid: true }
103
+ }
104
+
105
+ // Build helpful error message
106
+ const validCommands = stateConfig.transitions.map(c => `p. ${c}`).join(', ')
107
+
108
+ return {
109
+ valid: false,
110
+ error: `Cannot run 'p. ${command}' in ${currentState} state`,
111
+ suggestion: `Valid commands: ${validCommands}`,
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get the next state after a command
117
+ */
118
+ getNextState(currentState: WorkflowState, command: WorkflowCommand): WorkflowState {
119
+ switch (command) {
120
+ case 'task':
121
+ return 'working'
122
+ case 'done':
123
+ return 'completed'
124
+ case 'pause':
125
+ return 'paused'
126
+ case 'resume':
127
+ return 'working'
128
+ case 'ship':
129
+ return 'shipped'
130
+ case 'next':
131
+ return currentState // next doesn't change state
132
+ default:
133
+ return currentState
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get state definition
139
+ */
140
+ getStateInfo(state: WorkflowState): StateDefinition {
141
+ return WORKFLOW_STATES[state]
142
+ }
143
+
144
+ /**
145
+ * Get prompt for current state
146
+ */
147
+ getPrompt(state: WorkflowState): string {
148
+ return WORKFLOW_STATES[state].prompt
149
+ }
150
+
151
+ /**
152
+ * Get valid commands for current state
153
+ */
154
+ getValidCommands(state: WorkflowState): WorkflowCommand[] {
155
+ return WORKFLOW_STATES[state].transitions
156
+ }
157
+
158
+ /**
159
+ * Format next steps for display
160
+ */
161
+ formatNextSteps(state: WorkflowState): string[] {
162
+ const stateConfig = WORKFLOW_STATES[state]
163
+ return stateConfig.transitions.map(cmd => {
164
+ switch (cmd) {
165
+ case 'task':
166
+ return 'p. task <desc> Start new task'
167
+ case 'done':
168
+ return 'p. done Complete current task'
169
+ case 'pause':
170
+ return 'p. pause Pause and switch context'
171
+ case 'resume':
172
+ return 'p. resume Continue paused task'
173
+ case 'ship':
174
+ return 'p. ship Ship the feature'
175
+ case 'next':
176
+ return 'p. next View task queue'
177
+ default:
178
+ return `p. ${cmd}`
179
+ }
180
+ })
181
+ }
182
+ }
183
+
184
+ // Singleton
185
+ export const workflowStateMachine = new WorkflowStateMachine()