crewly 1.8.7 → 1.8.9

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 (165) hide show
  1. package/dist/backend/backend/src/constants.d.ts +12 -0
  2. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  3. package/dist/backend/backend/src/constants.js +12 -0
  4. package/dist/backend/backend/src/constants.js.map +1 -1
  5. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
  6. package/dist/backend/backend/src/controllers/browser/browser.controller.js +17 -0
  7. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  8. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  9. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +8 -1
  10. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  11. package/dist/backend/backend/src/index.d.ts.map +1 -1
  12. package/dist/backend/backend/src/index.js +15 -7
  13. package/dist/backend/backend/src/index.js.map +1 -1
  14. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +7 -0
  15. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
  16. package/dist/backend/backend/src/services/browser/browser-bridge.service.js +69 -12
  17. package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
  18. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +122 -1
  19. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
  20. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +252 -17
  21. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
  22. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +37 -3
  23. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -1
  24. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +140 -23
  25. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -1
  26. package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts +75 -0
  27. package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/cloud/cloud-client.service.js +164 -12
  29. package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
  30. package/dist/cli/backend/src/constants.d.ts +12 -0
  31. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  32. package/dist/cli/backend/src/constants.js +12 -0
  33. package/dist/cli/backend/src/constants.js.map +1 -1
  34. package/dist/cli/cli/src/index.js +0 -0
  35. package/package.json +1 -1
  36. package/config/constants.d.ts.map +0 -1
  37. package/config/index.d.ts.map +0 -1
  38. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +0 -169
  39. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +0 -1
  40. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +0 -1779
  41. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +0 -1
  42. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +0 -513
  43. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +0 -1
  44. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +0 -1568
  45. package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +0 -1
  46. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts +0 -86
  47. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts.map +0 -1
  48. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js +0 -147
  49. package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js.map +0 -1
  50. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts +0 -68
  51. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts.map +0 -1
  52. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js +0 -131
  53. package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js.map +0 -1
  54. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts +0 -130
  55. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts.map +0 -1
  56. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js +0 -263
  57. package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js.map +0 -1
  58. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts +0 -74
  59. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts.map +0 -1
  60. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js +0 -140
  61. package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js.map +0 -1
  62. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts +0 -29
  63. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts.map +0 -1
  64. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js +0 -279
  65. package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js.map +0 -1
  66. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts +0 -340
  67. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts.map +0 -1
  68. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js +0 -1176
  69. package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js.map +0 -1
  70. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts +0 -79
  71. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts.map +0 -1
  72. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js +0 -145
  73. package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js.map +0 -1
  74. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts +0 -79
  75. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts.map +0 -1
  76. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js +0 -218
  77. package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js.map +0 -1
  78. package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts +0 -16
  79. package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts.map +0 -1
  80. package/dist/backend/backend/src/services/agent/crewly-agent/index.js +0 -16
  81. package/dist/backend/backend/src/services/agent/crewly-agent/index.js.map +0 -1
  82. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts +0 -135
  83. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts.map +0 -1
  84. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js +0 -185
  85. package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js.map +0 -1
  86. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts +0 -141
  87. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts.map +0 -1
  88. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js +0 -310
  89. package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js.map +0 -1
  90. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts +0 -91
  91. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts.map +0 -1
  92. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js +0 -143
  93. package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js.map +0 -1
  94. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts +0 -103
  95. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts.map +0 -1
  96. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js +0 -256
  97. package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js.map +0 -1
  98. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts +0 -143
  99. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts.map +0 -1
  100. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js +0 -264
  101. package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js.map +0 -1
  102. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts +0 -13
  103. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts.map +0 -1
  104. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js +0 -91
  105. package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js.map +0 -1
  106. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts +0 -135
  107. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts.map +0 -1
  108. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js +0 -1937
  109. package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js.map +0 -1
  110. package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts +0 -429
  111. package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts.map +0 -1
  112. package/dist/backend/backend/src/services/autonomous/auto-assign.service.js +0 -852
  113. package/dist/backend/backend/src/services/autonomous/auto-assign.service.js.map +0 -1
  114. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +0 -171
  115. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
  116. package/dist/backend/backend/src/services/project/task-tracking.service.js +0 -725
  117. package/dist/backend/backend/src/services/project/task-tracking.service.js.map +0 -1
  118. package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts +0 -118
  119. package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts.map +0 -1
  120. package/dist/backend/backend/src/services/v3/project-task-watcher.service.js +0 -326
  121. package/dist/backend/backend/src/services/v3/project-task-watcher.service.js.map +0 -1
  122. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts +0 -74
  123. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts.map +0 -1
  124. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js +0 -154
  125. package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js.map +0 -1
  126. package/dist/backend/backend/src/types/auto-assign.types.d.ts +0 -271
  127. package/dist/backend/backend/src/types/auto-assign.types.d.ts.map +0 -1
  128. package/dist/backend/backend/src/types/auto-assign.types.js +0 -136
  129. package/dist/backend/backend/src/types/auto-assign.types.js.map +0 -1
  130. package/dist/backend/backend/src/utils/esm-require.utils.d.ts +0 -111
  131. package/dist/backend/backend/src/utils/esm-require.utils.d.ts.map +0 -1
  132. package/dist/backend/backend/src/utils/esm-require.utils.js +0 -124
  133. package/dist/backend/backend/src/utils/esm-require.utils.js.map +0 -1
  134. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +0 -220
  135. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +0 -1
  136. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js +0 -37
  137. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +0 -1
  138. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts +0 -56
  139. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts.map +0 -1
  140. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js +0 -91
  141. package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js.map +0 -1
  142. package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts +0 -159
  143. package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts.map +0 -1
  144. package/dist/cli/backend/src/services/knowledge/learnings-index.service.js +0 -304
  145. package/dist/cli/backend/src/services/knowledge/learnings-index.service.js.map +0 -1
  146. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts +0 -115
  147. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts.map +0 -1
  148. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js +0 -215
  149. package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js.map +0 -1
  150. package/dist/cli/backend/src/services/memory/embedding-provider.d.ts +0 -78
  151. package/dist/cli/backend/src/services/memory/embedding-provider.d.ts.map +0 -1
  152. package/dist/cli/backend/src/services/memory/embedding-provider.js +0 -179
  153. package/dist/cli/backend/src/services/memory/embedding-provider.js.map +0 -1
  154. package/dist/cli/backend/src/services/memory/vector-store.service.d.ts +0 -331
  155. package/dist/cli/backend/src/services/memory/vector-store.service.d.ts.map +0 -1
  156. package/dist/cli/backend/src/services/memory/vector-store.service.js +0 -814
  157. package/dist/cli/backend/src/services/memory/vector-store.service.js.map +0 -1
  158. package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +0 -171
  159. package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
  160. package/dist/cli/backend/src/services/project/task-tracking.service.js +0 -725
  161. package/dist/cli/backend/src/services/project/task-tracking.service.js.map +0 -1
  162. package/dist/cli/backend/src/types/auto-assign.types.d.ts +0 -271
  163. package/dist/cli/backend/src/types/auto-assign.types.d.ts.map +0 -1
  164. package/dist/cli/backend/src/types/auto-assign.types.js +0 -136
  165. package/dist/cli/backend/src/types/auto-assign.types.js.map +0 -1
