prjct-cli 0.41.0 → 0.44.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/bin/prjct.ts +26 -0
  3. package/core/agentic/command-executor.ts +15 -5
  4. package/core/ai-tools/formatters.ts +302 -0
  5. package/core/ai-tools/generator.ts +124 -0
  6. package/core/ai-tools/index.ts +15 -0
  7. package/core/ai-tools/registry.ts +195 -0
  8. package/core/cli/linear.ts +440 -0
  9. package/core/commands/analysis.ts +36 -2
  10. package/core/commands/commands.ts +2 -2
  11. package/core/commands/planning.ts +8 -4
  12. package/core/commands/shipping.ts +8 -6
  13. package/core/commands/workflow.ts +67 -17
  14. package/core/index.ts +3 -1
  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/next-steps.ts +95 -0
  25. package/core/utils/output.ts +26 -0
  26. package/core/utils/project-credentials.ts +148 -0
  27. package/core/workflow/index.ts +6 -0
  28. package/core/workflow/state-machine.ts +185 -0
  29. package/dist/bin/prjct.mjs +2399 -540
  30. package/dist/core/infrastructure/setup.js +238 -192
  31. package/package.json +1 -1
  32. package/templates/_bases/tracker-base.md +11 -0
  33. package/templates/commands/done.md +18 -13
  34. package/templates/commands/enrich.md +152 -18
  35. package/templates/commands/linear.md +169 -135
  36. package/templates/commands/sync.md +17 -0
  37. package/templates/commands/task.md +20 -11
  38. package/templates/global/CLAUDE.md +58 -0
@@ -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) {
@@ -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),
@@ -75,10 +75,16 @@ export interface CreateIssueInput {
75
75
  }
76
76
 
77
77
  /**
78
- * Update input for enriching issues
78
+ * Update input for issues
79
79
  */
80
80
  export interface UpdateIssueInput {
81
+ title?: string
81
82
  description?: string
83
+ priority?: IssuePriority
84
+ assigneeId?: string | null // null to unassign
85
+ stateId?: string
86
+ projectId?: string
87
+ labels?: string[]
82
88
  // Provider-specific: may update custom fields for AC, etc.
83
89
  customFields?: Record<string, unknown>
84
90
  }
