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
@@ -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
+ }
@@ -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
- const issues = await linearService.fetchAssignedIssues({ limit })
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.spin('initializing...')
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.spin('creating structure...')
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.spin('analyzing project...')
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.spin('generating agents...')
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
- out.spin(`shipping ${featureName}...`)
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.spin('running tests...')
78
+ out.step(2, 5, 'Running tests...')
78
79
  const testResult = await this._runTests(projectPath)
79
80
 
80
- out.spin('updating version...')
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.spin('committing...')
85
+ out.step(4, 5, 'Committing...')
85
86
  const commitResult = await this._createShipCommit(featureName, projectPath)
86
87
 
87
88
  if (commitResult.success) {
88
- out.spin('pushing...')
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\n🤖 Generated with [p/](https://www.prjct.app/)\nDesigned for [Claude](https://www.anthropic.com/claude)`
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: task,
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} [specialists: ${agentsList}]`)
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
- out.done(`${task}${duration ? ` (${duration})` : ''}`)
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),