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.
- package/dist/backend/backend/src/constants.d.ts +12 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +12 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +17 -0
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +8 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +15 -7
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +7 -0
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js +69 -12
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +122 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +252 -17
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +37 -3
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +140 -23
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts +75 -0
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js +164 -12
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +12 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +12 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/cli/src/index.js +0 -0
- package/package.json +1 -1
- package/config/constants.d.ts.map +0 -1
- package/config/index.d.ts.map +0 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +0 -169
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +0 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +0 -1779
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts +0 -513
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js +0 -1568
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-runner.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts +0 -86
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js +0 -147
- package/dist/backend/backend/src/services/agent/crewly-agent/agent-worker.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts +0 -68
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js +0 -131
- package/dist/backend/backend/src/services/agent/crewly-agent/api-client.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts +0 -130
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js +0 -263
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-log.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts +0 -74
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js +0 -140
- package/dist/backend/backend/src/services/agent/crewly-agent/audit-trail.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts +0 -29
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js +0 -279
- package/dist/backend/backend/src/services/agent/crewly-agent/auditor-tools.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts +0 -340
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js +0 -1176
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-runtime.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts +0 -79
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js +0 -145
- package/dist/backend/backend/src/services/agent/crewly-agent/deepseek-sse-transform.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts +0 -79
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js +0 -218
- package/dist/backend/backend/src/services/agent/crewly-agent/env-isolation.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts +0 -16
- package/dist/backend/backend/src/services/agent/crewly-agent/index.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/index.js +0 -16
- package/dist/backend/backend/src/services/agent/crewly-agent/index.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts +0 -135
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js +0 -185
- package/dist/backend/backend/src/services/agent/crewly-agent/mcp-tool-bridge.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts +0 -141
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js +0 -310
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts +0 -91
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js +0 -143
- package/dist/backend/backend/src/services/agent/crewly-agent/output-filter.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts +0 -103
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js +0 -256
- package/dist/backend/backend/src/services/agent/crewly-agent/prompt-guard.service.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts +0 -143
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js +0 -264
- package/dist/backend/backend/src/services/agent/crewly-agent/rate-limiter.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts +0 -13
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js +0 -91
- package/dist/backend/backend/src/services/agent/crewly-agent/smoke-test.js.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts +0 -135
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.d.ts.map +0 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js +0 -1937
- package/dist/backend/backend/src/services/agent/crewly-agent/tool-registry.js.map +0 -1
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts +0 -429
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.js +0 -852
- package/dist/backend/backend/src/services/autonomous/auto-assign.service.js.map +0 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +0 -171
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.js +0 -725
- package/dist/backend/backend/src/services/project/task-tracking.service.js.map +0 -1
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts +0 -118
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.js +0 -326
- package/dist/backend/backend/src/services/v3/project-task-watcher.service.js.map +0 -1
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts +0 -74
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.d.ts.map +0 -1
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js +0 -154
- package/dist/backend/backend/src/services/wiki/wiki-chat-subscriber.service.js.map +0 -1
- package/dist/backend/backend/src/types/auto-assign.types.d.ts +0 -271
- package/dist/backend/backend/src/types/auto-assign.types.d.ts.map +0 -1
- package/dist/backend/backend/src/types/auto-assign.types.js +0 -136
- package/dist/backend/backend/src/types/auto-assign.types.js.map +0 -1
- package/dist/backend/backend/src/utils/esm-require.utils.d.ts +0 -111
- package/dist/backend/backend/src/utils/esm-require.utils.d.ts.map +0 -1
- package/dist/backend/backend/src/utils/esm-require.utils.js +0 -124
- package/dist/backend/backend/src/utils/esm-require.utils.js.map +0 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +0 -220
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +0 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js +0 -37
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +0 -1
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts +0 -56
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.d.ts.map +0 -1
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js +0 -91
- package/dist/cli/backend/src/services/knowledge/fts5-search-strategy.js.map +0 -1
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts +0 -159
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.js +0 -304
- package/dist/cli/backend/src/services/knowledge/learnings-index.service.js.map +0 -1
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts +0 -115
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js +0 -215
- package/dist/cli/backend/src/services/knowledge/wiki-compiler.service.js.map +0 -1
- package/dist/cli/backend/src/services/memory/embedding-provider.d.ts +0 -78
- package/dist/cli/backend/src/services/memory/embedding-provider.d.ts.map +0 -1
- package/dist/cli/backend/src/services/memory/embedding-provider.js +0 -179
- package/dist/cli/backend/src/services/memory/embedding-provider.js.map +0 -1
- package/dist/cli/backend/src/services/memory/vector-store.service.d.ts +0 -331
- package/dist/cli/backend/src/services/memory/vector-store.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/memory/vector-store.service.js +0 -814
- package/dist/cli/backend/src/services/memory/vector-store.service.js.map +0 -1
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +0 -171
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +0 -1
- package/dist/cli/backend/src/services/project/task-tracking.service.js +0 -725
- package/dist/cli/backend/src/services/project/task-tracking.service.js.map +0 -1
- package/dist/cli/backend/src/types/auto-assign.types.d.ts +0 -271
- package/dist/cli/backend/src/types/auto-assign.types.d.ts.map +0 -1
- package/dist/cli/backend/src/types/auto-assign.types.js +0 -136
- 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
|