@@ -84,7 +84,8 @@ export class LinearProvider implements IssueTrackerProvider {
84
84
  // Verify connection
85
85
  try {
86
86
  const viewer = await this.sdk.viewer
87
- console.log(`[linear] Connected as ${viewer.name} (${viewer.email})`)
87
+ // Use stderr for logs to not break JSON output
88
+ console.error(`[linear] Connected as ${viewer.name} (${viewer.email})`)
88
89
  } catch (error) {
89
90
  this.sdk = null
90
91
  throw new Error(`Linear connection failed: ${(error as Error).message}`)
@@ -93,16 +94,28 @@ export class LinearProvider implements IssueTrackerProvider {
93
94
 
94
95
  /**
95
96
  * Get issues assigned to current user
97
+ * Filters by configured team if defaultTeamId is set
96
98
  */
97
99
  async fetchAssignedIssues(options?: FetchOptions): Promise<Issue[]> {
98
100
  if (!this.sdk) throw new Error('Linear not initialized')
99
101
 
100
102
  const viewer = await this.sdk.viewer
103
+
104
+ // Build filter - always filter by team if configured
105
+ const filter: Record<string, unknown> = {}
106
+
107
+ if (!options?.includeCompleted) {
108
+ filter.state = { type: { nin: ['completed', 'canceled'] } }
109
+ }
110
+
111
+ // Filter by configured team to only show relevant issues
112
+ if (this.config?.defaultTeamId) {
113
+ filter.team = { id: { eq: this.config.defaultTeamId } }
114
+ }
115
+
101
116
  const assignedIssues = await viewer.assignedIssues({
102
117
  first: options?.limit || 50,
103
- filter: options?.includeCompleted
104
- ? undefined
105
- : { state: { type: { nin: ['completed', 'canceled'] } } },
118
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
106
119
  })
107
120
 
108
121
  return Promise.all(
@@ -128,28 +141,34 @@ export class LinearProvider implements IssueTrackerProvider {
128
141
  }
129
142
 
130
143
  /**
131
- * Get a single issue by ID or identifier (e.g., "ENG-123")
144
+ * Get a single issue by ID or identifier (e.g., "PRJ-123")
132
145
  */
133
146
  async fetchIssue(id: string): Promise<Issue | null> {
134
147
  if (!this.sdk) throw new Error('Linear not initialized')
135
148
 
136
149
  try {
137
- // Check if it looks like an identifier (e.g., "ENG-123")
150
+ // Check if it looks like an identifier (e.g., "PRJ-123")
138
151
  if (id.includes('-') && /^[A-Z]+-\d+$/.test(id)) {
139
- // Use raw GraphQL to search by identifier
140
- const result = await this.sdk.client.rawRequest(`
141
- query SearchByIdentifier($query: String!) {
142
- searchIssues(query: $query, first: 1) {
143
- nodes {
144
- id
145
- }
146
- }
147
- }
148
- `, { query: id }) as { data?: { searchIssues?: { nodes?: Array<{ id: string }> } } }
149
-
150
- if (result.data?.searchIssues?.nodes?.length) {
151
- const issue = await this.sdk.issue(result.data.searchIssues.nodes[0].id)
152
- return this.mapIssue(issue)
152
+ // Parse identifier into team key and issue number
153
+ const match = id.match(/^([A-Z]+)-(\d+)$/)
154
+ if (!match) return null
155
+
156
+ const [, teamKey, numberStr] = match
157
+ const issueNumber = parseInt(numberStr, 10)
158
+
159
+ // Find team by key
160
+ const teams = await this.sdk.teams({ first: 50 })
161
+ const team = teams.nodes.find((t) => t.key === teamKey)
162
+ if (!team) return null
163
+
164
+ // Query issue by team and number
165
+ const issues = await team.issues({
166
+ first: 1,
167
+ filter: { number: { eq: issueNumber } },
168
+ })
169
+
170
+ if (issues.nodes.length > 0) {
171
+ return this.mapIssue(issues.nodes[0])
153
172
  }
154
173
  return null
155
174
  }
@@ -197,15 +216,28 @@ export class LinearProvider implements IssueTrackerProvider {
197
216
  async updateIssue(id: string, input: UpdateIssueInput): Promise<Issue> {
198
217
  if (!this.sdk) throw new Error('Linear not initialized')
199
218
 
200
- // Get the issue first to get UUID
219
+ // Get the issue first to get UUID (if identifier like PRJ-123 was passed)
201
220
  const issue = await this.fetchIssue(id)
202
221
  if (!issue) {
203
222
  throw new Error(`Issue ${id} not found`)
204
223
  }
205
224
 
206
- await this.sdk.updateIssue(issue.id, {
207
- description: input.description,
208
- })
225
+ // Build update payload with all supported fields
226
+ const updatePayload: Record<string, unknown> = {}
227
+
228
+ if (input.title !== undefined) updatePayload.title = input.title
229
+ if (input.description !== undefined) updatePayload.description = input.description
230
+ if (input.priority !== undefined) updatePayload.priority = PRIORITY_TO_LINEAR[input.priority]
231
+ if (input.assigneeId !== undefined) updatePayload.assigneeId = input.assigneeId
232
+ if (input.stateId !== undefined) updatePayload.stateId = input.stateId
233
+ if (input.projectId !== undefined) updatePayload.projectId = input.projectId
234
+
235
+ // Handle labels - need to resolve names to IDs
236
+ if (input.labels !== undefined && issue.team) {
237
+ updatePayload.labelIds = await this.resolveLabelIds(issue.team.id, input.labels)
238
+ }
239
+
240
+ await this.sdk.updateIssue(issue.id, updatePayload)
209
241
 
210
242
  // Fetch updated issue
211
243
  const updated = await this.fetchIssue(issue.id)
@@ -9,6 +9,9 @@ export { LinearProvider, linearProvider } from './client'
9
9
  // Service layer with caching (preferred API)
10
10
  export { LinearService, linearService } from './service'
11
11
 
12
+ // Sync layer for bidirectional sync with issues.json
13
+ export { LinearSync, linearSync } from './sync'
14
+
12
15
  // Cache utilities
13
16
  export {
14
17
  issueCache,