prjct-cli 0.20.0 → 0.20.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 (226) hide show
  1. package/CHANGELOG.md +24 -6
  2. package/CLAUDE.md +56 -15
  3. package/README.md +5 -6
  4. package/bin/prjct +59 -42
  5. package/bin/prjct.ts +60 -0
  6. package/core/__tests__/agentic/memory-system.test.ts +18 -3
  7. package/core/__tests__/agentic/plan-mode.test.ts +55 -26
  8. package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
  9. package/core/__tests__/utils/project-commands.test.ts +72 -0
  10. package/core/agentic/agent-router.ts +3 -12
  11. package/core/agentic/command-executor.ts +372 -3
  12. package/core/agentic/context-builder.ts +7 -27
  13. package/core/agentic/ground-truth.ts +604 -5
  14. package/core/agentic/index.ts +180 -0
  15. package/core/agentic/loop-detector.ts +418 -4
  16. package/core/agentic/memory-system.ts +857 -3
  17. package/core/agentic/plan-mode.ts +491 -4
  18. package/core/agentic/prompt-builder.ts +44 -65
  19. package/core/agentic/services.ts +13 -5
  20. package/core/agentic/skill-loader.ts +112 -0
  21. package/core/agentic/smart-context.ts +37 -122
  22. package/core/agentic/template-loader.ts +79 -122
  23. package/core/agentic/tool-registry.ts +5 -11
  24. package/core/agents/index.ts +1 -1
  25. package/core/agents/performance.ts +4 -2
  26. package/core/bus/bus.ts +262 -0
  27. package/core/bus/index.ts +3 -313
  28. package/core/commands/analysis.ts +5 -5
  29. package/core/commands/analytics.ts +11 -11
  30. package/core/commands/base.ts +33 -209
  31. package/core/commands/cleanup.ts +148 -0
  32. package/core/commands/command-data.ts +346 -0
  33. package/core/commands/commands.ts +216 -0
  34. package/core/commands/design.ts +83 -0
  35. package/core/commands/index.ts +13 -207
  36. package/core/commands/maintenance.ts +52 -473
  37. package/core/commands/planning.ts +3 -3
  38. package/core/commands/register.ts +104 -0
  39. package/core/commands/registry.ts +441 -0
  40. package/core/commands/setup.ts +25 -9
  41. package/core/commands/shipping.ts +48 -11
  42. package/core/commands/snapshots.ts +299 -0
  43. package/core/commands/workflow.ts +2 -2
  44. package/core/constants/index.ts +254 -4
  45. package/core/domain/agent-loader.ts +5 -6
  46. package/core/domain/task-stack.ts +555 -4
  47. package/core/errors.ts +127 -1
  48. package/core/events/events.ts +87 -0
  49. package/core/events/index.ts +4 -138
  50. package/core/index.ts +15 -23
  51. package/core/infrastructure/agent-detector.ts +126 -201
  52. package/core/infrastructure/author-detector.ts +99 -171
  53. package/core/infrastructure/command-installer.ts +476 -4
  54. package/core/infrastructure/config-manager.ts +41 -37
  55. package/core/infrastructure/path-manager.ts +59 -9
  56. package/core/infrastructure/permission-manager.ts +286 -0
  57. package/core/outcomes/analyzer.ts +7 -41
  58. package/core/outcomes/index.ts +1 -1
  59. package/core/outcomes/recorder.ts +1 -1
  60. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  61. package/core/plugin/loader.ts +5 -5
  62. package/core/plugin/registry.ts +2 -2
  63. package/core/schemas/ideas.ts +85 -54
  64. package/core/schemas/index.ts +14 -33
  65. package/core/schemas/permissions.ts +177 -0
  66. package/core/schemas/project.ts +39 -12
  67. package/core/schemas/roadmap.ts +94 -59
  68. package/core/schemas/schemas.ts +39 -0
  69. package/core/schemas/shipped.ts +87 -60
  70. package/core/schemas/state.ts +110 -70
  71. package/core/server/index.ts +21 -0
  72. package/core/server/routes.ts +165 -0
  73. package/core/server/server.ts +136 -0
  74. package/core/server/sse.ts +135 -0
  75. package/core/services/agent-service.ts +170 -0
  76. package/core/services/breakdown-service.ts +126 -0
  77. package/core/services/index.ts +21 -0
  78. package/core/services/memory-service.ts +108 -0
  79. package/core/services/project-service.ts +146 -0
  80. package/core/services/skill-service.ts +253 -0
  81. package/core/session/compaction.ts +257 -0
  82. package/core/session/index.ts +20 -8
  83. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  84. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  85. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  86. package/core/session/utils.ts +1 -1
  87. package/core/storage/ideas-storage.ts +10 -26
  88. package/core/storage/index.ts +14 -162
  89. package/core/storage/queue-storage.ts +13 -11
  90. package/core/storage/shipped-storage.ts +4 -17
  91. package/core/storage/state-storage.ts +35 -43
  92. package/core/storage/storage-manager.ts +42 -52
  93. package/core/storage/storage.ts +160 -0
  94. package/core/sync/auth-config.ts +1 -8
  95. package/core/sync/index.ts +17 -10
  96. package/core/sync/oauth-handler.ts +1 -6
  97. package/core/sync/sync-client.ts +6 -34
  98. package/core/sync/sync-manager.ts +11 -40
  99. package/core/types/agentic.ts +577 -0
  100. package/core/types/agents.ts +145 -0
  101. package/core/types/bus.ts +82 -0
  102. package/core/types/commands.ts +366 -0
  103. package/core/types/config.ts +66 -0
  104. package/core/types/core.ts +96 -0
  105. package/core/types/domain.ts +71 -0
  106. package/core/types/events.ts +42 -0
  107. package/core/types/fs.ts +56 -0
  108. package/core/types/index.ts +387 -500
  109. package/core/types/infrastructure.ts +196 -0
  110. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  111. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  112. package/core/types/plugin.ts +25 -0
  113. package/core/types/server.ts +54 -0
  114. package/core/types/services.ts +65 -0
  115. package/core/types/session.ts +135 -0
  116. package/core/types/storage.ts +148 -0
  117. package/core/types/sync.ts +121 -0
  118. package/core/types/task.ts +72 -0
  119. package/core/types/template.ts +24 -0
  120. package/core/types/utils.ts +90 -0
  121. package/core/utils/cache.ts +195 -0
  122. package/core/utils/collection-filters.ts +245 -0
  123. package/core/utils/date-helper.ts +1 -5
  124. package/core/utils/file-helper.ts +20 -10
  125. package/core/utils/jsonl-helper.ts +5 -8
  126. package/core/utils/markdown-builder.ts +277 -0
  127. package/core/utils/project-commands.ts +132 -0
  128. package/core/utils/runtime.ts +119 -0
  129. package/dist/bin/prjct.mjs +12568 -0
  130. package/package.json +13 -8
  131. package/scripts/build.js +106 -0
  132. package/scripts/postinstall.js +50 -8
  133. package/templates/agentic/subagent-generation.md +1 -1
  134. package/templates/commands/serve.md +118 -0
  135. package/templates/commands/ship.md +13 -2
  136. package/templates/commands/skill.md +110 -0
  137. package/templates/commands/sync.md +1 -1
  138. package/templates/commands/test.md +23 -4
  139. package/templates/permissions/default.jsonc +60 -0
  140. package/templates/permissions/permissive.jsonc +49 -0
  141. package/templates/permissions/strict.jsonc +62 -0
  142. package/templates/skills/code-review.md +47 -0
  143. package/templates/skills/debug.md +61 -0
  144. package/templates/skills/refactor.md +47 -0
  145. package/templates/subagents/domain/devops.md +1 -1
  146. package/templates/subagents/domain/testing.md +6 -10
  147. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  148. package/templates/tools/bash.txt +22 -0
  149. package/templates/tools/edit.txt +18 -0
  150. package/templates/tools/glob.txt +19 -0
  151. package/templates/tools/grep.txt +21 -0
  152. package/templates/tools/read.txt +14 -0
  153. package/templates/tools/task.txt +20 -0
  154. package/templates/tools/webfetch.txt +16 -0
  155. package/templates/tools/websearch.txt +18 -0
  156. package/templates/tools/write.txt +17 -0
  157. package/core/agentic/command-executor/command-executor.ts +0 -312
  158. package/core/agentic/command-executor/index.ts +0 -16
  159. package/core/agentic/command-executor/status-signal.ts +0 -38
  160. package/core/agentic/command-executor/types.ts +0 -79
  161. package/core/agentic/ground-truth/index.ts +0 -76
  162. package/core/agentic/ground-truth/types.ts +0 -33
  163. package/core/agentic/ground-truth/utils.ts +0 -48
  164. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  165. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  166. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  167. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  168. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  169. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  170. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  171. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  172. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  173. package/core/agentic/ground-truth/verifiers.ts +0 -6
  174. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  175. package/core/agentic/loop-detector/hallucination.ts +0 -71
  176. package/core/agentic/loop-detector/index.ts +0 -41
  177. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  178. package/core/agentic/loop-detector/types.ts +0 -66
  179. package/core/agentic/memory-system/history.ts +0 -53
  180. package/core/agentic/memory-system/index.ts +0 -192
  181. package/core/agentic/memory-system/patterns.ts +0 -156
  182. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  183. package/core/agentic/memory-system/session.ts +0 -21
  184. package/core/agentic/plan-mode/approval.ts +0 -57
  185. package/core/agentic/plan-mode/constants.ts +0 -44
  186. package/core/agentic/plan-mode/index.ts +0 -28
  187. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  188. package/core/agentic/plan-mode/types.ts +0 -193
  189. package/core/agents/types.ts +0 -126
  190. package/core/command-registry/categories.ts +0 -23
  191. package/core/command-registry/commands.ts +0 -15
  192. package/core/command-registry/core-commands.ts +0 -344
  193. package/core/command-registry/index.ts +0 -158
  194. package/core/command-registry/optional-commands.ts +0 -163
  195. package/core/command-registry/setup-commands.ts +0 -83
  196. package/core/command-registry/types.ts +0 -59
  197. package/core/command-registry.ts +0 -9
  198. package/core/commands/types.ts +0 -185
  199. package/core/commands.ts +0 -11
  200. package/core/constants/formats.ts +0 -187
  201. package/core/context-sync.ts +0 -18
  202. package/core/data/index.ts +0 -27
  203. package/core/data/md-base-manager.ts +0 -203
  204. package/core/data/md-ideas-manager.ts +0 -155
  205. package/core/data/md-queue-manager.ts +0 -180
  206. package/core/data/md-shipped-manager.ts +0 -90
  207. package/core/data/md-state-manager.ts +0 -137
  208. package/core/domain/task-stack/index.ts +0 -19
  209. package/core/domain/task-stack/parser.ts +0 -86
  210. package/core/domain/task-stack/storage.ts +0 -123
  211. package/core/domain/task-stack/task-stack.ts +0 -340
  212. package/core/domain/task-stack/types.ts +0 -51
  213. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  214. package/core/infrastructure/command-installer/global-config.ts +0 -136
  215. package/core/infrastructure/command-installer/index.ts +0 -25
  216. package/core/infrastructure/command-installer/types.ts +0 -41
  217. package/core/infrastructure/session-manager/index.ts +0 -23
  218. package/core/infrastructure/session-manager/types.ts +0 -45
  219. package/core/infrastructure/session-manager.ts +0 -8
  220. package/core/serializers/ideas-serializer.ts +0 -187
  221. package/core/serializers/index.ts +0 -36
  222. package/core/serializers/queue-serializer.ts +0 -210
  223. package/core/serializers/shipped-serializer.ts +0 -108
  224. package/core/serializers/state-serializer.ts +0 -136
  225. package/core/session/types.ts +0 -29
  226. /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
