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
|
@@ -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.
|
|
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
|
-
//
|
|
214
|
+
// 7. Update state.json with enterprise fields
|
|
154
215
|
await this.updateStateJson(stats, stack)
|
|
155
216
|
|
|
156
|
-
//
|
|
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
|
package/core/utils/branding.ts
CHANGED
|
@@ -65,9 +65,8 @@ const branding: Branding = {
|
|
|
65
65
|
footer: '⚡ prjct'
|
|
66
66
|
},
|
|
67
67
|
|
|
68
|
-
// Default Git commit footer (
|
|
69
|
-
commitFooter:
|
|
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 }
|
package/core/utils/output.ts
CHANGED
|
@@ -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,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()
|