prjct-cli 0.13.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. package/templates/commands/workflow.md +0 -12
@@ -1,16 +1,20 @@
1
1
  /**
2
- * Workflow Commands: now, done, next, build
3
- * Core task management
2
+ * Workflow Commands: work (now), done, next, pause, resume
3
+ * Core task management - Write-Through Architecture
4
+ *
5
+ * Uses storage layer: JSON (source) → MD (context) → Event (sync)
4
6
  */
5
7
 
6
8
  import type { CommandResult, Context } from './types'
9
+ import { generateUUID } from '../schemas'
7
10
  import {
8
11
  PrjctCommandsBase,
9
12
  contextBuilder,
10
- toolRegistry,
13
+ configManager,
11
14
  dateHelper,
12
15
  out
13
16
  } from './base'
17
+ import { stateStorage, queueStorage } from '../storage'
14
18
 
15
19
  export class WorkflowCommands extends PrjctCommandsBase {
16
20
  /**
@@ -21,39 +25,43 @@ export class WorkflowCommands extends PrjctCommandsBase {
21
25
  const initResult = await this.ensureProjectInit(projectPath)
22
26
  if (!initResult.success) return initResult
23
27
 
24
- const context = await contextBuilder.build(projectPath, { task }) as Context
28
+ const projectId = await configManager.getProjectId(projectPath)
29
+ if (!projectId) {
30
+ out.fail('no project ID')
31
+ return { success: false, error: 'No project ID found' }
32
+ }
25
33
 
26
34
  if (task) {
35
+ const context = await contextBuilder.build(projectPath, { task }) as Context
27
36
  const agentResult = await this._assignAgentForTask(task, projectPath, context)
28
37
  const agent = agentResult.agent?.name || 'generalist'
29
- const confidence = agentResult.routing?.confidence || 0.5
30
38
 
31
- const nowContent = `# NOW\n\n**${task}**\n\nStarted: ${new Date().toLocaleString()}\nAgent: ${agent} (${Math.round(confidence * 100)}% confidence)\n`
32
- await toolRegistry.get('Write')!(context.paths.now, nowContent)
39
+ // Write-through: JSON MD Event
40
+ await stateStorage.startTask(projectId, {
41
+ id: generateUUID(),
42
+ description: task,
43
+ sessionId: generateUUID()
44
+ })
33
45
 
34
46
  out.done(`${task} [${agent}]`)
35
47
 
36
48
  await this.logToMemory(projectPath, 'task_started', {
37
49
  task,
38
50
  agent,
39
- confidence,
40
51
  timestamp: dateHelper.getTimestamp(),
41
52
  })
42
53
  return { success: true, task, agent }
43
54
  } else {
44
- const nowContent = await toolRegistry.get('Read')!(context.paths.now) as string
55
+ // Read from storage (JSON is source of truth)
56
+ const currentTask = await stateStorage.getCurrentTask(projectId)
45
57
 
46
- if (!nowContent || nowContent.includes('No current task')) {
58
+ if (!currentTask) {
47
59
  out.warn('no active task')
48
60
  return { success: true, message: 'No active task' }
49
61
  }
50
62
 
51
- const taskMatch = nowContent.match(/\*\*(.+?)\*\*/)
52
- const agentMatch = nowContent.match(/Agent: ([^\s(]+)/)
53
- const currentTask = taskMatch ? taskMatch[1] : 'unknown'
54
- const currentAgent = agentMatch ? agentMatch[1] : ''
55
- out.done(`working on: ${currentTask}${currentAgent ? ` [${currentAgent}]` : ''}`)
56
- return { success: true, content: nowContent }
63
+ out.done(`working on: ${currentTask.description}`)
64
+ return { success: true, task: currentTask.description, currentTask }
57
65
  }