@@ -1,8 +1,559 @@
1
1
  /**
2
- * Task Stack Manager
3
- * Re-exports from task-stack/index.ts for backwards compatibility.
2
+ * Task Stack
3
+ * Manages task breakdown and hierarchical task tracking.
4
4
  */
5
5
 
6
- import TaskStack from './task-stack/index'
7
- export * from './task-stack/index'
6
+ import path from 'path'
7
+ import fs from 'fs/promises'
8
+ import { exec } from 'child_process'
9
+ import { promisify } from 'util'
10
+ import log from '../utils/logger'
11
+ import type {
12
+ TaskStackEntry,
13
+ ParsedNowFile,
14
+ TaskStackMigrationResult,
15
+ TaskSwitchResult,
16
+ TaskStackSummary,
17
+ } from '../types'
18
+
19
+ const execAsync = promisify(exec)
20
+
21
+
22
+ // =============================================================================
23
+ // Parser
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Parse legacy now.md format
28
+ */
29
+ export function parseNowFile(content: string): ParsedNowFile {
30
+ const result: ParsedNowFile = {
31
+ description: '',
32
+ started: null,
33
+ agent: null,
34
+ complexity: null,
35
+ dev: null,
36
+ }
37
+
38
+ // Check for frontmatter
39
+ if (content.startsWith('---')) {
40
+ const frontmatterEnd = content.indexOf('---', 3)
41
+ if (frontmatterEnd > 0) {
42
+ const frontmatter = content.substring(3, frontmatterEnd)
43
+ const lines = frontmatter.split('\n')
44
+
45
+ for (const line of lines) {
46
+ if (line.includes('task:')) {
47
+ result.description = line.split('task:')[1].trim().replace(/['"]/g, '')
48
+ }
49
+ if (line.includes('started:')) {
50
+ result.started = line.split('started:')[1].trim()
51
+ }
52
+ if (line.includes('agent:')) {
53
+ result.agent = line.split('agent:')[1].trim()
54
+ }
55
+ if (line.includes('complexity:')) {
56
+ result.complexity = line.split('complexity:')[1].trim()
57
+ }
58
+ if (line.includes('dev:')) {
59
+ result.dev = line.split('dev:')[1].trim()
60
+ }
61
+ }
62
+
63
+ // Get description from content if not in frontmatter
64
+ if (!result.description) {
65
+ const contentBody = content.substring(frontmatterEnd + 3).trim()
66
+ const firstLine = contentBody.split('\n')[0]
67
+ if (firstLine && !firstLine.startsWith('#')) {
68
+ result.description = firstLine.replace(/^[*-]\s*/, '').trim()
69
+ }
70
+ }
71
+ }
72
+ } else {
73
+ // No frontmatter, try to extract task from content
74
+ const lines = content.split('\n')
75
+ for (const line of lines) {
76
+ if (line.trim() && !line.startsWith('#') && !line.startsWith('---')) {
77
+ result.description = line.replace(/^[*-]\s*/, '').trim()
78
+ break
79
+ }
80
+ }
81
+ }
82
+
83
+ return result
84
+ }
85
+
86
+ /**
87
+ * Format duration in human-readable format
88
+ */
89
+ export function formatDuration(ms: number): string {
90
+ const seconds = Math.floor(ms / 1000)
91
+ const minutes = Math.floor(seconds / 60)
92
+ const hours = Math.floor(minutes / 60)
93
+ const days = Math.floor(hours / 24)
94
+
95
+ if (days > 0) {
96
+ return `${days}d ${hours % 24}h`
97
+ } else if (hours > 0) {
98
+ return `${hours}h ${minutes % 60}m`
99
+ } else if (minutes > 0) {
100
+ return `${minutes}m`
101
+ } else {
102
+ return `${seconds}s`
103
+ }
104
+ }
105
+
106
+ // =============================================================================
107
+ // Storage
108
+ // =============================================================================
109
+
110
+ /**
111
+ * Ensure stack file exists
112
+ */
113
+ export async function ensureStackFile(stackPath: string): Promise<void> {
114
+ try {
115
+ await fs.access(stackPath)
116
+ } catch {
117
+ // Create empty file
118
+ await fs.writeFile(stackPath, '')
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Append entry to stack
124
+ */
125
+ export async function appendToStack(stackPath: string, entry: TaskStackEntry): Promise<void> {
126
+ await ensureStackFile(stackPath)
127
+ const line = JSON.stringify(entry) + '\n'
128
+ await fs.appendFile(stackPath, line)
129
+ }
130
+
131
+ /**
132
+ * Read all stack entries
133
+ */
134
+ export async function readStack(stackPath: string): Promise<TaskStackEntry[]> {
135
+ await ensureStackFile(stackPath)
136
+ const content = await fs.readFile(stackPath, 'utf8')
137
+
138
+ if (!content.trim()) {
139
+ return []
140
+ }
141
+
142
+ const entries: TaskStackEntry[] = []
143
+ const lines = content.split('\n').filter((line) => line.trim())
144
+
145
+ for (const line of lines) {
146
+ try {
147
+ entries.push(JSON.parse(line))
148
+ } catch (error) {
149
+ log.error('Error parsing stack line:', (error as Error).message)
150
+ }
151
+ }
152
+
153
+ return entries
154
+ }
155
+
156
+ /**
157
+ * Write full stack to file
158
+ */
159
+ export async function writeStack(stackPath: string, stack: TaskStackEntry[]): Promise<void> {
160
+ const content = stack.map((task) => JSON.stringify(task)).join('\n') + '\n'
161
+ await fs.writeFile(stackPath, content)
162
+ }
163
+
164
+ /**
165
+ * Generate now.md content for a task
166
+ */
167
+ export function generateNowContent(task: TaskStackEntry | null, customContent: string | null, formatDurationFn: (ms: number) => string): string {
168
+ if (customContent !== undefined && customContent !== null) {
169
+ return customContent
170
+ }
171
+
172
+ if (!task) {
173
+ return `# Current Task
174
+
175
+ **No active task**
176
+
177
+ Use \`/p:work\` or \`/p:resume\` to start working.
178
+
179
+ ---
180
+
181
+ _Track your focus with \`/p:work [task]\`_
182
+ `
183
+ }
184
+
185
+ const started = new Date(task.started)
186
+ const now = new Date()
187
+ const elapsed = formatDurationFn(now.getTime() - started.getTime() - (task.pausedDuration || 0))
188
+
189
+ return `---
190
+ task: "${task.task}"
191
+ started: ${task.started}
192
+ agent: ${task.agent}
193
+ complexity: ${task.complexity}
194
+ dev: ${task.dev}
195
+ ---
196
+
197
+ # Current Task
198
+
199
+ **${task.task}**
200
+
201
+ - Started: ${started.toLocaleTimeString()} (${elapsed} ago)
202
+ - Agent: ${task.agent}
203
+ - Complexity: ${task.complexity}
204
+
205
+ ---
206
+
207
+ When done: \`/p:done\`
208
+ Need to pause: \`/p:pause\`
209
+ `
210
+ }
211
+
212
+ /**
213
+ * Update now.md file
214
+ */
215
+ export async function updateNowFile(
216
+ nowPath: string,
217
+ task: TaskStackEntry | null,
218
+ customContent: string | null,
219
+ formatDurationFn: (ms: number) => string
220
+ ): Promise<void> {
221
+ const content = generateNowContent(task, customContent, formatDurationFn)
222
+ await fs.writeFile(nowPath, content)
223
+ }
224
+
225
+ // =============================================================================
226
+ // Task Stack Class
227
+ // =============================================================================
228
+
229
+ export class TaskStack {
230
+ projectPath: string
231
+ stackPath: string
232
+ nowPath: string
233
+
234
+ constructor(projectPath: string) {
235
+ this.projectPath = projectPath
236
+ this.stackPath = path.join(projectPath, 'core', 'stack.jsonl')
237
+ this.nowPath = path.join(projectPath, 'core', 'now.md')
238
+ }
239
+
240
+ /**
241
+ * Initialize stack system - migrate from legacy now.md if needed
242
+ */
243
+ async initialize(): Promise<TaskStackMigrationResult> {
244
+ try {
245
+ // Check if stack already exists
246
+ await fs.access(this.stackPath)
247
+ return { migrated: false }
248
+ } catch {
249
+ // Stack doesn't exist, check for legacy now.md
250
+ return await this.migrateFromLegacy()
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Migrate from legacy now.md to stack system
256
+ */
257
+ async migrateFromLegacy(): Promise<TaskStackMigrationResult> {
258
+ try {
259
+ const nowContent = await fs.readFile(this.nowPath, 'utf8')
260
+
261
+ if (!nowContent.trim() || nowContent.includes('No active task')) {
262
+ // Empty or no task, just create empty stack
263
+ await ensureStackFile(this.stackPath)
264
+ return { migrated: true, hadTask: false }
265
+ }
266
+
267
+ // Parse task from now.md
268
+ const task = parseNowFile(nowContent)
269
+
270
+ // Create initial stack entry
271
+ const entry: TaskStackEntry = {
272
+ id: `task-${Date.now()}`,
273
+ task: task.description || 'Migrated task',
274
+ agent: task.agent || 'unknown',
275
+ status: 'active',
276
+ started: task.started || new Date().toISOString(),
277
+ paused: null,
278
+ resumed: null,
279
+ completed: null,
280
+ duration: null,
281
+ complexity: task.complexity || 'moderate',
282
+ dev: task.dev || 'unknown',
283
+ }
284
+
285
+ // Write to stack
286
+ await appendToStack(this.stackPath, entry)
287
+
288
+ return { migrated: true, hadTask: true, task: entry }
289
+ } catch (error) {
290
+ // No now.md or error reading, just create empty stack
291
+ await ensureStackFile(this.stackPath)
292
+ return { migrated: true, hadTask: false, error: (error as Error).message }
293
+ }
294
+ }
295
+
296
+ // Re-expose parseNowFile for compatibility
297
+ parseNowFile(content: string) {
298
+ return parseNowFile(content)
299
+ }
300
+
301
+ // Re-expose formatDuration for compatibility
302
+ formatDuration(ms: number): string {
303
+ return formatDuration(ms)
304
+ }
305
+
306
+ /**
307
+ * Get active task
308
+ */
309
+ async getActiveTask(): Promise<TaskStackEntry | null> {
310
+ const stack = await readStack(this.stackPath)
311
+ return stack.find((task) => task.status === 'active') || null
312
+ }
313
+
314
+ /**
315
+ * Get paused tasks
316
+ */
317
+ async getPausedTasks(): Promise<TaskStackEntry[]> {
318
+ const stack = await readStack(this.stackPath)
319
+ return stack
320
+ .filter((task) => task.status === 'paused')
321
+ .sort((a, b) => new Date(b.paused!).getTime() - new Date(a.paused!).getTime())
322
+ }
323
+
324
+ /**
325
+ * Get all incomplete tasks
326
+ */
327
+ async getIncompleteTasks(): Promise<TaskStackEntry[]> {
328
+ const stack = await readStack(this.stackPath)
329
+ return stack.filter((task) => task.status !== 'completed')
330
+ }
331
+
332
+ /**
333
+ * Start a new task
334
+ */
335
+ async startTask(description: string, agent: string = 'general', complexity: string = 'moderate'): Promise<TaskStackEntry> {
336
+ // Check if there's already an active task
337
+ const active = await this.getActiveTask()
338
+ if (active) {
339
+ throw new Error(`Already working on: ${active.task}. Use /p:pause to pause it first.`)
340
+ }
341
+
342
+ const entry: TaskStackEntry = {
343
+ id: `task-${Date.now()}`,
344
+ task: description,
345
+ agent,
346
+ status: 'active',
347
+ started: new Date().toISOString(),
348
+ paused: null,
349
+ resumed: null,
350
+ completed: null,
351
+ duration: null,
352
+ complexity,
353
+ dev: await this.getCurrentDev(),
354
+ }
355
+
356
+ await appendToStack(this.stackPath, entry)
357
+ await updateNowFile(this.nowPath, entry, null, formatDuration)
358
+
359
+ return entry
360
+ }
361
+
362
+ /**
363
+ * Pause the active task
364
+ */
365
+ async pauseTask(reason: string = ''): Promise<TaskStackEntry> {
366
+ const active = await this.getActiveTask()
367
+ if (!active) {
368
+ throw new Error('No active task to pause')
369
+ }
370
+
371
+ // Update the task
372
+ active.status = 'paused'
373
+ active.paused = new Date().toISOString()
374
+ if (reason) {
375
+ active.pauseReason = reason
376
+ }
377
+
378
+ // Rewrite stack with updated task
379
+ await this.updateTask(active)
380
+
381
+ // Update now.md to show paused state
382
+ await updateNowFile(this.nowPath, null, `Paused: ${active.task}`, formatDuration)
383
+
384
+ return active
385
+ }
386
+
387
+ /**
388
+ * Resume a paused task
389
+ */
390
+ async resumeTask(taskId: string | null = null): Promise<TaskStackEntry> {
391
+ // Check if there's an active task
392
+ const active = await this.getActiveTask()
393
+ if (active) {
394
+ throw new Error(`Already working on: ${active.task}. Complete or pause it first.`)
395
+ }
396
+
397
+ const paused = await this.getPausedTasks()
398
+ if (paused.length === 0) {
399
+ throw new Error('No paused tasks to resume')
400
+ }
401
+
402
+ let taskToResume: TaskStackEntry | undefined
403
+ if (taskId) {
404
+ taskToResume = paused.find((t) => t.id === taskId)
405
+ if (!taskToResume) {
406
+ throw new Error(`Task ${taskId} not found or not paused`)
407
+ }
408
+ } else {
409
+ // Resume most recently paused
410
+ taskToResume = paused[0]
411
+ }
412
+
413
+ // Update the task
414
+ taskToResume.status = 'active'
415
+ taskToResume.resumed = new Date().toISOString()
416
+
417
+ // Calculate paused duration
418
+ if (taskToResume.paused) {
419
+ const pausedMs = Date.now() - new Date(taskToResume.paused).getTime()
420
+ taskToResume.pausedDuration = (taskToResume.pausedDuration || 0) + pausedMs
421
+ }
422
+
423
+ // Rewrite stack with updated task
424
+ await this.updateTask(taskToResume)
425
+
426
+ // Update now.md
427
+ await updateNowFile(this.nowPath, taskToResume, null, formatDuration)
428
+
429
+ return taskToResume
430
+ }
431
+
432
+ /**
433
+ * Complete the active task
434
+ */
435
+ async completeTask(): Promise<TaskStackEntry> {
436
+ const active = await this.getActiveTask()
437
+ if (!active) {
438
+ throw new Error('No active task to complete')
439
+ }
440
+
441
+ // Update the task
442
+ active.status = 'completed'
443
+ active.completed = new Date().toISOString()
444
+
445
+ // Calculate duration (excluding paused time)
446
+ const totalMs = Date.now() - new Date(active.started).getTime()
447
+ const pausedMs = active.pausedDuration || 0
448
+ active.duration = totalMs - pausedMs
449
+ active.durationFormatted = formatDuration(active.duration)
450
+
451
+ // Rewrite stack with updated task
452
+ await this.updateTask(active)
453
+
454
+ // Clear now.md
455
+ await updateNowFile(this.nowPath, null, '', formatDuration)
456
+
457
+ return active
458
+ }
459
+
460
+ /**
461
+ * Switch tasks (atomic pause + resume/start)
462
+ */
463
+ async switchTask(targetTaskOrDescription: string): Promise<TaskSwitchResult> {
464
+ const active = await this.getActiveTask()
465
+ let pausedTask: TaskStackEntry | null = null
466
+
467
+ // Pause current if exists
468
+ if (active) {
469
+ pausedTask = await this.pauseTask('Switched to another task')
470
+ }
471
+
472
+ try {
473
+ // Check if target is a task ID or description
474
+ const paused = await this.getPausedTasks()
475
+ const existingTask = paused.find((t) => t.id === targetTaskOrDescription)
476
+
477
+ if (existingTask) {
478
+ // Resume existing task
479
+ return {
480
+ paused: pausedTask,
481
+ resumed: await this.resumeTask(targetTaskOrDescription),
482
+ type: 'resumed',
483
+ }
484
+ } else {
485
+ // Start new task
486
+ return {
487
+ paused: pausedTask,
488
+ started: await this.startTask(targetTaskOrDescription),
489
+ type: 'started',
490
+ }
491
+ }
492
+ } catch (error) {
493
+ // If switch fails, resume the original task
494
+ if (pausedTask) {
495
+ await this.resumeTask(pausedTask.id)
496
+ }
497
+ throw error
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Update a task in the stack
503
+ */
504
+ async updateTask(updatedTask: TaskStackEntry): Promise<void> {
505
+ const stack = await readStack(this.stackPath)
506
+ const index = stack.findIndex((t) => t.id === updatedTask.id)
507
+
508
+ if (index === -1) {
509
+ throw new Error(`Task ${updatedTask.id} not found`)
510
+ }
511
+
512
+ stack[index] = updatedTask
513
+ await writeStack(this.stackPath, stack)
514
+ }
515
+
516
+ /**
517
+ * Update now.md to reflect current state
518
+ */
519
+ async updateNowFile(task: TaskStackEntry | null, customContent: string | null = null): Promise<void> {
520
+ await updateNowFile(this.nowPath, task, customContent, formatDuration)
521
+ }
522
+
523
+ /**
524
+ * Get current developer from git or system
525
+ */
526
+ async getCurrentDev(): Promise<string> {
527
+ try {
528
+ const { stdout } = await execAsync('git config user.name')
529
+ return stdout.trim()
530
+ } catch {
531
+ return 'unknown'
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Get stack summary for display
537
+ */
538
+ async getStackSummary(): Promise<TaskStackSummary> {
539
+ const active = await this.getActiveTask()
540
+ const paused = await this.getPausedTasks()
541
+ const stack = await readStack(this.stackPath)
542
+ const completed = stack.filter((t) => t.status === 'completed')
543
+
544
+ return {
545
+ active,
546
+ paused,
547
+ pausedCount: paused.length,
548
+ completed,
549
+ completedCount: completed.length,
550
+ totalTasks: stack.length,
551
+ }
552
+ }
553
+ }
554
+
555
+ // =============================================================================
556
+ // Exports
557
+ // =============================================================================
558
+
8
559
  export default TaskStack
package/core/errors.ts CHANGED
@@ -4,9 +4,135 @@
4
4
  * Base error class with specific subclasses for different error domains.
5
5
  * Enables typed error handling and better error messages.
6
6
  *
7
+ * Features:
8
+ * - Zod-validated structured error data
9
+ * - NamedError pattern (inspired by opencode)
10
+ * - Type-safe error creation and handling
11
+ *
7
12
  * @module core/errors
8
- * @version 1.0.0
13
+ * @version 2.0.0
14
+ */
15
+
16
+ import { z, type ZodType } from 'zod'
17
+
18
+ // =============================================================================
19
+ // Named Error Pattern (Zod-based)
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Creates a typed error class with Zod schema validation
24
+ * Inspired by opencode's NamedError pattern
25
+ *
26
+ * @example
27
+ * const FileNotFound = NamedError.create('FileNotFound', z.object({
28
+ * path: z.string(),
29
+ * operation: z.enum(['read', 'write', 'delete'])
30
+ * }))
31
+ *
32
+ * throw FileNotFound.throw({ path: '/foo/bar', operation: 'read' })
9
33
  */
34
+ export const NamedError = {
35
+ create<T extends ZodType>(name: string, schema: T) {
36
+ type Data = z.infer<T>
37
+
38
+ class TypedError extends Error {
39
+ readonly errorName: string
40
+ readonly data: Data
41
+ readonly isOperational = true
42
+
43
+ constructor(data: Data) {
44
+ const parsed = schema.parse(data)
45
+ super(`${name}: ${JSON.stringify(parsed)}`)
46
+ this.name = name
47
+ this.errorName = name
48
+ this.data = parsed
49
+ Error.captureStackTrace?.(this, this.constructor)
50
+ }
51
+
52
+ static throw(data: Data): never {
53
+ throw new TypedError(data)
54
+ }
55
+
56
+ static is(error: unknown): error is TypedError {
57
+ return error instanceof TypedError && (error as TypedError).errorName === name
58
+ }
59
+
60
+ static create(data: Data): TypedError {
61
+ return new TypedError(data)
62
+ }
63
+ }
64
+
65
+ return TypedError
66
+ },
67
+ }
68
+
69
+ // =============================================================================
70
+ // Typed Error Definitions (New Pattern)
71
+ // =============================================================================
72
+
73
+ /** File operation errors with path context */
74
+ export const FileError = NamedError.create(
75
+ 'FileError',
76
+ z.object({
77
+ path: z.string(),
78
+ operation: z.enum(['read', 'write', 'delete', 'create', 'copy']),
79
+ reason: z.string().optional(),
80
+ })
81
+ )
82
+
83
+ /** Validation errors with field context */
84
+ export const ValidationError = NamedError.create(
85
+ 'ValidationError',
86
+ z.object({
87
+ field: z.string(),
88
+ expected: z.string(),
89
+ received: z.string().optional(),
90
+ message: z.string().optional(),
91
+ })
92
+ )
93
+
94
+ /** Permission errors */
95
+ export const PermissionError = NamedError.create(
96
+ 'PermissionError',
97
+ z.object({
98
+ action: z.string(),
99
+ resource: z.string(),
100
+ reason: z.string().optional(),
101
+ })
102
+ )
103
+
104
+ /** Task operation errors */
105
+ export const TaskError = NamedError.create(
106
+ 'TaskError',
107
+ z.object({
108
+ taskId: z.string().optional(),
109
+ operation: z.enum(['create', 'update', 'complete', 'pause', 'resume', 'delete']),
110
+ reason: z.string(),
111
+ })
112
+ )
113
+
114
+ /** Session errors */
115
+ export const SessionError = NamedError.create(
116
+ 'SessionError',
117
+ z.object({
118
+ sessionId: z.string().optional(),
119
+ reason: z.string(),
120
+ })
121
+ )
122
+
123
+ /** Sync errors */
124
+ export const SyncError = NamedError.create(
125
+ 'SyncError',
126
+ z.object({
127
+ projectId: z.string().optional(),
128
+ operation: z.enum(['push', 'pull', 'auth', 'connect']),
129
+ reason: z.string(),
130
+ })
131
+ )
132
+
133
+ // =============================================================================
134
+ // Legacy Error Classes (Preserved for Backward Compatibility)
135
+ // =============================================================================
10
136
 
11
137
  /**
12
138
  * Base error class for all prjct errors