prjct-cli 0.46.0 → 0.47.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.
- package/CHANGELOG.md +42 -0
- package/bin/prjct.ts +3 -47
- package/core/agentic/command-executor.ts +8 -1
- package/core/commands/command-data.ts +16 -0
- package/core/commands/commands.ts +7 -0
- package/core/commands/register.ts +1 -0
- package/core/commands/setup.ts +4 -4
- package/core/commands/shipping.ts +26 -3
- package/core/commands/workflow.ts +105 -2
- package/core/utils/help.ts +321 -0
- package/core/utils/subtask-table.ts +234 -0
- package/core/workflow/index.ts +1 -0
- package/core/workflow/workflow-preferences.ts +312 -0
- package/dist/bin/prjct.mjs +4394 -3828
- package/package.json +1 -1
- package/templates/commands/workflow.md +150 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Preferences - Natural Language Driven Hooks
|
|
3
|
+
*
|
|
4
|
+
* Users configure workflow hooks via natural language.
|
|
5
|
+
* The LLM interprets preferences and stores them in memory.
|
|
6
|
+
*
|
|
7
|
+
* Scopes:
|
|
8
|
+
* - permanent: persisted via memorySystem.recordDecision()
|
|
9
|
+
* - session: in-memory Map, cleared on process exit
|
|
10
|
+
* - once: consumed after first use
|
|
11
|
+
*
|
|
12
|
+
* @see PRJ-137
|
|
13
|
+
* @module workflow/workflow-preferences
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { exec } from 'node:child_process'
|
|
17
|
+
import { promisify } from 'node:util'
|
|
18
|
+
import memorySystem from '../agentic/memory-system'
|
|
19
|
+
|
|
20
|
+
const execAsync = promisify(exec)
|
|
21
|
+
|
|
22
|
+
// ANSI colors
|
|
23
|
+
const DIM = '\x1b[2m'
|
|
24
|
+
const GREEN = '\x1b[32m'
|
|
25
|
+
const RED = '\x1b[31m'
|
|
26
|
+
const YELLOW = '\x1b[33m'
|
|
27
|
+
const RESET = '\x1b[0m'
|
|
28
|
+
|
|
29
|
+
export type PreferenceScope = 'permanent' | 'session' | 'once'
|
|
30
|
+
export type HookPhase = 'before' | 'after' | 'skip'
|
|
31
|
+
export type HookCommand = 'task' | 'done' | 'ship' | 'sync'
|
|
32
|
+
|
|
33
|
+
export interface WorkflowPreference {
|
|
34
|
+
hook: HookPhase
|
|
35
|
+
command: HookCommand
|
|
36
|
+
action: string // command to run or 'true' for skip
|
|
37
|
+
scope: PreferenceScope
|
|
38
|
+
createdAt: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HookResult {
|
|
42
|
+
success: boolean
|
|
43
|
+
failed?: string
|
|
44
|
+
skipped?: string[]
|
|
45
|
+
output?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Session and once preferences (in-memory)
|
|
49
|
+
const sessionPreferences: Map<string, WorkflowPreference> = new Map()
|
|
50
|
+
const oncePreferences: Map<string, WorkflowPreference> = new Map()
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a key for a preference
|
|
54
|
+
*/
|
|
55
|
+
function prefKey(hook: HookPhase, command: HookCommand): string {
|
|
56
|
+
return `workflow:${hook}_${command}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set a workflow preference
|
|
61
|
+
*/
|
|
62
|
+
export async function setWorkflowPreference(
|
|
63
|
+
projectId: string,
|
|
64
|
+
pref: WorkflowPreference
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
const key = prefKey(pref.hook, pref.command)
|
|
67
|
+
|
|
68
|
+
switch (pref.scope) {
|
|
69
|
+
case 'permanent':
|
|
70
|
+
// Use memory system for persistent storage
|
|
71
|
+
await memorySystem.recordDecision(projectId, key, pref.action, 'workflow')
|
|
72
|
+
break
|
|
73
|
+
case 'session':
|
|
74
|
+
sessionPreferences.set(key, pref)
|
|
75
|
+
break
|
|
76
|
+
case 'once':
|
|
77
|
+
oncePreferences.set(key, pref)
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get workflow preferences for a command
|
|
84
|
+
* Combines permanent + session + once preferences
|
|
85
|
+
*/
|
|
86
|
+
export async function getWorkflowPreferences(
|
|
87
|
+
projectId: string,
|
|
88
|
+
command: HookCommand
|
|
89
|
+
): Promise<{
|
|
90
|
+
before?: string
|
|
91
|
+
after?: string
|
|
92
|
+
skip?: boolean
|
|
93
|
+
}> {
|
|
94
|
+
const result: {
|
|
95
|
+
before?: string
|
|
96
|
+
after?: string
|
|
97
|
+
skip?: boolean
|
|
98
|
+
} = {}
|
|
99
|
+
|
|
100
|
+
// Check each phase
|
|
101
|
+
for (const phase of ['before', 'after', 'skip'] as const) {
|
|
102
|
+
const key = prefKey(phase, command)
|
|
103
|
+
|
|
104
|
+
// Check once first (highest priority)
|
|
105
|
+
const once = oncePreferences.get(key)
|
|
106
|
+
if (once) {
|
|
107
|
+
if (phase === 'skip') {
|
|
108
|
+
result.skip = once.action === 'true'
|
|
109
|
+
} else {
|
|
110
|
+
result[phase] = once.action
|
|
111
|
+
}
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check session
|
|
116
|
+
const session = sessionPreferences.get(key)
|
|
117
|
+
if (session) {
|
|
118
|
+
if (phase === 'skip') {
|
|
119
|
+
result.skip = session.action === 'true'
|
|
120
|
+
} else {
|
|
121
|
+
result[phase] = session.action
|
|
122
|
+
}
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check permanent (via memory system)
|
|
127
|
+
const permanent = await memorySystem.getSmartDecision(projectId, key)
|
|
128
|
+
if (permanent) {
|
|
129
|
+
if (phase === 'skip') {
|
|
130
|
+
result.skip = permanent === 'true'
|
|
131
|
+
} else {
|
|
132
|
+
result[phase] = permanent
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run workflow hooks for a command
|
|
142
|
+
* Consumes 'once' preferences after use
|
|
143
|
+
*/
|
|
144
|
+
export async function runWorkflowHooks(
|
|
145
|
+
projectId: string,
|
|
146
|
+
phase: 'before' | 'after',
|
|
147
|
+
command: HookCommand,
|
|
148
|
+
options: { projectPath?: string; skipHooks?: boolean } = {}
|
|
149
|
+
): Promise<HookResult> {
|
|
150
|
+
if (options.skipHooks) {
|
|
151
|
+
return { success: true }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const prefs = await getWorkflowPreferences(projectId, command)
|
|
155
|
+
|
|
156
|
+
// Check if this step should be skipped
|
|
157
|
+
if (prefs.skip) {
|
|
158
|
+
return { success: true, skipped: [command] }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const action = prefs[phase]
|
|
162
|
+
if (!action) {
|
|
163
|
+
return { success: true }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Consume 'once' preference if it exists
|
|
167
|
+
const key = prefKey(phase, command)
|
|
168
|
+
if (oncePreferences.has(key)) {
|
|
169
|
+
oncePreferences.delete(key)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log(`\n${DIM}Running ${phase}-${command}: ${action}${RESET}`)
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const startTime = Date.now()
|
|
176
|
+
await execAsync(action, {
|
|
177
|
+
timeout: 60000,
|
|
178
|
+
cwd: options.projectPath || process.cwd(),
|
|
179
|
+
env: { ...process.env },
|
|
180
|
+
})
|
|
181
|
+
const elapsed = Date.now() - startTime
|
|
182
|
+
const timeStr = elapsed > 1000 ? `${(elapsed / 1000).toFixed(1)}s` : `${elapsed}ms`
|
|
183
|
+
console.log(`${GREEN}✓${RESET} ${DIM}(${timeStr})${RESET}`)
|
|
184
|
+
return { success: true }
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.log(`${RED}✗ failed${RESET}`)
|
|
187
|
+
const errorMessage = (error as Error).message || 'Unknown error'
|
|
188
|
+
console.log(`${DIM}${errorMessage.split('\n')[0]}${RESET}`)
|
|
189
|
+
return { success: false, failed: action, output: errorMessage }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* List all workflow preferences for a project
|
|
195
|
+
*/
|
|
196
|
+
export async function listWorkflowPreferences(projectId: string): Promise<
|
|
197
|
+
Array<{
|
|
198
|
+
key: string
|
|
199
|
+
action: string
|
|
200
|
+
scope: PreferenceScope
|
|
201
|
+
}>
|
|
202
|
+
> {
|
|
203
|
+
const results: Array<{
|
|
204
|
+
key: string
|
|
205
|
+
action: string
|
|
206
|
+
scope: PreferenceScope
|
|
207
|
+
}> = []
|
|
208
|
+
|
|
209
|
+
const commands: HookCommand[] = ['task', 'done', 'ship', 'sync']
|
|
210
|
+
const phases: HookPhase[] = ['before', 'after', 'skip']
|
|
211
|
+
|
|
212
|
+
for (const command of commands) {
|
|
213
|
+
for (const phase of phases) {
|
|
214
|
+
const key = prefKey(phase, command)
|
|
215
|
+
|
|
216
|
+
// Check once
|
|
217
|
+
const once = oncePreferences.get(key)
|
|
218
|
+
if (once) {
|
|
219
|
+
results.push({ key: `${phase} ${command}`, action: once.action, scope: 'once' })
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check session
|
|
224
|
+
const session = sessionPreferences.get(key)
|
|
225
|
+
if (session) {
|
|
226
|
+
results.push({ key: `${phase} ${command}`, action: session.action, scope: 'session' })
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check permanent
|
|
231
|
+
const permanent = await memorySystem.getSmartDecision(projectId, key)
|
|
232
|
+
if (permanent) {
|
|
233
|
+
results.push({ key: `${phase} ${command}`, action: permanent, scope: 'permanent' })
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return results
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Remove a workflow preference
|
|
243
|
+
*/
|
|
244
|
+
export async function removeWorkflowPreference(
|
|
245
|
+
projectId: string,
|
|
246
|
+
hook: HookPhase,
|
|
247
|
+
command: HookCommand
|
|
248
|
+
): Promise<boolean> {
|
|
249
|
+
const key = prefKey(hook, command)
|
|
250
|
+
|
|
251
|
+
// Remove from all scopes
|
|
252
|
+
oncePreferences.delete(key)
|
|
253
|
+
sessionPreferences.delete(key)
|
|
254
|
+
|
|
255
|
+
// For permanent, we record an empty value
|
|
256
|
+
// (the memory system will treat low-confidence empty values as non-existent)
|
|
257
|
+
await memorySystem.recordDecision(projectId, key, '', 'workflow:remove')
|
|
258
|
+
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Format workflow preferences for display
|
|
264
|
+
*/
|
|
265
|
+
export function formatWorkflowPreferences(
|
|
266
|
+
preferences: Array<{
|
|
267
|
+
key: string
|
|
268
|
+
action: string
|
|
269
|
+
scope: PreferenceScope
|
|
270
|
+
}>
|
|
271
|
+
): string {
|
|
272
|
+
if (preferences.length === 0) {
|
|
273
|
+
return `${DIM}No workflow preferences configured.${RESET}\n\nSet one: "p. workflow antes de ship corre los tests"`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lines: string[] = ['', 'WORKFLOW PREFERENCES', '────────────────────────────']
|
|
277
|
+
|
|
278
|
+
for (const pref of preferences) {
|
|
279
|
+
const scopeBadge =
|
|
280
|
+
pref.scope === 'permanent'
|
|
281
|
+
? `${GREEN}permanent${RESET}`
|
|
282
|
+
: pref.scope === 'session'
|
|
283
|
+
? `${YELLOW}session${RESET}`
|
|
284
|
+
: `${DIM}once${RESET}`
|
|
285
|
+
|
|
286
|
+
lines.push(` [${scopeBadge}] ${pref.key.padEnd(15)} → ${pref.action}`)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
lines.push('')
|
|
290
|
+
lines.push(`${DIM}Modify: "p. workflow antes de ship corre npm test"${RESET}`)
|
|
291
|
+
lines.push(`${DIM}Remove: "p. workflow quita el hook de ship"${RESET}`)
|
|
292
|
+
|
|
293
|
+
return lines.join('\n')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Clear all session preferences (for testing)
|
|
298
|
+
*/
|
|
299
|
+
export function clearSessionPreferences(): void {
|
|
300
|
+
sessionPreferences.clear()
|
|
301
|
+
oncePreferences.clear()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export default {
|
|
305
|
+
setWorkflowPreference,
|
|
306
|
+
getWorkflowPreferences,
|
|
307
|
+
runWorkflowHooks,
|
|
308
|
+
listWorkflowPreferences,
|
|
309
|
+
removeWorkflowPreference,
|
|
310
|
+
formatWorkflowPreferences,
|
|
311
|
+
clearSessionPreferences,
|
|
312
|
+
}
|