58
66
  } catch (error) {
59
67
  out.fail((error as Error).message)
@@ -69,26 +77,29 @@ export class WorkflowCommands extends PrjctCommandsBase {
69
77
  const initResult = await this.ensureProjectInit(projectPath)
70
78
  if (!initResult.success) return initResult
71
79
 
72
- const context = await contextBuilder.build(projectPath) as Context
73
- const nowContent = await toolRegistry.get('Read')!(context.paths.now) as string
80
+ const projectId = await configManager.getProjectId(projectPath)
81
+ if (!projectId) {
82
+ out.fail('no project ID')
83
+ return { success: false, error: 'No project ID found' }
84
+ }
85
+
86
+ // Read from storage
87
+ const currentTask = await stateStorage.getCurrentTask(projectId)
74
88
 
75
- if (!nowContent || nowContent.includes('No current task') || nowContent.trim() === '# NOW') {
89
+ if (!currentTask) {
76
90
  out.warn('no active task')
77
91
  return { success: true, message: 'No active task to complete' }
78
92
  }
79
93
 
80
- const taskMatch = nowContent.match(/\*\*(.+?)\*\*/)
81
- const task = taskMatch ? taskMatch[1] : 'task'
82
-
83
- const startedMatch = nowContent.match(/Started: (.+)/)
94
+ const task = currentTask.description
84
95
  let duration = ''
85
- if (startedMatch) {
86
- const started = new Date(startedMatch[1])
96
+ if (currentTask.startedAt) {
97
+ const started = new Date(currentTask.startedAt)
87
98
  duration = dateHelper.calculateDuration(started)
88
99
  }
89
100
 
90
- const emptyNow = '# NOW\n\nNo current task. Use `/p:now` to set focus.\n'
91
- await toolRegistry.get('Write')!(context.paths.now, emptyNow)
101
+ // Write-through: Complete task (JSON MD Event)
102
+ await stateStorage.completeTask(projectId)
92
103
 
93
104
  out.done(`${task}${duration ? ` (${duration})` : ''}`)
94
105
 
@@ -112,18 +123,23 @@ export class WorkflowCommands extends PrjctCommandsBase {
112
123
  const initResult = await this.ensureProjectInit(projectPath)
113
124
  if (!initResult.success) return initResult
114
125
 
115
- const context = await contextBuilder.build(projectPath) as Context
116
- const nextContent = await toolRegistry.get('Read')!(context.paths.next) as string
126
+ const projectId = await configManager.getProjectId(projectPath)
127
+ if (!projectId) {
128
+ out.fail('no project ID')
129
+ return { success: false, error: 'No project ID found' }
130
+ }
131
+
132
+ // Read queue from storage
133
+ const tasks = await queueStorage.getActiveTasks(projectId)
117
134
 
118
- if (!nextContent || nextContent.trim() === '# NEXT\n\n## Priority Queue') {
135
+ if (tasks.length === 0) {
119
136
  out.warn('queue empty')
120
137
  return { success: true, message: 'Queue is empty' }
121
138
  }
122
139
 
123
- const taskCount = (nextContent.match(/^- \[/gm) || []).length
124
- out.done(`${taskCount} task${taskCount !== 1 ? 's' : ''} queued`)
140
+ out.done(`${tasks.length} task${tasks.length !== 1 ? 's' : ''} queued`)
125
141
 
126
- return { success: true, content: nextContent }
142
+ return { success: true, tasks, count: tasks.length }
127
143
  } catch (error) {
128
144
  out.fail((error as Error).message)
129
145
  return { success: false, error: (error as Error).message }
@@ -131,95 +147,84 @@ export class WorkflowCommands extends PrjctCommandsBase {
131
147
  }
132
148
 
133
149
  /**
134
- * /p:build - Start task with agent assignment
150
+ * /p:pause - Pause active task to handle interruption
135
151
  */
136
- async build(taskOrNumber: string, projectPath: string = process.cwd()): Promise<CommandResult> {
152
+ async pause(reason: string = '', projectPath: string = process.cwd()): Promise<CommandResult> {
137
153
  try {
138
154
  const initResult = await this.ensureProjectInit(projectPath)
139
155
  if (!initResult.success) return initResult
140
156
 
141
- const context = await contextBuilder.build(projectPath, { task: taskOrNumber }) as Context
142
-
143
- const nowContent = await toolRegistry.get('Read')!(context.paths.now) as string
144
- if (nowContent && !nowContent.includes('No current task')) {
145
- console.log('⚠️ Already working on a task!')
146
- console.log(' Complete it with /p:done first\n')
147
- const taskMatch = nowContent.match(/\*\*(.+?)\*\*/)
148
- const currentTask = taskMatch ? taskMatch[1] : 'current task'
149
- console.log(` Current: ${currentTask}`)
150
- return { success: false, message: 'Task already active' }
157
+ const projectId = await configManager.getProjectId(projectPath)
158
+ if (!projectId) {
159
+ out.fail('no project ID')
160
+ return { success: false, error: 'No project ID found' }
151
161
  }
152
162
 
153
- let task = taskOrNumber
154
-
155
- if (!isNaN(Number(taskOrNumber))) {
156
- const nextContent = await toolRegistry.get('Read')!(context.paths.next) as string
157
- const tasks = nextContent
158
- .split('\n')
159
- .filter((line) => line.trim().match(/^\d+\./) || line.includes('[ ]'))
160
-
161
- const index = parseInt(taskOrNumber) - 1
162
- if (index >= 0 && index < tasks.length) {
163
- task = tasks[index].replace(/^\d+\.\s*\[.\]\s*/, '').trim()
164
- console.log(`📋 Selected from queue: ${task}\n`)
165
- } else {
166
- console.log(`❌ Invalid task number. Queue has ${tasks.length} tasks.`)
167
- console.log(' Use /p:next to see queue')
168
- return { success: false, error: 'Invalid task number' }
169
- }
170
- }
163
+ const currentTask = await stateStorage.getCurrentTask(projectId)
171
164
 
172
- if (!task) {
173
- console.log(' Task description required')
174
- console.log('Usage: /p:build "task description"')
175
- console.log(' or: /p:build 1 (select from queue)')
176
- return { success: false, error: 'Task required' }
165
+ if (!currentTask) {
166
+ out.warn('no active task to pause')
167
+ return { success: false, message: 'No active task to pause' }
177
168
  }
178
169
 
179
- console.log(`🏗️ Building: ${task}\n`)
170
+ // Write-through: Pause task (JSON → MD → Event)
171
+ await stateStorage.pauseTask(projectId, reason)
180
172
 
181
- const complexity = this._detectComplexity(task)
182
- const estimate = complexity.hours
173
+ const taskDesc = currentTask.description.slice(0, 40)
174
+ out.done(`paused: ${taskDesc}${reason ? ` (${reason})` : ''}`)
183
175
 
184
- console.log('📊 Analysis:')
185
- console.log(` Complexity: ${complexity.level}`)
186
- console.log(` Estimated: ${estimate}h`)
187
- console.log(` Type: ${complexity.type}\n`)
176
+ await this.logToMemory(projectPath, 'task_paused', {
177
+ task: currentTask.description,
178
+ reason,
179
+ timestamp: dateHelper.getTimestamp(),
180
+ })
188
181
 
189
- const agentResult = await this._assignAgentForTask(task, projectPath, context)
190
- const agent = agentResult.agent?.name || 'generalist'
191
- const confidence = agentResult.routing?.confidence || 0.5
192
- console.log(`🤖 Agent: ${agent} (${Math.round(confidence * 100)}% confidence)\n`)
182
+ return { success: true, task: currentTask.description, reason }
183
+ } catch (error) {
184
+ out.fail((error as Error).message)
185
+ return { success: false, error: (error as Error).message }
186
+ }
187
+ }
188
+
189
+ /**
190
+ * /p:resume - Resume most recently paused task
191
+ */
192
+ async resume(taskId: string | null = null, projectPath: string = process.cwd()): Promise<CommandResult> {
193
+ try {
194
+ const initResult = await this.ensureProjectInit(projectPath)
195
+ if (!initResult.success) return initResult
193
196
 
194
- const nowContentNew = `# NOW
197
+ const projectId = await configManager.getProjectId(projectPath)
198
+ if (!projectId) {
199
+ out.fail('no project ID')
200
+ return { success: false, error: 'No project ID found' }
201
+ }
195
202
 
196
- **${task}**
203
+ // Check if already working on a task
204
+ const currentTask = await stateStorage.getCurrentTask(projectId)
205
+ if (currentTask) {
206
+ out.warn('already working on a task')
207
+ return { success: false, message: `Already working on: ${currentTask.description}` }
208
+ }
197
209
 
198
- Started: ${new Date().toLocaleString()}
199
- Estimated: ${estimate}h
200
- Complexity: ${complexity.level}
201
- Agent: ${agent} (${Math.round(confidence * 100)}% confidence)
202
- `
203
- await toolRegistry.get('Write')!(context.paths.now, nowContentNew)
210
+ // Write-through: Resume task (JSON → MD → Event)
211
+ const resumed = await stateStorage.resumeTask(projectId)
204
212
 
205
- console.log('✅ Task started!\n')
206
- console.log('💡 Next steps:')
207
- console.log(' Start coding')
208
- console.log('• /p:done → Mark complete')
209
- console.log('• /p:stuck → Get help if needed')
213
+ if (!resumed) {
214
+ out.warn('no paused task to resume')
215
+ return { success: false, message: 'No paused task found' }
216
+ }
210
217
 
211
- await this.logToMemory(projectPath, 'task_built', {
212
- task,
213
- complexity: complexity.level,
214
- estimate,
215
- agent,
216
- confidence,
218
+ out.done(`resumed: ${resumed.description.slice(0, 40)}`)
219
+
220
+ await this.logToMemory(projectPath, 'task_resumed', {
221
+ task: resumed.description,
217
222
  timestamp: dateHelper.getTimestamp(),
218
223
  })
219
224
 
220
- return { success: true, task, complexity, estimate, agent }
225
+ return { success: true, task: resumed.description }
221
226
  } catch (error) {
222
- console.error('❌ Error:', (error as Error).message)
227
+ out.fail((error as Error).message)
223
228
  return { success: false, error: (error as Error).message }
224
229
  }
225
230
  }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Context Generator
3
+ *
4
+ * Generates MD files in context/ from JSON data in data/
5
+ * These MD files are for Claude to read.
6
+ *
7
+ * context/
8
+ * ├── CLAUDE.md - Full project context
9
+ * ├── now.md - Current task context
10
+ * ├── queue.md - Task queue
11
+ * └── summary.md - Project summary
12
+ */
13
+
14
+ import fs from 'fs/promises'
15
+ import path from 'path'
16
+ import os from 'os'
17
+ import { exec } from 'child_process'
18
+ import { promisify } from 'util'
19
+ import { getStorage } from '../storage'
20
+
21
+ const execAsync = promisify(exec)
22
+
23
+ interface Task {
24
+ id: string
25
+ description: string
26
+ status: string
27
+ priority?: string
28
+ startedAt?: string
29
+ completedAt?: string
30
+ }
31
+
32
+ interface Feature {
33
+ id: string
34
+ name: string
35
+ status: string
36
+ description?: string
37
+ }
38
+
39
+ interface Idea {
40
+ id: string
41
+ title: string
42
+ status?: string
43
+ }
44
+
45
+ interface ProjectData {
46
+ name?: string
47
+ repoPath?: string
48
+ techStack?: string[]
49
+ version?: string
50
+ }
51
+
52
+ interface Agent {
53
+ name: string
54
+ role?: string
55
+ domain?: string
56
+ }
57
+
58
+ /**
59
+ * Generate all context MD files from JSON data
60
+ */
61
+ export async function generateContext(projectId: string, repoPath: string): Promise<void> {
62
+ const globalPath = path.join(os.homedir(), '.prjct-cli/projects', projectId)
63
+ const contextPath = path.join(globalPath, 'context')
64
+ const storage = getStorage(projectId)
65
+
66
+ // Ensure context directory exists
67
+ await fs.mkdir(contextPath, { recursive: true })
68
+
69
+ // Read all data
70
+ const project = await storage.read<ProjectData>(['project']) || {}
71
+ const taskPaths = await storage.list(['task'])
72
+ const featurePaths = await storage.list(['feature'])
73
+ const ideaPaths = await storage.list(['idea'])
74
+ const agentPaths = await storage.list(['agent'])
75
+
76
+ const tasks: Task[] = []
77
+ for (const p of taskPaths) {
78
+ const task = await storage.read<Task>(p)
79
+ if (task) tasks.push({ ...task, id: p[1] })
80
+ }
81
+
82
+ const features: Feature[] = []
83
+ for (const p of featurePaths) {
84
+ const feature = await storage.read<Feature>(p)
85
+ if (feature) features.push({ ...feature, id: p[1] })
86
+ }
87
+
88
+ const ideas: Idea[] = []
89
+ for (const p of ideaPaths) {
90
+ const idea = await storage.read<Idea>(p)
91
+ if (idea) ideas.push({ ...idea, id: p[1] })
92
+ }
93
+
94
+ const agents: Agent[] = []
95
+ for (const p of agentPaths) {
96
+ const agent = await storage.read<Agent>(p)
97
+ if (agent) agents.push({ ...agent, name: p[1] })
98
+ }
99
+
100
+ // Get git data
101
+ const gitData = await getGitData(repoPath)
102
+
103
+ // Get package.json data
104
+ const pkgData = await getPackageData(repoPath)
105
+
106
+ // Generate each context file
107
+ await generateClaudeMd(contextPath, projectId, project, tasks, features, ideas, agents, gitData, pkgData, repoPath)
108
+ await generateNowMd(contextPath, tasks)
109
+ await generateQueueMd(contextPath, tasks)
110
+ await generateSummaryMd(contextPath, project, gitData, pkgData)
111
+ }
112
+
113
+ async function getGitData(repoPath: string) {
114
+ const data = {
115
+ branch: 'main',
116
+ commits: 0,
117
+ contributors: 0,
118
+ hasChanges: false,
119
+ recentCommits: [] as { hash: string; message: string; date: string }[]
120
+ }
121
+
122
+ try {
123
+ const { stdout: branch } = await execAsync('git branch --show-current', { cwd: repoPath })
124
+ data.branch = branch.trim() || 'main'
125
+
126
+ const { stdout: commits } = await execAsync('git rev-list --count HEAD', { cwd: repoPath })
127
+ data.commits = parseInt(commits.trim()) || 0
128
+
129
+ const { stdout: contributors } = await execAsync('git shortlog -sn --all | wc -l', { cwd: repoPath })
130
+ data.contributors = parseInt(contributors.trim()) || 0
131
+
132
+ const { stdout: status } = await execAsync('git status --porcelain', { cwd: repoPath })
133
+ data.hasChanges = status.trim().length > 0
134
+
135
+ const { stdout: log } = await execAsync('git log --oneline -10 --pretty=format:"%h|%s|%ad" --date=short', { cwd: repoPath })
136
+ data.recentCommits = log.split('\n').filter(Boolean).map(line => {
137
+ const [hash, message, date] = line.split('|')
138
+ return { hash, message, date }
139
+ })
140
+ } catch { /* not git */ }
141
+
142
+ return data
143
+ }
144
+
145
+ async function getPackageData(repoPath: string) {
146
+ const data = {
147
+ dependencies: {} as Record<string, string>,
148
+ devDependencies: {} as Record<string, string>,
149
+ scripts: {} as Record<string, string>
150
+ }
151
+
152
+ try {
153
+ const pkgPath = path.join(repoPath, 'package.json')
154
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'))
155
+ data.dependencies = pkg.dependencies || {}
156
+ data.devDependencies = pkg.devDependencies || {}
157
+ data.scripts = pkg.scripts || {}
158
+ } catch { /* no package.json */ }
159
+
160
+ return data
161
+ }
162
+
163
+ async function generateClaudeMd(
164
+ contextPath: string,
165
+ projectId: string,
166
+ project: ProjectData,
167
+ tasks: Task[],
168
+ features: Feature[],
169
+ ideas: Idea[],
170
+ agents: Agent[],
171
+ gitData: Awaited<ReturnType<typeof getGitData>>,
172
+ pkgData: Awaited<ReturnType<typeof getPackageData>>,
173
+ repoPath: string
174
+ ) {
175
+ const projectName = project.name || path.basename(repoPath)
176
+ const currentTask = tasks.find(t => t.status === 'in_progress')
177
+ const pendingTasks = tasks.filter(t => t.status === 'pending')
178
+ const activeFeatures = features.filter(f => f.status === 'in_progress' || f.status === 'active')
179
+
180
+ const deps = Object.keys(pkgData.dependencies)
181
+ const devDeps = Object.keys(pkgData.devDependencies)
182
+
183
+ const content = `# ${projectName} - Project Context
184
+ <!-- projectId: ${projectId} -->
185
+ <!-- Generated: ${new Date().toISOString()} -->
186
+
187
+ ## PROJECT DATA
188
+
189
+ ### Dependencies (${deps.length + devDeps.length})
190
+
191
+ **Production** (${deps.length}):
192
+ ${deps.length > 0 ? deps.map(d => `- ${d}: ${pkgData.dependencies[d]}`).join('\n') : '_None_'}
193
+
194
+ **Dev** (${devDeps.length}):
195
+ ${devDeps.length > 0 ? devDeps.map(d => `- ${d}: ${pkgData.devDependencies[d]}`).join('\n') : '_None_'}
196
+
197
+ ### Scripts
198
+
199
+ ${Object.keys(pkgData.scripts).length > 0 ? Object.entries(pkgData.scripts).map(([k, v]) => `- \`${k}\`: ${v}`).join('\n') : '_None_'}
200
+
201
+ ### Git
202
+
203
+ - Branch: ${gitData.branch}
204
+ - Commits: ${gitData.commits}
205
+ - Contributors: ${gitData.contributors}
206
+ - Uncommitted: ${gitData.hasChanges ? 'Yes' : 'No'}
207
+
208
+ **Recent:**
209
+ ${gitData.recentCommits.length > 0 ? gitData.recentCommits.slice(0, 5).map(c => `- \`${c.hash}\` ${c.message}`).join('\n') : '_None_'}
210
+
211
+ ---
212
+
213
+ ## CURRENT STATE
214
+
215
+ **Now:** ${currentTask ? currentTask.description : '_No active task_'}
216
+
217
+ **Queue (${pendingTasks.length}):**
218
+ ${pendingTasks.length > 0 ? pendingTasks.slice(0, 10).map((t, i) => `${i + 1}. ${t.description}`).join('\n') : '_Empty_'}
219
+
220
+ **Active Features (${activeFeatures.length}):**
221
+ ${activeFeatures.length > 0 ? activeFeatures.map(f => `- ${f.name}`).join('\n') : '_None_'}
222
+
223
+ **Ideas (${ideas.length}):**
224
+ ${ideas.length > 0 ? ideas.slice(0, 5).map(i => `- ${i.title}`).join('\n') : '_None_'}
225
+
226
+ ---
227
+
228
+ ## AGENTS
229
+
230
+ ${agents.length > 0 ? agents.map(a => `- **${a.name}**: ${a.role || 'Specialist'}`).join('\n') : '_None_'}
231
+
232
+ ---
233
+
234
+ ## DATA LOCATION
235
+
236
+ \`\`\`
237
+ ~/.prjct-cli/projects/${projectId}/
238
+ ├── data/ # JSON (source of truth)
239
+ │ ├── project.json
240
+ │ ├── tasks/
241
+ │ ├── features/
242
+ │ ├── ideas/
243
+ │ └── agents/
244
+ ├── context/ # MD (for Claude)
245
+ │ ├── CLAUDE.md
246
+ │ ├── now.md
247
+ │ └── queue.md
248
+ └── sync/ # Sync state
249
+ └── pending.json
250
+ \`\`\`
251
+ `
252
+
253
+ await fs.writeFile(path.join(contextPath, 'CLAUDE.md'), content, 'utf-8')
254
+ }
255
+
256
+ async function generateNowMd(contextPath: string, tasks: Task[]) {
257
+ const currentTask = tasks.find(t => t.status === 'in_progress')
258
+
259
+ const content = currentTask
260
+ ? `# NOW
261
+
262
+ **Task:** ${currentTask.description}
263
+
264
+ **Started:** ${currentTask.startedAt || 'Unknown'}
265
+
266
+ **Priority:** ${currentTask.priority || 'medium'}
267
+ `
268
+ : `# NOW
269
+
270
+ _No active task. Use /p:now to start._
271
+ `
272
+
273
+ await fs.writeFile(path.join(contextPath, 'now.md'), content, 'utf-8')
274
+ }
275
+
276
+ async function generateQueueMd(contextPath: string, tasks: Task[]) {
277
+ const pendingTasks = tasks.filter(t => t.status === 'pending')
278
+
279
+ const content = `# QUEUE
280
+
281
+ ${pendingTasks.length > 0
282
+ ? pendingTasks.map((t, i) => `${i + 1}. ${t.description}${t.priority ? ` [${t.priority}]` : ''}`).join('\n')
283
+ : '_Empty queue. Use /p:next to add tasks._'
284
+ }
285
+ `
286
+
287
+ await fs.writeFile(path.join(contextPath, 'queue.md'), content, 'utf-8')
288
+ }
289
+
290
+ async function generateSummaryMd(
291
+ contextPath: string,
292
+ project: ProjectData,
293
+ gitData: Awaited<ReturnType<typeof getGitData>>,
294
+ pkgData: Awaited<ReturnType<typeof getPackageData>>
295
+ ) {
296
+ const content = `# PROJECT SUMMARY
297
+
298
+ **Name:** ${project.name || 'Unknown'}
299
+ **Version:** ${project.version || 'N/A'}
300
+ **Stack:** ${project.techStack?.join(', ') || 'Not detected'}
301
+
302
+ ## Git
303
+
304
+ - Branch: ${gitData.branch}
305
+ - Commits: ${gitData.commits}
306
+ - Status: ${gitData.hasChanges ? 'Has uncommitted changes' : 'Clean'}
307
+
308
+ ## Dependencies
309
+
310
+ - Production: ${Object.keys(pkgData.dependencies).length}
311
+ - Dev: ${Object.keys(pkgData.devDependencies).length}
312
+ `
313
+
314
+ await fs.writeFile(path.join(contextPath, 'summary.md'), content, 'utf-8')
315
+ }
316
+
317
+ export default { generateContext }