prjct-cli 0.45.5 → 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 CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.47.0] - 2026-01-30
4
+
5
+ ### Features
6
+
7
+ - Complete and improve help text documentation - PRJ-133 (#73)
8
+ - Workflow hooks via natural language - PRJ-137 (#72)
9
+ - Subtask progress dashboard with domain colors - PRJ-138 (#71)
10
+
11
+
12
+ ## [0.48.1] - 2026-01-30
13
+
14
+ ### Improved
15
+
16
+ - **Complete and improve help text documentation** (PRJ-133)
17
+ - New structured help system with `prjct help <command>` support
18
+ - Per-command help with usage, parameters, and features
19
+ - Commands grouped by category with `prjct help commands`
20
+ - Clean visual formatting with Quick Start, Terminal Commands, AI Agent Commands sections
21
+
22
+
23
+ ## [0.48.0] - 2026-01-29
24
+
25
+ ### Added
26
+
27
+ - **Workflow hooks via natural language** (PRJ-137)
28
+ - Configure hooks with `p. workflow "antes de ship corre los tests"`
29
+ - Supports before/after hooks for task, done, ship, sync commands
30
+ - Three scopes: permanent (persisted), session, one-time
31
+ - Uses existing memory system for storage
32
+ - No JSON config needed - just talk to the LLM
33
+
34
+
35
+ ## [0.47.0] - 2026-01-29
36
+
37
+ ### Added
38
+
39
+ - **Subtask progress dashboard with domain-specific colors** (PRJ-138)
40
+ - Each domain (frontend, backend, database, etc.) displays in unique color
41
+ - Hash-based color assignment ensures consistency across sessions
42
+ - Integrated into command execution flow for visual task tracking
43
+
44
+
45
+ ## [0.46.0] - 2026-01-30
46
+
47
+ ### Features
48
+
49
+ - preserve user customizations during sync - PRJ-115 (#70)
50
+
51
+
52
+ ## [0.46.0] - 2026-01-30
53
+
54
+ ### Added
55
+
56
+ - **Preserve user customizations during sync** (PRJ-115)
57
+ - Wrap custom content in `<!-- prjct:preserve -->` markers
58
+ - Content survives `p. sync` regeneration
59
+ - Supports named sections: `<!-- prjct:preserve:my-rules -->`
60
+ - Works with CLAUDE.md, .cursorrules, and all context files
61
+
62
+
3
63
  ## [0.45.5] - 2026-01-30
4
64
 
5
65
  ### Bug Fixes
package/bin/prjct.ts CHANGED
@@ -184,53 +184,9 @@ if (args[0] === 'start' || args[0] === 'setup') {
184
184
  }
185
185
  } else if (args[0] === 'help' || args[0] === '-h' || args[0] === '--help') {
186
186
  // Show help - bypass setup check to always show help
187
- console.log(`
188
- prjct - Context layer for AI coding agents
189
- Works with Claude Code, Gemini CLI, Antigravity, Cursor IDE, and more.
190
-
191
- QUICK START
192
- -----------
193
- Claude/Gemini:
194
- 1. prjct start Configure your AI provider
195
- 2. cd my-project && prjct init
196
- 3. Open in Claude Code or Gemini CLI
197
- 4. Type: p. sync Analyze project
198
-
199
- Cursor IDE:
200
- 1. cd my-project && prjct init
201
- 2. Open in Cursor
202
- 3. Type: /sync Analyze project
203
-
204
- COMMANDS (inside your AI agent)
205
- -------------------------------
206
- Claude/Gemini Cursor Description
207
- ─────────────────────────────────────────────────────
208
- p. sync /sync Analyze project
209
- p. task "desc" /task "desc" Start a task
210
- p. done /done Complete subtask
211
- p. ship "name" /ship "name" Ship with PR
212
-
213
- TERMINAL COMMANDS (this CLI)
214
- ----------------------------
215
- prjct start First-time setup (Claude/Gemini global config)
216
- prjct init Initialize project (required for Cursor)
217
- prjct setup Reconfigure installations
218
- prjct sync Sync project state
219
- prjct watch Auto-sync on file changes (Ctrl+C to stop)
220
- prjct doctor Check system health and dependencies
221
- prjct uninstall Complete system removal of prjct
222
-
223
- FLAGS
224
- -----
225
- --quiet, -q Suppress all output (only errors to stderr)
226
- --version, -v Show version
227
- --help, -h Show this help
228
-
229
- MORE INFO
230
- ---------
231
- Documentation: https://prjct.app
232
- GitHub: https://github.com/jlopezlira/prjct-cli
233
- `)
187
+ const { getHelp } = await import('../core/utils/help')
188
+ const topic = args[1] // Optional: prjct help <command>
189
+ console.log(getHelp(topic))
234
190
  process.exitCode = 0
235
191
  } else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
236
192
  // Show version with provider status
@@ -0,0 +1,216 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import {
3
+ extractPreservedContent,
4
+ extractPreservedSections,
5
+ hasPreservedSections,
6
+ mergePreservedSections,
7
+ stripPreservedSections,
8
+ validatePreserveBlocks,
9
+ wrapInPreserveMarkers,
10
+ } from '../../utils/preserve-sections'
11
+
12
+ describe('preserve-sections', () => {
13
+ describe('extractPreservedSections', () => {
14
+ it('should extract a single preserved section', () => {
15
+ const content = `# Header
16
+
17
+ <!-- prjct:preserve -->
18
+ My custom content
19
+ <!-- /prjct:preserve -->
20
+
21
+ # Footer`
22
+
23
+ const sections = extractPreservedSections(content)
24
+ expect(sections).toHaveLength(1)
25
+ expect(sections[0].content).toContain('My custom content')
26
+ })
27
+
28
+ it('should extract multiple preserved sections', () => {
29
+ const content = `# Header
30
+
31
+ <!-- prjct:preserve -->
32
+ First section
33
+ <!-- /prjct:preserve -->
34
+
35
+ Some content
36
+
37
+ <!-- prjct:preserve -->
38
+ Second section
39
+ <!-- /prjct:preserve -->`
40
+
41
+ const sections = extractPreservedSections(content)
42
+ expect(sections).toHaveLength(2)
43
+ expect(sections[0].content).toContain('First section')
44
+ expect(sections[1].content).toContain('Second section')
45
+ })
46
+
47
+ it('should handle named sections', () => {
48
+ const content = `<!-- prjct:preserve:custom-rules -->
49
+ My rules
50
+ <!-- /prjct:preserve -->`
51
+
52
+ const sections = extractPreservedSections(content)
53
+ expect(sections).toHaveLength(1)
54
+ expect(sections[0].id).toBe('custom-rules')
55
+ })
56
+
57
+ it('should return empty array when no preserved sections', () => {
58
+ const content = '# Just normal content'
59
+ const sections = extractPreservedSections(content)
60
+ expect(sections).toHaveLength(0)
61
+ })
62
+
63
+ it('should ignore unclosed preserve blocks', () => {
64
+ const content = `<!-- prjct:preserve -->
65
+ No closing tag here`
66
+
67
+ const sections = extractPreservedSections(content)
68
+ expect(sections).toHaveLength(0)
69
+ })
70
+ })
71
+
72
+ describe('extractPreservedContent', () => {
73
+ it('should extract inner content without markers', () => {
74
+ const content = `<!-- prjct:preserve -->
75
+ My content
76
+ <!-- /prjct:preserve -->`
77
+
78
+ const inner = extractPreservedContent(content)
79
+ expect(inner).toHaveLength(1)
80
+ expect(inner[0]).toBe('My content')
81
+ })
82
+ })
83
+
84
+ describe('hasPreservedSections', () => {
85
+ it('should return true when preserved sections exist', () => {
86
+ const content = '<!-- prjct:preserve -->content<!-- /prjct:preserve -->'
87
+ expect(hasPreservedSections(content)).toBe(true)
88
+ })
89
+
90
+ it('should return false when no preserved sections', () => {
91
+ const content = '# Normal markdown'
92
+ expect(hasPreservedSections(content)).toBe(false)
93
+ })
94
+ })
95
+
96
+ describe('mergePreservedSections', () => {
97
+ it('should append preserved sections to new content', () => {
98
+ const oldContent = `# Old Header
99
+
100
+ <!-- prjct:preserve -->
101
+ # My Rules
102
+ - Use tabs
103
+ <!-- /prjct:preserve -->`
104
+
105
+ const newContent = `# New Generated Content
106
+
107
+ This is fresh.`
108
+
109
+ const merged = mergePreservedSections(newContent, oldContent)
110
+
111
+ expect(merged).toContain('# New Generated Content')
112
+ expect(merged).toContain('My Rules')
113
+ expect(merged).toContain('Use tabs')
114
+ expect(merged).toContain('prjct:preserve')
115
+ })
116
+
117
+ it('should return new content unchanged when no preserved sections', () => {
118
+ const oldContent = '# Old content without preserve markers'
119
+ const newContent = '# New content'
120
+
121
+ const merged = mergePreservedSections(newContent, oldContent)
122
+ expect(merged).toBe(newContent)
123
+ })
124
+
125
+ it('should preserve multiple sections', () => {
126
+ const oldContent = `<!-- prjct:preserve -->
127
+ Section 1
128
+ <!-- /prjct:preserve -->
129
+
130
+ <!-- prjct:preserve -->
131
+ Section 2
132
+ <!-- /prjct:preserve -->`
133
+
134
+ const newContent = '# New'
135
+
136
+ const merged = mergePreservedSections(newContent, oldContent)
137
+ expect(merged).toContain('Section 1')
138
+ expect(merged).toContain('Section 2')
139
+ })
140
+ })
141
+
142
+ describe('wrapInPreserveMarkers', () => {
143
+ it('should wrap content with default markers', () => {
144
+ const content = 'My content'
145
+ const wrapped = wrapInPreserveMarkers(content)
146
+
147
+ expect(wrapped).toContain('<!-- prjct:preserve -->')
148
+ expect(wrapped).toContain('My content')
149
+ expect(wrapped).toContain('<!-- /prjct:preserve -->')
150
+ })
151
+
152
+ it('should wrap content with named markers', () => {
153
+ const content = 'My content'
154
+ const wrapped = wrapInPreserveMarkers(content, 'custom')
155
+
156
+ expect(wrapped).toContain('<!-- prjct:preserve:custom -->')
157
+ })
158
+ })
159
+
160
+ describe('stripPreservedSections', () => {
161
+ it('should remove preserved sections from content', () => {
162
+ const content = `# Header
163
+
164
+ <!-- prjct:preserve -->
165
+ Custom stuff
166
+ <!-- /prjct:preserve -->
167
+
168
+ # Footer`
169
+
170
+ const stripped = stripPreservedSections(content)
171
+ expect(stripped).toContain('# Header')
172
+ expect(stripped).toContain('# Footer')
173
+ expect(stripped).not.toContain('Custom stuff')
174
+ expect(stripped).not.toContain('prjct:preserve')
175
+ })
176
+
177
+ it('should return content unchanged when no preserved sections', () => {
178
+ const content = '# Normal content'
179
+ const stripped = stripPreservedSections(content)
180
+ expect(stripped).toBe(content)
181
+ })
182
+ })
183
+
184
+ describe('validatePreserveBlocks', () => {
185
+ it('should validate correct blocks', () => {
186
+ const content = `<!-- prjct:preserve -->
187
+ Content
188
+ <!-- /prjct:preserve -->`
189
+
190
+ const result = validatePreserveBlocks(content)
191
+ expect(result.valid).toBe(true)
192
+ expect(result.errors).toHaveLength(0)
193
+ })
194
+
195
+ it('should detect mismatched markers', () => {
196
+ const content = `<!-- prjct:preserve -->
197
+ Content without closing`
198
+
199
+ const result = validatePreserveBlocks(content)
200
+ expect(result.valid).toBe(false)
201
+ expect(result.errors.length).toBeGreaterThan(0)
202
+ })
203
+
204
+ it('should detect nested blocks', () => {
205
+ const content = `<!-- prjct:preserve -->
206
+ <!-- prjct:preserve -->
207
+ Nested
208
+ <!-- /prjct:preserve -->
209
+ <!-- /prjct:preserve -->`
210
+
211
+ const result = validatePreserveBlocks(content)
212
+ expect(result.valid).toBe(false)
213
+ expect(result.errors.some((e) => e.includes('Nested'))).toBe(true)
214
+ })
215
+ })
216
+ })
@@ -18,6 +18,7 @@ import type {
18
18
  SimpleExecutionResult,
19
19
  } from '../types'
20
20
  import { agentStream } from '../utils/agent-stream'
21
+ import { printSubtaskProgress, type SubtaskDisplay } from '../utils/subtask-table'
21
22
  import chainOfThought from './chain-of-thought'
22
23
  import contextBuilder from './context-builder'
23
24
  import groundTruth from './ground-truth'
@@ -192,7 +193,13 @@ export class CommandExecutor {
192
193
 
193
194
  // Show subtasks if fragmented
194
195
  if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
195
- agentStream.status('📋', `${orchestratorContext.subtasks.length} subtasks planned`)
196
+ const subtaskDisplay: SubtaskDisplay[] = orchestratorContext.subtasks.map((s) => ({
197
+ id: s.id,
198
+ domain: s.domain,
199
+ description: s.description,
200
+ status: s.status,
201
+ }))
202
+ printSubtaskProgress(subtaskDisplay)
196
203
  }
197
204
  } catch (error) {
198
205
  // Orchestration failed - log warning but continue without it
@@ -7,6 +7,7 @@
7
7
 
8
8
  import fs from 'node:fs/promises'
9
9
  import path from 'node:path'
10
+ import { mergePreservedSections } from '../utils/preserve-sections'
10
11
  import { getFormatter, type ProjectContext } from './formatters'
11
12
  import { AI_TOOLS, type AIToolConfig, DEFAULT_AI_TOOLS, getAIToolConfig } from './registry'
12
13
 
@@ -71,7 +72,7 @@ async function generateForTool(
71
72
 
72
73
  try {
73
74
  // Generate content
74
- const content = formatter(context, config)
75
+ let content = formatter(context, config)
75
76
 
76
77
  // Determine output path
77
78
  let outputPath: string
@@ -84,6 +85,14 @@ async function generateForTool(
84
85
  // Ensure directory exists
85
86
  await fs.mkdir(path.dirname(outputPath), { recursive: true })
86
87
 
88
+ // Read existing file to preserve user customizations
89
+ try {
90
+ const existingContent = await fs.readFile(outputPath, 'utf-8')
91
+ content = mergePreservedSections(content, existingContent)
92
+ } catch {
93
+ // File doesn't exist yet - use generated content as-is
94
+ }
95
+
87
96
  // Write file
88
97
  await fs.writeFile(outputPath, content, 'utf-8')
89
98
 
@@ -285,6 +285,22 @@ export const COMMANDS: CommandMeta[] = [
285
285
  requiresProject: true,
286
286
  isOptional: true,
287
287
  },
288
+ {
289
+ name: 'workflow',
290
+ group: 'optional',
291
+ description: 'Configure workflow hooks via natural language',
292
+ usage: { claude: '/p:workflow ["config"]', terminal: 'prjct workflow ["config"]' },
293
+ params: '["natural language config"]',
294
+ implemented: true,
295
+ hasTemplate: true,
296
+ requiresProject: true,
297
+ isOptional: true,
298
+ features: [
299
+ 'Natural language configuration',
300
+ 'Before/after hooks for task, done, ship, sync',
301
+ 'Permanent, session, or one-time preferences',
302
+ ],
303
+ },
288
304
 
289
305
  // ===== SETUP COMMANDS =====
290
306
  {
@@ -90,6 +90,13 @@ class PrjctCommands {
90
90
  return this.workflow.resume(taskId, projectPath)
91
91
  }
92
92
 
93
+ async workflowPrefs(
94
+ input: string | null = null,
95
+ projectPath: string = process.cwd()
96
+ ): Promise<CommandResult> {
97
+ return this.workflow.workflow(input, projectPath)
98
+ }
99
+
93
100
  // ========== Planning Commands ==========
94
101
 
95
102
  async init(
@@ -58,6 +58,7 @@ export function registerAllCommands(): void {
58
58
  commandRegistry.registerMethod('next', workflow, 'next', getMeta('next'))
59
59
  commandRegistry.registerMethod('pause', workflow, 'pause', getMeta('pause'))
60
60
  commandRegistry.registerMethod('resume', workflow, 'resume', getMeta('resume'))
61
+ commandRegistry.registerMethod('workflow', workflow, 'workflow', getMeta('workflow'))
61
62
 
62
63
  // Planning commands
63
64
  commandRegistry.registerMethod('init', planning, 'init', getMeta('init'))
@@ -50,9 +50,9 @@ export class SetupCommands extends PrjctCommandsBase {
50
50
 
51
51
  if ((result.errors?.length ?? 0) > 0) {
52
52
  console.log(`\n⚠️ ${result.errors?.length ?? 0} errors:`)
53
- result.errors?.forEach((e: { file: string; error: string }) =>
53
+ for (const e of result.errors ?? []) {
54
54
  console.log(` - ${e.file}: ${e.error}`)
55
- )
55
+ }
56
56
  }
57
57
 
58
58
  console.log('\n🎉 Setup complete!')
@@ -92,9 +92,9 @@ export class SetupCommands extends PrjctCommandsBase {
92
92
 
93
93
  if ((result.errors?.length ?? 0) > 0) {
94
94
  console.log(`\n⚠️ ${result.errors?.length ?? 0} errors:`)
95
- result.errors?.forEach((e: { file: string; error: string }) =>
95
+ for (const e of result.errors ?? []) {
96
96
  console.log(` - ${e.file}: ${e.error}`)
97
- )
97
+ }
98
98
  }
99
99
 
100
100
  console.log('\n📝 Installing global configuration...')
@@ -10,6 +10,7 @@ import type { CommandResult } from '../types'
10
10
  import { isNotFoundError } from '../types/fs'
11
11
  import { showNextSteps } from '../utils/next-steps'
12
12
  import { detectProjectCommands } from '../utils/project-commands'
13
+ import { runWorkflowHooks } from '../workflow/workflow-preferences'
13
14
  import { configManager, dateHelper, fileHelper, out, PrjctCommandsBase, toolRegistry } from './base'
14
15
 
15
16
  export class ShippingCommands extends PrjctCommandsBase {
@@ -48,13 +49,20 @@ export class ShippingCommands extends PrjctCommandsBase {
48
49
  /**
49
50
  * /p:ship - Ship feature with complete automated workflow
50
51
  */
51
- async ship(feature: string | null, projectPath: string = process.cwd()): Promise<CommandResult> {
52
+ async ship(
53
+ feature: string | null,
54
+ projectPath: string = process.cwd(),
55
+ options: { skipHooks?: boolean } = {}
56
+ ): Promise<CommandResult> {
52
57
  try {
53
58
  const initResult = await this.ensureProjectInit(projectPath)
54
59
  if (!initResult.success) return initResult
55
60
 
56
- const config = await configManager.readConfig(projectPath)
57
- const projectId = config!.projectId
61
+ const projectId = await configManager.getProjectId(projectPath)
62
+ if (!projectId) {
63
+ out.fail('no project ID')
64
+ return { success: false, error: 'No project ID found' }
65
+ }
58
66
 
59
67
  let featureName = feature
60
68
  if (!featureName) {
@@ -63,6 +71,15 @@ export class ShippingCommands extends PrjctCommandsBase {
63
71
  featureName = currentTask?.description || 'current work'
64
72
  }
65
73
 
74
+ // Run before_ship hooks (using memory-based preferences)
75
+ const beforeResult = await runWorkflowHooks(projectId, 'before', 'ship', {
76
+ projectPath,
77
+ skipHooks: options.skipHooks,
78
+ })
79
+ if (!beforeResult.success) {
80
+ return { success: false, error: `Hook failed: ${beforeResult.failed}` }
81
+ }
82
+
66
83
  // Ship steps with progress indicator
67
84
  out.step(1, 5, `Linting ${featureName}...`)
68
85
  const lintResult = await this._runLint(projectPath)
@@ -108,6 +125,12 @@ export class ShippingCommands extends PrjctCommandsBase {
108
125
  })
109
126
  }
110
127
 
128
+ // Run after_ship hooks
129
+ await runWorkflowHooks(projectId, 'after', 'ship', {
130
+ projectPath,
131
+ skipHooks: options.skipHooks,
132
+ })
133
+
111
134
  out.done(`v${newVersion} shipped`)
112
135
  showNextSteps('ship')
113
136
 
@@ -16,6 +16,16 @@ import { queueStorage, stateStorage } from '../storage'
16
16
  import type { CommandResult } from '../types'
17
17
  import { showNextSteps, showStateInfo } from '../utils/next-steps'
18
18
  import { getLinearApiKey, getProjectCredentials } from '../utils/project-credentials'
19
+ import {
20
+ formatWorkflowPreferences,
21
+ type HookCommand,
22
+ type HookPhase,
23
+ listWorkflowPreferences,
24
+ type PreferenceScope,
25
+ removeWorkflowPreference,
26
+ runWorkflowHooks,
27
+ setWorkflowPreference,
28
+ } from '../workflow/workflow-preferences'
19
29
  import { configManager, dateHelper, out, PrjctCommandsBase } from './base'
20
30
 
21
31
  export class WorkflowCommands extends PrjctCommandsBase {
@@ -24,7 +34,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
24
34
  */
25
35
  async now(
26
36
  task: string | null = null,
27
- projectPath: string = process.cwd()
37
+ projectPath: string = process.cwd(),
38
+ options: { skipHooks?: boolean } = {}
28
39
  ): Promise<CommandResult> {
29
40
  try {
30
41
  const initResult = await this.ensureProjectInit(projectPath)
@@ -37,6 +48,15 @@ export class WorkflowCommands extends PrjctCommandsBase {
37
48
  }
38
49
 
39
50
  if (task) {
51
+ // Run before_task hooks (using memory-based preferences)
52
+ const beforeResult = await runWorkflowHooks(projectId, 'before', 'task', {
53
+ projectPath,
54
+ skipHooks: options.skipHooks,
55
+ })
56
+ if (!beforeResult.success) {
57
+ return { success: false, error: `Hook failed: ${beforeResult.failed}` }
58
+ }
59
+
40
60
  // AGENTIC: Use CommandExecutor for full orchestration support
41
61
  const result = await commandExecutor.execute('task', { task }, projectPath)
42
62
 
@@ -97,6 +117,12 @@ export class WorkflowCommands extends PrjctCommandsBase {
97
117
  timestamp: dateHelper.getTimestamp(),
98
118
  })
99
119
 
120
+ // Run after_task hooks
121
+ await runWorkflowHooks(projectId, 'after', 'task', {
122
+ projectPath,
123
+ skipHooks: options.skipHooks,
124
+ })
125
+
100
126
  return {
101
127
  // Include full CommandExecutor result first (orchestratorContext, prompt, etc.)
102
128
  ...result,
@@ -127,7 +153,10 @@ export class WorkflowCommands extends PrjctCommandsBase {
127
153
  /**
128
154
  * /p:done - Complete current task
129
155
  */
130
- async done(projectPath: string = process.cwd()): Promise<CommandResult> {
156
+ async done(
157
+ projectPath: string = process.cwd(),
158
+ options: { skipHooks?: boolean } = {}
159
+ ): Promise<CommandResult> {
131
160
  try {
132
161
  const initResult = await this.ensureProjectInit(projectPath)
133
162
  if (!initResult.success) return initResult
@@ -146,6 +175,15 @@ export class WorkflowCommands extends PrjctCommandsBase {
146
175
  return { success: true, message: 'No active task to complete' }
147
176
  }
148
177
 
178
+ // Run before_done hooks (using memory-based preferences)
179
+ const beforeResult = await runWorkflowHooks(projectId, 'before', 'done', {
180
+ projectPath,
181
+ skipHooks: options.skipHooks,
182
+ })
183
+ if (!beforeResult.success) {
184
+ return { success: false, error: `Hook failed: ${beforeResult.failed}` }
185
+ }
186
+
149
187
  const task = currentTask.description
150
188
  let duration = ''
151
189
  if (currentTask.startedAt) {
@@ -184,6 +222,13 @@ export class WorkflowCommands extends PrjctCommandsBase {
184
222
  duration,
185
223
  timestamp: dateHelper.getTimestamp(),
186
224
  })
225
+
226
+ // Run after_done hooks
227
+ await runWorkflowHooks(projectId, 'after', 'done', {
228
+ projectPath,
229
+ skipHooks: options.skipHooks,
230
+ })
231
+
187
232
  return { success: true, task, duration }
188
233
  } catch (error) {
189
234
  out.fail((error as Error).message)
@@ -312,4 +357,62 @@ export class WorkflowCommands extends PrjctCommandsBase {
312
357
  return { success: false, error: (error as Error).message }
313
358
  }
314
359
  }
360
+
361
+ /**
362
+ * /p:workflow - View and manage workflow preferences
363
+ *
364
+ * When called without arguments, shows current preferences.
365
+ * With arguments, parses natural language and updates preferences.
366
+ */
367
+ async workflow(
368
+ input: string | null = null,
369
+ projectPath: string = process.cwd()
370
+ ): Promise<CommandResult> {
371
+ try {
372
+ const initResult = await this.ensureProjectInit(projectPath)
373
+ if (!initResult.success) return initResult
374
+
375
+ const projectId = await configManager.getProjectId(projectPath)
376
+ if (!projectId) {
377
+ out.fail('no project ID')
378
+ return { success: false, error: 'No project ID found' }
379
+ }
380
+
381
+ if (!input) {
382
+ // Show current preferences
383
+ const preferences = await listWorkflowPreferences(projectId)
384
+ console.log(formatWorkflowPreferences(preferences))
385
+ return { success: true, preferences }
386
+ }
387
+
388
+ // Return info for template-based processing
389
+ // The template/LLM will parse the natural language and call the appropriate functions
390
+ return {
391
+ success: true,
392
+ projectId,
393
+ input,
394
+ // Export functions for template use
395
+ setWorkflowPreference: async (pref: {
396
+ hook: HookPhase
397
+ command: HookCommand
398
+ action: string
399
+ scope: PreferenceScope
400
+ }) => {
401
+ await setWorkflowPreference(projectId, {
402
+ ...pref,
403
+ createdAt: dateHelper.getTimestamp(),
404
+ })
405
+ },
406
+ removeWorkflowPreference: async (hook: HookPhase, command: HookCommand) => {
407
+ await removeWorkflowPreference(projectId, hook, command)
408
+ },
409
+ listWorkflowPreferences: async () => {
410
+ return listWorkflowPreferences(projectId)
411
+ },
412
+ }
413
+ } catch (error) {
414
+ out.fail((error as Error).message)
415
+ return { success: false, error: (error as Error).message }
416
+ }
417
+ }
315
418
  }