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.
- package/CHANGELOG.md +97 -0
- package/core/agentic/command-executor.ts +15 -5
- package/core/ai-tools/formatters.ts +302 -0
- package/core/ai-tools/generator.ts +124 -0
- package/core/ai-tools/index.ts +15 -0
- package/core/ai-tools/registry.ts +195 -0
- package/core/cli/linear.ts +61 -2
- package/core/commands/analysis.ts +36 -2
- package/core/commands/commands.ts +2 -2
- package/core/commands/planning.ts +8 -4
- package/core/commands/shipping.ts +9 -7
- package/core/commands/workflow.ts +67 -17
- package/core/index.ts +3 -1
- package/core/infrastructure/ai-provider.ts +11 -36
- package/core/integrations/issue-tracker/types.ts +7 -1
- package/core/integrations/linear/client.ts +56 -24
- package/core/integrations/linear/index.ts +3 -0
- package/core/integrations/linear/sync.ts +313 -0
- package/core/schemas/index.ts +3 -0
- package/core/schemas/issues.ts +144 -0
- package/core/schemas/state.ts +3 -0
- package/core/services/sync-service.ts +71 -4
- package/core/utils/agent-stream.ts +138 -0
- package/core/utils/branding.ts +2 -3
- package/core/utils/next-steps.ts +95 -0
- package/core/utils/output.ts +26 -0
- package/core/workflow/index.ts +6 -0
- package/core/workflow/state-machine.ts +185 -0
- package/dist/bin/prjct.mjs +2382 -541
- package/package.json +1 -1
- package/templates/_bases/tracker-base.md +11 -0
- package/templates/commands/done.md +18 -13
- package/templates/commands/git.md +143 -54
- package/templates/commands/merge.md +121 -13
- package/templates/commands/review.md +1 -1
- package/templates/commands/ship.md +165 -20
- package/templates/commands/sync.md +17 -0
- package/templates/commands/task.md +123 -17
- package/templates/global/ANTIGRAVITY.md +2 -4
- package/templates/global/CLAUDE.md +115 -28
- package/templates/global/CURSOR.mdc +1 -3
- package/templates/global/GEMINI.md +2 -4
- package/templates/global/WINDSURF.md +1 -3
- package/templates/subagents/workflow/prjct-shipper.md +1 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Tools Registry
|
|
3
|
+
*
|
|
4
|
+
* Registry of AI coding tools (Claude Code, Cursor, Copilot, etc.)
|
|
5
|
+
* Each tool has its own context file format and token budget.
|
|
6
|
+
*
|
|
7
|
+
* Phase 1: Claude Code + Cursor
|
|
8
|
+
* Phase 2: + Copilot + Windsurf
|
|
9
|
+
* Phase 3: + Continue.dev + Auto-detection
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'child_process'
|
|
13
|
+
import fs from 'fs'
|
|
14
|
+
import path from 'path'
|
|
15
|
+
import os from 'os'
|
|
16
|
+
|
|
17
|
+
export interface AIToolConfig {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
outputFile: string
|
|
21
|
+
outputPath: 'repo' | 'global' // 'repo' = project root, 'global' = ~/.prjct-cli/projects/{id}/context/
|
|
22
|
+
maxTokens: number
|
|
23
|
+
format: 'detailed' | 'concise' | 'minimal' | 'json'
|
|
24
|
+
description: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Supported AI tools registry
|
|
29
|
+
*/
|
|
30
|
+
export const AI_TOOLS: Record<string, AIToolConfig> = {
|
|
31
|
+
claude: {
|
|
32
|
+
id: 'claude',
|
|
33
|
+
name: 'Claude Code',
|
|
34
|
+
outputFile: 'CLAUDE.md',
|
|
35
|
+
outputPath: 'global',
|
|
36
|
+
maxTokens: 3000,
|
|
37
|
+
format: 'detailed',
|
|
38
|
+
description: 'Anthropic Claude Code CLI',
|
|
39
|
+
},
|
|
40
|
+
cursor: {
|
|
41
|
+
id: 'cursor',
|
|
42
|
+
name: 'Cursor',
|
|
43
|
+
outputFile: '.cursorrules',
|
|
44
|
+
outputPath: 'repo',
|
|
45
|
+
maxTokens: 2000,
|
|
46
|
+
format: 'concise',
|
|
47
|
+
description: 'Cursor AI Editor',
|
|
48
|
+
},
|
|
49
|
+
copilot: {
|
|
50
|
+
id: 'copilot',
|
|
51
|
+
name: 'GitHub Copilot',
|
|
52
|
+
outputFile: '.github/copilot-instructions.md',
|
|
53
|
+
outputPath: 'repo',
|
|
54
|
+
maxTokens: 1500,
|
|
55
|
+
format: 'minimal',
|
|
56
|
+
description: 'GitHub Copilot',
|
|
57
|
+
},
|
|
58
|
+
windsurf: {
|
|
59
|
+
id: 'windsurf',
|
|
60
|
+
name: 'Windsurf',
|
|
61
|
+
outputFile: '.windsurfrules',
|
|
62
|
+
outputPath: 'repo',
|
|
63
|
+
maxTokens: 2000,
|
|
64
|
+
format: 'concise',
|
|
65
|
+
description: 'Codeium Windsurf Editor',
|
|
66
|
+
},
|
|
67
|
+
continue: {
|
|
68
|
+
id: 'continue',
|
|
69
|
+
name: 'Continue.dev',
|
|
70
|
+
outputFile: '.continue/config.json',
|
|
71
|
+
outputPath: 'repo',
|
|
72
|
+
maxTokens: 1500,
|
|
73
|
+
format: 'json',
|
|
74
|
+
description: 'Continue.dev open-source AI assistant',
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default tools to generate (Phase 2: all major tools)
|
|
80
|
+
*/
|
|
81
|
+
export const DEFAULT_AI_TOOLS = ['claude', 'cursor', 'copilot', 'windsurf']
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* All supported tool IDs
|
|
85
|
+
*/
|
|
86
|
+
export const SUPPORTED_AI_TOOLS = Object.keys(AI_TOOLS)
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get tool config by ID
|
|
90
|
+
*/
|
|
91
|
+
export function getAIToolConfig(id: string): AIToolConfig | null {
|
|
92
|
+
return AI_TOOLS[id] || null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse --agents flag value
|
|
97
|
+
* Examples: "claude,cursor" or "all"
|
|
98
|
+
*/
|
|
99
|
+
export function parseAgentsFlag(value: string): string[] {
|
|
100
|
+
if (value === 'all') {
|
|
101
|
+
return SUPPORTED_AI_TOOLS
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const requested = value.split(',').map(s => s.trim().toLowerCase())
|
|
105
|
+
return requested.filter(id => AI_TOOLS[id])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate tool IDs
|
|
110
|
+
*/
|
|
111
|
+
export function validateToolIds(ids: string[]): { valid: string[]; invalid: string[] } {
|
|
112
|
+
const valid: string[] = []
|
|
113
|
+
const invalid: string[] = []
|
|
114
|
+
|
|
115
|
+
for (const id of ids) {
|
|
116
|
+
if (AI_TOOLS[id]) {
|
|
117
|
+
valid.push(id)
|
|
118
|
+
} else {
|
|
119
|
+
invalid.push(id)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { valid, invalid }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if a command exists in PATH
|
|
128
|
+
*/
|
|
129
|
+
function commandExists(cmd: string): boolean {
|
|
130
|
+
try {
|
|
131
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' })
|
|
132
|
+
return true
|
|
133
|
+
} catch {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Detect installed AI tools
|
|
140
|
+
* Returns list of tool IDs that are detected on the system
|
|
141
|
+
*/
|
|
142
|
+
export function detectInstalledTools(repoPath: string = process.cwd()): string[] {
|
|
143
|
+
const detected: string[] = []
|
|
144
|
+
|
|
145
|
+
// Claude Code: check for 'claude' command
|
|
146
|
+
if (commandExists('claude')) {
|
|
147
|
+
detected.push('claude')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Cursor: check for command or .cursor/ directory in repo
|
|
151
|
+
if (commandExists('cursor') || fs.existsSync(path.join(repoPath, '.cursor'))) {
|
|
152
|
+
detected.push('cursor')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Copilot: check for .github/ directory (likely has Copilot if using GitHub)
|
|
156
|
+
if (fs.existsSync(path.join(repoPath, '.github'))) {
|
|
157
|
+
detected.push('copilot')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Windsurf: check for command or .windsurf/ directory
|
|
161
|
+
if (commandExists('windsurf') || fs.existsSync(path.join(repoPath, '.windsurf'))) {
|
|
162
|
+
detected.push('windsurf')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Continue.dev: check for .continue/ directory
|
|
166
|
+
if (fs.existsSync(path.join(repoPath, '.continue')) || fs.existsSync(path.join(os.homedir(), '.continue'))) {
|
|
167
|
+
detected.push('continue')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return detected
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get tools to generate based on mode
|
|
175
|
+
* - 'auto': detect installed tools
|
|
176
|
+
* - 'all': all supported tools
|
|
177
|
+
* - specific: use provided list
|
|
178
|
+
*/
|
|
179
|
+
export function resolveToolIds(
|
|
180
|
+
mode: 'auto' | 'all' | string[],
|
|
181
|
+
repoPath: string = process.cwd()
|
|
182
|
+
): string[] {
|
|
183
|
+
if (mode === 'auto') {
|
|
184
|
+
const detected = detectInstalledTools(repoPath)
|
|
185
|
+
// Always include claude if nothing detected (safe default)
|
|
186
|
+
return detected.length > 0 ? detected : ['claude']
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (mode === 'all') {
|
|
190
|
+
return SUPPORTED_AI_TOOLS
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Specific list provided
|
|
194
|
+
return mode.filter(id => AI_TOOLS[id])
|
|
195
|
+
}
|
package/core/cli/linear.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* list - List my assigned issues
|
|
10
10
|
* list-team <teamId> - List issues from a team
|
|
11
11
|
* get <id> - Get issue by ID or identifier (PRJ-123)
|
|
12
|
+
* get-local <id> - Get issue from local cache (no API call)
|
|
13
|
+
* sync - Pull all assigned issues to local storage
|
|
14
|
+
* sync-status - Check local cache status
|
|
12
15
|
* create <json> - Create issue from JSON input
|
|
13
16
|
* update <id> <json> - Update issue
|
|
14
17
|
* start <id> - Mark issue as in progress
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
* All output is JSON for easy parsing by Claude.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
|
-
import { linearService } from '../integrations/linear'
|
|
27
|
+
import { linearService, linearSync } from '../integrations/linear'
|
|
25
28
|
import {
|
|
26
29
|
getLinearApiKey,
|
|
27
30
|
setLinearCredentials,
|
|
@@ -136,7 +139,16 @@ async function main(): Promise<void> {
|
|
|
136
139
|
case 'list': {
|
|
137
140
|
await initFromProject()
|
|
138
141
|
const limit = commandArgs[0] ? parseInt(commandArgs[0], 10) : 20
|
|
139
|
-
|
|
142
|
+
|
|
143
|
+
// Use team issues if teamId is configured, otherwise assigned issues
|
|
144
|
+
const creds = await getProjectCredentials(projectId!)
|
|
145
|
+
let issues
|
|
146
|
+
if (creds.linear?.teamId) {
|
|
147
|
+
issues = await linearService.fetchTeamIssues(creds.linear.teamId, { limit })
|
|
148
|
+
} else {
|
|
149
|
+
issues = await linearService.fetchAssignedIssues({ limit })
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
output({
|
|
141
153
|
count: issues.length,
|
|
142
154
|
issues: issues.map((issue) => ({
|
|
@@ -192,6 +204,50 @@ async function main(): Promise<void> {
|
|
|
192
204
|
break
|
|
193
205
|
}
|
|
194
206
|
|
|
207
|
+
case 'get-local': {
|
|
208
|
+
if (!projectId) {
|
|
209
|
+
error('--project required for get-local')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const id = commandArgs[0]
|
|
213
|
+
if (!id) {
|
|
214
|
+
error('Issue ID required. Usage: get-local <id>')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const cachedIssue = await linearSync.getIssueLocal(projectId, id)
|
|
218
|
+
if (!cachedIssue) {
|
|
219
|
+
error(`Issue not in local cache: ${id}. Run 'sync' first.`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
output(cachedIssue)
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case 'sync': {
|
|
227
|
+
if (!projectId) {
|
|
228
|
+
error('--project required for sync')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await initFromProject()
|
|
232
|
+
const result = await linearSync.pullAll(projectId)
|
|
233
|
+
|
|
234
|
+
output({
|
|
235
|
+
success: result.errors.length === 0,
|
|
236
|
+
...result,
|
|
237
|
+
})
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'sync-status': {
|
|
242
|
+
if (!projectId) {
|
|
243
|
+
error('--project required for sync-status')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const status = await linearSync.getSyncStatus(projectId)
|
|
247
|
+
output(status)
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
|
|
195
251
|
case 'create': {
|
|
196
252
|
await initFromProject()
|
|
197
253
|
const inputJson = commandArgs[0]
|
|
@@ -357,6 +413,9 @@ async function main(): Promise<void> {
|
|
|
357
413
|
list: 'list [limit] - List my assigned issues',
|
|
358
414
|
'list-team': 'list-team <teamId> [limit] - List team issues',
|
|
359
415
|
get: 'get <id> - Get issue by ID or identifier',
|
|
416
|
+
'get-local': 'get-local <id> - Get from local cache (no API)',
|
|
417
|
+
sync: 'sync - Pull all assigned issues to local storage',
|
|
418
|
+
'sync-status': 'sync-status - Check local cache status',
|
|
360
419
|
create: 'create <json> - Create issue',
|
|
361
420
|
update: 'update <id> <json> - Update issue',
|
|
362
421
|
start: 'start <id> - Mark as in progress',
|
|
@@ -17,6 +17,7 @@ import analyzer from '../domain/analyzer'
|
|
|
17
17
|
import { generateContext } from '../context/generator'
|
|
18
18
|
import commandInstaller from '../infrastructure/command-installer'
|
|
19
19
|
import { syncService } from '../services'
|
|
20
|
+
import { showNextSteps } from '../utils/next-steps'
|
|
20
21
|
|
|
21
22
|
export class AnalysisCommands extends PrjctCommandsBase {
|
|
22
23
|
/**
|
|
@@ -205,15 +206,16 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
205
206
|
*
|
|
206
207
|
* This eliminates the need for Claude to make 50+ individual tool calls.
|
|
207
208
|
*/
|
|
208
|
-
async sync(projectPath: string = process.cwd()): Promise<CommandResult> {
|
|
209
|
+
async sync(projectPath: string = process.cwd(), options: { aiTools?: string[] } = {}): Promise<CommandResult> {
|
|
209
210
|
try {
|
|
210
211
|
const initResult = await this.ensureProjectInit(projectPath)
|
|
211
212
|
if (!initResult.success) return initResult
|
|
212
213
|
|
|
214
|
+
const startTime = Date.now()
|
|
213
215
|
console.log('🔄 Syncing project...\n')
|
|
214
216
|
|
|
215
217
|
// Use syncService to do EVERYTHING in one call
|
|
216
|
-
const result = await syncService.sync(projectPath)
|
|
218
|
+
const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
|
|
217
219
|
|
|
218
220
|
if (!result.success) {
|
|
219
221
|
console.error('❌ Sync failed:', result.error)
|
|
@@ -246,6 +248,17 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
246
248
|
}
|
|
247
249
|
console.log('')
|
|
248
250
|
|
|
251
|
+
// Show AI Tools generated (multi-agent output)
|
|
252
|
+
if (result.aiTools && result.aiTools.length > 0) {
|
|
253
|
+
const successTools = result.aiTools.filter(t => t.success)
|
|
254
|
+
console.log(`🤖 AI Tools Context (${successTools.length})`)
|
|
255
|
+
for (const tool of result.aiTools) {
|
|
256
|
+
const status = tool.success ? '✓' : '✗'
|
|
257
|
+
console.log(`├── ${status} ${tool.outputFile} (${tool.toolId})`)
|
|
258
|
+
}
|
|
259
|
+
console.log('')
|
|
260
|
+
}
|
|
261
|
+
|
|
249
262
|
const workflowAgents = result.agents.filter(a => a.type === 'workflow').map(a => a.name)
|
|
250
263
|
const domainAgents = result.agents.filter(a => a.type === 'domain').map(a => a.name)
|
|
251
264
|
|
|
@@ -267,9 +280,30 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
267
280
|
console.log('✨ Repository is clean!\n')
|
|
268
281
|
}
|
|
269
282
|
|
|
283
|
+
showNextSteps('sync')
|
|
284
|
+
|
|
285
|
+
// Summary metrics
|
|
286
|
+
const elapsed = Date.now() - startTime
|
|
287
|
+
const contextFilesCount = result.contextFiles.length + (result.aiTools?.filter(t => t.success).length || 0)
|
|
288
|
+
const agentCount = result.agents.length
|
|
289
|
+
|
|
290
|
+
console.log('─'.repeat(45))
|
|
291
|
+
console.log(`📊 Sync Summary`)
|
|
292
|
+
console.log(` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(', ') || 'no frameworks'})`)
|
|
293
|
+
console.log(` Files: ${result.stats.fileCount} analyzed → ${contextFilesCount} context files`)
|
|
294
|
+
console.log(` Agents: ${agentCount} (${result.agents.filter(a => a.type === 'domain').length} domain)`)
|
|
295
|
+
console.log(` Time: ${(elapsed / 1000).toFixed(1)}s`)
|
|
296
|
+
console.log('')
|
|
297
|
+
|
|
270
298
|
return {
|
|
271
299
|
success: true,
|
|
272
300
|
data: result,
|
|
301
|
+
metrics: {
|
|
302
|
+
elapsed,
|
|
303
|
+
contextFilesCount,
|
|
304
|
+
agentCount,
|
|
305
|
+
fileCount: result.stats.fileCount,
|
|
306
|
+
},
|
|
273
307
|
}
|
|
274
308
|
} catch (error) {
|
|
275
309
|
console.error('❌ Error:', (error as Error).message)
|
|
@@ -154,8 +154,8 @@ class PrjctCommands {
|
|
|
154
154
|
return this.analysis.analyze(options, projectPath)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
async sync(projectPath: string = process.cwd()): Promise<CommandResult> {
|
|
158
|
-
return this.analysis.sync(projectPath)
|
|
157
|
+
async sync(projectPath: string = process.cwd(), options: { aiTools?: string[] } = {}): Promise<CommandResult> {
|
|
158
|
+
return this.analysis.sync(projectPath, options)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// ========== Context Commands ==========
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { queueStorage, ideasStorage } from '../storage'
|
|
22
22
|
import authorDetector from '../infrastructure/author-detector'
|
|
23
23
|
import commandInstaller from '../infrastructure/command-installer'
|
|
24
|
+
import { showNextSteps } from '../utils/next-steps'
|
|
24
25
|
|
|
25
26
|
// Lazy-loaded to avoid circular dependencies
|
|
26
27
|
let _analysisCommands: import('./analysis').AnalysisCommands | null = null
|
|
@@ -47,7 +48,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
47
48
|
return { success: false, message: 'Already initialized' }
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
out.
|
|
51
|
+
out.step(1, 4, 'Detecting author...')
|
|
51
52
|
|
|
52
53
|
const detectedAuthor = await authorDetector.detect()
|
|
53
54
|
// Convert null to undefined for createConfig
|
|
@@ -59,7 +60,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
59
60
|
const config = await configManager.createConfig(projectPath, author)
|
|
60
61
|
const projectId = config.projectId
|
|
61
62
|
|
|
62
|
-
out.
|
|
63
|
+
out.step(2, 4, 'Creating structure...')
|
|
63
64
|
|
|
64
65
|
await pathManager.ensureProjectStructure(projectId)
|
|
65
66
|
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
@@ -91,12 +92,12 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
91
92
|
const hasCode = await this._detectExistingCode(projectPath)
|
|
92
93
|
|
|
93
94
|
if (hasCode || !isEmpty) {
|
|
94
|
-
out.
|
|
95
|
+
out.step(3, 4, 'Analyzing project...')
|
|
95
96
|
const analysis = await getAnalysisCommands()
|
|
96
97
|
const analysisResult = await analysis.analyze({}, projectPath)
|
|
97
98
|
|
|
98
99
|
if (analysisResult.success) {
|
|
99
|
-
out.
|
|
100
|
+
out.step(4, 4, 'Generating agents...')
|
|
100
101
|
await analysis.sync(projectPath)
|
|
101
102
|
out.done('initialized')
|
|
102
103
|
return { success: true, mode: 'existing', projectId }
|
|
@@ -123,6 +124,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
123
124
|
await commandInstaller.installGlobalConfig()
|
|
124
125
|
|
|
125
126
|
out.done('initialized')
|
|
127
|
+
showNextSteps('init')
|
|
126
128
|
return { success: true, projectId }
|
|
127
129
|
} catch (error) {
|
|
128
130
|
out.fail((error as Error).message)
|
|
@@ -250,6 +252,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
250
252
|
})
|
|
251
253
|
|
|
252
254
|
out.done(`bug [${severity}] → ${agent}`)
|
|
255
|
+
showNextSteps('bug')
|
|
253
256
|
|
|
254
257
|
return { success: true, bug: description, severity, agent }
|
|
255
258
|
} catch (error) {
|
|
@@ -416,6 +419,7 @@ Generated: ${new Date().toLocaleString()}
|
|
|
416
419
|
})
|
|
417
420
|
|
|
418
421
|
out.done(`idea captured: ${description.slice(0, 40)}`)
|
|
422
|
+
showNextSteps('idea')
|
|
419
423
|
|
|
420
424
|
return { success: true, mode: 'capture', idea: description }
|
|
421
425
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
out
|
|
19
19
|
} from './base'
|
|
20
20
|
import { stateStorage, shippedStorage } from '../storage'
|
|
21
|
+
import { showNextSteps } from '../utils/next-steps'
|
|
21
22
|
|
|
22
23
|
export class ShippingCommands extends PrjctCommandsBase {
|
|
23
24
|
/**
|
|
@@ -70,22 +71,22 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
70
71
|
featureName = currentTask?.description || 'current work'
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// Ship steps with progress indicator
|
|
75
|
+
out.step(1, 5, `Linting ${featureName}...`)
|
|
75
76
|
const lintResult = await this._runLint(projectPath)
|
|
76
77
|
|
|
77
|
-
out.
|
|
78
|
+
out.step(2, 5, 'Running tests...')
|
|
78
79
|
const testResult = await this._runTests(projectPath)
|
|
79
80
|
|
|
80
|
-
out.
|
|
81
|
+
out.step(3, 5, 'Updating version...')
|
|
81
82
|
const newVersion = await this._bumpVersion(projectPath)
|
|
82
83
|
await this._updateChangelog(featureName, newVersion, projectPath)
|
|
83
84
|
|
|
84
|
-
out.
|
|
85
|
+
out.step(4, 5, 'Committing...')
|
|
85
86
|
const commitResult = await this._createShipCommit(featureName, projectPath)
|
|
86
87
|
|
|
87
88
|
if (commitResult.success) {
|
|
88
|
-
out.
|
|
89
|
+
out.step(5, 5, 'Pushing...')
|
|
89
90
|
await this._gitPush(projectPath)
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -116,6 +117,7 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
out.done(`v${newVersion} shipped`)
|
|
120
|
+
showNextSteps('ship')
|
|
119
121
|
|
|
120
122
|
return { success: true, feature: featureName, version: newVersion }
|
|
121
123
|
} catch (error) {
|
|
@@ -215,7 +217,7 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
215
217
|
try {
|
|
216
218
|
await toolRegistry.get('Bash')!('git add .')
|
|
217
219
|
|
|
218
|
-
const commitMsg = `feat: ${feature}\n\
|
|
220
|
+
const commitMsg = `feat: ${feature}\n\nGenerated with [p/](https://www.prjct.app/)`
|
|
219
221
|
|
|
220
222
|
await toolRegistry.get('Bash')!(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`)
|
|
221
223
|
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
import { stateStorage, queueStorage } from '../storage'
|
|
21
21
|
import { templateExecutor } from '../agentic/template-executor'
|
|
22
22
|
import commandExecutor from '../agentic/command-executor'
|
|
23
|
+
import { showNextSteps, showStateInfo } from '../utils/next-steps'
|
|
24
|
+
import { workflowStateMachine } from '../workflow/state-machine'
|
|
25
|
+
import { linearService } from '../integrations/linear'
|
|
26
|
+
import { getProjectCredentials, getLinearApiKey } from '../utils/project-credentials'
|
|
23
27
|
|
|
24
28
|
export class WorkflowCommands extends PrjctCommandsBase {
|
|
25
29
|
/**
|
|
@@ -45,24 +49,39 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
45
49
|
return { success: false, error: result.error }
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
// Check if task is a Linear issue ID (e.g., PRJ-139)
|
|
53
|
+
let linearId: string | undefined
|
|
54
|
+
let taskDescription = task
|
|
55
|
+
const linearPattern = /^[A-Z]+-\d+$/
|
|
56
|
+
if (linearPattern.test(task)) {
|
|
57
|
+
try {
|
|
58
|
+
const creds = await getProjectCredentials(projectId)
|
|
59
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
60
|
+
if (apiKey && creds.linear?.teamId) {
|
|
61
|
+
await linearService.initializeFromApiKey(
|
|
62
|
+
apiKey,
|
|
63
|
+
creds.linear.teamId
|
|
64
|
+
)
|
|
65
|
+
const issue = await linearService.fetchIssue(task)
|
|
66
|
+
if (issue) {
|
|
67
|
+
linearId = task
|
|
68
|
+
taskDescription = `${task}: ${issue.title}`
|
|
69
|
+
// Mark as in progress in Linear
|
|
70
|
+
await linearService.markInProgress(task)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Linear fetch failed - continue with task as-is
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
// Write-through: JSON → MD → Event
|
|
49
79
|
await stateStorage.startTask(projectId, {
|
|
50
80
|
id: generateUUID(),
|
|
51
|
-
description:
|
|
52
|
-
sessionId: generateUUID()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Log orchestrator results if available
|
|
56
|
-
if (result.orchestratorContext) {
|
|
57
|
-
const oc = result.orchestratorContext
|
|
58
|
-
const agentsList = oc.agents.map((a: { name: string }) => a.name).join(', ') || 'none'
|
|
59
|
-
const domainsList = oc.detectedDomains.join(', ')
|
|
60
|
-
console.log(`🎯 Orchestrator: ${domainsList} → [${agentsList}]`)
|
|
61
|
-
|
|
62
|
-
if (oc.requiresFragmentation && oc.subtasks) {
|
|
63
|
-
console.log(` → ${oc.subtasks.length} subtasks created`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
81
|
+
description: taskDescription,
|
|
82
|
+
sessionId: generateUUID(),
|
|
83
|
+
linearId,
|
|
84
|
+
} as Parameters<typeof stateStorage.startTask>[1])
|
|
66
85
|
|
|
67
86
|
// Get available agents for backward compatibility
|
|
68
87
|
const availableAgents = await templateExecutor.getAvailableAgents(projectPath)
|
|
@@ -70,7 +89,9 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
70
89
|
? availableAgents.join(', ')
|
|
71
90
|
: 'none (run p. sync)'
|
|
72
91
|
|
|
73
|
-
out.done(`${task}
|
|
92
|
+
out.done(`${task}`)
|
|
93
|
+
showStateInfo('working')
|
|
94
|
+
showNextSteps('task')
|
|
74
95
|
|
|
75
96
|
await this.logToMemory(projectPath, 'task_started', {
|
|
76
97
|
task,
|
|
@@ -139,7 +160,31 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
139
160
|
// Write-through: Complete task (JSON → MD → Event)
|
|
140
161
|
await stateStorage.completeTask(projectId)
|
|
141
162
|
|
|
142
|
-
|
|
163
|
+
// Sync to Linear if task has linearId
|
|
164
|
+
const linearId = (currentTask as { linearId?: string }).linearId
|
|
165
|
+
if (linearId) {
|
|
166
|
+
try {
|
|
167
|
+
const creds = await getProjectCredentials(projectId)
|
|
168
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
169
|
+
if (apiKey && creds.linear?.teamId) {
|
|
170
|
+
await linearService.initializeFromApiKey(
|
|
171
|
+
apiKey,
|
|
172
|
+
creds.linear.teamId
|
|
173
|
+
)
|
|
174
|
+
await linearService.markDone(linearId)
|
|
175
|
+
out.done(`${task}${duration ? ` (${duration})` : ''} → Linear ✓`)
|
|
176
|
+
} else {
|
|
177
|
+
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Linear sync failed silently - don't block the workflow
|
|
181
|
+
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
185
|
+
}
|
|
186
|
+
showStateInfo('completed')
|
|
187
|
+
showNextSteps('done')
|
|
143
188
|
|
|
144
189
|
await this.logToMemory(projectPath, 'task_completed', {
|
|
145
190
|
task,
|
|
@@ -176,6 +221,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
176
221
|
}
|
|
177
222
|
|
|
178
223
|
out.done(`${tasks.length} task${tasks.length !== 1 ? 's' : ''} queued`)
|
|
224
|
+
showNextSteps('next')
|
|
179
225
|
|
|
180
226
|
return { success: true, tasks, count: tasks.length }
|
|
181
227
|
} catch (error) {
|
|
@@ -210,6 +256,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
210
256
|
|
|
211
257
|
const taskDesc = currentTask.description.slice(0, 40)
|
|
212
258
|
out.done(`paused: ${taskDesc}${reason ? ` (${reason})` : ''}`)
|
|
259
|
+
showStateInfo('paused')
|
|
260
|
+
showNextSteps('pause')
|
|
213
261
|
|
|
214
262
|
await this.logToMemory(projectPath, 'task_paused', {
|
|
215
263
|
task: currentTask.description,
|
|
@@ -254,6 +302,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
254
302
|
}
|
|
255
303
|
|
|
256
304
|
out.done(`resumed: ${resumed.description.slice(0, 40)}`)
|
|
305
|
+
showStateInfo('working')
|
|
306
|
+
showNextSteps('resume')
|
|
257
307
|
|
|
258
308
|
await this.logToMemory(projectPath, 'task_resumed', {
|
|
259
309
|
task: resumed.description,
|
package/core/index.ts
CHANGED
|
@@ -118,7 +118,9 @@ async function main(): Promise<void> {
|
|
|
118
118
|
redo: () => commands.redo(),
|
|
119
119
|
history: () => commands.history(),
|
|
120
120
|
// Setup
|
|
121
|
-
sync: () => commands.sync(),
|
|
121
|
+
sync: () => commands.sync(process.cwd(), {
|
|
122
|
+
aiTools: options.agents ? String(options.agents).split(',') : undefined,
|
|
123
|
+
}),
|
|
122
124
|
start: () => commands.start(),
|
|
123
125
|
// Context (for Claude templates)
|
|
124
126
|
context: (p) => commands.context(p),
|