@@ -1,1779 +0,0 @@
1
- import { readFile, writeFile, mkdir, readdir, stat, unlink } from 'fs/promises';
2
- import { join, basename, dirname, resolve } from 'path';
3
- import { existsSync } from 'fs';
4
- import { resolveStepConfig } from '../../utils/prompt-resolver.js';
5
- import { updateAgentHeartbeat } from '../../services/agent/agent-heartbeat.service.js';
6
- import { CREWLY_CONSTANTS } from '../../constants.js';
7
- import { LoggerService } from '../../services/core/logger.service.js';
8
- import { TaskPlanningService } from '../../services/agent/task-planning.service.js';
9
- import { TaskOutputValidatorService } from '../../services/quality/task-output-validator.service.js';
10
- import { TracingService } from '../../services/core/tracing.service.js';
11
- import { TRACING_CONSTANTS } from '../../constants.js';
12
- import { TASK_OUTPUT_CONSTANTS } from '../../types/task-output.types.js';
13
- const logger = LoggerService.getInstance().createComponentLogger('TaskManagementController');
14
- /** Module-level reference to EventBusService for auto-cleanup on task completion */
15
- let eventBusService = null;
16
- // ---------------------------------------------------------------------------
17
- // V3.1 Projection Hooks — fire-and-forget wrappers injected into legacy routes
18
- // ---------------------------------------------------------------------------
19
- /**
20
- * Projects a legacy task-management accept into a TaskRecord (fire-and-forget).
21
- * Called when an agent successfully calls /task-management/take-next.
22
- *
23
- * @param taskTitle - Task title parsed from the .md file
24
- * @param taskPath - Absolute path to the task file (used as stable key in title)
25
- * @param sessionName - Agent session that accepted the task
26
- */
27
- function projectTaskAccepted(taskTitle, taskPath, sessionName) {
28
- setImmediate(async () => {
29
- try {
30
- const { TaskProjectionService } = await import('../../services/v3/task-projection.service.js');
31
- const proj = TaskProjectionService.getInstance();
32
- const record = await proj.createRecord({
33
- title: taskTitle || basename(taskPath, '.md'),
34
- type: 'self_execution',
35
- ownerAgent: sessionName,
36
- });
37
- await proj.markStarted(record.id, sessionName);
38
- }
39
- catch {
40
- // Non-critical
41
- }
42
- });
43
- }
44
- /**
45
- * Projects a legacy task completion into the most recent running TaskRecord for this agent.
46
- * Called when /task-management/complete succeeds.
47
- *
48
- * @param sessionName - Agent session that completed the task
49
- * @param taskTitle - Task title (for matching)
50
- */
51
- function projectTaskCompleted(sessionName, taskTitle) {
52
- setImmediate(async () => {
53
- try {
54
- const { TaskProjectionService } = await import('../../services/v3/task-projection.service.js');
55
- const proj = TaskProjectionService.getInstance();
56
- // Find the most recent running TaskRecord for this agent
57
- const records = proj.listRecords({ ownerAgent: sessionName, status: 'running' });
58
- // Prefer an exact title match, fall back to most recent
59
- const match = records.find(r => r.title === taskTitle) || records[records.length - 1];
60
- if (match) {
61
- await proj.markDone(match.id, sessionName);
62
- }
63
- }
64
- catch {
65
- // Non-critical
66
- }
67
- });
68
- }
69
- /**
70
- * Projects a legacy task block into the most recent running TaskRecord for this agent.
71
- * Called when /task-management/block succeeds.
72
- *
73
- * @param sessionName - Agent session that blocked the task
74
- * @param reason - Block reason
75
- * @param taskTitle - Task title (for matching)
76
- */
77
- function projectTaskBlocked(sessionName, reason, taskTitle) {
78
- setImmediate(async () => {
79
- try {
80
- const { TaskProjectionService } = await import('../../services/v3/task-projection.service.js');
81
- const proj = TaskProjectionService.getInstance();
82
- const records = proj.listRecords({ ownerAgent: sessionName, status: 'running' });
83
- const match = records.find(r => r.title === taskTitle) || records[records.length - 1];
84
- if (match) {
85
- await proj.markBlocked(match.id, sessionName, reason);
86
- }
87
- }
88
- catch {
89
- // Non-critical
90
- }
91
- });
92
- }
93
- /**
94
- * Publish a task:completed event to the EventBus (#137) and send a Slack
95
- * notification to the user (#202). Both are fire-and-forget — errors are
96
- * logged but do not block task completion.
97
- *
98
- * @param task - The completed task metadata
99
- * @param sessionName - The session that completed the task
100
- */
101
- function publishTaskCompletedEvent(task, sessionName) {
102
- if (!eventBusService)
103
- return;
104
- try {
105
- eventBusService.publish({
106
- id: `task-completed-${task.id}-${Date.now()}`,
107
- type: 'task:completed',
108
- timestamp: new Date().toISOString(),
109
- teamId: task.teamId || '',
110
- teamName: '',
111
- memberId: task.assignedTeamMemberId || '',
112
- memberName: '',
113
- sessionName: sessionName || task.assignedSessionName || '',
114
- previousValue: task.status,
115
- newValue: 'completed',
116
- changedField: 'taskStatus',
117
- });
118
- }
119
- catch (err) {
120
- logger.warn('Failed to publish task:completed event (non-fatal)', {
121
- taskId: task.id,
122
- error: err instanceof Error ? err.message : String(err),
123
- });
124
- }
125
- // #202: Auto-notify user via Slack when any task completes (including background deploys)
126
- notifyTaskCompletedViaSlack(task, sessionName);
127
- }
128
- /**
129
- * Send a Slack notification for task completion (#202).
130
- * Uses lazy import to avoid circular dependencies. Fire-and-forget.
131
- *
132
- * @param task - The completed task
133
- * @param sessionName - The session that completed the task
134
- */
135
- function notifyTaskCompletedViaSlack(task, sessionName) {
136
- import('../../services/orchestrator/slack-bridge-lazy.js')
137
- .then(({ getSlackBridgeLazy }) => getSlackBridgeLazy())
138
- .then(bridge => {
139
- const agentName = sessionName || task.assignedSessionName || 'unknown';
140
- const projectName = task.teamId || 'crewly';
141
- return bridge.notifyTaskCompleted(task.taskName, agentName, projectName);
142
- })
143
- .catch(err => {
144
- logger.debug('Slack task-completion notification skipped (non-fatal)', {
145
- taskId: task.id,
146
- error: err instanceof Error ? err.message : String(err),
147
- });
148
- });
149
- }
150
- /**
151
- * Set the EventBusService instance for task monitoring cleanup.
152
- * Called during server initialization.
153
- *
154
- * @param service - The EventBusService instance
155
- */
156
- export function setEventBusServiceForTaskCleanup(service) {
157
- eventBusService = service;
158
- }
159
- /**
160
- * Get the current EventBusService reference (exposed for testing).
161
- *
162
- * @returns The EventBusService or null
163
- */
164
- export function getEventBusServiceForTaskCleanup() {
165
- return eventBusService;
166
- }
167
- /**
168
- * Creates a new task MD file in the project's .crewly/tasks/ directory.
169
- * Optionally assigns it immediately if a sessionName is provided.
170
- *
171
- * @param req - Request containing projectPath, task, priority, sessionName (optional), milestone (optional)
172
- * @param res - Response with success status, created task path, and status
173
- */
174
- export async function createTask(req, res) {
175
- return TracingService.getInstance().withSpan(TRACING_CONSTANTS.SPANS.TASK_CREATE, {
176
- attributes: {
177
- 'task.priority': req.body.priority || 'medium',
178
- 'task.milestone': req.body.milestone || 'delegated',
179
- }
180
- }, async (span) => {
181
- try {
182
- const { projectPath, task, priority = 'medium', sessionName, milestone = 'delegated', outputSchema, requestId, } = req.body;
183
- if (!projectPath) {
184
- res.status(400).json({ success: false, error: 'projectPath is required' });
185
- return;
186
- }
187
- if (!task) {
188
- res.status(400).json({ success: false, error: 'task is required' });
189
- return;
190
- }
191
- // Determine initial status folder based on whether an assignee is provided
192
- const statusFolder = sessionName ? 'in_progress' : 'open';
193
- const tasksDir = join(projectPath, '.crewly', 'tasks', milestone, statusFolder);
194
- // Ensure directory exists
195
- await ensureDirectoryExists(tasksDir);
196
- // Generate sanitized filename from task description
197
- const sanitizedName = task
198
- .toLowerCase()
199
- .replace(/[^a-z0-9]+/g, '_')
200
- .replace(/^_|_$/g, '')
201
- .substring(0, 80);
202
- const timestamp = Date.now();
203
- const fileName = `${sanitizedName}_${timestamp}.md`;
204
- const taskPath = join(tasksDir, fileName);
205
- // Build task markdown content
206
- let taskContent = `# ${task}\n\n## Task Information\n- **Priority**: ${priority}\n- **Milestone**: ${milestone}\n- **Created at**: ${new Date().toISOString()}\n- **Status**: ${statusFolder === 'in_progress' ? 'In Progress' : 'Open'}\n`;
207
- if (sessionName) {
208
- taskContent += `\n## Assignment Information\n- **Assigned to**: ${sessionName}\n- **Assigned at**: ${new Date().toISOString()}\n- **Status**: In Progress\n`;
209
- }
210
- taskContent += `\n## Task Description\n\n${task}\n`;
211
- // Embed output schema if provided
212
- if (outputSchema && typeof outputSchema === 'object') {
213
- const validator = TaskOutputValidatorService.getInstance();
214
- taskContent += validator.generateSchemaMarkdown(outputSchema);
215
- }
216
- await writeFile(taskPath, taskContent, 'utf-8');
217
- // Create planning files (plan.md, findings.md, progress.md) when task is assigned
218
- if (sessionName) {
219
- try {
220
- const planningService = TaskPlanningService.getInstance();
221
- await planningService.createPlanningFiles({
222
- taskFilePath: taskPath,
223
- taskTitle: task,
224
- priority,
225
- sessionName,
226
- });
227
- }
228
- catch (planErr) {
229
- logger.warn('Failed to create planning files (non-fatal)', {
230
- error: planErr instanceof Error ? planErr.message : String(planErr),
231
- });
232
- }
233
- }
234
- // trackedTaskId derived from V3 hook below (legacy TaskTrackingService removed)
235
- let trackedTaskId;
236
- // V3 Hook: emit v3:task_delegated for automatic WorkItem creation.
237
- // Uses explicit requestId only — no time-window fallback — to avoid
238
- // mis-association in high-concurrency scenarios.
239
- if (sessionName && eventBusService) {
240
- try {
241
- const taskIdForEvent = trackedTaskId || `${sanitizedName}_${timestamp}`;
242
- eventBusService.emit('v3:task_delegated', {
243
- taskId: taskIdForEvent,
244
- title: task,
245
- description: task,
246
- assignedTo: sessionName,
247
- priority,
248
- projectPath,
249
- milestone,
250
- requestId: requestId || undefined,
251
- timestamp: new Date().toISOString(),
252
- });
253
- logger.debug('Emitted v3:task_delegated', {
254
- taskId: taskIdForEvent,
255
- assignedTo: sessionName,
256
- requestId: requestId || undefined,
257
- });
258
- }
259
- catch (err) {
260
- logger.warn('Failed to emit v3:task_delegated (non-fatal)', {
261
- error: err instanceof Error ? err.message : String(err),
262
- });
263
- }
264
- }
265
- res.json({
266
- success: true,
267
- message: `Task file created: ${fileName}`,
268
- taskPath,
269
- fileName,
270
- status: statusFolder,
271
- milestone,
272
- taskId: trackedTaskId,
273
- });
274
- }
275
- catch (error) {
276
- logger.error('Error creating task', { error: error instanceof Error ? error.message : String(error) });
277
- res.status(500).json({ success: false, error: 'Failed to create task' });
278
- }
279
- });
280
- }
281
- /**
282
- * Assigns a task to a team member by moving it from open/ to in_progress/ folder
283
- *
284
- * @param req - Request containing taskPath and memberId
285
- * @param res - Response with success status and task information
286
- */
287
- export async function assignTask(req, res) {
288
- return TracingService.getInstance().withSpan(TRACING_CONSTANTS.SPANS.TASK_ASSIGN, {
289
- attributes: {
290
- 'task.path': req.body.taskPath || 'unknown',
291
- 'agent.session': req.body.sessionName || 'unknown',
292
- }
293
- }, async (span) => {
294
- try {
295
- const { taskPath, sessionName } = req.body;
296
- // Update agent heartbeat (proof of life)
297
- try {
298
- await updateAgentHeartbeat(sessionName, undefined, CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE);
299
- }
300
- catch (error) {
301
- logger.warn('Failed to update agent heartbeat', { error: error instanceof Error ? error.message : String(error) });
302
- // Continue execution - heartbeat failures shouldn't break task assignment
303
- }
304
- if (!taskPath) {
305
- res.status(400).json({ success: false, error: 'taskPath is required' });
306
- return;
307
- }
308
- if (!sessionName) {
309
- res.status(400).json({ success: false, error: 'sessionName is required' });
310
- return;
311
- }
312
- // Verify source task file exists
313
- if (!existsSync(taskPath)) {
314
- res.status(200).json({
315
- success: false,
316
- error: 'Task file does not exist at the specified path',
317
- details: `No task file found at: ${taskPath}. Make sure the task file exists in the open/ folder.`,
318
- taskPath,
319
- suggestion: 'Verify the task file path is correct and the file exists'
320
- });
321
- return;
322
- }
323
- // Ensure task is in open/ folder
324
- if (!taskPath.includes('/open/')) {
325
- res.status(200).json({
326
- success: false,
327
- error: 'Task is not in the correct folder for assignment',
328
- details: `Task must be in open/ folder to be assigned. Current path: ${taskPath}`,
329
- taskPath,
330
- expectedFolder: 'open',
331
- currentFolder: taskPath.includes('/in_progress/') ? 'in_progress' : taskPath.includes('/done/') ? 'done' : taskPath.includes('/blocked/') ? 'blocked' : 'unknown'
332
- });
333
- return;
334
- }
335
- // Parse task information
336
- const taskContent = await readFile(taskPath, 'utf-8');
337
- const taskInfo = parseTaskInfo(taskContent, basename(taskPath));
338
- // Extract project and team information from task path
339
- const pathMatch = taskPath.match(/\/([^/]+)\/\.crewly/);
340
- if (!pathMatch) {
341
- res.status(400).json({ success: false, error: 'Cannot determine project from task path' });
342
- return;
343
- }
344
- const projectPath = taskPath.substring(0, taskPath.indexOf('.crewly'));
345
- // Find project by path
346
- const projects = await this.storageService.getProjects();
347
- const project = projects.find(p => resolve(p.path) === resolve(projectPath));
348
- if (!project) {
349
- res.status(404).json({ success: false, error: 'Project not found' });
350
- return;
351
- }
352
- // Find team and member by sessionName
353
- const teams = await this.storageService.getTeams();
354
- let teamId = '';
355
- let memberId = '';
356
- for (const team of teams) {
357
- const member = team.members.find(m => m.sessionName === sessionName);
358
- if (member) {
359
- teamId = team.id;
360
- memberId = member.id;
361
- break;
362
- }
363
- }
364
- if (!teamId) {
365
- res.status(404).json({ success: false, error: 'Team member not found for sessionName' });
366
- return;
367
- }
368
- // Create target path in in_progress/ folder
369
- const fileName = basename(taskPath);
370
- const targetPath = taskPath.replace('/open/', '/in_progress/');
371
- const targetDir = dirname(targetPath);
372
- // Ensure in_progress directory exists
373
- await ensureDirectoryExists(targetDir);
374
- // Read, update, and move task file
375
- const updatedContent = addTaskAssignmentInfo(taskContent, memberId, sessionName);
376
- await writeFile(targetPath, updatedContent, 'utf-8');
377
- await unlinkFile(taskPath);
378
- // Legacy TaskTrackingService removed — V3 WorkItems handle tracking
379
- res.json({
380
- success: true,
381
- message: `Task ${fileName} assigned to member ${sessionName}`,
382
- originalPath: taskPath,
383
- newPath: targetPath,
384
- memberId,
385
- sessionName,
386
- status: 'in_progress',
387
- });
388
- }
389
- catch (error) {
390
- logger.error('Error assigning task', { error: error instanceof Error ? error.message : String(error) });
391
- res.status(500).json({ success: false, error: 'Failed to assign task' });
392
- }
393
- });
394
- }
395
- /**
396
- * Completes a task by moving it from in_progress/ to done/ folder
397
- *
398
- * @param req - Request containing taskPath
399
- * @param res - Response with success status and completion information
400
- */
401
- export async function completeTask(req, res) {
402
- return TracingService.getInstance().withSpan(TRACING_CONSTANTS.SPANS.TASK_COMPLETE, {
403
- attributes: {
404
- 'task.path': req.body.taskPath || 'unknown',
405
- 'agent.session': req.body.sessionName || 'unknown',
406
- }
407
- }, async (span) => {
408
- try {
409
- const { taskPath, sessionName, output, qualityScore } = req.body;
410
- // Update agent heartbeat (proof of life)
411
- try {
412
- await updateAgentHeartbeat(sessionName, undefined, CREWLY_CONSTANTS.AGENT_STATUSES.ACTIVE);
413
- }
414
- catch (error) {
415
- logger.warn('Failed to update agent heartbeat', { error: error instanceof Error ? error.message : String(error) });
416
- // Continue execution - heartbeat failures shouldn't break task completion
417
- }
418
- if (!taskPath) {
419
- res.status(400).json({ success: false, error: 'taskPath is required' });
420
- return;
421
- }
422
- // Verify source task file exists
423
- if (!existsSync(taskPath)) {
424
- res.status(200).json({
425
- success: false,
426
- error: 'Task file does not exist at the specified path',
427
- details: `No task file found at: ${taskPath}. Make sure the task has been properly assigned and is in the in_progress folder.`,
428
- taskPath,
429
- suggestion: 'Check if the task file exists and use accept_task first to move it from open/ to in_progress/'
430
- });
431
- return;
432
- }
433
- // Ensure task is in in_progress/ folder
434
- if (!taskPath.includes('/in_progress/')) {
435
- res.status(200).json({
436
- success: false,
437
- error: 'Task is not in the correct folder for completion',
438
- details: `Task must be in in_progress/ folder to be completed. Current path: ${taskPath}. Use accept_task first to move the task from open/ to in_progress/.`,
439
- taskPath,
440
- expectedFolder: 'in_progress',
441
- currentFolder: taskPath.includes('/open/') ? 'open' : taskPath.includes('/done/') ? 'done' : taskPath.includes('/blocked/') ? 'blocked' : 'unknown',
442
- action: 'Use accept_task tool first to assign the task'
443
- });
444
- return;
445
- }
446
- // Read task content to check for output schema
447
- const taskContent = await readFile(taskPath, 'utf-8');
448
- const validator = TaskOutputValidatorService.getInstance();
449
- const schema = validator.extractSchemaFromMarkdown(taskContent);
450
- // If task has an output schema, validate the output
451
- if (schema) {
452
- if (!output || typeof output !== 'object') {
453
- res.status(200).json({
454
- success: false,
455
- error: 'Task requires structured output but none was provided',
456
- details: 'This task has an output schema. Provide an "output" object matching the schema.',
457
- taskPath,
458
- });
459
- return;
460
- }
461
- // Check output size
462
- const sizeCheck = validator.validateOutputSize(output);
463
- if (!sizeCheck.valid) {
464
- res.status(200).json({
465
- success: false,
466
- error: 'Output size exceeds maximum',
467
- details: sizeCheck.error,
468
- taskPath,
469
- });
470
- return;
471
- }
472
- // Validate output against schema
473
- const validationResult = validator.validate(output, schema);
474
- if (!validationResult.valid) {
475
- // Check retry info
476
- const existingRetryInfo = validator.extractRetryInfoFromMarkdown(taskContent);
477
- const retryCount = existingRetryInfo ? existingRetryInfo.retryCount + 1 : 1;
478
- const maxRetries = TASK_OUTPUT_CONSTANTS.MAX_RETRIES;
479
- if (retryCount > maxRetries) {
480
- // Max retries exceeded - move to blocked
481
- const blockedPath = taskPath.replace('/in_progress/', '/blocked/');
482
- const blockedDir = dirname(blockedPath);
483
- await ensureDirectoryExists(blockedDir);
484
- const failureInfo = `\n\n${TASK_OUTPUT_CONSTANTS.SECTION_HEADERS.VALIDATION_FAILURE}\n- **Status**: Blocked (max validation retries exceeded)\n- **Retry count**: ${retryCount}/${maxRetries}\n- **Errors**: ${validationResult.errors.join('; ')}\n- **Blocked at**: ${new Date().toISOString()}\n`;
485
- const blockedContent = taskContent + failureInfo;
486
- await writeFile(blockedPath, blockedContent, 'utf-8');
487
- await unlinkFile(taskPath);
488
- res.status(200).json({
489
- success: false,
490
- validationFailed: true,
491
- maxRetriesExceeded: true,
492
- errors: validationResult.errors,
493
- retryCount,
494
- maxRetries,
495
- message: `Task moved to blocked/ after ${maxRetries} failed validation attempts`,
496
- taskPath: blockedPath,
497
- });
498
- return;
499
- }
500
- // Retries remaining - update retry info in task file
501
- const retryInfo = {
502
- retryCount,
503
- maxRetries,
504
- lastErrors: validationResult.errors,
505
- lastAttemptAt: new Date().toISOString(),
506
- };
507
- // Remove existing retry info section if present, then append new one
508
- const retryHeader = TASK_OUTPUT_CONSTANTS.SECTION_HEADERS.RETRY_INFO;
509
- let updatedTaskContent = taskContent;
510
- const retryHeaderIdx = updatedTaskContent.indexOf(retryHeader);
511
- if (retryHeaderIdx !== -1) {
512
- // Find the end of the retry section (next ## header or end of file)
513
- const afterRetry = updatedTaskContent.substring(retryHeaderIdx + retryHeader.length);
514
- const nextSectionMatch = afterRetry.match(/\n## /);
515
- if (nextSectionMatch && nextSectionMatch.index !== undefined) {
516
- updatedTaskContent = updatedTaskContent.substring(0, retryHeaderIdx) +
517
- updatedTaskContent.substring(retryHeaderIdx + retryHeader.length + nextSectionMatch.index);
518
- }
519
- else {
520
- updatedTaskContent = updatedTaskContent.substring(0, retryHeaderIdx);
521
- }
522
- }
523
- updatedTaskContent += validator.generateRetryMarkdown(retryInfo);
524
- await writeFile(taskPath, updatedTaskContent, 'utf-8');
525
- res.status(200).json({
526
- success: false,
527
- validationFailed: true,
528
- errors: validationResult.errors,
529
- retryCount,
530
- maxRetries,
531
- message: `Output validation failed. ${maxRetries - retryCount} retries remaining.`,
532
- taskPath,
533
- });
534
- return;
535
- }
536
- // Validation passed - store output file alongside the done task
537
- const doneTargetPath = taskPath.replace('/in_progress/', '/done/');
538
- const outputFilePath = doneTargetPath.replace(/\.md$/, TASK_OUTPUT_CONSTANTS.OUTPUT_FILE_EXTENSION);
539
- const doneDir = dirname(doneTargetPath);
540
- await ensureDirectoryExists(doneDir);
541
- const outputData = {
542
- output,
543
- producedAt: new Date().toISOString(),
544
- sessionName: sessionName || 'unknown',
545
- };
546
- await writeFile(outputFilePath, JSON.stringify(outputData, null, 2), 'utf-8');
547
- // Move task to done
548
- const updatedContent = addTaskCompletionInfo(taskContent);
549
- await writeFile(doneTargetPath, updatedContent, 'utf-8');
550
- await unlinkFile(taskPath);
551
- // Remove from task tracking and clean up monitoring via TaskPool
552
- let cleanupResultSchema = { cancelledSchedules: 0, unsubscribedEvents: 0 };
553
- try {
554
- const { TaskPoolService } = await import('../../services/task-pool/task-pool.service.js');
555
- const pool = TaskPoolService.getInstance();
556
- const allItems = await pool.getAllItems();
557
- const matchingItem = allItems.find(wi => wi.target === sessionName && wi.status === 'running');
558
- if (matchingItem) {
559
- await pool.completeItem(matchingItem.id, { completedAt: new Date().toISOString() });
560
- }
561
- }
562
- catch (poolErr) {
563
- logger.debug('TaskPool cleanup skipped (non-fatal)', { error: poolErr instanceof Error ? poolErr.message : String(poolErr) });
564
- }
565
- res.json({
566
- success: true,
567
- message: `Task ${basename(taskPath)} marked as completed with validated output`,
568
- originalPath: taskPath,
569
- newPath: doneTargetPath,
570
- outputPath: outputFilePath,
571
- status: 'done',
572
- completedAt: new Date().toISOString(),
573
- monitoringCleanup: cleanupResultSchema,
574
- });
575
- // V3.1 Projection (fire-and-forget)
576
- if (sessionName)
577
- projectTaskCompleted(sessionName, basename(taskPath, '.md'));
578
- return;
579
- }
580
- // No schema - original behavior (backward compatible)
581
- const fileName = basename(taskPath);
582
- const targetPath = taskPath.replace('/in_progress/', '/done/');
583
- const targetDir = dirname(targetPath);
584
- // Ensure done directory exists
585
- await ensureDirectoryExists(targetDir);
586
- // Read, update, and move task file
587
- const updatedContent = addTaskCompletionInfo(taskContent);
588
- await writeFile(targetPath, updatedContent, 'utf-8');
589
- await unlinkFile(taskPath);
590
- // Remove from task tracking and clean up monitoring via TaskPool
591
- let cleanupResult = { cancelledSchedules: 0, unsubscribedEvents: 0 };
592
- try {
593
- const { TaskPoolService } = await import('../../services/task-pool/task-pool.service.js');
594
- const pool = TaskPoolService.getInstance();
595
- const allItems = await pool.getAllItems();
596
- const matchingItem = allItems.find(wi => wi.target === sessionName && wi.status === 'running');
597
- if (matchingItem) {
598
- await pool.completeItem(matchingItem.id, { completedAt: new Date().toISOString() });
599
- }
600
- }
601
- catch (poolErr) {
602
- logger.debug('TaskPool cleanup skipped (non-fatal)', { error: poolErr instanceof Error ? poolErr.message : String(poolErr) });
603
- }
604
- res.json({
605
- success: true,
606
- message: `Task ${fileName} marked as completed`,
607
- originalPath: taskPath,
608
- newPath: targetPath,
609
- status: 'done',
610
- completedAt: new Date().toISOString(),
611
- monitoringCleanup: cleanupResult,
612
- });
613
- // V3.1 Projection (fire-and-forget)
614
- if (sessionName)
615
- projectTaskCompleted(sessionName, basename(taskPath, '.md'));
616
- }
617
- catch (error) {
618
- logger.error('Error completing task', { error: error instanceof Error ? error.message : String(error) });
619
- res.status(500).json({ success: false, error: 'Failed to complete task' });
620
- }
621
- });
622
- }
623
- /**
624
- * Blocks a task by moving it from in_progress/ to blocked/ folder
625
- *
626
- * @param req - Request containing taskPath and blockReason
627
- * @param res - Response with success status and block information
628
- */
629
- export async function blockTask(req, res) {
630
- try {
631
- const { taskPath, blockReason } = req.body;
632
- if (!taskPath) {
633
- res.status(400).json({ success: false, error: 'taskPath is required' });
634
- return;
635
- }
636
- // Verify source task file exists
637
- if (!existsSync(taskPath)) {
638
- res.status(200).json({
639
- success: false,
640
- error: 'Task file does not exist at the specified path',
641
- details: `No task file found at: ${taskPath}. Make sure the task has been properly assigned and is in the in_progress folder.`,
642
- taskPath,
643
- suggestion: 'Check if the task file exists and use accept_task first to move it from open/ to in_progress/'
644
- });
645
- return;
646
- }
647
- // Ensure task is in in_progress/ folder
648
- if (!taskPath.includes('/in_progress/')) {
649
- res.status(200).json({
650
- success: false,
651
- error: 'Task is not in the correct folder for blocking',
652
- details: `Task must be in in_progress/ folder to be blocked. Current path: ${taskPath}. Use accept_task first to assign the task.`,
653
- taskPath,
654
- expectedFolder: 'in_progress',
655
- currentFolder: taskPath.includes('/open/') ? 'open' : taskPath.includes('/done/') ? 'done' : taskPath.includes('/blocked/') ? 'blocked' : 'unknown',
656
- action: 'Use accept_task tool first to assign the task'
657
- });
658
- return;
659
- }
660
- // Create target path in blocked/ folder
661
- const fileName = basename(taskPath);
662
- const targetPath = taskPath.replace('/in_progress/', '/blocked/');
663
- const targetDir = dirname(targetPath);
664
- // Ensure blocked directory exists
665
- await ensureDirectoryExists(targetDir);
666
- // Read, update, and move task file
667
- const taskContent = await readFile(taskPath, 'utf-8');
668
- const updatedContent = addTaskBlockInfo(taskContent, blockReason);
669
- await writeFile(targetPath, updatedContent, 'utf-8');
670
- await unlinkFile(taskPath);
671
- res.json({
672
- success: true,
673
- message: `Task ${fileName} marked as blocked`,
674
- originalPath: taskPath,
675
- newPath: targetPath,
676
- status: 'blocked',
677
- blockReason: blockReason || 'No reason provided',
678
- blockedAt: new Date().toISOString(),
679
- });
680
- // V3.1 Projection (fire-and-forget)
681
- const { sessionName: blockSession } = req.body;
682
- if (blockSession)
683
- projectTaskBlocked(blockSession, blockReason || 'unspecified', basename(taskPath, '.md'));
684
- }
685
- catch (error) {
686
- logger.error('Error blocking task', { error: error instanceof Error ? error.message : String(error) });
687
- res.status(500).json({ success: false, error: 'Failed to block task' });
688
- }
689
- }
690
- /**
691
- * Unblocks a task by moving it from blocked/ to open/ folder for reassignment
692
- *
693
- * @param req - Request containing taskPath and optional unblockNote
694
- * @param res - Response with success status and unblock information
695
- */
696
- export async function unblockTask(req, res) {
697
- try {
698
- const { taskPath, unblockNote } = req.body;
699
- if (!taskPath) {
700
- res.status(400).json({ success: false, error: 'taskPath is required' });
701
- return;
702
- }
703
- // Verify task file exists
704
- if (!existsSync(taskPath)) {
705
- res.status(404).json({
706
- success: false,
707
- error: 'Task file not found',
708
- path: taskPath,
709
- });
710
- return;
711
- }
712
- // Verify task is in blocked/ folder
713
- if (!taskPath.includes('/blocked/')) {
714
- res.status(400).json({
715
- success: false,
716
- error: 'Task is not in the blocked folder',
717
- details: `Task must be in blocked/ folder to be unblocked. Current path: ${taskPath}`,
718
- currentFolder: taskPath.includes('/open/') ? 'open' : taskPath.includes('/in_progress/') ? 'in_progress' : taskPath.includes('/done/') ? 'done' : 'unknown',
719
- });
720
- return;
721
- }
722
- // Read current task content
723
- const taskContent = await readFile(taskPath, 'utf-8');
724
- const fileName = basename(taskPath);
725
- // Create target path in open/ folder
726
- const targetPath = taskPath.replace('/blocked/', '/open/');
727
- // Ensure open directory exists
728
- await mkdir(dirname(targetPath), { recursive: true });
729
- // Add unblock information to task content
730
- const updatedContent = addTaskUnblockInfo(taskContent, unblockNote);
731
- // Move file and update content
732
- await writeFile(targetPath, updatedContent, 'utf-8');
733
- await unlink(taskPath);
734
- res.json({
735
- success: true,
736
- message: `Task ${fileName} unblocked and moved to open folder for reassignment`,
737
- taskPath: targetPath,
738
- fileName,
739
- status: 'open',
740
- unblockNote: unblockNote || 'No note provided',
741
- unblockedAt: new Date().toISOString(),
742
- });
743
- }
744
- catch (error) {
745
- logger.error('Error unblocking task', { error: error instanceof Error ? error.message : String(error) });
746
- res.status(500).json({ success: false, error: 'Failed to unblock task' });
747
- }
748
- }
749
- /**
750
- * Gets the next available task from the open/ folder
751
- *
752
- * @param req - Request containing optional taskGroup filter
753
- * @param res - Response with next available task information
754
- */
755
- export async function takeNextTask(req, res) {
756
- try {
757
- const { taskGroup, projectPath } = req.body;
758
- if (!projectPath) {
759
- res.status(400).json({ success: false, error: 'projectPath is required' });
760
- return;
761
- }
762
- // Construct path to open tasks
763
- const openTasksPath = taskGroup
764
- ? join(projectPath, '.crewly', 'tasks', taskGroup, 'open')
765
- : join(projectPath, '.crewly', 'tasks', 'm0_build_spec_tasks', 'open');
766
- if (!existsSync(openTasksPath)) {
767
- res.status(404).json({
768
- success: false,
769
- error: 'No open tasks directory found',
770
- path: openTasksPath,
771
- });
772
- return;
773
- }
774
- // Get all .md files in open directory
775
- const files = await readdir(openTasksPath);
776
- const taskFiles = files.filter((f) => f.endsWith('.md')).sort();
777
- if (taskFiles.length === 0) {
778
- res.status(404).json({
779
- success: false,
780
- error: 'No open tasks available',
781
- path: openTasksPath,
782
- });
783
- return;
784
- }
785
- // Get the first task (sorted alphabetically)
786
- const nextTaskFile = taskFiles[0];
787
- const taskPath = join(openTasksPath, nextTaskFile);
788
- const taskContent = await readFile(taskPath, 'utf-8');
789
- // Parse basic task info from markdown
790
- const taskInfo = parseTaskInfo(taskContent, nextTaskFile);
791
- res.json({
792
- success: true,
793
- task: {
794
- taskPath,
795
- ...taskInfo,
796
- },
797
- totalAvailable: taskFiles.length,
798
- openTasksPath,
799
- });
800
- // V3.1 Projection: record that this agent accepted a task (fire-and-forget)
801
- const { sessionName: sn } = req.body;
802
- if (sn) {
803
- projectTaskAccepted(taskInfo.title || nextTaskFile, taskPath, sn);
804
- }
805
- }
806
- catch (error) {
807
- logger.error('Error getting next task', { error: error instanceof Error ? error.message : String(error) });
808
- res.status(500).json({ success: false, error: 'Failed to get next task' });
809
- }
810
- }
811
- /**
812
- * Synchronizes task status across the system
813
- *
814
- * @param req - Request containing projectPath and optional taskGroup
815
- * @param res - Response with sync status and task counts
816
- */
817
- export async function syncTaskStatus(req, res) {
818
- try {
819
- const { projectPath, taskGroup } = req.body;
820
- if (!projectPath) {
821
- res.status(400).json({ success: false, error: 'projectPath is required' });
822
- return;
823
- }
824
- // Construct base tasks path
825
- const basePath = taskGroup
826
- ? join(projectPath, '.crewly', 'tasks', taskGroup)
827
- : join(projectPath, '.crewly', 'tasks', 'm0_build_spec_tasks');
828
- if (!existsSync(basePath)) {
829
- res.status(404).json({
830
- success: false,
831
- error: 'Tasks directory not found',
832
- path: basePath,
833
- });
834
- return;
835
- }
836
- // Count tasks in each status folder
837
- const statusCounts = {
838
- open: 0,
839
- in_progress: 0,
840
- blocked: 0,
841
- done: 0,
842
- };
843
- const statusDirs = ['open', 'in_progress', 'blocked', 'done'];
844
- for (const status of statusDirs) {
845
- const statusPath = join(basePath, status);
846
- if (existsSync(statusPath)) {
847
- const files = await readdir(statusPath);
848
- statusCounts[status] = files.filter((f) => f.endsWith('.md')).length;
849
- }
850
- }
851
- // Calculate progress percentage
852
- const totalTasks = Object.values(statusCounts).reduce((sum, count) => sum + count, 0);
853
- const completedTasks = statusCounts.done;
854
- const progressPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
855
- res.json({
856
- success: true,
857
- syncedAt: new Date().toISOString(),
858
- projectPath,
859
- taskGroup: taskGroup || 'm0_build_spec_tasks',
860
- statusCounts,
861
- totalTasks,
862
- completedTasks,
863
- progressPercentage,
864
- });
865
- }
866
- catch (error) {
867
- logger.error('Error syncing task status', { error: error instanceof Error ? error.message : String(error) });
868
- res.status(500).json({ success: false, error: 'Failed to sync task status' });
869
- }
870
- }
871
- /**
872
- * Gets team progress across all task groups for a project
873
- *
874
- * @param req - Request containing projectPath
875
- * @param res - Response with comprehensive team progress information
876
- */
877
- export async function getTeamProgress(req, res) {
878
- try {
879
- const { projectPath } = req.body;
880
- if (!projectPath) {
881
- res.status(400).json({ success: false, error: 'projectPath is required' });
882
- return;
883
- }
884
- const tasksBasePath = join(projectPath, '.crewly', 'tasks');
885
- if (!existsSync(tasksBasePath)) {
886
- res.status(404).json({
887
- success: false,
888
- error: 'Tasks directory not found',
889
- path: tasksBasePath,
890
- });
891
- return;
892
- }
893
- // Get all task groups (subdirectories in tasks/)
894
- const taskGroups = await readdir(tasksBasePath);
895
- const progressData = [];
896
- for (const taskGroup of taskGroups) {
897
- const groupPath = join(tasksBasePath, taskGroup);
898
- const groupStat = await stat(groupPath);
899
- if (groupStat.isDirectory()) {
900
- const statusCounts = {
901
- open: 0,
902
- in_progress: 0,
903
- blocked: 0,
904
- done: 0,
905
- };
906
- const statusDirs = ['open', 'in_progress', 'blocked', 'done'];
907
- for (const status of statusDirs) {
908
- const statusPath = join(groupPath, status);
909
- if (existsSync(statusPath)) {
910
- const files = await readdir(statusPath);
911
- statusCounts[status] = files.filter((f) => f.endsWith('.md')).length;
912
- }
913
- }
914
- const totalTasks = Object.values(statusCounts).reduce((sum, count) => sum + count, 0);
915
- const completedTasks = statusCounts.done;
916
- const progressPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
917
- progressData.push({
918
- taskGroup,
919
- statusCounts,
920
- totalTasks,
921
- completedTasks,
922
- progressPercentage,
923
- });
924
- }
925
- }
926
- // Calculate overall progress
927
- const overallStats = progressData.reduce((acc, group) => ({
928
- totalTasks: acc.totalTasks + group.totalTasks,
929
- completedTasks: acc.completedTasks + group.completedTasks,
930
- }), { totalTasks: 0, completedTasks: 0 });
931
- const overallProgress = overallStats.totalTasks > 0
932
- ? Math.round((overallStats.completedTasks / overallStats.totalTasks) * 100)
933
- : 0;
934
- res.json({
935
- success: true,
936
- projectPath,
937
- reportedAt: new Date().toISOString(),
938
- overallProgress,
939
- overallStats,
940
- taskGroups: progressData,
941
- });
942
- }
943
- catch (error) {
944
- logger.error('Error getting team progress', { error: error instanceof Error ? error.message : String(error) });
945
- res.status(500).json({ success: false, error: 'Failed to get team progress' });
946
- }
947
- }
948
- /**
949
- * Starts task execution by sending assignment prompt to orchestrator
950
- * Initiates monitoring process to detect task acceptance with retry logic
951
- *
952
- * @param req - Request containing task execution parameters
953
- * @param res - Response with execution status and monitoring information
954
- */
955
- export async function startTaskExecution(req, res) {
956
- try {
957
- const { taskPath, projectPath, projectName, taskId, taskTitle, taskDescription, taskPriority = 'medium', taskMilestone = 'm0_initial_tasks', retryCount = 3, timeoutSeconds = 30, } = req.body;
958
- if (!taskPath || !projectPath || !projectName) {
959
- res.status(400).json({
960
- success: false,
961
- error: 'Missing required fields: taskPath, projectPath, projectName',
962
- });
963
- return;
964
- }
965
- // Verify task file exists in open/ folder
966
- if (!existsSync(taskPath)) {
967
- res.status(404).json({
968
- success: false,
969
- error: `Task file not found: ${taskPath}`,
970
- });
971
- return;
972
- }
973
- if (!taskPath.includes('/open/')) {
974
- res.status(400).json({
975
- success: false,
976
- error: 'Task must be in open/ folder to be started',
977
- });
978
- return;
979
- }
980
- // Load assignment prompt template
981
- const promptTemplate = await readFile(join(process.cwd(), 'config', 'orchestrator_tasks', 'prompts', 'assign-task-orchestrator-prompt-template.md'), 'utf-8');
982
- // Replace template variables
983
- const assignmentPrompt = promptTemplate
984
- .replace(/\{projectName\}/g, projectName)
985
- .replace(/\{projectPath\}/g, projectPath)
986
- .replace(/\{taskId\}/g, taskId || basename(taskPath, '.md'))
987
- .replace(/\{taskTitle\}/g, taskTitle || 'Task Assignment')
988
- .replace(/\{taskDescription\}/g, taskDescription || 'Please check task file for details')
989
- .replace(/\{taskPriority\}/g, taskPriority)
990
- .replace(/\{taskMilestone\}/g, taskMilestone);
991
- // Find orchestrator session
992
- const sessions = await this.tmuxService.listSessions();
993
- const orchestratorSession = sessions.find((s) => s.sessionName.includes('orc') || s.sessionName.includes('orchestrator'));
994
- if (!orchestratorSession) {
995
- res.status(404).json({
996
- success: false,
997
- error: 'Orchestrator session not found. Please ensure orchestrator is running.',
998
- });
999
- return;
1000
- }
1001
- // Send assignment prompt to orchestrator
1002
- await this.tmuxService.sendMessage(orchestratorSession.sessionName, assignmentPrompt);
1003
- // Start monitoring for task acceptance
1004
- const monitoringId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1005
- // Initialize task assignment monitor
1006
- const monitorResult = await this.taskAssignmentMonitor.startMonitoring({
1007
- monitoringId,
1008
- taskPath,
1009
- originalPath: taskPath,
1010
- targetPath: taskPath.replace('/open/', '/in_progress/'),
1011
- orchestratorSession: orchestratorSession.sessionName,
1012
- assignmentPrompt,
1013
- retryCount,
1014
- timeoutSeconds,
1015
- projectPath,
1016
- taskId: taskId || basename(taskPath, '.md'),
1017
- });
1018
- res.json({
1019
- success: true,
1020
- message: `Task execution started. Assignment sent to orchestrator.`,
1021
- monitoringId,
1022
- orchestratorSession: orchestratorSession.sessionName,
1023
- taskPath,
1024
- monitoring: monitorResult,
1025
- retryCount,
1026
- timeoutSeconds,
1027
- });
1028
- }
1029
- catch (error) {
1030
- logger.error('Error starting task execution', { error: error instanceof Error ? error.message : String(error) });
1031
- res.status(500).json({
1032
- success: false,
1033
- error: 'Failed to start task execution',
1034
- details: error instanceof Error ? error.message : String(error),
1035
- });
1036
- }
1037
- }
1038
- /**
1039
- * Recovers abandoned in-progress tasks when orchestrator starts
1040
- *
1041
- * @param req - Request containing sessionName (optional)
1042
- * @param res - Response with recovery report
1043
- */
1044
- export async function recoverAbandonedTasks(req, res) {
1045
- // Legacy — no-op, will be rebuilt on V3 WorkItem system
1046
- res.json({ success: true, message: 'Deprecated — use TaskPool API', data: { recovered: 0, skipped: 0, total: 0 } });
1047
- }
1048
- export async function createTasksFromConfig(req, res) {
1049
- try {
1050
- const { projectId, projectName, projectPath, configType } = req.body;
1051
- if (!projectId || !projectName || !projectPath) {
1052
- res.status(400).json({
1053
- success: false,
1054
- error: 'Missing required fields: projectId, projectName, projectPath',
1055
- });
1056
- return;
1057
- }
1058
- // Read initial goal and user journey from saved .md files
1059
- const specsPath = join(projectPath, '.crewly', 'specs');
1060
- const goalFilePath = join(specsPath, 'initial_goal.md');
1061
- const journeyFilePath = join(specsPath, 'initial_user_journey.md');
1062
- let initialGoal = 'No specific goal provided';
1063
- let userJourney = 'Standard user workflow - register, login, use core features';
1064
- try {
1065
- if (existsSync(goalFilePath)) {
1066
- initialGoal = await readFile(goalFilePath, 'utf-8');
1067
- logger.info('Loaded initial goal', { goalFilePath });
1068
- }
1069
- else {
1070
- logger.warn('Initial goal file not found', { goalFilePath });
1071
- }
1072
- }
1073
- catch (error) {
1074
- logger.warn('Failed to read initial goal file', { error: error instanceof Error ? error.message : String(error) });
1075
- }
1076
- try {
1077
- if (existsSync(journeyFilePath)) {
1078
- userJourney = await readFile(journeyFilePath, 'utf-8');
1079
- logger.info('Loaded user journey', { journeyFilePath });
1080
- }
1081
- else {
1082
- logger.warn('Initial user journey file not found', { journeyFilePath });
1083
- }
1084
- }
1085
- catch (error) {
1086
- logger.warn('Failed to read initial user journey file', { error: error instanceof Error ? error.message : String(error) });
1087
- }
1088
- // Load the configuration based on configType
1089
- const configPath = join(process.cwd(), 'config', 'task_starters', `${configType}.json`);
1090
- let configContent;
1091
- try {
1092
- configContent = JSON.parse(await readFile(configPath, 'utf-8'));
1093
- }
1094
- catch (error) {
1095
- logger.error('Error loading config', { configType, error: error instanceof Error ? error.message : String(error) });
1096
- res.status(500).json({
1097
- success: false,
1098
- error: `Failed to load ${configType} configuration`,
1099
- });
1100
- return;
1101
- }
1102
- if (!configContent.steps || !Array.isArray(configContent.steps)) {
1103
- res.status(500).json({
1104
- success: false,
1105
- error: `Invalid ${configType} configuration: missing steps`,
1106
- });
1107
- return;
1108
- }
1109
- // Create tasks directory for initial tasksß
1110
- const taskDirName = 'm0_initial_tasks';
1111
- const tasksDir = join(projectPath, '.crewly', 'tasks', taskDirName, 'open');
1112
- try {
1113
- await ensureDirectoryExists(tasksDir);
1114
- }
1115
- catch (error) {
1116
- logger.error('Error creating tasks directory', { error: error instanceof Error ? error.message : String(error) });
1117
- res.status(500).json({ success: false, error: 'Failed to create tasks directory' });
1118
- return;
1119
- }
1120
- // Generate task files from configuration steps
1121
- const createdTasks = [];
1122
- for (let i = 0; i < configContent.steps.length; i++) {
1123
- const step = configContent.steps[i];
1124
- const taskNumber = String(i + 1).padStart(2, '0');
1125
- const fileName = `${taskNumber}_${step.name
1126
- .toLowerCase()
1127
- .replace(/[^a-z0-9]+/g, '_')}.md`;
1128
- const filePath = join(tasksDir, fileName);
1129
- // Generate task markdown content using loaded values from .md files
1130
- const taskContent = await generateTaskMarkdown(step, {
1131
- projectId,
1132
- projectName,
1133
- projectPath,
1134
- initialGoal,
1135
- userJourney,
1136
- });
1137
- try {
1138
- await writeFile(filePath, taskContent, 'utf-8');
1139
- createdTasks.push({
1140
- step: i + 1,
1141
- name: step.name,
1142
- fileName,
1143
- targetRole: step.targetRole,
1144
- delayMinutes: step.delayMinutes || 0,
1145
- });
1146
- }
1147
- catch (error) {
1148
- logger.error('Error creating task file', { fileName, error: error instanceof Error ? error.message : String(error) });
1149
- res.status(500).json({
1150
- success: false,
1151
- error: `Failed to create task file: ${fileName}`,
1152
- });
1153
- return;
1154
- }
1155
- }
1156
- res.json({
1157
- success: true,
1158
- message: `Successfully created ${createdTasks.length} build spec task files in ${tasksDir}`,
1159
- tasksDirectory: tasksDir,
1160
- createdTasks,
1161
- totalSteps: configContent.steps.length,
1162
- });
1163
- }
1164
- catch (error) {
1165
- logger.error('Error creating tasks from config', { error: error instanceof Error ? error.message : String(error) });
1166
- res.status(500).json({
1167
- success: false,
1168
- error: 'Failed to create tasks from configuration',
1169
- });
1170
- }
1171
- }
1172
- // Helper function to ensure directory exists
1173
- async function ensureDirectoryExists(dirPath) {
1174
- if (!existsSync(dirPath)) {
1175
- await mkdir(dirPath, { recursive: true });
1176
- }
1177
- }
1178
- // Helper function to generate task markdown content
1179
- async function generateTaskMarkdown(step, projectVars) {
1180
- const { projectId, projectName, projectPath, initialGoal, userJourney } = projectVars;
1181
- // Resolve prompts using the prompt resolver utility
1182
- const templateVars = {
1183
- PROJECT_NAME: projectName,
1184
- PROJECT_PATH: projectPath,
1185
- INITIAL_GOAL: initialGoal,
1186
- USER_JOURNEY: userJourney,
1187
- PROJECT_ID: projectId
1188
- };
1189
- // Use prompt resolver to handle both prompt_file and legacy prompts array
1190
- const resolvedStep = await resolveStepConfig(step, templateVars);
1191
- const processedPrompts = resolvedStep.prompts;
1192
- // Generate markdown content
1193
- const markdown = `# ${step.name}
1194
-
1195
- ## Task Information
1196
- - **Step**: ${step.id}
1197
- - **Target Role**: ${step.targetRole}
1198
- - **Estimated Delay**: ${step.delayMinutes || 0} minutes
1199
- - **Project**: ${projectName}
1200
- - **Project Path**: ${projectPath}
1201
-
1202
- ## Project Context
1203
- **Initial Goal**: ${initialGoal}
1204
-
1205
- **User Journey**: ${userJourney}
1206
-
1207
- ## Task Description
1208
-
1209
- ${processedPrompts.join('\n\n')}
1210
-
1211
- ## Verification Criteria
1212
- ${step.verification
1213
- ? `
1214
- - **Type**: ${step.verification.type}
1215
- ${step.verification.paths ? `- **Required Paths**: ${step.verification.paths.join(', ')}` : ''}
1216
- ${step.verification.min_files ? `- **Minimum Files**: ${step.verification.min_files}` : ''}
1217
- ${step.verification.file_pattern ? `- **File Pattern**: ${step.verification.file_pattern}` : ''}
1218
- `
1219
- : 'No specific verification criteria defined.'}
1220
-
1221
- ## Conditional Requirements
1222
- ${step.conditional
1223
- ? `This task should only be executed: ${step.conditional}`
1224
- : 'No conditional requirements.'}
1225
-
1226
- ---
1227
- *Generated from configuration with prompt resolution*
1228
- *Task can be assigned to orchestrator for execution when ready*
1229
- `;
1230
- // Embed output schema if present in step config
1231
- if (step.outputSchema && typeof step.outputSchema === 'object') {
1232
- const validator = TaskOutputValidatorService.getInstance();
1233
- return markdown + validator.generateSchemaMarkdown(step.outputSchema);
1234
- }
1235
- return markdown;
1236
- }
1237
- // Helper function to delete a file
1238
- async function unlinkFile(filePath) {
1239
- const { unlink } = await import('fs/promises');
1240
- await unlink(filePath);
1241
- }
1242
- // Helper function to add assignment information to task content
1243
- function addTaskAssignmentInfo(content, memberId, sessionName) {
1244
- const assignmentInfo = `\n\n## Assignment Information\n- **Assigned to**: ${memberId}\n- **Session**: ${sessionName || 'N/A'}\n- **Assigned at**: ${new Date().toISOString()}\n- **Status**: In Progress\n`;
1245
- return content + assignmentInfo;
1246
- }
1247
- // Helper function to add completion information to task content
1248
- function addTaskCompletionInfo(content) {
1249
- const completionInfo = `\n\n## Completion Information\n- **Status**: Completed\n- **Completed at**: ${new Date().toISOString()}\n`;
1250
- return content + completionInfo;
1251
- }
1252
- // Helper function to add block information to task content
1253
- function addTaskBlockInfo(content, blockReason) {
1254
- const blockInfo = `\n\n## Block Information\n- **Status**: Blocked\n- **Block reason**: ${blockReason || 'No reason provided'}\n- **Blocked at**: ${new Date().toISOString()}\n`;
1255
- return content + blockInfo;
1256
- }
1257
- // Helper function to add unblock information to task content
1258
- function addTaskUnblockInfo(content, unblockNote) {
1259
- const unblockInfo = `\n\n## Unblock Information\n- **Status**: Unblocked (moved to open for reassignment)\n- **Unblock note**: ${unblockNote || 'No note provided'}\n- **Unblocked at**: ${new Date().toISOString()}\n`;
1260
- return content + unblockInfo;
1261
- }
1262
- // Helper function to parse basic task information from markdown content
1263
- function parseTaskInfo(content, fileName) {
1264
- const lines = content.split('\n');
1265
- const info = { fileName };
1266
- // Extract title (first # heading)
1267
- const titleMatch = lines.find((line) => line.startsWith('# '));
1268
- if (titleMatch) {
1269
- info.title = titleMatch.substring(2).trim();
1270
- }
1271
- // Extract metadata from the task information section (lines starting with - **Key**: Value)
1272
- lines.forEach(line => {
1273
- const match = line.match(/^- \*\*([^*]+)\*\*: (.*)$/);
1274
- if (match) {
1275
- const key = match[1].trim().toLowerCase().replace(/ /g, '');
1276
- const value = match[2].trim();
1277
- // Map specific keys if needed, otherwise use the lowercased key
1278
- if (key === 'targetrole')
1279
- info.targetRole = value;
1280
- else if (key === 'estimateddelay')
1281
- info.estimatedDelay = value;
1282
- else
1283
- info[key] = value;
1284
- }
1285
- });
1286
- return info;
1287
- }
1288
- /**
1289
- * Reads a task file from the filesystem with security validation
1290
- *
1291
- * @param req - Request containing taskPath
1292
- * @param res - Response with task file content
1293
- */
1294
- export async function readTask(req, res) {
1295
- try {
1296
- const { taskPath } = req.body;
1297
- if (!taskPath) {
1298
- res.status(400).json({ success: false, error: 'taskPath is required' });
1299
- return;
1300
- }
1301
- // Security validation: ensure path is within allowed directories
1302
- const resolvedPath = resolve(taskPath);
1303
- const allowedPattern = /\.crewly[\/\\]tasks[\/\\]/;
1304
- if (!allowedPattern.test(resolvedPath)) {
1305
- res.status(403).json({
1306
- success: false,
1307
- error: 'Access denied: path must be within .crewly/tasks/ directory'
1308
- });
1309
- return;
1310
- }
1311
- // Verify file exists
1312
- if (!existsSync(taskPath)) {
1313
- res.status(200).json({
1314
- success: false,
1315
- error: 'Task file does not exist at the specified path',
1316
- details: `No task file found at: ${taskPath}. Verify the path is correct and the file exists.`,
1317
- taskPath,
1318
- suggestion: 'Check that the file path is correct and the file exists in the filesystem'
1319
- });
1320
- return;
1321
- }
1322
- // Read file content
1323
- const content = await readFile(taskPath, 'utf-8');
1324
- const metadata = parseTaskInfo(content, basename(taskPath));
1325
- res.json({
1326
- success: true,
1327
- data: {
1328
- content: content,
1329
- metadata: metadata,
1330
- taskPath: taskPath,
1331
- fileSize: content.length,
1332
- }
1333
- });
1334
- }
1335
- catch (error) {
1336
- logger.error('Error reading task', { error: error instanceof Error ? error.message : String(error) });
1337
- res.status(500).json({ success: false, error: 'Failed to read task file' });
1338
- }
1339
- }
1340
- /**
1341
- * Retrieves the stored output JSON for a completed task.
1342
- *
1343
- * @param req - Request containing taskPath (path to the .md file in done/)
1344
- * @param res - Response with the parsed output data
1345
- */
1346
- export async function getTaskOutput(req, res) {
1347
- try {
1348
- const { taskPath } = req.body;
1349
- if (!taskPath) {
1350
- res.status(400).json({ success: false, error: 'taskPath is required' });
1351
- return;
1352
- }
1353
- // Derive the output file path from the task file path
1354
- const outputFilePath = taskPath.replace(/\.md$/, TASK_OUTPUT_CONSTANTS.OUTPUT_FILE_EXTENSION);
1355
- if (!existsSync(outputFilePath)) {
1356
- res.status(200).json({
1357
- success: false,
1358
- error: 'No output file found for this task',
1359
- details: `Expected output at: ${outputFilePath}`,
1360
- taskPath,
1361
- });
1362
- return;
1363
- }
1364
- const rawContent = await readFile(outputFilePath, 'utf-8');
1365
- const outputData = JSON.parse(rawContent);
1366
- res.json({
1367
- success: true,
1368
- data: outputData,
1369
- outputPath: outputFilePath,
1370
- });
1371
- }
1372
- catch (error) {
1373
- logger.error('Error getting task output', { error: error instanceof Error ? error.message : String(error) });
1374
- res.status(500).json({ success: false, error: 'Failed to get task output' });
1375
- }
1376
- }
1377
- /**
1378
- * POST /api/task-management/request-review
1379
- *
1380
- * Request a code review for a completed task. Logs the review request
1381
- * and returns success. In the future this may notify the reviewer agent
1382
- * or broadcast a review request to the team.
1383
- *
1384
- * @param req - Request with body: { ticketId, reviewer?, message?, branch? }
1385
- * @param res - Response with { success, data: { message: string } }
1386
- */
1387
- export async function requestReview(req, res) {
1388
- try {
1389
- const { ticketId, reviewer, message, branch } = req.body;
1390
- if (!ticketId) {
1391
- res.status(400).json({
1392
- success: false,
1393
- error: 'ticketId is required',
1394
- });
1395
- return;
1396
- }
1397
- logger.info('Review requested', {
1398
- ticketId,
1399
- reviewer: reviewer || 'any',
1400
- branch: branch || 'current',
1401
- message: message || 'No message provided',
1402
- });
1403
- res.json({
1404
- success: true,
1405
- data: {
1406
- message: 'Review requested',
1407
- ticketId,
1408
- reviewer: reviewer || null,
1409
- branch: branch || null,
1410
- requestedAt: new Date().toISOString(),
1411
- },
1412
- });
1413
- }
1414
- catch (error) {
1415
- logger.error('Error requesting review', { error: error instanceof Error ? error.message : String(error) });
1416
- res.status(500).json({
1417
- success: false,
1418
- error: 'Failed to request review',
1419
- });
1420
- }
1421
- }
1422
- /**
1423
- * Cleans up monitoring resources (scheduled checks and event subscriptions)
1424
- * linked to a task. Called automatically when a task is completed.
1425
- *
1426
- * @param controller - The ApiController instance (for schedulerService access)
1427
- * @param task - The task whose monitoring to clean up
1428
- * @returns Object with counts of cancelled schedules and unsubscribed events
1429
- */
1430
- async function cleanupTaskMonitoring(controller, task) {
1431
- let cancelledSchedules = 0;
1432
- let unsubscribedEvents = 0;
1433
- // Cancel linked schedules
1434
- if (task.scheduleIds && task.scheduleIds.length > 0) {
1435
- for (const scheduleId of task.scheduleIds) {
1436
- try {
1437
- controller.schedulerService.cancelCheck(scheduleId);
1438
- cancelledSchedules++;
1439
- }
1440
- catch (error) {
1441
- logger.warn('Failed to cancel schedule during task cleanup', {
1442
- scheduleId,
1443
- error: error instanceof Error ? error.message : String(error),
1444
- });
1445
- }
1446
- }
1447
- }
1448
- // Unsubscribe linked event subscriptions
1449
- if (task.subscriptionIds && task.subscriptionIds.length > 0 && eventBusService) {
1450
- for (const subscriptionId of task.subscriptionIds) {
1451
- try {
1452
- eventBusService.unsubscribe(subscriptionId);
1453
- unsubscribedEvents++;
1454
- }
1455
- catch (error) {
1456
- logger.warn('Failed to unsubscribe event during task cleanup', {
1457
- subscriptionId,
1458
- error: error instanceof Error ? error.message : String(error),
1459
- });
1460
- }
1461
- }
1462
- }
1463
- if (cancelledSchedules > 0 || unsubscribedEvents > 0) {
1464
- logger.info('Task monitoring cleaned up', { cancelledSchedules, unsubscribedEvents });
1465
- }
1466
- return { cancelledSchedules, unsubscribedEvents };
1467
- }
1468
- export async function addMonitoring(req, res) {
1469
- // Legacy — no-op, will be rebuilt on V3 WorkItem system
1470
- res.json({ success: true, message: 'Deprecated — use TaskPool API' });
1471
- }
1472
- /**
1473
- * Completes all tasks assigned to a given agent session.
1474
- * Finds running WorkItems for the session in the TaskPool and marks them done.
1475
- *
1476
- * @param req - Request containing sessionName
1477
- * @param res - Response with list of completed tasks
1478
- */
1479
- export async function completeTasksBySession(req, res) {
1480
- try {
1481
- const { sessionName } = req.body;
1482
- if (!sessionName) {
1483
- res.status(400).json({ success: false, error: 'sessionName is required' });
1484
- return;
1485
- }
1486
- const { TaskPoolService } = await import('../../services/task-pool/task-pool.service.js');
1487
- const pool = TaskPoolService.getInstance();
1488
- const allItems = await pool.getAllItems();
1489
- const activeItems = allItems.filter(wi => wi.target === sessionName && (wi.status === 'running' || wi.status === 'queued'));
1490
- if (activeItems.length === 0) {
1491
- res.json({
1492
- success: true,
1493
- message: 'No active tasks found for session',
1494
- sessionName,
1495
- completedCount: 0,
1496
- completedTasks: [],
1497
- });
1498
- return;
1499
- }
1500
- const completedTasks = [];
1501
- for (const item of activeItems) {
1502
- try {
1503
- // Move task file from in_progress/ to done/ if applicable
1504
- // (WorkItems may not always have a file path, but we handle it gracefully)
1505
- await pool.completeItem(item.id, { completedAt: new Date().toISOString() });
1506
- completedTasks.push(item.title);
1507
- logger.info('Auto-completed WorkItem for session', {
1508
- workItemId: item.id,
1509
- title: item.title,
1510
- sessionName,
1511
- });
1512
- // V3 Hook: emit v3:task_completed
1513
- if (eventBusService) {
1514
- try {
1515
- eventBusService.emit('v3:task_completed', {
1516
- taskId: item.id,
1517
- sessionName,
1518
- timestamp: new Date().toISOString(),
1519
- });
1520
- }
1521
- catch {
1522
- // non-fatal
1523
- }
1524
- }
1525
- }
1526
- catch (error) {
1527
- logger.warn('Failed to auto-complete WorkItem', {
1528
- workItemId: item.id,
1529
- title: item.title,
1530
- error: error instanceof Error ? error.message : String(error),
1531
- });
1532
- }
1533
- }
1534
- res.json({
1535
- success: true,
1536
- message: `Completed ${completedTasks.length} task(s) for session ${sessionName}`,
1537
- sessionName,
1538
- completedCount: completedTasks.length,
1539
- completedTasks,
1540
- });
1541
- }
1542
- catch (error) {
1543
- logger.error('Error completing tasks by session', { error: error instanceof Error ? error.message : String(error) });
1544
- res.status(500).json({ success: false, error: 'Failed to complete tasks by session' });
1545
- }
1546
- }
1547
- /**
1548
- * GET /task-management/tasks
1549
- *
1550
- * List task files for a project, optionally filtered by status.
1551
- * Returns task filenames and paths grouped by milestone.
1552
- *
1553
- * @param req - Request with query: { projectPath, status? }
1554
- * @param res - Response with { success, tasks }
1555
- */
1556
- /**
1557
- * Detect and optionally clean up orphan tasks.
1558
- * Legacy — no-op, will be rebuilt on V3 WorkItem system.
1559
- *
1560
- * @param req - Request with cleanup parameters
1561
- * @param res - Response with deprecation notice
1562
- */
1563
- export async function cleanupOrphanTasks(req, res) {
1564
- // Legacy — no-op, will be rebuilt on V3 WorkItem system
1565
- res.json({ success: true, message: 'Deprecated — use TaskPool API', data: { orphanCount: 0, orphans: [], cleaned: 0 } });
1566
- }
1567
- export async function listTasks(req, res) {
1568
- try {
1569
- const projectPath = req.query.projectPath;
1570
- const statusFilter = req.query.status;
1571
- if (!projectPath) {
1572
- res.status(400).json({ success: false, error: 'projectPath query parameter is required' });
1573
- return;
1574
- }
1575
- // Validate status filter against known values to prevent path traversal
1576
- const VALID_STATUSES = ['open', 'in_progress', 'blocked', 'done'];
1577
- if (statusFilter && !VALID_STATUSES.includes(statusFilter)) {
1578
- res.status(400).json({ success: false, error: `status must be one of: ${VALID_STATUSES.join(', ')}` });
1579
- return;
1580
- }
1581
- // Resolve and validate project path to prevent directory traversal
1582
- const resolvedPath = resolve(projectPath);
1583
- const tasksBasePath = join(resolvedPath, '.crewly', 'tasks');
1584
- if (!existsSync(tasksBasePath)) {
1585
- res.json({ success: true, tasks: [] });
1586
- return;
1587
- }
1588
- const tasks = [];
1589
- const milestones = await readdir(tasksBasePath);
1590
- for (const milestone of milestones) {
1591
- const milestonePath = join(tasksBasePath, milestone);
1592
- const milestoneStat = await stat(milestonePath).catch(() => null);
1593
- if (!milestoneStat?.isDirectory())
1594
- continue;
1595
- const statusDirs = statusFilter ? [statusFilter] : [...VALID_STATUSES];
1596
- for (const status of statusDirs) {
1597
- const statusPath = join(milestonePath, status);
1598
- if (!existsSync(statusPath))
1599
- continue;
1600
- const files = await readdir(statusPath);
1601
- for (const file of files) {
1602
- if (file.endsWith('.md')) {
1603
- tasks.push({
1604
- name: file.replace('.md', ''),
1605
- path: join(statusPath, file),
1606
- milestone,
1607
- status,
1608
- });
1609
- }
1610
- }
1611
- }
1612
- }
1613
- res.json({ success: true, tasks });
1614
- }
1615
- catch (error) {
1616
- logger.error('Error listing tasks', { error: error instanceof Error ? error.message : String(error) });
1617
- res.status(500).json({ success: false, error: 'Failed to list tasks' });
1618
- }
1619
- }
1620
- export async function scoreTask(req, res) {
1621
- try {
1622
- const { taskId, qualityScore, scoredBy } = req.body;
1623
- if (!taskId) {
1624
- res.status(400).json({ success: false, error: 'taskId is required' });
1625
- return;
1626
- }
1627
- if (typeof qualityScore !== 'number' || qualityScore < 0 || qualityScore > 100) {
1628
- res.status(400).json({ success: false, error: 'qualityScore must be a number between 0 and 100' });
1629
- return;
1630
- }
1631
- // Legacy TaskTrackingService removed — score acknowledged but not persisted in legacy store
1632
- logger.info('Task scored (legacy store removed, score acknowledged)', { taskId, qualityScore, scoredBy: scoredBy || 'auditor' });
1633
- res.json({ success: true, message: 'Score acknowledged (legacy tracking removed)', taskId, qualityScore });
1634
- }
1635
- catch (error) {
1636
- logger.error('Error scoring task', { error: error instanceof Error ? error.message : String(error) });
1637
- res.status(500).json({ success: false, error: 'Failed to score task' });
1638
- }
1639
- }
1640
- export async function recordHandoff(req, res) {
1641
- try {
1642
- const { from, to, taskPath, reason, progress, projectPath } = req.body;
1643
- if (!from || !to || !reason) {
1644
- res.status(400).json({ success: false, error: 'from, to, and reason are required' });
1645
- return;
1646
- }
1647
- // Legacy TaskTrackingService removed — log handoff, return acknowledgement
1648
- logger.info('Task handoff recorded (legacy store removed)', { from, to, reason });
1649
- res.json({
1650
- success: true,
1651
- handoff: {
1652
- from,
1653
- to,
1654
- reason,
1655
- taskId: null,
1656
- taskPath: taskPath || null,
1657
- progress: progress || null,
1658
- timestamp: new Date().toISOString(),
1659
- },
1660
- });
1661
- }
1662
- catch (error) {
1663
- logger.error('Error recording handoff', { error: error instanceof Error ? error.message : String(error) });
1664
- res.status(500).json({ success: false, error: 'Failed to record handoff' });
1665
- }
1666
- }
1667
- export async function acceptTask(req, res) {
1668
- // Legacy — no-op, will be rebuilt on V3 WorkItem system
1669
- res.json({ success: true, message: 'Deprecated — use TaskPool API' });
1670
- }
1671
- export async function requestClarification(req, res) {
1672
- // Legacy — no-op, will be rebuilt on V3 WorkItem system
1673
- res.json({ success: true, message: 'Deprecated — use TaskPool API' });
1674
- }
1675
- export async function saveWorkingNotes(req, res) {
1676
- // Legacy — no-op, will be rebuilt on V3 WorkItem system
1677
- res.json({ success: true, message: 'Deprecated — use TaskPool API' });
1678
- }
1679
- // ---------------------------------------------------------------------------
1680
- // Checklist Management — TL acceptance checklist alignment
1681
- // ---------------------------------------------------------------------------
1682
- /**
1683
- * POST /task-management/:taskId/checklist
1684
- * Submit or update a checklist for a task. Called by the TL's design-checklist skill.
1685
- *
1686
- * @param req.params.taskId - Task ID
1687
- * @param req.body - Checklist JSON (items, objective, etc.)
1688
- */
1689
- export async function submitChecklist(req, res) {
1690
- try {
1691
- const { taskId } = req.params;
1692
- const checklist = req.body;
1693
- if (!taskId || !checklist?.items || !Array.isArray(checklist.items)) {
1694
- res.status(400).json({ success: false, error: 'taskId and items[] are required' });
1695
- return;
1696
- }
1697
- // Store checklist in project's .crewly/tasks/ directory
1698
- const projectPath = checklist.projectPath || process.cwd();
1699
- const checklistDir = join(projectPath, '.crewly', 'tasks');
1700
- await mkdir(checklistDir, { recursive: true });
1701
- const checklistPath = join(checklistDir, `checklist-${taskId}.json`);
1702
- const checklistData = {
1703
- ...checklist,
1704
- taskId,
1705
- status: 'pending_approval',
1706
- submittedAt: new Date().toISOString(),
1707
- };
1708
- await writeFile(checklistPath, JSON.stringify(checklistData, null, 2));
1709
- logger.info('Checklist submitted for approval', { taskId, itemCount: checklist.items.length });
1710
- res.status(201).json({
1711
- success: true,
1712
- data: { taskId, checklistPath, status: 'pending_approval', itemCount: checklist.items.length },
1713
- });
1714
- }
1715
- catch (error) {
1716
- const msg = error instanceof Error ? error.message : String(error);
1717
- logger.error('Error submitting checklist', { error: msg });
1718
- res.status(500).json({ success: false, error: msg });
1719
- }
1720
- }
1721
- /**
1722
- * POST /task-management/:taskId/checklist/approve
1723
- * Approve (or adjust) a pending checklist. Called by Orchestrator or user.
1724
- *
1725
- * @param req.params.taskId - Task ID
1726
- * @param req.body.adjustments - Optional adjustments to checklist items
1727
- */
1728
- export async function approveChecklist(req, res) {
1729
- try {
1730
- const { taskId } = req.params;
1731
- const { adjustments, projectPath: reqProjectPath } = req.body;
1732
- const projectPath = reqProjectPath || process.cwd();
1733
- const checklistPath = join(projectPath, '.crewly', 'tasks', `checklist-${taskId}.json`);
1734
- let checklist;
1735
- try {
1736
- const raw = await readFile(checklistPath, 'utf-8');
1737
- checklist = JSON.parse(raw);
1738
- }
1739
- catch {
1740
- res.status(404).json({ success: false, error: `Checklist not found for task ${taskId}` });
1741
- return;
1742
- }
1743
- // Apply adjustments if provided
1744
- if (adjustments && Array.isArray(adjustments)) {
1745
- checklist.items = adjustments;
1746
- }
1747
- checklist.status = 'approved';
1748
- checklist.approvedAt = new Date().toISOString();
1749
- checklist.approvedBy = req.body.approvedBy || 'orchestrator';
1750
- await writeFile(checklistPath, JSON.stringify(checklist, null, 2));
1751
- logger.info('Checklist approved', { taskId, approvedBy: checklist.approvedBy });
1752
- res.json({
1753
- success: true,
1754
- data: { taskId, status: 'approved', itemCount: checklist.items.length },
1755
- });
1756
- }
1757
- catch (error) {
1758
- const msg = error instanceof Error ? error.message : String(error);
1759
- logger.error('Error approving checklist', { error: msg });
1760
- res.status(500).json({ success: false, error: msg });
1761
- }
1762
- }
1763
- /**
1764
- * GET /task-management/:taskId/checklist
1765
- * Get the current checklist for a task.
1766
- */
1767
- export async function getChecklist(req, res) {
1768
- try {
1769
- const { taskId } = req.params;
1770
- const projectPath = req.query.projectPath || process.cwd();
1771
- const checklistPath = join(projectPath, '.crewly', 'tasks', `checklist-${taskId}.json`);
1772
- const raw = await readFile(checklistPath, 'utf-8');
1773
- res.json({ success: true, data: JSON.parse(raw) });
1774
- }
1775
- catch {
1776
- res.status(404).json({ success: false, error: 'Checklist not found' });
1777
- }
1778
- }
1779
- //# sourceMappingURL=task-management.controller.js.map